node-pptx-templater 1.0.18 → 1.0.20
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 +121 -0
- package/package.json +1 -1
- package/src/core/PPTXTemplater.js +186 -1
- package/src/managers/MediaManager.js +26 -98
- package/src/managers/ShapeManager.js +700 -0
- package/src/managers/TableManager.js +759 -69
package/README.md
CHANGED
|
@@ -243,6 +243,63 @@ ppt.useSlide(1).updateTable('summary-table', [
|
|
|
243
243
|
]);
|
|
244
244
|
```
|
|
245
245
|
|
|
246
|
+
#### `addCellShape(tableId, rowIndex, colIndex, options)`
|
|
247
|
+
Dynamically adds a shape inside a table cell based on cell coordinates.
|
|
248
|
+
|
|
249
|
+
* **Arguments**:
|
|
250
|
+
* `tableId` (`string`): Table name or shape ID.
|
|
251
|
+
* `rowIndex` (`number`): 0-based row index.
|
|
252
|
+
* `colIndex` (`number`): 0-based column index.
|
|
253
|
+
* `options` (`Object`): Shape configuration options.
|
|
254
|
+
* **Returns**: `this` - The chainable presentation templater instance.
|
|
255
|
+
|
|
256
|
+
```javascript
|
|
257
|
+
await ppt.addCellShape('Table', 1, 2, { type: 'circle', fill: '#10B981' });
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
#### `updateCellShape(tableId, rowIndex, colIndex, shapeIndex, options)`
|
|
261
|
+
Updates an existing shape inside a table cell.
|
|
262
|
+
|
|
263
|
+
* **Arguments**:
|
|
264
|
+
* `tableId` (`string`): Table name or shape ID.
|
|
265
|
+
* `rowIndex` (`number`): 0-based row index.
|
|
266
|
+
* `colIndex` (`number`): 0-based column index.
|
|
267
|
+
* `shapeIndex` (`number`): 0-based shape index in the cell.
|
|
268
|
+
* `options` (`Object`): Shape configuration properties to update.
|
|
269
|
+
* **Returns**: `this` - The chainable presentation templater instance.
|
|
270
|
+
|
|
271
|
+
```javascript
|
|
272
|
+
await ppt.updateCellShape('Table', 1, 2, 0, { fill: '#EF4444' });
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
#### `removeCellShape(tableId, rowIndex, colIndex, shapeIndex)`
|
|
276
|
+
Removes a shape from a table cell.
|
|
277
|
+
|
|
278
|
+
* **Arguments**:
|
|
279
|
+
* `tableId` (`string`): Table name or shape ID.
|
|
280
|
+
* `rowIndex` (`number`): 0-based row index.
|
|
281
|
+
* `colIndex` (`number`): 0-based column index.
|
|
282
|
+
* `shapeIndex` (`number`): 0-based shape index in the cell.
|
|
283
|
+
* **Returns**: `this` - The chainable presentation templater instance.
|
|
284
|
+
|
|
285
|
+
```javascript
|
|
286
|
+
await ppt.removeCellShape('Table', 1, 2, 0);
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
#### `getCellShape(tableId, rowIndex, colIndex, shapeIndex)`
|
|
290
|
+
Discovers and retrieves details of an existing cell shape on the targeted slide.
|
|
291
|
+
|
|
292
|
+
* **Arguments**:
|
|
293
|
+
* `tableId` (`string`): Table name or shape ID.
|
|
294
|
+
* `rowIndex` (`number`): 0-based row index.
|
|
295
|
+
* `colIndex` (`number`): 0-based column index.
|
|
296
|
+
* `shapeIndex` (`number`): 0-based shape index in the cell.
|
|
297
|
+
* **Returns**: `Object|null` - Shape details object, or null if not found.
|
|
298
|
+
|
|
299
|
+
```javascript
|
|
300
|
+
const shape = ppt.getCellShape('Table', 1, 2, 0);
|
|
301
|
+
```
|
|
302
|
+
|
|
246
303
|
#### `addTableRow(())`
|
|
247
304
|
Delegates core actions to slide element sub-managers.
|
|
248
305
|
|
|
@@ -1016,6 +1073,70 @@ Updates the position and/or dimensions of an existing textbox on targeted slides
|
|
|
1016
1073
|
ppt.useSlide(1).updateTextBoxPosition('TextBox 2', { x: 1000000, y: 1500000 });
|
|
1017
1074
|
```
|
|
1018
1075
|
|
|
1076
|
+
#### `validateShape(options)`
|
|
1077
|
+
Validates shape options configuration.
|
|
1078
|
+
|
|
1079
|
+
* **Arguments**:
|
|
1080
|
+
* `options` (`Object`):
|
|
1081
|
+
* **Returns**: `string[]` - List of validation error messages.
|
|
1082
|
+
|
|
1083
|
+
```javascript
|
|
1084
|
+
const errors = ppt.validateShape(shapeOptions);
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
#### `addShape(options)`
|
|
1088
|
+
Adds a new shape dynamically to the targeted slide(s).
|
|
1089
|
+
|
|
1090
|
+
* **Arguments**:
|
|
1091
|
+
* `options` (`Object`):
|
|
1092
|
+
* **Returns**: `this` - The chainable presentation templater instance.
|
|
1093
|
+
|
|
1094
|
+
```javascript
|
|
1095
|
+
await ppt.useSlide(1).addShape({
|
|
1096
|
+
type: 'rectangle',
|
|
1097
|
+
id: 'sales-box',
|
|
1098
|
+
x: 100,
|
|
1099
|
+
y: 100,
|
|
1100
|
+
width: 200,
|
|
1101
|
+
height: 100,
|
|
1102
|
+
fill: '#2563EB'
|
|
1103
|
+
});
|
|
1104
|
+
```
|
|
1105
|
+
|
|
1106
|
+
#### `updateShape(shapeId, options)`
|
|
1107
|
+
Updates an existing shape in-place.
|
|
1108
|
+
|
|
1109
|
+
* **Arguments**:
|
|
1110
|
+
* `shapeId` (`string`):
|
|
1111
|
+
* `options` (`Object`):
|
|
1112
|
+
* **Returns**: `this` - The chainable presentation templater instance.
|
|
1113
|
+
|
|
1114
|
+
```javascript
|
|
1115
|
+
await ppt.useSlide(1).updateShape('sales-box', { fill: '#10B981' });
|
|
1116
|
+
```
|
|
1117
|
+
|
|
1118
|
+
#### `removeShape(shapeId)`
|
|
1119
|
+
Removes a shape from the targeted slide(s).
|
|
1120
|
+
|
|
1121
|
+
* **Arguments**:
|
|
1122
|
+
* `shapeId` (`string`):
|
|
1123
|
+
* **Returns**: `this` - The chainable presentation templater instance.
|
|
1124
|
+
|
|
1125
|
+
```javascript
|
|
1126
|
+
await ppt.useSlide(1).removeShape('sales-box');
|
|
1127
|
+
```
|
|
1128
|
+
|
|
1129
|
+
#### `getShape(shapeId)`
|
|
1130
|
+
Discovers and retrieves details of an existing shape on the targeted slides.
|
|
1131
|
+
|
|
1132
|
+
* **Arguments**:
|
|
1133
|
+
* `shapeId` (`string`):
|
|
1134
|
+
* **Returns**: `Object|null` - Shape details object, or null if not found.
|
|
1135
|
+
|
|
1136
|
+
```javascript
|
|
1137
|
+
const shape = ppt.getShape('sales-box');
|
|
1138
|
+
```
|
|
1139
|
+
|
|
1019
1140
|
#### `updateShapeText(())`
|
|
1020
1141
|
Delegates core actions to slide element sub-managers.
|
|
1021
1142
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-pptx-templater",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.20",
|
|
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",
|
|
@@ -588,7 +588,13 @@ class PPTXTemplater {
|
|
|
588
588
|
const targetIndices = this.#getTargetSlideIndices()
|
|
589
589
|
|
|
590
590
|
for (const slideIndex of targetIndices) {
|
|
591
|
-
this.#tableManager.updateTable(
|
|
591
|
+
this.#tableManager.updateTable(
|
|
592
|
+
slideIndex,
|
|
593
|
+
tableId,
|
|
594
|
+
rows,
|
|
595
|
+
this.#slideManager,
|
|
596
|
+
this.#shapeManager
|
|
597
|
+
)
|
|
592
598
|
}
|
|
593
599
|
|
|
594
600
|
logger.debug(`Updated table "${tableId}" in ${targetIndices.length} slide(s)`)
|
|
@@ -1914,6 +1920,185 @@ class PPTXTemplater {
|
|
|
1914
1920
|
return shapes
|
|
1915
1921
|
}
|
|
1916
1922
|
|
|
1923
|
+
/**
|
|
1924
|
+
* Validates shape options configuration.
|
|
1925
|
+
*
|
|
1926
|
+
* @param {Object} options Shape creation/update options.
|
|
1927
|
+
* @returns {string[]} List of validation error messages.
|
|
1928
|
+
*/
|
|
1929
|
+
validateShape(options) {
|
|
1930
|
+
return this.#shapeManager.validateShape(options)
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
/**
|
|
1934
|
+
* Adds a new shape dynamically to the targeted slide(s).
|
|
1935
|
+
*
|
|
1936
|
+
* @param {Object} options Shape configuration options.
|
|
1937
|
+
* @returns {this} The chainable presentation templater instance.
|
|
1938
|
+
*/
|
|
1939
|
+
async addShape(options) {
|
|
1940
|
+
this.#assertLoaded()
|
|
1941
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
1942
|
+
for (const idx of targetIndices) {
|
|
1943
|
+
this.#shapeManager.addShape(idx, options, this.#slideManager)
|
|
1944
|
+
}
|
|
1945
|
+
return this
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
/**
|
|
1949
|
+
* Updates an existing shape in-place.
|
|
1950
|
+
*
|
|
1951
|
+
* @param {string} shapeId Shape ID or template name to update.
|
|
1952
|
+
* @param {Object} options Configuration properties to update.
|
|
1953
|
+
* @returns {this} The chainable presentation templater instance.
|
|
1954
|
+
*/
|
|
1955
|
+
async updateShape(shapeId, options) {
|
|
1956
|
+
this.#assertLoaded()
|
|
1957
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
1958
|
+
for (const idx of targetIndices) {
|
|
1959
|
+
this.#shapeManager.updateShape(idx, shapeId, options, this.#slideManager)
|
|
1960
|
+
}
|
|
1961
|
+
return this
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
/**
|
|
1965
|
+
* Removes a shape from the targeted slide(s).
|
|
1966
|
+
*
|
|
1967
|
+
* @param {string} shapeId Shape ID or template name to remove.
|
|
1968
|
+
* @returns {this} The chainable presentation templater instance.
|
|
1969
|
+
*/
|
|
1970
|
+
async removeShape(shapeId) {
|
|
1971
|
+
this.#assertLoaded()
|
|
1972
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
1973
|
+
for (const idx of targetIndices) {
|
|
1974
|
+
this.#shapeManager.removeShape(idx, shapeId, this.#slideManager)
|
|
1975
|
+
}
|
|
1976
|
+
return this
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
/**
|
|
1980
|
+
* Discovers and retrieves details of an existing shape on the targeted slides.
|
|
1981
|
+
*
|
|
1982
|
+
* @param {string} shapeId Shape ID or template name to locate.
|
|
1983
|
+
* @returns {Object|null} Shape details object, or null if not found.
|
|
1984
|
+
*/
|
|
1985
|
+
getShape(shapeId) {
|
|
1986
|
+
this.#assertLoaded()
|
|
1987
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
1988
|
+
for (const idx of targetIndices) {
|
|
1989
|
+
const shape = this.#shapeManager.getShape(idx, shapeId, this.#slideManager)
|
|
1990
|
+
if (shape) return shape
|
|
1991
|
+
}
|
|
1992
|
+
return null
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
/**
|
|
1996
|
+
* Dynamically adds a shape inside a table cell based on cell coordinates.
|
|
1997
|
+
*
|
|
1998
|
+
* @param {string} tableId - Table name or shape ID.
|
|
1999
|
+
* @param {number} rowIndex - 0-based row index.
|
|
2000
|
+
* @param {number} colIndex - 0-based column index.
|
|
2001
|
+
* @param {Object} options - Shape configuration options.
|
|
2002
|
+
* @returns {this} The chainable presentation templater instance.
|
|
2003
|
+
*/
|
|
2004
|
+
async addCellShape(tableId, rowIndex, colIndex, options) {
|
|
2005
|
+
this.#assertLoaded()
|
|
2006
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
2007
|
+
for (const idx of targetIndices) {
|
|
2008
|
+
this.#tableManager.addCellShape(
|
|
2009
|
+
idx,
|
|
2010
|
+
tableId,
|
|
2011
|
+
rowIndex,
|
|
2012
|
+
colIndex,
|
|
2013
|
+
options,
|
|
2014
|
+
this.#slideManager,
|
|
2015
|
+
this.#shapeManager
|
|
2016
|
+
)
|
|
2017
|
+
}
|
|
2018
|
+
return this
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
/**
|
|
2022
|
+
* Updates an existing shape inside a table cell.
|
|
2023
|
+
*
|
|
2024
|
+
* @param {string} tableId - Table name or shape ID.
|
|
2025
|
+
* @param {number} rowIndex - 0-based row index.
|
|
2026
|
+
* @param {number} colIndex - 0-based column index.
|
|
2027
|
+
* @param {number} shapeIndex - 0-based shape index in the cell.
|
|
2028
|
+
* @param {Object} options - Shape configuration properties to update.
|
|
2029
|
+
* @returns {this} The chainable presentation templater instance.
|
|
2030
|
+
*/
|
|
2031
|
+
async updateCellShape(tableId, rowIndex, colIndex, shapeIndex, options) {
|
|
2032
|
+
this.#assertLoaded()
|
|
2033
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
2034
|
+
for (const idx of targetIndices) {
|
|
2035
|
+
this.#tableManager.updateCellShape(
|
|
2036
|
+
idx,
|
|
2037
|
+
tableId,
|
|
2038
|
+
rowIndex,
|
|
2039
|
+
colIndex,
|
|
2040
|
+
shapeIndex,
|
|
2041
|
+
options,
|
|
2042
|
+
this.#slideManager,
|
|
2043
|
+
this.#shapeManager
|
|
2044
|
+
)
|
|
2045
|
+
}
|
|
2046
|
+
return this
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
/**
|
|
2050
|
+
* Removes a shape from a table cell.
|
|
2051
|
+
*
|
|
2052
|
+
* @param {string} tableId - Table name or shape ID.
|
|
2053
|
+
* @param {number} rowIndex - 0-based row index.
|
|
2054
|
+
* @param {number} colIndex - 0-based column index.
|
|
2055
|
+
* @param {number} shapeIndex - 0-based shape index in the cell.
|
|
2056
|
+
* @returns {this} The chainable presentation templater instance.
|
|
2057
|
+
*/
|
|
2058
|
+
async removeCellShape(tableId, rowIndex, colIndex, shapeIndex) {
|
|
2059
|
+
this.#assertLoaded()
|
|
2060
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
2061
|
+
for (const idx of targetIndices) {
|
|
2062
|
+
this.#tableManager.removeCellShape(
|
|
2063
|
+
idx,
|
|
2064
|
+
tableId,
|
|
2065
|
+
rowIndex,
|
|
2066
|
+
colIndex,
|
|
2067
|
+
shapeIndex,
|
|
2068
|
+
this.#slideManager,
|
|
2069
|
+
this.#shapeManager
|
|
2070
|
+
)
|
|
2071
|
+
}
|
|
2072
|
+
return this
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
/**
|
|
2076
|
+
* Discovers and retrieves details of an existing cell shape on the targeted slide.
|
|
2077
|
+
*
|
|
2078
|
+
* @param {string} tableId - Table name or shape ID.
|
|
2079
|
+
* @param {number} rowIndex - 0-based row index.
|
|
2080
|
+
* @param {number} colIndex - 0-based column index.
|
|
2081
|
+
* @param {number} shapeIndex - 0-based shape index in the cell.
|
|
2082
|
+
* @returns {Object|null} Shape details object, or null if not found.
|
|
2083
|
+
*/
|
|
2084
|
+
getCellShape(tableId, rowIndex, colIndex, shapeIndex) {
|
|
2085
|
+
this.#assertLoaded()
|
|
2086
|
+
const targetIndices = this.#getTargetSlideIndices()
|
|
2087
|
+
for (const idx of targetIndices) {
|
|
2088
|
+
const shape = this.#tableManager.getCellShape(
|
|
2089
|
+
idx,
|
|
2090
|
+
tableId,
|
|
2091
|
+
rowIndex,
|
|
2092
|
+
colIndex,
|
|
2093
|
+
shapeIndex,
|
|
2094
|
+
this.#slideManager,
|
|
2095
|
+
this.#shapeManager
|
|
2096
|
+
)
|
|
2097
|
+
if (shape) return shape
|
|
2098
|
+
}
|
|
2099
|
+
return null
|
|
2100
|
+
}
|
|
2101
|
+
|
|
1917
2102
|
// === Image Features ===
|
|
1918
2103
|
async replaceImage(imageIdOrName, sourcePathOrBuffer) {
|
|
1919
2104
|
this.#assertLoaded()
|
|
@@ -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 })
|