node-pptx-templater 1.0.16 → 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 +178 -4
- package/package.json +1 -1
- package/src/core/OutputWriter.js +12 -14
- package/src/core/PPTXTemplater.js +205 -5
- package/src/managers/ChartManager.js +0 -3
- package/src/managers/ImageManager.js +14 -18
- package/src/managers/MediaManager.js +151 -35
- package/src/managers/ShapeManager.js +19 -28
- package/src/managers/SlideManager.js +247 -4
- package/src/managers/TableManager.js +56 -87
- package/src/managers/ZOrderManager.js +0 -5
- package/src/managers/ZipManager.js +120 -14
- package/src/managers/charts/ChartWorkbookUpdater.js +1 -1
- package/src/utils/contentTypesHelper.js +6 -9
- package/src/utils/imageMetadata.js +227 -0
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.
|
|
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",
|
package/src/core/OutputWriter.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|