Odborný článok

Tesseract OCR na prehľadávateľné PDF v Delphi s HotPDF

HotPDF mení naskenované PDF strany na prehľadávateľné PDF s Tesseractom cez HPDFCreateTesseractOCREngine, factory, ktorá zabalí lokálne nainštalovaný spustiteľný Tesseract do IHPDFOCREngine. Ten engine podáte do ApplyLoadedOCRTextLayer, ktorý renderuje každú stranu, pustí Tesseract raz na stranu, parsuje jeho word-level TSV výstup a commitne neviditeľnú Unicode textovú vrstvu pre všetky vyžiadané strany v jednej transakcii, alebo pre žiadnu

OCR pipeline HotPDF na stranu: vyrenderujte stranu na nastavenej DPI, uložte input.bmp do privátneho adresára HotPDF-OCR, spustite child proces Tesseract s tessedit_create_tsv, parsujte dvanásťstĺpcové TSV, filtrujte slová podľa confidence a commitnite neviditeľnú textovú vrstvu pre všetky vyžiadané strany alebo žiadnu
Adaptér mení len rozpoznávanie: renderovanie, parsovanie, validácia a all-or-nothing commit zostávajú v existujúcej text-layer pipeline, takže downstream kód sa nemení

Dôvod, prečo tento adaptér existuje, je rozsah. Vstavaný OCR engine s template matchingom je zámerne úzky: tlačené ASCII písmená a číslice, nič viac. Faktúry s menami s diakritikou, čínske zmluvy a viacjazyčné archívy potrebujú skutočný recognizer s natrénovanými jazykovými modelmi a Tesseract je očividný kandidát, lebo je to command-line program, ktorý si provisionsujete vedľa aplikácie. Volanie externého programu z dokumentovej knižnice znie triviálne. Nie je a väčšina zaujímavého kódu v adaptéri je o tom, čo sa deje, keď sa program správa zle, zavisa, dostane zrušenie alebo zdedí veci, ktoré nikdy vidieť nemá

Ako HotPDF riadi Tesseract z Delphi aplikácie?

HotPDF pustí Tesseract ako skrytý child proces na stranu, kŕmi ho vyrenderovanou bitmapou a číta späť TSV súbor, pričom výsledok vystavuje cez to isté rozhranie IHPDFOCREngine, ktoré používa vstavaný engine. Nič downstream sa nemení: mapovanie súradníc, obsluha rotácie, Unicode validácia, filtrovanie podľa confidence a atómový commit sú text-layer pipeline, ktorú už máte. Factory býva v jednotke HPDFTesseractRecognition a validuje záhorlivé: spustiteľný súbor musí existovať, adresár tessdata musí existovať, timeout musí byť medzi 1 a 3 600 000 milisekundami a identifikátor jazyka smie obsahovať len ASCII písmená, číslice, _ a +. Tá posledná kontrola má význam, lebo reťazec jazyka končí na command line a eng+chi_sim je legitímna hodnota Tesseractu, kým čokoľvek s úvodzovkami alebo medzerami nie

uses
  SysUtils, HPDFTypes, HPDFDoc, HPDFTesseractRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string;
  Token: THPDFCancellationToken);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // vyhodí EArgumentException pri chýbajúcom spustiteľnom súbore, chýbajúcom tessdata,
  // zlom identifikátore jazyka alebo timeoute mimo 1..3600000 ms
  Engine := HPDFCreateTesseractOCREngine(
    'C:\OCR\Tesseract\tesseract.exe',
    'C:\OCR\Tesseract\tessdata',
    'eng+chi_sim',      // viacero modelov spojených s '+'
    120000);            // limit na stranu, predvolené 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ázdny zoznam strán znamená každá strana; strany s textom sa predvolene preskočia
    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;

Pre každú stranu Recognize vytvorí privátny adresár pod temp cestou menom HotPDF-OCR-{GUID}, uloží vyrenderovanú bitmapu ako input.bmp a spustí tesseract input.bmp output --tessdata-dir … -l … --dpi N --psm 3 -c tessedit_create_tsv=1, pričom každý cestový argument je v úvodzovkách podľa pravidiel escapovania Windows command line pre spätné lomky a vložené úvodzovky. Hodnota --dpi je render DPI z THPDFOCRTextLayerOptions.DPI, takže Tesseract nikdy nemusí hádať rozlíšenie z metadát obrázka a --psm 3 žiada plne automatickú segmentáciu strany. Engine sa hlási ako Tesseract (local CLI), čo pristane v Info.EngineName. Tesseract a jeho jazykové modely nie sú súčasťou HotPDF; ich inštalácia je úloha aplikácie

Prečo je TSV parser taký prísny?

TSV parser v HotPDF prepasuje celú stranu na každom deformovanom riadku, lebo čiastočne parsnutý zoznam slov vyrobí textovú vrstvu, ktorá sa potichu rozchádza s obrázkom. TSV výstup Tesseractu má fixnú dvanásťstĺpcovú hlavičku od level po text a HotPDF porovná prvý riadok s touto presnou hlavičkou po odstrihnutí voliteľného byte order mark. Každý ďalší riadok sa musí rozdeliť na presne dvanásť polí a delenie zastane po jedenástom tabe, takže tab vo vnútri rozpoznaného textu ostáva súčasťou slova namiesto vytvorenia trinásteho stĺpca. Slová sú len riadky level 5; levely 1 až 4 opisujú strany, bloky, odstavce a riadky a preskakujú sa. Riadky level 5 s prázdnym alebo čisto bielym textom sa preskočia tiež, lebo prázdne slovo má box, ale nič na lokalizovanie alebo hľadanie. Všetko ostatné sa kontroluje tvrdo: celočíselná geometria, confidence parsnutá s invariantným formátom en-US, takže nemecké locale nečíta 93.5 ako odpad, box ležiaci úplne vnútri bitmapy a confidence medzi 0 a 100. Jedno zlyhanie vyhodí výnimku, engine vráti False a pole slov sa vyčistí. Regresné testy obsahujú presne ten prípad: jedno platné slovo nasledované pokazeným riadkom musí dať nula slov, nie jedno

Šesť brán, ktoré prechádza každý riadok TSV Tesseractu v HotPDF: presná dvanásťstĺpcová hlavička, presne dvanásť polí, len level 5, neprázdny text, box vnútri bitmapy a confidence od 0 do 100 parsnutá invariantne, pričom jeden pokazený riadok prepasuje celú stranu na nula slov
Čiastočne parsnutý zoznam slov by sa potichu rozchádzal s obrázkom, takže parser odmietne celú stranu na prvom deformovanom riadku namiesto ponechania slov, ktoré už prečítal
// zhusotené zo sluč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;                 // riadky strana/blok/odstavec/riadok
WordText := Fields[11];
if Trim(WordText) = '' then Continue;        // slová z bielich miest nemajú pozíciu
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čakáva 0..1

Tá posledná riadka interaguje s predvolenou, ktorej by ste nemuseli čakať. Confidence Tesseractu beží od 0 do 100, pipeline pracuje v 0 až 1 a THPDFOCRTextLayerOptions.MinimumConfidence má predvolené 0,5, takže každé Tesseract slovo pod 50 sa započíta do Info.DroppedWordCount a nikdy sa nedostane na stranu. Na čistom skene 300 DPI je to rozumná podlaha. Na šumivom faxe to môže zhodiť prekvapujúci podiel strany a správny ťah je pozrieť sa na dropped počet pred znížením prahu, lebo slová s nízkou confidence sú presne tie, ktoré sú najpravdepodobnejšie zlé

Čo zdedí child proces Tesseractu?

Child proces Tesseractu zdedí od HotPDF presne dva handly: NUL handle pre štandardný vstup a výstup a file handle pre štandardný error. Tá presnosť je pointa. CreateProcess s bInheritHandles = True je spôsob, ako podáte štandardné handly dieťaťu, ale sám o sebe podá každý dedičný handle hostiteľského procesu, vrátane súborov, rúr a eventov otvorených nesúvisiacim kódom vo vašej aplikácii. Dieťa potom drží tie objekty nažive, kým neskončí, takže súbor ostane zamknutý alebo rúra nikdy nezazrie svoj koniec, kým Tesseract melie stranu. HotPDF tú medzeru zatvára rozšíreným štartovacím záznamom: STARTUPINFOEX, zoznam atribútov nesúci PROC_THREAD_ATTRIBUTE_HANDLE_LIST a creation flag EXTENDED_STARTUPINFO_PRESENT. So zoznamom handlov stále musí byť bInheritHandles True, ale cez hranicu prejdú len vypísané handly. Rovnaké zmýšľanie v uzavretosti poháňa izoláciu PDF image kodekov vo worker procesoch, kde dieťa je nedôveryhodný kód; tu je dieťa dôveryhodné, ale hostiteľ nie je jediný majiteľ vlastnej tabuľky handlov

Dedenie handlov child procesu Tesseractu v HotPDF: holé CreateProcess s bInheritHandles podá dieťaťu každý dedičný handle súboru, rúry aj eventu, kým STARTUPINFOEX s PROC_THREAD_ATTRIBUTE_HANDLE_LIST obmedzí množinu na NUL handle pre stdin a stdout plus file handle pre stderr
Bez zoznamu atribútov drží dieťa nesúvisiace objekty nažive, kým neskončí, a zamyká súbory a vyhladuje rúry; s ním prekročia hranicu len tie dva vypísané handly
// konštanty ukázané menami; zdroj podáva ich číselné hodnoty
// oba handly sa vytvárajú s bInheritHandle = True
InheritedHandles[0] := NullHandle;    // stdin a stdout
InheritedHandles[1] := ErrorHandle;   // stderr.txt v privátnom adresári
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žadované zoznamom handlov
  CREATE_NO_WINDOW or EXTENDED_STARTUPINFO_PRESENT,
  nil, PChar(DirectoryName), Startup.StartupInfo, ProcessInfo);

Prečo môže zrušený OCR beh vyzerať ako zlyhanie enginu?

Zrušený OCR beh vyzerá ako zlyhanie enginu preto, lebo IHPDFOCREngine.Recognize vracia jediný Boolean a False znamená oboje, „Tesseract zlyhal" aj „používateľ stlačil Cancel". Adaptér polluje cancellation token aj timeout každých 25 milisekúnd, kým dieťa beží, a keď token zaznie, vyhodí výnimku vnútri Recognize, chytí vlastnú výnimku, uprace a vráti False s diagnostikou. Keby pipeline brala toto ako chybu enginu, volajúci by videl otlsEngineError pre job, ktorý používateľ zámerne zastavil. ApplyLoadedOCRTextLayer preto kontroluje token najprv vždy, keď Recognize vráti False, a na zlyhanie enginu výsledok premení len vtedy, keď token nebol nastavený. Toto poradie chráni viacstranový kontrakt: rozpoznávanie, validácia, rozpočtovanie a stavba obsahu bežia pre každú vyžiadanú stranu, skôr než sa otvorí transakcia grafu, takže zrušenie na strane 40 z 50 nahlási otlsCancelled a nechá dokument vrátane prvých 39 strán nedotknutý. Neexistuje čiastočne prehľadávateľný súbor na vysvetľovanie neskôr a zvyšok obsluhy zlyhaní nasleduje ten istý ohraničený štýl:

  • Timeout je na volanie Recognize, meraný od jeho začiatku, takže predvolených 60 000 ms sa týka každej strany, nie celého dokumentu
  • Dieťa ešte bežiace pri timeoute alebo zrušení sa ukončí, čaká sa naň najviac 5 sekúnd a jeho privátny adresár sa zmaže v bloku finally
  • output.tsv je kapovaný na 64 MiB a stderr.txt na 1 MiB, kontrolované počas behu dieťaťa aj po jeho skončení
  • Počet slov a UTF-16 code unitov sa kapuje na stranu zostávajúcimi rozpočtami MaxWordsPerPage, MaxTotalWords a MaxTextCodeUnits a ich prekročenie prepasuje beh namiesto skrátenia zoznamu slov
  • Štandardný výstup ide do NUL, lebo Tesseract zapisuje output.tsv, zatiaľ čo štandardný error ide do súboru, takže nenulový exit kód sa hlási s až 4 096 znakmi vlastnej sťažnosti enginu, zvyčajne najrýchlejší spôsob, ako sa dozvedieť, že chýba súbor .traineddata

Ako sa rozpoznané slová stanú neviditeľnou textovou vrstvou

HotPDF zapisuje slová Tesseractu ako neviditeľný text s text rendering režimom 3, režimom ani-výplň-ani-obrys z ISO 32000-1 §9.3.6, takže strana stále ukazuje naskenovaný obrázok, kým hľadanie a kopírovanie pracujú na rozpoznaných slovách. Content stream otvára BT s 3 Tr a každé slovo dostane maticu Tm na svojej baseline, veľkosť fontu odvodenú z výšky boxu v pixeloch pri render DPI a horizontálne škálovanie Tz, ktoré roztiahne beh glyfov na odmeranú šírku boxu, a preto search highlight pristane na slove v obrázku namiesto driftovania cez neho

TSV Tesseractu má boxy, ale nie baseliny, takže adaptér hlási každé slovo bez nej a pipeline odhaduje baseline na pätine výšky boxu nad spodným okrajom. Samotný text ide cez zdieľaný nevložený Type0 font s kódovaním Identity-H a generovanou CMap ToUnicode, jedno CID na odlišný Unicode scalar v celom behu, a práve takto prežijú kopírovanie a hľadanie čínština, latinka s diakritikou aj znaky doplnkovej roviny. Tento dizajn má dva limity, ktoré stojí za to povedať hneď: jeden beh môže niesť najviac 65 535 odlišných scalarov a nevložený font nesplňuje požiadavku vkladania fontov z ISO 19005, takže PDF/A výstup potrebuje osobitne vložený konformný font. Kontrola výsledku je jednoduchá a stojí za automatizáciu: uložte, znovu načítajte a pustite obyčajnú cestu textu načítaného dokumentu z článku o extrakcii textu z načítaného PDF v Delphi; ak sa slová vrátia na očakávaných stranách, vrstva je reálna

RapidOCR a ďalšie enginy na tom istom TSV protokole

HotPDF znovu používa rovnaký process runner aj TSV parser pre RapidOCR cez HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds), čo je užitočnejšia voľba pre skeny v zjednodušenej čínštine. Command line je identický, len sa cesta bridge skriptu vloží za Python spustiteľný súbor a jazyk je fixovaný na chi_sim. HotPDF dodáva bridge ako tools/OCR/rapidocr_tsv.py; očakáva balíky rapidocr a onnxruntime plus tri lokálne ONNX modely, vypína automatické sťahovanie modelov a zapisuje TSV v tvare Tesseractu, takže Delphi strana nepotrebuje druhý parser. Meno enginu hlásené v Info.EngineName je RapidOCR (local ONNX). Ten tvar napovedá všeobecný recept: akýkoľvek recognizer, ktorý zabalíte do malého skriptu prijímajúceho zoznam argumentov v štýle Tesseractu a vypúšťajúceho dvanásťstĺpcové TSV, dedí izoláciu handlov, timeout, zrušenie, výstupné rozpočty aj all-or-nothing commit zadarmo. Adaptéry sú len pre Windows, púšťajú jednu stranu naraz synchrónne a nedeskewujú ani nepredspracujú obrázok nad to, čo vyprodukuje renderer, takže kvalita vstupného obrázka stále určuje strop toho, čo vypadne

Adaptéry Tesseract a RapidOCR, zapisovač neviditeľnej textovej vrstvy, page renderer, ktorý ich kŕmi, a textová extrakcia overujúca výsledok prichádzajú všetky v tom istom natívnom VCL komponente pre Delphi a C++Builder. Ak pridávate OCR do aplikácie na zber alebo archiváciu dokumentov, HotPDF Delphi PDF component vám dá pipeline, pri ktorej zostáva nainštalovať len samotný OCR engine