技術記事

Delphiで正確なPDFバージョン保存を行う:PDFiumPasの適合性チェック

GoogleのPDFiumエンジンを包むDelphi・C++Builder用ラッパーであるPDFiumPasは、TPdf.SaveAsメソッドのPdfVersionパラメータを通じて、文書を1.3から1.7までの正確なPDFバージョンで保存する。PDFium自身のFPDF_SaveWithVersion呼び出しは%PDF-M.mヘッダーを書き換えるだけであり、文書の実際のコンテンツがそのバージョンで合法かどうかはチェックしない。PDFiumPasは、そのファイルがメソッドを離れる前にアクティブな相互参照の改訂チェーンを歩き、Adobe Extension Levelの宣言をチェックする保存後の適合性パスでそのギャップを埋める

この区別が最も重要になるのは印刷制作の現場であり、そこではPDF/Xプロファイルが正確なPDFバージョンを指定し、プリフライトツールやRIPは自身のヘッダーと静かに食い違う何かを拒否する。このシナリオは出力側からPDFiumPasによる印刷対応PDF/X文書の検証で扱われている。SaveAsはターゲットを、より古いpv10からpv12の値と並んでpv13からpv17を持つTPdfVersion列挙型として、さらに増分保存か完全書き換えかを表す独立したTSaveOptionとして公開する。PdfVersionを渡すと、PDFiumPasは1回の呼び出しで2つの仕事をこなす:PDFiumに要求されたヘッダーを刻印するよう求め、その後、たった今書き込まれたばかりのバイトを読み直し、アクティブなコンテンツがそのバージョンで合法に存在できない場合、ファイルを返すことを拒否する

var
  Pdf: TPdf;
begin
  Pdf:= TPdf.Create(nil);
  try
    Pdf.FileName:= 'source.pdf';
    Pdf.Active:= True;
    try
      Pdf.SaveAs('press-ready.pdf', saNoIncremental, pv17);
    except
      on E: Exception do
        // E.Message names the offending feature and the version or
        // extension level it actually needs, for example:
        // "RichMedia annotations and RichMediaExecute actions require
        // /Extensions /ADBE with /BaseVersion /1.7 and /ExtensionLevel 3
        // or newer."
        raise;
    end;
  finally
    Pdf.Free;
  end;
end;

なぜファイル内の最後のオブジェクト定義を信頼するのは間違いなのか

PDFファイル内で与えられた番号を持つ最後の物理オブジェクトが、今日準拠したリーダーがその番号のために解決するオブジェクトであるとは限らない。複数回の増分更新を経たPDFは1つのオブジェクトグラフを持っているのではなく、単一のファイルの内部に層をなして重なったそれらの歴史を持っている。そして追記のサイクルはそれぞれ、あるオブジェクトを解放したり、新しい世代番号のもとで再定義したり、もはやそれを指す相互参照エントリがない状態で古い物理的な本体を2つのendobjマーカーの間に残したりできる

PDFiumPasは、xrefの改訂を明示的に追跡する前は、まさにその失敗モードにぶつかっていた:後のページオブジェクトの書き換えによって孤立したRedact注釈、あるいはもはやそれを指すxrefエントリがない状態で物理的に残された/MarkInfo辞書は、依然としてバイトスキャンで見つかることがあり、リーダーが実際に開くであろう文書にはもはや適用されないバージョン機能チェックを依然として引っかけてしまうことがあった。この失敗の方向は誤った受理ではなく誤った拒否だった:現行の改訂ですでにある機能を越えて本当に移行したファイルが、もはや誰も到達できないコンテンツのせいで低いバージョンでの保存をブロックされることがあった

PDFiumPasはどのオブジェクト定義が実際にアクティブかをどう判定するのか

PDFiumPasは、バイトをスキャンしてオブジェクトヘッダーを探すのではなく相互参照チェーンを歩くことで、準拠したリーダーと同じ方法でアクティブなオブジェクトの集合を解決する。リゾルバはファイル内の最後のstartxrefオフセットから始まり、それぞれの/Prevリンクを後方へ辿ってより古い改訂を通り、その途中で従来型の相互参照テーブル、ハイブリッドの/XRefStmリンク済みストリーム、純粋な相互参照ストリームを解析する。この走査は新しいものから古いものへと実行され、各オブジェクト番号は最初に見つかった時点で確定する。そのため後の改訂での解放エントリは、より古い改訂で書かれたオブジェクトの本体を正しく覆い隠し、新しいオフセットや世代のもとでの再定義は、それが置き換えたものに常に勝つ

オブジェクトストリームのメンバーは、単純なオフセットルックアップだけでは提供できない追加のチェックを受ける。この仕組みはPDFiumPasによるオブジェクト・相互参照ストリームの検証でより詳しく扱われている。/ObjStmから回収された圧縮オブジェクトは、その親ストリームが同じ走査でアクティブであると確認されなければならず、その索引はPDFiumPasがそれをライブなコンテンツとして扱う前に、そのストリームのヘッダー内でのメンバー自身の位置と一致しなければならない。ISO 32000-1のセクション7.5.8.4は、従来型の互換性テーブルがあるオブジェクトを解放済みとマークしている一方で、トレーラーの/XRefStmエントリが同じオブジェクトを他のどこかの圧縮メンバーとして同時に定義しているハイブリッド参照のケースさえ説明している。PDFiumPasは、従来型のエントリが適用される前に、補足的なxrefストリームを同じ改訂にマージする。そのため圧縮された定義は、仕様が意図する通りに勝つ

Adobe Extension Level:バージョン番号の上にあるゲート

%PDF-1.7ヘッダーは、ISO 32000-1が2008年に標準化した機能セットしか約束しないが、今日PDFの生成者が頼っているいくつかの機能は、その同じバージョン番号の上に重ねられたAdobe専用の補足として後から出荷された。Adobeは各補足を、開発者接頭辞のもとで文書カタログの/Extensions辞書に記録されるBaseVersionExtensionLevelのペアとして登録した。Adobe自身の拡張にはADBEを使う。そのためリーダーは、単なるPDF 1.7ファイルと、番号付きの拡張レベルも実装しているファイルとを見分けられる。その宣言なしにpv17で保存することはそれ自体エラーではない;それは、アクティブなコンテンツがその宣言がカバーすべき機能に実際に依存している瞬間にだけエラーになる

どの高バージョン機能が明示的バージョンのゲートを引っかけるのか

PDFiumPasは、単にバージョン番号から推測するのではなく、特定の仕様に基づいたリストをチェックする。明示的な/SMaskInDataエントリを持つ画像辞書、あるいは16という/BitsPerComponent値を持つ画像辞書は、どちらもPDF 1.5を要求し、16ビットのケースはPDF Reference 1.5のセクション4.8の画像成分規則に直接従う。RichMedia注釈とRichMediaExecuteアクションは、/BaseVersion /1.7/ExtensionLevel 3以上を要求する。/Type /3D/Subtype /PRCの両方を持つ辞書によって識別されるPRC 3Dストリームは、同じベースバージョンだが/ExtensionLevel 1だけを要求する。地理空間Measure辞書とProjection注釈は、RichMediaが依存するのと同じAdobeの補足である/BaseVersion /1.7/ExtensionLevel 3を要求する

この地理空間チェックには、PDFiumPasの上に自分自身のバージョンゲート付きロジックを構築することがあるなら知っておく価値のある仕様読解上の詳細がある。ISO 32000-1の表254はMeasure辞書の/Typeエントリを任意とマークしており、「存在する場合はMeasureでなければならない」とだけ注記している。一方、表311はPRCコンテンツが存在する3Dストリーム辞書に対して/Typeを必須にしている。マッピングツールからの実世界のGeoPDF出力は、Measure辞書の/Typeを日常的に省略し、/Subtype /GEOだけを書く。そのためPDFiumPasの地理空間検出器は、そのPRC 3D検出器が安全に両方のキーを要求できるのとは異なり、/Subtypeだけで一致を判定する。両方の辞書に/Typeを要求していたら、準拠したGeoPDFコンテンツが検出されずにゲートをすり抜け、それを裏付ける拡張レベルの宣言のない単なるPDF 1.7ファイルに着地してしまっていただろう

PDFiumPasはサポートされていない機能を自動的にダウングレードするのか

一般的な機能としてはそうしない、そしてそう仮定することがここで避けるべき間違いである。SaveAsはターゲットバージョンを内部ルーチンであるValidatePdfVersionComplianceに流し込み、そのルーチンがターゲットバージョンやその拡張レベルの宣言がサポートできない機能を見つけると、SaveAsはファイルを書き込む代わりにそのルーチンのエラーテキストを運ぶ例外を発生させる;呼び出し元は静かに書き換えられた文書ではなく、正確な機能名を伴う理由を返される。PDFiumPasが自動的にコンテンツを書き換える唯一の場所はPDF 1.3ターゲットであり、そこではPDFiumがターゲットバージョンに関わらず常にExtGState辞書に書き込む意味的に中立な/BM /Normal/CA 1/ca 1の透明度の既定値が取り除かれる。なぜならこれらの特定の値は視覚的な意味を一切持たず、PDF 1.3はこれらのキーが存在する以前のものだからだ

// PDF 1.3 targets rewrite the saved bytes to strip transparency
// defaults PDFium always emits, so incremental mode cannot apply
Pdf.SaveAs('legacy-archive.pdf', saIncremental, pv13);
// raises: PDF 1.3 normalization is incompatible with incremental
// save mode

本物の非既定の透明度と画像ソフトマスクは、PDF 1.3ターゲットではそれでも完全に失敗する。なぜならそれらを取り除くとページの実際の見た目が変わってしまい、PDFiumPasはあなたの代わりにその判断を下さないからだ。正確なバージョンをバッチパイプラインに投入する前に計画しておく価値のある、関連する2つの制限がある。明示的バージョンの出力は決して/Encrypt辞書を運ばない;ソースが保護されている場合、保存は即座に失敗する。これはたまたま、暗号化を禁じるPDF/XやPDF/Aのプロファイルと一致するが、復号があなたのワークフローの中でSaveAsが代わりにやってくれることではなく別のステップであることを意味する。PDFiumPasにはまた、カタログに/Extensions /ADBE宣言を書き込む公開メソッドが存在しない。そのため、RichMedia、PRC 3D、あるいは地理空間コンテンツを含むがその宣言を欠くソースファイルは、どんなPdfVersionを要求してもゲートを通過しない;その宣言はすでにソースに存在していなければならない(通常はオーサリングツールがそれを書いたため)、さもなければその機能は保存前に取り除かれなければならない。読み取り専用のTPdf.PdfVersionプロパティは、正確なバージョンでの保存を試みる前にチェックする価値がある。なぜなら、それは保存時のバリデータ自身が頼っているのと同じカタログを認識した実効バージョン——ヘッダーか/Versionの上書きのうちどちらが現在有効であれ——を解決するからだ

Pdf.FileName:= 'incoming.pdf';
Pdf.Active:= True;
// PdfVersion resolves the same catalog-aware effective version the
// save-time validator uses, so a mismatch here is worth investigating
// before spending a full SaveAs attempt on it
LogSourceVersion('incoming.pdf', Pdf.PdfVersion);

正確なバージョンターゲットに対するSaveAsの例外を、バグとしてではなくプリフライトレポートとして扱ってほしい:そのメッセージは、ソース文書が違反している正確な条項を名指ししており、それはまさに、印刷会社やアーカイブパイプラインがファイルをこれ以上先に進める前に必要とする情報である。ここで説明した明示的バージョン保存経路、アクティブなxref改訂リゾルバ、そしてAdobe Extension Levelのチェックは、DelphiおよびC++Builder向け標準PDFiumPasコンポーネントの一部として出荷される。製品ページには、適合性・フォームAPIの残りとともにTPdf.SaveAsの全リファレンスが掲載されている