O HotPDF faz OCR de chinês e multilíngue em Delphi através do adapter nativo de DLL RapidOCR dele: o THPDFRapidOCRDLLOptions.ForLanguage mapeia uma tag de idioma como 'zh-CN', 'zh-TW', 'ru' ou 'ar' para um par matched de recognition model e character dictionary, e o THotPDF.ApplyLoadedOCRTextLayer transforma as linhas reconhecidas numa camada de texto Unicode invisível e pesquisável em páginas de PDF escaneadas
Fazer uma demo em script latino funcionar é a parte fácil. As falhas interessantes começam quando você troca para chinês tradicional ou russo e o output vira nonsense confiante e bem formado, ou quando toda linha perde silenciosamente o último caractere, ou quando uma página em árabe volta com as text boxes delas na ordem errada. Nada disso levanta exceção por conta própria. Os presets de idioma acrescentados no HotPDF v2.775.0 existem sobretudo para fechar essas brechas, e as quatro armadilhas abaixo valem a pena entender mesmo que você nunca toque no código nativo, porque cada uma explica um sintoma pelo qual você de outro modo gastaria um dia caçando
Como o ForLanguage escolhe um model e um dictionary?
O THPDFRapidOCRDLLOptions.ForLanguage resolve uma tag para um de nove perfis e retorna options que apontam para <profile>/recognition.onnx e <profile>/dictionary.txt abaixo do seu model directory, mantendo o detector compartilhado, o angle classifier opcional, e os defaults de thread, pixel e timeout do THPDFRapidOCRDLLOptions.Default. O método converte a tag para minúsculas, vira underscores em hyphens e apara whitespace nas pontas, então 'zh_TW', 'ZH-tw' e ' zh-tw ' todos pousam no mesmo perfil. Aliases são uma lista explícita em vez de um prefix match: 'zh-Hant-TW' é aceito porque está listado, enquanto uma variante regional arbitrária que não esteja levanta EArgumentException antes de qualquer model ser carregado
| Perfil | Idiomas | Tags de exemplo | Model pregado |
|---|---|---|---|
ch | Chinês simplificado e inglês | zh, zh-CN, zh-Hans, chi_sim | PP-OCRv4 |
chinese_cht | Chinês 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ão, espanhol, português, italiano, holandê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 | Russo, ucraniano, búlgaro, bielorrusso | ru, ru-RU, uk, bg | PP-OCRv3 |
arabic | Árabe, persa, urdu | ar, ar-SA, fa, ur | PP-OCRv4 |
devanagari | Hindi, marata, nepalês | hi, mr, ne | PP-OCRv4 |
O adapter em si nunca baixa nada. Você provisiona os arquivos uma vez com o helper embarcado, por exemplo tools/Install-RapidOCRModels.ps1 -Destination C:/OCR/models -Language ch,chinese_cht,cyrillic (ou -Language All para todos os nove perfis), e o helper coloca um detector e um classifier compartilhados nos filenames na raiz que o Default espera. Depois disso, um scan em chinês simplificado fica pesquisável com umas poucas linhas. A parte de plumbing da engine é a mesma emenda IHPDFOCREngine descrita no artigo sobre a DLL RapidOCR in-process e a fronteira de ABI dela, então este fica focado nos 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 e classifier compartilhados
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
// uma lista de páginas vazia significa todas as páginas; páginas que já têm texto são puladas
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;
Dois detalhes nesse output merecem nota. O pipeline nativo retorna um resultado por linha de texto detectada, não por palavra, então o AcceptedWordCount conta linhas aqui, e o MinimumConfidence é comparado contra a confidence média de caractere da linha inteira: uma linha com média 0.45 é descartada como uma unidade. O UniqueScalarCount reporta quantos Unicode scalars distintos a camada de texto teve que mapear na fonte e na tabela ToUnicode dela, um sanity check útil de que texto CJK de fato chegou em vez de um punhado de fallbacks latinos. Mantenha a interface da engine viva entre documentos, porque a inicialização dos models acontece na factory e é o passo caro
Por que trocar só o recognition model produz lixo?
Um recognition model CTC nunca produz caracteres, só índices de classe, e o dictionary é a única coisa que transforma o índice 1.204 num glyph. Troque o ch/recognition.onnx pelo cyrillic/recognition.onnx mas mantenha o dictionary chinês, e o model vai emitir felicemente índices de cirílico válidos que o dictionary velho traduz em caracteres Han aleatórios. O resultado parece texto, passa validação UTF-8, e é pesquisável por exatamente nada. É por isso que o ForLanguage sempre seta RecognitionModel e CharacterDictionary juntos, e por que options construídas à mão nunca devem mudar um sem o outro
A checagem de segurança óbvia, comparar o tamanho do dictionary com a largura de output do model, é necessária mas não suficiente. Dois dictionaries podem ter o mesmo número de entradas numa ordem diferente, e um off-by-one na ordem desloca cada caractere por um code point. O HotPDF portanto confere em dois estágios quando a factory inicializa o model. Primeiro, a contagem de classes de output precisa igualar as entradas do dictionary mais duas. Segundo, quando o arquivo ONNX embute uma lista de metadata character, cada entrada do dictionary é comparada com ela em ordem, e um desencontro falha a inicialização com EInvalidOperation e um diagnóstico nativo em vez de produzir lixo plausível mais tarde
O "mais duas" vem do layout de classes. A classe 0 é o blank CTC, as classes 1 a N são as linhas do dictionary em ordem de arquivo, e a classe final é um espaço. Alguns dictionaries também carregam a própria entrada de espaço deles, e essa linha precisa ser mantida exatamente como é. É aqui que um Trim bem-intencionado causa dano real: ele transforma uma entrada de espaço único numa string vazia e desloca ou quebra a tabela. A única normalização segura é remover um carriage return final, então um dictionary salvo com line endings CRLF carrega corretamente, enquanto um byte order mark UTF-8, uma linha vazia, ou uma entrada contendo um tab é rejeitada. O esqueleto abaixo mostra o layout em Pascal; é código explicativo, não uma API do HotPDF
// Só ilustração: a class table que um recognizer CTC espera
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 no fim do arquivo
SetLength(Result, Last + 3);
Result[0] := ''; // classe 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: descarte só o CR
if (Entry = '') or (Pos(#9, Entry) > 0) then
raise EArgumentException.Create('Invalid dictionary entry');
Result[I + 1] := Entry; // nunca Trim: ' ' é uma classe
end;
Result[Last + 2] := ' '; // classe final: espaço
// Length(Result) precisa igualar a contagem de classes do output do model
end;
O que o greedy CTC decoding realmente faz?
O greedy CTC decoding escolhe a classe de maior score a cada time step, colapsa repetições consecutivas num caractere, e descarta a classe blank; o blank é o que permite que letras genuinamente dobradas sobrevivam. Um recognition model olha uma linha de texto como uma sequência de fatias verticais estreitas, e para cada fatia, ou time step, ele produz uma probabilidade para cada classe. Uma linha contendo AA中 pode produzir a sequência argmax A A blank A 中 space. Colapsar os dois primeiros steps de A dá um A, o blank o separa do próximo A, e o resultado é AA中 com o espaço final intacto. Sem a regra do blank, book e bok seriam indistinguíveis
Como o decoder tem só uma dúzia de linhas, é fácil errar as fronteiras, e as falhas são silenciosas. Se o loop argmax interno para uma classe antes, a classe de espaço nunca pode vencer e toda linha volta sem espaçamento de palavras, o que destrói phrase search em páginas inglesas e latinas. Se o loop externo para um time step antes, o último caractere de toda linha desaparece, o que numa linha curta pode ser um terço do texto. E se a guarda de repetição não é resetada por um blank, caracteres dobrados como ll ou reduplicações chinesas como 谢谢 colapsam num só. O decoder do HotPDF inclui a última classe e o último time step, mantém repetições separadas por blank, e adicionalmente rejeita scores que não são finitos ou caem fora de 0 a 1, e qualquer contagem de classes que não bata com o dictionary. Aqui está a mesma lógica como ilustração Pascal
// Só ilustração: greedy CTC decoding com fronteiras corretas.
// Scores guarda Steps * Classes probabilidades, uma linha 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; // classe 0 é o blank CTC
for Step := 0 to Steps - 1 do // inclua o último time step
begin
Best := 0;
BestScore := Scores[Step * Classes];
for C := 1 to Classes - 1 do // inclua a última classe (espaço)
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; // um blank reseta a guarda de repetição
end;
end;
Greedy decoding não é a estratégia CTC mais precisa disponível; beam search com um language model pode consertar algumas fatias ambíguas. Para documentos impressos a 300 DPI o resultado greedy costuma ser o que o model tem para oferecer, e o decoder não é o lugar para compensar fraquezas do model. O model latino PP-OCRv3, por exemplo, pode ler ñ como n mesmo em input limpo. O HotPDF não disfarça isso com replacements de caracteres em post-processing, porque uma substitution table que conserta o espanhol quebra outra coisa, e um caractere errado numa camada pesquisável é pior do que um miss honesto
Como o HotPDF ordena linhas de texto, inclusive árabe da direita para a esquerda?
O HotPDF ordena as text boxes detectadas de cima para baixo, agrupa boxes numa linha quando elas se sobrepõem verticalmente em pelo menos metade da altura da box menor, e ordena cada linha da esquerda para a direita, ou da direita para a esquerda quando o RightToLeft está habilitado; os caracteres dentro de cada linha reconhecida nunca são invertidos. O agrupamento importa porque um detector costuma partir uma linha visual em várias boxes, por exemplo um label e um value separados por um vão largo, e uma ordenação pura por coordenada de topo os intercalaria com a linha vizinha sempre que os topos deles diferissem por um pixel ou dois
O preset árabe seta RightToLeft := True, o que diz à DLL ordenar as boxes de cada linha pela borda direita delas, da margem direita para dentro. Esse é todo o efeito. O texto que o model retorna para uma linha já está em ordem lógica Unicode, a ordem em que um leitor de árabe lê e digita, e essa também é a ordem que a extração de texto e a busca de PDF esperam. Inverter mecanicamente a string para que ela "pareça certa" num debugger quebraria busca, cópia e colagem, e screen readers. Display bidirecional e glyph shaping são trabalho do viewer
Uma engine serve um language profile. Não existe detecção automática de script, então um documento que mistura scripts precisa de uma engine por perfil, aplicada às páginas que o usam. Como o ApplyLoadedOCRTextLayer recebe uma lista de páginas explícita e efetiva cada chamada como a própria transação all-or-nothing dela, isso é direto
uses
SysUtils, HPDFDoc, HPDFRapidOCRRecognition;
function CreateRapidEngine(const Tag: string): IHPDFOCREngine;
var
Models: THPDFRapidOCRDLLOptions;
begin
// levanta EArgumentException para tag desconhecida, antes de qualquer model carregar
Models := THPDFRapidOCRDLLOptions.ForLanguage(Tag);
Models.MaxPixels := 33554432; // espaço 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'); // chinese_cht profile
Arabic := CreateRapidEngine('ar-SA'); // arabic profile, 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;
A linha do MaxPixels está ali por um motivo. As options da DLL têm como default 16.777.216 pixels por request, o que cobre A4 e US Letter a 300 DPI com folga, mas uma página A3 a 300 DPI tem uns 3508 por 4961 pixels, cerca de 17,4 milhões, e o request é recusado como acima do orçamento. Suba o MaxPixels (o teto é 67.108.864) ou baixe o THPDFOCRTextLayerOptions.DPI para formatos grandes. A ordenação da direita para a esquerda usa o export opcional HPDFRapidOCRSetReadingDirection da ABI version 1; o adapter só o exige quando o RightToLeft está setado, então uma DLL mais velha continua servindo idiomas da esquerda para a direita e falha na criação da engine com um EArgumentException nomeando o export faltante para árabe
Por que models OCR mais novos falham ao carregar?
A DLL RapidOCR do HotPDF linka um ONNX Runtime 1.14 estático, que não consegue ler models salvos com ONNX IR version 10, e exports mais novos como os models PP-OCRv5 podem exigir uma runtime mais nova que essa; tal model falha na criação da engine com um diagnóstico nativo. Essa restrição é a razão de os language packs estarem pregados a pares específicos de recognizer e dictionary PP-OCRv3 e PP-OCRv4 em vez de "latest", e de a tabela acima misturar as duas gerações: todo par pregado é um que carrega e verifica sob aquela runtime
O installer impõe o pareamento. Todo arquivo no manifest dele carrega um hash SHA256, um arquivo existente com hash diferente para o install em vez de ser sobrescrito, e cada download pousa sob um nome temporário e só se move para o lugar depois de o hash dele bater. Isso protege contra a versão silenciosa do problema de dictionary: alguém derruba um recognition.onnx mais novo numa pasta de perfil na mão, a contagem de classes por acaso bate, e nada falha até um cliente reportar que a busca não acha palavras que ele vê claramente. Em runtime o adapter fica offline e nunca busca um model faltante. O recognizer também valida a shape do model no load, aceitando input NCHW com altura fixa de 32 ou 48 pixels ou altura dinâmica, que ele roda a 48
Se você precisa de um script que nenhum dos nove perfis cobre, ainda pode apontar RecognitionModel e CharacterDictionary para os seus próprios arquivos. As mesmas checagens se aplicam, e esse é o ponto: um par desalinhado falha na inicialização, não na archive do seu cliente. Para páginas em que nenhum perfil RapidOCR se encaixa, o adapter Tesseract para PDF pesquisável se pluga na mesma chamada de ApplyLoadedOCRTextLayer, e para formulários ASCII impressos por máquina a engine OCR embutida de template matching não precisa de models de forma alguma
Referência rápida: checklist RapidOCR multilíngue
- Crie options com o
THPDFRapidOCRDLLOptions.ForLanguagee trateEArgumentExceptioncomo tag não suportada, não como falha de runtime - Mude
RecognitionModeleCharacterDictionaryjuntos, nunca um sozinho; contagens de classes iguais não provam ordem de caracteres igual - Mantenha dictionaries como UTF-8 sem BOM, nunca apague entradas, e espere que o model tenha N + 2 classes: blank, N entradas, espaço
- Um decoder CTC custom precisa cobrir a última classe e o último time step e manter repetições separadas por blank
- Use uma engine por language profile e passe listas de páginas explícitas para documentos de scripts mistos
- O
RightToLeftmuda só a ordem das boxes; o texto reconhecido permanece em ordem lógica Unicode - Instale models com o
Install-RapidOCRModels.ps1para que os pins SHA256 segurem o pareamento model e dictionary; seteUseAngleClassifier := Falsese você instalou com-SkipClassifier - Suba o
MaxPixelsacima do default 16.777.216 antes de rodar páginas A3 ou maiores a 300 DPI
Os presets de idioma RapidOCR, o adapter nativo de DLL e o pipeline de camada de texto OCR fazem parte do HotPDF Delphi PDF Component para Delphi, C++Builder e Windows FPC/Lazarus, começando com a v2.775.0 para os perfis multilíngues