跳转到内容

字体:值类型、嵌入与后备机制

在 NextPDF 中,字体由不可变的 FontInfo 值对象,以及决定引擎如何嵌入它的技术类型共同表示。引擎使用的每个字体都会被嵌入。旧式 Base 14 引用会后备到一个随附的、度量兼容的替代字体。

Terminal window
composer require nextpdf/core:^3

FontInfo 是单一的不可变值对象,包含引擎嵌入字体所需的一切:字族与样式、PostScript 名称、descriptor 标志、缩放到 1000 单位 em 的度量、字符宽度、glyph 到 Unicode 的映射、forward cmap(Unicode 到 glyph 识别码)、原始字体字节,以及在存在时的变化轴、命名实例、变化选择器、kern 配对与垂直度量。它是 final readonly。构造函数签名和公开属性都已冻结,因此解析后的字体就是一份稳定、可共享的事实数据。FontInfo::encodeText() 是唯一包含行为的方法。它会交由 encoding resolver 解析,并返回一个 EncodedGlyphRun

FontType 枚举了引擎会嵌入的各种技术:TrueType(单字节编码)、TrueTypeUnicode(面向 Unicode 字符丰富书写系统的多字节 CID 编码)、OpenType(Compact Font Format 轮廓)、Type1(PostScript Type 1,由 PFB 与 AFM 一对文件注册),以及 CidFont0(以 PostScript 为基础的 CID 字体)。解析器指派的类型会决定写入器输出的字体字典结构。

引擎会嵌入字体程序,让文档在任何查看器中都能一致呈现,而不依赖系统已安装的字体——ISO 32000-2 §9。TrueType 程序通过 FontFile2 字体 descriptor 条目嵌入,并且必须包含 glyfheadhheahmtxlocamaxp 等表——ISO 32000-2 §9.6.5(RAG 摘要因授权上限而截断;已记录于 _downgraded-claims-o3.md)。带有 Compact Font Format 轮廓表的 OpenType 程序则通过 FontFile3 嵌入——ISO 32000-2 §9.6.5(RAG 摘要已截断;见同一份日志)。subsetter 会精确重建这组必要表集合,因此嵌入的子集仍是一个符合规范的程序。

后备机制负责处理旧式 Base 14 场景。Base14SubstituteFonts 会将规范化后的 Base 14 键——helveticahelveticabtimescourier 以及其余各项——映射到随附的 Liberation Fonts 文件。Liberation Sans、Serif 与 Mono 分别与 Helvetica 或 Arial、Times Roman 以及 Courier 度量兼容。它们各自都是嵌入式 TrueType 字样,因此能呈现标准 14 字体参考所要求的完整 WinAnsiEncoding(Windows-1252)拉丁字集——带重音符号的拉丁字符、欧元符号,以及常见的排版标点(ISO 32000-2 Annex D.2)。SymbolZapfDingbats 没有授权宽松的度量兼容替代品,因此刻意不予替代;需要它们的文档必须注册一个可嵌入字体。这个 resolver 没有副作用——它只回答某个键会映射到哪个文件,除此之外什么也不做。向 registry 注册仍是调用方的责任,这样可以保留锁语义与 warmup 流水线。

类型类别主要成员稳定度自版本
FontInfofinal readonly class$family, $style, $type, $unitsPerEm, $widths, $unicodeMap, $cmapForward, $fileData, $variationAxes, $kernPairs, getKey(), encodeText()稳定1.0.0
FontTypeenum(字符串)TrueType, TrueTypeUnicode, OpenType, Type1, CidFont0稳定1.0.0
Base14SubstituteFontsfinal class(内部)规范化的 Base 14 键映射到随附的 Liberation 文件路径稳定2.7.0
ShaperFactoryfinal classdefault(), create(), wouldUseRealShaper()稳定3.2.0
ShapingResultfinal readonly class$glyphRuns, $originalText, $script, $direction, $shaperImpl稳定3.2.0

Base14SubstituteFonts 标记为 @internal——仅供 framework 内部使用,其接口不提供向后兼容保证。

examples/35-cjk-cmap-demo.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Typography\FontRegistry;
use NextPDF\Typography\FontType;
$registry = new FontRegistry();
$font = $registry->register('/path/to/NotoSansTC-Regular.ttf', alias: 'NotoSansTC');
// FontInfo is the immutable parsed fact about the face.
echo $font->family, ' / ', $font->type->value, "\n"; // e.g. "Noto Sans TC / TrueTypeUnicode"
assert($font->type === FontType::TrueTypeUnicode);

解析器会填充 FontInfo,并指派 FontType。带有 Unicode cmap 的 TrueType 字面量会变成 TrueTypeUnicode,写入器会将它输出为 Type 0 复合字体。

examples/font/base14-fallback.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Typography\Base14SubstituteFonts;
use NextPDF\Typography\FontRegistry;
final readonly class Base14EmbeddingResolver
{
public function __construct(private FontRegistry $registry) {}
/**
* Register an embeddable substitute for a legacy Base 14 key so the
* output document embeds every font (PDF/A-4 and PDF/UA-2 require it).
*/
public function ensureEmbeddable(string $base14Key): void
{
$path = Base14SubstituteFonts::resolve($base14Key);
if ($path === null) {
// Symbol / ZapfDingbats have no permissive substitute — the
// caller must supply its own embeddable font.
throw new \RuntimeException("No bundled substitute for {$base14Key}");
}
if (!$this->registry->has($base14Key)) {
$this->registry->register($path, alias: $base14Key);
}
}
}

这个 resolver 没有副作用。注册保持显式进行,这样 registry 的锁与 warmup 合约才会成立。SymbolZapfDingbats 按设计不会返回任何路径。

  • SymbolZapfDingbats 是刻意不予替代的。对这些键返回 null 是已记录的行为,不是缺少字体的 bug。
  • FontInfofinal readonly。把解析后的字体当成一个值看待:永远不要期望原地修改宽度或度量;如果来源变了,就重新注册。
  • 一个 Type 1 字体同时需要 PFB 轮廓与 AFM 度量。FontRegistry::registerType1() 接收这一对文件;自动发现会按扩展名从 PFB 路径推导出 AFM 路径。
  • FontType::TrueTypeFontType::TrueTypeUnicode 的差别在于单字节与多字节。encoding resolver 会根据已填入的 forward cmap 判断,而不是根据字族名称;因此 Unicode TrueType 字面量会自动走 Identity-H 路径。
  • 变化字体的轴与命名实例在存在时会被解析进 FontInfo,但这个 CJK 实现范例刻意使用静态字面量,以保持解析后的 FontInfo 具有确定性。

FontInfo 在每个进程中每个字体只由 registry 分配一次,之后都通过引用共享。它包含原始字体字节,这是主要的内存成本。一个 worker 应该只预热它所需要的字体,并跟踪 memoryUsage()。Base 14 替代 resolver 是一个常量时间的 map 查找,在调用方注册解析出的文件之前都不会有 I/O。performance_budget 设为 1500 ms 墙钟时间与 64 MB 峰值,涵盖典型字体集合的预热与渲染。在 subsetter 运行之前,每个字体的内存占用随字体文件大小而非 glyph 数量缩放。

FontInfo 本身只是被动数据——它是解析后的数据,除了纯粹的 encodeText() 转换之外没有其他行为。攻击面在上游,也就是解析阶段:任意字体字节会进入 TrueType 或 Type 1 解析器。解析器会对每个二进制偏移量做边界检查,并拒绝路径中的流包装器与 null 字节。不受信任的字体输入在注册之前,必须通过一个会限制大小与 glyph 数量的外部资源策略。随附的 Liberation 替代品是与软件包一起发布的受信任资产,因此后备路径不会引入任何新的不受信任输入。

主张标准条款佐证
文档使用的每个字体都会被嵌入,因此文档呈现时不必依赖系统字体。ISO 32000-2§9
TrueType 程序通过 FontFile2 嵌入,并带有 glyfheadhheahmtxlocamaxp 等表。ISO 32000-2§9.6.5RAG 摘要因授权上限而截断,完整值仅保存于内部 localization 工件;前缀为 7b26f37996239b2a,见 _downgraded-claims-o3.md
OpenType(CFF)程序通过 FontFile3 嵌入。ISO 32000-2§9.6.5RAG 摘要因授权上限而截断,完整值仅保存于内部 localization 工件;前缀为 801549ee00623baf,见 _downgraded-claims-o3.md

第一个条款来自摘要钉选,并由 B1 佐证。FontFile2 与 FontFile3 两个条款是改写表述。它们完整的 RAG 摘要并未返回(因授权上限截断),而是由 FontSubsetter(它会精确重建这组 glyf/head/hhea/hmtx/loca/maxp 集合)以及 FontType enum 佐证。NextPDF 不会重现规范性文本。Base14SubstituteFonts 在源代码中引用 ISO 32000-2 §9.6.2.2(标准 Type 1 字体处理)、ISO 14289-2:2024 §8.4.5.5.1(PDF/UA-2 字体嵌入),以及 ISO 19005-4:2020 §6.3.5(PDF/A-4 字体嵌入)。无障碍与规范符合性页面承载完整的规范配置文件符合性。

一个商业字体授权套件与一个动态 subsetting 服务,构建在 Core 的 FontInfo 与 registry 之上。Core 字体模块在没有授权的情况下也能嵌入、subset 与后备。按设计不放置转换链接。