外部 PDF を結合する、または既存ドキュメントにページを追加する
ディスク上に複数の PDF ファイルがあり、それらを 1 つの PDF にまとめたいとします。このレシピでは、Core のマージ用 API である NextPDF\Document\PdfMerger を使用して、既存ドキュメントを順番に結合します。入力には、生の PDF バイト文字列を渡します。マージャーは衝突を避けるためにすべてのオブジェクトを再採番し、1 つのページツリーと 1 つのクロスリファレンステーブルを構築して、NextPDF\Document\MergeResult を返します。この結果をディスクに書き出すか、クライアントへストリーミングします。
同じ API で、開発者がよく必要とする次の 3 つのタスクを扱えます。
- Merge: 順序付きの PDF リストを 1 つのドキュメントに結合します。
- Append: ベース PDF の末尾に 2 つ目の PDF を追加します。
- Prepend: 新しいドキュメントを入力順の先頭に置き、ページを先頭に追加します。
マージはプロセス内で実行され、ヘッドレスブラウザもネットワーク呼び出しも使用しません。Core のインストール(composer require nextpdf/core:^3)と、読み取り可能な 2 つ以上の PDF ファイルが必要です。
インストール
「インストール」という見出しのセクションcomposer require nextpdf/core:^3概念の概要
「概念の概要」という見出しのセクションPDF では、ページはルートが /Pages ノードであるページツリーに編成され、すべての間接オブジェクトはクロスリファレンステーブルで特定されます。2 つのソースドキュメントを結合すると、それぞれのオブジェクト番号が重複します。どちらのファイルにも、ほぼ必ずオブジェクト 1 0 obj、/Catalog、および /Pages ノードが含まれています。バイト列を単純に連結すると、参照が正しい場所を指さなくなるため、破損したファイルが生成されます。
PdfMerger はこの問題を解決します。各入力からページオブジェクトを抽出し、すべてのオブジェクトを 1 つのアドレス空間に再採番し、各ページの /Parent 参照が結合後の単一の /Pages ノードを指すように書き換えて、1 つのカタログ、1 つのページツリー、1 つのトレーラーを出力します。出力は、単純につなぎ合わせたものではなく、構造的に新しいドキュメントです。
順序付けのルールは単純です。ページは、ソースファイルが入力リストに現れる順序で並びます。末尾に追加するには、ベースドキュメントを先頭に置きます。先頭に追加するには、新しいドキュメントを先頭に置きます。先頭追加のための専用メソッドはありません。入力順だけで必要な制御ができるためです。
API サーフェス
「API サーフェス」という見出しのセクションnew NextPDF\Document\PdfMerger() は 2 つのメソッドを提供します。
merge(list<string> $pdfFiles, int $maxFiles = 100, int $maxTotalBytes = 200_000_000): MergeResultは、順序付きの生の PDF バイト文字列リストを結合します。この 2 つの境界パラメーターで、ファイル数と入力の合計サイズに上限を設けます。どちらも本番環境向けの安全な値がデフォルトであり、ワークロードに応じてより厳しく設定します。append(string $basePdf, string $appendPdf): MergeResultは、ちょうど 2 つのドキュメントを順番に結合するための便利なラッパーです。これはmerge([$basePdf, $appendPdf])と同等です。
どちらも NextPDF\Document\MergeResult を返します。これは readonly オブジェクトで、$pdfData(結合されたバイト列)、$totalPages、$sourceCount、$mergedSize を保持し、出力が %PDF ヘッダーで始まることを確認する isValid() ヘルパーを備えています。
入力はファイルパスではなく、生のバイト文字列です。ファイルは file_get_contents() を使って自分で読み取ります(またはオブジェクトストレージからバイト列を取得します)。これにより、マージャーはファイルシステムに関する前提を持たず、ディスクに一切触れずにドキュメントを結合できます。
外部 PDF の単一ページを再利用可能な Form XObject としてインポートする必要がある場合、たとえば生成されたコンテンツの背後にレターヘッドのページをスタンプする場合は、パッケージ横断のインポーターコントラクト NextPDF\Contracts\ImportedFormObjectInterface を使用します。これは nextpdf/artisan などのインポーターが実装しています。ドキュメント全体およびページ全体の合成には、ここで説明する PdfMerger の API を使用します。
コードサンプル — クイックスタート
「コードサンプル — クイックスタート」という見出しのセクションこの例では、2 つのファイルを読み取り、結合結果を書き出します。呼び出しの形だけを示すため、エラー処理は省略しています。以下の本番向けサンプルでは、完全なガードを追加します。
<?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);コードサンプル — 本番向け
「コードサンプル — 本番向け」という見出しのセクションこれは自己完結型のプログラムです。外部ファイルなしで実行できるよう、メモリ内に小さなドキュメントを構築して結合し、結果を検証して出力を書き出します。マージ用 API が送出する 2 つの例外をキャッチし、それぞれにコンテキストを添えて、無視せずに再送出します。メモリ内の入力を、独自の 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が送出されます。マージャーがファイルを暗黙に破棄したりバイトを切り取ったりすることは決してないため、両方の境界をワークロードに合わせて調整してください。 - 出力はバイト単位で安定したものではなく、構造的に新しいものです。 結合されたドキュメントは、新しいカタログ、ページツリー、トレーラーを持ちます。同じ入力に対する 2 回の実行は構造的には等価ですが、バイト単位で同一であることは保証されません。このため、このレシピは
structuralという再現性プロファイルを宣言しています。 - ページレベルの注釈と共有リソース。 マージはページオブジェクトを 1 つのツリーに合成します。ソースファイル内でページオブジェクトの外側に存在するドキュメントレベルの構造は引き継がれません。単一ページをそのリソースとともに再利用可能なグラフィックとしてインポートする必要がある場合は、
nextpdf/artisanなどのインポーターを介してImportedFormObjectInterfaceのパスを使用してください。
パフォーマンス
「パフォーマンス」という見出しのセクションマージ処理は合計ページ数に対して線形であり、処理時間の大半はマージャー自体の管理処理ではなく、解析とオブジェクトの再採番に費やされます。出力の組み立て中はすべてのソースが文字列としてメモリに保持されるため、ピークメモリは入力の合計バイト数に比例します。maxTotalBytes ガードは、そのピークを境界内に保ちます。大量処理のパイプラインでは、maxFiles と maxTotalBytes をワークロードが必要とする最小値に設定してください。そうすれば、不正な形式や過大なバッチがメモリを使い果たす前に、すぐに失敗します。一般的な小規模マージは、実時間 1500 ms およびピーク 64 MB の予算内に収まります。
セキュリティに関する注意
「セキュリティに関する注意」という見出しのセクションマージはプロセス内で実行されます。ドキュメントのバイト列がホストの外に出ることはなく、ネットワーク呼び出しも行われません。すべての外部 PDF を信頼できない入力として扱ってください。
- 境界を厳しく保ってください。
maxFilesとmaxTotalBytesは、サービス拒否(DoS)入力に対する第一の防御線です。アップロードを受け付ける箇所では、余裕のあるデフォルトではなく、実際の上限値に設定してください。 - 信頼する前に検証してください。 マージが成功したということは、バイト列が結合されたことを意味するだけで、入力が安全であることを意味するわけではありません。信頼できない入力は、まず Core インスペクターに通してください。より重い処理の前に暗号化、署名、リスクマーカーを検出する、境界を設定したトリアージスキャンについては、PDF を解析して検査するを参照してください。
- ユーザー入力をパスに埋め込まないでください。 このレシピは、固定パスまたはクックブックのサイドチャネルに書き込みます。パストラバーサルを回避するため、出力パスはリクエストフィールドからではなく、サーバーが制御する値から導出してください。
- ドキュメントに機密情報を含めないでください。 クライアントに返す結合済みドキュメントには、認証情報、トークン、内部識別子を埋め込まないでください。
このレシピ自体は、規範的な標準への準拠を主張しません。Core のマージ用 API を通じて既存ドキュメントを合成し、MergeResult::isValid() のヘッダーチェックで結果を検証します。PdfMerger が再構築するページツリーモデルは、/modules/core/document/ リファレンスで説明されている PDF 2.0 のページツリー構造です。入力ドキュメントまたは出力ドキュメントの構造情報(バージョン、ページ数、暗号化フラグ、署名フラグ)を読み取るには、PDF を解析して検査するで説明されている Core インスペクターを使用してください。
- Document モジュールリファレンス — 分割、結合、ドキュメントパートの API 全体。
- PDF を解析して検査する — 結合前に信頼できない入力をトリアージする方法。
- 例外を意識したエラー処理 —
PageLayoutExceptionとWriterExceptionの背後にある NextPDF の例外階層。 - 複数ページのドキュメントを作成する — その後に結合するページの作成。