O HotPDF faz OCR de chinês e multilingue em Delphi através do seu adaptador nativo da DLL RapidOCR: o THPDFRapidOCRDLLOptions.ForLanguage mapeia uma tag de língua como 'zh-CN', 'zh-TW', 'ru' ou 'ar' para um modelo de reconhecimento e um dicionário de caracteres correspondentes, e o THotPDF.ApplyLoadedOCRTextLayer transforma as linhas reconhecidas numa camada de texto Unicode invisível e pesquisável em páginas PDF digitalizadas
Levantar uma demo em escrita latina a funcionar é a parte fácil. As falhas interessantes começam quando muda para chinês tradicional ou russo e o output se transforma em disparates confiantes e bem formados, ou quando todas as linhas perdem silenciosamente o último carácter, ou quando uma página árabe volta com as suas caixas de texto pela ordem errada. Nenhum destes casos levanta uma exceção por si. Os presets de língua acrescentados no HotPDF v2.775.0 existem sobretudo para fechar essas falhas, e as quatro armadilhas abaixo valem a pena perceber mesmo que nunca toque no código nativo, porque cada uma explica um sintoma que de outra forma podia passar um dia a perseguir
Como é que o ForLanguage escolhe um modelo e um dicionário?
O THPDFRapidOCRDLLOptions.ForLanguage resolve uma tag para um de nove perfis e devolve opções que apontam para <profile>/recognition.onnx e <profile>/dictionary.txt por baixo do seu diretório de modelos, mantendo o detetor partilhado, o classificador de ângulos opcional, e as predefinições de threads, pixels e timeout do THPDFRapidOCRDLLOptions.Default. O método passa a tag a minúsculas, transforma underscores em hífenes e corta espaços em branco em volta, por isso 'zh_TW', 'ZH-tw' e ' zh-tw ' caem todos no mesmo perfil. Os aliases são uma lista explícita e não uma correspondência por prefixo: 'zh-Hant-TW' é aceite porque está listado, enquanto uma variante regional arbitrária não listada levanta EArgumentException antes de qualquer modelo ser carregado
| Perfil | Línguas | Tags de exemplo | Modelo fixado |
|---|---|---|---|
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, 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 | Russo, ucraniano, búlgaro, bielorrusso | ru, ru-RU, uk, bg | PP-OCRv3 |
arabic | Árabe, persa, urdu | ar, ar-SA, fa, ur | PP-OCRv4 |
devanagari | Híndi, marata, nepalês | hi, mr, ne | PP-OCRv4 |
O adaptador em si nunca descarrega nada. Você provisiona os ficheiros uma vez com o helper incluído, por exemplo tools/Install-RapidOCRModels.ps1 -Destination C:/OCR/models -Language ch,chinese_cht,cyrillic (ou -Language All para os nove perfis), e o helper coloca um detetor e um classificador partilhados nos nomes de ficheiro na raiz que o Default espera. Depois disso, um scan em chinês simplificado torna-se pesquisável com umas linhas. A canalização do engine é a mesma costura IHPDFOCREngine descrita no artigo sobre a DLL RapidOCR in-process e a sua fronteira de ABI, por que este fica focado nas línguas
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, detetor e classificador partilhados
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 com texto são saltadas
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 uma nota. O pipeline nativo devolve um resultado por linha de texto detetada, não por palavra, por isso o AcceptedWordCount conta linhas aqui, e o MinimumConfidence é comparado com a confiança média de caracteres da linha inteira: uma linha a fazer em média 0.45 é descartada como unidade. O UniqueScalarCount reporta quantos escalares Unicode distintos a camada de texto teve de mapear para a sua fonte e tabela ToUnicode, um sanity check útil de que texto CJK realmente chegou em vez de um punhado de fallbacks latinos. Mantenha a interface do engine viva entre documentos, porque a inicialização de modelos acontece na fábrica e é o passo caro
Porque é que trocar só o modelo de reconhecimento produz lixo?
Um modelo de reconhecimento CTC nunca produz caracteres, só índices de classes, e o dicionário é a única coisa que transforma o índice 1204 num glifo. Troque o ch/recognition.onnx pelo cyrillic/recognition.onnx mas mantenha o dicionário chinês, e o modelo emitirá alegremente índices cirílicos válidos que o dicionário antigo traduz em caracteres Han aleatórios. O resultado parece texto, passa na validação UTF-8, e é pesquisável para nada. É por isso que o ForLanguage define sempre RecognitionModel e CharacterDictionary juntos, e por que opções construídas à mão nunca devem mudar um sem o outro
A verificação de segurança óbvia, comparar o tamanho do dicionário com a largura do output do modelo, é necessária mas não suficiente. Dois dicionários podem ter o mesmo número de entradas por ordem diferente, e um off-by-one na ordem desloca cada carácter um code point. O HotPDF verifica por isso em duas fases quando a fábrica inicializa o modelo. Primeiro, a contagem de classes do output tem de igualar as entradas do dicionário mais dois. Segundo, quando o ficheiro ONNX incorpora uma lista de metadados character, cada entrada do dicionário é comparada com ela por ordem, e uma discrepância falha a inicialização com EInvalidOperation e um diagnóstico nativo em vez de produzir lixo plausível mais tarde
O "mais dois" vem do layout de classes. A classe 0 é o blank CTC, as classes 1 a N são as linhas do dicionário pela ordem do ficheiro, e a classe final é um espaço. Alguns dicionários trazem também a sua própria entrada de espaço, e essa linha tem de ser mantida exatamente como está. É aqui que um Trim bem-intencionado faz dano real: transforma uma entrada de um único espaço numa string vazia e desloca ou parte a tabela. A única normalização segura é remover um carriage return final, por isso um dicionário gravado com fins de linha CRLF carrega corretamente, enquanto uma 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 tabela de classes que um reconhecedor 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 ficheiro
SetLength(Result, Last + 3);
Result[0] := ''; // class 0: CTC blank
for I := 0 to Last do
begin
Entry := Lines[I];
if (Entry <> '') and (Entry[Length(Entry)] = #13) then
SetLength(Entry, Length(Entry) - 1); // CRLF: largar 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] := ' '; // final class: space
// Length(Result) tem de igualar a contagem de classes do output do modelo
end;
O que faz afinal a descodificação CTC gulosa?
A descodificação CTC gulosa escolhe a classe com a pontuação mais alta em cada passo de tempo, colapsa repetições consecutivas num único carácter, e deita fora a classe blank; o blank é o que permite que letras genuinamente duplicadas sobrevivam. Um modelo de reconhecimento olha para uma linha de texto como uma sequência de fatias verticais estreitas, e para cada fatia, ou passo de tempo, 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 passos A dá um A, o blank separa-o do A seguinte, e o resultado é AA中 com o espaço final intacto. Sem a regra do blank, book e bok seriam indistinguíveis
Como o descodificador é só uma dúzia de linhas, é fácil errar as fronteiras, e as falhas são silenciosas. Se o loop argmax interno pára uma classe antes, a classe espaço nunca pode ganhar e todas as linhas voltam sem espaços entre palavras, o que estraga a pesquisa de frases em páginas inglesas e latinas. Se o loop externo pára um passo de tempo antes, o último carácter de todas as linhas desaparece, o que numa linha curta pode ser um terço do texto. E se a guarda de repetição não for reposta por um blank, caracteres duplicados como ll ou reduplicações chinesas como 谢谢 colapsam num só. O descodificador do HotPDF inclui a última classe e o último passo de tempo, mantém repetições separadas por blank, e ainda rejeita pontuações que não sejam finitas ou caiam fora de 0 a 1, e qualquer contagem de classes que não corresponda ao dicionário. Aqui está a mesma lógica como ilustração em Pascal
// Só ilustração: descodificação CTC gulosa com fronteiras corretas.
// Scores guarda Steps * Classes probabilidades, uma linha por passo de tempo
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; // a classe 0 é o blank CTC
for Step := 0 to Steps - 1 do // incluir o último passo de tempo
begin
Best := 0;
BestScore := Scores[Step * Classes];
for C := 1 to Classes - 1 do // incluir 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 repõe a guarda de repetição
end;
end;
A descodificação gulosa não é a estratégia CTC mais precisa disponível; beam search com um modelo de língua consegue corrigir algumas fatias ambíguas. Para documentos impressos a 300 DPI o resultado guloso é normalmente o que o modelo tem para oferecer, e o descodificador não é o sítio para compensar fraquezas do modelo. O modelo latino PP-OCRv3, por exemplo, consegue ler ñ como n mesmo em input limpo. O HotPDF não tapa isso com substituições de caracteres em pós-processamento, porque uma tabela de substituição que corrige espanhol parte outra coisa, e um carácter errado numa camada pesquisável é pior do que uma falha honesta
Como é que o HotPDF ordena linhas de texto, incluindo árabe da direita para a esquerda?
O HotPDF ordena as caixas de texto detetadas de cima para baixo, agrupa caixas numa linha quando se sobrepõem verticalmente em pelo menos metade da altura da caixa menor, e ordena cada linha da esquerda para a direita, ou da direita para a esquerda quando o RightToLeft está ligado; os caracteres dentro de cada linha reconhecida nunca são invertidos. O agrupamento interessa porque um detetor frequentemente parte uma linha visual em várias caixas, por exemplo uma etiqueta e um valor separados por um vão largo, e uma ordenação pura por coordenada superior intercalá-las-ia com a linha vizinha sempre que os seus topos difiram um ou dois pixels
O preset árabe põe RightToLeft := True, o que diz à DLL ordenar as caixas de cada linha pela sua margem direita, da margem direita para dentro. Esse é todo o efeito. O texto que o modelo devolve para uma linha já está em ordem lógica Unicode, a ordem em que um leitor de árabe lê e escreve, e essa é também a ordem que a extração de texto PDF e a pesquisa esperam. Inverter mecanicamente a string para ficar "com bom aspecto" num debugger partia a pesquisa, o copiar e colar, e os leitores de ecrã. A exibição bidirecional e a modelação de glifos são trabalho do viewer
Um engine serve um perfil de língua. Não há deteção automática de escrita, por isso um documento que misture escritas precisa de um engine por perfil, aplicado às páginas que o usam. Como o ApplyLoadedOCRTextLayer recebe uma lista de páginas explícita e consolida cada chamada como a sua própria transação tudo-ou-nada, isso é direto
uses
SysUtils, HPDFDoc, HPDFRapidOCRRecognition;
function CreateRapidEngine(const Tag: string): IHPDFOCREngine;
var
Models: THPDFRapidOCRDLLOptions;
begin
// levanta EArgumentException para uma tag desconhecida, antes de qualquer modelo 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'); // 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;
A linha do MaxPixels está ali por uma razão. As opções da DLL têm por omissão 16 777 216 pixels por pedido, o que cobre A4 e US Letter a 300 DPI folgadamente, mas uma página A3 a 300 DPI dá cerca de 3508 por 4961 pixels, cerca de 17,4 milhões, e o pedido é recusado como fora de orçamento. Suba o MaxPixels (o teto é 67 108 864) ou desça o THPDFOCRTextLayerOptions.DPI para formatos grandes. A ordenação da direita para a esquerda usa o export opcional HPDFRapidOCRSetReadingDirection da versão 1 da ABI; o adaptador só o exige quando o RightToLeft está definido, por isso uma DLL mais antiga continua a servir línguas da esquerda para a direita e falha na criação do engine com um EArgumentException a nomear o export em falta para árabe
Porque é que modelos OCR mais recentes falham ao carregar?
A DLL RapidOCR do HotPDF liga um ONNX Runtime 1.14 estático, que não consegue ler modelos gravados com a versão 10 do ONNX IR, e exports mais recentes como os modelos PP-OCRv5 podem exigir um runtime mais recente do que esse; tal modelo falha na criação do engine com um diagnóstico nativo. Essa restrição é a razão pela qual os pacotes de línguas estão fixados a pares específicos de reconhecedor e dicionário PP-OCRv3 e PP-OCRv4 em vez de "o mais recente", e por que a tabela acima mistura as duas gerações: cada par fixado é um que carrega e verifica sob esse runtime
O instalador impõe o emparelhamento. Cada ficheiro no seu manifesto traz um hash SHA256, um ficheiro existente com um hash diferente pára a instalação em vez de ser sobrescrito, e cada download aterrissa sob um nome temporário e só se move para o sítio depois do hash bater certo. Isso protege contra a versão silenciosa do problema do dicionário: alguém larga um recognition.onnx mais recente numa pasta de perfil à mão, a contagem de classes por acaso bate certo, e nada falha até um cliente reportar que a pesquisa não encontra palavras que claramente se veem. Em execução o adaptador fica offline e nunca vai buscar um modelo em falta. O reconhecedor também valida a forma do modelo ao carregar, aceitando input NCHW com altura fixa de 32 ou 48 pixels ou altura dinâmica, que corre a 48
Se precisa de uma escrita que nenhum dos nove perfis cobre, ainda pode apontar o RecognitionModel e o CharacterDictionary aos seus próprios ficheiros. As mesmas verificações aplicam-se, que é o ponto: um par desencontrado falha na inicialização, não no arquivo do seu cliente. Para páginas em que nenhum perfil RapidOCR serve, o adaptador Tesseract para PDF pesquisável liga-se à mesma chamada ApplyLoadedOCRTextLayer, e para formulários ASCII impressos por máquina o engine OCR integrado por correspondência de modelos não precisa de modelos de todo
Referência rápida: checklist RapidOCR multilingue
- Crie opções com o
THPDFRapidOCRDLLOptions.ForLanguagee trateEArgumentExceptioncomo uma tag não suportada, não como uma falha de runtime - Mude
RecognitionModeleCharacterDictionaryjuntos, nunca um sozinho; contagens de classes iguais não provam ordem de caracteres igual - Mantenha os dicionários como UTF-8 sem BOM, nunca corte entradas, e espere que o modelo tenha N + 2 classes: blank, N entradas, espaço
- Um descodificador CTC personalizado tem de cobrir a última classe e o último passo de tempo e manter repetições separadas por um blank
- Use um engine por perfil de língua e passe listas de páginas explícitas para documentos de escritas mistas
- O
RightToLeftmuda só a ordem das caixas; o texto reconhecido fica em ordem lógica Unicode - Instale modelos com o
Install-RapidOCRModels.ps1para os pins SHA256 segurarem o emparelhamento modelo-dicionário; ponhaUseAngleClassifier := Falsese instalou com-SkipClassifier - Suba o
MaxPixelsacima da predefinição de 16 777 216 antes de correr páginas A3 ou maiores a 300 DPI
Os presets de língua RapidOCR, o adaptador nativo da DLL e o pipeline da camada de texto OCR são parte do HotPDF Delphi PDF Component para Delphi, C++Builder e FPC/Lazarus Windows, a começar pela v2.775.0 para os perfis multilingues