Leseordnung & XY-Cut — Mehrspaltige PDFs in natürlicher Reihenfolge extrahieren
Mehrspaltige PDFs — wissenschaftliche Arbeiten, Lehrbücher, Magazinartikel, politische Kurzdossiers — bringen die meisten Extraktionswerkzeuge aus dem Tritt. Ein naiver Top-to-Bottom-Durchlauf zieht ein Wort aus Spalte 1, dann eins aus Spalte 2, dann wieder aus Spalte 1 — und produziert Kauderwelsch wie accompaally ("accompa" aus Spalte 1, verklebt mit "ally" aus Spalte 2).
PDF Oxide setzt einen XY-Cut-Algorithmus ein, um Spalten automatisch zu erkennen und eine natürliche Leseordnung zu erzeugen. Seit v0.3.34 schützt er zusätzlich vor falsch-positiven Treffern auf dünn besetzten Layouts (Impressum-, Titelseiten) und verarbeitet gemischte Layouts korrekt, in denen eine Tabelle inmitten eines Fließtexts steht.
Kurzes Beispiel
Die Extraktion ist standardmäßig spaltenbewusst — ohne zusätzlichen Schalter:
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("academic-paper.pdf")
text = doc.extract_text(0)
# Spalten werden von oben nach unten innerhalb jeder Spalte gelesen, nicht verschränkt.
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));
Was XY-Cut leistet
Der XY-Cut-Algorithmus zerlegt eine Seite rekursiv in rechteckige Regionen, indem er abwechselnd vertikale und horizontale Schnitte entlang von Weißraum-Fugen anlegt:
- Alle Zeichen werden auf die X-Achse projiziert. Zeigt sich eine hohe, breite vertikale Lücke (die Spaltenfuge), wird die Seite an dieser X-Koordinate in zwei Regionen geteilt.
- Innerhalb jeder Region wird auf die Y-Achse projiziert und an horizontalen Fugen (Absatzumbrüche, Abschnittsgrenzen) geteilt.
- Die Rekursion endet, wenn eine Blattregion keine deutliche Fuge mehr enthält — das sind die atomaren Blöcke.
- Blöcke werden in der Reihenfolge oben nach unten, links nach rechts serialisiert.
Das entspricht der Lesegewohnheit eines Menschen: Spalte 1 von oben nach unten, dann Spalte 2 von oben nach unten, dann eine etwaige seitenbreite Fußzeile.
Wann XY-Cut greift
XY-Cut läuft automatisch, sobald extract_text ein mehrspaltiges Layout erkennt. Übersprungen wird er bei:
- Einspaltigen Seiten (keine vertikale Fuge gefunden; es greift die standardmäßige zeilenbewusste Sortierung)
- Dünn besetzten Seiten mit weniger als etwa 10 Textspannen pro vermuteter Spalte — typischerweise Titel- oder Impressumsseiten, bei denen zwei X-Zentren-Peaks ein Artefakt und keine echten Spalten sind (behoben in v0.3.34)
Für den Normalfall ist keine Konfiguration nötig. Wer einen der beiden Modi erzwingen möchte, findet weiter unten den Abschnitt „Opt-out".
Was v0.3.34 behoben hat
Verschränkte Mehrspaltenausgabe bei nicht getaggten PDFs
Bei nicht getaggten mehrspaltigen PDFs (wissenschaftlichen Lehrbüchern, Genetik-Nachschlagewerken) wendete extract_text den XY-Cut zuvor innerhalb von extract_spans() an und sortierte das Ergebnis in extract_text_with_options anschließend mit einer zeilenbewussten Sortierung neu — wodurch die Spaltenstruktur wieder zerschlagen wurde. Ergebnis: Fragmente wie accompaally.
Fix: Die zeilenbewusste Nachsortierung wird bei Seiten, die tatsächlich mehrspaltig sind, nun übersprungen. Verifiziert auf Hartwell Genetics, Murphy ML und Kandel Neural Science.
Seiten mit Tabelle im Fließtext
Gemischte Layouts (eine Tabelle in laufendem Fließtext) konnten den Spalten-Detektor täuschen, weil tabulatorgefüllte Tabellenzeilen die Spaltenfuge ausfüllten. Fix:
- Breite Spannen (über 55 % der Regionsbreite) werden aus der Projektionsdichte ausgeschlossen — tabulatorgefüllte Zeilen verdecken die Fuge nicht mehr.
- Ein-Zeichen-Spannen (Tabellenzellen-Werte wie
G,T) werden aus der Projektion ausgeschlossen, damit sie sich nicht über die Fuge verstreuen. - Die Abdeckung wird anhand einer Zeichenzahl-Schätzung statt der rohen Bbox-Breite berechnet, sodass tabulatorgefüllte Zeilen nicht mehr als dichter Fließtext durchgehen.
Falsch-positive Treffer bei dünnen Layouts
Impressum-, Titel- und Kolophonseiten können zwei X-Zentren-Peaks erzeugen — bei nur 7–10 Spannen pro „Spalte". Sie werden nicht mehr als mehrspaltig behandelt, sodass der XY-Cut keine Sätze mehr auseinanderreißt, deren Hälften an unterschiedlichen X-Positionen derselben Zeile sitzen.
Strukturierter Zugriff pro Spalte
Eine Ebene unter extract_text können Sie Wörter oder zeichenweise Daten mit derselben Spaltenordnung abrufen:
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# liefern Zeilen der Form (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})");
Jedes Wort bzw. jede Zeile trägt ihre Bounding Box mit sich, sodass Sie nach Spalten gruppieren und selbst neu sortieren können, falls Sie eine eigene Regel brauchen (z. B. für arabische Layouts zuerst die rechte Spalte lesen).
Mehrspaltigkeit manuell erkennen
Wer vor der Extraktion verzweigen will, ob eine Seite mehrspaltig ist:
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("mixed.pdf")
for i in range(doc.page_count()):
words = doc.extract_words(i)
# Heuristik: unterschiedliche X-Zentren-Cluster
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)")
Im produktiven Einsatz ist extract_text vorzuziehen — lassen Sie den XY-Cut samt Schutz gegen dünne Layouts die Entscheidung treffen.
Opt-out oder eigene Reihenfolge
Wer Rohspannen in Positionsreihenfolge braucht (etwa für eine eigene Layout-Engine), verwendet extract_chars oder extract_words — diese liefern Datensätze mit Bounding Boxes, auf die eine eigene Sortierung angewendet werden kann:
Python
chars = doc.extract_chars(0)
# Oben nach unten, dann links nach rechts — Spalten werden ignoriert
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()));
Verwandte Seiten
- Textextraktion — vollständige Extraktions-API
- Extraktionsprofile — Leerzeichen-Erkennung pro Dokumenttyp anpassen
- Tabellen aus PDF extrahieren — strukturierte Tabellenausgabe
- Changelog — v0.3.34 Fixes für Mehrspaltigkeit und gemischte Layouts