跳到內容

ContentStream:PDF content-stream 輸出器

ContentStream 模組負責輸出標記內容(marked content)運算子。它會開啟與關閉結構標籤與 artifact、追蹤巢狀深度,並回傳運算子緩衝區。

Terminal window
composer require nextpdf/core:^3

ContentStreamBuilder 是此模組唯一的類別,負責建構頁面 content stream 的標記內容層。content stream 會依 ISO 32000-2 §8,將頁面內容編碼成一連串運算子;builder 則會在該內容外圍輸出標記內容運算子。

append() 會原封不動地附加原始運算子位元組。builder 不會對這段輸入做跳脫處理;有效性由呼叫端自行負責。這也是 HTML pipeline 與 Graphics 模組交錯插入各自運算子的位置。

beginTag() 會開啟一個帶有結構標籤的序列。它會依 ISO 32000-2 §14.6 輸出一個 BDC 運算子,並附帶一份 MCID 屬性清單。endTag() 會輸出對應的 EMC 運算子。builder 會計算巢狀深度。若在沒有開啟序列的情況下呼叫 endTag(),會擲出 PageLayoutException,而不會寫出不成對的 EMC

beginArtifact() 會開啟一個 artifact 序列。artifact 用來承載分頁裝飾——頁首、頁尾、頁碼、分隔線——這些內容依 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(): voidEMC 關閉最內層序列
beginArtifact()型別多載 beginArtifact(ArtifactSubtype|string $type): void 接受 enum 或字串開啟一個 artifact 序列
endArtifact()endArtifact(): void關閉最內層的 artifact
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();

這段程式會將一段文字內容包進結構標籤,並將頁尾包進 artifact。它會透過 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()
  • 深度計數器不會區分標籤與 artifact。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 產生 artifact 序列。這些都是實作事實。佐證來源包括 src/ContentStream/ContentStreamBuilder.phpsrc/Accessibility/ArtifactSubtype.php enum,以及 tests/Unit/ContentStream/ContentStreamBuilderMarkedContentBalanceCoverageTest 加上 ContentStreamBuilderRelabelTagInvariantTest。這並非端對端 PDF/UA-2 或 PDF 2.0 符合性的主張。這些運算子所參與的標記式 PDF 結構,是由外部 oracle 驗證的:tests/Integration/Accessibility/VeraPdfUa2GoldenTest 會針對 PDF/UA-2 規範,以 veraPDF 檢查一份產生的 fixture。該 oracle 測試會在找不到 veraPDF 執行檔時略過,因此它是一個選擇性啟用的 gate。請陳述此模組「會產生標記內容結構;PDF/UA-2 符合性由 veraPDF 驗證」,而不要主張無條件的符合性。