跳到內容

文件:DParts、split / merge 與廠商擴充

Document 模組處理的是整份 PDF 文件,而非頁面內容。它會建立文件部件階層,供受規範管控的工作流程附加中繼資料。它也會依頁碼範圍將 PDF 切分為多個片段、將多份 PDF 合併為一份,並在文件目錄中註冊開發者擴充。

Terminal window
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)參照形式。只有在對應表不可用的測試路徑上,它才會退回使用整數形式。

PdfMergerPdfSplitter 是文件組合的介面層。PdfMerger 會合併數份輸入 PDF 的頁面物件、重新編號物件以避免衝突,並重建單一的頁面樹與交叉參照表。它產生的頁面樹是一個平衡的 Pages 節點,包含 KidsCount,以及 PDF 為頁面樹節點定義的可繼承屬性模型——§7.7.3。PdfSplitter 則執行相反流程:它將頁碼範圍擷取為獨立的 SplitDocument 物件。PageRange 是兩者共同使用的值物件。它以 1 為起始基準,會驗證自身邊界,並提供 contains()count()toArray()

VendorExtensionRegistryExtensionsDictionaryDeveloperExtensionEntry 用來建模文件目錄中的開發者擴充字典——也就是引擎用以宣告超出基礎規範之廠商擴充等級的機制。若同一廠商前綴發生衝突性重複註冊,此註冊表會以 VendorExtensionRegistryConflictException 予以拒絕。CollectionDictionaryCollectionSort 用來建模 PDF collection(可攜式收藏/文件集)的目錄項目。

類別主要方法角色
DPartisLeaf(), hasMetadata(), resolveWithPageObjects(), write()不可變的文件部件節點(@since 1.12.0
DPartRootisEmpty(), write()Writer 序列化的 DPart 樹根(@since 1.12.0
PdfMergermerge(array $pdfFiles, int $maxFiles = 100, int $maxTotalBytes = 200_000_000), append()具物件重新編號的多份 PDF 合併(@since 1.9.0
PdfSplittersplit(), splitEvery(), extractPages()依頁碼範圍切分為 SplitDocument@since 1.9.0
PageRangecontains(int $page), count(), toArray()以 1 為起始基準的頁碼範圍值物件
MergeResult / SplitResultisValid(), count(), document(), totalOutputSize()組合結果物件
VendorExtensionRegistry擴充註冊開發者擴充註冊表(@since 2.2.0
ExtensionsDictionarywithEntry(), entries(), isEmpty(), toPdfDictionary()不可變的擴充字典建構器(@since 2.0.0
CollectionDictionarytoPdfDictionary()可攜式收藏的目錄項目(@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 為起始基準,且包含端點。葉節點 DPartpages 清單則是以 0 為起始基準的頁碼索引。這兩種抽象並不共用同一個索引基準。在兩者之間轉換時,請明確轉換。
  • DPartreadonly 的。若要建立不同的樹,就要建構新節點,而非變更既有節點。只有在頁面物件對應表為空時,resolveWithPageObjects() 才會回傳以整數索引表示的退回形式。請勿在正式環境輸出中倚賴該路徑。
  • 對於重複的廠商前綴,VendorExtensionRegistry 會引發 VendorExtensionRegistryConflictException。每個前綴只註冊一次。

切分與合併的成本與頁數呈線性關係,且主要受限於剖析與物件重新編號,而非模組本身的內部記錄維護。預設的參考工作負載落在 1500 ms 牆鐘時間 / 64 MB 峰值的預算之內。大型合併主要受限於輸入位元組總量。maxTotalBytes 防護機制是為了讓峰值記憶體保持有界。其可重現性設定檔為 structural:合併或切分後的 PDF 會帶有全新的 trailer 與 /ID,因此兩次執行在結構上相等,但並非逐位元組相同。

PdfMerger::merge()PdfSplitter::split() 會處理不可信的 PDF 位元組。兩者在剖析前都會先以 ResourceGuard::assertSize() / assertCount() 檢查輸入,藉此限制因解壓縮或物件數量放大造成的阻斷服務攻擊。請為部署環境將 maxFilesmaxTotalBytesmaxBytes 參數設得更嚴格,而非倚賴預設值。請將每一份輸入 PDF 都視為具敵意。當來源為使用者提供時,請在受限制的 worker 中執行批次組合。關於信任邊界,請參閱 /modules/core/security/ 中的引擎威脅模型。

此模組所建立的 DPart 樹遵循 ISO 32000-2 §14.12 的文件部件模型,其葉節點的 /Start/End 項目會依同一條款,以指向頁面物件的間接參照發出。合併輸出使用 §7.7.3 所定義的頁面樹節點結構。這些是由 src/Document/ 產生、並由 tests/Unit/Document/DPartTestDPartRootTestDPartPageRefTestDocumentPdfMergerDeepTestDocumentPageRangeParseDeepTest)所驗證的實作事實。它們並非端到端 PDF 2.0 一致性的聲明。整份文件的一致性由 /modules/core/conformance/ 中所述的 oracle 與 golden 套件驗證。