HotPDF face paginile PDF scanate căutabile cu RapidOCR în proces prin HPDFCreateRapidOCRDLLOCREngine, o fabrică adăugată în v2.774.0 care încarcă HotPDFRapidOCR.dll, ține modelele ONNX de detecție, de clasificare a unghiului și de recunoaștere rezidente în memorie și întoarce un IHPDFOCREngine. Pasați motorul acela către THotPDF.ApplyLoadedOCRTextLayer, care randează fiecare pagină, rulează inferență pe CPU fără Python sau proces copil și comite un strat de text Unicode invizibil
Motivația e costul per pagină. Adapter-ul de proces RapidOCR livrat mai devreme, HPDFCreateRapidOCREngine, pornește un worker Python pentru fiecare apel Recognize, iar worker-ul acela își importă runtime-ul și își încarcă modelele ONNX înainte să citească primul pixel. Pe o arhivă de 500 de pagini taxa de pornire se repetă de 500 de ori, iar livrarea înseamnă să cari un mediu Python lângă un executabil Delphi. DLL-ul nativ încarcă modelele o singură dată, când creați motorul, iar livrarea se strânge la DLL, fișierele lui de modele și un dicționar de caractere. Ce renunțați în schimb e posibilitatea de a ucide un recunoscător blocat, iar cea mai mare parte din ingineria acestui adapter e despre a trăi cu asta cinstit
Cum faci căutabil un PDF scanat cu DLL-ul RapidOCR?
Crearea unui PDF căutabil cu DLL-ul RapidOCR nativ cere un apel de fabrică și același apel ApplyLoadedOCRTextLayer pe care îl folosește fiecare motor OCR HotPDF. Fabrica trăiește în unitatea HPDFRapidOCRRecognition și validează nerăbdătoare: DLL-ul și directorul de modele trebuie să existe, fiecare fișier de model și de dicționar trebuie să se rezolve, versiunea ABI trebuie să fie 1, iar toate exporturile cerute trebuie să fie prezente înainte ca vreun model să fie inițializat. Greșelile de configurare ridică EArgumentException; un model care eșuează la încărcare ridică EInvalidOperation care cară textul de diagnostic scris de DLL
uses
SysUtils, HPDFTypes, HPDFDoc, HPDFRapidOCRRecognition;
procedure MakeSearchable(const SourceFile, TargetFile: string);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// Modelele se încarcă aici, în afara oricărui termen limită de recunoaștere.
// Numele de modele relative din THPDFRapidOCRDLLOptions.Default se
// rezolvă față de directorul de modele
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
// o listă de pagini goală înseamnă toate paginile; paginile care au deja text sunt sărite
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 numește ch_PP-OCRv3_det_infer.onnx, ch_PP-OCRv3_rec_infer.onnx, ch_ppocr_mobile_v2.0_cls_infer.onnx și ppocr_keys_v1.txt, cu un singur thread CPU, o limită de input de 16.777.216 de pixeli și un termen limită de recunoaștere de 60.000 ms. Din v2.775.0, THPDFRapidOCRDLLOptions.ForLanguage interschimbă un model de recunoaștere și un dicționar potrivite pentru chineză tradițională, rusă, japoneză, arabă și alte profile; de ce modelul și dicționarul trebuie să se schimbe împreună e acoperit în modelele multilingve RapidOCR și dicționarele CTC din HotPDF. Motorul se raportează ca RapidOCR (native DLL) în Info.EngineName, ceea ce ține logurile fără ambiguitate lângă adapter-ul de proces Tesseract OCR extern și motorul OCR integrat de potrivire prin șabloane
De ce C ABI-ul vorbește doar int32_t și octeți UTF-8?
ABI-ul lui HotPDFRapidOCR.dll folosește doar întregi pe lățime fixă, pointeri raw și lungimi de octeți explicite, pentru că Delphi, C++Builder și Free Pascal nu partajează nimic cu MSVC dincolo de convenția de apel C. Un std::string, un std::vector sau o excepție C++ are un layout și un model de unwinding care aparțin unui singur compilator și unei singure biblioteci runtime. Lăsați vreunul dintre ele să traverseze frontiera, iar eșecul e un stack corupt sau un bloc de heap eliberat de allocator-ul greșit, nu o eroare curată
Versiunea ABI 1 urmează prin urmare o listă scurtă de reguli. Fiecare export e cdecl și întoarce o stare int32_t, unde 1 înseamnă succes și 0 înseamnă eșec. Fiecare funcție care poate eșua primește un buffer de diagnostic deținut de apelant și capacitatea lui în octeți; DLL-ul scrie un mesaj UTF-8 terminat cu NUL, trunchiat să încapă, iar adapter-ul îl decodează cu un terminator rigid în ultimul octet al propriului lui buffer de 4.096 de octeți. Corpul fiecărui export e înfășurat în try cu ambele catch (const std::exception &) și catch (...), astfel încât o eroare ONNX Runtime, o asserție OpenCV sau un dicționar invalid devine starea 0 plus text, niciodată o excepție care scapă în cod Pascal
| Export | Rol | Când îl rezolvă adapter-ul |
|---|---|---|
HPDFRapidOCRAbiVersion | Întoarce 1; orice altă valoare e respinsă | Primul, înaintea oricui |
HPDFRapidOCRCreate | Încarcă modelele de detecție, de clasificare opțional, de recunoaștere și dicționarul | În fabrică |
HPDFRapidOCRRecognize | Rulează o bitmap și emite câte un callback per linie de text | În fabrică |
HPDFRapidOCRDestroy | Eliberează instanța de model | În fabrică |
HPDFRapidOCRSetReadingDirection | Ordine de rânduri de la dreapta la stânga opțională, adăugată în v2.775.0 | Doar când RightToLeft e setat |
Exportul opțional e rezolvat leneș din principiu: un DLL v2.774.0 care nu-l are servește în continuare cererile de la stânga la dreapta. DLL-ul e încărcat cu LoadLibraryEx cu flag-uri de căutare care acoperă folderul propriu al DLL-ului plus directoarele sigure implicite, deci dependențele ONNX Runtime sau OpenCV plasate lângă HotPDFRapidOCR.dll sunt găsite fără a atinge PATH. Căile de modele și de dicționar călătoresc ca UTF-8, iar DLL-ul le convertește cu MultiByteToWideChar în mod strict înainte să deschidă fișiere prin API-uri wide-character, astfel încât un director de modele sub un nume de utilizator chinezesc sau chirilic funcționează în loc să fie lățit octet cu octet în un fără sens
O regulă trăiește în build, nu în header. DLL-ul leagă static ONNX Runtime și OpenCV, iar configurația CMake implicită folosește CRT-ul static de release (/MT). Bibliotecile statice compilate contra /MD amestecate într-un DLL /MT produc în cel mai bun caz erori de link, iar în cel mai rău caz două heap-uri independente, deci bibliotecile provizionate trebuie să se potrivească cu modul de CRT pe care îl folosește DLL-ul
Ce se întâmplă între un TBitmap și o linie de text?
HotPDF pasează DLL-ului un snapshot independent top-down BGR al paginii randate, iar DLL-ul înapoi câte un callback per linie de text recunoscută, cu text UTF-8 împrumutat pe care adapter-ul trebuie să-l copieze înainte de a reveni
Pe Delphi adapter-ul asignează bitmap-ul paginii unui TBitmap privat, forțează pf24bit și citește rândurile cu GetDIBits folosind un biHeight negativ, ceea ce dă rânduri top-down pad-uite la aliniere de patru octeți; stride-ul acela e pasat explicit. Pe FPC citește prin CreateIntfImage, pentru că scrierile pe scanline din LCL pot actualiza imaginea raw fără să reîmprospăteze handle-ul GDI. Bitmap-ul apelantului nu e modificat niciodată, iar bugetul de pixeli (MaxPixels, 16.777.216 implicit și configurabil până la 67.108.864) și limita de 32.767 de pixeli per dimensiune sunt verificate înainte ca buffer-ul de snapshot să fie alocat
În interiorul DLL-ului snapshot-ul e pad-uit cu 50 de pixeli albi, regiunile de text sunt detectate cu o latură maximă de 1.024 de pixeli, casetele sunt ordonate în rânduri orizontale, iar fiecare crop e rotit opțional de clasificatorul de unghiuri înaintea recunoașterii. Fiecare linie de text trece apoi printr-un callback care primește un const char*, un număr de octeți, o casetă întreagă în pixelii imaginii originale și confidence-ul mediu de caracter. Pointer-ul de text e valid doar în timpul callback-ului, deci adapter-ul îl copiază imediat și e strict în ce acceptă:
- UTF-8 e decodat cu
MB_ERR_INVALID_CHARS; o secvență malformată eșuează pagina în loc să producă caractere de înlocuire într-un strat căutabil - Caracterele de control C0 și C1 sunt respinse, iar liniile cu doar spații albe sunt sărite
- Caseta trebuie să stea în interiorul bitmap-ului, iar confidence-ul trebuie să fie o valoare finită de la 0 la 1
- Textul e numărat contra lui
MaxTextCodeUnitsal cererii, cu un plafon rigid de 1.048.576 de unități UTF-16 per apel, iar caracterele din planul suplimentar costă două unități - Orice excepție Pascal în interiorul callback-ului e prinsă acolo, stocată și transformată într-o returnare 0, ceea ce face ca DLL-ul să se oprească și să raporteze eșec; mesajul stocat devine apoi diagnosticul
Două consecințe contează pentru tuning. Prima, unitatea de output e o linie, nu un cuvânt: fiecare linie consumă un slot MaxWords, Info.AcceptedWordCount și Info.DroppedWordCount numără linii, iar highlight-ul de căutare acoperă caseta liniei. A doua, MinimumConfidence (0,5 implicit) e comparat cu confidence-ul mediu de caracter al liniei, deci o linie cu un caracter ilizibil printre douăzeci curate supraviețuiește de obicei. DLL-ul nu furnizează niciun baseline, deci pipeline-ul de strat de text estimează unul din casetă. O pagină goală reușește cu zero linii, iar orice eșec curăță rezultatele parțiale, astfel încât comiterea multi-pagină rămâne tot-sau-nimic
Deținerea modelelor și thread safety
Fiecare motor DLL RapidOCR deține exact o instanță de model pentru toată durata lui de viață, iar apelurile Recognize pe motorul acela sunt serializate de o secțiune critică. Deținerea interfeței IHPDFOCREngine e ceea ce ține modelele calde, deci tiparul corect pentru muncă în serie e să creați motorul o dată și să-l refolosiți peste documente
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; // scanări drepte: niciun model de clasificator nu e încărcat
Models.Threads := 4; // 1..64, plafonat la numărul de procesoare logice
Models.TimeoutMilliseconds := 120000; // per apel Recognize, cooperativ
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; // ultima referință eliberată: modelele distruse, apoi DLL-ul e descărcat
Valoarea Threads setează atât numărul de thread-uri intra-op, cât și inter-op al fiecărei sesiuni ONNX, iar DLL-ul o limitează la numărul de procesoare active. Două thread-uri care partajează un motor nu rulează în paralel; al doilea așteaptă lock-ul. Așteptarea aceea nu e un EnterCriticalSection orbește: adapter-ul apelează TryEnterCriticalSection la fiecare 25 ms și verifică token-ul de anulare și termenul limită între încercări, astfel încât o cerere în coadă poate fi în continuare anulată sau poate primi timeout. Dacă aveți nevoie de paralelism adevărat, creați câte un motor per worker și acceptați că fiecare motor ține propria copie a modelelor în memorie
Ordinea de demolare e fixată de destructorul motorului: HPDFRapidOCRDestroy eliberează mai întâi instanța de model, apoi FreeLibrary descarcă DLL-ul. Pe partea nativă, inițializarea modelelor e la fel de atentă; când modelul de recunoaștere eșuează după ce sesiunile de detector și de clasificator fuseseră deja construite, sesiunile acelea sunt eliberate înainte ca eroarea să fie raportată, iar numărul de clase al dicționarului e verificat contra output-ului modelului în timpul inițializării, nu la prima pagină
De ce nu poate fi ucis un apel OCR nativ în mijlocul inferenței?
Un apel RapidOCR nativ nu poate fi ucis în mijlocul inferenței pentru că rulează pe thread-ul tău, în interiorul procesului tău, în mijlocul unei sesiuni ONNX Runtime care nu acceptă întrerupere. Anularea în adapter-ul DLL din HotPDF e deci cooperativă: DLL-ul apelează un callback de abort înainte și după detecție, după clasificare și după fiecare linie recunoscută, și se oprește la primul punct de control unde callback-ul întoarce 0. Un singur Run ONNX care a pornit se va termina mai întâi
Alternativele sunt mai rele decât așteptatul. TerminateThread ar lăsa heap lock-ul CRT-ului, thread pool-ul ONNX Runtime și orice stare OpenCV în orice condiție se întâmplau să fie, otrăvind restul procesului. FreeLibrary în timp ce un apel se execută încă descarcă cod care stă pe stack. Niciunul nu poate fi făcut sigur, deci adapter-ul nu încearcă niciodată. Termenul limită din TimeoutMilliseconds e prin urmare un termen cooperativ, iar un termen expirat iese la suprafață ca eroare de motor cu un diagnostic de timeout, în timp ce un token anulat iese ca otlsCancelled:
// Token-ul e creat de apelant și partajat cu UI thread-ul,
// care apelează Token.Cancel când utilizatorul apasă Stop
Options := THPDFOCRTextLayerOptions.Default;
Options.CancellationToken := Token;
if not Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
case Info.Status of
otlsCancelled:
// întors la următoarea frontieră de etapă sau de linie; document neschimbat
Writeln('Cancelled');
otlsEngineError:
// include expirarea unui termen cooperativ și diagnostice native
Writeln('Engine: ', string(Info.Diagnostic));
otlsBudgetExceeded:
Writeln('Budget: ', string(Info.Diagnostic));
else
Writeln(string(Info.Diagnostic));
end;
Asta e compromisul central între adapter-ele de proces din HotPDF și DLL-ul în proces, și nicio parte nu câștigă pe fiecare rând:
- Cost de pornire: adapter-ele Tesseract și RapidOCR Python lansează un proces și încarcă modele pentru fiecare pagină; DLL-ul încarcă modelele o dată per motor
- Oprirea: un proces copil poate fi terminat de tot, iar worker-ul Python rulează în interiorul unui Job Object kill-on-close, astfel încât tot arborele lui de proces merge cu el; DLL-ul se poate opri doar la frontiere de etapă și de linie
- Conținerea defectelor: o blocare în
tesseract.exeeșuează o pagină; o access violation în interiorul DLL-ului coboară procesul dvs. cu totul - Livrare: adapter-ele de proces au nevoie de un program instalat sau de un mediu Python; DLL-ul are nevoie de el însuși, de modelele lui și de dicționarul lui, potrivite la bitness-ul aplicației
- Memorie: adapter-ele de proces eliberează totul când copilul iese; un motor DLL își ține modelele rezidente până când ultima referință de interfață e eliberată
Pentru o aplicație desktop interactivă care face OCR câte o pagină pe dată, responsivitatea DLL-ului câștigă de obicei. Pentru un server care înghite scanări nesigure zi și noapte, frontiera de proces merită costul de pornire
Construirea și livrarea lui HotPDFRapidOCR.dll
HotPDFRapidOCR.dll e construit din sursele C++ din Native/RapidOCR cu MSVC, C++17, un Windows SDK și CMake 3.20 sau mai nou, folosind un script helper care primește directoarele de surse native de rețea, ONNX Runtime și OpenCV plus o platformă Win32 sau Win64. Construiți ambele dacă livrați ambele, pentru că o aplicație Delphi pe 32 de biți nu poate încărca un DLL pe 64 de biți, iar bibliotecile statice pe care le provizionați trebuie să se potrivească și cu arhitectura țintă, și cu modul de CRT
Partea de modele are propriile limite de compatibilitate. Detector-ul e un detector de text DB; recunoscătorul acceptă modele CTC în layout NCHW cu o înălțime de input fixă de 32 sau 48 și folosește 48 pentru modelele cu înălțime dinamică. ONNX Runtime-ul static inclus nu poate încărca modele salvate cu o versiune IR mai nouă, deci exporturile PP-OCRv5 recente eșuează inițializarea cu un diagnostic în loc să se încarce pe jumătate. Dicționarul trebuie să fie UTF-8 fără BOM, în exact ordinea de caractere a modelului, iar numărul lui de clase trebuie să se potrivească cu output-ul modelului; terminațiile de linie CRLF sunt acceptate. Recunoașterea e offline: DLL-ul nu descarcă niciodată un model lipsă
Referință rapidă
- Fabrica:
HPDFCreateRapidOCRDLLOCREngine(LibraryPath, ModelDirectory[, Options])înHPDFRapidOCRRecognition, disponibilă din v2.774.0 în build-urile Delphi, C++Builder și FPC/Lazarus Windows - Țineți
IHPDFOCREngineîntors în viață peste pagini și documente; eliberarea lui distruge modelele și descarcă DLL-ul - Un motor rulează o recunoaștere pe dată; creați mai multe motoare pentru worker-i paraleli și bugetați memorie pentru fiecare copie de model
- Output-ul e o intrare per linie de text cu confidence mediu de caracter, filtrat de
THPDFOCRTextLayerOptions.MinimumConfidence - Anularea și
TimeoutMillisecondssunt cooperative; un run ONNX în curs se termină mereu - Potriviți bitness-ul DLL-ului cu aplicația și modul de CRT al bibliotecilor statice ONNX Runtime și OpenCV cu DLL-ul
- Alegeți un profil de limbă per motor cu
THPDFRapidOCRDLLOptions.ForLanguage(v2.775.0); un motor nu detectează singur limbile
Adapter-ul RapidOCR nativ, adapter-ele OCR pe bază de proces, renderer-ul de pagini care îi hrănește și scriitorul de strat de text Unicode invizibil se livrează împreună în HotPDF, o componentă PDF VCL nativă pentru Delphi și C++Builder. Dacă aplicația dvs. de captură sau arhivare de documente are nevoie de output căutabil fără un runtime Python pe mașina țintă, componenta HotPDF Delphi PDF furnizează tot pipeline-ul, rămânând de livrat doar DLL-ul și modelele lui