pptxtojson 1.5.1 → 1.6.0

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/favicon.ico CHANGED
Binary file
package/index.html CHANGED
@@ -5,52 +5,248 @@
5
5
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1">
7
7
  <meta name="description" content="Office PowerPoint(.pptx) file to JSON | 将 PPTX 文件转为可读的 JSON 数据" />
8
- <meta name="keywords" content="pptx2json,pptxtojson,ppt,powerpoint,json,javascript,PPT解析,PPT转JSON" />
8
+ <meta name="keywords" content="pptx2json,pptxtojson,ppt,powerpoint,json,PPT解析,PPT转JSON" />
9
9
  <link rel="icon" href="favicon.ico">
10
10
  <title>pptxtojson - PPTX转JSON</title>
11
11
 
12
12
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jsoneditor@9.9.2/dist/jsoneditor.min.css">
13
+ <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css">
13
14
  <script src="https://cdn.jsdelivr.net/npm/jsoneditor@9.9.2/dist/jsoneditor.min.js"></script>
15
+ <script src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
14
16
  <script src="dist/index.umd.js"></script>
15
17
 
16
- <style>
17
- * {
18
- margin: 0;
19
- padding: 0;
20
- box-sizing: border-box;
21
- }
22
- html, body {
23
- height: 100%;
24
- }
25
- ::-webkit-scrollbar {
26
- width: 5px;
27
- height: 5px;
28
- background-color: #fff;
29
- }
30
- ::-webkit-scrollbar-thumb {
31
- background-color: #c1c1c1;
32
- }
18
+ <style>
19
+ :root {
20
+ --primary-color: #33bcfc;
21
+ --secondary-color: #d897fd;
22
+ }
33
23
 
34
- body {
35
- display: flex;
36
- }
37
- .main {
38
- width: 40%;
39
- min-width: 600px;
40
- height: 100%;
41
- margin-right: 10px;
42
- display: flex;
43
- flex-direction: column;
44
- justify-content: center;
45
- align-items: center;
46
- }
47
- #jsoneditor {
48
- flex: 1;
49
- height: 100%;
50
- padding: 10px;
51
- }
24
+ * {
25
+ margin: 0;
26
+ padding: 0;
27
+ box-sizing: border-box;
28
+ font-family: 'Segoe UI', 'Roboto', sans-serif;
29
+ }
30
+
31
+ body {
32
+ color: #1e293b;
33
+ min-height: 100vh;
34
+ display: flex;
35
+ flex-direction: column;
36
+ background-color: #f3f4f6;
37
+ }
38
+
39
+ .container {
40
+ display: flex;
41
+ flex: 1;
42
+ padding: 32px;
43
+ padding-bottom: 0;
44
+ }
45
+
46
+ .left-panel {
47
+ flex: 1;
48
+ display: flex;
49
+ flex-direction: column;
50
+ }
51
+
52
+ .header {
53
+ margin-bottom: 16px;
54
+ }
55
+
56
+ .logo {
57
+ font-size: 32px;
58
+ font-weight: 700;
59
+ color: #1e293b;
60
+ margin-bottom: 8px;
61
+ display: flex;
62
+ align-items: center;
63
+ }
64
+
65
+ .logo span {
66
+ background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
67
+ -webkit-background-clip: text;
68
+ background-clip: text;
69
+ color: transparent;
70
+ }
71
+
72
+ .tagline {
73
+ font-size: 14px;
74
+ color: #64748b;
75
+ }
76
+
77
+ .upload-section {
78
+ background-color: #fff;
79
+ border-radius: 12px;
80
+ padding: 32px;
81
+ margin-bottom: 32px;
82
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.03);
83
+ border: 1px dashed #d1d5db;
84
+ text-align: center;
85
+ transition: all 0.3s ease;
86
+ }
87
+
88
+ .upload-section.highlight {
89
+ border-color: var(--primary-color);
90
+ background-color: #f0f8ff;
91
+ }
92
+
93
+ .upload-section:hover {
94
+ border-color: var(--primary-color);
95
+ }
96
+
97
+ .upload-icon {
98
+ font-size: 48px;
99
+ margin-bottom: 16px;
100
+ }
101
+
102
+ .upload-title {
103
+ font-size: 20px;
104
+ font-weight: 700;
105
+ margin-bottom: 8px;
106
+ }
107
+
108
+ .upload-desc {
109
+ font-size: 13px;
110
+ color: #64748b;
111
+ margin-bottom: 24px;
112
+ }
113
+
114
+ .upload-btn {
115
+ background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
116
+ color: #fff;
117
+ border: none;
118
+ padding: 12px 32px;
119
+ border-radius: 50px;
120
+ font-weight: 700;
121
+ cursor: pointer;
122
+ transition: all 0.3s ease;
123
+ box-shadow: 0 4px 15px rgba(58, 134, 255, 0.2);
124
+ }
125
+
126
+ .upload-btn:hover {
127
+ box-shadow: 0 6px 20px rgba(58, 134, 255, 0.3);
128
+ }
129
+
130
+ .features {
131
+ background-color: #fff;
132
+ border-radius: 12px;
133
+ padding: 24px;
134
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.03);
135
+ }
136
+
137
+ .features + .features {
138
+ margin-top: 32px;
139
+ }
140
+
141
+ .features-title {
142
+ font-size: 20px;
143
+ font-weight: 700;
144
+ margin-bottom: 20px;
145
+ position: relative;
146
+ padding-bottom: 8px;
147
+ }
148
+
149
+ .features-title::after {
150
+ content: '';
151
+ position: absolute;
152
+ bottom: 0;
153
+ left: 0;
154
+ width: 40px;
155
+ height: 3px;
156
+ background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
157
+ border-radius: 3px;
158
+ }
159
+
160
+ .feature-item {
161
+ display: flex;
162
+ align-items: center;
163
+ }
164
+
165
+ .feature-item.link {
166
+ cursor: pointer;
167
+ }
168
+
169
+ .feature-item.link:hover .feature-text {
170
+ color: var(--primary-color);
171
+ }
172
+
173
+ .feature-item + .feature-item {
174
+ margin-top: 16px;
175
+ }
176
+
177
+ .feature-icon {
178
+ min-width: 24px;
179
+ color: var(--primary-color);
180
+ margin-right: 12px;
181
+ font-size: 20px;
182
+ }
183
+
184
+ .feature-text {
185
+ font-size: 15px;
186
+ color: #334155;
187
+ transition: all 0.3s ease;
188
+ }
189
+
190
+ a {
191
+ color: unset;
192
+ text-decoration: none;
193
+ }
194
+
195
+ .footer {
196
+ font-size: 14px;
197
+ color: #64748b;
198
+ text-align: center;
199
+ padding: 24px;
200
+ }
201
+
202
+ .right-panel {
203
+ flex: 1;
204
+ display: flex;
205
+ flex-direction: column;
206
+ margin-left: 32px;
207
+ }
208
+
209
+ .result-header {
210
+ display: flex;
211
+ justify-content: space-between;
212
+ align-items: center;
213
+ margin-bottom: 16px;
214
+ }
215
+
216
+ .result-title {
217
+ font-size: 18px;
218
+ font-weight: 700;
219
+ }
220
+
221
+ .result-actions button {
222
+ background-color: transparent;
223
+ border: 1px solid #e2e8f0;
224
+ padding: 4px 16px;
225
+ border-radius: 4px;
226
+ margin-left: 8px;
227
+ color: #475569;
228
+ cursor: pointer;
229
+ transition: all 0.3s ease;
230
+ }
231
+
232
+ .result-actions button:hover {
233
+ background-color: #f8fafc;
234
+ color: var(--primary-color);
235
+ border-color: var(--primary-color);
236
+ }
237
+
238
+ .json-preview {
239
+ flex: 1;
240
+ background-color: #fff;
241
+ border-radius: 12px;
242
+ padding: 24px;
243
+ overflow: auto;
244
+ font-family: 'Courier New', monospace;
245
+ position: relative;
246
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.03);
247
+ }
52
248
  .jsoneditor-mode-view {
53
- border: 2px solid #ddd;
249
+ border: none;
54
250
  }
55
251
  .jsoneditor-menu {
56
252
  display: none;
@@ -63,70 +259,218 @@
63
259
  padding-top: 0 !important;
64
260
  }
65
261
 
66
- .input-wrap {
67
- width: 300px;
68
- height: 80px;
69
- background-color: #d14424;
70
- color: #fff;
71
- border-radius: 2px;
72
- line-height: 80px;
73
- font-size: 18px;
74
- font-weight: 700;
75
- text-align: center;
76
- cursor: pointer;
77
- user-select: none;
78
- margin-bottom: 20px;
79
- }
80
- .upload-input {
81
- display: none;
82
- }
83
- .link {
84
- display: flex;
85
- }
86
- .link a {
87
- padding: 5px 10px;
88
- color: #d14424;
89
- }
90
- </style>
262
+ /* 响应式调整 */
263
+ @media screen and (max-width: 768px) {
264
+ .container {
265
+ flex-direction: column;
266
+ }
267
+
268
+ .left-panel,
269
+ .right-panel {
270
+ width: 100%;
271
+ flex: none;
272
+ }
273
+
274
+ .right-panel {
275
+ min-height: 50vh;
276
+ margin-left: 0;
277
+ margin-top: 32px;
278
+ }
279
+ }
280
+ </style>
91
281
  </head>
92
282
 
93
283
  <body>
94
- <div class="main">
95
- <div class="input-wrap">
96
- 点击上传 .pptx 文件
97
- <input class="upload-input" type="file" accept="application/vnd.openxmlformats-officedocument.presentationml.presentation"/>
98
- </div>
99
-
100
- <div class="link">
101
- <a target="_blank" href="https://github.com/pipipi-pikachu/pptx2json">Github 仓库</a>
102
- <a target="_blank" href="https://pipipi-pikachu.github.io/PPTist/">在 PPTist 中测试</a>
103
- </div>
104
- </div>
105
- <div id="jsoneditor"></div>
284
+ <div class="container">
285
+ <!-- 左侧面板 -->
286
+ <div class="left-panel">
287
+ <div class="header">
288
+ <div class="logo">PPTX<span>TO</span>JSON</div>
289
+ <p class="tagline">将 PPTX 文件转为可读的 JSON 数据</p>
290
+ </div>
291
+
292
+ <div class="upload-section">
293
+ <div class="upload-icon">📤</div>
294
+ <h3 class="upload-title">上传PPTX文件</h3>
295
+ <p class="upload-desc">「点击选择文件」或「将文件拖放到区域内」上传</p>
296
+ <button class="upload-btn">选择文件</button>
297
+ </div>
298
+
299
+ <div class="features">
300
+ <h3 class="features-title">功能介绍</h3>
301
+ <div class="feature-item">
302
+ <span class="feature-icon">✓</span>
303
+ <p class="feature-text">解析PPTX数据,包括幻灯片主题、尺寸、背景、备注、母版、切页动画、页内元素及布局信息等</p>
304
+ </div>
305
+ <div class="feature-item">
306
+ <span class="feature-icon">✓</span>
307
+ <p class="feature-text">支持页内元素包括:文字(HTML富文本)、图片、形状、线条、表格、图表、音视频、公式等复杂元素</p>
308
+ </div>
309
+ <div class="feature-item">
310
+ <span class="feature-icon">✓</span>
311
+ <p class="feature-text">易读易写的结构化 JSON 数据格式</p>
312
+ </div>
313
+ <div class="feature-item">
314
+ <span class="feature-icon">✓</span>
315
+ <p class="feature-text">不依赖任何服务器端环境,直接在浏览器端运行</p>
316
+ </div>
317
+ </div>
318
+
319
+ <div class="features">
320
+ <h3 class="features-title">相关链接</h3>
321
+ <div class="feature-item link">
322
+ <span class="feature-icon">📦</span>
323
+ <p class="feature-text"><a href="https://github.com/pipipi-pikachu/pptx2json" target="_blank">GitHub 仓库</a></p>
324
+ </div>
325
+ <div class="feature-item link">
326
+ <span class="feature-icon">🧩</span>
327
+ <p class="feature-text"><a href="https://pipipi-pikachu.github.io/PPTist/" target="_blank">在 PPTist 中测试</a></p>
328
+ </div>
329
+ </div>
330
+ </div>
331
+
332
+ <!-- 右侧面板 -->
333
+ <div class="right-panel">
334
+ <div class="result-header">
335
+ <h2 class="result-title">解析结果</h2>
336
+ <div class="result-actions">
337
+ <button class="copy-btn">复制</button>
338
+ <button class="download-btn">下载</button>
339
+ </div>
340
+ </div>
341
+
342
+ <div class="json-preview">
343
+
344
+ </div>
345
+ </div>
346
+ </div>
347
+
348
+ <div class="footer">
349
+ <p>MIT License | Copyright © 2020-PRESENT <a href="https://github.com/pipipi-pikachu" target="_blank">pipipi-pikachu</a></p>
350
+ </div>
106
351
 
107
352
  <script>
108
- const container = document.querySelector('#jsoneditor')
109
- const inputWrapRef = document.querySelector('.input-wrap')
110
- const inputRef = document.querySelector('.upload-input')
353
+ document.ondragleave = e => e.preventDefault()
354
+ document.ondrop = e => e.preventDefault()
355
+ document.ondragenter = e => e.preventDefault()
356
+ document.ondragover = e => e.preventDefault()
111
357
 
112
- const editor = new JSONEditor(container, { mode: 'view' }, {})
358
+ const jsonPreviewRef = document.querySelector('.json-preview')
359
+ const uploadBtnRef = document.querySelector('.upload-btn')
360
+ const uploadSectionRef = document.querySelector('.upload-section')
361
+ const copyBtnRef = document.querySelector('.copy-btn')
362
+ const downloadBtnRef = document.querySelector('.download-btn')
113
363
 
114
- inputRef.addEventListener('change', evt => {
115
- const fileName = evt.target.files[0]
116
-
117
- const reader = new FileReader()
118
- reader.onload = async e => {
119
- const json = await pptxtojson.parse(e.target.result)
120
- editor.set(json)
121
- console.log(json)
364
+ let jsonString = ''
365
+
366
+ const editor = new JSONEditor(jsonPreviewRef, { mode: 'view' }, {})
367
+
368
+ uploadBtnRef.addEventListener('click', () => {
369
+ const input = document.createElement('input')
370
+ input.type = 'file'
371
+ input.accept = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
372
+ input.click()
373
+
374
+ input.onchange = e => {
375
+ const files = e.target.files
376
+ if (files && files[0]) {
377
+ const reader = new FileReader()
378
+ reader.onload = async e => {
379
+ const json = await pptxtojson.parse(e.target.result)
380
+ editor.set(json)
381
+ console.log(json)
382
+ jsonString = JSON.stringify(json, null, 2)
383
+ }
384
+ reader.readAsArrayBuffer(files[0])
385
+ }
386
+ }
387
+ })
388
+
389
+ uploadSectionRef.addEventListener('drop', async e => {
390
+ uploadSectionRef.classList.remove('highlight')
391
+
392
+ const file = e.dataTransfer.files[0]
393
+
394
+ if (!file) return
395
+ if (file.type === 'application/vnd.openxmlformats-officedocument.presentationml.presentation') {
396
+ const json = await pptxtojson.parse(file)
397
+ editor.set(json)
398
+ console.log(json)
399
+ jsonString = JSON.stringify(json, null, 2)
400
+ }
401
+ })
402
+
403
+ uploadSectionRef.addEventListener('dragenter', e => {
404
+ e.preventDefault()
405
+ uploadSectionRef.classList.add('highlight')
406
+ })
407
+
408
+ uploadSectionRef.addEventListener('dragleave', e => {
409
+ e.preventDefault()
410
+ if (!uploadSectionRef.contains(e.relatedTarget)) {
411
+ uploadSectionRef.classList.remove('highlight')
412
+ }
413
+ })
414
+
415
+ copyBtnRef.addEventListener('click', () => {
416
+ if (!jsonString) {
417
+ Toastify({
418
+ text: '请先上传文件',
419
+ duration: 1000,
420
+ gravity: 'top',
421
+ position: 'center',
422
+ style: {
423
+ background: 'linear-gradient(to right, #33bcfc, #d897fd)',
424
+ padding: '10px 20px',
425
+ fontSize: '14px',
426
+ },
427
+ }).showToast()
428
+ return
429
+ }
430
+
431
+ navigator.clipboard.writeText(jsonString)
432
+ Toastify({
433
+ text: '已复制到剪贴板',
434
+ duration: 1000,
435
+ gravity: 'top',
436
+ position: 'center',
437
+ style: {
438
+ background: 'linear-gradient(to right, #33bcfc, #d897fd)',
439
+ padding: '10px 20px',
440
+ fontSize: '14px',
441
+ },
442
+ }).showToast()
443
+ })
444
+
445
+ downloadBtnRef.addEventListener('click', () => {
446
+ if (!jsonString) {
447
+ Toastify({
448
+ text: '请先上传文件',
449
+ duration: 1000,
450
+ gravity: 'top',
451
+ position: 'center',
452
+ style: {
453
+ background: 'linear-gradient(to right, #33bcfc, #d897fd)',
454
+ padding: '10px 20px',
455
+ fontSize: '14px',
456
+ },
457
+ }).showToast()
458
+ return
122
459
  }
123
- reader.readAsArrayBuffer(fileName)
124
- })
125
460
 
126
- inputWrapRef.addEventListener('click', () => {
127
- inputRef.value = ''
128
- inputRef.click()
129
- })
461
+ const blob = new Blob([jsonString], { type: 'application/json' })
462
+ const url = URL.createObjectURL(blob)
463
+
464
+ const a = document.createElement('a')
465
+ a.href = url
466
+ a.download = 'slides.json'
467
+
468
+ document.body.appendChild(a)
469
+ a.click()
470
+
471
+ document.body.removeChild(a)
472
+ URL.revokeObjectURL(url)
473
+ })
130
474
  </script>
131
475
  </body>
132
- </html>
476
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pptxtojson",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "description": "A javascript tool for parsing .pptx file",
5
5
  "type": "module",
6
6
  "main": "./dist/index.umd.js",
@@ -0,0 +1,81 @@
1
+ import { getTextByPathList } from './utils'
2
+
3
+ export function findTransitionNode(content, rootElement) {
4
+ if (!content || !rootElement) return null
5
+
6
+ const path1 = [rootElement, 'p:transition']
7
+ let transitionNode = getTextByPathList(content, path1)
8
+ if (transitionNode) return transitionNode
9
+
10
+ const path2 = [rootElement, 'mc:AlternateContent', 'mc:Choice', 'p:transition']
11
+ transitionNode = getTextByPathList(content, path2)
12
+ if (transitionNode) return transitionNode
13
+
14
+ const path3 = [rootElement, 'mc:AlternateContent', 'mc:Fallback', 'p:transition']
15
+ transitionNode = getTextByPathList(content, path3)
16
+
17
+ return transitionNode
18
+ }
19
+
20
+ export function parseTransition(transitionNode) {
21
+ if (!transitionNode) return null
22
+
23
+ const transition = {
24
+ type: 'none',
25
+ duration: 1000,
26
+ direction: null,
27
+ }
28
+
29
+ const attrs = transitionNode.attrs || {}
30
+
31
+ let durationFound = false
32
+ const durRegex = /^p\d{2}:dur$/
33
+ for (const key in attrs) {
34
+ if (durRegex.test(key) && !isNaN(parseInt(attrs[key], 10))) {
35
+ transition.duration = parseInt(attrs[key], 10)
36
+ durationFound = true
37
+ break
38
+ }
39
+ }
40
+
41
+ if (!durationFound && attrs.spd) {
42
+ switch (attrs.spd) {
43
+ case 'slow':
44
+ transition.duration = 1000
45
+ break
46
+ case 'med':
47
+ transition.duration = 800
48
+ break
49
+ case 'fast':
50
+ transition.duration = 500
51
+ break
52
+ default:
53
+ transition.duration = 1000
54
+ break
55
+ }
56
+ }
57
+
58
+ if (attrs.advClick === '0' && attrs.advTm) {
59
+ transition.autoNextAfter = parseInt(attrs.advTm, 10)
60
+ }
61
+
62
+ const effectRegex = /^(p|p\d{2}):/
63
+ for (const key in transitionNode) {
64
+ if (key !== 'attrs' && effectRegex.test(key)) {
65
+ const effectNode = transitionNode[key]
66
+ transition.type = key.substring(key.indexOf(':') + 1)
67
+
68
+ if (effectNode && effectNode.attrs) {
69
+ const effectAttrs = effectNode.attrs
70
+
71
+ if (effectAttrs.dur && !isNaN(parseInt(effectAttrs.dur, 10))) {
72
+ if (!durationFound) transition.duration = parseInt(effectAttrs.dur, 10)
73
+ }
74
+ if (effectAttrs.dir) transition.direction = effectAttrs.dir
75
+ }
76
+ break
77
+ }
78
+ }
79
+
80
+ return transition
81
+ }
package/src/pptxtojson.js CHANGED
@@ -13,6 +13,7 @@ import { getTableBorders, getTableCellParams, getTableRowParams } from './table'
13
13
  import { RATIO_EMUs_Points } from './constants'
14
14
  import { findOMath, latexFormart, parseOMath } from './math'
15
15
  import { getShapePath } from './shapePath'
16
+ import { parseTransition, findTransitionNode } from './animation'
16
17
 
17
18
  export async function parse(file) {
18
19
  const slides = []
@@ -274,11 +275,18 @@ async function processSingleSlide(zip, sldFileName, themeContent, defaultTextSty
274
275
  }
275
276
  }
276
277
 
278
+ let transitionNode = findTransitionNode(slideContent, 'p:sld')
279
+ if (!transitionNode) transitionNode = findTransitionNode(slideLayoutContent, 'p:sldLayout')
280
+ if (!transitionNode) transitionNode = findTransitionNode(slideMasterContent, 'p:sldMaster')
281
+
282
+ const transition = parseTransition(transitionNode)
283
+
277
284
  return {
278
285
  fill,
279
286
  elements,
280
287
  layoutElements,
281
288
  note,
289
+ transition,
282
290
  }
283
291
  }
284
292
 
package/src/shape.js CHANGED
@@ -50,7 +50,6 @@ export function getCustomShapePath(custShapType, w, h) {
50
50
  const arcToNodes = pathNodes['a:arcTo']
51
51
  let closeNode = getTextByPathList(pathNodes, ['a:close'])
52
52
  if (!Array.isArray(moveToNode)) moveToNode = [moveToNode]
53
- if (!Array.isArray(lnToNodes)) lnToNodes = [lnToNodes]
54
53
 
55
54
  const multiSapeAry = []
56
55
  if (moveToNode.length > 0) {
@@ -72,6 +71,7 @@ export function getCustomShapePath(custShapType, w, h) {
72
71
  }
73
72
  })
74
73
  if (lnToNodes) {
74
+ if (!Array.isArray(lnToNodes)) lnToNodes = [lnToNodes]
75
75
  Object.keys(lnToNodes).forEach(key => {
76
76
  const lnToPtNode = lnToNodes[key]['a:pt']
77
77
  if (lnToPtNode) {