O HotPDF transforma páginas de PDF escaneadas em PDF pesquisável com Tesseract por meio do HPDFCreateTesseractOCREngine, uma factory que embrulha um executável Tesseract instalado localmente como um IHPDFOCREngine. Você passa essa engine ao ApplyLoadedOCRTextLayer, que renderiza cada página, roda o Tesseract uma vez por página, parseia o output TSV em nível de palavra dele, e efetiva uma camada de texto Unicode invisível para todas as páginas pedidas numa única transação, ou para nenhuma delas
O motivo de este adapter existir é escopo. A engine OCR embutida de template matching é deliberadamente estreita: letras e dígitos ASCII impressos por máquina, nada além. Notas fiscais com nomes acentuados, contratos em chinês e arquivos multilíngues precisam de um reconhecedor de verdade com language models treinados, e o Tesseract é o candidato óbvio porque é um programa de linha de comando que você pode provisionar ao lado da sua aplicação. Chamar um programa externo de dentro de uma biblioteca de documentos parece trivial. Não é, e a maior parte do código interessante do adapter é sobre o que acontece quando o programa se porta mal, trava, é cancelado, ou herda coisas que nunca deveria ver
Como o HotPDF comanda o Tesseract de uma aplicação Delphi?
O HotPDF roda o Tesseract como um processo filho oculto por página, alimentando-o com um bitmap renderizado e lendo de volta um arquivo TSV, e expõe o resultado pela mesma emenda IHPDFOCREngine que a engine embutida usa. Nada a jusante muda: mapeamento de coordenadas, tratamento de rotação, validação Unicode, filtragem por confidence, e o commit atômico são o pipeline de camada de texto que você já tem. A factory mora na unit HPDFTesseractRecognition e valida com antecedência: o executável precisa existir, o diretório tessdata precisa existir, o timeout precisa estar entre 1 e 3.600.000 milissegundos, e o identificador de idioma só pode conter letras ASCII, dígitos, _ e +. Essa última checagem importa porque a string de idioma acaba numa linha de comando, e eng+chi_sim é um valor Tesseract legítimo enquanto qualquer coisa com aspas ou espaços não é
uses
SysUtils, HPDFTypes, HPDFDoc, HPDFTesseractRecognition;
procedure MakeSearchable(const SourceFile, TargetFile: string;
Token: THPDFCancellationToken);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// levanta EArgumentException para executável faltando, tessdata faltando,
// um identificador de idioma ruim, ou um timeout fora de 1..3600000 ms
Engine := HPDFCreateTesseractOCREngine(
'C:\OCR\Tesseract\tesseract.exe',
'C:\OCR\Tesseract\tessdata',
'eng+chi_sim', // vários models unidos com '+'
120000); // limite por página, o default é 60000
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
Options.CancellationToken := Token;
// uma lista de páginas vazia significa todas as páginas; páginas com texto são puladas por default
if Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
begin
Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
' words accepted, ', Info.DroppedWordCount, ' dropped');
Doc.SaveLoadedDocument(TargetFile);
end
else
case Info.Status of
otlsCancelled: Writeln('Cancelled, document unchanged');
otlsEngineError: Writeln('Engine: ', string(Info.Diagnostic));
otlsBudgetExceeded: Writeln('Budget: ', string(Info.Diagnostic));
else
Writeln(string(Info.Diagnostic));
end;
finally
Doc.Free;
end;
end;
Para cada página, o Recognize cria um diretório privado sob o caminho temp chamado HotPDF-OCR-{GUID}, salva o bitmap renderizado como input.bmp, e lança tesseract input.bmp output --tessdata-dir … -l … --dpi N --psm 3 -c tessedit_create_tsv=1, com todo argumento de caminho entre aspas usando as regras de escape de linha de comando do Windows para barras invertidas e aspas embutidas. O valor do --dpi é o DPI de renderização de THPDFOCRTextLayerOptions.DPI, então o Tesseract nunca precisa adivinhar a resolução pelos metadados da imagem, e o --psm 3 pede segmentação de página totalmente automática. A engine se reporta como Tesseract (local CLI), que é o que cai no Info.EngineName. O Tesseract e os language models dele não vêm embutidos no HotPDF; instalá-los é trabalho da aplicação
Por que o parser TSV é tão estrito?
O parser TSV do HotPDF derruba a página inteira em qualquer linha malformada, porque uma lista de palavras parcialmente parseada produz uma camada de texto que discorda silenciosamente da imagem. O output TSV do Tesseract tem um header fixo de doze colunas, de level até text, e o HotPDF compara a primeira linha com exatamente esse header depois de tirar um byte order mark opcional. Toda linha seguinte precisa se dividir em exatamente doze campos, e o split para depois do décimo primeiro tab para que um tab dentro do texto reconhecido continue parte da palavra em vez de criar uma décima terceira coluna. Só linhas de level 5 são palavras; os levels 1 a 4 descrevem páginas, blocos, parágrafos e linhas, e são pulados. Linhas de level 5 cujo texto é vazio ou só whitespace também são puladas, porque uma palavra em branco tem box mas nada para localizar ou buscar. Todo o resto é checado com rigor: geometria inteira, um confidence parseado com formato invariante en-US para que um locale alemão não leia 93.5 como lixo, um box inteiramente dentro do bitmap, e um confidence entre 0 e 100. Uma única falha levanta exceção, a engine retorna False, e o array de palavras é limpo. Os testes de regressão incluem exatamente esse caso: uma palavra válida seguida de uma linha quebrada precisa resultar em zero palavras, não uma
// condensado do loop de level 5 em HPDFLocalTSVRecognition
if (Fields.Count <> 12) or not TryStrToInt(Fields[0], Level) then
raise EConvertError.Create('Invalid Local OCR TSV row');
if Level <> 5 then Continue; // linhas de page/block/paragraph/line
WordText := Fields[11];
if Trim(WordText) = '' then Continue; // palavras de whitespace não têm posição
if not TryStrToInt(Fields[6], X) or not TryStrToInt(Fields[7], Y) or
not TryStrToInt(Fields[8], W) or not TryStrToInt(Fields[9], H) or
not TryStrToFloat(Fields[10], Confidence, Settings) then
raise EConvertError.Create('Invalid Local OCR word geometry');
if (X < 0) or (Y < 0) or (W <= 0) or (H <= 0) or
(Int64(X) + W > Request.Bitmap.Width) or
(Int64(Y) + H > Request.Bitmap.Height) or
not ((Confidence >= 0) and (Confidence <= 100)) then
raise EConvertError.Create('Local OCR word is outside the image');
Words[Count].Confidence := Confidence / 100; // o pipeline espera 0..1
Essa última linha interage com um default que você talvez não espere. O confidence do Tesseract vai de 0 a 100, o pipeline trabalha em 0 a 1, e o THPDFOCRTextLayerOptions.MinimumConfidence tem default 0.5, então qualquer palavra do Tesseract abaixo de 50 é contada no Info.DroppedWordCount e nunca chega à página. Num scan limpo de 300 DPI isso é um piso razoável. Num fax ruidoso pode derrubar uma parcela surpreendente da página, e o movimento certo é olhar a contagem de descartadas antes de baixar o threshold, porque palavras de confidence baixo são exatamente as mais prováveis de estar erradas
O que o processo filho do Tesseract herda?
O processo filho do Tesseract herda exatamente dois handles do HotPDF: um handle NUL para entrada e saída padrão, e um handle de arquivo para stderr. Essa precisão é o ponto. CreateProcess com bInheritHandles = True é como se passa handles padrão a um filho, mas por si só ele passa todo handle herdável do processo hospedeiro, incluindo arquivos, pipes e events abertos por código sem relação nenhuma na sua aplicação. O filho então mantém esses objetos vivos até sair, então um arquivo fica travado ou um pipe nunca vê o fim dele enquanto o Tesseract moe uma página. O HotPDF fecha essa lacuna com um record de startup estendido: STARTUPINFOEX, uma attribute list carregando PROC_THREAD_ATTRIBUTE_HANDLE_LIST, e a flag de criação EXTENDED_STARTUPINFO_PRESENT. Com a handle list no lugar, o bInheritHandles ainda precisa ser True, mas só os handles listados cruzam a fronteira. O mesmo pensamento de contenção guia isolar codecs de imagem PDF em worker processes, em que o filho é código não confiável; aqui o filho é confiável, mas o host não é o único dono da própria handle table dele
// constantes mostradas pelo nome; o fonte passa os valores numéricos delas
// ambos os handles são criados com bInheritHandle = True
InheritedHandles[0] := NullHandle; // stdin e stdout
InheritedHandles[1] := ErrorHandle; // stderr.txt no diretório privado
InitializeProcThreadAttributeList(Startup.AttributeList, 1, 0, AttributeBytes);
UpdateProcThreadAttribute(Startup.AttributeList, 0,
PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
@InheritedHandles[0], SizeOf(InheritedHandles), nil, nil);
CreateProcess(PChar(Executable), PChar(Command), nil, nil,
True, // exigido pela handle list
CREATE_NO_WINDOW or EXTENDED_STARTUPINFO_PRESENT,
nil, PChar(DirectoryName), Startup.StartupInfo, ProcessInfo);
Por que uma rodada OCR cancelada pode parecer falha de engine?
Uma rodada OCR cancelada parece falha de engine porque o IHPDFOCREngine.Recognize retorna um único Boolean, e False significa tanto "Tesseract falhou" quanto "o usuário apertou Cancelar". O adapter pola o cancellation token e o timeout a cada 25 milissegundos enquanto o filho roda, e quando o token dispara ele levanta dentro do Recognize, captura a própria exceção, limpa, e retorna False com um diagnóstico. Se o pipeline tratasse isso como erro de engine, quem chama veria otlsEngineError para um job que o usuário parou de propósito. O ApplyLoadedOCRTextLayer portanto confere o token primeiro sempre que o Recognize retorna False, e só converte o resultado em falha de engine se o token não estava setado. Essa ordenação preserva o contrato multi-página: reconhecimento, validação, contabilidade de budget e construção de conteúdo rodam para toda página pedida antes de a transação do grafo abrir, então um cancelamento na página 40 de 50 reporta otlsCancelled e deixa o documento, incluindo as primeiras 39 páginas, intacto. Não existe arquivo parcialmente pesquisável para explicar depois, e o resto do tratamento de falhas segue o mesmo estilo limitado:
- O timeout vale por chamada
Recognize, medido do início dela, então o default de 60.000 ms vale para cada página em vez de para o documento inteiro - Um filho ainda rodando no timeout ou no cancelamento é terminado, esperado por até 5 segundos, e o diretório privado dele é apagado num bloco
finally output.tsvtem teto de 64 MiB estderr.txtde 1 MiB, conferidos enquanto o filho roda e também depois que ele sai- Contagem de palavras e code units UTF-16 têm teto por página pelos budgets remanescentes de
MaxWordsPerPage,MaxTotalWordseMaxTextCodeUnits, e excedê-los falha a rodada em vez de truncar a lista de palavras - A saída padrão vai para
NULporque o Tesseract escreve ooutput.tsv, enquanto a saída de erro vai para um arquivo para que um exit code não zero seja reportado com até 4.096 caracteres da reclamação da própria engine, geralmente o jeito mais rápido de descobrir que um arquivo.traineddataestá faltando
Como as palavras reconhecidas viram uma camada de texto invisível
O HotPDF escreve as palavras do Tesseract como texto invisível usando text rendering mode 3, o modo nem-fill-nem-stroke definido na ISO 32000-1 §9.3.6, então a página continua mostrando a imagem escaneada enquanto busca e cópia funcionam sobre as palavras reconhecidas. O content stream abre o BT com 3 Tr, e cada palavra ganha uma matriz Tm na baseline dela, um tamanho de fonte derivado da altura do box em pixels no DPI de renderização, e uma escala horizontal Tz que estica a sequência de glyphs até a largura do box medida, e é por isso que um destaque de busca cai em cima da palavra na imagem em vez de deslizar por ela
O TSV do Tesseract tem boxes mas não baselines, então o adapter reporta toda palavra sem uma e o pipeline estima a baseline a um quinto da altura do box acima da borda inferior. O texto em si passa por uma fonte Type0 compartilhada e não embutida com codificação Identity-H e um CMap ToUnicode gerado, um CID por scalar Unicode distinto ao longo de toda a rodada, e é assim que chinês, latim acentuado e caracteres do plano suplementar sobrevivem todos à cópia e à busca. Esse design tem dois limites que vale dizer de antemão: uma rodada carrega no máximo 65.535 scalars distintos, e a fonte não embutida não satisfaz o requisito de embutir fontes da ISO 19005, então output PDF/A precisa de uma fonte conformante embutida separadamente. Conferir o resultado é simples e vale automatizar: salve, recarregue, e rode o caminho comum de texto de documento carregado de extrair texto de um PDF carregado no Delphi; se as palavras voltarem nas páginas esperadas, a camada é real
RapidOCR e outras engines no mesmo protocolo TSV
O HotPDF reutiliza o mesmo runner de processos e parser TSV para o RapidOCR por meio do HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds), que é a escolha mais útil para scans em chinês simplificado. A linha de comando é idêntica exceto que o caminho do bridge script entra depois do executável Python, e o idioma é fixado em chi_sim. O HotPDF distribui o bridge como tools/OCR/rapidocr_tsv.py; ele espera os pacotes rapidocr e onnxruntime mais três models ONNX locais, desabilita downloads automáticos de models, e escreve TSV no formato do Tesseract para que o lado Delphi não precise de um segundo parser. O nome de engine reportado no Info.EngineName é RapidOCR (local ONNX). Essa forma sugere a receita geral: qualquer reconhecedor que você consiga embrulhar num script pequeno que aceite a lista de argumentos estilo Tesseract e emita o TSV de doze colunas herda de graça o isolamento de handles, o timeout, o cancelamento, os budgets de output e o commit all-or-nothing. Os adapters são só para Windows, rodam uma página por vez de forma síncrona, e não fazem deskew nem pré-processamento da imagem além do que o renderizador produz, então a qualidade da imagem entrando ainda define o teto do que sai
Os adapters Tesseract e RapidOCR, o escritor de camada de texto invisível, o renderizador de páginas que os alimenta, e a extração de texto que verifica o resultado entram todos no mesmo componente VCL nativo para Delphi e C++Builder. Se você está acrescentando OCR a uma aplicação de captura ou arquivamento de documentos, o componente HotPDF PDF para Delphi te entrega o pipeline restando só a própria engine OCR para instalar