跳转到内容

字体:真正困难的部分

Evidence: Mixed evidence

字体是 PDF 最容易「看起来完全正确、实际上已经悄悄损坏」的地方。一个页面可以呈现正确的字形,却无法搜索、无法作为文本复制,也不符合归档配置文件。这些问题可能同时发生,而且没有任何可见迹象会提醒你。本页讨论的是必须做对的三件事——内嵌、子集化、编码——以及 NextPDF 分别如何处理它们。

「看起来没问题」是 PDF 工作中最危险的一句话,而字体正是这句话最容易造成严重后果的地方。有三件彼此独立的事情必须成立:

  1. 内嵌——字体程序随文件一起传递,因此在未安装该字体的机器上也能呈现相同结果。
  2. 子集化——只携带实际用到的字形,因此一个 20 MB 的 CJK 字体不会让每份文件臃肿。
  3. 编码——存在一份正确的映射,能从页面上的字符码回到 Unicode,因此文字可被搜索、复制、索引,并由辅助技术读取。

视觉呈现只能部分证明第一项。一份文件可以显示完美的字形,却完全无法满足第三项——这时文字只是文字的图像,而不是文字本身。这类错误能通过每一次「看起来没问题」的审查,随后却在合规审计或调阅请求中失败。

  • PDF 中的字体是一个字典,加上通常会有的一个内嵌字体程序流。
  • 子集化会重写该程序,使其只包含用到的字形。子集字体的名称会加上一个六个大写字母的标签与一个 +,让阅读器将其视为独立字体。
  • 编码是另一个独立问题:将字符码映射到 Unicode。一份 /ToUnicode CMap 是让文字可搜索、可复制的关键——而它独立于字形看起来是否正确。
  • 看起来正确、却没有(或带有错误)/ToUnicode 的文字,正是典型的无声失败:屏幕上完美,实际却无法搜索。
  • NextPDF 会对 TrueType 字体进行子集化、保留字形 ID 以确保正确呈现,并输出一份 /ToUnicode CMap 让提取正常工作——并且能强制执行 PDF 2.0 的内嵌规则,而不只是发出警告。

子集化。 FontSubsettersrc/Typography/FontSubsetter.php)会解析原始 TrueType 表目录,并读取 cmap,将 Unicode 码位映射到字形 ID。它同时处理 BMP 的格式 4 与全 Unicode 的格式 12,后者是 CJK 所需要的。接着它会执行朴素子集化工具容易漏掉的步骤:通过传递闭包解析复合字形依赖关系。由一个基底字母加上一个组合标记构成的重音字形,会以组件形式引用其他字形。若那些组件被丢弃,字形就会呈现错误。子集化工具会遍历该图,直到不再出现新组件,并设有循环防护,使格式错误的字体无法无限循环下去。

该文件中有两项值得一提的工程选择。第一,字形 ID 会被保留、而非重新映射——未使用的位置在 glyf/loca 中以零填补,因此内容流的原始字形索引在 CIDToGIDMap /Identity 下仍然有效。重新映射虽然可以更小,却需要重写每一处字形引用。保留 ID 才是这里正确的设计。第二,遍历是排序过的(依 gid 递增),因此子集在字节层级是确定性的——相同的字体与相同的已用字形会产生相同的子集字节,这正是可重现构建所要求的。若子集化所能节省的不到文件的约 10%,便会原封不动地返回原始字体。为了边际收益付出这份额外成本并不值得。

内嵌。 字体程序是否要随文件携带,由一项明确政策决定——绝不臆测。Pdf20FontEmbeddingPolicysrc/Writer/Pdf20FontEmbeddingPolicy.php)有两种模式。在 PDF 2.0 配置文件下,Strict 会以一个带类型的例外,拒绝未内嵌的标准 Type 1(「Base14」)引用——这是符合规范的正确行为。AllowBase14 则保留历史上的建议性路径。作为迁移窗口,它会输出标准仍要求的最小字体描述子,并发出一则警告,而非抛出例外。调用方必须在文件上明确做出此选择;引擎绝不会从字体推断得出。

编码。 对于复合(Type 0)字体,EmbeddedTtfFontDictBuildersrc/Writer/EmbeddedTtfFontDictBuilder.php)会输出 CIDFontType2 后代、Type0 父代,以及一份 /ToUnicode CMap 流,让字符码能解析回 Unicode。/ToUnicode 流只有在一种情况下可以正当地缺席:一份自我描述的预定义 CJK CMap 已经为阅读器提供了从字符到 Unicode 的映射。在这种情况下,该 CMap 就是编码,因此纯配置文件会省略多余的 /ToUnicode 流以节省字节。除此之外,/ToUnicode 流正是让文字维持为文字的关键。

考虑事项它保证的内容保证的内容出错时的无声失败
内嵌未安装字体时也能一致呈现文字可被搜索替换字体;在另一台机器上度量错误
子集化文件更小;只含用到的字形任何与编码相关的事缺少复合组件 → 重音字形损坏
编码(/ToUnicode可搜索、可复制、可访问的文字字形能正确呈现页面看似完美,却无法搜索/复制时呈乱码

这三项字体考量彼此独立。内嵌与子集化关乎的是 外观与大小;编码关乎的是意义。一个页面可以通过前两项,却在第三项失败,而没有任何可见之处能显示出来。

子集命名规则既是规范性的,也非常精确。 Spec: ISO 32000-2, §9.9.2 要求字体子集的 PostScript 名称——BaseFont 与描述子的 FontName—— 须以恰好六个大写字母的标签开头,接着一个加号, 再接原始字体的 PostScript 名称。它还要求同一文件中同一字体的不同子集必须使用不同的标签。正是这条规则让阅读器能分辨两个子集, 并正确合并文件。 Evidence: Standard-backed

编码与呈现分属不同的条款。 Spec: ISO 32000-2, §9.10.3 /ToUnicode 定义为一个流,内含一份将字符码映射到 Unicode 值的 CMap, 而后续的文字提取程序 Spec: ISO 32000-2, §9.10.2 使用该 CMap 将字符码转换为 Unicode,以供搜索与索引。在字形绘制机制中,没有任何部分会触及 /ToUnicode——这正是为什么文字可以看起来正确、提取却错误。

关于内嵌,标准指出大多数字体字典带有一个字体描述子,其中的内嵌字体文件流是可选,但强烈建议的。PDF 2.0 针对那十四个标准 Type 1 字体特别收紧了这一点。NextPDF 的 Strict 政策正是对该收紧要求的规范性解读。AllowBase14 则是明确、需主动选用的向后兼容出口——引擎绝不会无声降级。

Strict PDF 2.0 font-embedding enforcement — edition availability
Edition Availability
Core

可用。子集化、/ToUnicode 输出,以及明确的 Strict / AllowBase14 内嵌政策都是核心引擎行为。

Pro

在配置文件层级,围绕字体内嵌增加更深入的规范强制执行与报告。

Enterprise

在企业运营接口下增加相同的规范强制执行。

以下是一个正确内嵌、子集化且可搜索的复合字体的两个部分。子集标签遵循标准的六字母规则;/ToUnicode 引用则是让文字保持可提取的关键。

% The Type 0 (composite) font dictionary
20 0 obj
<< /Type /Font /Subtype /Type0
/BaseFont /ABCDEF+NotoSans % six-letter subset tag + '+'
/Encoding /Identity-H
/DescendantFonts [21 0 R]
/ToUnicode 23 0 R >> % the map that makes text searchable
endobj
% The descendant CIDFontType2 (carries the subsetted program)
21 0 obj
<< /Type /Font /Subtype /CIDFontType2
/BaseFont /ABCDEF+NotoSans
/CIDToGIDMap /Identity % glyph IDs preserved, not remapped
/FontDescriptor 22 0 R >>
endobj

对象 20 的 /ToUnicode 23 0 R 正是可搜索文件与图片化文件之间的差别。把它丢掉(在预定义 CMap 的情况之外),每个字形仍会完美绘制,但在页面上搜索任何词语都会一无所获。

直白地说,这个陷阱是:字形正确呈现,完全不代表文字就是文字。 呈现走的是从编码到字形的路径。搜索与复制走的是从码到 Unicode 的路径(/ToUnicode)。它们是读取字体字典中不同部分的不同机制。因此,一份文件可以有完美无瑕的视觉输出,却缺失 /ToUnicode 或带有错误的 /ToUnicode。结果就是一个看起来权威、实际上却无法搜索的页面——这种错误能逃过每一次视觉审查,因为按定义它就没有任何可见迹象。

另一个相关陷阱是:假设「字体已内嵌,所以我们的归档没问题」。内嵌是必要的,但并不充分。像 PDF/A 这样的配置文件还会要求子集依六字母规则命名,并具有正确的编码。已经内嵌但无法搜索,仍然失败。

NextPDF 的子集化工具具体来说是一个 TrueType 子集化工具。它需要必要的 TrueType 表;当这些表缺失,或收益低于约 10% 门槛时,会原封不动地返回原始字体。子集化与 /ToUnicode CMap 能让文字可提取,但无法挽救一个缺乏信息、无法将字形映射回有意义字符的来源字体。在无法判定任何 Unicode 值之处,再多的 CMap 输出也无法凭空造出一个值。

本页讨论的是在 NextPDF 所写的文件中生成正确的字体结构。它不是用于修复任意传入 PDF 的字体修复工具。而且,输出一个符合规范的子集与编码,本身并不会让文件通过完整归档配置文件的认证——那是另一项更广泛的检查。

为什么要六字母标签——为什么不用字体名称? 这样阅读器才能分辨同一字体的两个不同子集,并在合并文件时避免它们的字形集相互冲突。不同的子集,依规则使用不同的标签。

何时可以没有 /ToUnicode 当一份自我描述的预定义 CJK CMap 已经提供了字符到 Unicode 的映射时。在这种情况下,该 CMap 就是编码。再额外提供一份 /ToUnicode 将是多余的。除此之外,它的缺席就是一项缺陷。

子集化会造成伤害吗? 只有在做错时才会。丢弃复合字形的组件会破坏重音字形。重新映射字形 ID 却不重写引用,会破坏呈现。NextPDF 通过解析组件闭包并保留字形 ID,避免了这两种情况。

  • 内嵌字体程序——以流形式携带于 PDF 内部的实际字体文件(TrueType/CFF/Type 1),因此呈现不依赖阅读器已安装的字体。
  • 子集化——重写字体程序,使其只包含文件所用的字形,以缩减大小。
  • 子集标签——子集字体名称上强制要求的六个大写字母前缀加上 +(例如 ABCDEF+NotoSans)。
  • /ToUnicode——一份将字符码映射到 Unicode 值的 CMap 流;让 PDF 文字可搜索、可复制、可访问的关键。
  • 复合字形——通过引用其他字形作为组件而构成的字形;子集化时必须保留其组件。
  • CIDToGIDMap /Identity——一种模式,其中内容流的字形索引就是字体自身未变更的字形 ID;NextPDF 保留字形 ID 以维持其有效性。
  • Base14——那十四个标准 Type 1 字体;PDF 2.0 期望字体被内嵌,而非以名称引用。