HotPDF transformă paginile PDF scanate în PDF căutabil cu Tesseract prin HPDFCreateTesseractOCREngine, o fabrică care înfășoară un executabil Tesseract instalat local ca un IHPDFOCREngine. Pasați motorul acela către ApplyLoadedOCRTextLayer, care randează fiecare pagină, rulează Tesseract o dată per pagină, parsează output-ul lui TSV la nivel de cuvânt și comite un strat de text Unicode invizibil pentru toate paginile cerute într-o singură tranzacție, sau pentru niciuna
Motivul pentru care există adapter-ul acesta e scopa. Motorul OCR integrat de potrivire prin șabloane e deliberat îngust: litere și cifre ASCII tipărite, nimic altceva. Facturile cu nume diacritice, contractele în chineză și arhivele multilingve au nevoie de un recunoscător adevărat, cu modele lingvistice antrenate, iar Tesseract e candidatul evident pentru că e un program de linie de comandă pe care îl poți proviziona lângă aplicație. Apelarea unui program extern dintr-o bibliotecă de documente sună banal. Nu e, iar cea mai mare parte din codul interesant al adapter-ului e despre ce se întâmplă când programul se poartă rău, se blochează, e anulat sau moștenește lucruri pe care nu ar trebui niciodată să le vadă
Cum conduce HotPDF Tesseract dintr-o aplicație Delphi?
HotPDF rulează Tesseract ca proces copil ascuns per pagină, hrănindu-l cu o bitmap randată și citind înapoi un fișier TSV, și expune rezultatul prin aceeași cusătură IHPDFOCREngine pe care o folosește motorul integrat. Nimic din aval nu se schimbă: maparea de coordonate, tratarea rotației, validarea Unicode, filtrarea după confidence și comiterea atomică sunt pipeline-ul de strat de text pe care îl aveți deja. Fabrica trăiește în unitatea HPDFTesseractRecognition și validează nerăbdătoare: executabilul trebuie să existe, directorul tessdata trebuie să existe, timeout-ul trebuie să fie între 1 și 3.600.000 de milisecunde, iar identificatorul de limbă poate conține doar litere ASCII, cifre, _ și +. Verificarea din urmă contează pentru că șirul de limbă ajunge pe o linie de comandă, iar eng+chi_sim e o valoare legitimă Tesseract, pe când orice cu ghilimele sau spații nu e
uses
SysUtils, HPDFTypes, HPDFDoc, HPDFTesseractRecognition;
procedure MakeSearchable(const SourceFile, TargetFile: string;
Token: THPDFCancellationToken);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// ridică EArgumentException pentru un executabil lipsă, tessdata lipsă,
// un identificator de limbă greșit sau un timeout în afara 1..3600000 ms
Engine := HPDFCreateTesseractOCREngine(
'C:\OCR\Tesseract\tesseract.exe',
'C:\OCR\Tesseract\tessdata',
'eng+chi_sim', // mai multe modele unite cu '+'
120000); // limită per pagină, implicit 60000
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
Options.CancellationToken := Token;
// o listă de pagini goală înseamnă toate paginile; paginile cu text sunt sărite implicit
if Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
begin
Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
' words accepted, ', Info.DroppedWordCount, ' dropped');
Doc.SaveLoadedDocument(TargetFile);
end
else
case Info.Status of
otlsCancelled: Writeln('Cancelled, document unchanged');
otlsEngineError: Writeln('Engine: ', string(Info.Diagnostic));
otlsBudgetExceeded: Writeln('Budget: ', string(Info.Diagnostic));
else
Writeln(string(Info.Diagnostic));
end;
finally
Doc.Free;
end;
end;
Pentru fiecare pagină, Recognize creează un director privat sub calea temp, numit HotPDF-OCR-{GUID}, salvează bitmap-ul randat ca input.bmp și lansează tesseract input.bmp output --tessdata-dir … -l … --dpi N --psm 3 -c tessedit_create_tsv=1, cu fiecare argument de cale citat după regulile de escaping din linia de comandă Windows pentru backslash-uri și ghilimele încorporate. Valoarea --dpi e DPI-ul de randare din THPDFOCRTextLayerOptions.DPI, deci Tesseract nu are niciodată de ghicit rezoluția din metadatele imaginii, iar --psm 3 cere segmentare complet automată a paginii. Motorul se raportează ca Tesseract (local CLI), ceea ce aterizează în Info.EngineName. Tesseract și modelele lui lingvistice nu sunt livrate cu HotPDF; instalarea lor e treaba aplicației
De ce parser-ul TSV e atât de strict?
Parser-ul TSV din HotPDF eșuează toată pagina la orice rând malformat, pentru că o listă de cuvinte parsată pe jumătate produce un strat de text care nu se potrivește în tăcere cu imaginea. Output-ul TSV al Tesseract are un header fix cu douăsprezece coloane, de la level până la text, iar HotPDF compară prima linie cu acel header exact după ce scoate un byte order mark opțional. Fiecare rând următor trebuie să se despartă în exact douăsprezece câmpuri, iar despărțirea se oprește după al unsprezecelea tab, astfel încât un tab din interiorul textului recunoscut rămâne parte din cuvânt în loc să creeze o a treisprezecea coloană. Doar rândurile de nivel 5 sunt cuvinte; nivelurile 1 până la 4 descriu pagini, blocuri, paragrafe și linii, și sunt sărite. Rândurile de nivel 5 al căror text e gol sau spațiu alb pur sunt și ele sărite, pentru că un cuvânt gol are o casetă, dar nimic de localizat sau căutat. Tot restul e verificat aspru: geometrie întreagă, un confidence parsat cu un format invariant en-US, astfel încât un locale german să nu citească 93.5 ca gunoi, o casetă care stă complet în interiorul bitmap-ului și un confidence între 0 și 100. Un singur eșec ridică excepție, motorul întoarce False, iar tabloul de cuvinte e golit. Testele de regresie includ exact acest caz: un cuvânt valid urmat de un rând stricat trebuie să dea zero cuvinte, nu unul
// condensat din bucla level-5 din HPDFLocalTSVRecognition
if (Fields.Count <> 12) or not TryStrToInt(Fields[0], Level) then
raise EConvertError.Create('Invalid Local OCR TSV row');
if Level <> 5 then Continue; // rânduri de pagină/bloc/paragraf/linie
WordText := Fields[11];
if Trim(WordText) = '' then Continue; // cuvintele de spațiu alb n-au poziție
if not TryStrToInt(Fields[6], X) or not TryStrToInt(Fields[7], Y) or
not TryStrToInt(Fields[8], W) or not TryStrToInt(Fields[9], H) or
not TryStrToFloat(Fields[10], Confidence, Settings) then
raise EConvertError.Create('Invalid Local OCR word geometry');
if (X < 0) or (Y < 0) or (W <= 0) or (H <= 0) or
(Int64(X) + W > Request.Bitmap.Width) or
(Int64(Y) + H > Request.Bitmap.Height) or
not ((Confidence >= 0) and (Confidence <= 100)) then
raise EConvertError.Create('Local OCR word is outside the image');
Words[Count].Confidence := Confidence / 100; // pipeline-ul așteaptă 0..1
Ultimul rând interacționează cu un implicit pe care s-ar putea să nu îl așteptați. Confidence-ul Tesseract rulează de la 0 la 100, pipeline-ul lucrează în 0 până la 1, iar THPDFOCRTextLayerOptions.MinimumConfidence are implicit 0,5, deci orice cuvânt Tesseract sub 50 e numărat în Info.DroppedWordCount și nu ajunge niciodată pe pagină. Pe un scan curat la 300 DPI e un podei rezonabil. Pe un fax zgomotos poate arunca o parte surprinzătoare din pagină, iar mișcarea corectă e să priviți numărul de aruncate înainte să coborâți pragul, pentru că exact cuvintele cu confidence scăzut sunt cele mai probabil greșite
Ce moștenește procesul copil Tesseract?
Procesul copil Tesseract moștenește exact două handle-uri de la HotPDF: un handle NUL pentru stdin și stdout și un handle de fișier pentru stderr. Această precizie e chiar ideea. CreateProcess cu bInheritHandles = True e felul în care transmiți handle-uri standard unui copil, dar de unul singur transmite fiecare handle moștenibil din procesul gazdă, inclusiv fișiere, pipe-uri și evenimente deschise de cod neînrudit din aplicația voastră. Copilul ține apoi acele obiecte în viață până când iese, deci un fișier rămâne blocat sau un pipe nu își vede niciodată capătul cât timp Tesseract mărunțește o pagină. HotPDF închide golul acesta cu un record de pornire extins: STARTUPINFOEX, o listă de atribute care cară PROC_THREAD_ATTRIBUTE_HANDLE_LIST și flag-ul de creare EXTENDED_STARTUPINFO_PRESENT. Cu lista de handle-uri la locul ei, bInheritHandles trebuie să fie în continuare True, dar doar handle-urile listate traversează frontiera. Același tip de gândire de conținere guvernează izolarea codecurilor de imagini PDF în procese worker, unde copilul e cod neverificat; aici copilul e de încredere, dar gazda nu e singura deținătoare a propriei sale tabele de handle-uri
// constante arătate după nume; sursa le pasa cu valorile numerice
// ambele handle-uri sunt create cu bInheritHandle = True
InheritedHandles[0] := NullHandle; // stdin și stdout
InheritedHandles[1] := ErrorHandle; // stderr.txt în directorul privat
InitializeProcThreadAttributeList(Startup.AttributeList, 1, 0, AttributeBytes);
UpdateProcThreadAttribute(Startup.AttributeList, 0,
PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
@InheritedHandles[0], SizeOf(InheritedHandles), nil, nil);
CreateProcess(PChar(Executable), PChar(Command), nil, nil,
True, // cerut de lista de handle-uri
CREATE_NO_WINDOW or EXTENDED_STARTUPINFO_PRESENT,
nil, PChar(DirectoryName), Startup.StartupInfo, ProcessInfo);
De ce o rulare OCR anulată poate părea un eșec de motor?
O rulare OCR anulată pare un eșec de motor pentru că IHPDFOCREngine.Recognize întoarce un singur Boolean, iar False înseamnă și „Tesseract a eșuat” și „utilizatorul a apăsat Cancel”. Adapter-ul poll-uiește token-ul de anulare și timeout-ul la fiecare 25 de milisecunde cât timp copilul rulează, iar când token-ul se declanșează, ridică excepție în interiorul lui Recognize, își prinde propria excepție, curăță și întoarce False cu un diagnostic. Dacă pipeline-ul ar trata asta ca eroare de motor, apelantul ar vedea otlsEngineError pentru un job oprit deliberat de utilizator. ApplyLoadedOCRTextLayer verifică deci token-ul primul ori de câte ori Recognize întoarce False, și abia apoi transformă rezultatul într-un eșec de motor dacă token-ul nu fusese setat. Ordinea aceea păstrează contractul multi-pagină: recunoașterea, validarea, contabilitatea de buget și construcția de conținut rulează pentru fiecare pagină cerută înainte ca tranzacția de graf să se deschidă, deci o anulare la pagina 40 din 50 raportează otlsCancelled și lasă documentul, inclusiv primele 39 de pagini, neatins. Nu există un fișier parțial căutabil de explicat mai târziu, iar restul tratării eșecurilor urmează același stil mărginit:
- Timeout-ul e per apel
Recognize, măsurat de la pornirea lui, deci implicitul de 60.000 ms se aplică fiecărei pagini, nu întregului document - Un copil care rulează în continuare la timeout sau anulare e terminat, așteptat până la 5 secunde, iar directorul lui privat e șters într-un bloc
finally output.tsve plafonat la 64 MiB șistderr.txtla 1 MiB, verificate cât timp copilul rulează, dar și după ce iese- Numărul de cuvinte și unitățile de cod UTF-16 sunt plafonate per pagină de bugetele rămase
MaxWordsPerPage,MaxTotalWordsșiMaxTextCodeUnits, iar depășirea lor eșuează rularea în loc să trunchieze lista de cuvinte - Stdout-ul merge spre
NULpentru că Tesseract scrieoutput.tsv, pe când stderr-ul merge într-un fișier, astfel încât un cod de ieșire non-zero e raportat cu până la 4.096 de caractere din plângerea proprie a motorului, de regulă cea mai rapidă cale de a afla că un fișier.traineddatalipsește
Cum devin cuvintele recunoscute un strat de text invizibil
HotPDF scrie cuvintele Tesseract ca text invizibil folosind modul de randare a textului 3, modul nici-umplere-nici-contur definit în ISO 32000-1 §9.3.6, deci pagina arată în continuare imaginea scanată, în timp ce căutarea și copierea lucrează pe cuvintele recunoscute. Stream-ul de conținut deschide BT cu 3 Tr, iar fiecare cuvânt primește o matrice Tm la baseline-ul lui, o dimensiune de font derivată din înălțimea casetei în pixeli la DPI-ul de randare și o scalare orizontală Tz care întinde seria de glife până la lățimea măsurată a casetei, motiv pentru care un highlight de căutare aterizează pe cuvântul din imagine, nu alunecă peste el
TSV-ul Tesseract are casete, dar niciun baseline, deci adapter-ul raportează fiecare cuvânt fără baseline, iar pipeline-ul estimează baseline-ul la o cincime din înălțimea casetei deasupra laturii de jos. Textul în sine trece printr-un font Type0 neîncorporat partajat, cu encodare Identity-H și un CMap ToUnicode generat, un CID per scalar Unicode distinct peste toată rularea, ceea ce e felul în care chineza, latina diacritică și caracterele din planul suplimentar supraviețuiesc tuturor copierii și căutării. Design-ul acesta are două limite care merită spuse de la început: o singură rulare poate cară cel mult 65.535 de scalari distincți, iar fontul neîncorporat nu satisface cerința de încorporare a fonturilor din ISO 19005, deci output-ul PDF/A are nevoie de un font conform încorporat separat. Verificarea rezultatului e simplă și merită automatizată: salvați, reîncărcați și rulați calea obișnuită de text a documentului încărcat din extragerea de text dintr-un PDF încărcat în Delphi; dacă cuvintele revin în paginile așteptate, stratul e real
RapidOCR și alte motoare pe același protocol TSV
HotPDF refolosește același runner de proces și același parser TSV pentru RapidOCR prin HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds), care e alegerea mai utilă pentru scanări în chineză simplificată. Linia de comandă e identică, cu excepția că calea scriptului punte e inserată după executabilul Python, iar limba e fixată la chi_sim. HotPDF livrează puntea ca tools/OCR/rapidocr_tsv.py; ea așteaptă pachetele rapidocr și onnxruntime plus trei modele ONNX locale, dezactivează descărcările automate de modele și scrie un TSV cu forma Tesseract, astfel încât partea Delphi să nu aibă nevoie de un al doilea parser. Numele motorului raportat în Info.EngineName e RapidOCR (local ONNX). Forma aceasta sugerează rețeta generală: orice recunoscător pe care îl puteți înfășura într-un script mic, care acceptă lista de argumente în stil Tesseract și emite TSV-ul cu douăsprezece coloane, moștenește gratuit izolarea de handle-uri, timeout-ul, anularea, bugetele de output și comiterea tot-sau-nimic. Adapter-ele sunt doar pentru Windows, rulează câte o pagină pe rând sincron și nu îndreaptă înclinația și nu preprocesează imaginea dincolo de ce produce renderer-ul, deci calitatea imaginii la intrare plafonează în continuare ce iese
Adapter-ele Tesseract și RapidOCR, scriitorul de strat de text invizibil, renderer-ul de pagini care îi hrănește și extragerea de text care verifică rezultatul se livrează în aceeași componentă VCL nativă pentru Delphi și C++Builder. Dacă adăugați OCR unei aplicații de captură sau arhivare de documente, HotPDF Delphi PDF component vă dă pipeline-ul cu doar motorul OCR rămas de instalat