问题:名字相同,解码范围不一定相同
将字体裁剪工具从 Python 迁移到 Bun 时,Yggdrasil 需要保留严格 GB2312 汉字集合。直接使用 TextDecoder(‘gb2312’, { fatal: true }),没有复现原 Python 严格解码的字集。
原因是 WHATWG 编码标签映射:gb2312 被映射到 GBK 解码器。fatal: true 表示遇到该解码器认定的错误时报错,并不表示切换到严格 GB2312 字符表。
本文整理自 2026-10-27 的迁移记录;本文没有重新运行原始字体对照实验。
五个容易漏掉的位置
项目遍历 GB2312 汉字区 B0A1–F7FE,每个字节都限制在相应的双字节网格内。其中 D7FA–D7FE 是 GB2312 未分配位置。
在该项目的 Bun / WHATWG TextDecoder 环境中,这五个位置不会因 fatal: true 抛错,而是被解码为私用字符。把“没报错”当成“属于严格 GB2312”,就会把额外字符加入字体子集。
项目为保持 6763 个 GB2312 汉字,显式跳过 D7FA、D7FB、D7FC、D7FD、D7FE。这个规则针对该汉字区的枚举,不是完整 GB2312 校验器。
迁移验证不能只看输出文件能否打开
字体子集看似正常,也可能发生字符集合、字形轮廓或字体行为变化。历史对照使用同一份上游完整 Noto 字体,对比原 Python / fontTools 输出与 Bun 输出,检查了:
- 实际保留的字符集合,而不只是字符数。
- 字体名称。
- 所有保留字符及 .notdef 的轮廓与字宽。
- GSUB / GPOS 特性。
- 生产 Rust 渲染路径能否输出 PNG。
记录中,两份中文字体各保留 7112 个实际字符,Bold 子集保留 95 个字符。这里的总数还包括该裁剪任务保留的其他字符,不能与 6763 个 GB2312 汉字混为一谈。
包装库没有开放选项时,检查底层能力
迁移过程中,subset-font 没有开放保留 .notdef 轮廓所需的 NOTDEF_OUTLINE(0x40)选项。项目改为直接调用锁定版本的 harfbuzzjs WASM。
直接调用 WASM 也不能假定它暴露了完整 HarfBuzz C API:当时使用的二进制没有导出 hb_face_get_glyph_count。实现要以实际导出符号为准,不照搬桌面 C API 的函数列表。
可复用的迁移原则
- 核对编码标签实际映射的解码器,不只看构造参数的名称。
- 为标准中的未分配位置保留显式边界样本。
- 对生成资产比较语义内容,不只比较文件大小或“生成成功”。
- 包装库缺功能时,先确认底层选项和实际导出,再决定是否直接调用。
- 保持上游输入一致,避免把字体版本差异误认为工具迁移差异。
来源
Yggdrasil 字体裁剪工具由 Python 迁移到 Bun 的历史记录,修复提交 d4f801bf,工具为 scripts/subset-og-fonts.cjs。该名称是当时的路径,后续可能已随脚本迁移更名。