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
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
| Export | Rolle | Wann der Adapter ihn auflöst |
|---|---|---|
HPDFRapidOCRAbiVersion | Liefert 1; jeder andere Wert wird abgewiesen | Zuerst, vor allem anderen |
HPDFRapidOCRCreate | Lädt Detection-, optionale Klassifikations- und Erkennungsmodelle und das Wörterbuch | In der Factory |
HPDFRapidOCRRecognize | Fährt eine Bitmap und feuert einen Callback pro Textzeile | In der Factory |
HPDFRapidOCRDestroy | Gibt die Modellinstanz frei | In der Factory |
HPDFRapidOCRSetReadingDirection | Optionale Rechts-nach-links-Zeilenreihenfolge, hinzugekommen in v2.775.0 | Nur 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
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_CHARSdekodiert; 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
MaxTextCodeUnitsder 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:
- 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.exelä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])inHPDFRapidOCRRecognition, 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
TimeoutMillisecondssind 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