O HotPDF torna páginas de PDF escaneadas pesquisáveis com RapidOCR in-process através do HPDFCreateRapidOCRDLLOCREngine, uma factory acrescentada na v2.774.0 que carrega a HotPDFRapidOCR.dll, mantém os models ONNX de detection, angle classification e recognition residentes na memória, e retorna um IHPDFOCREngine. Você passa essa engine ao THotPDF.ApplyLoadedOCRTextLayer, que renderiza cada página, roda inference de CPU sem Python nem processo filho, e efetiva uma camada de texto Unicode invisível
A motivação é custo por página. O adapter de processo do RapidOCR que saiu antes, o HPDFCreateRapidOCREngine, lança um worker Python para cada chamada de Recognize, e esse worker importa o runtime dele e carrega os models ONNX dele antes de ler um único pixel. Numa archive de 500 páginas essa taxa de start-up se repete 500 vezes, e deploy significa embarcar um ambiente Python ao lado de um executável Delphi. A DLL nativa carrega os models uma vez, quando você cria a engine, e o deploy encolhe para a DLL, os model files dela e um character dictionary. O que você abre mão em troca é a capacidade de matar um recognizer travado, e boa parte da engenharia deste adapter é sobre conviver com isso honestamente
Como tornar um PDF escaneado pesquisável com a DLL RapidOCR?
Criar um PDF pesquisável com a DLL nativa RapidOCR leva uma chamada de factory e a mesma chamada de ApplyLoadedOCRTextLayer que toda engine OCR do HotPDF usa. A factory mora na unit HPDFRapidOCRRecognition e valida com antecedência: a DLL e o model directory precisam existir, todo model e arquivo de dictionary precisa resolver, a ABI version precisa ser 1, e todos os exports requeridos precisam estar presentes antes de qualquer model ser inicializado. Erros de configuração levantam EArgumentException; um model que falha ao carregar levanta EInvalidOperation carregando 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 models carregam aqui, fora de qualquer recognition deadline.
// Nomes de model relativos em THPDFRapidOCRDLLOptions.Default resolvem
// contra o model directory.
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 que já têm texto são puladas
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 recognition deadline de 60.000 ms. Desde a v2.775.0, o THPDFRapidOCRDLLOptions.ForLanguage troca por um recognition model e dictionary correspondentes para chinês tradicional, russo, japonês, árabe e outros perfis; por que o model e o dictionary precisam mudar juntos está coberto em models multilíngues RapidOCR e CTC dictionaries no HotPDF. A engine se reporta como RapidOCR (native DLL) no Info.EngineName, o que mantém os logs sem ambiguidade ao lado do adapter de processo OCR externo Tesseract e da engine OCR embutida de template matching
Por que a C ABI só fala int32_t e bytes UTF-8?
A ABI da HotPDFRapidOCR.dll usa só inteiros de largura fixa, ponteiros crus e comprimentos de byte explícitos porque Delphi, C++Builder e Free Pascal não compartilham nada com o MSVC além da convenção de chamada C. Um 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 runtime library. Deixe qualquer um deles cruzar a fronteira e a falha é uma stack corrompida ou um heap block liberado pelo allocator errado, não um erro limpo
A ABI version 1 portanto segue uma lista curta de regras. Todo export é cdecl e retorna um status int32_t, em que 1 significa sucesso e 0 significa falha. Toda função que pode falhar recebe um buffer de diagnóstico de propriedade do caller e a capacidade dele em bytes; a DLL escreve uma mensagem UTF-8 terminada em NUL truncada para caber, e o adapter a decodifica com um terminador duro no último byte do próprio buffer dele de 4.096 bytes. O corpo de cada export é embrulhado em try com ambos catch (const std::exception &) e catch (...), então um erro de ONNX Runtime, uma assertion de OpenCV ou um dictionary inválido vira status 0 mais texto, nunca uma exceção escapando para código Pascal
| Export | Função | Quando o adapter o resolve |
|---|---|---|
HPDFRapidOCRAbiVersion | Retorna 1; qualquer outro valor é rejeitado | Primeiro, antes de qualquer coisa |
HPDFRapidOCRCreate | Carrega os models de detection, classification opcional, recognition e o dictionary | Na factory |
HPDFRapidOCRRecognize | Roda um bitmap e emite um callback por linha de texto | Na factory |
HPDFRapidOCRDestroy | Libera a instância de models | Na factory |
HPDFRapidOCRSetReadingDirection | Ordem de linhas da direita para a esquerda opcional, adicionada na v2.775.0 | Só quando RightToLeft está setado |
O export opcional é resolvido lazily de propósito: uma DLL da v2.774.0 que não o tem ainda serve requests da esquerda para a direita. A DLL é carregada com LoadLibraryEx com flags de busca que cobrem a própria pasta da DLL mais os default safe directories, então dependências de ONNX Runtime ou OpenCV colocadas ao lado da HotPDFRapidOCR.dll são achadas sem tocar no PATH. Os caminhos de model e de dictionary viajam como UTF-8 e a DLL os converte com MultiByteToWideChar em modo estrito antes de abrir arquivos por APIs wide-character, então um model directory sob um nome de usuário chinês ou cirílico funciona em vez de ser alargado byte a byte em nonsense
Uma regra mora no build em vez de no header. A DLL linka estaticamente ONNX Runtime e OpenCV, e a configuração CMake default usa a CRT release estática (/MT). Static libraries compiladas contra /MD misturadas numa DLL /MT produzem link errors na melhor das hipóteses e dois heaps independentes na pior, então as libraries provisionadas precisam bater com o modo de 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 adapter precisa copiar antes de retornar
No Delphi o adapter atribui o bitmap da página a um TBitmap privado, força pf24bit, e lê linhas com GetDIBits usando um biHeight negativo, o que produz linhas top-down com padding para alinhamento de quatro bytes; esse stride é passado explicitamente. No FPC ele lê através de CreateIntfImage, porque escritas de scanline da LCL podem atualizar a imagem crua sem refrescar o handle GDI. O bitmap do caller nunca é modificado, e o orçamento de pixels (MaxPixels, 16.777.216 por default e configurável até 67.108.864) e o limite de 32.767 pixels por dimensão são conferidos antes do buffer de snapshot ser alocado
Dentro da DLL o snapshot recebe padding de 50 pixels brancos, as regiões de texto são detectadas com lado máximo de 1.024 pixels, as boxes são ordenadas em linhas horizontais, e cada crop é opcionalmente rotacionado pelo angle classifier antes do recognition. Cada linha de texto então passa por um callback que recebe um const char*, uma contagem de bytes, uma box inteira em pixels da imagem original, e a confidence média de caractere. O ponteiro de texto vale só durante o callback, então o adapter o copia imediatamente, e ele é estrito quanto ao que aceita:
- UTF-8 é decodificado com
MB_ERR_INVALID_CHARS; uma sequência malformada derruba a página em vez de produzir replacement characters numa camada pesquisável - Caracteres de controle C0 e C1 são rejeitados, e linhas só de whitespace são puladas
- A box precisa estar dentro do bitmap e a confidence precisa ser um valor finito de 0 a 1
- O texto é contado contra o
MaxTextCodeUnitsdo request com teto duro de 1.048.576 unidades UTF-16 por chamada, e caracteres do supplementary plane custam duas unidades - Qualquer exceção Pascal dentro do callback é capturada ali, guardada, e virada num retorno 0, o que faz a DLL parar e reportar falha; a mensagem guardada então vira o diagnóstico
Duas consequências importam para tuning. Primeira, a unidade de output é uma linha, não uma palavra: cada linha consome uma vaga de MaxWords, o Info.AcceptedWordCount e o Info.DroppedWordCount contam linhas, e o destaque de busca atravessa a box da linha. Segunda, o MinimumConfidence (0.5 por default) é comparado contra a confidence média de caractere da linha, então uma linha com um caractere ilegível entre vinte limpos costuma sobreviver. A DLL não fornece baseline, então o pipeline de camada de texto estima uma a partir da box. Uma página vazia tem sucesso com zero linhas, e qualquer falha limpa resultados parciais para que o commit multi-página continue all-or-nothing
Posse de models e thread safety
Cada engine de DLL RapidOCR é dona de exatamente uma instância de models por toda a vida dela, e as chamadas de Recognize nessa engine são serializadas por uma critical section. Segurar a interface IHPDFOCREngine é o que mantém os models quentes, então o padrão certo para trabalho em lote é criar a engine uma vez e reutilizá-la 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 em pé: nenhum model de classifier é carregado
Models.Threads := 4; // 1..64, teto na contagem de 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 liberada: models destruídos, então a DLL é descarregada
O valor de Threads seta tanto as contagens de intra-op quanto de inter-op threads de cada ONNX session, e a DLL o clampa para a contagem de processadores ativa. Duas threads compartilhando uma engine não rodam em paralelo; a segunda espera o lock. Essa espera não é um EnterCriticalSection às cegas: o adapter chama TryEnterCriticalSection a cada 25 ms e confere o cancellation token e o deadline entre tentativas, então um request na fila ainda pode ser cancelado ou estourar o tempo. Se você precisa de paralelismo de verdade, crie uma engine por worker e aceite que cada engine segura a própria cópia dos models na memória
A ordem de teardown é fixada pelo destructor da engine: o HPDFRapidOCRDestroy libera a instância de models primeiro, depois o FreeLibrary descarrega a DLL. No lado nativo, a inicialização dos models é igualmente cuidadosa; quando o recognition model falha depois de as sessions de detector e classifier já terem sido construídas, essas sessions são liberadas antes de o erro ser reportado, e a contagem de classes do dictionary é conferida contra o output do model durante a inicialização em vez de na primeira página
Por que uma chamada OCR nativa não pode ser morta no meio da inference?
Uma chamada nativa RapidOCR não pode ser morta no meio da inference porque ela roda na sua thread, dentro do seu processo, no meio de uma ONNX Runtime session que não aceita interrupção. O cancelamento no adapter de DLL do HotPDF é portanto cooperativo: a DLL chama um abort callback antes e depois da detection, depois da classification, e depois de cada linha reconhecida, e para no primeiro checkpoint em que o callback retorna 0. Um único Run de ONNX que já começou vai terminar primeiro
As alternativas são piores do que esperar. O TerminateThread deixaria o heap lock da CRT, o thread pool do ONNX Runtime e qualquer estado de OpenCV na condição em que estivessem, envenenando o resto do processo. Um FreeLibrary enquanto uma chamada ainda executa descarrega código que está na stack. Nenhum dos dois pode ser tornado seguro, então o adapter nunca os tenta. O deadline no TimeoutMilliseconds é consequentemente um deadline cooperativo, e um deadline expirado aparece como engine error com um diagnóstico de timeout, enquanto um token cancelado aparece como otlsCancelled:
// O token é criado pelo caller e compartilhado com a UI thread,
// que chama Token.Cancel quando o usuário aperta Stop
Options := THPDFOCRTextLayerOptions.Default;
Options.CancellationToken := Token;
if not Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
case Info.Status of
otlsCancelled:
// retornado na próxima fronteira de stage ou de linha; documento inalterado
Writeln('Cancelled');
otlsEngineError:
// inclui expiração de deadline cooperativa e diagnósticos nativos
Writeln('Engine: ', string(Info.Diagnostic));
otlsBudgetExceeded:
Writeln('Budget: ', string(Info.Diagnostic));
else
Writeln(string(Info.Diagnostic));
end;
Este é o trade-off central entre os adapters de processo do HotPDF e a DLL in-process, e nenhum dos lados vence em todas as linhas:
- Custo de start-up: os adapters Tesseract e RapidOCR Python lançam um processo e carregam models para cada página; a DLL carrega models uma vez por engine
- Parar: um processo filho pode ser terminado outright, e o worker Python roda dentro de um Job Object kill-on-close para que a tree de processos dele inteira vá junto; a DLL só pode parar em fronteiras de stage e de linha
- Contenção de falhas: um crash no
tesseract.exederruba uma página; um access violation dentro da DLL leva o seu processo embora - Deploy: adapters de processo precisam de um programa instalado ou de um ambiente Python; a DLL precisa dela mesma, dos models dela e do dictionary dela, casados com o bitness da aplicação
- Memória: adapters de processo liberam tudo quando o filho sai; uma engine de DLL mantém os models residentes até a última referência de interface ser liberada
Para uma aplicação desktop interativa que faz OCR de uma página por vez, a responsividade da DLL costuma vencer. Para um server que ingere scans não confiáveis sem parar, a fronteira de processo vale o custo de start-up dela
Construindo e fazendo deploy da HotPDFRapidOCR.dll
A HotPDFRapidOCR.dll é construída a partir das fontes C++ em Native/RapidOCR com MSVC, C++17, um Windows SDK e CMake 3.20 ou posterior, usando um script helper que recebe os diretórios de native network sources, ONNX Runtime e OpenCV mais uma plataforma Win32 ou Win64. Construa ambas se você embarca ambas, porque uma aplicação Delphi de 32 bits não pode carregar uma DLL de 64 bits, e as static libraries que você provisionar precisam bater com a arquitetura alvo e também com o modo de CRT
O lado dos models tem limites de compatibilidade próprios. O detector é um DB text detector; o recognizer aceita models CTC em layout NCHW com altura de input fixa de 32 ou 48, e usa 48 para models de altura dinâmica. O ONNX Runtime estático embarcado não pode carregar models salvos com uma IR version mais nova, então exports recentes de PP-OCRv5 falham a inicialização com um diagnóstico em vez de carregar parcialmente. O dictionary precisa ser UTF-8 sem BOM, na ordem de caracteres exata do model, e a contagem de classes dele precisa bater com o output do model; line endings CRLF são aceitos. O recognition é offline: a DLL nunca baixa um model faltante
Referência rápida
- Factory:
HPDFCreateRapidOCRDLLOCREngine(LibraryPath, ModelDirectory[, Options])naHPDFRapidOCRRecognition, disponível desde a v2.774.0 em builds Delphi, C++Builder e Windows FPC/Lazarus - Mantenha o
IHPDFOCREngineretornado vivo entre páginas e documentos; liberá-lo destrói os models e descarrega a DLL - Uma engine roda um recognition por vez; crie várias engines para workers em paralelo e orce memória para cada cópia de models
- O output é uma entrada por linha de texto com confidence média de caractere, filtrada pelo
THPDFOCRTextLayerOptions.MinimumConfidence - Cancelamento e
TimeoutMillisecondssão cooperativos; um run de ONNX em progresso sempre completa - Casamento do bitness da DLL com a aplicação e do modo de CRT das static libraries ONNX Runtime e OpenCV com a DLL
- Escolha um language profile por engine com o
THPDFRapidOCRDLLOptions.ForLanguage(v2.775.0); uma engine não detecta idiomas por conta própria
O adapter nativo RapidOCR, os adapters OCR baseados em processo, o page renderer que os alimenta e o escritor de camada de texto Unicode invisível saem juntos no HotPDF, um componente VCL nativo de PDF para Delphi e C++Builder. Se a sua aplicação de captura ou arquivamento de documentos precisa de output pesquisável sem uma runtime Python na máquina alvo, o componente HotPDF PDF para Delphi fornece o pipeline inteiro restando só a DLL e os models dela para fazer deploy