Artigo Técnico

Tesseract OCR para PDF pesquisável no Delphi com HotPDF

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

Pipeline OCR do HotPDF por página: renderiza a página no DPI configurado, salva input.bmp num diretório privado HotPDF-OCR, lança o processo filho do Tesseract com tessedit_create_tsv, parseia o TSV de doze colunas, filtra palavras por confidence, e efetiva a camada de texto invisível para todas as páginas pedidas ou nenhuma
O adapter só troca o reconhecimento: renderização, parse, validação e o commit all-or-nothing ficam no pipeline de camada de texto existente, então o código a jusante nunca muda

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

Seis gates que toda linha TSV do Tesseract passa no HotPDF: header exato de doze colunas, exatamente doze campos, só level 5, texto não vazio, um box dentro do bitmap, e confidence de 0 a 100 parseado invariamente, em que uma linha quebrada derruba a página inteira para zero palavras
Uma lista de palavras parcialmente parseada discordaria silenciosamente da imagem, então o parser recusa a página inteira na primeira linha malformada em vez de manter as palavras que já leu
// 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

Herança de handles do processo filho Tesseract no HotPDF: um CreateProcess comum com bInheritHandles passa todo handle herdável de arquivo, pipe e event ao filho, enquanto STARTUPINFOEX com PROC_THREAD_ATTRIBUTE_HANDLE_LIST limita o conjunto a um handle NUL para stdin e stdout mais o handle de arquivo do stderr
Sem a attribute list o filho mantém objetos sem relação vivos até sair, travando arquivos e famintendo pipes; com ela, só os dois handles listados cruzam a fronteira
// 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.tsv tem teto de 64 MiB e stderr.txt de 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, MaxTotalWords e MaxTextCodeUnits, e excedê-los falha a rodada em vez de truncar a lista de palavras
  • A saída padrão vai para NUL porque o Tesseract escreve o output.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 .traineddata está 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