字体的战争与嵌入
4.1 为什么字体是 PDF 的核心难题
4.1.1 一个简单的思想实验
你在自己电脑上用"霞鹜文楷"字体写了一份文档,导出为 PDF,发给朋友。朋友的电脑上没装这个字体。
问题来了:朋友打开这份 PDF 时,看到的是什么?
答案取决于 PDF 生成器做了什么:
- 嵌入了字体:朋友看到的和你完全一样。PDF 里自带了字体数据。
- 只嵌入了字体子集:用到的字都正常显示,但只有文档中出现过的字符可用。
- 没嵌入字体:阅读器尝试用系统字体替代。运气好字形差不多,运气不好就面目全非。
字体嵌入是 PDF 实现"跨平台视觉一致"承诺的关键机制。没有它,PDF 和 HTML 一样——显示效果取决于接收端有什么字体。
4.1.2 字体文件有多大?
这也是字体嵌入不能"无脑全嵌"的原因:
| 字体 | 完整文件大小 |
|---|---|
| 英文字体(Inter Regular) | ~300 KB |
| 中文字体(思源黑体 Regular) | ~8 MB |
| 日文字体(Noto Sans JP) | ~5 MB |
| 数学字体(STIX Two Math) | ~1 MB |
一份中文文档如果嵌入完整字体,文件瞬间多 8MB。这就是为什么**子集化(subsetting)**是中日韩 PDF 的标配——只嵌入文档中实际用到的那些字形。比如一份文档用了 800 个不同汉字,子集化后只打包这 800 个,而不是全部 27000+。
4.2 字体到底是什么
4.2.1 一个字体文件里装着什么
你在电脑上安装一个 .ttf 或 .otf 文件,它就变成了一种"字体"。但这个文件里到底有什么?
本质上,一个字体文件是一个小型数据库,包含三类核心信息:
字形轮廓(glyph outlines):每个字符长什么样——用贝塞尔曲线描述的矢量图形。比如字母 'A' 的轮廓就是两条斜线、一条横线围成的封闭区域,用若干个控制点精确定义。
度量信息(metrics):每个字符占多宽、基线在哪里、字符之间的默认间距是多少。这是排版引擎用来决定"下一个字放在哪"的数据。
映射表(cmap):Unicode 码点和字形编号的对应关系——"Unicode U+0041 对应本字体中编号 36 的字形"(也就是 'A')。
除此之外还有元数据(字体名称、设计者、许可协议)、hinting 指令、kerning 对表、OpenType 高级特性(连字规则、上下文替换)等。
4.2.2 字形是怎么"设计"出来的
字体设计师的工作本质上就是用贝塞尔曲线画每一个字符的轮廓。
打开一个字体编辑器(如 Glyphs、FontForge、Robofont),界面和 Illustrator 很像——一个画布,上面是控制点和曲线。设计师逐点调整,控制每一笔的弧度、粗细、起落。
一个英文字体通常包含 200-500 个字形(26 个大写 + 26 个小写 + 数字 + 标点 + 各种变体和连字)。每个字形少则十几个控制点,多则上百个。
一个中文字体通常包含 6763(GB2312)到 27000+(GB18030)个字形。每个汉字的笔画比拉丁字母复杂得多——一个"龍"字可能需要几百个控制点。这就是为什么:
- 英文字体 300KB,中文字体 8MB——不是因为存储格式不同,而是因为字形数量差了 50 倍、每个字形的复杂度也高出好几倍
- 中文字体的设计周期是英文的 10-50 倍——一个人做一套英文字体可能几个月,做一套完整中文字体需要好几年(所以高质量中文字体很贵也很少)
4.2.3 矢量而非像素
字体存的是轮廓(矢量),不是像素图。这意味着同一个字形可以被渲染成任何大小而不会模糊——12px 的 'A' 和 120px 的 'A' 用的是同一份轮廓数据,只是缩放比例不同。
这和图片格式完全不同。如果字体用像素存储,每个字号都需要一套位图(12px 一套、14px 一套、16px 一套……),文件会大得离谱且无法自由缩放。
矢量轮廓 → 渲染为像素的过程叫栅格化(rasterization)。渲染器把贝塞尔曲线按目标分辨率转换为像素网格上的亮度值——这一步就是第三章讲的"画字"的物理实现。
4.2.4 回退(Fallback):缺字时怎么办
没有哪个字体包含世界上所有字符。Inter 只有拉丁字母和少量符号;思源黑体有中日韩字符但没有 Emoji;数学字体只有数学符号。
当系统或 PDF 阅读器遇到一个当前字体中不存在的字符时,会启动**字体回退(fallback)**机制:从一组预设的候选字体中找一个包含该字符的字体来渲染。
渲染 "Hello 你好 🎉"
→ "Hello" 用 Inter(当前字体有)
→ "你好" Inter 没有 → 回退到思源黑体
→ "🎉" 思源也没有 → 回退到 Emoji 字体
这就是为什么你偶尔会看到文档中某些字突然"变了一种风格"——那不是排版 bug,是回退到了另一种字体。
在 PDF 中,如果字体被嵌入了,就不存在回退问题——嵌入的字形数据直接包含在文件里。回退只在"字体没嵌入,系统也找不到对应字体"时才会触发,通常结果就是方块(□)或问号。
4.3 字体格式的三十年战争
4.3.1 Type 1:Adobe 的先手
1984 年,Adobe 随 PostScript 一起推出了 Type 1 字体格式。它用三次贝塞尔曲线描述字形轮廓,是 PostScript 和早期 PDF 的原生字体格式。
关键决策:Adobe 最初把 Type 1 的规范保密,只有付费授权的厂商才能制作 Type 1 字体。这引发了整个行业的愤怒——你的打印机只认 Adobe 的字体格式,字体厂商必须交保护费。
4.3.2 TrueType:Apple 和 Microsoft 的反击
1991 年,Apple 设计、Microsoft 参与推广的 TrueType 问世。这是对 Adobe 垄断的直接反击——完全开放的规范,任何人都可以免费制作 TrueType 字体。
技术上,TrueType 用二次贝塞尔曲线(3 个控制点)描述轮廓,而 Type 1 用三次(4 个控制点)。二次曲线计算更快但表达力弱一些——一条三次曲线可能需要两条二次的来近似。这是 1991 年硬件性能有限时的工程权衡。
4.3.3 Hinting:让字在小字号下依然清晰
一个有趣的技术细节:在 300 DPI 打印机上,12 号字母大约 50 像素高,轮廓曲线可以精确渲染。但在 96 DPI 屏幕上,同样的字只有 16 像素高——曲线必须"卡"到像素网格上,否则会模糊。
Hinting 就是告诉渲染器"在低分辨率下怎么调整"的一组指令:确保笔画对齐像素边界、保证竖线一样粗、防止小字号下细节消失。
TrueType 的 hinting 是一门完整的字节码语言(图灵完备的!)。这也是为什么同一份 PDF 在 Windows 和 macOS 上"感觉"不一样——Windows 严格执行 hinting 让小字更锐利,macOS 选择忽略大部分 hint 以保持曲线本身的"真实感"。
4.3.4 OpenType:大一统
1996 年,Microsoft 和 Adobe 握手言和,联合推出 OpenType。它本质上是一个容器格式——既能装 TrueType 轮廓(.ttf),也能装 Type 1/CFF 轮廓(.otf),统一了元数据和高级排版特性(连字、小型大写字母等)。
OpenType 结束了字体格式战争。你电脑上装的字体,绝大多数都是 OpenType 格式。
4.4 字体嵌入:PDF 怎么打包字体
4.4.1 三种嵌入级别
| 级别 | 文件体积影响 | 适用场景 |
|---|---|---|
| 完整嵌入 | 大(中文 8MB+) | 需要在 PDF 中编辑文字 |
| 子集嵌入 | 小(通常 50-200KB) | 只需显示,不需要编辑 |
| 不嵌入 | 无额外开销 | 仅引用系统字体名(有乱码风险) |
子集化后,字体在 PDF 中的名字会带一个前缀标记:ABCDEF+SimSun——加号前的 6 个随机大写字母表明这是一个子集,而非完整字体。
4.4.2 字体在 PDF 对象树中的位置
回忆第二章的知识:PDF 是一棵对象树。字体在树中的结构大致是:
Page 对象
└── Resources
└── Font
└── F1 → 字体对象
├── /BaseFont: BCDEAA+Times-Roman(字体名)
├── /Widths: [每个字符的宽度...]
├── /ToUnicode → CMap 映射表
└── /FontDescriptor → 字体描述
└── /FontFile → 嵌入的字体二进制数据
阅读器渲染文字时,顺着这棵树找到字体数据,解码出字形轮廓,再画到页面上。
4.4.3 CJK 字体的特殊挑战
中日韩字体比西文字体复杂得多:
- 字符集巨大:一个中文字体可能包含 27,000+ 字形,编码从单字节变为双字节
- 竖排文字:中文和日文有竖排模式,同一个字竖排时的位置偏移和横排不同
- 预定义映射:PDF 规范预定义了多组 CJK 字符映射(如
UniGB-UCS2-H对应简体中文),阅读器必须内置这些映射表
4.5 从 glyph 到 Unicode:那张救命的映射表
4.5.1 为什么需要映射
上一章讲过:PDF 内容流里画文字用的是 glyph ID(字体内部编号),不是 Unicode 字符。想让用户能复制文字、让搜索能找到内容,阅读器必须把 glyph ID 翻译回 Unicode。
这个翻译靠的是 PDF 中的 ToUnicode 映射表。它的本质很简单——一张对照表:
glyph 16 → U+0048 (H)
glyph 17 → U+0065 (e)
glyph 18 → U+006C (l)
glyph 19 → U+006F (o)
有了这张表,阅读器看到内容流里的 <0010> 就知道"这是字母 H"。没有这张表?那就只知道"画了一个形状",但不知道那个形状代表什么字——复制出来就是乱码。
4.5.2 映射链条
完整的链条是这样的:
- PDF 内容流里写着原始字节,比如
<0041> - Encoding / CMap 把这个字节翻译成字体内部的 glyph 编号——"0041 → 第 36 号字形"
- 字体数据(嵌入的或系统的)里存着第 36 号字形的贝塞尔曲线轮廓——渲染器加载它,画到页面上。到这一步,阅读就没问题了。
- ToUnicode 表把 glyph 编号翻译回 Unicode 字符——"第 36 号 → U+0041 → 字母 A"。这一步是复制和搜索的基础。
<0041> ──Encoding/CMap──→ glyph #36 ──字体数据──→ 画出字形 ✓ 可阅读
│
ToUnicode 表
│
↓
Unicode 'A' ✓ 可复制/搜索
关键点:渲染和文字提取走的是两条独立的路径。一份 PDF 可以渲染得完美无缺(字体数据在,字形画得出来),但复制出来全是乱码(ToUnicode 缺失,不知道那个字形"是什么字")。它们是分开的两件事。
4.6 乱码的根本原因
4.6.1 乱码分类表
| 你看到的症状 | 背后的原因 | 常见来源 |
|---|---|---|
| 方块(□□□) | 字体未嵌入,系统也没有 | 旧版生成器、非标准字体 |
| 乱字符(é±) | 编码解释错误(UTF-8 当 GBK 读) | 跨平台转换 |
| 空白/不可选 | 无 ToUnicode 且自定义编码 | 数学公式字体、老旧排版软件 |
| 所有字都变成一种字体 | 字体名冲突 | 合并多份 PDF 时 |
| 显示正常但复制乱码 | 渲染没问题但映射表缺失/错误 | 子集化 bug |
4.6.2 最常见场景:2000 年代的中文 PDF
2000 年代初的中文 PDF 几乎都不嵌入字体——8MB 的宋体在拨号时代是不可接受的。PDF 里只写了字体名 "SimSun",指望对方电脑有宋体。
Windows 用户打开没问题(系统自带宋体)。macOS 或 Linux 用户打开就崩了——没有 SimSun,阅读器只能用替代字体凑合,笔画粗细不对、间距不对、有些字显示不出。
现代生成器基本都做子集嵌入了,这个问题少多了。但互联网上几十亿份历史 PDF 还在。
4.6.3 用工具诊断
一行命令检查任何 PDF 的字体状况:
pdffonts document.pdf
输出中看三列:emb(是否嵌入)、sub(是否子集化)、uni(是否有 Unicode 映射)。任何一列是 no,就可能有问题。
4.7 为什么同一份 PDF "看起来"有细微差异
4.7.1 嵌入字体了还能不一样?
PDF 保证的是字形轮廓完全一致——坐标、大小、位置不会变。但最终把矢量轮廓变成屏幕像素时,还有几个因素影响观感:
- 抗锯齿算法:不同渲染器对曲线边缘的平滑策略不同
- 子像素渲染:Windows 的 ClearType 会利用 RGB 子像素增加水平分辨率,macOS 在 Retina 屏上已经不做这个了
- Hinting 执行策略:严格执行 = 锐利但不够"真",忽略 hint = 柔和但更忠于原始曲线
所以同一份 PDF 在 Windows 上看起来"锐利、瘦",在 macOS 上看起来"圆润、胖"——不是字体不对,是最后一步栅格化策略不同。
4.7.2 屏幕 vs 打印
在 600 DPI 打印输出上,hinting 几乎不重要——分辨率足够高,曲线本身就足够精确。但在 96 DPI 屏幕上,每个像素的决策都影响可读性。
这也是为什么设计师审稿要"打印后确认"——屏幕上看到的和打印出来的是两码事,尤其在小字号时差异明显。
4.8 现代趋势
4.8.1 可变字体
OpenType 1.8(2016)引入了可变字体(Variable Fonts)——一个文件包含连续的字重/字宽变化轴。传统方式需要 Regular、Bold、Light 各一个文件;可变字体一个文件搞定,字重 100-900 随意调。
但 PDF 对可变字体支持有限——大多数生成器嵌入时会把可变字体"固化"为某个特定实例(比如只取 weight=400 的版本)。可变的能力在 PDF 中丢失了。
4.8.2 Emoji 和彩色字体
现代字体可以包含彩色信息(用于 Emoji 和彩色图标)。但 PDF 的字体模型设计于 1990 年代——它假设字体是单色的。大多数 PDF 生成器碰到 Emoji 会直接渲染成图片嵌入,而不是使用彩色字体特性。这是 PDF 格式"年龄"的又一个体现。