HotPDF Delphi Component は v2.768.15 から、擬似ボールドと上塗りロゴが抽出テキストに残す重複文字を除去します。プロデューサが同じグリフを数分の 1 ポイントずらして 2 回描いている場合、ExtractLoadedPageText、ExtractLoadedPageTextLayout、ExtractLoadedPageTextColumns はその文字を 1 回だけ読むようになりました。「Bold words」が「Bold wordsBold words」になることはもうありません
これは、デコーダの失敗に見えてそうでない種類のバグです。どのグリフも正しくデコードされます。テキストは本当にページに 2 回あるのです。修正が決めねばならないのは、2 つの完全に正当なグリフのどちらがコピーか、そして「fill」の l の重なりや表の同一セルのような、正当な繰り返し文字を食わずにそれをやることです。以下では、HotPDF が使うルール、その背後の閾値、他の抽出ツールと意図的に異なる点、そして同じリリース系統から出た、行末のテキストを欠落させていたもう 1 つの無関係な抽出バグを説明します
擬似ボールドが PDF 抽出で重複テキストを生む理由
擬似ボールドが抽出テキストを倍化させるのは、プロデューサが文字列を 2 回描いており、どちらのコピーも本物のテキスト表示オペレーションだからです。フォントファミリーにボールドフェイスがないとき、あるいはオーサリングツールが単に手間を省くとき、多くのジェネレータは Tj か TJ で同じ文字列を表示し、テキスト位置を数十分の 1 ポイント右へずらし、もう 1 回表示することでボールドをエミュレートします。画面上では 2 つのコピーが合成されて太いストロークになります。コンテンツストリームには文字コードの完全な 2 ランがあり、ISO 32000-1 §9.4 は抽出器に「こちらは飾りです」と告げるフラグを何も与えません
上塗りロゴは別の角度から同じ問題に当たります。ロゴタイプは、同じフォントで同じ文字を使い、1 回は塗りとして、もう 1 回はストロークやわずかにずらした影として描かれることがあります。描画順にコンテンツストリームをなぞる素朴な抽出器は、最初のラン全体を出力し、次に 2 つ目のラン全体を出力します。だから結果は「BBoolldd」ではなく「Bold wordsBold words」になるのです。v2.768.15 より前の HotPDF はテキスト組み立てで両方のコピーを保持していました。重複は、ほとんどの呼び出し側が本当に気にしているもの——ページテキスト全体を通しての「Bold words」検索——を壊します。2 つのランの間の単語境界が消えるためです
本物のテキストレンダリングモード 2(塗ってから線、ISO 32000-1 §9.3.6)にはこの問題がありません。1 つのテキスト表示オペレーションが 1 組のグリフを生むからです。重複が出るのは、プロデューサがテキストを 2 回発行するときだけです
HotPDF はどのグリフが重複かをどう判定するのか
HotPDF は、2 つのグリフが同じ Unicode 値を持ち、書字方向が同じで、サイズが互いに 5% 以内で、ほぼ真上に重なっている——ベースライン方向にグリフサイズの 0.1 以内、ベースラインを横切る方向に 0.2 以内——とき、それらを 1 つの描画文字として扱います。そのようなペアごとに、先に描かれたグリフを保持し、後のものを落とします。これらは v2.768.15 実装の値で、それぞれに理由があります
- 同じ Unicode、同じ方向。重複になり得るのは同一文字だけで、水平グリフが縦書き(WMode 1)グリフと比較されることはありません
- サイズは 5% 以内。たまたま重なった大きな見出し文字と小さなキャプション文字は同じテキストではないため、サイズの不一致はペアを排除します
- ベースライン方向にサイズの 0.1。擬似ボールドのオフセットは数十分の 1 ポイント、通常のテキストサイズの数パーセントです。隣接する本物の 2 文字は、密にカーニングされた「ll」でさえ、ほぼアドバンス幅 1 つぶん離れており、この許容差の 1 桁上です
- ベースラインを横切る方向にサイズの 0.2。影やアウトラインパスは右だけでなく下にもずれることが多いため、縦方向の許容差は緩めですが、それでも次の行までの距離よりはるかに下です
- 先に描かれたほうが勝つ。早い方のグリフを保持することで最初のランの読み順が保たれ、生き残ったテキストはプロデューサが書いた通りに流れます
距離は生のページ X と Y ではなく、グリフ自身のベースライン座標系で測ります。回転したテキストや斜めベースライン上のテキストも、普通の水平行と同じ許容差を使えるからです。使うサイズは、Tf に与えられたサイズオペランドではなく、quad から導かれる、ページ上に描かれた通りのグリフのサイズです。Tf を 1 にしてすべてをテキストマトリクスでスケールする文書では、生のフォントサイズではすべての許容差が無意味になるため、これが効いてきます
空白は決して候補になりません。U+0020 までのコードポイント、ノーブレークスペースの U+00A0、全角スペースの U+3000 はスキップされます。空白グリフには複製すべき目に見えるインキがなく、1 つ落とせば単語が融合するからです
密なページでも検査を速く保つ
HotPDF の重複検査は n の 2 乗ではなく、おおよそ n log n の時間で動きます。まずソートし、小さなウィンドウ内のグリフ同士しか比較しないからです。全グリフ同士を比較するのは正しいけれど使い物になりません。小さい活字の密なページは簡単に数万のグリフを運び、この検査はテキスト抽出の呼び出しのたびに走るからです
内部ルーチンは、空白でないグリフを、Unicode 値、次に原点 X、次に描画順で、安定なマージソートで並べます。ソート後、グリフの潜在的重複はすべて順序内ですぐ後ろに座るため、走査は Unicode 値が一致し X 差がグリフサイズの 0.25 以内である限り前へ進むだけです。この 0.25 のウィンドウは、ベースライン方向の許容差 0.1 より意図的に広く、わずかに傾いたベースライン上のペアも、精密なベースライン座標系のテストが走る前に窓の中に収まります。微妙なケースが 1 つあります。グリフ A からの走査が、A より先に描かれた一致グリフに出会った場合、A がコピーなので A を落とし、走査を止めます。先のグリフが、さらに先の何かとの比較を続けます。ペアだけでなく三重打ちでも「先に描かれたほうが勝つ」を成立させるのは、この規則です
比較はフォントや文字コードではなく Unicode 値で行います。異なるフォントを通じて同じ Unicode に写像される 2 つのグリフ、たとえば片方の埋め込みサブセットの塗りパスと別のサブセットのストロークパスも、それでも 1 文字として認識されます。裏面は、コードが Unicode 0 に解決されるグリフには比較のしようがないことで、これはHotPDF で読み込んだ PDF からテキストを抽出する記事で扱われている未解決カウントに気を配る、もう 1 つの理由です
重複グリフを除去する HotPDF の抽出呼び出しはどれか
HotPDF のテキストを生む呼び出しはすべて重複を除去し、生のグリフ呼び出しはしません。重複除去は THotPDF.ExtractVisibleLoadedPageGlyphs に住んでいます。ページの可視ボックスの外のグリフを捨てるのもこの段階で、プレーンテキスト、列、レイアウト、セマンティックテキストの各抽出はすべてここからグリフを読みます。THotPDF.ExtractLoadedPageGlyphs は今も、上塗りを含む、コンテンツストリームが描くすべてのグリフを返します。ハイライト、黒塗り、フォレンジックツールを組む呼び出し側は、何が描かれたかの完全な記録を必要とするからです
普通のテキスト処理では、呼び出しコードは何も変わりません。v2.768.15 より前に倍化テキストを抽出していた同じループが、今は各単語を 1 回ずつ抽出します
var
Pdf: THotPDF;
PageCount, I: Integer;
PageText: UnicodeString;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('brochure.pdf');
for I := 0 to PageCount - 1 do
if Pdf.ExtractLoadedPageText(I, PageText) then
if Pos('Bold words', PageText) > 0 then
Writeln('Found on page ', I + 1);
finally
Pdf.Free;
end;
end;
レイアウトと列の出力も同様に恩恵を受けます。重複したランは、従来は文字グリッド上で行の残りを横へ押しやっていました。ExtractLoadedPageTextColumns は任意のギャップ倍率をそのまま保ちます。重複フィルタは列分割の前に走るため、この設定と干渉しません
var
Grid, Columns: UnicodeString;
begin
if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
TFile.WriteAllText('page1-layout.txt', Grid, TEncoding.UTF8);
// ギャップがグリフのアドバンスの中央値の 3 倍を超える位置で列を分割
if Pdf.ExtractLoadedPageTextColumns(0, Columns, 3.0) then
TFile.WriteAllText('page1-columns.txt', Columns, TEncoding.UTF8);
end;
ページに上塗りテキストがそもそも含まれるかどうかを知りたければ、2 つのグリフ呼び出しを比較します。この差には crop box の外に落ちたグリフも数えられるため、正確な数字ではなく重複の上限として扱ってください
var
AllGlyphs, Visible: THPDFGlyphArray;
begin
if Pdf.ExtractLoadedPageGlyphs(0, AllGlyphs) and
Pdf.ExtractVisibleLoadedPageGlyphs(0, Visible) then
Writeln(Format('%d painted, %d read as text (%d overprinted or off-page)',
[Length(AllGlyphs), Length(Visible),
Length(AllGlyphs) - Length(Visible)]));
end;
これらのグリフをテキストへ変える単語間隔と改行の規則は、フィルタによって変わりません。詳細はHotPDF がページテキストに単語スペースと改行を置く仕組みで扱っています
このルールが他ツールと意図的に異なる点
HotPDF は、他のいくつかの抽出ツールが保持する完全重複を落とします。この違いは意図的です。v2.768.15 の変更をリファレンスコーパスで確認したとき、まったく同じ位置に 2 回描かれた 1 つのページフッターは、HotPDF では 1 回、リファレンス抽出器では 2 回抽出されました。同一グリフの上に描かれたグリフは、別の文字としては見えません。1 回読むのが、ページが実際に示しているものです
誠実な限界は逆向きにも走ります。2 パス目をベースライン方向にグリフサイズの 0.1 より大きくずらす擬似ボールドは捕まりませんし、明らかに違うサイズで描かれた影のパスも捕まりません。こうしたレイアウトは実務では稀です。オフセットがそこまで大きくなると、見た目はもうボールドに見えなくなるからです。それでも飾りの見出しから重複テキストが出る場合は、フィルタが壊れたと決めつける前に ExtractLoadedPageGlyphs のグリフ X 値を比較してください。このフィルタは、幾何的に同居していない重複——ページの別の場所の隠しレイヤーに繰り返された柱のようなもの——には何もしません。それはコンテンツの問題であって、上塗りではありません
CID フォントのテキストで行末が欠けていた理由
HotPDF v2.768.12 で修正された別のバグは、/W 幅配列が CID 順にソートされていない CID キー(Type 0)フォントで組まれたテキストの行末を落としていました。ISO 32000-1 §9.7.4.3 は /W 配列の形式を定義しますが、エントリが CID の昇順であることを要求していません。一部のジェネレータは、グリフが文書で最初に使われた順に書き出します
テキスト抽出のデコーダは幅をバイナリサーチで引いていました。ソート済み入力を黙って仮定する方式です。ソートされていない配列ではほとんどの参照が外れ、既定幅の /DW、つまり 1000 単位(全角 1 em)にフォールバックします。このためそのようなフォントの Latin テキストは、すべての文字が全幅であるかのように組まれ、行はページの右端をはるかに超えて走り、可視ボックスの外に落ちたグリフは捨てられました。行末は抽出テキストからも検索からも消えます。クラッシュも報告も何もなく、だからこそ発見が難しかったのです
HotPDF は今や、/W エントリを読み込んだ直後に安定なマージソートで並べます。2 つのエントリが重なる場所では、先に書かれた方が依然勝ち、配列がファイル順に読める通りになります。ページレンダラーは一度も影響を受けていません。幅データの自分のコピーを保持し、線形に走査するからです。縦書き用の幅配列 /W2 も線形に走査されます。ページは完璧に見えるのに抽出がひどい、という場面で思い出す価値のある分業です。PDF ライブラリのレンダリングと抽出は、期待よりずっと少ないコードを共有しがちです
FAQ
HotPDF は「fill」の l 重ねのような繰り返し文字を除去するのか
いいえ。隣接する本物の 2 文字はアドバンス幅 1 つぶんほど離れています。HotPDF が融合するのは、ベースライン方向にグリフサイズの 0.1 以内、横方向に 0.2 以内で重なる同一文字だけです。正当な繰り返しは保持されます
上塗りの重複を含むすべてのグリフを取得するには
THotPDF.ExtractLoadedPageGlyphs を呼びます。ページが描画順に描くすべてのグリフを返します。THotPDF.ExtractVisibleLoadedPageGlyphs とすべてのテキスト抽出呼び出しは、重複除去済みの可視セットを返します
擬似ボールドの重複テキストを直した HotPDF のバージョンはどれか
HotPDF v2.768.15 がテキスト、レイアウト、列、セマンティックテキストの各抽出に重複グリフ除去を追加し、v2.768.12 がソートされていない /W 配列を持つ CID フォントの行末欠落を修正しました
どちらの修正も現行のHotPDF Delphi PDF コンポーネントに搭載されています。PDF の作成、編集、レンダリング機能とともに、Delphi と C++Builder から読み込んだ PDF のテキストを抽出します