PDFiumPas liefert Seitentext als Struktur statt als String zurück. GetStructuredText erzeugt eine TPdfStructuredTextPage, die Blöcke enthält, jeder mit Zeilen, jede mit formatierten Spans, mit seitenraumbezogenen Grenzen auf jeder Ebene und erhaltenen Quellzeichenindizes, sodass sich jedes Fragment auf die zugrunde liegende Textseite zurückführen lässt
Die einfache String-Extraktion, mit der die meisten Codebasen beginnen, gibt es weiterhin, und sie ist für ihren Zweck weiterhin korrekt. Sie hört auf zu genügen, sobald man wissen muss, welche Wörter eine Überschrift waren, welche zur linken Spalte gehörten oder wo auf der Seite ein Treffer tatsächlich sitzt
Warum ist ein flacher String die falsche Ausgabe für die meisten Aufgaben?
Weil die Fragen, die man an extrahierten Text stellt, fast nie "welche Zeichen stehen auf dieser Seite" lauten. Sie lauten "was ist der Titel", "ist das eine Tabelle", "gehört dieser Absatz zu Abschnitt 4", "wo zeichne ich die Hervorhebung". Ein einzelner String beantwortet keine davon, und jede Antwort, die man aus ihm rekonstruiert, ist eine Heuristik, die man nun selbst besitzt
Zweispaltige Layouts machen den Punkt konkret. Extrahiert man einen zweispaltigen Artikel als String, erhält man, je nachdem, wie der Erzeuger den Content-Stream geschrieben hat, entweder Spalte eins gefolgt von Spalte zwei, oder man erhält Zeile eins von Spalte eins, Zeile eins von Spalte zwei, Zeile zwei von Spalte eins, und so weiter die Seite hinunter. Beides kommt aus einem konformen PDF. Keines ist auf Formatebene falsch, weil PDF Markierungen auf einer Seite beschreibt, nicht eine Dokumentgliederung. Ein blockbasiertes Modell lässt den Extraktor die Reihenfolgeentscheidung explizit treffen und mitteilen, welche Entscheidung er getroffen hat
Inhaltsreihenfolge oder physisches Layout?
TPdfStructuredTextOptions.ReadingOrder wählt zwischen roContentOrder und roPhysicalLayout, und die richtige Antwort hängt davon ab, wem man mehr vertraut, dem Erzeuger oder der Geometrie
Content Order liefert Text in der Reihenfolge zurück, in der der Content-Stream ihn zeichnet. Das ist schnell und für Dokumente eines wohlerzogenen Erzeugers meist die beabsichtigte Lesereihenfolge. Physical Layout ignoriert die Stream-Reihenfolge und rekonstruiert die Reihenfolge aus der tatsächlichen Position der Zeichen, gruppiert in Zeilen und dann in Spalten. Das will man bei gescannten und dann per OCR erfassten Seiten, bei Ausgaben von Tools, die Text in Schriftreihenfolge statt in Lesereihenfolge ausgeben, und bei allem, wo das visuelle Ergebnis das Einzige ist, worauf man sich verlassen kann
uses
PDFium;
var
Pdf: TPdf;
Options: TPdfStructuredTextOptions;
Page: TPdfStructuredTextPage;
B, L: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'article.pdf';
Pdf.LoadDocument;
Pdf.PageNumber := 1; // 1-basiert
Options := TPdfStructuredTextOptions.Default;
Options.ReadingOrder := roPhysicalLayout;
Options.IncludeFontInfo := True;
Options.IncludeSemantics := True;
Options.MaxCharacters := 200000; // fail-closed Budget
Page := Pdf.GetStructuredText(Options);
for B := 0 to High(Page.Blocks) do
begin
if Page.Blocks[B].Kind = cfHeading then
Emit(Format('H%d: %s',
[Page.Blocks[B].HeadingLevel, Page.Blocks[B].Text]))
else
for L := 0 to High(Page.Blocks[B].Lines) do
Emit(Page.Blocks[B].Lines[L].Text);
end;
finally
Pdf.Free;
end;
end;
Was fügt Tagging hinzu, das Geometrie nicht kann?
Absicht. Bei aktiviertem IncludeSemantics tragen Blöcke aus einem getaggten PDF ein Kind aus dem Strukturbaum, sodass eine Überschrift eine Überschrift ist, weil der Erzeuger es so gesagt hat, nicht weil ihre Schrift größer als der Durchschnitt war. Die Arten decken die Formen ab, die für Weiterverwendung zählen: cfParagraph, cfHeading mit einem HeadingLevel, cfListItem, cfTableCell, cfCaption, cfFigure und der ungetaggte Fallback cfPlain
Das Feld Source erfasst, woher jede Klassifizierung stammt, rosStructure für den Strukturbaum und rosHeuristic für Inferenz, das Feld, das man protokollieren sollte, wenn man entscheidet, wie weit man einer Extraktions-Pipeline über einen Dokumentbestand hinweg vertraut. Figuren sind ein besonderer Fall, den man kennen sollte: Bei einem cfFigure-Block stammt der Text aus der alternativen Beschreibung statt aus irgendwelchen Glyphen, da eine Figur keine eigenen Zeichen hat. Nicht zugeordneter Alternativtext wird weiterhin dargestellt, statt verworfen zu werden, was es einem Barrierefreiheits-Audit erlaubt zu sehen, dass eine Beschreibung existiert, selbst wenn nichts auf der Seite sie zeichnet. Das Tagging-Modell selbst wird in PDF/UA-Strukturbaum-Validierung behandelt
Spans tragen die Formatierung und die Herkunft
Jeder TPdfStructuredTextSpan trägt seinen Text, seine seitenraumbezogenen Grenzen, FontName, FontSize, FontWeight und Angle, plus SourceStartIndex und SourceCharacterCount. Spans brechen dort, wo sich die Formatierung ändert, sodass ein Satz mit drei fetten Wörtern zu drei Spans wird, und Hervorhebung in HTML oder Markdown wiederherzustellen ist eine Frage des Lesens von Eigenschaften statt des Ratens aus Schriftnamen
Die beiden Quellindex-Felder sind diejenigen, die aus Extraktion ein Feature statt eines Berichts machen. Sie zeigen zurück in die Zeichenfolge der Seite, was bedeutet, dass ein in einer Suche gefundener Block ohne einen zweiten, anders geordneten Durchlauf über den Text in zeichengenaue Auswahlgeometrie oder ein Highlight-Rechteck umgewandelt werden kann; die Mechanik wird in visueller Textzeilenauswahl mit Character Boxes beschrieben. Das Feld Angle zählt mehr, als es aussieht: Gedrehter Text in einem Stempel oder Wasserzeichen landet im selben Koordinatenraum wie Fließtext, und eine Pipeline, die Winkel ignoriert, verschmilzt bereitwillig ein diagonales "ENTWURF" mitten in einen Absatz
Budget, und die zwei Qualitätszähler
MaxCharacters ist ein fail-closed Budget, keine Abschneide-Einstellung: Eine Seite, die es überschreitet, stoppt, statt still einen Teil des Inhalts zurückzugeben. Auf einem nicht vertrauenswürdigen Aufnahmepfad ist das das gewünschte Verhalten, weil eine Seite mit einer Million Zeichen entweder ein maschinell erzeugtes Monstrum oder ein Versuch ist, den eigenen Extraktor zum langsamsten Teil des Systems zu machen
Zwei Zähler auf der zurückgegebenen Seite beschreiben die Extraktionsqualität direkt. UnmappedCharacterCount zählt Zeichen ohne brauchbare Unicode-Zuordnung, das klassische Symptom einer Subset-Schriftart, die ohne /ToUnicode-CMap eingebettet wurde; solcher Text rendert perfekt und extrahiert als nichts Brauchbares. GeometryFailureCount zählt Zeichen, deren Begrenzungsrahmen nicht bestimmt werden konnte, was die Physical-Layout-Reihenfolge verschlechtert. Protokollieren Sie beide. Ein Dokumentbestand, bei dem diese Zahlen konstant nahe null liegen, lässt sich mit Vertrauen indizieren, und einer, bei dem das nicht der Fall ist, sagt Ihnen, dass manche Erzeuger in Ihrer Pipeline Aufmerksamkeit brauchen, bevor irgendein nachgelagertes Ergebnis vertrauenswürdig ist
var
Page: TPdfStructuredTextPage;
B, S, L: Integer;
Emphasised: Boolean;
begin
Page := Pdf.GetStructuredText(Options);
if Page.UnmappedCharacterCount > 0 then
Log(Format('page %d: %d characters without a Unicode mapping',
[Page.PageNumber, Page.UnmappedCharacterCount]));
if Page.GeometryFailureCount > 0 then
Log(Format('page %d: %d characters without geometry',
[Page.PageNumber, Page.GeometryFailureCount]));
for B := 0 to High(Page.Blocks) do
for L := 0 to High(Page.Blocks[B].Lines) do
for S := 0 to High(Page.Blocks[B].Lines[L].Spans) do
begin
Emphasised := Page.Blocks[B].Lines[L].Spans[S].FontWeight >= 600;
AppendRun(Page.Blocks[B].Lines[L].Spans[S].Text, Emphasised,
Page.Blocks[B].Lines[L].Spans[S].SourceStartIndex);
end;
end;
Performance auf echten Seiten
Physical-Layout-Extraktion ist der teure Modus, und die Implementierung ist für Seiten gebaut, die tatsächlich groß sind: Die Zeichenordnung läuft in O(n log n) statt durch wiederholtes Scannen, Zeilen- und Span-Puffer wachsen geometrisch statt bei jedem Zeichen neu zu allozieren, Unicode-Text wird in Puffern statt durch String-Konkatenation aufgebaut, und Schrift-Lookups für benachbarte Textobjekte werden gecacht. Diese Kombination hält eine dichte 5.000-Zeichen-Seite vorhersagbar statt quadratisch
Bei einem seitenzahllastigen Job lohnt es sich weiterhin, wo möglich den günstigeren Modus zu wählen. Nutzen Sie roContentOrder mit aktivierter Semantik für getaggte Dokumente, denen Sie vertrauen, und reservieren Sie roPhysicalLayout für gescanntes und veraltetes Material, bei dem Geometrie das einzige Signal ist. Braucht man nur einen einfachen String, bleibt die in Extrahieren von Text aus PDF-Dokumenten beschriebene einfachere API der schnellere Weg, und wenn man Text auf Marked-Content-Kennungen zurückführen muss, deckt Lesen und Schreiben von BDC- und MCID-Marked-Content diese Ebene ab
Das Blockmodell passt auch sauber auf das, was Retrieval-Pipelines wollen: eine Überschrift mit ihren Absätzen ist ein Chunk mit einem Titel, und die Grenzen lassen ein Zitat auf eine Position auf einer Seite zeigen statt auf ein ganzes Dokument. PDFiumPas ist eine Delphi- und Lazarus-Komponente rund um die PDFium-Engine, dokumentiert mit Beispielen auf der PDFium Delphi-Komponente-Seite