Articolo tecnico

Tesseract OCR verso PDF ricercabile in Delphi con HotPDF

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

Pipeline OCR HotPDF per pagina: renderizza la pagina al DPI configurato, salva input.bmp in una directory privata HotPDF-OCR, lancia il processo figlio Tesseract con tessedit_create_tsv, parsifica il TSV a dodici colonne, filtra le parole per confidence, e commette il layer di testo invisibile per tutte le pagine richieste o per nessuna
L'adapter sostituisce solo il riconoscimento: rendering, parsing, validazione e il commit tutto-o-nulla restano nella pipeline del layer di testo esistente, quindi il codice a valle non cambia mai

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

Sei gate che ogni riga TSV di Tesseract passa in HotPDF: header esatto a dodici colonne, esattamente dodici campi, solo livello 5, testo non vuoto, una box dentro la bitmap, e confidence da 0 a 100 parsificata invariante, dove una riga rotta sola fa fallire l'intera pagina fino a zero parole
Una lista di parole parzialmente parsificata discorderebbe in silenzio dall'immagine, quindi il parser rifiuta l'intera pagina alla prima riga malformata invece di tenersi le parole già lette
// 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

Eredità degli handle del processo figlio Tesseract in HotPDF: un CreateProcess semplice con bInheritHandles passa al figlio ogni handle ereditabile di file, pipe ed evento, mentre STARTUPINFOEX con PROC_THREAD_ATTRIBUTE_HANDLE_LIST limita l'insieme a un handle NUL per stdin e stdout più l'handle del file stderr
Senza la lista di attributi il figlio tiene in vita oggetti non correlati finché esce, bloccando file e facendo morire di fame le pipe; con essa, solo i due handle elencati attraversano il confine
// 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.tsv ha un tetto a 64 MiB e stderr.txt a 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, MaxTotalWords e MaxTextCodeUnits, e superarli fa fallire il run anziché troncare la lista di parole
  • Lo standard output va a NUL perché Tesseract scrive output.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