A HotPDF szkennelt PDF oldalakat alakít kereshető PDF-fé Tesseracttal a HPDFCreateTesseractOCREngine-en keresztül, egy factory, ami egy lokálisan telepített Tesseract végrehajtható fájlt csomagol IHPDFOCREngine-ként. Ezt az engine-t adod át az ApplyLoadedOCRTextLayer-nek, ami minden oldalt renderel, oldalonként egyszer futtatja a Tesseractot, parszolja a szószintű TSV kimenetét, és láthatatlan Unicode szövegréteget rögzít az összes kért oldalra egyetlen tranzakcióban, vagy egyikre sem
Az ok, amiért ez az adapter létezik, a hatókör. A beépített template-matching OCR engine szándékosan szűk: géppel nyomtatott ASCII betűk és számjegyek, semmi más. Ékezetes neveket hordozó számlák, kínai szerződések és többnyelvű archívumok valódi felismerőt igényelnek betanított nyelvi modellekkel, a Tesseract pedig kézenfekvő jelölt, mert parancssori program, amit az alkalmazásod mellé telepíthetsz. Külső programot hívni egy document libraryből triviálisan hangzik. Nem az, és az adapter érdekesebb kódjának nagy része arról szól, mi történik, amikor a program rosszul viselkedik, lóg, megszakításra kerül, vagy olyan dolgokat örököl, amiket soha nem láthatna
Hogyan hajtja a HotPDF a Tesseractot egy Delphi alkalmazásból?
A HotPDF oldalonként rejtett gyerekprocesszként futtatja a Tesseractot, renderelt bitmapot etet neki, és egy TSV fájlt olvas vissza, az eredményt pedig ugyanazon a IHPDFOCREngine varraton exponálja, amit a beépített engine használ. Semmi nem változik lejjebb: a koordinátaleképezés, a forgatás kezelése, a Unicode validáció, a confidence-szűrés és az atomi rögzítés az a szövegréteg-pipeline, ami már megvan. A factory a HPDFTesseractRecognition unitban lakik, és lelkesen validál: a végrehajtható fájlnak léteznie kell, a tessdata könyvtárnak léteznie kell, a timeoutnak 1 és 3 600 000 ezredmásodperc közé kell esnie, a nyelvazonosító pedig csak ASCII betűket, számjegyeket, _-t és +-ot tartalmazhat. Az utolsó ellenőrzés azért számít, mert a nyelvi string parancssorra kerül, és az eng+chi_sim legitim Tesseract érték, amíg bármi, ami idézőjelet vagy szóközt tartalmaz, nem az
uses
SysUtils, HPDFTypes, HPDFDoc, HPDFTesseractRecognition;
procedure MakeSearchable(const SourceFile, TargetFile: string;
Token: THPDFCancellationToken);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// EArgumentException-t dob hiányzó végrehajtható fájl, hiányzó tessdata,
// rossz nyelvazonosító vagy 1..3600000 ms-en kívüli timeout esetén
Engine := HPDFCreateTesseractOCREngine(
'C:\OCR\Tesseract\tesseract.exe',
'C:\OCR\Tesseract\tessdata',
'eng+chi_sim', // több modell '+'-cal összefűzve
120000); // oldalankénti limit, az alapérték 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;
// üres oldallista minden oldalt jelent; a szöveges oldalak alapból átugródnak
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;
Minden oldalhoz a Recognize privát könyvtárat hoz létre a temp útvonal alatt HotPDF-OCR-{GUID} néven, elmenti a renderelt bitmapot input.bmp-ként, és elindítja a tesseract input.bmp output --tessdata-dir … -l … --dpi N --psm 3 -c tessedit_create_tsv=1 parancsot, minden útvonal argumentumot a Windows parancssori escape-szabályok szerint idézve a backslashek és beágyazott idézőjelek esetére. A --dpi érték a renderelési DPI a THPDFOCRTextLayerOptions.DPI-ből, így a Tesseractnak soha nem kell a felbontást képi metaadatokból találgatnia, a --psm 3 pedig teljesen automatikus oldal-szegmentálást kér. Az engine magát Tesseract (local CLI)-ként jelenti, ez kerül az Info.EngineName-be. A Tesseract és a nyelvi modelljei nem járnak a HotPDF-fel; a telepítésük az alkalmazás dolga
Miért olyan szigorú a TSV parser?
A HotPDF TSV parsere bármilyen rosszul formált sornál az egész oldalt megbuktatja, mert egy részben parszolt szólista olyan szövegréteget gyárt, ami csendben ellentmond a képnek. A Tesseract TSV kimenetének fix tizenkét oszlopos fejléce van, a level-től a text-ig, és a HotPDF az első sort ahhoz a pontos fejléchez méri egy opcionális byte order mark levágása után. Minden következő sornak pontosan tizenkét mezőre kell szakadnia, és a felbontás a tizenegyedik tab után megáll, hogy a felismert szövegen belüli tab a szó része maradjon egy tizenharmadik oszlop gyártása helyett. Csak az 5-ös szintű sorok szavak; az 1-től 4-ig terjedő szintek oldalakat, blokkokat, bekezdéseket és sorokat írnak le, azok átugródnak. Az 5-ös szintű sorok közül azok is átugródnak, amiknek a szövege üres vagy tiszta whitespace, mert egy üres szónak van doboza, de nincs mit lokálni vagy keresni. Minden más keményen ellenőrzött: egész számú geometria, invariant en-US formátummal parszolt confidence, hogy egy német locale ne olvassa a 93.5-öt szemétként, teljesen a bitmapon belüli doboz, és 0 és 100 közti confidence. Egyetlen hiba is kivételt dob, az engine -t ad vissza, és a szótömb törlődik. A regressziós tesztek pontosan azt az esetet tartalmazzák: egy érvényes szó, majd egy törött sor nulla szót kell adjon, nem egyetFalse
// tömörítve a HPDFLocalTSVRecognition 5-ös szintű ciklusából
if (Fields.Count <> 12) or not TryStrToInt(Fields[0], Level) then
raise EConvertError.Create('Invalid Local OCR TSV row');
if Level <> 5 then Continue; // oldal/blokk/bekezdés/sor sorok
WordText := Fields[11];
if Trim(WordText) = '' then Continue; // whitespace szavaknak nincs pozíciójuk
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; // a pipeline 0..1-et vár
Az utolsó sor egy alapértelmezéssel lép kapcsolatba, amire talán nem számítanál. A Tesseract confidence 0-tól 100-ig fut, a pipeline 0-tól 1-ig dolgozik, a THPDFOCRTextLayerOptions.MinimumConfidence pedig 0,5-re alapértelmezett, így bármelyik 50 alatti Tesseract szó az Info.DroppedWordCount-ba számolódik, és soha nem jut az oldalra. Egy tiszta 300 DPI-es szkennen ez ésszerű alsó határ. Egy zajos faxon az oldal meglepő hányadát dobhatja el, és a helyes lépés a küszöb lejjebb vétele előtt megnézni az eldobott számot, mert az alacsony confidence-ű szók pontosan azok, amik leginkább tévesek lehetnek
Mit örököl a Tesseract gyerekprocessz?
A Tesseract gyerekprocessz pontosan két handlet örököl a HotPDF-től: egy NUL handlet a standard bemenethez és kimenethez, és egy fájlhandlet a standard hibához. Ez a pontosság a lényeg. A CreateProcess bInheritHandles = True-dal az, ahogy standard handereket adsz át egy gyereknek, de önmagában a gazdaprocessz minden örökölhető handlejét átadja, beleértve fájlokat, pipe-okat és eseményeket, amiket az alkalmazásodban nem kapcsolódó kód nyitott. A gyerek aztán életben tartja azokat az objektumokat, amíg ki nem lép, így egy fájl zárolva marad, vagy egy pipe soha nem látja a végét, miközben a Tesseract végigcsiszol egy oldalon. A HotPDF kiterjesztett indítórekorddal zárja be ezt a rést: STARTUPINFOEX, PROC_THREAD_ATTRIBUTE_HANDLE_LIST-et hordozó attribútumlista és a EXTENDED_STARTUPINFO_PRESENT létrehozási flag. A handlelista megléte mellett a bInheritHandles-nek továbbra is True-nak kell lennie, de csak a listázott handerek lépik át a határt. Ugyanez a bezárási gondolkodás hajtja a PDF image codecek izolálását worker processzekben, ahol a gyerek nem megbízható kód; itt a gyerek megbízható, de a gazda nem az egyetlen tulajdonosa a saját handertáblájának
// a konstansok név szerint látszanak; a forrás a numerikus értékeiket adja át
// mindkét handel bInheritHandle = True beállítással készül
InheritedHandles[0] := NullHandle; // stdin és stdout
InheritedHandles[1] := ErrorHandle; // stderr.txt a privát könyvtárban
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, // a handlelista megköveteli
CREATE_NO_WINDOW or EXTENDED_STARTUPINFO_PRESENT,
nil, PChar(DirectoryName), Startup.StartupInfo, ProcessInfo);
Miért nézhet ki megszakított OCR futás, mint egy engine-hiba?
Egy megszakított OCR futás engine-hibának néz ki, mert az IHPDFOCREngine.Recognize egyetlen Boolean-t ad vissza, és a False egyszerre jelenti azt, hogy „a Tesseract megbukott" és azt, hogy „a felhasználó megnyomta a Cancel-t". Az adapter 25 ezredmásodpercenként pollolja a megszakítási tokent és a timeoutot, amíg a gyerek fut, és amikor a token elsül, a Recognize belül kivételt dob, elkapja a saját kivételét, takarít, és False-t ad vissza diagnosztikával. Ha a pipeline engine-hibának tekintené, a hívó otlsEngineError-t látna egy olyan feladatra, amit a felhasználó szándékosan állított le. Az ApplyLoadedOCRTextLayer ezért mindig először a tokent ellenőrzi, valahányszor a Recognize False-t ad vissza, és csak akkor fordítja az eredményt engine-hibává, ha a token nem volt beállítva. Ez a sorrend őrzi meg a többoldalas szerződést: a felismerés, a validáció, a budgetelszámolás és a tartalomépítés minden kért oldalra lefut, mielőtt a gráftranzakció kinyílna, így egy megszakítás az 50 oldal 40. oldalán otlsCancelled-ot jelent, és érintetlenül hagyja a dokumentumot, az első 39 oldalt is beleértve. Nincs részben kereshető fájl, amit később magyarázni kellene, és a hibakezelés többi része ugyanazt a határolt stílust követi:
- A timeout
Recognizehívásonkénti, az elejétől mérve, így az alapértelmezett 60 000 ms minden oldalra vonatkozik, nem a teljes dokumentumra - Egy timeouton vagy megszakításon még futó gyerek leállításra kerül, legfeljebb 5 másodpercig vár rá, és a privát könyvtára
finallyblokkban törlődik - Az
output.tsv64 MiB-re, astderr.txt1 MiB-re van szabva, ellenőrizve a gyerek futása alatt és kilépése után egyaránt - A szószám és a UTF-16 kódegységek oldalanként a megmaradó
MaxWordsPerPage,MaxTotalWordsésMaxTextCodeUnitsbudgetekkel vannak szabva, és azok túllépése megbuktatja a futást a szólista csonkolása helyett - A standard kimenet
NUL-ba megy, mert a Tesseract azoutput.tsv-t írja, a standard hiba pedig fájlba, hogy egy nemnulla kilépési kód legfeljebb 4 096 karakternyi engine-panasszal jelentődjön, ami általában a leggyorsabb módja annak, hogy megtudd: hiányzik egy.traineddatafájl
Hogyan válnak a felismert szavak láthatatlan szövegréteggé
A HotPDF a Tesseract szavait láthatatlan szövegként írja 3-as szövegrenderelési móddal, a se-kitöltés-se-körvonal móddal, amit az ISO 32000-1 §9.3.6-a definiál, így az oldal továbbra is a szkennelt képet mutatja, miközben a keresés és a másolás a felismert szavakon dolgozik. A tartalomstream BT-t nyit 3 Tr-rel, minden szó kap egy Tm mátrixot a bázisvonalán, a doboz pixelben mért magasságából a render DPI-n származtatott fontméretet, és egy Tz horizontális skálát, ami a glyph futamot a mért dobozszélességre nyújtja, ezért landol a keresési kiemelés a képen lévő szón, nem pedig sodródik át rajta
A Tesseract TSV-je dobozokat tartalmaz, de bázisvonalakat nem, így az adapter minden szót bázisvonal nélkül jelent, és a pipeline a bázisvonalat a doboz magasságának ötödére becsüli az alsó él fölött. Maga a szöveg egy megosztott, be nem ágyazott Type0 fonton megy át Identity-H kódolással és generált ToUnicode CMap-pel, egy CID minden különböző Unicode skalárra a teljes futamon át, ezért élik túl a másolást és a keresést a kínai, az ékezetes latin és a kiegészítő sikbeli karakter is. Ennek a dizájnnak két korlátja van, amit érdemes előre kimondani: egy futam legfeljebb 65 535 különböző skalárt hordozhat, és a be nem ágyazott font nem elégíti ki az ISO 19005 fontbeágyazási követelményét, így PDF/A kimenethez külön beágyazott, megfelelő font kell. Az eredmény ellenőrzése egyszerű, és megéri automatizálni: ments, töltsd újra, és futtasd a közönséges betöltött-dokumentum szövegútvonalat a szöveg kinyeréséből betöltött PDF-ből Delphiben; ha a szavak a várt oldalakon jönnek vissza, a réteg valódi
RapidOCR és más engine-ek ugyanazon a TSV protokollon
A HotPDF ugyanazt a processfuttatót és TSV parsert használja újra a RapidOCR-hez a HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds)-en át, ami a hasznosabb választás egyszerűsített kínai szkenneléseknél. A parancssor azonos, azzal a különbséggel, hogy a bridge szkript útvonala a Python végrehajtható után illeszkedik be, és a nyelv chi_sim-re van rögzítve. A HotPDF a bridge-et tools/OCR/rapidocr_tsv.py-ként szállítja; a rapidocr és az onnxruntime csomagokat várja plusz három helyi ONNX modellt, letiltja az automatikus modellletöltéseket, és Tesseract-alakú TSV-t ír, hogy a Delphi oldalnak ne kelljen második parser. Az Info.EngineName-ben jelentett engine-név RapidOCR (local ONNX). Az a forma az általános receptre utal: minden felismerő, amit be tudsz csomagolni egy kis szkriptbe, ami elfogadja a Tesseract-stílusú argumentumlistát, és kibocsátja a tizenkét oszlopos TSV-t, ingyen örökli a handleizolációt, a timeoutot, a megszakítást, a kimeneti budgeteket és a minden-vagy-semmi rögzítést. Az adapterek csak Windowson futnak, egyszerre egy oldalt szinkronban, és nem végeznek deskew-t vagy előfeldolgozást a renderelő kimenetén túl, így a befutó képminőség továbbra is a plafonja annak, ami kijön
A Tesseract és RapidOCR adapterek, a láthatatlan szövegréteg-író, az őket tápláló oldalrenderelő és az eredményt hitelesítő szövegkinyerés mind ugyanabban a natív VCL komponensben érkezik Delphihez és C++Builderhez. Ha OCR-t adsz dokumentumrögzítő vagy archiváló alkalmazáshoz, a HotPDF Delphi PDF komponens megadja a pipeline-t, és már csak magát az OCR engine-t kell telepíteni