Die HotPDF-Komponente extrahiert Unicode-Text aus jedem PDF, das Sie in Delphi laden, über zwei Aufrufe: ExtractLoadedPageText gibt den Text im Lesefluss einer Seite zurück, und ExtractLoadedPageTextLayout (hinzugefügt in v2.263.0) rekonstruiert die visuelle Anordnung der Seite als Klartext, sodass Spalten, Einrückungen und Tabellenausrichtungen in der Ausgabe erhalten bleiben. Beide Methoden funktionieren auf Dokumenten, die HotPDF nicht selbst erstellt hat. Dies ist der Fall, auf den es in der Praxis ankommt: die Rechnung, die ein Kunde per E-Mail geschickt hat, der Bericht, den ein Scandienstleister geliefert hat, oder der Vertrag, der von einer Software erstellt wurde, an deren Namen sich niemand mehr erinnert
Dahin zu gelangen erfordert jedoch mehr Programmlogik, als die beiden Signaturen vermuten lassen. Ein PDF speichert Text nämlich nicht so ab wie eine Textdatei. Dieser Artikel führt durch beide Extraktionsmodi und wirft anschließend einen Blick unter die Haube auf die drei darunterliegenden Komponenten — den CMap-Reader, den Content-Stream-Interpreter und die Schriftdekodierungs-Fallback-Kette — denn das Wissen um die Funktionsweise dieser Zuordnung entscheidet darüber, ob man sich über fehlerhafte Ausgaben wundert oder sie gezielt diagnostizieren kann
Warum ist Textextraktion schwieriger als das einfache Auslesen von Zeichenketten aus einer Datei?
Ein PDF-Inhaltsstrom (Content Stream) zeichnet Zeichencodes auf, keine Zeichen. Die Operatoren Tj und TJ (ISO 32000-1 §9.4.3) enthalten Byte-Zeichenfolgen, deren Bedeutung vollständig von der Schriftart abhängt, die durch das vorherige Tf ausgewählt wurde: Das Byte 0x41 kann der Buchstabe A unter WinAnsi sein, eine beliebige Glyphe in einer Subset-Schriftart oder die Hälfte einer Zwei-Byte-CID in einer zusammengesetzten CJK-Schriftart. ISO 32000-1 §9.10 definiert die Textextraktion genau als dieses Dekodierungsproblem — die Rückabbildung jedes Codes auf Unicode unter Verwendung aller vom Schriftverzeichnis bereitgestellten Informationen. Der Standard besagt explizit, dass eine konforme Datei nicht zwingend genügend Informationen bereitstellen muss, um dies zu tun
Dieser letzte Halbsatz erklärt jeden Fehlerbericht der Sorte „Warum erzeugt das Kopieren und Einfügen aus dieser PDF-Datei Kauderwelsch?“, den Sie jemals gesehen haben. Ein Erstellungsprogramm, das eine Subset-Schriftart ohne /ToUnicode-Tabelle einbettet, schreibt eine Datei, die perfekt gerendert wird, sich aber als Unsinn extrahieren lässt. Die Zuordnung von Code zu Glyphe existiert zwar, aber die Zuordnung von Code zu Unicode wurde nie mitgeliefert. Jede seriöse Extraktions-API ist daher eine Kette von Fallbacks, und die entscheidende Frage lautet, wie tief diese Kette reicht
Extraktion im Lesefluss mit ExtractLoadedPageText
Für Suchindizes, Schlüsselwortabgleiche oder die Weiterleitung von Text an eine Analyse-Pipeline ist ExtractLoadedPageText der richtige Aufruf. Die Signatur lautet: function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean — die Seitenindizes sind nullbasiert, das Ergebnis wird als natives Delphi-UnicodeString zurückgegeben, und die Funktion gibt False zurück, wenn die Seite keinen lesbaren Inhaltsstrom besitzt, anstatt eine Ausnahme auszulösen
var
Pdf: THotPDF;
PageCount, I: Integer;
PageText, AllText: UnicodeString;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('invoice.pdf');
AllText := '';
for I := 0 to PageCount - 1 do
if Pdf.ExtractLoadedPageText(I, PageText) then
AllText := AllText + PageText + #13#10;
// AllText enthält nun den Fließtext des Dokuments
finally
Pdf.Free;
end;
end;
Zeilenumbrüche in der Ausgabe resultieren aus einer bewusst einfachen Heuristik: Wenn sich der vertikale Ursprung einer Glyphe um mehr als die Hälfte der aktuellen Schriftgröße verschiebt — das Merkmal eines Td- oder T*-Schritts im Inhaltsstrom — wird ein Zeilenumbruch eingefügt. Zeichen, die der Decoder nicht auflösen kann, werden durch Leerzeichen ersetzt, anstatt zu verschwinden, sodass Wortgrenzen erhalten bleiben, selbst wenn einzelne Glyphen dies nicht tun. Was dieser Modus nicht versucht, ist die Erkennung der Lesereihenfolge oder die Erkennung von Mehrspaltigkeit: Eine zweispaltige Seite wird in der Reihenfolge des Inhaltsstroms ausgegeben, was meist, aber nicht immer, der visuellen Reihenfolge entspricht
Wann sollten Sie stattdessen die layouttreue Extraktion verwenden?
ExtractLoadedPageTextLayout ist der richtige Aufruf, wann immer die Position eine Bedeutung trägt: Tabellen, Formulare, Codelisten oder alles, was Sie vergleichen, filtern oder spaltenweise parsen möchten. Anstatt Glyphen in einen Fließtext zu pressen, gruppiert das Verfahren sie in Basislinien, sortiert jede Basislinie nach X und rekonstruiert horizontale und vertikale Abstände auf einem Monospace-Zeichenraster, das sich an der mittleren Glyphenbreite und der Schriftgröße orientiert. Breite Lücken zwischen Abschnitten auf derselben Basislinie werden zu Leerzeichen; große Lücken zwischen Basislinien werden zu Leerzeilen. Das Ergebnis liest sich so, wie die Seite aussieht
var
Grid: UnicodeString;
begin
if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
TFile.WriteAllText('page1.txt', Grid, TEncoding.UTF8);
// Spalten, Einrückungen und Tabellenausrichtungen bleiben als
// Leerzeichen und Leerzeilen auf einem Zeichenraster erhalten
end;
Die beiden Modi teilen sich die gesamte Dekodierungslogik und unterscheiden sich nur darin, wie sie die dekodierten Glyphen anordnen, sodass die Entscheidung keine Qualitätsverluste verursacht. Wählen Sie ExtractLoadedPageText, wenn nur die Wörter zählen, und ExtractLoadedPageTextLayout, wenn es auf die Anordnung ankommt. Die Erkennung der Lesereihenfolge bei Mehrspaltigkeit liegt außerhalb des Leistungsumfangs beider Methoden — ein Raster-Rendering einer zweispaltigen Seite zeigt Ihnen beide Spalten nebeneinander, was für Codevergleiche genau richtig, für das Lesen von Fließtext jedoch ungeeignet ist
Wie dekodiert HotPDF Zeichencodes in Unicode?
Die HotPDF-Komponente löst jeden Zeichencode über eine prioritätsgesteuerte Fallback-Kette auf: Zuerst die im Font eingebettete /ToUnicode-CMap, dann der Eintrag /Encoding (Stream oder benannte CMap), dann — bei zusammengesetzten Schriften — die Adobe-Standard-CMap-Dateien für Zeichensammlungen wie Adobe-GB1, Adobe-CNS1, Adobe-Japan1 und Adobe-KR, und schließlich die integrierten WinAnsi- und MacRoman-Tabellen für einfache Schriften. Eine Strategie, die kein Ergebnis liefert, geht geräuschlos zur nächsten über, anstatt abzubrechen. Ein Code, der die gesamte Kette durchläuft, wird als 0 aufgelöst, sodass der Aufrufer Fehlversuche zählen kann, anstatt raten zu müssen
Die /ToUnicode-CMap (ISO 32000-1 §9.10.3) steht an erster Stelle, da es sich um die Zuordnung handelt, die das Erstellungsprogramm speziell für die Extraktion geschrieben hat. Der Pfad über die Adobe-Standard-CMap ist wichtig für CJK-Dokumente, die vordefinierte CMaps wie UniGB-UTF16-H verwenden, anstatt eigene CMaps einzubetten: HotPDF liefert diese Sammlungsdateien im Verzeichnis resources\CMap mit, lokalisiert sie zur Laufzeit relativ zur ausführbaren Datei und cacht jede geparste Map pro Prozess. Dies ist erwähnenswert, da die größte davon, die Adobe-GB1-Map, etwa 2 MB Quelltext umfasst, die man nicht pro Seite neu parsen möchte. Fehlt das Verzeichnis, überspringt der Decoder die dateibasierten CMaps und arbeitet mit den eingebetteten Tabellen sowie den integrierten Encodings. Dies ist das leseseitige Spiegelbild des Formatierungsproblems, das unter Textgestaltung für komplexe Skripte mit HotPDF behandelt wird, wo dieselbe Unterscheidung zwischen Code und Glyphe beim Schreiben auftritt
Zwei CMap-Syntaxfallen, die man kennen sollte
CMap-Dateien wirken einfach zu parsen, sind es aber nicht. Zwei Details sind für die meisten Fehler beim ersten Parser-Entwurf verantwortlich. Das erste ist, dass die Datensatzanzahl vor dem Abschnittsschlüsselwort steht: Ein Abschnitt lautet 2 beginbfchar und nicht beginbfchar 2. Ein Parser, der die Anzahl nach dem Schlüsselwort erwartet, interpretiert die Zahl als unzugeordnetes Token und findet in jedem Abschnitt null Einträge. Der robustere Ansatz — für den sich der Reader von HotPDF entschieden hat — besteht darin, die Anzahl vollständig zu ignorieren und die Schleife bis zum passenden Schlüsselwort endbfchar / endbfrange fortzusetzen. Dies hat den Vorteil, dass auch Dateien toleriert werden, deren Zählerwerte schlicht fehlerhaft sind
Die zweite Falle besteht darin, dass die Ziele von `bfchar` und `bfrange` UTF-16BE-Strings und keine Ganzzahlen sind
Das Ziel <D83DDE00> steht für U+1F600 — ein Surrogatpaar, das zu einem einzigen Codepunkt zusammengeführt werden muss. Liest man diese vier Bytes als Big-Endian-Ganzzahl, führt dies bei jedem Codepunkt außerhalb der Basic Multilingual Plane zu einem sinnlosen Wert. Emojis in PDFs sind nicht mehr exotisch. Ein Decoder, der die Rekombination von Surrogatpaaren überspringt, schlägt bei Dateien fehl, die Ihre Benutzer tatsächlich verwenden. HotPDF parst das Hex-Literal zuerst in Rohdaten-Bytes und rekombiniert dann die UTF-16BE-Codeeinheiten. Dies deckt auch die aus mehreren Zeichen bestehenden Ziele ab, die Ligatur-Mappings erzeugen
Herabsteigen auf Glyphen-Ebene mit ExtractLoadedPageGlyphs
Beide Textaufrufe basieren auf ExtractLoadedPageGlyphs, und das zugrunde liegende THPDFGlyphArray steht auch Ihrem Code zur Verfügung. Jeder THPDFGlyphRecord enthält den aufgelösten Unicode-Codepunkt zusammen mit dem rohen Zeichencode, der Byte-Breite des Codes (1, 2 oder 4, bestimmt durch die codespacerange der CMap), der aktiven Schriftressourcen-ID und -Größe, dem X- und Y-Ursprung im Benutzerbereich sowie der horizontalen Schrittweite. Das reicht aus, um eine Wortgrenzenerkennung, eine positionsgenaue Hervorhebung oder einen eigenen Layout-Algorithmus zu erstellen, ohne den Inhaltsstrom selbst analysieren zu müssen
var
Glyphs: THPDFGlyphArray;
I, Unresolved: Integer;
begin
if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
begin
Unresolved := 0;
for I := 0 to High(Glyphs) do
if Glyphs[I].Unicode = 0 then
Inc(Unresolved);
if Unresolved > 0 then
ShowMessageFmt('%d of %d glyphs have no Unicode mapping',
[Unresolved, Length(Glyphs)]);
end;
end;
Das Zählen von Datensätzen mit Unicode = 0, wie oben gezeigt, ist die ehrlichste Methode, um die Extraktionsqualität für ein bestimmtes Dokument zu messen, bevor Sie den Text weiterverarbeiten. Die Glyphen-Datensätze verknüpfen zudem jedes Zeichen mit dem Quell-Operanden im Inhaltsstrom, was die Textsuche und das Ersetzen in geladenen Dokumenten von HotPDF auf demselben Fundament ermöglicht
Welche PDFs geben ihren Text nicht preis?
Einige Dateien entziehen sich jedem Extraktionsversuch, und es ist besser, diese zu erkennen, als ihr Ergebnis weiterzuverarbeiten. Gescannte Dokumente sind der offensichtlichste Fall: Eine Seite, die aus einem einzigen großen Bild besteht, enthält keinerlei Textoperatoren, sodass die Extraktion korrekterweise eine leere Zeichenkette zurückgibt — die Lösung hierfür ist OCR, und das Extrahieren der Seitenbilder aus dem geladenen PDF ist der erste Schritt in dieser Pipeline. Subset-Schriften ohne /ToUnicode-Tabelle sind der schwierigere Fall: Wenn auch der Pfad /Encoding und die Standard-CMaps leer bleiben, werden diese Glyphen als 0 aufgelöst und erscheinen in den Textaufrufen als Leerzeichen. Verschlüsselte Dokumente lassen sich normal extrahieren, vorausgesetzt, Sie laden sie mit ihrem Passwort über die Überladung von LoadFromFile, sodass die Streams entschlüsselt werden, bevor der Interpreter sie verarbeitet
Eine engere Grenze sei klar benannt: Die Dekodierungskette liest CMap- und Inhaltsströme über den Flate-Pfad von HotPDF. Ein Font, dessen ToUnicode-Stream einen ungewöhnlichen Filter verwendet, fällt daher auf die nächste Strategie zurück, anstatt die Seite abbrechen zu lassen. In der Praxis deckt FlateDecode fast alles ab, was in den letzten zwei Jahrzehnten erstellt wurde, und der Rückfall erfolgt absichtlich geräuschlos — Sie erhalten den bestmöglichen Text, den die Datei zulässt, anstatt einer Ausnahme. Dieselbe leseseitige Objektlogik, die hier Schriftverzeichnisse auflöst, treibt auch das Bearbeiten von Metadaten in geladenen Dokumenten an, sodass eine Dokumenten-Pipeline in einem einzigen Durchgang extrahieren, prüfen und annotieren kann
Textextraktion, layouttreues Rendern, Zugriff auf Glyphenebene sowie die darauf aufbauenden Such- und Ersetzungsfunktionen sind alle Teil der standardmäßigen HotPDF Component für Delphi und C++Builder — ohne externe DLLs, ohne Betriebssystem-Textdienste, einfach reines Object Pascal, das Sie im Debugger durchlaufen können, wenn eine ungewöhnliche Datei in Ihrer Warteschlange landet