HotPDF nuskaitytus PDF puslapius paverčia ieškomu PDF su Tesseract per HPDFCreateTesseractOCREngine – gamyklą, kuri vietiniu būdu įdiegtą Tesseract vykdomąjį failą apgaubia kaip IHPDFOCREngine. Tą variklį perduodate ApplyLoadedOCRTextLayer, kuri atvaizduoja kiekvieną puslapį, paleidžia Tesseract po kartą puslapiui, išanalizuoja jo žodžio lygio TSV išvestį ir patvirtina nematomą Unicode teksto sluoksnį visiems prašytiems puslapiams viena transakcija – arba nei vienam
Šio adapterio egzistavimo priežastis – taikymo sritis. Įtaisytasis šablonų atitikimo OCR variklis sąmoningai siauras: mašininio spaudimo ASCII raidės ir skaitmenys, daugiau nieko. Sąskaitos su kirčiuotais vardais, kinų sutartys ir daugiakalbiai archyvai reikalauja tikro atpažintojo su apmokytais kalbos modeliais, o Tesseract – akivaizdus kandidatas, nes tai komandinės eilutės programa, kurią galite įrengti šalia savos programos. Išorinės programos kvietimas iš dokumentų bibliotekos skamba paprastai. Jis toks nėra, ir dauguma įdomaus adapterio kodo yra apie tai, kas nutinka, kai programa elgiasi blogai, užstringa, būna atšaukta arba paveldi dalykus, kurių ji niekada neturėtų matyti
Kaip HotPDF valdo Tesseract iš Delphi programos?
HotPDF paleidžia Tesseract kaip paslėptą vaikų procesą po kartą puslapiui, jam duodamas atvaizduotą bitmap ir atgal skaitodamas TSV failą, o rezultatą atidengia per tą patį IHPDFOCREngine siūlą, kurį naudoja įtaisytasis variklis. Niekas toliau grandyje nesikeičia: koordinačių susiejimas, pasukimų apdorojimas, Unicode patikra, pasitikėjimo atranka ir atominis patvirtinimas yra jau jūsų turimas teksto sluoksnio konvejeris. Gamykla gyvena HPDFTesseractRecognition unite ir patikrina užsispyrusiai: vykdomasis failas turi egzistuoti, tessdata katalogas turi egzistuoti, laiko limitas turi būti tarp 1 ir 3 600 000 milisekundžių, o kalbos identifikatorius gali turėti tik ASCII raides, skaitmenis, _ ir +. Ta paskutinė patikra svarbi, nes kalbos eilutė nukeliauja į komandinę eilutę, o eng+chi_sim yra teisėta Tesseract reikšmė, bet kas nors su kabutėmis ar tarpais – ne
uses
SysUtils, HPDFTypes, HPDFDoc, HPDFTesseractRecognition;
procedure MakeSearchable(const SourceFile, TargetFile: string;
Token: THPDFCancellationToken);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// kelia EArgumentException dėl dingusio vykdomojo failo, dingusios tessdata,
// blogo kalbos identifikatoriaus arba laiko limito už 1..3600000 ms ribų
Engine := HPDFCreateTesseractOCREngine(
'C:\OCR\Tesseract\tesseract.exe',
'C:\OCR\Tesseract\tessdata',
'eng+chi_sim', // keli modeliai sujungti su '+'
120000); // limitas puslapiui, numatytasis 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;
// tuščias puslapių sąrašas reiškia visus puslapius; puslapiai su tekstu praleidžiami pagal numatymą
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;
Kiekvienam puslapiui Recognize sukuria privatų katalogą po laikinu keliu, pavadintą HotPDF-OCR-{GUID}, išsaugoja atvaizduotą bitmap kaip input.bmp ir paleidžia tesseract input.bmp output --tessdata-dir … -l … --dpi N --psm 3 -c tessedit_create_tsv=1, kiekvieną kelio argumentą cituodamas pagal Windows komandinės eilutės pabėgimo taisykles pasviriesiems brūkšniams ir įtaisytoms kabutėms. --dpi reikšmė yra atvaizdavimo DPI iš THPDFOCRTextLayerOptions.DPI, tad Tesseract niekada neturi spėlioti raiškos iš paveikslėlio metaduomenų, o --psm 3 prašo visiškai automatinio puslapio segmentavimo. Variklis praneša apie save kaip Tesseract (local CLI), ir tai nukeliauja į Info.EngineName. Tesseract ir jo kalbos modeliai nėra tiekiami su HotPDF; jų įdiegimas yra programos darbas
Kodėl TSV analizatorius toks griežtas?
HotPDF TSV analizatorius žlugdo visą puslapį dėl bet kurios netinkamai suformuotos eilutės, nes iš dalies išanalizuotas žodžių sąrašas duoda teksto sluoksnį, kuris tyliai nesutampa su paveikslėliu. Tesseract TSV išvestis turi fiksuotą dvylikos stulpelių antraštę, nuo level iki text, ir HotPDF pirmąją eilutę sugretina su ta tikslia antrašte, nuplėšęs pasirinktinę baitų eiliškumo žymą. Kiekviena tolesnė eilutė turi suskilti į lygiai dvylika laukų, o skaidymas sustoja po vienuoliktosios tabuliacijos, kad tabuliacija atpažintame tekste liktų žodžio dalimi, vietoj to, kad sukurtų tryliktąjį stulpelį. Tik 5 lygio eilutės yra žodžiai; 1–4 lygiai aprašo puslapius, blokus, pastraipas ir eilutes, ir jie praleidžiami. 5 lygio eilutės, kurių tekstas tuščias arba grynai iš tarpo simbolių, praleidžiamos irgi, nes tuščias žodis turi langelį, bet neturi ko rasti ar ieškoti. Visa kita tikrinama griežtai: sveikųjų skaičių geometrija, pasitikėjimas, išanalizuotas invariantine en-US forma, kad vokiečių lokalė neperskaitytų 93.5 kaip šiukšlių, langelis, gulintis visiškai bitmap viduje, ir pasitikėjimas tarp 0 ir 100. Vienas žlugimas kelia išimtį, variklis grąžina False, o žodžių masyvas išvalomas. Regresiniai testai turi būtent tą atvejį: vienas teisėtas žodis, po kurio seka sugadinta eilutė, turi duoti nulį žodžių, o ne vieną
// sutraukta iš 5 lygio kilpos 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; // puslapio/bloko/pastraipos/eilutės eilutės
WordText := Fields[11];
if Trim(WordText) = '' then Continue; // tarpo simbolių žodžiai neturi pozicijos
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; // konvejeris tikisi 0..1
Ta paskutinė eilutė sąveikauja su numatymu, kurio galbūt nesitikėjote. Tesseract pasitikėjimas bėga nuo 0 iki 100, konvejeris dirba 0 iki 1, o THPDFOCRTextLayerOptions.MinimumConfidence pagal numatymą lygus 0.5, tad bet kuris Tesseract žodis žemiau 50 skaičiuojamas Info.DroppedWordCount ir niekada nepasiekia puslapio. Švariai 300 DPI skenuotam vaizdui tai pagrįsta riba. Triukšmingam faksui tai gali numesti stebėtiną puslapio dalį, ir teisingas žingsnis – pažiūrėti į atmestųjų skaičių dar prieš žeminant slenkstį, nes mažo pasitikėjimo žodžiai yra būtent tie, kurie labiausiai tikėtina neteisingi
Ką paveldi Tesseract vaikų procesas?
Tesseract vaikų procesas paveldi lygiai dvi rankenas iš HotPDF: NUL rankeną standartiniam įvedimui ir išvedimui bei failo rankeną standartinei klaidai. Ta precizija yra esmė. CreateProcess su bInheritHandles = True yra būdas perduoti standartines rankenas vaikui, bet vienas jis perduoda kiekvieną paveldimą pagrindinio proceso rankeną, įskaitant failus, vamzdžius ir įvykius, atvertus nesusijusio kodo jūsų programoje. Vaikas tada laiko tuos objektus gyvus iki savo išėjimo, tad failas lieka užrakintas arba vamzdis niekada nepamatys savo pabaigos, kol Tesseract varosi per puslapį. HotPDF tą spragą užveria išplėstiniu starto įrašu: STARTUPINFOEX, atributų sąrašu, nešančiu PROC_THREAD_ATTRIBUTE_HANDLE_LIST, ir EXTENDED_STARTUPINFO_PRESENT kūrimo vėliavėle. Turint rankenų sąrašą, bInheritHandles vis tiek turi būti True, bet per ribą kerta tik sąrašo rankenos. Tą patį izoliavimo mąstymą vysto PDF paveikslėlių kodekų izoliavimas darbo procesuose, kur vaikas yra nepatikimas kodas; čia vaikas patikimas, bet pagrindinis nėra vienintelis savos rankenų lentelės savininkas
// konstantos rodomos vardais; šaltinis perduoda jų skaitines reikšmes
// abi rankenos sukuriamos su bInheritHandle = True
InheritedHandles[0] := NullHandle; // stdin ir stdout
InheritedHandles[1] := ErrorHandle; // stderr.txt privačiame kataloge
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, // reikalaujama rankenų sąrašo
CREATE_NO_WINDOW or EXTENDED_STARTUPINFO_PRESENT,
nil, PChar(DirectoryName), Startup.StartupInfo, ProcessInfo);
Kodėl atšauktas OCR paleidimas gali atrodyti kaip variklio gedimas?
Atšauktas OCR paleidimas atrodo kaip variklio gedimas, nes IHPDFOCREngine.Recognize grąžina vieną Boolean, o False reiškia ir „Tesseract žlugo“, ir „vartotojas paspaudė Atšaukti“. Adapteris kas 25 milisekundes apklausia atšaukimo tokeną ir laiko limitą, kol vaikas bėga, ir tokenui suveikus kelia išimtį Recognize viduje, sugauna savąją išimtį, sutvarko ir grąžina False su diagnostika. Jei konvejeris tai laikytų variklio klaida, kviečiantysis matytų otlsEngineError už darbą, kurį vartotojas sąmoningai sustabdė. ApplyLoadedOCRTextLayer todėl tikrina tokeną pirmiausia kaskart, kai Recognize grąžina False, ir rezultatą į variklio gedimą paverčia tik tada, kai tokenas nebuvo nustatytas. Ta tvarka išlaiko daugiapuslapio sutartį: atpažinimas, patikra, biudžeto apskaita ir turinio statymas bėga kiekvienam prašytam puslapiui, dar neatvėrus grafo transakcijos, tad atšaukimas 50 puslapių 40-ajame praneša otlsCancelled ir palieka dokumentą, įskaitant pirmuosius 39 puslapius, neliestą. Nėra pusiau ieškomo failo, kurio vėliau tektų aiškintis, o likusi žlugimų apdorojimo dalis seka tą patį apibrėžtą stilių:
- Laiko limitas tenka kiekvienam
Recognizeiškvietimui, matuojamas nuo jo pradžios, tad numatytosios 60 000 ms galioja kiekvienam puslapiui, o ne visam dokumentui - Vaikas, vis dar bėgantis pasibaigus laikui ar atšaukus, nutraukiamas, palaukiamas iki 5 sekundžių, o jo privatus katalogas ištrinamas
finallybloke output.tsvribojamas 64 MiB, ostderr.txt– 1 MiB, tikrinama tiek kol vaikas bėga, tiek jam išėjus- Žodžių skaičius ir UTF-16 kodo vienetai puslapiui ribojami likusiais
MaxWordsPerPage,MaxTotalWordsirMaxTextCodeUnitsbiudžetais, ir jų viršijimas žlugdo paleidimą, vietoj to, kad nukirptų žodžių sąrašą - Standartinė išvestina keliauja į
NUL, nes Tesseract rašooutput.tsv, o standartinė klaida – į failą, kad nenulinis išėjimo kodas būtų praneštas su iki 4 096 variklio paties skundo simbolių – paprastai greičiausias būdas sužinoti, kad dingęs.traineddatafailas
Kaip atpažinti žodžiai tampa nematomu teksto sluoksniu
HotPDF Tesseract žodžius rašo kaip nematomą tekstą naudodamas teksto atvaizdavimo režimą 3 – nei užpildymo, nei brūkšnio režimą, apibrėžtą ISO 32000-1 §9.3.6 – tad puslapis vis tiek rodo nuskaitytą paveikslėlį, kol paieška ir kopijavimas veikia ant atpažintų žodžių. Turinio srautas atveria BT su 3 Tr, o kiekvienas žodis gauna Tm matricą ties savo pagrindo linija, šrifto dydį, išvestą iš langelio aukščio pikseliais atvaizdavimo DPI, ir Tz horizontalųjį mastelį, kuris ištempia glifų eigą iki išmatuoto langelio pločio – todėl paieškos paryškinimas nusileidžia ant žodžio paveikslėlyje, o ne klykioja per jį
Tesseract TSV turi langelius, bet neturi pagrindo linijų, tad adapteris praneša kiekvieną žodį be jos, o konvejeris pagrindo liniją įvertina penktadaliu langelio aukščio virš apatinio krašto. Pats tekstas eina pro bendrąjį neįtaikytą Type0 šriftą su Identity-H koduote ir sugeneruota ToUnicode CMap – po vieną CID kiekvienam atskiram Unicode skaliarui per visą eigą – todėl kinų, kirčiuotos lotyniškos ir papildomos plokštumos simboliai visi išgyvena kopijavimą ir paiešką. Tas dizainas turi dvi ribas, vertas pasakyti iš anksto: viena eiga gali nešti ne daugiau kaip 65 535 atskirų skaliarų, o neįtaikytas šriftas nepatenkina ISO 19005 šrifto įtaikymo reikalavimo, tad PDF/A išvedimui reikia atskirai įtaikyto atitinkančio šrifto. Rezultato patikrinimas paprastas ir vertas automatizuoti: įrašykite, perkraukite ir paleiskite paprastąjį įkelto dokumento teksto kelią iš teksto ištraukimo iš įkelto PDF Delphi; jei žodžiai sugrįžta tikėtinuose puslapiuose, sluoksnis tikras
RapidOCR ir kiti varikliai ant to paties TSV protokolo
HotPDF tą patį procesų paleidėją ir TSV analizatorių pakartotinai naudoja RapidOCR per HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds), kuri naudingesnė pasirinktis Supaprastintosios kinų skenavimams. Komandinė eilutė identiška, tik tiltinio scenarijaus kelias įterpiamas po Python vykdomojo failo, o kalba fiksuota chi_sim. HotPDF tiltą tiekia kaip tools/OCR/rapidocr_tsv.py; jis tikisi rapidocr ir onnxruntime paketų plius trijų vietinių ONNX modelių, išjungia automatinį modelių atsisiuntimą ir rašo Tesseract formos TSV, tad Delphi pusei nereikia antro analizatoriaus. Variklio vardas, pranešamas Info.EngineName, yra RapidOCR (local ONNX). Ta forma užuomina bendrąjį receptą: bet koks atpažintojas, kurį galite apgaubti mažu scenarijumi, priimančiu Tesseract stiliaus argumentų sąrašą ir išvedančiu dvylikos stulpelių TSV, paveldi rankenų izoliaciją, laiko limitą, atšaukimą, išvesties biudžetus ir visko-arba-nieko patvirtinimą nemokamai. Adapteriai tik Windows, bėga po vieną puslapį sinchroniškai ir neišlygina pasukimo, nei apdoroja paveikslėlį toliau už tai, ką duoda atvaizduoklis, tad paveikslėlio kokybė įėjime vis tiek nulemia lubas išėjimui
Tesseract ir RapidOCR adapteriai, nematomo teksto sluoksnio rašytojas, juos maitinantis puslapių atvaizduoklis ir rezultatą patvirtinantis teksto ištraukimas visi atkeliauja tame pačiame natyviame VCL komponente Delphi ir C++Builder aplinkoms. Jei į dokumentų fiksavimo arba archyvavimo programą pridedate OCR, HotPDF Delphi PDF komponentas duoda konvejerį, likus įdiegti tik patį OCR variklį