Artículo técnico

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

HotPDF corre Tesseract dentro de su proceso Delphi a través de HPDFCreateTesseractDLLOCREngine, una factory agregada en v2.772.0 que carga dinámicamente una DLL compatible con Tesseract 5, maneja su API C (TessBaseAPIInit2, TessBaseAPIRecognize, el result iterator) y devuelve un IHPDFOCREngine. THotPDF.ApplyLoadedOCRTextLayer usa ese engine para agregar una capa de texto Unicode invisible y buscable a las páginas PDF escaneadas

El mismo reconocedor ya estaba al alcance por el adapter externo de tesseract.exe 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 de Pascal se sienta directo sobre estructuras C, booleanos C y strings asignados por C. La mayor parte de lo que vale la pena saber de este adapter es dónde ese binding puede torcerse en silencio

¿Cómo se corre Tesseract in-process desde Delphi con HotPDF?

Correr Tesseract in-process con HotPDF toma una llamada a la factory en la unidad HPDFTesseractRecognition y la misma llamada a ApplyLoadedOCRTextLayer que usa todo engine de OCR de HotPDF. La factory valida con antelación. El archivo de la DLL y el directorio tessdata deben existir, el identificador de idioma solo puede contener letras ASCII, dígitos, _ y +, cada modelo en una combinación como chi_sim+eng debe tener un archivo .traineddata a juego, y todos los 21 exports requeridos deben resolver antes de devolver el engine. Los errores de configuración levantan EArgumentException; una DLL que falla al cargar levanta EOSError con el código de error de Windows y la pista de chequear 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 DLLs de dependencias 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 pone PageSegMode en tpsAuto, EngineMode en temDefault, TimeoutMilliseconds en 60,000 y MaxPixels en 16,777,216. El presupuesto de píxeles importa más de lo que parece. Una página US Letter al default de 300 DPI se renderiza a 2,550 × 3,300 píxeles, unos 8.4 millones, que caben. La misma página a 600 DPI es 5,100 × 6,600, unos 33.7 millones, y el adapter la rechaza antes de que Tesseract vea un píxel. Suba MaxPixels (el techo es 67,108,864) o deje el DPI donde está; cada lado además se topea a 32,767 píxeles

La DLL se carga con LoadLibraryEx usando los flags de búsqueda para el folder propio de la DLL más los directorios safe por defecto, así que las librerías de imagen de las que depende Tesseract pueden vivir junto a ella sin tocar el PATH ni el directorio actual. HotPDF no empaqueta ni descarga ningún runtime ni modelo de OCR; usted provisiona ambos

¿Qué cambia respecto del adapter de tesseract.exe?

El adapter de DLL canjea aislamiento de proceso por salida más rica y menos overhead por página. Ambos adapters se enchufan al mismo pipeline de capa de texto, así que el mapeo de coordenadas, el filtrado por confianza y la confirmación all-or-nothing son idénticos; lo que difiere es cómo entran los píxeles y cómo salen las palabras

AspectoAdapter de tesseract.exeAdapter de la DLL Tesseract
FactoryHPDFCreateTesseractOCREngineHPDFCreateTesseractDLLOCREngine
Píxeles de entradaArchivo BMP en un directorio temporal privadoBuffer grayscale de 8 bits en memoria
Palabras de salidaTSV a nivel de palabra, topeado a 64 MiBResult iterator, UTF-8 por palabra
BaselinesNo disponiblesPasadas a través de TessPageIteratorBaseline
Segmentación de página y modo de engineSolo segmentación automáticaTHPDFTesseractPageSegMode, THPDFTesseractEngineMode
TimeoutDuro: el proceso hijo se terminaCooperativo: Tesseract debe darse cuenta
Aislamiento de crash y memoriaProceso separadoNinguno, comparte su address space

Un costo 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 vez de una vez por engine. El file cache del sistema operativo suaviza la recarga, pero en conjuntos de modelos grandes multilingüe sigue siendo el costo fijo dominante por página, y cuenta contra el deadline de reconocimiento. El engine de la DLL RapidOCR in-process toma el diseño opuesto y mantiene sus modelos ONNX residentes por la vida del engine; los problemas de frontera (ABI C, buffers prestados, trabajo nativo ininterrumpible) son de la misma familia

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

Delphi no puede reflejar con seguridad el monitor de progreso de Tesseract porque ETEXT_DESC contiene campos internos que dependen de la versión, así que un record copiado a mano pone el callback de cancelación y el deadline en offsets equivocados en algunos builds. Nada falla estruendosamente cuando eso pasa. Tesseract simplemente lee su puntero de callback de un campo que ahora sostiene otra cosa, o jamás ve el deadline

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

Manejo del monitor de la DLL Tesseract en HotPDF: copiar el record ETEXT_DESC dependiente de versión pone el callback de cancelación y el deadline en offsets equivocados y falla en silencio, mientras que HotPDF trata al 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 completo; el callback se queda 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 el stack de Tesseract: lea flags y el reloj, jamás levante
  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 boceto son deliberados. El callback devuelve Boolean, que es un byte tanto en Delphi como en Free Pascal, coincidiendo 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 haya quedado allí, y un false puede llegar como true. El mismo header complica las cosas más aún, porque funciones como TessPageIteratorBoundingBox devuelven un int, que HotPDF declara como Integer. Lea el tipo C de cada valor de retorno en vez de asumir una convención para toda la API

El segundo detalle es que el callback jamás levanta. Una excepción de 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 GetTickCount64 monótono. El adapter convierte el resultado en un diagnóstico de cancelación o de timeout después de que TessBaseAPIRecognize devuelve, y hace ese chequeo sin importar el código de retorno nativo

¿Qué punteros nativos posee el lado Delphi?

El adapter de la DLL Tesseract de HotPDF posee tres objetos nativos por request, la instancia de API, el monitor y el result iterator, 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 engine descarga la librería

Propiedad de objetos de la DLL Tesseract en HotPDF por llamada a Recognize: el result iterator, el monitor y la instancia de API son de su propiedad y se liberan en ese orden dentro de finally, el page iterator de TessResultIteratorGetPageIterator es una vista prestada que jamás debe liberarse, y los strings de GetUTF8Text se copian y se devuelven vía TessDeleteText
tres objetos propios, todo lo demás prestado: libere en el orden fijo, jamás haga double-free del page iterator, y jamás mezcle allocators
  • TessResultIteratorGetPageIterator devuelve una vista prestada dentro del result iterator, no un objeto nuevo. HotPDF la usa para TessPageIteratorBoundingBox y TessPageIteratorBaseline y jamás la libera; borrarla por separado liberaría la misma memoria dos veces
  • TessResultIteratorGetUTF8Text devuelve un string asignado por el runtime propio de la DLL. HotPDF lo copia y lo devuelve por TessDeleteText en un bloque finally; un FreeMem de Pascal lo liberaría en el heap equivocado
  • El texto de las palabras se decodifica con validación UTF-8 estricta y chequeo de largo antes de la conversión. Palabras con caracteres de control, UTF-8 mal formado, cajas fuera de la imagen, rectángulos invertidos o confianza fuera de 0–100 hacen fallar el request en vez de parcharse en silencio
  • El texto total por request se topea a 1,048,576 unidades de código UTF-16, y el conteo de palabras debe caber en el presupuesto del request que entrega ApplyLoadedOCRTextLayer

La confianza llega como 0–100 y se escala a 0–1, así que THPDFOCRTextLayerOptions.MinimumConfidence significa lo mismo para todo engine. Cuando Tesseract reporta una baseline, ambos extremos pasan a través; si no, el pipeline de la capa de texto recurre a su estimación geométrica, exactamente como lo hace para input TSV

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

HotPDF copia el ordinal crudo de PageSegMode y EngineMode a un Integer antes del chequeo de rango, porque un compilador puede asumir que una variable enum siempre sostiene un valor declarado y plegar Ord(X) > Ord(High(T)) a un false constante. 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 modos de engine de 0 a 3, y ambos van a la DLL como enteros planos. Un record de opciones armado con FillChar, llenado desde un stream, o pasado desde C++Builder con un integer casteado puede cargar un byte como 200. Validar el ordinal copiado convierte eso en un EArgumentException a la hora de la factory en vez de un modo indefinido dentro del código nativo. La factory además rechaza tpsOSDOnly y tpsAutoOnly, que no producen palabras, y exige osd.traineddata para tpsAutoOSD y tpsSparseTextOSD

¿Qué garantiza en realidad el timeout de reconocimiento?

El timeout de la DLL Tesseract es cooperativo: HotPDF puede detener su propio trabajo y pedirle a Tesseract que se detenga, pero no puede forzar a que el código nativo devuelva. 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 chequea el tiempo transcurrido y el token de cancelación durante la conversión a grayscale y entre palabras mientras itera 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 modelo lenta o un layout patológico puede correr más allá del deadline antes de que el timeout se reporte. Los presupuestos de píxeles y de salida tampoco topean el uso de memoria propio de la librería nativa. Si necesita un worker que usted pueda matar, use el adapter de proceso; ese es el trade-off honesto, no una función faltante

Anatomía del timeout cooperativo de la DLL Tesseract en HotPDF: el reloj arranca cuando Recognize comienza y cubre la conversión a grayscale, 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 del deadline antes de que HotPDF reporte otlsEngineError u otlsCancelled
un deadline aquí es un pedido, 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 adapter de proceso

La segmentación de página es donde el adapter de DLL gana su sustento sobre input difícil. Formularios, etiquetas y tablas escaneadas con campos dispersos suelen reconocer mejor con tpsSparseText que con segmentación automática, que intenta armar 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 ensamblar 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 sale a la luz como otlsEngineError con el diagnóstico Tesseract DLL OCR timed out, mientras que un token cancelado sale como otlsCancelled. En ambos casos ApplyLoadedOCRTextLayer ha reconocido cada página seleccionada antes de arrancar la transacción de confirmación, así que una falla en la página 40 de 50 deja el documento cargado exactamente como estaba. Note que tpsSingleLine, tpsSingleBlock y tpsSparseText cambian solo la segmentación; ninguno endereza un escaneo torcido

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

Ambas factories de Tesseract funcionan en builds Win32 y Win64 de Free Pascal y Lazarus para Windows desde v2.772.1, después de dos correcciones específicas de FPC. Recompile el paquete de Lazarus para la arquitectura objetivo primero; el porte general queda cubierto en HotPDF en Free Pascal y Lazarus Win64

La primera corrección concierne a los píxeles. Un TBitmap de LCL escrito por scanlines puede actualizar su imagen cruda sin refrescar el handle de bitmap de Windows, así que GetDIBits sobre ese handle devuelve los píxeles viejos. El síntoma era desconcertante: texto dibujado directo 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 adapter ahora lee un snapshot consciente del formato por CreateIntfImage, que respeta el formato de píxel y el orden de filas de la imagen cruda. El build de Delphi conserva el camino GetDIBits sobre una copia privada de 24 bits. Ningún build modifica el bitmap del caller

La segunda corrección pertenece al adapter de tesseract.exe. El TStringList de FPC guarda strings ANSI, así que asignar el texto TSV UTF-8 decodificado a Lines.Text soltaba en silencio cada carácter chino o del plano suplementario que la code page ANSI del sistema no pudiera representar. El camino de FPC ahora conserva el TSV como bytes UTF-8, quita el BOM a nivel de byte y decodifica cada palabra a UnicodeString individualmente. El adapter de DLL jamás tuvo este problema porque decodifica cada palabra directo del iterator

Referencia rápida

  • Factory: HPDFCreateTesseractDLLOCREngine(LibraryPath, TessDataDirectory, Language[, Options]) en HPDFTesseractRecognition, agregada en v2.772.0, soporte FPC en v2.772.1
  • Defaults: 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
  • Empareje el bitness de la DLL con el de la aplicación y coloque las DLLs de dependencias junto a la DLL de Tesseract
  • Trate al monitor como opaco; jamás copie ETEXT_DESC a un record de Pascal
  • Declare el callback de cancelación cdecl con un resultado Boolean de un byte, y jamás deje que una excepción se escape de él
  • Libere el texto del iterator con TessDeleteText; jamás libere el page iterator obtenido del result iterator
  • Espere que el deadline sea cooperativo: la inicialización de modelos y el análisis de layout pueden pasarse de él
  • Use el adapter de tesseract.exe cuando necesite terminación dura o aislamiento de crashes

El adapter de la DLL Tesseract, los adapters de proceso y el engine de OCR integrado vienen todos con el componente HotPDF Delphi PDF para Delphi, C++Builder y Free Pascal; vea la página del producto HotPDF para ediciones y descargas