Technischer Artikel

HotPDF Tesseract-DLL-OCR: Die C-API aus Delphi aufrufen

HotPDF fährt Tesseract innerhalb Ihres Delphi-Prozesses über HPDFCreateTesseractDLLOCREngine, eine in v2.772.0 hinzugekommene Factory, die dynamisch eine Tesseract-5-kompatible DLL lädt, ihre C-API antreibt (TessBaseAPIInit2, TessBaseAPIRecognize, den Result-Iterator) und ein IHPDFOCREngine zurückgibt. THotPDF.ApplyLoadedOCRTextLayer nutzt diese Engine, um gescannten PDF-Seiten eine unsichtbare, durchsuchbare Unicode-Textebene hinzuzufügen

Derselbe Recognizer war bereits über den externen tesseract.exe-Adapter, der ein BMP schreibt und TSV parst, erreichbar. Dieser Pfad funktioniert, aber jede Seite zahlt für einen Prozessstart, eine temporäre Bitmap-Datei und ein Textformat ohne Baselines und ohne Kontrolle über die Seitensegmentierung. Der DLL-Aufruf nimmt alle drei Kosten. Er nimmt auch die Prozesswand, was bedeutet, dass eine Pascal-Bindung direkt auf C-Strukturen, C-Booleans und C-allokierten Strings sitzt. Der größte Teil dessen, was an diesem Adapter wissenswert ist, betrifft die Stellen, an denen diese Bindung stillschweigend schiefgehen kann

Wie fährt man Tesseract in-process aus Delphi mit HotPDF?

Tesseract in-process mit HotPDF zu fahren braucht einen Factory-Aufruf in der Unit HPDFTesseractRecognition und denselben ApplyLoadedOCRTextLayer-Aufruf, den jede HotPDF-OCR-Engine benutzt. Die Factory validiert eifrig. Die DLL-Datei und das tessdata-Verzeichnis müssen existieren, der Sprachbezeichner darf nur ASCII-Buchstaben, Ziffern, _ und + enthalten, jedes Modell in einer Kombination wie chi_sim+eng muss eine passende .traineddata-Datei haben, und alle 21 benötigten Exporte müssen auflösbar sein, bevor die Engine zurückgegeben wird. Konfigurationsfehler werfen EArgumentException; eine DLL, die nicht lädt, wirft EOSError mit dem Windows-Fehlercode und dem Hinweis, Architektur und Abhängigkeiten zu prüfen

uses
  SysUtils, HPDFDoc, HPDFTesseractRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // Eine Win64-Anwendung braucht eine 64-Bit-DLL; Abhängigkeits-DLLs daneben
  Engine := HPDFCreateTesseractDLLOCREngine('C:\OCR\Win64\libtesseract-5.dll',
    'C:\OCR\tessdata', 'chi_sim+eng');   // THPDFTesseractOptions.Default
  Doc := THotPDF.Create(nil);
  try
    Doc.AutoLaunch := False;
    if Doc.LoadFromFile(SourceFile) < 1 then
      raise Exception.Create('Cannot load ' + SourceFile);
    Options := THPDFOCRTextLayerOptions.Default;   // 300 DPI, MinimumConfidence 0.5
    // Eine leere Seitenliste bedeutet jede Seite; Seiten mit vorhandenem Text werden übersprungen
    if not Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
      raise Exception.Create(string(Info.Diagnostic));
    Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
      ' words accepted, ', Info.DroppedWordCount, ' dropped');
    Doc.SaveLoadedDocument(TargetFile);
  finally
    Doc.Free;
  end;
end;

THPDFTesseractOptions.Default setzt PageSegMode auf tpsAuto, EngineMode auf temDefault, TimeoutMilliseconds auf 60.000 und MaxPixels auf 16.777.216. Das Pixelbudget zählt mehr, als es aussieht. Eine US-Letter-Seite bei den Default-300 DPI rendert zu 2.550 × 3.300 Pixeln, rund 8,4 Millionen, das passt. Dieselbe Seite bei 600 DPI ist 5.100 × 6.600, rund 33,7 Millionen, und der Adapter weist sie ab, bevor Tesseract ein Pixel sieht. Heben Sie MaxPixels an (die Obergrenze ist 67.108.864) oder lassen Sie die DPI, wo sie ist; jede Seite ist zudem auf 32.767 Pixel begrenzt

Die DLL wird mit LoadLibraryEx geladen, mit den Suchflags für den eigenen Ordner der DLL plus die Standard-Safe-Verzeichnisse, die Bildbibliotheken, von denen Tesseract abhängt, können also neben ihr leben, ohne PATH oder das aktuelle Verzeichnis anzufassen. HotPDF bündelt oder lädt keinerlei OCR-Runtime oder Modell herunter; beides stellen Sie bereit

Was ändert sich gegenüber dem tesseract.exe-Adapter?

Der DLL-Adapter tauscht Prozessisolation gegen reichhaltigere Ausgabe und weniger Overhead pro Seite. Beide Adapter docken an dieselbe Textebenen-Pipeline an, Koordinatenmapping, Confidence-Filterung und der Alles-oder-Nichts-Commit sind also identisch; unterschiedlich ist, wie Pixel hinein- und Wörter herauskommen

Aspekttesseract.exe-AdapterTesseract-DLL-Adapter
FactoryHPDFCreateTesseractOCREngineHPDFCreateTesseractDLLOCREngine
Pixel hineinBMP-Datei in einem privaten temporären Verzeichnis8-Bit-Graustufenpuffer im Speicher
Wörter herausWortlevel-TSV, begrenzt auf 64 MiBResult-Iterator, UTF-8 pro Wort
BaselinesNicht verfügbarDurchgereicht aus TessPageIteratorBaseline
Seitensegmentierung und Engine-ModusNur automatische SegmentierungTHPDFTesseractPageSegMode, THPDFTesseractEngineMode
TimeoutHart: Der Kindprozess wird terminiertKooperativ: Tesseract muss es bemerken
Absturz- und SpeicherisolationGetrennter ProzessKeine, teilt Ihren Adressraum

Ein Kostenposten verschwindet nicht. Jeder Recognize-Aufruf erzeugt seine eigene API-Instanz und ruft TessBaseAPIInit2 auf, die Sprachmodelle werden also pro Seite initialisiert statt einmal pro Engine. Der Dateicache des Betriebssystems mildert das Nachladen, aber bei großen mehrsprachigen Modellsets bleibt es der dominierende Fixkostenposten pro Seite, und er zählt gegen die Erkennungsfrist. Die In-Process-RapidOCR-DLL-Engine nimmt das entgegengesetzte Design und hält ihre ONNX-Modelle für die Lebensdauer der Engine resident; die Grenzprobleme (C-ABI, geliehene Puffer, ununterbrechbare native Arbeit) sind dieselbe Familie

Warum kann Delphi die Tesseract-Monitor-Struktur nicht kopieren?

Delphi kann den Tesseract-Fortschrittsmonitor nicht sicher spiegeln, denn ETEXT_DESC enthält versionsabhängige interne Felder, ein von Hand kopierter Record legt den Cancel-Callback und die Frist auf manchen Builds also an die falschen Offsets. Nichts scheitert laut, wenn das passiert. Tesseract liest Ihren Callback-Zeiger schlicht aus einem Feld, das inzwischen etwas anderes hält, oder sieht die Frist überhaupt nie

HotPDF behandelt den Monitor deshalb als opaken Zeiger und fasst ihn nur über exportierte Funktionen an: TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc, TessMonitorSetDeadlineMSecs und TessMonitorDelete. Binden Sie die C-API selbst für einen anderen Zweck, gilt dasselbe Muster. Die Skizze unten ist Ihr eigener Bindungscode, keine HotPDF-API, und spiegelt die Deklarationen, die HotPDF intern nutzt

HotPDF-Tesseract-DLL-Monitor-Behandlung: Den versionsabhängigen ETEXT_DESC-Record zu kopieren legt den Cancel-Callback und die Frist auf falsche Offsets und scheitert still, während HotPDF den Monitor als opak behandelt, TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc und TessMonitorSetDeadlineMSecs antreibt und den cdecl-Callback exception-frei hält
Ein opaker Zeiger plus fünf Exporte ist der ganze Vertrag; der Callback bleibt ein Ein-Byte-Boolean, der nur ein Flag und eine Uhr liest
type
  // C: typedef bool (*TessCancelFunc)(void *cancel_this, int words);
  TTessCancelFunc = function(CancelThis: Pointer; Words: Integer): Boolean; cdecl;
  TTessMonitorCreate = function: Pointer; cdecl;   // ETEXT_DESC*, nie dereferenziert
  TTessMonitorDelete = procedure(Monitor: Pointer); cdecl;
  TTessMonitorSetCancelFunc = procedure(Monitor: Pointer; Func: TTessCancelFunc); cdecl;
  TTessMonitorSetCancelThis = procedure(Monitor, CancelThis: Pointer); cdecl;
  TTessMonitorSetDeadlineMSecs = procedure(Monitor: Pointer; MSecs: Integer); cdecl;
  TTessBaseAPIRecognize = function(Handle, Monitor: Pointer): Integer; cdecl;

  TOCRJob = record
    CancelRequested: Boolean;
    DeadlineTick: UInt64;
  end;
  POCRJob = ^TOCRJob;

function ShouldCancel(CancelThis: Pointer; Words: Integer): Boolean; cdecl;
begin
  // Läuft auf Tesseracts Stack: Flags und die Uhr lesen, niemals werfen
  Result := (CancelThis = nil) or POCRJob(CancelThis)^.CancelRequested or
    (GetTickCount64 >= POCRJob(CancelThis)^.DeadlineTick);
end;

// Verwendung, mit den per GetProcAddress aufgelösten Funktionszeigern:
//   Monitor := MonitorCreate();
//   try
//     MonitorSetCancelThis(Monitor, @Job);
//     MonitorSetCancelFunc(Monitor, ShouldCancel);
//     MonitorSetDeadlineMSecs(Monitor, RemainingMs);
//     RC := BaseAPIRecognize(API, Monitor);
//   finally
//     MonitorDelete(Monitor);
//   end;

Zwei Details in dieser Skizze sind Absicht. Der Callback gibt Boolean zurück, was in Delphi und Free Pascal ein Byte ist und zum C-bool in TessCancelFunc passt. Das vier Byte große Windows-BOOL oder das Delphi-LongBool sieht austauschbar aus und ist es nicht: Schreibt die eine Seite ein einzelnes Byte und liest die andere vier, sind die oberen Bytes des Rückgaberegisters, was auch immer dort lag, und ein False kann als True ankommen. Derselbe Header verkompliziert Dinge zusätzlich, denn Funktionen wie TessPageIteratorBoundingBox geben ein int zurück, das HotPDF als Integer deklariert. Lesen Sie den C-Typ jedes Rückgabewerts, statt eine Konvention für die ganze API anzunehmen

Das zweite Detail ist, dass der Callback nie wirft. Eine Delphi-Exception, die durch Tesseracts C++-Frames unwinde, ist undefiniertes Verhalten, HotPDFs Callback liest also nur das Abbruchtoken und einen monotonen GetTickCount64-Wert. Der Adapter macht aus dem Ergebnis nach der Rückkehr von TessBaseAPIRecognize eine Abbruch- oder Timeout-Diagnose, und er führt diese Prüfung unabhängig vom nativen Rückgabecode durch

Welche nativen Zeiger besitzt die Delphi-Seite?

Der HotPDF-Tesseract-DLL-Adapter besitzt drei native Objekte pro Anfrage — die API-Instanz, den Monitor und den Result-Iterator — und leiht alles andere. Jeder Recognize-Aufruf erzeugt sein eigenes Set und gibt es in einem finally-Block frei: TessResultIteratorDelete, dann TessMonitorDelete, dann TessBaseAPIDelete. Die Engine-Schnittstelle freizugeben entlädt die Bibliothek

HotPDF-Tesseract-DLL-Objektbesitz pro Recognize-Aufruf: Result-Iterator, Monitor und API-Instanz sind im Besitz und werden in dieser Reihenfolge innerhalb von finally freigegeben, der Page-Iterator aus TessResultIteratorGetPageIterator ist eine geliehene Ansicht, die nie freigegeben werden darf, und GetUTF8Text-Strings werden kopiert und über TessDeleteText zurückgegeben
Drei Objekte im Besitz, alles andere geliehen: in der festen Reihenfolge freigeben, den Page-Iterator nie doppelt freigeben und die Allokatoren nie mischen
  • TessResultIteratorGetPageIterator liefert eine geliehene Ansicht in den Result-Iterator, kein neues Objekt. HotPDF nutzt sie für TessPageIteratorBoundingBox und TessPageIteratorBaseline und gibt sie nie frei; sie separat zu löschen würde denselben Speicher zweimal freigeben
  • TessResultIteratorGetUTF8Text liefert einen String, der von der eigenen Runtime der DLL allokiert wurde. HotPDF kopiert ihn und reicht ihn über TessDeleteText in einem finally-Block zurück; Pascal-FreeMem würde ihn auf dem falschen Heap freigeben
  • Worttext wird mit strenger UTF-8-Validierung dekodiert und vor der Konversion längengeprüft. Wörter mit Steuerzeichen, missgebildetem UTF-8, Boxen außerhalb des Bilds, invertierten Rechtecken oder Confidence außerhalb 0–100 lassen die Anfrage scheitern, statt stillschweigend geflickt zu werden
  • Der Gesamttext pro Anfrage ist auf 1.048.576 UTF-16-Code-Einheiten begrenzt, und die Wortanzahl muss ins Anfragebudget passen, das ApplyLoadedOCRTextLayer weiterreicht

Confidence kommt als 0–100 an und wird auf 0–1 skaliert, THPDFOCRTextLayerOptions.MinimumConfidence bedeutet also für jede Engine dasselbe. Meldet Tesseract eine Baseline, werden beide Endpunkte durchgereicht; andernfalls fällt die Textebenen-Pipeline auf ihre geometrische Schätzung zurück, exakt wie bei TSV-Eingabe

Warum ein Enum validieren, bevor er die DLL erreicht?

HotPDF kopiert den rohen Ordinalwert von PageSegMode und EngineMode in einen Integer, bevor es den Bereich prüft, denn ein Compiler darf annehmen, dass eine Enum-Variable stets einen deklarierten Wert hält, und Ord(X) > Ord(High(T)) zu einem konstanten False falten. Die Ordinalwerte sind keine Dekoration: THPDFTesseractPageSegMode folgt Tesseracts Seitensegmentierungsnummerierung von 0 bis 13, THPDFTesseractEngineMode folgt der Engine-Modus-Nummerierung von 0 bis 3, und beide gehen als schlichte Integer an die DLL. Ein mit FillChar gebauter Options-Record, aus einem Stream gefüllt oder aus C++Builder mit gecastetem Integer übergeben, kann ein Byte wie 200 tragen. Den kopierten Ordinalwert zu validieren macht daraus eine EArgumentException zur Factory-Zeit statt eines undefinierten Modus in nativem Code. Die Factory weist zudem tpsOSDOnly und tpsAutoOnly ab, die keine Wörter produzieren, und verlangt osd.traineddata für tpsAutoOSD und tpsSparseTextOSD

Was garantiert die Erkennungs-Timeout tatsächlich?

Das Tesseract-DLL-Timeout ist kooperativ: HotPDF kann seine eigene Arbeit stoppen und Tesseract bitten, zu stoppen, aber es kann nativen Code nicht zur Rückkehr zwingen. Die Uhr startet, wenn Recognize beginnt, Bitmap-Konversion und Modellinitialisierung verbrauchen also dasselbe Budget wie die Erkennung. HotPDF prüft verstrichene Zeit und das Abbruchtoken während der Graustufenkonversion und zwischen Wörtern beim Iterieren der Ergebnisse und reicht die verbleibenden Millisekunden an TessMonitorSetDeadlineMSecs, bevor es TessBaseAPIRecognize aufruft

Die Lücke liegt innerhalb des nativen Aufrufs. Tesseracts Monitor wird während der Wörterkennung konsultiert, nicht während TessBaseAPIInit2 oder der Seitenlayoutanalyse, ein langsames Modellladen oder ein pathologisches Layout kann also über die Frist hinauslaufen, bevor das Timeout gemeldet wird. Pixel- und Ausgabebudgets begrenzen zudem nicht den eigenen Speicherverbrauch der nativen Bibliothek. Brauchen Sie einen Worker, den Sie killen können, nehmen Sie den Prozessadapter; das ist der ehrliche Kompromiss, kein fehlendes Feature

HotPDF-Tesseract-DLL-Kooperativ-Timeout-Anatomie: Die Uhr startet, wenn Recognize beginnt, und deckt Graustufenkonversion, TessBaseAPIInit2 und Layoutanalyse ab, aber der Monitor wird nur während der Wörterkennung konsultiert, Modellladen und Layout können also überlaufen, bevor HotPDF otlsEngineError oder otlsCancelled meldet
Eine Frist hier ist eine Bitte, keine Garantie: Initialisierung und Layoutanalyse können lang laufen, und ein Worker, den Sie wirklich killen können, braucht den Prozessadapter

Die Seitensegmentierung ist der Ort, an dem der DLL-Adapter sich bei schwieriger Eingabe bezahlt macht. Formulare, Labels und gescannte Tabellen mit verstreuten Feldern erkennen mit tpsSparseText oft besser als mit automatischer Segmentierung, die versucht, Spalten und Absätze zusammenzusetzen, die gar nicht da sind

procedure OCRFormPages(Doc: THotPDF; const Pages: array of Integer);
var
  Engine: IHPDFOCREngine;
  TessOptions: THPDFTesseractOptions;
  LayerOptions: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  TessOptions := THPDFTesseractOptions.Default;
  TessOptions.PageSegMode := tpsSparseText;  // verstreute Felder, keine Spaltenzusammensetzung
  TessOptions.EngineMode := temLSTMOnly;     // braucht LSTM-Modelle im tessdata
  TessOptions.TimeoutMilliseconds := 20000;  // eingeschlossen Modellinitialisierung
  Engine := HPDFCreateTesseractDLLOCREngine('C:\OCR\Win64\libtesseract-5.dll',
    'C:\OCR\tessdata', 'eng+deu', TessOptions);

  LayerOptions := THPDFOCRTextLayerOptions.Default;
  LayerOptions.MinimumConfidence := 0.6;
  if not Doc.ApplyLoadedOCRTextLayer(Pages, Engine, LayerOptions, Info) then
    case Info.Status of
      otlsCancelled:
        Writeln('OCR cancelled, document unchanged');
      otlsEngineError:
        Writeln('Tesseract failed or timed out: ', string(Info.Diagnostic));
    else
      Writeln(string(Info.Diagnostic));
    end;
end;

Ein Timeout zeigt sich als otlsEngineError mit der Diagnose Tesseract DLL OCR timed out, ein abgebrochenes Token als otlsCancelled. In beiden Fällen hat ApplyLoadedOCRTextLayer jede gewählte Seite erkannt, bevor es die Commit-Transaktion startet, ein Fehlschlag auf Seite 40 von 50 lässt das geladene Dokument also exakt so, wie es war. Beachten Sie, dass tpsSingleLine, tpsSingleBlock und tpsSparseText nur die Segmentierung ändern; keiner davon richtet einen schiefen Scan gerade

Free Pascal und Lazarus: veraltete Pixel und verlorenes Chinesisch

Beide Tesseract-Factorys funktionieren in Windows-Free-Pascal- und Lazarus-Win32- und Win64-Builds seit v2.772.1, nach zwei FPC-spezifischen Fixes. Bauen Sie das Lazarus-Paket zuerst für die Zielarchitektur neu; der allgemeine Port ist in HotPDF auf Free Pascal und Lazarus Win64 behandelt

Der erste Fix betrifft Pixel. Ein über Scanlines beschriebenes LCL-TBitmap kann sein rohes Bild aktualisieren, ohne das Windows-Bitmap-Handle aufzufrischen, GetDIBits auf diesem Handle liefert also die alten Pixel. Das Symptom war rätselhaft: Direkt auf eine Bitmap gezeichneter Text wurde erkannt, während eine vom PDF-Renderer von HotPDF gerenderte Seite eine leere Wortliste produzierte. Auf FPC liest der Adapter jetzt einen formatbewussten Snapshot über CreateIntfImage, der das Pixelformat und die Zeilenreihenfolge des rohen Bilds respektiert. Der Delphi-Build behält den GetDIBits-Pfad auf einer privaten 24-Bit-Kopie. Keiner der Builds verändert die Bitmap des Aufrufers

Der zweite Fix gehört zum tesseract.exe-Adapter. FPCs TStringList speichert ANSI-Strings, die Zuweisung dekodierten UTF-8-TSV-Texts an Lines.Text verlor also stillschweigend jedes chinesische oder Supplementary-Plane-Zeichen, das die System-ANSI-Codepage nicht darstellen konnte. Der FPC-Pfad hält den TSV jetzt als UTF-8-Bytes, entfernt die BOM auf Byteebene und dekodiert jedes Wort einzeln zu UnicodeString. Der DLL-Adapter hatte dieses Problem nie, denn er dekodiert jedes Wort direkt aus dem Iterator

Kurzreferenz

  • Factory: HPDFCreateTesseractDLLOCREngine(LibraryPath, TessDataDirectory, Language[, Options]) in HPDFTesseractRecognition, hinzugekommen in v2.772.0, FPC-Unterstützung in v2.772.1
  • Defaults: tpsAuto, temDefault, 60.000 ms, 16.777.216 Pixel; Timeout-Bereich 1–3.600.000 ms, Pixel-Obergrenze 67.108.864
  • Passen Sie die DLL-Bitness an die Anwendung an und legen Sie Abhängigkeits-DLLs neben die Tesseract-DLL
  • Behandeln Sie den Monitor als opak; kopieren Sie ETEXT_DESC nie in einen Pascal-Record
  • Deklarieren Sie den Cancel-Callback cdecl mit einem Ein-Byte-Boolean-Ergebnis, und lassen Sie nie eine Exception aus ihm entkommen
  • Geben Sie Iteratortext mit TessDeleteText frei; geben Sie den aus dem Result-Iterator gewonnenen Page-Iterator nie frei
  • Rechnen Sie damit, dass die Frist kooperativ ist: Modellinitialisierung und Layoutanalyse können über sie hinauslaufen
  • Nehmen Sie den tesseract.exe-Adapter, wenn Sie harte Terminierung oder Absturzisolation brauchen

Der Tesseract-DLL-Adapter, die Prozessadapter und die eingebaute OCR-Engine kommen alle mit der HotPDF-Delphi-PDF-Komponente für Delphi, C++Builder und Free Pascal; Editionen und Downloads finden Sie auf der HotPDF-Produktseite