PDFium Componentは、BuildReflowDocumentを使って、固定レイアウトのPDFをリフロー可能な意味モデルへと変換し、ToHtmlを通じてそのモデルを自己完結型のHTMLとしてエクスポートします。見出しは見出しのまま、リスト項目はリスト項目のままであり、ページ上で検出されたテーブルは、ヘッダーセルとスパンを保持した本物のテーブルマークアップとして出力されます。出力には、外部のスクリプトやスタイルシートへの参照は一切含まれません
これが求められる理由は、PDFのページが位置づけられたグリフの集まりであり、それはスマートフォンの画面、スクリーンリーダー、検索インデックスにとってはまさに不都合な形式だからです。プレーンテキストを抽出することでこれを解決しようとする試みは、ドキュメントを読みやすくしていた構造を失いますし、ページを画像に変換することで解決しようとする試みは、テキストそのものを完全に失います。リフローモデルは両方を保持します——単語と、それらの間の関係の両方です
意味情報はどこから得られるのか?
すべては、このコンポーネントにおけるテキストと意味論の唯一の情報源であるGetStructuredTextから始まります。PDFがISO 32000-1第14.7項で定義されるタグ付きPDFのような構造ツリーを持っている場合、モデルは作成者が記録した論理階層に従います。そうでない場合——実際に流通しているPDFの大半はそうなのですが——モデルは、読み取り順序のためにすでに計算済みの物理的なレイアウト順序にフォールバックします
この選択によって、はっきりとした境界が保たれます。既存のものが答えられる問いに答えるために、2つ目のPDFパーサーや2つ目のレンダリングエンジンを導入することはありません。その基盤となる読み取り順序の仕組みについては、構造化テキストブロックと読み取り順序で説明しています。リフローモデルは、それを置き換えるものではなく、その上に重ねられた意味論的なレイヤーです
各ノードは、その情報がどこから得られたかを記録しているため、利用側は、ドキュメントが宣言した見出しと、レイアウトのヒューリスティックが推測した見出しを区別できます。信頼度に敏感なパイプラインは、すべてのノードを等しく権威あるものとして扱うのではなく、このフィールドを参照すべきです
フラットなツリー、そしてなぜオブジェクトのツリーではないのか
このモデルは、行きがけ順で平坦化されたツリーです。各ノードがParentIndexとDepthを持つノードの配列であり、再帰的なレコードや所有権を持つオブジェクトグラフではありません。ページ、見出し、段落、リスト、リスト項目、図、キャプション、テーブル、行、セルは、すべてこの1つの線形配列の中に存在します
ここから2つの利点が生まれます。利用側は再帰なしで配列を順番にストリーム処理でき、HTML、Markdown、ツリービューの出力が単純なループで済みます。また、このレイアウトは、ABI境界をまたぐ再帰的なマネージド型の扱いが異なるDelphi、C++Builder、Free Pascalの間で移植性を保ちます。動的配列の再帰的なレコードは、まさに、どこでもコンパイルは通るのにそれぞれで微妙に挙動が異なるという類いの構造です
uses
PDFium;
var
Pdf: TPdf;
Options: TPdfReflowOptions;
Doc: TPdfReflowDocument;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.LoadDocument;
Options := TPdfReflowOptions.Default;
Options.FullDocument := True;
Options.DetectTables := True;
Options.IncludeCss := True; // インラインのstyleブロック、外部ファイルはなし
Options.MaxNodes := 200000; // フェイルクローズの予算上限
Options.MaxCharacters := 4000000;
Doc := Pdf.BuildReflowDocument(Options);
for I := 0 to High(Doc.Nodes) do
case Doc.Nodes[I].Kind of
prnkHeading:
Writeln(Format('%sH%d: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
Doc.Nodes[I].HeadingLevel, Doc.Nodes[I].Text]));
prnkParagraph:
Writeln(Format('%sp: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
Copy(Doc.Nodes[I].Text, 1, 60)]));
prnkTable:
Writeln(Format('table on page %d', [Doc.Nodes[I].PageNumber]));
end;
Writeln(Format('%d node(s), %d table(s), %d character(s)',
[Length(Doc.Nodes), Doc.TableCount, Doc.CharacterCount]));
finally
Pdf.Free;
end;
end;
テーブルが2回現れないようにするにはどうしているのか?
テーブル検出は、ページの構造化テキストが収集された後に実行されるため、明白な危険が生じます。同じセルの内容が、テキストブロックと検出済みテーブルの両方に存在してしまうのです。両方をそのまま出力すると、各テーブルの後にその内容がもう一度、寄せ集めの段落として続くHTMLができてしまいます
これを解決する規則は幾何学的なものです。検出されたテーブルがテキストブロックの面積の半分を超えて覆っている場合、テーブルノードはそのブロックに合流するのではなく、それを置き換えます。行内のセルのインデックス付けは、バケットに数え入れる方式で構築されるため、行ごとにすべてのセルを再スキャンするのではなく、モデルの構築はセル数と行数に対して線形のままです。これは、1ページに数百のセルを含み得る財務ドキュメントで重要になります
検出された構造は、それが検出結果であることについて正直です。罫線のあるテーブルは、空白だけで揃えられたテーブルよりも確実に認識され、ノードの信頼度にはそれが反映されます。テーブルなしよりも間違ったテーブルの方がまだましなコンテンツでは、検出をオンのままにしておいてください。間違ったテーブルの方が悪いアーカイブ用の変換では、信頼度でゲートしてください
自己完結型のHTMLをエクスポートする
ToHtmlは、すでに構築済みのモデルをたどるだけでPDFiumを再訪することは一切ないため、2回エクスポートしても余計なコストはかからず、同じモデルから異なる結果が生まれることもありません。テキストと属性値は一様にエスケープされ、見出しレベルはHTMLが実際に定義しているh1からh6までの範囲にクランプされ、ヘッダーセル、RowSpan、ColumnSpanはそのまま渡されます
オプションのCSSは、単純なインラインのstyleブロックです。スクリプトも、Webフォントも、いかなる種類の外部リソースも一切ありません。だからこそ、この出力はメール、ヘルプビューア、サンドボックス化されたブラウザコントロールに埋め込んでも安全なのです:
var
Html: WideString;
Stream: TFileStream;
Bytes: TBytes;
begin
Options := TPdfReflowOptions.Default;
Options.FullDocument := True;
Options.IncludeCss := True;
Options.IncludePageSections := True; // ページの境界を見えるようにする
Options.PreserveLineBreaks := False; // 段落の折り返しはブラウザに任せる
Html := Pdf.BuildReflowDocument(Options).ToHtml;
Bytes := TEncoding.UTF8.GetBytes(string(Html));
Stream := TFileStream.Create('report.html', fmCreate);
try
if Length(Bytes) > 0 then
Stream.WriteBuffer(Bytes[0], Length(Bytes));
finally
Stream.Free;
end;
end;
PreserveLineBreaksは、最もよく考える価値のあるオプションです。PDFの改行は、固定のページ幅を前提に下された組版上の決定であるため、それを狭い画面でそのまま保持すると、まさにリフローが解決しようとしている問題を再現してしまいます。詩、コードリスト、住所には改行を保持し、地の文では取り除いてください
予算、キャンセル、ページの状態
文字数、ノード数、テーブル数、セル数にはそれぞれ上限があり、いずれも確保の後ではなく前にチェックされます。そのため、不正な形式のドキュメントや悪意のあるドキュメントは、何か他のものが力尽きるまでメモリを消費し続けるのではなく、きれいに失敗します。キャンセルトークンは、ページ、ブロック、テーブル、行、セルの境界でチェックされるため、1000ページのドキュメントをキャンセルした場合でも、応答性が保たれます
GUIアプリケーションに特に関わる挙動が1つあります。ドキュメント全体のスキャンは、アクティブなページを復元するスコープの内部で実行されるため、成功・予算超過による失敗・キャンセルのいずれの場合でも、呼び出し側の現在のページは変更されないまま残ります。ユーザーが340ページ目を見ながらエクスポートできるビューアは、処理の後も340ページ目を表示したままになります
リフローが向いているもの、向いていないもの
リフロー出力は、検索インデックス作成、アクセシブルな読み取りビュー、モバイル表示、コンテンツ移行にとって優れた入力です。これは忠実度を保つコンバーターではありません。絶対位置、正確なフォント、ベクターアートワーク、正確なページジオメトリは、設計上その目的の範囲外です。ページの見た目をそのまま保つ必要があるならレンダリングし、ページを別の場所で読めるようにする必要があるならリフローしてください
支援技術に特化した用途では、リフローモデルはアクセシブルなリーダーの構築で説明した読み取り機能と組み合わさります。本物の構造ツリーを持つドキュメントは、目に見えて優れたモデルを生み出します。これは、PDF/UA構造ツリーの検証で説明したように、上流でタグ付けを検証しておくべき良い理由になります
リフロー、構造化テキスト、タグ付けの検証、レンダリングは、Delphi、C++Builder、Lazarusにおいて1つのドキュメントオブジェクトを共有します。完全なAPIについてはPDFium Component for Delphiページで説明しています