PDFium Component version 3.117.0は、ページ境界をまたいで分断された表を、両方の断片がページ端に接しているか、1つ目の断片の下と2つ目の断片の上に本文テキストが存在しないかのいずれかで連結します。このとき、繰り返しヘッダーとフッターは無視します。ExtractDocumentTablesは、この内容ベースの判定を古いページ余白の判定の代替として適用し、次のページの断片の先頭行が全幅のキャプションセル1つである場合を拒否し、次のページへあふれた1行をその継続チェーンの一部として保ちます
表の検出と抽出の記事では、継続を4つの厳格なゲートとして提示し、「ページ端に接している」をその1つとして扱っていました。その説明はそれが扱ったリリースに対しては正確で、同時に、実際にコンポーネントへ流し込まれるほとんどの表に対しては間違っていました。この記事はその訂正です。余白の判定が扱えない文書はどれか、何がそれを置き換えたか、そして修正が一緒に持ち込んだ2つの周辺ケースです
ページ余白の判定がWord出力で失敗する理由
ページ余白の判定が失敗するのは、ワープロが行のレイアウトを紙の端ではなく下余白で止めるからです。既定のContinuationMarginは36ポイントで、元のルールは、前の断片の下端がページ下端から36ポイント以内にあり、後の断片の上端がページ上端から36ポイント以内にあることを要求していました。既定の1インチ余白でWordからエクスポートした文書では、最後の行はページ下端から少なくとも72ポイント上にあり、フッターがあればさらに上になります。そのためこの条件は決して成立しませんでした。そうした文書の長い表はすべて、ContinuationGroupがゼロの独立した断片として返り、呼び出し側は手作業での接続に逆戻りしていました。この判定は、設計対象に対しては今も意味があります。固定の内容ボックスいっぱいにページを埋め、次のページを上端ぴったりから始めるレイアウトエンジンが生成するレポートです。悪いルールではなく、不完全なルールなのです。だからこそversion 3.117.0はそれを残し、置き換えるのではなく2つ目の経路を追加しました
内容ベースの判定は代わりに何を調べるのか
内容ベースの判定は、2つの断片の間の空間を表以外の何かが占めているかどうかを、ページの幾何ではなく各ページの単語ボックスを使って調べます。ExtractDocumentTablesは文書をたどりながら、ページごとに、上端がフッターバンドより上にある単語のうち最も低い下端と、下端がヘッダーバンドより下にある単語のうち最も高い上端を記録します。どちらのバンドもContinuationMarginポイントの深さなので、同じオプションがページ端の余裕と、繰り返しヘッダーとフッターの領域の高さという二役をこなすようになりました。断片の組が通るのは、前の断片の下端がそのページの最も低い本文テキスト以下にあり、後の断片の上端が次ページの最も高い本文テキスト以上にあり、それぞれがAlignmentTolerance以内にある場合です。平たく言えば、その表がページNで最後の内容であり、ページN+1で最初の内容であり、余白バンドにあるページ番号や文書タイトルは数えないということです。この除外は恣意的ではありません。ISO 32000-1 §14.8.2.2は繰り返しヘッダーとフッターをページ割り付けのアーティファクト、つまり改ページのせいで存在する内容として分類しています。タグ付きリーダーがそれらを読み飛ばせるのと同じ考え方が、表がそれらを越えて継続できるようにするものです。マーク付きコンテンツの記事では、タグ付きファイルがそうしたアーティファクトを明示的に宣言する方法を扱っています。ここでは、エクスポートされた表のほとんどがタグを一切持たないため、分類は位置から推測されます
2つの判定はORで結合されます。表が紙の端まで届くレイアウトエンジンのレポートは1つ目を通り、表が余白で止まるWordのエクスポートは2つ目を通り、両方を満たす文書は2回通ります。どちらかが成功して初めて残りのゲートが走り、固定の順序で進みます。ページ番号が隣接していること、後の断片がキャプション行で始まらないこと、列境界がAlignmentToleranceの2倍以内——既定値では6ポイント——で一致することです。列挙はTPdfTableContinuationで、値はptcNone、ptcStart、ptcMiddle、ptcEndです。ptcEndと印を付けられた断片がさらに別のページへ連結するとptcMiddleへ昇格するため、3ページの表はページ順にstart、middle、endとして読めます。グループ番号は1から始まり、0は未連結を意味し、ToJsonは同じ情報をcontinuationとcontinuationGroupメンバーとして出力します。接続を下流のサービスが行うなら、こちらを好むべきです
uses
PDFium;
var
Pdf: TPdf;
Options: TPdfTableExtractionOptions;
Tables: TPdfTables;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'itinerary-from-word.pdf';
Pdf.LoadDocument;
Options := TPdfTableExtractionOptions.Default;
Options.DetectContinuations := True; // 既定値。明示のために記載
Options.ContinuationMargin := 54; // 2行のフッター、深さ約50 pt
Tables := Pdf.ExtractDocumentTables(Options);
for I := 0 to High(Tables) do
case Tables[I].Continuation of
ptcStart:
Writeln(Format('group %d starts on page %d (%d rows)',
[Tables[I].ContinuationGroup, Tables[I].PageNumber,
Tables[I].RowCount]));
ptcMiddle, ptcEnd:
Writeln(Format('group %d continues on page %d (%d rows)',
[Tables[I].ContinuationGroup, Tables[I].PageNumber,
Tables[I].RowCount]));
else
Writeln(Format('standalone table on page %d (%d rows)',
[Tables[I].PageNumber, Tables[I].RowCount]));
end;
finally
Pdf.Free;
end;
end;
キャプション行が2つの表の融合をどう止めるのか
次のページの断片の先頭行が全列にまたがる1つのセルである場合、それは新しい表として扱われ、前の表の続きとは決して見なされません。このルールがあるのは、内容ベースの判定が単独では連結しすぎるからです。それを露呈したのは、調書風のフォームでした。表がページ1の下端付近で終わり、同じ列幅の2つ目の表がページ2の上端付近で始まり、その間にはフッターしかなく、列は完全に一致しています。余白の判定では、どちらも端に接していないため両者は出会いませんでしたが、内容の判定では即座に連結し、セクションのあるフォームが1つの支離滅裂なグリッドになりました。両者を分けるものはセル構造に見えます。2つ目の表は「RECIPIENT INFORMATION」のようなセクションキャプションを、全幅にまたがる単一の結合セルとして先頭に置いて始まります。本物の継続はそうしません。キャプションはすでに前のページで始まっている表に属するものだからです。TableStartsWithCaptionRowはまさにそれを符号化します。断片が少なくとも2列を持ち、RowIndex = 0、ColumnIndex = 0、ColumnSpan = ColumnCountのセルを含む場合です。このチェックは後の断片にだけ走るため、自分のキャプション行が最初のページにある表は影響を受けません。キャプションはページNにあり、検査されるのはページN+1の断片だけです
続く列の比較TablesHaveMatchingColumnsは、「列数が同じ」より厳格です。セル矩形から各断片の境界位置を再構築し、結合セルが隠している境界を補間し、いずれかの境界が許容値を超えてずれていればその組を拒否します。したがって、比率の異なる2つの4列表は、他のすべてが揃っていても別々のままです
次のページへあふれた1行はどうなるのか
罫線グリッドが1行を次のページへ持ち越す場合、それが継続チェーンに入ることを条件に、検出され連結されるようになりました。単独であれば破棄されます。既定のMinRowsが2なのは、はぐれた2本の線が表として報告されるのを防ぐためですが、改ページで押し出された最終行は本物の行であり、2という下限がそれを黙って落としていました。しかも表の残りは完全に見えていたのです。文書レベルの走査はこれを3段階で処理します。DetectContinuationsとDetectRuledTablesが両方設定されているとき、ページ単位のパスは罫線検出器を行の下限を一時的に1へ下げて走らせます。だからこそExtractTablesは罫線グリッドに対してMinRows1を受け付けるようになり、空白検出は内部の下限2を保ちます。継続は結果全体に対して印を付けられます。次に、呼び出し側のMinRowsより短く、どのチェーンにも属さない表がすべて削除されます。1行の断片が生き残るのは連結されたからだけで、それ以外は普通のページの途中にある1行のグリッドは従来どおり除去されます
// 各チェーンを1つのCSVとして再構築し、継続断片で繰り返される
// ヘッダー行を落とす
procedure ExportChains(const Tables: TPdfTables; const Folder: string);
var
I, R: Integer;
Lines: TStringList;
Csv: TStringList;
begin
Csv := TStringList.Create;
Lines := TStringList.Create;
try
for I := 0 to High(Tables) do
begin
if Tables[I].Continuation in [ptcNone, ptcStart] then
Csv.Clear;
Lines.Text := string(Tables[I].ToCsv);
if (Tables[I].Continuation in [ptcMiddle, ptcEnd]) and
(Lines.Count > 1) and (Tables[I].RowCount > 1) then
Lines.Delete(0); // ワープロが繰り返したヘッダー
for R := 0 to Lines.Count - 1 do
Csv.Add(Lines[R]);
if Tables[I].Continuation in [ptcNone, ptcEnd] then
Csv.SaveToFile(Format('%s\page%d-group%d.csv',
[Folder, Tables[I].PageNumber, Tables[I].ContinuationGroup]));
end;
finally
Lines.Free;
Csv.Free;
end;
end;
このルーチンの2つの詳細は意図的です。1行のあふれは決して削られません。RowCountのガードがそれを保つからです。また、各ページでヘッダー行を繰り返すワープロは、先頭行が再びヘッダーである断片を生成するため、middleとendの断片で0行目を落とすのはそのケースには正しく、ヘッダーを繰り返さないジェネレーターには誤りです。フォルダに対してルーチンを走らせる前に、必ず1つの文書で確認してください
ルールがまだ止まる場所
内容ベースの判定は、読み取るテキストレイヤー以上の精度を持ちません。テキストがまったくないスキャンページでは、記録される本文テキストの極値がページ境界にフォールバックし、「間に何もない」という条件が空虚に成立し、残るのはキャプション行と列のゲートだけです。そうしたページの罫線グリッドは依然として空の骨組みとして見つかるため、チェーンは正しく連結されるかもしれませんが、周囲のテキストについては何も実際には検証されていません。それが問題になるなら、まずテキストレイヤーを追加してください。テキストではなく画像として描かれたフッターはバンドのロジックからは見えず、同じ理由で無害です
バンドは1つの数値です。ContinuationMarginより深いフッターは、その下の行を本文ゾーン内に残し、前の断片がテキストに続いているように見せて連結を妨げます。1つ目の例のように、オプションを実際のバンドの深さまで上げてください。上げすぎると、ページ下端付近の短い結びの段落がバンドに入り込んで無視され、表がその後に続くものと連結されます。キャプションのルールには裏返しの失敗もあります。すべての継続断片の先頭行に結合された「continued」バナーを書くジェネレーターでは、それらの断片が新しい表として拒否されます。今日のところ唯一の対処は、ルールにスイッチがないため、何も緩めずに自分でContinuationGroupで接続することです
空白検出の表には、1行の救済が一切ありません。空白戦略は表を見るのに整列した2行を必要とするため、罫線のない表が1行だけあふれると、その1行分だけ短く報告されます。それに遭遇したときは、構造化テキストブロックと読み順の背後にある単語ボックスが、それを復元するための生の位置を与えてくれます。この作業を駆動したサンプル集——ワープロとブラウザのエクスポート13件——では、本物の複数ページ表を持つ5つの文書がすべて単一のチェーンに連結し、以前融合していた調書フォームは別々のままでした。これがこのリリースを測った基準であり、あらゆるレイアウトについての約束ではありません
継続の印付け、キャプションのルール、1行のパスはすべて、Delphi、C++Builder、Lazarusのビルドで共有される文書レベルの経路にあります。表抽出APIの全体はPDFium Component for Delphiのページで説明しています