Artigo Técnico

OCR com Tesseract DLL no HotPDF: a C API do Delphi

O HotPDF roda o Tesseract dentro do seu processo Delphi através do HPDFCreateTesseractDLLOCREngine, uma factory acrescentada na v2.772.0 que carrega dinamicamente uma DLL compatível com Tesseract 5, comandar a C API dela (TessBaseAPIInit2, TessBaseAPIRecognize, o result iterator) e retorna um IHPDFOCREngine. O THotPDF.ApplyLoadedOCRTextLayer usa essa engine para acrescentar uma camada de texto Unicode invisível e pesquisável a páginas de PDF escaneadas

O mesmo recognizer já era alcançável pelo adapter externo tesseract.exe que grava um BMP e parseia TSV. Esse caminho funciona, mas toda página paga por um launch de processo, um arquivo de bitmap temporário e um formato de texto sem baselines e sem controle sobre segmentação de página. Chamar a DLL elimina os três. Ela também elimina a parede de processo, o que significa que um binding Pascal senta direto em cima de estruturas C, booleans C e strings alocadas em C. Boa parte do que vale saber sobre este adapter é onde esse binding pode dar errado em silêncio

Como rodar o Tesseract in-process do Delphi com o HotPDF?

Rodar o Tesseract in-process com o HotPDF leva uma chamada de factory na unit HPDFTesseractRecognition e a mesma chamada de ApplyLoadedOCRTextLayer que toda engine OCR do HotPDF usa. A factory valida com antecedência. O arquivo DLL e o diretório tessdata precisam existir, o identificador de idioma só pode conter letras ASCII, dígitos, _ e +, todo model numa combinação como chi_sim+eng precisa ter um arquivo .traineddata correspondente, e todos os 21 exports requeridos precisam resolver antes de a engine ser devolvida. Erros de configuração levantam EArgumentException; uma DLL que falha ao carregar levanta EOSError com o error code do Windows e uma dica para conferir arquitetura e dependências

uses
  SysUtils, HPDFDoc, HPDFTesseractRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // Uma aplicação Win64 precisa de uma DLL de 64 bits; DLLs de dependência ficam ao lado dela
  Engine := HPDFCreateTesseractDLLOCREngine('C:\OCR\Win64\libtesseract-5.dll',
    'C:\OCR\tessdata', 'chi_sim+eng');   // THPDFTesseractOptions.Default
  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,
      ' words accepted, ', Info.DroppedWordCount, ' dropped');
    Doc.SaveLoadedDocument(TargetFile);
  finally
    Doc.Free;
  end;
end;

O THPDFTesseractOptions.Default seta PageSegMode para tpsAuto, EngineMode para temDefault, TimeoutMilliseconds para 60.000 e MaxPixels para 16.777.216. O orçamento de pixels importa mais do que parece. Uma página US Letter no default de 300 DPI renderiza em 2.550 × 3.300 pixels, uns 8,4 milhões, o que cabe. A mesma página a 600 DPI é 5.100 × 6.600, uns 33,7 milhões, e o adapter a recusa antes de o Tesseract ver um pixel. Suba o MaxPixels (o teto é 67.108.864) ou mantenha o DPI onde está; cada lado também é limitado a 32.767 pixels

A DLL é carregada com LoadLibraryEx usando as flags de busca para a própria pasta da DLL mais os default safe directories, então as image libraries de que o Tesseract depende podem morar ao lado dela sem tocar no PATH nem no diretório atual. O HotPDF não embarca nem baixa nenhuma runtime ou model OCR; você provisiona ambos

O que muda em comparação com o adapter tesseract.exe?

O adapter de DLL troca isolamento de processo por output mais rico e overhead por página menor. Ambos os adapters se plugam no mesmo pipeline de camada de texto, então mapeamento de coordenadas, filtragem por confidence e o commit all-or-nothing são idênticos; o que difere é como os pixels entram e as palavras saem

AspectoAdapter tesseract.exeAdapter Tesseract DLL
FactoryHPDFCreateTesseractOCREngineHPDFCreateTesseractDLLOCREngine
Pixels entrandoArquivo BMP num diretório temporário privadoBuffer grayscale de 8 bits em memória
Palavras saindoTSV em nível de palavra, limitado a 64 MiBResult iterator, UTF-8 por palavra
BaselinesNão disponíveisPassadas através do TessPageIteratorBaseline
Segmentação de página e engine modeSó segmentação automáticaTHPDFTesseractPageSegMode, THPDFTesseractEngineMode
TimeoutDuro: o processo filho é terminadoCooperativo: o Tesseract precisa notar
Isolamento de crash e memóriaProcesso separadoNenhum, compartilha o seu address space

Um custo não desaparece. Cada chamada de Recognize cria a própria instância de API dela e chama TessBaseAPIInit2, então os language models são inicializados por página em vez de uma vez por engine. O file cache do sistema operacional suaviza o reload, mas em conjuntos grandes de models multi-language ainda é o custo fixo dominante por página, e ele conta contra o recognition deadline. A engine de DLL RapidOCR in-process toma o design oposto e mantém os models ONNX dela residentes pelo lifetime da engine; os problemas de fronteira (C ABI, buffers emprestados, trabalho nativo ininterruptível) são da mesma família

Por que o Delphi não pode copiar a struct monitor do Tesseract?

O Delphi não pode espelhar com segurança o progress monitor do Tesseract porque o ETEXT_DESC contém campos internos dependentes de versão, então um record copiado à mão põe o cancel callback e o deadline nos offsets errados em alguns builds. Nada falha ruidosamente quando isso acontece. O Tesseract simplesmente lê o seu ponteiro de callback de um campo que agora guarda outra coisa, ou nunca vê o deadline

O HotPDF portanto trata o monitor como um ponteiro opaco e o toca só através de funções exportadas: TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc, TessMonitorSetDeadlineMSecs e TessMonitorDelete. Se você faz o binding da C API você mesmo para outro propósito, o mesmo padrão se aplica. O esqueleto abaixo é código de binding seu, não API do HotPDF, e espelha as declarações que o HotPDF usa internamente

Tratamento do monitor da DLL Tesseract no HotPDF: copiar o record ETEXT_DESC dependente de versão põe o cancel callback e o deadline em offsets errados e falha em silêncio, enquanto o HotPDF trata o monitor como opaco, comanda TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc e TessMonitorSetDeadlineMSecs, e mantém o callback cdecl livre de exceções
um ponteiro opaco mais cinco exports é todo o contrato; o callback continua sendo um Boolean de um byte que só lê uma flag e um clock
type
  // C: typedef bool (*TessCancelFunc)(void *cancel_this, int words);
  TTessCancelFunc = function(CancelThis: Pointer; Words: Integer): Boolean; cdecl;
  TTessMonitorCreate = function: Pointer; cdecl;   // ETEXT_DESC*, nunca desreferenciado
  TTessMonitorDelete = procedure(Monitor: Pointer); cdecl;
  TTessMonitorSetCancelFunc = procedure(Monitor: Pointer; Func: TTessCancelFunc); cdecl;
  TTessMonitorSetCancelThis = procedure(Monitor, CancelThis: Pointer); cdecl;
  TTessMonitorSetDeadlineMSecs = procedure(Monitor: Pointer; MSecs: Integer); cdecl;
  TTessBaseAPIRecognize = function(Handle, Monitor: Pointer): Integer; cdecl;

  TOCRJob = record
    CancelRequested: Boolean;
    DeadlineTick: UInt64;
  end;
  POCRJob = ^TOCRJob;

function ShouldCancel(CancelThis: Pointer; Words: Integer): Boolean; cdecl;
begin
  // Roda na stack do Tesseract: leia flags e o clock, nunca levante
  Result := (CancelThis = nil) or POCRJob(CancelThis)^.CancelRequested or
    (GetTickCount64 >= POCRJob(CancelThis)^.DeadlineTick);
end;

// Uso, com os function pointers resolvidos por GetProcAddress:
//   Monitor := MonitorCreate();
//   try
//     MonitorSetCancelThis(Monitor, @Job);
//     MonitorSetCancelFunc(Monitor, ShouldCancel);
//     MonitorSetDeadlineMSecs(Monitor, RemainingMs);
//     RC := BaseAPIRecognize(API, Monitor);
//   finally
//     MonitorDelete(Monitor);
//   end;

Dois detalhes nesse esqueleto são deliberados. O callback retorna Boolean, que é um byte tanto em Delphi quanto em Free Pascal, casando com o bool de C no TessCancelFunc. O BOOL de quatro bytes do Windows ou o LongBool do Delphi parece intercambiável e não é: quando um lado escreve um único byte e o outro lê quatro, os bytes altos do return register são o que quer que tenha sobrado ali, e um false pode chegar como true. O mesmo header complica mais ainda, porque funções como TessPageIteratorBoundingBox retornam um int, que o HotPDF declara como Integer. Leia o tipo C de todo valor de retorno em vez de assumir uma convenção para a API inteira

O segundo detalhe é que o callback nunca levanta. Uma exceção Delphi fazendo unwinding através dos frames C++ do Tesseract é undefined behavior, então o callback do HotPDF só lê o cancellation token e um valor monotônico de GetTickCount64. O adapter transforma o resultado num diagnóstico de cancelamento ou de timeout depois de TessBaseAPIRecognize retornar, e ele faz essa checagem independentemente do return code nativo

Quais ponteiros nativos o lado Delphi possui?

O adapter de DLL Tesseract do HotPDF é dono de três objetos nativos por request, a instância de API, o monitor e o result iterator, e empresta todo o resto. Cada chamada de Recognize cria o próprio conjunto dela e o libera num bloco finally: TessResultIteratorDelete, depois TessMonitorDelete, depois TessBaseAPIDelete. Liberar a interface da engine descarrega a library

Posse de objetos da DLL Tesseract no HotPDF por chamada Recognize: o result iterator, o monitor e a instância de API são de propriedade e liberados nessa ordem dentro do finally, o page iterator do TessResultIteratorGetPageIterator é uma view emprestada que nunca deve ser liberada, e strings de GetUTF8Text são copiadas e devolvidas via TessDeleteText
três objetos de propriedade, todo o resto emprestado: libere na ordem fixa, nunca faça double-free do page iterator, e nunca misture allocators
  • TessResultIteratorGetPageIterator retorna uma view emprestada dentro do result iterator, não um objeto novo. O HotPDF o usa para TessPageIteratorBoundingBox e TessPageIteratorBaseline e nunca o libera; deletá-lo separadamente liberaria a mesma memória duas vezes
  • TessResultIteratorGetUTF8Text retorna uma string alocada pela própria runtime da DLL. O HotPDF a copia e a devolve através do TessDeleteText num bloco finally; um FreeMem Pascal a liberaria no heap errado
  • O texto de palavra é decodificado com validação UTF-8 estrita e checagem de comprimento antes da conversão. Palavras com caracteres de controle, UTF-8 malformado, boxes fora da imagem, retângulos invertidos ou confidence fora de 0–100 falham o request em vez de serem silenciosamente remendadas
  • O texto total por request é limitado a 1.048.576 code units UTF-16, e a contagem de palavras precisa caber no budget do request handed down pelo ApplyLoadedOCRTextLayer

A confidence chega como 0–100 e é escalada para 0–1, então o THPDFOCRTextLayerOptions.MinimumConfidence significa a mesma coisa para toda engine. Quando o Tesseract reporta uma baseline, ambos os endpoints são passados através; caso contrário o pipeline de camada de texto cai para a estimativa geométrica dele, exatamente como faz para input TSV

Por que validar um enum antes de ele chegar à DLL?

O HotPDF copia o ordinal cru de PageSegMode e EngineMode para um Integer antes de checar a faixa, porque um compilador pode assumir que uma variável enum sempre guarda um valor declarado e dobrar Ord(X) > Ord(High(T)) para uma constante false. Os ordinais não são enfeite: o THPDFTesseractPageSegMode segue a numeração de page segmentation do Tesseract de 0 a 13, o THPDFTesseractEngineMode segue a numeração de engine mode de 0 a 3, e ambos vão para a DLL como inteiros simples. Um options record construído com FillChar, preenchido a partir de um stream, ou passado do C++Builder com um integer com cast pode carregar um byte como 200. Validar o ordinal copiado transforma isso num EArgumentException na hora da factory em vez de um modo indefinido dentro de código nativo. A factory também rejeita tpsOSDOnly e tpsAutoOnly, que não produzem palavras, e exige osd.traineddata para tpsAutoOSD e tpsSparseTextOSD

O que o timeout de recognition realmente garante?

O timeout da DLL Tesseract é cooperativo: o HotPDF pode parar o próprio trabalho dele e pedir ao Tesseract que pare, mas não pode forçar código nativo a retornar. O clock começa quando o Recognize começa, então conversão de bitmap e inicialização de models consomem o mesmo orçamento que o recognition. O HotPDF confere tempo decorrido e o cancellation token durante a conversão para grayscale e entre palavras enquanto itera resultados, e passa os milissegundos restantes ao TessMonitorSetDeadlineMSecs antes de chamar TessBaseAPIRecognize

O vão está dentro da chamada nativa. O monitor do Tesseract é consultado durante o word recognition, não durante TessBaseAPIInit2 nem a análise de page layout, então um load de model lento ou um layout patológico pode passar do deadline antes de o timeout ser reportado. Os orçamentos de pixel e de output também não limitam o uso de memória da própria native library. Se você precisa de um worker que possa matar, use o adapter de processo; esse é o trade-off honesto, não uma feature que falta

Anatomia do timeout cooperativo da DLL Tesseract no HotPDF: o clock começa quando o Recognize começa e cobre conversão grayscale, TessBaseAPIInit2 e análise de layout, mas o monitor só é consultado durante o word recognition, então loads de model e layout podem passar do prazo antes de o HotPDF reportar otlsEngineError ou otlsCancelled
um deadline aqui é um request, não uma garantia: init e análise de layout podem demorar, e um worker que você possa matar de verdade precisa do adapter de processo

Page segmentation é onde o adapter de DLL ganha o sustento dele em input difícil. Formulários, labels e tabelas escaneadas com campos espalhados costumam reconhecer melhor com tpsSparseText do que com segmentação automática, que tenta montar colunas e parágrafos que não estão lá

procedure OCRFormPages(Doc: THotPDF; const Pages: array of Integer);
var
  Engine: IHPDFOCREngine;
  TessOptions: THPDFTesseractOptions;
  LayerOptions: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  TessOptions := THPDFTesseractOptions.Default;
  TessOptions.PageSegMode := tpsSparseText;  // campos espalhados, sem montagem de colunas
  TessOptions.EngineMode := temLSTMOnly;     // precisa de models LSTM no tessdata
  TessOptions.TimeoutMilliseconds := 20000;  // inclui inicialização de models
  Engine := HPDFCreateTesseractDLLOCREngine('C:\OCR\Win64\libtesseract-5.dll',
    'C:\OCR\tessdata', 'eng+deu', TessOptions);

  LayerOptions := THPDFOCRTextLayerOptions.Default;
  LayerOptions.MinimumConfidence := 0.6;
  if not Doc.ApplyLoadedOCRTextLayer(Pages, Engine, LayerOptions, Info) then
    case Info.Status of
      otlsCancelled:
        Writeln('OCR cancelled, document unchanged');
      otlsEngineError:
        Writeln('Tesseract failed or timed out: ', string(Info.Diagnostic));
    else
      Writeln(string(Info.Diagnostic));
    end;
end;

Um timeout aparece como otlsEngineError com o diagnóstico Tesseract DLL OCR timed out, enquanto um token cancelado aparece como otlsCancelled. Em ambos os casos o ApplyLoadedOCRTextLayer reconheceu toda página selecionada antes de começar a transação de commit, então uma falha na página 40 de 50 deixa o documento carregado exatamente como estava. Note que tpsSingleLine, tpsSingleBlock e tpsSparseText mudam só a segmentação; nenhum deles endireita um scan torto

Free Pascal e Lazarus: pixels obsoletos e chinês perdido

Ambas as factories de Tesseract funcionam em builds Windows Free Pascal e Lazarus Win32 e Win64 desde a v2.772.1, depois de duas correções específicas de FPC. Reconstrua o package do Lazarus para a arquitetura alvo primeiro; a port em geral está coberta em HotPDF no Free Pascal e Lazarus Win64

A primeira correção diz respeito a pixels. Um TBitmap da LCL escrito através de scanlines pode atualizar a raw image dele sem refrescar o Windows bitmap handle, então o GetDIBits naquele handle retorna os pixels velhos. O sintoma era desconcertante: texto desenhado direto num bitmap era reconhecido, enquanto uma página renderizada pelo PDF renderer do HotPDF produzia uma lista de palavras vazia. No FPC o adapter agora lê um snapshot consciente de formato através do CreateIntfImage, que respeita o pixel format e a ordem de linhas da raw image. O build Delphi mantém o caminho GetDIBits numa cópia privada de 24 bits. Nenhum dos builds modifica o bitmap do caller

A segunda correção pertence ao adapter tesseract.exe. O TStringList do FPC guarda ANSI strings, então atribuir texto TSV UTF-8 decodificado a Lines.Text descartava silenciosamente todo caractere chinês ou do supplementary plane que a ANSI code page do sistema não podia representar. O caminho FPC agora mantém o TSV como bytes UTF-8, tira o BOM em nível de byte e decodifica cada palavra para UnicodeString individualmente. O adapter de DLL nunca teve esse problema porque decodifica cada palavra direto do iterator

Referência rápida

  • Factory: HPDFCreateTesseractDLLOCREngine(LibraryPath, TessDataDirectory, Language[, Options]) na HPDFTesseractRecognition, acrescentada na v2.772.0, suporte a FPC na v2.772.1
  • Defaults: tpsAuto, temDefault, 60.000 ms, 16.777.216 pixels; faixa de timeout 1–3.600.000 ms, teto de pixels 67.108.864
  • Casamento do bitness da DLL com a aplicação e colocação das DLLs de dependência ao lado da DLL Tesseract
  • Trate o monitor como opaco; nunca copie ETEXT_DESC para um record Pascal
  • Declare o cancel callback cdecl com resultado Boolean de um byte, e nunca deixe uma exceção escapar dele
  • Libere texto de iterator com TessDeleteText; nunca libere o page iterator obtido do result iterator
  • Espere que o deadline seja cooperativo: inicialização de models e análise de layout podem passá-lo
  • Use o adapter tesseract.exe quando você precisar de terminação dura ou isolamento de crash

O adapter de DLL Tesseract, os adapters de processo e a engine OCR embutida saem todos com o componente HotPDF PDF para Delphi, C++Builder e Free Pascal; veja a página de produto do HotPDF para edições e downloads