6.2 KiB
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:
{
"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"
}
处理链路如下:
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 |
| Typora 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 忽略的:
output/docx-theme-matrix/
7. 下一步
阶段 12D-R3 建立 PDF 与 DOCX 自动视觉差异引擎,优先量化:
- 页面数和分页边界;
- 文本块位置和尺寸;
- 字体、字号、颜色和字重;
- 封面、版头、版记、表格及代码块边界;
- Word、WPS 与 Chromium PDF 的差异。
红头主题的 WPS 版记溢出作为首个固定回归样例。