読み取り順序と XY-cut — 段組 PDF を自然な順序で抽出
段組の PDF — 学術論文、教科書、雑誌記事、政策ブリーフ — は、多くの抽出ツールをつまずかせます。素朴に上から下へ読むと、段 1 から 1 単語、続いて段 2 から 1 単語、また段 1 に戻る…という具合に、accompaally(段 1 の "accompa" と段 2 の "ally" が結合した状態)のような破綻した出力になります。
PDF Oxide は XY-cut アルゴリズムで段を検出し、自然な読み取り順を自動生成します。v0.3.34 からは、疎なレイアウト(奥付や扉)の誤検出を防ぎ、本文中に表が埋め込まれた混在レイアウトも正しく処理します。
クイック例
抽出はデフォルトで段組対応です。フラグ指定は不要です。
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("academic-paper.pdf")
text = doc.extract_text(0)
# 段ごとに上から下へ読み、段を交互に読むことはありません。
Rust
use pdf_oxide::PdfDocument;
let mut doc = PdfDocument::open("academic-paper.pdf")?;
let text = doc.extract_text(0)?;
JavaScript / TypeScript (Node)
const { PdfDocument } = require("pdf-oxide");
const doc = new PdfDocument("academic-paper.pdf");
const text = doc.extractText(0);
doc.close();
JavaScript (WASM)
import { WasmPdfDocument } from "pdf-oxide-wasm";
const doc = new WasmPdfDocument(bytes);
console.log(doc.extractText(0));
doc.free();
Go
doc, _ := pdfoxide.Open("academic-paper.pdf")
defer doc.Close()
text, _ := doc.ExtractText(0)
fmt.Println(text)
C#
using PdfOxide;
using var doc = PdfDocument.Open("academic-paper.pdf");
Console.WriteLine(doc.ExtractText(0));
XY-cut が行うこと
XY-cut は、ページを白地の溝に沿って垂直・水平に交互に切り分け、矩形領域へ再帰的に分解します。
- 全文字を X 軸 に射影します。縦に高く横に広いギャップ(段間の溝)が見つかったら、その X 座標でページを 2 領域に分割します。
- 各領域内で Y 軸 に射影し、水平方向の溝(段落や節の境界)で分割します。
- 葉の領域に強い溝が残らなくなるまで再帰します。これが最小ブロックです。
- ブロックを上から下、左から右の順にシリアライズします。
これは人間の読み方と一致します。段 1 を上から下、続いて段 2 を上から下、最後にページ幅のフッターがあればそれを読みます。
XY-cut が作動する条件
extract_text が段組レイアウトを検出したとき、XY-cut は自動で走ります。次のケースでは スキップ されます。
- 一段組のページ(縦の溝が見つからないため、デフォルトの行単位ソートが使われます)
- 見かけ上の段あたりテキストスパンが約 10 未満の疎なページ — 扉や奥付で、2 つの X 中心ピークが段ではなくアーティファクトである場合(v0.3.34 で修正)
一般的なケースでは設定は不要です。どちらかのモードを強制したい場合は、下の「オプトアウト」を参照してください。
v0.3.34 で修正された点
非タグ付き PDF の段組出力が交互に混ざる問題
非タグ付きの段組 PDF(学術教科書、遺伝学のリファレンス)では、extract_text はまず extract_spans() 内で XY-cut を適用し、その後 extract_text_with_options 内で行単位ソートで再整列していたため、段構造が崩れていました。結果として accompaally のような破片が生じました。
修正: 真に段組のページでは、行単位の再整列をスキップするようになりました。Hartwell Genetics、Murphy ML、Kandel Neural Science の教科書でクリーンな出力を確認済みです。
本文中にテーブルがあるページ
本文に表が埋め込まれた混在レイアウトでは、タブ展開されたテーブル行が段間の溝を埋めてしまい、段検出器を欺いていました。修正内容:
- 領域幅の 55 % を超える幅広スパンは射影密度から除外します — タブ埋めの行が溝を覆い隠さなくなりました。
G、Tのような単一文字スパン(テーブルセル値)は射影から除外し、溝をまたいで散乱しないようにしました。- カバレッジは bbox の生の幅ではなく文字数推定を使うため、タブ埋めの行が密な本文として誤認されなくなりました。
疎なレイアウトでの誤検出
奥付、扉、コロフォンのページは、「段」あたり 7〜10 スパン程度で 2 つの X 中心ピークが現れることがあります。これらは段組として扱われなくなり、同一行の両端に分かれた半分同士を XY-cut が切り離してしまう問題を防ぎます。
段ごとの構造化アクセス
extract_text より下の層では、同じ段順序を保ったまま単語や文字レベルのデータを取得できます。
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("paper.pdf")
for w in doc.extract_words(0):
print(f"{w.text} ({w.x0:.0f},{w.y0:.0f})")
Rust
let mut doc = PdfDocument::open("paper.pdf")?;
for w in doc.extract_words(0)? {
println!("{} ({:.0},{:.0})", w.text, w.x0, w.y0);
}
Go
doc, _ := pdfoxide.Open("paper.pdf")
defer doc.Close()
words, _ := doc.ExtractWords(0)
for _, w := range words {
fmt.Printf("%s (%.0f,%.0f)\n", w.Text, w.X0, w.Y0)
}
C#
using var doc = PdfDocument.Open("paper.pdf");
// Node/C# は (text, x, y, w, h) の行を返します:
var lines = doc.ExtractTextLines(0);
foreach (var (text, x, y, w, h) in lines)
Console.WriteLine($"{text} ({x:F0},{y:F0})");
各単語・行はバウンディングボックスを持つため、段ごとにグループ化して独自の順序を適用できます(例: アラビア語レイアウトで右段から読むなど)。
段組ページを手動で検出する
抽出前にページが段組かどうかで分岐したい場合:
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("mixed.pdf")
for i in range(doc.page_count()):
words = doc.extract_words(i)
# ヒューリスティック: 離れた X 中心クラスタ
x_centers = {round((w.x0 + w.x1) / 2 / 50) * 50 for w in words}
if len(x_centers) >= 2:
print(f"Page {i}: likely multi-column ({len(x_centers)} X-centers)")
本番運用では extract_text を使い、XY-cut と疎レイアウトガードの組み合わせに判断を任せるのが推奨です。
オプトアウトやカスタム順序
位置順に並んだ生のスパンが欲しい場合(独自レイアウトエンジン向けなど)は、extract_chars や extract_words を使ってください。これらはバウンディングボックス付きのレコードを返すので、独自の並べ替えを適用できます。
Python
chars = doc.extract_chars(0)
# 上から下、次に左から右 — 段を無視
chars_sorted = sorted(chars, key=lambda c: (-c.y, c.x))
Rust
let mut chars = doc.extract_chars(0)?;
chars.sort_by(|a, b| b.y.partial_cmp(&a.y).unwrap()
.then(a.x.partial_cmp(&b.x).unwrap()));
関連ページ
- テキスト抽出 — 抽出 API 一式
- 抽出プロファイル — ドキュメント種別ごとのスペース検出チューニング
- PDF から表を抽出 — 構造化されたテーブル出力
- Changelog — v0.3.34 の段組と混在レイアウトの修正