技術記事

PDFをPDF/Aへ変換しメタデータを修復するDelphi実装

ConvertToPDFAは1回の呼び出しで普通のドキュメントをアーカイブ向けに変える。選択したパートが禁じるものを削除し、パートが要求するものを追加し、ドキュメントが主張するパートを明示した上で、結果を検証する。主張が満たされたと報告されるのは検証が通ったときだけであり、GetPDFAConversionReportが何が行われ、何がまだ障害として残っているかを一覧する

この最後の性質こそ、掘り下げる価値のある設計判断だ。検証せずに主張のスタンプを押すコンバーターは、コンバーターがないより悪い。アーカイブ向けだと主張しながらそうでないファイルは、本来ならそれを捕捉してくれるはずのシステムを素通りしてしまうからだ。障害は何年もあと、監査の中で、もう再生成できないドキュメント上で表面化する

なぜ妥当に見えるPDFがPDF/A検査で落ちるのか

多くの場合、PDFが「誰が書いたか」を2カ所で異なる主張をしているからだ。バリデータはドキュメント情報辞書とXMPパケットの両方を読み、両者が異なるファイルを拒否する。そしてこの点で落ちるファイルの多くは、単にXMP側が一度も書かれていないだけだ

RepairDocumentMetadataは両者を一致させ、修復したエントリ数を返す。片方だけが値を持つ場合は、もう片方がそこから埋められるので、すでに記録されたものが捨てられることはない。どちらのコピーが権威かを人が決める必要はない。実地ではどちらか一方が空だからだ

同じ呼び出しの中に、より微妙なケースを捕捉する2つ目の修復がある。PDF/Aモードに設定されたドキュメントは、失われていた場合に標準識別子を復元される。これは呼び出し側が独自のXMPパケットを供給したときにいつも起きる。この識別子がないと、バリデータはファイルを通常のPDFと読み、主張されたパートの全ルールを未達として報告する。小さな原因による派手な失敗だ

var
  Lib: TPDFlib;
  Repaired: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('incoming.pdf', '');
    Repaired := Lib.RepairDocumentMetadata;
    Log(Format('%d metadata entries brought into agreement', [Repaired]));
    Lib.SaveToFile('incoming-fixed.pdf');
  finally
    Lib.Free;
  end;
end;

変換前にパートを選ぶ

SetPDFAModeConvertToPDFAは同じモード番号を共有し、3つの値が比較的新しい。モード9はPDF/A-4、PDF 2.0上に構築されたパートだ。モード10はPDF/A-4eで、3Dやリッチメディアを追加で許可する。モード11はPDF/A-4fで、任意フォーマットの埋め込みファイルを許可する

パート4は、それ以前のパートとは異なる方法で自己識別する。パート番号とそのパートが発行された年で識別し、通常のPDF/A-4には適合度文字を持たず、2つの拡張にはEまたはFの文字を持つ。検査はパート4を認識し、そのファイルを1.7ではなくPDF 2.0に対して判定し、改訂年を明示していないパート4ファイルを報告する

パート4ドキュメント内のすべての埋め込みファイルは、ドキュメントとの関係を明示する。これはパート3とパート4の両方が要求する。かつて通常の添付ファイルを捕捉していたのはこのルールだ。関係は最初以降の添付ファイルにだけ書かれ、最後のものには書かれなかった。そのため添付ファイル1つという一般的なケースでは、関係が1つも書かれず、まさにその点で検証に落ちた

var
  Verdict: Integer;
begin
  Lib.LoadFromFile('report.pdf', '');
  Verdict := Lib.ConvertToPDFA(9);        // 9 = PDF/A-4, 10 = 4e, 11 = 4f
  Memo1.Lines.Text := Lib.GetPDFAConversionReport;
  if Verdict = 1 then
    Lib.SaveToFile('report-pdfa4.pdf')
  else
    Log('conversion incomplete - see the report for what stands in the way');
end;

変換レポートの目的

次に何をすべきかを決めることだ。成功した変換にレポートは不要だが、失敗した変換こそがレポートの存在理由だ。ある障害はコンバーターで取り除けるが、ある障害は取り除けない。暗号化、意味を持つ禁制コンテンツ、マシン上のどこにも存在しないフォントプログラムなどだ。レポートは「何が行われたか」と「何が残っているか」を区別し、「変換失敗」を作業項目に変える

评定はバッチパイプラインのゲートとして扱うこと。変換し、评定を読み、ファイルを振り分ける。通ったものはアーカイブし、残りはレポートを添えて人のキューへ入れればよい。やってはいけないのは、入力よりマシに見えるからといって失敗した変換の出力をアーカイブに保存することだ。それはいまや、検査が確認を拒否した主張を帯びている

ファイルがすでに持つマークを読む

何かを変換する前に、ドキュメントが自分自身について何と言っているかを知る。既存の標準マークを読めないPDF/A検査は、宣言にかかわらずすべてのファイルをパート1に対して判定する。つまり完全に妥当なPDF/A-2やPDF/A-3ドキュメントが、マークを持たない、またバージョンが高すぎると報告される。事実の逆だ

マークは、プロデューサーがXMP要素として書いたか属性として書いたかにかかわらず読まれる。どちらの形も通常のXMPであり、片方だけを受け入れると他のプロデューサーからのファイルがマークなしに見えてしまう。よそでは検証できるドキュメントが自社パイプラインで落ちる理由に悩んだことがあるなら、ここを最初に見るのが良い

アーカイブ前のサニタイズと、知っておくべきバグ

アーカイブ変換とサニタイズはしばしば併走する。セキュリティポリシーが除去したいコンテンツと、PDF/Aが禁じるコンテンツの重なりが大きいからだ。SanitizeDocumentはJavaScriptを除去するが、最後のスクリプトを除去すると同時に、そのスクリプトが残していく空の名前ツリーも取り除く。このツリーが残っていると、ドキュメントがスクリプトを持っていたとリーダーに伝え続けるからだ

後半の挙動は苦い経験で学ばれた。パッケージリストのオフバイワンが、サニタイズが1つもスクリプトを除去していないのに除去したと報告する事態を生んだ。その結果、サニタイズ済みのドキュメントが開かれたときにまだスクリプトを走らせていた。この記事全体が依拠する一般原則、つまり「ライブラリと同じように自社パイプラインでも、操作を信じるのではなく結果を検証する」を裏付ける良い例証だ

周辺のアーカイブ作業については、PDF/AとPDF/UAのプレフライト真の再編集とコンテンツ除去、そしてアーカイブされたドキュメントが構造化された請求書データも持つ場合のメタデータ側を扱うFactur-X向けPDF/A-3 XMP拡張スキーマの各解説を参照のこと

PDFlibPasはDelphi、C++Builder、Lazarus向けのネイティブPascal PDFライブラリであり、変換、修復、検証はすべて自社プロセス内で完結し、チェーンに外部ツールを必要としない。サポートされるPDF/AパートとプラットフォームについてはPDFlibPas製品ページを参照のこと