# 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 版记溢出作为首个固定回归样例。