Files
MorphDoc/docs/V0.6.0_DOCX_FONT_EMBEDDING.md
T

165 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# v0.6.0 DOCX 字体嵌入与代表主题验收
最后更新:2026-07-31
## 1. 目标与边界
阶段 12D-R2 解决 DOCX 在目标机器没有安装主题字体时发生字体回退的问题。
实现必须同时满足:
- 纯 Node.js 运行,不增加 Python 环境;
- 接受主题现有的 OTF、TTF、WOFF 和 WOFF2 资源;
- 只嵌入明确声明且许可证位允许编辑的字体;
- 使用 Word 原生嵌入字体部件,正文、标题、表格和代码仍可编辑;
- Word 与 WPS 均可打开、编辑和互存;
- 转换、关系、内容类型和字体部件继续通过严格 OOXML 校验;
- 外部主题可以通过清单接入,不依赖主题 ID 分支。
字体采用完整字形嵌入,不做子集化。这样用户在 Word 或 WPS 中输入原文
没有出现过的新字符时,仍可继续使用同一字体。代价是生成文件明显增大。
## 2. 通用实现
主题清单新增可选的 `docxFonts.faces`
```json
{
"family": "FandolFang",
"aliases": ["FangSong", "STFangsong"],
"source": "theme-shared:official-fonts/FandolFang-Regular.woff2",
"weight": 400,
"style": "normal",
"license": "GPL-3.0-or-later WITH Font-exception-2.0"
}
```
处理链路如下:
```mermaid
flowchart LR
A["主题字体清单"] --> B["受限资源读取"]
B --> C["WOFF/WOFF2 转 SFNT"]
C --> D["解析名称、字重与 fsType"]
D --> E["许可证和体积门禁"]
E --> F["ODTTF 混淆"]
F --> G["写入 fontTable 与关系"]
G --> H["重写 Word 字体引用"]
H --> I["严格 OOXML 校验"]
```
核心约束:
- 单主题最多 16 个字形面;
- 单个源字体最多 8 MiB,源字体总量最多 32 MiB
- 单个解码后 SFNT 最多 16 MiB,总量最多 48 MiB
- 接受 `fsType=0x0000` 的可安装嵌入和 `fsType=0x0008` 的可编辑嵌入;
- 拒绝受限、仅预览打印、位图嵌入及无效权限组合;
- 字体键和 ODTTF 部件名由内容指纹稳定生成;
- WOFF2 WASM 解码采用进程内串行调度,避免底层非重入导致批量损坏;
- 同一字体内容按 SHA-256 合并并使用有界缓存;
- 内置主题可引用 `_shared` 字体,本地主题不能越权引用内置共享资源;
- 最终校验器会解混淆字体,重新检查 SFNT、内部字体族、权限位、关系和
孤立部件。
`themes/_shared/official-fonts/fonts.css` 会统一前置到内置主题 CSS,使网页、
PDF 和 DOCX 使用同一字体资产。字体选择和别名替换按字体族工作,没有
主题 ID 判断。
## 3. 许可证边界
Fandol 字体:
- 使用仓库已有的 Fandol WOFF2
- 字体内 `fsType=0x0008`,允许可编辑嵌入;
- 许可证为 GPL-3.0-or-later with Font Exception
- 完整许可证已位于
`themes/_shared/official-fonts/LICENSE.txt`
Open Sans
- `typora-github` 当前包含的是 `v17` 历史字体文件;
- 该批二进制的字体元数据和历史发行信息对应 Apache License 2.0
- 历史版权声明和完整许可证随字体放在
`themes/typora-github/github/OPEN-SANS-LICENSE.txt`
- 当前上游 Open Sans 已迁移到 SIL Open Font License 1.1。未来替换字体
二进制时必须重新核对许可证、版权声明和嵌入权限,不能沿用本阶段清单。
字体清单中的 `license` 是审计字段,不替代随包许可证文件。
## 4. 代表主题结果
测试机未安装 FandolSong、FandolFang、FandolHei、FandolKai 或
Open Sans,能够真实检验嵌入字体而不是本机字体回退。
| 主题 | 嵌入字形面 | 原始 DOCX 大小 | Word 页数 | WPS 页数 |
| --- | ---: | ---: | ---: | ---: |
| 政企正式·工作报告 | 2 | 9,932,187 字节 | 2 | 2 |
| 政企红头·标准文件 | 5 | 23,920,588 字节 | 2 | 3 |
| GitHub | 5 | 4,680,418 字节 | 2 | 2 |
三份文件均满足:
- 内置校验器确认嵌入字体部件、关系、内容类型和字体键完整;
- Word 使用 `OpenNoRepairDialog` 正常打开并导出 PDF
- WPS 正常打开并导出 PDF
- 未发现方框、缺字或明显系统字体回退;
- Word 追加编辑标记后,WPS 可以读取并继续编辑;
- WPS 另存后,Word 仍可禁止修复打开;
- 最终文件同时保留 Word 和 WPS 两段编辑文本。
14 套主题完整矩阵也已通过。未声明 `docxFonts` 的其余 11 套主题继续产生
零嵌入字体,不会被共享 CSS 隐式扩大 DOCX。
## 5. 已知客户端差异
### 5.1 WPS 红头主题分页
同一份红头 DOCX 在 Word 中为 2 页,在 WPS 中为 3 页。WPS 将版记整体
移动到近空白第 3 页。字体显示正确,内容没有丢失,但尚未达到跨客户端
视觉一致门禁。
该问题进入阶段 12D-R3,由 PDF 与 DOCX 自动视觉差异引擎量化后处理。
当前不通过主题 ID 特判或缩小正文来掩盖差异,避免在没有 PDF 基线的情况
下破坏 Word 与 Chromium 的一致性。
### 5.2 WPS 互存策略
WPS 默认另存会移除嵌入字体,三份文件会缩小到约 20 KiB。设置文档属性
“嵌入 TrueType 字体”并禁用子集后可以保留字体,但 WPS 会重新生成字体
部件,互存文件增大到约 14~45 MiB。
WPS 还会重排 `styles.xml``uiPriority`,并可能删除未使用的
`numbering.xml`。Microsoft Open XML SDK 会报告 WPS 写回文件自身产生的
顺序问题,但 Word 仍能禁止修复打开。项目不会因此放宽对自身生成文件的
严格 OOXML 门禁。
## 6. 验证结果
- `npm run verify:docx-themes`14/14 主题通过;
- `npm test`:全项目通过;
- `npm run typecheck`:全项目通过;
- `npm run build`:全项目通过;
- `git diff --check`:通过;
- Word 原始文档:3/3 禁止修复打开并导出 PDF;
- WPS 原始文档:3/3 打开并导出 PDF;
- Word → WPS → Word 双向编辑:3/3 通过;
- Word/WPS 原始渲染:共 13 页逐页检查完成。
本地验收产物位于被 Git 忽略的:
```text
output/docx-theme-matrix/
```
## 7. 下一步
阶段 12D-R3 建立 PDF 与 DOCX 自动视觉差异引擎,优先量化:
- 页面数和分页边界;
- 文本块位置和尺寸;
- 字体、字号、颜色和字重;
- 封面、版头、版记、表格及代码块边界;
- Word、WPS 与 Chromium PDF 的差异。
红头主题的 WPS 版记溢出作为首个固定回归样例。