ファイルを埋め込み、PDF ポートフォリオを作成する
このレシピでは、1 つ以上のファイルを PDF に添付し、複数の添付ファイルがある場合は、それらを PDF ポートフォリオとして整理します。ドキュメントが補足的な証拠を同じファイル内に保持する必要がある場合に使用します。たとえば、基になるタイムシートを同梱する請求書、Computer-Aided Design(CAD)エクスポートをバンドルした製品データシート、レンダリング済みレポートと並べてソーススプレッドシートを保持するアーカイブレコードなどです。
NextPDF は、ドキュメントオブジェクトに 2 つのエントリポイントを提供します。embedFile() はディスクからファイルを読み取り、embedFileFromString() は実行時に生成したメモリ内のバイト列を埋め込みます。どちらのメソッドも添付ファイルを登録します。save() の時点で、エンジンは各ファイルを埋め込みファイルストリームとして書き込み、ファイル仕様辞書でラップし、すべてのファイル仕様をドキュメントレベルの EmbeddedFiles 名前ツリーにリンクします。ISO 32000-2 は、その名前ツリーを、埋め込みファイルストリームが名前辞書を通じてドキュメント全体に添付される場所として定義しています。
これは商用ゲートのない Core 機能です。添付ファイルの Application Programming Interface(API)は 1.0.0 以降安定しており、8.1-8.4 のバックポートマトリックス全体で動作します。
インストール
「インストール」という見出しのセクションcomposer require nextpdf/core:^3オプションの拡張機能は不要です。
概念の概要
「概念の概要」という見出しのセクション添付ファイルは 3 つの PDF 構造を経由します。これらを理解しておくと、出力の読み解きや、非準拠ファイルのデバッグに役立ちます。
- 埋め込みファイルストリーム。 添付ファイルの生のバイト列で、Flate 圧縮され、
/Typeが/EmbeddedFileであるストリームオブジェクトとして書き込まれます。NextPDF は、元のサイズ、MD5 チェックサム、変更日をストリームのパラメーター辞書に記録します。検出された Multipurpose Internet Mail Extensions(MIME)タイプをストリームの/Subtypeとしてエンコードします。 - ファイル仕様辞書。 メタデータのラッパーです。表示用ファイル名(
/Fと Unicode の/UF)、人が読める説明(/Desc)、埋め込みストリームへの参照(/EF)、およびファイルがホストドキュメントに対して保持する関係(/AFRelationship)を保持します。 EmbeddedFiles名前ツリー。 各添付ファイルの名前をそのファイル仕様にマッピングする、単一のドキュメントレベルのインデックスです。ISO 32000-2 では、このツリーを通じて到達できるすべてのファイル仕様が、埋め込みファイルストリームを参照するEFエントリを保持することが求められます。NextPDF は、save()の時点でこのツリーを構築し、バランスを取ります。
関係値は準拠上重要です。PDF Association Application Note 0002 では、関連ファイルには固定された PDF 2.0 のセットから選択された AFRelationship エントリが必要であると規定されています。セットは Source、Data、Alternative、Supplement、EncryptedPayload、FormData、Schema、または Unspecified です。NextPDF はそのセットを AFRelationship 列挙としてモデル化し、それ以外の値はすべて拒否します。そのファイルが存在する理由を表す用語を選択してください。たとえば、請求書の背後にあるタイムシートは Source、チャートの背後にある機械可読のデータセットは Data です。
次に扱うのが PDF ポートフォリオ(ISO 32000-2 では コレクション と呼ばれます)です。ドキュメントが複数の添付ファイルを保持している場合、カタログの Collection 辞書が、それらをどのように提示するかをリーダーに伝えます。ソート可能な詳細テーブル、タイルレイアウト、または非表示のエンベロープです。ISO 32000-2 は、Collection 辞書を、PDF プロセッサーがファイル添付を整理されたポートフォリオとして提示するために使用するコントロールとして記述しています。NextPDF はこれを CollectionDictionary 値オブジェクトとしてモデル化し、詳細ビューの列の順序には CollectionSort を使用します。
API サーフェス
「API サーフェス」という見出しのセクションドキュメントレベルのメソッド(HasFileAttachments concern、\NextPDF\Core\Document 上):
embedFile(string $path, string $description = ''): static—$pathからファイルを読み取って添付します。MIME タイプは拡張子から検出され、関係はデフォルトでUnspecifiedになります。最大 100 MB まで読み取ります。より大きいペイロードにはembedFileFromString()を使用してください。チェーンできるようドキュメントを返します。embedFileFromString(string $data, string $filename, string $description = '', string $afRelationship = '/Unspecified'): static— メモリ内のバイト列を表示名$filenameで添付します。関係を設定するには、AFRelationshipリテラル(先頭のスラッシュの有無は問いません)を渡します。チェーンできるようドキュメントを返します。
サポートする型(名前空間 \NextPDF\Navigation および \NextPDF\Document):
\NextPDF\Navigation\AFRelationship— 8 つの有効な関係値を持つ列挙です。AFRelationship::coerce()は文字列または列挙ケースを正規化し、不明な値では例外をスローします。toPdfName()は/Nameリテラルを出力します。\NextPDF\Document\CollectionDictionary— カタログのCollection辞書を構築します。VIEW_DETAILS、VIEW_TILE、VIEW_HIDDEN、VIEW_CUSTOM、およびVIEW_NONE定数は表示モードを選択します。コンストラクターは、初期ドキュメント名とオプションのソートも受け付けます。\NextPDF\Document\CollectionSort— 詳細ビューのポートフォリオで列の並び順を表す値オブジェクトです。
コードサンプル — クイックスタート
「コードサンプル — クイックスタート」という見出しのセクションこの最小限の例では、生成した comma-separated values(CSV)データセットを請求書ページに添付し、それを請求書の生成元である Source データとして宣言します。
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Navigation\AFRelationship;
$doc = Document::createStandalone();$doc->addPage();$doc->setFont('helvetica', 'B', 18);$doc->cell(0, 12, 'Invoice INV-2026-0042', newLine: true);
// Attach the line-item dataset the invoice was rendered from.$csv = "sku,qty,unit_price\nA-100,3,49.00\nB-220,1,180.00\n";$doc->embedFileFromString( data: $csv, filename: 'line-items.csv', description: 'Source line items for INV-2026-0042', afRelationship: AFRelationship::Source->value,);
$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/invoice-with-attachment.pdf');PDF リーダーでは添付ファイルパネルに line-items.csv が表示され、関係値によって、請求書の派生元であるソースとしてマークされます。
コードサンプル — 本番
「コードサンプル — 本番」という見出しのセクションこの本番向けの完全な例では、ディスク上のファイルとメモリ内のデータセットを添付し、ディスク上のパスを読み取る前に許可リストに登録されたベースディレクトリと照合して検証し、添付ファイル用にソート可能なポートフォリオを構築します。添付ファイル処理で発生し得る最も具体的な NextPDF 例外をキャッチし、失敗を握りつぶすのではなく、定義された終了コードを返します。
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Document\CollectionDictionary;use NextPDF\Document\CollectionSort;use NextPDF\Exception\CompressionException;use NextPDF\Exception\InvalidConfigException;use NextPDF\Exception\PageLayoutException;use NextPDF\Navigation\AFRelationship;
/** * Resolve a caller-supplied filename against an allowed base directory. * * Rejects path traversal and stream wrappers so an embedded attachment can * never read outside the directory the application owns. Returns the * canonical absolute path, or null when the input escapes the base. * * @param non-empty-string $baseDir Absolute path to the allowed directory. * @param non-empty-string $userName Untrusted filename from the request. */function resolveWithinBase(string $baseDir, string $userName): ?string{ $base = \realpath($baseDir); if ($base === false) { return null; }
$candidate = \realpath($base . \DIRECTORY_SEPARATOR . \basename($userName)); if ($candidate === false || !\str_starts_with($candidate, $base . \DIRECTORY_SEPARATOR)) { return null; }
return $candidate;}
$attachmentsDir = __DIR__ . '/attachments';$requestedFile = 'timesheet-2026-05.pdf';
$safePath = resolveWithinBase($attachmentsDir, $requestedFile);if ($safePath === null) { \fwrite(\STDERR, "Rejected attachment path: outside the allowed directory\n"); exit(2);}
try { $doc = Document::createStandalone(); $doc->setTitle('Invoice INV-2026-0042 with supporting documents'); $doc->addPage(); $doc->setFont('helvetica', 'B', 18); $doc->cell(0, 12, 'Invoice INV-2026-0042', newLine: true);
// 1. A validated file from disk: the supporting timesheet. $doc->embedFile( $safePath, 'Timesheet supporting the billed hours', );
// 2. An in-memory dataset generated at runtime. $lineItems = "sku,qty,unit_price\nA-100,3,49.00\nB-220,1,180.00\n"; $doc->embedFileFromString( data: $lineItems, filename: 'line-items.csv', description: 'Machine-readable line items', afRelationship: AFRelationship::Data->value, );
// Present both attachments as a sortable details portfolio. The sort // keys reference columns declared in the portfolio /Schema; here the // built-in filename and modification-date fields order the view. $portfolio = new CollectionDictionary( view: CollectionDictionary::VIEW_DETAILS, initialDocument: 'line-items.csv', sort: new CollectionSort( keys: ['_Filename', '_ModDate'], ascending: [true, false], ), ); // $portfolio->toPdfDictionary() yields the catalog /Collection literal, // shared with the unencrypted-wrapper envelope path.
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/invoice-portfolio.pdf'; $doc->save($out);
echo "Wrote {$out} with 2 attachments and a details portfolio\n";} catch (PageLayoutException $e) { // Unreadable path, oversized file, null byte, or a MIME-type name that // exceeds the 127-byte PDF name limit. \fwrite(\STDERR, "Attachment rejected: {$e->getMessage()}\n"); exit(1);} catch (CompressionException | InvalidConfigException $e) { // The attachment data could not be compressed, or a config value was invalid. \fwrite(\STDERR, "Write failed: {$e->getMessage()}\n"); exit(1);}CollectionDictionary と CollectionSort は値オブジェクトです。構築時に入力を検証し、リーダー上のポートフォリオビューを駆動するカタログの /Collection リテラルにシリアライズします。
エッジケースと注意点
「エッジケースと注意点」という見出しのセクション- パス入力の検証は利用側の責任です。
embedFile()は null バイトやストリームラッパーから保護し、実際のパスを resolve(解決)しますが、ベースディレクトリの許可リストを適用することはありません。パスがリクエストから渡される場合は、本番サンプルがresolveWithinBase()で行っているように、まず検証してください。 - 100 MB の上限は
embedFile()のみに適用されます。104,857,600バイトを超えるファイルはPageLayoutExceptionを発生させます。より大きいペイロードの場合は、バイト列を自分でストリーミングし、embedFileFromString()に渡してください。 - 長い MIME タイプ名は拒否されます。 検出された MIME タイプは埋め込みストリームの
/Subtypeになり、これは ISO 32000-2 によって 127 バイトに制限された PDF 名前トークンです。異常に長いタイプ(一部の Office 形式は 90 バイトに近づきます)でも上限を十分に下回りますが、それを超える手動で指定されたタイプはPageLayoutExceptionを発生させます。明示的に上書きする理由がない限り、エンジンに拡張子からタイプを検出させてください。 - 不明な関係は例外をスローします。
AFRelationship::coerce()は、固定セット以外の値をUnspecifiedにダウングレードするのではなく拒否します。タイプミスが実行時まで残ることを避けるため、列挙ケース(AFRelationship::Source->value)を渡してください。 - ファイル名は名前ツリー内で一意である必要があります。 同じ表示名を持つ 2 つの添付ファイルは、
EmbeddedFilesインデックス内で衝突します。各添付ファイルには一意のファイル名を付けてください。 _ModDateは Coordinated Universal Time(UTC)で記録されます。embedFile()はファイルの変更時刻を読み取り、gmdate()で書き込むため、同じフィクスチャからは、タイムゾーン設定にかかわらず、マシン間でバイト単位で同一の日付が生成されます。
パフォーマンス
「パフォーマンス」という見出しのセクション各添付ファイルは gzcompress() のレベル 9 で一度圧縮され、save() の時点で単一のストリームとして書き込まれます。処理コストの大部分は圧縮が占め、ページコンテンツではなく、添付されたペイロードのサイズに応じてスケールします。少数の小さな補足ファイル(データセット、スプレッドシート、タイムシート PDF)であれば、2000 ms / 64 MB の予算内に収まります。大きな添付ファイルが多数ある場合、埋め込まれるバイト列が必要メモリの下限になります。文字列として保持された 50 MB の添付ファイルは、圧縮前に少なくともそれだけのメモリを占有します。複数の大きなファイルを一度に読み込むよりも、チャンク生成を伴う embedFileFromString() を優先してください。
名前ツリーは save() の時点で一度構築されます。最大 64 エントリまでは、フラットな単一ルートツリーに保持されます。それを超えると、NextPDF はツリーをバランスの取れた Kids と Limits の範囲に分割するため、インデックスのコストは大規模な添付ファイルセットでも対数的なままになります。
セキュリティに関する注意事項
「セキュリティに関する注意事項」という見出しのセクション- 信頼できないパスはすべて許可リストと照合して検証してください。 埋め込みは、PHP プロセスが到達できる任意のファイルを読み取ります。ベースディレクトリのチェックがないと、細工されたファイル名により添付処理が Local File Inclusion(LFI)に変わってしまいます。本番サンプルは許可リストのガードを示しています。ファイル名がコンパイル時定数でない場合は、常にこれを適用してください。
- 消費側では、添付されたバイト列を信頼できないものとして扱ってください。 埋め込みファイルは NextPDF にとって不透明です。エンジンはそれを解析も実行もしません。リスクは、そのファイルが後で開かれる場所にあります。下流の消費者が各添付ファイルを抽出する前にその内容を把握できるよう、関係と説明を設定してください。
- 添付ファイルや説明にシークレットを含めないでください。 ファイル名、説明、バイト列は、ドキュメント全体が暗号化されていない限り、平文で保存されます。添付ファイルを保護するには、権限ポリシーを使用してドキュメントを暗号化してください(関連レシピを参照)。レンダリング済みページに載せないような認証情報、鍵、個人データを埋め込まないでください。
- このレシピではネットワークアクセスは発生しません。 すべてのバイトは、検証済みのローカルパスから読み取られるか、メモリ内で供給されます。
| 記述 | 仕様 | 条項 | リファレンス ID |
|---|---|---|---|
埋め込みファイルストリームは、名前辞書内の EmbeddedFiles エントリを通じてドキュメントに添付されます。 | ISO 32000-2 | 7.11.4 | |
その EmbeddedFiles 名前ツリーは、名前を、EF エントリが埋め込みファイルストリームを参照するファイル仕様にマッピングします。 | ISO 32000-2 | 7.7.4 | |
関連ファイルには、固定された PDF 2.0 の値セットから選択された AFRelationship 値が必要です。 | PDF Association AN002 | 3 | |
カタログの Collection 辞書は、添付ファイルのポートフォリオ表示を制御します。 | ISO 32000-2 | 7.11.6 |
再現性プロファイル — 構造的。 トレーラーの /ID、保存ごとに変わる日付アトム、および埋め込みストリームの /ModDate は実行ごとに異なるため、構造比較ではオブジェクトグラフを差分する前にこれらを取り除きます。このレシピは、NextPDF がどのように構造を生成するかを説明します。これは、ドキュメント全体に依存する PDF/A-4f への全面的な準拠を主張するものではありません。すべての添付ファイルに関係と説明の宣言を求めるアーカイブプロファイルについては、PDF/A-4 のレシピを参照してください。