Artigo Técnico

OCR com a DLL Tesseract no HotPDF: chamar a API C de Delphi

O HotPDF corre o Tesseract dentro do seu processo Delphi através do HPDFCreateTesseractDLLOCREngine, uma fábrica acrescentada na v2.772.0 que carrega dinamicamente uma DLL compatível com Tesseract 5, comanda a sua API C (TessBaseAPIInit2, TessBaseAPIRecognize, o iterador de resultados) e devolve um IHPDFOCREngine. O THotPDF.ApplyLoadedOCRTextLayer usa esse engine para acrescentar uma camada de texto Unicode invisível e pesquisável a páginas PDF digitalizadas

O mesmo reconhecedor já era alcançável pelo adaptador externo tesseract.exe que escreve um BMP e analisa TSV. Esse caminho funciona, mas cada página paga um lançamento de processo, um ficheiro de bitmap temporário e um formato de texto sem baselines e sem controlo sobre a segmentação de página. Chamar a DLL remove os três. Também remove a parede de processo, o que significa que uma binding Pascal senta-se diretamente sobre estruturas C, booleanos C e strings alocadas em C. A maior parte do que vale a pena saber sobre este adaptador é onde essa binding pode correr mal em silêncio

Como correr o Tesseract in-process de Delphi com o HotPDF?

Correr o Tesseract in-process com o HotPDF leva uma chamada de fábrica na unidade HPDFTesseractRecognition e a mesma chamada ApplyLoadedOCRTextLayer que todo o engine OCR do HotPDF usa. A fábrica valida de imediato. O ficheiro da DLL e o diretório tessdata têm de existir, o identificador de língua só pode conter letras ASCII, dígitos, _ e +, cada modelo numa combinação como chi_sim+eng tem de ter um ficheiro .traineddata correspondente, e todos os 21 exports obrigatórios têm de resolver antes de o engine ser devolvido. Erros de configuração levantam EArgumentException; uma DLL que falhe ao carregar levanta EOSError com o código de erro do Windows e uma dica para verificar 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; dependências ficam ao lado
  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 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,
      ' words accepted, ', Info.DroppedWordCount, ' dropped');
    Doc.SaveLoadedDocument(TargetFile);
  finally
    Doc.Free;
  end;
end;

O THPDFTesseractOptions.Default põe PageSegMode em tpsAuto, EngineMode em temDefault, TimeoutMilliseconds em 60 000 e MaxPixels em 16 777 216. O orçamento de pixels interessa mais do que parece. Uma página US Letter aos 300 DPI por omissão renderiza para 2 550 × 3 300 pixels, cerca de 8,4 milhões, o que cabe. A mesma página a 600 DPI é 5 100 × 6 600, cerca de 33,7 milhões, e o adaptador recusa-a 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 procura para a pasta própria da DLL mais os diretórios seguros por omissão, por isso as bibliotecas de imagem de que o Tesseract depende podem viver ao lado dele sem tocar no PATH nem no diretório atual. O HotPDF não embala nem descarrega nenhum runtime ou modelo OCR; você provisiona ambos

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

O adaptador DLL troca isolamento de processo por output mais rico e menos overhead por página. Ambos os adaptadores ligam-se ao mesmo pipeline de camada de texto, por isso mapeamento de coordenadas, filtragem por confiança e a consolidação tudo-ou-nada são idênticos; o que difere é como os pixels entram e as palavras saem

AspetoAdaptador tesseract.exeAdaptador DLL Tesseract
FábricaHPDFCreateTesseractOCREngineHPDFCreateTesseractDLLOCREngine
Pixels a entrarFicheiro BMP num diretório temporário privadoBuffer em tons de cinzento de 8 bits em memória
Palavras a sairTSV ao nível de palavra, limitado a 64 MiBIterador de resultados, UTF-8 por palavra
BaselinesIndisponíveisPassadas através do TessPageIteratorBaseline
Segmentação de página e modo de engineSó segmentação automáticaTHPDFTesseractPageSegMode, THPDFTesseractEngineMode
TimeoutDuro: o processo filho é terminadoCooperativo: o Tesseract tem de notar
Isolamento de crashes e de memóriaProcesso separadoNenhum, partilha o seu espaço de endereçamento

Um custo não desaparece. Cada chamada Recognize cria a sua própria instância de API e chama TessBaseAPIInit2, por isso os modelos de língua são inicializados por página em vez de uma vez por engine. A cache de ficheiros do sistema operativo amortece o recarregamento, mas em conjuntos de modelos grandes multilingue continua a ser o custo fixo dominante por página, e conta para o prazo de reconhecimento. O engine DLL RapidOCR in-process toma o desenho oposto e mantém os seus modelos ONNX residentes pelo tempo de vida do engine; os problemas de fronteira (ABI C, buffers emprestados, trabalho nativo ininterruptível) são da mesma família

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

O Delphi não consegue espelhar com segurança o monitor de progresso do Tesseract porque o ETEXT_DESC contém campos internos dependentes da versão, por isso um registo copiado à mão põe o callback de cancelamento e o prazo nos offsets errados em algumas builds. Nada falha ruidosamente quando isso acontece. O Tesseract simplesmente lê o seu apontador de callback de um campo que agora guarda outra coisa, ou nunca chega a ver o prazo

O HotPDF trata por isso o monitor como um apontador opaco e toca nele só através de funções exportadas: TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc, TessMonitorSetDeadlineMSecs e TessMonitorDelete. Se você fizer a binding da API C você próprio para outro propósito, o mesmo padrão aplica-se. 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 registo ETEXT_DESC dependente da versão põe o callback de cancelamento e o prazo 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 sem exceções
um apontador opaco mais cinco exports é todo o contrato; o callback continua a ser um Boolean de um byte que só lê uma flag e um relógio
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
  // Corre na stack do Tesseract: leia flags e o relógio, nunca levante
  Result := (CancelThis = nil) or POCRJob(CancelThis)^.CancelRequested or
    (GetTickCount64 >= POCRJob(CancelThis)^.DeadlineTick);
end;

// Uso, com os apontadores de função 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 devolve Boolean, que é um byte tanto em Delphi como em Free Pascal, a condizer com o bool C no TessCancelFunc. O BOOL do Windows de quatro bytes 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 superiores do registo de retorno são o que lá ficou, e um false pode chegar como true. O mesmo header complica mais as coisas, porque funções como TessPageIteratorBoundingBox devolvem um int, que o HotPDF declara como Integer. Leia o tipo C de cada 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 a desenrolar através de frames C++ do Tesseract é comportamento indefinido, por isso o callback do HotPDF só lê o token de cancelamento e um valor GetTickCount64 monótono. O adaptador transforma o resultado num diagnóstico de cancelamento ou de timeout depois de o TessBaseAPIRecognize devolver, e faz essa verificação independentemente do código de retorno nativo

Que apontadores nativos são da posse do lado Delphi?

O adaptador DLL Tesseract do HotPDF é dono de três objetos nativos por pedido, a instância de API, o monitor e o iterador de resultados, e toma emprestado todo o resto. Cada chamada Recognize cria o seu próprio conjunto e liberta-o num bloco finally: TessResultIteratorDelete, depois TessMonitorDelete, depois TessBaseAPIDelete. Libertar a interface do engine descarrega a biblioteca

Posse de objetos da DLL Tesseract no HotPDF por chamada Recognize: o iterador de resultados, o monitor e a instância de API são possuídos e libertados por essa ordem dentro de finally, o iterador de página do TessResultIteratorGetPageIterator é uma vista emprestada que nunca deve ser libertada, e as strings do GetUTF8Text são copiadas e devolvidas via TessDeleteText
três objetos possuídos, todo o resto emprestado: liberte pela ordem fixa, nunca faça double-free do iterador de página, e nunca misture alocadores
  • O TessResultIteratorGetPageIterator devolve uma vista emprestada para dentro do iterador de resultados, não um objeto novo. O HotPDF usa-o para TessPageIteratorBoundingBox e TessPageIteratorBaseline e nunca o liberta; apagá-lo separadamente libertaria a mesma memória duas vezes
  • O TessResultIteratorGetUTF8Text devolve uma string alocada pelo runtime da própria DLL. O HotPDF copia-a e devolve-a através do TessDeleteText num bloco finally; um FreeMem Pascal libertá-la-ia na heap errada
  • O texto das palavras é descodificado com validação UTF-8 estrita e verificação de comprimento antes da conversão. Palavras com caracteres de controlo, UTF-8 malformado, caixas fora da imagem, retângulos invertidos ou confiança fora de 0–100 falham o pedido em vez de ser remendadas em silêncio
  • O texto total por pedido está limitado a 1 048 576 unidades de código UTF-16, e a contagem de palavras tem de caber no orçamento do pedido entregue pelo ApplyLoadedOCRTextLayer

A confiança chega como 0–100 e é escalada para 0–1, por isso o THPDFOCRTextLayerOptions.MinimumConfidence significa a mesma coisa para todos os engines. Quando o Tesseract reporta uma baseline, ambos os pontos são passados; caso contrário o pipeline da camada de texto recua para a sua estimativa geométrica, exatamente como faz para input TSV

Porque validar um enum antes de ele chegar à DLL?

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

O que garante afinal o timeout de reconhecimento?

O timeout da DLL Tesseract é cooperativo: o HotPDF consegue parar o seu próprio trabalho e pedir ao Tesseract que pare, mas não consegue forçar código nativo a devolver. O relógio começa quando o Recognize começa, por isso a conversão de bitmap e a inicialização de modelos consomem o mesmo orçamento que o reconhecimento. O HotPDF verifica o tempo decorrido e o token de cancelamento durante a conversão para tons de cinzento e entre palavras enquanto itera resultados, e passa os milissegundos restantes ao TessMonitorSetDeadlineMSecs antes de chamar TessBaseAPIRecognize

A falha está dentro da chamada nativa. O monitor do Tesseract é consultado durante o reconhecimento de palavras, não durante o TessBaseAPIInit2 ou a análise de layout da página, por isso um load de modelo lento ou um layout patológico pode passar do prazo antes de o timeout ser reportado. Os orçamentos de pixels e de output também não limitam o uso de memória da própria biblioteca nativa. Se precisa de um worker que possa matar, use o adaptador de processo; essa é a troca honesta, não uma funcionalidade em falta

Anatomia do timeout cooperativo da DLL Tesseract no HotPDF: o relógio começa quando o Recognize começa e cobre a conversão para tons de cinzento, o TessBaseAPIInit2 e a análise de layout, mas o monitor só é consultado durante o reconhecimento de palavras, por isso loads de modelos e layout podem ultrapassar o prazo antes de o HotPDF reportar otlsEngineError ou otlsCancelled
um prazo aqui é um pedido, não uma garantia: a inicialização e a análise de layout podem demorar muito, e um worker que se consiga matar de verdade precisa do adaptador de processo

A segmentação de página é onde o adaptador DLL vale o que custa em input difícil. Formulários, etiquetas e tabelas digitalizadas com campos espalhados frequentemente reconhecem 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 colunas montadas
  TessOptions.EngineMode := temLSTMOnly;     // precisa de modelos LSTM na tessdata
  TessOptions.TimeoutMilliseconds := 20000;  // inclui a inicialização de modelos
  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 já reconheceu todas as páginas selecionadas antes de começar a transação de consolidação, por isso 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 endireita um scan torto

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

Ambas as fábricas Tesseract funcionam em builds Free Pascal e Lazarus Windows Win32 e Win64 desde a v2.772.1, depois de duas correções específicas de FPC. Recompile primeiro o pacote Lazarus para a arquitetura alvo; a porta geral está coberta em HotPDF em Free Pascal e Lazarus Win64

A primeira correção diz respeito aos pixels. Um TBitmap da LCL escrito através de scanlines pode atualizar a sua imagem crua sem refrescar o handle de bitmap do Windows, por isso GetDIBits sobre esse handle devolve os pixels velhos. O sintoma era desconcertante: texto desenhado diretamente num bitmap era reconhecido, enquanto uma página renderizada pelo renderer PDF do HotPDF produzia uma lista de palavras vazia. Em FPC o adaptador lê agora um snapshot consciente do formato através do CreateIntfImage, que respeita o formato de pixels e a ordem de linhas da imagem crua. A build Delphi mantém o caminho GetDIBits sobre uma cópia privada de 24 bits. Nenhuma das builds modifica o bitmap do chamador

A segunda correção pertence ao adaptador tesseract.exe. O TStringList do FPC guarda strings ANSI, por isso atribuir texto TSV UTF-8 descodificado a Lines.Text deixava cair em silêncio cada carácter chinês ou do plano suplementar que a página de código ANSI do sistema não conseguisse representar. O caminho FPC agora mantém o TSV como bytes UTF-8, tira o BOM ao nível de bytes e descodifica cada palavra individualmente para UnicodeString. O adaptador DLL nunca teve este problema porque descodifica cada palavra diretamente do iterador

Referência rápida

  • Fábrica: HPDFCreateTesseractDLLOCREngine(LibraryPath, TessDataDirectory, Language[, Options]) em HPDFTesseractRecognition, acrescentada na v2.772.0, suporte FPC na v2.772.1
  • Predefinições: tpsAuto, temDefault, 60 000 ms, 16 777 216 pixels; intervalo de timeout 1–3 600 000 ms, teto de pixels 67 108 864
  • Faça a bitness da DLL corresponder à aplicação e coloque as DLLs de dependência ao lado da DLL Tesseract
  • Trate o monitor como opaco; nunca copie ETEXT_DESC para um registo Pascal
  • Declare o callback de cancelamento cdecl com um resultado Boolean de um byte, e nunca deixe uma exceção escapar dele
  • Liberte o texto do iterador com TessDeleteText; nunca liberte o iterador de página obtido do iterador de resultados
  • Espere que o prazo seja cooperativo: a inicialização de modelos e a análise de layout podem ultrapassá-lo
  • Use o adaptador tesseract.exe quando precisar de terminação dura ou isolamento de crashes

O adaptador DLL Tesseract, os adaptadores de processo e o engine OCR integrado vêm todos com o componente PDF Delphi HotPDF para Delphi, C++Builder e Free Pascal; veja a página de produto do HotPDF para edições e downloads