Technischer Artikel

HotPDF In-Process RapidOCR: Native-DLL-OCR in Delphi

HotPDF macht gescannte PDF-Seiten mit In-Process-RapidOCR durchsuchbar, über HPDFCreateRapidOCRDLLOCREngine, eine in v2.774.0 hinzugekommene Factory, die HotPDFRapidOCR.dll lädt, die ONNX-Modelle für Detection, Winkelklassifikation und Erkennung resident im Speicher hält und ein IHPDFOCREngine zurückgibt. Sie reichen diese Engine an THotPDF.ApplyLoadedOCRTextLayer weiter, die jede Seite rendert, CPU-Inferenz ohne Python oder Kindprozess fährt und eine unsichtbare Unicode-Textebene einbringt

Die Motivation sind die Kosten pro Seite. Der früher ausgelieferte RapidOCR-Prozessadapter, HPDFCreateRapidOCREngine, startet für jeden Recognize-Aufruf einen Python-Worker, und dieser Worker importiert seine Runtime und lädt seine ONNX-Modelle, bevor er ein einziges Pixel liest. Auf einem 500-Seiten-Archiv wiederholt sich diese Startsteuer 500 Mal, und Deployment bedeutet, eine Python-Umgebung neben einer Delphi-Executable auszuliefern. Die native DLL lädt die Modelle einmal, wenn Sie die Engine erzeugen, und das Deployment schrumpft auf die DLL, ihre Modelldateien und ein Zeichenwörterbuch. Was Sie dafür hergeben, ist die Fähigkeit, einen hängenden Erkenners zu killen, und der größte Teil der Ingenieursarbeit an diesem Adapter dreht sich darum, mit dieser Wahrheit ehrlich umzugehen

Wie macht man ein gescanntes PDF mit der RapidOCR-DLL durchsuchbar?

Ein durchsuchbares PDF mit der nativen RapidOCR-DLL braucht einen Factory-Aufruf und denselben ApplyLoadedOCRTextLayer-Aufruf, den jede HotPDF-OCR-Engine benutzt. Die Factory lebt in der Unit HPDFRapidOCRRecognition und validiert eifrig: DLL und Modellverzeichnis müssen existieren, jede Modell- und Wörterbuchdatei muss auflösbar sein, die ABI-Version muss 1 sein, und alle benötigten Exporte müssen vorhanden sein, bevor irgendein Modell initialisiert wird. Konfigurationsfehler werfen EArgumentException; ein Modell, das nicht lädt, wirft EInvalidOperation mit dem Diagnosetext, den die DLL geschrieben hat

HotPDF-RapidOCR-DLL-Factory-Validierungsfolge für HPDFCreateRapidOCRDLLOCREngine: Pfade und Modelldateien müssen existieren, HPDFRapidOCRAbiVersion muss 1 zurückgeben, benötigte Exporte müssen auflösbar sein, und HPDFRapidOCRCreate muss die Modelle initialisieren, mit EArgumentException oder EInvalidOperation, die eifrig geworfen werden, bevor irgendeine Erkennung läuft, letztere mit dem nativen Diagnosetext
Validierung ist mit Absicht eifrig: Konfigurationsprobleme werfen, bevor irgendein Modell initialisiert, ein schlechter Pfad oder eine falsche ABI erreicht also nie eine Erkennungsfrist
uses
  SysUtils, HPDFTypes, HPDFDoc, HPDFRapidOCRRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // Die Modelle laden hier, außerhalb jeder Erkennungsfrist.
  // Relative Modellnamen in THPDFRapidOCRDLLOptions.Default lösen
  // sich gegen das Modellverzeichnis auf.
  Engine := HPDFCreateRapidOCRDLLOCREngine(
    'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models');
  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,
      ' lines accepted, ', Info.DroppedWordCount, ' dropped');
    Doc.SaveLoadedDocument(TargetFile);
  finally
    Doc.Free;
  end;
end;

THPDFRapidOCRDLLOptions.Default nennt ch_PP-OCRv3_det_infer.onnx, ch_PP-OCRv3_rec_infer.onnx, ch_ppocr_mobile_v2.0_cls_infer.onnx und ppocr_keys_v1.txt, mit einem CPU-Thread, einem 16.777.216-Pixel-Eingabelimit und einer 60.000 ms Erkennungsfrist. Seit v2.775.0 tauscht THPDFRapidOCRDLLOptions.ForLanguage ein passendes Erkennungsmodell und Wörterbuch für Traditionelles Chinesisch, Russisch, Japanisch, Arabisch und weitere Profile ein; warum Modell und Wörterbuch gemeinsam wechseln müssen, behandelt RapidOCR-Multilingual-Modelle und CTC-Wörterbücher in HotPDF. Die Engine meldet sich als RapidOCR (native DLL) in Info.EngineName, was Logs neben dem externen Tesseract-OCR-Prozessadapter und der eingebauten Template-Matching-OCR-Engine eindeutig hält

Warum spricht die C-ABI nur int32_t und UTF-8-Bytes?

Die HotPDFRapidOCR.dll-ABI nutzt nur feste Breiten-Integer, rohe Zeiger und explizite Bytelängen, denn Delphi, C++Builder und Free Pascal teilen mit MSVC nichts außer der C-Aufrufkonvention. Ein std::string, ein std::vector oder eine C++-Exception haben ein Layout und ein Unwinding-Modell, die zu einem Compiler und einer Runtime-Bibliothek gehören. Lassen Sie irgendeines davon die Grenze überqueren, ist der Ausfall ein korrupter Stack oder ein Heap-Block, den der falsche Allokator freigibt — nicht ein sauberer Fehler

ABI-Version 1 folgt deshalb einer kurzen Regelliste. Jeder Export ist cdecl und liefert einen int32_t-Status zurück, wobei 1 Erfolg bedeutet und 0 Misserfolg. Jede Funktion, die scheitern kann, nimmt einen Diagnosepuffer in Aufruferbesitz samt seiner Kapazität in Bytes; die DLL schreibt eine NUL-terminierte UTF-8-Nachricht, auf passende Länge gekürzt, und der Adapter dekodiert sie mit einem harten Terminator im letzten Byte seines eigenen 4.096-Byte-Puffers. Jeder Exportkörper ist in try mit sowohl catch (const std::exception &) als auch catch (...) gewickelt, ein ONNX-Runtime-Fehler, eine OpenCV-Assertion oder ein ungültiges Wörterbuch wird also zu Status 0 plus Text, nie zu einer Exception, die in Pascal-Code entkommt

ExportRolleWann der Adapter ihn auflöst
HPDFRapidOCRAbiVersionLiefert 1; jeder andere Wert wird abgewiesenZuerst, vor allem anderen
HPDFRapidOCRCreateLädt Detection-, optionale Klassifikations- und Erkennungsmodelle und das WörterbuchIn der Factory
HPDFRapidOCRRecognizeFährt eine Bitmap und feuert einen Callback pro TextzeileIn der Factory
HPDFRapidOCRDestroyGibt die Modellinstanz freiIn der Factory
HPDFRapidOCRSetReadingDirectionOptionale Rechts-nach-links-Zeilenreihenfolge, hinzugekommen in v2.775.0Nur wenn RightToLeft gesetzt ist

Der optionale Export wird mit Absicht träge aufgelöst: Eine DLL aus v2.774.0 ohne ihn bedient weiterhin Links-nach-rechts-Anfragen. Die DLL wird mit LoadLibraryEx geladen, mit Suchflags, die den eigenen Ordner der DLL plus die Standard-Safe-Verzeichnisse abdecken, ONNX-Runtime- oder OpenCV-Abhängigkeiten neben HotPDFRapidOCR.dll werden also gefunden, ohne PATH anzufassen. Modell- und Wörterbuchpfade reisen als UTF-8, und die DLL konvertiert sie mit MultiByteToWideChar im Strict-Modus, bevor sie Dateien über Wide-Character-APIs öffnet, ein Modellverzeichnis unter einem chinesischen oder kyrillischen Benutzernamen funktioniert also, statt byte für byte in Unsinn verbreitert zu werden

Eine Regel lebt im Build statt im Header. Die DLL linkt ONNX Runtime und OpenCV statisch, und die Default-CMake-Konfiguration nutzt die statische Release-CRT (/MT). Gegen /MD kompilierte statische Bibliotheken, in eine /MT-DLL gemischt, erzeugen im besten Fall Link-Fehler und im schlechtesten zwei unabhängige Heaps, die bereitgestellten Bibliotheken müssen also zum jeweiligen CRT-Modus der DLL passen

Was passiert zwischen einer TBitmap und einer Textzeile?

HotPDF reicht der DLL einen unabhängigen Top-Down-BGR-Snapshot der gerenderten Seite, und die DLL reicht einen Callback pro erkannter Textzeile zurück, mit geliehenem UTF-8-Text, den der Adapter kopieren muss, bevor er zurückkehrt

Auf Delphi weist der Adapter die Seitenbitmap einem privaten TBitmap zu, erzwingt pf24bit und liest Zeilen mit GetDIBits über ein negatives biHeight, was Top-Down-Zeilen liefert, auf Vier-Byte-Ausrichtung gepolstert; dieser Stride wird explizit übergeben. Auf FPC liest er über CreateIntfImage, denn LCL-Scanline-Schreibvorgänge können das rohe Bild aktualisieren, ohne das GDI-Handle aufzufrischen. Die Bitmap des Aufrufers wird nie verändert, und das Pixelbudget (MaxPixels, standardmäßig 16.777.216, konfigurierbar bis 67.108.864) und das 32.767-Pixel-Limit pro Dimension werden geprüft, bevor der Snapshot-Puffer allokiert wird

HotPDF-RapidOCR-DLL-Pipeline von der Bitmap zur Textebene: Der Adapter snapshotet die Seite als Top-Down-pf24bit-BGR, die DLL polstert, detektiert, ordnet und erkennt Crops, liefert einen Callback pro Zeile mit geliehenem UTF-8-Text, Box und Confidence, und der Adapter validiert jede Zeile, bevor die Textebene committed wird
Pixel überqueren die ABI einmal als Snapshot, Zeilen kommen einzeln als Callback zurück, und nichts erreicht die durchsuchbare Ebene, bevor jede Prüfung bestanden ist

In der DLL wird der Snapshot mit 50 weißen Pixeln gepolstert, Textregionen werden mit maximal 1.024 Pixeln Seitenlänge detektiert, Boxen werden zu horizontalen Zeilen geordnet, und jeder Crop wird vor der Erkennung optional vom Winkelklassifikator gedreht. Jede Textzeile läuft dann durch einen Callback, der einen const char* empfängt, eine Byteanzahl, eine Integer-Box in Originalbild-Pixeln und die mittlere Zeichen-Confidence. Der Textzeiger ist nur während des Callbacks gültig, der Adapter kopiert ihn also sofort und ist streng darin, was er akzeptiert:

  • UTF-8 wird mit MB_ERR_INVALID_CHARS dekodiert; eine missgebildete Sequenz lässt die Seite scheitern, statt Replacement-Zeichen in einer durchsuchbaren Ebene zu erzeugen
  • C0- und C1-Steuerzeichen werden abgewiesen, und Zeilen aus reinem Whitespace werden übersprungen
  • Die Box muss innerhalb der Bitmap liegen, und die Confidence muss ein endlicher Wert von 0 bis 1 sein
  • Text wird gegen das MaxTextCodeUnits der Anfrage gezählt, mit harter Obergrenze von 1.048.576 UTF-16-Einheiten pro Aufruf, und Supplementary-Plane-Zeichen kosten zwei Einheiten
  • Jede Pascal-Exception innerhalb des Callbacks wird dort gefangen, gespeichert und in eine 0-Rückgabe verwandelt, was die DLL stoppen und Misserfolg melden lässt; die gespeicherte Nachricht wird dann zur Diagnose

Zwei Konsequenzen zählen beim Tunen. Erstens ist die Ausgabeeinheit eine Zeile, kein Wort: Jede Zeile verbraucht einen MaxWords-Slot, Info.AcceptedWordCount und Info.DroppedWordCount zählen Zeilen, und die Suchhervorhebung spannt über die Zeilenbox. Zweitens wird MinimumConfidence (Standard 0,5) gegen die mittlere Zeichen-Confidence der Zeile verglichen, eine Zeile mit einem unlesbaren Zeichen unter zwanzig sauberen überlebt also meistens. Die DLL liefert keine Baseline, die Textebenen-Pipeline schätzt eine aus der Box. Eine leere Seite gelingt mit null Zeilen, und jeder Ausfall löscht Teilergebnisse, der Mehrseiten-Commit bleibt also Alles-oder-Nichts

Modellbesitz und Thread-Safety

Jede RapidOCR-DLL-Engine besitzt exakt eine Modellinstanz über ihre gesamte Lebensdauer, und Aufrufe von Recognize auf dieser Engine werden durch eine Critical Section serialisiert. Die IHPDFOCREngine-Schnittstelle zu halten ist es, was die Modelle warm hält, das richtige Muster für Batch-Arbeit ist also, die Engine einmal zu erzeugen und dokumentübergreifend wiederzuverwenden

procedure OcrBatch(const Files: TStrings; const OutputDir: string);
var
  Models: THPDFRapidOCRDLLOptions;
  Engine: IHPDFOCREngine;
  Doc: THotPDF;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
  I: Integer;
begin
  Models := THPDFRapidOCRDLLOptions.Default;
  Models.UseAngleClassifier := False;    // aufrechte Scans: kein Klassifikatormodell wird geladen
  Models.Threads := 4;                   // 1..64, begrenzt auf die logische Prozessoranzahl
  Models.TimeoutMilliseconds := 120000;  // pro Recognize-Aufruf, kooperativ
  Engine := HPDFCreateRapidOCRDLLOCREngine(
    'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models', Models);
  Options := THPDFOCRTextLayerOptions.Default;
  for I := 0 to Files.Count - 1 do
  begin
    Doc := THotPDF.Create(nil);
    try
      Doc.AutoLaunch := False;
      if (Doc.LoadFromFile(Files[I]) > 0) and
        Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
        Doc.SaveLoadedDocument(IncludeTrailingPathDelimiter(OutputDir) +
          ExtractFileName(Files[I]))
      else
        Writeln(Files[I], ': ', string(Info.Diagnostic));
    finally
      Doc.Free;
    end;
  end;
end;  // letzte Referenz freigegeben: Modelle zerstört, dann wird die DLL entladen

Der Threads-Wert setzt sowohl die Intra-op- als auch die Inter-op-Threadanzahl jeder ONNX-Session, und die DLL klemmt ihn auf die aktive Prozessoranzahl. Zwei Threads, die sich eine Engine teilen, laufen nicht parallel; der zweite wartet auf das Lock. Dieses Warten ist kein blindes EnterCriticalSection: Der Adapter ruft alle 25 ms TryEnterCriticalSection auf und prüft das Abbruchtoken und die Frist zwischen den Versuchen, eine eingereihte Anfrage lässt sich also weiterhin abbrechen oder kann per Timeout scheitern. Wenn Sie echte Parallelität brauchen, erzeugen Sie eine Engine pro Worker und akzeptieren Sie, dass jede Engine ihre eigene Kopie der Modelle im Speicher hält

Die Abbau-Reihenfolge ist vom Engine-Destruktor festgelegt: HPDFRapidOCRDestroy gibt zuerst die Modellinstanz frei, dann entlädt FreeLibrary die DLL. Auf der nativen Seite ist die Modellinitialisierung genauso sorgfältig; scheitert das Erkennungsmodell, nachdem Detector- und Klassifikator-Sessions bereits gebaut waren, werden diese Sessions freigegeben, bevor der Fehler gemeldet wird, und die Wörterbuch-Klassenanzahl wird während der Initialisierung gegen die Modellausgabe geprüft statt auf der ersten Seite

Warum kann ein nativer OCR-Aufruf nicht mittendrin in der Inferenz gekillt werden?

Ein nativer RapidOCR-Aufruf lässt sich nicht mittendrin in der Inferenz killen, weil er auf Ihrem Thread läuft, innerhalb Ihres Prozesses, mitten in einer ONNX-Runtime-Session, die keine Unterbrechung annimmt. Abbruch im DLL-Adapter von HotPDF ist deshalb kooperativ: Die DLL ruft einen Abort-Callback vor und nach der Detection, nach der Klassifikation und nach jeder erkannten Zeile auf und stoppt am ersten Checkpoint, an dem der Callback 0 zurückgibt. Ein einzelner begonnener ONNX-Run wird zuerst fertig

Die Alternativen sind schlechter als Warten. TerminateThread würde den CRT-Heap-Lock, den Thread-Pool der ONNX Runtime und jeden OpenCV-Zustand in dem Zustand zurücklassen, in dem sie sich gerade befinden, und vergiftet damit den Rest des Prozesses. FreeLibrary, während ein Aufruf noch läuft, entlädt Code, der auf dem Stack liegt. Beides lässt sich nicht sicher machen, der Adapter versucht es also nie. Die Frist in TimeoutMilliseconds ist folgerichtig eine kooperative Frist, eine abgelaufene Frist zeigt sich als Engine-Fehler mit Timeout-Diagnose, ein abgebrochenes Token als otlsCancelled:

// Das Token erzeugt der Aufrufer und teilt es mit dem UI-Thread,
// der Token.Cancel aufruft, wenn der Benutzer Stop drückt
Options := THPDFOCRTextLayerOptions.Default;
Options.CancellationToken := Token;
if not Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
  case Info.Status of
    otlsCancelled:
      // zurückgegeben an der nächsten Stufen- oder Zeilengrenze; Dokument unverändert
      Writeln('Cancelled');
    otlsEngineError:
      // eingeschlossen kooperativer Fristablauf und native Diagnosen
      Writeln('Engine: ', string(Info.Diagnostic));
    otlsBudgetExceeded:
      Writeln('Budget: ', string(Info.Diagnostic));
  else
    Writeln(string(Info.Diagnostic));
  end;

Das ist der Kernkompromiss zwischen HotPDFs Prozessadaptern und der In-Process-DLL, und keine Seite gewinnt in jeder Zeile:

HotPDF-OCR-Adapter-Abwägungen: Prozessadapter starten einen Worker und laden Modelle bei jeder Seite, lassen sich aber killen und enthalten Abstürze, während die In-Process-RapidOCR-DLL Modelle einmal lädt, nur an kooperativen Checkpoints stoppt, sich den Adressraum teilt und als DLL samt Modellen und Wörterbuch ausgeliefert wird
Wählen Sie pro Workload: Eine Seite-für-Seite-Desktop-Anwendung profitiert von der warmen DLL, ein Server, der nicht vertrauenswürdige Scans schluckt, sollte für die Prozesswand zahlen
  • Startkosten: Die Tesseract- und Python-RapidOCR-Adapter starten einen Prozess und laden Modelle für jede Seite; die DLL lädt Modelle einmal pro Engine
  • Stoppen: Ein Kindprozess lässt sich direkt terminieren, und der Python-Worker läuft in einem Kill-on-Close-Job-Objekt, sein ganzer Prozessbaum geht also mit; die DLL kann nur an Stufen- und Zeilengrenzen stoppen
  • Fault Containment: Ein Absturz in tesseract.exe lässt eine Seite scheitern; eine Access Violation in der DLL reißt Ihren Prozess mit
  • Deployment: Prozessadapter brauchen ein installiertes Programm oder eine Python-Umgebung; die DLL braucht sich selbst, ihre Modelle und ihr Wörterbuch, passend zur Bitness der Anwendung
  • Speicher: Prozessadapter geben beim Exit des Kindes alles frei; eine DLL-Engine hält ihre Modelle resident, bis die letzte Schnittstellenreferenz freigegeben wird

Für eine interaktive Desktop-Anwendung, die seitenweise OCR fährt, gewinnt meist die Responsiveness der DLL. Für einen Server, der rund um die Uhr nicht vertrauenswürdige Scans schluckt, ist die Prozessgrenze ihre Startkosten wert

HotPDFRapidOCR.dll bauen und ausliefern

HotPDFRapidOCR.dll wird aus den C++-Quellen in Native/RapidOCR gebaut, mit MSVC, C++17, einem Windows SDK und CMake 3.20 oder später, über ein Hilfsskript, das die nativen Netzwerkquellen, ONNX-Runtime- und OpenCV-Verzeichnisse plus eine Win32- oder Win64-Plattform nimmt. Bauen Sie beide, wenn Sie beide ausliefern, denn eine 32-Bit-Delphi-Anwendung kann keine 64-Bit-DLL laden, und die bereitgestellten statischen Bibliotheken müssen sowohl zur Zielarchitektur als auch zum CRT-Modus passen

Die Modellseite hat ihre eigenen Kompatibilitätsgrenzen. Der Detector ist ein DB-Text-Detector; der Recognizer akzeptiert CTC-Modelle im NCHW-Layout mit fester Eingabehöhe 32 oder 48 und nutzt 48 für Modelle mit dynamischer Höhe. Die gebündelte statische ONNX Runtime kann keine Modelle laden, die mit einer neueren IR-Version gespeichert wurden, aktuelle PP-OCRv5-Exporte scheitern also an der Initialisierung mit einer Diagnose, statt teilweise zu laden. Das Wörterbuch muss UTF-8 ohne BOM sein, in exakt der Zeichenreihenfolge des Modells, und seine Klassenanzahl muss zur Modellausgabe passen; CRLF-Zeilenenden werden akzeptiert. Erkennung läuft offline: Die DLL lädt nie ein fehlendes Modell herunter

Kurzreferenz

  • Factory: HPDFCreateRapidOCRDLLOCREngine(LibraryPath, ModelDirectory[, Options]) in HPDFRapidOCRRecognition, verfügbar seit v2.774.0 in Delphi-, C++Builder- und Windows-FPC/Lazarus-Builds
  • Halten Sie das zurückgegebene IHPDFOCREngine über Seiten und Dokumente hinweg am Leben; seine Freigabe zerstört die Modelle und entlädt die DLL
  • Eine Engine fährt eine Erkennung auf einmal; erzeugen Sie mehrere Engines für parallele Worker und budgetieren Sie Speicher für jede Modellkopie
  • Die Ausgabe ist ein Eintrag pro Textzeile mit mittlerer Zeichen-Confidence, gefiltert durch THPDFOCRTextLayerOptions.MinimumConfidence
  • Abbruch und TimeoutMilliseconds sind kooperativ; ein laufender ONNX-Run wird stets fertig
  • Passen Sie die DLL-Bitness an die Anwendung an und den CRT-Modus der statischen ONNX-Runtime- und OpenCV-Bibliotheken an die DLL
  • Wählen Sie ein Sprachprofil pro Engine mit THPDFRapidOCRDLLOptions.ForLanguage (v2.775.0); eine Engine erkennt Sprachen nicht selbstständig

Der native RapidOCR-Adapter, die prozessbasierten OCR-Adapter, der sie speisende Seitenrenderer und der Schreiber der unsichtbaren Unicode-Textebene kommen alle gemeinsam in HotPDF, einer nativen VCL-PDF-Komponente für Delphi und C++Builder. Wenn Ihre Dokumenterfassungs- oder Archivierungsanwendung durchsuchbare Ausgabe ohne Python-Runtime auf dem Zielrechner braucht, liefert die HotPDF Delphi PDF component die ganze Pipeline, übrig bleibt nur die DLL samt ihren Modellen auszuliefern