HotPDF mění naskenované stránky PDF na prohledávatelné PDF s Tesseractem přes HPDFCreateTesseractOCREngine, factory obalující lokálně nainstalovaný spustitelný soubor Tesseract jako IHPDFOCREngine. Tenhle engine podejte do ApplyLoadedOCRTextLayer, které vyrenderuje každou stránku, pustí Tesseract jednou na stránku, parsuje jeho word-level TSV výstup a commitne neviditelnou Unicode textovou vrstvu pro všechny požadované stránky v jedné transakci, nebo pro žádnou
Důvod existence tohohle adapteru je rozsah. Vestavěný OCR engine porovnávající šablony je záměrně úzký: tiskem čitelná ASCII písmena a číslice, nic víc. Faktury s diakritikou v jménech, čínské smlouvy a vícejazyčné archivy potřebují skutečný recognizer s natrénovanými jazykovými modely a Tesseract je očividný kandidát, protože je to command-line program, který si můžete zaopatřit vedle své aplikace. Volání externího programu z dokumentové knihovny zní triviálně. Není, a většina zajímavého kódu v adapteru je o tom, co se stane, když se program zblázní, zasekne, dostane cancellation, nebo zdědí věci, které nikdy vidět nemá
Jak řídí HotPDF Tesseract z Delphi aplikace?
HotPDF pouští Tesseract jako skrytý child proces na stránku, podstrkává mu vyrenderovanou bitmapu a čte zpátky soubor TSV a výsledek vystavuje přes stejné rozhraní IHPDFOCREngine, jaké používá vestavěný engine. Nic downstream se nemění: mapování souřadnic, obsluha rotace, Unicode validace, filtrování podle confidence a atomický commit jsou text-layer pipeline, kterou už máte. Factory bydlí v unitě HPDFTesseractRecognition a validuje hned: spustitelný soubor musí existovat, adresář tessdata musí existovat, timeout musí být mezi 1 a 3 600 000 milisekundami a identifikátor jazyka smí obsahovat jen ASCII písmena, číslice, _ a +. Ta poslední kontrola záleží, protože jazykový řetězec skončí na command line a eng+chi_sim je legitimní hodnota Tesseractu, zatímco cokoli s uvozovkami nebo mezerami není
uses
SysUtils, HPDFTypes, HPDFDoc, HPDFTesseractRecognition;
procedure MakeSearchable(const SourceFile, TargetFile: string;
Token: THPDFCancellationToken);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// vyhazuje EArgumentException za chybějící spustitelný soubor, chybějící tessdata,
// špatný identifikátor jazyka nebo timeout mimo 1..3600000 ms
Engine := HPDFCreateTesseractOCREngine(
'C:\OCR\Tesseract\tesseract.exe',
'C:\OCR\Tesseract\tessdata',
'eng+chi_sim', // několik modelů spojených '+'
120000); // limit na stránku, default je 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;
// prázdný seznam stránek znamená každou stránku; stránky s textem se defaultně přeskočí
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;
Pro každou stránku vytvoří Recognize soukromý adresář pod temp cestou pojmenovaný HotPDF-OCR-{GUID}, uloží vyrenderovanou bitmapu jako input.bmp a spustí tesseract input.bmp output --tessdata-dir … -l … --dpi N --psm 3 -c tessedit_create_tsv=1, s každým path argumentem v uvozovkách podle Windows pravidel escapování command line pro zpětná lomítka a vložené uvozovky. Hodnota --dpi je render DPI z THPDFOCRTextLayerOptions.DPI, takže Tesseract nikdy nemusí hádat rozlišení z metadat obrázku a --psm 3 žádá plně automatickou segmentaci stránky. Engine se hlásí jako Tesseract (local CLI), což je to, co přistane v Info.EngineName. Tesseract a jeho jazykové modely nejsou součástí balíčku HotPDF; nainstalovat je je úkol aplikace
Proč je TSV parser tak přísný?
TSV parser v HotPDF nechá padnout celou stránku na jakémkoli deformovaném řádku, protože částečně rozparsovaný seznam slov produkuje textovou vrstvu, která se potichu rozejde s obrázkem. TSV výstup Tesseractu má fixní dvanáctisloupcovou hlavičku, od level po text, a HotPDF porovná první řádek s tou přesnou hlavičkou po oříznutí volitelné byte order mark. Každý další řádek se musí rozdělit na přesně dvanáct polí a dělení se zastaví po jedenáctém tabu, takže tab uvnitř rozpoznaného textu zůstane součástí slova místo vytvoření třináctého sloupce. Slova jsou jen řádky level 5; levely 1 až 4 popisují stránky, bloky, odstavce a řádky a přeskočí se. Řádky level 5 s prázdným nebo čistě bílým textem se přeskočí taky, protože prázdné slovo má box, ale nic k lokalizování ani hledání. Všechno ostatní se kontroluje tvrdě: celočíselná geometrie, confidence parsovaná invariantním formátem en-US, takže německé locale nečte 93.5 jako nesmysl, box ležící celý uvnitř bitmapy a confidence mezi 0 a 100. Jedno selhání vyhodí výjimku, engine vrátí False a pole slov se vynuluje. Regresní testy zahrnují přesně tenhle případ: jedno validní slovo následované rozbitým řádkem musí dát nula slov, ne jedno
// zhuštěno ze smyčky level-5 v 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; // řádky page/block/paragraph/line
WordText := Fields[11];
if Trim(WordText) = '' then Continue; // slova z bílých znaků nemají pozici
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 očekává 0..1
Ten poslední řádek interaguje s defaultem, který byste nemuseli čekat. Confidence Tesseractu běží od 0 do 100, pipeline pracuje v 0 až 1 a THPDFOCRTextLayerOptions.MinimumConfidence defaultně stojí na 0.5, takže jakékoli Tesseract slovo pod 50 se započítá do Info.DroppedWordCount a na stránku nikdy nedojde. Na čistém skenu 300 DPI je to rozumná podlaha. Na zašuměném faxu to může upustit překvapivou část stránky a správný krok je podívat se na počet upuštěných, než snížíte práh, protože slova s nízkou confidence jsou přesně ta, která jsou nejvíc pravděpodobně špatně
Co child proces Tesseractu dědí?
Child proces Tesseractu dědí z HotPDF přesně dva handly: NUL handle pro standardní vstup a výstup a file handle pro standardní chybu. Ta přesnost je pointa. CreateProcess s bInheritHandles = True je způsob, jak předat standardní handly dítěti, ale sám o sobě předá každý dědičný handle v host procesu, včetně souborů, pipes a eventů otevřených nesouvisejícím kódem vaší aplikace. Dítě pak ty objekty drží naživu, dokud neskončí, takže soubor zůstane zamčený nebo pipe nikdy neuvidí svůj konec, zatímco Tesseract mele stránku. HotPDF tu mezeru zavírá rozšířeným startup záznamem: STARTUPINFOEX, attribute list nesoucí PROC_THREAD_ATTRIBUTE_HANDLE_LIST a creation flag EXTENDED_STARTUPINFO_PRESENT. S handle listem na místě musí být bInheritHandles pořád True, ale hranici překročí jen vyjmenované handly. Stejné uvažování v kontejnmentu řídí izolaci PDF image kodeků ve worker procesech, kde je dítě nedůvěryhodný kód; tady je dítě důvěryhodné, ale host není jediným majitelem vlastní handle tabulky
// konstanty ukázané jménem; zdroj předává jejich číselné hodnoty
// oba handly se vytvářejí s bInheritHandle = True
InheritedHandles[0] := NullHandle; // stdin a stdout
InheritedHandles[1] := ErrorHandle; // stderr.txt v soukromém adresáři
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, // vyžadováno handle listem
CREATE_NO_WINDOW or EXTENDED_STARTUPINFO_PRESENT,
nil, PChar(DirectoryName), Startup.StartupInfo, ProcessInfo);
Proč může zrušený OCR běh vypadat jako selhání engine?
Zrušený OCR běh vypadá jako selhání enginu, protože IHPDFOCREngine.Recognize vrací jediný Boolean a False znamená obojí, „Tesseract selhal“ i „uživatel stiskl Cancel“. Adapter polluje cancellation token i timeout každých 25 milisekund, dokud dítě běží, a když token zaklapne, vyhodí výjimku uvnitř Recognize, chytne vlastní výjimku, uklidí a vrátí False s diagnostikou. Kdyby pipeline brala tohle jako chybu enginu, volající by viděl otlsEngineError pro zakázku, kterou uživatel záměrně zastavil. ApplyLoadedOCRTextLayer proto kontroluje token jako první, kdykoli Recognize vrátí False, a výsledek převede na chybu enginu jen tehdy, když token nastaven nebyl. Tohle pořadí chrání kontrakt více stránek: rozpoznání, validace, účtování budgetu a stavba obsahu běží pro každou požadovanou stránku, než se otevře grafová transakce, takže cancellation na stránce 40 z 50 nahlásí otlsCancelled a nechá dokument, včetně prvních 39 stránek, nedotčený. Částečně prohledávatelný soubor na vysvětlování později neexistuje a zbytek obsluhy selhání následuje týž ohraničený styl:
- Timeout je na jedno volání
Recognize, měřený od jeho startu, takže defaultních 60 000 ms platí pro každou stránku, ne pro celý dokument - Dítě, které ještě běží na timeoutu nebo cancellation, se ukončí, počká se na něj nejvýš 5 sekund a jeho soukromý adresář se smaže v bloku
finally output.tsvje zastropováno na 64 MiB astderr.txtna 1 MiB, kontrolované za běhu dítěte i po jeho skončení- Počet slov a UTF-16 code units se zastropovává na stránku zbývajícími budgety
MaxWordsPerPage,MaxTotalWordsaMaxTextCodeUnitsa jejich překročení nechá běh padnout, místo useknutí seznamu slov - Standardní výstup jde do
NUL, protože Tesseract zapisujeoutput.tsv, zatímco standardní chyba jde do souboru, takže nenulový exit kód se hlásí s nejvýš 4 096 znaky vlastní stížnosti enginu, což bývá nejrychlejší cesta, jak zjistit, že chybí soubor.traineddata
Jak se rozpoznaná slova stanou neviditelnou textovou vrstvou
HotPDF zapisuje slova Tesseractu jako neviditelný text s text rendering mode 3, mód ani výplň ani tah z ISO 32000-1 §9.3.6, takže stránka pořád ukazuje naskenovaný obrázek, zatímco hledání a kopírování pracují na rozpoznaných slovech. Content stream otvírá BT s 3 Tr a každé slovo dostane matici Tm na své baselině, velikost fontu odvozenou z výšky boxu v pixelech při render DPI a Tz horizontální škálu, která natáhne glyph běh na změřenou šířku boxu, a proto se search highlight trefí na slovo v obrázku, místo aby po něm táhlo
TSV Tesseractu má boxy, ale ne baseline, takže adapter hlásí každé slovo bez ní a pipeline odhaduje baseline na jedné pětině výšky boxu nad spodní hranou. Samotný text jde přes sdílený nevložený Type0 font s kódováním Identity-H a generovanou CMap ToUnicode, jedno CID na každý odlišný Unicode scalar napříč celým během, a proto přežijí kopírování i hledání čínština, latinka s diakritikou i znaky ze supplementary plane. Tenhle design má dva limity, které stojí za zmínku rovnou: jeden běh nese nejvýš 65 535 odlišných scalarů a nevložený font nesplňuje požadavek vkládání fontů z ISO 19005, takže výstup PDF/A potřebuje zvlášť vložený konformní font. Kontrola výsledku je jednoduchá a stojí za automatizaci: uložte, znovu načtěte a pusťte obyčejnou textovou cestu načteného dokumentu z článku o extrakci textu z načteného PDF v Delphi; když slova přijde na očekávaných stránkách, vrstva je skutečná
RapidOCR a další enginy na témž TSV protokolu
HotPDF znovu používá stejného process runnera i TSV parser pro RapidOCR přes HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds), což je užitečnější volba pro skeny ve zjednodušené čínštině. Command line je identický, jen se cesta bridge skriptu vloží za spustitelný Python a jazyk je fixně chi_sim. HotPDF dodává bridge jako tools/OCR/rapidocr_tsv.py; očekává balíčky rapidocr a onnxruntime plus tři lokální ONNX modely, vypne automatické stahování modelů a zapisuje TSV ve tvaru Tesseractu, takže Delphi strana nepotřebuje druhý parser. Jméno enginu hlášené v Info.EngineName je RapidOCR (local ONNX). Tenhle tvar naznačuje obecný recept: jakýkoli recognizer, který zabalíte do malého skriptu přijímajícího argument list ve stylu Tesseractu a emitujícího dvanáctisloupcové TSV, zdědí zdarma izolaci handlů, timeout, cancellation, výstupní budgety i all-or-nothing commit. Adaptery jsou jen pro Windows, běží synchronně jednu stránku najednou a nedeskewují ani nepředzpracovávají obrázek nad rámec toho, co vyprodukuje renderer, takže kvalita obrázku na vstupu pořád nastavuje strop tomu, co vyleze
Adaptery Tesseract a RapidOCR, zapisovač neviditelné textové vrstvy, page renderer, který je živí, i textová extrakce verifikující výsledek všechny vycházejí ve stejném nativním VCL komponentu pro Delphi a C++Builder. Pokud přidáváte OCR do aplikace na dokumentový capture nebo archivaci, HotPDF Delphi PDF component vám dá pipeline, u které zbývá nainstalovat jen samotný OCR engine