Technischer Artikel

Struktur-geordnete PDF-Textextraktion in Delphi mit HotPDF

Jeder geometrische Textextraktor rät. Er liest die Glyphen, die eine Seite zeichnet, sortiert sie nach Grundlinie und horizontaler Position und hofft, dass die visuelle Anordnung der Reihenfolge entspricht, in der ein Mensch lesen würde. Bei einem einspaltigen Bericht ist dieser Rat richtig. Bei einem zweispaltigen Journalartikel, einem Formular mit Seitenleiste oder einer Tabelle, deren Zellen spaltenweise ausgegeben wurden, ist er falsch, und zwar auf Arten, die schwer zu bemerken und downstream teuer zu entdecken sind. HotPDF antwortet darauf mit ExtractLoadedPageStructureText, das die Geometrie vollständig ignoriert: Es durchläuft den Dokumentstrukturbaum in Autorenreihenfolge, wie ISO 32000-1 §14.8.4 sie definiert, und setzt dann die Seitenglyphen anhand ihrer Marked-Content-Kennung wieder zusammen. Für ein getaggtes PDF ist das keine Heuristik, es ist die Reihenfolge, die die erzeugende Anwendung deklariert hat

Die Funktion gibt False zurück, wenn die Seite keinen brauchbaren Strukturbaum hat, was das Signal ist, auf den geometrischen Extraktor zurückzufallen statt zu scheitern. Dieses Zwei-Wege-Design wiegt schwerer als der Algorithmus: Eine echte Dokumentaufnahme sieht getaggte Behördenformulare und Scanner-Ausgabe im selben Ordner, und eine Pipeline, die nur eines davon behandelt, ist keine Pipeline

Warum vermasselt die geometrische Extraktion die Lesereihenfolge?

Weil ein PDF-Content-Stream überhaupt keine Lesereihenfolge trägt. Er ist eine Sequenz von Zeichenoperatoren, und ein Producer darf sie in welcher Sequenz ausgeben, die zu seiner eigenen Layout-Engine passt. Textverarbeitungen geben üblicherweise in Flussreihenfolge aus, und geometrisches Sortieren sieht gut aus. Layout-Werkzeuge, Formulardesigner und Berichtsgeneratoren häufig nicht: Eine Seitenfußzeile kann vor dem Haupttext ausgegeben werden, eine Tabelle kann spaltenweise gefüllt werden, und eine zweispaltige Seite kann Zeilen aus beiden Spalten verschränken, weil der Setzer sie gemeinsam aufgelöst hat

Vergleich einer zweispaltigen PDF-Seite: baseline-sortierte geometrische Extraktion verschränkt Spalten gegen die struktur-geordnete MCID-Extraktion in HotPDF
Glyphen nach Grundlinie sortieren verschränkt zwei Spalten zu Unsinn, während der Strukturbaum die vom Producer deklarierte Reihenfolge abspielt

Der Fehlermodus ist still. Ein geometrischer Extraktor meldet nie einen Fehler, er reicht einfach Prosa zurück, deren Sätze aus zwei Spalten zusammengeschnitten sind. Alles, was diesen Text konsumiert – ein Suchindex, ein E-Invoice-Feldmapper, eine Retrieval-Pipeline, die ein Sprachmodell füttert –, erbt den Schaden ohne Warnung. HotPDF liefert auch die geometrischen Extraktoren für geladene Dokumente mit, und sie bleiben das richtige Werkzeug für ungetaggte Dateien; der Punkt des Struktur-Ordnung-Pfads ist, aufzuhören zu raten, wenn das Dokument die Antwort bereits trägt

Was der Strukturbaum tatsächlich speichert

Ein getaggtes PDF hält eine zweite, parallele Beschreibung der Seite. Der Katalog zeigt auf eine /StructTreeRoot, deren /K-Kinder einen Baum von Strukturelementen bilden: /Document, /Sect, /P, /Table, /TR, /TD und so weiter. Die Blätter dieses Baums sind Marked-Content-Referenzen, Integer, die eine Spanne des Seiten-Content-Streams benennen. Auf der Inhaltsseite werden diese Spannen mit einem BDC-Operator geöffnet, der eine /MCID trägt, und mit EMC geschlossen. Jedes Strukturelement trägt außerdem einen /Pg-Eintrag, der die Seite benennt, zu der es gehört, was die Traversierung je Seite in einem Dokument möglich macht, dessen Strukturbaum hunderte Seiten umspannt

Anatomie des PDF-Strukturbaums: StructTreeRoot-Elemente wie Sect, Table, TR und TD sind mit BDC-MCID-Spannen im HotPDF-Seiten-Content-Stream verknüpft
Blätter des Baums sind Marked-Content-Referenzen, und jedes Element trägt einen Pg-Eintrag, der den Lauf auf die aktuelle Seite filtern lässt

HotPDF traversiert diesen Baum mit einer Tiefenbegrenzung von 128 Ebenen und filtert auf /Pg, sodass nur die aktuelle Seite beiträgt. Die Ausgabe der Traversierung ist kein Text, sondern eine geordnete Liste von MCID-Werten: die Autorenreihenfolge der Marked-Content-Spannen auf dieser Seite. Text wieder zusammenzusetzen ist dann eine Frage, die Glyphen in dieser Reihenfolge abzuspielen

Die MCID wird während der Glyphenextraktion aufgezeichnet, nicht danach nachgeschlagen

Das ist das Implementierungsdetail, das das Feature billig macht. HotPDF zeichnet die aktive Marked-Content-Kennung bereits bei jeder Glyph auf, die es extrahiert, im MCID-Feld von THPDFGlyphRecord, denn der Content-Stream-Interpreter weiß, welcher BDC-Geltungsbereich offen ist, wenn er jeden Tj- oder TJ-Operator verarbeitet. Struktur-Ordnungs-Extraktion braucht also keinen zweiten Durchlauf über den Content-Stream. Sie sammelt die MCID-Sequenz aus dem Strukturbaum, sortiert die bereits extrahierten Glyphen in Eimer nach MCID und gibt sie in dieser Sequenz aus

var
  Pdf: THotPDF;
  PageCount, I, Untagged: Integer;
  PageText, AllText: UnicodeString;
  Report: TStrings;   // Diagnose-Senke im Besitz des Aufrufers
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('accessible-form.pdf');
    AllText := '';
    for I := 0 to PageCount - 1 do
    begin
      if Pdf.ExtractLoadedPageStructureText(I, PageText, Untagged) then
      begin
        // Autorenreihenfolge direkt aus dem Strukturbaum
        if Untagged > 0 then
          Report.Add(Format('page %d: %d glyphs outside the structure tree',
            [I, Untagged]));
      end
      else
        // Kein brauchbarer Strukturbaum auf dieser Seite: geometrischer Fallback
        Pdf.ExtractLoadedPageText(I, PageText);
      AllText := AllText + PageText + #13#10;
    end;
  finally
    Pdf.Free;
  end;
end;

Ungetaggte Glyphen werden gezählt, nie still fallengelassen

Eine Seite kann teilweise getaggt sein. Producer fügen eine Zierlinie, eine Seitenzahl oder ein spätes Wasserzeichen außerhalb jedes BDC-Geltungsbereichs hinzu, und diese Glyphen gehören zu keiner MCID. Sie fallen zu lassen wäre die aufgeräumte Implementierung und die falsche, denn dieselbe Lücke erscheint auch, wenn ein Producer den Haupttext taggt, aber die Tabelle vergisst, und Sie würden die Tabelle verlieren, ohne es zu bemerken

HotPDF hängt nicht beanspruchte Glyphen als geometrischen Schwanz hinter den struktur-geordneten Text an und meldet ihre Anzahl über den Ausgabeparameter UntaggedGlyphCount. Diese Zahl ist ein Qualitätssignal, auf das man reagieren kann. Eine Handvoll Glyphen auf einer Seite von zweitausend ist Seitenmobiliar und kann ignoriert werden. Vierzig Prozent der Seite außerhalb des Strukturbaums bedeuten, dass das Tagging dekorativ ist und der geometrische Extraktor für diese Datei die ehrlichere Antwort ist

Entscheidungsfluss der HotPDF-Strukturtextextraktion mit geometrischem Fallback, wenn eine Seite keinen brauchbaren Strukturbaum oder dekoratives Tagging hat
True bedeutet Strukturordnung mit angehängtem ungetaggtem Schwanz, und False leitet die Seite an den geometrischen Extraktor weiter, statt zu scheitern
function ExtractPageBestEffort(Pdf: THotPDF; PageIndex: Integer;
  out AText: UnicodeString; out UsedStructure: Boolean): Boolean;
var
  Untagged, TotalGlyphs: Integer;
  Glyphs: THPDFGlyphArray;
begin
  UsedStructure := False;
  if Pdf.ExtractLoadedPageStructureText(PageIndex, AText, Untagged) then
  begin
    TotalGlyphs := 0;
    if Pdf.ExtractLoadedPageGlyphs(PageIndex, Glyphs) then
      TotalGlyphs := Length(Glyphs);
    // Dem Strukturbaum nur vertrauen, wenn er den Großteil der Seite beansprucht
    if (TotalGlyphs = 0) or (Untagged * 4 <= TotalGlyphs) then
    begin
      UsedStructure := True;
      Result := True;
      Exit;
    end;
  end;
  Result := Pdf.ExtractLoadedPageText(PageIndex, AText);
end;

Was die Funktion False zurückgeben lässt

Drei Fälle, und es lohnt sich, sie zu unterscheiden, denn nur einer von ihnen ist ein Defekt im Dokument. Der erste ist ein gewöhnliches ungetaggtes PDF: keine /StructTreeRoot, nichts zu durchlaufen, und False ist schlicht die Wahrheit. Der zweite ist eine gescannte Seite, deren Text aus einer nie getaggten OCR-Schicht stammt. Der dritte ist der interessante: Inhalt, der BDC-Operatoren mit /MCID-Werten trägt, dessen Seite aber keinen /StructParents-Eintrag hat und dessen Strukturbaum diese Kennungen nie referenziert. Der Marked Content existiert, die Strukturseite nicht, und es gibt keine Reihenfolge zu bergen. HotPDF meldet False, statt eine zu erfinden

Dieser letzte Fall zeigt sich in handeditierten Dateien und in der Ausgabe von Werkzeugen, die Marked Content für Optional-Content- oder Artifact-Zwecke ausgeben, ohne einen Strukturbaum zu bauen. Wer selbst getaggte PDFs erzeugt, für den ist dieselbe Asymmetrie das, worauf die PDF/UA-Validierung prüft, und das Gegenstück auf der Writer-Seite behandelt das Layout-DOM, das getaggte, paginierte Ausgabe emittiert

Wo sich Strukturordnung bezahlt macht

Barrierefreiheits-Auditing ist der naheliegende: Wer ein Dokument gegen PDF/UA zertifiziert, für den ist die Lesereihenfolge, die ein Screenreader ansagen wird, exakt die Strukturordnung, also ist ihre Extraktion der Weg, sie ohne Screenreader zu prüfen. Datenerfassung ist der größere kommerzielle Fall. Getaggte Behördenformulare, regulierte Offenlegungen und E-Invoice-Anhänge tragen Feldbezeichnungen und Werte in deklarierter Reihenfolge, und sie in dieser Reihenfolge zu lesen entfernt eine ganze Klasse von Mapping-Bugs, die geometrische Extraktion auf mehrspaltigen Layouts erzeugt

Der neueste Konsument ist Retrieval für Sprachmodelle. Ein Dokument für Embeddings zu chunken ist nur so gut wie die Textreihenfolge, und ein Chunk, der zwei Spalten zusammenschneidet, erzeugt Sätze, die nie existierten. Struktur-Ordnungs-Extraktion ist die billigste verfügbare Korrektur dafür, denn bei getaggten Dokumenten steht die korrekte Reihenfolge bereits in der Datei und muss nur gelesen werden

HotPDF ist eine native VCL-Komponente für Delphi und C++Builder, also laufen die Strukturbaum-Traversierung und die Glyphen-Wiedergabe beide in-process gegen ein geladenes Dokument, ohne dass ein externer Renderer beteiligt ist. Vollständige API-Details zur Extraktionsfamilie für geladene Dokumente stehen auf der Produktseite der HotPDF Delphi PDF-Komponente