Tehnički članak

Tesseract OCR u pretraživi PDF u Delphi-ju sa HotPDF-om

HotPDF pretvara skenirane stranice PDF-a u pretraživi PDF sa Tesseractom kroz HPDFCreateTesseractOCREngine, fabriku koja lokalno instalirani Tesseract izvršni fajl omota kao IHPDFOCREngine. Taj engine predajete ApplyLoadedOCRTextLayer-u, koji renderuje svaku stranicu, pokreće Tesseract jednom po stranici, parsira njegov word-level TSV izlaz, i posvećuje nevidljivi Unicode sloj teksta za sve zatražene stranice u jednoj transakciji, ili za nijednu

HotPDF OCR pipeline po stranici: renderuj stranicu na podešenoj DPI vrednosti, sačuvaj input.bmp u privatnom HotPDF-OCR direktorijumu, pokreni Tesseract child proces sa tessedit_create_tsv, parsiraj dvanaestokoloni TSV, filtriraj reči po poverenju i posvećuj nevidljivi sloj teksta za sve zatražene stranice ili nijednu
Adapter samo menja prepoznavanje: rendering, parsiranje, validacija i sve-ili-ništa posveta ostaju u postojećem text-layer pipeline-u, pa nizvodni kod se nikada ne menja

Razlog što ovaj adapter postoji je obuhvat. Ugrađeni OCR engine sa pretragom po šablonu je namerno uzak: mašinski štampana ASCII slova i cifre, ništa drugo. Fakture sa imenima sa dijakritikama, kineski ugovori i višejezične arhive trebaju pravi prepoznavac sa istreniranim jezičkim modelima, a Tesseract je očigledan kandidat jer je komandno-linijski program koji možete obezbediti pored svoje aplikacije. Zvanje spoljnog programa iz biblioteke dokumenata zvuči trivijalno. Nije, i najveći deo zanimljivog koda u adapteru je o tome šta se dešava kad se program loše ponaša, zaglavi, bude otkazan, ili nasledi stvari koje nikada ne bi trebalo da vidi

Kako HotPDF pokreće Tesseract iz Delphi aplikacije?

HotPDF pokreće Tesseract kao skriven child proces po stranici, hraneći ga renderovanom bitmapom i čitajući nazad TSV fajl, i izlaže rezultat kroz isti IHPDFOCREngine šav koji koristi ugrađeni engine. Ništa nizvodno se ne menja: mapiranje koordinata, rukovanje rotacijom, Unicode validacija, filtriranje po poverenju i atomska posveta su text-layer pipeline koji već imate. Fabrika živi u HPDFTesseractRecognition jedinici i validira unapred: izvršni fajl mora postojati, tessdata direktorijum mora postojati, tajm-aut mora biti između 1 i 3.600.000 milisekundi, a jezički identifikator sme sadržati samo ASCII slova, cifre, _ i +. Ta poslednja provera je bitna jer string jezika završi na komandnoj liniji, a eng+chi_sim je legitimna Tesseract vrednost dok sve sa navodnicima ili razmacima nije

uses
  SysUtils, HPDFTypes, HPDFDoc, HPDFTesseractRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string;
  Token: THPDFCancellationToken);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // podiže EArgumentException za nedostajući izvršni fajl, nedostajući tessdata,
  // loš jezički identifikator ili tajm-aut van 1..3600000 ms
  Engine := HPDFCreateTesseractOCREngine(
    'C:\OCR\Tesseract\tesseract.exe',
    'C:\OCR\Tesseract\tessdata',
    'eng+chi_sim',      // više modelova spojenih sa '+'
    120000);            // ograničenje po stranici, podrazumevano 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;
    // prazna lista stranica znači svaku stranicu; stranice sa tekstom se preskaču po podrazumevanu
    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;

Za svaku stranicu, Recognize pravi privatni direktorijum pod temp putanjom imenovan HotPDF-OCR-{GUID}, čuva renderovanu bitmapu kao input.bmp, i pokreće tesseract input.bmp output --tessdata-dir … -l … --dpi N --psm 3 -c tessedit_create_tsv=1, sa svakim argumentom putanje u navodnicima po Windows pravilima bekstva komandne linije za obrnute kose crte i ugrađene navodnike. --dpi vrednost je render DPI iz THPDFOCRTextLayerOptions.DPI, pa Tesseract nikada ne mora da pogađa rezoluciju iz metapodataka slike, a --psm 3 traži potpuno automatsku segmentaciju stranice. Engine se prijavljuje kao Tesseract (local CLI), što je ono što padne u Info.EngineName. Tesseract i njegovi jezički modeli nisu upakovani sa HotPDF-om; njihova instalacija je posao aplikacije

Zašto je TSV parser tako strog?

TSV parser u HotPDF-u obara celu stranicu na svakom nepravilnom redu, jer delimično parsirana lista reči proizvodi sloj teksta koji tiho odstupa od slike. Tesseract TSV izlaz ima fiksni dvanaestokoloni header, od level do text, i HotPDF poredi prvu liniju sa tim tačnim headerom posle skidanja opcionog byte order marka. Svaki sledeći red mora da se podeli na tačno dvanaest polja, i podela staje posle jedanaestog tabulatora da tabulator unutar prepoznatog teksta ostane deo reči umesto da napravi trinaestu kolonu. Samo level 5 redovi su reči; nivoi 1 do 4 opisuju stranice, blokove, pasuse i linije, i preskaču se. Level 5 redovi čiji je tekst prazan ili čist prazan znak takođe se preskaču, jer prazna reč ima kutiju ali ništa za lociranje ili pretragu. Sve ostalo se proverava strogo: celobrojna geometrija, poverenje parsirano sa invariant en-US formatom da nemačka lokalizacija ne pročita 93.5 kao đubre, kutija koja leži potpuno unutar bitmape, i poverenje između 0 i 100. Jedan jedini pad podiže izuzetak, engine vraća False, i niz reči se briše. Regresioni testovi uključuju baš taj slučaj: jedna validna reč pa pokvaren red mora dati nula reči, ne jednu

Šest kapija kroz koje prolazi svaki Tesseract TSV red u HotPDF-u: tačan dvanaestokoloni header, tačno dvanaest polja, samo level 5, neprazan tekst, kutija unutar bitmape i poverenje od 0 do 100 parsirano invariantno, gde jedan pokvaren red obara celu stranicu na nula reči
Delimično parsirana lista reči tiho bi odsupala od slike, pa parser odbija celu stranicu na prvom nepravilnom redu umesto da zadrži reči koje je već pročitao
// zbijeno iz level-5 petlje u 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;                 // redovi stranice/bloka/pasusa/linije
WordText := Fields[11];
if Trim(WordText) = '' then Continue;        // reči od praznih znakova nemaju poziciju
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čekuje 0..1

Ta poslednja linija stupa u interakciju sa podrazumevanom vrednošću koju možda ne očekujete. Tesseract poverenje ide od 0 do 100, pipeline radi u 0 do 1, a THPDFOCRTextLayerOptions.MinimumConfidence podrazumeva 0,5, pa se svaka Tesseract reč ispod 50 broji u Info.DroppedWordCount i nikada ne stiže do stranice. Na čistom skenu od 300 DPI to je razumna donja granica. Na bučnom faksu može odbaciti iznenađujući deo stranice, i pravi potez je pogledati broj odbačenih pre spuštanja praga, jer su reči niskog poverenja upravo one najverovatnije pogrešne

Šta Tesseract child proces nasleđuje?

Tesseract child proces nasleđuje tačno dva handle-a od HotPDF-a: NUL handle za standardni ulaz i izlaz, i fajl handle za standardnu grešku. Ta preciznost je suština. CreateProcess sa bInheritHandles = True je način da standardne handle-ove predate detetu, ali sam po sebi predaje svaki nasledivi handle u domaćinskom procesu, uključujući fajlove, pipe-ove i događaje otvorene od strane nevezanog koda u vašoj aplikaciji. Dete zatim drži te objekte živima dok ne izađe, pa fajl ostaje zaključan ili pipe nikada ne vidi svoj kraj dok Tesseract muči stranicu. HotPDF zatvara tu rupu proširenim startup zapisom: STARTUPINFOEX, lista atributa koja nosi PROC_THREAD_ATTRIBUTE_HANDLE_LIST, i EXTENDED_STARTUPINFO_PRESENT creation flag. Sa listom handle-ova na mestu, bInheritHandles i dalje mora biti True, ali samo navedeni handle-ovi prelaze granicu. Isto razmišljanje o ograničavanju pokreće izolaciju PDF image kodeka u radnim procesima, gde je dete kod kojem se ne veruje; ovde je dete kome se veruje, ali domaćin nije jedini vlasnik svoje handle tabele

Nasleđivanje handle-ova Tesseract child procesa u HotPDF-u: običan CreateProcess sa bInheritHandles predaje detetu svaki nasledivi fajl, pipe i event handle, dok STARTUPINFOEX sa PROC_THREAD_ATTRIBUTE_HANDLE_LIST ograničava skup na NUL handle za stdin i stdout plus stderr fajl handle
Bez liste atributa dete drži nevezane objekte živima dok ne izađe, zaključavajući fajlove i izgladnjujući pipe-ove; sa njom, samo dva navedena handle-a prelaze granicu
// konstante prikazane po imenu; izvor predaje njihove numeričke vrednosti
// oba handle-a su napravljena sa bInheritHandle = True
InheritedHandles[0] := NullHandle;    // stdin i stdout
InheritedHandles[1] := ErrorHandle;   // stderr.txt u privatnom direktorijumu
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,                                        // obavezno zbog liste handle-ova
  CREATE_NO_WINDOW or EXTENDED_STARTUPINFO_PRESENT,
  nil, PChar(DirectoryName), Startup.StartupInfo, ProcessInfo);

Zašto otkazani OCR prolaz može ličiti na kvar engine-a?

Otkazani OCR prolaz liči na kvar engine-a jer IHPDFOCREngine.Recognize vraća jedan Boolean, i False znači i „Tesseract je pao” i „korisnik je pritisnuo Otkaži”. Adapter uzorkuje token otkazivanja i tajm-aut na svakih 25 milisekundi dok dete radi, i kada token okine, podiže izuzetak unutar Recognize-a, uhvati sopstveni izuzetak, počisti, i vraća False sa dijagnostikom. Da je pipeline to tretirao kao kvar engine-a, pozivalac bi video otlsEngineError za posao koji je korisnik namerno zaustavio. ApplyLoadedOCRTextLayer zato prvo proverava token kad god Recognize vrati False, i u kvar engine-a pretvara rezultat samo ako token nije bio postavljen. Taj redosled čini višestranični ugovor: prepoznavanje, validacija, računovanje budžeta i izgradnja sadržaja rade za svaku zatraženu stranicu pre nego što se grafička transakcija otvori, pa otkazivanje na stranici 40 od 50 prijavljuje otlsCancelled i ostavlja dokument, uključujući prvih 39 stranica, netaknutim. Nema delimično pretraživog fajla koji treba objašnjavati kasnije, a ostatak rukovanja kvarovima prati isti ograničeni stil:

  • Tajm-aut je po Recognize pozivu, meren od njegovog starta, pa podrazumevanih 60.000 ms važi za svaku stranicu, a ne za ceo dokument
  • Dete koje i dalje radi na tajm-autu ili otkazivanju se terminira, čeka se do 5 sekundi, i njegov privatni direktorijum se briše u finally bloku
  • output.tsv je ograničen na 64 MiB a stderr.txt na 1 MiB, proverava se dok dete radi kao i posle njegovog izlaska
  • Broj reči i UTF-16 code jedinica ograničavaju se po stranici preostalim budžetima MaxWordsPerPage, MaxTotalWords i MaxTextCodeUnits, i njihovo prekoračenje obara prolaz umesto da skrati listu reči
  • Standardni izlaz ide u NUL jer Tesseract piše output.tsv, dok standardna greška ide u fajl pa se ne-nulti exit kod prijavljuje sa do 4.096 znakova sopstvene pritužbe engine-a, obično najbrži način da saznate da .traineddata fajl nedostaje

Kako prepoznate reči postaju nevidljivi sloj teksta

HotPDF upisuje Tesseract reči kao nevidljivi tekst korišćenjem text rendering mode 3, režima ni-popuna-ni-obruba iz ISO 32000-1 §9.3.6, pa stranica i dalje pokazuje skeniranu sliku dok pretraga i kopiranje rade na prepoznatim rečima. Content stream otvara BT sa 3 Tr, i svaka reč dobija Tm matricu na svojoj baseline-i, veličinu fonta izvedenu iz visine kutije u pikselima na render DPI, i Tz horizontalno razmeranje koje razvlači niz glifova do izmerene širine kutije, pa pretragom istaknuto sedi na reči u slici umesto da luta preko nje

Tesseract TSV ima kutije ali nema baseline-e, pa adapter prijavljuje svaku reč bez nje i pipeline procenjuje baseline na petinu visine kutije iznad donje ivice. Sam tekst prolazi kroz deljeni neugrađeni Type0 font sa Identity-H enkodovanjem i generisanim ToUnicode CMap-om, jedan CID po različitom Unicode skalaru kroz ceo prolaz, po čemu kineski, latinični sa dijakritikama i karakteri dopunskog plana svi prežive kopiranje i pretragu. Taj dizajn ima dve granice koje vredi reći unapred: jedan prolaz može nositi najviše 65.535 različitih skalara, i neugrađeni font ne zadovoljava zahtev ugrađivanja fontova iz ISO 19005, pa PDF/A izlaz treba posebno ugrađen usaglašen font. Provera rezultata je prosta i vredi je automatizovati: sačuvajte, ponovo učitajte, i pokrenite običan text put učitanog dokumenta iz izvlačenja teksta iz učitanog PDF-a u Delphi-ju; ako se reči vrate na očekivanim stranicama, sloj je pravi

RapidOCR i drugi engine-ovi na istom TSV protokolu

HotPDF koristi isti pokretač procesa i TSV parser i za RapidOCR kroz HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds), što je korisniji izbor za skenove na pojednostavljenom kineskom. Komandna linija je identična osim što se putanja bridge skripte ubacuje posle Python izvršnog fajla, a jezik je fiksiran na chi_sim. HotPDF isporučuje most kao tools/OCR/rapidocr_tsv.py; očekuje pakete rapidocr i onnxruntime plus tri lokalna ONNX modela, isključuje automatsko preuzimanje modela, i piše Tesseract-oblikovan TSV da Delphi strani ne treba drugi parser. Ime engine-a prijavljeno u Info.EngineName je RapidOCR (local ONNX). Taj oblik sugeriše opšti recept: svaki prepoznavac koga možete umotati u malu skriptu koja prima Tesseract-stilsku listu argumenata i emituje dvanaestokoloni TSV nasleđuje izolaciju handle-ova, tajm-aut, otkazivanje, budžete izlaza i sve-ili-ništa posvetu besplatno. Adapteri su samo za Windows, rade jednu stranicu istovremeno sinhrono, i ne ispravljaju naginjanje ni ne pripremaju sliku dalje od onoga što renderer proizvede, pa kvalitet slike na ulazu i dalje postavlja plafon onome što izlazi

Tesseract i RapidOCR adapteri, pisac nevidljivog text-layer-a, renderer stranica koji ih hrani i izvlačenje teksta koje verifikuje rezultat isporučuju se svi u istoj nativnoj VCL komponenti za Delphi i C++Builder. Ako dodajete OCR aplikaciji za snimanje ili arhiviranje dokumenata, HotPDF Delphi PDF component vam daje pipeline, uz ostalo samo da instalirate sam OCR engine