PDFlibPasは、埋め込みファイルをドキュメント全体ではなく1つの特定のページに添付します。やり方は、ページディクショナリに/AF配列を書き込みつつ、データ本体はドキュメントのEmbeddedFilesネームツリーに登録したままにしておく、というものです。この分離こそがISO 32000-2 §14.13が定義している構造であり、ドキュメントレベルの添付では答えられない問い、すなわち「このデータはどのページに属するのか」にリーダーが答えられるようにするものです
ユースケースは汎用の添付より具体的です。図表の背後にある生の測定系列を各ページが保持する調査報告書。テキストレイヤーを生成したOCR結果をページごとに保持するスキャン一括処理。レンダリング元のCAD抽出データを各シートが保持する図面セット。いずれの場合も、ドキュメントレベルの添付リストで済ませると、ページ番号をファイル名に織り込んだファイルの山になります。慣習であって構造ではありません
実体は1つ、参照箇所は2つ
構造上の重要なポイントは、ページレベルの関連付けは何の複製も作らないことです。ファイルは1度だけ埋め込まれ、ドキュメントレベルの添付とまったく同じファイル指定の仕組みを使ってEmbeddedFilesネームツリーに登録されます。違うのは参照とそのリレーションシップキーをどこに書くかで、ドキュメントカタログではなくページディクショナリに書く点です
帰結は2つあります。1つ目、ドキュメントレベルの添付しか知らないリーダーでも、データ本体を見つけられます。そうしたリーダーが探す場所、つまりネームツリーの中にあるからです。2つ目、ページ関連付けのクリアで取り除かれるのはバインディングであってファイルではありません。ClearPageAssociatedFilesはページを関連ファイルから切り離す一方、データ本体はネームツリー経由で到達可能なままにします。これは保守的な振る舞いです。関連付けをクリアせよという操作が、ドキュメントの別の部分が参照しているかもしれないデータを黙って破壊すべきではありません
この関数には知っておく価値のある、意図的に狭い成功条件が1つあります。成功を報告するのは、ページが実際に/AFキーを保持していた場合だけです。関連付けを一度も持ったことのないページは、陽気な確認ではなく失敗を返します。呼び出し側が何もしなかった操作を完了済みのクリーンアップと見間違えることはありません
var
Lib: TPDFlib;
Idx, I: Integer;
begin
Lib := TPDFlib.Create(nil);
try
Lib.LoadFromFile('survey-report.pdf');
// ページ 3 の図表を生んだ測定系列を添付する
Idx := Lib.AddPageAssociatedFileFromFile(3,
'series-03.csv', // ディスク上のファイル
'measurements.csv', // PDF 内での表示名
'text/csv', // MIME タイプ
'Raw measurement series for figure 3',
'Data'); // AFRelationship、ISO 32000-2 14.13
if Idx < 0 then
raise Exception.Create('page association refused');
for I := 0 to Lib.GetPageAssociatedFileCount(3) - 1 do
Writeln('page 3 associated file, embedded index ',
Lib.GetPageAssociatedFileEmbeddedIndex(3, I));
Lib.SaveToFile('survey-report-with-data.pdf');
finally
Lib.Free;
end;
end;
リレーションシップ文字列は、実務上は自由記述ではありません。ISO 32000-2は語彙を定めており、Source、Data、Alternative、Supplement、EncryptedPayload、FormData、Schema、Unspecifiedの各値をコンシューマはキーとして見ます。図表の背後の数値にはData、ページの生成元ドキュメントにはSource、等価な表現にはAlternative。自パイプラインのどこもまだ読んでいない段階でも、この語彙から選んでください。チェーンの次のツールが読むかもしれないからです
なぜ同じルックアップにFollowRefが両方向に必要なのか
参照を辿る処理が答えるのは2種類の異なる問いであり、コードは今どちらを問うているのかを自覚していなければならないからです。間接参照を辿るキー検索は、参照が指すオブジェクトを返します。辿らない検索は参照そのものを返します。どちらも正しく、誤った方を使うとエラーではなく静かな誤動作になります
関連ファイルの読み取りは1つ目の方向の例です。ファイル指定の/EFと/Fキーの背後にある埋め込みストリームのオブジェクト番号を得るには、検索は辿ってはいけません。辿ると参照がストリームオブジェクトまで解決され、オブジェクト番号が消えてしまうからです。ルールは一般化できます。オブジェクトの中身ではなくオブジェクトの同一性が必要なコードパスは、すべて生の参照を受け取る必要があります
オプショナルコンテンツは逆の方向の例で、こちらは発見までに余計に時間がかかりました。オプショナルコンテンツのプロパティディクショナリはカタログに間接オブジェクトとして書き込まれるため、辿らずに読み戻すコードが受け取るのはディクショナリではなく参照です。その値への型チェックは失敗し、自然なフォールバック分岐、つまり「設定がなければ作る」が走って、すでに存在していた設定を上書きします。例外は一切上がりません。オプショナルコンテンツグループとレイヤーで説明したレイヤーは、デフォルトの表示状態を黙って失います
教訓は両方のケースを越えて一般化できます。検索が参照とオブジェクトのどちらも返し得る場合、素の型チェックはエラー処理ではありません。いずれ誤った理由で通ることになる分岐です。各呼び出し箇所が何を必要としているのかを明示的に決め、カタログディクショナリへprotectedアクセサ越しに手を伸ばすより、オプショナルコンテンツのカウントプロパティのように問いに直接答える公開APIを優先してください
// ドキュメントレベルの添付とページレベルの関連付けは共存できる。埋め
// 込みファイルはドキュメントレベルでも関連付きとしてマークできる
if Lib.IsEmbeddedFileAssociated(0) = 0 then
Lib.SetEmbeddedFileAssociated(0, 1, 'Supplement');
Writeln('document associated files: ', Lib.GetAssociatedFileCount);
Writeln('page 3 associated files : ',
Lib.GetPageAssociatedFileCount(3));
// クリアはページのバインディングを切り離すだけ。実体はネームツリーに残る
if Lib.ClearPageAssociatedFiles(3) > 0 then
Writeln('page 3 associations removed, payloads still reachable');
コンフォーマンスモードが添付に及ぼす影響
アーカイブ系プロファイルは何を埋め込めるかを制限し、その制限は保存時ではなくエントリポイントで強制されます。PDF/A-1は埋め込みファイルを完全に禁止し、PDF/A-2は埋め込みPDF/Aドキュメントのみ許可、PDF/A-3は任意のファイルタイプへの埋め込みを開放したプロファイルで、ハイブリッド請求書フォーマットがまさにこれの上に築かれている理由です
アクティブなコンフォーマンスモードが許可しない場合、PDFlibPasは添付を呼び出しの時点で拒否します。数百の操作を経た後の出力時にではなく。これはエラーの対処コストが最も安い場所を選ぶ意図的な判断です。呼び出し箇所での拒否は追加しようとしたファイルの名前を教えてくれますが、保存時の拒否はドキュメントの名前しか告げず、40個の添付のどれが原因かを自分で割り出すことになります
電子請求書の分野で関連ファイルが頻出するのも同じ理由からです。ハイブリッド請求書とは、人が読むPDFに機械可読のXMLデータを添付し、正しいリレーションシップをマークしたもの。コンテナプロファイルとリレーションシップキーの両方が、慣習ではなく仕様の一部を成しています。この構成はFactur-XとZUGFeRDハイブリッド請求書の構築で扱い、メタデータ側はPDF/A-3のXMP拡張スキーマで扱います
関連付けはドキュメント単位でなくページ単位にすべきとき
コンシューマがデータの属するページを知る必要があるとき、そしてそのときに限ります。ドキュメントレベルの添付はより単純で、ビューアの対応も広く、データが文書全体を記述するもの、たとえば請求書XML、署名マニフェスト、ソースアーカイブ、であれば十分です。ページレベルの関連付けに手を伸ばすのは、データが真にページスコープで、ページの同一性がその意味の一部を成しているときにしてください
実用上の制約は対応状況です。ページレベルの関連ファイルはPDF 2.0の構成要素で、ビューアの対応はドキュメントレベルの添付より薄くなります。とはいえデータ本体はどちらにせよネームツリーにあるため、ページ上の/AFを無視するビューアでも添付リストにはファイルを表示でき、劣化は穏やかです。ただしページのバインディングが有用なメタデータではなく必須要件であるなら、想定せず、実際にターゲットとするリーダーで検証してください
ページレベルの関連ファイル、ドキュメントレベルの添付、そして両者を律するアーカイブプロファイルのゲートは、PDFlibPas Delphi PDFライブラリに同梱されています。取り込み時に古いファイルの修復も併せて行うなら、メタデータ修復付きのPDF/A変換のメタデータとコンフォーマンスの作業こそが、そもそもどの添付経路が使えるかを決めます