技術記事

Delphi の PDFium で行う PDF/UA 構造ツリー検証

プリフライトではそのファイルは PDF/UA 適合と出ます。一方で veraPDF は同じファイルを開き、7.3 条に基づいて代替テキストのない Figure を指摘します。両方とも正しく、その差こそがバイト列だけでアクセシビリティを確認する問題の本質です。バイトレベルの検査で確認できるのは、そのファイルが 自称している タグ付きであるという事実だけです。/StructTreeRoot/StructTreeRoot/MarkInfo /Marked true/MarkInfopdfuaid:partpdfuaid:part

PDFium Component は Delphi と C++Builder 向けのネイティブ VCL PDF ライブラリであり、その ValidatePdfUaValidatePdfUA

なぜバイトスキャンでは Alt の欠落を見抜けないのか

ISO 14289-1(PDF/UA-1)は ISO 32000 の上に積み上がる要求事項の層です。その一部は構造的で、生のファイルから見えます。たとえば、カタログは構造ツリーを宣言していなければならず、ビューア設定では DisplayDocTitleDisplayDocTitleValidatePdfUaComplianceValidatePdfUABytes

しかし「すべての Figure に代替テキストがある」という条件は、ファイル構文の性質ではありません。それは 論理構造 の性質です。つまり、コンテンツを意味へ対応付けるタグ付き要素のツリーです。Figure の Alt エントリは構造要素辞書に置かれていることもあれば、/ActualTextActualText/Alt/Altどの 構造要素に属するのかも分かりません。この問いに正直に答える方法は、文書自身の構造ツリーへ要素ごとに問い合わせることです。veraPDF や PAC も同じ面を評価しています。ここが PDFium の Tier-1 検査の基準線です。形式はバイトスキャンで、内容はツリー走査で確認します

実際のタグツリーを読む

元になるのは TPdf.GetStructureElementsGetStructureElementsStructureElementsStructureElementsTPdfStructureElementsTPdfStructureElementArrayTPdfStructureElementTPdfStructureElement

type
  TPdfStructureElement = record
    Level: Integer;            // depth in the tag tree
    ParentIndex: Integer;      // index of parent element, or -1
    TypeName: WString;         // standard /S name: Figure, Formula, Note...
    Title: WString;            // /T
    AlternateText: WString;    // /Alt   (FPDF_StructElement_GetAltText)
    ActualText: WString;       // /ActualText
    Expansion: WString;        // /E
    ID: WString;               // /ID    (FPDF_StructElement_GetID)
    Language: WString;         // /Lang
    MarkedContentIDs: TPdfIntegerArray;
    // ... child bookkeeping fields
  end;

レコードです。これは PDFium のアクセサ関数を通して 1 つの構造要素を投影したもので、アクセシビリティ規則が実際に必要とするフィールドだけを持ちます。TypeNameStructureTypeFPDF_StructElement_GetTypeFPDF_StructElement_GetType/S/SAlternateTextAltTextFPDF_StructElement_GetAltTextFPDF_StructElement_GetAltTextActualTextActualTextFPDF_StructElement_GetActualTextFPDF_StructElement_GetActualTextIDIDFPDF_StructElement_GetIDFPDF_StructElement_GetID

チェッカーは純粋関数であり、それは意図的です

規則ロジックは DLL と会話するメソッドの中にはありません。独立した公開の純粋関数です

function ValidatePdfUaStructureElements(
  const Elements: TPdfStructureElements): TPdfUaValidationIssues;

TPdfStructureElementArrayTPdfStructureElementsfpdfium.dllpdfium.dll がなくても結果セットをアサートできます。規則ロジックはオフラインで検証され、DLL 走査はライブラリ不在時にスキップされる実文書のスモークテストで別に検証されます

次に責務の明確さです。TPdf.ValidatePdfUaValidatePdfUA

3 つの規則が実際に何を確認しているのか

構造ツリー走査が追加で報告するのは、TPdfUaValidationIssuesTPdfUAValidationIssuepvuaiFigureMissingAltpuviFigureMissingAltpvuaiFormulaMissingAltpuviFormulaMissingAltpvuaiNoteMissingIdpuviNoteIdDuplicate

for I := 0 to High(Elements) do
begin
  T := string(Elements[I].TypeName);
  if T = 'Figure' then
  begin
    // §7.3 — a Figure needs an alternate representation:
    // an Alt entry OR ActualText. Flag only when BOTH are empty.
    if (Elements[I].AlternateText = '') and (Elements[I].ActualText = '') then
      Include(Result, pvuaiFigureMissingAlt);
  end
  else if T = 'Formula' then
  begin
    // §7.7 — same rule as Figure: Alt OR ActualText.
    if (Elements[I].AlternateText = '') and (Elements[I].ActualText = '') then
      Include(Result, pvuaiFormulaMissingAlt);
  end
  else if T = 'Note' then
  begin
    // §7.9 — every Note must have a unique ID.
    NoteId := string(Elements[I].ID);
    if NoteId = '' then
      Include(Result, pvuaiNoteMissingId)
    else
      for J := 0 to I - 1 do
        if (string(Elements[J].TypeName) = 'Note') and
           (string(Elements[J].ID) = NoteId) then
        begin
          Include(Result, pvuaiNoteMissingId);
          Break;
        end;
  end;
end;

7.3 条は図版を扱います。FigureFigureActualTextActualText両方とも 空の場合にだけ Figure を失敗とします。7.7 条は数式を扱い、同じ修正のあと、まったく同じ Alt または ActualText の判定を使います。アクセス可能テキストを ActualText のみで与えていた適合コーパスのサンプルが、Formula 分岐を Figure 分岐に合わせるまでは誤って拒否されていました

7.9 条は性質が異なります。NoteNote/IDID重複した ID は 2 つの要素間の関係です。そのためフラット配列が重要になります。各 Note について、チェッカーはすでに見た要素を後ろ向きに走査し、同じ ID を持つ先行 Note があれば衝突として報告します。計算量は Note 数に対する明白な O(n²) ですが、実際の文書では問題にならず、補助インデックスを同期させる必要のない読みやすい 1 本のループに保てます

ページをまたいで集約し、一意性を文書全体で判定する

PDFium は構造要素を文書単位ではなくページ単位で公開するため、ValidatePdfUaValidatePdfUAFPDF_LoadPageLoadPageGetStructureElementsForPageGetPageStructureElementsFPDF_ClosePageUnloadPage

// inside TPdf.ValidatePdfUa, after the byte-level pass
if (FDocument <> nil) and
   (not (pvuaiMissingStructTreeRoot in Result.Issues)) then
begin
  AllElems := nil;
  PageTotal := FPDF_GetPageCount(FDocument);
  for I := 0 to PageTotal - 1 do
  begin
    Page := FPDF_LoadPage(FDocument, I);
    if Page = nil then Continue;
    try
      PageElems := GetStructureElementsForPage(Page);
    finally
      FPDF_ClosePage(Page);
    end;
    // append PageElems into AllElems ...
  end;
  Result.Issues := Result.Issues + ValidatePdfUaStructureElements(AllElems);
end;

重複のしないpuviMissingStructTreeRootpvuaiMissingStructTreeRoot を報告している場合だけです。タグなし文書には歩くべきツリーがなく、構造ルート欠落ですでにフラグが立っているため、ページ単位の読み込みは丸ごと省略されます。恩恵のない文書では深い検査は一切コストを払いません

設計上あえて保守的: 静かに見逃しても、誤検出はしない

このバリデータで最も重要なのは、何をしないかです。標準の /S/SFPDF_StructElement_GetTypeFPDF_StructElement_GetTypeFigureFigureFormulaFormulaNoteNote過少報告を選び、決して誤検出を出さない ことです。適合したファイルに向かって狼少年をするプリフライトツールは、利用者に無視されるようになります。無視されるバリデータは、存在しないのより悪いからです。装飾画像は構造ツリーではなく artifact ストリームに存在するため、そもそも Figure として現れません。正しく artifact としてマークされた背景罫線に対して「Alt がない」とは報告されません

このため対象範囲も 3 つの規則に限定されています。見出しレベルの入れ子(7.4 条)、表ヘッダーのスコープ(7.5 条)、role map の循環検出(7.1 条)はいずれも正当な PDF/UA 要件ですが、適切に検査するには本格的なグラフ解析や属性解析が必要です。安易に検査すると、設計が禁止している誤検出をまさに生みます。PDF/UA では H1、H2、H3、H3 のような見出しパターンが許されますが、単純な「必ず厳密に増加すること」規則なら誤って拒否してしまいます。そうした検査は専用の適合ツールへ委ねています。Tier-1 は、属性の欠落が曖昧さなく不具合を意味する部分集合です

境界を率直に述べると

これをリリースゲートに組み込む前に知っておくべき制限は 2 つあります。1 つ目は、チェッカーの精度は PDFium が構造要素から読み出せる情報に依存するということです。参照バリデータが通す適合コーパスの一部には、PDFium が露出していない代替テキスト機構を使うものがあり、その場合 FPDF_StructElement_GetAltTextFPDF_StructElement_GetAltText

ValidatePdfUaValidatePdfUADelphi でアクセシブルな PDF リーダーを構築する の基盤になります。タグツリーが読み順と読み上げテキストを駆動するからです。また、Delphi から PDF 注釈をレビューする のようなメタデータ層の作業も補完します

構造ツリー API と、ここで示した ValidatePdfUaValidatePdfUAPDFium Component は、Delphi と C++Builder(VCL)および Lazarus/FPC(LCL)向けの製品に含まれています。製品ページには、完全な TPdfStructureElementTPdfStructureElement