pptxtojson 0.0.1

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.
@@ -0,0 +1,1082 @@
1
+ import JSZip from 'jszip'
2
+ import * as txml from 'txml/dist/txml.mjs'
3
+ import tinycolor from 'tinycolor2'
4
+
5
+ import { extractFileExtension, base64ArrayBuffer, eachElement, getTextByPathList, angleToDegrees } from './utils'
6
+
7
+ const FACTOR = 96 / 914400
8
+
9
+ let themeContent = null
10
+
11
+ export async function parse(file) {
12
+ const slides = []
13
+
14
+ const zip = await JSZip.loadAsync(file)
15
+
16
+ const filesInfo = await getContentTypes(zip)
17
+ const size = await getSlideSize(zip)
18
+ themeContent = await loadTheme(zip)
19
+
20
+ for (const filename of filesInfo.slides) {
21
+ const singleSlide = await processSingleSlide(zip, filename)
22
+ slides.push(singleSlide)
23
+ }
24
+
25
+ return { slides, size }
26
+ }
27
+
28
+ function simplifyLostLess(children, parentAttributes = {}) {
29
+ const out = {}
30
+ if (!children.length) return out
31
+
32
+ if (children.length === 1 && typeof children[0] === 'string') {
33
+ return Object.keys(parentAttributes).length ? {
34
+ attrs: parentAttributes,
35
+ value: children[0],
36
+ } : children[0]
37
+ }
38
+ for (const child of children) {
39
+ if (typeof child !== 'object') return
40
+ if (child.tagName === '?xml') continue
41
+
42
+ if (!out[child.tagName]) out[child.tagName] = []
43
+
44
+ const kids = simplifyLostLess(child.children || [], child.attributes)
45
+ out[child.tagName].push(kids)
46
+
47
+ if (Object.keys(child.attributes).length) {
48
+ kids.attrs = child.attributes
49
+ }
50
+ }
51
+ for (const child in out) {
52
+ if (out[child].length === 1) out[child] = out[child][0]
53
+ }
54
+
55
+ return out
56
+ }
57
+
58
+ async function readXmlFile(zip, filename) {
59
+ const data = await zip.file(filename).async('string')
60
+ return simplifyLostLess(txml.parse(data))
61
+ }
62
+
63
+ async function getContentTypes(zip) {
64
+ const ContentTypesJson = await readXmlFile(zip, '[Content_Types].xml')
65
+ const subObj = ContentTypesJson['Types']['Override']
66
+ const slidesLocArray = []
67
+ const slideLayoutsLocArray = []
68
+
69
+ for (const item of subObj) {
70
+ switch (item['attrs']['ContentType']) {
71
+ case 'application/vnd.openxmlformats-officedocument.presentationml.slide+xml':
72
+ slidesLocArray.push(item['attrs']['PartName'].substr(1))
73
+ break
74
+ case 'application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml':
75
+ slideLayoutsLocArray.push(item['attrs']['PartName'].substr(1))
76
+ break
77
+ default:
78
+ }
79
+ }
80
+ return {
81
+ slides: slidesLocArray,
82
+ slideLayouts: slideLayoutsLocArray,
83
+ }
84
+ }
85
+
86
+ async function getSlideSize(zip) {
87
+ const content = await readXmlFile(zip, 'ppt/presentation.xml')
88
+ const sldSzAttrs = content['p:presentation']['p:sldSz']['attrs']
89
+ return {
90
+ width: parseInt(sldSzAttrs['cx']) * FACTOR,
91
+ height: parseInt(sldSzAttrs['cy']) * FACTOR,
92
+ }
93
+ }
94
+
95
+ async function loadTheme(zip) {
96
+ const preResContent = await readXmlFile(zip, 'ppt/_rels/presentation.xml.rels')
97
+ const relationshipArray = preResContent['Relationships']['Relationship']
98
+ let themeURI
99
+
100
+ if (relationshipArray.constructor === Array) {
101
+ for (const relationshipItem of relationshipArray) {
102
+ if (relationshipItem['attrs']['Type'] === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme') {
103
+ themeURI = relationshipItem['attrs']['Target']
104
+ break
105
+ }
106
+ }
107
+ }
108
+ else if (relationshipArray['attrs']['Type'] === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme') {
109
+ themeURI = relationshipArray['attrs']['Target']
110
+ }
111
+
112
+ if (!themeURI) throw Error(`Can't open theme file.`)
113
+
114
+ return await readXmlFile(zip, 'ppt/' + themeURI)
115
+ }
116
+
117
+ async function processSingleSlide(zip, sldFileName) {
118
+ const resName = sldFileName.replace('slides/slide', 'slides/_rels/slide') + '.rels'
119
+ const resContent = await readXmlFile(zip, resName)
120
+ let relationshipArray = resContent['Relationships']['Relationship']
121
+ let layoutFilename = ''
122
+ const slideResObj = {}
123
+
124
+ if (relationshipArray.constructor === Array) {
125
+ for (const relationshipArrayItem of relationshipArray) {
126
+ switch (relationshipArrayItem['attrs']['Type']) {
127
+ case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout':
128
+ layoutFilename = relationshipArrayItem['attrs']['Target'].replace('../', 'ppt/')
129
+ break
130
+ case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide':
131
+ case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image':
132
+ case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart':
133
+ case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink':
134
+ default:
135
+ slideResObj[relationshipArrayItem['attrs']['Id']] = {
136
+ type: relationshipArrayItem['attrs']['Type'].replace('http://schemas.openxmlformats.org/officeDocument/2006/relationships/', ''),
137
+ target: relationshipArrayItem['attrs']['Target'].replace('../', 'ppt/'),
138
+ }
139
+ }
140
+ }
141
+ }
142
+ else layoutFilename = relationshipArray['attrs']['Target'].replace('../', 'ppt/')
143
+
144
+ const slideLayoutContent = await readXmlFile(zip, layoutFilename)
145
+ const slideLayoutTables = await indexNodes(slideLayoutContent)
146
+
147
+ const slideLayoutResFilename = layoutFilename.replace('slideLayouts/slideLayout', 'slideLayouts/_rels/slideLayout') + '.rels'
148
+ const slideLayoutResContent = await readXmlFile(zip, slideLayoutResFilename)
149
+ relationshipArray = slideLayoutResContent['Relationships']['Relationship']
150
+
151
+ let masterFilename = ''
152
+ if (relationshipArray.constructor === Array) {
153
+ for (const relationshipArrayItem of relationshipArray) {
154
+ switch (relationshipArrayItem['attrs']['Type']) {
155
+ case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster':
156
+ masterFilename = relationshipArrayItem['attrs']['Target'].replace('../', 'ppt/')
157
+ break
158
+ default:
159
+ }
160
+ }
161
+ }
162
+ else masterFilename = relationshipArray['attrs']['Target'].replace('../', 'ppt/')
163
+
164
+ const slideMasterContent = await readXmlFile(zip, masterFilename)
165
+ const slideMasterTextStyles = getTextByPathList(slideMasterContent, ['p:sldMaster', 'p:txStyles'])
166
+ const slideMasterTables = indexNodes(slideMasterContent)
167
+
168
+ const slideContent = await readXmlFile(zip, sldFileName)
169
+ const nodes = slideContent['p:sld']['p:cSld']['p:spTree']
170
+ const warpObj = {
171
+ zip,
172
+ slideLayoutTables: slideLayoutTables,
173
+ slideMasterTables: slideMasterTables,
174
+ slideResObj: slideResObj,
175
+ slideMasterTextStyles: slideMasterTextStyles,
176
+ }
177
+
178
+ const bgColor = '#' + getSlideBackgroundFill(slideContent, slideLayoutContent, slideMasterContent)
179
+
180
+ const elements = []
181
+ for (const nodeKey in nodes) {
182
+ if (nodes[nodeKey].constructor === Array) {
183
+ for (const node of nodes[nodeKey]) {
184
+ const ret = await processNodesInSlide(nodeKey, node, warpObj)
185
+ if (ret) elements.push(ret)
186
+ }
187
+ }
188
+ else {
189
+ const ret = await processNodesInSlide(nodeKey, nodes[nodeKey], warpObj)
190
+ if (ret) elements.push(ret)
191
+ }
192
+ }
193
+
194
+ return {
195
+ fill: bgColor,
196
+ elements,
197
+ }
198
+ }
199
+
200
+ function indexNodes(content) {
201
+
202
+ const keys = Object.keys(content)
203
+ const spTreeNode = content[keys[0]]['p:cSld']['p:spTree']
204
+
205
+ const idTable = {}
206
+ const idxTable = {}
207
+ const typeTable = {}
208
+
209
+ for (const key in spTreeNode) {
210
+ if (key === 'p:nvGrpSpPr' || key === 'p:grpSpPr') continue
211
+
212
+ const targetNode = spTreeNode[key]
213
+
214
+ if (targetNode.constructor === Array) {
215
+ for (const targetNodeItem of targetNode) {
216
+ const nvSpPrNode = targetNodeItem['p:nvSpPr']
217
+ const id = getTextByPathList(nvSpPrNode, ['p:cNvPr', 'attrs', 'id'])
218
+ const idx = getTextByPathList(nvSpPrNode, ['p:nvPr', 'p:ph', 'attrs', 'idx'])
219
+ const type = getTextByPathList(nvSpPrNode, ['p:nvPr', 'p:ph', 'attrs', 'type'])
220
+
221
+ if (id) idTable[id] = targetNodeItem
222
+ if (idx) idxTable[idx] = targetNodeItem
223
+ if (type) typeTable[type] = targetNodeItem
224
+ }
225
+ }
226
+ else {
227
+ const nvSpPrNode = targetNode['p:nvSpPr']
228
+ const id = getTextByPathList(nvSpPrNode, ['p:cNvPr', 'attrs', 'id'])
229
+ const idx = getTextByPathList(nvSpPrNode, ['p:nvPr', 'p:ph', 'attrs', 'idx'])
230
+ const type = getTextByPathList(nvSpPrNode, ['p:nvPr', 'p:ph', 'attrs', 'type'])
231
+
232
+ if (id) idTable[id] = targetNode
233
+ if (idx) idxTable[idx] = targetNode
234
+ if (type) typeTable[type] = targetNode
235
+ }
236
+ }
237
+
238
+ return { idTable, idxTable, typeTable }
239
+ }
240
+
241
+ async function processNodesInSlide(nodeKey, nodeValue, warpObj) {
242
+ let json
243
+
244
+ switch (nodeKey) {
245
+ case 'p:sp': // Shape, Text
246
+ json = processSpNode(nodeValue, warpObj)
247
+ break
248
+ case 'p:cxnSp': // Shape, Text (with connection)
249
+ json = processCxnSpNode(nodeValue, warpObj)
250
+ break
251
+ case 'p:pic': // Picture
252
+ json = processPicNode(nodeValue, warpObj)
253
+ break
254
+ case 'p:graphicFrame': // Chart, Diagram, Table
255
+ json = await processGraphicFrameNode(nodeValue, warpObj)
256
+ break
257
+ case 'p:grpSp':
258
+ json = await processGroupSpNode(nodeValue, warpObj)
259
+ break
260
+ default:
261
+ }
262
+
263
+ return json
264
+ }
265
+
266
+ async function processGroupSpNode(node, warpObj) {
267
+ const xfrmNode = node['p:grpSpPr']['a:xfrm']
268
+ const x = parseInt(xfrmNode['a:off']['attrs']['x']) * FACTOR
269
+ const y = parseInt(xfrmNode['a:off']['attrs']['y']) * FACTOR
270
+ const chx = parseInt(xfrmNode['a:chOff']['attrs']['x']) * FACTOR
271
+ const chy = parseInt(xfrmNode['a:chOff']['attrs']['y']) * FACTOR
272
+ const cx = parseInt(xfrmNode['a:ext']['attrs']['cx']) * FACTOR
273
+ const cy = parseInt(xfrmNode['a:ext']['attrs']['cy']) * FACTOR
274
+ const chcx = parseInt(xfrmNode['a:chExt']['attrs']['cx']) * FACTOR
275
+ const chcy = parseInt(xfrmNode['a:chExt']['attrs']['cy']) * FACTOR
276
+
277
+ const elements = []
278
+ for (const nodeKey in node) {
279
+ if (node[nodeKey].constructor === Array) {
280
+ for (const item of node[nodeKey]) {
281
+ const ret = await processNodesInSlide(nodeKey, item, warpObj)
282
+ if (ret) elements.push(ret)
283
+ }
284
+ }
285
+ else {
286
+ const ret = await processNodesInSlide(nodeKey, node[nodeKey], warpObj)
287
+ if (ret) elements.push(ret)
288
+ }
289
+ }
290
+
291
+ return {
292
+ type: 'group',
293
+ top: y - chy,
294
+ left: x - chx,
295
+ width: cx - chcx,
296
+ height: cy - chcy,
297
+ elements,
298
+ }
299
+ }
300
+
301
+ function processSpNode(node, warpObj) {
302
+ const id = node['p:nvSpPr']['p:cNvPr']['attrs']['id']
303
+ const name = node['p:nvSpPr']['p:cNvPr']['attrs']['name']
304
+ const idx = node['p:nvSpPr']['p:nvPr']['p:ph'] ? node['p:nvSpPr']['p:nvPr']['p:ph']['attrs']['idx'] : undefined
305
+ let type = node['p:nvSpPr']['p:nvPr']['p:ph'] ? node['p:nvSpPr']['p:nvPr']['p:ph']['attrs']['type'] : undefined
306
+
307
+ let slideLayoutSpNode, slideMasterSpNode
308
+
309
+ if (type) {
310
+ if (idx) {
311
+ slideLayoutSpNode = warpObj['slideLayoutTables']['typeTable'][type]
312
+ slideMasterSpNode = warpObj['slideMasterTables']['typeTable'][type]
313
+ }
314
+ else {
315
+ slideLayoutSpNode = warpObj['slideLayoutTables']['typeTable'][type]
316
+ slideMasterSpNode = warpObj['slideMasterTables']['typeTable'][type]
317
+ }
318
+ }
319
+ else if (idx) {
320
+ slideLayoutSpNode = warpObj['slideLayoutTables']['idxTable'][idx]
321
+ slideMasterSpNode = warpObj['slideMasterTables']['idxTable'][idx]
322
+ }
323
+
324
+ if (!type) type = getTextByPathList(slideLayoutSpNode, ['p:nvSpPr', 'p:nvPr', 'p:ph', 'attrs', 'type'])
325
+ if (!type) type = getTextByPathList(slideMasterSpNode, ['p:nvSpPr', 'p:nvPr', 'p:ph', 'attrs', 'type'])
326
+
327
+ return genShape(node, slideLayoutSpNode, slideMasterSpNode, id, name, idx, type, warpObj)
328
+ }
329
+
330
+ function processCxnSpNode(node, warpObj) {
331
+ const id = node['p:nvCxnSpPr']['p:cNvPr']['attrs']['id']
332
+ const name = node['p:nvCxnSpPr']['p:cNvPr']['attrs']['name']
333
+
334
+ return genShape(node, undefined, undefined, id, name, undefined, undefined, warpObj)
335
+ }
336
+
337
+ function genShape(node, slideLayoutSpNode, slideMasterSpNode, id, name, idx, type, warpObj) {
338
+ const xfrmList = ['p:spPr', 'a:xfrm']
339
+ const slideXfrmNode = getTextByPathList(node, xfrmList)
340
+ const slideLayoutXfrmNode = getTextByPathList(slideLayoutSpNode, xfrmList)
341
+ const slideMasterXfrmNode = getTextByPathList(slideMasterSpNode, xfrmList)
342
+
343
+ const shapType = getTextByPathList(node, ['p:spPr', 'a:prstGeom', 'attrs', 'prst'])
344
+
345
+ const { top, left } = getPosition(slideXfrmNode, slideLayoutXfrmNode, slideMasterXfrmNode)
346
+ const { width, height } = getSize(slideXfrmNode, slideLayoutXfrmNode, slideMasterXfrmNode)
347
+
348
+ let isFlipV = false
349
+ let isFlipH = false
350
+ if (getTextByPathList(slideXfrmNode, ['attrs', 'flipV']) === '1') {
351
+ isFlipV = true
352
+ }
353
+ if (getTextByPathList(slideXfrmNode, ['attrs', 'flipH']) === '1') {
354
+ isFlipH = true
355
+ }
356
+
357
+ const rotate = angleToDegrees(getTextByPathList(slideXfrmNode, ['attrs', 'rot']))
358
+
359
+ const txtXframeNode = getTextByPathList(node, ['p:txXfrm'])
360
+ let txtRotate
361
+ if (txtXframeNode) {
362
+ const txtXframeRot = getTextByPathList(txtXframeNode, ['attrs', 'rot'])
363
+ if (txtXframeRot) txtRotate = angleToDegrees(txtXframeRot) + 90
364
+ }
365
+ else txtRotate = rotate
366
+
367
+ let content = ''
368
+ if (node['p:txBody']) content = genTextBody(node['p:txBody'], slideLayoutSpNode, slideMasterSpNode, type, warpObj)
369
+
370
+ if (shapType) {
371
+ const ext = getTextByPathList(slideXfrmNode, ['a:ext', 'attrs'])
372
+ const cx = parseInt(ext['cx']) * FACTOR
373
+ const cy = parseInt(ext['cy']) * FACTOR
374
+
375
+ const { borderColor, borderWidth, borderType } = getBorder(node, true)
376
+ const fillColor = getShapeFill(node, true)
377
+
378
+ return {
379
+ type: 'shape',
380
+ left,
381
+ top,
382
+ width,
383
+ height,
384
+ cx,
385
+ cy,
386
+ borderColor,
387
+ borderWidth,
388
+ borderType,
389
+ fillColor,
390
+ content,
391
+ isFlipV,
392
+ isFlipH,
393
+ rotate,
394
+ shapType,
395
+ id,
396
+ name,
397
+ idx,
398
+ }
399
+ }
400
+
401
+ const { borderColor, borderWidth, borderType } = getBorder(node, false)
402
+ const fillColor = getShapeFill(node, false)
403
+
404
+ return {
405
+ type: 'text',
406
+ left,
407
+ top,
408
+ width,
409
+ height,
410
+ borderColor,
411
+ borderWidth,
412
+ borderType,
413
+ fillColor,
414
+ isFlipV,
415
+ isFlipH,
416
+ rotate: txtRotate,
417
+ content,
418
+ id,
419
+ name,
420
+ idx,
421
+ }
422
+ }
423
+
424
+ async function processPicNode(node, warpObj) {
425
+ const rid = node['p:blipFill']['a:blip']['attrs']['r:embed']
426
+ const imgName = warpObj['slideResObj'][rid]['target']
427
+ const imgFileExt = extractFileExtension(imgName).toLowerCase()
428
+ const zip = warpObj['zip']
429
+ const imgArrayBuffer = await zip.file(imgName).async('arraybuffer')
430
+ const xfrmNode = node['p:spPr']['a:xfrm']
431
+ let mimeType = ''
432
+
433
+ switch (imgFileExt) {
434
+ case 'jpg':
435
+ case 'jpeg':
436
+ mimeType = 'image/jpeg'
437
+ break
438
+ case 'png':
439
+ mimeType = 'image/png'
440
+ break
441
+ case 'gif':
442
+ mimeType = 'image/gif'
443
+ break
444
+ case 'emf':
445
+ mimeType = 'image/x-emf'
446
+ break
447
+ case 'wmf':
448
+ mimeType = 'image/x-wmf'
449
+ break
450
+ default:
451
+ mimeType = 'image/*'
452
+ }
453
+ const { top, left } = getPosition(xfrmNode, undefined, undefined)
454
+ const { width, height } = getSize(xfrmNode, undefined, undefined)
455
+ const src = `data:${mimeType};base64,${base64ArrayBuffer(imgArrayBuffer)}`
456
+
457
+ let rotate = 0
458
+ const rotateNode = getTextByPathList(node, ['p:spPr', 'a:xfrm', 'attrs', 'rot'])
459
+ if (rotateNode) rotate = angleToDegrees(rotateNode)
460
+
461
+ return {
462
+ type: 'image',
463
+ top,
464
+ left,
465
+ width,
466
+ height,
467
+ src,
468
+ rotate,
469
+ }
470
+ }
471
+
472
+ async function processGraphicFrameNode(node, warpObj) {
473
+ const graphicTypeUri = getTextByPathList(node, ['a:graphic', 'a:graphicData', 'attrs', 'uri'])
474
+
475
+ let result
476
+ switch (graphicTypeUri) {
477
+ case 'http://schemas.openxmlformats.org/drawingml/2006/table':
478
+ result = genTable(node, warpObj)
479
+ break
480
+ case 'http://schemas.openxmlformats.org/drawingml/2006/chart':
481
+ result = await genChart(node, warpObj)
482
+ break
483
+ case 'http://schemas.openxmlformats.org/drawingml/2006/diagram':
484
+ result = genDiagram(node, warpObj)
485
+ break
486
+ default:
487
+ }
488
+ return result
489
+ }
490
+
491
+ function genTextBody(textBodyNode, slideLayoutSpNode, slideMasterSpNode, type, warpObj) {
492
+ if (!textBodyNode) return ''
493
+
494
+ let text = ''
495
+ const slideMasterTextStyles = warpObj['slideMasterTextStyles']
496
+
497
+ if (textBodyNode['a:p'].constructor === Array) {
498
+ for (const pNode of textBodyNode['a:p']) {
499
+ const rNode = pNode['a:r']
500
+ text += `<div class="${getHorizontalAlign(pNode, slideLayoutSpNode, slideMasterSpNode, type, slideMasterTextStyles)}">`
501
+ text += genBuChar(pNode)
502
+ if (!rNode) text += genSpanElement(pNode, slideLayoutSpNode, type, warpObj)
503
+ else if (rNode.constructor === Array) {
504
+ for (const rNodeItem of rNode) text += genSpanElement(rNodeItem, slideLayoutSpNode, type, warpObj)
505
+ }
506
+ else text += genSpanElement(rNode, slideLayoutSpNode, type, warpObj)
507
+ text += '</div>'
508
+ }
509
+ }
510
+ else {
511
+ const pNode = textBodyNode['a:p']
512
+ const rNode = pNode['a:r']
513
+ text += `<div class="${getHorizontalAlign(pNode, slideLayoutSpNode, slideMasterSpNode, type, slideMasterTextStyles)}">`
514
+ text += genBuChar(pNode)
515
+ if (!rNode) text += genSpanElement(pNode, slideLayoutSpNode, type, warpObj)
516
+ else if (rNode.constructor === Array) {
517
+ for (const rNodeItem of rNode) text += genSpanElement(rNodeItem, slideLayoutSpNode, type, warpObj)
518
+ }
519
+ else text += genSpanElement(rNode, slideLayoutSpNode, type, warpObj)
520
+ text += '</div>'
521
+ }
522
+ return text
523
+ }
524
+
525
+ function genBuChar(node) {
526
+ const pPrNode = node['a:pPr']
527
+
528
+ let lvl = parseInt(getTextByPathList(pPrNode, ['attrs', 'lvl']))
529
+ if (isNaN(lvl)) lvl = 0
530
+
531
+ const buChar = getTextByPathList(pPrNode, ['a:buChar', 'attrs', 'char'])
532
+ if (buChar) {
533
+ const buFontAttrs = getTextByPathList(pPrNode, ['a:buFont', 'attrs'])
534
+
535
+ let marginLeft = parseInt(getTextByPathList(pPrNode, ['attrs', 'marL'])) * FACTOR
536
+ if (buFontAttrs) {
537
+ let marginRight = parseInt(buFontAttrs['pitchFamily'])
538
+
539
+ if (isNaN(marginLeft)) marginLeft = 328600 * FACTOR
540
+ if (isNaN(marginRight)) marginRight = 0
541
+
542
+ const typeface = buFontAttrs['typeface']
543
+
544
+ return `<span style="font-family: ${typeface}; margin-left: ${marginLeft * lvl}px; margin-right: ${marginRight}px; font-size: 20pt;">${buChar}</span>`
545
+ }
546
+ marginLeft = 328600 * FACTOR * lvl
547
+ return `<span style="margin-left: ${marginLeft}px;">${buChar}</span>`
548
+ }
549
+ return `<span style="margin-left: ${328600 * FACTOR * lvl}px; margin-right: 0;"></span>`
550
+ }
551
+
552
+ function genSpanElement(node, slideLayoutSpNode, type, warpObj) {
553
+ const slideMasterTextStyles = warpObj['slideMasterTextStyles']
554
+
555
+ let text = node['a:t']
556
+ if (typeof text !== 'string') text = getTextByPathList(node, ['a:fld', 'a:t'])
557
+ if (typeof text !== 'string') text = '&nbsp;'
558
+
559
+ const styleText = `
560
+ color: ${getFontColor(node)};
561
+ font-size: ${getFontSize(node, slideLayoutSpNode, type, slideMasterTextStyles)};
562
+ font-family: ${getFontType(node, type)};
563
+ font-weight: ${getFontBold(node)};
564
+ font-style: ${getFontItalic(node)};
565
+ text-decoration: ${getFontDecoration(node)};
566
+ vertical-align: ${getTextVerticalAlign(node)};
567
+ `
568
+
569
+ const linkID = getTextByPathList(node, ['a:rPr', 'a:hlinkClick', 'attrs', 'r:id'])
570
+ if (linkID) {
571
+ const linkURL = warpObj['slideResObj'][linkID]['target']
572
+ return `<span class="text-block" style="${styleText}"><a href="${linkURL}" target="_blank">${text.replace(/\s/i, '&nbsp;')}</a></span>`
573
+ }
574
+ return `<span class="text-block" style="${styleText}">${text.replace(/\s/i, '&nbsp;')}</span>`
575
+ }
576
+
577
+ function genTable(node, warpObj) {
578
+ const tableNode = getTextByPathList(node, ['a:graphic', 'a:graphicData', 'a:tbl'])
579
+ const xfrmNode = getTextByPathList(node, ['p:xfrm'])
580
+ const { top, left } = getPosition(xfrmNode, undefined, undefined)
581
+ const { width, height } = getSize(xfrmNode, undefined, undefined)
582
+
583
+ const trNodes = tableNode['a:tr']
584
+
585
+ const data = []
586
+ if (trNodes.constructor === Array) {
587
+ for (const trNode of trNodes) {
588
+ const tcNodes = trNode['a:tc']
589
+ const tr = []
590
+
591
+ if (tcNodes.constructor === Array) {
592
+ for (const tcNode of tcNodes) {
593
+ const text = genTextBody(tcNode['a:txBody'], undefined, undefined, undefined, warpObj)
594
+ const rowSpan = getTextByPathList(tcNode, ['attrs', 'rowSpan'])
595
+ const colSpan = getTextByPathList(tcNode, ['attrs', 'gridSpan'])
596
+ const vMerge = getTextByPathList(tcNode, ['attrs', 'vMerge'])
597
+ const hMerge = getTextByPathList(tcNode, ['attrs', 'hMerge'])
598
+
599
+ tr.push({ text, rowSpan, colSpan, vMerge, hMerge })
600
+ }
601
+ }
602
+ else {
603
+ const text = genTextBody(tcNodes['a:txBody'])
604
+ tr.push({ text })
605
+ }
606
+
607
+ data.push(tr)
608
+ }
609
+ }
610
+ else {
611
+ const tcNodes = trNodes['a:tc']
612
+ const tr = []
613
+
614
+ if (tcNodes.constructor === Array) {
615
+ for (const tcNode of tcNodes) {
616
+ const text = genTextBody(tcNode['a:txBody'])
617
+ tr.push({ text })
618
+ }
619
+ }
620
+ else {
621
+ const text = genTextBody(tcNodes['a:txBody'])
622
+ tr.push({ text })
623
+ }
624
+ data.push(tr)
625
+ }
626
+
627
+ return {
628
+ type: 'table',
629
+ top,
630
+ left,
631
+ width,
632
+ height,
633
+ data,
634
+ }
635
+ }
636
+
637
+ async function genChart(node, warpObj) {
638
+ const xfrmNode = getTextByPathList(node, ['p:xfrm'])
639
+ const { top, left } = getPosition(xfrmNode, undefined, undefined)
640
+ const { width, height } = getSize(xfrmNode, undefined, undefined)
641
+
642
+ const rid = node['a:graphic']['a:graphicData']['c:chart']['attrs']['r:id']
643
+ const refName = warpObj['slideResObj'][rid]['target']
644
+ const content = await readXmlFile(warpObj['zip'], refName)
645
+ const plotArea = getTextByPathList(content, ['c:chartSpace', 'c:chart', 'c:plotArea'])
646
+
647
+ let chart = null
648
+ for (const key in plotArea) {
649
+ switch (key) {
650
+ case 'c:lineChart':
651
+ chart = {
652
+ type: 'lineChart',
653
+ data: extractChartData(plotArea[key]['c:ser']),
654
+ }
655
+ break
656
+ case 'c:barChart':
657
+ chart = {
658
+ type: getTextByPathList(plotArea[key], ['c:grouping', 'attrs', 'val']) === 'stacked' ? 'stackedBarChart' : 'barChart',
659
+ data: extractChartData(plotArea[key]['c:ser']),
660
+ }
661
+ break
662
+ case 'c:pieChart':
663
+ chart = {
664
+ type: 'pieChart',
665
+ data: extractChartData(plotArea[key]['c:ser']),
666
+ }
667
+ break
668
+ case 'c:pie3DChart':
669
+ chart = {
670
+ type: 'pie3DChart',
671
+ data: extractChartData(plotArea[key]['c:ser']),
672
+ }
673
+ break
674
+ case 'c:areaChart':
675
+ chart = {
676
+ type: getTextByPathList(plotArea[key], ['c:grouping', 'attrs', 'val']) === 'percentStacked' ? 'stackedAreaChart' : 'areaChart',
677
+ data: extractChartData(plotArea[key]['c:ser']),
678
+ }
679
+ break
680
+ case 'c:scatterChart':
681
+ chart = {
682
+ type: 'scatterChart',
683
+ data: extractChartData(plotArea[key]['c:ser']),
684
+ }
685
+ break
686
+ case 'c:catAx':
687
+ break
688
+ case 'c:valAx':
689
+ break
690
+ default:
691
+ }
692
+ }
693
+
694
+ if (!chart) return {}
695
+ return {
696
+ type: 'chart',
697
+ top,
698
+ left,
699
+ width,
700
+ height,
701
+ data: chart.data,
702
+ chartType: chart.type,
703
+ }
704
+ }
705
+
706
+ function genDiagram(node) {
707
+ const xfrmNode = getTextByPathList(node, ['p:xfrm'])
708
+ const { left, top } = getPosition(xfrmNode, undefined, undefined)
709
+ const { width, height } = getSize(xfrmNode, undefined, undefined)
710
+
711
+ return {
712
+ type: 'diagram',
713
+ left,
714
+ top,
715
+ width,
716
+ height,
717
+ }
718
+ }
719
+
720
+ function getPosition(slideSpNode, slideLayoutSpNode, slideMasterSpNode) {
721
+ let off
722
+
723
+ if (slideSpNode) off = slideSpNode['a:off']['attrs']
724
+ else if (slideLayoutSpNode) off = slideLayoutSpNode['a:off']['attrs']
725
+ else if (slideMasterSpNode) off = slideMasterSpNode['a:off']['attrs']
726
+
727
+ if (!off) return { top: 0, left: 0 }
728
+
729
+ return {
730
+ top: parseInt(off['y']) * FACTOR,
731
+ left: parseInt(off['x']) * FACTOR,
732
+ }
733
+ }
734
+
735
+ function getSize(slideSpNode, slideLayoutSpNode, slideMasterSpNode) {
736
+ let ext
737
+
738
+ if (slideSpNode) ext = slideSpNode['a:ext']['attrs']
739
+ else if (slideLayoutSpNode) ext = slideLayoutSpNode['a:ext']['attrs']
740
+ else if (slideMasterSpNode) ext = slideMasterSpNode['a:ext']['attrs']
741
+
742
+ if (!ext) return { width: 0, height: 0 }
743
+
744
+ return {
745
+ width: parseInt(ext['cx']) * FACTOR,
746
+ height: parseInt(ext['cy']) * FACTOR,
747
+ }
748
+ }
749
+
750
+ function getHorizontalAlign(node, slideLayoutSpNode, slideMasterSpNode, type, slideMasterTextStyles) {
751
+ let algn = getTextByPathList(node, ['a:pPr', 'attrs', 'algn'])
752
+
753
+ if (!algn) algn = getTextByPathList(slideLayoutSpNode, ['p:txBody', 'a:p', 'a:pPr', 'attrs', 'algn'])
754
+ if (!algn) algn = getTextByPathList(slideMasterSpNode, ['p:txBody', 'a:p', 'a:pPr', 'attrs', 'algn'])
755
+ if (!algn) {
756
+ switch (type) {
757
+ case 'title':
758
+ case 'subTitle':
759
+ case 'ctrTitle':
760
+ algn = getTextByPathList(slideMasterTextStyles, ['p:titleStyle', 'a:lvl1pPr', 'attrs', 'alng'])
761
+ break
762
+ default:
763
+ algn = getTextByPathList(slideMasterTextStyles, ['p:otherStyle', 'a:lvl1pPr', 'attrs', 'alng'])
764
+ }
765
+ }
766
+ if (!algn) {
767
+ if (type === 'title' || type === 'subTitle' || type === 'ctrTitle') return 'h-mid'
768
+ else if (type === 'sldNum') return 'h-right'
769
+ }
770
+ return algn === 'ctr' ? 'h-mid' : algn === 'r' ? 'h-right' : 'h-left'
771
+ }
772
+
773
+ function getFontType(node, type) {
774
+ let typeface = getTextByPathList(node, ['a:rPr', 'a:latin', 'attrs', 'typeface'])
775
+
776
+ if (!typeface) {
777
+ const fontSchemeNode = getTextByPathList(themeContent, ['a:theme', 'a:themeElements', 'a:fontScheme'])
778
+
779
+ if (type === 'title' || type === 'subTitle' || type === 'ctrTitle') {
780
+ typeface = getTextByPathList(fontSchemeNode, ['a:majorFont', 'a:latin', 'attrs', 'typeface'])
781
+ }
782
+ else if (type === 'body') {
783
+ typeface = getTextByPathList(fontSchemeNode, ['a:minorFont', 'a:latin', 'attrs', 'typeface'])
784
+ }
785
+ else {
786
+ typeface = getTextByPathList(fontSchemeNode, ['a:minorFont', 'a:latin', 'attrs', 'typeface'])
787
+ }
788
+ }
789
+
790
+ return typeface || 'inherit'
791
+ }
792
+
793
+ function getFontColor(node) {
794
+ const color = getTextByPathList(node, ['a:rPr', 'a:solidFill', 'a:srgbClr', 'attrs', 'val'])
795
+ return color ? `#${color}` : '#000'
796
+ }
797
+
798
+ function getFontSize(node, slideLayoutSpNode, type, slideMasterTextStyles) {
799
+ let fontSize
800
+
801
+ if (node['a:rPr']) fontSize = parseInt(node['a:rPr']['attrs']['sz']) / 100
802
+
803
+ if ((isNaN(fontSize) || !fontSize)) {
804
+ const sz = getTextByPathList(slideLayoutSpNode, ['p:txBody', 'a:lstStyle', 'a:lvl1pPr', 'a:defRPr', 'attrs', 'sz'])
805
+ fontSize = parseInt(sz) / 100
806
+ }
807
+
808
+ if (isNaN(fontSize) || !fontSize) {
809
+ let sz
810
+ if (type === 'title' || type === 'subTitle' || type === 'ctrTitle') {
811
+ sz = getTextByPathList(slideMasterTextStyles, ['p:titleStyle', 'a:lvl1pPr', 'a:defRPr', 'attrs', 'sz'])
812
+ }
813
+ else if (type === 'body') {
814
+ sz = getTextByPathList(slideMasterTextStyles, ['p:bodyStyle', 'a:lvl1pPr', 'a:defRPr', 'attrs', 'sz'])
815
+ }
816
+ else if (type === 'dt' || type === 'sldNum') {
817
+ sz = '1200'
818
+ }
819
+ else if (!type) {
820
+ sz = getTextByPathList(slideMasterTextStyles, ['p:otherStyle', 'a:lvl1pPr', 'a:defRPr', 'attrs', 'sz'])
821
+ }
822
+ if (sz) fontSize = parseInt(sz) / 100
823
+ }
824
+
825
+ const baseline = getTextByPathList(node, ['a:rPr', 'attrs', 'baseline'])
826
+ if (baseline && !isNaN(fontSize)) fontSize -= 10
827
+
828
+ return (isNaN(fontSize) || !fontSize) ? 'inherit' : (fontSize + 'pt')
829
+ }
830
+
831
+ function getFontBold(node) {
832
+ return (node['a:rPr'] && node['a:rPr']['attrs']['b'] === '1') ? 'bold' : 'initial'
833
+ }
834
+
835
+ function getFontItalic(node) {
836
+ return (node['a:rPr'] && node['a:rPr']['attrs']['i'] === '1') ? 'italic' : 'normal'
837
+ }
838
+
839
+ function getFontDecoration(node) {
840
+ return (node['a:rPr'] && node['a:rPr']['attrs']['u'] === 'sng') ? 'underline' : 'initial'
841
+ }
842
+
843
+ function getTextVerticalAlign(node) {
844
+ const baseline = getTextByPathList(node, ['a:rPr', 'attrs', 'baseline'])
845
+ return baseline ? (parseInt(baseline) / 1000) + '%' : 'baseline'
846
+ }
847
+
848
+ function getBorder(node, isSvgMode) {
849
+ const lineNode = node['p:spPr']['a:ln']
850
+
851
+ let borderWidth = parseInt(getTextByPathList(lineNode, ['attrs', 'w'])) / 12700
852
+ if (isNaN(borderWidth)) borderWidth = 0
853
+
854
+ let borderColor = getTextByPathList(lineNode, ['a:solidFill', 'a:srgbClr', 'attrs', 'val'])
855
+ if (!borderColor) {
856
+ const schemeClrNode = getTextByPathList(lineNode, ['a:solidFill', 'a:schemeClr'])
857
+ const schemeClr = 'a:' + getTextByPathList(schemeClrNode, ['attrs', 'val'])
858
+ borderColor = getSchemeColorFromTheme(schemeClr)
859
+ }
860
+
861
+ if (!borderColor) {
862
+ const schemeClrNode = getTextByPathList(node, ['p:style', 'a:lnRef', 'a:schemeClr'])
863
+ const schemeClr = 'a:' + getTextByPathList(schemeClrNode, ['attrs', 'val'])
864
+ borderColor = getSchemeColorFromTheme(schemeClr)
865
+
866
+ if (borderColor) {
867
+ let shade = getTextByPathList(schemeClrNode, ['a:shade', 'attrs', 'val'])
868
+
869
+ if (shade) {
870
+ shade = parseInt(shade) / 100000
871
+
872
+ const color = tinycolor('#' + borderColor).toHsl()
873
+ borderColor = tinycolor({ h: color.h, s: color.s, l: color.l * shade, a: color.a }).toHex()
874
+ }
875
+ }
876
+ }
877
+
878
+ if (!borderColor) {
879
+ if (isSvgMode) borderColor = 'none'
880
+ else borderColor = '#000'
881
+ }
882
+ else {
883
+ borderColor = `#${borderColor}`
884
+ }
885
+
886
+ const type = getTextByPathList(lineNode, ['a:prstDash', 'attrs', 'val'])
887
+ let borderType = 'solid'
888
+ let strokeDasharray = '0'
889
+ switch (type) {
890
+ case 'solid':
891
+ borderType = 'solid'
892
+ strokeDasharray = '0'
893
+ break
894
+ case 'dash':
895
+ borderType = 'dashed'
896
+ strokeDasharray = '5'
897
+ break
898
+ case 'dashDot':
899
+ borderType = 'dashed'
900
+ strokeDasharray = '5, 5, 1, 5'
901
+ break
902
+ case 'dot':
903
+ borderType = 'dotted'
904
+ strokeDasharray = '1, 5'
905
+ break
906
+ case 'lgDash':
907
+ borderType = 'dashed'
908
+ strokeDasharray = '10, 5'
909
+ break
910
+ case 'lgDashDotDot':
911
+ borderType = 'dashed'
912
+ strokeDasharray = '10, 5, 1, 5, 1, 5'
913
+ break
914
+ case 'sysDash':
915
+ borderType = 'dashed'
916
+ strokeDasharray = '5, 2'
917
+ break
918
+ case 'sysDashDot':
919
+ borderType = 'dashed'
920
+ strokeDasharray = '5, 2, 1, 5'
921
+ break
922
+ case 'sysDashDotDot':
923
+ borderType = 'dashed'
924
+ strokeDasharray = '5, 2, 1, 5, 1, 5'
925
+ break
926
+ case 'sysDot':
927
+ borderType = 'dotted'
928
+ strokeDasharray = '2, 5'
929
+ break
930
+ default:
931
+ }
932
+
933
+ return {
934
+ borderColor,
935
+ borderWidth,
936
+ borderType,
937
+ strokeDasharray,
938
+ }
939
+ }
940
+
941
+ function getSlideBackgroundFill(slideContent, slideLayoutContent, slideMasterContent) {
942
+ let bgColor = getSolidFill(getTextByPathList(slideContent, ['p:sld', 'p:cSld', 'p:bg', 'p:bgPr', 'a:solidFill']))
943
+ if (!bgColor) bgColor = getSolidFill(getTextByPathList(slideLayoutContent, ['p:sldLayout', 'p:cSld', 'p:bg', 'p:bgPr', 'a:solidFill']))
944
+ if (!bgColor) bgColor = getSolidFill(getTextByPathList(slideMasterContent, ['p:sldMaster', 'p:cSld', 'p:bg', 'p:bgPr', 'a:solidFill']))
945
+ if (!bgColor) bgColor = 'fff'
946
+ return bgColor
947
+ }
948
+
949
+ function getShapeFill(node, isSvgMode) {
950
+ if (getTextByPathList(node, ['p:spPr', 'a:noFill'])) {
951
+ return isSvgMode ? 'none' : 'background-color: initial;'
952
+ }
953
+
954
+ let fillColor
955
+ if (!fillColor) {
956
+ fillColor = getTextByPathList(node, ['p:spPr', 'a:solidFill', 'a:srgbClr', 'attrs', 'val'])
957
+ }
958
+
959
+ if (!fillColor) {
960
+ const schemeClr = 'a:' + getTextByPathList(node, ['p:spPr', 'a:solidFill', 'a:schemeClr', 'attrs', 'val'])
961
+ fillColor = getSchemeColorFromTheme(schemeClr)
962
+ }
963
+
964
+ if (!fillColor) {
965
+ const schemeClr = 'a:' + getTextByPathList(node, ['p:style', 'a:fillRef', 'a:schemeClr', 'attrs', 'val'])
966
+ fillColor = getSchemeColorFromTheme(schemeClr)
967
+ }
968
+
969
+ if (fillColor) {
970
+ fillColor = `#${fillColor}`
971
+
972
+ let lumMod = parseInt(getTextByPathList(node, ['p:spPr', 'a:solidFill', 'a:schemeClr', 'a:lumMod', 'attrs', 'val'])) / 100000
973
+ let lumOff = parseInt(getTextByPathList(node, ['p:spPr', 'a:solidFill', 'a:schemeClr', 'a:lumOff', 'attrs', 'val'])) / 100000
974
+ if (isNaN(lumMod)) lumMod = 1.0
975
+ if (isNaN(lumOff)) lumOff = 0
976
+
977
+ const color = tinycolor(fillColor).toHsl()
978
+ const lum = color.l * (1 + lumOff)
979
+ return tinycolor({ h: color.h, s: color.s, l: lum, a: color.a }).toHexString()
980
+ }
981
+
982
+ if (isSvgMode) return 'none'
983
+ return fillColor
984
+ }
985
+
986
+ function getSolidFill(solidFill) {
987
+ if (!solidFill) return solidFill
988
+
989
+ let color = 'fff'
990
+
991
+ if (solidFill['a:srgbClr']) {
992
+ color = getTextByPathList(solidFill['a:srgbClr'], ['attrs', 'val'])
993
+ }
994
+ else if (solidFill['a:schemeClr']) {
995
+ const schemeClr = 'a:' + getTextByPathList(solidFill['a:schemeClr'], ['attrs', 'val'])
996
+ color = getSchemeColorFromTheme(schemeClr)
997
+ }
998
+
999
+ return color
1000
+ }
1001
+
1002
+ function getSchemeColorFromTheme(schemeClr) {
1003
+ switch (schemeClr) {
1004
+ case 'a:tx1':
1005
+ schemeClr = 'a:dk1'
1006
+ break
1007
+ case 'a:tx2':
1008
+ schemeClr = 'a:dk2'
1009
+ break
1010
+ case 'a:bg1':
1011
+ schemeClr = 'a:lt1'
1012
+ break
1013
+ case 'a:bg2':
1014
+ schemeClr = 'a:lt2'
1015
+ break
1016
+ default:
1017
+ break
1018
+ }
1019
+ const refNode = getTextByPathList(themeContent, ['a:theme', 'a:themeElements', 'a:clrScheme', schemeClr])
1020
+ let color = getTextByPathList(refNode, ['a:srgbClr', 'attrs', 'val'])
1021
+ if (!color) color = getTextByPathList(refNode, ['a:sysClr', 'attrs', 'lastClr'])
1022
+ return color
1023
+ }
1024
+
1025
+ function extractChartData(serNode) {
1026
+ const dataMat = []
1027
+ if (!serNode) return dataMat
1028
+
1029
+ if (serNode['c:xVal']) {
1030
+ let dataRow = []
1031
+ eachElement(serNode['c:xVal']['c:numRef']['c:numCache']['c:pt'], innerNode => {
1032
+ dataRow.push(parseFloat(innerNode['c:v']))
1033
+ return ''
1034
+ })
1035
+ dataMat.push(dataRow)
1036
+ dataRow = []
1037
+ eachElement(serNode['c:yVal']['c:numRef']['c:numCache']['c:pt'], innerNode => {
1038
+ dataRow.push(parseFloat(innerNode['c:v']))
1039
+ return ''
1040
+ })
1041
+ dataMat.push(dataRow)
1042
+ }
1043
+ else {
1044
+ eachElement(serNode, (innerNode, index) => {
1045
+ const dataRow = []
1046
+ const colName = getTextByPathList(innerNode, ['c:tx', 'c:strRef', 'c:strCache', 'c:pt', 'c:v']) || index
1047
+
1048
+ const rowNames = {}
1049
+ if (getTextByPathList(innerNode, ['c:cat', 'c:strRef', 'c:strCache', 'c:pt'])) {
1050
+ eachElement(innerNode['c:cat']['c:strRef']['c:strCache']['c:pt'], innerNode => {
1051
+ rowNames[innerNode['attrs']['idx']] = innerNode['c:v']
1052
+ return ''
1053
+ })
1054
+ }
1055
+ else if (getTextByPathList(innerNode, ['c:cat', 'c:numRef', 'c:numCache', 'c:pt'])) {
1056
+ eachElement(innerNode['c:cat']['c:numRef']['c:numCache']['c:pt'], innerNode => {
1057
+ rowNames[innerNode['attrs']['idx']] = innerNode['c:v']
1058
+ return ''
1059
+ })
1060
+ }
1061
+
1062
+ if (getTextByPathList(innerNode, ['c:val', 'c:numRef', 'c:numCache', 'c:pt'])) {
1063
+ eachElement(innerNode['c:val']['c:numRef']['c:numCache']['c:pt'], innerNode => {
1064
+ dataRow.push({
1065
+ x: innerNode['attrs']['idx'],
1066
+ y: parseFloat(innerNode['c:v']),
1067
+ })
1068
+ return ''
1069
+ })
1070
+ }
1071
+
1072
+ dataMat.push({
1073
+ key: colName,
1074
+ values: dataRow,
1075
+ xlabels: rowNames,
1076
+ })
1077
+ return ''
1078
+ })
1079
+ }
1080
+
1081
+ return dataMat
1082
+ }