Artículo técnico

OCR de chino y multilingüe con RapidOCR en Delphi

HotPDF ejecuta OCR de chino y multilingüe en Delphi a través de su adaptador de DLL nativa RapidOCR: THPDFRapidOCRDLLOptions.ForLanguage mapea una etiqueta de idioma como 'zh-CN', 'zh-TW', 'ru' o 'ar' a un modelo de reconocimiento y un diccionario de caracteres a juego, y THotPDF.ApplyLoadedOCRTextLayer convierte las líneas reconocidas en una capa de texto Unicode invisible y buscable sobre páginas PDF escaneadas

Que una demo en escritura latina funcione es la parte fácil. Los fallos interesantes empiezan cuando usted cambia a chino tradicional o ruso y la salida se convierte en galimatías bien formado y con confianza, o cuando cada línea pierde silenciosamente su último carácter, o cuando una página árabe vuelve con sus cajas de texto en orden equivocado. Ninguno de estos lanza una excepción por sí mismo. Los presets de idioma añadidos en HotPDF v2.775.0 existen sobre todo para cerrar esas brechas, y las cuatro trampas de abajo merecen entenderse aunque usted nunca toque el código nativo, porque cada una explica un síntoma que de otro modo podría llevarse un día entero de persecución

¿Cómo elige ForLanguage un modelo y un diccionario?

THPDFRapidOCRDLLOptions.ForLanguage resuelve una etiqueta a uno de nueve perfiles y devuelve opciones que apuntan a <profile>/recognition.onnx y <profile>/dictionary.txt bajo su directorio de modelos, conservando el detector compartido, el clasificador de ángulo opcional y los valores por defecto de hilos, píxeles y timeout de THPDFRapidOCRDLLOptions.Default. El método pasa la etiqueta a minúsculas, convierte guiones bajos en guiones y recorta los espacios de los extremos, así que 'zh_TW', 'ZH-tw' y ' zh-tw ' aterrizan en el mismo perfil. Los alias son una lista explícita y no un match por prefijo: 'zh-Hant-TW' se acepta porque está listado, mientras que una variante regional arbitraria que no esté listada lanza EArgumentException antes de cargar modelo alguno

Resolución de perfiles ForLanguage en HotPDF para THPDFRapidOCRDLLOptions: etiquetas como zh_TW, ZH-tw y zh-TW se normalizan y cotejan contra nueve perfiles listados, cada uno fijando un modelo de reconocimiento y un diccionario que siempre se fijan juntos, mientras que una etiqueta no listada lanza EArgumentException antes de cargar cualquier modelo
una etiqueta escoge un par modelo-diccionario fijado; el detector, el clasificador y los presupuestos siguen compartidos, y una etiqueta desconocida falla rápido en lugar de cargar nada
PerfilIdiomasEtiquetas de ejemploModelo fijado
chChino simplificado e inglészh, zh-CN, zh-Hans, chi_simPP-OCRv4
chinese_chtChino tradicionalzh-TW, zh-HK, zh-Hant, chi_traPP-OCRv3
enInglésen, en-US, en-GB, engPP-OCRv4
latinFrancés, alemán, español, portugués, italiano, neerlandés, turcofr, de, es-419, pt-BR, trPP-OCRv3
japanJaponésja, ja-JP, jpnPP-OCRv4
koreanCoreanoko, ko-KR, korPP-OCRv4
cyrillicRuso, ucraniano, búlgaro, bielorrusoru, ru-RU, uk, bgPP-OCRv3
arabicÁrabe, persa, urduar, ar-SA, fa, urPP-OCRv4
devanagariHindi, maratí, nepalíhi, mr, nePP-OCRv4

El adaptador en sí jamás descarga nada. Usted provisiona los archivos una vez con el ayudante incluido, por ejemplo tools/Install-RapidOCRModels.ps1 -Destination C:/OCR/models -Language ch,chinese_cht,cyrillic (o -Language All para los nueve perfiles), y el ayudante coloca un detector y un clasificador compartidos en los nombres de archivo de raíz que Default espera. Después de eso, un escaneo en chino simplificado se vuelve buscable con unas líneas. La fontanería del motor es la misma costura IHPDFOCREngine descrita en el artículo sobre la DLL RapidOCR en proceso y su frontera ABI, así que este se queda centrado en los idiomas

uses
  SysUtils, HPDFDoc, HPDFRapidOCRRecognition;

procedure MakeChineseScanSearchable(const SourceFile, TargetFile: string);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Models: THPDFRapidOCRDLLOptions;
  Layer: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // ch/recognition.onnx + ch/dictionary.txt, detector y clasificador compartidos
  Models := THPDFRapidOCRDLLOptions.ForLanguage('zh-CN');
  Engine := HPDFCreateRapidOCRDLLOCREngine(
    'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models', Models);
  Doc := THotPDF.Create(nil);
  try
    Doc.AutoLaunch := False;
    if Doc.LoadFromFile(SourceFile) < 1 then
      raise Exception.Create('Cannot load ' + SourceFile);
    Layer := 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, Layer, Info) then
      raise Exception.Create(string(Info.Diagnostic));
    Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
      ' lines, ', Info.UniqueScalarCount, ' distinct characters');
    Doc.SaveLoadedDocument(TargetFile);
  finally
    Doc.Free;
  end;
end;

Dos detalles de esa salida merecen una nota. El pipeline nativo devuelve un resultado por línea de texto detectada, no por palabra, así que AcceptedWordCount cuenta líneas aquí, y MinimumConfidence se compara contra la confianza media de caracteres de la línea entera: una línea que promedia 0.45 se descarta como unidad. UniqueScalarCount reporta cuántos escalares Unicode distintos tuvo que mapear la capa de texto a su fuente y su tabla ToUnicode, un sanity check útil de que el texto CJK llegó de verdad en lugar de un puñado de fallbacks latinos. Mantenga viva la interfaz del motor entre documentos, porque la inicialización de modelos ocurre en la factoría y es el paso caro

¿Por qué cambiar solo el modelo de reconocimiento produce basura?

Un modelo de reconocimiento CTC jamás emite caracteres, solo índices de clase, y el diccionario es lo único que convierte el índice 1.204 en un glifo. Cambie ch/recognition.onnx por cyrillic/recognition.onnx conservando el diccionario chino, y el modelo emitirá encantado índices cirílicos válidos que el diccionario viejo traduce a caracteres Han al azar. El resultado parece texto, pasa la validación UTF-8, y es buscable para exactamente nada. Por eso ForLanguage fija siempre RecognitionModel y CharacterDictionary juntos, y por eso las opciones construidas a mano nunca deberían cambiar uno sin el otro

El check de seguridad obvio, comparar el tamaño del diccionario con el ancho de salida del modelo, es necesario pero no suficiente. Dos diccionarios pueden tener el mismo número de entradas en orden distinto, y un off-by-one en el orden desplaza cada carácter un punto de código. HotPDF comprueba por eso en dos etapas cuando la factoría inicializa el modelo. Primera: el recuento de clases de salida debe igualar las entradas del diccionario más dos. Segunda: cuando el archivo ONNX incrusta una lista de metadatos character, cada entrada del diccionario se coteja con ella en orden, y un desajuste hace fallar la inicialización con EInvalidOperation y un diagnóstico nativo en lugar de producir basura plausible más tarde

El "más dos" viene de la disposición de clases. La clase 0 es el blank CTC, las clases 1 a N son las líneas del diccionario en orden de archivo, y la clase final es un espacio. Algunos diccionarios llevan además su propia entrada de espacio, y esa línea debe conservarse exactamente como está. Aquí es donde un Trim bienintencionado hace daño de verdad: convierte una entrada de un solo espacio en una cadena vacía y desplaza o rompe la tabla. La única normalización segura es retirar un retorno de carro final, de modo que un diccionario guardado con finales de línea CRLF carga correctamente, mientras que una marca de orden de bytes UTF-8, una línea vacía o una entrada que contenga un tabulador se rechazan. El esqueleto de abajo muestra la disposición en Pascal; es código explicativo, no una API de HotPDF

Disposición de la tabla de clases CTC en HotPDF para diccionarios RapidOCR: la clase 0 es el blank, las clases 1 a N son las líneas del diccionario en orden de archivo con cualquier entrada de espacio solitario conservada, y la clase final es un espacio, lo que da N más 2 clases de salida que la factoría verifica contra el modelo, metadatos incluidos
el diccionario es lo único que convierte índices de clase en caracteres, así que su tamaño, su orden y su entrada de espacio se verifican antes de reconocer una sola página
// Solo ilustración: la tabla de clases que espera un reconocedor CTC
uses
  SysUtils, IOUtils;

function BuildCTCClassTable(const FileName: string): TArray<string>;
var
  Text, Entry: string;
  Lines: TArray<string>;
  I, Last: Integer;
begin
  Text := TEncoding.UTF8.GetString(TFile.ReadAllBytes(FileName));
  if (Text <> '') and (Text[1] = #$FEFF) then
    raise EArgumentException.Create('Dictionary must be UTF-8 without a BOM');
  Lines := Text.Split([#10]);
  Last := High(Lines);
  if (Last >= 0) and (Lines[Last] = '') then
    Dec(Last);                                   // salto de línea al final del archivo
  SetLength(Result, Last + 3);
  Result[0] := '';                               // clase 0: blank CTC
  for I := 0 to Last do
  begin
    Entry := Lines[I];
    if (Entry <> '') and (Entry[Length(Entry)] = #13) then
      SetLength(Entry, Length(Entry) - 1);       // CRLF: retire solo el CR
    if (Entry = '') or (Pos(#9, Entry) > 0) then
      raise EArgumentException.Create('Invalid dictionary entry');
    Result[I + 1] := Entry;                      // nunca Trim: ' ' es una clase
  end;
  Result[Last + 2] := ' ';                       // clase final: espacio
  // Length(Result) debe igualar el recuento de clases de salida del modelo
end;

¿Qué hace realmente la decodificación CTC greedy?

La decodificación CTC greedy escoge la clase de mayor puntuación en cada time step, colapsa las repeticiones consecutivas en un carácter y descarta el blank; el blank es lo que permite que letras genuinamente dobladas sobrevivan. Un modelo de reconocimiento mira una línea de texto como una secuencia de rebanadas verticales estrechas, y para cada rebanada, o time step, emite una probabilidad por cada clase. Una línea con AA中 podría producir la secuencia argmax A A blank A 中 space. Colapsar los dos primeros pasos A da un solo A, el blank lo separa de la siguiente A, y el resultado es AA中 con el espacio final intacto. Sin la regla del blank, book y bok serían indistinguibles

Recorrido del GreedyCTCDecode de HotPDF: seis time steps votan clases argmax A, A, blank, A, un carácter Han y espacio, las repeticiones consecutivas colapsan, el blank reinicia la guardia de repetición así que una letra genuinamente doblada sobrevive, y tres bugs de frontera descartan en silencio el espaciado de palabras, el último carácter o los caracteres doblados
el decodificador son una docena de líneas y cada frontera importa: incluya la última clase, incluya el último step, y deje que solo un blank separe repeticiones

Como el decodificador son apenas una docena de líneas, es fácil errarle a las fronteras, y los fallos son silenciosos. Si el bucle argmax interno se queda una clase corto, la clase espacio jamás puede ganar y cada línea vuelve sin espaciado de palabras, lo que destroza la búsqueda de frases en páginas inglesas y latinas. Si el bucle externo se queda un time step corto, el último carácter de cada línea desaparece, lo que en una línea corta puede ser un tercio del texto. Y si la guardia de repetición no la reinicia un blank, caracteres doblados como ll o reduplicaciones chinas como 谢谢 colapsan en uno. El decodificador de HotPDF incluye la última clase y el último time step, conserva las repeticiones separadas por blank, y además rechaza puntuaciones que no sean finitas o caigan fuera de 0 a 1, y cualquier recuento de clases que no case con el diccionario. Aquí está la misma lógica como ilustración Pascal

// Solo ilustración: decodificación CTC greedy con fronteras correctas.
// Scores guarda Steps * Classes probabilidades, una fila por time step
function GreedyCTCDecode(const Scores: array of Single;
  Steps, Classes: Integer; const Characters: array of string): string;
var
  Step, C, Best, Previous: Integer;
  BestScore: Single;
begin
  if (Classes < 3) or (Length(Characters) <> Classes) or
    (Length(Scores) <> Steps * Classes) then
    raise EArgumentException.Create('Model output does not match the dictionary');
  Result := '';
  Previous := 0;                            // la clase 0 es el blank CTC
  for Step := 0 to Steps - 1 do             // incluya el último time step
  begin
    Best := 0;
    BestScore := Scores[Step * Classes];
    for C := 1 to Classes - 1 do            // incluya la última clase (espacio)
      if Scores[Step * Classes + C] > BestScore then
      begin
        Best := C;
        BestScore := Scores[Step * Classes + C];
      end;
    if (Best <> 0) and (Best <> Previous) then
      Result := Result + Characters[Best];
    Previous := Best;                       // un blank reinicia la guardia de repetición
  end;
end;

La decodificación greedy no es la estrategia CTC más precisa disponible; el beam search con un modelo de lenguaje puede arreglar alguna rebanada ambigua. Para documentos impresos a 300 DPI el resultado greedy suele ser lo que el modelo tiene que ofrecer, y el decodificador no es el sitio para compensar debilidades del modelo. El modelo latino PP-OCRv3, por ejemplo, puede leer ñ como n incluso sobre entrada limpia. HotPDF no lo maquilla con sustituciones de caracteres de postprocesado, porque una tabla de sustitución que arregla el español rompe otra cosa, y un carácter equivocado en una capa buscable es peor que un fallo honesto

¿Cómo ordena HotPDF las líneas de texto, incluido el árabe de derecha a izquierda?

HotPDF ordena las cajas de texto detectadas de arriba abajo, agrupa cajas en una fila cuando se solapan verticalmente al menos la mitad de la altura de la caja menor, y ordena cada fila de izquierda a derecha, o de derecha a izquierda cuando RightToLeft está activo; los caracteres dentro de cada línea reconocida jamás se invierten. El agrupamiento importa porque un detector parte a menudo una línea visual en varias cajas, por ejemplo una etiqueta y un valor separados por un hueco ancho, y una ordenación pura por coordenada superior los entrelazaría con la línea vecina cada vez que sus bordes superiores difieran un píxel o dos

El preset árabe fija RightToLeft := True, lo que le dice a la DLL que ordene las cajas de cada fila por su borde derecho, desde el margen derecho hacia dentro. Ese es el efecto entero. El texto que el modelo devuelve para una línea ya está en orden lógico Unicode, el orden en que un lector árabe lo lee y lo teclea, y ese es también el orden que la extracción de texto PDF y la búsqueda esperan. Invertir mecánicamente la cadena para que "parezca bien" en un depurador rompería la búsqueda, el copiar y pegar y los lectores de pantalla. La visualización bidireccional y el shaping de glifos son trabajo del visor

Un motor sirve un perfil de idioma. No hay detección automática de escritura, así que un documento que mezcle escrituras necesita un motor por perfil, aplicado a las páginas que lo usan. Como ApplyLoadedOCRTextLayer toma una lista de páginas explícita y compromete cada llamada como su propia transacción todo-o-nada, eso es directo

uses
  SysUtils, HPDFDoc, HPDFRapidOCRRecognition;

function CreateRapidEngine(const Tag: string): IHPDFOCREngine;
var
  Models: THPDFRapidOCRDLLOptions;
begin
  // lanza EArgumentException para una etiqueta desconocida, antes de cargar modelo alguno
  Models := THPDFRapidOCRDLLOptions.ForLanguage(Tag);
  Models.MaxPixels := 33554432;          // espacio para páginas A3 a 300 DPI
  Result := HPDFCreateRapidOCRDLLOCREngine(
    'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models', Models);
end;

procedure OCRMixedArchive(Doc: THotPDF);
var
  Chinese, Arabic: IHPDFOCREngine;
  Layer: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  Chinese := CreateRapidEngine('zh-TW');  // perfil chinese_cht
  Arabic := CreateRapidEngine('ar-SA');   // perfil arabic, RightToLeft = True
  Layer := THPDFOCRTextLayerOptions.Default;
  if not Doc.ApplyLoadedOCRTextLayer([0, 1, 2], Chinese, Layer, Info) then
    raise Exception.Create(string(Info.Diagnostic));
  if not Doc.ApplyLoadedOCRTextLayer([3], Arabic, Layer, Info) then
    raise Exception.Create(string(Info.Diagnostic));
end;

La línea de MaxPixels está ahí por algo. Las opciones de la DLL toman por defecto 16.777.216 píxeles por petición, lo que cubre A4 y US Letter a 300 DPI de sobra, pero una página A3 a 300 DPI son unos 3508 por 4961 píxeles, aproximadamente 17,4 millones, y la petición se rechaza por exceder presupuesto. Suba MaxPixels (el techo es 67.108.864) o baje THPDFOCRTextLayerOptions.DPI para formatos grandes. La ordenación de derecha a izquierda usa el export opcional HPDFRapidOCRSetReadingDirection de la versión de ABI 1; el adaptador solo lo exige cuando RightToLeft está fijado, así que una DLL más vieja sigue sirviendo idiomas de izquierda a derecha y falla en la creación del motor con una EArgumentException que nombra el export ausente para el árabe

¿Por qué los modelos OCR más nuevos fallan al cargar?

La DLL RapidOCR de HotPDF enlaza un ONNX Runtime 1.14 estático, que no puede leer modelos guardados con la versión de IR 10 de ONNX, y los exports más nuevos como los modelos PP-OCRv5 pueden exigir un runtime más nuevo que ese; semejante modelo falla en la creación del motor con un diagnóstico nativo. Esa restricción es la razón por la que los packs de idiomas están fijados a pares concretos de reconocedor y diccionario PP-OCRv3 y PP-OCRv4 en lugar de "el último", y por qué la tabla de arriba mezcla las dos generaciones: cada par fijado es uno que carga y verifica bajo ese runtime

El instalador aplica el emparejamiento. Cada archivo de su manifiesto lleva un hash SHA256, un archivo existente con un hash distinto detiene la instalación en lugar de sobrescribirse, y cada descarga aterriza bajo un nombre temporal y solo se mueve a su sitio cuando su hash casa. Eso protege contra la versión silenciosa del problema del diccionario: alguien suelta a mano un recognition.onnx más nuevo en una carpeta de perfil, el recuento de clases casualmente casa, y nada falla hasta que un cliente reporta que la búsqueda no encuentra palabras que se ven a simple vista. En ejecución el adaptador se queda offline y jamás busca un modelo que falte. El reconocedor valida además la forma del modelo al cargar, aceptando entrada NCHW con altura fija de 32 o 48 píxeles o altura dinámica, que ejecuta a 48

Si usted necesita una escritura que ninguno de los nueve perfiles cubre, aún puede apuntar RecognitionModel y CharacterDictionary a sus propios archivos. Los mismos checks aplican, que es la gracia: un par mal emparejado falla en la inicialización, no en el archivo de su cliente. Para páginas donde ningún perfil RapidOCR encaja, el adaptador Tesseract para PDF buscable se enchufa a la misma llamada ApplyLoadedOCRTextLayer, y para formularios ASCII impresos por máquina el motor OCR de plantillas incorporado no necesita modelos para nada

Referencia rápida: lista de comprobación RapidOCR multilingüe

  • Cree las opciones con THPDFRapidOCRDLLOptions.ForLanguage y trate EArgumentException como una etiqueta no soportada, no como un fallo de runtime
  • Cambie RecognitionModel y CharacterDictionary juntos, jamás uno solo; recuentos de clases iguales no prueban orden de caracteres igual
  • Guarde los diccionarios como UTF-8 sin BOM, nunca recorte entradas, y espere que el modelo tenga N + 2 clases: blank, N entradas, espacio
  • Un decodificador CTC a medida debe cubrir la última clase y el último time step y conservar las repeticiones separadas por un blank
  • Use un motor por perfil de idioma y pase listas de páginas explícitas para documentos de escritura mixta
  • RightToLeft cambia solo el orden de cajas; el texto reconocido permanece en orden lógico Unicode
  • Instale los modelos con Install-RapidOCRModels.ps1 para que los pins SHA256 sostengan el emparejamiento modelo-diccionario; fije UseAngleClassifier := False si instaló con -SkipClassifier
  • Suba MaxPixels por encima del valor por defecto de 16.777.216 antes de correr páginas A3 o mayores a 300 DPI

Los presets de idioma RapidOCR, el adaptador de DLL nativa y el pipeline de capas de texto OCR son parte del HotPDF Delphi PDF Component para Delphi, C++Builder y FPC/Lazarus para Windows, a partir de v2.775.0 para los perfiles multilingües