跳转到内容

契约 / 排版

排版领域包含字体登录表合约,以及一组文字预处理合约:FontRegistryInterfaceTextPreprocessorInterface,以及不可变的 TextPreprocessResultTextSegment 值对象。全部都是 stable(稳定)。

Terminal window
composer require nextpdf/core:^3

FontRegistryInterface 是进程生命周期内的字体存储区。它会注册 TrueType、OpenType、TTC 或 PFB 字体,并返回解析后的 FontInfo 元数据。登录表的生命周期长于单个文件,因此一个 worker(工作进程)只需解析每个字体一次。它可以在启动时预热一批字体,随后锁定,使生产环境流量无法再修改它。已锁定的登录表会在调用 register()addFontDirectory()warmup() 时抛出 LogicException,但查询仍可使用。登录表也可以通过 registerFromBinary() 从原始二进制数据接收字体。@font-face 桥接会使用这个方法,注册从远程来源或 data URI 获取的字体。登录表只持有纯 PHP 数据,没有任何资源 handle,因此可以安全地在 worker pool 之间共享。

引擎会嵌入并子集化它用到的每一个字体。嵌入的字体程序会随 PDF 一起携带,因此文件在任何查看器中都会一致呈现,不受系统已安装字体影响——ISO 32000-2 §9。字体子集只携带文件实际引用到的字形(glyph),这对 CJK 或大量使用 Unicode 的内容尤其关键——ISO 32000-2 §9。登录表合约会公开解析后的元数据,供子集化与嵌入阶段使用。

TextPreprocessorInterface 会在文字进入字符排版、字体子集化、ToUnicode CMap 与结构树之前,先拦截文字。这个位置正是它的安全属性:负责遮蔽内容的预处理器,会在内容抵达内容流、字体子集或元数据之前先将其移除。该合约包含两项不变式。预处理器不得引入会影响排版的字符,而且必须保留逻辑阅读顺序;它的职责是内容替换,而不是排版。结果是不可变的 TextPreprocessResult,包含一份有序的 TextSegment 值列表。每个 segment 要么直接通过,要么已被遮蔽。对于已遮蔽的 segment,显示文字取决于遮蔽模式:黑框矩形为空字符串、与原始长度匹配的星号,或一个固定标签。segment 上的 originalCharCount 是不可逆的测量提示,只用于决定遮蔽矩形的大小。它绝不能用来重建原始内容。

类型类别主要成员稳定性起始版本
FontRegistryInterface接口(interface)register()get()has()all()addFontDirectory()warmup()lock()isLocked()registerBase14()registerFromBinary()memoryUsage()稳定1.7.0
TextPreprocessorInterface接口(interface)process(string): TextPreprocessResult稳定1.9.0
TextPreprocessResultfinal readonly class(最终只读类)$segmentshasRedactions()getDisplayText()稳定1.9.0
TextSegment最终只读类$displayText$isRedacted$originalCharCount$fillColor稳定1.9.0

TextPreprocessResultTextSegment 会冻结其构造函数签名与公共属性;可以新增方法,但属性不得变动。

examples/04-text-and-fonts.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();
$doc->addPage();
$doc->setFont('helvetica', 'B', 18);
$doc->cell(0, 12, 'Bold heading', newLine: true);
$doc->setFont('helvetica', '', 11);
$doc->multiCell(0, 7, 'Body text rendered with a registered font.');
$doc->save(__DIR__ . '/output/04-text-and-fonts.pdf');

setFont() 会通过 FontRegistryInterface resolve(解析)字体家族。独立文件会使用一个私有登录表。worker 则会共享同一个登录表(请见 document 页面)。

examples/contracts/typography-production.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\FontRegistryInterface;
use NextPDF\Contracts\TextPreprocessorInterface;
use NextPDF\Exception\NextPdfException;
use Psr\Log\LoggerInterface;
final readonly class FontWarmupService
{
public function __construct(
private FontRegistryInterface $fonts,
private TextPreprocessorInterface $preprocessor,
private LoggerInterface $logger,
) {}
/**
* Warm a font set at boot, then lock the registry.
*
* @param list<string> $fontFiles Absolute paths to font files.
*/
public function boot(array $fontFiles): void
{
try {
$this->fonts->warmup($fontFiles);
$this->fonts->lock();
} catch (NextPdfException $e) {
$this->logger->error('Font warmup failed', ['error' => $e->getMessage()]);
throw $e;
}
}
public function redact(string $text): string
{
$result = $this->preprocessor->process($text);
return $result->hasRedactions()
? $result->getDisplayText()
: $text;
}
}

warmup() 后接 lock(),就是 worker 的启动顺序。在 lock() 之后,变更操作会抛出异常;查询则会继续服务流量。

  • 已锁定的登录表会拒绝所有变更方法。请在启动时预热并锁定;切勿在处理请求期间调用 register()
  • registerFromBinary() 会把字体字节写入临时文件以便解析。未受信任的字体数据构成解析攻击面——请通过 ExternalResourcePolicyInterface 加以把关(请见 security-policy 页面)。
  • 任何 TextPreprocessor 都不得加入换行、回车字符或 tab。这样做会改变排版,并破坏合约的第一项不变式。
  • TextSegment::$originalCharCount 只是宽度提示。用它来推断原始内容会破坏遮蔽,并违反合约的第三项不变式。
  • TextPreprocessResult::getDisplayText() 对黑框 segment 按设计会返回空字符串。不要把空 segment 视为预处理失败。

字体解析主导首次使用成本;登录表会将这个成本摊销为每个进程只承担一次。预热之后,get()has() 都是 O(1) 的 map 查询。memoryUsage() 会返回一个 MemoryReport,让 worker 能按预算追踪字体缓存。文字预处理的成本与输入长度呈线性关系。segment 列表会增加有上限的额外开销,并与遮蔽匹配的数量成比例。1500 ms 墙钟时间与 64 MB 峰值的 performance_budget,足以覆盖一组典型字体的预热加上文件绘制。子集化成本会随实际用到的字符数量缩放,而不是随字体的完整字符表缩放。因此,子集化会降低 CJK 内容的输出大小与绘制成本。

排版领域有两个与安全性相关的方面。第一个是字体输入:registerFromBinary() 会解析任意字节。未受信任的字体数据必须先通过一个 ExternalResourcePolicyInterface,在抵达解析器之前限制文件大小与字符数量。第二个是遮蔽:TextPreprocessorInterface 被刻意安排在字符排版、字体子集化、ToUnicode CMap 与结构树之前,正是为了让已遮蔽的内容永不进入绘制后的成品。若遮蔽通过绘制时的覆盖层实现,原始文字会泄漏到内容流与子集中。合约的位置可防止这一类缺陷。segment 上的测量提示被刻意设计为不可逆。请把任何由外部提供的字体或文字都视为未受信任。

主张标准条款佐证
文件用到的每一个字体都会被嵌入,因此文件无需依赖系统字体即可呈现。ISO 32000-2§9
嵌入的字体会被子集化为文件引用到的那些字符。ISO 32000-2§9

两项条款皆为改写。NextPDF 不会复述规范性条文。PDF/A-4 强制要求嵌入每一个字体。该符合性记录在提取与无障碍页面。