PDFium Component fügt gescannten PDF-Seiten von Delphi aus über ApplyOcrSearchLayer eine durchsuchbare Textebene hinzu. Es rendert jede ausgewählte Seite, übergibt die Pixel an einen selbst bereitgestellten OCR-Provider und schreibt die erkannten Wörter als unsichtbare Textobjekte zurück, positioniert über den Wörtern im Scan. Das ursprüngliche Seitenbild wird dabei nie dekodiert, neu kodiert oder ersetzt, sodass das visuelle Ergebnis byteidentisch der Ausgangsseite entspricht
Die Erkennungs-Engine ist bewusst nicht Teil der Bibliothek. PDFium stellt Seitenrendering, Koordinatenabbildung, Schriftladen, Textobjekterstellung und unsichtbare Rendermodi bereit, enthält aber keine OCR-Engine, und das Gegenteil vorzutäuschen würde bedeuten, das Erkennungsprodukt eines Dritten in eine PDF-Komponente zu bündeln. Stattdessen lebt die Erkennung hinter der Schnittstelle IPdfOcrProvider: Die Bibliothek übergibt BGRA-Pixel mit festem Layout und Ursprung oben, und der Provider gibt Unicode-Text, Konfidenzwerte und Wort-Vierecke zurück
Was genau ist eine durchsuchbare Textebene?
Ein gescanntes PDF ist ein Bild eines Dokuments. Der Seiteninhalt ist ein einziges großes Bild, und es gibt nichts zu markieren, zu durchsuchen, zu kopieren oder zu indizieren. Eine durchsuchbare Textebene fügt echte Textobjekte über diesem Bild hinzu, mit auf unsichtbar gesetztem Rendermodus, sodass Viewer nichts zeichnen, während Markierung, Suche und Extraktion die Wörter genau dort finden, wo sie erscheinen
Positionierung ist das Ganze. Sitzt der unsichtbare Text nur wenige Punkte daneben, landen Markierungs-Hervorhebungen neben statt auf den Wörtern, und das Kopieren eines Absatzes erzeugt Text in falscher Reihenfolge. Deshalb muss die Geometrie aus denselben Transformationen stammen, die PDFium zum Rendern der Seite verwendet, statt aus einer proportionalen Schätzung
Den Provider implementieren
Der Provider-Vertrag besteht aus einer Methode. Sie erhält einen Seitenbild-Datensatz mit Abmessungen, Stride, DPI, Pixelformat und den Pixel-Bytes selbst, dazu ein Abbruch-Token, und gibt Wörter oder eine Fehlermeldung zurück:
uses
PDFium;
type
TMyOcrProvider = class(TInterfacedObject, IPdfOcrProvider)
public
function RecognizePage(const Image: TPdfOcrImage;
const CancellationToken: IPdfCancellationToken;
out Words: TPdfOcrWords; out ErrorMessage: string): Boolean;
end;
function TMyOcrProvider.RecognizePage(const Image: TPdfOcrImage;
const CancellationToken: IPdfCancellationToken;
out Words: TPdfOcrWords; out ErrorMessage: string): Boolean;
var
I: Integer;
begin
// Image.Pixels enthält BGRA-Zeilen mit Ursprung oben und Image.Stride Bytes.
// An die eigene Engine übergeben, dann einen Eintrag pro erkanntem Wort füllen
SetLength(Words, RecognisedCount);
for I := 0 to RecognisedCount - 1 do
begin
Words[I].Text := EngineWordText(I);
Words[I].Confidence := EngineWordConfidence(I); // 0..1
Words[I].Quad := TPdfOcrQuad.FromRectangle(
EngineLeft(I), EngineTop(I), EngineRight(I), EngineBottom(I));
end;
ErrorMessage := '';
Result := True;
end;
Vierecke statt Rechtecke, weil ein Scan selten seitengerecht ausgerichtet ist. Ein Wort auf einer leicht gedrehten Seite belegt ein Parallelogramm, und TPdfOcrQuad trägt vier Eckpunkte, sodass schräge und gedrehte Wörter eine genaue Auswahlregion behalten. Engines, die nur achsenparallele Rechtecke melden, können FromRectangle nutzen, das das entartete Viereck aufbaut
Warum lassen sich Wortpositionen nicht proportional skalieren?
Es ist verlockend, eine Pixelkoordinate in eine Seitenkoordinate umzurechnen, indem man durch die Renderbreite teilt und mit der Seitenbreite multipliziert. Das funktioniert nur bei Seiten ohne Rotation, mit einer CropBox identisch zur MediaBox und einem Ursprung bei null, und zahlreiche gescannte Dokumente verfehlen mindestens eine dieser Bedingungen
PDFium Component bildet jede der vier Viereck-Ecken einzeln über FPDF_DeviceToPage ab, dieselbe Abbildung, die der Renderer zur Erzeugung der Pixel verwendet hat, sodass /Rotate-Einträge und versetzte Crop-Boxen konstruktionsbedingt korrekt behandelt werden. Die affine Matrix für das Textobjekt wird dann aus drei der abgebildeten Punkte gebaut, der unteren linken, unteren rechten und oberen linken Ecke, was genau ausreicht, um Position, Skalierung, Rotation und Scherung auszudrücken
Das Textobjekt selbst wird mit Einheitsschriftgröße erstellt, damit seine tatsächlichen Schriftgrenzen gemessen werden können, und die gemessenen Objektgrenzen werden anschließend auf das Zielviereck abgebildet. Die Größe anhand einer geschätzten Punktgröße festzulegen und zu hoffen, dass sie zum gescannten Wort passt, würde mit jedem Schriftaustausch driften; erst zu messen macht die Passform unabhängig davon, welche Schrift die Ebene verwendet
Über ein Dokument laufen lassen
Der Options-Datensatz steuert Auflösung, Filterung und jedes Budget. Die Konfidenzfilterung ist wichtiger, als sie wirkt: Datenmüll-Wörter mit niedriger Konfidenz verschmutzen Suchergebnisse dauerhaft, und anders als bei einem falschen Rendering bemerkt es niemand, bis eine Suche Unsinn zurückliefert:
var
Pdf: TPdf;
Options: TPdfOcrOptions;
Report: TPdfOcrReport;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'scanned-contract.pdf';
Pdf.LoadDocument;
Options := TPdfOcrOptions.Default;
Options.Dpi := 300; // Erkennungsauflösung
Options.MinConfidence := 0.60; // unsichere Wörter verwerfen
Options.SkipPagesWithText := True; // born-digital Seiten unangetastet lassen
Options.ContinueOnError := True; // eine fehlerhafte Seite darf den Job nicht stoppen
Options.MaxPixelsPerPage := 40 * 1000 * 1000;
if Pdf.ApplyOcrSearchLayer(TMyOcrProvider.Create, Options, Report) then
Pdf.SaveAs('scanned-contract-searchable.pdf');
for I := 0 to High(Report.Pages) do
if Report.Pages[I].Status = popsFailed then
Writeln(Format('page %d failed: %s',
[Report.Pages[I].PageNumber, Report.Pages[I].ErrorMessage]));
Writeln(Format('%d word(s) inserted, %d rejected, %d page(s) skipped',
[Report.InsertedWordCount, Report.RejectedWordCount,
Report.SkippedPageCount]));
finally
Pdf.Free;
end;
end;
SkipPagesWithText verdient in gemischten Archiven besondere Beachtung. Ein PDF, das bereits echten Text trägt, ob born-digital oder zuvor verarbeitet, bekommt eine zweite Textebene, wenn man OCR blind darüber laufen lässt, und das Duplikat lässt die Extraktion jedes Wort zweimal liefern. Der Seitenstatus popsSkippedExistingText zeigt genau, welche Seiten unangetastet blieben
Budgets, Abbruch und Fehlereindämmung
Jede Größe, die ein feindseliges oder schlicht riesiges Dokument aufblähen kann, hat eine Obergrenze: Pixel pro Seite und insgesamt, Wörter pro Seite und insgesamt sowie Zeichen pro Wort. Alle werden vor dem Schreiben der Seite geprüft, nicht danach, und die Pixelschätzung wird aus Seitenabmessungen und DPI berechnet, bevor überhaupt eine Bitmap zugewiesen wird. Eine Erhöhung der DPI von 150 auf 300 vervierfacht den Speicher pro Seite, daher ist die Obergrenze pro Seite der Parameter, der zuerst angepasst werden sollte, wenn ein Batch-Job bei großen Formaten zu scheitern beginnt
Das Abbruch-Token zieht sich durch den gesamten Pfad: fortschreitendes Rendering, den Provider-Aufruf und die Einfügeschleife pro Wort. Das bedeutet, ein Nutzer, der während der Erkennung einer 400-seitigen Datei abbricht, stoppt innerhalb einer Seite statt am Ende des Dokuments, und dasselbe an anderer Stelle in der Komponente verwendete Token-Muster, beschrieben in abbrechbarem fortschreitendem Rendering, gilt hier unverändert
Fehlereindämmung erfolgt pro Seite. Die Bibliothek sammelt die auf einer Seite eingefügten Objekt-Handles und ruft FPDFPage_GenerateContent einmal auf, nachdem alle Wörter platziert wurden. Schlägt mittendrin etwas fehl, ob ein Provider-Fehler oder ein Schriftproblem, werden die auf dieser Seite eingefügten Objekte in umgekehrter Reihenfolge entfernt und der Seiteninhalt neu erzeugt, sodass eine fehlgeschlagene Seite in ihren ursprünglichen Zustand zurückkehrt, statt eine halbe Textebene zu behalten. Die Dokumentschleife läuft dann gemäß ContinueOnError weiter oder stoppt, und die aktive Seite wird immer wiederhergestellt
Prüfen, ob das Bild wirklich unangetastet blieb
Die stärkste verfügbare Prüfung ist zugleich die einfachste: die Seite vor und nach dem Anwenden der Ebene in derselben Größe rendern und die Bitmaps vergleichen. Sie sollten byteidentisch sein, weil unsichtbarer Text nichts zeichnet und der Bildstream nie dekodiert wurde. Jede Abweichung bedeutet, dass etwas anderes als die Textebene die Seite verändert hat
Danach die Textseite prüfen, indem man aus der verarbeiteten Datei extrahiert und bestätigt, dass Wortpositionen auf dem Scan landen. Der Extraktionspfad ist derselbe, der in Text aus PDF-Dokumenten extrahieren beschrieben wird, und für eine schnelle visuelle Prüfung der Ausrichtung erlaubt das Rendern von Seiten zu Bildern wie in PDF-Seiten in JPEG umwandeln, Wort-Boxen über dem Scan zu überlagern
OCR-Ebenen, Rendering, Extraktion und Bearbeitung laufen alle gegen dasselbe Dokumentobjekt in Delphi, C++Builder und Lazarus; die vollständige API-Oberfläche wird auf der PDFium-Component-für-Delphi-Seite beschrieben