Un pipe que jamás reporta end-of-file es un bug raro de perseguir en una feature de OCR. Un thread de su aplicación lee la salida estándar de un programa auxiliar y espera a que ReadFile falle con ERROR_BROKEN_PIPE. Otro thread arranca tesseract.exe vía CreateProcess con bInheritHandles en True, porque así es como se le entrega a un proceso hijo un archivo para su error estándar. El extremo de escritura del pipe del auxiliar era heredable, así que Tesseract ahora posee una copia, y el thread lector se cuelga hasta que Tesseract termina su página — que, en un escaneo de 300 DPI con dos modelos de idioma cargados, no es rápido
Un reconocedor externo trae más que el cuelgue: qué puede ver el hijo, qué hacer con salida medio válida, y cómo convertir cajas de píxeles en texto PDF buscable. El engine OCR de plantillas integrado cubre a propósito letras y dígitos ASCII impresos por máquina. Nombres acentuados, contratos en chino y archivos multilingües necesitan modelos de idioma entrenados, y Tesseract es el candidato usual porque es un programa de línea de comandos que se puede instalar junto a su ejecutable
Enchufar Tesseract a la capa de texto OCR de HotPDF
HotPDF envuelve un ejecutable Tesseract instalado localmente como un IHPDFOCREngine vía HPDFCreateTesseractOCREngine en la unit HPDFTesseractRecognition. Usted pasa ese engine a THotPDF.ApplyLoadedOCRTextLayer, que renderiza cada página pedida, corre Tesseract una vez por página, filtra las palabras y escribe una capa de texto Unicode invisible para todas las páginas pedidas en una transacción, o para ninguna. El mapeo de coordenadas, la rotación, el filtro de confianza y la confirmación son el pipeline que el engine integrado ya usa; solo cambia el reconocimiento
La factory lanza EArgumentException antes de tocar cualquier página. El ejecutable y el directorio tessdata deben existir, el timeout debe estar entre 1 y 3,600,000 milisegundos, y el idioma puede contener solo letras ASCII, dígitos, _ y +, porque termina en una línea de comandos: eng+chi_sim es legítimo, un valor con comillas o espacios es una inyección esperando ocurrir. Un segundo overload toma un record THPDFTesseractOptions. Su Default da --psm 3, --oem 3, 60 segundos por página y un tope de 16,777,216 píxeles, y rechaza tpsOSDOnly y tpsAutoOnly, los dos modos que no devuelven palabras
uses
SysUtils, HPDFTypes, HPDFDoc, HPDFTesseractRecognition;
function MakeSearchable(const SourceFile, TargetFile: string;
Token: THPDFCancellationToken): Boolean;
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
TessOptions: THPDFTesseractOptions;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
TessOptions := THPDFTesseractOptions.Default; // psm 3, oem 3, 60 s, 16,777,216 px
TessOptions.TimeoutMilliseconds := 120000; // aplica a cada página
// lanza EArgumentException por un ejecutable o carpeta tessdata faltante,
// un identificador de idioma malo, u opciones fuera de sus rangos
Engine := HPDFCreateTesseractOCREngine(
'C:\OCR\Tesseract\tesseract.exe',
'C:\OCR\Tesseract\tessdata',
'eng+chi_sim', // modelos unidos con '+'
TessOptions);
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 que ya tienen texto se saltan
Result := Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info);
if Result then
begin
Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
' words kept, ', Info.DroppedWordCount, ' below the threshold');
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;
Haga la aritmética de píxeles una vez. Una página A4 a 300 DPI es cerca de 2,481 por 3,508 píxeles, unos 8.7 millones, y cabe. Una página A3 a la misma resolución es cerca de 3,508 por 4,961, casi 17.4 millones, así que falla con un diagnóstico de presupuesto de píxeles y la corrida termina como otlsEngineError. El overload que toma solo un timeout no aplica tope de píxeles alguno. Para un servicio que acepta archivos de afuera, conserve el overload de opciones y suba MaxPixels (hasta 67,108,864) o baje THPDFOCRTextLayerOptions.DPI deliberadamente cuando se esperen formatos grandes
Por página, el engine crea un directorio privado HotPDF-OCR-{GUID} bajo la ruta temporal, guarda el bitmap como input.bmp y corre tesseract input.bmp output --tessdata-dir … -l … --dpi N --psm 3 -c tessedit_create_tsv=1, agregando --oem cuando hay un modo de engine asignado. Las rutas se citan con las reglas de Windows para backslashes y comillas embebidas, y --dpi carga la resolución de render para que Tesseract jamás la adivine. Tesseract y sus archivos .traineddata no vienen incluidos; provisionarlos es trabajo de la aplicación. En nuestras propias máquinas de prueba con Linux el reconocedor corre desde un bundle a nivel de usuario del binario, su cierre de librerías compartidas y la carpeta tessdata, porque depender de /usr/bin/tesseract solo funciona donde alguien instaló el package del sistema
¿Por qué una fila TSV rota vacía la página completa?
El adaptador Tesseract hace fallar la página entera ante la primera fila malformada, porque una lista de palabras parcialmente parseada produce una capa de texto que discrepa con la imagen, y nada en el PDF guardado dice qué palabras faltaron. La salida debe decodificar como UTF-8 estricto sin NUL embebido. La primera línea debe ser igual al header de doce columnas de Tesseract, de level a text, una vez quitada una marca de orden de bytes opcional. Cada fila debe partirse en exactamente doce campos, y la partición se detiene tras el tabulador undécimo, así que un tab dentro del texto reconocido cae en el campo de texto en vez de crear una decimotercera columna, donde el chequeo de caracteres de control de abajo la rechazaría
Solo las filas de nivel 5 son palabras; las filas de página, bloque, párrafo y línea se saltan, igual que las palabras vacías o de puro espacio. Una palabra con un carácter de control (debajo de U+0020, o de U+007F a U+009F) hace fallar la página. El resto exige geometría entera, una confianza parseada con formato invariante para que una aplicación con locale alemán igual lea 93.5, una caja totalmente dentro del bitmap, y una confianza de 0 a 100. Una palabra válida seguida de una fila rota produce cero palabras
Una fila rara hace entonces fallar toda la corrida y deja todas las páginas intactas. Ese es el trade correcto. Una re-corrida es barata; un PDF buscable donde una búsqueda de número de factura no encuentra la única página que importa suele aparecer meses después, en manos de otra persona
El último paso además se topa con un default que quizá no espera. Tesseract reporta confianza de 0 a 100, el pipeline divide entre 100, y THPDFOCRTextLayerOptions.MinimumConfidence hace default a 0.5, así que toda palabra debajo de 50 cae en Info.DroppedWordCount en vez de en la página. En un escaneo limpio ese piso es sensato. En un fax ruidoso puede quitarse una buena parte del texto, así que compare Info.ReceivedWordCount con Info.AcceptedWordCount antes de bajarlo
Correr OCR otra vez sobre un archivo que ya tiene texto
SkipPagesWithText es True por default, y eso atrapa a la gente a la segunda vuelta. Suponga que un lote se procesó con eng y ahora necesita eng+deu: toda página ya carga texto invisible, así que Info.SkippedPageCount es igual al conteo de páginas. HotPDF marca el content stream que escribe como su propia capa OCR, y ReplaceExistingOCRTextLayer usa esa marca para reconocer esas páginas otra vez, soltando la capa vieja y conservando el contenido original de la página
Options := THPDFOCRTextLayerOptions.Default;
Options.ReplaceExistingOCRTextLayer := True; // cambia nuestra capa anterior
Options.UseOptionalContentGroup := True; // capa llamada 'OCR Text Layer'
Options.CancellationToken := Token;
if Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
Writeln(Info.ReplacedPageCount, ' replaced, ',
Info.SkippedPageCount, ' skipped, ',
Info.RecognizedPageCount, ' recognized')
else
Writeln(string(Info.Diagnostic));
Un archivo OCR'd por un driver de escáner no carga tal marca, así que sus páginas siguen contando como páginas con texto. Apagar SkipPagesWithText apilaría una segunda capa invisible sobre la primera, y todo resultado de búsqueda aparecería dos veces. Deje esos archivos quietos
¿Qué handles hereda el proceso de Tesseract?
El proceso hijo de Tesseract hereda exactamente dos handles de HotPDF: uno abierto sobre NUL para entrada y salida estándar, y uno para stderr.txt en el directorio privado. El cuelgue del pipe de arriba viene del propio CreateProcess: bInheritHandles = True es obligatorio para pasar handles estándar, y por sí solo pasa al hijo todo handle heredable del host, incluidos pipes, archivos y eventos abiertos por código sin relación. HotPDF usa STARTUPINFOEX con una lista de atributos que carga PROC_THREAD_ATTRIBUTE_HANDLE_LIST y el flag EXTENDED_STARTUPINFO_PRESENT. bInheritHandles se queda en True, pero solo cruzan los handles listados
El mismo patrón va en todo otro proceso hijo que su aplicación arranque, porque un solo lanzador que hereda todo alcanza para colgar a todo lector de pipes del proceso:
// Patrón para sus propias llamadas CreateProcess (fragmento: NullHandle, ErrorHandle,
// Exe, CommandLine, WorkDir y ProcessInfo se declaran en otro lado).
// Las RTL de Delphi viejas no declaran las funciones de lista de atributos; agregue
// sus propias declaraciones external de kernel32 si el compilador no las encuentra
// Revise cada resultado BOOL en código de producción
type
TStartupInfoExRec = record
StartupInfo: TStartupInfo;
AttributeList: Pointer;
end;
const
HANDLE_LIST_ATTRIBUTE = $00020002; // PROC_THREAD_ATTRIBUTE_HANDLE_LIST
EXTENDED_STARTUP_FLAG = $00080000; // EXTENDED_STARTUPINFO_PRESENT
var
Startup: TStartupInfoExRec;
HandleList: array[0..1] of THandle;
Size: NativeUInt;
begin
FillChar(Startup, SizeOf(Startup), 0);
HandleList[0] := NullHandle; // creado con bInheritHandle = True
HandleList[1] := ErrorHandle; // igual
Size := 0;
InitializeProcThreadAttributeList(nil, 1, 0, Size);
GetMem(Startup.AttributeList, Size);
InitializeProcThreadAttributeList(Startup.AttributeList, 1, 0, Size);
UpdateProcThreadAttribute(Startup.AttributeList, 0, HANDLE_LIST_ATTRIBUTE,
@HandleList[0], SizeOf(HandleList), nil, nil);
Startup.StartupInfo.cb := SizeOf(Startup);
// ... asigne dwFlags y hStdInput / hStdOutput / hStdError como siempre ...
CreateProcess(PChar(Exe), PChar(CommandLine), nil, nil,
True, // sigue siendo obligatorio con lista de handles
CREATE_NO_WINDOW or EXTENDED_STARTUP_FLAG,
nil, PChar(WorkDir), Startup.StartupInfo, ProcessInfo);
// tras salir el hijo: DeleteProcThreadAttributeList, FreeMem, CloseHandle
end;
Los scripts puente agregan un detalle más de Windows. Un launcher de Python puede arrancar un segundo proceso intérprete, y terminar el proceso que usted creó deja ese corriendo. Cuando al engine se le da una ruta de script, como el adaptador RapidOCR de abajo, HotPDF arranca el hijo suspendido, lo asigna a un job object con JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE y luego lo reanuda, así que cerrar el job termina todo el árbol. La misma idea de contención impulsa aislar codecs de imagen PDF en procesos worker, donde el hijo no es de confianza; aquí el hijo es de confianza, pero su proceso no es el único código sosteniendo handles dentro
Cancelación, timeouts y la confirmación todo-o-nada
IHPDFOCREngine.Recognize devuelve un Boolean, y False cubre tanto "Tesseract falló" como "el usuario pulsó Cancelar". El adaptador consulta el token y su timeout cada 25 milisegundos mientras Tesseract corre, y ante una cancelación limpia y devuelve False. Tomado a la letra eso se leería como otlsEngineError, así que ApplyLoadedOCRTextLayer revisa el token primero cada vez que Recognize devuelve False. El reconocimiento y la construcción de contenido terminan para toda página pedida antes de que se abra la transacción del documento, así que una cancelación en la página 40 de 50 reporta otlsCancelled y deja las 50 páginas tal como se cargaron. Los demás límites siguen el mismo estilo:
- El timeout corre desde el inicio de cada llamada
Recognize, así que el default de 60,000 ms es un límite por página - Un hijo que siga corriendo al expirar el timeout o ante una cancelación se termina y se espera hasta 5 segundos; el directorio privado se elimina en un bloque
finally, con el borrado reintentado hasta por un segundo porque los archivos de salida pueden quedar bloqueados un momento tras terminar el proceso output.tsvtiene tope de 64 MiB ystderr.txtde 1 MiB, revisados mientras el hijo corre y después de salir- Las palabras y unidades de código UTF-16 por página tienen tope según lo que quede de
MaxWordsPerPage,MaxTotalWordsyMaxTextCodeUnits; pasarse hace fallar la corrida en vez de truncar la lista - Un código de salida distinto de cero regresa con hasta 4,096 caracteres de
stderr.txt, la manera más rápida de enterarse de que falta un archivo.traineddata
Cómo se ve la capa de texto invisible dentro de la página
HotPDF escribe las palabras con el modo de renderizado de texto 3 (ni relleno ni trazo, ISO 32000-1 §9.3.6), así que el escaneo sigue visible mientras la búsqueda y la copia trabajan sobre las palabras. El content stream abre BT con 3 Tr; cada palabra recibe una matriz Tm en su baseline, un tamaño de fuente de la altura de caja en píxeles por 72 sobre el DPI de render, y una escala Tz que estira la corrida al ancho de la caja. Ese estirado mantiene el resaltado de búsqueda sobre la palabra en la imagen
El TSV trae cajas pero no baselines, así que cada baseline se coloca a un quinto de la altura de caja sobre el borde inferior. Entre palabras vecinas de una línea izquierda-derecha en scripts separados por espacios como el latino o el cirílico, un espacio estirado llena el hueco, así que el texto copiado conserva sus cortes de palabra; el chino no los recibe. Los glifos van por una fuente Type0 compartida sin embeber con codificación Identity-H y un CMap ToUnicode generado, un CID por escalar Unicode distinto, que es cómo el chino, el latino acentuado y los caracteres del plano complementario sobreviven la copia y la búsqueda. Una corrida sostiene hasta 65,535 escalares distintos, y como ISO 19005 exige fuentes embebidas, la salida PDF/A necesita una fuente conforme embebida aparte. Para verificar un resultado, guarde, recargue y corra el camino ordinario de extraer texto de un PDF cargado en Delphi; si las palabras esperadas regresan en las páginas esperadas, la capa es real
RapidOCR y la DLL nativa sobre el mismo contrato
Para escaneos en chino simplificado, HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds) reutiliza el mismo corredor de procesos y el mismo parser de TSV. La ruta del script puente va tras el ejecutable de Python y el idioma queda fijo en chi_sim. HotPDF distribuye el puente como tools/OCR/rapidocr_tsv.py; necesita los packages rapidocr y onnxruntime, modelos ONNX locales y un archivo de fuente, se niega a descargas automáticas, y escribe TSV con forma de Tesseract; un overload de THPDFRapidOCROptions asigna archivos de modelo, conteos de threads y un tope de píxeles. La factory genérica detrás de ambos adaptadores, HPDFCreateLocalTSVOCREngine en HPDFLocalTSVRecognition, acepta cualquier programa que tome los argumentos estilo Tesseract y escriba el TSV de doce columnas, y hereda la lista de handles, el timeout, la cancelación y los presupuestos
HPDFCreateTesseractDLLOCREngine(LibraryPath, TessDataDirectory, Language) carga en cambio la API C de Tesseract in-process, se reporta como Tesseract (native DLL), verifica de entrada que cada archivo .traineddata pedido exista, y mapea cancelación y timeout sobre el monitor de progreso propio de Tesseract. Se ahorra un arranque de proceso y un viaje de BMP por página; el ejecutable mantiene los crashes y el crecimiento de memoria del reconocedor fuera de su address space. Para una herramienta de escritorio que trabaja los escaneos del propio usuario, la DLL está bien. Para un servicio que ingiere documentos de extraños, el engine de línea de comandos es el default más seguro. En cualquier caso el reconocimiento corre en Windows, una página por vez, sobre el bitmap renderizado, así que enderece y limpie los escaneos difíciles antes de que lleguen al engine
Los adaptadores OCR, el escritor de la capa de texto, el renderer que los alimenta y la extracción que verifica la salida viven todos en un componente VCL nativo para Delphi y C++Builder. Si su aplicación de captura o archivado necesita salida PDF buscable, el componente PDF de HotPDF para Delphi cubre todo salvo el engine OCR que usted instala