Articolo tecnico

OCR HotPDF con la DLL Tesseract: la C API da Delphi

HotPDF fa girare Tesseract dentro il tuo processo Delphi attraverso HPDFCreateTesseractDLLOCREngine, una factory aggiunta nella v2.772.0 che carica dinamicamente una DLL compatibile Tesseract 5, pilota la sua C API (TessBaseAPIInit2, TessBaseAPIRecognize, l'iteratore dei risultati) e restituisce un IHPDFOCREngine. THotPDF.ApplyLoadedOCRTextLayer usa quel motore per aggiungere un livello di testo Unicode invisibile e ricercabile alle pagine PDF scansionate

Lo stesso recognizer era già raggiungibile attraverso l'adapter esterno tesseract.exe che scrive una BMP e analizza il TSV. Quella via funziona, ma ogni pagina paga un avvio di processo, un file bitmap temporaneo e un formato di testo senza baseline e senza controllo sulla segmentazione di pagina. Chiamare la DLL rimuove tutti e tre. Rimuove anche il muro del processo, il che significa che un binding Pascal sta dritto sopra strutture C, booleani C e stringhe allocate in C. Gran parte di ciò che vale la pena sapere su questo adapter è dove quel binding può andare storto in silenzio

Come fai girare Tesseract in-process da Delphi con HotPDF?

Far girare Tesseract in-process con HotPDF richiede una chiamata alla factory nell'unità HPDFTesseractRecognition e la stessa chiamata ApplyLoadedOCRTextLayer che usa ogni motore OCR di HotPDF. La factory valida con eager. Il file DLL e la directory tessdata devono esistere, l'identificatore di lingua può contenere solo lettere ASCII, cifre, _ e +, ogni modello in una combinazione come chi_sim+eng deve avere un file .traineddata corrispondente, e tutti i 21 export richiesti devono risolversi prima che il motore venga restituito. Gli errori di configurazione sollevano EArgumentException; una DLL che non carica solleva EOSError con il codice di errore Windows e un suggerimento di controllare architettura e dipendenze

uses
  SysUtils, HPDFDoc, HPDFTesseractRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // Un'applicazione Win64 richiede una DLL a 64 bit; le DLL di dipendenza stanno accanto
  Engine := HPDFCreateTesseractDLLOCREngine('C:\OCR\Win64\libtesseract-5.dll',
    'C:\OCR\tessdata', 'chi_sim+eng');   // THPDFTesseractOptions.Default
  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
    // Una lista pagine vuota significa tutte; le pagine con testo vengono saltate
    if not Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
      raise Exception.Create(string(Info.Diagnostic));
    Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
      ' words accepted, ', Info.DroppedWordCount, ' dropped');
    Doc.SaveLoadedDocument(TargetFile);
  finally
    Doc.Free;
  end;
end;

THPDFTesseractOptions.Default imposta PageSegMode a tpsAuto, EngineMode a temDefault, TimeoutMilliseconds a 60.000 e MaxPixels a 16.777.216. Il budget di pixel conta più di quanto sembri. Una pagina US Letter alla default di 300 DPI si renderizza in 2.550 × 3.300 pixel, circa 8,4 milioni, che ci stanno. La stessa pagina a 600 DPI è 5.100 × 6.600, circa 33,7 milioni, e l'adapter la rifiuta prima che Tesseract veda un pixel. Alza MaxPixels (il tetto è 67.108.864) o tieni il DPI dov'è; ogni lato è limitato anche a 32.767 pixel

La DLL viene caricata con LoadLibraryEx usando i flag di ricerca per la cartella della DLL stessa più le directory sicure di default, così le librerie immagini di cui Tesseract dipende possono stare accanto a lei senza toccare PATH o la directory corrente. HotPDF non include né scarica alcun runtime o modello OCR; provisioni entrambi tu

Che cosa cambia rispetto all'adapter tesseract.exe?

L'adapter DLL scambia l'isolamento di processo con un output più ricco e un overhead per pagina più basso. Entrambi gli adapter si innestano nella stessa pipeline del livello di testo, quindi mappatura delle coordinate, filtro di confidenza e la scrittura tutto-o-nulla sono identici; ciò che differisce è come entrano i pixel e come escono le parole

AspettoAdapter tesseract.exeAdapter DLL Tesseract
FactoryHPDFCreateTesseractOCREngineHPDFCreateTesseractDLLOCREngine
Pixel in ingressoFile BMP in una directory temporanea privataBuffer grayscale a 8 bit in memoria
Parole in uscitaTSV a livello parola, limitato a 64 MiBIteratore dei risultati, UTF-8 per parola
BaselineNon disponibiliTrasmessi da TessPageIteratorBaseline
Segmentazione di pagina e modalità motoreSolo segmentazione automaticaTHPDFTesseractPageSegMode, THPDFTesseractEngineMode
TimeoutDuro: il processo figlio viene terminatoCooperativo: Tesseract deve accorgersene
Isolamento di crash e memoriaProcesso separatoNessuno, condivide il tuo spazio di indirizzi

Un costo non sparisce. Ogni chiamata Recognize crea la propria istanza API e chiama TessBaseAPIInit2, quindi i modelli linguistici vengono inizializzati per pagina anziché una volta per motore. La cache dei file del sistema operativo ammorbidisce il ricaricamento, ma su grandi insiemi di modelli multilingua resta comunque il costo fisso dominante per pagina, e va a carico della scadenza di riconoscimento. Il motore della DLL RapidOCR in-process prende il progetto opposto e tiene i propri modelli ONNX residenti per tutta la vita del motore; i problemi di confine (C ABI, buffer in prestito, lavoro nativo non interrompibile) sono della stessa famiglia

Perché Delphi non può copiare la struct monitor di Tesseract?

Delphi non può replicare in sicurezza il monitor di avanzamento di Tesseract perché ETEXT_DESC contiene campi interni dipendenti dalla versione, quindi un record copiato a mano mette il callback di cancel e la scadenza agli offset sbagliati su alcune build. Niente fallisce rumorosamente quando succede. Tesseract semplicemente legge il tuo puntatore al callback da un campo che ora contiene altro, o non vede mai la scadenza

HotPDF quindi tratta il monitor come un puntatore opaco e lo tocca solo attraverso funzioni esportate: TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc, TessMonitorSetDeadlineMSecs e TessMonitorDelete. Se bindi tu la C API per un altro scopo, lo stesso pattern si applica. Lo schizzo qui sotto è il tuo codice di binding, non API HotPDF, e rispecchia le dichiarazioni che HotPDF usa internamente

Gestione del monitor della DLL Tesseract in HotPDF: copiare il record ETEXT_DESC dipendente dalla versione mette il callback di cancel e la scadenza a offset sbagliati e fallisce in silenzio, mentre HotPDF tratta il monitor come opaco, pilota TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc e TessMonitorSetDeadlineMSecs, e tiene il callback cdecl senza eccezioni
un puntatore opaco più cinque export sono l'intero contratto; il callback resta un Boolean da un byte che legge solo un flag e un orologio
type
  // C: typedef bool (*TessCancelFunc)(void *cancel_this, int words);
  TTessCancelFunc = function(CancelThis: Pointer; Words: Integer): Boolean; cdecl;
  TTessMonitorCreate = function: Pointer; cdecl;   // ETEXT_DESC*, mai dereferenziato
  TTessMonitorDelete = procedure(Monitor: Pointer); cdecl;
  TTessMonitorSetCancelFunc = procedure(Monitor: Pointer; Func: TTessCancelFunc); cdecl;
  TTessMonitorSetCancelThis = procedure(Monitor, CancelThis: Pointer); cdecl;
  TTessMonitorSetDeadlineMSecs = procedure(Monitor: Pointer; MSecs: Integer); cdecl;
  TTessBaseAPIRecognize = function(Handle, Monitor: Pointer): Integer; cdecl;

  TOCRJob = record
    CancelRequested: Boolean;
    DeadlineTick: UInt64;
  end;
  POCRJob = ^TOCRJob;

function ShouldCancel(CancelThis: Pointer; Words: Integer): Boolean; cdecl;
begin
  // Gira sullo stack di Tesseract: leggi flag e orologio, non sollevare mai
  Result := (CancelThis = nil) or POCRJob(CancelThis)^.CancelRequested or
    (GetTickCount64 >= POCRJob(CancelThis)^.DeadlineTick);
end;

// Uso, con i puntatori a funzione risolti da GetProcAddress:
//   Monitor := MonitorCreate();
//   try
//     MonitorSetCancelThis(Monitor, @Job);
//     MonitorSetCancelFunc(Monitor, ShouldCancel);
//     MonitorSetDeadlineMSecs(Monitor, RemainingMs);
//     RC := BaseAPIRecognize(API, Monitor);
//   finally
//     MonitorDelete(Monitor);
//   end;

Due dettagli in quello schizzo sono deliberati. Il callback restituisce Boolean, che è un byte sia in Delphi sia in Free Pascal, corrispondente al bool C in TessCancelFunc. Il BOOL Windows a quattro byte o il LongBool Delphi sembra intercambiabile e non lo è: quando un lato scrive un singolo byte e l'altro ne legge quattro, i byte alti del registro di ritorno sono ciò che vi era rimasto, e un false può arrivare come true. Lo stesso header complica ulteriormente le cose, perché funzioni come TessPageIteratorBoundingBox restituiscono un int, che HotPDF dichiara come Integer. Leggi il tipo C di ogni valore di ritorno invece di presumere una convenzione per tutta l'API

Il secondo dettaglio è che il callback non solleva mai. Un'eccezione Delphi che fa unwinding attraverso i frame C++ di Tesseract è comportamento indefinito, quindi il callback di HotPDF legge solo il token di cancellazione e un valore monotono GetTickCount64. L'adapter trasforma il risultato in una diagnostica di cancellazione o timeout dopo che TessBaseAPIRecognize torna, e esegue quel controllo indipendentemente dal codice di ritorno nativo

Quali puntatori nativi possiede il lato Delphi?

L'adapter DLL Tesseract di HotPDF possiede tre oggetti nativi per richiesta, l'istanza API, il monitor e l'iteratore dei risultati, e prende in prestito tutto il resto. Ogni chiamata Recognize crea il proprio insieme e lo rilascia in un blocco finally: TessResultIteratorDelete, poi TessMonitorDelete, poi TessBaseAPIDelete. Rilasciare l'interfaccia del motore scarica la libreria

Proprietà degli oggetti della DLL Tesseract in HotPDF per chiamata Recognize: l'iteratore dei risultati, il monitor e l'istanza API sono posseduti e liberati in quell'ordine dentro finally, l'iteratore di pagina da TessResultIteratorGetPageIterator è una vista in prestito che non deve mai venire liberata, e le stringhe GetUTF8Text vengono copiate e restituite via TessDeleteText
tre oggetti posseduti, tutto il resto in prestito: libera nell'ordine fisso, non fare mai doppia liberazione dell'iteratore di pagina, e non mescolare mai allocatori
  • TessResultIteratorGetPageIterator restituisce una vista in prestito dentro l'iteratore dei risultati, non un nuovo oggetto. HotPDF la usa per TessPageIteratorBoundingBox e TessPageIteratorBaseline e non la libera mai; cancellarla separatamente libererebbe la stessa memoria due volte
  • TessResultIteratorGetUTF8Text restituisce una stringa allocata dal runtime della DLL stessa. HotPDF la copia e la restituisce attraverso TessDeleteText in un blocco finally; una FreeMem Pascal la rilascerebbe sull'heap sbagliato
  • Il testo delle parole viene decodificato con validazione UTF-8 strict e controllo di lunghezza prima della conversione. Le parole con caratteri di controllo, UTF-8 malformato, box fuori dall'immagine, rettangoli invertiti o confidenza fuori da 0–100 fanno fallire la richiesta invece di venire sistemate in silenzio
  • Il testo totale per richiesta è limitato a 1.048.576 code unit UTF-16, e il conteggio di parole deve stare nel budget della richiesta consegnato da ApplyLoadedOCRTextLayer

La confidenza arriva come 0–100 e viene scalata a 0–1, quindi THPDFOCRTextLayerOptions.MinimumConfidence significa la stessa cosa per ogni motore. Quando Tesseract riporta una baseline, entrambi gli estremi vengono trasmessi; altrimenti la pipeline del livello di testo ripiega sulla propria stima geometrica, esattamente come fa per l'input TSV

Perché validare un enum prima che raggiunga la DLL?

HotPDF copia l'ordinale grezzo di PageSegMode e EngineMode in una Integer prima del controllo di intervallo, perché un compilatore può presumere che una variabile enum tenga sempre un valore dichiarato e ridurre Ord(X) > Ord(High(T)) a una costante false. Gli ordinali non sono decorazione: THPDFTesseractPageSegMode segue la numerazione di segmentazione pagina di Tesseract da 0 a 13, THPDFTesseractEngineMode segue la numerazione delle modalità motore da 0 a 3, e entrambi vanno alla DLL come interi semplici. Un record di opzioni costruito con FillChar, riempito da uno stream, o passato da C++Builder con un intero castato può portare un byte come 200. Validare l'ordinale copiato trasforma quello in una EArgumentException al tempo della factory invece di una modalità indefinita dentro codice nativo. La factory rifiuta anche tpsOSDOnly e tpsAutoOnly, che non producono parole, e richiede osd.traineddata per tpsAutoOSD e tpsSparseTextOSD

Che cosa garantisce davvero il timeout di riconoscimento?

Il timeout della DLL Tesseract è cooperativo: HotPDF può fermare il proprio lavoro e chiedere a Tesseract di fermarsi, ma non può forzare il codice nativo a tornare. L'orologio parte quando Recognize comincia, quindi la conversione bitmap e l'inizializzazione dei modelli consumano lo stesso budget del riconoscimento. HotPDF controlla il tempo trascorso e il token di cancellazione durante la conversione in grayscale e tra le parole mentre itera i risultati, e passa i millisecondi rimanenti a TessMonitorSetDeadlineMSecs prima di chiamare TessBaseAPIRecognize

Il varco sta dentro la chiamata nativa. Il monitor di Tesseract viene consultato durante il riconoscimento delle parole, non durante TessBaseAPIInit2 o l'analisi del layout di pagina, così un caricamento lento di modello o un layout patologico può superare la scadenza prima che il timeout venga riportato. I budget di pixel e output inoltre non limitano l'uso di memoria della libreria nativa stessa. Se ti serve un worker che puoi uccidere, usa l'adapter di processo; è il compromesso onesto, non una funzionalità mancante

Anatomia del timeout cooperativo della DLL Tesseract in HotPDF: l'orologio parte quando Recognize comincia e copre la conversione grayscale, TessBaseAPIInit2 e l'analisi del layout, ma il monitor viene consultato solo durante il riconoscimento delle parole, quindi caricamenti di modelli e layout possono sforare prima che HotPDF riporti otlsEngineError o otlsCancelled
una scadenza qui è una richiesta, non una garanzia: init e analisi del layout possono allungarsi, e un worker che puoi davvero uccidere richiede l'adapter di processo

La segmentazione di pagina è dove l'adapter DLL si ripaga sull'input difficile. Moduli, etichette e tabelle scansionate con campi sparsi spesso riconoscono meglio con tpsSparseText che con la segmentazione automatica, che prova ad assemblare colonne e paragrafi che non ci sono

procedure OCRFormPages(Doc: THotPDF; const Pages: array of Integer);
var
  Engine: IHPDFOCREngine;
  TessOptions: THPDFTesseractOptions;
  LayerOptions: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  TessOptions := THPDFTesseractOptions.Default;
  TessOptions.PageSegMode := tpsSparseText;  // campi sparsi, nessun assemblaggio colonne
  TessOptions.EngineMode := temLSTMOnly;     // richiede modelli LSTM in tessdata
  TessOptions.TimeoutMilliseconds := 20000;  // include l'inizializzazione dei modelli
  Engine := HPDFCreateTesseractDLLOCREngine('C:\OCR\Win64\libtesseract-5.dll',
    'C:\OCR\tessdata', 'eng+deu', TessOptions);

  LayerOptions := THPDFOCRTextLayerOptions.Default;
  LayerOptions.MinimumConfidence := 0.6;
  if not Doc.ApplyLoadedOCRTextLayer(Pages, Engine, LayerOptions, Info) then
    case Info.Status of
      otlsCancelled:
        Writeln('OCR cancelled, document unchanged');
      otlsEngineError:
        Writeln('Tesseract failed or timed out: ', string(Info.Diagnostic));
    else
      Writeln(string(Info.Diagnostic));
    end;
end;

Un timeout emerge come otlsEngineError con la diagnostica Tesseract DLL OCR timed out, mentre un token cancellato emerge come otlsCancelled. In entrambi i casi ApplyLoadedOCRTextLayer ha riconosciuto ogni pagina selezionata prima di iniziare la transazione di scrittura, quindi un fallimento alla pagina 40 di 50 lascia il documento caricato esattamente com'era. Nota che tpsSingleLine, tpsSingleBlock e tpsSparseText cambiano solo la segmentazione; nessuna di esse raddrizza una scansione storta

Free Pascal e Lazarus: pixel stanti e cinese perduto

Entrambe le factory Tesseract funzionano nelle build Windows Free Pascal e Lazarus Win32 e Win64 dalla v2.772.1, dopo due correzioni specifiche FPC. Ricompila prima il pacchetto Lazarus per l'architettura bersaglio; il porting generale è coperto in HotPDF su Free Pascal e Lazarus Win64

La prima correzione riguarda i pixel. Una TBitmap LCL scritta attraverso scanline può aggiornare la propria immagine grezza senza rinfrescare l'handle bitmap Windows, così GetDIBits su quell'handle restituisce i vecchi pixel. Il sintomo era sconcertante: il testo disegnato direttamente su una bitmap veniva riconosciuto, mentre una pagina renderizzata dal renderer PDF di HotPDF produceva una lista parole vuota. Su FPC l'adapter ora legge uno snapshot consapevole del formato attraverso CreateIntfImage, che rispetta il formato pixel e l'ordine delle righe dell'immagine grezza. La build Delphi conserva la via GetDIBits su una copia privata a 24 bit. Nessuna delle due build modifica la bitmap del chiamante

La seconda correzione appartiene all'adapter tesseract.exe. La TStringList di FPC conserva stringhe ANSI, quindi assegnare il testo TSV decodificato UTF-8 a Lines.Text scartava in silenzio ogni carattere cinese o del piano supplementare che la code page ANSI di sistema non poteva rappresentare. La via FPC ora tiene il TSV come byte UTF-8, toglie il BOM a livello di byte e decodifica ogni parola a UnicodeString singolarmente. L'adapter DLL non ha mai avuto questo problema perché decodifica ogni parola direttamente dall'iteratore

Riferimento rapido

  • Factory: HPDFCreateTesseractDLLOCREngine(LibraryPath, TessDataDirectory, Language[, Options]) in HPDFTesseractRecognition, aggiunta nella v2.772.0, supporto FPC nella v2.772.1
  • Default: tpsAuto, temDefault, 60.000 ms, 16.777.216 pixel; intervallo di timeout 1–3.600.000 ms, tetto pixel 67.108.864
  • Abbina la bitness della DLL all'applicazione e colloca le DLL di dipendenza accanto alla DLL Tesseract
  • Tratta il monitor come opaco; non copiare mai ETEXT_DESC in un record Pascal
  • Dichiara il callback di cancel cdecl con un risultato Boolean da un byte, e non lasciare mai che un'eccezione gli sfugga
  • Libera il testo dell'iteratore con TessDeleteText; non liberare mai l'iteratore di pagina ottenuto dall'iteratore dei risultati
  • Aspettati che la scadenza sia cooperativa: l'inizializzazione dei modelli e l'analisi del layout possono sforarla
  • Usa l'adapter tesseract.exe quando ti serve terminazione dura o isolamento dai crash

L'adapter DLL Tesseract, gli adapter di processo e il motore OCR integrato viaggiano tutti con il componente HotPDF Delphi per Delphi, C++Builder e Free Pascal; vedi la pagina prodotto HotPDF per edizioni e download