Tehnički članak

Tesseract OCR u pretraživi PDF u Delphiju s HotPDF-om

HotPDF pretvara skenirane PDF stranice u pretraživi PDF s Tesseractom kroz HPDFCreateTesseractOCREngine, tvornicu koja lokalno instaliranu Tesseract executivu zamota kao IHPDFOCREngine. Taj engine predajete ApplyLoadedOCRTextLayer, koji renderira svaku stranicu, pokreće Tesseract jednom po stranici, parsira njegov TSV izlaz na razini riječi, i potvrđuje nevidljivi Unicode tekstualni sloj za sve zatražene stranice u jednoj transakciji, ili ni za jednu

HotPDF OCR cjevovod po stranici: renderiraj stranicu na konfiguriranom DPI-ju, spremi input.bmp u privatni HotPDF-OCR direktorij, pokreni Tesseract dijete-proces s tessedit_create_tsv, parsiraj dvanaest-stupčani TSV, filtriraj riječi po confidenceu, i potvrdi nevidljivi tekstualni sloj za sve zatražene stranice ili ni za jednu
Adapter mijenja samo prepoznavanje: renderiranje, parsiranje, validacija i sve-ili-ništa potvrda ostaju u postojećem tekstualno-slojnom cjevovodu, pa se nizvodni kod nikad ne mijenja

Razlog postojanja ovog adaptera je opseg. Ugrađeni engine OCR-a s usporedbom predložaka namjerno je uzak: strojem tiskana ASCII slova i znamenke, ništa više. Računi s naglašenim imenima, kineski ugovori i višejezični arhivi trebaju pravi prepoznavatelj s istreniranim jezičnim modelima, a Tesseract je očiti kandidat jer je program za naredbeni redak koji možete opskrbiti uz svoju aplikaciju. Zvati vanjski program iz biblioteke za dokumente zvuči trivijalno. Nije, i najveći dio zanimljivog koda u adapteru bavi se time što se dogodi kad se program ponaša pogrešno, zaglavi, otkaze ili naslijedi stvari koje nikad ne bi smio vidjeti

Kako HotPDF upravlja Tesseractom iz Delphi aplikacije?

HotPDF pokreće Tesseract kao skriveni dijete-proces po stranici, hrani ga renderiranom bitmapom i čita natrag TSV datoteku, a rezultat izlaže kroz isti IHPDFOCREngine šav koji koristi ugrađeni engine. Ništa nizvodno se ne mijenja: preslikavanje koordinata, rukovanje rotacijom, Unicode validacija, filtriranje po confidenceu i atomična potvrda to je tekstualno-slojni cjevovod koji već imate. Tvornica živi u uniti HPDFTesseractRecognition i validira unaprijed: executable mora postojati, tessdata direktorij mora postojati, timeout mora biti između 1 i 3.600.000 milisekundi, a identifikator jezika smije sadržavati samo ASCII slova, znamenke, _ i +. Ta zadnja provjera ima veze jer string jezika završi na naredbenom retku, i eng+chi_sim valjana je Tesseract vrijednost dok bilo što s 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
  // baca EArgumentException za nedostajući executable, nedostajući tessdata,
  // loš identifikator jezika ili timeout izvan 1..3600000 ms
  Engine := HPDFCreateTesseractOCREngine(
    'C:\OCR\Tesseract\tesseract.exe',
    'C:\OCR\Tesseract\tessdata',
    'eng+chi_sim',      // više modelova spojenih s '+'
    120000);            // limit po stranici, zadano 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 s tekstom se po zadanom preskaču
    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 stvara privatni direktorij pod temp putanjom imena HotPDF-OCR-{GUID}, sprema renderiranu 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 escapiranja naredbenog retka za obrnute kose crte i ugrađene navodnike. Vrijednost --dpi je render DPI iz THPDFOCRTextLayerOptions.DPI, pa Tesseract nikad ne mora pogađati rezoluciju iz metapodataka slike, a --psm 3 traži potpuno automatsku segmentaciju stranice. Engine se prijavljuje kao Tesseract (local CLI), što je ono što završi u Info.EngineName. Tesseract i njegovi jezični modeli ne isporučuju se s HotPDF-om; njihova instalacija posao je aplikacije

Zašto je TSV parser tako strog?

TSV parser u HotPDF-u baca cijelu stranicu na svakom neispravnom retku, jer djelomično parsirana lista riječi proizvodi tekstualni sloj koji tiho ne slaže sa slikom. Tesseractov TSV izlaz ima fiksno dvanaest-stupčano zaglavlje, od level do text, i HotPDF usporedi prvi redak s tim točnim zaglavljem nakon skidanja opcionalnog byte order marka. Svaki sljedeći redak mora se rasprsnuti u točno dvanaest polja, i rasprsavanje staje nakon jedanaestog taba tako da tab unutar prepoznatog teksta ostane dio riječi umjesto da stvori trinaesti stupac. Samo su retci razine 5 riječi; razine 1 do 4 opisuju stranice, blokove, odlomke i retke, i preskaču se. Retci razine 5 čiji je tekst prazan ili čist whitespace preskaču se također, jer prazna riječ ima box ali nema što locirati ni tražiti. Sve ostalo provjerava se strogo: cjelobrojna geometrija, confidence parsiran s invariant en-US formatom da njemačka locale ne pročita 93.5 kao smeće, box koji leži posve unutar bitmape, i confidence između 0 i 100. Jedan jedini pad baci iznimku, engine vrati False, i lista riječi se očisti. Regresijski testovi uključuju upravo taj slučaj: jedna valjana riječ iza koje slijedi pokvaren redak mora dati nula riječi, a ne jednu

Šest gateova kroz koje prolazi svaki Tesseract TSV redak u HotPDF-u: točno dvanaest-stupčano zaglavlje, točno dvanaest polja, samo razina 5, neprazan tekst, box unutar bitmape, i confidence od 0 do 100 parsiran invariantno, pri čemu jedan pokvaren redak baca cijelu stranicu na nula riječi
Djelomično parsirana lista riječi tiho bi se protivila slici, pa parser odbija cijelu stranicu na prvom neispravnom retku umjesto da zadrži riječi koje je već pročitao
// sažeto iz petlje razine 5 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;                 // retci stranice/bloka/odlomka/retka
WordText := Fields[11];
if Trim(WordText) = '' then Continue;        // whitespace riječi 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;  // cjevovod očekuje 0..1

Zadnji redak komunicira sa zadanim kojeg možda ne očekujete. Tesseract confidence ide od 0 do 100, cjevovod radi u 0 do 1, a THPDFOCRTextLayerOptions.MinimumConfidence po zadanom je 0.5, pa svaka Tesseract riječ ispod 50 broji se u Info.DroppedWordCount i nikad ne stiže do stranice. Na čistom skenu od 300 DPI to je razumna granica. Na bučnom faksu može izbaciti iznenađujući udio stranice, i pravi potez je pogledati brojač izbačenih prije nego spustite prag, jer su nisko-pouzdane riječi upravo one koje su najvjerojatnije krive

Što dijete-proces Tesseracta nasljeđuje?

Dijete-proces Tesseracta nasljeđuje točno dva handlea iz HotPDF-a: NUL handle za standardni ulaz i izlaz, i file handle za standardnu grešku. Ta preciznost jest poanta. CreateProcess s bInheritHandles = True način je da standardnim handleovima dođete do djeteta, ali sam po sebi prenosi svaki nasljedivi handle u procesu domaćina, uključujući datoteke, pipeove i eventove otvorene od nevezanog koda u vašoj aplikaciji. Dijete zatim drži te objekte živima dok ne izađe, pa datoteka ostaje zaključana ili pipe nikad ne vidi kraj dok Tesseract melje kroz stranicu. HotPDF taj procjep zatvara proširenim startup zapisom: STARTUPINFOEX, lista atributa koja nosi PROC_THREAD_ATTRIBUTE_HANDLE_LIST, i creation flag EXTENDED_STARTUPINFO_PRESENT. S listom handleova na mjestu, bInheritHandles i dalje mora biti True, ali granicu prelaze samo nabrani handleovi. Isto razmišljanje o zatvaranju pogoni i izolaciju PDF image kodeka u worker procesima, gdje je dijete nepouzdani kod; ovdje je dijete pouzdano, ali domaćin nije jedini vlasnik vlastite tabele handleova

Nasljeđivanje handleova dijete-procesa Tesseracta u HotPDF-u: obični CreateProcess s bInheritHandles prenosi svaki nasljedivi file, pipe i event handle djetetu, dok STARTUPINFOEX s PROC_THREAD_ATTRIBUTE_HANDLE_LIST ograničava skup na NUL handle za stdin i stdout plus stderr file handle
Bez liste atributa dijete drži nevezane objekte živima dok ne izađe, zaključavajući datoteke i gladujući pipeove; s njom granicu prelaze samo dva nabrana handlea
// konstante prikazane imenom; izvor predaje njihove numeričke vrijednosti
// oba handlea stvaraju se s bInheritHandle = True
InheritedHandles[0] := NullHandle;    // stdin i stdout
InheritedHandles[1] := ErrorHandle;   // stderr.txt u privatnom direktoriju
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,                                        // traži handle lista
  CREATE_NO_WINDOW or EXTENDED_STARTUPINFO_PRESENT,
  nil, PChar(DirectoryName), Startup.StartupInfo, ProcessInfo);

Zašto otkazani OCR run može izgledati kao kvar enginea?

Otkazani OCR run izgleda kao kvar enginea jer IHPDFOCREngine.Recognize vraća jedan Boolean, i False znači i "Tesseract je pao" i "korisnik je pritisnuo Cancel". Adapter polla cancellation token i timeout svakih 25 milisekundi dok dijete radi, i kad token opali, baca unutar Recognize, uhvati vlastitu iznimku, počisti, i vrati False s dijagnostikom. Da je cjevovod to tretirao kao engine grešku, pozivatelj bi vidio otlsEngineError za posao koji je korisnik namjerno zaustavio. ApplyLoadedOCRTextLayer zato prvo provjerava token svaki put kad Recognize vrati False, i rezultat pretvara u engine kvar samo ako token nije bio postavljen. Taj redoslijed čuva višestanični ugovor: prepoznavanje, validacija, obračun budžeta i gradnja sadržaja rade za svaku zatraženu stranicu prije nego se otvori grafička transakcija, pa otkazivanje na stranici 40 od 50 javlja otlsCancelled i ostavlja dokument, uključujući prvih 39 stranica, netaknutim. Nema djelomično pretražive datoteke za objašnjavanje kasnije, a ostatak rukovanja kvarovima slijedi isti ograničeni stil:

  • Timeout je po pozivu Recognize, mjeri se od njegovog početka, pa zadanih 60.000 ms vrijedi za svaku stranicu, a ne za cijeli dokument
  • Dijete koje i dalje radi na timeoutu ili otkazivanju terminira se, čeka se do 5 sekundi, a njegov privatni direktorij briše se u finally bloku
  • output.tsv ima plafon 64 MiB i stderr.txt 1 MiB, provjeravano dok dijete radi kao i nakon njegovog izlaska
  • Broj riječi i UTF-16 code jedinica ima plafon po stranici kroz preostale budžete MaxWordsPerPage, MaxTotalWords i MaxTextCodeUnits, i njihovo prekoračenje baca run, umjesto da skraćuje listu riječi
  • Standardni izlaz ide u NUL jer Tesseract piše output.tsv, dok standardna greška ide u datoteku pa se ne-nulti exit code javlja s do 4.096 znakova engineova vlastitog prigovora, obično najbrži način da saznate da .traineddata datoteka nedostaje

Kako prepoznate riječi postaju nevidljivi tekstualni sloj

HotPDF piše Tesseractove riječi kao nevidljivi tekst koristeći text rendering mod 3, mod ni-ispune-ni-poteza definiran u ISO 32000-1 §9.3.6, pa stranica i dalje pokazuje skeniranu sliku dok pretraga i kopiranje rade nad prepoznatim riječima. Content stream otvara BT s 3 Tr, i svaka riječ dobiva Tm matricu na svojem baselineu, veličinu fonta izvedenu iz visine boxa u pikselima na render DPI, i Tz horizontalni scale koji razvlači niz glifova na izmjerenu širinu boxa, pa highlight pretrage sleti na riječ u slici umjesto da drifta preko nje

Tesseractov TSV ima boxove ali nema baselinea, pa adapter javlja svaku riječ bez nje, i cjevovod procijeni baseline na petinu visine boxa iznad donjeg ruba. Sam tekst ide kroz zajednički neugrađeni Type0 font s Identity-H kodiranjem i generiranim ToUnicode CMapom, jedan CID po različitom Unicode scalaru kroz cijeli run, pa tako kineski, naglašeni latinski i znakovi dopunskih ravnina svi prežive kopiranje i pretragu. Taj dizajn ima dva ograničenja vrijedna ranog izgovaranja: jedan run može nositi najviše 65.535 različitih scalara, i neugrađeni font ne zadovoljava zahtjev ugrađivanja fontova iz ISO 19005, pa PDF/A izlaz treba zasebno ugrađen sukladan font. Provjera rezultata jednostavna je i vrijedi je automatizirati: spremite, ponovno učitajte, i pokrenite običnu putanju teksta učitanog dokumenta iz članka o izvlačenju teksta iz učitanog PDF-a u Delphiju; ako riječi dođu natrag na očekivanim stranicama, sloj je stvaran

RapidOCR i drugi enginei na istom TSV protokolu

HotPDF ponovno koristi isti pokretač procesa i TSV parser za RapidOCR kroz HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds), što je korisniji izbor za skenove pojednostavljenog kineskog. Naredbeni redak identičan je osim što se putanja bridge skripte umeće iza Python executiva, 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 automatska preuzimanja modelova, i piše TSV oblika Tesseracta pa Delphi strana ne treba drugi parser. Ime enginea javljeno u Info.EngineName jest RapidOCR (local ONNX). Taj oblik nudi opći recept: svaki prepoznavatelj kojeg možete zamotati u malu skriptu koja prima argument listu u stilu Tesseracta i ispisuje dvanaest-stupčani TSV nasljeđuje izolaciju handleova, timeout, otkazivanje, budžete izlaza i sve-ili-ništa potvrdu besplatno. Adapteri su samo za Windows, rade jednu stranicu istovremeno sinkrono, i ne ispravljaju kosinu ni prethodno obrađuju sliku izvan onoga što renderer proizvede, pa kvaliteta slike na ulazu i dalje postavlja plafon onome što izlazi

Tesseract i RapidOCR adapteri, pisac nevidljivog tekstualnog sloja, renderer stranica koji ih hrani, i izvlačenje teksta koje verificira rezultat svi isporučuju se u istoj nativnoj VCL komponenti za Delphi i C++Builder. Ako OCR dodajete u aplikaciju za hvatanje ili arhiviranje dokumenata, HotPDF Delphi PDF komponenta daje vam cjevovod uz samo OCR engine koji još treba instalirati