node-pptx-templater 1.0.17 → 1.0.19

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.
@@ -41,10 +41,7 @@ class ImageManager {
41
41
  mediaManager,
42
42
  relationshipManager
43
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)
44
+ const picRes = slideManager.getSlidePic(slideIndex, imageIdOrName)
48
45
 
49
46
  if (!picRes) {
50
47
  throw new PPTXError(`Image shape "${imageIdOrName}" not found on slide ${slideIndex}`)
@@ -85,9 +82,11 @@ class ImageManager {
85
82
  relationshipManager
86
83
  ) {
87
84
  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']
85
+ const slideObj = slideManager.getSlideObj(slideIndex)
86
+ const spTree =
87
+ slideObj?.['p:sld']?.['p:cSld']?.['p:spTree'] ||
88
+ slideObj?.['p:sldLayout']?.['p:cSld']?.['p:spTree'] ||
89
+ slideObj?.['p:sldMaster']?.['p:cSld']?.['p:spTree']
91
90
 
92
91
  if (!spTree) {
93
92
  throw new PPTXError(`Invalid slide structure for slide ${slideIndex}`)
@@ -155,8 +154,7 @@ class ImageManager {
155
154
  }
156
155
  spTree['p:pic'].push(picObj)
157
156
 
158
- const decl = this.#xmlParser.extractDeclaration(slideXml)
159
- slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
157
+ slideManager.markSlideObjDirty(slideIndex)
160
158
  logger.debug(`Added image "${name}" with ID ${newId} and rId ${rId} to slide ${slideIndex}`)
161
159
  }
162
160
 
@@ -169,10 +167,7 @@ class ImageManager {
169
167
  * @param {RelationshipManager} relationshipManager
170
168
  */
171
169
  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)
170
+ const picRes = slideManager.getSlidePic(slideIndex, imageIdOrName)
176
171
 
177
172
  if (!picRes) {
178
173
  throw new PPTXError(`Image shape "${imageIdOrName}" not found on slide ${slideIndex}`)
@@ -195,8 +190,7 @@ class ImageManager {
195
190
  relationshipManager.removeRelationship(slideInfo.zipPath, rId)
196
191
  }
197
192
 
198
- const decl = this.#xmlParser.extractDeclaration(slideXml)
199
- slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
193
+ slideManager.markSlideObjDirty(slideIndex)
200
194
  logger.debug(`Removed image "${imageIdOrName}" from slide ${slideIndex}`)
201
195
  }
202
196
 
@@ -210,9 +204,11 @@ class ImageManager {
210
204
  */
211
205
  getImages(slideIndex, slideManager, relationshipManager) {
212
206
  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']
207
+ const slideObj = slideManager.getSlideObj(slideIndex)
208
+ const spTree =
209
+ slideObj?.['p:sld']?.['p:cSld']?.['p:spTree'] ||
210
+ slideObj?.['p:sldLayout']?.['p:cSld']?.['p:spTree'] ||
211
+ slideObj?.['p:sldMaster']?.['p:cSld']?.['p:spTree']
216
212
 
217
213
  const imagesInfo = []
218
214
  this.#collectImagesInfo(spTree, slideInfo.zipPath, relationshipManager, imagesInfo)
@@ -43,6 +43,15 @@ const fsExtra = require('fs-extra')
43
43
 
44
44
  const logger = createLogger('MediaManager')
45
45
 
46
+ function streamToBuffer(stream) {
47
+ return new Promise((resolve, reject) => {
48
+ const chunks = []
49
+ stream.on('data', chunk => chunks.push(chunk))
50
+ stream.on('end', () => resolve(Buffer.concat(chunks)))
51
+ stream.on('error', reject)
52
+ })
53
+ }
54
+
46
55
  /**
47
56
  * Extension to MIME type mapping.
48
57
  */
@@ -106,30 +115,37 @@ class MediaManager {
106
115
  this.#zipManager = zipManager
107
116
  const mediaFiles = zipManager.listFiles('ppt/media/')
108
117
 
109
- // Index all existing media files by content hash for deduplication
110
- await Promise.all(
111
- mediaFiles.map(async mediaPath => {
112
- const data = await zipManager.readBinaryFile(mediaPath)
118
+ // Register all existing media files without loading/hashing them yet
119
+ for (const mediaPath of mediaFiles) {
120
+ const ext = mediaPath.split('.').pop().toLowerCase()
121
+ const mimeType = EXT_TO_MIME[ext] || 'application/octet-stream'
122
+
123
+ const mediaInfo = { zipPath: mediaPath, hash: null, mimeType, size: null }
124
+ this.#mediaRegistry.set(mediaPath, mediaInfo)
125
+
126
+ // Track the highest media ID to avoid collisions
127
+ const numMatch = /\d+/.exec(mediaPath.split('/').pop())
128
+ if (numMatch) {
129
+ const num = parseInt(numMatch[0], 10)
130
+ if (num >= this.#nextMediaId) this.#nextMediaId = num + 1
131
+ }
132
+ }
133
+
134
+ logger.debug(`Registered ${this.#mediaRegistry.size} media file(s) (lazy loading enabled)`)
135
+ }
136
+
137
+ async #ensureAllMediaHashed() {
138
+ for (const [mediaPath, mediaInfo] of this.#mediaRegistry.entries()) {
139
+ if (mediaInfo.hash === null) {
140
+ const data = await this.#zipManager.readBinaryFile(mediaPath)
113
141
  if (data) {
114
142
  const hash = this.#hashBytes(data)
115
- const ext = mediaPath.split('.').pop().toLowerCase()
116
- const mimeType = EXT_TO_MIME[ext] || 'application/octet-stream'
117
-
118
- const mediaInfo = { zipPath: mediaPath, hash, mimeType, size: data.length }
143
+ mediaInfo.hash = hash
144
+ mediaInfo.size = data.length
119
145
  this.#mediaHashIndex.set(hash, mediaPath)
120
- this.#mediaRegistry.set(mediaPath, mediaInfo)
121
-
122
- // Track the highest media ID to avoid collisions
123
- const numMatch = /\d+/.exec(mediaPath.split('/').pop())
124
- if (numMatch) {
125
- const num = parseInt(numMatch[0], 10)
126
- if (num >= this.#nextMediaId) this.#nextMediaId = num + 1
127
- }
128
146
  }
129
- })
130
- )
131
-
132
- logger.debug(`Indexed ${this.#mediaRegistry.size} media file(s)`)
147
+ }
148
+ }
133
149
  }
134
150
 
135
151
  /**
@@ -152,23 +168,50 @@ class MediaManager {
152
168
  async embedImage(source, mimeType) {
153
169
  let data
154
170
  let ext
171
+ let hash
172
+ let size
173
+ const isStream = source && typeof source.on === 'function' && typeof source.pipe === 'function'
155
174
 
156
- if (typeof source === 'string') {
157
- // Load from file path
175
+ if (isStream) {
176
+ // Buffer the stream to avoid JSZip streaming pipeline crashes and file locks
177
+ data = await streamToBuffer(source)
178
+ if (source.path && typeof source.path === 'string') {
179
+ const filePath = source.path
180
+ ext = filePath.split('.').pop().toLowerCase()
181
+ } else {
182
+ ext = this.#detectExtension(data)
183
+ }
184
+ mimeType = mimeType || EXT_TO_MIME[ext] || 'image/png'
185
+ hash = this.#hashBytes(data)
186
+ size = data.length
187
+ } else if (typeof source === 'string') {
188
+ // Load from file path directly to buffer
158
189
  data = await fsExtra.readFile(source)
159
190
  ext = source.split('.').pop().toLowerCase()
160
191
  mimeType = mimeType || EXT_TO_MIME[ext] || 'image/png'
192
+ hash = this.#hashBytes(data)
193
+ size = data.length
161
194
  } else if (Buffer.isBuffer(source) || source instanceof Uint8Array) {
162
195
  data = source
163
- // Detect format from magic bytes
164
196
  ext = this.#detectExtension(data)
165
197
  mimeType = mimeType || EXT_TO_MIME[ext] || 'image/png'
198
+ hash = this.#hashBytes(data)
199
+ size = data.length
166
200
  } else {
167
- throw new PPTXError('embedImage: source must be a file path string or Buffer')
201
+ throw new PPTXError(
202
+ 'embedImage: source must be a file path string, Buffer, or Readable Stream'
203
+ )
204
+ }
205
+
206
+ if (this.#mediaHashIndex.has(hash)) {
207
+ const existingPath = this.#mediaHashIndex.get(hash)
208
+ logger.debug(`Reusing existing media: ${existingPath} (hash: ${hash.substring(0, 8)}...)`)
209
+ return existingPath
168
210
  }
169
211
 
170
- // Check for duplicate (content-addressable dedup)
171
- const hash = this.#hashBytes(data)
212
+ // Ensure all media from template is hashed to check for duplicates
213
+ await this.#ensureAllMediaHashed()
214
+
172
215
  if (this.#mediaHashIndex.has(hash)) {
173
216
  const existingPath = this.#mediaHashIndex.get(hash)
174
217
  logger.debug(`Reusing existing media: ${existingPath} (hash: ${hash.substring(0, 8)}...)`)
@@ -180,13 +223,14 @@ class MediaManager {
180
223
  const zipPath = `ppt/media/image${mediaId}.${ext}`
181
224
 
182
225
  this.#zipManager.writeBinaryFile(zipPath, data)
226
+
183
227
  this.#mediaHashIndex.set(hash, zipPath)
184
- this.#mediaRegistry.set(zipPath, { zipPath, hash, mimeType, size: data.length })
228
+ this.#mediaRegistry.set(zipPath, { zipPath, hash, mimeType, size })
185
229
 
186
230
  // Register content type
187
231
  this.#registerContentType(ext, mimeType)
188
232
 
189
- logger.debug(`Embedded new media: ${zipPath} (${data.length} bytes)`)
233
+ logger.debug(`Embedded new media: ${zipPath} (${size} bytes)`)
190
234
  return zipPath
191
235
  }
192
236
 
@@ -31,19 +31,14 @@ class ShapeManager {
31
31
  * @param {SlideManager} slideManager
32
32
  */
33
33
  updateShapeText(slideIndex, shapeId, text, slideManager) {
34
- const slideXml = slideManager.getSlideXml(slideIndex)
35
- const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
36
- const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
37
- const res = this.findShapeRecursive(spTree, shapeId)
34
+ const res = slideManager.getSlideShape(slideIndex, shapeId)
38
35
 
39
36
  if (!res) {
40
37
  throw new PPTXError(`Shape "${shapeId}" not found in slide ${slideIndex}`)
41
38
  }
42
39
 
43
40
  this.#setShapeTextObj(res.shape, text)
44
-
45
- const decl = this.#xmlParser.extractDeclaration(slideXml)
46
- slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
41
+ slideManager.markSlideObjDirty(slideIndex)
47
42
  logger.debug(`Updated text for shape "${shapeId}" on slide ${slideIndex}`)
48
43
  }
49
44
 
@@ -60,10 +55,7 @@ class ShapeManager {
60
55
  * @param {SlideManager} slideManager
61
56
  */
62
57
  updateShapePosition(slideIndex, shapeId, options = {}, slideManager) {
63
- const slideXml = slideManager.getSlideXml(slideIndex)
64
- const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
65
- const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
66
- const res = this.findShapeRecursive(spTree, shapeId)
58
+ const res = slideManager.getSlideShape(slideIndex, shapeId)
67
59
 
68
60
  if (!res) {
69
61
  throw new PPTXError(`Shape "${shapeId}" not found in slide ${slideIndex}`)
@@ -108,8 +100,7 @@ class ShapeManager {
108
100
  xfrm['a:ext']['@_cy'] = '0'
109
101
  }
110
102
 
111
- const decl = this.#xmlParser.extractDeclaration(slideXml)
112
- slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
103
+ slideManager.markSlideObjDirty(slideIndex)
113
104
  logger.debug(`Updated position/dimensions for shape "${shapeId}" on slide ${slideIndex}`)
114
105
  }
115
106
 
@@ -146,10 +137,8 @@ class ShapeManager {
146
137
  * @param {SlideManager} slideManager
147
138
  */
148
139
  cloneShape(slideIndex, shapeId, newShapeId, options = {}, slideManager) {
149
- const slideXml = slideManager.getSlideXml(slideIndex)
150
- const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
151
- const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
152
- const res = this.findShapeRecursive(spTree, shapeId)
140
+ const slideObj = slideManager.getSlideObj(slideIndex)
141
+ const res = slideManager.getSlideShape(slideIndex, shapeId)
153
142
 
154
143
  if (!res) {
155
144
  throw new PPTXError(`Shape "${shapeId}" not found in slide ${slideIndex}`)
@@ -158,6 +147,11 @@ class ShapeManager {
158
147
  const newShape = this.#xmlParser.deepClone(res.shape)
159
148
  const cNvPr = newShape['p:nvSpPr']?.['p:cNvPr']
160
149
 
150
+ const spTree =
151
+ slideObj?.['p:sld']?.['p:cSld']?.['p:spTree'] ||
152
+ slideObj?.['p:sldLayout']?.['p:cSld']?.['p:spTree'] ||
153
+ slideObj?.['p:sldMaster']?.['p:cSld']?.['p:spTree']
154
+
161
155
  const existingIds = this.#getAllShapeIds(spTree)
162
156
  const maxId = existingIds.length > 0 ? Math.max(...existingIds) : 1000
163
157
  const newId = maxId + 1
@@ -204,8 +198,7 @@ class ShapeManager {
204
198
  }
205
199
  parent['p:sp'].push(newShape)
206
200
 
207
- const decl = this.#xmlParser.extractDeclaration(slideXml)
208
- slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
201
+ slideManager.markSlideObjDirty(slideIndex)
209
202
  logger.debug(`Cloned shape "${shapeId}" as "${newShapeId}"`)
210
203
  }
211
204
 
@@ -217,10 +210,7 @@ class ShapeManager {
217
210
  * @param {SlideManager} slideManager
218
211
  */
219
212
  deleteShape(slideIndex, shapeId, slideManager) {
220
- const slideXml = slideManager.getSlideXml(slideIndex)
221
- const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
222
- const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
223
- const res = this.findShapeRecursive(spTree, shapeId)
213
+ const res = slideManager.getSlideShape(slideIndex, shapeId)
224
214
 
225
215
  if (!res) {
226
216
  throw new PPTXError(`Shape "${shapeId}" not found in slide ${slideIndex}`)
@@ -235,8 +225,7 @@ class ShapeManager {
235
225
  }
236
226
  }
237
227
 
238
- const decl = this.#xmlParser.extractDeclaration(slideXml)
239
- slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
228
+ slideManager.markSlideObjDirty(slideIndex)
240
229
  logger.debug(`Deleted shape "${shapeId}" from slide ${slideIndex}`)
241
230
  }
242
231
 
@@ -248,9 +237,11 @@ class ShapeManager {
248
237
  * @returns {Array<Object>}
249
238
  */
250
239
  getShapes(slideIndex, slideManager) {
251
- const slideXml = slideManager.getSlideXml(slideIndex)
252
- const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
253
- const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
240
+ const slideObj = slideManager.getSlideObj(slideIndex)
241
+ const spTree =
242
+ slideObj?.['p:sld']?.['p:cSld']?.['p:spTree'] ||
243
+ slideObj?.['p:sldLayout']?.['p:cSld']?.['p:spTree'] ||
244
+ slideObj?.['p:sldMaster']?.['p:cSld']?.['p:spTree']
254
245
 
255
246
  const shapesInfo = []
256
247
  this.#collectShapesInfo(spTree, shapesInfo)
@@ -71,6 +71,12 @@ class SlideManager {
71
71
  */
72
72
  #slideXmlCache = new Map()
73
73
 
74
+ /**
75
+ * Slide states: maps 1-based index → state object.
76
+ * @private @type {Map<number, Object>}
77
+ */
78
+ #slideStates = new Map()
79
+
74
80
  /**
75
81
  * Custom tags: maps tag → array of 1-based indices.
76
82
  * @private @type {Map<string, number[]>}
@@ -177,6 +183,16 @@ class SlideManager {
177
183
  }
178
184
 
179
185
  this.#slides.set(slideIndex, slideInfo)
186
+ this.#slideStates.set(slideIndex, {
187
+ xmlStr: null,
188
+ xmlObj: null,
189
+ dirty: false,
190
+ indexBuilt: false,
191
+ shapeMap: new Map(),
192
+ picMap: new Map(),
193
+ tableMap: new Map(),
194
+ chartMap: new Map(),
195
+ })
180
196
  slideIndex++
181
197
  }
182
198
 
@@ -237,9 +253,27 @@ class SlideManager {
237
253
  getSlideXml(slideIndex) {
238
254
  this.#assertSlideExists(slideIndex)
239
255
  const info = this.#slides.get(slideIndex)
256
+ const state = this.#slideStates.get(slideIndex)
257
+
258
+ if (state.dirty && state.xmlObj) {
259
+ const decl = this.#xmlParser.extractDeclaration(
260
+ state.xmlStr || '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
261
+ )
262
+ state.xmlStr = this.#xmlParser.build(state.xmlObj, decl)
263
+ this.#zipManager.writeFile(info.zipPath, state.xmlStr)
264
+ this.#slideXmlCache.set(info.zipPath, state.xmlStr)
265
+ state.dirty = false
266
+ }
267
+
268
+ if (state.xmlStr) {
269
+ return state.xmlStr
270
+ }
240
271
 
241
- if (this.#slideXmlCache.has(info.zipPath)) {
242
- return this.#slideXmlCache.get(info.zipPath)
272
+ const cached = this.#zipManager.readCachedFile(info.zipPath)
273
+ if (cached) {
274
+ state.xmlStr = cached
275
+ this.#slideXmlCache.set(info.zipPath, cached)
276
+ return cached
243
277
  }
244
278
 
245
279
  // This is sync because we pre-load; async callers should use getSlideXmlAsync
@@ -255,14 +289,16 @@ class SlideManager {
255
289
  async getSlideXmlAsync(slideIndex) {
256
290
  this.#assertSlideExists(slideIndex)
257
291
  const info = this.#slides.get(slideIndex)
292
+ const state = this.#slideStates.get(slideIndex)
258
293
 
259
- if (!this.#slideXmlCache.has(info.zipPath)) {
294
+ if (!state.xmlStr) {
260
295
  const xml = await this.#zipManager.readFile(info.zipPath)
261
296
  if (!xml) throw new SlideNotFoundError(`Slide ${slideIndex} XML not found at ${info.zipPath}`)
297
+ state.xmlStr = xml
262
298
  this.#slideXmlCache.set(info.zipPath, xml)
263
299
  }
264
300
 
265
- return this.#slideXmlCache.get(info.zipPath)
301
+ return state.xmlStr
266
302
  }
267
303
 
268
304
  /**
@@ -274,6 +310,13 @@ class SlideManager {
274
310
  setSlideXml(slideIndex, xml) {
275
311
  this.#assertSlideExists(slideIndex)
276
312
  const info = this.#slides.get(slideIndex)
313
+ const state = this.#slideStates.get(slideIndex)
314
+
315
+ state.xmlStr = xml
316
+ state.xmlObj = null
317
+ state.dirty = false
318
+ state.indexBuilt = false
319
+
277
320
  this.#slideXmlCache.set(info.zipPath, xml)
278
321
  this.#zipManager.writeFile(info.zipPath, xml)
279
322
  }
@@ -360,6 +403,16 @@ class SlideManager {
360
403
  }
361
404
 
362
405
  this.#slides.set(newIndex, slideInfo)
406
+ this.#slideStates.set(newIndex, {
407
+ xmlStr: slideXml,
408
+ xmlObj: null,
409
+ dirty: false,
410
+ indexBuilt: false,
411
+ shapeMap: new Map(),
412
+ picMap: new Map(),
413
+ tableMap: new Map(),
414
+ chartMap: new Map(),
415
+ })
363
416
 
364
417
  // Update presentation.xml sldIdLst
365
418
  this.#addSlideToPresentation(rId, newSlideId)
@@ -424,6 +477,16 @@ class SlideManager {
424
477
  }
425
478
 
426
479
  this.#slides.set(newIndex, slideInfo)
480
+ this.#slideStates.set(newIndex, {
481
+ xmlStr: sourceXml,
482
+ xmlObj: null,
483
+ dirty: false,
484
+ indexBuilt: false,
485
+ shapeMap: new Map(),
486
+ picMap: new Map(),
487
+ tableMap: new Map(),
488
+ chartMap: new Map(),
489
+ })
427
490
  this.#addSlideToPresentation(rId, newSlideId)
428
491
  this.#registerSlideContentType(slideFileName)
429
492
 
@@ -448,6 +511,7 @@ class SlideManager {
448
511
 
449
512
  // Remove from cache
450
513
  this.#slideXmlCache.delete(info.zipPath)
514
+ this.#slideStates.delete(slideIndex)
451
515
 
452
516
  // Remove relationship from presentation.xml
453
517
  this.#relationshipManager.removeRelationship('ppt/presentation.xml', info.relationshipId)
@@ -479,13 +543,18 @@ class SlideManager {
479
543
  }
480
544
 
481
545
  const slidesCopy = new Map(this.#slides)
546
+ const statesCopy = new Map(this.#slideStates)
482
547
  this.#slides.clear()
548
+ this.#slideStates.clear()
483
549
 
484
550
  order.forEach((oldIndex, newPos) => {
485
551
  const info = slidesCopy.get(oldIndex)
486
552
  if (!info) throw new SlideNotFoundError(`Slide ${oldIndex} not found`)
487
553
  info.index = newPos + 1
488
554
  this.#slides.set(newPos + 1, info)
555
+
556
+ const state = statesCopy.get(oldIndex)
557
+ this.#slideStates.set(newPos + 1, state)
489
558
  })
490
559
 
491
560
  // Rebuild presentation sldIdLst
@@ -741,6 +810,16 @@ class SlideManager {
741
810
  }
742
811
 
743
812
  this.#slides.set(newIndex, slideInfo)
813
+ this.#slideStates.set(newIndex, {
814
+ xmlStr: slideXml,
815
+ xmlObj: null,
816
+ dirty: false,
817
+ indexBuilt: false,
818
+ shapeMap: new Map(),
819
+ picMap: new Map(),
820
+ tableMap: new Map(),
821
+ chartMap: new Map(),
822
+ })
744
823
 
745
824
  // Add entry in presentation.xml sldIdLst
746
825
  this.#addSlideToPresentation(rId, newSlideId)
@@ -917,10 +996,18 @@ class SlideManager {
917
996
  #reindexSlides() {
918
997
  const sorted = Array.from(this.#slides.entries()).sort(([a], [b]) => a - b)
919
998
  this.#slides.clear()
999
+
1000
+ const sortedStates = Array.from(this.#slideStates.entries()).sort(([a], [b]) => a - b)
1001
+ this.#slideStates.clear()
1002
+
920
1003
  sorted.forEach(([, info], i) => {
921
1004
  info.index = i + 1
922
1005
  this.#slides.set(i + 1, info)
923
1006
  })
1007
+
1008
+ sortedStates.forEach(([, state], i) => {
1009
+ this.#slideStates.set(i + 1, state)
1010
+ })
924
1011
  }
925
1012
 
926
1013
  /**
@@ -1044,6 +1131,162 @@ class SlideManager {
1044
1131
  )
1045
1132
  }
1046
1133
  }
1134
+
1135
+ getSlideObj(slideIndex) {
1136
+ this.#assertSlideExists(slideIndex)
1137
+ const state = this.#slideStates.get(slideIndex)
1138
+
1139
+ if (!state.xmlObj) {
1140
+ const xml = this.getSlideXml(slideIndex)
1141
+ const info = this.#slides.get(slideIndex)
1142
+ state.xmlObj = this.#xmlParser.parse(xml, info.zipPath.split('/').pop())
1143
+ state.indexBuilt = false
1144
+ }
1145
+
1146
+ if (!state.indexBuilt) {
1147
+ state.shapeMap.clear()
1148
+ state.picMap.clear()
1149
+ state.tableMap.clear()
1150
+ state.chartMap.clear()
1151
+
1152
+ const spTree =
1153
+ state.xmlObj?.['p:sld']?.['p:cSld']?.['p:spTree'] ||
1154
+ state.xmlObj?.['p:sldLayout']?.['p:cSld']?.['p:spTree'] ||
1155
+ state.xmlObj?.['p:sldMaster']?.['p:cSld']?.['p:spTree']
1156
+
1157
+ this.#buildSlideIndexRecursive(
1158
+ spTree,
1159
+ state.shapeMap,
1160
+ state.picMap,
1161
+ state.tableMap,
1162
+ state.chartMap
1163
+ )
1164
+ state.indexBuilt = true
1165
+ }
1166
+
1167
+ return state.xmlObj
1168
+ }
1169
+
1170
+ #buildSlideIndexRecursive(container, shapeMap, picMap, tableMap, chartMap) {
1171
+ if (!container) return
1172
+
1173
+ // Shapes
1174
+ let shapes = container['p:sp'] || []
1175
+ if (!Array.isArray(shapes)) shapes = [shapes]
1176
+ for (const shape of shapes) {
1177
+ const cNvPr = shape?.['p:nvSpPr']?.['p:cNvPr']
1178
+ if (cNvPr) {
1179
+ const name = cNvPr['@_name']
1180
+ const id = String(cNvPr['@_id'])
1181
+ const entry = { shape, parent: container, type: 'sp' }
1182
+ if (name) shapeMap.set(name, entry)
1183
+ if (id) shapeMap.set(id, entry)
1184
+ }
1185
+ }
1186
+
1187
+ // Pictures
1188
+ let pics = container['p:pic'] || []
1189
+ if (!Array.isArray(pics)) pics = [pics]
1190
+ for (const pic of pics) {
1191
+ const cNvPr = pic?.['p:nvPicPr']?.['p:cNvPr']
1192
+ if (cNvPr) {
1193
+ const name = cNvPr['@_name']
1194
+ const id = String(cNvPr['@_id'])
1195
+ const embedId = pic?.['p:blipFill']?.['a:blip']?.['@_r:embed']
1196
+ const entry = { pic, parent: container, type: 'pic' }
1197
+ if (name) picMap.set(name, entry)
1198
+ if (id) picMap.set(id, entry)
1199
+ if (embedId) picMap.set(embedId, entry)
1200
+ }
1201
+ }
1202
+
1203
+ // Graphic frames
1204
+ let frames = container['p:graphicFrame'] || []
1205
+ if (!Array.isArray(frames)) frames = [frames]
1206
+ for (const frame of frames) {
1207
+ const cNvPr = frame?.['p:nvGraphicFramePr']?.['p:cNvPr']
1208
+ const name = cNvPr ? cNvPr['@_name'] : null
1209
+ const id = cNvPr ? String(cNvPr['@_id']) : null
1210
+
1211
+ const tbl = frame?.['a:graphic']?.['a:graphicData']?.['a:tbl']
1212
+ if (tbl) {
1213
+ const entry = { table: tbl, frame, parent: container, type: 'table' }
1214
+ if (name) tableMap.set(name, entry)
1215
+ if (id) tableMap.set(id, entry)
1216
+ }
1217
+
1218
+ const chart = frame?.['a:graphic']?.['a:graphicData']?.['c:chart']
1219
+ if (chart) {
1220
+ const embedId = chart['@_r:id']
1221
+ const entry = { chart, frame, parent: container, type: 'chart' }
1222
+ if (name) chartMap.set(name, entry)
1223
+ if (id) chartMap.set(id, entry)
1224
+ if (embedId) chartMap.set(embedId, entry)
1225
+ }
1226
+ }
1227
+
1228
+ // Groups
1229
+ let groups = container['p:grpSp'] || []
1230
+ if (!Array.isArray(groups)) groups = [groups]
1231
+ for (const group of groups) {
1232
+ const cNvPr = group?.['p:nvGrpSpPr']?.['p:cNvPr']
1233
+ if (cNvPr) {
1234
+ const name = cNvPr['@_name']
1235
+ const id = String(cNvPr['@_id'])
1236
+ const entry = { shape: group, parent: container, type: 'grpSp' }
1237
+ if (name) shapeMap.set(name, entry)
1238
+ if (id) shapeMap.set(id, entry)
1239
+ }
1240
+ this.#buildSlideIndexRecursive(group, shapeMap, picMap, tableMap, chartMap)
1241
+ }
1242
+ }
1243
+
1244
+ flush() {
1245
+ for (const [index, state] of this.#slideStates) {
1246
+ if (state.dirty && state.xmlObj) {
1247
+ this.getSlideXml(index)
1248
+ }
1249
+ }
1250
+ }
1251
+
1252
+ markSlideObjDirty(slideIndex) {
1253
+ this.#assertSlideExists(slideIndex)
1254
+ const state = this.#slideStates.get(slideIndex)
1255
+ state.dirty = true
1256
+ state.indexBuilt = false
1257
+ }
1258
+
1259
+ getSlideShape(slideIndex, shapeId) {
1260
+ this.getSlideObj(slideIndex)
1261
+ return this.#slideStates.get(slideIndex).shapeMap.get(String(shapeId)) || null
1262
+ }
1263
+
1264
+ getSlidePic(slideIndex, picId) {
1265
+ this.getSlideObj(slideIndex)
1266
+ const state = this.#slideStates.get(slideIndex)
1267
+ if (picId === 'first') {
1268
+ return Array.from(state.picMap.values())[0] || null
1269
+ }
1270
+ return state.picMap.get(String(picId)) || null
1271
+ }
1272
+
1273
+ getSlideTable(slideIndex, tableId) {
1274
+ this.getSlideObj(slideIndex)
1275
+ const state = this.#slideStates.get(slideIndex)
1276
+ if (tableId === 'first') {
1277
+ return Array.from(state.tableMap.values())[0] || null
1278
+ }
1279
+ return state.tableMap.get(String(tableId)) || null
1280
+ }
1281
+
1282
+ getSlideChart(slideIndex, chartId) {
1283
+ this.getSlideObj(slideIndex)
1284
+ const state = this.#slideStates.get(slideIndex)
1285
+ if (chartId === 'first') {
1286
+ return Array.from(state.chartMap.values())[0] || null
1287
+ }
1288
+ return state.chartMap.get(String(chartId)) || null
1289
+ }
1047
1290
  }
1048
1291
 
1049
1292
  module.exports = { SlideManager }