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