跳转到内容

文字:塑形接缝、CJK 与文字段处理

文字模块提供的是塑形接缝:一个小型接口,用于把 UTF-8 文字段转换为定位后的字形;主机具备真正的 OpenType 后端时使用它,不可用时则退回确定性的回退路径;此外还提供一个注册表,用来登记特定 script 的塑形器。

Terminal window
composer require nextpdf/core:^3

ShaperInterface 是文字排版管线与 OpenType 塑形引擎之间的接缝。它刻意保持最小化:只有一个 shape() 方法,接收一个 ShaperInput 并返回一个 ShapingResult。返回类型是使用方唯一可见的输出——实现不得泄漏塑形引擎的内部细节,而强类型的返回值会在结构上强制这一点。ShapingResult 包含 GlyphRun 记录列表、返回的原始文字、script 与方向,以及一个 shaperImpl 标记,用来指出这个结果由哪个后端产生。

后端选择是显式的,并且会如实反映能力。ShaperFactory 会执行一次能力探测:如果主机具备可运行的 HarfBuzz 绑定,create() 就返回以 HarfBuzz 为后端的塑形器;否则返回 NullShaperNullShaper 是一个直通式的回退路径。它会为每个 Unicode 码位输出一个合成字形,且宽进量为零、偏移量为零。它会标记结果,让可观测性能够检测到这次回退。它把宽进量的解析(resolve)留给字体度量模块处理。这是一条有明确记录的降级路径,而不是完整塑形:替换、连字、标记定位与上下文字形都需要真正的后端才能完成。wouldUseRealShaper() 是一个诊断用判断式。生产环境代码应根据结果上的 shaperImpl 标记来分支。

特定 script 的塑形是一个 SPI,而不是内置实现。ScriptShaperRegistry 是一个 PSR-11 风格的注册表,会根据 ISO 15924 script 标记解析出 MongolianShaperInterfaceTibetanShaperInterface。注册表以不区分大小写的方式存储键值,并把 script 代码是否可接受的判断交由单一可信来源决定。注册表与 script 塑形器接口构成一份冻结的合约,因此扩展功能可以在不修改调用方的情况下注册一个 Phase-12 提供者。引擎本身只提供这道接缝,复杂 script 的提供者由使用方自行提供。

CJK 文字段处理位于排版的编码接缝上。嵌入的 CJK TrueType 字体会以 Type 0 字体输出,搭配 Identity-H CMap 与一个 CIDFontType2 后代字体——ISO 32000-2 §9.7.4(RAG 摘要因授权上限被截断;记录于 _downgraded-claims-o3.md)。嵌入 TrueType 程序时,Type 2 CIDFont 会通过 CIDToGIDMap 条目,把字符标识符映射到字形的 Index(索引)——ISO 32000-2 §9(摘要由 B1 合约页面固定)。子集化工具会精确保留原始字形编号,因此 /CIDToGIDMap /Identity 对子集而言仍然有效。CjkFontValidator 是一个诊断工具,会在选用某个候选字体之前,检查它是否覆盖某个 script 所需的 Unicode 区块。

类型种类主要成员稳定性起始版本
ShaperInterface接口(interface)shape(ShaperInput): ShapingResult稳定3.2.0
ShaperFactoryfinal classdefault()create()wouldUseRealShaper()稳定3.2.0
NullShaperfinal readonly class直通式的备援塑形器稳定3.2.0
ShapingResultfinal readonly class$glyphRuns$originalText$script$direction$shaperImpl稳定3.2.0
ScriptShaperRegistryfinal classregisterMongolian()getMongolian()hasMongolian(),以及对应的 Tibetan 版本稳定3.1.0
CjkFontValidatorfinal classvalidateCoverage()detectScript()isCjkCodepoint()稳定1.0.0

无论是 register*get*has* 形式,还是 ScriptShaperRegistry 与各 script 塑形器接口,都构成一份冻结的合约。按设计,ShapingResult 是塑形器唯一对使用方可见的输出。

examples/text/shaper-factory.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Font\Shaper\ShaperFactory;
use NextPDF\Font\Shaper\ShaperImpl;
$factory = ShaperFactory::default();
$shaper = $factory->create();
// Branch on the result tag, not on the concrete class.
$wouldShape = $factory->wouldUseRealShaper()
? 'HarfBuzz backend available'
: 'NullShaper fallback (degraded — no substitution or positioning)';
echo $wouldShape, "\n";

ShaperFactory::default() 会接入生产环境的能力探测。create() 会在工厂生命周期内缓存所选后端。能力状态的真实判断来自 wouldUseRealShaper(),以及每个结果上的 shaperImpl 标记。

examples/text/script-shaper-registry.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Text\Shaping\MongolianShaperInterface;
use NextPDF\Text\Shaping\ScriptShaperRegistry;
final readonly class ComplexScriptBootstrap
{
public function __construct(private ScriptShaperRegistry $registry) {}
/**
* Register a consumer-supplied Mongolian shaper provider at boot so
* the layout pipeline can resolve it by ISO 15924 script tag.
*/
public function register(MongolianShaperInterface $mongolian): void
{
$this->registry->registerMongolian($mongolian);
}
public function hasMongolian(): bool
{
return $this->registry->hasMongolian();
}
}

注册表是复杂 script 提供者的集成点。引擎提供这道接缝以及冻结的访问器形式。Mongolian 与 Tibetan 的实现则由使用方提供。

  • 一个 NullShaper 的结果,其宽进量为零、偏移量为零。不要把这些位置直接传给文字排版——请改为从字体度量模块解析宽进量,并通过 shaperImpl 标记检测这次回退。
  • 空输入会产生一个空的 glyphRuns 列表,而不是一个空文字段。使用方的迭代代码不需要特别处理长度为零的文字段。
  • ScriptShaperRegistry 没有直接实现 Psr\Container\ContainerInterface,这样强类型访问器才能在静态分析中保留收窄后的返回类型。请使用 getMongolian()getTibetan(),而不是通用的 get()
  • script 标记是以规范的 ISO 15924 alpha-4 值来比对,且以不区分大小写的方式存储。传入 MongTibt。查找时大小写无关紧要。
  • CJK 扩展 B 区字符位于 Unicode 第 2 平面,会要求子集中生成一个 cmap Format 12 子表。编码路径会处理这种情况。不要假设所有 CJK 内容都落在基本多文种平面内。

能力探测在每个 ShaperFactory 实例上只执行一次,且后端会被缓存,因此重复调用 create() 几乎没有成本。NullShaper 的成本与输入文字段的码位数呈线性关系,且不涉及 I/O。ScriptShaperRegistry 的解析是一次常数时间的键值查找。CjkFontValidator 会按固定间隔抽样码位,而不是逐一测试每一个码位,因此即使面对 20,000 个字形的 CJK 字体,覆盖率检查的成本仍然很低。1500 毫秒实际耗时与 64 MB 峰值用量的 performance_budget 足以涵盖一次典型执行。执行真实塑形时,主要成本来自 OpenType 后端本身;当回退启用时,这部分不属于本流程范围。

塑形接缝接收一个 UTF-8 字符串。NullShaper 会以尽力切分的方式容忍格式错误的 UTF-8,而不会抛出异常,因为这个回退路径的既定合约就是「不做真正的塑形」。调用方已预期会得到质量较低的输出。字节偏移的簇合约采用面向字节的长度,这对多字节输入而言是正确的,也避免了簇映射中差一个码位的缺陷。真正的后端(若存在)是一个第三方原生库。请把它的输入视为不可信,并在上游限制文字段长度。script 塑形器注册表存储的是使用方提供的提供者——这些实现的信任边界属于使用方,而非引擎。

宣称标准条款证据
嵌入的 CJK TrueType 字体会以 Type 0 字体输出,搭配 Identity-H CMap 与一个 CIDFontType2 后代字体。ISO 32000-2§9.7.4摘要因授权上限而被截断,此处仅保留前缀 7a5258772f508e3b,详见 _downgraded-claims-o3.md
嵌入的 Type 2 CIDFont 会通过 CIDToGIDMap 把字符标识符映射到字形索引。ISO 32000-2§9

两项条款均已改写。第二项以固定摘要为准(沿用自 B1 合约页面),第一项则由 ADR-013 与 cmap 编码器开发者概览佐证。NextPDF 不重现规范性文本。塑形后端与 PDF 符合性无关。此处的符合性宣称针对的是编码接缝所产生的 CJK 字体字典输出,ADR-013 与 cmap 编码器开发者概览中有进一步说明。

一套高级文字前处理管线与提取服务,构建在 Core 的塑形接缝与文字段处理值类型之上。Core 文字模块会提供这道接缝、回退路径与 script 塑形器注册表,且无需授权。省略转换链接是刻意的。