HotPDF maakt van gescande PDF-pagina's een doorzoekbare PDF met Tesseract via HPDFCreateTesseractOCREngine, een factory die een lokaal geïnstalleerde Tesseract-executable omwikkelt als IHPDFOCREngine. Die engine geeft u mee aan ApplyLoadedOCRTextLayer, dat elke pagina rendert, Tesseract één keer per pagina draait, zijn word-level TSV-uitvoer parseert, en een onzichtbare Unicode-tekstlaag voor alle gevraagde pagina's in één transactie vastlegt, of voor geen enkele
De reden dat deze adapter bestaat is scope. De ingebouwde template-matching-OCR-engine is met opzet smal: machinedrukkens ASCII-letters en -cijfers, niets anders. Facturen met namen met diakrieten, Chinese contracten en meertalige archieven hebben een echte herkenner met getrainde taalmodellen nodig, en Tesseract is de voor de hand liggende kandidaat omdat het een commandline-programma is dat u naast uw applicatie kunt installeren. Een extern programma aanroepen vanuit een documentlibrary klinkt triviaal. Is het niet, en het grootste deel van de interessante code in de adapter gaat over wat er gebeurt wanneer het programma zich misdraagt, blijft hangen, wordt geannuleerd, of dingen erft die het nooit zou mogen zien
Hoe stuurt HotPDF Tesseract aan vanuit een Delphi-applicatie?
HotPDF draait Tesseract als verborgen kindproces per pagina, voert het een gerenderde bitmap en leest een TSV-bestand terug, en stelt het resultaat bloot via dezelfde IHPDFOCREngine-naad die de ingebouwde engine gebruikt. Er verandert niets downstream: coördinaatmapping, rotatie-afhandeling, Unicode-validatie, confidence-filtering en de atomische commit zijn de tekstlaag-pipeline die u al had. De factory woont in de unit HPDFTesseractRecognition en valideert eager: de executable moet bestaan, de tessdata-map moet bestaan, de timeout moet tussen 1 en 3.600.000 milliseconden liggen, en de language identifier mag alleen ASCII-letters, cijfers, _ en + bevatten. Die laatste controle telt omdat de taalstring op een commandline eindigt, en eng+chi_sim is een legitieme Tesseract-waarde terwijl alles met aanhalingstekens of spaties dat niet is
uses
SysUtils, HPDFTypes, HPDFDoc, HPDFTesseractRecognition;
procedure MakeSearchable(const SourceFile, TargetFile: string;
Token: THPDFCancellationToken);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// gooit EArgumentException bij een ontbrekende executable, ontbrekende tessdata,
// een verkeerde language identifier, of een timeout buiten 1..3600000 ms
Engine := HPDFCreateTesseractOCREngine(
'C:\OCR\Tesseract\tesseract.exe',
'C:\OCR\Tesseract\tessdata',
'eng+chi_sim', // meerdere modellen samengevoegd met '+'
120000); // limiet per pagina, default is 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;
// een lege paginalijst betekent elke pagina; pagina's met tekst worden standaard overgeslagen
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;
Per pagina maakt Recognize een privémap aan onder het temppad met de naam HotPDF-OCR-{GUID}, slaat de gerenderde bitmap op als input.bmp, en start tesseract input.bmp output --tessdata-dir … -l … --dpi N --psm 3 -c tessedit_create_tsv=1, met elk padargument gequote volgens de Windows-commandline-escaperegels voor backslashes en ingebedde quotes. De --dpi-waarde is de render-DPI uit THPDFOCRTextLayerOptions.DPI, dus Tesseract hoeft de resolutie nooit uit beeldmetadata te raden, en --psm 3 vraagt volledig automatische paginasegmentatie. De engine meldt zichzelf als Tesseract (local CLI), en dat is wat in Info.EngineName belandt. Tesseract en zijn taalmodellen worden niet met HotPDF meegeleverd; ze installeren is het werk van de applicatie
Waarom is de TSV-parser zo streng?
De TSV-parser in HotPDF laat de hele pagina vallen op elke misvormde rij, want een gedeeltelijk geparsede woordenlijst levert een tekstlaag op die geruisloos uit de pas loopt met de afbeelding. De TSV-uitvoer van Tesseract heeft een vaste header van twaalf kolommen, van level tot text, en HotPDF vergelijkt de eerste regel met exact die header na het strippen van een optionele byte order mark. Elke volgende rij moet splitten in precies twaalf velden, en de splitsing stopt na de elfde tab zodat een tab binnen de herkende tekst deel van het woord blijft in plaats van een dertiende kolom te maken. Alleen level 5-rijen zijn woorden; de levels 1 tot en met 4 beschrijven pagina's, blokken, paragrafen en regels, en die worden overgeslagen. Level 5-rijen waarvan de tekst leeg of pure witruimte is worden ook overgeslagen, want een blanko woord heeft een box maar niets te lokaliseren of te zoeken. Al het resterende wordt hard gecontroleerd: geheeltallige geometrie, een confidence geparsd met een invariant en-US-formaat zodat een Duitse locale 93.5 niet als rommel leest, een box die volledig binnen de bitmap ligt, en een confidence tussen 0 en 100. Eén mislukking gooit, de engine geeft False terug, en de woordenarray wordt gewist. De regressietests bevatten precies dat geval: één geldig woord gevolgd door een kapotte rij moet nul woorden opleveren, niet één
// ingekort uit de level-5-lus in 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; // pagina-/blok-/paragraaf-/regel-rijen
WordText := Fields[11];
if Trim(WordText) = '' then Continue; // witruimte-woorden hebben geen positie
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; // de pipeline verwacht 0..1
Die laatste regel speelt samen met een default die u misschien niet verwacht. Tesseract-confidence loopt van 0 tot 100, de pipeline rekent in 0 tot 1, en THPDFOCRTextLayerOptions.MinimumConfidence staat default op 0.5, dus elk Tesseract-woord onder de 50 wordt geteld in Info.DroppedWordCount en bereikt de pagina nooit. Op een schone scan van 300 DPI is dat een redelijke vloer. Op een ruisige fax kan het een verrassend deel van de pagina laten vallen, en het juiste is eerst naar het aantal gedropte woorden te kijken voordat u de drempel verlaagt, want low-confidence-woorden zijn precies degenen die het meest waarschijnlijk fout zijn
Wat erft het Tesseract-kindproces?
Het Tesseract-kindproces erft precies twee handles van HotPDF: een NUL-handle voor standard input en output, en een bestandshandle voor standard error. Die precisie is het punt. CreateProcess met bInheritHandles = True is hoe u standaardhandles aan een kind doorgeeft, maar op zichzelf geeft hij elke overneembare handle in het hostproces door, inclusief bestanden, pipes en events die door ongerelateerde code in uw applicatie zijn geopend. Het kind houdt die objecten dan in leven tot hij afsluit, dus een bestand blijft vergrendeld of een pipe ziet zijn einde nooit terwijl Tesseract door een pagina woelt. HotPDF dicht dat gat met een extended startup record: STARTUPINFOEX, een attributenlijst met PROC_THREAD_ATTRIBUTE_HANDLE_LIST, en de creation-flag EXTENDED_STARTUPINFO_PRESENT. Met de handle-lijst op zijn plek moet bInheritHandles nog steeds True zijn, maar alleen de opgesomde handles steken de grens over. Hetzelfde inperkingsdenken drijft het isoleren van PDF-image-codecs in workerprocessen, waar het kind onbetrouwbare code is; hier is het kind betrouwbaar, maar de host is niet de enige eigenaar van zijn eigen handle-tabel
// constanten bij naam getoond; de source geeft hun numerieke waarden door
// beide handles worden aangemaakt met bInheritHandle = True
InheritedHandles[0] := NullHandle; // stdin en stdout
InheritedHandles[1] := ErrorHandle; // stderr.txt in de privé-map
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, // vereist door de handle-lijst
CREATE_NO_WINDOW or EXTENDED_STARTUPINFO_PRESENT,
nil, PChar(DirectoryName), Startup.StartupInfo, ProcessInfo);
Waarom kan een geannuleerde OCR-run op een enginefalen lijken?
Een geannuleerde OCR-run lijkt op een enginefalen omdat IHPDFOCREngine.Recognize één enkele Boolean teruggeeft, en False zowel "Tesseract faalde" als "de gebruiker drukte op Annuleren" betekent. De adapter polld de cancellation token en de timeout elke 25 milliseconden terwijl het kind draait, en wanneer het token afgaat gooit hij binnen Recognize, vangt zijn eigen exception op, ruimt op, en geeft False terug met een diagnose. Zou de pipeline dat als enginefout behandelen, dan zou de aanroeper otlsEngineError zien voor een klus die de gebruiker bewust stopte. ApplyLoadedOCRTextLayer controleert daarom eerst het token zodra Recognize False teruggeeft, en zet het resultaat alleen om in een enginefalen als het token niet gezet was. Die volgorde houdt het multi-page-contract overeind: herkenning, validatie, budgetverwerking en contentconstructie draaien voor elke gevraagde pagina voordat de graaftransactie opent, dus een annulering op pagina 40 van 50 meldt otlsCancelled en laat het document, inclusief de eerste 39 pagina's, onaangetast. Er is geen gedeeltelijk doorzoekbaar bestand dat u later moet uitleggen, en de rest van de foutafhandeling volgt dezelfde begrensde stijl:
- De timeout geldt per
Recognize-aanroep, gemeten vanaf zijn start, dus de default van 60.000 ms geldt per pagina in plaats van voor het hele document - Een kind dat bij timeout of annulering nog draait wordt beëindigd, hoogstens 5 seconden afgewacht, en zijn privémap wordt in een
finally-blok verwijderd output.tsvis gecapteerd op 64 MiB enstderr.txtop 1 MiB, gecontroleerd terwijl het kind draait én nadat het is afgesloten- Woordenaantal en UTF-16-code-eenheden worden per pagina begrensd door de resterende budgets
MaxWordsPerPage,MaxTotalWordsenMaxTextCodeUnits, en overschrijding laat de run falen in plaats van de woordenlijst af te kappen - Standard output gaat naar
NULomdat Tesseractoutput.tsvwegschrijft, terwijl standard error naar een bestand gaat zodat een exitcode ongelijk aan nul wordt gemeld met tot 4.096 tekens van de eigen klacht van de engine, doorgaans de snelste manier om te leren dat een.traineddata-bestand ontbreekt
Hoe de herkende woorden een onzichtbare tekstlaag worden
HotPDF schrijft Tesseract-woorden als onzichtbare tekst met text rendering mode 3, de neither-fill-nor-stroke-modus uit ISO 32000-1 §9.3.6, zodat de pagina nog steeds de gescande afbeelding toont terwijl zoeken en kopiëren op de herkende woorden werken. De content stream opent BT met 3 Tr, en elk woord krijgt een Tm-matrix op zijn baseline, een fontgrootte afgeleid uit de boxhoogte in pixels bij de render-DPI, en een Tz-horizontale schaling die de glyph-run uitrekt tot de gemeten boxbreedte, en daarom landt een zoekmarkering op het woord in de afbeelding in plaats van eroverheen te drijven
De TSV van Tesseract heeft boxes maar geen baselines, dus de adapter meldt elk woord zonder baseline en de pipeline schat de baseline op één vijfde van de boxhoogte boven de onderrand. De tekst zelf gaat door een gedeeld unembedded Type0-font met Identity-H-codering en een gegenereerde ToUnicode-CMap, één CID per onderscheiden Unicode-scalar over de hele run, en daardoor overleven Chinees, Latijn met diakrieten en supplementary-plane-tekens allemaal kopiëren en zoeken. Dat ontwerp heeft twee grenzen die u vooraf wilt weten: één run kan hoogstens 65.535 onderscheiden scalars meedragen, en het unembedded font voldoet niet aan de font-embedding-eis van ISO 19005, dus PDF/A-uitvoer heeft een apart ingebed conformerend font nodig. Het resultaat controleren is simpel en de automatisering waard: sla op, herlaad, en draai het gewone geladen-document-tekstpad uit tekst extraheren uit een geladen PDF in Delphi; komen de woorden terug op de verwachte pagina's, dan is de laag echt
RapidOCR en andere engines op hetzelfde TSV-protocol
HotPDF hergebruikt dezelfde process runner en TSV-parser voor RapidOCR via HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds), de nuttigere keuze voor vereenvoudigd-Chinese scans. De commandline is identiek, op één na: het bridge-scriptpad wordt achter de Python-executable geplaatst, en de taal staat vast op chi_sim. HotPDF levert de bridge als tools/OCR/rapidocr_tsv.py; hij verwacht de pakketten rapidocr en onnxruntime plus drie lokale ONNX-modellen, schakelt automatische modeldownloads uit, en schrijft Tesseract-vormige TSV zodat de Delphi-kant geen tweede parser nodig heeft. De enginesnaam die in Info.EngineName wordt gemeld is RapidOCR (local ONNX). Die vorm suggereert het algemene recept: elke herkenner die u in een klein script wikkelt dat de Tesseract-stijl-argumentlijst accepteert en de twaalfkoloms TSV uitspuugt erft handle-isolatie, de timeout, annulering, uitvoerbudgetten en de alles-of-niets-commit gratis. De adapters zijn Windows-only, draaien synchroon één pagina per keer, en deskewen of voorbewerken de afbeelding niet verder dan wat de renderer oplevert, dus de beeldkwaliteit aan de ingang blijft het plafond bepalen van wat eruit komt
De Tesseract- en RapidOCR-adapters, de schrijver van de onzichtbare tekstlaag, de paginarenderer die ze voedt en de tekstextractie die het resultaat verifieert zitten allemaal in dezelfde native VCL-component voor Delphi en C++Builder. Voegt u OCR toe aan een documentcapturing- of archiveringsapplicatie, dan geeft de HotPDF Delphi PDF component u de pipeline met alleen de OCR-engine zelf nog te installeren