pptx2js 0.4.0 → 0.4.3
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.html +45 -37
- package/README.md +54 -34
- package/lib/{utils/bounds.js → bounds.js} +27 -4
- package/lib/chart.js +104 -30
- package/lib/codegen.js +66 -7
- package/lib/color.js +275 -0
- package/lib/convert.js +12 -8
- package/lib/extractor.js +200 -153
- package/lib/mapper.js +4 -0
- package/lib/packager.js +65 -8
- package/lib/placeholder.js +161 -29
- package/lib/presentation.js +5 -5
- package/lib/rels.js +19 -6
- package/lib/run-utils.js +181 -0
- package/lib/smartart.js +7 -15
- package/lib/table.js +33 -28
- package/lib/text-utils.js +235 -0
- package/lib/xml-parser.js +191 -15
- package/lib/xml-utils.js +82 -36
- package/package.json +4 -4
- package/lib/utils/color.js +0 -128
- package/lib/utils/emu.js +0 -20
package/README.html
CHANGED
|
@@ -536,7 +536,7 @@
|
|
|
536
536
|
</p>
|
|
537
537
|
<div class="hero-meta">
|
|
538
538
|
<span><strong>运行环境</strong>Node.js ≥ 18</span>
|
|
539
|
-
<span><strong>版本</strong>v0.4.
|
|
539
|
+
<span><strong>版本</strong>v0.4.3</span>
|
|
540
540
|
<span><strong>许可</strong>MIT</span>
|
|
541
541
|
<span><strong>仓库</strong>GitHub 开源</span>
|
|
542
542
|
</div>
|
|
@@ -585,19 +585,22 @@
|
|
|
585
585
|
|
|
586
586
|
<!-- 转换能力 -->
|
|
587
587
|
<section class="section" id="capabilities">
|
|
588
|
-
<div class="section-header"><h2 class="section-title">当前转换能力(v0.4.
|
|
588
|
+
<div class="section-header"><h2 class="section-title">当前转换能力(v0.4.3)</h2></div>
|
|
589
589
|
<table>
|
|
590
590
|
<thead><tr><th>元素</th><th>状态</th><th>说明</th></tr></thead>
|
|
591
591
|
<tbody>
|
|
592
|
-
<tr><td>文本框(段落对齐、lvl
|
|
593
|
-
<tr><td>图片</td><td><span class="badge badge-full">精确</span></td><td><code>addImage()</code
|
|
594
|
-
<tr><td>表格(含单元格边框)</td><td><span class="badge badge-full">精确</span></td><td><code>addTable()</code></td></tr>
|
|
595
|
-
<tr><td>图表 BAR/LINE/PIE/AREA/DOUGHNUT/SCATTER/RADAR/BUBBLE</td><td><span class="badge badge-full">精确</span></td><td><code>addChart()</code></td></tr>
|
|
596
|
-
<tr><td
|
|
597
|
-
<tr><td
|
|
598
|
-
<tr><td>
|
|
599
|
-
<tr><td
|
|
600
|
-
<tr><td
|
|
592
|
+
<tr><td>文本框(段落对齐、lvl、段距/行距 <code>spcPct</code>、列表、超链接)</td><td><span class="badge badge-full">精确</span></td><td><code>addText()</code>;<code>lstStyle</code>/<code>defRPr</code> 继承</td></tr>
|
|
593
|
+
<tr><td>图片</td><td><span class="badge badge-full">精确</span></td><td><code>addImage()</code>;<code>media/</code> 重名自动后缀</td></tr>
|
|
594
|
+
<tr><td>表格(含单元格边框)</td><td><span class="badge badge-full">精确</span></td><td><code>addTable()</code>;<code>text-utils.js</code></td></tr>
|
|
595
|
+
<tr><td>图表 BAR/LINE/PIE/AREA/DOUGHNUT/SCATTER/RADAR/BUBBLE</td><td><span class="badge badge-full">精确</span></td><td><code>addChart()</code>;散点图 <code>c:xVal</code></td></tr>
|
|
596
|
+
<tr><td>预设形状 / 线条(虚线、<code>flipH</code>/<code>flipV</code>)</td><td><span class="badge badge-full">精确</span></td><td><code>addShape()</code>;<code>spTree</code> 文档顺序</td></tr>
|
|
597
|
+
<tr><td>母版/版式装饰形状</td><td><span class="badge badge-full">精确</span></td><td>master → layout → slide 层叠</td></tr>
|
|
598
|
+
<tr><td>纯色 / 渐变 / <code>prstClr</code> / <code>sysClr</code> 背景</td><td><span class="badge badge-full">精确</span></td><td><code>p:bgPr</code>、<code>p:bgRef</code>;系统色修饰符</td></tr>
|
|
599
|
+
<tr><td>母版/版式占位符继承</td><td><span class="badge badge-full">精确</span></td><td>xfrm、<code>txBody</code>/<code>lstStyle</code> 合并</td></tr>
|
|
600
|
+
<tr><td>组合形状(<code>p:grpSp</code>)</td><td><span class="badge badge-full">精确</span></td><td>无 <code>p:spTree</code> 时直接展平</td></tr>
|
|
601
|
+
<tr><td>SmartArt</td><td><span class="badge badge-degrade">退化</span></td><td>文本列表或占位</td></tr>
|
|
602
|
+
<tr><td>渐变填充 / 弯曲连接线</td><td><span class="badge badge-degrade">退化</span></td><td>首色标;<code>bentConnector3</code>→直线</td></tr>
|
|
603
|
+
<tr><td>不支持图表类型</td><td><span class="badge badge-degrade">退化</span></td><td>缓存图或占位文本</td></tr>
|
|
601
604
|
<tr><td>复杂动画</td><td><span class="badge badge-plan">计划</span></td><td>design.html</td></tr>
|
|
602
605
|
</tbody>
|
|
603
606
|
</table>
|
|
@@ -631,7 +634,7 @@
|
|
|
631
634
|
<tr><td>--strict-degrade</td><td>false</td><td>任意退化项触发非零退出码</td></tr>
|
|
632
635
|
<tr><td>--strict-skip</td><td>false</td><td><code>severity:error</code> 跳过项触发非零退出码</td></tr>
|
|
633
636
|
<tr><td>--log-level</td><td>info</td><td><code>minimal</code> / <code>info</code> / <code>verbose</code></td></tr>
|
|
634
|
-
<tr><td>--max-file-size</td><td>50MB</td><td
|
|
637
|
+
<tr><td>--max-file-size</td><td>50MB</td><td>超过阈值抛出错误(避免静默 OOM)</td></tr>
|
|
635
638
|
</tbody>
|
|
636
639
|
</table>
|
|
637
640
|
|
|
@@ -697,15 +700,17 @@ console.log(result.log.statistics);</pre>
|
|
|
697
700
|
</div>
|
|
698
701
|
<div class="pipeline-step">
|
|
699
702
|
<div class="pipeline-connector"><div class="pipeline-dot"></div><div class="pipeline-line"></div></div>
|
|
700
|
-
<div class="pipeline-box"><strong
|
|
703
|
+
<div class="pipeline-box"><strong>⑥ 资源打包器</strong> — lib/packager.js(先于代码生成,更新 IR 中 mediaPath)</div>
|
|
701
704
|
</div>
|
|
702
705
|
<div class="pipeline-step">
|
|
703
706
|
<div class="pipeline-connector"><div class="pipeline-dot"></div></div>
|
|
704
|
-
<div class="pipeline-box"><strong
|
|
707
|
+
<div class="pipeline-box"><strong>⑤ 代码生成器</strong> — lib/codegen.js</div>
|
|
705
708
|
</div>
|
|
706
709
|
<div class="pipeline-io out">output.js + media/ + conversion.log</div>
|
|
707
710
|
</div>
|
|
708
|
-
<p style="margin-top:16px;font-size:13px;color:var(--ink-faint)">辅助模块:<code>lib/presentation.js</code>、<code>lib/graphic.js</code>、<code>lib/
|
|
711
|
+
<p style="margin-top:16px;font-size:13px;color:var(--ink-faint)">辅助模块:<code>lib/presentation.js</code>、<code>lib/graphic.js</code>、<code>lib/xml-utils.js</code>、<code>lib/color.js</code>、<code>lib/bounds.js</code>、<code>lib/text-utils.js</code>、<code>lib/run-utils.js</code>、<code>lib/table.js</code>、<code>lib/chart.js</code>、<code>lib/smartart.js</code></p>
|
|
712
|
+
<h3 style="margin-top:24px;font-size:15px;font-weight:600">XML 节点模型(OONode)</h3>
|
|
713
|
+
<p style="font-size:13px;color:var(--ink-light)">自研 <code>lib/xml-parser.js</code>,节点结构 <code>{ tag, attrs, children, text }</code>。<code>childNodes</code> 保留文档顺序(Z 轴与段落内 <code>pPr</code>/<code>r</code> 顺序)。</p>
|
|
709
714
|
</section>
|
|
710
715
|
|
|
711
716
|
<!-- 技术栈 -->
|
|
@@ -715,7 +720,7 @@ console.log(result.log.statistics);</pre>
|
|
|
715
720
|
<thead><tr><th>用途</th><th>选型</th></tr></thead>
|
|
716
721
|
<tbody>
|
|
717
722
|
<tr><td>ZIP 处理</td><td><a href="https://www.npmjs.com/package/jszip" target="_blank" rel="noopener">JSZip</a></td></tr>
|
|
718
|
-
<tr><td>XML 解析</td><td
|
|
723
|
+
<tr><td>XML 解析</td><td>自研 <code>lib/xml-parser.js</code>(无 xml2js 运行时依赖)</td></tr>
|
|
719
724
|
<tr><td>CLI</td><td><a href="https://www.npmjs.com/package/commander" target="_blank" rel="noopener">Commander.js</a></td></tr>
|
|
720
725
|
<tr><td>终端输出</td><td><a href="https://www.npmjs.com/package/chalk" target="_blank" rel="noopener">chalk</a></td></tr>
|
|
721
726
|
<tr><td>测试</td><td><a href="https://jestjs.io/" target="_blank" rel="noopener">Jest</a>(单元 + 集成)</td></tr>
|
|
@@ -727,28 +732,31 @@ console.log(result.log.statistics);</pre>
|
|
|
727
732
|
<!-- 开发 -->
|
|
728
733
|
<section class="section" id="dev">
|
|
729
734
|
<div class="section-header"><h2 class="section-title">开发</h2></div>
|
|
730
|
-
<pre class="shell"><span class="prompt">$</span>npm test <span class="com" style="color:#666">#
|
|
735
|
+
<pre class="shell"><span class="prompt">$</span>npm test <span class="com" style="color:#666"># 51 用例(29 套件,单元 + 集成)</span>
|
|
731
736
|
<span class="prompt">$</span>npm run test:watch <span class="com" style="color:#666"># 监听模式</span></pre>
|
|
732
737
|
|
|
733
738
|
<h3>目录结构</h3>
|
|
734
739
|
<pre class="tree">pptx2js/
|
|
735
740
|
├── bin/pptx2js.js # CLI 入口
|
|
736
|
-
├── lib/
|
|
737
|
-
│ ├──
|
|
738
|
-
│ ├──
|
|
739
|
-
│ ├──
|
|
740
|
-
│ ├──
|
|
741
|
-
│ ├──
|
|
742
|
-
│ ├──
|
|
743
|
-
│ ├──
|
|
744
|
-
│ ├──
|
|
745
|
-
│ ├──
|
|
746
|
-
│ ├──
|
|
747
|
-
│ ├──
|
|
748
|
-
│ ├──
|
|
749
|
-
│ ├──
|
|
750
|
-
│ ├──
|
|
751
|
-
│
|
|
741
|
+
├── lib/ # 20 个模块(扁平布局)
|
|
742
|
+
│ ├── index.js # 库入口
|
|
743
|
+
│ ├── convert.js # 流水线编排
|
|
744
|
+
│ ├── unpacker.js # ① 解压
|
|
745
|
+
│ ├── xml-parser.js # ② OONode 解析
|
|
746
|
+
│ ├── xml-utils.js # child / children / childNodes
|
|
747
|
+
│ ├── rels.js # ② 关系索引
|
|
748
|
+
│ ├── presentation.js # 幻灯片列表 / 尺寸
|
|
749
|
+
│ ├── placeholder.js # 母版/版式继承与装饰层
|
|
750
|
+
│ ├── text-utils.js # 文本 run 提取
|
|
751
|
+
│ ├── color.js # 颜色(srgb/scheme/prst/sys)
|
|
752
|
+
│ ├── bounds.js # EMU 与 xfrm 边界
|
|
753
|
+
│ ├── graphic.js # graphicFrame 识别
|
|
754
|
+
│ ├── table.js / chart.js / smartart.js
|
|
755
|
+
│ ├── extractor.js # ③ 实体提取
|
|
756
|
+
│ ├── mapper.js # ④ IR 映射
|
|
757
|
+
│ ├── codegen.js # ⑤ 代码生成
|
|
758
|
+
│ ├── packager.js # ⑥ 资源打包
|
|
759
|
+
│ └── run-utils.js # run 合并与压缩
|
|
752
760
|
├── test/
|
|
753
761
|
│ ├── unit/
|
|
754
762
|
│ ├── integration/
|
|
@@ -762,14 +770,14 @@ console.log(result.log.statistics);</pre>
|
|
|
762
770
|
<div class="section-header"><h2 class="section-title">已知局限</h2></div>
|
|
763
771
|
<ul class="limits-list">
|
|
764
772
|
<li><span class="limit-num">L1</span><span><strong>字体</strong>依赖运行环境,不负责检测或打包嵌入字体</span></li>
|
|
765
|
-
<li><span class="limit-num">L2</span><span><strong>母版继承</strong
|
|
773
|
+
<li><span class="limit-num">L2</span><span><strong>母版继承</strong>已实现装饰层与 <code>lstStyle</code>/<code>defRPr</code>;复杂列表/多主题样式仍可能不完整</span></li>
|
|
766
774
|
<li><span class="limit-num">L3</span><span><strong>SmartArt</strong> 仅文本列表退化,缓存图片因 PPT 版本差异未实现</span></li>
|
|
767
775
|
<li><span class="limit-num">L4</span><span><strong>动画</strong>尚未转换(设计为退化淡入,待实现)</span></li>
|
|
768
776
|
<li><span class="limit-num">L5</span><span><strong>不保证</strong>往返 PPTX 二进制一致,追求视觉可接受</span></li>
|
|
769
777
|
<li><span class="limit-num">L6</span><span><strong>不支持</strong>密码保护或 <code>.ppt</code> 旧格式</span></li>
|
|
770
778
|
<li><span class="limit-num">L7</span><span><strong>不支持</strong>增量转换,每次全量重写</span></li>
|
|
771
|
-
<li><span class="limit-num">L8</span><span><strong>大文件</strong
|
|
772
|
-
<li><span class="limit-num">L9</span><span><strong
|
|
779
|
+
<li><span class="limit-num">L8</span><span><strong>大文件</strong>超过 <code>--max-file-size</code> 会直接报错,需调高阈值或拆分</span></li>
|
|
780
|
+
<li><span class="limit-num">L9</span><span><strong>非标 OOXML</strong>无 XML 前缀的文档可能解析失败</span></li>
|
|
773
781
|
</ul>
|
|
774
782
|
</section>
|
|
775
783
|
|
|
@@ -777,7 +785,7 @@ console.log(result.log.statistics);</pre>
|
|
|
777
785
|
<section class="section" id="status">
|
|
778
786
|
<div class="section-header"><h2 class="section-title">项目状态</h2></div>
|
|
779
787
|
<div class="status-banner">
|
|
780
|
-
当前为 <strong>v0.4.
|
|
788
|
+
当前为 <strong>v0.4.3</strong>:母版/版式装饰层按 Z 轴叠加;<code>lstStyle</code>/<code>defRPr</code> 与 <code>prstClr</code>/<code>sysClr</code> 颜色;<code>spcPct</code> 段距;形状 <code>flipH</code>/<code>flipV</code>;<code>lib/utils</code> 合并为 <code>lib/color.js</code>、<code>lib/bounds.js</code>。复杂动画、部分连接器与 SmartArt 图形化按 <a href="./design.html" style="color:var(--accent)">design.html</a> 继续推进,欢迎贡献。
|
|
781
789
|
</div>
|
|
782
790
|
<p class="license" style="margin-top:32px;border:none;padding:0">License: MIT</p>
|
|
783
791
|
</section>
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
| 层级 | 策略 | 示例 |
|
|
12
12
|
|------|------|------|
|
|
13
13
|
| 精确转换 | PptxGenJS 原生支持的元素一比一映射 | 文本框、图片、基本形状、线条、纯色背景 |
|
|
14
|
-
| 退化转换 | 尽力保留内容,降级为近似表示 |
|
|
14
|
+
| 退化转换 | 尽力保留内容,降级为近似表示 | 渐变→纯色、图案填充→前景色、未知形状→矩形 |
|
|
15
15
|
| 静默跳过 | 记录日志,不中断流程 | 不支持的 graphicFrame、ActiveX、OLE、VBA |
|
|
16
16
|
|
|
17
17
|
## 功能特性
|
|
@@ -21,20 +21,22 @@
|
|
|
21
21
|
- 转换报告为 JSON,含 `slideIndex`、`elementBounds`、`severity`,便于 CI 集成
|
|
22
22
|
- 仅支持 OOXML(`.pptx`),不做双向转换、不做 GUI
|
|
23
23
|
|
|
24
|
-
## 当前转换能力(v0.4.
|
|
24
|
+
## 当前转换能力(v0.4.3)
|
|
25
25
|
|
|
26
26
|
| 元素 | 状态 | 说明 |
|
|
27
27
|
|------|------|------|
|
|
28
|
-
| 文本框(run 格式、段落对齐、`lvl
|
|
29
|
-
| 图片(PNG/JPEG 等) | ✅ 精确 | `addImage()
|
|
30
|
-
| 表格(内联 `a:tbl
|
|
31
|
-
| 图表(BAR / LINE / PIE / AREA / DOUGHNUT / SCATTER / RADAR / BUBBLE) | ✅ 精确 | `addChart()
|
|
32
|
-
| 预设形状 /
|
|
33
|
-
|
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
|
|
|
28
|
+
| 文本框(run 格式、段落对齐、`lvl`、段距/行距、`spcPct`、列表、超链接) | ✅ 精确 | `addText()`;`lstStyle`/`defRPr` 继承;同段多 `a:pPr` 按 XML 顺序 |
|
|
29
|
+
| 图片(PNG/JPEG 等) | ✅ 精确 | `addImage()`;`media/` 重名自动 `name_2.ext` |
|
|
30
|
+
| 表格(内联 `a:tbl`、单元格样式与四边边框) | ✅ 精确 | `addTable()`;文本经 `lib/text-utils.js` 提取 |
|
|
31
|
+
| 图表(BAR / LINE / PIE / AREA / DOUGHNUT / SCATTER / RADAR / BUBBLE) | ✅ 精确 | `addChart()`;散点图读取 `c:xVal` |
|
|
32
|
+
| 预设形状 / 线条(虚线、`flipH`/`flipV`) | ✅ 精确 | `addShape()`;`spTree` 按文档顺序渲染 |
|
|
33
|
+
| 母版/版式装饰形状 | ✅ 精确 | master → layout → slide 层叠;跳过占位符定义形状 |
|
|
34
|
+
| 纯色 / 渐变首色标 / 图案前景色 / `prstClr` / `sysClr` | ✅ / ⚡ | `p:bgPr`、`p:bgRef`;系统色应用 lumMod 等修饰 |
|
|
35
|
+
| 幻灯片尺寸 | ✅ 精确 | `defineLayout()` |
|
|
36
|
+
| 组合形状(`p:grpSp`) | ✅ 精确 | 无 `p:spTree` 包装时直接递归展平 |
|
|
37
|
+
| 母版/版式占位符继承 | ✅ 精确 | xfrm、`txBody`/`lstStyle`/`defRPr` 合并 |
|
|
38
|
+
| SmartArt | ⚡ 退化 | 从 `dgm:data` 提取文本列表,否则占位 |
|
|
39
|
+
| 渐变填充 / 弯曲连接线 | ⚡ 退化 | 渐变文字/填充取首色标;`bentConnector3`→直线 |
|
|
38
40
|
| 不支持图表类型 | ⚡ 退化 | 图表部件 rels 缓存图或占位文本 |
|
|
39
41
|
| 复杂动画 | 🔜 计划中 | 见设计文档 |
|
|
40
42
|
|
|
@@ -71,7 +73,7 @@ npx pptx2js input.pptx -o ./pptx2js-output
|
|
|
71
73
|
| `--strict-degrade` | `false` | 任意退化项触发非零退出码 |
|
|
72
74
|
| `--strict-skip` | `false` | `severity:error` 跳过项触发非零退出码 |
|
|
73
75
|
| `--log-level` | `info` | `minimal` / `info` / `verbose` |
|
|
74
|
-
| `--max-file-size` | 50MB |
|
|
76
|
+
| `--max-file-size` | 50MB | 超过阈值抛出错误(避免静默 OOM) |
|
|
75
77
|
|
|
76
78
|
> **注意:** Commander 的 `--no-media` 对应内部选项 `media`(默认 `true`)。传入 `--no-media` 后 `media` 为 `false`,才会跳过媒体提取。
|
|
77
79
|
|
|
@@ -106,7 +108,7 @@ node output.js # 生成 output.pptx
|
|
|
106
108
|
```
|
|
107
109
|
pptx2js-output/
|
|
108
110
|
├── output.js # 主生成脚本,可直接 node 运行
|
|
109
|
-
├── media/ #
|
|
111
|
+
├── media/ # 提取的图片等媒体(重名自动加后缀)
|
|
110
112
|
├── conversion.log # JSON 格式转换报告
|
|
111
113
|
└── README.md # 自动生成的说明
|
|
112
114
|
```
|
|
@@ -121,21 +123,38 @@ pptx2js-output/
|
|
|
121
123
|
input.pptx
|
|
122
124
|
→ ① 解压与索引 (lib/unpacker.js)
|
|
123
125
|
→ ② XML 预解析 (lib/xml-parser.js, lib/rels.js)
|
|
124
|
-
→ ③ 实体提取器 (lib/extractor.js, lib/
|
|
126
|
+
→ ③ 实体提取器 (lib/extractor.js, lib/text-utils.js, lib/placeholder.js, …)
|
|
125
127
|
→ ④ 映射引擎 (lib/mapper.js)
|
|
128
|
+
→ ⑥ 资源打包器 (lib/packager.js) ← 先于代码生成,就地更新 IR 中 mediaPath
|
|
126
129
|
→ ⑤ 代码生成器 (lib/codegen.js)
|
|
127
|
-
→ ⑥ 资源打包器 (lib/packager.js)
|
|
128
130
|
→ output.js + media/ + conversion.log
|
|
129
131
|
```
|
|
130
132
|
|
|
131
|
-
辅助模块:`lib/presentation.js`、`lib/graphic.js`、`lib/xml-utils.js`、`lib/
|
|
133
|
+
辅助模块:`lib/presentation.js`、`lib/graphic.js`、`lib/xml-utils.js`、`lib/color.js`、`lib/bounds.js`、`lib/run-utils.js`、`lib/table.js`、`lib/chart.js`、`lib/smartart.js`。
|
|
134
|
+
|
|
135
|
+
### XML 节点模型(v0.4.2+)
|
|
136
|
+
|
|
137
|
+
自研轻量解析器(`lib/xml-parser.js`),统一为 **OONode**:
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
{ tag: 'a:r', attrs: { … }, children: [ … ], text: '' }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`lib/xml-utils.js` 提供:
|
|
144
|
+
|
|
145
|
+
| API | 用途 |
|
|
146
|
+
|-----|------|
|
|
147
|
+
| `child(node, tag)` | 第一个子节点 |
|
|
148
|
+
| `children(node, tag)` | 所有同名子节点(始终为数组) |
|
|
149
|
+
| `childNodes(node)` | 有序子节点列表(Z 轴顺序、`pPr`/`r` 顺序) |
|
|
150
|
+
| `documentRoot(doc, …tags)` | 兼容 parseXml 直接返回根节点 |
|
|
132
151
|
|
|
133
152
|
## 技术栈
|
|
134
153
|
|
|
135
154
|
| 用途 | 选型 |
|
|
136
155
|
|------|------|
|
|
137
156
|
| ZIP 处理 | [JSZip](https://www.npmjs.com/package/jszip) |
|
|
138
|
-
| XML 解析 |
|
|
157
|
+
| XML 解析 | 自研 `lib/xml-parser.js`(无 xml2js 运行时依赖) |
|
|
139
158
|
| CLI | [Commander.js](https://www.npmjs.com/package/commander) |
|
|
140
159
|
| 终端输出 | [chalk](https://www.npmjs.com/package/chalk) |
|
|
141
160
|
| 测试 | [Jest](https://jestjs.io/)(单元 + 集成) |
|
|
@@ -145,7 +164,7 @@ input.pptx
|
|
|
145
164
|
## 开发
|
|
146
165
|
|
|
147
166
|
```bash
|
|
148
|
-
npm test # 单元测试 + 集成测试(
|
|
167
|
+
npm test # 单元测试 + 集成测试(51 用例)
|
|
149
168
|
npm run test:watch # 监听模式
|
|
150
169
|
```
|
|
151
170
|
|
|
@@ -154,47 +173,48 @@ npm run test:watch # 监听模式
|
|
|
154
173
|
```
|
|
155
174
|
pptx2js/
|
|
156
175
|
├── bin/pptx2js.js # CLI 入口
|
|
157
|
-
├── lib/
|
|
176
|
+
├── lib/ # 20 个模块文件(扁平布局)
|
|
158
177
|
│ ├── index.js # 库入口(export convert)
|
|
159
178
|
│ ├── convert.js # 流水线编排
|
|
160
179
|
│ ├── unpacker.js # ① 解压
|
|
161
|
-
│ ├── xml-parser.js # ②
|
|
180
|
+
│ ├── xml-parser.js # ② OONode XML 解析
|
|
181
|
+
│ ├── xml-utils.js # child / children / childNodes
|
|
162
182
|
│ ├── rels.js # ② 关系索引
|
|
163
183
|
│ ├── presentation.js # 幻灯片列表 / 尺寸
|
|
164
|
-
│ ├── placeholder.js
|
|
184
|
+
│ ├── placeholder.js # 母版/版式继承与装饰层
|
|
185
|
+
│ ├── text-utils.js # 文本 run 提取
|
|
186
|
+
│ ├── color.js # 颜色规范化(srgb/scheme/prst/sys)
|
|
187
|
+
│ ├── bounds.js # EMU 换算与 xfrm 边界
|
|
165
188
|
│ ├── graphic.js # graphicFrame URI 识别
|
|
166
|
-
│ ├── table.js
|
|
167
|
-
│ ├── chart.js # 图表提取
|
|
168
|
-
│ ├── smartart.js # SmartArt 退化
|
|
189
|
+
│ ├── table.js / chart.js / smartart.js
|
|
169
190
|
│ ├── extractor.js # ③ 实体提取
|
|
170
191
|
│ ├── mapper.js # ④ IR 映射
|
|
171
192
|
│ ├── codegen.js # ⑤ 代码生成
|
|
172
193
|
│ ├── packager.js # ⑥ 资源打包
|
|
173
|
-
│
|
|
174
|
-
│ └── utils/ # EMU、颜色、坐标
|
|
194
|
+
│ └── run-utils.js # run 合并与选项压缩
|
|
175
195
|
├── test/
|
|
176
196
|
│ ├── unit/
|
|
177
197
|
│ ├── integration/
|
|
178
|
-
│ └── helpers/
|
|
179
|
-
├── design.html
|
|
180
|
-
└── README.html
|
|
198
|
+
│ └── helpers/
|
|
199
|
+
├── design.html
|
|
200
|
+
└── README.html
|
|
181
201
|
```
|
|
182
202
|
|
|
183
203
|
## 已知局限
|
|
184
204
|
|
|
185
205
|
1. **字体**依赖运行环境,不负责检测或打包嵌入字体
|
|
186
|
-
2.
|
|
206
|
+
2. **母版继承**已实现装饰层与 `lstStyle`/`defRPr`;复杂列表/多主题/嵌套样式仍可能不完整
|
|
187
207
|
3. **SmartArt** 仅文本列表退化,缓存图片路径因 PPT 版本差异未实现
|
|
188
208
|
4. **动画**尚未转换(设计为退化淡入,待实现)
|
|
189
209
|
5. **不保证**往返 PPTX 二进制一致,追求视觉可接受
|
|
190
210
|
6. **不支持**密码保护或 `.ppt` 旧格式
|
|
191
211
|
7. **不支持**增量转换,每次全量重写
|
|
192
|
-
8.
|
|
193
|
-
9.
|
|
212
|
+
8. **大文件**超过 `--max-file-size` 会直接报错,超大 PPT 需调高阈值或拆分
|
|
213
|
+
9. **默认命名空间**无 XML 前缀的非标 OOXML 可能解析失败
|
|
194
214
|
|
|
195
215
|
## 项目状态
|
|
196
216
|
|
|
197
|
-
当前为 **v0.4.
|
|
217
|
+
当前为 **v0.4.3**:母版/版式装饰层按 Z 轴叠加;`lstStyle`/`defRPr` 与 `prstClr`/`sysClr` 颜色;`spcPct` 段距;形状 `flipH`/`flipV`;`lib/utils` 合并为 `lib/color.js`、`lib/bounds.js`。复杂动画、部分连接器与 SmartArt 图形化等待 [`design.html`](./design.html) 继续推进,欢迎贡献。
|
|
198
218
|
|
|
199
219
|
## License
|
|
200
220
|
|
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* EMU 换算与形状坐标:a:xfrm → 英寸边界
|
|
3
3
|
*/
|
|
4
|
-
const { attr, child } = require('
|
|
5
|
-
|
|
4
|
+
const { attr, child } = require('./xml-utils');
|
|
5
|
+
|
|
6
|
+
/** EMU(English Metric Units)与英寸换算,1 英寸 = 914400 EMU */
|
|
7
|
+
const EMU_PER_INCH = 914400;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {number} emu
|
|
11
|
+
* @returns {number}
|
|
12
|
+
*/
|
|
13
|
+
function emuToInch(emu) {
|
|
14
|
+
return emu / EMU_PER_INCH;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {number} inch
|
|
19
|
+
* @returns {number}
|
|
20
|
+
*/
|
|
21
|
+
function inchToEmu(inch) {
|
|
22
|
+
return inch * EMU_PER_INCH;
|
|
23
|
+
}
|
|
6
24
|
|
|
7
25
|
/**
|
|
8
26
|
* @param {object|null|undefined} xfrm
|
|
@@ -31,4 +49,9 @@ function round3(n) {
|
|
|
31
49
|
return Math.round(n * 1000) / 1000;
|
|
32
50
|
}
|
|
33
51
|
|
|
34
|
-
module.exports = {
|
|
52
|
+
module.exports = {
|
|
53
|
+
EMU_PER_INCH,
|
|
54
|
+
emuToInch,
|
|
55
|
+
inchToEmu,
|
|
56
|
+
boundsFromXfrm,
|
|
57
|
+
};
|
package/lib/chart.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* 图表提取(design.html §4.3)
|
|
3
3
|
*/
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const {
|
|
5
|
+
const { attr, child, children, documentRoot, textContent } = require('./xml-utils');
|
|
6
6
|
const { getGraphicXfrm } = require('./graphic');
|
|
7
|
-
const { boundsFromXfrm } = require('./
|
|
7
|
+
const { boundsFromXfrm } = require('./bounds');
|
|
8
8
|
|
|
9
9
|
const CHART_NS_BAR = 'barChart';
|
|
10
10
|
const CHART_NS_LINE = 'lineChart';
|
|
@@ -42,12 +42,13 @@ function extractChart(graphicFrame, ctx) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
const chartDoc = ctx.parsed[chartPath];
|
|
45
|
-
const chartSpace =
|
|
45
|
+
const chartSpace = documentRoot(chartDoc, 'c:chartSpace') ?? chartDoc;
|
|
46
46
|
const chartRoot = child(chartSpace, 'c:chart');
|
|
47
47
|
const plotArea = child(chartRoot, 'c:plotArea');
|
|
48
48
|
|
|
49
49
|
const parsed = parseChartPlotArea(plotArea);
|
|
50
50
|
if (parsed) {
|
|
51
|
+
parsed.title = extractChartTitle(chartRoot);
|
|
51
52
|
return {
|
|
52
53
|
slideIndex: ctx.slideIndex,
|
|
53
54
|
slidePath: ctx.slidePath,
|
|
@@ -71,14 +72,13 @@ function parseChartPlotArea(plotArea) {
|
|
|
71
72
|
const chartNode = child(plotArea, `c:${xmlName}`);
|
|
72
73
|
if (!chartNode) continue;
|
|
73
74
|
|
|
74
|
-
const series = extractSeries(chartNode, plotArea);
|
|
75
|
+
const series = extractSeries(chartNode, plotArea, xmlName);
|
|
75
76
|
if (!series.length) continue;
|
|
76
77
|
|
|
77
|
-
const title = extractChartTitle(plotArea);
|
|
78
78
|
return {
|
|
79
79
|
type: pptxType,
|
|
80
80
|
data: series,
|
|
81
|
-
title,
|
|
81
|
+
title: '',
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
84
|
return null;
|
|
@@ -88,16 +88,30 @@ function parseChartPlotArea(plotArea) {
|
|
|
88
88
|
* @param {object} chartNode c:barChart 等
|
|
89
89
|
* @param {object} plotArea
|
|
90
90
|
*/
|
|
91
|
-
function extractSeries(chartNode, plotArea) {
|
|
91
|
+
function extractSeries(chartNode, plotArea, chartXmlName) {
|
|
92
92
|
const result = [];
|
|
93
|
-
const seriesNodes =
|
|
93
|
+
const seriesNodes = children(chartNode, 'c:ser');
|
|
94
|
+
const isScatter = chartXmlName === 'scatterChart';
|
|
94
95
|
|
|
95
96
|
for (const ser of seriesNodes) {
|
|
96
97
|
const titleText = extractSeriesName(ser);
|
|
97
|
-
const cat = extractCategoryLabels(ser
|
|
98
|
-
const values = extractSeriesValues(ser
|
|
98
|
+
const cat = extractCategoryLabels(ser);
|
|
99
|
+
const values = extractSeriesValues(ser);
|
|
99
100
|
if (!values.length) continue;
|
|
100
101
|
|
|
102
|
+
if (isScatter) {
|
|
103
|
+
const xs = extractSeriesXValues(ser);
|
|
104
|
+
const pairs = values.map((y, i) => [
|
|
105
|
+
xs[i] != null && !Number.isNaN(xs[i]) ? xs[i] : i,
|
|
106
|
+
y,
|
|
107
|
+
]);
|
|
108
|
+
result.push({
|
|
109
|
+
name: titleText || `Series ${result.length + 1}`,
|
|
110
|
+
values: pairs,
|
|
111
|
+
});
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
101
115
|
result.push({
|
|
102
116
|
name: titleText || `Series ${result.length + 1}`,
|
|
103
117
|
labels: cat.length ? cat : values.map((_, i) => String(i + 1)),
|
|
@@ -113,64 +127,124 @@ function extractSeries(chartNode, plotArea) {
|
|
|
113
127
|
function extractSeriesName(ser) {
|
|
114
128
|
const tx = child(child(ser, 'c:tx'), 'c:strRef');
|
|
115
129
|
const cache = child(tx, 'c:strCache');
|
|
116
|
-
const pt =
|
|
130
|
+
const pt = children(cache, 'c:pt')[0];
|
|
117
131
|
return pt ? textContent(child(pt, 'c:v')) : '';
|
|
118
132
|
}
|
|
119
133
|
|
|
120
134
|
/**
|
|
121
135
|
* @param {object} ser
|
|
122
|
-
* @param {object} plotArea
|
|
123
136
|
*/
|
|
124
|
-
function extractCategoryLabels(ser
|
|
125
|
-
void plotArea;
|
|
137
|
+
function extractCategoryLabels(ser) {
|
|
126
138
|
const cat = child(ser, 'c:cat');
|
|
127
139
|
if (!cat) return [];
|
|
140
|
+
|
|
128
141
|
const strRef = child(cat, 'c:strRef');
|
|
142
|
+
if (strRef) {
|
|
143
|
+
const pts = extractCachePoints(strRef, 'c:strCache', 'c:v');
|
|
144
|
+
if (pts.length) return pts;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const multiRef = child(cat, 'c:multiLvlStrRef');
|
|
148
|
+
if (multiRef) {
|
|
149
|
+
const pts = extractMultiLvlStrLabels(multiRef);
|
|
150
|
+
if (pts.length) return pts;
|
|
151
|
+
}
|
|
152
|
+
|
|
129
153
|
const numRef = child(cat, 'c:numRef');
|
|
130
|
-
if (
|
|
131
|
-
|
|
154
|
+
if (numRef) {
|
|
155
|
+
const pts = extractCachePoints(numRef, 'c:numCache', 'c:v').map(String);
|
|
156
|
+
if (pts.length) return pts;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const strLit = child(cat, 'c:strLit');
|
|
160
|
+
if (strLit) {
|
|
161
|
+
return extractCachePoints(strLit, null, 'c:v').filter(Boolean);
|
|
162
|
+
}
|
|
163
|
+
|
|
132
164
|
return [];
|
|
133
165
|
}
|
|
134
166
|
|
|
167
|
+
/**
|
|
168
|
+
* PptxGenJS 常用 multiLvlStrRef 缓存分类标签
|
|
169
|
+
* @param {object} multiRef
|
|
170
|
+
*/
|
|
171
|
+
function extractMultiLvlStrLabels(multiRef) {
|
|
172
|
+
const cache = child(multiRef, 'c:multiLvlStrCache');
|
|
173
|
+
if (!cache) return [];
|
|
174
|
+
const lvl = child(cache, 'c:lvl');
|
|
175
|
+
if (!lvl) return [];
|
|
176
|
+
const pts = children(lvl, 'c:pt');
|
|
177
|
+
return pts
|
|
178
|
+
.sort(
|
|
179
|
+
(a, b) =>
|
|
180
|
+
parseInt(attr(a, 'idx') ?? '0', 10) - parseInt(attr(b, 'idx') ?? '0', 10)
|
|
181
|
+
)
|
|
182
|
+
.map((pt) => textContent(child(pt, 'c:v')))
|
|
183
|
+
.filter(Boolean);
|
|
184
|
+
}
|
|
185
|
+
|
|
135
186
|
/**
|
|
136
187
|
* @param {object} ser
|
|
137
|
-
* @param {object} plotArea
|
|
138
188
|
*/
|
|
139
|
-
function extractSeriesValues(ser
|
|
140
|
-
void plotArea;
|
|
189
|
+
function extractSeriesValues(ser) {
|
|
141
190
|
const val = child(ser, 'c:val');
|
|
142
191
|
if (!val) return [];
|
|
143
192
|
const numRef = child(val, 'c:numRef');
|
|
144
|
-
if (
|
|
145
|
-
|
|
193
|
+
if (numRef) {
|
|
194
|
+
return extractCachePoints(numRef, 'c:numCache', 'c:v').map((v) => parseFloat(v) || 0);
|
|
195
|
+
}
|
|
196
|
+
const numLit = child(val, 'c:numLit');
|
|
197
|
+
if (numLit) {
|
|
198
|
+
return extractCachePoints(numLit, null, 'c:v').map((v) => parseFloat(v) || 0);
|
|
199
|
+
}
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 散点图 X 轴(c:xVal)
|
|
205
|
+
* @param {object} ser
|
|
206
|
+
*/
|
|
207
|
+
function extractSeriesXValues(ser) {
|
|
208
|
+
const xVal = child(ser, 'c:xVal');
|
|
209
|
+
if (!xVal) return [];
|
|
210
|
+
const numRef = child(xVal, 'c:numRef');
|
|
211
|
+
if (numRef) {
|
|
212
|
+
return extractCachePoints(numRef, 'c:numCache', 'c:v').map((v) => parseFloat(v) || 0);
|
|
213
|
+
}
|
|
214
|
+
const numLit = child(xVal, 'c:numLit');
|
|
215
|
+
if (numLit) {
|
|
216
|
+
return extractCachePoints(numLit, null, 'c:v').map((v) => parseFloat(v) || 0);
|
|
217
|
+
}
|
|
218
|
+
return [];
|
|
146
219
|
}
|
|
147
220
|
|
|
148
221
|
/**
|
|
149
222
|
* @param {object} refNode
|
|
150
|
-
* @param {string} cacheKey
|
|
223
|
+
* @param {string|null} cacheKey null 时直接在 refNode 上找 c:pt(如 strLit)
|
|
151
224
|
* @param {string} valueKey
|
|
152
225
|
*/
|
|
153
226
|
function extractCachePoints(refNode, cacheKey, valueKey) {
|
|
154
|
-
const
|
|
155
|
-
|
|
227
|
+
const container = cacheKey ? child(refNode, cacheKey) : refNode;
|
|
228
|
+
if (!container) return [];
|
|
229
|
+
const pts = children(container, 'c:pt');
|
|
156
230
|
return pts
|
|
157
231
|
.sort((a, b) => parseInt(attr(a, 'idx') ?? '0', 10) - parseInt(attr(b, 'idx') ?? '0', 10))
|
|
158
232
|
.map((pt) => textContent(child(pt, valueKey)));
|
|
159
233
|
}
|
|
160
234
|
|
|
161
235
|
/**
|
|
162
|
-
* @param {object}
|
|
236
|
+
* @param {object} chartRoot c:chart
|
|
163
237
|
*/
|
|
164
|
-
function extractChartTitle(
|
|
165
|
-
const title = child(
|
|
238
|
+
function extractChartTitle(chartRoot) {
|
|
239
|
+
const title = child(chartRoot, 'c:title');
|
|
166
240
|
if (!title) return '';
|
|
167
241
|
const tx = child(title, 'c:tx');
|
|
168
242
|
const rich = child(tx, 'c:rich');
|
|
169
243
|
if (!rich) return '';
|
|
170
244
|
const parts = [];
|
|
171
|
-
for (const p of
|
|
172
|
-
for (const r of
|
|
173
|
-
parts.push(textContent(r
|
|
245
|
+
for (const p of children(rich, 'a:p')) {
|
|
246
|
+
for (const r of children(p, 'a:r')) {
|
|
247
|
+
parts.push(textContent(child(r, 'a:t')));
|
|
174
248
|
}
|
|
175
249
|
}
|
|
176
250
|
return parts.join('');
|