HotPDF dělá čínské a vícejazyčné OCR v Delphi přes svůj nativní RapidOCR DLL adapter: THPDFRapidOCRDLLOptions.ForLanguage namapuje jazykovou značku jako 'zh-CN', 'zh-TW', 'ru' nebo 'ar' na odpovídající rozpoznávací model a znakový slovník a THotPDF.ApplyLoadedOCRTextLayer z rozpoznaných řádků udělá neviditelnou, prohledávatelnou Unicode textovou vrstvu na naskenovaných PDF stránkách
Rozchodit demo pro latinku je ta snadná část. Zajímavé selhání začínají, když přepnete na tradiční čínštinu nebo ruštinu a výstup se změní v sebejistý, formálně dokonalý nesmysl, když každý řádek potichu ztratí poslední znak, nebo když arabská stránka přijde s textovými boxy ve špatném pořadí. Ani jedno z toho samo od sebe nevyhodí výjimku. Jazykové presety přidané v HotPDF v2.775.0 existují hlavně proto, aby tyhle mezery zavřely, a čtyři pasti dole stojí za pochopení, i kdybyste se nativního kódu nikdy nedotkli, protože každá z nich vysvětluje příznak, po kterém byste jinak mohli den pátrat
Jak ForLanguage vybírá model a slovník?
THPDFRapidOCRDLLOptions.ForLanguage převede značku na jeden z devíti profilů a vrátí nastavení mířící na <profile>/recognition.onnx a <profile>/dictionary.txt pod vaším adresářem modelů, přičemž ponechá sdílený detektor, volitelný angle classifier a defaulty pro vlákna, pixely i timeout z THPDFRapidOCRDLLOptions.Default. Metoda převede značku na malá písmena, podtržítka změní na spojovníky a ořízne okolní bílé znaky, takže 'zh_TW', 'ZH-tw' i ' zh-tw ' skončí u téhož profilu. Aliasy jsou výčtový seznam, ne shoda prefixu: 'zh-Hant-TW' se přijme, protože je na seznamu, zatímco libovolná neuvedená regionální varianta vyhodí EArgumentException, dřív než se kterýkoli model nahraje
| Profil | Jazyky | Ukázkové značky | Pevný model |
|---|---|---|---|
ch | Zjednodušená čínština a angličtina | zh, zh-CN, zh-Hans, chi_sim | PP-OCRv4 |
chinese_cht | Tradiční čínština | zh-TW, zh-HK, zh-Hant, chi_tra | PP-OCRv3 |
en | Angličtina | en, en-US, en-GB, eng | PP-OCRv4 |
latin | Francouzština, němčina, španělština, portugalština, italština, nizozemština, turečtina | fr, de, es-419, pt-BR, tr | PP-OCRv3 |
japan | Japonština | ja, ja-JP, jpn | PP-OCRv4 |
korean | Korejština | ko, ko-KR, kor | PP-OCRv4 |
cyrillic | Ruština, ukrajinština, bulharština, běloruština | ru, ru-RU, uk, bg | PP-OCRv3 |
arabic | Arabština, perština, urdu | ar, ar-SA, fa, ur | PP-OCRv4 |
devanagari | Hindština, maráthština, nepálština | hi, mr, ne | PP-OCRv4 |
Adapter sám o sobě nic nestahuje. Soubory jednou připravíte přibaleným helperem, například tools/Install-RapidOCRModels.ps1 -Destination C:/OCR/models -Language ch,chinese_cht,cyrillic (nebo -Language All pro všech devět profilů), a helper umístí sdílený detektor a klasifikátor do kořenových názvů souborů, které očekává Default. Potom je sken ve zjednodušené čínštině prohledávatelný za pár řádků. Engine se zapojuje přes totéž rozhraní IHPDFOCREngine, které popisuje článek o in-process RapidOCR DLL a její hranici ABI, takže tenhle zůstává zaměřený na jazyky
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, sdílený detektor a klasifikátor
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
// prázdný seznam stránek znamená všechny stránky; stránky, které už text mají, se přeskakují
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;
Dva detaily v tomhle výstupu stojí za poznámku. Nativní pipeline vrací jeden výsledek na detekovaný textový řádek, ne na slovo, takže AcceptedWordCount počítá tady řádky a MinimumConfidence se porovnává s průměrnou confidence znaků celého řádku: řádek s průměrem 0,45 se zahodí jako celek. UniqueScalarCount hlásí, kolik různých Unicode skalárů musela textová vrstva namapovat do svého fontu a tabulky ToUnicode, což je užitečná kontrola, že CJK text skutečně dorazil a ne jen hrstka latinských fallbacků. Interface engine udržujte naživu napříč dokumenty, protože inicializace modelů probíhá v továrně a je to ta nejdražší část
Proč samotná výměna rozpoznávacího modelu produkuje nesmysly?
CTC rozpoznávací model nikdy nevypisuje znaky, jen indexy tříd, a slovník je jediná věc, která z indexu 1 204 udělá glyf. Vyměňte ch/recognition.onnx za cyrillic/recognition.onnx, ale ponechte čínský slovník, a model s nadšením vysílá validní cyrilské indexy, které starý slovník překládá na náhodné znaky Han. Výsledek vypadá jako text, projde validací UTF-8 a vyhledávání v něm nenajde vůbec nic. Proto ForLanguage vždy nastavuje RecognitionModel a CharacterDictionary společně a ručně poskládané nastavení by nikdy nemělo měnit jedno bez druhého
Zjevná bezpečnostní kontrola, srovnání velikosti slovníku se šířkou výstupu modelu, je nutná, ale nestačí. Dva slovníky můžou mít stejný počet položek v jiném pořadí a off-by-one v pořadí posune každý znak o jeden code point. HotPDF proto kontroluje při inicializaci modelu v továrně ve dvou krocích. Za prvé, počet výstupních tříd se musí rovnat položkám slovníku plus dva. Za druhé, když má ONNX soubor v metadatech seznam character, porovná se s ním každá položka slovníku po pořadě a neshoda ukončí inicializaci s EInvalidOperation a nativní diagnostikou místo toho, aby později produkovala věrohodný nesmysl
„Plus dva" pochází z rozložení tříd. Třída 0 je CTC blank, třídy 1 až N jsou řádky slovníku v pořadí podle souboru a poslední třída je mezera. Některé slovníky nesou i vlastní položku mezery a tenhle řádek musí zůstat přesně takový, jaký je. Tady napáchá dobromyslný Trim skutečné škody: z položky s jedinou mezerou udělá prázdný řetězec a posune nebo rozbije tabulku. Jediná bezpečná normalizace je odstranění koncového carriage return, takže slovník uložený s konci řádků CRLF se nahraje správně, zatímco UTF-8 byte order mark, prázdný řádek nebo položka obsahující tabulátor se odmítnou. Náčrt dole ukazuje rozložení v Pascalu; je to vysvětlující kód, ne API HotPDF
// Jen ilustrace: tabulka tříd, kterou CTC recognizer očekává
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); // konec řádku na konci souboru
SetLength(Result, Last + 3);
Result[0] := ''; // třída 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: ubere se jen CR
if (Entry = '') or (Pos(#9, Entry) > 0) then
raise EArgumentException.Create('Invalid dictionary entry');
Result[I + 1] := Entry; // nikdy Trim: ' ' je třída
end;
Result[Last + 2] := ' '; // poslední třída: mezera
// Length(Result) se musí rovnat počtu výstupních tříd modelu
end;
Co vlastně dělá greedy CTC dekódování?
Greedy CTC dekódování v každém časovém kroku vybere třídu s nejvyšším skóre, sloučí po sobě jdoucí opakování do jednoho znaku a blank třídu zahodí; právě blank totiž umožňuje, aby opravdu zdvojené písmeno přežilo. Rozpoznávací model se na textový řádek dívá jako na sekvenci úzkých svislých řezů a pro každý řez, tedy časový krok, vypíše pravděpodobnost pro každou třídu. Řádek obsahující AA中 může produkovat argmax sekvenci A A blank A 中 space. Sloučení prvních dvou kroků A dá jedno A, blank ho oddělí od dalšího A a výsledkem je AA中 s koncovou mezerou zachovanou. Bez pravidla blanku by book a bok byly nerozlišitelné
Když má dekodér jen tucet řádků, snadno se pokazí hranice a selhání jsou tichá. Skončí-li vnitřní argmax smyčka o třídu dřív, třída mezera nemůže nikdy vyhrát a každý řádek se vrátí bez mezer mezi slovy, což rozbije vyhledávání frází na anglických a latinských stránkách. Skončí-li vnější smyčka o časový krok dřív, zmizí poslední znak každého řádku, což u krátkého řádku může být třetina textu. A pokud hlídač opakování nerestuje blank, sloučí se zdvojené znaky jako ll nebo čínské reduplikace jako 谢谢 do jednoho. Dekodér HotPDF zahrnuje poslední třídu i poslední časový krok, zachovává opakování oddělená blankem a navíc odmítá skóre, které není konečné nebo leží mimo rozsah 0 až 1, a jakýkoli počet tříd, který neodpovídá slovníku. Tady je tatáž logika jako ilustrace v Pascalu
// Jen ilustrace: greedy CTC dekódování se správnými hranicemi.
// Scores drží Steps * Classes pravděpodobností, jeden řádek na časový krok
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; // třída 0 je CTC blank
for Step := 0 to Steps - 1 do // zahrň i poslední časový krok
begin
Best := 0;
BestScore := Scores[Step * Classes];
for C := 1 to Classes - 1 do // zahrň i poslední třídu (mezera)
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; // blank resetuje hlídač opakování
end;
end;
Greedy dekódování není nejpřesnější dostupná CTC strategie; beam search s jazykovým modelem zachrání pár nejednoznačných řezů. U tištěných dokumentů v 300 DPI je greedy výsledek obvykle to nejlepší, co model nabízí, a dekodér není místo, kde by se kompenzovaly slabosti modelu. Latinský model PP-OCRv3 umí třeba přečíst ñ jako n, i na čistém vstupu. HotPDF to nepřetahuje post-processingem s náhradami znaků, protože substituční tabulka, která spraví španělštinu, rozbije něco jiného a špatný znak v prohledávatelné vrstvě je horší než čestné selhání
Jak HotPDF řadí textové řádky, včetně arabského zápisu zprava doleva?
HotPDF řadí detekované textové boxy shora dolů, sdružuje boxy do řádku, když se svisle překrývají alespoň o polovinu výšky menšího boxu, a každý řádek seřadí zleva doprava, případně zprava doleva, když je zapnuté RightToLeft; znaky uvnitř každého rozpoznaného řádku se nikdy neobracejí. Na sdružování záleží, protože detektor často rozdělí jeden vizuální řádek do několika boxů, třeba jmenovku a hodnotu oddělené širokou mezerou, a čisté řazení podle horní souřadnice by je propletlo se sousedním řádkem pokaždé, když se jejich horní hrany liší o pixel či dva
Arabský preset nastaví RightToLeft := True, čímž řekne DLL, aby boxy v každém řádku srovnala podle pravé hrany, od pravého okraje dovnitř. To je celý efekt. Text, který model pro řádek vrátí, už je v logickém pořadí Unicode, v pořadí, v jakém ho arabský čtenář čte a píše, a přesně toto pořadí očekává extrakce textu z PDF i hledání. Mechanické obrácení řetězce, aby v debuggeru „vypadal správně", rozbije hledání, kopírování, vkládání i screen readery. Obousměrné zobrazení a tvarování glyfů je práce vieweru
Jeden engine obsluhuje jeden jazykový profil. Automatická detekce písma neexistuje, takže dokument míchající skripty potřebuje jeden engine na profil, aplikovaný na stránky, které ho používají. Protože ApplyLoadedOCRTextLayer bere explicitní seznam stránek a každé volání commituje jako vlastní all-or-nothing transakci, je to přímočaré
uses
SysUtils, HPDFDoc, HPDFRapidOCRRecognition;
function CreateRapidEngine(const Tag: string): IHPDFOCREngine;
var
Models: THPDFRapidOCRDLLOptions;
begin
// vyhodí EArgumentException pro neznámou značku, dřív než se cokoli nahraje
Models := THPDFRapidOCRDLLOptions.ForLanguage(Tag);
Models.MaxPixels := 33554432; // prostor pro A3 stránky v 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'); // profil chinese_cht
Arabic := CreateRapidEngine('ar-SA'); // profil 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;
Řádek MaxPixels tam není náhodou. Nastavení DLL defaultuje na 16 777 216 pixelů na požadavek, což A4 i US Letter v 300 DPI pokryje s rezervou, ale A3 stránka v 300 DPI má zhruba 3 508 × 4 961 pixelů, kolem 17,4 milionu, a požadavek se odmítne jako přes rozpočet. Zvyšte MaxPixels (strop je 67 108 864) nebo pro velké formáty snižte THPDFOCRTextLayerOptions.DPI. Řazení zprava doleva používá volitelný export HPDFRapidOCRSetReadingDirection z ABI verze 1; adapter ho vyžaduje jen tehdy, když je nastavené RightToLeft, takže starší DLL dál obsluhuje jazyky zleva doprava a u vytváření engine selže s EArgumentException, která jmenuje chybějící export pro arabštinu
Proč se novější OCR modely nedaří nahrát?
HotPDF RapidOCR DLL linkuje statický ONNX Runtime 1.14, který neumí číst modely uložené s ONNX IR verzí 10, a novější exporty jako modely PP-OCRv5 můžou vyžadovat novější runtime; takový model selže už při vytváření engine s nativní diagnostikou. Právě tohle omezení je důvod, proč jsou jazykové balíčky přichycené ke konkrétním párům recognizer a slovník PP-OCRv3 a PP-OCRv4 místo „latest", a proč tabulka výše míchá obě generace: každý přichycený pár je takový, který se pod tímto runtimem nahraje a ověří
Instalátor pár vynucuje. Každý soubor v manifestu má SHA256 hash, existující soubor s jiným hashem instalaci zastaví místo aby se přepsal a každé stažení se nejdřív uloží pod dočasným názvem a na své místo se přesune, až hash sedne. Tím se chrání před tichou verzí problému se slovníkem: někdo ručně odloží novější recognition.onnx do složky profilu, počet tříd náhodou sedí a nic neselže, dokud zákazník nenahlásí, že hledání nenajde slova, která vidí na vlastní oči. Za běhu zůstává adapter offline a chybějící model nikdy nestáhne. Recognizer také validuje tvar modelu při nahrání, přijímá NCHW vstup s pevnou výškou 32 nebo 48 pixelů nebo dynamickou výškou, kterou provozuje na 48
Pokud potřebujete písmo, které žádný z devíti profilů nepokrývá, můžete RecognitionModel a CharacterDictionary i tak nasměrovat na vlastní soubory. Platí tytéž kontroly, a o to jde: nespárovaný pár selže při inicializaci, ne v archivu vašeho zákazníka. Pro stránky, kde se nehodí žádný profil RapidOCR, se do téhož volání ApplyLoadedOCRTextLayer zapojí Tesseract adapter pro prohledávatelné PDF a pro strojově tištěné ASCII formuláře vystačí vestavěný template-matching OCR engine, který nepotřebuje žádné modely
Rychlá reference: checklist vícejazyčného RapidOCR
- Vytvářejte nastavení přes
THPDFRapidOCRDLLOptions.ForLanguagea berteEArgumentExceptionjako nepodporovanou značku, ne jako runtime chybu - Měňte
RecognitionModelaCharacterDictionaryspolečně, nikdy jedno bez druhého; stejné počty tříd nedokazují stejné pořadí znaků - Slovníky uchovávejte jako UTF-8 bez BOM, položky nikdy neorezávejte a počítejte s tím, že model má N + 2 třídy: blank, N položek, mezera
- Vlastní CTC dekodér musí pokrýt poslední třídu i poslední časový krok a nechávat opakování oddělená blankem
- Používejte jeden engine na jazykový profil a pro dokumenty míchající skripty předávejte explicitní seznamy stránek
RightToLeftmění jen pořadí boxů; rozpoznaný text zůstává v logickém pořadí Unicode- Instalujte modely přes
Install-RapidOCRModels.ps1, aby SHA256 piny držely pár model-slovník; nastavteUseAngleClassifier := False, pokud jste instalovali s-SkipClassifier - Zvyšte
MaxPixelsnad defaultních 16 777 216, než spustíte A3 nebo větší stránky v 300 DPI
Jazykové presety RapidOCR, nativní DLL adapter i pipeline OCR textové vrstvy jsou součástí HotPDF Delphi PDF Component pro Delphi, C++Builder a Windows FPC/Lazarus, vícejazyčné profily od v2.775.0