跳到內容

Writer:PDF 2.0 序列化器與 xref

Writer 模組會將文件序列化為 PDF 位元組。它會選擇一套版本策略、寫出物件圖,接著輸出交互參照結構與 trailer。

Terminal window
composer require nextpdf/core:^3

PdfWriter 是進入點。write() 方法會接收一個 DocumentData 值物件,並以位元組字串回傳完整 PDF。寫入器會組裝物件圖、指派物件編號、記錄位元組位移,最後才寫出交互參照結構。

寫入器每次呼叫只會使用一套序列化策略。PdfSerializationStrategy 介面定義四個方法:writeHeader()getCatalogVersion()writeXrefAndTrailer(),以及 usesXrefStream()。共有三套策略實作此介面。Pdf20StreamStrategy 會寫出 %PDF-2.0 標頭、把 catalog 版本設為 /2.0,並輸出交互參照 streamPdf17TableStrategy 會寫出 %PDF-1.7 與傳統交互參照 tablePdf14TableStrategy 會寫出 %PDF-1.4 與交互參照表。PdfWriter 會透過一個 match(比對 DocumentData::$outputProfile)來挑選策略。預設使用 Pdf20StreamStrategy

這個 PdfOutputProfile enum 表示三種目標版本:Pdf20Pdf17,以及 Pdf14。這個 enum 對外提供 headerVersion()catalogVersion()allowsObjectStreams(),以及 usesXrefStream()。封存(archival)一致性模式會在挑選策略之前覆寫所選的設定檔。Pdf14FeatureGuard 會在設定檔為 Pdf14 時拒絕 PDF 2.0 的功能。

交互參照 stream 會將每個物件編號對映到其位元組位移(ISO 32000-2 §7)。增量更新會把新物件附加到檔案結尾(ISO 32000-2 §7.5.6)。寫入器會讓每個 literal 字串都經由正規的 PdfStringEscaper::escapeLiteral() 進行跳脫處理;此方法遵循 ISO 32000-2 §7.3.4.2 的規範性跳脫表(ADR-015)。

寫入器支援決定性輸出。setDeterministicMode() 會固定物件識別碼與字典鍵的順序。setReproducibleClock() 會固定文件的時間戳記。兩者都固定之後,固定輸入就會產生完全相同的輸出。writeChunked() 方法會回傳一個 generator,以固定大小的區塊逐段產生 PDF。Streaming/StreamingPdfWriter 會把文件一次一頁寫到呼叫端提供的 stream,適用於超出記憶體預算的文件。

Linearizer 會把已完成的 PDF 重寫成線性化的檔案配置。它會把第一頁放在前面,讓檢視器能在完整下載完成前先顯示該頁。shadowValidate() 會在不變動輸入的情況下檢查這次重寫結果。

注意: PdfWriter.phpLinearizer.php 對位元組位移與物件圖極為敏感(manifest 中的危險區)。在未通過 Writer golden 測試套件前,請勿變動物件編號或 xref 位移的運算。

類別主要方法角色
PdfWriterwrite(DocumentData): stringwriteChunked(DocumentData, int): GeneratorsetDeterministicMode()setReproducibleClock()setOutputColorProfile()getLastXrefOffset()getFileId()主要序列化器
PdfSerializationStrategy(介面)writeHeader()getCatalogVersion()writeXrefAndTrailer()usesXrefStream()版本策略合約
Pdf20StreamStrategywriteHeader()%PDF-2.0getCatalogVersion()/2.0usesXrefStream()truePDF 2.0 的 xref-stream 策略
Pdf17TableStrategywriteHeader()%PDF-1.7,xref 表PDF 1.7 的 xref-table 策略
Pdf14TableStrategywriteHeader()%PDF-1.4,xref 表PDF 1.4 的 xref-table 策略
PdfOutputProfile(enum)Pdf20Pdf17Pdf14headerVersion()catalogVersion()allowsObjectStreams()目標版本選擇器
PdfXrefWritergenerateFileId()finalizeTrailerAndXref()File ID 與 trailer/xref 最終化
Linearizerlinearize(string): stringshadowValidate(string): array快速網頁檢視(fast-web-view)的重寫
Streaming\StreamingPdfWriteropen()newPage()close()單次寫入的串流寫入器

執行 composer docs:generate-api-php -- --module=Writer 可以取得完整的 PHPDoc 表。

原始碼:examples/02-pdf-factory.php

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Writer\PdfWriter;
$writer = new PdfWriter();
$pdfBytes = $writer->write($documentData);
file_put_contents('out.pdf', $pdfBytes);

預設的設定檔是 PDF 2.0。輸出會以 %PDF-2.0 開頭,並以一個交互參照 stream 結尾。

這個範例會固定決定性模式與時鐘,以取得完全相同的輸出,並以固定大小的區塊串流輸出。

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use DateTimeImmutable;
use NextPDF\Writer\PdfWriter;
use NextPDF\Writer\ReproducibleClock;
$pinned = new DateTimeImmutable('2026-01-01T00:00:00Z');
$writer = new PdfWriter();
$writer->setDeterministicMode($pinned, 'nextpdf-fixed-file-id');
$writer->setReproducibleClock(new ReproducibleClock($pinned));
$out = fopen('php://output', 'wb');
foreach ($writer->writeChunked($documentData, chunkSize: 65536) as $chunk) {
fwrite($out, $chunk);
}
fclose($out);
  • 每次 write() 呼叫只會執行一套策略。寫入器每次呼叫都會依設定檔重設策略;先前呼叫的版本狀態不會帶到下一次呼叫。
  • 封存(archival)一致性模式會覆寫所要求的設定檔。PDF/A-3 的建構會強制使用 PDF 1.7。PDF/A-4 的建構會強制使用 PDF 2.0。
  • 要取得完全相同的輸出,需要兩個固定設定:同時設定決定性模式 以及 一個可重現的時鐘。只設定其中一個並不足夠。
  • writeChunked() 會產生一個 generator。請完整消費它。只讀取一部分會產生被截斷、無效的 PDF。
  • Linearizer 會重寫交互參照位移。在無法容忍重寫失敗的管線中,請先執行 shadowValidate()
  • Pdf14TableStrategyfinal readonly。PDF 1.4 路徑會透過 Pdf14FeatureGuard 拒絕 PDF 2.0 的功能,而不是將它們降級。

序列化的時間複雜度與物件數量及總位元組大小呈線性關係。交互參照 stream 會額外掃過物件表一次。writeChunked() 會保留已組裝好的文件,但以有界切片逐段產生輸出,因此尖峰記憶體是文件大小加上一個區塊。Streaming\StreamingPdfWriter 不會保留整份文件,它是處理超出記憶體預算之輸入時的路徑。參考工作負載的預算為 1500 ms 牆鐘時間與 64 MB 尖峰記憶體。線性化會增加第二次完整掃描與一次量測掃描。請為此明確編列預算。

寫入器序列化的對象,是受信任的記憶體內物件圖。主要威脅來自其輸入端。每個 literal 字串都會經過正規的 PdfStringEscaper::escapeLiteral()(ADR-015),因此內嵌的控制位元組無法從字串 token 逸出。加密是透過 PdfEncryptionWriter/Encrypt trailer 項目串接的。公開金鑰加密會透過明確例外被拒絕,不會靜默降級。決定性模式與可重現時鐘模式會從輸出中移除時間戳記與排序造成的旁路通道。文件的威脅模型與加密信任邊界,請參閱 /modules/core/security/ 一節。

Writer 會產生 PDF 2.0 的檔案結構:%PDF-2.0 標頭、/2.0 catalog 版本、一個交互參照 stream,以及依 ISO 32000-2 §7.3.4.2 跳脫表進行的 literal 字串跳脫。這些都是實作層面的事實。其佐證來自 src/Writer/Pdf20StreamStrategy.phpsrc/Writer/PdfSerializationStrategy.php,以及 src/Writer/PdfWriter.php 裡的策略選擇,並由 tests/Unit/Writer/(192 個測試,包含 Pdf20StreamStrategyPdfXrefWriterLinearizer* 測試套組)以及 tests/Golden/PdfWriter/PdfWriterGoldenBaselineSmokeTest 基準驗證。

並非 對完整 PDF 2.0 一致性的主張。完整的 ISO 32000-2 一致性,是完整文件經外部 oracle 驗證後才具備的屬性,不能只看序列化器本身。只有在 oracle 已確認的地方,才會主張端對端一致性:tests/Integration/Accessibility/VeraPdfUa2GoldenTest 會使用 veraPDF 對產生出的 fixture 進行 PDF/UA-2 驗證,tests/Standards/Profile/PdfRConformanceTest 則涵蓋 PDF/R 設定檔。若 runner 上沒有 veraPDF binary,這個 veraPDF golden 測試會被略過;因此它是選擇性啟用的 oracle gate,而非無條件執行。設定 VERAPDF_BINARY 即可執行它。封存設定檔的選擇(PDF/A-3 → PDF 1.7、PDF/A-4 → PDF 2.0)由 ADR-011 與一致性模式決定,並由 /modules/core/conformance/ 裡的一致性套組驗證。在那些有 oracle 背書的設定檔之外,請表述為 Writer「會產生 PDF 2.0 結構;其一致性由 veraPDF 針對 PDF/UA-2 設定檔驗證」,而非主張未經限定的一致性。