HotPDF trasforma pagine PDF scansionate in PDF ricercabile con Tesseract attraverso HPDFCreateTesseractOCREngine, una factory che avvolge un eseguibile Tesseract installato localmente in un IHPDFOCREngine. Passi quel motore a ApplyLoadedOCRTextLayer, che renderizza ogni pagina, esegue Tesseract una volta per pagina, parsifica il suo output TSV a livello di parola, e commette un layer di testo Unicode invisibile per tutte le pagine richieste in un'unica transazione, o per nessuna
Il motivo per cui questo adapter esiste è la portata. Il motore OCR integrato a template matching è deliberamente stretto: lettere e cifre ASCII a stampa, nient'altro. Fatture con nomi accentati, contratti in cinese e archivi multilingua hanno bisogno di un riconoscitore vero con modelli linguistici addestrati, e Tesseract è il candidato ovvio perché è un programma da riga di comando che puoi installare accanto alla tua applicazione. Chiamare un programma esterno da una libreria documentale sembra banale. Non lo è, e la maggior parte del codice interessante dell'adapter riguarda ciò che accade quando il programma si comporta male, si appende, viene annullato, o eredita cose che non dovrebbe mai vedere
Come pilota HotPDF Tesseract da un'applicazione Delphi?
HotPDF esegue Tesseract come processo figlio nascosto per pagina, dandogli in pasto una bitmap renderizzata e rileggendo un file TSV, ed espone il risultato attraverso la stessa fessura IHPDFOCREngine che usa il motore integrato. Nulla a valle cambia: mappatura delle coordinate, gestione della rotazione, validazione Unicode, filtraggio per confidence, e il commit atomico sono la pipeline del layer di testo che hai già. La factory vive nella unit HPDFTesseractRecognition e valida con zelo: l'eseguibile deve esistere, la directory tessdata deve esistere, il timeout deve stare tra 1 e 3.600.000 millisecondi, e l'identificatore di lingua può contenere solo lettere ASCII, cifre, _ e +. Quest'ultimo controllo conta perché la stringa di lingua finisce su una riga di comando, e eng+chi_sim è un valore Tesseract legittimo mentre qualunque cosa con virgolette o spazi no
uses
SysUtils, HPDFTypes, HPDFDoc, HPDFTesseractRecognition;
procedure MakeSearchable(const SourceFile, TargetFile: string;
Token: THPDFCancellationToken);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// alza EArgumentException per un eseguibile mancante, tessdata mancante,
// un identificatore di lingua sbagliato, o un timeout fuori da 1..3600000 ms
Engine := HPDFCreateTesseractOCREngine(
'C:\OCR\Tesseract\tesseract.exe',
'C:\OCR\Tesseract\tessdata',
'eng+chi_sim', // parecchi modelli uniti con '+'
120000); // limite per pagina, il default è 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;
// una lista pagine vuota significa tutte; le pagine con testo sono saltate per default
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 ogni pagina, Recognize crea una directory privata sotto il percorso temp chiamata HotPDF-OCR-{GUID}, salva la bitmap renderizzata come input.bmp, e lancia tesseract input.bmp output --tessdata-dir … -l … --dpi N --psm 3 -c tessedit_create_tsv=1, con ogni argomento di percorso tra virgolette usando le regole di escaping della riga di comando Windows per backslash e virgolette interne. Il valore --dpi è il DPI di rendering da THPDFOCRTextLayerOptions.DPI, così Tesseract non deve mai indovinare la risoluzione dai metadati dell'immagine, e --psm 3 chiede la segmentazione di pagina completamente automatica. Il motore si dichiara come Tesseract (local CLI), che è ciò che finisce in Info.EngineName. Tesseract e i suoi modelli linguistici non sono distribuiti con HotPDF; installarli è compito dell'applicazione
Perché il parser TSV è così severo?
Il parser TSV di HotPDF fa fallire l'intera pagina su qualunque riga malformata, perché una lista di parole parzialmente parsificata produce un layer di testo che discorda in silenzio dall'immagine. L'output TSV di Tesseract ha un header fisso a dodici colonne, da level a text, e HotPDF confronta la prima riga con quell'header esatto dopo aver tolto un eventuale byte order mark. Ogni riga successiva deve spezzarsi in esattamente dodici campi, e lo split si ferma dopo l'undicesimo tab così che un tab dentro il testo riconosciuto resti parte della parola invece di creare una tredicesima colonna. Solo le righe di livello 5 sono parole; i livelli da 1 a 4 descrivono pagine, blocchi, paragrafi e righe, e vengono saltati. Anche le righe di livello 5 con testo vuoto o puro whitespace vengono saltate, perché una parola vuota ha una box ma niente da localizzare o cercare. Tutto il resto viene controllato duro: geometria intera, una confidence parsificata con formato invariante en-US così una locale tedesca non legge 93.5 come spazzatura, una box che giace interamente dentro la bitmap, e una confidence tra 0 e 100. Un singolo fallimento alza un'eccezione, il motore restituisce False, e l'array di parole viene azzerato. I test di regression includono esattamente quel caso: una parola valida seguita da una riga rotta deve dare zero parole, non una
// condensato dal loop dei livelli 5 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; // righe pagina/blocco/paragrafo/riga
WordText := Fields[11];
if Trim(WordText) = '' then Continue; // le parole di whitespace non hanno posizione
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; // la pipeline si aspetta 0..1
Quell'ultima riga interagisce con un default che potresti non aspettarti. La confidence di Tesseract va da 0 a 100, la pipeline lavora da 0 a 1, e THPDFOCRTextLayerOptions.MinimumConfidence fa default a 0.5, quindi qualunque parola Tesseract sotto 50 viene contata in Info.DroppedWordCount e non arriva mai alla pagina. Su una scansionata pulita a 300 DPI è un piano minimo ragionevole. Su un fax rumoroso può buttare una quota sorprendente della pagina, e la mossa giusta è guardare il conteggio dei scartati prima di abbassare la soglia, perché le parole a bassa confidence sono esattamente quelle con più probabilità di essere sbagliate
Che cosa eredita il processo figlio Tesseract?
Il processo figlio Tesseract eredita da HotPDF esattamente due handle: un handle NUL per standard input e output, e un handle di file per standard error. Quella precisione è il punto. CreateProcess con bInheritHandles = True è come si passano gli handle standard a un figlio, ma da solo passa ogni handle ereditabile del processo ospite, inclusi file, pipe ed eventi aperti da codice non correlato nella tua applicazione. Il figlio tiene allora quegli oggetti in vita finché esce, così un file resta bloccato o una pipe non vede mai la sua fine mentre Tesseract macina una pagina. HotPDF chiude quel buco con un record di avvio esteso: STARTUPINFOEX, una lista di attributi che porta PROC_THREAD_ATTRIBUTE_HANDLE_LIST, e il flag di creazione EXTENDED_STARTUPINFO_PRESENT. Con la lista di handle in piedi, bInheritHandles deve comunque essere True, ma solo gli handle elencati attraversano il confine. Lo stesso pensiero di contenimento guida isolare i codec di immagini PDF in processi worker, dove il figlio è codice non fidato; qui il figlio è fidato, ma l'ospite non è l'unico proprietario della sua tabella di handle
// costanti mostrate per nome; la sorgente passa i loro valori numerici
// entrambi gli handle sono creati con bInheritHandle = True
InheritedHandles[0] := NullHandle; // stdin e stdout
InheritedHandles[1] := ErrorHandle; // stderr.txt nella directory privata
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, // richiesto dalla lista di handle
CREATE_NO_WINDOW or EXTENDED_STARTUPINFO_PRESENT,
nil, PChar(DirectoryName), Startup.StartupInfo, ProcessInfo);
Perché un run OCR annullato può sembrare un fallimento del motore?
Un run OCR annullato sembra un fallimento del motore perché IHPDFOCREngine.Recognize restituisce un singolo Boolean, e False significa sia "Tesseract è fallito" sia "l'utente ha premuto Cancel". L'adapter polla il token di cancellazione e il timeout ogni 25 millisecondi mentre il figlio gira, e quando il token scatta alza un'eccezione dentro Recognize, cattura la sua stessa eccezione, pulisce, e restituisce False con una diagnostica. Se la pipeline trattasse quello come un errore del motore, chi chiama vedrebbe otlsEngineError per un lavoro che l'utente ha fermato deliberatamente. ApplyLoadedOCRTextLayer quindi controlla prima il token ogni volta che Recognize restituisce False, e converte il risultato in fallimento del motore solo se il token non era impostato. Quell'ordine preserva il contratto multipagina: riconoscimento, validazione, contabilità del budget e costruzione del contenuto girano per ogni pagina richiesta prima che la transazione del graph si apra, quindi una cancellazione a pagina 40 di 50 riporta otlsCancelled e lascia il documento, prime 39 pagine incluse, intatto. Non c'è un file parzialmente ricercabile da spiegare dopo, e il resto della gestione dei fallimenti segue lo stesso stile limitato:
- Il timeout è per chiamata
Recognize, misurato dal suo inizio, quindi il default di 60.000 ms vale per ogni pagina anziché per l'intero documento - Un figlio ancora in esecuzione al timeout o alla cancellazione viene terminato, atteso fino a 5 secondi, e la sua directory privata viene cancellata in un blocco
finally output.tsvha un tetto a 64 MiB estderr.txta 1 MiB, controllati mentre il figlio gira oltre che dopo la sua uscita- Conteggio parole e code unit UTF-16 hanno un tetto per pagina dai budget residui
MaxWordsPerPage,MaxTotalWordseMaxTextCodeUnits, e superarli fa fallire il run anziché troncare la lista di parole - Lo standard output va a
NULperché Tesseract scriveoutput.tsv, mentre lo standard error va a un file così un exit code non nullo viene riportato con fino a 4.096 caratteri del lamento del motore, di solito il modo più rapido di scoprire che manca un file.traineddata
Come le parole riconosciute diventano un layer di testo invisibile
HotPDF scrive le parole di Tesseract come testo invisibile usando il text rendering mode 3, la modalità né fill né stroke definita in ISO 32000-1 §9.3.6, così la pagina mostra ancora l'immagine scansionata mentre ricerca e copia lavorano sulle parole riconosciute. Il content stream apre BT con 3 Tr, e ogni parola riceve una matrice Tm alla sua baseline, una dimensione del font derivata dall'altezza della box in pixel al DPI di rendering, e una scala orizzontale Tz che stira la corsa di glyph alla larghezza della box misurata, che è perché l'evidenziazione di ricerca atterra sulla parola nell'immagine invece di vagare per essa
Il TSV di Tesseract ha box ma nessuna baseline, quindi l'adapter riporta ogni parola senza di essa e la pipeline stima la baseline a un quinto dell'altezza della box sopra il bordo inferiore. Il testo in sé passa per un font Type0 non incluso condiviso con codifica Identity-H e una CMap ToUnicode generata, un CID per ogni scalare Unicode distinto lungo l'intera corsa, che è come cinese, latino accentato e caratteri del piano supplementare sopravvivono tutti a copia e ricerca. Quel progetto ha due limiti che vale la pena dire subito: una corsa può portare al massimo 65.535 scalari distinti, e il font non incluso non soddisfa il requisito di incorporamento dei font di ISO 19005, quindi l'output PDF/A ha bisogno di un font conforme incorporato a parte. Controllare il risultato è semplice e merita automazione: salva, ricarica, e percorri l'ordinario percorso di testo del documento caricato da estrarre testo da un PDF caricato in Delphi; se le parole tornano nelle pagine attese, il layer è vero
RapidOCR e altri motori sullo stesso protocollo TSV
HotPDF riusa lo stesso runner di processo e parser TSV per RapidOCR attraverso HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds), che è la scelta più utile per le scansioni in cinese semplificato. La riga di comando è identica tranne che il percorso dello script bridge viene inserito dopo l'eseguibile Python, e la lingua è fissata a chi_sim. HotPDF spedisce il bridge come tools/OCR/rapidocr_tsv.py; si aspetta i pacchetti rapidocr e onnxruntime più tre modelli ONNX locali, disattiva i download automatici dei modelli, e scrive TSV a forma Tesseract così il lato Delphi non ha bisogno di un secondo parser. Il nome del motore riportato in Info.EngineName è RapidOCR (local ONNX). Quella forma suggerisce la ricetta generale: qualunque riconoscitore che tu possa avvolgere in un piccolo script che accetta la lista di argomenti in stile Tesseract ed emette il TSV a dodici colonne eredita per gratis isolamento degli handle, timeout, cancellazione, budget di output e commit tutto-o-nulla. Gli adapter sono solo Windows, girano una pagina alla volta in modo sincrono, e non raddrizzano né preprocessano l'immagine oltre ciò che produce il renderer, quindi la qualità dell'immagine in ingresso continua a fissare il tetto su ciò che esce
Gli adapter Tesseract e RapidOCR, il writer del layer di testo invisibile, il renderer di pagine che li alimenta, e l'estrazione di testo che verifica il risultato sono tutti nella stessa componente VCL nativa per Delphi e C++Builder. Se stai aggiungendo OCR a un'applicazione di document capture o archiviazione, la PDF component HotPDF Delphi ti dà la pipeline con solo il motore OCR in sé rimasto da installare