技術記事

HotPDF による Delphi での構造順 PDF テキスト抽出

どの幾何学的テキスト抽出器も推測しています。ページが描くグリフを読み、ベースラインと水平位置で並べ替え、視覚的な配置が人間の読む順序と一致することを願います。1 カラムのレポートではその推測は正しいです。2 カラムの学術論文、サイドバー付きのフォーム、セルがカラムごとに出力されたテーブルでは、気づきにくく、下流で発見するには高くつく形で誤ります。HotPDF はこれに ExtractLoadedPageStructureText で答えます。幾何を完全に無視します。ISO 32000-1 §14.8.4 で定義された著述順に文書構造ツリーを歩き、それからページのグリフをマークコンテンツ識別子で再構築します。タグ付き PDF にとってこれはヒューリスティックではなく、生成アプリケーションが宣言した順序です

ページに使える構造ツリーがないとき、この関数は False を返します。失敗ではなく幾何抽出器へフォールバックする合図です。その 2 経路の設計はアルゴリズム以上に重要です。実際の文書取り込みは、タグ付きの政府フォームとスキャナ出力を同じフォルダで見ます。片方しか扱えないパイプラインはパイプラインではありません

幾何学的抽出はなぜ読み順を誤るのか

PDF のコンテンツストリームは読み順をまったく運ばないからです。描画オペレータの列であり、生成側は自分のレイアウトエンジンに都合のよいどんな順序でも出力する自由があります。ワードプロセッサは通常フロー順に出力し、幾何ソートは問題なく見えます。レイアウトツール、フォームデザイナー、レポートジェネレータはしばしばそうではありません。ページフッターが本文より先に出力され、テーブルがカラム優先で埋められ、2 カラムページは両カラムの行を織り交ぜることがあります。組版器がそれらをまとめて解決したからです

2 カラム PDF ページの比較。ベースラインソートの幾何抽出がカラムを継ぎ接ぎにする様子を、HotPDF の構造順 MCID 抽出と対比して示す。
ベースラインでのグリフの並べ替えは 2 カラムを織り交ぜて無意味な文にします。一方、構造ツリーは生成側が宣言した順序を再生します

障害モードは静かです。幾何抽出器はエラーを報告せず、文が 2 カラムから継ぎ接ぎされた散文を返すだけです。そのテキストを消費するもの、検索インデックス、電子請求書のフィールドマッパー、言語モデルへ供給する検索パイプラインは、警告なしに損傷を引き継ぎます。HotPDF はロードした文書向けの幾何抽出器も同梱しており、タグなしファイルには引き続き正しい道具です。構造順経路の要点は、文書がすでに答えを運んでいるときに推測をやめることです

構造ツリーが実際に格納するもの

タグ付き PDF は、ページの 2 つ目の並行した記述を保持します。カタログは /StructTreeRoot を指し、その /K の子は構造要素のツリーを形成します。/Document/Sect/P/Table/TR/TD などです。そのツリーの葉はマークコンテンツ参照、ページコンテンツストリームの区間を名指す整数です。コンテンツ側では、それらの区間は /MCID を運ぶ BDC オペレータで開かれ、EMC で閉じられます。各構造要素はさらに、それが属するページを名指す /Pg エントリを運びます。構造ツリーが数百ページに及ぶ文書でページごとの走査を可能にするのがこれです

Sect、Table、TR、TD といった StructTreeRoot 要素を、HotPDF のページコンテンツストリーム内の BDC MCID 区間へ結び付ける PDF 構造ツリーの構造。
ツリーの葉はマークコンテンツ参照であり、各要素は走査を現在のページへフィルタできる Pg エントリを運びます

HotPDF は深さ上限 128 レベルでこのツリーを走査し、/Pg でフィルタするため、現在のページだけが寄与します。走査の出力はテキストではなく、MCID 値の順序付きリストです。このページのマークコンテンツ区間の著述順です。テキストの再構築は、その後、その順序でグリフを再生する問題になります

MCID はグリフ抽出中に記録される。後から照会されるのではない

この機能を安価にしている実装の細部です。HotPDF は抽出するすべてのグリフに、アクティブなマークコンテンツ識別子を THPDFGlyphRecordMCID フィールドへすでに記録しています。コンテンツストリームインタプリタは、各 TjTJ オペレータを処理する時点で、どの BDC スコープが開いているかを知っているからです。したがって構造順抽出に、コンテンツストリームの 2 回目のパスは不要です。構造ツリーから MCID 列を収集し、その後、すでに抽出済みのグリフを MCID ごとに仕分けし、その順序で出力します

var
  Pdf: THotPDF;
  PageCount, I, Untagged: Integer;
  PageText, AllText: UnicodeString;
  Report: TStrings;   // 呼び出し側所有の診断出力先
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('accessible-form.pdf');
    AllText := '';
    for I := 0 to PageCount - 1 do
    begin
      if Pdf.ExtractLoadedPageStructureText(I, PageText, Untagged) then
      begin
        // 構造ツリーから直接の著述順
        if Untagged > 0 then
          Report.Add(Format('page %d: %d glyphs outside the structure tree',
            [I, Untagged]));
      end
      else
        // このページに使える構造ツリーがない:幾何フォールバック
        Pdf.ExtractLoadedPageText(I, PageText);
      AllText := AllText + PageText + #13#10;
    end;
  finally
    Pdf.Free;
  end;
end;

タグなしグリフは数えられる。黙って捨てられることはない

ページは部分的にタグ付けされ得ます。生成側は装飾的な罫線、ページ番号、後段の透かしをどの BDC スコープの外へも追加し、それらのグリフはどの MCID にも属しません。捨てるのはきれいな実装であり、かつ誤った実装です。生成側が本文はタグ付けしたがテーブルを忘れたときにも同じ隙間が現れ、気づかずにテーブルを失うからです

HotPDF は主張のないグリフを、構造順テキストの後ろへ幾何の末尾として追記し、その数を UntaggedGlyphCount 出力パラメータ経由で報告します。この数は、対応できる品質シグナルです。2000 グリフのページでの一握りはページの飾りであり、無視できます。ページの 4 割が構造ツリーの外にあるなら、タグ付けは装飾であり、そのファイルにとっては幾何抽出器のほうが正直な答えです

ページに使える構造ツリーがないかタグ付けが装飾的なときに幾何へフォールバックする、HotPDF 構造テキスト抽出の判断フロー。
True はタグなし末尾を追記した構造順を意味し、False は失敗の代わりにページを幾何抽出器へ振り向けます
function ExtractPageBestEffort(Pdf: THotPDF; PageIndex: Integer;
  out AText: UnicodeString; out UsedStructure: Boolean): Boolean;
var
  Untagged, TotalGlyphs: Integer;
  Glyphs: THPDFGlyphArray;
begin
  UsedStructure := False;
  if Pdf.ExtractLoadedPageStructureText(PageIndex, AText, Untagged) then
  begin
    TotalGlyphs := 0;
    if Pdf.ExtractLoadedPageGlyphs(PageIndex, Glyphs) then
      TotalGlyphs := Length(Glyphs);
    // ページの大部分を主張するときだけ構造ツリーを信頼する
    if (TotalGlyphs = 0) or (Untagged * 4 <= TotalGlyphs) then
    begin
      UsedStructure := True;
      Result := True;
      Exit;
    end;
  end;
  Result := Pdf.ExtractLoadedPageText(PageIndex, AText);
end;

関数が False を返すのはどんなときか

3 つのケースがあり、区別する価値があります。そのうち 1 つだけが文書の欠陥だからです。1 つ目は普通のタグなし PDF です。/StructTreeRoot がなく、歩くものがなく、False は単に真実です。2 つ目は、タグ付けされたことのない OCR レイヤーからテキストが来るスキャンページです。3 つ目が興味深いケースです。/MCID 値を持つ BDC オペレータを運ぶコンテンツでありながら、ページに /StructParents エントリがなく、構造ツリーがそれらの識別子を決して参照しないものです。マークコンテンツは存在し、構造側は存在せず、回復すべき順序がありません。HotPDF は 1 つを捏造する代わりに False を報告します

この最後のケースは、手で編集されたファイルや、構造ツリーを構築せずにオプションコンテンツやアーティファクトの目的でマークコンテンツを出力するツールの出力に現れます。自分でタグ付き PDF を生成しているなら、同じ非対称性は PDF/UA 検証が確認するものであり、ライター側の対応物はタグ付きページ分割出力を生むレイアウト DOM で扱っています

構造順が見合う場所

アクセシビリティ監査は自明な例です。PDF/UA に対して文書を認証するなら、スクリーンリーダーが読み上げる読み順は正確に構造順であり、それを抽出することが、スクリーンリーダーなしでの確認方法です。データキャプチャはより大きな商業的ケースです。タグ付き政府フォーム、規制された開示文書、電子請求書の添付は、宣言された順序でフィールドラベルと値を運びます。その順序で読むことで、複数カラムのレイアウトで幾何抽出が生み出す一連のマッピングバグのクラス全体が取り除かれます

最新の消費者は、言語モデル向けの検索です。埋め込みのために文書をチャンク分割する良し悪しはテキスト順に依存し、2 カラムを継ぎ接ぎするチャンクは実在しない文を生みます。構造順抽出はこれに対する最も安価な利用可能な修正です。タグ付き文書にとって正しい順序はすでにファイルの中にあり、読むだけでよいからです

HotPDF は Delphi と C++Builder 向けのネイティブ VCL コンポーネントであるため、構造ツリーの走査もグリフの再生も、外部レンダラーを介さず、ロードした文書に対してプロセス内で動きます。ロード文書抽出ファミリーの完全な API 詳細は、HotPDF Delphi PDF component の製品ページにあります