Artigo Técnico

Extração de Texto de Ficheiros PDF com o PDFium Component em Delphi

A extração de texto em PDF parece simples até se deparar com documentos sem camada de texto, com dados corrompidos ou fragmentados em sequências de caracteres sem ordem lógica. O PDFium Component disponibiliza duas abordagens: o array Character[] para acesso de baixo nível indexado a cada elemento gráfico (glyph) da página, e o método ReadablePageContent para leitura estruturada que reconstrói parágrafos e cabeçalhos a partir da árvore de etiquetas (tags) ou análise heurística. Conhecer a finalidade de cada método é essencial para escolher a lógica adequada

Abertura do documento e tratamento de falhas silenciosas

O componente TPdf abre o ficheiro definindo FileName e aplicando Active := True. Detalhe relevante: a atribuição Active := True não gera exceções. Se o ficheiro estiver em falta, encriptado com palavra-passe ou corrompido, o PDFium gere a falha internamente e a propriedade Active permanece False. Desta forma, as rotinas de extração devem validar este estado:

Pdf := TPdf.Create(nil);
try
  Pdf.FileName := 'report.pdf';
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    ShowMessage('Não foi possível abrir o PDF (ficheiro corrompido ou palavra-passe incorreta)');
    Exit;
  end;
  // a extração de texto segue aqui
finally
  Pdf.Active := False;
  Pdf.Free;
end;

Ficheiros protegidos com palavra-passe requerem a definição prévia de Pdf.Password := '...' antes de aplicar Active := True. Caso a validação de Active falhe, deverá fechar e reabrir o documento com a palavra-passe correta

Extração página a página com Character[]

O método de baixo nível analisa individualmente os caracteres de cada página. Defina a propriedade Pdf.PageNumber para carregar os dados textuais da página e percorra os elementos até ao limite de CharacterCount através de Character[]. Convém validar dois sinalizadores em cada iteração: CharacterGenerated[i] identifica caracteres sintéticos inseridos pelo renderizador (ex: hífenes em quebras de linha) sem correspondência Unicode real, e CharacterMapError[i] indica que o PDFium não conseguiu mapear o elemento gráfico para um Unicode (frequente em codificações de fonte sem tabela ToUnicode)

procedure ExtractAllText(Pdf: TPdf; Output: TStrings);
var
  Page, I: Integer;
  Line: string;
  Ch: WideChar;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := Page;
    Line := '';
    for I := 0 to Pdf.CharacterCount - 1 do
    begin
      if Pdf.CharacterGenerated[I] or Pdf.CharacterMapError[I] then
        Continue;
      Ch := Pdf.Character[I];
      if Ch = #13 then
        Ch := #10;   // normalizar CR para LF
      Line := Line + Ch;
    end;
    Output.Add(Line);
  end;
end;

O resultado consiste numa string simples de caracteres Unicode ordenados conforme a leitura lógica do PDFium (a ordem em que surgem no stream de conteúdo), não correspondendo obrigatoriamente à ordem visual de leitura da esquerda para a direita. Para a maioria dos documentos ocidentais gerados por ferramentas comuns, esta ordem é adequada. Em PDFs digitalizados processados por OCR com sequências não padrão ou textos com escrita da direita para a esquerda, a ordenação pode apresentar falhas. Nesses cenários, recomenda-se adotar o método ReadablePageContent

Extração estruturada com ReadablePageContent

O método ReadablePageContent opera a um nível superior: retorna um registo TPdfReadableContent cujo array Fragments contém blocos de conteúdo etiquetados, com a propriedade Kind a identificar parágrafos, cabeçalhos, tabelas, itens de listas, etc. Se o documento possuir uma estrutura lógica de tags (verificável via Pdf.IsTagged), a leitura adota a origem rosStructure (cuja ordem de leitura é fidedigna). Em documentos sem tags, o PDFium recorre a rosHeuristic, agrupando os caracteres com base em caixas delimitadoras lógicas sem garantia absoluta de exatidão

procedure ExtractStructured(Pdf: TPdf; Output: TStrings);
var
  Page: Integer;
  Content: TPdfReadableContent;
  Fragment: TPdfContentFragment;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Content := Pdf.ReadablePageContent(Page);
    for Fragment in Content.Fragments do
    begin
      case Fragment.Kind of
        cfHeading   : Output.Add('# ' + Fragment.Text);
        cfParagraph : Output.Add(Fragment.Text);
        cfListItem  : Output.Add('- ' + Fragment.Text);
      else
        Output.Add(Fragment.Text);
      end;
    end;
  end;
end;

Se a origem for rosHeuristic e o texto resultante surgir misturado, a camada de texto original não foi gerada respeitando a ordem de leitura. Nestes casos, a solução requer a reexportação do documento original com marcações de tags corretas ou um tratamento pós-extração que ordene as coordenadas dos caracteres por Y e depois por X

Coordenadas e áreas delimitadoras de caracteres

Ambas as propriedades indicam a localização do carácter na página (em pontos, com origem no canto inferior esquerdo e eixo Y crescente para o topo). CharacterOrigin[i] indica a coordenada de ancoragem na linha de base e CharacterRectangle[i] define a caixa delimitadora (bounding box). Estes dados suportam análises complexas: identificação de colunas, agrupamento de linhas por comparação de Y dentro de uma tolerância ou desenho de seleções de texto na interface. Para localizar o carácter sob as coordenadas do rato, o método CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) efetua a pesquisa sem necessidade de percorrer as caixas manualmente

Distribuição e carregamento das DLLs nativas

O PDFium Component delega o processamento em DLLs nativas (pdfium32.dll ou pdfium64.dll em função da plataforma). O componente inclui a rotina CopyDlls.bat que copia os ficheiros para a diretoria de sistema do Windows. Executá-lo como Administrador no ambiente de desenvolvimento é suficiente, devendo em produção copiar a DLL para a pasta do executável da aplicação. As variantes com motor V8 (pdfium32v8.dll, pdfium64v8.dll) são maiores e necessárias apenas para execução de JavaScript em formulários, sendo a DLL padrão a recomendada para extração simples de texto

Se a DLL nativa não estiver disponível, a chamada Active := True falhará silenciosamente, tal como em ficheiros inexistentes. Valide a distribuição das bibliotecas antes do deploy da aplicação

Utilização de FontSize[] e Character[] em análises de layout

Adicionalmente, a API disponibiliza a propriedade FontSize[i] que indica o tamanho do carácter em pontos. Em conjunto com CharacterOrigin[i] e CharacterRectangle[i], permite distinguir o corpo do texto de títulos em documentos sem tags. Variações bruscas no tamanho dos caracteres identificam títulos, rodapés ou legendas. Estas informações são lidas diretamente da camada de texto sem processamentos gráficos ou renderização

Nota técnica: o valor de FontSize[i] reflete a dimensão após a aplicação da matriz de transformação (CTM) da página. Se o documento contiver páginas redimensionadas globalmente, os valores de tamanho serão ajustados proporcionalmente. Em comparações entre páginas com dimensões distintas, normalize os valores contra a altura da MediaBox da página

Gravar a informação extraída em ficheiro

A classe TStringList do Delphi suporta exportações UTF-8 de forma nativa. Defina WriteBOM := False para evitar a escrita de marcas BOM (aspeto que inviabiliza leituras noutras aplicações):

var
  Lines: TStringList;
begin
  Lines := TStringList.Create;
  try
    ExtractAllText(Pdf, Lines);
    Lines.WriteBOM := False;
    Lines.SaveToFile('output.txt', TEncoding.UTF8);
  finally
    Lines.Free;
  end;
end;

Para documentos grandes onde o uso de memória é relevante, grave o fluxo diretamente via TStreamWriter com codificação TEncoding.UTF8 no ciclo de leitura da página em vez de acumular os dados em memória

As rotinas de gestão e análise de texto Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent e CharacterIndexAtPos integram o PDFium Component para Delphi e C++Builder