-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpsd2pptx.js
226 lines (208 loc) · 8.61 KB
/
psd2pptx.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
#!/usr/bin/env node
/*!
** psd2pptx -- Convert Photoshop (PSD) layers to PowerPoint (PPTX) slides
** Copyright (c) 2019-2022 Dr. Ralf S. Engelschall <[email protected]>
** Licensed under MIT Open Source license.
*/
/* internal requirements */
const fs = require("fs")
/* external requirements */
const tmp = require("tmp")
const mkdirp = require("mkdirp")
const bluebird = require("bluebird")
const execa = require("execa")
const Jimp = require("jimp")
const PPTXGenJS = require("pptxgenjs")
const yargs = require("yargs")
const chalk = require("chalk")
const PSD = require("psd.js")
const zipProcess = require("zip-process")
/* act in an asynchronous context */
;(async () => {
/* command-line option parsing */
const argv = yargs
/* eslint indent: off */
.usage("Usage: $0 [-h] [-v] [-o <pptx-file>] <psd-file>")
.help("h").alias("h", "help").default("h", false)
.describe("h", "show usage help")
.boolean("v").alias("v", "verbose").default("v", false)
.describe("v", "print verbose messages")
.string("o").nargs("o", 1).alias("o", "output").default("o", "")
.describe("o", "output PPTX file")
.string("c").nargs("c", 1).alias("c", "canvas").default("c", "Canvas")
.describe("c", "name of canvas layer group (default: \"Canvas\")")
.string("s").nargs("s", 1).alias("s", "skip").default("s", "^Background$")
.describe("s", "regular expression matching layers to skip (default: \"^Background$\")")
.string("t").nargs("t", 1).alias("t", "transition").default("t", "none")
.describe("t", "slide transition (\"none\", \"fade\" or \"wipe\", default: \"none\")")
.version(false)
.strict()
.showHelpOnFail(true)
.demand(1)
.parse(process.argv.slice(2))
/* verbose message printing */
const verbose = (msg) => {
if (argv.verbose)
process.stdout.write(`++ ${msg}\n`)
}
/* create temporary filesystem area */
const tmpdir = tmp.dirSync({ unsafeCleanup: true })
/* read and parse PSD file */
const psdfile = argv._[0]
verbose(`reading PSD file: ${chalk.blue(psdfile)}`)
const psd = PSD.fromFile(psdfile)
psd.parse()
/* extract PSD layers (as PNG files) */
const basename = psdfile.replace(/\.psd$/, "")
let layers = []
const walkNode = async (node) => {
if (!node)
return
if (node.hasChildren()) {
/* recursively enter child layers */
await bluebird.each(node.children(), async (child) => {
await walkNode(child)
})
}
else if (node.layer.image) {
verbose(`extracting layer: ${chalk.blue(node.path())}`)
let path = node.path().replace(/\//g, "-").replace(/[^a-zA-Z0-9.]/g, "")
let pngfile = `${tmpdir.name}/extracted-${path}.png`
await node.layer.image.saveAsPng(pngfile)
layers.push({
file: pngfile,
name: node.get("name"),
path: node.path(),
opacity: node.layer.opacity,
width: node.layer.width,
height: node.layer.height
})
}
}
await walkNode(psd.tree())
/* skip some layers */
let regex1 = new RegExp(argv.skip)
layers = layers.filter((item) => !item.path.match(regex1))
/* divide layers into canvas, slides and other layers */
let regex2 = new RegExp(`^${argv.canvas}\/`, "i")
let layersCanvas = layers.filter((item) => item.path.match(regex2))
let layersSlides = layers.filter((item) => !item.path.match(regex2))
let layersOthers = layersSlides.filter((item) => !item.path.match(/^.+\/[^\/]+$/i))
layersSlides = layersSlides.filter((item) => item.path.match(/^.+\/[^\/]+$/i))
/* generate canvas */
let bottom = layersCanvas.slice(-1)[0]
let w = bottom.width
let h = bottom.height
verbose(`generating canvas: ${layersCanvas.map((item) => chalk.blue(item.path)).join(", ")} (${w}x${h})`)
let canvas = await Jimp.read(bottom.file)
await canvas.opacity(bottom.opacity / 255)
await bluebird.each(layersCanvas.slice(0, -1).reverse(), async (item) => {
let src = await Jimp.read(item.file)
await src.opacity(item.opacity / 255)
await canvas.blit(src, 0, 0)
})
let canvasFilename = `${tmpdir.name}/canvas.png`
await canvas.writeAsync(canvasFilename)
/* generate empty image */
const empty = await new Promise((resolve, reject) => {
new Jimp(w, h, 0x00000000, (err, image) => {
if (err) reject(err)
else resolve(image)
})
})
/* generate slides */
let pngs = []
let n = 1
let slide = null
let prefix = null
await bluebird.each(layersSlides.reverse(), async (item) => {
let p = item.path.replace(/^(.+\/)[^\/]+$/, "$1")
if (prefix !== p) {
prefix = p
slide = empty.clone()
verbose(`generating image: ${chalk.blue(item.path)} (scratch)`)
}
else
verbose(`generating image: ${chalk.blue(item.path)} (merged)`)
let src = await Jimp.read(item.file)
await src.opacity(item.opacity / 255)
await slide.blit(src, 0, 0)
let out = `${tmpdir.name}/slide-${n++}.png`
await slide.writeAsync(out)
pngs.push({ file: out, path: item.path })
})
/* generate PPTX out of PNG images */
let pptx = new PPTXGenJS()
pptx.defineLayout({ name: "Custom", width: 10, height: 10 * (h/w) })
pptx.layout = "Custom"
pptx.defineSlideMaster({
title: "psd2pptx",
bkgd: "FFFFFF",
objects: [
{ "image": { x: 0, y: 0, w: 10, h: 10 * (h/w), path: canvasFilename } }
]
})
for (let i = 0; i < pngs.length; i++) {
verbose(`generating slide: ${chalk.blue(pngs[i].path)}`)
let slide = pptx.addSlide("psd2pptx")
slide.addImage({ path: pngs[i].file, x: 0, y: 0, w: 10, h: 10 * (h/w) })
}
let pptxfile = `${tmpdir.name}/slides.pptx`
verbose("generating PPTX")
await pptx.writeFile({ fileName: pptxfile })
/* post-adjust PPTX: optionally add slide transition */
if (argv.transition !== "none") {
let transition = ""
if (argv.transition === "fade")
transition = "<p:fade/>"
else if (argv.transition === "wipe")
transition = "<p:strips dir=\"rd\"/>"
else
throw new Error("invalid transition type")
verbose("post-adjusting PPTX")
let zip = fs.readFileSync(pptxfile)
let out = await zipProcess(zip, {
compression: "DEFLATE",
extendOptions: { compressionOptions: { level: 9 } }
}, {
string: {
filter: (relativePath, fileInfo) => {
return relativePath.match(/^ppt\/slides\/slide\d+\.xml$/)
},
callback: (data, relativePath, zipObject) => {
let transition =
'<mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">' +
'<mc:Choice xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main" Requires="p14">' +
'<p:transition spd="med" p14:dur="700">' +
transition +
'</p:transition>' +
'</mc:Choice>' +
'<mc:Fallback>' +
'<p:transition spd="med">' +
transition +
'</p:transition>' +
'</mc:Fallback>' +
'</mc:AlternateContent>'
data = data.replace(/(<\/p:sld>)/, transition + "$1")
return data
}
}
})
/* write output file */
let pptxname = (argv.output !== "" ? argv.output : `${basename}.pptx`)
verbose(`writing PPTX file: ${chalk.blue(pptxname)}`)
fs.writeFileSync(pptxname, out)
}
else {
/* write output file */
let pptxname = (argv.output !== "" ? argv.output : `${basename}.pptx`)
verbose(`writing PPTX file: ${chalk.blue(pptxname)}`)
fs.copyFileSync(pptxfile, pptxname)
}
/* delete temporary filesystem area */
tmpdir.removeCallback()
})().catch((err) => {
/* report error */
process.stderr.write(chalk.red(`** ERROR: ${err.stack}\n`))
process.exit(1)
})