ドキュメント:DPart、split / merge、ベンダー拡張
Document モジュールは、ページコンテンツではなく PDF ドキュメント全体を操作対象にします。規制対象のワークフローでメタデータを付加するためのドキュメントパート階層を構築し、PDF をページ範囲ごとのセグメントに分割し、複数の PDF を 1 つに結合し、ドキュメントカタログへ開発者拡張を登録します。
インストール
「インストール」という見出しのセクションcomposer require nextpdf/core:^3概念の概要
「概念の概要」という見出しのセクションこのモジュールはページコンテンツより上位の層に位置します。Graphics と Content がオペレーターを出力するのに対し、Document はページツリー、ドキュメントカタログ、ドキュメントパートツリーといった構造レベルを扱います。
ドキュメントパート(DPart)は、PDF における論理的な区画です。ISO 32000-2 は、各ノードがドキュメントパートメタデータ(DPM)を保持する DPart 階層を定義しています。製薬、法務、アーカイブなどの規制対象ワークフローでは、ファイル全体ではなくページのサブ範囲にメタデータを関連付けられます — §14.12。DPart は不変の readonly ノードです。リーフはページインデックスの連続範囲を参照し、中間ノードは子の DPart ノードをツリーにまとめます。DPartRoot は、Writer がシリアライズするツリーのルートです。リーフノードの /Start および /End エントリは、ページインデックス整数ではなく、ページオブジェクトへの間接参照です — §14.12。DPart::resolveWithPageObjects() は、Writer から提供されるページインデックス→オブジェクト番号マップに基づいてその解決を行い、/Start(および省略可能な /End)の参照形式を返します。マップを利用できないテストパスでのみ、整数形式へフォールバックします。
PdfMerger と PdfSplitter は、ドキュメント合成のための API サーフェスです。PdfMerger は、複数の入力 PDF からページオブジェクトを収集し、衝突を避けるためにオブジェクトを再採番し、単一のページツリーとクロスリファレンステーブルを再構築します。生成されるページツリーは、Pages ノードを Kids と Count でバランスさせて構成され、PDF がページツリーノードに対して定義する継承可能な属性モデルを備えています — §7.7.3。PdfSplitter は逆方向の処理として、ページ範囲を独立した SplitDocument オブジェクトとして抽出します。PageRange は、両者で利用される値オブジェクトです。1 始まりで境界を検証し、contains()、count()、toArray() に対応します。
VendorExtensionRegistry、ExtensionsDictionary、DeveloperExtensionEntry は、ドキュメントカタログ内の開発者拡張辞書をモデル化し、エンジンが基本仕様を超えるベンダー拡張レベルを宣言するための仕組みを提供します。レジストリは、同一ベンダープレフィックスの競合する再登録を VendorExtensionRegistryConflictException で拒否します。CollectionDictionary と CollectionSort は、PDF コレクション(ポータブルコレクション/ポートフォリオ)のカタログエントリをモデル化します。
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) |
完全な PHPDoc テーブルを確認するには、composer docs:generate-api-php -- --module=Document を実行してください。
コードサンプル — クイックスタート
「コードサンプル — クイックスタート」という見出しのセクションPDF を 1 ページごとのドキュメントに分割し、その結果を確認します。
<?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 始まりのページインデックスです。この 2 つの抽象化はインデックスの基準を共有しません。両者をまたぐ際は明示的に変換してください。DPartはreadonlyです。異なるツリーを構築するには、既存ノードを変更するのではなく、新しいノードを構築します。resolveWithPageObjects()は、ページオブジェクトマップが空の場合にのみ整数インデックスのフォールバック形式を返します。本番出力でそのパスに依存しないでください。VendorExtensionRegistryは、重複するベンダープレフィックスに対してVendorExtensionRegistryConflictExceptionを送出します。各プレフィックスは一度だけ登録してください。
パフォーマンス
「パフォーマンス」という見出しのセクション分割と結合の計算量はページ数に対して線形であり、処理時間を左右するのはモジュール自身の管理処理ではなく、解析とオブジェクト再採番です。デフォルトの参照ワークロードは、1500 ms のウォール時間 / 64 MB のピークという予算内に収まります。大規模な結合は、主に入力の総バイト数によって制約されます。maxTotalBytes ガードは、ピークメモリを境界内に保つために存在します。再現性プロファイルは structural です。結合または分割された PDF は新しいトレーラーと /ID を持つため、2 回の実行は構造的には等しくなりますが、バイト単位では同一になりません。
セキュリティ上の注意
「セキュリティ上の注意」という見出しのセクションPdfMerger::merge() と PdfSplitter::split() は、信頼できない PDF バイト列を処理します。いずれも解析の前に入力を ResourceGuard::assertSize() / assertCount() に通し、これにより解凍またはオブジェクト数の増幅によるサービス拒否を境界内に抑えます。maxFiles、maxTotalBytes、maxBytes の各引数は、デフォルトに依存せず、デプロイ環境に合わせて厳しめに保ってください。すべての入力 PDF を敵対的なものとして扱ってください。ソースがユーザーから提供される場合は、制限付きワーカー内でバッチ合成を実行してください。信頼境界については、/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/ で説明されているオラクルおよびゴールデンスイートによって検証されます。
- Core モジュール
- Writer モジュール — DPart ツリーとページツリーをシリアライズします。
- Metadata モジュール — DPM と組み合わせる XMP。
- Navigation モジュール
- 適合性の概要
- エンジンセキュリティモデル