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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-pptx-templater",
3
- "version": "1.0.18",
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
- if (source.path) {
189
- // It's a file stream (fs.createReadStream)
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
- hash = await getFileHash(source)
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
- if (this.#mediaHashIndex.has(hash)) {
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
- if (streamForZip) {
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 })