跳转到内容

ContentStream:PDF 内容流发射器

ContentStream 模块负责发射标记内容运算符。它会打开和关闭结构标签与构件、跟踪嵌套深度,并返回运算符缓冲区。

Terminal window
composer require nextpdf/core:^3

ContentStreamBuilder 是此模块中唯一的类。它负责构建页面内容流的标记内容层。内容流会把页面内容编码成一连串运算符——ISO 32000-2 §8。builder 会在这些内容外围发射标记内容运算符。

append() 会按原样追加原始运算符字节。builder 不会对这段输入做转义处理,其有效性由调用方自行负责。这正是 HTML pipeline 与 Graphics 模块交错插入各自运算符的位置。

beginTag() 会打开一个带结构标签的序列。它会发射一个 BDC 运算符,并附带一份 MCID 属性列表,依照 ISO 32000-2 §14.6。endTag() 会发射对应的 EMC 运算符。builder 会维护嵌套深度。若在没有打开序列的情况下调用 endTag(),会抛出 PageLayoutException,而不会写出未配对的 EMC

beginArtifact() 会打开一个构件序列。构件用来承载分页装饰——页眉、页脚、页码、分隔线——这些内容必须排除在结构树之外,依照 ISO 32000-2 §14.8.2.2。子类型是四个 ISO 值之一:PaginationLayoutPageBackground。建议优先使用强类型的 ArtifactSubtype enum。字符串重载会通过该 enum 进行校验,因此非标准值会立即导致调用失败。

relabelTag() 会就地改写先前已发射的标签。finish() 会返回完整缓冲区,若标记内容未配对则抛出异常。drain() 会返回截至目前的缓冲区,但不做配对检查,适用于增量流式处理。peek() 会返回缓冲区,但不消耗它。reset() 会清除状态。

方法签名角色
append()append(string $raw): void按原样追加原始运算符字节(不做转义)
beginTag()beginTag(string $structType, int $mcid): void打开一个 BDC 结构序列
endTag()endTag(): void使用 EMC 关闭最内层序列
beginArtifact()beginArtifact(ArtifactSubtype|string $type): void打开一个构件序列
endArtifact()endArtifact(): void关闭最内层的构件
getMarkedContentDepth()getMarkedContentDepth(): int返回当前的嵌套深度
relabelTag()relabelTag(string $old, string $new, int $mcid): void就地改写已发射的标签
finish()finish(): string返回完整缓冲区;若未配对则抛出异常
drain()drain(): string返回缓冲区,但不做配对检查
peek()peek(): string返回缓冲区,但不消耗它
reset()reset(): void清除所有状态

运行 composer docs:generate-api-php -- --module=ContentStream 可生成完整的 PHPDoc 表格。

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\ContentStream\ContentStreamBuilder;
$builder = new ContentStreamBuilder();
$builder->beginTag('P', mcid: 0);
$builder->append("BT /F1 12 Tf 72 720 Td (Hello) Tj ET\n");
$builder->endTag();
$pageContent = $builder->finish();

下面的代码会将一段文本内容包入结构标签,并将页脚包入构件。它会通过 drain() 以增量方式流式输出缓冲区。

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Accessibility\ArtifactSubtype;
use NextPDF\ContentStream\ContentStreamBuilder;
$builder = new ContentStreamBuilder();
$builder->beginTag('H1', mcid: 0);
$builder->append($titleOperators);
$builder->endTag();
$builder->beginArtifact(ArtifactSubtype::Pagination);
$builder->append($footerOperators);
$builder->endArtifact();
if ($builder->getMarkedContentDepth() !== 0) {
throw new RuntimeException('Unbalanced marked content before flush.');
}
$chunk = $builder->drain();
  • append() 不会转义输入。只能传入有效的运算符字节;builder 信任调用方。
  • endTag()endArtifact() 会在发生下溢时抛出异常。绝不可关闭一个尚未打开的序列。
  • finish() 会检查是否配对,并在深度不为零时抛出异常。drain() 不会检查。请仅在增量流式处理时使用 drain()
  • 深度计数器不会区分标签与构件。EMC 会关闭二者之中最内层的序列。请严格按顺序嵌套它们。
  • 传给 beginArtifact() 的字符串重载会通过该 enum 进行校验。非标准的子类型会在调用时失败,而不会出现在输出中。
  • relabelTag() 会改写已发射的标签。请使用与最初发射时相同的 mcid

每个操作都是 O(1) 字符串追加,relabelTag() 则是 O(buffer) 的改写。此模块只持有一个字符串缓冲区和一个整数深度计数器。除缓冲区本身外,不执行任何解析,也不做额外内存分配。参考工作负载的预算为 1500 ms 墙钟时间和 64 MB 峰值内存。此模块远低于该预算。

append() 是信任边界。builder 会按原样写出字节,因此上游代码必须转义任何会送入字面字符串运算符的字符串。标准转义工具是 PdfStringEscaper::escapeLiteral()(ADR-015)。绝不可通过 append() 传入未转义的用户文本。endTag()endArtifact()finish() 中的配对检查,可防止格式错误的标记内容树传到 Writer。文档的威胁模型请参阅 /modules/core/security/ 一节。

此模块发射的标记内容运算符结构符合 ISO 32000-2:按 §14.6 生成 BDC/EMC 配对,并附带一份 MCID 属性列表;按 §14.8.2.2 生成构件序列。这些属于实现事实。佐证包括 src/ContentStream/ContentStreamBuilder.phpsrc/Accessibility/ArtifactSubtype.php enum,以及 tests/Unit/ContentStream/ContentStreamBuilderMarkedContentBalanceCoverageTestContentStreamBuilderRelabelTagInvariantTest。它们并不是端到端 PDF/UA-2 或 PDF 2.0 符合性的主张。这些运算符参与形成的标记式 PDF 结构,由外部 oracle 校验:tests/Integration/Accessibility/VeraPdfUa2GoldenTest 会针对 PDF/UA-2 规范,用 veraPDF 检查一份生成的 fixture。该 oracle 测试在找不到 veraPDF 可执行文件时会跳过,因此它是可选启用的 gate。请陈述此模块「会生成标记内容结构;PDF/UA-2 符合性由 veraPDF 校验」,而不要主张无条件的符合性。