コンテンツにスキップ

メタデータ:XMP パケットの構築とストリーミング読み取り

Metadata モジュールは、エンジンの XMP レイヤーです。PDF がメタデータストリームとして保持する XMP パケットを構築し、ドキュメント全体をメモリに読み込まずに既存のパケットを読み取ります。また、エンジンの監査証跡 XMP 拡張も出力します。

Terminal window
composer require nextpdf/core:^3

PDF は、ドキュメントカタログに紐付けられたメタデータストリーム内の XMP パケットとして、ドキュメントレベルのメタデータを保持します(ISO 32000-2 §14.3)。このモジュールは、そのパケットの生成と読み取りを担います。意図的に小さく絞り込んだサーフェスであり、NextPDF\Metadata\Xmp 配下の 3 つのクラスで構成されます。

XmpMetadataBuilder はパケットを生成します。プロパティセットを、標準の <?xpacket?> 処理命令でラップされた整形式の XMP ドキュメントとしてシリアライズします。XMP 仕様で定められた正規のパケット GUID とバイトオーダーマークを使用します。出力は、Writer がメタデータストリームとして埋め込むバイト文字列であり、§14.3 で記述される PDF 内 XMP 表現です。

XmpStreamReader はパケットを読み取ります。敵対的な入力を前提に設計されています。ソースは、解析前に 64 KB のチャンク単位で、上限付きの一時ファイルへストリーミングされます。その書き込み中に、合計バイト数の上限が適用されます。libxml のエンティティローダーは解析中に null 化され、その後に復元されます。DOCTYPE は必ず拒否されます。iterateProperties() を公開しています。これは、ツリー全体を実体化せずに各リーフ要素について (namespaceUri, localName, textContent) のタプルを生成するジェネレーターであり、任意の時点でパーサー内に存在するのは現在の要素とそのテキストノードのみです。サイズ超過のパケットは PacketTooLargeException を送出します。不正な XML、DOCTYPE、または UTF-8 以外の入力は InvalidConfigException を送出します。

XmpAuditFieldEmitter は、エンジン固有の拡張です。AuditReportnextpdfAudit 名前空間配下のカスタム XMP フィールドとしてレンダリングします。これにより、ドキュメントの適合性監査はサイドカーではなく、標準準拠の XMP としてファイルとともに移動します。レンダリングされる AuditReport は、エミッター自身が生成するものではありません。エンリッチメントは、Config(auditCollector: ...) 経由で構成された呼び出し側提供の auditCollector を使って、CssRenderingMode::Audit 下でレンダリングを実行したときに有効化されます。コレクターは呼び出し側主導です。呼び出し側がデータを供給し、エミッターは収集された内容をそのままレンダリングします。これはコアの XMP サーフェスよりも新しいものです(@since 5.4.0)。ビルダーとリーダーは @since 2.0.0 です。

クラス主なメンバー役割
XmpMetadataBuilderbuild(): stringXPACKET_GUIDXPACKET_BOMプロパティセットを XMP パケットへとシリアライズ(@since 2.0.0
XmpStreamReaderiterateProperties(mixed $source, int $byteCap = DEFAULT_BYTE_CAP): \GeneratorDEFAULT_BYTE_CAP上限付きストリーミングと DOCTYPE 拒否を行う XMP リーダー(@since 2.0.0
PacketTooLargeExceptionNextPdfException を継承XMP パケットがバイト上限を超えたときに送出(@since 2.0.0
XmpAuditFieldEmitterrender(?AuditReport $report): stringNAMESPACE_URI監査証跡をカスタム XMP フィールドとしてレンダリング(@since 5.4.0

完全な PHPDoc テーブルは、composer docs:generate-api-php -- --module=Metadata を実行して確認してください。

明示的なバイト上限のもとで、既存の XMP パケットからプロパティをストリーミングで取り出します。

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Metadata\Xmp\XmpStreamReader;
$reader = new XmpStreamReader();
foreach ($reader->iterateProperties(file_get_contents('/srv/in/xmp.xml'), byteCap: 1_048_576) as [$ns, $name, $value]) {
printf("%s:%s = %s\n", $ns, $name, $value);
}

生のパーサー障害を外に漏らすのではなく、モジュールの型付き失敗をアプリケーションレベルの結果へマッピングし、防御的にパケットを読み取ります。

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Exception\InvalidConfigException;
use NextPDF\Metadata\Xmp\PacketTooLargeException;
use NextPDF\Metadata\Xmp\XmpStreamReader;
use Psr\Log\LoggerInterface;
final readonly class XmpIngestService
{
public function __construct(
private XmpStreamReader $reader,
private LoggerInterface $logger,
) {}
/**
* @param resource|string $source A stream resource or XMP byte string.
*
* @return array<string, string> Flattened "ns:name" => value map.
*/
public function ingest(mixed $source): array
{
$properties = [];
try {
// Cap untrusted XMP at 4 MB regardless of the 1 GiB default.
foreach ($this->reader->iterateProperties($source, byteCap: 4_194_304) as [$ns, $name, $value]) {
$properties["{$ns}:{$name}"] = $value;
}
} catch (PacketTooLargeException $e) {
$this->logger->warning('XMP packet exceeded ingest cap; rejected.', ['error' => $e->getMessage()]);
return [];
} catch (InvalidConfigException $e) {
$this->logger->warning('XMP packet malformed or unsafe; rejected.', ['error' => $e->getMessage()]);
return [];
}
return $properties;
}
}
  • XmpStreamReader は、いかなる DOCTYPE も即座に拒否します。これは検証上の形式要件ではなく、XXE 防御です。DOCTYPE を必要とするパケットは受け付けられないため、上流でサニタイズしてください。
  • バイト上限のデフォルトは 1 GiB です(DEFAULT_BYTE_CAP)。このデフォルトは上限であり、推奨値ではありません。信頼できない入力には、より厳しい byteCap を渡してください。
  • iterateProperties() はジェネレーターです。一度だけ消費してください。2 回目に反復しても値は再生成されません。
  • リーダーは解析中に libxml のエンティティローダーを null 化し、その後に復元します。同一リクエスト内で、エンティティローダーに依存する他の libxml ベースの解析と同時に実行しないでください。
  • XmpAuditFieldEmitter::render(null) は有効であり、空のレンダリングを返します。null の AuditReport は「監査なし」であり、エラーではありません。

ビルダーは、プロパティ数に対して線形です。パーサー内に存在するのは現在の要素のみであるため、リーダーのメモリ使用量はドキュメントサイズではなく、最も長い単一のテキストランに左右されます。大きなパケットは読み込まれず、ストリーミングされます。デフォルトの参照ワークロードは、ウォール時間 1500 ms/ピーク 64 MB の予算内に収まります。再現性プロファイルは structural です。XMP パケットは変更タイムスタンプを記録します。同じ論理メタデータから作成した 2 つのビルドは、構造は同一ですが、それらのフィールドは異なります。

XmpStreamReader は信頼できない XML を扱うパーサーであり、その前提で堅牢化されています。バイト上限を強制するストリーミングチャンク化により、メモリ増幅型のサービス拒否を抑制します。XXE を防ぐため、DOCTYPE は拒否されます。LIBXML_NONET はネットワーク経由のエンティティ解決をブロックします。UTF-8 以外の入力は拒否されます。それでも、外部由来のパケットについては、ギガバイト単位のデフォルトに頼らず、デプロイ環境に適した byteCap を設定してください。XMP プロパティ値がアプリケーションに再び入る際は、信頼できない文字列として扱ってください。エンジンの脅威モデルについては /modules/core/security/ を参照してください。

XmpMetadataBuilder が生成するパケットは、ISO 32000-2 §14.3 で定義された PDF 内 XMP メタデータストリーム表現です ()。XMP シリアライズ形式そのものは XMP 仕様(ISO 16684-1)によって規定されており、これは検証可能な引用コーパスには含まれていません。この要件は番号で参照されており、チャンクにピン留めされてはいません。これらは src/Metadata/Xmp/ が生成し、tests/Unit/Metadata/Xmp/ が検証する実装上の事実です。プロファイル(PDF/A、PDF/UA)に対するエンドツーエンドのメタデータ適合性は、/modules/core/conformance/ に記載されたオラクルおよびゴールデンスイートによって検証されます。