HotPDF hace OCR en chino y multilingüe en Delphi a través de su adapter nativo de la DLL RapidOCR: THPDFRapidOCRDLLOptions.ForLanguage mapea un tag 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
Hacer funcionar una demo en escritura latina es la parte fácil. Las fallas interesantes empiezan cuando usted cambia a chino tradicional o ruso y la salida se convierte en un disparate seguro de sí mismo y bien formado, o cuando cada línea pierde en silencio su último carácter, o cuando una página árabe vuelve con sus cajas de texto en el orden equivocado. Ninguno de estos levanta una excepción por sí solo. Los presets de idioma agregados en HotPDF v2.775.0 existen sobre todo para cerrar esas brechas, y las cuatro trampas de abajo vale la pena entenderlas aunque jamás toque el código nativo, porque cada una explica un síntoma por el que de otro modo gastaría un día cazando
¿Cómo elige ForLanguage un modelo y un diccionario?
THPDFRapidOCRDLLOptions.ForLanguage resuelve un tag a uno de nueve perfiles y devuelve opciones que apuntan a <profile>/recognition.onnx y <profile>/dictionary.txt debajo de su directorio de modelos, conservando el detector compartido, el clasificador de ángulos opcional, y los defaults de threads, píxeles y timeout de THPDFRapidOCRDLLOptions.Default. El método pasa el tag a minúsculas, convierte underscores en guiones y recorta espacios a los costados, así que 'zh_TW', 'ZH-tw' y ' zh-tw ' todos 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 levanta EArgumentException antes de que se cargue cualquier modelo
| Perfil | Idiomas | Tags de ejemplo | Modelo fijado |
|---|---|---|---|
ch | Chino simplificado e inglés | zh, zh-CN, zh-Hans, chi_sim | PP-OCRv4 |
chinese_cht | Chino tradicional | zh-TW, zh-HK, zh-Hant, chi_tra | PP-OCRv3 |
en | Inglés | en, en-US, en-GB, eng | PP-OCRv4 |
latin | Francés, alemán, español, portugués, italiano, neerlandés, turco | fr, de, es-419, pt-BR, tr | PP-OCRv3 |
japan | Japonés | ja, ja-JP, jpn | PP-OCRv4 |
korean | Coreano | ko, ko-KR, kor | PP-OCRv4 |
cyrillic | Ruso, ucraniano, búlgaro, bielorruso | ru, ru-RU, uk, bg | PP-OCRv3 |
arabic | Árabe, persa, urdu | ar, ar-SA, fa, ur | PP-OCRv4 |
devanagari | Hindi, marati, nepalí | hi, mr, ne | PP-OCRv4 |
El adapter en sí jamás descarga nada. Usted provisiona los archivos una vez con el helper 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 helper coloca un detector y un clasificador compartidos en los nombres de archivo de la raíz que Default espera. Después de eso, un escaneo en chino simplificado se vuelve buscable con unas pocas líneas. El plomería del engine es la misma costura IHPDFOCREngine descrita en el artículo sobre la DLL RapidOCR in-process y su frontera de ABI, así que este se queda enfocado 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 por carácter 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 a su tabla ToUnicode, un sanity check útil de que el texto CJK de verdad llegó en vez de un puñado de fallbacks latinos. Mantenga viva la interfaz del engine entre documentos, porque la inicialización de modelos ocurre en la factory 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 pero conserve el diccionario chino, y el modelo felizmente emitirá í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 siempre asigna RecognitionModel y CharacterDictionary juntos, y por qué las opciones armadas a mano jamás deberían cambiar uno sin el otro
El chequeo 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 otro orden, y un off-by-one en el orden corre cada carácter un code point. HotPDF por eso chequea en dos etapas cuando la factory inicializa el modelo. Primero, el conteo de clases de salida debe igualar las entradas del diccionario más dos. Segundo, cuando el archivo ONNX incrusta una lista de metadatos character, cada entrada del diccionario se compara con ella en orden, y un desajuste hace fallar la inicialización con EInvalidOperation y un diagnóstico nativo en vez de producir basura plausible más tarde
El "más dos" sale del layout de clases. La clase 0 es el blank de 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 también traen su propia entrada de espacio, y esa línea debe conservarse exactamente como está. Aquí es donde un Trim bien intencionado hace daño real: convierte una entrada de un solo espacio en un string vacío y corre o rompe la tabla. La única normalización segura es quitar un retorno de carro final, así un diccionario guardado con fines 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 tab se rechazan. El boceto de abajo muestra el layout en Pascal; es código explicativo, no una API de HotPDF
// 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); // newline al final del archivo
SetLength(Result, Last + 3);
Result[0] := ''; // clase 0: blank de 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: suelte 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 conteo de clases de salida del modelo
end;
¿Qué hace en realidad el decodificado CTC greedy?
El decodificado CTC greedy toma la clase de mayor puntaje en cada time step, colapsa las repeticiones consecutivas en un carácter, y descarta la clase blank; el blank es lo que permite que letras genuinamente dobles sobrevivan. Un modelo de reconocimiento mira una línea de texto como una secuencia de tajadas verticales angostas, y por cada tajada, o time step, emite una probabilidad para cada clase. Una línea que contiene AA中 podría producir la secuencia argmax A A blank A 中 space. Colapsar los primeros dos steps A da una A, el blank la 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
Como el decoder es solo una docena de líneas, es fácil equivocarse en las fronteras, y las fallas son silenciosas. Si el loop interno de argmax se detiene una clase antes, la clase espacio jamás puede ganar y cada línea vuelve sin espacios entre palabras, lo que destroza la búsqueda de frases en páginas inglesas y latinas. Si el loop externo se detiene un time step antes, el último carácter de cada línea desaparece, lo que para una línea corta puede ser un tercio del texto. Y si la guardia de repetición no la resetea un blank, caracteres dobles como ll o reduplicaciones chinas como 谢谢 colapsan en uno. El decoder de HotPDF incluye la última clase y el último time step, conserva las repeticiones separadas por blank, y además rechaza puntajes que no sean finitos o que caigan fuera de 0 a 1, y cualquier conteo de clases que no coincida con el diccionario. Aquí está la misma lógica como ilustración en Pascal
// Solo ilustración: decodificado CTC greedy con límites correctos.
// 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; // clase 0 es el blank de 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 (el 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 resetea la guardia de repetición
end;
end;
El decodificado greedy no es la estrategia CTC más precisa disponible; beam search con un modelo de lenguaje puede arreglar algunas tajadas ambiguas. Para documentos impresos a 300 DPI el resultado greedy suele ser lo que el modelo tiene para ofrecer, y el decoder no es el lugar para compensar debilidades del modelo. El modelo latino PP-OCRv3, por ejemplo, puede leer ñ como n incluso en input limpio. HotPDF no lo disimula con reemplazos de caracteres de post-procesamiento, 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 miss 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 a menudo parte una línea visual en varias cajas, por ejemplo una etiqueta y un valor separados por una brecha ancha, y un ordenamiento puro por coordenada superior los intercalaría con la línea vecina cada vez que sus topes difieran en un píxel o dos
El preset árabe asigna 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 adentro. Ese es todo el efecto. El texto que el modelo devuelve para una línea ya está en orden lógico Unicode, el orden en que un lector de árabe lo lee y lo tipea, y ese es también el orden que la extracción de texto PDF y la búsqueda esperan. Invertir mecánicamente el string para que "se vea bien" en un debugger rompería la búsqueda, el copiado y el pegado, y los lectores de pantalla. El display bidireccional y el shaping de glifos son asunto del visor
Un engine sirve un perfil de idioma. No hay detección automática de escritura, así que un documento que mezcla escrituras necesita un engine por perfil, aplicado a las páginas que lo usan. Como ApplyLoadedOCRTextLayer recibe una lista de páginas explícita y confirma cada llamada como su propia transacción all-or-nothing, eso es directo
uses
SysUtils, HPDFDoc, HPDFRapidOCRRecognition;
function CreateRapidEngine(const Tag: string): IHPDFOCREngine;
var
Models: THPDFRapidOCRDLLOptions;
begin
// levanta EArgumentException para un tag desconocido, 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 hacen default a 16,777,216 píxeles por request, lo que cubre A4 y US Letter a 300 DPI con holgura, pero una página A3 a 300 DPI ronda los 3508 por 4961 píxeles, cerca de 17.4 millones, y el request se rechaza por pasarse del presupuesto. Suba MaxPixels (el techo es 67,108,864) o baje el THPDFOCRTextLayerOptions.DPI para formatos grandes. El ordenamiento de derecha a izquierda usa el export opcional HPDFRapidOCRSetReadingDirection de la versión de ABI 1; el adapter solo lo exige cuando RightToLeft está activo, así que una DLL más vieja sigue sirviendo los idiomas de izquierda a derecha y falla en la creación del engine con un EArgumentException que nombra el export faltante para el árabe
¿Por qué los modelos OCR más nuevos fallan al cargar?
La DLL RapidOCR de HotPDF enlaza un ONNX Runtime estático 1.14, que no puede leer modelos guardados con la versión de IR 10 de ONNX, y exports más nuevos como los modelos PP-OCRv5 pueden exigir un runtime más nuevo que ese; tal modelo falla en la creación del engine con un diagnóstico nativo. Esa restricción es la razón por la que los paquetes de idioma están fijados a pares reconocedor y diccionario PP-OCRv3 y PP-OCRv4 específicos en vez de "el más reciente", 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 la pareja. Cada archivo de su manifiesto lleva un hash SHA256, un archivo existente con un hash distinto detiene la instalación en vez de sobrescribirse, y cada descarga aterriza bajo un nombre temporal y solo se mueve a su lugar después de que su hash coincide. Eso protege contra la versión silenciosa del problema del diccionario: alguien deja a mano un recognition.onnx más nuevo en un folder de perfil, el conteo de clases casualmente coincide, y nada falla hasta que un cliente reporta que la búsqueda no encuentra palabras que se ven clarísimas. En runtime el adapter se queda offline y jamás va por un modelo faltante. El reconocedor además valida la forma del modelo al cargar, aceptando input NCHW con altura fija de 32 o 48 píxeles o altura dinámica, que corre a 48
Si necesita una escritura que ninguno de los nueve perfiles cubre, todavía puede apuntar RecognitionModel y CharacterDictionary a sus propios archivos. Los mismos chequeos aplican, que es justo el punto: un par desparejo falla en la inicialización, no en el archivo de su cliente. Para páginas donde ningún perfil de RapidOCR encaja, el adapter de Tesseract para PDF buscable se enchufa a la misma llamada ApplyLoadedOCRTextLayer, y para formularios ASCII de máquina el engine de OCR por matching de plantillas integrado no necesita modelos para nada
Referencia rápida: checklist de RapidOCR multilingüe
- Cree las opciones con
THPDFRapidOCRDLLOptions.ForLanguagey trateEArgumentExceptioncomo un tag no soportado, no como una falla de runtime - Cambie
RecognitionModelyCharacterDictionaryjuntos, jamás uno solo; conteos de clases iguales no prueban un orden de caracteres igual - Deje los diccionarios como UTF-8 sin BOM, nunca recorte entradas, y espere que el modelo tenga N + 2 clases: blank, N entradas, espacio
- Un decoder CTC custom debe cubrir la última clase y el último time step, y conservar las repeticiones separadas por un blank
- Use un engine por perfil de idioma y pase listas de páginas explícitas para documentos de escrituras mezcladas
RightToLeftcambia solo el orden de las cajas; el texto reconocido se queda en orden lógico Unicode- Instale los modelos con
Install-RapidOCRModels.ps1así los pines SHA256 sostienen la pareja modelo-diccionario; asigneUseAngleClassifier := Falsesi instaló con-SkipClassifier - Suba
MaxPixelspor encima del default de 16,777,216 antes de correr páginas A3 o mayores a 300 DPI
Los presets de idioma RapidOCR, el adapter de la DLL nativa y el pipeline de la capa de texto OCR son parte del HotPDF Delphi PDF Component para Delphi, C++Builder y FPC/Lazarus para Windows, arrancando con v2.775.0 para los perfiles multilingües