node-pptx-templater 1.0.2 → 1.0.4

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +28 -3
  2. package/README.md +175 -327
  3. package/package.json +12 -3
  4. package/src/cli/commands/build.js +30 -31
  5. package/src/cli/commands/debug.js +23 -23
  6. package/src/cli/commands/extract.js +21 -21
  7. package/src/cli/commands/inspect.js +23 -23
  8. package/src/cli/commands/validate.js +17 -17
  9. package/src/cli/index.js +39 -36
  10. package/src/core/OutputWriter.js +79 -78
  11. package/src/core/PPTXTemplater.js +856 -273
  12. package/src/core/TemplateEngine.js +67 -71
  13. package/src/core/ValidationEngine.js +246 -0
  14. package/src/index.js +30 -17
  15. package/src/managers/ChartManager.js +195 -70
  16. package/src/managers/ContentTypesManager.js +49 -45
  17. package/src/managers/HyperlinkManager.js +146 -142
  18. package/src/managers/ImageManager.js +336 -0
  19. package/src/managers/MediaManager.js +62 -81
  20. package/src/managers/RelationshipManager.js +99 -95
  21. package/src/managers/ShapeManager.js +340 -0
  22. package/src/managers/SlideManager.js +408 -311
  23. package/src/managers/TableManager.js +979 -262
  24. package/src/managers/TextManager.js +197 -0
  25. package/src/managers/ZipManager.js +69 -69
  26. package/src/managers/charts/ChartCacheGenerator.js +75 -58
  27. package/src/managers/charts/ChartParser.js +9 -13
  28. package/src/managers/charts/ChartRelationshipManager.js +12 -10
  29. package/src/managers/charts/ChartWorkbookUpdater.js +59 -56
  30. package/src/parsers/XMLParser.js +47 -50
  31. package/src/templates/blankPptx.js +3 -2
  32. package/src/templates/slideTemplate.js +28 -34
  33. package/src/utils/contentTypesHelper.js +40 -54
  34. package/src/utils/errors.js +18 -18
  35. package/src/utils/idUtils.js +16 -14
  36. package/src/utils/logger.js +18 -16
  37. package/src/utils/relationshipUtils.js +19 -20
  38. package/src/utils/xmlUtils.js +26 -26
@@ -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
- const { createHash } = require('crypto');
40
- const { createLogger } = require('../utils/logger.js');
41
- const { PPTXError } = require('../utils/errors.js');
42
- const { REL_TYPES } = require('./RelationshipManager.js');
43
- const fsExtra = require('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,7 +59,7 @@ 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
@@ -86,34 +67,34 @@ const EXT_TO_MIME = {
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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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,25 +266,25 @@ 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] === 0xFF && sig[1] === 0xD8) return 'jpg';
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] === 0x4D) return 'bmp';
280
+ if (sig[0] === 0x42 && sig[1] === 0x4d) return 'bmp'
300
281
 
301
- return 'png'; // Default fallback
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
  }
308
289
 
309
- module.exports = { MediaManager };
290
+ module.exports = { MediaManager }