HotPDF vuelve buscables las páginas escaneadas de un PDF con RapidOCR in-process a través de HPDFCreateRapidOCRDLLOCREngine, una factory agregada 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 le pasa ese engine a THotPDF.ApplyLoadedOCRTextLayer, que renderiza cada página, corre inferencia en CPU sin Python ni proceso hijo, y confirma una capa de texto Unicode invisible
La motivación es el costo por página. El adapter de proceso de 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. En un archivo de 500 páginas ese impuesto de arranque se repite 500 veces, y desplegar implica mandar un entorno de Python junto al ejecutable de Delphi. La DLL nativa carga los modelos una vez, cuando usted crea el engine, y el despliegue se achica a la DLL, sus archivos de modelos y un diccionario de caracteres. Lo que usted entrega a cambio es la posibilidad de matar un reconocedor trabado, y casi toda la ingeniería de este adapter va de convivir con eso honestamente
¿Cómo se vuelve buscable un PDF escaneado con la DLL de RapidOCR?
Armar un PDF buscable con la DLL nativa de RapidOCR toma una llamada a la factory y la misma llamada a ApplyLoadedOCRTextLayer que usa todo engine de OCR de HotPDF. La factory vive en la unidad HPDFRapidOCRRecognition y valida con antelación: la DLL y el directorio de modelos deben existir, cada archivo de modelo y de diccionario debe resolver, la versión de ABI debe ser 1, y todos los exports requeridos deben estar presentes antes de que se inicialice cualquier modelo. Los errores de configuración levantan EArgumentException; un modelo que falla al cargar levanta EInvalidOperation con el texto de 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 modelos 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 thread 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 queda cubierto en los modelos multilingües RapidOCR y los diccionarios CTC en HotPDF. El engine se reporta como RapidOCR (native DLL) en Info.EngineName, lo que mantiene los logs sin ambigüedad junto al adapter de proceso OCR Tesseract externo y el engine de OCR por matching de plantillas integrado
¿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 crudos y largos de bytes explícitos porque Delphi, C++Builder y Free Pascal no comparten nada con MSVC más allá de la convención de llamadas C. Un std::string, un std::vector o una excepción de C++ tienen un layout y un modelo de unwinding que pertenecen a un compilador y a una librería runtime. Deje que cualquiera cruce la frontera y la falla es un stack corrupto o un bloque del heap liberado por el allocator equivocado, no un error limpio
La versión de ABI 1 sigue entonces una lista corta de reglas. Cada export es cdecl y devuelve un status int32_t, donde 1 significa éxito y 0 significa falla. Toda función que puede fallar recibe un buffer de diagnóstico del caller y su capacidad en bytes; la DLL escribe un mensaje UTF-8 terminado en NUL truncado para caber, y el adapter 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 tanto catch (const std::exception &) como catch (...), así que un error de ONNX Runtime, un assert de OpenCV o un diccionario inválido se vuelve status 0 más texto, jamás una excepción escapándose hacia código Pascal
| Export | Rol | Cuándo lo resuelve el adapter |
|---|---|---|
HPDFRapidOCRAbiVersion | Devuelve 1; cualquier otro valor se rechaza | Primero, antes que cualquier cosa |
HPDFRapidOCRCreate | Carga los modelos de detección, clasificación opcional y reconocimiento, y el diccionario | En la factory |
HPDFRapidOCRRecognize | Procesa un bitmap y emite un callback por línea de texto | En la factory |
HPDFRapidOCRDestroy | Libera la instancia de modelos | En la factory |
HPDFRapidOCRSetReadingDirection | Orden de filas derecha a izquierda opcional, agregado en v2.775.0 | Solo cuando RightToLeft está activo |
El export opcional se resuelve lazily a propósito: una DLL de v2.774.0 que no lo tenga igual sirve requests de izquierda a derecha. La DLL se carga con LoadLibraryEx con flags de búsqueda que cubren el folder propio de la DLL más los directorios safe por defecto, así que las dependencias de ONNX Runtime u OpenCV puestas junto a HotPDFRapidOCR.dll se encuentran sin tocar el 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 vez de ensancharse byte por byte en pura basura
Una regla vive en el build y no en el header. 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 a lo sumo errores de link y en el peor caso dos heaps independientes, así que las librerías que usted provisione deben coincidir 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 un snapshot BGR top-down independiente de la página renderizada, y la DLL devuelve un callback por línea de texto reconocida con texto UTF-8 prestado que el adapter debe copiar antes de devolver
En Delphi el adapter 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 arroja filas top-down acolchadas a alineación de cuatro bytes; ese stride se pasa explícito. En FPC lee a través de CreateIntfImage, porque las escrituras por scanline de LCL pueden actualizar la imagen cruda sin refrescar el handle GDI. El bitmap del caller jamás 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 chequean antes de asignar el buffer del snapshot
Dentro de la DLL el snapshot se acolcha 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 opcionalmente rota por el clasificador de ángulos antes del reconocimiento. Cada línea de texto pasa entonces por un callback que recibe un const char*, un conteo de bytes, una caja entera en píxeles de la imagen original, y la confianza media por carácter. El puntero de texto solo es válido durante el callback, así que el adapter lo copia de inmediato, y es estricto con lo que acepta:
- El UTF-8 se decodifica con
MB_ERR_INVALID_CHARS; una secuencia mal formada hace fallar la página en vez de producir caracteres de reemplazo 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 quedar dentro del bitmap y la confianza debe ser un valor finito de 0 a 1
- El texto se cuenta contra el
MaxTextCodeUnitsdel request con un techo duro de 1,048,576 unidades UTF-16 por llamada, y los caracteres del plano suplementario cuestan dos unidades - Cualquier excepción de Pascal dentro del callback se atrapa allí, se guarda, y se convierte en un retorno 0, lo que hace que la DLL se detenga y reporte falla; el mensaje guardado se vuelve entonces el diagnóstico
Dos consecuencias importan al afinar. Primera: la unidad de salida es una línea, no una palabra: cada línea consume un slot de 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 por carácter de la línea, así que una línea con un carácter ilegible entre veinte limpios normalmente sobrevive. La DLL no entrega baseline, así que el pipeline de la capa de texto estima una desde la caja. Una página vacía tiene éxito con cero líneas, y cualquier falla limpia los resultados parciales para que la confirmación multipágina siga siendo all-or-nothing
Propiedad de los modelos y thread safety
Cada engine de la DLL RapidOCR posee exactamente una instancia de modelos por toda su vida, y las llamadas a Recognize sobre ese engine 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 engine 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 derechos: no se carga modelo clasificador
Models.Threads := 4; // 1..64, topeado al conteo 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 tanto el conteo de threads intra-op como el inter-op de cada sesión ONNX, y la DLL lo clampea al conteo de procesadores activos. Dos threads compartiendo un engine no corren en paralelo; el segundo espera el lock. Esa espera no es un EnterCriticalSection a ciegas: el adapter llama a TryEnterCriticalSection cada 25 ms y chequea el token de cancelación y el deadline entre intentos, así que un request en cola todavía puede cancelarse o vencer. Si necesita paralelismo de verdad, cree un engine por worker y acepte que cada engine sostiene su propia copia de los modelos en memoria
El orden de desarmado lo fija el destructor del engine: HPDFRapidOCRDestroy libera primero la instancia de modelos, luego FreeLibrary descarga la DLL. Del 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 estaban armadas, esas sesiones se liberan antes de reportar el error, y el conteo de clases del diccionario se chequea contra la salida del modelo durante la inicialización y no en la primera página
¿Por qué una llamada de OCR nativa no se puede matar a mitad de inferencia?
Una llamada nativa de RapidOCR no se puede matar a mitad de inferencia porque corre en su thread, dentro de su proceso, en medio de una sesión de ONNX Runtime que no acepta interrupción. La cancelación en el adapter de DLL de HotPDF es entonces cooperativa: la DLL llama a un callback de aborto antes y después de la detección, después de la clasificación, y después de cada línea reconocida, y se detiene en el primer checkpoint donde el callback devuelve 0. Un Run de ONNX que ya arrancó terminará primero
Las alternativas son peores que esperar. TerminateThread dejaría el heap lock del CRT, el thread pool de ONNX Runtime y cualquier estado de OpenCV en la condición que les tocara, envenenando el resto del proceso. FreeLibrary mientras una llamada sigue ejecutándose descarga código que está en el stack. Ninguna de las dos se puede volver segura, así que el adapter jamás lo intenta. El deadline en TimeoutMilliseconds es por consecuencia un deadline cooperativo, y un deadline vencido sale a la luz como error de engine con un diagnóstico de timeout, mientras que un token cancelado sale como otlsCancelled:
// El token lo crea el caller y lo comparte con el thread de UI,
// que llama a Token.Cancel cuando el usuario aprieta Stop
Options := THPDFOCRTextLayerOptions.Default;
Options.CancellationToken := Token;
if not Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
case Info.Status of
otlsCancelled:
// devuelto en el siguiente límite de stage o de línea; documento sin cambios
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 adapters de proceso de HotPDF y la DLL in-process, y ningún lado gana en todas las filas:
- Costo de arranque: los adapters de Tesseract y de RapidOCR Python lanzan un proceso y cargan modelos por cada página; la DLL carga los modelos una vez por engine
- Detener: un proceso hijo puede terminarse de plano, y el worker de Python corre dentro de un Job Object kill-on-close así que su árbol de procesos completo se va con él; la DLL solo puede detenerse en límites de stage y de línea
- Contención de fallas: un crash en
tesseract.exehace fallar una página; un access violation dentro de la DLL se lleva su proceso puesto - Despliegue: los adapters de proceso necesitan un programa instalado o un entorno de Python; la DLL se necesita a sí misma, sus modelos y su diccionario, en el bitness de la aplicación
- Memoria: los adapters de proceso liberan todo cuando el hijo sale; un engine de DLL mantiene sus modelos residentes hasta que la última referencia de interfaz se libera
Para una aplicación de escritorio interactiva que hace OCR de a una página por vez, la responsividad de la DLL suele ganar. Para un server que ingiere escaneos no confiables las 24 horas, la frontera de proceso vale su costo 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 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 coincidir con la arquitectura objetivo además del modo CRT
El lado de los modelos tiene sus propios límites de compatibilidad. El detector es un detector de texto DB; el reconocedor acepta modelos CTC en layout NCHW con 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 vez de cargar a medias. El diccionario debe ser UTF-8 sin BOM, en exactamente el orden de caracteres del modelo, y su conteo de clases debe coincidir con la salida del modelo; los fines de línea CRLF se aceptan. El reconocimiento es offline: la DLL jamás descarga un modelo faltante
Referencia rápida
- Factory:
HPDFCreateRapidOCRDLLOCREngine(LibraryPath, ModelDirectory[, Options])enHPDFRapidOCRRecognition, disponible desde v2.774.0 en builds de 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 engine corre un reconocimiento a la vez; cree varios engines 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 por carácter, filtrada por
THPDFOCRTextLayerOptions.MinimumConfidence - La cancelación y
TimeoutMillisecondsson cooperativas; un run de ONNX en curso siempre termina - Empareje el bitness de la DLL con el de la aplicación, y el modo CRT de las librerías estáticas de ONNX Runtime y OpenCV con el de la DLL
- Escoja un perfil de idioma por engine con
THPDFRapidOCRDLLOptions.ForLanguage(v2.775.0); un engine no detecta idiomas por su cuenta
El adapter nativo de RapidOCR, los adapters de OCR por proceso, el renderer de páginas que los alimenta y el escritor de capas de texto Unicode invisibles 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 de Python en la máquina objetivo, el componente HotPDF Delphi PDF entrega el pipeline completo con solo la DLL y sus modelos por desplegar