node-pptx-templater 1.0.18 → 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.
- package/package.json +1 -1
- package/src/managers/MediaManager.js +26 -98
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-pptx-templater",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.19",
|
|
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",
|
|
@@ -40,20 +40,9 @@ const { createHash } = require('crypto')
|
|
|
40
40
|
const { createLogger } = require('../utils/logger.js')
|
|
41
41
|
const { PPTXError } = require('../utils/errors.js')
|
|
42
42
|
const fsExtra = require('fs-extra')
|
|
43
|
-
const fs = require('fs')
|
|
44
43
|
|
|
45
44
|
const logger = createLogger('MediaManager')
|
|
46
45
|
|
|
47
|
-
function getFileHash(filePath) {
|
|
48
|
-
return new Promise((resolve, reject) => {
|
|
49
|
-
const hash = createHash('sha1')
|
|
50
|
-
const stream = fs.createReadStream(filePath)
|
|
51
|
-
stream.on('data', chunk => hash.update(chunk))
|
|
52
|
-
stream.on('end', () => resolve(hash.digest('hex')))
|
|
53
|
-
stream.on('error', reject)
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
|
-
|
|
57
46
|
function streamToBuffer(stream) {
|
|
58
47
|
return new Promise((resolve, reject) => {
|
|
59
48
|
const chunks = []
|
|
@@ -182,119 +171,58 @@ class MediaManager {
|
|
|
182
171
|
let hash
|
|
183
172
|
let size
|
|
184
173
|
const isStream = source && typeof source.on === 'function' && typeof source.pipe === 'function'
|
|
185
|
-
let streamForZip = null
|
|
186
174
|
|
|
187
175
|
if (isStream) {
|
|
188
|
-
|
|
189
|
-
|
|
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') {
|
|
190
179
|
const filePath = source.path
|
|
191
|
-
hash = await getFileHash(filePath)
|
|
192
180
|
ext = filePath.split('.').pop().toLowerCase()
|
|
193
|
-
mimeType = mimeType || EXT_TO_MIME[ext] || 'image/png'
|
|
194
|
-
|
|
195
|
-
// Check for duplicate (content-addressable dedup)
|
|
196
|
-
if (this.#mediaHashIndex.has(hash)) {
|
|
197
|
-
const existingPath = this.#mediaHashIndex.get(hash)
|
|
198
|
-
logger.debug(`Reusing existing media: ${existingPath} (hash: ${hash.substring(0, 8)}...)`)
|
|
199
|
-
return existingPath
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Ensure all media from template is hashed to check for duplicates
|
|
203
|
-
await this.#ensureAllMediaHashed()
|
|
204
|
-
|
|
205
|
-
if (this.#mediaHashIndex.has(hash)) {
|
|
206
|
-
const existingPath = this.#mediaHashIndex.get(hash)
|
|
207
|
-
logger.debug(`Reusing existing media: ${existingPath} (hash: ${hash.substring(0, 8)}...)`)
|
|
208
|
-
return existingPath
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const stat = await fsExtra.stat(filePath)
|
|
212
|
-
size = stat.size
|
|
213
|
-
streamForZip = fs.createReadStream(filePath)
|
|
214
181
|
} else {
|
|
215
|
-
// Generic stream - we must buffer it to hash and reuse
|
|
216
|
-
data = await streamToBuffer(source)
|
|
217
|
-
hash = this.#hashBytes(data)
|
|
218
182
|
ext = this.#detectExtension(data)
|
|
219
|
-
mimeType = mimeType || EXT_TO_MIME[ext] || 'image/png'
|
|
220
|
-
size = data.length
|
|
221
|
-
|
|
222
|
-
// Check for duplicate (content-addressable dedup)
|
|
223
|
-
if (this.#mediaHashIndex.has(hash)) {
|
|
224
|
-
const existingPath = this.#mediaHashIndex.get(hash)
|
|
225
|
-
logger.debug(`Reusing existing media: ${existingPath} (hash: ${hash.substring(0, 8)}...)`)
|
|
226
|
-
return existingPath
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Ensure all media from template is hashed to check for duplicates
|
|
230
|
-
await this.#ensureAllMediaHashed()
|
|
231
|
-
|
|
232
|
-
if (this.#mediaHashIndex.has(hash)) {
|
|
233
|
-
const existingPath = this.#mediaHashIndex.get(hash)
|
|
234
|
-
logger.debug(`Reusing existing media: ${existingPath} (hash: ${hash.substring(0, 8)}...)`)
|
|
235
|
-
return existingPath
|
|
236
|
-
}
|
|
237
183
|
}
|
|
184
|
+
mimeType = mimeType || EXT_TO_MIME[ext] || 'image/png'
|
|
185
|
+
hash = this.#hashBytes(data)
|
|
186
|
+
size = data.length
|
|
238
187
|
} else if (typeof source === 'string') {
|
|
239
|
-
// Load from file path
|
|
240
|
-
|
|
188
|
+
// Load from file path directly to buffer
|
|
189
|
+
data = await fsExtra.readFile(source)
|
|
241
190
|
ext = source.split('.').pop().toLowerCase()
|
|
242
191
|
mimeType = mimeType || EXT_TO_MIME[ext] || 'image/png'
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const existingPath = this.#mediaHashIndex.get(hash)
|
|
246
|
-
logger.debug(`Reusing existing media: ${existingPath} (hash: ${hash.substring(0, 8)}...)`)
|
|
247
|
-
return existingPath
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Ensure all media from template is hashed to check for duplicates
|
|
251
|
-
await this.#ensureAllMediaHashed()
|
|
252
|
-
|
|
253
|
-
if (this.#mediaHashIndex.has(hash)) {
|
|
254
|
-
const existingPath = this.#mediaHashIndex.get(hash)
|
|
255
|
-
logger.debug(`Reusing existing media: ${existingPath} (hash: ${hash.substring(0, 8)}...)`)
|
|
256
|
-
return existingPath
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const stat = await fsExtra.stat(source)
|
|
260
|
-
size = stat.size
|
|
261
|
-
streamForZip = fs.createReadStream(source)
|
|
192
|
+
hash = this.#hashBytes(data)
|
|
193
|
+
size = data.length
|
|
262
194
|
} else if (Buffer.isBuffer(source) || source instanceof Uint8Array) {
|
|
263
195
|
data = source
|
|
264
196
|
ext = this.#detectExtension(data)
|
|
265
197
|
mimeType = mimeType || EXT_TO_MIME[ext] || 'image/png'
|
|
266
198
|
hash = this.#hashBytes(data)
|
|
267
199
|
size = data.length
|
|
268
|
-
|
|
269
|
-
if (this.#mediaHashIndex.has(hash)) {
|
|
270
|
-
const existingPath = this.#mediaHashIndex.get(hash)
|
|
271
|
-
logger.debug(`Reusing existing media: ${existingPath} (hash: ${hash.substring(0, 8)}...)`)
|
|
272
|
-
return existingPath
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Ensure all media from template is hashed to check for duplicates
|
|
276
|
-
await this.#ensureAllMediaHashed()
|
|
277
|
-
|
|
278
|
-
if (this.#mediaHashIndex.has(hash)) {
|
|
279
|
-
const existingPath = this.#mediaHashIndex.get(hash)
|
|
280
|
-
logger.debug(`Reusing existing media: ${existingPath} (hash: ${hash.substring(0, 8)}...)`)
|
|
281
|
-
return existingPath
|
|
282
|
-
}
|
|
283
200
|
} else {
|
|
284
201
|
throw new PPTXError(
|
|
285
202
|
'embedImage: source must be a file path string, Buffer, or Readable Stream'
|
|
286
203
|
)
|
|
287
204
|
}
|
|
288
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
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Ensure all media from template is hashed to check for duplicates
|
|
213
|
+
await this.#ensureAllMediaHashed()
|
|
214
|
+
|
|
215
|
+
if (this.#mediaHashIndex.has(hash)) {
|
|
216
|
+
const existingPath = this.#mediaHashIndex.get(hash)
|
|
217
|
+
logger.debug(`Reusing existing media: ${existingPath} (hash: ${hash.substring(0, 8)}...)`)
|
|
218
|
+
return existingPath
|
|
219
|
+
}
|
|
220
|
+
|
|
289
221
|
// Create a new media file
|
|
290
222
|
const mediaId = this.#nextMediaId++
|
|
291
223
|
const zipPath = `ppt/media/image${mediaId}.${ext}`
|
|
292
224
|
|
|
293
|
-
|
|
294
|
-
this.#zipManager.writeBinaryFile(zipPath, streamForZip)
|
|
295
|
-
} else {
|
|
296
|
-
this.#zipManager.writeBinaryFile(zipPath, data)
|
|
297
|
-
}
|
|
225
|
+
this.#zipManager.writeBinaryFile(zipPath, data)
|
|
298
226
|
|
|
299
227
|
this.#mediaHashIndex.set(hash, zipPath)
|
|
300
228
|
this.#mediaRegistry.set(zipPath, { zipPath, hash, mimeType, size })
|