文件:DParts、split / merge 與廠商擴充
Document 模組處理的是整份 PDF 文件,而非頁面內容。它會建立文件部件階層,供受規範管控的工作流程附加中繼資料。它也會依頁碼範圍將 PDF 切分為多個片段、將多份 PDF 合併為一份,並在文件目錄中註冊開發者擴充。
composer require nextpdf/core:^3概念綜覽
標題為「概念綜覽」的區段此模組位於頁面內容之上的層級。Graphics 與 Content 會發出運算子,而 Document 則在結構層級運作——頁面樹、文件目錄,以及文件部件樹。
文件部件(DPart)是 PDF 中的一個邏輯分區。ISO 32000-2 定義 DPart 階層,其節點會攜帶文件部件中繼資料(DPM)。受規範管控的工作流程,例如製藥、法務或封存,可將中繼資料關聯到特定頁碼子範圍,而非整份檔案——§14.12。DPart 是一個不可變的 readonly 節點:葉節點參照一段連續的頁碼索引;中介節點則將子 DPart 節點群組成一棵樹。DPartRoot 是供 Writer 序列化的樹根。葉節點的 /Start 與 /End 項目是指向頁面物件的間接參照,而非頁碼索引整數——§14.12。DPart::resolveWithPageObjects() 會依據 Writer 提供的頁碼索引→物件編號對應表執行解析,並回傳 /Start(以及選用的 /End)參照形式。只有在對應表不可用的測試路徑上,它才會退回使用整數形式。
PdfMerger 與 PdfSplitter 是文件組合的介面層。PdfMerger 會合併數份輸入 PDF 的頁面物件、重新編號物件以避免衝突,並重建單一的頁面樹與交叉參照表。它產生的頁面樹是一個平衡的 Pages 節點,包含 Kids 與 Count,以及 PDF 為頁面樹節點定義的可繼承屬性模型——§7.7.3。PdfSplitter 則執行相反流程:它將頁碼範圍擷取為獨立的 SplitDocument 物件。PageRange 是兩者共同使用的值物件。它以 1 為起始基準,會驗證自身邊界,並提供 contains()、count() 與 toArray()。
VendorExtensionRegistry、ExtensionsDictionary 與 DeveloperExtensionEntry 用來建模文件目錄中的開發者擴充字典——也就是引擎用以宣告超出基礎規範之廠商擴充等級的機制。若同一廠商前綴發生衝突性重複註冊,此註冊表會以 VendorExtensionRegistryConflictException 予以拒絕。CollectionDictionary 與 CollectionSort 用來建模 PDF collection(可攜式收藏/文件集)的目錄項目。
API 介面
標題為「API 介面」的區段| 類別 | 主要方法 | 角色 |
|---|---|---|
DPart | isLeaf(), hasMetadata(), resolveWithPageObjects(), write() | 不可變的文件部件節點(@since 1.12.0) |
DPartRoot | isEmpty(), write() | Writer 序列化的 DPart 樹根(@since 1.12.0) |
PdfMerger | merge(array $pdfFiles, int $maxFiles = 100, int $maxTotalBytes = 200_000_000), append() | 具物件重新編號的多份 PDF 合併(@since 1.9.0) |
PdfSplitter | split(), splitEvery(), extractPages() | 依頁碼範圍切分為 SplitDocument(@since 1.9.0) |
PageRange | contains(int $page), count(), toArray() | 以 1 為起始基準的頁碼範圍值物件 |
MergeResult / SplitResult | isValid(), count(), document(), totalOutputSize() | 組合結果物件 |
VendorExtensionRegistry | 擴充註冊 | 開發者擴充註冊表(@since 2.2.0) |
ExtensionsDictionary | withEntry(), entries(), isEmpty(), toPdfDictionary() | 不可變的擴充字典建構器(@since 2.0.0) |
CollectionDictionary | toPdfDictionary() | 可攜式收藏的目錄項目(@since 2.0.0) |
執行 composer docs:generate-api-php -- --module=Document 即可取得完整的 PHPDoc 表。
程式碼範例——快速上手
標題為「程式碼範例——快速上手」的區段將 PDF 切分為單頁文件並檢視結果。
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Document\PageRange;use NextPDF\Document\PdfSplitter;
$splitter = new PdfSplitter();$result = $splitter->splitEvery(file_get_contents('/srv/in/report.pdf'), 1);
foreach (range(0, $result->count() - 1) as $index) { $segment = $result->document($index); file_put_contents("/srv/out/page-{$index}.pdf", $segment->pdfData);}
$singlePage = $splitter->extractPages( file_get_contents('/srv/in/report.pdf'), new PageRange(2, 4),);程式碼範例——正式環境
標題為「程式碼範例——正式環境」的區段在明確的輸入預算內合併數份 PDF,並在寫出合併輸出之前先檢查其有效性。
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Document\PdfMerger;use NextPDF\Exception\PageLayoutException;
/** @var list<string> $sources Raw PDF byte strings to combine. */$sources = array_map( static fn (string $path): string => file_get_contents($path), glob('/srv/batch/*.pdf') ?: [],);
$merger = new PdfMerger();
try { // Bound the merge: at most 50 files, 100 MB total. $merged = $merger->merge($sources, maxFiles: 50, maxTotalBytes: 100_000_000);} catch (PageLayoutException $e) { throw new \RuntimeException('Merge rejected: empty or invalid input set.', previous: $e);}
if (!$merged->isValid()) { throw new \RuntimeException('Merged document failed structural validation.');}
file_put_contents('/srv/out/combined.pdf', $merged->pdfData);邊界案例與陷阱
標題為「邊界案例與陷阱」的區段PdfMerger::merge()與PdfSplitter::split()透過ResourceGuard強制套用輸入邊界。檔案數量或大小超限的輸入會引發例外,而不會靜默截斷。請刻意針對你的工作負載調整maxFiles/maxTotalBytes。- 空的檔案清單或空的範圍清單會引發
PageLayoutException——這些是設定錯誤,而非空結果。 PageRange以 1 為起始基準,且包含端點。葉節點DPart的pages清單則是以 0 為起始基準的頁碼索引。這兩種抽象並不共用同一個索引基準。在兩者之間轉換時,請明確轉換。DPart是readonly的。若要建立不同的樹,就要建構新節點,而非變更既有節點。只有在頁面物件對應表為空時,resolveWithPageObjects()才會回傳以整數索引表示的退回形式。請勿在正式環境輸出中倚賴該路徑。- 對於重複的廠商前綴,
VendorExtensionRegistry會引發VendorExtensionRegistryConflictException。每個前綴只註冊一次。
切分與合併的成本與頁數呈線性關係,且主要受限於剖析與物件重新編號,而非模組本身的內部記錄維護。預設的參考工作負載落在 1500 ms 牆鐘時間 / 64 MB 峰值的預算之內。大型合併主要受限於輸入位元組總量。maxTotalBytes 防護機制是為了讓峰值記憶體保持有界。其可重現性設定檔為 structural:合併或切分後的 PDF 會帶有全新的 trailer 與 /ID,因此兩次執行在結構上相等,但並非逐位元組相同。
安全性說明
標題為「安全性說明」的區段PdfMerger::merge() 與 PdfSplitter::split() 會處理不可信的 PDF 位元組。兩者在剖析前都會先以 ResourceGuard::assertSize() / assertCount() 檢查輸入,藉此限制因解壓縮或物件數量放大造成的阻斷服務攻擊。請為部署環境將 maxFiles、maxTotalBytes 與 maxBytes 參數設得更嚴格,而非倚賴預設值。請將每一份輸入 PDF 都視為具敵意。當來源為使用者提供時,請在受限制的 worker 中執行批次組合。關於信任邊界,請參閱 /modules/core/security/ 中的引擎威脅模型。
一致性
標題為「一致性」的區段此模組所建立的 DPart 樹遵循 ISO 32000-2 §14.12 的文件部件模型,其葉節點的 /Start 與 /End 項目會依同一條款,以指向頁面物件的間接參照發出。合併輸出使用 §7.7.3 所定義的頁面樹節點結構。這些是由 src/Document/ 產生、並由 tests/Unit/Document/(DPartTest、DPartRootTest、DPartPageRefTest、DocumentPdfMergerDeepTest、DocumentPageRangeParseDeepTest)所驗證的實作事實。它們並非端到端 PDF 2.0 一致性的聲明。整份文件的一致性由 /modules/core/conformance/ 中所述的 oracle 與 golden 套件驗證。
另請參閱
標題為「另請參閱」的區段- Core 模組
- Writer 模組——序列化 DPart 樹與頁面樹。
- Metadata 模組——搭配 DPM 的 XMP。
- Navigation 模組
- 一致性綜覽
- 引擎安全性模型