Artigo Técnico

Adicionar uma Camada de Texto Pesquisável a PDF Digitalizados em Delphi

O PDFium Component adiciona uma camada de texto pesquisável a páginas PDF digitalizadas a partir do Delphi através de ApplyOcrSearchLayer. Renderiza cada página selecionada, entrega os pixels a um fornecedor de OCR fornecido pelo utilizador, e escreve as palavras reconhecidas de volta como objetos de texto invisíveis posicionados sobre as palavras na digitalização. A imagem original da página nunca é descodificada, recodificada ou substituída, pelo que o resultado visual é, byte a byte, a página com que se começou

O motor de reconhecimento não faz deliberadamente parte da biblioteca. O PDFium expõe renderização de página, mapeamento de coordenadas, carregamento de fontes, criação de objetos de texto e modos de renderização invisíveis, mas não contém nenhum motor de OCR, e fingir o contrário significaria incluir o produto de reconhecimento de outra empresa num componente PDF. Em vez disso, o reconhecimento reside atrás da interface IPdfOcrProvider: a biblioteca passa pixels BGRA de esquema fixo, com origem no topo, e o fornecedor devolve texto Unicode, valores de confiança e quadriláteros de palavras

O que é exatamente uma camada de texto pesquisável?

Um PDF digitalizado é uma fotografia de um documento. O conteúdo da página é uma única imagem grande, e não há nada para selecionar, pesquisar, copiar ou indexar. Uma camada de texto pesquisável acrescenta objetos de texto reais por cima dessa imagem, com o modo de renderização definido como invisível, pelo que os visualizadores não desenham nada, mas a seleção, a pesquisa e a extração encontram as palavras exatamente onde aparecem

O posicionamento é tudo o que importa. Se o texto invisível ficar alguns pontos desviado, os realces de seleção caem ao lado das palavras em vez de sobre elas, e copiar um parágrafo produz texto pela ordem errada. É por isso que a geometria tem de vir das mesmas transformações que o PDFium usa para renderizar a página, e não de uma estimativa proporcional

Implementar o fornecedor

O contrato do fornecedor é um único método. Recebe um registo de imagem de página que transporta dimensões, stride, DPI, formato de pixel e os próprios bytes de pixel, mais um token de cancelamento, e devolve palavras ou uma mensagem de erro:

uses
  PDFium;

type
  TMyOcrProvider = class(TInterfacedObject, IPdfOcrProvider)
  public
    function RecognizePage(const Image: TPdfOcrImage;
      const CancellationToken: IPdfCancellationToken;
      out Words: TPdfOcrWords; out ErrorMessage: string): Boolean;
  end;

function TMyOcrProvider.RecognizePage(const Image: TPdfOcrImage;
  const CancellationToken: IPdfCancellationToken;
  out Words: TPdfOcrWords; out ErrorMessage: string): Boolean;
var
  I: Integer;
begin
  // Image.Pixels contém linhas BGRA com origem no topo, de Image.Stride bytes.
  // Entregue-as ao seu motor, depois preencha uma entrada por palavra reconhecida
  SetLength(Words, RecognisedCount);
  for I := 0 to RecognisedCount - 1 do
  begin
    Words[I].Text := EngineWordText(I);
    Words[I].Confidence := EngineWordConfidence(I);   // 0..1
    Words[I].Quad := TPdfOcrQuad.FromRectangle(
      EngineLeft(I), EngineTop(I), EngineRight(I), EngineBottom(I));
  end;
  ErrorMessage := '';
  Result := True;
end;

Quadriláteros em vez de retângulos, porque uma digitalização raramente está perfeitamente alinhada com a página. Uma palavra numa página ligeiramente rodada ocupa um paralelogramo, e TPdfOcrQuad transporta quatro pontos de canto para que as palavras inclinadas e rodadas mantenham uma região de seleção rigorosa. Os motores que só reportam caixas alinhadas com os eixos podem usar FromRectangle, que constrói o quadrilátero degenerado

Por que razão as posições das palavras não podem ser escaladas proporcionalmente?

É tentador converter uma coordenada de pixel numa coordenada de página dividindo pela largura de renderização e multiplicando pela largura da página. Isso só funciona para páginas sem rotação, com uma CropBox idêntica à MediaBox, e uma origem em zero, e muitos documentos digitalizados falham pelo menos uma dessas condições

O PDFium Component mapeia cada um dos quatro cantos do quadrilátero individualmente através de FPDF_DeviceToPage, o mesmo mapeamento que o renderizador usou para produzir os pixels, pelo que as entradas /Rotate e as caixas de corte deslocadas são tratadas por construção. A matriz afim para o objeto de texto é depois construída a partir de três dos pontos mapeados, os cantos inferior esquerdo, inferior direito e superior esquerdo, o que é exatamente suficiente para exprimir posição, escala, rotação e inclinação

O próprio objeto de texto é criado com um tamanho de fonte unitário para que os seus limites de fonte reais possam ser medidos, e os limites do objeto medidos são depois mapeados para o quadrilátero alvo. Dimensionar por um tamanho de ponto estimado e esperar que corresponda à palavra digitalizada iria desviar-se a cada substituição de fonte; medir primeiro torna o ajuste independente da fonte que a camada usa

Executar sobre um documento

O registo de opções controla a resolução, a filtragem e todos os orçamentos. A filtragem por confiança importa mais do que parece: palavras de lixo com baixa confiança poluem os resultados de pesquisa permanentemente, e, ao contrário de uma renderização errada, ninguém repara até uma pesquisa devolver disparates:

var
  Pdf: TPdf;
  Options: TPdfOcrOptions;
  Report: TPdfOcrReport;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'scanned-contract.pdf';
    Pdf.LoadDocument;

    Options := TPdfOcrOptions.Default;
    Options.Dpi := 300;                  // resolução de reconhecimento
    Options.MinConfidence := 0.60;       // descartar palavras incertas
    Options.SkipPagesWithText := True;   // não tocar em páginas nativamente digitais
    Options.ContinueOnError := True;     // uma página má não pode parar o trabalho
    Options.MaxPixelsPerPage := 40 * 1000 * 1000;

    if Pdf.ApplyOcrSearchLayer(TMyOcrProvider.Create, Options, Report) then
      Pdf.SaveAs('scanned-contract-searchable.pdf');

    for I := 0 to High(Report.Pages) do
      if Report.Pages[I].Status = popsFailed then
        Writeln(Format('page %d failed: %s',
          [Report.Pages[I].PageNumber, Report.Pages[I].ErrorMessage]));
    Writeln(Format('%d word(s) inserted, %d rejected, %d page(s) skipped',
      [Report.InsertedWordCount, Report.RejectedWordCount,
       Report.SkippedPageCount]));
  finally
    Pdf.Free;
  end;
end;

SkipPagesWithText merece destaque em arquivos mistos. Um PDF que já transporta texto real, quer seja nativamente digital quer tenha sido processado anteriormente, recebe uma segunda camada de texto se o OCR correr sobre ele às cegas, e a duplicação faz com que a extração devolva cada palavra duas vezes. O estado por página popsSkippedExistingText diz exatamente quais páginas foram deixadas intocadas

Orçamentos, cancelamento e contenção de falhas

Toda a grandeza que um documento hostil ou simplesmente enorme possa inflacionar tem um teto: pixels por página e no total, palavras por página e no total, e carateres por palavra. Todos são verificados antes de a página ser escrita, não depois, e a estimativa de pixels é calculada a partir das dimensões da página e do DPI antes de qualquer bitmap ser alocado. Aumentar o DPI de 150 para 300 quadruplica a memória por página, pelo que o teto por página é o parâmetro a ajustar primeiro quando um trabalho em lote começa a falhar em formatos grandes

O token de cancelamento atravessa todo o percurso: a renderização progressiva, a chamada ao fornecedor e o ciclo de inserção por palavra. Isso significa que um utilizador que cancele durante o reconhecimento de um ficheiro de 400 páginas para dentro de uma única página, em vez de no fim do documento, e o mesmo padrão de token usado noutras partes do componente, descrito em renderização progressiva cancelável, aplica-se aqui sem alterações

A contenção de falhas é por página. A biblioteca recolhe os identificadores de objeto que inseriu numa página e chama FPDFPage_GenerateContent uma vez, depois de todas as palavras estarem colocadas. Se algo falhar a meio, seja um erro do fornecedor seja um problema de fonte, os objetos inseridos nessa página são removidos pela ordem inversa e o conteúdo da página é regenerado, pelo que uma página falhada reverte para o seu estado original em vez de manter meia camada de texto. O ciclo do documento continua então ou para, consoante ContinueOnError, e a página ativa é sempre restaurada

Verificar se a imagem realmente ficou intocada

A verificação mais forte disponível é também a mais simples: renderizar a página antes e depois de aplicar a camada, ao mesmo tamanho, e comparar os bitmaps. Devem ser idênticos byte a byte, porque o texto invisível não desenha nada e o fluxo de imagem nunca foi descodificado. Qualquer diferença significa que algo além da camada de texto alterou a página

Depois disso, verifique o lado do texto extraindo do ficheiro processado e confirmando que as posições das palavras caem sobre a digitalização. O percurso de extração é o mesmo descrito em extrair texto de documentos PDF, e para uma verificação visual rápida do alinhamento, renderizar páginas para imagens como em converter páginas PDF para JPEG permite sobrepor as caixas de palavras à digitalização

A camada de OCR, a renderização, a extração e a edição correm todas contra o mesmo objeto de documento em Delphi, C++Builder e Lazarus; a superfície completa da API está descrita na página do PDFium Component para Delphi