A extração de texto de PDF parece simples até se deparar com um documento onde a camada de texto está ausente, corrompida ou dividida por dezenas de pequenas sequências de caracteres sem qualquer ordem lógica. O PDFium VCL fornece dois pontos de entrada: o array Character[] para acesso direto e baseado em índices a cada glifo de uma página, e o ReadablePageContent para obter uma perspetiva estruturada que reconstrói parágrafos e cabeçalhos a partir da árvore de etiquetas (tag tree) do PDF ou através de análise heurística. Nenhum deles representa sempre a escolha ideal, pelo que é importante compreender o que cada um expõe.
Abrir o documento e a armadilha da falha silenciosa
O TPdf abre um ficheiro definindo a propriedade FileName e alterando o estado para Active := True. O detalhe crítico: a atribuição Active := True nunca gera exceções. Se o ficheiro estiver em falta, protegido por palavra-passe ou corrompido, o PDFium captura o erro internamente e a propriedade Active simplesmente permanece False. Isto significa que qualquer ciclo de extração de texto deve proteger-se desta forma:
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;
Ficheiros protegidos por palavra-passe necessitam que se defina Pdf.Password := '...' antes de alterar para Active := True. Não existe uma segunda oportunidade: assim que o Active falha, deve fechar e reabrir com a palavra-passe correta.
Extração página a página com o Character[]
A abordagem de nível mais baixo percorre cada caráter em cada página. Defina o Pdf.PageNumber para carregar a camada de texto dessa página e, em seguida, percorra os elementos de CharacterCount utilizando a propriedade Character[]. Vale a pena verificar dois sinalizadores (flags) em cada entrada: o CharacterGenerated[i] assinala glifos sintéticos inseridos pelo renderizador (hífens suaves em quebras de linha, por exemplo) que não têm valor Unicode real, e o CharacterMapError[i] indica que o PDFium não conseguiu mapear o glifo para um ponto de código, o que acontece 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 é uma string linear 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, e não necessariamente a ordem de leitura da esquerda para a direita. Para a maioria dos documentos com alfabeto latino produzidos por ferramentas padrão de escritório, este comportamento é adequado. Para PDFs digitalizados que foram processados com OCR com sequências de glifos invulgares, 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 o ReadablePageContent
O ReadablePageContent situa-se um nível acima: retorna um registo TPdfReadableContent cujo array Fragments contém fragmentos de conteúdo etiquetados, cada um com um Kind que identifica parágrafos, cabeçalhos, itens de lista, células de tabelas, entre outros. Quando o PDF contém uma árvore de estrutura (verifique através de Pdf.IsTagged), a origem é rosStructure e a ordem de leitura é autoritária. Para ficheiros sem etiquetas, o PDFium reverte para rosHeuristic, que agrupa caracteres pelas suas caixas delimitadoras em unidades de leitura plausíveis, mas sem garantia de 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 o seu texto parecer confuso, a camada de texto do documento provavelmente não foi gravada com a ordem de leitura em mente. Nesse ponto, a única solução fiável é exportar novamente a partir da aplicação de origem com a etiquetagem correta, ou executar um passo de pós-processamento que ordene os pontos de origem dos caracteres por Y e depois por X.
O que o CharacterOrigin e o CharacterRectangle disponibilizam
Ambas as propriedades retornam a posição de um caráter no espaço da página (pontos, com origem no canto inferior esquerdo e o Y a crescer para cima). O CharacterOrigin[i] representa o ponto de ancoragem da linha de base do glifo; o CharacterRectangle[i] representa a caixa delimitadora completa. Estes são os blocos de construção para qualquer funcionalidade além de texto simples: detetar limites de colunas, agrupar caracteres em linhas comparando coordenadas Y dentro de uma tolerância, ou construir um mapa de posicionamento para seleção de texto num visualizador. Se necessitar de encontrar qual o caráter que está sob o clique do rato, a função CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) efetua essa pesquisa diretamente sem ter de percorrer os retângulos manualmente.
Instalar a DLL no local correto
O PDFium VCL delega toda a análise de PDF para uma DLL nativa, pdfium32.dll ou pdfium64.dll, dependendo da sua plataforma de destino. O componente disponibiliza o script CopyDlls.bat que copia o ficheiro correto para a pasta de sistema do Windows. Executá-lo uma vez como Administrador na máquina de desenvolvimento é suficiente; para a distribuição, deve copiar a DLL juntamente com o executável da aplicação. As variantes com suporte a V8 (pdfium32v8.dll, pdfium64v8.dll) são substancialmente maiores e apenas necessárias se os seus PDFs contiverem JavaScript que precise de ser executado. Para extração simples de texto, a versão padrão é a escolha indicada.
Se a DLL estiver ausente em tempo de execução, a definição Active := True falhará silenciosamente da mesma forma que acontece com um ficheiro em falta, porque o componente captura o erro de carregamento internamente. Teste sempre num sistema limpo antes de distribuir a aplicação.
Utilizar o FontSize[] com o Character[] para análise de layout
Além de texto simples, a API ao nível do caráter expõe o FontSize[i], que retorna o tamanho em pontos renderizado de cada glifo. Combinado com o CharacterOrigin[i] e o CharacterRectangle[i], isto permite distinguir texto do corpo de cabeçalhos sem depender da árvore de estrutura. Uma sequência de caracteres onde o tamanho da fonte ultrapassa um determinado limite é quase certamente um cabeçalho num documento não etiquetado. A mesma técnica aplica-se à deteção de legendas (texto pequeno sob a caixa delimitadora de uma imagem) ou notas de rodapé (texto pequeno perto da margem inferior da página). Nenhuma destas leituras requer renderização; todas leem diretamente da camada de texto que o PDFium constrói durante a ativação.
Uma nuance: o FontSize[i] reflete o tamanho após a aplicação da matriz de transformação de corrente (CTM) da página, de modo que um documento onde o autor redimensionou a página inteira reportará tamanhos ajustados proporcionalmente. Se estiver a comparar tamanhos em páginas com dimensões diferentes, normalize em relação à altura do MediaBox de cada página antes de tomar decisões de limite.
Gravar a saída num ficheiro
O TStringList do Delphi gere a gravação de UTF-8 de forma limpa desde a versão XE. Defina WriteBOM := False se necessitar de um ficheiro sem a marca de ordem de bytes (BOM) (muitos sistemas a jusante esperam esta configuração):
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 seja uma preocupação, grave diretamente utilizando um TStreamWriter com TEncoding.UTF8 dentro do ciclo de páginas, em vez de acumular tudo numa lista primeiro.
As APIs Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent e CharacterIndexAtPos apresentadas aqui fazem parte do Componente PDFium VCL para Delphi e C++Builder.