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

6.2 KiB
Raw Blame History

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.xmluiPriority,并可能删除未使用的 numbering.xml。Microsoft Open XML SDK 会报告 WPS 写回文件自身产生的 顺序问题,但 Word 仍能禁止修复打开。项目不会因此放宽对自身生成文件的 严格 OOXML 门禁。

6. 验证结果

  • npm run verify:docx-themes14/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 版记溢出作为首个固定回归样例。