合并两个字体的实战

最近在填坑“某系列标题生成器”项目,需要把 一个日文为主的字体(简称1.otf) 和 一个中文为主的字体(简称2.ttf) 合并成一个字体文件。踩了一堆坑,故记录。

目标

合并两个字体:

1
2
1.otf
2.ttf

目标:

  • 保留1.otf的字体(以日文为主)
  • 2.ttf补充字符(以中文为主)
  • 生成一个新的字体

字体的前置知识

字体文件后缀

常见的字体文件后缀有:

后缀 类型 说明
.ttf TrueType Font 最常见的字体格式之一
.otf OpenType Font TrueType 的扩展格式
.ttc TrueType Collection 多个字体打包在一个文件里
.otc OpenType Collection OpenType 的集合版本
.woff / .woff2 Web Open Font 用于网页

TTF

特点:

  • 使用 quadratic Bézier 曲线
  • 字形数据存储在 glyf 表中
  • 结构相对简单

TTF 的核心结构大致是:

1
2
3
4
5
glyf
loca
cmap
hmtx
maxp

由于结构比较直接,因此 比较容易用脚本修改

OTF

OTF 其实分为两种:

1
2
OTF (TrueType outlines)
OTF (CFF outlines)

区别在于 字形轮廓的存储方式

OTF (TrueType)

和 TTF 类似:

1
glyf

只是外层容器是 OpenType。

OTF (CFF)

使用:

1
CFF

表存储字形。

特点:

  • 使用 PostScript cubic Bézier 曲线
  • 字形存在 CFF
  • 结构比 TTF 更复杂

很多专业字体(尤其是 CJK 字体)使用这种结构。

Glyph(字形)

字体里的每一个字符都叫:glyph,例如:A、B、中、あ

每个 glyph 包含:

  • 轮廓
  • 字宽
  • unicode 映射

cmap

字体里的一个重要表:cmap

作用:Unicode → glyph

例如:

1
2
U+0041 → A
U+4E2D → 中

合并字体时,通常需要修改这个表。

CJK 字体的特殊结构

很多日文 / 中文字体不是普通 OTF,而是:

1
CID-keyed OpenType CFF

例如:

1
2
3
思源
小明朝
KozMin

这种字体结构是:

1
2
3
4
CID
FDArray
FDSelect
CharStrings

与普通字体不同:

普通字体:

1
glyphName → glyph

CID 字体:

1
CID → glyph

这也是很多字体工具无法直接合并它们的原因。

为什么 CJK 字体使用 CID

原因很简单:字符数量太多

例如:

1
2
GB2312     ~ 6000
Unicode CJK ~ 20000+

如果使用普通结构,字体会非常庞大。

CID 结构可以:

  • 压缩数据
  • 共享字形
  • 减少重复信息

因此 绝大多数东亚字体都采用这种结构

小结

在合并字体之前,需要注意:

  • .ttf 通常比较容易处理
  • .otf 可能包含 CFF 结构
  • CJK 字体往往是CID-keyed CFF
  • 这种字体很多工具无法直接 merge

这也是本文后面踩坑的主要原因。

准备环境

首先需要安装两个工具:

1
2
pip install afdko
pip install fonttools

分别来自:

  • Adobe Font Development Kit for OpenType
  • fontTools

第一次尝试:fonttools merge

最直接想到的方法是:

1
fonttools merge 1.otf 2.ttf

结果报错:

1
NotImplementedError: Merging CID-keyed CFF tables is not supported yet

原因:

1.otf属于:

1
CID-keyed OpenType CFF

这种字体结构与普通 OTF 不同:

1
2
3
4
CID
FDArray
FDSelect
CharStrings

fonttools 目前不支持合并这种字体。

第二次尝试:直接修改 CFF 表

尝试用 Python:

1
TTF glyph → CFF charstring

插入到字体里。

代码大致类似:

1
charStrings[new_name] = charstring

结果报错:

1
KeyError: 'uni0102'

原因:

CID 字体不是:

1
glyphName → glyph

而是:

1
CID → glyph

所以不能直接新增 glyph。

第三次尝试:AFDKO tx 转换字体

尝试使用 AFDKO 的 tx

1
2
tx -t1 font.otf > font.pfa
tx font.pfa -ttf > font.ttf

结果又失败:

1
bad font file

原因:

很多 CID CFF 字体无法直接转换为 Type1

第四次尝试:FontForge 转换

使用 FontForge:

1
OTF → TTF

虽然能生成 TTF,但会出现大量 warning:

1
2
3
Self Intersecting
Missing Points at Extrema
Duplicate Unicode

生成的ttf字体在windows系统里显示不是正确的字体。

第五次尝试:最终解决方案

最终采用的方法:

1
2
3
4
5
OTF → TTF

Python 合并 glyph

输出 merged.ttf

思路:

  1. 1.otf转换为 TTF
  2. 复制缺失的 glyph
  3. 更新 cmap / glyphOrder / metrics

转换命令

先安装库

1
pip install afdko

有直接转换的命令

1
otf2ttf 1.otf

会直接生成1.ttf

合并字体脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from fontTools.ttLib import TTFont
import copy

BASE_FONT = "1.ttf"
ADDON_FONT = "2.ttf"
OUTPUT_FONT = "merged.ttf"


def merge_fonts(base_path, addon_path, output_path):

base = TTFont(base_path)
addon = TTFont(addon_path)

base_cmap = base.getBestCmap()
addon_cmap = addon.getBestCmap()

base_glyf = base["glyf"]
addon_glyf = addon["glyf"]

base_hmtx = base["hmtx"]
addon_hmtx = addon["hmtx"]

glyph_order = list(base.getGlyphOrder())

added = 0

for uni, name in addon_cmap.items():

if uni in base_cmap:
continue

if name not in addon_glyf:
continue

# 避免重复 glyph
if name in base_glyf.glyphs:
continue

# deep copy glyph
base_glyf.glyphs[name] = copy.deepcopy(addon_glyf[name])

# copy metrics
base_hmtx.metrics[name] = addon_hmtx.metrics[name]

glyph_order.append(name)
base_cmap[uni] = name

added += 1

# 更新 glyph order
base.setGlyphOrder(glyph_order)
base_glyf.glyphOrder = glyph_order

# 更新 maxp
base["maxp"].numGlyphs = len(glyph_order)

print("Added glyphs:", added)

base.save(output_path)
print("Saved:", output_path)


if __name__ == "__main__":
merge_fonts(BASE_FONT, ADDON_FONT, OUTPUT_FONT)

注意事项

1 不要直接 merge 整个字体

否则可能把几万 glyph 合并进去。

通常只需要:

1
2
3
ASCII
Latin
符号

2 glyphOrder 必须同步

否则会报:

1
2
AssertionError
len(glyphOrder) == len(glyf)

3 必须更新 maxp.numGlyphs

否则字体保存会失败。

参考

  • fontTools
  • AFDKO
  • OpenType specification