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 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.0</span>
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.0)</h2></div>
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 缩进、段距、行距、列表、超链接)</td><td><span class="badge badge-full">精确</span></td><td><code>addText()</code></td></tr>
593
- <tr><td>图片</td><td><span class="badge badge-full">精确</span></td><td><code>addImage()</code></td></tr>
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>形状 / 背景 / 组合</td><td><span class="badge badge-full">精确</span></td><td><code>addShape()</code> 等</td></tr>
597
- <tr><td>母版/版式占位符</td><td><span class="badge badge-full">精确</span></td><td><code>placeholder.js</code></td></tr>
598
- <tr><td>SmartArt</td><td><span class="badge badge-degrade">退化</span></td><td>文本列表或占位;缓存 PNG 暂不实现</td></tr>
599
- <tr><td>渐变 / 未知形状</td><td><span class="badge badge-degrade">退化</span></td><td>首色标、矩形 fallback</td></tr>
600
- <tr><td>不支持图表</td><td><span class="badge badge-degrade">退化</span></td><td>缓存图或占位文本</td></tr>
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>超过阈值切换流式解析(实现中)</td></tr>
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>⑤ 代码生成器</strong> — lib/codegen.js</div>
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>⑥ 资源打包器</strong> — lib/packager.js</div>
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/smartart.js</code>、<code>lib/xml-utils.js</code>、<code>lib/utils/color.js</code>、<code>lib/utils/bounds.js</code></p>
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><a href="https://www.npmjs.com/package/xml2js" target="_blank" rel="noopener">xml2js</a>(统一配置,见 <code>lib/xml-parser.js</code>)</td></tr>
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"># 24 用例(单元 + 集成)</span>
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
- │ ├── convert.js # 流水线编排
738
- │ ├── unpacker.js # ① 解压
739
- │ ├── xml-parser.js # 统一 XML 解析
740
- │ ├── rels.js # ② 关系索引
741
- │ ├── presentation.js # 幻灯片列表 / 尺寸
742
- │ ├── placeholder.js # 母版/版式占位符
743
- │ ├── graphic.js # graphicFrame 识别
744
- │ ├── table.js # 表格提取
745
- │ ├── chart.js # 图表提取
746
- │ ├── smartart.js # SmartArt 退化
747
- │ ├── extractor.js # 实体提取
748
- │ ├── mapper.js # IR 映射
749
- │ ├── codegen.js # 代码生成
750
- │ ├── packager.js # 资源打包
751
- └── utils/ # EMU、颜色
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>已实现 xfrm 与基础 txBody;复杂列表样式继承仍有限;<code>a:spcPct</code> 百分比段距/行距暂不处理</span></li>
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>流式解析仍在实现中</span></li>
772
- <li><span class="limit-num">L9</span><span><strong>媒体重名</strong>同名文件可能互相覆盖(packager 待去重)</span></li>
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.0</strong>:在 v0.3.0 基础上新增段落级格式、表格单元格边框、扩展图表类型、SmartArt 文本列表退化。复杂动画、SmartArt 缓存图、外部链接表格等按 <a href="./design.html" style="color:var(--accent)">design.html</a> 继续推进,欢迎贡献。
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.0
24
+ ## 当前转换能力(v0.4.3
25
25
 
26
26
  | 元素 | 状态 | 说明 |
27
27
  |------|------|------|
28
- | 文本框(run 格式、段落对齐、`lvl` 缩进、段前/段后距、行距、列表、超链接) | ✅ 精确 | `addText()`;`indent`(首行缩进 EMU)暂不映射 |
29
- | 图片(PNG/JPEG 等) | ✅ 精确 | `addImage()`,提取至 `media/` |
30
- | 表格(内联 `a:tbl`、单元格四边边框) | ✅ 精确 | `addTable()`,含合并单元格与 `border` |
31
- | 图表(BAR / LINE / PIE / AREA / DOUGHNUT / SCATTER / RADAR / BUBBLE) | ✅ 精确 | `addChart()`,从 `chartN.xml` 读数据 |
32
- | 预设形状 / 线条 | ✅ 精确 | `addShape()` |
33
- | 纯色背景 / 幻灯片尺寸 | ✅ 精确 | `background`、`defineLayout()` |
34
- | 组合形状(`p:grpSp`) | ✅ 精确 | 递归展平 |
35
- | 母版/版式占位符继承 | ✅ 精确 | `lib/placeholder.js`,按 `p:ph idx` 合并 xfrm |
36
- | SmartArt | 退化 | `lib/smartart.js`:从 `dgm:data` 提取文本列表,否则占位;缓存 PNG 因 rels 差异大暂不实现 |
37
- | 渐变填充 / 未知形状 | 退化 | 首色标→纯色;未知 `prstGeom`→矩形 |
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/placeholder.js, lib/table.js, lib/chart.js, lib/smartart.js)
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/utils/color.js`、`lib/utils/bounds.js`、`lib/smartart.js`。
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 解析 | [xml2js](https://www.npmjs.com/package/xml2js)(统一配置,见 `lib/xml-parser.js`) |
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 # 单元测试 + 集成测试(24 用例)
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 # ② 统一 XML 解析
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
- ├── xml-utils.js
174
- │ └── utils/ # EMU、颜色、坐标
194
+ └── run-utils.js # run 合并与选项压缩
175
195
  ├── test/
176
196
  │ ├── unit/
177
197
  │ ├── integration/
178
- │ └── helpers/ # 最小 PPTX 构造
179
- ├── design.html # 完整设计文档
180
- └── README.html # 本页 HTML 版
198
+ │ └── helpers/
199
+ ├── design.html
200
+ └── README.html
181
201
  ```
182
202
 
183
203
  ## 已知局限
184
204
 
185
205
  1. **字体**依赖运行环境,不负责检测或打包嵌入字体
186
- 2. **母版继承**已实现 xfrm 与基础 txBody 补全;复杂段落/列表样式继承仍有限;`a:spcPct` 百分比段距/行距暂不处理
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. **大文件**(200MB+)可能内存压力较大,超过 50MB 流式解析仍在实现中
193
- 9. **媒体重名**不同路径同名图片可能互相覆盖(`packager` 待实现去重)
212
+ 8. **大文件**超过 `--max-file-size` 会直接报错,超大 PPT 需调高阈值或拆分
213
+ 9. **默认命名空间**无 XML 前缀的非标 OOXML 可能解析失败
194
214
 
195
215
  ## 项目状态
196
216
 
197
- 当前为 **v0.4.0**:在 v0.3.0 基础上新增段落级格式(对齐、`lvl`、段距、行距)、表格单元格边框、扩展图表类型、SmartArt 文本列表退化。复杂动画、SmartArt 缓存图、外部链接表格等按 [`design.html`](./design.html) 继续推进,欢迎贡献。
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
- * 形状坐标:a:xfrm → 英寸边界
2
+ * EMU 换算与形状坐标:a:xfrm → 英寸边界
3
3
  */
4
- const { attr, child } = require('../xml-utils');
5
- const { emuToInch } = require('./emu');
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 = { boundsFromXfrm };
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 { asArray, attr, child, textContent } = require('./xml-utils');
5
+ const { attr, child, children, documentRoot, textContent } = require('./xml-utils');
6
6
  const { getGraphicXfrm } = require('./graphic');
7
- const { boundsFromXfrm } = require('./utils/bounds');
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 = child(chartDoc, 'c:chartSpace') ?? chartDoc;
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 = asArray(chartNode['c:ser']);
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, plotArea);
98
- const values = extractSeriesValues(ser, plotArea);
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 = asArray(child(cache, 'c:pt'))[0];
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, plotArea) {
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 (strRef) return extractCachePoints(strRef, 'c:strCache', 'c:v');
131
- if (numRef) return extractCachePoints(numRef, 'c:numCache', 'c:v').map(String);
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, plotArea) {
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 (!numRef) return [];
145
- return extractCachePoints(numRef, 'c:numCache', 'c:v').map((v) => parseFloat(v) || 0);
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 cache = child(refNode, cacheKey);
155
- const pts = asArray(child(cache, 'c:pt'));
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} plotArea
236
+ * @param {object} chartRoot c:chart
163
237
  */
164
- function extractChartTitle(plotArea) {
165
- const title = child(plotArea, 'c:title');
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 asArray(rich['a:p'])) {
172
- for (const r of asArray(p['a:r'])) {
173
- parts.push(textContent(r['a:t']));
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('');