合併外部 PDF,或附加既有文件的頁面
當磁碟上有多個 PDF 檔案,而你只需要一份 PDF 時,可以使用這則範例。這則範例使用 Core 的合併介面 NextPDF\Document\PdfMerger,把多份既有文件依序合併起來。你傳入的是原始 PDF 位元組字串。合併器會重新編號每個物件以避免衝突,建立單一頁面樹與單一交叉參照表,並回傳一個 NextPDF\Document\MergeResult,讓你寫入磁碟或串流至用戶端。
同一個介面涵蓋開發者最常用到的三項任務:
- 合併(Merge) 將一份排序好的 PDF 清單合併為單一文件。
- 附加(Append) 將第二份 PDF 接到一份基礎 PDF 之後。
- 前置(Prepend) 頁面:只要把新文件放在輸入順序的最前面即可。
合併會在處理程序內進行,不啟動 headless 瀏覽器,也不發出網路呼叫。你需要安裝 Core(composer require nextpdf/core:^3),以及兩個以上可讀取的 PDF 檔案。
composer require nextpdf/core:^3概念總覽
標題為「概念總覽」的區段PDF 會把頁面組織在一棵頁面樹中,其根節點是一個 /Pages 節點,並透過交叉參照表定位每個間接物件。當你合併兩份來源文件時,它們的物件編號會重疊。兩個檔案幾乎一定都含有一個 1 0 obj 物件、一個 /Catalog,以及一個 /Pages 節點。直接把這些位元組串接起來會產生一個損毀的檔案,因為那些參照不再指向該編號應對應的位置。
PdfMerger 解決了這個問題。它會從每個輸入擷取頁面物件,把每個物件重新編號到單一位址空間,改寫每個頁面的 /Parent 參照,使其指向單一已合併的 /Pages 節點,並輸出單一型錄、單一頁面樹與單一尾段。輸出的是一份結構上全新的文件,而不是把檔案硬接在一起的串接結果。
排序規則很單純:頁面會依其來源檔案在輸入清單中出現的順序排列。若要附加,把基礎文件放在最前面。若要前置,把新文件放在最前面。沒有獨立的前置方法,因為輸入順序就是你唯一需要的控制方式。
API 介面
標題為「API 介面」的區段new NextPDF\Document\PdfMerger() 提供兩個方法。
merge(list<string> $pdfFiles, int $maxFiles = 100, int $maxTotalBytes = 200_000_000): MergeResult會合併一份排序好的原始 PDF 位元組字串清單。這兩個界限參數會限制檔案數量與輸入總大小。兩者都預設為安全的正式環境值,而你可以依個別工作負載收緊它們。append(string $basePdf, string $appendPdf): MergeResult是一個便利包裝方法,會依序合併剛好兩份文件。它等同於merge([$basePdf, $appendPdf])。
兩者都會回傳一個 NextPDF\Document\MergeResult。它是一個 readonly 物件,包含 $pdfData(已合併的位元組)、$totalPages、$sourceCount、$mergedSize,以及 isValid() 輔助方法,用來確認輸出是否以 %PDF 標頭開頭。
輸入是原始位元組字串,而不是檔案路徑。你要自己用 file_get_contents() 讀取檔案(或從物件儲存體取出位元組)。這讓合併器不受檔案系統假設限制,也讓你能合併完全不落地到磁碟的文件。
如果你需要把外部 PDF 的單一頁面匯入成可重複使用的 Form XObject——例如,把信頭頁印在產生的內容背後——請使用跨套件匯入器契約 NextPDF\Contracts\ImportedFormObjectInterface,它由 nextpdf/artisan 之類的匯入器實作。至於整份文件與整頁的組合,則由這裡記載的 PdfMerger 介面處理。
程式碼範例——快速上手
標題為「程式碼範例——快速上手」的區段這段程式會讀取兩個檔案,並寫出它們的合併結果。它略過了錯誤處理,以呈現呼叫方式;下方的正式環境範例會補上完整防護。
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Document\PdfMerger;
$merger = new PdfMerger();
$result = $merger->merge([ file_get_contents(__DIR__ . '/cover.pdf'), file_get_contents(__DIR__ . '/body.pdf'), file_get_contents(__DIR__ . '/appendix.pdf'),]);
file_put_contents(__DIR__ . '/combined.pdf', $result->pdfData);
printf("Merged %d source(s) into %d page(s).\n", $result->sourceCount, $result->totalPages);程式碼範例——正式環境
標題為「程式碼範例——正式環境」的區段這是一支可獨立執行的程式。它在記憶體中建立兩份小型文件,所以執行時不需要任何外部檔案;接著合併它們、驗證結果,並寫出輸出。它會捕捉合併介面拋出的那兩個例外,為每一個補上情境後重新拋出,而不是把它吞掉。把記憶體中的輸入換成你自己的 file_get_contents() 讀取(或物件儲存體擷取),再把輸出接到你的回應或儲存層。
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Document\MergeResult;use NextPDF\Document\PdfMerger;use NextPDF\Exception\PageLayoutException;use NextPDF\Exception\WriterException;
/** * Build a tiny labelled PDF so the program is self-contained. * * In your own code, replace calls to this helper with reads of the external * PDFs you want to combine, for example file_get_contents($path). */function buildSample(string $label, int $pages): string{ $doc = Document::createStandalone(); $doc->setTitle($label);
for ($page = 1; $page <= $pages; $page++) { $doc->addPage(); $doc->setFont('helvetica', '', 12); $doc->cell(0, 10, sprintf('%s - page %d', $label, $page), newLine: true); }
return $doc->getPdfData();}
// Validate the input set before touching the merger. An empty set is a// configuration error, not an empty success./** @var list<string> $sources Raw PDF byte strings, in output order. */$sources = [ buildSample('Cover', 1), // first in the list -> first in the output (prepend position) buildSample('Body', 2), buildSample('Appendix', 1), // last in the list -> appended after the body];
if ($sources === []) { throw new RuntimeException('No source PDFs supplied to merge.');}
$merger = new PdfMerger();
try { // Bound the merge deliberately: at most 50 files, 100 MB total input. $result = $merger->merge($sources, maxFiles: 50, maxTotalBytes: 100_000_000);} catch (PageLayoutException $e) { // Raised when the list is empty or an input does not begin with %PDF. throw new RuntimeException( sprintf('Merge rejected an input: %s', $e->getConstraint()), previous: $e, );} catch (WriterException $e) { // Raised when the total input size exceeds the configured byte cap. throw new RuntimeException( sprintf('Merge exceeded its size budget at stage "%s".', $e->getWriterState()), previous: $e, );}
if (!$result->isValid()) { throw new RuntimeException('Merged output failed its structural header check.');}
emitResult($result);
/** * Write the merged document to the cookbook side-channel, or to a default file. */function emitResult(MergeResult $result): void{ printf( "Merged %d source(s) into %d page(s), %d bytes.\n", $result->sourceCount, $result->totalPages, $result->mergedSize, );
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT'); $path = $out !== false && $out !== '' ? $out : __DIR__ . '/combined.pdf';
if (file_put_contents($path, $result->pdfData) === false) { throw new RuntimeException(sprintf('Could not write merged PDF to "%s".', $path)); }}預期的 STDOUT(頁面總數為各來源頁數的總和,而位元組大小則取決於建置過程):
Merged 3 source(s) into 4 page(s), <n> bytes.邊界情況與陷阱
標題為「邊界情況與陷阱」的區段- 輸入是位元組,不是路徑。
merge()接受的是原始 PDF 字串。請先用file_get_contents()讀取檔案。傳入路徑字串會讓該輸入無法通過%PDF標頭檢查,並拋出PageLayoutException。 - 順序即輸出順序。 頁面會依其來源檔案在清單中出現的順序排列。沒有前置方法:要前置就把新文件放在最前面,要附加就放在最後面。
- 空清單是錯誤。 空的
$pdfFiles會拋出PageLayoutException,而不是回傳空結果。請在呼叫前先驗證這組輸入。 - 每個輸入都會先經過驗證。 每一筆都必須非空,且以
%PDF開頭。第一個未通過的輸入會拋出PageLayoutException,並帶上被違反的限制條件,且不會合併任何內容。 - 超出界限時拋出例外,而非截斷。 超過
maxFiles會透過內部資源防護拋出例外,而超過maxTotalBytes則拋出WriterException。合併器絕不會默默丟棄檔案或裁切位元組,所以請為你的工作負載調整這兩個界限。 - 輸出在結構上是全新的,而非位元組穩定。 已合併的文件會帶有新的型錄、頁面樹與尾段。對同一組輸入執行兩次,結果在結構上相等,但不保證位元組完全相同,因此這則範例宣告的是
structural可重現性設定檔。 - 頁面層級的註解與共用資源。 合併會把頁面物件組合到單一頁面樹中。在來源檔案中、存在於頁面物件之外的文件層級結構,不會一併帶入。當你需要把單一頁面連同其資源匯入成可重複使用的圖形時,請走
ImportedFormObjectInterface路徑,透過nextpdf/artisan之類的匯入器處理。
合併的時間複雜度與總頁數成線性關係,且主要由剖析與物件重新編號主導,而不是合併器自身的管理開銷。尖峰記憶體會隨輸入位元組總量增加,因為組裝輸出時,每個來源都會以字串形式留在記憶體中。maxTotalBytes 防護會把這個尖峰控制在界限內。對於高流量管線,請把 maxFiles 與 maxTotalBytes 設成你的工作負載所需的最小值,讓畸形或過大的批次快速失敗,而不是耗盡記憶體。典型的小型合併會落在 1500 ms 牆鐘時間與 64 MB 尖峰的預算之內。
安全性注意事項
標題為「安全性注意事項」的區段合併會在處理程序內進行;沒有任何文件位元組離開主機,也不會發出網路呼叫。請把每一份外部 PDF 都當成不可信的輸入:
- 維持嚴格界限。
maxFiles與maxTotalBytes是你抵禦阻斷服務輸入的第一道防線。凡是接受上傳的介面,都請把它們設成你真正的上限,而不是寬鬆的預設值。 - 先驗證再信任。 合併成功只代表位元組被組合在一起,並不代表輸入是安全的。請先讓不可信的輸入通過 Core 的檢查器。請參閱 剖析並檢視 PDF 一節,了解一種受限分流掃描,可在進行更繁重的處理前標記加密、簽章與風險標記。
- 絕不將使用者輸入插入路徑。 這則範例會寫入固定路徑或 cookbook 旁路通道。請從伺服器控制的值推導輸出路徑,絕不從請求欄位取得,以避免路徑穿越。
- 文件中不放任何機密。 不要在你要回傳給用戶端的已合併文件中嵌入憑證、權杖或內部識別碼。
符合性
標題為「符合性」的區段這則範例本身不提出任何規範性標準主張。它透過 Core 的合併介面組合既有文件,並以 MergeResult::isValid() 標頭檢查驗證結果。PdfMerger 所重建的頁面樹模型,就是 /modules/core/document/ 參考文件中描述的 PDF 2.0 頁面樹結構。若要對任何輸入或輸出文件做結構性讀取——版本、頁數、加密與簽章旗標——請使用 剖析並檢視 PDF 中記載的 Core 檢查器。
另請參閱
標題為「另請參閱」的區段- Document 模組參考 —— 完整的分割、合併與文件部件介面。
- 剖析並檢視 PDF —— 在合併前先分流不可信的輸入。
- 例外感知的錯誤處理 —— NextPDF 在
PageLayoutException與WriterException背後的例外階層。 - 建構多頁文件 —— 撰寫之後要合併的頁面。