TTF/OTF/WOFF/WOFF2 字体容器互转
为什么需要字体容器互转
Web 开发场景经常需要把桌面上用的 TTF/OTF 字体转成网页友好的 WOFF/WOFF2 格式——WOFF 压缩率更高、字体更小、加载更快。反过来,某些只有 WOFF2 格式的网页字体,你需要转成 TTF/OTF 才能安装到电脑或手机上使用。
ZenConverter 把这个场景覆盖到了手机端——之前这类工具几乎全是桌面端的。
四种容器格式的关系
| 格式 | 引擎 | 类型 | 说明 |
|---|---|---|---|
| TTF / OTF | 原生库 + Kotlin | SFNT(TrueType/OpenType) | 桌面/系统安装用的标准格式 |
| WOFF 1.0 | 纯 Kotlin zlib | SFNT 压缩容器 | 早期网页格式,WOFF2 的前一代 |
| WOFF2 | arm64 原生 google woff2 | SFNT 高度压缩容器 | 现代网页格式,比 WOFF 小约 30% |
互转路线覆盖六种方向:
- TTF/OTF → WOFF
- TTF/OTF → WOFF2
- WOFF → TTF/OTF
- WOFF2 → TTF/OTF
WOFF 和 WOFF2 之间不直接互转——需要先解回 TTF/OTF 再压到另一个容器。ZenConverter 没有暴露这条中间路线。
为什么分两种引擎
| 路线 | 实现 | 为什么 |
|---|---|---|
| WOFF2 ↔ TTF/OTF | arm64 原生 libzen_woff2.so(google woff2) | WOFF2 格式使用 Brotli 类的高压缩算法,原生库才能正确编解码 |
| WOFF ↔ TTF/OTF | 纯 Kotlin java.util.zip | WOFF 1.0 是 SFNT + zlib 压缩,Kotlin 原生就能处理,不需要原生库 |
这种混合引擎设计的好处是:
- WOFF 路线没有 arm64 依赖——纯 Kotlin,理论上能跑在任何 Android 设备上;
- WOFF2 路线必须 arm64——原生库只编译了 arm64,32 位 ARM 设备不走 WOFF2 但可以走 WOFF;
- APK 体积更小——没有为了 WOFF 单独打包原生库。
自动选择输出扩展名
从 WOFF 或 WOFF2 解回 SFNT 时,ZenConverter 会检查 SFNT 文件头的 flavor 标签:
- flavor =
true→ 输出.ttf(TrueType); - flavor =
OTTO→ 输出.otf(OpenType CFF)。
不会让用户手动选——因为用户不需要关心字体内部是 TrueType 还是 OpenType。
转换是"字节级"的
字体容器互转是字节级容器转换,不触及字体的轮廓数据:
- TTF → WOFF2:读取 SFNT 字体数据 → 用 woff2 算法压缩 → 写入 WOFF2 容器;
- WOFF2 → TTF:读取 WOFF2 容器 → 解压 → 写入 SFNT 容器;
- 转换过程中字体的 glyph 轮廓、kerning、OpenType features 全部原样保留。
这意味着转换后的字体可以被任何标准字体渲染器正确识别——ZenConverter 不"处理"字体内容,只换容器。
已知边界
- arm64 only for WOFF2:WOFF2 ↔ TTF/OTF 路线依赖 arm64 原生库,32 位 ARM 设备不走这条;
- WOFF 不写元数据:纯 Kotlin zlib 实现只做容器转换,不重写 WOFF metadata 和 private blocks(WOFF 容器的元数据);
- WOFF2 不重写元数据:google woff2 实现也是字节级转换,字体元数据不被修改;
- 不支持可变字体(Variable Fonts):如果输入是可变字体的 SFNT(带
gvar、HVAR、VVAR表),转换容器不会破坏可变字体特性,但 ZenConverter 不做任何可变字体相关的处理。
使用场景
| 场景 | 推荐 |
|---|---|
| 桌面设计稿用 TTF 字体,网页需要更小的字体 | TTF → WOFF2 |
| 从网站下载了 WOFF2 字体,想装到电脑或手机上 | WOFF2 → TTF |
| 想检查某个字体文件的 SFNT flavor(TrueType/OpenType) | 转一下,输出扩展名告诉你 |
相关页面
- 音频格式互转 —— 也是字节级容器互转(OPUS 只能 48kHz 倍数的故事和 WOFF2 的 arm64-only 故事类似)
- 本地优先就是隐私优先 —— 为什么字体转换也应该在本机做
