Artículo técnico

RapidOCR en proceso con HotPDF: OCR de DLL nativa en Delphi

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

Secuencia de validación de la factoría RapidOCR DLL de HotPDF para HPDFCreateRapidOCRDLLOCREngine: las rutas y archivos de modelo deben existir, HPDFRapidOCRAbiVersion debe devolver 1, los exports requeridos deben resolverse, y HPDFRapidOCRCreate debe inicializar los modelos, con EArgumentException o EInvalidOperation lanzadas con ansias antes de que corra reconocimiento alguno, la segunda llevando el texto diagnóstico nativo
la validación es ansiosa a propósito: los problemas de configuración lanzan antes de que se inicialice cualquier modelo, así que una mala ruta o ABI jamás llega a un deadline de reconocimiento
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

ExportPapelCuándo lo resuelve el adaptador
HPDFRapidOCRAbiVersionDevuelve 1; cualquier otro valor se rechazaPrimero, antes que nada
HPDFRapidOCRCreateCarga los modelos de detección, clasificación opcional y reconocimiento, y el diccionarioEn la factoría
HPDFRapidOCRRecognizeEjecuta un bitmap y emite un callback por línea de textoEn la factoría
HPDFRapidOCRDestroyLibera la instancia de modelosEn la factoría
HPDFRapidOCRSetReadingDirectionOrden de filas de derecha a izquierda opcional, añadido en v2.775.0Solo 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

Pipeline RapidOCR DLL de HotPDF desde el bitmap a la capa de texto: el adaptador toma la página como instantánea BGR pf24bit de arriba abajo, la DLL rellena, detecta, ordena y reconoce recortes, entrega un callback por línea con texto UTF-8 prestado, caja y confianza, y el adaptador valida cada línea antes del commit de la capa de texto
los píxeles cruzan la ABI una vez como instantánea, las líneas vuelven de un callback en uno, y nada llega a la capa buscable hasta que pasa cada check

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 MaxTextCodeUnits de 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:

Trade-offs de los adaptadores OCR de HotPDF: los adaptadores de proceso arrancan un worker y cargan modelos en cada página pero pueden matarse y contienen crashes, mientras que la DLL RapidOCR en proceso carga los modelos una vez, se detiene solo en checkpoints cooperativos, comparte el espacio de direcciones y se despliega como DLL con sus modelos y diccionario
escoja por carga de trabajo: una app de escritorio página a página se beneficia de la DLL caliente, mientras que un servidor que ingiere escaneos no fiables debería pagar el muro de proceso
  • 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.exe hace 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]) en HPDFRapidOCRRecognition, disponible desde v2.774.0 en builds Delphi, C++Builder y FPC/Lazarus para Windows
  • Mantenga vivo el IHPDFOCREngine devuelto 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 TimeoutMilliseconds son 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