node-pptx-templater 1.0.17 → 1.0.18

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 CHANGED
@@ -1340,22 +1340,38 @@ Saves the modified presentation XML structures directly to a folder.
1340
1340
  await ppt.saveToFolder('./output-template');
1341
1341
  ```
1342
1342
 
1343
- #### `toBuffer()`
1343
+ #### `toBuffer(options = {})`
1344
1344
  Returns the PPTX content as a Node.js Buffer.
1345
1345
 
1346
+ * **Arguments**:
1347
+ * `[options]` (`Object`): Save options.
1346
1348
  * **Returns**: `Promise<Buffer>` -
1347
1349
 
1348
1350
  ```javascript
1349
- ppt.useSlide(1).toBuffer();
1351
+ ppt.useSlide(1).toBuffer(options = {});
1350
1352
  ```
1351
1353
 
1352
- #### `toStream()`
1354
+ #### `toStream(options = {})`
1353
1355
  Returns the PPTX content as a readable Node.js Stream.
1354
1356
 
1357
+ * **Arguments**:
1358
+ * `[options]` (`Object`): Save options.
1355
1359
  * **Returns**: `Promise<NodeJS.ReadableStream>` -
1356
1360
 
1357
1361
  ```javascript
1358
- ppt.useSlide(1).toStream();
1362
+ ppt.useSlide(1).toStream(options = {});
1363
+ ```
1364
+
1365
+ #### `saveToStream(writableOrOptions, options = {})`
1366
+ Saves the presentation to a readable stream or pipes it to a writable stream.
1367
+
1368
+ * **Arguments**:
1369
+ * `[writableOrOptions]` (`NodeJS.WritableStream|Object`): Writable stream to pipe to, or options object.
1370
+ * `[options]` (`Object`): Save options if writable stream was passed first.
1371
+ * **Returns**: `Promise<NodeJS.ReadableStream|void>` -
1372
+
1373
+ ```javascript
1374
+ ppt.useSlide(1).saveToStream(writableOrOptions, options = {});
1359
1375
  ```
1360
1376
 
1361
1377
  #### `validatePresentationXml()`
@@ -1387,6 +1403,60 @@ OpenXML relationship IDs follow the format rId1, rId2, rId3, ... They must be un
1387
1403
  ppt.useSlide(1).function();
1388
1404
  ```
1389
1405
 
1406
+ #### `preload(())`
1407
+ Delegates core actions to slide element sub-managers.
1408
+
1409
+ * **Returns**: `PPTXTemplater` - The fluent engine instance.
1410
+
1411
+ ```javascript
1412
+ ppt.useSlide(1).preload(());
1413
+ ```
1414
+
1415
+ #### `cache(())`
1416
+ Delegates core actions to slide element sub-managers.
1417
+
1418
+ * **Returns**: `PPTXTemplater` - The fluent engine instance.
1419
+
1420
+ ```javascript
1421
+ ppt.useSlide(1).cache(());
1422
+ ```
1423
+
1424
+ #### `fromCache(())`
1425
+ Delegates core actions to slide element sub-managers.
1426
+
1427
+ * **Returns**: `PPTXTemplater` - The fluent engine instance.
1428
+
1429
+ ```javascript
1430
+ ppt.useSlide(1).fromCache(());
1431
+ ```
1432
+
1433
+ #### `clearCache(())`
1434
+ Delegates core actions to slide element sub-managers.
1435
+
1436
+ * **Returns**: `PPTXTemplater` - The fluent engine instance.
1437
+
1438
+ ```javascript
1439
+ ppt.useSlide(1).clearCache(());
1440
+ ```
1441
+
1442
+ #### `enablePerformanceProfile(())`
1443
+ Delegates core actions to slide element sub-managers.
1444
+
1445
+ * **Returns**: `PPTXTemplater` - The fluent engine instance.
1446
+
1447
+ ```javascript
1448
+ ppt.useSlide(1).enablePerformanceProfile(());
1449
+ ```
1450
+
1451
+ #### `getPerformanceMetrics(())`
1452
+ Delegates core actions to slide element sub-managers.
1453
+
1454
+ * **Returns**: `PPTXTemplater` - The fluent engine instance.
1455
+
1456
+ ```javascript
1457
+ ppt.useSlide(1).getPerformanceMetrics(());
1458
+ ```
1459
+
1390
1460
  #### `fromPresentationXml(())`
1391
1461
  Delegates core actions to slide element sub-managers.
1392
1462
 
@@ -1881,6 +1951,110 @@ Below are benchmark results compiled on a standard Intel Core i7 system processi
1881
1951
 
1882
1952
  ---
1883
1953
 
1954
+ ## ⚡ Performance Optimization & Caching APIs
1955
+
1956
+ The library provides first-class support for memory optimization, template caching, lazy loading, and streaming saves.
1957
+
1958
+ ### 1. In-Memory Template Caching (IIS & Server Environments)
1959
+ Instead of loading and parsing the PPTX ZIP structure from disk on every request, preload the template once. Subsequent templates can be instantiated from the cache in **0ms**:
1960
+
1961
+ ```javascript
1962
+ const { PPTXTemplater } = require('node-pptx-templater');
1963
+
1964
+ // Preload templates into memory cache at server startup
1965
+ await PPTXTemplater.preload('./templates/report.pptx');
1966
+
1967
+ // Load from cache instantly inside request handlers
1968
+ app.post('/generate-report', async (req, res) => {
1969
+ const ppt = await PPTXTemplater.fromCache('./templates/report.pptx');
1970
+
1971
+ ppt.useSlide(1).replaceText({ '{{title}}': req.body.title });
1972
+
1973
+ const buffer = await ppt.toBuffer();
1974
+ res.send(buffer);
1975
+ });
1976
+
1977
+ // Clear cache if templates change
1978
+ PPTXTemplater.clearCache();
1979
+ ```
1980
+
1981
+ ### 2. Performance Profiling
1982
+ Expose timing and memory metrics across the generation pipeline:
1983
+
1984
+ ```javascript
1985
+ const ppt = await PPTXTemplater.load('report.pptx');
1986
+ ppt.enablePerformanceProfile();
1987
+
1988
+ // Perform modifications...
1989
+ ppt.useSlide(1).replaceText({ '{{title}}': 'Performance' });
1990
+
1991
+ await ppt.toBuffer();
1992
+
1993
+ // Retrieve timing statistics (in milliseconds)
1994
+ const metrics = ppt.getPerformanceMetrics();
1995
+ console.log(metrics);
1996
+ /*
1997
+ Output:
1998
+ {
1999
+ enabled: true,
2000
+ templateLoadMs: 12.5,
2001
+ parseMs: 45.2,
2002
+ chartUpdateMs: 0,
2003
+ imageUpdateMs: 0,
2004
+ zipGenerationMs: 65.8,
2005
+ totalMs: 125.4,
2006
+ memoryUsedMB: 38.45
2007
+ }
2008
+ */
2009
+ ```
2010
+
2011
+ ### 3. Configurable ZIP Compression
2012
+ Balance CPU execution time and file size when saving the presentation. Compression options support `'none' | 'fast' | 'balanced' | 'maximum'`:
2013
+
2014
+ ```javascript
2015
+ // balanced is the default (level 6 DEFLATE)
2016
+ await ppt.save('output.pptx', { compression: 'balanced' });
2017
+
2018
+ // maximum compression (level 9 DEFLATE) - best file size, slightly slower
2019
+ await ppt.save('output.pptx', { compression: 'maximum' });
2020
+
2021
+ // fast compression (level 1 DEFLATE) - fast packaging, good compression
2022
+ await ppt.save('output.pptx', { compression: 'fast' });
2023
+
2024
+ // none / store (0% compression) - extremely fast, skips compression entirely
2025
+ const fastBuffer = await ppt.toBuffer({ compression: 'none' });
2026
+ ```
2027
+
2028
+ ### 4. Streaming Save & Streaming Image Input
2029
+ Avoid buffering large output files in memory by saving directly to readable/writable streams. You can also pass Readable streams (like `fs.createReadStream`) directly to image APIs:
2030
+
2031
+ ```javascript
2032
+ const fs = require('fs');
2033
+
2034
+ const ppt = await PPTXTemplater.load('report.pptx');
2035
+
2036
+ // Stream image from file path without loading into memory buffer
2037
+ const imageStream = fs.createReadStream('large-image.png');
2038
+ await ppt.useSlide(1).replaceImage('placeholder-img', imageStream);
2039
+
2040
+ // Stream final PPTX directly to file disk or HTTP response
2041
+ const writeStream = fs.createWriteStream('output.pptx');
2042
+ await ppt.saveToStream(writeStream);
2043
+ ```
2044
+
2045
+ ---
2046
+
2047
+ ## 🌐 IIS & Windows Server Deployment Guide
2048
+
2049
+ When deploying the library on **Windows Server** with **IIS** using `httpPlatformHandler` or `iisnode`, follow these production-ready recommendations:
2050
+
2051
+ 1. **Preload Large Templates**: Always call `await PPTXTemplater.preload(templatePath)` during application startup. This avoids high IIS request queue concurrency from competing for file handles or causing disk bottlenecks.
2052
+ 2. **Use Streaming Saves**: For concurrent routes serving large PPTX outputs, use `saveToStream()` to stream data straight into the HTTP response stream rather than buffering the output as large Node buffers.
2053
+ 3. **Optimize Compression**: If CPU cycles are a bottleneck on the IIS worker process, set `{ compression: 'fast' }` or `{ compression: 'none' }` on your save options.
2054
+ 4. **Increase httpPlatformHandler Request Limits**: Ensure the `requestTimeout` and `maxConnections` settings in your IIS `web.config` are set appropriately to allow long-running file streaming tasks.
2055
+
2056
+ ---
2057
+
1884
2058
  ## 🤝 Contributing
1885
2059
 
1886
2060
  We welcome contributions from the community. Please read our [CONTRIBUTING.md](./CONTRIBUTING.md) to set up the development environment, format code, and submit Pull Requests.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-pptx-templater",
3
- "version": "1.0.17",
3
+ "version": "1.0.18",
4
4
  "description": "High-performance, low-level PowerPoint (PPTX) OpenXML template engine for Node.js. Dynamically replace text, insert images, update charts (with Excel workbook data caching), and merge table cells without PowerPoint corruption or Repair Mode prompts.",
5
5
  "main": "./src/index.js",
6
6
  "type": "commonjs",
@@ -47,9 +47,9 @@ class OutputWriter {
47
47
  * @param {ZipManager} zipManager
48
48
  * @returns {Promise<void>}
49
49
  */
50
- async saveToFile(filePath, slideManager, zipManager) {
50
+ async saveToFile(filePath, slideManager, zipManager, options = {}) {
51
51
  try {
52
- const buffer = await this.toBuffer(slideManager, zipManager)
52
+ const buffer = await this.toBuffer(slideManager, zipManager, options)
53
53
  const dir = path.dirname(filePath)
54
54
  await ensureDir(dir)
55
55
  await writeFile(filePath, buffer)
@@ -60,13 +60,6 @@ class OutputWriter {
60
60
  }
61
61
  }
62
62
 
63
- /**
64
- * Returns the PPTX as a Node.js Buffer.
65
- *
66
- * @param {SlideManager} slideManager
67
- * @param {ZipManager} zipManager
68
- * @returns {Promise<Buffer>}
69
- */
70
63
  /**
71
64
  * Flushes all pending changes from all managers into the ZipManager.
72
65
  *
@@ -75,6 +68,9 @@ class OutputWriter {
75
68
  * @returns {Promise<void>}
76
69
  */
77
70
  async flush(slideManager, zipManager) {
71
+ if (slideManager && typeof slideManager.flush === 'function') {
72
+ slideManager.flush()
73
+ }
78
74
  await this.#flushAllSlides(slideManager, zipManager)
79
75
  this.#contentTypesManager.flush(zipManager)
80
76
  await zipManager.waitForPendingWrites()
@@ -85,12 +81,13 @@ class OutputWriter {
85
81
  *
86
82
  * @param {SlideManager} slideManager
87
83
  * @param {ZipManager} zipManager
84
+ * @param {Object} [options]
88
85
  * @returns {Promise<Buffer>}
89
86
  */
90
- async toBuffer(slideManager, zipManager) {
87
+ async toBuffer(slideManager, zipManager, options = {}) {
91
88
  await this.flush(slideManager, zipManager)
92
89
 
93
- const buffer = await zipManager.toBuffer()
90
+ const buffer = await zipManager.toBuffer(options)
94
91
  logger.debug(`Generated buffer: ${(buffer.length / 1024).toFixed(1)} KB`)
95
92
 
96
93
  if (this.debugZip) {
@@ -105,14 +102,15 @@ class OutputWriter {
105
102
  *
106
103
  * @param {SlideManager} slideManager
107
104
  * @param {ZipManager} zipManager
105
+ * @param {Object} [options]
108
106
  * @returns {Promise<Readable>}
109
107
  */
110
- async toStream(slideManager, zipManager) {
108
+ async toStream(slideManager, zipManager, options = {}) {
111
109
  await this.flush(slideManager, zipManager)
112
- const nodeStream = await zipManager.toStream()
110
+ const nodeStream = await zipManager.toStream(options)
113
111
 
114
112
  if (this.debugZip) {
115
- const buffer = await zipManager.toBuffer()
113
+ const buffer = await zipManager.toBuffer(options)
116
114
  this.printDebugZip(buffer)
117
115
  }
118
116
 
@@ -42,6 +42,7 @@ const { OutputWriter } = require('./OutputWriter.js')
42
42
  const { TemplateEngine } = require('./TemplateEngine.js')
43
43
  const { createLogger } = require('../utils/logger.js')
44
44
  const { PPTXError } = require('../utils/errors.js')
45
+ const { performance } = require('perf_hooks')
45
46
 
46
47
  const logger = createLogger('PPTXTemplater')
47
48
 
@@ -158,6 +159,14 @@ class PPTXTemplater {
158
159
  */
159
160
  #loaded = false
160
161
 
162
+ /**
163
+ * @private
164
+ * @type {Object}
165
+ */
166
+ #profiler
167
+
168
+ static #templateCache = new Map()
169
+
161
170
  constructor() {
162
171
  this.#xmlParser = new XMLParser()
163
172
  this.#zipManager = new ZipManager()
@@ -178,6 +187,18 @@ class PPTXTemplater {
178
187
  this.#templateEngine = new TemplateEngine(this.#xmlParser)
179
188
  this.#zOrderManager = new ZOrderManager(this.#xmlParser)
180
189
  this.#outputWriter = new OutputWriter(this.#zipManager, this.#contentTypesManager)
190
+
191
+ this.#profiler = {
192
+ enabled: false,
193
+ templateLoadMs: 0,
194
+ parseMs: 0,
195
+ chartUpdateMs: 0,
196
+ imageUpdateMs: 0,
197
+ zipGenerationMs: 0,
198
+ totalMs: 0,
199
+ memoryUsedMB: 0,
200
+ startTime: performance.now(),
201
+ }
181
202
  }
182
203
 
183
204
  /**
@@ -202,6 +223,95 @@ class PPTXTemplater {
202
223
  return engine
203
224
  }
204
225
 
226
+ static async preload(source) {
227
+ let key = source
228
+ if (Buffer.isBuffer(source)) {
229
+ const crypto = require('crypto')
230
+ key = crypto.createHash('sha256').update(source).digest('hex')
231
+ } else if (typeof source === 'object' && source !== null) {
232
+ key = JSON.stringify(source)
233
+ }
234
+
235
+ if (PPTXTemplater.#templateCache.has(key)) {
236
+ return PPTXTemplater.#templateCache.get(key)
237
+ }
238
+
239
+ const zipManager = new ZipManager()
240
+ await zipManager.load(source)
241
+ const files = zipManager.listFiles()
242
+ const cachedFiles = new Map()
243
+ for (const file of files) {
244
+ const ext = file.split('.').pop().toLowerCase()
245
+ const isText = ext === 'xml' || ext === 'rels' || ext === 'txt'
246
+ if (isText) {
247
+ const content = await zipManager.readFile(file)
248
+ cachedFiles.set(file, { type: 'text', content })
249
+ } else {
250
+ const content = await zipManager.readBinaryFile(file)
251
+ cachedFiles.set(file, { type: 'binary', content })
252
+ }
253
+ }
254
+
255
+ PPTXTemplater.#templateCache.set(key, cachedFiles)
256
+ return cachedFiles
257
+ }
258
+
259
+ static async cache(source) {
260
+ return PPTXTemplater.preload(source)
261
+ }
262
+
263
+ static async fromCache(source) {
264
+ let key = source
265
+ if (Buffer.isBuffer(source)) {
266
+ const crypto = require('crypto')
267
+ key = crypto.createHash('sha256').update(source).digest('hex')
268
+ } else if (typeof source === 'object' && source !== null) {
269
+ key = JSON.stringify(source)
270
+ }
271
+
272
+ let cachedFiles = PPTXTemplater.#templateCache.get(key)
273
+ if (!cachedFiles) {
274
+ cachedFiles = await PPTXTemplater.preload(source)
275
+ }
276
+
277
+ const engine = new PPTXTemplater()
278
+ await engine.#initializeFromCache(cachedFiles)
279
+ return engine
280
+ }
281
+
282
+ static clearCache() {
283
+ PPTXTemplater.#templateCache.clear()
284
+ }
285
+
286
+ enablePerformanceProfile() {
287
+ this.#profiler.enabled = true
288
+ this.#profiler.startTime = performance.now()
289
+ return this
290
+ }
291
+
292
+ getPerformanceMetrics() {
293
+ if (!this.#profiler.enabled) {
294
+ return {
295
+ enabled: false,
296
+ message: 'Performance profiling not enabled. Call enablePerformanceProfile() first.',
297
+ }
298
+ }
299
+ const endTime = performance.now()
300
+ this.#profiler.totalMs = endTime - this.#profiler.startTime
301
+ this.#profiler.memoryUsedMB = Math.round((process.memoryUsage().rss / 1024 / 1024) * 100) / 100
302
+
303
+ return {
304
+ enabled: true,
305
+ templateLoadMs: Math.round(this.#profiler.templateLoadMs * 100) / 100,
306
+ parseMs: Math.round(this.#profiler.parseMs * 100) / 100,
307
+ chartUpdateMs: Math.round(this.#profiler.chartUpdateMs * 100) / 100,
308
+ imageUpdateMs: Math.round(this.#profiler.imageUpdateMs * 100) / 100,
309
+ zipGenerationMs: Math.round(this.#profiler.zipGenerationMs * 100) / 100,
310
+ totalMs: Math.round(this.#profiler.totalMs * 100) / 100,
311
+ memoryUsedMB: this.#profiler.memoryUsedMB,
312
+ }
313
+ }
314
+
205
315
  /**
206
316
  * Loads a template from a PowerPoint XML Presentation format.
207
317
  *
@@ -236,10 +346,13 @@ class PPTXTemplater {
236
346
  * @param {string|Buffer} source
237
347
  */
238
348
  async #initialize(source) {
349
+ const t0 = performance.now()
239
350
  logger.debug(`Loading PPTX from ${typeof source === 'string' ? source : 'buffer'}`)
240
351
 
241
352
  // Load and extract the ZIP archive (PPTX is just a ZIP)
242
353
  await this.#zipManager.load(source)
354
+ const t1 = performance.now()
355
+ this.#profiler.templateLoadMs = t1 - t0
243
356
 
244
357
  // Initialize content types manager first!
245
358
  await this.#contentTypesManager.initialize(this.#zipManager)
@@ -259,10 +372,46 @@ class PPTXTemplater {
259
372
  // Deduplicate and index media files
260
373
  await this.#mediaManager.initialize(this.#zipManager)
261
374
 
375
+ const t2 = performance.now()
376
+ this.#profiler.parseMs = t2 - t1
377
+
262
378
  this.#loaded = true
263
379
  logger.debug(`Loaded ${this.#slideManager.slideCount} slides successfully`)
264
380
  }
265
381
 
382
+ async #initializeFromCache(cachedFiles) {
383
+ const t0 = performance.now()
384
+ logger.debug('Initializing PPTX from cached template')
385
+
386
+ const clonedCache = new Map(cachedFiles)
387
+ await this.#zipManager.loadFromCache(clonedCache)
388
+ const t1 = performance.now()
389
+ this.#profiler.templateLoadMs = t1 - t0
390
+
391
+ // Initialize content types manager first!
392
+ await this.#contentTypesManager.initialize(this.#zipManager)
393
+
394
+ // Parse the core presentation relationships and structure
395
+ await this.#relationshipManager.initialize(this.#zipManager)
396
+
397
+ // Load all slide references from presentation.xml
398
+ await this.#slideManager.initialize(this.#zipManager)
399
+
400
+ // Pre-load all slide XML into cache to allow synchronous operations like replaceText()
401
+ await this.#slideManager.preloadAll()
402
+
403
+ // Initialize chart manager with zip context
404
+ await this.#chartManager.initialize(this.#zipManager)
405
+
406
+ // Deduplicate and index media files
407
+ await this.#mediaManager.initialize(this.#zipManager)
408
+
409
+ const t2 = performance.now()
410
+ this.#profiler.parseMs = t2 - t1
411
+
412
+ this.#loaded = true
413
+ }
414
+
266
415
  /**
267
416
  * Initializes a blank PPTX structure from embedded template XML.
268
417
  * @private
@@ -401,6 +550,7 @@ class PPTXTemplater {
401
550
  */
402
551
  updateChart(chartId, data) {
403
552
  this.#assertLoaded()
553
+ const t0 = performance.now()
404
554
  const targetIndices = this.#getTargetSlideIndices()
405
555
 
406
556
  for (const slideIndex of targetIndices) {
@@ -413,6 +563,7 @@ class PPTXTemplater {
413
563
  )
414
564
  }
415
565
 
566
+ this.#profiler.chartUpdateMs += performance.now() - t0
416
567
  logger.debug(`Updated chart "${chartId}" in ${targetIndices.length} slide(s)`)
417
568
  return this
418
569
  }
@@ -999,7 +1150,11 @@ class PPTXTemplater {
999
1150
  }
1000
1151
  }
1001
1152
  await this.validateArchive()
1002
- await this.#outputWriter.saveToFile(filePath, this.#slideManager, this.#zipManager)
1153
+
1154
+ const t0 = performance.now()
1155
+ await this.#outputWriter.saveToFile(filePath, this.#slideManager, this.#zipManager, options)
1156
+ this.#profiler.zipGenerationMs += performance.now() - t0
1157
+
1003
1158
  logger.info(`Saved PPTX to ${filePath}`)
1004
1159
  }
1005
1160
 
@@ -1040,23 +1195,64 @@ class PPTXTemplater {
1040
1195
  /**
1041
1196
  * Returns the PPTX content as a Node.js Buffer.
1042
1197
  *
1198
+ * @param {Object} [options] - Save options.
1043
1199
  * @returns {Promise<Buffer>}
1044
1200
  */
1045
- async toBuffer() {
1201
+ async toBuffer(options = {}) {
1046
1202
  this.#assertLoaded()
1047
1203
  await this.validateArchive()
1048
- return this.#outputWriter.toBuffer(this.#slideManager, this.#zipManager)
1204
+
1205
+ const t0 = performance.now()
1206
+ const buffer = await this.#outputWriter.toBuffer(this.#slideManager, this.#zipManager, options)
1207
+ this.#profiler.zipGenerationMs += performance.now() - t0
1208
+
1209
+ return buffer
1049
1210
  }
1050
1211
 
1051
1212
  /**
1052
1213
  * Returns the PPTX content as a readable Node.js Stream.
1053
1214
  *
1215
+ * @param {Object} [options] - Save options.
1054
1216
  * @returns {Promise<NodeJS.ReadableStream>}
1055
1217
  */
1056
- async toStream() {
1218
+ async toStream(options = {}) {
1057
1219
  this.#assertLoaded()
1058
1220
  await this.validateArchive()
1059
- return this.#outputWriter.toStream(this.#slideManager, this.#zipManager)
1221
+
1222
+ const t0 = performance.now()
1223
+ const stream = await this.#outputWriter.toStream(this.#slideManager, this.#zipManager, options)
1224
+ this.#profiler.zipGenerationMs += performance.now() - t0
1225
+
1226
+ return stream
1227
+ }
1228
+
1229
+ /**
1230
+ * Saves the presentation to a readable stream or pipes it to a writable stream.
1231
+ *
1232
+ * @param {NodeJS.WritableStream|Object} [writableOrOptions] - Writable stream to pipe to, or options object.
1233
+ * @param {Object} [options] - Save options if writable stream was passed first.
1234
+ * @returns {Promise<NodeJS.ReadableStream|void>}
1235
+ */
1236
+ async saveToStream(writableOrOptions, options = {}) {
1237
+ this.#assertLoaded()
1238
+ let writable = null
1239
+ let opts = options
1240
+ if (writableOrOptions && typeof writableOrOptions.write === 'function') {
1241
+ writable = writableOrOptions
1242
+ } else if (writableOrOptions) {
1243
+ opts = writableOrOptions
1244
+ }
1245
+
1246
+ const stream = await this.toStream(opts)
1247
+ if (writable) {
1248
+ return new Promise((resolve, reject) => {
1249
+ stream.pipe(writable)
1250
+ writable.on('finish', resolve)
1251
+ writable.on('error', reject)
1252
+ stream.on('error', reject)
1253
+ })
1254
+ }
1255
+ return stream
1060
1256
  }
1061
1257
 
1062
1258
  // === Slide Features ===
@@ -1721,6 +1917,7 @@ class PPTXTemplater {
1721
1917
  // === Image Features ===
1722
1918
  async replaceImage(imageIdOrName, sourcePathOrBuffer) {
1723
1919
  this.#assertLoaded()
1920
+ const t0 = performance.now()
1724
1921
  const targetIndices = this.#getTargetSlideIndices()
1725
1922
  for (const idx of targetIndices) {
1726
1923
  await this.#imageManager.replaceImage(
@@ -1732,11 +1929,13 @@ class PPTXTemplater {
1732
1929
  this.#relationshipManager
1733
1930
  )
1734
1931
  }
1932
+ this.#profiler.imageUpdateMs += performance.now() - t0
1735
1933
  return this
1736
1934
  }
1737
1935
 
1738
1936
  async addImage(sourcePathOrBuffer, options = {}) {
1739
1937
  this.#assertLoaded()
1938
+ const t0 = performance.now()
1740
1939
  const targetIndices = this.#getTargetSlideIndices()
1741
1940
  for (const idx of targetIndices) {
1742
1941
  await this.#imageManager.addImage(
@@ -1748,6 +1947,7 @@ class PPTXTemplater {
1748
1947
  this.#relationshipManager
1749
1948
  )
1750
1949
  }
1950
+ this.#profiler.imageUpdateMs += performance.now() - t0
1751
1951
  return this
1752
1952
  }
1753
1953
 
@@ -112,8 +112,6 @@ class ChartManager {
112
112
  // Chart name is inferred from file name
113
113
  const chartName = chartPath.split('/').pop().replace('.xml', '')
114
114
  this.#chartRegistry.set(chartName, { zipPath: chartPath, slideIndex: null })
115
- // Pre-load the chart XML into cache so that we can read it synchronously if needed
116
- await zipManager.readFile(chartPath)
117
115
  }
118
116
 
119
117
  logger.debug(`Found ${chartFiles.length} chart file(s)`)
@@ -869,7 +867,6 @@ class ChartManager {
869
867
  #findChartCoordinates(slideXml, chartId, relationshipManager, slideZipPath) {
870
868
  const gfPattern = /<p:graphicFrame>([\s\S]*?)<\/p:graphicFrame>/g
871
869
  let match
872
- const escapedChartId = chartId.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
873
870
  while ((match = gfPattern.exec(slideXml)) !== null) {
874
871
  const gfContent = match[0]
875
872
  const nameMatch = /<p:cNvPr[^>]*name="([^"]+)"/.exec(gfContent)