Artículo técnico

OCR de Tesseract DLL en HotPDF: la API C desde Delphi

HotPDF ejecuta Tesseract dentro de su proceso Delphi a través de HPDFCreateTesseractDLLOCREngine, una factoría añadida en v2.772.0 que carga dinámicamente una DLL compatible con Tesseract 5, maneja su API C (TessBaseAPIInit2, TessBaseAPIRecognize, el iterador de resultados) y devuelve un IHPDFOCREngine. THotPDF.ApplyLoadedOCRTextLayer usa ese motor para añadir una capa de texto Unicode invisible y buscable a páginas PDF escaneadas

El mismo reconocedor ya era alcanzable a través del adaptador tesseract.exe externo que escribe un BMP y parsea TSV. Ese camino funciona, pero cada página paga un lanzamiento de proceso, un archivo bitmap temporal y un formato de texto sin baselines y sin control sobre la segmentación de página. Llamar a la DLL elimina los tres. También elimina el muro de proceso, lo que significa que un binding Pascal se sienta directamente encima de estructuras C, booleanos C y cadenas asignadas en C. La mayor parte de lo que vale la pena saber de este adaptador es dónde ese binding puede torcerse en silencio

¿Cómo se ejecuta Tesseract en proceso desde Delphi con HotPDF?

Ejecutar Tesseract en proceso con HotPDF lleva una llamada a la factoría en la unidad HPDFTesseractRecognition y la misma llamada ApplyLoadedOCRTextLayer que usa todo motor OCR de HotPDF. La factoría valida con ansias. El archivo DLL y el directorio tessdata deben existir, el identificador de idioma solo puede contener letras ASCII, dígitos, _ y +, cada modelo de una combinación como chi_sim+eng debe tener su archivo .traineddata correspondiente, y los 21 exports requeridos deben resolverse antes de devolverse el motor. Los errores de configuración lanzan EArgumentException; una DLL que no carga lanza EOSError con el código de error de Windows y una pista de revisar arquitectura y dependencias

uses
  SysUtils, HPDFDoc, HPDFTesseractRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // Una aplicación Win64 necesita una DLL de 64 bits; las DLL de dependencia van junto a ella
  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 de páginas vacía significa todas; las que ya tienen texto se saltan
    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 fija PageSegMode a tpsAuto, EngineMode a temDefault, TimeoutMilliseconds a 60.000 y MaxPixels a 16.777.216. El presupuesto de píxeles importa más de lo que parece. Una página US Letter a los 300 DPI por defecto renderiza a 2.550 × 3.300 píxeles, unos 8,4 millones, lo que cabe. La misma página a 600 DPI son 5.100 × 6.600, unos 33,7 millones, y el adaptador la rechaza antes de que Tesseract vea un píxel. Suba MaxPixels (el techo es 67.108.864) o mantenga el DPI donde está; cada lado además se topa en 32.767 píxeles

La DLL se carga con LoadLibraryEx usando los flags de búsqueda para la carpeta propia de la DLL más los directorios seguros por defecto, así que las librerías de imagen de las que depende Tesseract pueden vivir a su lado sin tocar PATH ni el directorio en curso. HotPDF no incluye ni descarga ningún runtime ni modelo OCR; usted provisiona ambos

¿Qué cambia respecto al adaptador tesseract.exe?

El adaptador de DLL cambia aislamiento de proceso por salida más rica y menos sobrecarga por página. Ambos adaptadores se enchufan al mismo pipeline de capa de texto, así que el mapeo de coordenadas, el filtrado por confianza y el commit todo-o-nada son idénticos; lo que difiere es cómo entran los píxeles y cómo salen las palabras

AspectoAdaptador tesseract.exeAdaptador DLL Tesseract
FactoríaHPDFCreateTesseractOCREngineHPDFCreateTesseractDLLOCREngine
Píxeles que entranArchivo BMP en un directorio temporal privadoBuffer de escala de grises de 8 bits en memoria
Palabras que salenTSV a nivel de palabra, topado a 64 MiBIterador de resultados, UTF-8 por palabra
BaselinesNo disponiblesSe trasladas desde TessPageIteratorBaseline
Segmentación de página y modo de motorSolo segmentación automáticaTHPDFTesseractPageSegMode, THPDFTesseractEngineMode
TimeoutDuro: el proceso hijo se terminaCooperativo: Tesseract debe darse por enterado
Aislamiento de crash y memoriaProceso separadoNinguno, comparte su espacio de direcciones

Un coste no desaparece. Cada llamada a Recognize crea su propia instancia de API y llama a TessBaseAPIInit2, así que los modelos de idioma se inicializan por página en lugar de una vez por motor. La caché de archivos del sistema operativo suaviza la recarga, pero en conjuntos de modelos grandes multilingües sigue siendo el coste fijo dominante por página, y consume el deadline de reconocimiento. El motor de DLL RapidOCR en proceso toma el diseño opuesto y mantiene residentes sus modelos ONNX durante la vida del motor; los problemas de frontera (ABI C, buffers prestados, trabajo nativo no interrumpible) son de la misma familia

¿Por qué no puede Delphi copiar la struct monitor de Tesseract?

Delphi no puede reflejar con seguridad el monitor de progreso de Tesseract porque ETEXT_DESC contiene campos internos dependientes de la versión, así que un registro copiado a mano coloca el callback de cancelación y el deadline en offsets equivocados en algunos builds. Nada falla ruidosamente cuando eso pasa. Tesseract sencillamente lee su puntero de callback de un campo que ahora contiene otra cosa, o no ve nunca el deadline

HotPDF trata por eso el monitor como un puntero opaco y solo lo toca a través de funciones exportadas: TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc, TessMonitorSetDeadlineMSecs y TessMonitorDelete. Si usted bindea la API C por su cuenta para otro propósito, el mismo patrón aplica. El esqueleto de abajo es su propio código de binding, no una API de HotPDF, y refleja las declaraciones que HotPDF usa internamente

Manejo del monitor de la DLL Tesseract en HotPDF: copiar el registro ETEXT_DESC dependiente de la versión coloca el callback de cancelación y el deadline en offsets equivocados y falla en silencio, mientras que HotPDF trata el monitor como opaco, maneja TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc y TessMonitorSetDeadlineMSecs, y mantiene el callback cdecl libre de excepciones
un puntero opaco más cinco exports es el contrato entero; el callback sigue siendo un Boolean de un byte que solo lee un flag y un reloj
type
  // C: typedef bool (*TessCancelFunc)(void *cancel_this, int words);
  TTessCancelFunc = function(CancelThis: Pointer; Words: Integer): Boolean; cdecl;
  TTessMonitorCreate = function: Pointer; cdecl;   // ETEXT_DESC*, jamás desreferenciado
  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
  // Corre en la pila de Tesseract: lea flags y el reloj, jamás lance
  Result := (CancelThis = nil) or POCRJob(CancelThis)^.CancelRequested or
    (GetTickCount64 >= POCRJob(CancelThis)^.DeadlineTick);
end;

// Uso, con los punteros de función resueltos por GetProcAddress:
//   Monitor := MonitorCreate();
//   try
//     MonitorSetCancelThis(Monitor, @Job);
//     MonitorSetCancelFunc(Monitor, ShouldCancel);
//     MonitorSetDeadlineMSecs(Monitor, RemainingMs);
//     RC := BaseAPIRecognize(API, Monitor);
//   finally
//     MonitorDelete(Monitor);
//   end;

Dos detalles de ese esqueleto son deliberados. El callback devuelve Boolean, que es un byte tanto en Delphi como en Free Pascal, casando con el bool de C en TessCancelFunc. El BOOL de Windows de cuatro bytes o el LongBool de Delphi parece intercambiable y no lo es: cuando un lado escribe un solo byte y el otro lee cuatro, los bytes altos del registro de retorno son lo que quedara ahí, y un false puede llegar como true. La misma cabecera complica aún más las cosas, porque funciones como TessPageIteratorBoundingBox devuelven un int, que HotPDF declara como Integer. Lea el tipo C de cada valor de retorno en lugar de asumir una convención para toda la API

El segundo detalle es que el callback jamás lanza. Una excepción Delphi deshaciéndose por los frames de C++ de Tesseract es comportamiento indefinido, así que el callback de HotPDF solo lee el token de cancelación y un valor monótono de GetTickCount64. El adaptador convierte el resultado en un diagnóstico de cancelación o timeout después de que TessBaseAPIRecognize retorna, y hace ese check independientemente del código de retorno nativo

¿Qué punteros nativos posee el lado Delphi?

El adaptador de DLL Tesseract de HotPDF posee tres objetos nativos por petición, la instancia de API, el monitor y el iterador de resultados, y toma prestado todo lo demás. Cada llamada a Recognize crea su propio conjunto y lo libera en un bloque finally: TessResultIteratorDelete, luego TessMonitorDelete, luego TessBaseAPIDelete. Liberar la interfaz del motor descarga la biblioteca

Propiedad de objetos nativos de la DLL Tesseract en HotPDF por llamada a Recognize: el iterador de resultados, el monitor y la instancia de API son de su propiedad y se liberan en ese orden dentro de finally, el iterador de página de TessResultIteratorGetPageIterator es una vista prestada que jamás debe liberarse, y las cadenas de GetUTF8Text se copian y se devuelven vía TessDeleteText
tres objetos en propiedad, todo lo demás prestado: libere en el orden fijo, jamás libere dos veces el iterador de página, y jamás mezcle asignadores
  • TessResultIteratorGetPageIterator devuelve una vista prestada dentro del iterador de resultados, no un objeto nuevo. HotPDF lo usa para TessPageIteratorBoundingBox y TessPageIteratorBaseline y jamás lo libera; borrarlo por separado liberaría la misma memoria dos veces
  • TessResultIteratorGetUTF8Text devuelve una cadena asignada por el runtime propio de la DLL. HotPDF la copia y la devuelve a través de TessDeleteText en un bloque finally; un FreeMem de Pascal la liberaría en el heap equivocado
  • El texto de palabra se decodifica con validación UTF-8 estricta y comprobación de longitud antes de la conversión. Las palabras con caracteres de control, UTF-8 malformado, cajas fuera de la imagen, rectángulos invertidos o confianza fuera de 0–100 hacen fallar la petición en lugar de parchearse en silencio
  • El texto total por petición se topa en 1.048.576 unidades de código UTF-16, y el recuento de palabras debe caber en el presupuesto de la petición que baja desde ApplyLoadedOCRTextLayer

La confianza llega como 0–100 y se escala a 0–1, así que THPDFOCRTextLayerOptions.MinimumConfidence significa lo mismo para todo motor. Cuando Tesseract reporta una baseline, ambos extremos se trasladan; si no, el pipeline de la capa de texto recae en su estimación geométrica, exactamente igual que con entrada TSV

¿Por qué validar un enum antes de que llegue a la DLL?

HotPDF copia el ordinal en crudo de PageSegMode y EngineMode a un Integer antes del check de rango, porque un compilador puede asumir que una variable enum siempre contiene un valor declarado y plegar Ord(X) > Ord(High(T)) a una constante false. Los ordinales no son decoración: THPDFTesseractPageSegMode sigue la numeración de segmentación de página de Tesseract de 0 a 13, THPDFTesseractEngineMode sigue la numeración de modo de motor de 0 a 3, y ambos van a la DLL como enteros planos. Un registro de opciones construido con FillChar, rellenado desde un stream, o pasado desde C++Builder con un entero casteado puede llevar un byte como 200. Validar el ordinal copiado convierte eso en una EArgumentException en tiempo de factoría en lugar de un modo indefinido dentro del código nativo. La factoría además rechaza tpsOSDOnly y tpsAutoOnly, que no producen palabras, y exige osd.traineddata para tpsAutoOSD y tpsSparseTextOSD

¿Qué garantiza realmente el timeout de reconocimiento?

El timeout de la DLL Tesseract es cooperativo: HotPDF puede parar su propio trabajo y pedirle a Tesseract que pare, pero no puede forzar a código nativo a retornar. El reloj arranca cuando Recognize comienza, así que la conversión de bitmap y la inicialización de modelos consumen el mismo presupuesto que el reconocimiento. HotPDF comprueba el tiempo transcurrido y el token de cancelación durante la conversión a escala de grises y entre palabras mientras itera los resultados, y pasa los milisegundos restantes a TessMonitorSetDeadlineMSecs antes de llamar a TessBaseAPIRecognize

La brecha está dentro de la llamada nativa. El monitor de Tesseract se consulta durante el reconocimiento de palabras, no durante TessBaseAPIInit2 ni el análisis de layout de página, así que una carga de modelos lenta o un layout patológico puede pasarse del deadline antes de que se reporte el timeout. Los presupuestos de píxeles y de salida tampoco tapan el uso de memoria propio de la librería nativa. Si necesita un worker que pueda matar, use el adaptador de proceso; ese es el trade-off honesto, no una función ausente

Anatomía del timeout cooperativo de la DLL Tesseract en HotPDF: el reloj arranca cuando Recognize comienza y cubre la conversión a escala de grises, TessBaseAPIInit2 y el análisis de layout, pero el monitor solo se consulta durante el reconocimiento de palabras, así que las cargas de modelos y el layout pueden pasarse antes de que HotPDF reporte otlsEngineError u otlsCancelled
un deadline aquí es una petición, no una garantía: la inicialización y el análisis de layout pueden alargarse, y un worker que usted pueda matar de verdad necesita el adaptador de proceso

La segmentación de página es donde el adaptador de DLL se gana su sueldo con entrada difícil. Formularios, etiquetas y tablas escaneadas con campos dispersos suelen reconocer mejor con tpsSparseText que con la segmentación automática, que intenta ensamblar columnas y párrafos que no están

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;  // campos dispersos, sin ensamblado de columnas
  TessOptions.EngineMode := temLSTMOnly;     // necesita modelos LSTM en tessdata
  TessOptions.TimeoutMilliseconds := 20000;  // incluye la inicialización de modelos
  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 como otlsEngineError con el diagnóstico Tesseract DLL OCR timed out, mientras que un token cancelado emerge como otlsCancelled. En ambos casos ApplyLoadedOCRTextLayer ha reconocido toda página seleccionada antes de arrancar la transacción de commit, así que un fallo en la página 40 de 50 deja el documento cargado exactamente como estaba. Nótese que tpsSingleLine, tpsSingleBlock y tpsSparseText cambian solo la segmentación; ninguno endereza un escaneo torcido

Free Pascal y Lazarus: píxeles caducados y chino perdido

Ambas factorías Tesseract funcionan en builds Free Pascal y Lazarus Win32 y Win64 para Windows desde v2.772.1, tras dos arreglos específicos de FPC. Recompile primero el paquete Lazarus para la arquitectura objetivo; el porte general está cubierto en HotPDF sobre Free Pascal y Lazarus Win64

El primer arreglo trata de los píxeles. Un TBitmap de LCL escrito por scanlines puede actualizar su imagen en crudo sin refrescar el handle de bitmap de Windows, así que GetDIBits sobre ese handle devuelve los píxeles viejos. El síntoma era desconcertante: el texto dibujado directamente sobre un bitmap se reconocía, mientras que una página renderizada por el renderer PDF de HotPDF producía una lista de palabras vacía. En FPC el adaptador lee ahora una instantánea consciente del formato a través de CreateIntfImage, que respeta el formato de píxel y el orden de filas de la imagen en crudo. El build Delphi conserva el camino GetDIBits sobre una copia privada de 24 bits. Ningún build modifica el bitmap del llamador

El segundo arreglo pertenece al adaptador tesseract.exe. El TStringList de FPC guarda cadenas ANSI, así que asignar texto TSV UTF-8 decodificado a Lines.Text descartaba en silencio cada carácter chino o de plano suplementario que la code page ANSI del sistema no pudiera representar. El camino FPC conserva ahora el TSV como bytes UTF-8, retira el BOM a nivel de byte y decodifica cada palabra a UnicodeString individualmente. El adaptador de DLL nunca tuvo este problema porque decodifica cada palabra directamente del iterador

Referencia rápida

  • Factoría: HPDFCreateTesseractDLLOCREngine(LibraryPath, TessDataDirectory, Language[, Options]) en HPDFTesseractRecognition, añadida en v2.772.0, soporte FPC en v2.772.1
  • Valores por defecto: tpsAuto, temDefault, 60.000 ms, 16.777.216 píxeles; rango de timeout 1–3.600.000 ms, techo de píxeles 67.108.864
  • Casée el bitness de la DLL con la aplicación y coloque las DLL de dependencia junto a la DLL de Tesseract
  • Trate el monitor como opaco; jamás copie ETEXT_DESC a un registro Pascal
  • Declare el callback de cancelación cdecl con un resultado Boolean de un byte, y jamás deje escapar una excepción de él
  • Libere el texto del iterador con TessDeleteText; jamás libere el iterador de página obtenido del iterador de resultados
  • Espere que el deadline sea cooperativo: la inicialización de modelos y el análisis de layout pueden pasarse de él
  • Use el adaptador tesseract.exe cuando necesite terminación dura o aislamiento de crashes

El adaptador de DLL Tesseract, los adaptadores de proceso y el motor OCR incorporado vienen todos con el componente Delphi PDF de HotPDF para Delphi, C++Builder y Free Pascal; vea la página de producto de HotPDF para ediciones y descargas