技術記事

PDFlibPasでDelphiからPDF/E-1技術文書を書き出す

PDF/E-1は技術文書向けのアーカイブプロファイルであり、PDFlibPasはこれを、SetPDFEModeで切り替えるオーサーモードと、コンテンツストリームをオペレータ単位で読む有界プリフライトの組み合わせとして実装しています。このプロファイルはPDF/Aのラベル貼り替えではありません。独自の識別ネームスペース、独自のライフサイクルメタデータ要件を持ち、しかもコンテンツ検証を、あなたが今まで出会ったどのアーカイブプロファイルよりも厳しくするルールを1条掲げています

このプロファイルが存在する理由は、エンジニアリングの成果物です。20年後にも読めて、変更されていないことを証明でき、リビジョン履歴が生き残り、別の建物のプロッタでも同じ意味の色が出る図面セット。これらの要求が作り出す仕様の要求事項は、その大半がページコンテンツの外側、メタデータとカラーマネジメントにあります。汎用のPDFライタがまさにそこを間違えるのです

PDF/Aの亜種ではなく、独自の識別

最初に正しく押さえるべきは、PDF/E-1の識別がPDF/AやPDF/Xのパターン流用では作れない点です。別個のXMPネームスペースhttp://www.aim.org/pdfe/ns/id/を使い、バージョン値は2か所に出現しなければなりません。ドキュメント情報エントリとして、およびネームスペース修飾付きXMPプロパティとして。XMPプロパティだけ、あるいは情報エントリだけを出力すると、意図を表明してはいるが検証には落ちるファイルが出来上がります

アウトプットインテントも同様に具体形が決まっています。PDF/E-1はサブタイプ識別子ISO_PDFE1付きの埋め込みICCプロファイルを要求し、そのプロファイルのコンポーネント数は、ドキュメントが実際に使うデバイスカラーファミリーと一致しなければなりません。この最後の条項こそ、実装が静かに間違える場所です。インテントを事前に選んでそのまま放置することが許されない、という意味になるからです

デバイスカラーに文書全体のスイープが必要な理由

カラースペースが、ページレベルのスキャンでは決して届かないリソースディクショナリの中に隠れているからです。PDF/E-1はDeviceRGBとDeviceCMYKを、ドキュメントにとって相互排他のファミリーとして扱います。プロファイルを検証するということは、ファイル内の何かが使っているすべてのデバイスカラースペースを把握するということです。フォームXObjectには独自のリソースがあります。パターンにも、画像にもあります。ページの中のフォームXObjectの中のタイリングパターンは3階層の深さになり、トップレベルのページリソースだけをチェックするバリデータは、両ファミリーを使うドキュメントを通してしまいます

そこでスイープは、ページ、フォーム、画像、パターンを1回のトラバーサルで歩きながらカラースペースを登録し、その後に初めて、ドキュメントが整合しているか、アウトプットインテントが一致するかを判断します。同じ考え方がプリフライトアーキテクチャ全体を支えています。部分的なトラバーサルは偽のパスを生み、コンフォーマンスチェックの偽パスはチェックなしどころか悪いのです。証拠として記録されてしまうからです

var
  Lib: TPDFlib;
  Diag: WideString;
begin
  Lib := TPDFlib.Create(nil);
  try
    Lib.LoadFromFile('assembly-drawings.pdf');

    if Lib.SetPDFEMode(1) = 0 then
      raise Exception.Create('PDF/E author mode was refused');

    // オーサーモードはライフサイクルメタデータを保存のたびに同期し続ける
    // 保存の前に、このドキュメントが独自のゲートを通るか尋ねる
    if not Lib.PDFEReadyForSave then
    begin
      Diag := Lib.GetPDFEDiagnostics;
      Writeln('PDF/E blockers: ', Diag);
      Exit;
    end;

    Lib.SaveToFile('assembly-drawings-pdfe.pdf');
  finally
    Lib.Free;
  end;
end;

ライフサイクルメタデータは保存ごとの義務

PDF/E-1が求めるのはドキュメント識別子だけではありません。最低限のセットには、メディア管理ドキュメント識別子、バージョン識別子、レンディションクラス、作成時刻、変更時刻、メタデータ時刻、そしてタイトルが含まれます。これはリビジョン追跡のための語彙であり、エンジニアリングの成果物は1度書いて終わりではなく、再発行されることを期待されるから存在しています

実装への帰結として、これらのフィールドはドキュメント作成時に設定しておくことはできません。モード有効化の時点で変更時刻を書き、その後にドキュメントを編集したなら、XMPスナップショットと実際のドキュメント状態はズレており、両者を比較するバリデータは誰も意図していない不整合を報告します。そこでオーサーモードは保存の直前に毎回フィールドを同期します。メタデータが、モードを有効化した時点で存在したバイト列ではなく、今まさに書き出されようとしているバイト列を記述するようにするためです

これはコンフォーマンスメタデータ全般の原則であり、PDF/Eから切り離して言う価値があります。派生メタデータは編集パスではなく保存パスに属する、ということです。ドキュメント状態から計算されるフィールドはすべて、状態が凍結される瞬間に再計算しなければなりません。さもなければ無効化のないキャッシュです

PDFlibPas の PDF/E-1 図解。ページ、フォーム XObject、タイリングパターン、画像のリソースディクショナリを歩いて DeviceRGB と DeviceCMYK の各ファミリーを収集し、それから整合性を判断する文書全体のデバイスカラースイープと、オーサーモードが保存の直前に毎回再同期するライフサイクルメタデータの各フィールド。XMP スナップショットがこれから書き出されるバイト列と一致するようにする
カラースペースの整合は1回のトラバーサルですべてのリソースディクショナリに届いて初めて判断でき、派生ライフサイクルメタデータはモード有効化時ではなくドキュメント状態の凍結時に再計算されます

コンテンツ検証を厳しくするあのルール

PDF/E-1は、互換セクションのオペレータが未知のコンテンツを吸収することを許しません。通常のPDFでは、BXEXが、認識できないオペレータを無視しなければならない領域を括ります。生成側が古いリーダーを壊さずに新しい構文を出力できる、逃げ道です。PDF/E-1の下ではこの逃げ道は閉ざされ、プリフライトが認識しないオペレータは、互換セクションの中にあろうとなかろうと無条件に報告されます

バリデータへの影響は大きい。理解できない領域をスキップできません。つまりオペランドパーサは、すべてのコンテンツストリームのすべてのオペレータを実際にパースしなければなりません。ここで有界化が効いてきます。トラバーサルはネスト128レベル、100万オブジェクト、コンテンツ64 MiBで打ち止めになり、これらの上限は性能チューニングではありません。敵意の有無を問わず壊れたファイルは、循環を含むオブジェクトグラフや、再帰バリデータをスタックオーバーフローに変えるネスト深度を見せつけることができ、この上限こそが検証パスをDoSベクトルにしないための仕掛けです。同じ防御姿勢については信頼できないPDFの安全なパースで説明しています

// 自分が作ったのではないファイルを、ドキュメントインスタンスに
// ロードせずにスタンドアロンで検証する
var
  Issues: TStringList;
  Stream: TFileStream;
  I: Integer;
begin
  Issues := TStringList.Create;
  Stream := TFileStream.Create('incoming.pdf', fmOpenRead or fmShareDenyWrite);
  try
    if CheckCompliancePDFE(Stream, '', 0, Issues) = 0 then
      for I := 0 to Issues.Count - 1 do
        Writeln('PDF/E: ', Issues[I]);
  finally
    Stream.Free;
    Issues.Free;
  end;
end;

保存ゲートが修復するもの、拒否するもの

ゲートは作業を2段階に分けますが、この分割自体が単独で使える設計アイデアです。まず、安全に修復できるものを正規化します。注釈の印刷フラグ、テキスト注釈のズーム禁止・回転禁止フラグ、フォームディクショナリの外観生成フラグです。これらはプロファイルの下で正しい値が1つに決まり、情報内容を一切持たない設定です。だから黙って直すのが正しく、ここで拒否するのは杓子定規すぎます

次に、ドキュメントの意味を変えずには修復できない制約をチェックします。バージョン、識別、暗号化、アウトプットインテント、デバイスカラーの整合、そして動的フォームコンテンツの有無です。これらのいずれかに落ちるドキュメントは拒否されます。作者の代わりにアウトプットインテントを捏造したりカラーファミリーを選んだりすると、検証は通るが内容と食い違うファイルが出来上がるからです

Delphi 向け PDFlibPas の PDF/E-1 保存ゲート図解。ネスト 128 レベル、100 万オブジェクト、64 MiB の上限の下ですべてのコンテンツストリームのオペレータを走査する有界プリフライトが、注釈の印刷・ズーム・回転フラグを黙って修復し、誤ったバージョン、識別、暗号化、アウトプットインテント、デバイスカラー、動的フォームコンテンツを拒否し、GetPDFEDiagnostics 経由でブロッカーを報告する流れ
ゲートが黙って修復するのは情報を一切持たないものだけです。修復では歪んでしまう制約はすべて拒否し、1バイトがディスクに届く前に GetPDFEDiagnostics 経由で拒否をブロッカーリストへ変えます

保存前にGetPDFEDiagnosticsで診断を読み戻せば、拒否は失敗した操作ではなく、対処可能なリストになります。バッチパイプラインでは全ドキュメントで呼び出し、ファイルごとにブロッカーを記録し、失敗を人が見るキューへ振り分けてください。例外を上げる保存よりはるかに有用です。ブロッカーはたいてい固まって出るからです。同じ欠けたアウトプットインテントで40件落ちているなら、直すのは1か所であって40か所ではありません

アーカイブプロファイルの選び方

成果物がリビジョンライフサイクルを持つエンジニアリング文書であり、とりわけ出力がプロッタと大判プリンタに向かうためにデバイスカラーの整合が効いてくるなら、PDF/E-1が正しいターゲットです。文書全般の長期可読性が目的ならPDF/Aが正しいターゲットで、バリデータの対応も最も広いプロファイルです。両者は取り替え可能ではなく、片方を満たしながらもう片方に落ちるドキュメントがあり得ます

Delphi 向け PDFlibPas の PDF/E-1 と PDF/A アーカイブプロファイル比較の判断図。リビジョンライフサイクル、プロッタカラー、契約上の検証を伴うエンジニアリング成果物には、独自 XMP ネームスペースと ISO_PDFE1 アウトプットインテントを持つ PDF/E-1。一般的な長期可読性には、バリデータ対応が最も広い PDF/A
出発点は末端で誰がファイルを検証するかです。プロファイルごとに識別、メタデータ、カラーの保証は異なり、片方を満たしてもう片方に落ちるドキュメントがあり得ます

選ぶ立場なら、末端で誰がファイルを検証するかから始めてください。PDF/Aの検証ツールはどこにでもあり、PDFlibPasの対応するプリフライトはPDF/AとPDF/UAのプリフライトで説明しています。PDF/Eの検証はより専門的で、デフォルトではなく契約上の要件であることが普通です。既存アーカイブを、想定されていなかったプロファイルに引き上げなければならないときは、メタデータ修復付きのPDF/A変換のメタデータ修復パスが踏むべきパターンで、ここでも同じ形が使えます。識別し、安全なものは修復し、残りはリスト付きで拒否する

オーサーモード、有界コンテンツプリフライト、スタンドアロンのコンプライアンスチェックはいずれもPDFlibPas Delphi PDFライブラリに同梱されています。プロファイルの下でドキュメントを生成し、その後別のコードパスで独立に検証できる、コンフォーマンスの主張として信頼に値する唯一の構成です