Artigo Técnico

Extraindo Texto de Arquivos PDF com o PDFium Component em Delphi

A extração de texto de PDF parece simples até você encontrar um documento onde a camada de texto está ausente, corrompida ou dividida em dezenas de pequenos fluxos de caracteres sem nenhuma ordem lógica. O PDFium Component oferece dois pontos de entrada: a matriz Character[] para acesso bruto, baseado em índice, a cada glifo em uma página, e o ReadablePageContent para uma visualização estruturada que reconstrói parágrafos e títulos a partir da árvore de estrutura do PDF ou de análise heurística. Nenhum deles é a escolha certa para todas as situações, por isso é importante entender o que cada um expõe

Abertura do documento e a armadilha da falha silenciosa

O TPdf abre um arquivo configurando FileName e definindo Active := True. O detalhe crítico: Active := True nunca gera uma exceção. Se o arquivo estiver ausente, protegido por senha ou corrompido, o PDFium captura o erro internamente e o Active simplesmente permanece como False. Isso significa que cada loop de extração deve se proteger contra isso:

Pdf := TPdf.Create(nil);
try
  Pdf.FileName := 'report.pdf';
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    ShowMessage('Could not open PDF (damaged or wrong password)');
    Exit;
  end;
  // extraction follows here
finally
  Pdf.Active := False;
  Pdf.Free;
end;

Arquivos protegidos por senha precisam de Pdf.Password := '...' configurado antes de Active := True. Não há segunda chance: se o Active falhar, você deve fechar e reabrir com a senha correta

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

A abordagem de nível mais baixo percorre cada caractere em cada página. Defina Pdf.PageNumber para carregar a camada de texto dessa página e, em seguida, itere pelas entradas de CharacterCount usando a propriedade Character[]. Vale a pena verificar duas propriedades em cada entrada: CharacterGenerated[i] marca glifos sintéticos inseridos pelo renderizador (hifens suaves em quebras de linha, por exemplo) que não têm um valor Unicode real, e CharacterMapError[i] sinaliza que o PDFium não conseguiu mapear o glifo para um ponto de código, o que ocorre com codificações de fonte que carecem de uma 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;   // normalize CR to LF
      Line := Line + Ch;
    end;
    Output.Add(Line);
  end;
end;

O resultado é a uma string plana de pontos de código Unicode na ordem em que o PDFium os enumera, que é a ordem em que aparecem no fluxo de conteúdo, não necessariamente a ordem de leitura da esquerda para a direita. Para a maioria dos documentos em alfabeto latino produzidos por ferramentas de escritório padrão, isso funciona bem. Para PDFs digitalizados que passaram por OCR com sequências de glifos incomuns, ou para texto da direita para a esquerda, a ordenação pode estar incorreta. É nesses casos que o ReadablePageContent se torna mais útil

Extração estruturada com ReadablePageContent

O ReadablePageContent sobe um nível: ele retorna um registro TPdfReadableContent cuja matriz Fragments carrega fragmentos de conteúdo marcados, cada um com um Kind que identifica parágrafos, títulos, itens de lista, células de tabela e assim por diante. Quando o PDF possui uma árvore de estrutura (verifique Pdf.IsTagged), a origem é rosStructure e a ordem de leitura é autoritativa. Para arquivos não marcados, o PDFium recorre a rosHeuristic, que agrupa caracteres por suas caixas delimitadoras (bounding boxes) em unidades de leitura plausíveis, mas não garante precisã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 Content.Source = rosHeuristic e sua saída parecer distorcida, provavelmente a camada de texto do documento não foi gravada pensando na ordem de leitura. Nesse ponto, a única solução confiável é reexportar a partir do aplicativo de origem com a marcação adequada, ou executar uma etapa de pós-processamento que classifique as origens dos caracteres por Y e depois por X

O que CharacterOrigin e CharacterRectangle oferecem a você

Ambas as propriedades retornam a posição de um caractere no espaço da página (pontos, origem no canto inferior esquerdo, Y aumentando para cima). CharacterOrigin[i] é o ponto de ancoragem da linha de base do glifo; CharacterRectangle[i] é a caixa delimitadora completa. Esses são os blocos de construção para qualquer coisa além de texto simples: detectar limites de colunas, agrupar caracteres em linhas comparando coordenadas Y dentro de uma tolerância, ou construir um mapa de teste de clique (hit-test) para seleção de texto em um visualizador. Se você precisar descobrir qual caractere está sob o clique do mouse, o CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) faz essa busca diretamente, sem que você precise iterar pelos retângulos

Colocando a DLL no lugar

O PDFium Component delega toda a análise de PDF a uma DLL nativa, seja pdfium32.dll ou pdfium64.dll, dependendo da sua plataforma de destino. O componente vem com um script CopyDlls.bat que copia o arquivo correto para o diretório do sistema Windows. Executá-lo como Administrador uma vez na máquina de desenvolvimento é suficiente; para implantação, você deve copiar a DLL junto com o executável da aplicação. As variantes compatíveis com V8 (pdfium32v8.dll, pdfium64v8.dll) são consideravelmente maiores e necessárias apenas se os seus PDFs contiverem JavaScript que precise ser executado. Para extração de texto puro, a compilação padrão é a escolha certa

Se a DLL estiver ausente em tempo de execução, o Active := True falhará silenciosamente, assim como ocorre para um arquivo ausente, porque o componente captura o erro de carregamento internamente. Sempre teste em uma máquina limpa antes de distribuir

Usando FontSize[] ao lado de Character[] para análise de layout

Além do texto simples, a API em nível de caractere expõe FontSize[i], que retorna o tamanho do ponto renderizado de cada glifo. Combinado com CharacterOrigin[i] e CharacterRectangle[i], isso permite distinguir o texto do corpo dos cabeçalhos sem depender da árvore de estrutura. Um trecho de caracteres onde o tamanho da fonte salta acima de um limite é quase certamente um título em um documento não marcado. A mesma técnica se aplica à detecção de legendas (texto pequeno abaixo de uma caixa delimitadora de imagem) ou notas de rodapé (texto pequeno perto da parte inferior da página). Nada disso requer renderização; todas as três propriedades leem diretamente da camada de texto que o PDFium constrói durante o Active := True

Uma nuance: o FontSize[i] reflete o tamanho após a aplicação da CTM (matriz de transformação atual) da página, portanto, um documento onde o autor dimensionou a página inteira relatará tamanhos ajustados proporcionalmente. Se você estiver comparando tamanhos entre páginas com dimensões diferentes, normalize em relação à altura da MediaBox de cada página antes de tomar decisões de limite

Gravando a saída em um arquivo

A classe TStringList do Delphi gerencia saídas em UTF-8 de forma limpa desde a versão XE. Defina WriteBOM := False se precisar de um arquivo sem BOM (muitos sistemas de processamento subsequente engasgam com um BOM inicial):

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 muito grandes onde a memória é uma preocupação, grave diretamente em um TStreamWriter com TEncoding.UTF8 dentro do loop da página, em vez de acumular tudo em uma lista primeiro

As APIs Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent e CharacterIndexAtPos mostradas aqui fazem parte do PDFium Component para Delphi e C++Builder