HotPDF hace buscables las páginas PDF escaneadas con RapidOCR en proceso a través de HPDFCreateRapidOCRDLLOCREngine, una factoría añadida en v2.774.0 que carga HotPDFRapidOCR.dll, mantiene residentes en memoria los modelos ONNX de detección, clasificación de ángulo y reconocimiento, y devuelve un IHPDFOCREngine. Usted pasa ese motor a THotPDF.ApplyLoadedOCRTextLayer, que renderiza cada página, ejecuta la inferencia en CPU sin Python ni proceso hijo, y compromete una capa de texto Unicode invisible
La motivación es el coste por página. El adaptador de proceso RapidOCR que salió antes, HPDFCreateRapidOCREngine, arranca un worker de Python por cada llamada a Recognize, y ese worker importa su runtime y carga sus modelos ONNX antes de leer un solo píxel. Sobre un archivo de 500 páginas ese peaje de arranque se repite 500 veces, y desplegar significa llevar un entorno Python junto a un ejecutable Delphi. La DLL nativa carga los modelos una vez, cuando usted crea el motor, y el despliegue se reduce a la DLL, sus archivos de modelos y un diccionario de caracteres. Lo que entrega a cambio es la posibilidad de matar un reconocedor atascado, y buena parte de la ingeniería de este adaptador consiste en convivir con eso con honestidad
¿Cómo se hace buscable un PDF escaneado con la DLL RapidOCR?
Crear un PDF buscable con la DLL nativa RapidOCR lleva una llamada a la factoría y la misma llamada ApplyLoadedOCRTextLayer que usa todo motor OCR de HotPDF. La factoría vive en la unidad HPDFRapidOCRRecognition y valida con ansias: la DLL y el directorio de modelos deben existir, cada archivo de modelo y de diccionario debe resolverse, la versión de ABI debe ser 1, y todos los exports requeridos deben estar presentes antes de inicializar ningún modelo. Los errores de configuración lanzan EArgumentException; un modelo que no carga lanza EInvalidOperation llevando el texto diagnóstico que escribió la DLL
uses
SysUtils, HPDFTypes, HPDFDoc, HPDFRapidOCRRecognition;
procedure MakeSearchable(const SourceFile, TargetFile: string);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// Los modelos se cargan aquí, fuera de cualquier deadline de reconocimiento.
// Los nombres de modelo relativos en THPDFRapidOCRDLLOptions.Default
// se resuelven contra el directorio de modelos.
Engine := HPDFCreateRapidOCRDLLOCREngine(
'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models');
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,
' lines accepted, ', Info.DroppedWordCount, ' dropped');
Doc.SaveLoadedDocument(TargetFile);
finally
Doc.Free;
end;
end;
THPDFRapidOCRDLLOptions.Default nombra ch_PP-OCRv3_det_infer.onnx, ch_PP-OCRv3_rec_infer.onnx, ch_ppocr_mobile_v2.0_cls_infer.onnx y ppocr_keys_v1.txt, con un hilo de CPU, un límite de entrada de 16.777.216 píxeles y un deadline de reconocimiento de 60.000 ms. Desde v2.775.0, THPDFRapidOCRDLLOptions.ForLanguage mete un modelo de reconocimiento y un diccionario a juego para chino tradicional, ruso, japonés, árabe y otros perfiles; por qué el modelo y el diccionario deben cambiar juntos está cubierto en modelos multilingües RapidOCR y diccionarios CTC en HotPDF. El motor se reporta como RapidOCR (native DLL) en Info.EngineName, lo que mantiene los logs inequívocos junto al adaptador de proceso OCR Tesseract externo y el motor OCR de plantillas incorporado
¿Por qué la ABI C solo habla int32_t y bytes UTF-8?
La ABI de HotPDFRapidOCR.dll usa solo enteros de ancho fijo, punteros en crudo y longitudes de byte explícitas porque Delphi, C++Builder y Free Pascal no comparten nada con MSVC más allá de la convención de llamada C. Un std::string, un std::vector o una excepción de C++ tienen una disposición y un modelo de unwinding que pertenecen a un compilador y a una librería de runtime. Deje que cualquiera de ellos cruce la frontera y el fallo es una pila corrompida o un bloque de heap liberado por el asignador equivocado, no un error limpio
La versión de ABI 1 sigue por eso una lista corta de reglas. Cada export es cdecl y devuelve un estado int32_t, donde 1 significa éxito y 0 fracaso. Toda función que puede fallar recibe un buffer de diagnóstico propiedad del llamador y su capacidad en bytes; la DLL escribe un mensaje UTF-8 terminado en NUL truncado para caber, y el adaptador lo decodifica con un terminador duro en el último byte de su propio buffer de 4.096 bytes. El cuerpo de cada export va envuelto en try con catch (const std::exception &) y catch (...), así que un error de ONNX Runtime, un assert de OpenCV o un diccionario inválido se convierte en estado 0 más texto, nunca una excepción escapando a código Pascal
| Export | Papel | Cuándo lo resuelve el adaptador |
|---|---|---|
HPDFRapidOCRAbiVersion | Devuelve 1; cualquier otro valor se rechaza | Primero, antes que nada |
HPDFRapidOCRCreate | Carga los modelos de detección, clasificación opcional y reconocimiento, y el diccionario | En la factoría |
HPDFRapidOCRRecognize | Ejecuta un bitmap y emite un callback por línea de texto | En la factoría |
HPDFRapidOCRDestroy | Libera la instancia de modelos | En la factoría |
HPDFRapidOCRSetReadingDirection | Orden de filas de derecha a izquierda opcional, añadido en v2.775.0 | Solo cuando RightToLeft está activo |
El export opcional se resuelve perezosamente a propósito: una DLL de v2.774.0 que no lo tenga sigue sirviendo peticiones de izquierda a derecha. La DLL se carga con LoadLibraryEx con flags de búsqueda que cubren la carpeta propia de la DLL más los directorios seguros por defecto, así que las dependencias de ONNX Runtime u OpenCV colocadas junto a HotPDFRapidOCR.dll se encuentran sin tocar PATH. Las rutas de modelos y diccionarios viajan como UTF-8 y la DLL las convierte con MultiByteToWideChar en modo estricto antes de abrir archivos por APIs de caracteres anchos, así que un directorio de modelos bajo un nombre de usuario chino o cirílico funciona en lugar de ensancharse byte a byte en galimatías
Una regla vive en la build más que en la cabecera. La DLL enlaza estáticamente ONNX Runtime y OpenCV, y la configuración CMake por defecto usa el CRT release estático (/MT). Librerías estáticas compiladas contra /MD metidas en una DLL /MT producen errores de enlace como mucho y dos heaps independientes como poco, así que las librerías provisionadas deben casar con el modo CRT que use la DLL
¿Qué pasa entre un TBitmap y una línea de texto?
HotPDF le entrega a la DLL una instantánea BGR independiente de arriba abajo de la página renderizada, y la DLL devuelve un callback por línea de texto reconocida con texto UTF-8 prestado que el adaptador debe copiar antes de retornar
En Delphi el adaptador asigna el bitmap de la página a un TBitmap privado, fuerza pf24bit, y lee las filas con GetDIBits usando un biHeight negativo, lo que produce filas de arriba abajo rellenadas a alineación de cuatro bytes; ese stride se pasa explícitamente. En FPC lee a través de CreateIntfImage, porque las escrituras scanline de LCL pueden actualizar la imagen en crudo sin refrescar el handle GDI. El bitmap del llamador nunca se modifica, y el presupuesto de píxeles (MaxPixels, 16.777.216 por defecto y configurable hasta 67.108.864) y el límite de 32.767 píxeles por dimensión se comprueban antes de asignar el buffer de la instantánea
Dentro de la DLL la instantánea se rellena con 50 píxeles blancos, las regiones de texto se detectan con un lado máximo de 1.024 píxeles, las cajas se ordenan en filas horizontales, y cada recorte se rota opcionalmente por el clasificador de ángulo antes del reconocimiento. Cada línea de texto pasa entonces por un callback que recibe un const char*, un recuento de bytes, una caja entera en píxeles de la imagen original y la confianza media de caracteres. El puntero de texto solo es válido durante el callback, así que el adaptador lo copia de inmediato, y es estricto con lo que acepta:
- El UTF-8 se decodifica con
MB_ERR_INVALID_CHARS; una secuencia malformada hace fallar la página en lugar de producir caracteres de sustitución en una capa buscable - Los caracteres de control C0 y C1 se rechazan, y las líneas de solo espacios en blanco se saltan
- La caja debe caber dentro del bitmap y la confianza debe ser un valor finito de 0 a 1
- El texto se cuenta contra el
MaxTextCodeUnitsde la petición con un techo duro de 1.048.576 unidades UTF-16 por llamada, y los caracteres de plano suplementario cuestan dos unidades - Cualquier excepción Pascal dentro del callback se captura ahí, se guarda y se convierte en un retorno 0, lo que hace que la DLL se detenga y reporte fallo; el mensaje guardado se convierte entonces en el diagnóstico
Dos consecuencias importan para el ajuste. Primera: la unidad de salida es una línea, no una palabra; cada línea consume una ranura MaxWords, Info.AcceptedWordCount y Info.DroppedWordCount cuentan líneas, y el resaltado de búsqueda abarca la caja de la línea. Segunda: MinimumConfidence (0.5 por defecto) se compara contra la confianza media de caracteres de la línea, así que una línea con un carácter ilegible entre veinte limpios suele sobrevivir. La DLL no aporta baseline, así que el pipeline de la capa de texto estima una a partir de la caja. Una página vacía tiene éxito con cero líneas, y cualquier fallo limpia los resultados parciales para que el commit multipágina siga siendo todo-o-nada
Propiedad de modelos y thread safety
Cada motor de DLL RapidOCR posee exactamente una instancia de modelos durante toda su vida, y las llamadas a Recognize sobre ese motor se serializan con una critical section. Sostener la interfaz IHPDFOCREngine es lo que mantiene calientes los modelos, así que el patrón correcto para trabajo por lotes es crear el motor una vez y reutilizarlo entre documentos
procedure OcrBatch(const Files: TStrings; const OutputDir: string);
var
Models: THPDFRapidOCRDLLOptions;
Engine: IHPDFOCREngine;
Doc: THotPDF;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
I: Integer;
begin
Models := THPDFRapidOCRDLLOptions.Default;
Models.UseAngleClassifier := False; // escaneos rectos: no se carga modelo clasificador
Models.Threads := 4; // 1..64, topado al número de procesadores lógicos
Models.TimeoutMilliseconds := 120000; // por llamada a Recognize, cooperativo
Engine := HPDFCreateRapidOCRDLLOCREngine(
'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models', Models);
Options := THPDFOCRTextLayerOptions.Default;
for I := 0 to Files.Count - 1 do
begin
Doc := THotPDF.Create(nil);
try
Doc.AutoLaunch := False;
if (Doc.LoadFromFile(Files[I]) > 0) and
Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
Doc.SaveLoadedDocument(IncludeTrailingPathDelimiter(OutputDir) +
ExtractFileName(Files[I]))
else
Writeln(Files[I], ': ', string(Info.Diagnostic));
finally
Doc.Free;
end;
end;
end; // última referencia liberada: modelos destruidos, luego se descarga la DLL
El valor de Threads fija los recuentos de hilos intra-op e inter-op de cada sesión ONNX, y la DLL lo topa al número de procesadores activos. Dos hilos compartiendo un motor no corren en paralelo; el segundo espera al lock. Esa espera no es un EnterCriticalSection a ciegas: el adaptador llama a TryEnterCriticalSection cada 25 ms y comprueba el token de cancelación y el deadline entre intentos, así que una petición en cola aún puede cancelarse o agotar su tiempo. Si necesita paralelismo de verdad, cree un motor por worker y acepte que cada motor sostiene su propia copia de los modelos en memoria
El orden de desmontaje lo fija el destructor del motor: HPDFRapidOCRDestroy libera primero la instancia de modelos, luego FreeLibrary descarga la DLL. En el lado nativo, la inicialización de modelos es igual de cuidadosa; cuando el modelo de reconocimiento falla después de que las sesiones del detector y el clasificador ya se construyeron, esas sesiones se liberan antes de reportar el error, y el recuento de clases del diccionario se comprueba contra la salida del modelo durante la inicialización y no en la primera página
¿Por qué no se puede matar una llamada OCR nativa a mitad de inferencia?
Una llamada RapidOCR nativa no puede matarse a mitad de inferencia porque corre en su hilo, dentro de su proceso, en mitad de una sesión de ONNX Runtime que no acepta interrupciones. La cancelación en el adaptador de DLL de HotPDF es por tanto cooperativa: la DLL llama a un callback de aborto antes y después de la detección, tras la clasificación y tras cada línea reconocida, y se detiene en el primer checkpoint donde el callback devuelve 0. Un Run de ONNX que ya ha arrancado terminará primero
Las alternativas son peores que esperar. TerminateThread dejaría el lock del heap del CRT, el thread pool de ONNX Runtime y cualquier estado de OpenCV en la condición que les pillara, envenenando el resto del proceso. Un FreeLibrary mientras una llamada sigue ejecutándose descarga código que está en la pila. Ninguno de los dos puede hacerse seguro, así que el adaptador jamás lo intenta. El deadline en TimeoutMilliseconds es en consecuencia un deadline cooperativo, y un deadline vencido emerge como un error de motor con un diagnóstico de timeout, mientras que un token cancelado emerge como otlsCancelled:
// El token lo crea el llamador y lo comparte con el hilo de UI,
// que llama a Token.Cancel cuando el usuario pulsa Stop
Options := THPDFOCRTextLayerOptions.Default;
Options.CancellationToken := Token;
if not Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
case Info.Status of
otlsCancelled:
// se devuelve en la siguiente frontera de fase o de línea; documento sin cambiar
Writeln('Cancelled');
otlsEngineError:
// incluye un deadline cooperativo vencido y diagnósticos nativos
Writeln('Engine: ', string(Info.Diagnostic));
otlsBudgetExceeded:
Writeln('Budget: ', string(Info.Diagnostic));
else
Writeln(string(Info.Diagnostic));
end;
Este es el trade-off central entre los adaptadores de proceso de HotPDF y la DLL en proceso, y ninguno de los dos lados gana en todas las filas:
- Coste de arranque: los adaptadores Tesseract y RapidOCR de Python lanzan un proceso y cargan modelos por cada página; la DLL carga los modelos una vez por motor
- Parada: un proceso hijo puede terminarse sin más, y el worker de Python corre dentro de un Job Object de kill-on-close así que su árbol de procesos entero cae con él; la DLL solo puede pararse en fronteras de fase y de línea
- Contención de fallos: un crash en
tesseract.exehace fallar una página; un access violation dentro de la DLL se lleva su proceso por delante - Despliegue: los adaptadores de proceso necesitan un programa instalado o un entorno Python; la DLL se necesita a sí misma, sus modelos y su diccionario, casando con el bitness de la aplicación
- Memoria: los adaptadores de proceso liberan todo cuando el hijo sale; un motor de DLL mantiene sus modelos residentes hasta que se libera la última referencia de interfaz
Para una aplicación de escritorio interactiva que hace OCR a una página por vez, la respuesta de la DLL suele ganar. Para un servidor que ingiere escaneos no fiables a todas horas, la frontera de proceso merece su coste de arranque
Compilar y desplegar HotPDFRapidOCR.dll
HotPDFRapidOCR.dll se compila de los fuentes C++ en Native/RapidOCR con MSVC, C++17, un Windows SDK y CMake 3.20 o posterior, usando un script de ayuda que toma los directorios de las fuentes de red nativas, ONNX Runtime y OpenCV más una plataforma Win32 o Win64. Compile ambas si despacha ambas, porque una aplicación Delphi de 32 bits no puede cargar una DLL de 64 bits, y las librerías estáticas que provisione deben casar con la arquitectura objetivo además de con el modo CRT
El lado de los modelos tiene sus propios límites de compatibilidad. El detector es un DB text detector; el reconocedor acepta modelos CTC en disposición NCHW con una altura de entrada fija de 32 o 48, y usa 48 para modelos de altura dinámica. El ONNX Runtime estático incluido no puede cargar modelos guardados con una versión de IR más nueva, así que los exports recientes de PP-OCRv5 fallan la inicialización con un diagnóstico en lugar de cargar a medias. El diccionario debe ser UTF-8 sin BOM, en el orden de caracteres exacto del modelo, y su recuento de clases debe casar con la salida del modelo; se aceptan finales de línea CRLF. El reconocimiento es offline: la DLL jamás descarga un modelo que falte
Referencia rápida
- Factoría:
HPDFCreateRapidOCRDLLOCREngine(LibraryPath, ModelDirectory[, Options])enHPDFRapidOCRRecognition, disponible desde v2.774.0 en builds Delphi, C++Builder y FPC/Lazarus para Windows - Mantenga vivo el
IHPDFOCREnginedevuelto entre páginas y documentos; liberarlo destruye los modelos y descarga la DLL - Un motor ejecuta un reconocimiento a la vez; cree varios motores para workers en paralelo y presupueste memoria por cada copia de modelos
- La salida es una entrada por línea de texto con confianza media de caracteres, filtrada por
THPDFOCRTextLayerOptions.MinimumConfidence - La cancelación y
TimeoutMillisecondsson cooperativas; un run de ONNX en curso siempre termina - Casée el bitness de la DLL con la aplicación y el modo CRT de las librerías estáticas de ONNX Runtime y OpenCV con la DLL
- Escoja un perfil de idioma por motor con
THPDFRapidOCRDLLOptions.ForLanguage(v2.775.0); un motor no detecta idiomas por su cuenta
El adaptador RapidOCR nativo, los adaptadores OCR basados en proceso, el renderer de páginas que los alimenta y el escritor invisible de capas de texto Unicode vienen todos juntos en HotPDF, un componente PDF VCL nativo para Delphi y C++Builder. Si su aplicación de captura o archivado de documentos necesita salida buscable sin un runtime Python en la máquina destino, el componente Delphi PDF de HotPDF aporta el pipeline completo dejando solo la DLL y sus modelos por desplegar