契约 / 排版
排版领域包含字体登录表合约,以及一组文字预处理合约:FontRegistryInterface、TextPreprocessorInterface,以及不可变的 TextPreprocessResult 与 TextSegment 值对象。全部都是 stable(稳定)。
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 是不可逆的测量提示,只用于决定遮蔽矩形的大小。它绝不能用来重建原始内容。
API 接口
标题为“API 接口”的章节| 类型 | 类别 | 主要成员 | 稳定性 | 起始版本 |
|---|---|---|---|---|
FontRegistryInterface | 接口(interface) | register()、get()、has()、all()、addFontDirectory()、warmup()、lock()、isLocked()、registerBase14()、registerFromBinary()、memoryUsage() | 稳定 | 1.7.0 |
TextPreprocessorInterface | 接口(interface) | process(string): TextPreprocessResult | 稳定 | 1.9.0 |
TextPreprocessResult | final readonly class(最终只读类) | $segments、hasRedactions()、getDisplayText() | 稳定 | 1.9.0 |
TextSegment | 最终只读类 | $displayText、$isRedacted、$originalCharCount、$fillColor | 稳定 | 1.9.0 |
TextPreprocessResult 与 TextSegment 会冻结其构造函数签名与公共属性;可以新增方法,但属性不得变动。
代码示例——快速上手
标题为“代码示例——快速上手”的章节<?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 页面)。
代码示例——生产环境
标题为“代码示例——生产环境”的章节<?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 强制要求嵌入每一个字体。该符合性记录在提取与无障碍页面。
另请参阅
标题为“另请参阅”的章节- 合约:41 个公开接口(SPI)——SPI 总览与稳定度层级。
- 合约/Document——登录表在文件生命周期中的角色。
- 合约/安全性政策——
ExternalResourcePolicyInterface为未受信任的字体字节把关。 - 排版——文字塑形与排版模块。
- 字体——字体解析、子集化与嵌入。
- 文字——使用预处理器结果的文字输出。