HotPDF face OCR în chineză și multilingv în Delphi prin adapter-ul lui nativ RapidOCR DLL: THPDFRapidOCRDLLOptions.ForLanguage mapează un tag de limbă precum 'zh-CN', 'zh-TW', 'ru' sau 'ar' către un model de recunoaștere și un dicționar de caractere potrivite, iar THotPDF.ApplyLoadedOCRTextLayer transformă liniile recunoscute într-un strat de text Unicode invizibil și căutabil pe paginile PDF scanate
Să faci un demo în scriere latină să funcționeze e partea ușoară. Eșecurile interesante încep când treci la chineză tradițională sau rusă și output-ul devine o prostie încrezătoare și bine formată, sau când fiecare linie își pierde în tăcere ultimul caracter, sau când o pagină arabă revine cu casetele de text în ordinea greșită. Niciunul dintre acestea nu ridică o excepție de la sine. Presetările de limbă adăugate în HotPDF v2.775.0 există mai ales pentru a închide acele goluri, iar cele patru capcane de mai jos merită înțelese chiar dacă nu atingeți niciodată codul nativ, pentru că fiecare explică un simptom după care ai putea altfel alerga o zi întreagă
Cum alege ForLanguage un model și un dicționar?
THPDFRapidOCRDLLOptions.ForLanguage rezolvă un tag către unul dintre cele nouă profile și întoarce opțiuni care arată către <profile>/recognition.onnx și <profile>/dictionary.txt sub directorul propriu de modele, păstrând în același timp detector-ul partajat, clasificatorul opțional de unghiuri și implicitele de thread, pixeli și timeout din THPDFRapidOCRDLLOptions.Default. Metoda trece tag-ul la minuscule, transformă underscore-urile în cratime și taie spațiile albe din jur, deci 'zh_TW', 'ZH-tw' și ' zh-tw ' aterizează toate pe același profil. Aliasurile sunt o listă explicită, nu o potrivire de prefix: 'zh-Hant-TW' e acceptat pentru că e listat, în timp ce o variantă regională arbitrară nelistată ridică EArgumentException înainte să se încarce vreun model
| Profil | Limbi | Tag-uri exemplu | Model fixat |
|---|---|---|---|
ch | Chineză simplificată și engleză | zh, zh-CN, zh-Hans, chi_sim | PP-OCRv4 |
chinese_cht | Chineză tradițională | zh-TW, zh-HK, zh-Hant, chi_tra | PP-OCRv3 |
en | Engleză | en, en-US, en-GB, eng | PP-OCRv4 |
latin | Franceză, germană, spaniolă, portugheză, italiană, neerlandeză, turcă | fr, de, es-419, pt-BR, tr | PP-OCRv3 |
japan | Japoneză | ja, ja-JP, jpn | PP-OCRv4 |
korean | Coreeană | ko, ko-KR, kor | PP-OCRv4 |
cyrillic | Rusă, ucraineană, bulgară, belarusă | ru, ru-RU, uk, bg | PP-OCRv3 |
arabic | Arabă, persană, urdu | ar, ar-SA, fa, ur | PP-OCRv4 |
devanagari | Hindi, marathi, nepaleză | hi, mr, ne | PP-OCRv4 |
Adapter-ul însuși nu descarcă nimic. Provizionați fișierele o dată cu helper-ul inclus, de exemplu tools/Install-RapidOCRModels.ps1 -Destination C:/OCR/models -Language ch,chinese_cht,cyrillic (sau -Language All pentru toate cele nouă profile), iar helper-ul plasează un detector și un clasificator partajate la numele de fișier de rădăcină pe care le așteaptă Default. După aceea, un scan în chineză simplificată devine căutabil cu câteva rânduri de cod. Instalația de motor e aceeași cusătură IHPDFOCREngine descrisă în articolul despre DLL-ul RapidOCR în proces și frontiera lui de ABI, deci acesta rămâne concentrat pe limbi
uses
SysUtils, HPDFDoc, HPDFRapidOCRRecognition;
procedure MakeChineseScanSearchable(const SourceFile, TargetFile: string);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Models: THPDFRapidOCRDLLOptions;
Layer: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// ch/recognition.onnx + ch/dictionary.txt, detector și clasificator partajate
Models := THPDFRapidOCRDLLOptions.ForLanguage('zh-CN');
Engine := HPDFCreateRapidOCRDLLOCREngine(
'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models', Models);
Doc := THotPDF.Create(nil);
try
Doc.AutoLaunch := False;
if Doc.LoadFromFile(SourceFile) < 1 then
raise Exception.Create('Cannot load ' + SourceFile);
Layer := 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, Layer, Info) then
raise Exception.Create(string(Info.Diagnostic));
Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
' lines, ', Info.UniqueScalarCount, ' distinct characters');
Doc.SaveLoadedDocument(TargetFile);
finally
Doc.Free;
end;
end;
Două detalii din output-ul acela merită o mențiune. Pipeline-ul nativ întoarce un rezultat per linie de text detectată, nu per cuvânt, deci AcceptedWordCount numără aici linii, iar MinimumConfidence e comparat cu confidence-ul mediu de caracter al întregii linii: o linie care mediază 0,45 e aruncată ca unitate. UniqueScalarCount raportează câți scalari Unicode distincți a trebuit să mapeze stratul de text în fontul și tabela lui ToUnicode, o verificare de bun simț utilă că textul CJK chiar a ajuns, nu o mână de fallback-uri latine. Țineți interfața motorului în viață peste documente, pentru că inițializarea modelelor se întâmplă în fabrică și e pasul scump
De ce schimbarea doar a modelului de recunoaștere produce gunoi?
Un model de recunoaștere CTC nu emite niciodată caractere, ci doar indici de clasă, iar dicționarul e singurul lucru care transformă indicele 1.204 într-un glyph. Schimbați ch/recognition.onnx cu cyrillic/recognition.onnx dar păstrați dicționarul chinezesc, iar modelul va emita fericit indici chirilici valizi pe care vechiul dicționar îi traduce în caractere Han aleatorii. Rezultatul arată a text, trece validarea UTF-8 și e căutabil pentru exact nimic. De aceea ForLanguage setează mereu RecognitionModel și CharacterDictionary împreună, iar opțiunile construite de mână nu ar trebui să schimbe niciodată unul fără celălalt
Verificarea evidentă de siguranță, compararea mărimii dicționarului cu lățimea de output a modelului, e necesară, dar nu și suficientă. Două dicționare pot avea același număr de intrări într-o ordine diferită, iar un off-by-one în ordine deplasează fiecare caracter cu un code point. HotPDF verifică deci în două etape când fabrica inițializează modelul. Mai întâi, numărul de clase de output trebuie să egaleze intrările dicționarului plus doi. Apoi, când fișierul ONNX încorporează o listă de metadate character, fiecare intrare a dicționarului e comparată cu ea în ordine, iar o nepotrivire eșuează inițializarea cu EInvalidOperation și un diagnostic nativ în loc să producă gunoi plauzibil mai târziu
„Plus doi” vine din layout-ul de clase. Clasa 0 e CTC blank, clasele 1 până la N sunt liniile dicționarului în ordinea din fișier, iar clasa finală e un spațiu. Unele dicționare cară și o intrare de spațiu proprie, iar linia aceea trebuie păstrată exact cum e. Aici un Trim bine intenționat face pagube reale: transformă o intrare cu un singur spațiu într-un șir gol și deplasează sau rupe tabela. Singura normalizare sigură e eliminarea unui carriage return final, astfel încât un dicționar salvat cu terminații de linie CRLF se încarcă corect, în timp ce un byte order mark UTF-8, o linie goală sau o intrare care conține un tab sunt respinse. Schița de mai jos arată layout-ul în Pascal; e cod explicativ, nu un API HotPDF
// Doar ilustrație: tabela de clase pe care o așteaptă un recunoscător CTC
uses
SysUtils, IOUtils;
function BuildCTCClassTable(const FileName: string): TArray<string>;
var
Text, Entry: string;
Lines: TArray<string>;
I, Last: Integer;
begin
Text := TEncoding.UTF8.GetString(TFile.ReadAllBytes(FileName));
if (Text <> '') and (Text[1] = #$FEFF) then
raise EArgumentException.Create('Dictionary must be UTF-8 without a BOM');
Lines := Text.Split([#10]);
Last := High(Lines);
if (Last >= 0) and (Lines[Last] = '') then
Dec(Last); // newline la finalul fișierului
SetLength(Result, Last + 3);
Result[0] := ''; // clasa 0: CTC blank
for I := 0 to Last do
begin
Entry := Lines[I];
if (Entry <> '') and (Entry[Length(Entry)] = #13) then
SetLength(Entry, Length(Entry) - 1); // CRLF: aruncă doar CR-ul
if (Entry = '') or (Pos(#9, Entry) > 0) then
raise EArgumentException.Create('Invalid dictionary entry');
Result[I + 1] := Entry; // niciodată Trim: ' ' e o clasă
end;
Result[Last + 2] := ' '; // clasa finală: spațiu
// Length(Result) trebuie să egaleze numărul de clase de output al modelului
end;
Ce face de fapt decodarea CTC greedy?
Decodarea CTC greedy alege clasa cu scorul cel mai mare la fiecare time step, colapsează repetițiile consecutive într-un singur caracter și aruncă clasa blank; blank-ul e ceea ce permite literelor dublate cu adevărat să supraviețuiască. Un model de recunoaștere privește o linie de text ca o secvență de felii verticale înguste, iar pentru fiecare felie, sau time step, emite o probabilitate pentru fiecare clasă. O linie care conține AA中 ar putea produce secvența argmax A A blank A 中 space. Colapsarea primilor doi pași A dă un singur A, blank-ul îl separă de următorul A, iar rezultatul e AA中 cu spațiul final intact. Fără regula blank, book și bok ar fi de nedistins
Fiindcă decoder-ul are doar o duzină de linii, e ușor să greșești frontierele, iar eșecurile sunt tăcute. Dacă bucla argmax interioară se oprește cu o clasă mai devreme, clasa de spațiu nu poate câștiga niciodată și fiecare linie revine fără spațiere de cuvinte, ceea ce strică căutarea de fraze pe paginile engleze și latine. Dacă bucla exterioară se oprește cu un time step mai devreme, ultimul caracter al fiecărei linii dispare, ceea ce pentru o linie scurtă poate fi o treime din text. Iar dacă garda de repetiție nu e resetată de un blank, caracterele dublate precum ll sau reduplicările chinezești precum 谢谢 se colapsează într-unul. Decoder-ul HotPDF include ultima clasă și ultimul time step, păstrează repetițiile separate de blank și respinge în plus scorurile care nu sunt finite sau ies din 0 până la 1, precum și orice număr de clase care nu se potrivește cu dicționarul. Iată aceeași logică ca ilustrație Pascal
// Doar ilustrație: decodare CTC greedy cu frontiere corecte.
// Scores ține Steps * Classes probabilități, câte un rând per time step
function GreedyCTCDecode(const Scores: array of Single;
Steps, Classes: Integer; const Characters: array of string): string;
var
Step, C, Best, Previous: Integer;
BestScore: Single;
begin
if (Classes < 3) or (Length(Characters) <> Classes) or
(Length(Scores) <> Steps * Classes) then
raise EArgumentException.Create('Model output does not match the dictionary');
Result := '';
Previous := 0; // clasa 0 e CTC blank
for Step := 0 to Steps - 1 do // include ultimul time step
begin
Best := 0;
BestScore := Scores[Step * Classes];
for C := 1 to Classes - 1 do // include ultima clasă (spațiu)
if Scores[Step * Classes + C] > BestScore then
begin
Best := C;
BestScore := Scores[Step * Classes + C];
end;
if (Best <> 0) and (Best <> Previous) then
Result := Result + Characters[Best];
Previous := Best; // un blank resetează garda de repetiție
end;
end;
Decodarea greedy nu e cea mai precisă strategie CTC disponibilă; beam search cu un model de limbă poate repara unele felii ambigue. Pentru documente tipărite la 300 DPI rezultatul greedy e de obicei tot ce modelul are de oferit, iar decoder-ul nu e locul unde se compensează slăbiciunile modelului. Modelul latin PP-OCRv3, de pildă, poate citi ñ ca n chiar și pe input curat. HotPDF nu acoperă asta cu înlocuiri de caractere în post-procesare, pentru că o tabelă de substituție care repară spaniola strică altceva, iar un caracter greșit într-un strat căutabil e mai rău decât un miss sincer
Cum ordonează HotPDF liniile de text, inclusiv araba de la dreapta la stânga?
HotPDF sortează casetele de text detectate de sus în jos, grupează casetele într-un rând când se suprapun vertical cu cel puțin jumătate din înălțimea casetei mai mici și ordonează fiecare rând de la stânga la dreapta, sau de la dreapta la stânga când RightToLeft e activat; caracterele din interiorul fiecărei linii recunoscute nu sunt niciodată inversate. Gruparea contează pentru că un detector adesea sparge o linie vizuală în mai multe casete, de exemplu o etichetă și o valoare separate de un spațiu lat, iar un sortare pură după coordonata de sus le-ar intercala cu linia vecină ori de câte ori vârfurile lor diferă cu un pixel sau doi
Presetarea arabă setează RightToLeft := True, ceea ce îi spune DLL-ului să ordone casetele din fiecare rând după latura lor dreaptă, de la marginea dreaptă spre interior. Asta e tot efectul. Textul pe care modelul îl întoarce pentru o linie e deja în ordine logică Unicode, ordinea în care un cititor de arabă citește și tastează, și aceea e și ordinea pe care o așteaptă extragerea și căutarea de text PDF. Inversarea mecanică a șirului ca să „pară corect” într-un debugger ar rupe căutarea, copierea, lipirea și screen reader-ele. Afișarea bidirecțională și modelarea glyph-urilor sunt treaba viewer-ului
Un motor servește un profil de limbă. Nu există detecție automată de scriere, deci un document care amestecă scrieri are nevoie de câte un motor per profil, aplicat paginilor care îl folosesc. Pentru că ApplyLoadedOCRTextLayer primește o listă explicită de pagini și comite fiecare apel ca propria lui tranzacție tot-sau-nimic, asta e simplu de făcut
uses
SysUtils, HPDFDoc, HPDFRapidOCRRecognition;
function CreateRapidEngine(const Tag: string): IHPDFOCREngine;
var
Models: THPDFRapidOCRDLLOptions;
begin
// ridică EArgumentException pentru un tag necunoscut, înainte să se încarce vreun model
Models := THPDFRapidOCRDLLOptions.ForLanguage(Tag);
Models.MaxPixels := 33554432; // loc pentru pagini A3 la 300 DPI
Result := HPDFCreateRapidOCRDLLOCREngine(
'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models', Models);
end;
procedure OCRMixedArchive(Doc: THotPDF);
var
Chinese, Arabic: IHPDFOCREngine;
Layer: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
Chinese := CreateRapidEngine('zh-TW'); // profilul chinese_cht
Arabic := CreateRapidEngine('ar-SA'); // profilul arabic, RightToLeft = True
Layer := THPDFOCRTextLayerOptions.Default;
if not Doc.ApplyLoadedOCRTextLayer([0, 1, 2], Chinese, Layer, Info) then
raise Exception.Create(string(Info.Diagnostic));
if not Doc.ApplyLoadedOCRTextLayer([3], Arabic, Layer, Info) then
raise Exception.Create(string(Info.Diagnostic));
end;
Linia MaxPixels e acolo cu un motiv. Opțiunile DLL au implicit 16.777.216 de pixeli per cerere, ceea ce acoperă confortabil A4 și US Letter la 300 DPI, dar o pagină A3 la 300 DPI are cam 3508 pe 4961 de pixeli, cam 17,4 milioane, iar cererea e refuzată ca peste buget. Ridicați MaxPixels (plafonul e 67.108.864) sau coborâți THPDFOCRTextLayerOptions.DPI pentru formatele mari. Ordinea de la dreapta la stânga folosește exportul opțional HPDFRapidOCRSetReadingDirection al versiunii ABI 1; adapter-ul îl cere doar când RightToLeft e setat, deci un DLL mai vechi continuă să servească limbile de la stânga la dreapta și eșuează la crearea motorului cu un EArgumentException care numește exportul lipsă pentru arabă
De ce modelele OCR mai noi eșuează la încărcare?
DLL-ul RapidOCR din HotPDF leagă un ONNX Runtime static 1.14, care nu poate citi modele salvate cu versiunea IR 10 a ONNX, iar exporturile mai noi precum modelele PP-OCRv5 pot cere un runtime mai nou decât atât; un asemenea model eșuează la crearea motorului cu un diagnostic nativ. Constrângerea aceasta e motivul pentru care pachetele de limbă sunt fixate la perechi specifice de recunoscător și dicționar PP-OCRv3 și PP-OCRv4 în loc de „cel mai nou” și de ce tabelul de mai sus amestecă cele două generații: fiecare pereche fixată e una care se încarcă și se verifică sub runtime-ul acela
Instalatorul impune perechile. Fiecare fișier din manifestul lui cară un hash SHA256, un fișier existent cu alt hash oprește instalarea în loc să fie suprascris, iar fiecare descărcare aterizează sub un nume temporar și se mută la loc doar după ce hash-ul lui se potrivește. Asta protejează contra versiunii tăcute a problemei de dicționar: cineva aruncă de mână un recognition.onnx mai nou într-un folder de profil, numărul de clase se întâmplă să se potrivească, iar nimic nu eșuează până când un client raportează căutarea care nu găsește cuvinte pe care le vede clar. La runtime adapter-ul rămâne offline și nu aduce niciodată un model lipsă. Recunoscătorul validează și forma modelului la încărcare, acceptând input NCHW cu o înălțime fixă de 32 sau 48 de pixeli sau o înălțime dinamică, pe care o rulează la 48
Dacă aveți nevoie de o scriere pe care niciunul dintre cele nouă profile nu o acoperă, puteți arăta în continuare RecognitionModel și CharacterDictionary către fișierele proprii. Se aplică aceleași verificări, iar asta e chiar ideea: o pereche nepotrivită eșuează la inițializare, nu în arhiva clientului dvs. Pentru paginile unde niciun profil RapidOCR nu se potrivește, adapter-ul Tesseract pentru PDF căutabil se prinde în același apel ApplyLoadedOCRTextLayer, iar pentru formulare ASCII tipărite la mașină motorul OCR integrat de potrivire prin șabloane nu are nevoie de modele deloc
Referință rapidă: lista de verificare RapidOCR multilingv
- Creați opțiunile cu
THPDFRapidOCRDLLOptions.ForLanguageși tratațiEArgumentExceptionca un tag nesuportat, nu ca o defecțiune de runtime - Schimbați
RecognitionModelșiCharacterDictionaryîmpreună, niciodată unul singur; numărări de clase egale nu dovedesc o ordine de caractere egală - Păstrați dicționarele ca UTF-8 fără BOM, nu tăiați niciodată intrările și așteptați-vă ca modelul să aibă N + 2 clase: blank, N intrări, spațiu
- Un decoder CTC personalizat trebuie să acopere ultima clasă și ultimul time step și să păstreze repetițiile separate de un blank
- Folosiți un motor per profil de limbă și pasați liste explicite de pagini pentru documentele cu scrieri mixte
RightToLeftschimbă doar ordinea casetelor; textul recunoscut rămâne în ordine logică Unicode- Instalați modelele cu
Install-RapidOCRModels.ps1astfel încât fixările SHA256 să țină perechea model-dicționar; setațiUseAngleClassifier := Falsedacă ați instalat cu-SkipClassifier - Ridicați
MaxPixelspeste implicitul de 16.777.216 înainte să rulați pagini A3 sau mai mari la 300 DPI
Presetările de limbă RapidOCR, adapter-ul DLL nativ și pipeline-ul de strat de text OCR fac parte din HotPDF Delphi PDF Component pentru Delphi, C++Builder și FPC/Lazarus Windows, începând cu v2.775.0 pentru profilele multilingve