node-pptx-templater 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +336 -281
- package/package.json +6 -6
- package/src/cli/commands/build.js +32 -31
- package/src/cli/commands/debug.js +25 -24
- package/src/cli/commands/extract.js +23 -21
- package/src/cli/commands/inspect.js +25 -23
- package/src/cli/commands/validate.js +19 -17
- package/src/cli/index.js +45 -43
- package/src/core/OutputWriter.js +81 -78
- package/src/core/PPTXTemplater.js +859 -274
- package/src/core/TemplateEngine.js +69 -71
- package/src/core/ValidationEngine.js +246 -0
- package/src/index.js +51 -15
- package/src/managers/ChartManager.js +197 -70
- package/src/managers/ContentTypesManager.js +51 -45
- package/src/managers/HyperlinkManager.js +148 -142
- package/src/managers/ImageManager.js +336 -0
- package/src/managers/MediaManager.js +64 -81
- package/src/managers/RelationshipManager.js +102 -96
- package/src/managers/ShapeManager.js +340 -0
- package/src/managers/SlideManager.js +410 -311
- package/src/managers/TableManager.js +981 -262
- package/src/managers/TextManager.js +197 -0
- package/src/managers/ZipManager.js +71 -69
- package/src/managers/charts/ChartCacheGenerator.js +77 -58
- package/src/managers/charts/ChartParser.js +11 -13
- package/src/managers/charts/ChartRelationshipManager.js +14 -10
- package/src/managers/charts/ChartWorkbookUpdater.js +61 -56
- package/src/parsers/XMLParser.js +50 -49
- package/src/templates/blankPptx.js +3 -1
- package/src/templates/slideTemplate.js +31 -32
- package/src/utils/contentTypesHelper.js +41 -53
- package/src/utils/errors.js +33 -23
- package/src/utils/idUtils.js +23 -15
- package/src/utils/logger.js +21 -15
- package/src/utils/relationshipUtils.js +28 -22
- package/src/utils/xmlUtils.js +37 -29
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview ImageManager - Manages slide-level image operations like replace, add, remove, and list.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { createLogger } = require('../utils/logger.js')
|
|
6
|
+
const { PPTXError } = require('../utils/errors.js')
|
|
7
|
+
const { REL_TYPES } = require('./RelationshipManager.js')
|
|
8
|
+
|
|
9
|
+
const logger = createLogger('ImageManager')
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @class ImageManager
|
|
13
|
+
* @description Manages image elements and relationships on individual slides.
|
|
14
|
+
*/
|
|
15
|
+
class ImageManager {
|
|
16
|
+
/** @private @type {XMLParser} */
|
|
17
|
+
#xmlParser
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {XMLParser} xmlParser
|
|
21
|
+
*/
|
|
22
|
+
constructor(xmlParser) {
|
|
23
|
+
this.#xmlParser = xmlParser
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Replaces an existing image on a slide.
|
|
28
|
+
*
|
|
29
|
+
* @param {number} slideIndex
|
|
30
|
+
* @param {string} imageIdOrName - Shape name or shape ID.
|
|
31
|
+
* @param {string|Buffer} sourcePathOrBuffer - New image source path or Buffer.
|
|
32
|
+
* @param {SlideManager} slideManager
|
|
33
|
+
* @param {MediaManager} mediaManager
|
|
34
|
+
* @param {RelationshipManager} relationshipManager
|
|
35
|
+
*/
|
|
36
|
+
async replaceImage(
|
|
37
|
+
slideIndex,
|
|
38
|
+
imageIdOrName,
|
|
39
|
+
sourcePathOrBuffer,
|
|
40
|
+
slideManager,
|
|
41
|
+
mediaManager,
|
|
42
|
+
relationshipManager
|
|
43
|
+
) {
|
|
44
|
+
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
45
|
+
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
46
|
+
const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
|
|
47
|
+
const picRes = this.#findPicRecursive(spTree, imageIdOrName)
|
|
48
|
+
|
|
49
|
+
if (!picRes) {
|
|
50
|
+
throw new PPTXError(`Image shape "${imageIdOrName}" not found on slide ${slideIndex}`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const rId = picRes.pic?.['p:blipFill']?.['a:blip']?.['@_r:embed']
|
|
54
|
+
if (!rId) {
|
|
55
|
+
throw new PPTXError(`No relationship ID found on image shape "${imageIdOrName}"`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Embed the new image bytes
|
|
59
|
+
const destMediaZipPath = await mediaManager.embedImage(sourcePathOrBuffer)
|
|
60
|
+
const relativeTarget = `../media/${destMediaZipPath.split('/').pop()}`
|
|
61
|
+
|
|
62
|
+
// Update relationship target path
|
|
63
|
+
const slideInfo = slideManager.getSlideInfo(slideIndex)
|
|
64
|
+
relationshipManager.updateRelationshipTarget(slideInfo.zipPath, rId, relativeTarget)
|
|
65
|
+
|
|
66
|
+
logger.debug(`Replaced image "${imageIdOrName}" target with "${relativeTarget}"`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Adds a new image to a slide.
|
|
71
|
+
*
|
|
72
|
+
* @param {number} slideIndex
|
|
73
|
+
* @param {string|Buffer} sourcePathOrBuffer
|
|
74
|
+
* @param {Object} options - Position options (x, y, width, height in EMUs or inches).
|
|
75
|
+
* @param {SlideManager} slideManager
|
|
76
|
+
* @param {MediaManager} mediaManager
|
|
77
|
+
* @param {RelationshipManager} relationshipManager
|
|
78
|
+
*/
|
|
79
|
+
async addImage(
|
|
80
|
+
slideIndex,
|
|
81
|
+
sourcePathOrBuffer,
|
|
82
|
+
options = {},
|
|
83
|
+
slideManager,
|
|
84
|
+
mediaManager,
|
|
85
|
+
relationshipManager
|
|
86
|
+
) {
|
|
87
|
+
const slideInfo = slideManager.getSlideInfo(slideIndex)
|
|
88
|
+
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
89
|
+
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
90
|
+
const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
|
|
91
|
+
|
|
92
|
+
if (!spTree) {
|
|
93
|
+
throw new PPTXError(`Invalid slide structure for slide ${slideIndex}`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Embed the image
|
|
97
|
+
const destMediaZipPath = await mediaManager.embedImage(sourcePathOrBuffer)
|
|
98
|
+
const relativeTarget = `../media/${destMediaZipPath.split('/').pop()}`
|
|
99
|
+
|
|
100
|
+
// Add relationship
|
|
101
|
+
const rId = relationshipManager.addRelationship(
|
|
102
|
+
slideInfo.zipPath,
|
|
103
|
+
REL_TYPES.IMAGE,
|
|
104
|
+
relativeTarget
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
// Generate unique shape ID
|
|
108
|
+
const existingIds = this.#getAllShapeIds(spTree)
|
|
109
|
+
const maxId = existingIds.length > 0 ? Math.max(...existingIds) : 1000
|
|
110
|
+
const newId = maxId + 1
|
|
111
|
+
|
|
112
|
+
// Convert positions
|
|
113
|
+
const x =
|
|
114
|
+
options.x === undefined
|
|
115
|
+
? 0
|
|
116
|
+
: options.x < 100
|
|
117
|
+
? Math.round(options.x * 914400)
|
|
118
|
+
: Math.round(options.x)
|
|
119
|
+
const y =
|
|
120
|
+
options.y === undefined
|
|
121
|
+
? 0
|
|
122
|
+
: options.y < 100
|
|
123
|
+
? Math.round(options.y * 914400)
|
|
124
|
+
: Math.round(options.y)
|
|
125
|
+
const cx =
|
|
126
|
+
options.width === undefined
|
|
127
|
+
? 2743200
|
|
128
|
+
: options.width < 100
|
|
129
|
+
? Math.round(options.width * 914400)
|
|
130
|
+
: Math.round(options.width)
|
|
131
|
+
const cy =
|
|
132
|
+
options.height === undefined
|
|
133
|
+
? 1828800
|
|
134
|
+
: options.height < 100
|
|
135
|
+
? Math.round(options.height * 914400)
|
|
136
|
+
: Math.round(options.height)
|
|
137
|
+
const name = options.name || `Picture ${newId}`
|
|
138
|
+
|
|
139
|
+
// Build the pic XML snippet using mediaManager's builder and parse it
|
|
140
|
+
const picXml = mediaManager.buildImageXml(rId, {
|
|
141
|
+
x,
|
|
142
|
+
y,
|
|
143
|
+
width: cx,
|
|
144
|
+
height: cy,
|
|
145
|
+
name,
|
|
146
|
+
shapeId: newId,
|
|
147
|
+
})
|
|
148
|
+
const picObj = this.#xmlParser.parse(picXml, 'pic.xml')['p:pic']
|
|
149
|
+
|
|
150
|
+
if (!spTree['p:pic']) {
|
|
151
|
+
spTree['p:pic'] = []
|
|
152
|
+
}
|
|
153
|
+
if (!Array.isArray(spTree['p:pic'])) {
|
|
154
|
+
spTree['p:pic'] = [spTree['p:pic']]
|
|
155
|
+
}
|
|
156
|
+
spTree['p:pic'].push(picObj)
|
|
157
|
+
|
|
158
|
+
const decl = this.#xmlParser.extractDeclaration(slideXml)
|
|
159
|
+
slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
|
|
160
|
+
logger.debug(`Added image "${name}" with ID ${newId} and rId ${rId} to slide ${slideIndex}`)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Removes an image from a slide.
|
|
165
|
+
*
|
|
166
|
+
* @param {number} slideIndex
|
|
167
|
+
* @param {string} imageIdOrName
|
|
168
|
+
* @param {SlideManager} slideManager
|
|
169
|
+
* @param {RelationshipManager} relationshipManager
|
|
170
|
+
*/
|
|
171
|
+
removeImage(slideIndex, imageIdOrName, slideManager, relationshipManager) {
|
|
172
|
+
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
173
|
+
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
174
|
+
const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
|
|
175
|
+
const picRes = this.#findPicRecursive(spTree, imageIdOrName)
|
|
176
|
+
|
|
177
|
+
if (!picRes) {
|
|
178
|
+
throw new PPTXError(`Image shape "${imageIdOrName}" not found on slide ${slideIndex}`)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Remove from shape list
|
|
182
|
+
const parent = picRes.parent
|
|
183
|
+
if (parent['p:pic']) {
|
|
184
|
+
if (Array.isArray(parent['p:pic'])) {
|
|
185
|
+
parent['p:pic'] = parent['p:pic'].filter(p => p !== picRes.pic)
|
|
186
|
+
} else if (parent['p:pic'] === picRes.pic) {
|
|
187
|
+
delete parent['p:pic']
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Remove relationship
|
|
192
|
+
const rId = picRes.pic?.['p:blipFill']?.['a:blip']?.['@_r:embed']
|
|
193
|
+
if (rId) {
|
|
194
|
+
const slideInfo = slideManager.getSlideInfo(slideIndex)
|
|
195
|
+
relationshipManager.removeRelationship(slideInfo.zipPath, rId)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const decl = this.#xmlParser.extractDeclaration(slideXml)
|
|
199
|
+
slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
|
|
200
|
+
logger.debug(`Removed image "${imageIdOrName}" from slide ${slideIndex}`)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Enumerates all images on a slide.
|
|
205
|
+
*
|
|
206
|
+
* @param {number} slideIndex
|
|
207
|
+
* @param {SlideManager} slideManager
|
|
208
|
+
* @param {RelationshipManager} relationshipManager
|
|
209
|
+
* @returns {Array<Object>} List of image elements found.
|
|
210
|
+
*/
|
|
211
|
+
getImages(slideIndex, slideManager, relationshipManager) {
|
|
212
|
+
const slideInfo = slideManager.getSlideInfo(slideIndex)
|
|
213
|
+
const slideXml = slideManager.getSlideXml(slideIndex)
|
|
214
|
+
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
215
|
+
const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
|
|
216
|
+
|
|
217
|
+
const imagesInfo = []
|
|
218
|
+
this.#collectImagesInfo(spTree, slideInfo.zipPath, relationshipManager, imagesInfo)
|
|
219
|
+
return imagesInfo
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#findPicRecursive(container, targetId) {
|
|
223
|
+
if (!container) return null
|
|
224
|
+
|
|
225
|
+
let pics = container['p:pic'] || []
|
|
226
|
+
if (!Array.isArray(pics)) pics = [pics]
|
|
227
|
+
|
|
228
|
+
for (const pic of pics) {
|
|
229
|
+
const cNvPr = pic?.['p:nvPicPr']?.['p:cNvPr']
|
|
230
|
+
if (cNvPr) {
|
|
231
|
+
const name = cNvPr['@_name']
|
|
232
|
+
const id = String(cNvPr['@_id'])
|
|
233
|
+
const embedId = pic?.['p:blipFill']?.['a:blip']?.['@_r:embed']
|
|
234
|
+
if (name === targetId || id === targetId || embedId === targetId) {
|
|
235
|
+
return { pic, parent: container }
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let groups = container['p:grpSp'] || []
|
|
241
|
+
if (!Array.isArray(groups)) groups = [groups]
|
|
242
|
+
for (const group of groups) {
|
|
243
|
+
const res = this.#findPicRecursive(group, targetId)
|
|
244
|
+
if (res) return res
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return null
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#getAllShapeIds(container) {
|
|
251
|
+
const ids = []
|
|
252
|
+
if (!container) return ids
|
|
253
|
+
|
|
254
|
+
let shapes = container['p:sp'] || []
|
|
255
|
+
if (!Array.isArray(shapes)) shapes = [shapes]
|
|
256
|
+
for (const s of shapes) {
|
|
257
|
+
const id = parseInt(s?.['p:nvSpPr']?.['p:cNvPr']?.['@_id'], 10)
|
|
258
|
+
if (!isNaN(id)) ids.push(id)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let pics = container['p:pic'] || []
|
|
262
|
+
if (!Array.isArray(pics)) pics = [pics]
|
|
263
|
+
for (const p of pics) {
|
|
264
|
+
const id = parseInt(p?.['p:nvPicPr']?.['p:cNvPr']?.['@_id'], 10)
|
|
265
|
+
if (!isNaN(id)) ids.push(id)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let frames = container['p:graphicFrame'] || []
|
|
269
|
+
if (!Array.isArray(frames)) frames = [frames]
|
|
270
|
+
for (const f of frames) {
|
|
271
|
+
const id = parseInt(f?.['p:nvGraphicFramePr']?.['p:cNvPr']?.['@_id'], 10)
|
|
272
|
+
if (!isNaN(id)) ids.push(id)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
let groups = container['p:grpSp'] || []
|
|
276
|
+
if (!Array.isArray(groups)) groups = [groups]
|
|
277
|
+
for (const g of groups) {
|
|
278
|
+
const id = parseInt(g?.['p:nvGrpSpPr']?.['p:cNvPr']?.['@_id'], 10)
|
|
279
|
+
if (!isNaN(id)) ids.push(id)
|
|
280
|
+
ids.push(...this.#getAllShapeIds(g))
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return ids
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#collectImagesInfo(container, slideZipPath, relationshipManager, results) {
|
|
287
|
+
if (!container) return
|
|
288
|
+
|
|
289
|
+
let pics = container['p:pic'] || []
|
|
290
|
+
if (!Array.isArray(pics)) pics = [pics]
|
|
291
|
+
|
|
292
|
+
for (const pic of pics) {
|
|
293
|
+
const cNvPr = pic?.['p:nvPicPr']?.['p:cNvPr']
|
|
294
|
+
if (!cNvPr) continue
|
|
295
|
+
|
|
296
|
+
const name = cNvPr['@_name']
|
|
297
|
+
const id = String(cNvPr['@_id'])
|
|
298
|
+
|
|
299
|
+
const rId = pic?.['p:blipFill']?.['a:blip']?.['@_r:embed']
|
|
300
|
+
let targetPath = ''
|
|
301
|
+
if (rId) {
|
|
302
|
+
const rel = relationshipManager.getRelationshipById(slideZipPath, rId)
|
|
303
|
+
if (rel) {
|
|
304
|
+
targetPath = relationshipManager.resolveTarget(slideZipPath, rel.target)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const xfrm = pic['p:spPr']?.['a:xfrm']
|
|
309
|
+
const position = xfrm
|
|
310
|
+
? {
|
|
311
|
+
x: parseInt(xfrm['a:off']?.['@_x'] || 0, 10),
|
|
312
|
+
y: parseInt(xfrm['a:off']?.['@_y'] || 0, 10),
|
|
313
|
+
cx: parseInt(xfrm['a:ext']?.['@_cx'] || 0, 10),
|
|
314
|
+
cy: parseInt(xfrm['a:ext']?.['@_cy'] || 0, 10),
|
|
315
|
+
}
|
|
316
|
+
: null
|
|
317
|
+
|
|
318
|
+
results.push({
|
|
319
|
+
type: 'image',
|
|
320
|
+
id,
|
|
321
|
+
name,
|
|
322
|
+
relationshipId: rId,
|
|
323
|
+
targetPath,
|
|
324
|
+
position,
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let groups = container['p:grpSp'] || []
|
|
329
|
+
if (!Array.isArray(groups)) groups = [groups]
|
|
330
|
+
for (const g of groups) {
|
|
331
|
+
this.#collectImagesInfo(g, slideZipPath, relationshipManager, results)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
module.exports = { ImageManager }
|
|
@@ -36,31 +36,12 @@
|
|
|
36
36
|
* Audio: MP3, WAV, M4A
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
import fsExtra from 'fs-extra';
|
|
39
|
+
const { createHash } = require('crypto')
|
|
40
|
+
const { createLogger } = require('../utils/logger.js')
|
|
41
|
+
const { PPTXError } = require('../utils/errors.js')
|
|
42
|
+
const fsExtra = require('fs-extra')
|
|
44
43
|
|
|
45
|
-
const logger = createLogger('MediaManager')
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* MIME type to extension mapping for media files.
|
|
49
|
-
*/
|
|
50
|
-
const MEDIA_TYPES = {
|
|
51
|
-
'image/png': 'png',
|
|
52
|
-
'image/jpeg': 'jpeg',
|
|
53
|
-
'image/jpg': 'jpg',
|
|
54
|
-
'image/gif': 'gif',
|
|
55
|
-
'image/svg+xml': 'svg',
|
|
56
|
-
'image/tiff': 'tiff',
|
|
57
|
-
'image/bmp': 'bmp',
|
|
58
|
-
'image/x-wmf': 'wmf',
|
|
59
|
-
'image/x-emf': 'emf',
|
|
60
|
-
'image/webp': 'webp',
|
|
61
|
-
'video/mp4': 'mp4',
|
|
62
|
-
'audio/mpeg': 'mp3',
|
|
63
|
-
};
|
|
44
|
+
const logger = createLogger('MediaManager')
|
|
64
45
|
|
|
65
46
|
/**
|
|
66
47
|
* Extension to MIME type mapping.
|
|
@@ -78,42 +59,42 @@ const EXT_TO_MIME = {
|
|
|
78
59
|
webp: 'image/webp',
|
|
79
60
|
mp4: 'video/mp4',
|
|
80
61
|
mp3: 'audio/mpeg',
|
|
81
|
-
}
|
|
62
|
+
}
|
|
82
63
|
|
|
83
64
|
/**
|
|
84
65
|
* @class MediaManager
|
|
85
66
|
* @description Manages media embedding, deduplication, and retrieval in PPTX files.
|
|
86
67
|
*/
|
|
87
|
-
|
|
68
|
+
class MediaManager {
|
|
88
69
|
/** @private @type {ContentTypesManager} */
|
|
89
|
-
#contentTypesManager
|
|
70
|
+
#contentTypesManager
|
|
90
71
|
/** @private @type {ZipManager} */
|
|
91
|
-
#zipManager
|
|
72
|
+
#zipManager
|
|
92
73
|
|
|
93
74
|
/**
|
|
94
75
|
* @param {ContentTypesManager} contentTypesManager
|
|
95
76
|
*/
|
|
96
77
|
constructor(contentTypesManager) {
|
|
97
|
-
this.#contentTypesManager = contentTypesManager
|
|
78
|
+
this.#contentTypesManager = contentTypesManager
|
|
98
79
|
}
|
|
99
80
|
|
|
100
81
|
/**
|
|
101
82
|
* Content hash → existing media ZIP path for deduplication.
|
|
102
83
|
* @private @type {Map<string, string>}
|
|
103
84
|
*/
|
|
104
|
-
#mediaHashIndex = new Map()
|
|
85
|
+
#mediaHashIndex = new Map()
|
|
105
86
|
|
|
106
87
|
/**
|
|
107
88
|
* All known media files.
|
|
108
89
|
* @private @type {Map<string, MediaInfo>}
|
|
109
90
|
*/
|
|
110
|
-
#mediaRegistry = new Map()
|
|
91
|
+
#mediaRegistry = new Map()
|
|
111
92
|
|
|
112
93
|
/**
|
|
113
94
|
* Counter for generating unique media file names.
|
|
114
95
|
* @private @type {number}
|
|
115
96
|
*/
|
|
116
|
-
#nextMediaId = 1
|
|
97
|
+
#nextMediaId = 1
|
|
117
98
|
|
|
118
99
|
/**
|
|
119
100
|
* Initializes by scanning existing media files in the PPTX.
|
|
@@ -122,33 +103,33 @@ export class MediaManager {
|
|
|
122
103
|
* @returns {Promise<void>}
|
|
123
104
|
*/
|
|
124
105
|
async initialize(zipManager) {
|
|
125
|
-
this.#zipManager = zipManager
|
|
126
|
-
const mediaFiles = zipManager.listFiles('ppt/media/')
|
|
106
|
+
this.#zipManager = zipManager
|
|
107
|
+
const mediaFiles = zipManager.listFiles('ppt/media/')
|
|
127
108
|
|
|
128
109
|
// Index all existing media files by content hash for deduplication
|
|
129
110
|
await Promise.all(
|
|
130
111
|
mediaFiles.map(async mediaPath => {
|
|
131
|
-
const data = await zipManager.readBinaryFile(mediaPath)
|
|
112
|
+
const data = await zipManager.readBinaryFile(mediaPath)
|
|
132
113
|
if (data) {
|
|
133
|
-
const hash = this.#hashBytes(data)
|
|
134
|
-
const ext = mediaPath.split('.').pop().toLowerCase()
|
|
135
|
-
const mimeType = EXT_TO_MIME[ext] || 'application/octet-stream'
|
|
114
|
+
const hash = this.#hashBytes(data)
|
|
115
|
+
const ext = mediaPath.split('.').pop().toLowerCase()
|
|
116
|
+
const mimeType = EXT_TO_MIME[ext] || 'application/octet-stream'
|
|
136
117
|
|
|
137
|
-
const mediaInfo = { zipPath: mediaPath, hash, mimeType, size: data.length }
|
|
138
|
-
this.#mediaHashIndex.set(hash, mediaPath)
|
|
139
|
-
this.#mediaRegistry.set(mediaPath, mediaInfo)
|
|
118
|
+
const mediaInfo = { zipPath: mediaPath, hash, mimeType, size: data.length }
|
|
119
|
+
this.#mediaHashIndex.set(hash, mediaPath)
|
|
120
|
+
this.#mediaRegistry.set(mediaPath, mediaInfo)
|
|
140
121
|
|
|
141
122
|
// Track the highest media ID to avoid collisions
|
|
142
|
-
const numMatch = /\d+/.exec(mediaPath.split('/').pop())
|
|
123
|
+
const numMatch = /\d+/.exec(mediaPath.split('/').pop())
|
|
143
124
|
if (numMatch) {
|
|
144
|
-
const num = parseInt(numMatch[0], 10)
|
|
145
|
-
if (num >= this.#nextMediaId) this.#nextMediaId = num + 1
|
|
125
|
+
const num = parseInt(numMatch[0], 10)
|
|
126
|
+
if (num >= this.#nextMediaId) this.#nextMediaId = num + 1
|
|
146
127
|
}
|
|
147
128
|
}
|
|
148
129
|
})
|
|
149
|
-
)
|
|
130
|
+
)
|
|
150
131
|
|
|
151
|
-
logger.debug(`Indexed ${this.#mediaRegistry.size} media file(s)`)
|
|
132
|
+
logger.debug(`Indexed ${this.#mediaRegistry.size} media file(s)`)
|
|
152
133
|
}
|
|
153
134
|
|
|
154
135
|
/**
|
|
@@ -156,7 +137,7 @@ export class MediaManager {
|
|
|
156
137
|
* @returns {number}
|
|
157
138
|
*/
|
|
158
139
|
get mediaCount() {
|
|
159
|
-
return this.#mediaRegistry.size
|
|
140
|
+
return this.#mediaRegistry.size
|
|
160
141
|
}
|
|
161
142
|
|
|
162
143
|
/**
|
|
@@ -169,44 +150,44 @@ export class MediaManager {
|
|
|
169
150
|
* @returns {Promise<string>} ZIP path of the embedded image (e.g., 'ppt/media/image5.png').
|
|
170
151
|
*/
|
|
171
152
|
async embedImage(source, mimeType) {
|
|
172
|
-
let data
|
|
173
|
-
let ext
|
|
153
|
+
let data
|
|
154
|
+
let ext
|
|
174
155
|
|
|
175
156
|
if (typeof source === 'string') {
|
|
176
157
|
// Load from file path
|
|
177
|
-
data = await fsExtra.readFile(source)
|
|
178
|
-
ext = source.split('.').pop().toLowerCase()
|
|
179
|
-
mimeType = mimeType || EXT_TO_MIME[ext] || 'image/png'
|
|
158
|
+
data = await fsExtra.readFile(source)
|
|
159
|
+
ext = source.split('.').pop().toLowerCase()
|
|
160
|
+
mimeType = mimeType || EXT_TO_MIME[ext] || 'image/png'
|
|
180
161
|
} else if (Buffer.isBuffer(source) || source instanceof Uint8Array) {
|
|
181
|
-
data = source
|
|
162
|
+
data = source
|
|
182
163
|
// Detect format from magic bytes
|
|
183
|
-
ext = this.#detectExtension(data)
|
|
184
|
-
mimeType = mimeType || EXT_TO_MIME[ext] || 'image/png'
|
|
164
|
+
ext = this.#detectExtension(data)
|
|
165
|
+
mimeType = mimeType || EXT_TO_MIME[ext] || 'image/png'
|
|
185
166
|
} else {
|
|
186
|
-
throw new PPTXError('embedImage: source must be a file path string or Buffer')
|
|
167
|
+
throw new PPTXError('embedImage: source must be a file path string or Buffer')
|
|
187
168
|
}
|
|
188
169
|
|
|
189
170
|
// Check for duplicate (content-addressable dedup)
|
|
190
|
-
const hash = this.#hashBytes(data)
|
|
171
|
+
const hash = this.#hashBytes(data)
|
|
191
172
|
if (this.#mediaHashIndex.has(hash)) {
|
|
192
|
-
const existingPath = this.#mediaHashIndex.get(hash)
|
|
193
|
-
logger.debug(`Reusing existing media: ${existingPath} (hash: ${hash.substring(0, 8)}...)`)
|
|
194
|
-
return existingPath
|
|
173
|
+
const existingPath = this.#mediaHashIndex.get(hash)
|
|
174
|
+
logger.debug(`Reusing existing media: ${existingPath} (hash: ${hash.substring(0, 8)}...)`)
|
|
175
|
+
return existingPath
|
|
195
176
|
}
|
|
196
177
|
|
|
197
178
|
// Create a new media file
|
|
198
|
-
const mediaId = this.#nextMediaId
|
|
199
|
-
const zipPath = `ppt/media/image${mediaId}.${ext}
|
|
179
|
+
const mediaId = this.#nextMediaId++
|
|
180
|
+
const zipPath = `ppt/media/image${mediaId}.${ext}`
|
|
200
181
|
|
|
201
|
-
this.#zipManager.writeBinaryFile(zipPath, data)
|
|
202
|
-
this.#mediaHashIndex.set(hash, zipPath)
|
|
203
|
-
this.#mediaRegistry.set(zipPath, { zipPath, hash, mimeType, size: data.length })
|
|
182
|
+
this.#zipManager.writeBinaryFile(zipPath, data)
|
|
183
|
+
this.#mediaHashIndex.set(hash, zipPath)
|
|
184
|
+
this.#mediaRegistry.set(zipPath, { zipPath, hash, mimeType, size: data.length })
|
|
204
185
|
|
|
205
186
|
// Register content type
|
|
206
|
-
this.#registerContentType(ext, mimeType)
|
|
187
|
+
this.#registerContentType(ext, mimeType)
|
|
207
188
|
|
|
208
|
-
logger.debug(`Embedded new media: ${zipPath} (${data.length} bytes)`)
|
|
209
|
-
return zipPath
|
|
189
|
+
logger.debug(`Embedded new media: ${zipPath} (${data.length} bytes)`)
|
|
190
|
+
return zipPath
|
|
210
191
|
}
|
|
211
192
|
|
|
212
193
|
/**
|
|
@@ -223,7 +204,7 @@ export class MediaManager {
|
|
|
223
204
|
* @returns {string} XML snippet for the image.
|
|
224
205
|
*/
|
|
225
206
|
buildImageXml(rId, opts) {
|
|
226
|
-
const { x = 0, y = 0, width = 2743200, height = 1828800, name = 'image', shapeId = 1 } = opts
|
|
207
|
+
const { x = 0, y = 0, width = 2743200, height = 1828800, name = 'image', shapeId = 1 } = opts
|
|
227
208
|
|
|
228
209
|
return `<p:pic>
|
|
229
210
|
<p:nvPicPr>
|
|
@@ -244,7 +225,7 @@ export class MediaManager {
|
|
|
244
225
|
</a:xfrm>
|
|
245
226
|
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
|
|
246
227
|
</p:spPr>
|
|
247
|
-
</p:pic
|
|
228
|
+
</p:pic>`
|
|
248
229
|
}
|
|
249
230
|
|
|
250
231
|
/**
|
|
@@ -254,7 +235,7 @@ export class MediaManager {
|
|
|
254
235
|
* @returns {MediaInfo|undefined}
|
|
255
236
|
*/
|
|
256
237
|
getMediaInfo(zipPath) {
|
|
257
|
-
return this.#mediaRegistry.get(zipPath)
|
|
238
|
+
return this.#mediaRegistry.get(zipPath)
|
|
258
239
|
}
|
|
259
240
|
|
|
260
241
|
/**
|
|
@@ -262,7 +243,7 @@ export class MediaManager {
|
|
|
262
243
|
* @returns {MediaInfo[]}
|
|
263
244
|
*/
|
|
264
245
|
getAllMedia() {
|
|
265
|
-
return Array.from(this.#mediaRegistry.values())
|
|
246
|
+
return Array.from(this.#mediaRegistry.values())
|
|
266
247
|
}
|
|
267
248
|
|
|
268
249
|
/**
|
|
@@ -274,7 +255,7 @@ export class MediaManager {
|
|
|
274
255
|
* @returns {string} Hex digest.
|
|
275
256
|
*/
|
|
276
257
|
#hashBytes(data) {
|
|
277
|
-
return createHash('sha1').update(data).digest('hex')
|
|
258
|
+
return createHash('sha1').update(data).digest('hex')
|
|
278
259
|
}
|
|
279
260
|
|
|
280
261
|
/**
|
|
@@ -285,23 +266,25 @@ export class MediaManager {
|
|
|
285
266
|
* @returns {string} File extension.
|
|
286
267
|
*/
|
|
287
268
|
#detectExtension(data) {
|
|
288
|
-
const sig = data.slice(0, 8)
|
|
269
|
+
const sig = data.slice(0, 8)
|
|
289
270
|
|
|
290
271
|
// PNG: 89 50 4E 47 0D 0A 1A 0A
|
|
291
|
-
if (sig[0] === 0x89 && sig[1] === 0x50) return 'png'
|
|
272
|
+
if (sig[0] === 0x89 && sig[1] === 0x50) return 'png'
|
|
292
273
|
// JPEG: FF D8 FF
|
|
293
|
-
if (sig[0] ===
|
|
274
|
+
if (sig[0] === 0xff && sig[1] === 0xd8) return 'jpg'
|
|
294
275
|
// GIF: 47 49 46
|
|
295
|
-
if (sig[0] === 0x47 && sig[1] === 0x49) return 'gif'
|
|
276
|
+
if (sig[0] === 0x47 && sig[1] === 0x49) return 'gif'
|
|
296
277
|
// WEBP: 52 49 46 46 ... 57 45 42 50
|
|
297
|
-
if (sig[0] === 0x52 && sig[1] === 0x49 && sig[8] === 0x57) return 'webp'
|
|
278
|
+
if (sig[0] === 0x52 && sig[1] === 0x49 && sig[8] === 0x57) return 'webp'
|
|
298
279
|
// BMP: 42 4D
|
|
299
|
-
if (sig[0] === 0x42 && sig[1] ===
|
|
280
|
+
if (sig[0] === 0x42 && sig[1] === 0x4d) return 'bmp'
|
|
300
281
|
|
|
301
|
-
return 'png'
|
|
282
|
+
return 'png' // Default fallback
|
|
302
283
|
}
|
|
303
284
|
|
|
304
285
|
#registerContentType(ext, mimeType) {
|
|
305
|
-
this.#contentTypesManager.addDefault(ext, mimeType)
|
|
286
|
+
this.#contentTypesManager.addDefault(ext, mimeType)
|
|
306
287
|
}
|
|
307
288
|
}
|
|
289
|
+
|
|
290
|
+
module.exports = { MediaManager }
|