HotPDF convierte páginas PDF escaneadas en PDF buscable con Tesseract mediante HPDFCreateTesseractOCREngine, una factory que envuelve un ejecutable Tesseract instalado localmente como un IHPDFOCREngine. Usted pasa ese engine a ApplyLoadedOCRTextLayer, que renderiza cada página, corre Tesseract una vez por página, parsea su salida TSV a nivel de palabra, y confirma una capa de texto Unicode invisible para todas las páginas solicitadas en una sola transacción, o para ninguna
La razón de que exista este adapter es el alcance. El motor OCR integrado de template matching es angosto a propósito: letras y dígitos ASCII impresos por máquina, nada más. Facturas con nombres con tildes, contratos en chino y archivos multilenguaje necesitan un reconocedor de verdad con modelos de lenguaje entrenados, y Tesseract es el candidato obvio porque es un programa de línea de comandos que usted puede aprovisionar junto a su aplicación. Llamar a un programa externo desde una biblioteca de documentos suena trivial. No lo es, y la mayor parte del código interesante del adapter trata de qué pasa cuando el programa se porta mal, se cuelga, es cancelado, o hereda cosas que jamás debería ver
¿Cómo maneja HotPDF Tesseract desde una aplicación Delphi?
HotPDF corre Tesseract como un proceso hijo oculto por página, alimentándolo con un bitmap renderizado y leyendo de vuelta un archivo TSV, y expone el resultado por la misma costura IHPDFOCREngine que usa el motor integrado. Nada aguas abajo cambia: el mapeo de coordenadas, el manejo de rotación, la validación Unicode, el filtrado por confianza y la confirmación atómica son el pipeline de capa de texto que usted ya tiene. La factory vive en la unidad HPDFTesseractRecognition y valida temprano: el ejecutable debe existir, el directorio tessdata debe existir, el timeout debe estar entre 1 y 3,600,000 milisegundos, y el identificador de lenguaje solo puede contener letras ASCII, dígitos, _ y +. Ese último chequeo importa porque el string de lenguaje termina en una línea de comandos, y eng+chi_sim es un valor legítimo de Tesseract mientras que cualquier cosa con comillas o espacios no
uses
SysUtils, HPDFTypes, HPDFDoc, HPDFTesseractRecognition;
procedure MakeSearchable(const SourceFile, TargetFile: string;
Token: THPDFCancellationToken);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// lanza EArgumentException por ejecutable faltante, tessdata faltante,
// un identificador de lenguaje malo, o un timeout fuera de 1..3600000 ms
Engine := HPDFCreateTesseractOCREngine(
'C:\OCR\Tesseract\tesseract.exe',
'C:\OCR\Tesseract\tessdata',
'eng+chi_sim', // varios modelos unidos con '+'
120000); // límite por página, el default es 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 de páginas vacía significa todas; las páginas con texto se saltan por 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;
Por cada página, Recognize crea un directorio privado bajo la ruta temp llamado HotPDF-OCR-{GUID}, guarda el bitmap renderizado como input.bmp, y lanza tesseract input.bmp output --tessdata-dir … -l … --dpi N --psm 3 -c tessedit_create_tsv=1, con cada argumento de ruta entrecomillado según las reglas de escape de línea de comandos de Windows para backslashes y comillas incrustadas. El valor --dpi es el DPI de renderizado de THPDFOCRTextLayerOptions.DPI, así que Tesseract jamás tiene que adivinar la resolución desde los metadatos de la imagen, y --psm 3 pide segmentación de página totalmente automática. El engine se reporta como Tesseract (local CLI), que es lo que aterriza en Info.EngineName. Tesseract y sus modelos de lenguaje no vienen con HotPDF; instalarlos es trabajo de la aplicación
¿Por qué el parser TSV es tan estricto?
El parser TSV de HotPDF hace fallar la página completa ante cualquier fila malformada, porque una lista de palabras parcialmente parseada produce una capa de texto que discrepa en silencio con la imagen. La salida TSV de Tesseract tiene un header fijo de doce columnas, desde level hasta text, y HotPDF compara la primera línea contra ese header exacto tras quitar una marca de orden de bytes opcional. Cada fila siguiente debe dividirse en exactamente doce campos, y la división se corta después del tab número once para que un tab dentro del texto reconocido quede como parte de la palabra en vez de crear una décima tercera columna. Solo las filas de nivel 5 son palabras; los niveles 1 a 4 describen páginas, bloques, párrafos y líneas, y se saltan. Las filas de nivel 5 cuyo texto está vacío o es puro espacio en blanco también se saltan, porque una palabra en blanco tiene caja pero nada que ubicar ni buscar. Todo lo demás se chequea a fondo: geometría entera, una confianza parseada con formato invariante en-US para que un locale alemán no lea 93.5 como basura, una caja totalmente dentro del bitmap, y una confianza entre 0 y 100. Un solo fallo lanza excepción, el engine devuelve False, y el array de palabras se limpia. Las pruebas de regresión incluyen justo ese caso: una palabra válida seguida de una fila rota debe rendir cero palabras, no una
// condensado del loop de nivel 5 en 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; // filas de página/bloque/párrafo/línea
WordText := Fields[11];
if Trim(WordText) = '' then Continue; // las palabras de puro espacio no tienen posición
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; // el pipeline espera 0..1
Esa última línea interactúa con un default que quizá no espere. La confianza de Tesseract va de 0 a 100, el pipeline trabaja de 0 a 1, y THPDFOCRTextLayerOptions.MinimumConfidence hace default a 0.5, así que cualquier palabra de Tesseract por debajo de 50 se cuenta en Info.DroppedWordCount y jamás llega a la página. En un escaneo limpio a 300 DPI ese es un piso razonable. En un fax con ruido puede botar una porción sorprendente de la página, y lo correcto es mirar el conteo de botadas antes de bajar el umbral, porque las palabras de baja confianza son justo las más propensas a estar mal
¿Qué hereda el proceso hijo de Tesseract?
El proceso hijo de Tesseract hereda exactamente dos handles de HotPDF: un handle NUL para entrada y salida estándar, y un handle de archivo para el error estándar. Esa precisión es el punto. CreateProcess con bInheritHandles = True es como se le pasan los handles estándar a un hijo, pero por sí solo pasa cada handle heredable del proceso anfitrión, incluidos archivos, pipes y eventos abiertos por código no relacionado en su aplicación. El hijo entonces mantiene vivos esos objetos hasta que sale, así que un archivo queda bloqueado o un pipe jamás ve su fin mientras Tesseract trabaja una página. HotPDF cierra esa brecha con un record de arranque extendido: STARTUPINFOEX, una lista de atributos que lleva PROC_THREAD_ATTRIBUTE_HANDLE_LIST, y el flag de creación EXTENDED_STARTUPINFO_PRESENT. Con la lista de handles en su lugar, bInheritHandles todavía tiene que ser True, pero solo los handles listados cruzan la frontera. El mismo pensamiento de confinamiento impulsa aislar códecs de imágenes PDF en procesos worker, donde el hijo es código no confiable; aquí el hijo es confiable, pero el anfitrión no es el único dueño de su propia tabla de handles
// constantes mostradas por nombre; la fuente pasa sus valores numéricos
// ambos handles se crean con bInheritHandle = True
InheritedHandles[0] := NullHandle; // stdin y stdout
InheritedHandles[1] := ErrorHandle; // stderr.txt en el directorio privado
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, // requerido por la lista de handles
CREATE_NO_WINDOW or EXTENDED_STARTUPINFO_PRESENT,
nil, PChar(DirectoryName), Startup.StartupInfo, ProcessInfo);
¿Por qué una corrida OCR cancelada puede parecer un fallo del engine?
Una corrida OCR cancelada parece un fallo del engine porque IHPDFOCREngine.Recognize devuelve un único Boolean, y False significa tanto “Tesseract falló” como “el usuario presionó Cancelar”. El adapter sondea el cancellation token y el timeout cada 25 milisegundos mientras el hijo corre, y cuando el token se dispara lanza una excepción dentro de Recognize, atrapa su propia excepción, limpia, y devuelve False con un diagnóstico. Si el pipeline tratara eso como un error de engine, el caller vería otlsEngineError para un trabajo que el usuario detuvo a propósito. ApplyLoadedOCRTextLayer por eso chequea primero el token cada vez que Recognize devuelve False, y solo convierte el resultado en un fallo de engine si el token no estaba activado. Ese orden preserva el contrato multipágina: reconocimiento, validación, contabilidad de presupuesto y construcción de contenido corren para cada página solicitada antes de que abra la transacción del grafo, así que una cancelación en la página 40 de 50 reporta otlsCancelled y deja el documento, incluidas las primeras 39 páginas, intacto. No hay un archivo parcialmente buscable que explicar después, y el resto del manejo de fallos sigue el mismo estilo acotado:
- El timeout es por llamada a
Recognize, medido desde su arranque, así que el default de 60,000 ms aplica a cada página y no al documento completo - Un hijo que siga corriendo al vencer el timeout o al cancelarse se termina, se espera hasta 5 segundos, y su directorio privado se borra en un bloque
finally output.tsvtiene tope de 64 MiB ystderr.txtde 1 MiB, chequeados mientras el hijo corre y también después de salir- El conteo de palabras y las code units UTF-16 tienen tope por página según los presupuestos restantes de
MaxWordsPerPage,MaxTotalWordsyMaxTextCodeUnits, y pasarse hace fallar la corrida en lugar de truncar la lista de palabras - La salida estándar va a
NULporque Tesseract escribeoutput.tsv, mientras que el error estándar va a un archivo para que un código de salida distinto de cero se reporte con hasta 4,096 caracteres de la queja propia del engine, usualmente la vía más rápida de enterarse de que falta un archivo.traineddata
Cómo las palabras reconocidas se vuelven una capa de texto invisible
HotPDF escribe las palabras de Tesseract como texto invisible usando el text rendering mode 3, el modo sin relleno ni trazo definido en ISO 32000-1 §9.3.6, así que la página sigue mostrando la imagen escaneada mientras la búsqueda y la copia trabajan sobre las palabras reconocidas. El content stream abre BT con 3 Tr, y cada palabra recibe una matriz Tm en su baseline, un tamaño de fuente derivado de la altura de la caja en píxeles al DPI de renderizado, y una escala horizontal Tz que estira la corrida de glifos hasta el ancho de caja medido, razón por la que un resaltado de búsqueda cae sobre la palabra en la imagen en lugar de irse deslizando
El TSV de Tesseract trae cajas pero no baselines, así que el adapter reporta cada palabra sin baseline y el pipeline estima la baseline a un quinto de la altura de caja por encima del borde inferior. El texto mismo pasa por una fuente Type0 compartida sin incrustar con codificación Identity-H y un CMap ToUnicode generado, un CID por cada escalar Unicode distinto en toda la corrida, que es cómo el chino, el latino con tildes y los caracteres del plano suplementario sobreviven todos a la copia y la búsqueda. Ese diseño tiene dos límites que vale declarar de entrada: una corrida puede llevar a lo sumo 65,535 escalares distintos, y la fuente sin incrustar no satisface el requisito de incrustación de fuentes de ISO 19005, así que la salida PDF/A necesita una fuente conforme incrustada por separado. Verificar el resultado es simple y vale automatizarlo: guarde, recargue, y corra el camino de texto corriente de documento cargado de extraer texto de un PDF cargado en Delphi; si las palabras vuelven en las páginas esperadas, la capa es real
RapidOCR y otros engines sobre el mismo protocolo TSV
HotPDF reutiliza el mismo runner de procesos y parser TSV para RapidOCR mediante HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds), que es la elección más útil para escaneos en chino simplificado. La línea de comandos es idéntica salvo que la ruta del script puente se inserta después del ejecutable de Python, y el lenguaje queda fijo en chi_sim. HotPDF trae el puente como tools/OCR/rapidocr_tsv.py; espera los paquetes rapidocr y onnxruntime más tres modelos ONNX locales, deshabilita las descargas automáticas de modelos, y escribe TSV con forma de Tesseract para que el lado Delphi no necesite un segundo parser. El nombre de engine reportado en Info.EngineName es RapidOCR (local ONNX). Esa forma sugiere la receta general: cualquier reconocedor que usted pueda envolver en un script pequeño que acepte la lista de argumentos al estilo Tesseract y emita el TSV de doce columnas hereda gratis el aislamiento de handles, el timeout, la cancelación, los presupuestos de salida y la confirmación todo-o-nada. Los adapters son solo para Windows, corren una página a la vez de forma síncrona, y no enderezan ni preprocesan la imagen más allá de lo que el renderizador produce, así que la calidad de imagen que entra sigue fijando el techo de lo que sale
Los adapters de Tesseract y RapidOCR, el escritor de capa de texto invisible, el renderizador de páginas que los alimenta, y la extracción de texto que verifica el resultado vienen todos en el mismo componente VCL nativo para Delphi y C++Builder. Si está agregando OCR a una aplicación de captura o de archivado de documentos, el HotPDF Delphi PDF component le da el pipeline con solo el motor OCR mismo por instalar