node-pptx-templater 1.0.17 → 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/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 +72 -28
- 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
|
@@ -41,10 +41,7 @@ class ImageManager {
|
|
|
41
41
|
mediaManager,
|
|
42
42
|
relationshipManager
|
|
43
43
|
) {
|
|
44
|
-
const
|
|
45
|
-
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
46
|
-
const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
|
|
47
|
-
const picRes = this.#findPicRecursive(spTree, imageIdOrName)
|
|
44
|
+
const picRes = slideManager.getSlidePic(slideIndex, imageIdOrName)
|
|
48
45
|
|
|
49
46
|
if (!picRes) {
|
|
50
47
|
throw new PPTXError(`Image shape "${imageIdOrName}" not found on slide ${slideIndex}`)
|
|
@@ -85,9 +82,11 @@ class ImageManager {
|
|
|
85
82
|
relationshipManager
|
|
86
83
|
) {
|
|
87
84
|
const slideInfo = slideManager.getSlideInfo(slideIndex)
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
85
|
+
const slideObj = slideManager.getSlideObj(slideIndex)
|
|
86
|
+
const spTree =
|
|
87
|
+
slideObj?.['p:sld']?.['p:cSld']?.['p:spTree'] ||
|
|
88
|
+
slideObj?.['p:sldLayout']?.['p:cSld']?.['p:spTree'] ||
|
|
89
|
+
slideObj?.['p:sldMaster']?.['p:cSld']?.['p:spTree']
|
|
91
90
|
|
|
92
91
|
if (!spTree) {
|
|
93
92
|
throw new PPTXError(`Invalid slide structure for slide ${slideIndex}`)
|
|
@@ -155,8 +154,7 @@ class ImageManager {
|
|
|
155
154
|
}
|
|
156
155
|
spTree['p:pic'].push(picObj)
|
|
157
156
|
|
|
158
|
-
|
|
159
|
-
slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
|
|
157
|
+
slideManager.markSlideObjDirty(slideIndex)
|
|
160
158
|
logger.debug(`Added image "${name}" with ID ${newId} and rId ${rId} to slide ${slideIndex}`)
|
|
161
159
|
}
|
|
162
160
|
|
|
@@ -169,10 +167,7 @@ class ImageManager {
|
|
|
169
167
|
* @param {RelationshipManager} relationshipManager
|
|
170
168
|
*/
|
|
171
169
|
removeImage(slideIndex, imageIdOrName, slideManager, relationshipManager) {
|
|
172
|
-
const
|
|
173
|
-
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
174
|
-
const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
|
|
175
|
-
const picRes = this.#findPicRecursive(spTree, imageIdOrName)
|
|
170
|
+
const picRes = slideManager.getSlidePic(slideIndex, imageIdOrName)
|
|
176
171
|
|
|
177
172
|
if (!picRes) {
|
|
178
173
|
throw new PPTXError(`Image shape "${imageIdOrName}" not found on slide ${slideIndex}`)
|
|
@@ -195,8 +190,7 @@ class ImageManager {
|
|
|
195
190
|
relationshipManager.removeRelationship(slideInfo.zipPath, rId)
|
|
196
191
|
}
|
|
197
192
|
|
|
198
|
-
|
|
199
|
-
slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
|
|
193
|
+
slideManager.markSlideObjDirty(slideIndex)
|
|
200
194
|
logger.debug(`Removed image "${imageIdOrName}" from slide ${slideIndex}`)
|
|
201
195
|
}
|
|
202
196
|
|
|
@@ -210,9 +204,11 @@ class ImageManager {
|
|
|
210
204
|
*/
|
|
211
205
|
getImages(slideIndex, slideManager, relationshipManager) {
|
|
212
206
|
const slideInfo = slideManager.getSlideInfo(slideIndex)
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
|
|
207
|
+
const slideObj = slideManager.getSlideObj(slideIndex)
|
|
208
|
+
const spTree =
|
|
209
|
+
slideObj?.['p:sld']?.['p:cSld']?.['p:spTree'] ||
|
|
210
|
+
slideObj?.['p:sldLayout']?.['p:cSld']?.['p:spTree'] ||
|
|
211
|
+
slideObj?.['p:sldMaster']?.['p:cSld']?.['p:spTree']
|
|
216
212
|
|
|
217
213
|
const imagesInfo = []
|
|
218
214
|
this.#collectImagesInfo(spTree, slideInfo.zipPath, relationshipManager, imagesInfo)
|
|
@@ -43,6 +43,15 @@ const fsExtra = require('fs-extra')
|
|
|
43
43
|
|
|
44
44
|
const logger = createLogger('MediaManager')
|
|
45
45
|
|
|
46
|
+
function streamToBuffer(stream) {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const chunks = []
|
|
49
|
+
stream.on('data', chunk => chunks.push(chunk))
|
|
50
|
+
stream.on('end', () => resolve(Buffer.concat(chunks)))
|
|
51
|
+
stream.on('error', reject)
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
46
55
|
/**
|
|
47
56
|
* Extension to MIME type mapping.
|
|
48
57
|
*/
|
|
@@ -106,30 +115,37 @@ class MediaManager {
|
|
|
106
115
|
this.#zipManager = zipManager
|
|
107
116
|
const mediaFiles = zipManager.listFiles('ppt/media/')
|
|
108
117
|
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
118
|
+
// Register all existing media files without loading/hashing them yet
|
|
119
|
+
for (const mediaPath of mediaFiles) {
|
|
120
|
+
const ext = mediaPath.split('.').pop().toLowerCase()
|
|
121
|
+
const mimeType = EXT_TO_MIME[ext] || 'application/octet-stream'
|
|
122
|
+
|
|
123
|
+
const mediaInfo = { zipPath: mediaPath, hash: null, mimeType, size: null }
|
|
124
|
+
this.#mediaRegistry.set(mediaPath, mediaInfo)
|
|
125
|
+
|
|
126
|
+
// Track the highest media ID to avoid collisions
|
|
127
|
+
const numMatch = /\d+/.exec(mediaPath.split('/').pop())
|
|
128
|
+
if (numMatch) {
|
|
129
|
+
const num = parseInt(numMatch[0], 10)
|
|
130
|
+
if (num >= this.#nextMediaId) this.#nextMediaId = num + 1
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
logger.debug(`Registered ${this.#mediaRegistry.size} media file(s) (lazy loading enabled)`)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async #ensureAllMediaHashed() {
|
|
138
|
+
for (const [mediaPath, mediaInfo] of this.#mediaRegistry.entries()) {
|
|
139
|
+
if (mediaInfo.hash === null) {
|
|
140
|
+
const data = await this.#zipManager.readBinaryFile(mediaPath)
|
|
113
141
|
if (data) {
|
|
114
142
|
const hash = this.#hashBytes(data)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const mediaInfo = { zipPath: mediaPath, hash, mimeType, size: data.length }
|
|
143
|
+
mediaInfo.hash = hash
|
|
144
|
+
mediaInfo.size = data.length
|
|
119
145
|
this.#mediaHashIndex.set(hash, mediaPath)
|
|
120
|
-
this.#mediaRegistry.set(mediaPath, mediaInfo)
|
|
121
|
-
|
|
122
|
-
// Track the highest media ID to avoid collisions
|
|
123
|
-
const numMatch = /\d+/.exec(mediaPath.split('/').pop())
|
|
124
|
-
if (numMatch) {
|
|
125
|
-
const num = parseInt(numMatch[0], 10)
|
|
126
|
-
if (num >= this.#nextMediaId) this.#nextMediaId = num + 1
|
|
127
|
-
}
|
|
128
146
|
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
logger.debug(`Indexed ${this.#mediaRegistry.size} media file(s)`)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
133
149
|
}
|
|
134
150
|
|
|
135
151
|
/**
|
|
@@ -152,23 +168,50 @@ class MediaManager {
|
|
|
152
168
|
async embedImage(source, mimeType) {
|
|
153
169
|
let data
|
|
154
170
|
let ext
|
|
171
|
+
let hash
|
|
172
|
+
let size
|
|
173
|
+
const isStream = source && typeof source.on === 'function' && typeof source.pipe === 'function'
|
|
155
174
|
|
|
156
|
-
if (
|
|
157
|
-
//
|
|
175
|
+
if (isStream) {
|
|
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') {
|
|
179
|
+
const filePath = source.path
|
|
180
|
+
ext = filePath.split('.').pop().toLowerCase()
|
|
181
|
+
} else {
|
|
182
|
+
ext = this.#detectExtension(data)
|
|
183
|
+
}
|
|
184
|
+
mimeType = mimeType || EXT_TO_MIME[ext] || 'image/png'
|
|
185
|
+
hash = this.#hashBytes(data)
|
|
186
|
+
size = data.length
|
|
187
|
+
} else if (typeof source === 'string') {
|
|
188
|
+
// Load from file path directly to buffer
|
|
158
189
|
data = await fsExtra.readFile(source)
|
|
159
190
|
ext = source.split('.').pop().toLowerCase()
|
|
160
191
|
mimeType = mimeType || EXT_TO_MIME[ext] || 'image/png'
|
|
192
|
+
hash = this.#hashBytes(data)
|
|
193
|
+
size = data.length
|
|
161
194
|
} else if (Buffer.isBuffer(source) || source instanceof Uint8Array) {
|
|
162
195
|
data = source
|
|
163
|
-
// Detect format from magic bytes
|
|
164
196
|
ext = this.#detectExtension(data)
|
|
165
197
|
mimeType = mimeType || EXT_TO_MIME[ext] || 'image/png'
|
|
198
|
+
hash = this.#hashBytes(data)
|
|
199
|
+
size = data.length
|
|
166
200
|
} else {
|
|
167
|
-
throw new PPTXError(
|
|
201
|
+
throw new PPTXError(
|
|
202
|
+
'embedImage: source must be a file path string, Buffer, or Readable Stream'
|
|
203
|
+
)
|
|
204
|
+
}
|
|
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
|
|
168
210
|
}
|
|
169
211
|
|
|
170
|
-
//
|
|
171
|
-
|
|
212
|
+
// Ensure all media from template is hashed to check for duplicates
|
|
213
|
+
await this.#ensureAllMediaHashed()
|
|
214
|
+
|
|
172
215
|
if (this.#mediaHashIndex.has(hash)) {
|
|
173
216
|
const existingPath = this.#mediaHashIndex.get(hash)
|
|
174
217
|
logger.debug(`Reusing existing media: ${existingPath} (hash: ${hash.substring(0, 8)}...)`)
|
|
@@ -180,13 +223,14 @@ class MediaManager {
|
|
|
180
223
|
const zipPath = `ppt/media/image${mediaId}.${ext}`
|
|
181
224
|
|
|
182
225
|
this.#zipManager.writeBinaryFile(zipPath, data)
|
|
226
|
+
|
|
183
227
|
this.#mediaHashIndex.set(hash, zipPath)
|
|
184
|
-
this.#mediaRegistry.set(zipPath, { zipPath, hash, mimeType, size
|
|
228
|
+
this.#mediaRegistry.set(zipPath, { zipPath, hash, mimeType, size })
|
|
185
229
|
|
|
186
230
|
// Register content type
|
|
187
231
|
this.#registerContentType(ext, mimeType)
|
|
188
232
|
|
|
189
|
-
logger.debug(`Embedded new media: ${zipPath} (${
|
|
233
|
+
logger.debug(`Embedded new media: ${zipPath} (${size} bytes)`)
|
|
190
234
|
return zipPath
|
|
191
235
|
}
|
|
192
236
|
|
|
@@ -31,19 +31,14 @@ class ShapeManager {
|
|
|
31
31
|
* @param {SlideManager} slideManager
|
|
32
32
|
*/
|
|
33
33
|
updateShapeText(slideIndex, shapeId, text, slideManager) {
|
|
34
|
-
const
|
|
35
|
-
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
36
|
-
const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
|
|
37
|
-
const res = this.findShapeRecursive(spTree, shapeId)
|
|
34
|
+
const res = slideManager.getSlideShape(slideIndex, shapeId)
|
|
38
35
|
|
|
39
36
|
if (!res) {
|
|
40
37
|
throw new PPTXError(`Shape "${shapeId}" not found in slide ${slideIndex}`)
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
this.#setShapeTextObj(res.shape, text)
|
|
44
|
-
|
|
45
|
-
const decl = this.#xmlParser.extractDeclaration(slideXml)
|
|
46
|
-
slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
|
|
41
|
+
slideManager.markSlideObjDirty(slideIndex)
|
|
47
42
|
logger.debug(`Updated text for shape "${shapeId}" on slide ${slideIndex}`)
|
|
48
43
|
}
|
|
49
44
|
|
|
@@ -60,10 +55,7 @@ class ShapeManager {
|
|
|
60
55
|
* @param {SlideManager} slideManager
|
|
61
56
|
*/
|
|
62
57
|
updateShapePosition(slideIndex, shapeId, options = {}, slideManager) {
|
|
63
|
-
const
|
|
64
|
-
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
65
|
-
const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
|
|
66
|
-
const res = this.findShapeRecursive(spTree, shapeId)
|
|
58
|
+
const res = slideManager.getSlideShape(slideIndex, shapeId)
|
|
67
59
|
|
|
68
60
|
if (!res) {
|
|
69
61
|
throw new PPTXError(`Shape "${shapeId}" not found in slide ${slideIndex}`)
|
|
@@ -108,8 +100,7 @@ class ShapeManager {
|
|
|
108
100
|
xfrm['a:ext']['@_cy'] = '0'
|
|
109
101
|
}
|
|
110
102
|
|
|
111
|
-
|
|
112
|
-
slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
|
|
103
|
+
slideManager.markSlideObjDirty(slideIndex)
|
|
113
104
|
logger.debug(`Updated position/dimensions for shape "${shapeId}" on slide ${slideIndex}`)
|
|
114
105
|
}
|
|
115
106
|
|
|
@@ -146,10 +137,8 @@ class ShapeManager {
|
|
|
146
137
|
* @param {SlideManager} slideManager
|
|
147
138
|
*/
|
|
148
139
|
cloneShape(slideIndex, shapeId, newShapeId, options = {}, slideManager) {
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
|
|
152
|
-
const res = this.findShapeRecursive(spTree, shapeId)
|
|
140
|
+
const slideObj = slideManager.getSlideObj(slideIndex)
|
|
141
|
+
const res = slideManager.getSlideShape(slideIndex, shapeId)
|
|
153
142
|
|
|
154
143
|
if (!res) {
|
|
155
144
|
throw new PPTXError(`Shape "${shapeId}" not found in slide ${slideIndex}`)
|
|
@@ -158,6 +147,11 @@ class ShapeManager {
|
|
|
158
147
|
const newShape = this.#xmlParser.deepClone(res.shape)
|
|
159
148
|
const cNvPr = newShape['p:nvSpPr']?.['p:cNvPr']
|
|
160
149
|
|
|
150
|
+
const spTree =
|
|
151
|
+
slideObj?.['p:sld']?.['p:cSld']?.['p:spTree'] ||
|
|
152
|
+
slideObj?.['p:sldLayout']?.['p:cSld']?.['p:spTree'] ||
|
|
153
|
+
slideObj?.['p:sldMaster']?.['p:cSld']?.['p:spTree']
|
|
154
|
+
|
|
161
155
|
const existingIds = this.#getAllShapeIds(spTree)
|
|
162
156
|
const maxId = existingIds.length > 0 ? Math.max(...existingIds) : 1000
|
|
163
157
|
const newId = maxId + 1
|
|
@@ -204,8 +198,7 @@ class ShapeManager {
|
|
|
204
198
|
}
|
|
205
199
|
parent['p:sp'].push(newShape)
|
|
206
200
|
|
|
207
|
-
|
|
208
|
-
slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
|
|
201
|
+
slideManager.markSlideObjDirty(slideIndex)
|
|
209
202
|
logger.debug(`Cloned shape "${shapeId}" as "${newShapeId}"`)
|
|
210
203
|
}
|
|
211
204
|
|
|
@@ -217,10 +210,7 @@ class ShapeManager {
|
|
|
217
210
|
* @param {SlideManager} slideManager
|
|
218
211
|
*/
|
|
219
212
|
deleteShape(slideIndex, shapeId, slideManager) {
|
|
220
|
-
const
|
|
221
|
-
const slideObj = this.#xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
222
|
-
const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
|
|
223
|
-
const res = this.findShapeRecursive(spTree, shapeId)
|
|
213
|
+
const res = slideManager.getSlideShape(slideIndex, shapeId)
|
|
224
214
|
|
|
225
215
|
if (!res) {
|
|
226
216
|
throw new PPTXError(`Shape "${shapeId}" not found in slide ${slideIndex}`)
|
|
@@ -235,8 +225,7 @@ class ShapeManager {
|
|
|
235
225
|
}
|
|
236
226
|
}
|
|
237
227
|
|
|
238
|
-
|
|
239
|
-
slideManager.setSlideXml(slideIndex, this.#xmlParser.build(slideObj, decl))
|
|
228
|
+
slideManager.markSlideObjDirty(slideIndex)
|
|
240
229
|
logger.debug(`Deleted shape "${shapeId}" from slide ${slideIndex}`)
|
|
241
230
|
}
|
|
242
231
|
|
|
@@ -248,9 +237,11 @@ class ShapeManager {
|
|
|
248
237
|
* @returns {Array<Object>}
|
|
249
238
|
*/
|
|
250
239
|
getShapes(slideIndex, slideManager) {
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
|
|
240
|
+
const slideObj = slideManager.getSlideObj(slideIndex)
|
|
241
|
+
const spTree =
|
|
242
|
+
slideObj?.['p:sld']?.['p:cSld']?.['p:spTree'] ||
|
|
243
|
+
slideObj?.['p:sldLayout']?.['p:cSld']?.['p:spTree'] ||
|
|
244
|
+
slideObj?.['p:sldMaster']?.['p:cSld']?.['p:spTree']
|
|
254
245
|
|
|
255
246
|
const shapesInfo = []
|
|
256
247
|
this.#collectShapesInfo(spTree, shapesInfo)
|
|
@@ -71,6 +71,12 @@ class SlideManager {
|
|
|
71
71
|
*/
|
|
72
72
|
#slideXmlCache = new Map()
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Slide states: maps 1-based index → state object.
|
|
76
|
+
* @private @type {Map<number, Object>}
|
|
77
|
+
*/
|
|
78
|
+
#slideStates = new Map()
|
|
79
|
+
|
|
74
80
|
/**
|
|
75
81
|
* Custom tags: maps tag → array of 1-based indices.
|
|
76
82
|
* @private @type {Map<string, number[]>}
|
|
@@ -177,6 +183,16 @@ class SlideManager {
|
|
|
177
183
|
}
|
|
178
184
|
|
|
179
185
|
this.#slides.set(slideIndex, slideInfo)
|
|
186
|
+
this.#slideStates.set(slideIndex, {
|
|
187
|
+
xmlStr: null,
|
|
188
|
+
xmlObj: null,
|
|
189
|
+
dirty: false,
|
|
190
|
+
indexBuilt: false,
|
|
191
|
+
shapeMap: new Map(),
|
|
192
|
+
picMap: new Map(),
|
|
193
|
+
tableMap: new Map(),
|
|
194
|
+
chartMap: new Map(),
|
|
195
|
+
})
|
|
180
196
|
slideIndex++
|
|
181
197
|
}
|
|
182
198
|
|
|
@@ -237,9 +253,27 @@ class SlideManager {
|
|
|
237
253
|
getSlideXml(slideIndex) {
|
|
238
254
|
this.#assertSlideExists(slideIndex)
|
|
239
255
|
const info = this.#slides.get(slideIndex)
|
|
256
|
+
const state = this.#slideStates.get(slideIndex)
|
|
257
|
+
|
|
258
|
+
if (state.dirty && state.xmlObj) {
|
|
259
|
+
const decl = this.#xmlParser.extractDeclaration(
|
|
260
|
+
state.xmlStr || '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
261
|
+
)
|
|
262
|
+
state.xmlStr = this.#xmlParser.build(state.xmlObj, decl)
|
|
263
|
+
this.#zipManager.writeFile(info.zipPath, state.xmlStr)
|
|
264
|
+
this.#slideXmlCache.set(info.zipPath, state.xmlStr)
|
|
265
|
+
state.dirty = false
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (state.xmlStr) {
|
|
269
|
+
return state.xmlStr
|
|
270
|
+
}
|
|
240
271
|
|
|
241
|
-
|
|
242
|
-
|
|
272
|
+
const cached = this.#zipManager.readCachedFile(info.zipPath)
|
|
273
|
+
if (cached) {
|
|
274
|
+
state.xmlStr = cached
|
|
275
|
+
this.#slideXmlCache.set(info.zipPath, cached)
|
|
276
|
+
return cached
|
|
243
277
|
}
|
|
244
278
|
|
|
245
279
|
// This is sync because we pre-load; async callers should use getSlideXmlAsync
|
|
@@ -255,14 +289,16 @@ class SlideManager {
|
|
|
255
289
|
async getSlideXmlAsync(slideIndex) {
|
|
256
290
|
this.#assertSlideExists(slideIndex)
|
|
257
291
|
const info = this.#slides.get(slideIndex)
|
|
292
|
+
const state = this.#slideStates.get(slideIndex)
|
|
258
293
|
|
|
259
|
-
if (!
|
|
294
|
+
if (!state.xmlStr) {
|
|
260
295
|
const xml = await this.#zipManager.readFile(info.zipPath)
|
|
261
296
|
if (!xml) throw new SlideNotFoundError(`Slide ${slideIndex} XML not found at ${info.zipPath}`)
|
|
297
|
+
state.xmlStr = xml
|
|
262
298
|
this.#slideXmlCache.set(info.zipPath, xml)
|
|
263
299
|
}
|
|
264
300
|
|
|
265
|
-
return
|
|
301
|
+
return state.xmlStr
|
|
266
302
|
}
|
|
267
303
|
|
|
268
304
|
/**
|
|
@@ -274,6 +310,13 @@ class SlideManager {
|
|
|
274
310
|
setSlideXml(slideIndex, xml) {
|
|
275
311
|
this.#assertSlideExists(slideIndex)
|
|
276
312
|
const info = this.#slides.get(slideIndex)
|
|
313
|
+
const state = this.#slideStates.get(slideIndex)
|
|
314
|
+
|
|
315
|
+
state.xmlStr = xml
|
|
316
|
+
state.xmlObj = null
|
|
317
|
+
state.dirty = false
|
|
318
|
+
state.indexBuilt = false
|
|
319
|
+
|
|
277
320
|
this.#slideXmlCache.set(info.zipPath, xml)
|
|
278
321
|
this.#zipManager.writeFile(info.zipPath, xml)
|
|
279
322
|
}
|
|
@@ -360,6 +403,16 @@ class SlideManager {
|
|
|
360
403
|
}
|
|
361
404
|
|
|
362
405
|
this.#slides.set(newIndex, slideInfo)
|
|
406
|
+
this.#slideStates.set(newIndex, {
|
|
407
|
+
xmlStr: slideXml,
|
|
408
|
+
xmlObj: null,
|
|
409
|
+
dirty: false,
|
|
410
|
+
indexBuilt: false,
|
|
411
|
+
shapeMap: new Map(),
|
|
412
|
+
picMap: new Map(),
|
|
413
|
+
tableMap: new Map(),
|
|
414
|
+
chartMap: new Map(),
|
|
415
|
+
})
|
|
363
416
|
|
|
364
417
|
// Update presentation.xml sldIdLst
|
|
365
418
|
this.#addSlideToPresentation(rId, newSlideId)
|
|
@@ -424,6 +477,16 @@ class SlideManager {
|
|
|
424
477
|
}
|
|
425
478
|
|
|
426
479
|
this.#slides.set(newIndex, slideInfo)
|
|
480
|
+
this.#slideStates.set(newIndex, {
|
|
481
|
+
xmlStr: sourceXml,
|
|
482
|
+
xmlObj: null,
|
|
483
|
+
dirty: false,
|
|
484
|
+
indexBuilt: false,
|
|
485
|
+
shapeMap: new Map(),
|
|
486
|
+
picMap: new Map(),
|
|
487
|
+
tableMap: new Map(),
|
|
488
|
+
chartMap: new Map(),
|
|
489
|
+
})
|
|
427
490
|
this.#addSlideToPresentation(rId, newSlideId)
|
|
428
491
|
this.#registerSlideContentType(slideFileName)
|
|
429
492
|
|
|
@@ -448,6 +511,7 @@ class SlideManager {
|
|
|
448
511
|
|
|
449
512
|
// Remove from cache
|
|
450
513
|
this.#slideXmlCache.delete(info.zipPath)
|
|
514
|
+
this.#slideStates.delete(slideIndex)
|
|
451
515
|
|
|
452
516
|
// Remove relationship from presentation.xml
|
|
453
517
|
this.#relationshipManager.removeRelationship('ppt/presentation.xml', info.relationshipId)
|
|
@@ -479,13 +543,18 @@ class SlideManager {
|
|
|
479
543
|
}
|
|
480
544
|
|
|
481
545
|
const slidesCopy = new Map(this.#slides)
|
|
546
|
+
const statesCopy = new Map(this.#slideStates)
|
|
482
547
|
this.#slides.clear()
|
|
548
|
+
this.#slideStates.clear()
|
|
483
549
|
|
|
484
550
|
order.forEach((oldIndex, newPos) => {
|
|
485
551
|
const info = slidesCopy.get(oldIndex)
|
|
486
552
|
if (!info) throw new SlideNotFoundError(`Slide ${oldIndex} not found`)
|
|
487
553
|
info.index = newPos + 1
|
|
488
554
|
this.#slides.set(newPos + 1, info)
|
|
555
|
+
|
|
556
|
+
const state = statesCopy.get(oldIndex)
|
|
557
|
+
this.#slideStates.set(newPos + 1, state)
|
|
489
558
|
})
|
|
490
559
|
|
|
491
560
|
// Rebuild presentation sldIdLst
|
|
@@ -741,6 +810,16 @@ class SlideManager {
|
|
|
741
810
|
}
|
|
742
811
|
|
|
743
812
|
this.#slides.set(newIndex, slideInfo)
|
|
813
|
+
this.#slideStates.set(newIndex, {
|
|
814
|
+
xmlStr: slideXml,
|
|
815
|
+
xmlObj: null,
|
|
816
|
+
dirty: false,
|
|
817
|
+
indexBuilt: false,
|
|
818
|
+
shapeMap: new Map(),
|
|
819
|
+
picMap: new Map(),
|
|
820
|
+
tableMap: new Map(),
|
|
821
|
+
chartMap: new Map(),
|
|
822
|
+
})
|
|
744
823
|
|
|
745
824
|
// Add entry in presentation.xml sldIdLst
|
|
746
825
|
this.#addSlideToPresentation(rId, newSlideId)
|
|
@@ -917,10 +996,18 @@ class SlideManager {
|
|
|
917
996
|
#reindexSlides() {
|
|
918
997
|
const sorted = Array.from(this.#slides.entries()).sort(([a], [b]) => a - b)
|
|
919
998
|
this.#slides.clear()
|
|
999
|
+
|
|
1000
|
+
const sortedStates = Array.from(this.#slideStates.entries()).sort(([a], [b]) => a - b)
|
|
1001
|
+
this.#slideStates.clear()
|
|
1002
|
+
|
|
920
1003
|
sorted.forEach(([, info], i) => {
|
|
921
1004
|
info.index = i + 1
|
|
922
1005
|
this.#slides.set(i + 1, info)
|
|
923
1006
|
})
|
|
1007
|
+
|
|
1008
|
+
sortedStates.forEach(([, state], i) => {
|
|
1009
|
+
this.#slideStates.set(i + 1, state)
|
|
1010
|
+
})
|
|
924
1011
|
}
|
|
925
1012
|
|
|
926
1013
|
/**
|
|
@@ -1044,6 +1131,162 @@ class SlideManager {
|
|
|
1044
1131
|
)
|
|
1045
1132
|
}
|
|
1046
1133
|
}
|
|
1134
|
+
|
|
1135
|
+
getSlideObj(slideIndex) {
|
|
1136
|
+
this.#assertSlideExists(slideIndex)
|
|
1137
|
+
const state = this.#slideStates.get(slideIndex)
|
|
1138
|
+
|
|
1139
|
+
if (!state.xmlObj) {
|
|
1140
|
+
const xml = this.getSlideXml(slideIndex)
|
|
1141
|
+
const info = this.#slides.get(slideIndex)
|
|
1142
|
+
state.xmlObj = this.#xmlParser.parse(xml, info.zipPath.split('/').pop())
|
|
1143
|
+
state.indexBuilt = false
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
if (!state.indexBuilt) {
|
|
1147
|
+
state.shapeMap.clear()
|
|
1148
|
+
state.picMap.clear()
|
|
1149
|
+
state.tableMap.clear()
|
|
1150
|
+
state.chartMap.clear()
|
|
1151
|
+
|
|
1152
|
+
const spTree =
|
|
1153
|
+
state.xmlObj?.['p:sld']?.['p:cSld']?.['p:spTree'] ||
|
|
1154
|
+
state.xmlObj?.['p:sldLayout']?.['p:cSld']?.['p:spTree'] ||
|
|
1155
|
+
state.xmlObj?.['p:sldMaster']?.['p:cSld']?.['p:spTree']
|
|
1156
|
+
|
|
1157
|
+
this.#buildSlideIndexRecursive(
|
|
1158
|
+
spTree,
|
|
1159
|
+
state.shapeMap,
|
|
1160
|
+
state.picMap,
|
|
1161
|
+
state.tableMap,
|
|
1162
|
+
state.chartMap
|
|
1163
|
+
)
|
|
1164
|
+
state.indexBuilt = true
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
return state.xmlObj
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
#buildSlideIndexRecursive(container, shapeMap, picMap, tableMap, chartMap) {
|
|
1171
|
+
if (!container) return
|
|
1172
|
+
|
|
1173
|
+
// Shapes
|
|
1174
|
+
let shapes = container['p:sp'] || []
|
|
1175
|
+
if (!Array.isArray(shapes)) shapes = [shapes]
|
|
1176
|
+
for (const shape of shapes) {
|
|
1177
|
+
const cNvPr = shape?.['p:nvSpPr']?.['p:cNvPr']
|
|
1178
|
+
if (cNvPr) {
|
|
1179
|
+
const name = cNvPr['@_name']
|
|
1180
|
+
const id = String(cNvPr['@_id'])
|
|
1181
|
+
const entry = { shape, parent: container, type: 'sp' }
|
|
1182
|
+
if (name) shapeMap.set(name, entry)
|
|
1183
|
+
if (id) shapeMap.set(id, entry)
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Pictures
|
|
1188
|
+
let pics = container['p:pic'] || []
|
|
1189
|
+
if (!Array.isArray(pics)) pics = [pics]
|
|
1190
|
+
for (const pic of pics) {
|
|
1191
|
+
const cNvPr = pic?.['p:nvPicPr']?.['p:cNvPr']
|
|
1192
|
+
if (cNvPr) {
|
|
1193
|
+
const name = cNvPr['@_name']
|
|
1194
|
+
const id = String(cNvPr['@_id'])
|
|
1195
|
+
const embedId = pic?.['p:blipFill']?.['a:blip']?.['@_r:embed']
|
|
1196
|
+
const entry = { pic, parent: container, type: 'pic' }
|
|
1197
|
+
if (name) picMap.set(name, entry)
|
|
1198
|
+
if (id) picMap.set(id, entry)
|
|
1199
|
+
if (embedId) picMap.set(embedId, entry)
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Graphic frames
|
|
1204
|
+
let frames = container['p:graphicFrame'] || []
|
|
1205
|
+
if (!Array.isArray(frames)) frames = [frames]
|
|
1206
|
+
for (const frame of frames) {
|
|
1207
|
+
const cNvPr = frame?.['p:nvGraphicFramePr']?.['p:cNvPr']
|
|
1208
|
+
const name = cNvPr ? cNvPr['@_name'] : null
|
|
1209
|
+
const id = cNvPr ? String(cNvPr['@_id']) : null
|
|
1210
|
+
|
|
1211
|
+
const tbl = frame?.['a:graphic']?.['a:graphicData']?.['a:tbl']
|
|
1212
|
+
if (tbl) {
|
|
1213
|
+
const entry = { table: tbl, frame, parent: container, type: 'table' }
|
|
1214
|
+
if (name) tableMap.set(name, entry)
|
|
1215
|
+
if (id) tableMap.set(id, entry)
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
const chart = frame?.['a:graphic']?.['a:graphicData']?.['c:chart']
|
|
1219
|
+
if (chart) {
|
|
1220
|
+
const embedId = chart['@_r:id']
|
|
1221
|
+
const entry = { chart, frame, parent: container, type: 'chart' }
|
|
1222
|
+
if (name) chartMap.set(name, entry)
|
|
1223
|
+
if (id) chartMap.set(id, entry)
|
|
1224
|
+
if (embedId) chartMap.set(embedId, entry)
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// Groups
|
|
1229
|
+
let groups = container['p:grpSp'] || []
|
|
1230
|
+
if (!Array.isArray(groups)) groups = [groups]
|
|
1231
|
+
for (const group of groups) {
|
|
1232
|
+
const cNvPr = group?.['p:nvGrpSpPr']?.['p:cNvPr']
|
|
1233
|
+
if (cNvPr) {
|
|
1234
|
+
const name = cNvPr['@_name']
|
|
1235
|
+
const id = String(cNvPr['@_id'])
|
|
1236
|
+
const entry = { shape: group, parent: container, type: 'grpSp' }
|
|
1237
|
+
if (name) shapeMap.set(name, entry)
|
|
1238
|
+
if (id) shapeMap.set(id, entry)
|
|
1239
|
+
}
|
|
1240
|
+
this.#buildSlideIndexRecursive(group, shapeMap, picMap, tableMap, chartMap)
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
flush() {
|
|
1245
|
+
for (const [index, state] of this.#slideStates) {
|
|
1246
|
+
if (state.dirty && state.xmlObj) {
|
|
1247
|
+
this.getSlideXml(index)
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
markSlideObjDirty(slideIndex) {
|
|
1253
|
+
this.#assertSlideExists(slideIndex)
|
|
1254
|
+
const state = this.#slideStates.get(slideIndex)
|
|
1255
|
+
state.dirty = true
|
|
1256
|
+
state.indexBuilt = false
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
getSlideShape(slideIndex, shapeId) {
|
|
1260
|
+
this.getSlideObj(slideIndex)
|
|
1261
|
+
return this.#slideStates.get(slideIndex).shapeMap.get(String(shapeId)) || null
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
getSlidePic(slideIndex, picId) {
|
|
1265
|
+
this.getSlideObj(slideIndex)
|
|
1266
|
+
const state = this.#slideStates.get(slideIndex)
|
|
1267
|
+
if (picId === 'first') {
|
|
1268
|
+
return Array.from(state.picMap.values())[0] || null
|
|
1269
|
+
}
|
|
1270
|
+
return state.picMap.get(String(picId)) || null
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
getSlideTable(slideIndex, tableId) {
|
|
1274
|
+
this.getSlideObj(slideIndex)
|
|
1275
|
+
const state = this.#slideStates.get(slideIndex)
|
|
1276
|
+
if (tableId === 'first') {
|
|
1277
|
+
return Array.from(state.tableMap.values())[0] || null
|
|
1278
|
+
}
|
|
1279
|
+
return state.tableMap.get(String(tableId)) || null
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
getSlideChart(slideIndex, chartId) {
|
|
1283
|
+
this.getSlideObj(slideIndex)
|
|
1284
|
+
const state = this.#slideStates.get(slideIndex)
|
|
1285
|
+
if (chartId === 'first') {
|
|
1286
|
+
return Array.from(state.chartMap.values())[0] || null
|
|
1287
|
+
}
|
|
1288
|
+
return state.chartMap.get(String(chartId)) || null
|
|
1289
|
+
}
|
|
1047
1290
|
}
|
|
1048
1291
|
|
|
1049
1292
|
module.exports = { SlideManager }
|