PDFlibPas は、Delphi で PDF/UA-2(ISO 14289-2)ファイルを検証する際、ターゲットパートに 2 を指定して TPDFlib.CheckFilePDFUACompliance を呼び出します。これは独立した PDF/UA-2 プロファイルを実行するもので、構造ロールを PDF 2.0 名前空間と /RoleMapNS を通じて解決し、途中で切れたり読めなかったりする入力を失敗として報告し、完全な認証を決して主張しないカバレッジレポートを返します
最後の部分は、聞こえ以上に重要です。アクセシビリティ検証の現場では、緑のチェックマークが調達のスプレッドシートにコピーされたまま、二度と疑われることがありません。規格の半分を黙ってスキップしておきながら「pass」と印刷し続けるバリデータは、バリデータが存在しない場合より有害です。未知のものを偽りの既知に変えてしまうからです。PDFlibPas は逆の立場を取ります。機械的に検査できるものは検査し、検査しなかったものははっきり告げ、完了できなかったケースはすべて成功ではなく指摘として扱います
PDF/UA-2 は PDF/UA-1 のルールでは検証できないのか
PDF/UA-2 は、パート番号を 1 つ上げただけの PDF/UA-1 ではありません。PDF/UA-1 は PDF 1.7 と単一のフラットなロールマップの上に成り立ちます。一方 PDF/UA-2 は PDF 2.0(ISO 32000-2)の上に成り立ち、そこではすべての構造エレメントが名前空間に属せて、各名前空間がそれぞれ固有のロールマップを持ちます。UA-2 ファイルに UA-1 のチェッカーを向けると、実在の問題を見逃すと同時に、実在しない問題をでっち上げます
でっち上げのほうが始末が悪い部分です。PDF/UA-1 のバリデータは通常、ID を持つ Note エレメントや、番号なしの H 見出しの使用方法に関するルールを強制します。どちらのルールも持ち越せません。PDF 2.0 の構造名前空間では脚注は FENote で表現され、見出しは 6 レベル上限のない H1 から Hn になります。PDF/UA-2 はさらに進んで、番号なしの H ロールそのものを禁止しています。このツリーに UA-1 の見出しロジックを適用すると、規格が許していることに対して文句を言うか、規格が禁じているたった 1 つを見過ごすかのどちらかになります
そのため PDFlibPas の PDF/UA-2 プロファイルは独立したコードパスであり、旧来のパスへのフラグではありません。UA-1 の条項番号の再利用も拒んでいます。UA-1 チェッカーと共通の診断は、UA-2 レポートでは ISO 14289-2 に紐付け直され、条項番号が規格本文に対して検証されていない箇所では、レポートは推測の代わりに条項フィールドを空のままにします。自信たっぷりの条項番号より空の条項のほうが味気ないものですが、誰も対応関係を確認していない以上、防衛できる出力はこれだけです
PDFlibPas が RoleMapNS 経由でロールを解決する仕組み
PDFlibPas はすべての構造ロールを、既知の名前空間における標準ロールへ解決します。名前空間をまたぐ /RoleMapNS チェーンをたどり、カスタムロールで行き止まりになるチェーン、ループするチェーン、マッピングルールに違反するチェーンを報告します。これは PDF/UA-2 構造検証の中核です。支援技術が理解するのは標準ロールだけで、カスタムの /InvoiceLine は P や TR といったロールにたどり着くまで何の意味も持たないからです
リゾルバはエレメントの /S 名とその /NS 名前空間から出発します。名前空間がなければ、エレメントは暗黙に PDF 1.7 標準名前空間に属し、構造ツリールートの従来型 /RoleMap が適用されます。名前空間があれば名前空間辞書自身の /RoleMapNS が適用され、マッピングは 2 要素の配列を介して別の名前空間のロールを指せます。ここから導かれる検査は具体的です
- 構造エレメントから参照される名前空間は、有効な
/NSURI を持つ間接辞書であり、構造ツリールートの/Namespaces配列に列挙されていなければなりません(診断 10054) - カスタムロールは最終的に、PDF 1.7 名前空間、PDF 2.0 名前空間(
http://iso.org/pdf2/ssn)、MathML 名前空間のいずれかの標準ロールへ解決されなければならず、循環チェーンと行き止まりは 10055 として報告されます - 標準ロールをカスタムロールへ再マッピングすることはできず、ロールを明示的な所属名前空間の内部で再マッピングすることもできません
- 構造ルートは PDF 2.0 名前空間の
Documentエレメントをちょうど 1 つ含まねばならず(10057)、すべてのエレメントの/Pは実際の親を指さねばなりません(10058) - 番号なしの
H、および PDF 2.0 ロールへマッピングされていない従来型 PDF 1.7 のNoteは、10056 として報告されます
自作のツールにコピーしておく価値のある細部が 1 つあります。PDFlibPas は、ツリーから到達できるマッピングだけでなく、宣言されたすべてのマッピングを検証します。いまどのエレメントも使っていないロールマップの壊れたエントリは、それでも潜在欠陥です。次のコンテンツ更新で使い始めるからです。MathML も正直に扱われます。MathML 名前空間は有効な解決先として認識されますが、MathML の語彙自体は検証されず、そのことはカバレッジレポートに明記されます
Delphi から PDF/UA-2 検査を実行する
TPDFlib.CheckFilePDFUACompliance は 1 回の呼び出しで完結するエントリポイントです。ファイル、パスワード、ターゲットパート、オプションを渡すと、コード化された指摘を含む文字列リストハンドルが返ってきます。ハンドルが 0 で LastErrorCode が 0 なら、実装済みの検査では何も見つからなかったことを意味し、LastErrorCode が 0 以外なら呼び出し自体が失敗したことを意味します
var
Lib: TPDFlib;
Issues, I: Integer;
begin
Lib := TPDFlib.Create;
try
// TargetPart: 0 = XMP pdfuaid:part から判定, 1 = PDF/UA-1, 2 = PDF/UA-2
// Options: 1 = 最初の失敗で停止, 0 = 全指摘を収集
Issues := Lib.CheckFilePDFUACompliance('report.pdf', '', 2, 0);
if Issues = 0 then
begin
if Lib.LastErrorCode <> 0 then
Writeln('Validation did not run, error ', Lib.LastErrorCode)
else
Writeln('No issues from the implemented PDF/UA-2 checks');
end
else
try
for I := 1 to Lib.GetStringListCount(Issues) do // 項目は 1 始まり
Writeln(Lib.GetStringListItem(Issues, I));
finally
Lib.ReleaseStringList(Issues);
end;
finally
Lib.Free;
end;
end;
ターゲットパートは意図して選びます。0 を指定すると XMP メタデータから pdfuaid:part を読み、そこからプロファイルを選ぶので、汎用の受信箱バリデータにはそれが正しい選択です。自分の出力パイプラインでは、2 を明示的に渡します。ジェネレータが識別情報の書き込みを忘れていた場合、自動判定は UA-1 ルールにフォールバックし、間違った規格で検証することになります。明示的な 2 なら、代わりに識別情報の欠落に対する診断 10050 が出て、pdfuaid:rev が 2024 でなければ 10051 が、実効 PDF バージョンが 2.0 でなければ 10052 が加わります。作成側では、TPDFlib.SetPDFUA2Mode がこの識別情報を書き込み、バージョンを 2.0 に上げ、PDF 2.0 構造名前空間を既定として選択します。名前空間モデル自体についてはPDF 2.0、PDF/A-4、PDF/UA-2 名前空間の解説記事で説明しています
バリデータが予算を使い果たしたらどうなるのか
PDFlibPas はフェイルクローズで動きます。PDF/UA-2 チェッカーがトラバーサル上限、循環、パーサ上限、あるいは開けない文書に当たると診断を出力し、診断のない結果が合格に昇格することは決してありません。診断 10059 は、まさにこの「リーダーが完了できなかった」ケースのために予約されています
予算は明示されており、カバレッジ JSON に公開されています。構造の深さ 256、構造オブジェクト 100,000 個、任意のロールマップチェーン上のホップ数 256 です。これらの数値が存在するのは、構造ツリーが信頼されていない入力から来るからです。悪意のあるファイル、あるいは単に壊れたファイルは、エレメントを数千レベルの深さにネストしたり、子から先祖へ指し戻したり、何百もの名前空間をチェーン状にたどるロールマップを構築したりできます。予算のないバリデータはクラッシュするかハングするかどちらかです。そして予算はあるのに診断のないバリデータは、もっと微妙で悪質なことをします。早々に停止し、読んだ範囲では何も問題を見つけられず、成功を報告するのです
同じ原則が 1 つ上の層にも適用されます。基盤のリーダーが 100 未満のスコアを返しながら指摘行を 1 行も返さない場合——どのルールも実行される前にパースが中断されたときに起こり得ます——PDFlibPas は 10059 の行を挿入し、空のリストがクリーンな結果と誤解されないようにします。検査中の例外も 10059 の行になります。わずかなコード量で、検証パイプラインが所有者に嘘をつく最も一般的な経路を塞げます
PDF/A と 1 回のパースを共有し、レポートを組み立てる
バッチ処理向けには、バリデーションパイプライン API がファイルを 1 回だけパースし、共有されたパース結果に対して複数の検査を実行します。RunPDFUAValidationPipelineTask はそのファミリーの PDF/UA メンバーで、1 回呼び出し型の関数と同じターゲットパート値を取ります。PDF/A と PDF/UA-2 のプリフライトを組み合わせても、大きな文書を 2 回全量読みするコストを払わずに済みます
var
Pipe, PDFAIssues, UAIssues: Integer;
Json: WideString;
begin
Pipe := Lib.BeginValidationPipeline('archive.pdf', '');
if Pipe = 0 then
Exit;
try
PDFAIssues := Lib.RunValidationPipelineTask(Pipe, 1, 0); // 1 = PDF/A
UAIssues := Lib.RunPDFUAValidationPipelineTask(Pipe, 2, 0); // PDF/UA-2
// ... 前の例と同様に両方のハンドルを検査し、解放する ...
Writeln('Parses: ', Lib.GetValidationPipelineParseCount(Pipe));
// ComplianceTests: 1 = PDF/A, 2 = PDF/UA, 0 = 両方; ReportFormat 1 = JSON
Json := Lib.CreateValidationPipelineReport(Pipe, 0, 0, 1);
finally
Lib.ReleaseValidationPipeline(Pipe);
end;
end;
構造化レポートは、ファイルがパート 2 と自己識別する場合、アクセシビリティセクションに PDF/UA-2 と ISO 14289-2 のラベルを付け、すべての PDF/UA セクションに "fullCertification": false と埋め込みカバレッジオブジェクトを付けています。テキストのプリフライトレポートには、実装済みの検査だけが実行されたこと、完全なアクセシビリティ認証には追加の検証と人間によるレビューが必要なことを述べた行が加わります。より広範なレポート形式、ルール ID、CI ゲーティングのパターンについてはPDF/A と PDF/UA のプリフライトガイドで説明しています
カバレッジレポートが実際に保証するもの
TPDFlib.GetPDFUAReadValidationCoverageJSON が保証するのは 1 つだけです。PDF/UA-2 チェッカーが検査したものと検査しなかったものの正確なリストです。パート 2 では false に固定された fullCertification、checked 配列、unverified 配列、そしてトラバーサル上限を返します。ファイルがアクセシブルであることまでは保証せず、どんなコードパスもそれ以外を言わせない設計です
var
Coverage: WideString;
begin
Coverage := Lib.GetPDFUAReadValidationCoverageJSON(2);
// 検証結果の代わりではなく、隣に保存する
TFile.WriteAllText('report.coverage.json', Coverage, TEncoding.UTF8);
if Pos('"fullCertification":false', Coverage) > 0 then
Writeln('Machine checks only; schedule a human accessibility review');
end;
checked リストは、識別情報(パート、リビジョン、プレフィックス)、実効 PDF バージョン、タイトルメタデータ、/MarkInfo、DisplayDocTitle、名前空間参照、ロールマップ解決、文書ルート、構造の親子関係、ツリーの循環、H と Note のルールをカバーします。unverified リストも同じくらい具体的です。コンテンツのタグ付けと読み上げ順、構造の包含関係、フォントと Unicode マッピング、注釈とフォーム、自然言語、代替説明、MathML の語彙、そして人間によるアクセシビリティレビューです。このうちのいくつかは、機械的にはそもそも判定できません。alt テキストが実際にチャートを描写しているか、読み上げ順が聴いている人に意味が通るかを、パーサは教えてくれません
カバレッジ JSON は結果の一部として扱います。検証を実行するたびにアーカイブしておけば、監査人が特定の日付における「合格」の意味を問うたとき、記憶とバージョン番号から復元する代わりに、正確な範囲を手渡せます
veraPDF との相互検証
PDF/UA-2 コアは、veraPDF 1.30.3 のコマンドラインバリデータを ua2 プロファイルで使って相互検証され、カバレッジが重なる範囲では結果が一致しています。独立に生成した有効なコアサンプルは両ツールで合格し、pdfuaid:rev に 2023 を宣言するサンプルと、番号なし H ロールを使うサンプルは両ツールで不合格になりました
語る価値があるのは 4 番目のサンプルです。まったくタグ付けされていない描画コンテンツを含んでおり、veraPDF はこれを検出したのに PDFlibPas のコア検査は検出しませんでした。コンテンツのタグ付けが現行のカバレッジの外にあるからです。この隙間こそ、content-tagging-and-reading-order が unverified リストの先頭に置かれている理由です。2 つ目のバリデータを実行するのは安い保険であり、2 つのツールの食い違いは、それぞれがどこまで検査しているかの境界を見つける最速の方法になりがちです。同じ問題の作成側、つまり最初から合格するツリーを構築する話についてはタグ付き PDF 構造ツリーガイドを参照してください
FAQ
PDFlibPas はファイルを PDF/UA-2 準拠として認証するのか
いいえ。PDFlibPas は PDF/UA-2 に対する実装済みの機械検査を実行し、fullCertification が常に false のカバレッジレポートを返します。まだ他のツールや人間のレビューを必要とする領域がすべて列挙されます
CheckFilePDFUACompliance に渡す TargetPart の値はどれか
ファイルが PDF/UA-2 でなければならないと分かっているなら 2 を渡します。識別情報の欠落が報告されます。XMP の pdfuaid:part からパートを自動判定させたいなら 0 を、PDF/UA-1 のルールを強制したいなら 1 を渡します
PDF/UA-2 レポートにおける診断 10059 の意味は何か
診断 10059 は、バリデータが完了できなかったことを意味します。ファイルのパースやオープンができなかった、トラバーサル上限を超えた、循環が見つかった、例外が発生した、のいずれかです。常に失敗として数えられ、合格として数えられることはありません
PDF/UA-2 検証、共有バリデーションパイプライン、PDF 2.0 作成 API は、PDF/A プリフライトや構造化バリデーションレポートとともにlosLab PDF Library for Delphi に搭載されています