Die Text-Extraktion aus PDFs sieht einfach aus, bis Sie auf ein Dokument stoßen, bei dem die Textebene fehlt, beschädigt ist oder in Dutzende winziger Zeichenfolgen (Character Runs) ohne sinnvolle Reihenfolge zerlegt wurde. Die PDFium-Komponente bietet Ihnen zwei Einstiegspunkte: das Array Character[] für den rohen, indexbasierten Zugriff auf jede Glyphe einer Seite und ReadablePageContent für eine strukturierte Ansicht, die Absätze und Überschriften aus dem Tag-Baum oder der heuristischen Analyse des PDFs rekonstruiert. Keines von beiden ist immer die richtige Wahl, daher ist es wichtig zu verstehen, was die jeweiligen Methoden offenlegen
Öffnen des Dokuments und die Falle des lautlosen Fehlschlagens
TPdf öffnet eine Datei, indem FileName gesetzt und Active := True umgeschaltet wird. Das entscheidende Detail: Active := True löst niemals eine Exception aus. Wenn die Datei fehlt, passwortgeschützt oder beschädigt ist, fängt PDFium den Fehler intern ab, und Active bleibt einfach auf False. Das bedeutet, dass sich jede Extraktionsschleife dagegen absichern muss:
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.Active := True;
if not Pdf.Active then
begin
ShowMessage('Could not open PDF (damaged or wrong password)');
Exit;
end;
// extraction follows here
finally
Pdf.Active := False;
Pdf.Free;
end;
Passwortgeschützte Dateien erfordern das Setzen von Pdf.Password := '...' vor dem Aktivieren über Active := True. Es gibt keine zweite Chance: Wenn Active fehlschlägt, müssen Sie das Dokument schließen und mit dem korrekten Passwort erneut öffnen
Seitenweise Extraktion mit Character[]
Der einfachste Ansatz durchläuft jedes Zeichen auf jeder Seite. Setzen Sie Pdf.PageNumber, um die Textebene für diese Seite zu laden, und durchlaufen Sie dann die CharacterCount-Einträge mithilfe der Eigenschaft Character[]. Zwei Flags pro Eintrag sind dabei beachtenswert: CharacterGenerated[i] markiert synthetische Glyphen, die vom Renderer eingefügt wurden (z. B. weiche Trennstriche bei Zeilenumbrüchen), die keinen echten Unicode-Wert besitzen, und CharacterMapError[i] signalisiert, dass PDFium die Glyphe nicht auf einen Code-Point abbilden konnte, was bei Schrifttyp-Kodierungen ohne ToUnicode-Tabelle vorkommt
procedure ExtractAllText(Pdf: TPdf; Output: TStrings);
var
Page, I: Integer;
Line: string;
Ch: WideChar;
begin
for Page := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := Page;
Line := '';
for I := 0 to Pdf.CharacterCount - 1 do
begin
if Pdf.CharacterGenerated[I] or Pdf.CharacterMapError[I] then
Continue;
Ch := Pdf.Character[I];
if Ch = #13 then
Ch := #10; // normalize CR to LF
Line := Line + Ch;
end;
Output.Add(Line);
end;
end;
Das Ergebnis ist eine flache Zeichenfolge von Unicode-Codepunkten in der Reihenfolge, in der PDFium sie auflistet. Dies entspricht der Reihenfolge, in der sie im Content-Stream vorkommen, und nicht zwingend der Leserichtung von links nach rechts. Für die meisten Dokumente mit lateinischen Schriftzeichen, die mit Standard-Büroanwendungen erstellt wurden, ist dies in Ordnung. Bei gescannten PDFs, die per OCR mit ungewöhnlichen Glyphenfolgen verarbeitet wurden, oder bei Texten mit Leserichtung von rechts nach links kann die Reihenfolge falsch sein. In solchen Fällen ist ReadablePageContent nützlicher
Strukturierte Extraktion mit ReadablePageContent
ReadablePageContent geht eine Ebene höher: Es gibt einen TPdfReadableContent-Record zurück, dessen Array Fragments strukturierte Inhaltsfragmente enthält. Jedes dieser Fragmente besitzt ein Kind, das Absätze, Überschriften, Listenelemente, Tabellenzellen usw. kennzeichnet. Wenn das PDF einen Strukturbaum enthält (prüfbar über Pdf.IsTagged), ist die Quelle rosStructure und die Lesereihenfolge verbindlich. Bei ungetaggten Dateien greift PDFium auf rosHeuristic zurück, das Zeichen anhand ihrer Bounding-Boxes in plausible Leseeinheiten gruppiert, jedoch keine Genauigkeit garantieren kann:
procedure ExtractStructured(Pdf: TPdf; Output: TStrings);
var
Page: Integer;
Content: TPdfReadableContent;
Fragment: TPdfContentFragment;
begin
for Page := 1 to Pdf.PageCount do
begin
Content := Pdf.ReadablePageContent(Page);
for Fragment in Content.Fragments do
begin
case Fragment.Kind of
cfHeading : Output.Add('# ' + Fragment.Text);
cfParagraph : Output.Add(Fragment.Text);
cfListItem : Output.Add('- ' + Fragment.Text);
else
Output.Add(Fragment.Text);
end;
end;
end;
end;
Wenn Content.Source = rosHeuristic gilt und Ihre Ausgabe unleserlich erscheint, wurde die Textebene des Dokuments wahrscheinlich nicht mit Blick auf die Lesereihenfolge geschrieben. In diesem Fall besteht die einzige zuverlässige Lösung darin, das Dokument aus der Quellanwendung mit ordnungsgemäßem Tagging neu zu exportieren oder einen Nachbearbeitungsschritt auszuführen, der die Zeichenursprünge zuerst nach Y und dann nach X sortiert
Was CharacterOrigin und CharacterRectangle bieten
Beide Eigenschaften geben die Position eines Zeichens im Seitenbereich zurück (in Punkten, Ursprung in der linken unteren Ecke, Y nimmt nach oben zu). CharacterOrigin[i] ist der Grundlinien-Ankerpunkt der Glyphe; CharacterRectangle[i] ist die vollständige Bounding-Box. Dies sind die Bausteine für alles, was über einfachen Text hinausgeht: das Erkennen von Spaltengrenzen, das Gruppieren von Zeichen in Zeilen durch Vergleichen der Y-Koordinaten innerhalb einer Toleranz oder das Erstellen einer Hit-Test-Map für die Textauswahl in einem Viewer. Wenn Sie herausfinden müssen, welches Zeichen unter einem Mausklick liegt, CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) diese Suche direkt, ohne dass Sie Bounding-Boxes manuell durchlaufen müssen
Bereitstellen der DLL
Die PDFium-Komponente delegiert das gesamte Parsen von PDFs an eine native DLL, entweder pdfium32.dll oder pdfium64.dll, abhängig von Ihrer Zielplattform. Die Komponente enthält ein CopyDlls.bat-Skript, das die richtige Datei in das Windows-Systemverzeichnis kopiert. Es reicht aus, dieses einmalig als Administrator auf einem Entwicklungsrechner auszuführen; für die Bereitstellung kopieren Sie die DLL stattdessen in das Verzeichnis der ausführbaren Anwendungsdatei. Die V8-fähigen Varianten (pdfium32v8.dll, pdfium64v8.dll) are erheblich größer und nur erforderlich, wenn Ihre PDFs JavaScript enthalten, das ausgeführt werden muss. Für die reine Text-Extraktion ist die Standard-Build die richtige Wahl
Wenn die DLL zur Laufzeit fehlt, schlägt Active := True lautlos fehl – genau wie bei einer fehlenden Datei –, da die Komponente den Ladefehler intern abfängt. Testen Sie immer auf einem sauberen System, bevor Sie die Anwendung ausliefern
Verwenden von FontSize[] neben Character[] für die Layoutanalyse
Über den einfachen Text hinaus legt die Zeichen-API FontSize[i] offen, was die gerenderte Punktgröße jeder Glyphe zurückgibt. In Kombination mit CharacterOrigin[i] und CharacterRectangle[i] können Sie so Fließtext von Überschriften unterscheiden, ohne sich auf den Strukturbaum stützen zu müssen. Ein Zeichenbereich, in dem die Schriftgröße über einen Schwellenwert springt, ist in einem ungetaggten Dokument fast sicher eine Überschrift. Dieselbe Technik eignet sich zum Erkennen von Bildunterschriften (kleiner Text unter der Bounding-Box eines Bildes) oder Fußnoten (kleiner Text nahe dem unteren Seitenrand). Nichts davon erfordert ein Rendern; alle drei Eigenschaften lesen direkt aus der Textebene, die PDFium während Active := True aufbaut
Eine Nuance: FontSize[i] spiegelt die Größe nach Anwendung der CTM (Current Transformation Matrix) der Seite wider. Ein Dokument, in dem der Autor die gesamte Seite skaliert hat, meldet daher proportional angepasste Größen. Wenn Sie Größen über Seiten mit unterschiedlichen Abmessungen hinweg vergleichen, normalisieren Sie diese mit der MediaBox-Höhe jeder Seite, bevor Sie Schwellenwert-Entscheidungen treffen
Schreiben der Ausgabe in eine Datei
Die Delphi-Klasse TStringList verarbeitet die UTF-8-Ausgabe seit Version XE sauber. Setzen Sie WriteBOM := False, wenn Sie eine BOM-freie Datei benötigen (viele nachgelagerte Parser stolpern über ein führendes BOM):
var
Lines: TStringList;
begin
Lines := TStringList.Create;
try
ExtractAllText(Pdf, Lines);
Lines.WriteBOM := False;
Lines.SaveToFile('output.txt', TEncoding.UTF8);
finally
Lines.Free;
end;
end;
Bei sehr großen Dokumenten, bei denen der Speicherbedarf eine Rolle spielt, schreiben Sie innerhalb der Seitenschleife direkt in einen TStreamWriter mit TEncoding.UTF8, anstatt zuerst alles in einer Liste zu sammeln
Die hier gezeigten APIs Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent und CharacterIndexAtPos sind Teil der PDFium-Komponente für Delphi und C++Builder