O HotPDF torna páginas PDF digitalizadas pesquisáveis com RapidOCR in-process através do HPDFCreateRapidOCRDLLOCREngine, uma fábrica acrescentada na v2.774.0 que carrega a HotPDFRapidOCR.dll, mantém os modelos ONNX de deteção, classificação de ângulo e reconhecimento residentes em memória, e devolve um IHPDFOCREngine. Passa esse engine ao THotPDF.ApplyLoadedOCRTextLayer, que renderiza cada página, corre inferência em CPU sem Python nem processo filho, e consolida uma camada de texto Unicode invisível
A motivação é o custo por página. O adaptador de processo RapidOCR lançado antes, o HPDFCreateRapidOCREngine, arranca um worker Python para cada chamada Recognize, e esse worker importa o seu runtime e carrega os seus modelos ONNX antes de ler um único pixel. Num arquivo de 500 páginas esse imposto de arranque repete-se 500 vezes, e a instalação significa mandar um ambiente Python junto de um executável Delphi. A DLL nativa carrega os modelos uma vez, quando cria o engine, e a instalação encolhe para a DLL, os seus ficheiros de modelo, e um dicionário de caracteres. O que se abre mão em troca é a capacidade de matar um reconhecedor preso, e a maior parte da engenharia deste adaptador é saber viver com isso com honestidade
Como tornar um PDF digitalizado pesquisável com a DLL RapidOCR?
Criar um PDF pesquisável com a DLL RapidOCR nativa leva uma chamada de fábrica e a mesma chamada ApplyLoadedOCRTextLayer que todo o engine OCR do HotPDF usa. A fábrica vive na unidade HPDFRapidOCRRecognition e valida de imediato: a DLL e o diretório de modelos têm de existir, todos os ficheiros de modelo e dicionário têm de resolver, a versão da ABI tem de ser 1, e todos os exports obrigatórios têm de estar presentes antes de qualquer modelo ser inicializado. Erros de configuração levantam EArgumentException; um modelo que falhe ao carregar levanta EInvalidOperation com o texto de diagnóstico que a DLL escreveu
uses
SysUtils, HPDFTypes, HPDFDoc, HPDFRapidOCRRecognition;
procedure MakeSearchable(const SourceFile, TargetFile: string);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// Os modelos carregam aqui, fora de qualquer prazo de reconhecimento.
// Nomes de modelos relativos em THPDFRapidOCRDLLOptions.Default resolvem
// contra o diretório 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
// uma lista de páginas vazia significa todas as páginas; páginas com texto são saltadas
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;
O THPDFRapidOCRDLLOptions.Default nomeia ch_PP-OCRv3_det_infer.onnx, ch_PP-OCRv3_rec_infer.onnx, ch_ppocr_mobile_v2.0_cls_infer.onnx e ppocr_keys_v1.txt, com uma thread de CPU, um limite de input de 16 777 216 pixels, e um prazo de reconhecimento de 60 000 ms. Desde a v2.775.0, o THPDFRapidOCRDLLOptions.ForLanguage troca para dentro um modelo de reconhecimento e um dicionário correspondentes para chinês tradicional, russo, japonês, árabe e outros perfis; porque é que o modelo e o dicionário têm de mudar juntos está coberto em modelos multilingues RapidOCR e dicionários CTC no HotPDF. O engine reporta-se como RapidOCR (native DLL) no Info.EngineName, o que mantém os logs inequívocos ao lado do adaptador de processo Tesseract OCR externo e do engine OCR integrado por correspondência de modelos
Porque é que a ABI C só fala int32_t e bytes UTF-8?
A ABI da HotPDFRapidOCR.dll usa só inteiros de largura fixa, apontadores crus, e comprimentos de bytes explícitos porque Delphi, C++Builder e Free Pascal não partilham nada com o MSVC além da convenção de chamada C. Uma std::string, um std::vector ou uma exceção C++ tem um layout e um modelo de unwinding que pertencem a um compilador e a uma biblioteca de runtime. Deixe qualquer um deles atravessar a fronteira e a falha é uma stack corrompida ou um bloco da heap libertado pelo alocador errado, não um erro limpo
A versão 1 da ABI segue por isso uma lista curta de regras. Cada export é cdecl e devolve um estado int32_t, em que 1 significa sucesso e 0 significa falha. Toda a função que possa falhar recebe um buffer de diagnóstico da propriedade do chamador e a sua capacidade em bytes; a DLL escreve uma mensagem UTF-8 terminada em NUL truncada para caber, e o adaptador descodifica-a com um terminador firme no último byte do seu próprio buffer de 4096 bytes. O corpo de cada export está embrulhado num try com ambos catch (const std::exception &) e catch (...), por isso um erro do ONNX Runtime, uma asserção OpenCV ou um dicionário inválido tornam-se estado 0 mais texto, nunca uma exceção a escapar para código Pascal
| Export | Papel | Quando o adaptador o resolve |
|---|---|---|
HPDFRapidOCRAbiVersion | Devolve 1; qualquer outro valor é rejeitado | Primeiro, antes de mais nada |
HPDFRapidOCRCreate | Carrega deteção, classificação opcional, modelos de reconhecimento e o dicionário | Na fábrica |
HPDFRapidOCRRecognize | Corre um bitmap e emite um callback por linha de texto | Na fábrica |
HPDFRapidOCRDestroy | Liberta a instância de modelos | Na fábrica |
HPDFRapidOCRSetReadingDirection | Ordem de linhas da direita para a esquerda opcional, acrescentada na v2.775.0 | Só quando RightToLeft está definido |
O export opcional é resolvido preguiçosamente de propósito: uma DLL v2.774.0 que não o tenha continua a servir pedidos da esquerda para a direita. A DLL é carregada com LoadLibraryEx com flags de procura que cobrem a pasta própria da DLL mais os diretórios seguros por omissão, por isso dependências ONNX Runtime ou OpenCV colocadas ao lado da HotPDFRapidOCR.dll são encontradas sem tocar no PATH. Os caminhos de modelo e dicionário viajam como UTF-8 e a DLL converte-os com MultiByteToWideChar em modo estrito antes de abrir ficheiros através de APIs de caracteres wide, por isso um diretório de modelos sob um nome de utilizador chinês ou cirílico funciona em vez de ser alargado byte a byte em disparates
Uma regra vive na build e não no header. A DLL liga estaticamente o ONNX Runtime e o OpenCV, e a configuração CMake por omissão usa a CRT release estática (/MT). Bibliotecas estáticas compiladas contra /MD misturadas numa DLL /MT produzem erros de link na melhor das hipóteses e duas heaps independentes na pior, por isso as bibliotecas provisionadas têm de corresponder ao modo CRT que a DLL usar
O que acontece entre um TBitmap e uma linha de texto?
O HotPDF entrega à DLL um snapshot BGR top-down independente da página renderizada, e a DLL devolve um callback por linha de texto reconhecida com texto UTF-8 emprestado que o adaptador tem de copiar antes de devolver
Em Delphi o adaptador atribui o bitmap da página a um TBitmap privado, força pf24bit, e lê as linhas com GetDIBits usando um biHeight negativo, o que dá linhas top-down acolchoadas ao alinhamento de quatro bytes; esse stride é passado explicitamente. Em FPC lê através do CreateIntfImage, porque escritas de scanline da LCL podem atualizar a imagem crua sem refrescar o handle GDI. O bitmap do chamador nunca é modificado, e o orçamento de pixels (MaxPixels, 16 777 216 por omissão e configurável até 67 108 864) e o limite de 32 767 pixels por dimensão são verificados antes do buffer de snapshot ser alocado
Dentro da DLL o snapshot é acolchoado com 50 pixels brancos, as regiões de texto são detetadas com um lado máximo de 1024 pixels, as caixas são ordenadas em linhas horizontais, e cada recorte é opcionalmente rodado pelo classificador de ângulos antes do reconhecimento. Cada linha de texto passa então por um callback que recebe um const char*, uma contagem de bytes, uma caixa inteira em pixels da imagem original, e a confiança média de caracteres. O apontador de texto só é válido durante o callback, por isso o adaptador copia-o imediatamente, e é rigoroso quanto ao que aceita:
- O UTF-8 é descodificado com
MB_ERR_INVALID_CHARS; uma sequência malformada falha a página em vez de produzir caracteres de substituição numa camada pesquisável - Caracteres de controlo C0 e C1 são rejeitados, e linhas só com espaços em branco são saltadas
- A caixa tem de estar dentro do bitmap e a confiança tem de ser um valor finito de 0 a 1
- O texto é contado contra o
MaxTextCodeUnitsdo pedido com um teto firme de 1 048 576 unidades UTF-16 por chamada, e caracteres do plano suplementar custam duas unidades - Qualquer exceção Pascal dentro do callback é apanhada aí, guardada, e convertida num retorno 0, o que faz a DLL parar e reportar falha; a mensagem guardada torna-se então o diagnóstico
Duas consequências interessam para afinar. Primeiro, a unidade de output é uma linha, não uma palavra: cada linha consome um slot MaxWords, o Info.AcceptedWordCount e o Info.DroppedWordCount contam linhas, e o realce de pesquisa estende-se pela caixa da linha. Segundo, o MinimumConfidence (0.5 por omissão) é comparado com a confiança média de caracteres da linha, por isso uma linha com um carácter ilegível entre vinte limpos normalmente sobrevive. A DLL não fornece baseline, por isso o pipeline da camada de texto estima uma a partir da caixa. Uma página vazia tem sucesso com zero linhas, e qualquer falha limpa os resultados parciais para a consolidação multi-página continuar tudo-ou-nada
Posse de modelos e thread safety
Cada engine da DLL RapidOCR tem exatamente uma instância de modelos por toda a sua vida, e chamadas a Recognize nesse engine são serializadas por uma critical section. Segurar a interface IHPDFOCREngine é o que mantém os modelos quentes, por isso o padrão certo para trabalho em lote é criar o engine uma vez e reutilizá-lo 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; // scans verticais: nenhum classificador é carregado
Models.Threads := 4; // 1..64, limitado aos processadores lógicos
Models.TimeoutMilliseconds := 120000; // por chamada 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 referência libertada: modelos destruídos, depois a DLL sai
O valor Threads define tanto a contagem de threads intra-op como inter-op de cada sessão ONNX, e a DLL prende-a à contagem de processadores ativos. Duas threads a partilhar um engine não correm em paralelo; a segunda espera pelo lock. Essa espera não é um EnterCriticalSection às cegas: o adaptador chama TryEnterCriticalSection a cada 25 ms e verifica o token de cancelamento e o prazo entre tentativas, por isso um pedido em fila ainda pode ser cancelado ou esgotar o prazo. Se precisa de paralelismo a sério, crie um engine por worker e aceite que cada engine segura a sua própria cópia dos modelos em memória
A ordem de desmontagem é fixada pelo destrutor do engine: o HPDFRapidOCRDestroy liberta primeiro a instância de modelos, depois o FreeLibrary descarrega a DLL. Do lado nativo, a inicialização de modelos é igualmente cuidadosa; quando o modelo de reconhecimento falha depois das sessões de detetor e classificador já estarem construídas, essas sessões são libertadas antes de o erro ser reportado, e a contagem de classes do dicionário é verificada contra o output do modelo durante a inicialização em vez de na primeira página
Porque é que uma chamada OCR nativa não pode ser morta a meio da inferência?
Uma chamada RapidOCR nativa não pode ser morta a meio da inferência porque corre na sua thread, dentro do seu processo, no meio de uma sessão ONNX Runtime que não aceita interrupção. O cancelamento no adaptador DLL do HotPDF é por isso cooperativo: a DLL chama um callback de aborto antes e depois da deteção, depois da classificação, e depois de cada linha reconhecida, e pára no primeiro checkpoint em que o callback devolve 0. Um único Run ONNX que já começou vai acabar primeiro
As alternativas são piores do que esperar. O TerminateThread deixaria o lock da heap da CRT, o thread pool do ONNX Runtime, e qualquer estado OpenCV na condição em que se encontrassem, envenenando o resto do processo. Um FreeLibrary enquanto uma chamada ainda executa descarrega código que está na stack. Nenhum pode ser tornado seguro, por isso o adaptador nunca os tenta. O prazo em TimeoutMilliseconds é consequentemente um prazo cooperativo, e um prazo expirado aparece como erro de engine com um diagnóstico de timeout, enquanto um token cancelado aparece como otlsCancelled:
// O Token é criado pelo chamador e partilhado com a thread da UI,
// que chama Token.Cancel quando o utilizador prime Stop
Options := THPDFOCRTextLayerOptions.Default;
Options.CancellationToken := Token;
if not Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
case Info.Status of
otlsCancelled:
// devolvido na próxima fronteira de fase ou de linha; documento inalterado
Writeln('Cancelled');
otlsEngineError:
// inclui expiração cooperativa de prazo e diagnósticos nativos
Writeln('Engine: ', string(Info.Diagnostic));
otlsBudgetExceeded:
Writeln('Budget: ', string(Info.Diagnostic));
else
Writeln(string(Info.Diagnostic));
end;
Este é o compromisso central entre os adaptadores de processo do HotPDF e a DLL in-process, e nenhum dos lados ganha em todas as linhas:
- Custo de arranque: os adaptadores Tesseract e RapidOCR Python lançam um processo e carregam modelos por cada página; a DLL carrega modelos uma vez por engine
- Paragem: um processo filho pode ser terminado de uma vez, e o worker Python corre dentro de um Job Object kill-on-close para a sua árvore de processos inteira ir com ele; a DLL só consegue parar em fronteiras de fase e de linha
- Contenção de falhas: um crash no
tesseract.exefalha uma página; uma access violation dentro da DLL leva o seu processo abaixo - Instalação: os adaptadores de processo precisam de um programa instalado ou de um ambiente Python; a DLL precisa de si própria, dos seus modelos, e do seu dicionário, correspondentes à bitness da aplicação
- Memória: os adaptadores de processo libertam tudo quando o filho sai; um engine DLL mantém os seus modelos residentes até a última referência de interface ser libertada
Para uma aplicação desktop interativa que faz OCR a uma página de cada vez, a capacidade de resposta da DLL normalmente ganha. Para um servidor que ingere scans não confiáveis a toda a hora, a fronteira de processo vale o seu custo de arranque
Compilar e instalar a HotPDFRapidOCR.dll
A HotPDFRapidOCR.dll é compilada a partir dos fontes C++ em Native/RapidOCR com MSVC, C++17, um Windows SDK, e CMake 3.20 ou posterior, usando um script auxiliar que recebe os diretórios de fontes de rede nativos, ONNX Runtime e OpenCV mais uma plataforma Win32 ou Win64. Compile ambas se envia ambas, porque uma aplicação Delphi de 32 bits não consegue carregar uma DLL de 64 bits, e as bibliotecas estáticas que provisionar têm de corresponder à arquitetura alvo além do modo CRT
O lado dos modelos tem os seus próprios limites de compatibilidade. O detetor é um detetor de texto DB; o reconhecedor aceita modelos CTC em layout NCHW com uma altura de input fixa de 32 ou 48, e usa 48 para modelos com altura dinâmica. O ONNX Runtime estático incluído não consegue carregar modelos gravados com uma versão de IR mais recente, por isso exports PP-OCRv5 recentes falham a inicialização com um diagnóstico em vez de carregar parcialmente. O dicionário tem de ser UTF-8 sem BOM, pela exata ordem de caracteres do modelo, e a sua contagem de classes tem de corresponder ao output do modelo; fins de linha CRLF são aceites. O reconhecimento é offline: a DLL nunca descarrega um modelo em falta
Referência rápida
- Fábrica:
HPDFCreateRapidOCRDLLOCREngine(LibraryPath, ModelDirectory[, Options])emHPDFRapidOCRRecognition, disponível desde a v2.774.0 em builds Delphi, C++Builder e FPC/Lazarus Windows - Mantenha o
IHPDFOCREnginedevolvido vivo entre páginas e documentos; libertá-lo destrói os modelos e descarrega a DLL - Um engine corre um reconhecimento de cada vez; crie vários engines para workers paralelos e reserve memória para cada cópia de modelos
- O output é uma entrada por linha de texto com confiança média de caracteres, filtrado pelo
THPDFOCRTextLayerOptions.MinimumConfidence - Cancelamento e
TimeoutMillisecondssão cooperativos; um run ONNX em curso acaba sempre - Faça a bitness da DLL corresponder à aplicação e o modo CRT das bibliotecas estáticas ONNX Runtime e OpenCV à DLL
- Escolha um perfil de língua por engine com
THPDFRapidOCRDLLOptions.ForLanguage(v2.775.0); um engine não deteta línguas por si
O adaptador RapidOCR nativo, os adaptadores OCR baseados em processo, o renderizador de páginas que os alimenta, e o escritor de camadas de texto Unicode invisíveis vêm todos juntos no HotPDF, um componente PDF VCL nativo para Delphi e C++Builder. Se a sua aplicação de captura ou arquivo de documentos precisa de output pesquisável sem um runtime Python na máquina alvo, o componente PDF Delphi HotPDF fornece o pipeline completo com só a DLL e os seus modelos por instalar