PDF/A-1b とマークする変換器を出荷し、顧客の記録システムがそれらを 1 年取り込み、監査でバッチ全体を veraPDF にかけたら、その 3 分の 1 が非準拠で戻ってくる。クラッシュはなく、例外も投げられず、デスク上のどのビューアでも問題なく開ける。ただ、付けた標準そのものではなかったのだ。これはアーカイブ PDF で起きる典型的な失敗であり、「フラグを立てた」ことと「検証に通る」ことは同義ではない、という理由でもある
PDFium と PDF/A について最初に理解すべきなのは、エンジンはそれ自体とは無関係だということです。PDFium は PDF の描画、解析、書き込みを行いますが、その公開面には ConvertToPDFAも、OutputIntent ライターも、XMP API もありません。アーカイブ準拠に関わるあらゆる要素、XMP パケット、OutputIntent とその ICC プロファイル、カタログのマーカー、検証は、すべて PDFiumPas 自体にあり、約 2,000 行の純粋な Pascal ユニット (FPdfPdfa.pas) が保存済みバイト列を解析し、インクリメンタル更新として書き戻します。どこで作業が起きているかを知れば、どこにバグが潜むかも分かりますし、そのバグは PDFium には潜んでいません
PDF/A が本当に要求するものと、どこで引っかかるか
PDF/A は 1 つの形式ではありません。ISO 19005 は 3 つの部分 (PDF/A-1、-2、-3) を定義し、それぞれの中に、異なる内容を約束する準拠レベルがあります。Level B (basic) は、視覚的な外観が再現可能であることだけを保証します。Level A (accessible) は、その B の上にタグ付き構造ツリーと Unicode マッピングを追加します。Level U は parts 2 と 3 にしか存在せず、その中間に位置します。つまり、完全な構造ツリーは持たないが、Unicode テキストは信頼できるということです。ISO 19005-1 には Level U がなく、ライブラリはその制約を直接エンコードしています
実務では、形式の規則のうち少数のものが引っかかりやすい。暗号化は完全に禁止されており (ISO 19005-1 §6.1.3 と後継規格)、PDF/A ファイルは /Encryptdictionary を持てません。文書は、宛先が有効な ICC プロファイルである OutputIntent を通じて、出力レンダリング条件を宣言しなければなりません (§6.2.3.2)。準拠宣言そのものは、PDF/A identification スキーマの下で XMP メタデータとして現れる必要があります。Level A ではさらに §6.8 の論理構造、つまり文書を機械可読にするタグツリーが必要です。これらのどれか 1 つでも欠ければ、準拠検証ツールは、レンダリングが完全に正しくてもそのファイルを拒否します
アーカイブを作る 1 つの呼び出し
PDFiumPas は TPdf.SaveAsPdfA の背後にあるパイプライン全体を公開しています。シンプルなオーバーロードは対象の準拠レベルを受け取り、既定は PDF/A-1b です。これは「とにかく将来にわたって表示できるようにする」という一般的な用途に対する適切な既定値です
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.LoadFromFile('invoice.pdf');
// Default conformance is pac1b (PDF/A-1b)
if Pdf.SaveAsPdfA('invoice_archive.pdf') then
// file now carries XMP, sRGB OutputIntent, and catalog markers
else
raise Exception.Create('PDF/A save failed');
finally
Pdf.Free;
end;
end;
内部では、これは 2 段階の処理です。SaveAsPdfAまず PDFium に文書を FPDF_SaveAsCopy 付きでシリアライズさせ、そのバイトストリームを InjectPdfAMarkers に渡します。するとそこに XMP メタデータ、埋め込み ICC プロファイルを含む sRGB OutputIntent、そして書き換えられたカタログがインクリメンタル更新として追加されます。ソースは位置 0 から読み込まれ、宛先は位置 0 から書き込まれます。元のオブジェクトツリーはそのまま残り、マーカーは既存の %%EOF の後に乗ります。バイト列が必要でファイルは不要なら、SaveAsPdfAToStream は TStream と同じオプションを受け取ります
オプションレコードで準拠レベルを選ぶ
特定の part と level を対象にするには、TPdfASaveOptions レコードを渡します。その Conformance フィールドは TPdfAConformance の値を受け取ります。列挙型は有効な組み合わせをすべて含み、それ以外はありません: pac1b、pac1a は part 1 用; pac2b、pac2u、pac2a は part 2 用; pac3b、pac3u、pac3a は part 3 用、さらに pacUnknown と pacNone は検証側用です。pac1u は存在しません。標準にそのレベルがないからです
var
Pdf: TPdf;
Opts: TPdfASaveOptions;
begin
Pdf := TPdf.Create(nil);
try
Pdf.LoadFromFile('report.pdf');
Opts := TPdfASaveOptions.Default;
Opts.Conformance := pac2u; // PDF/A-2u: reliable Unicode text
Opts.Title := 'Quarterly Report 2026';
Opts.Author := 'Finance';
// Leave IccProfileData empty to use the built-in sRGB IEC61966-2.1 profile
if not Pdf.SaveAsPdfA('report_a2u.pdf', Opts) then
raise Exception.Create('PDF/A-2u save failed');
finally
Pdf.Free;
end;
end;
レコードの大半は空欄のままで構いません。Title, Author, Subject, Keywords, Creator, and Producerを空欄にし、SaveAsPdfA文書の Info 辞書からそれらを自動入力し、FPDF_GetMetaTextで処理します。続いて CreationDateと ModDateを空欄にすると、現在の UTC 時刻が XMP の両方の日付に使われます。続いて DocumentIdと InstanceIdを空欄にすると、ライブラリが FPDF_GetFileIdentifierからそれらを事前設定し、ソースバイト列から導出した決定論的な ID を代替として使います。明示的に上書きしたい唯一のフィールドは IccProfileData: 空にすると、組み込みの sRGB IEC61966-2.1 プロファイルが使われますが、CMYK またはグレースケールのワークフローでは独自のものを指定してください
Level A が劣化する理由と、それが正直な選択である理由
ここには、フラグさえ立てれば保証になると思っている人をつまずかせる細部があります。pac1aタグツリーのない文書に対しても SaveAsPdfA は本物のタグ付き構造をチェックします (/StructTreeRoot に加えて /MarkInfo と /Marked true) が存在しない場合は、主張を引き下げます: pac1a は pac1b にpac2a は pac2b に変わり、以後 3 つの part すべてで同様です。内部ヘルパーは PdfAIsLevelA と PdfADowngradeToLevelB です
理由ははっきり述べる価値があります。自分が満たしているレベルを正直に宣言するファイルは、存在しないレベルを偽るファイルよりもずっと有用です。Level U は別の扱いになります。本物の Unicode 対応を検出するには、単純な「/ToUnicodeがあるか」テストでは正当な文書を過剰に劣化させてしまいます (WinAnsi などのエンコーディングは例外です)。そのため保存側は、呼び出し側が宣言したとおりに U の主張を出し、食い違いは検証側でフラグを立てるようにしています。確実に Level A のアーカイブが必要なら、変換前に文書へタグを付けてください。コンバーターは、そこにない構造を作り出しません
ICC の落とし穴を、実際の検証器だけが暴く
これが最も厳しい教訓を教えた失敗です。ライブラリ自身のチェッカーは通ったのに、ISO 19005 の参照検証器である veraPDF は通しませんでした。PDF/A では OutputIntent の宛先プロファイルが有効な ICCBased ストリームである必要があり、§6.2.3.2 は検証器にそのストリームを色空間として検証させます。ICCBased ストリームは /N を宣言しなければなりません。これは色成分の数です。初期版のインジェクターは ICC ストリーム辞書に /Length だけを書き込み、/N を入れなかったため、veraPDF は "The N entry (value null)... is missing" という結果で拒否しました
何が厄介かというと、その拒否は PDF/A-1b と -1a でしか発生しなかったことです。part 2 と part 3 の準拠モデルは、その宛先プロファイルに対してそのチェックを実行しませんでした。そのため、まったく同じ注入構造でも pac2b、pac3b、および pac2u で検証に通ったのに、pac1b で pdfaid:part の値だけで失敗しました。ユニットテストでは到底見えません。なぜなら、ライブラリ自身の ValidatePdfAComplianceは、/DestOutputProfileキーが存在するかどうかしか見ておらず、ストリーム辞書の内部までは見ていませんでした。内部テストは緑のまま通過し、本番のアーカイブ検証は失敗しました
修正は IccComponentCountで、ICC ヘッダーのオフセット 16 にある data colour space signature を読み取り、コンポーネント数へマップします: GRAY は 1、RGB 、Lab 、および XYZ は 3、CMYK は 4、未知のプロファイルは既定で 3 になります。この数がストリーム辞書の /N に入ります。3 にハードコードされているのではなく計算しているのは、CMYK またはグレースケールのプロファイルを IccProfileData 経由で渡した呼び出し元でも正しい値になるようにするためです。より広い教訓は方法論的です。ライブラリ内チェッカーと権威ある検証器は、それぞれ盲点を持っています。PDF/A 出力は、self-check に頼るのではなく veraPDF のような参照実装に対して end-to-end でテストしなければなりません。きれいなアーカイブの背後にある同じインクリメンタル更新の規律は 圧縮された object と xref ストリームの検証で扱われています。これは、インジェクターが取り込む現代的な PDF の多くがクロスリファレンスストリーム上に構築されているため重要です
暗号化、xref ストリーム、その他の境界
ISO 19005 では暗号化が禁止されているため、保存経路は書き込む前にそれを取り除きます。SaveAsPdfAを適用し、FPDF_REMOVE_SECURITYシリアライズ時に適用するので、暗号化されたソース (パスワード付きで読み込まれたもの) はアーカイブに入る途中で復号されます。暗号化されていない文書ではこれは no-op で、何も変わりません。対になる制約は HotPDF が逆方向から強制しているものと同じです。1 つのファイルが暗号化済みでありながら PDF/A でもあることはできません。ワークフローで両方が必要なら、答えは 2 つの成果物です。配布用の暗号化コピーと、アーカイブ用の別のクリーンなコピーです
さらに 1 つ、壊れて初めて見える境界があります。純粋なクロスリファレンスストリームを使い、trailerkeyword を持たない PDF 1.5+ 文書です。インジェクターはソースの /Infoを見つけてインクリメンタル更新を追加するために trailer を読み取り、xref-stream 形式を受け入れなければなりません。そうしないと、そのような文書はマーカーを黙って落としたままコピーされてしまいます。ISO 32000-1 §7.5.6 は、xref-stream 文書の後に classic trailer のインクリメンタル更新を続けることを明示的に認めており、/Prevが xref-stream のオフセットを指します。これはまさにインジェクターが出力する構造です。PDFium 自身の FPDF_SaveAsCopyは常に classic trailer を書き出すので、通常のパイプラインではインジェクターが pure xref-stream のソースに出会うことはありませんが、読み取り側は他所から来た文書には対応します
検証してから、主張を信じる
ライブラリはバイトレベルのチェッカー、TPdf.ValidatePdfAを出荷しています。返り値は TPdfAValidationResultです。その Conformance フィールドは検出されたレベルを報告し、Issues は TPdfAValidationIssue 値の集合です。便宜メソッド IsCompliant は、実際のレベルが検出され、問題集合が空のときだけ true になります。バッチの高速な最初のゲートとして実行してください
var
Pdf: TPdf;
Res: TPdfAValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.LoadFromFile('invoice_archive.pdf');
Res := Pdf.ValidatePdfA;
if Res.IsCompliant then
Writeln('Conformant: detected level ', Ord(Res.Conformance))
else
Writeln('Issues found: ', SizeOf(Res.Issues), ' flags set');
finally
Pdf.Free;
end;
end;
その見返りを正直に見てください。バイトレベルのチェッカーは、構造上の問題 (OutputIntent の欠落、禁止されたアクション、存在する /Encrypt、part 1 で禁止されている透明性) を高い確度で捉えますし、フォント埋め込み検出は、グリフ単位の網羅を追うのではなく高信頼のシグナルだけを返すことを意図した件数ヒューリスティックを使っています。できないのはコンテンツストリームの演算子解析で、これは完全なコンテンツパーサーを必要とし、設計上スコープ外です。リリースのゲートとしては、ライブラリ内チェッカーと veraPDF を組み合わせてください。チェッカーは即時に動き DLL なしでどこでも実行でき、veraPDF は権威あるものです。その組み合わせをバッチ実行に組み込むのが バッチ前検査レポート CLIであり、実際のアーカイブワークフローではこの検証はそこに属します
ここで示した SaveAsPdfA, InjectPdfAMarkers, と ValidatePdfA API は PDFium Componentが Delphi、C++Builder、Lazarus/FPC 向けに付属しています。製品ページには、完全な準拠列挙とこれらの例の背後にあるオプションレコードを含む API リファレンス全体へのリンクがあります