A extração de texto em PDF parece simples até você se deparar com um documento no qual a camada de texto está ausente, corrompida ou dividida em dezenas de pequenos agrupamentos de caracteres sem ordem lógica. O PDFium VCL lhe oferece dois pontos de entrada: o vetor Character[] para um acesso bruto e baseado em índices a cada glifo de uma página, e o ReadablePageContent para uma visão estruturada que remonta os parágrafos e subtítulos partindo da árvore de estrutura nativa do PDF (tag tree) ou por meio de análise heurística. Nenhum deles é a escolha certa para tudo, por isso é importante entender o que cada um expõe
Abrindo o documento e a armadilha do erro silencioso
O componente TPdf carrega um arquivo ao definir FileName e acionar Active := True. A premissa central é que Active := True nunca lança uma exceção. Caso o arquivo esteja ausente, bloqueado com senha ou corrompido, o PDFium captura o erro internamente e o Active simplesmente permanece False. Isso significa que todos os loops de extração devem se proteger contra isso:
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 (danificado ou senha incorreta)');
Exit;
end;
// a extração prossegue aqui
finally
Pdf.Active := False;
Pdf.Free;
end;
Arquivos protegidos por senhas precisam que a propriedade Pdf.Password := '...' seja preenchida antes da chamada a Active := True. Não existe uma segunda chance: uma vez que Active venha a falhar, você fecha e reabre o documento 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 em CharacterCount usando a propriedade Character[]. Duas flags (sinalizadores) em cada entrada valem a pena verificar: CharacterGenerated[i] marca glifos sintéticos inseridos pelo renderizador (hífens suaves em quebras de linha, por exemplo) que não têm valor Unicode real, e CharacterMapError[i] indica que o PDFium não conseguiu mapear o glifo para um código (code point), 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; // normaliza CR para LF
Line := Line + Ch;
end;
Output.Add(Line);
end;
end;
O resultado é uma string linear de codepoints (pontos de código) Unicode na ordem em que o PDFium os enumera, que corresponde à ordem em que aparecem no fluxo de conteúdo (content stream), não necessariamente a ordem de leitura da esquerda para a direita. Para a maioria dos documentos com escrita latina gerados por ferramentas de escritório padrão, isso já é suficiente. Para PDFs digitalizados que sofreram OCR e apresentam sequências incomuns de glifos, ou para textos lidos da direita para a esquerda, a ordenação pode estar incorreta. É nesse momento em que ReadablePageContent se torna mais útil
Extração estruturada com ReadablePageContent
O ReadablePageContent sobe um nível: ele devolve um registro TPdfReadableContent cuja matriz (array) Fragments carrega fragmentos de conteúdo com marcação (tagged), e a cada qual associada a um Kind que identifica parágrafos, cabeçalhos, itens de lista, células de tabela e assim por diante. Quando o PDF carrega uma árvore de estrutura (verifique Pdf.IsTagged), a fonte é o rosStructure e a ordem de leitura é definitiva e autorizada. Para arquivos sem essas marcações, o PDFium recorre a rosHeuristic, que agrupa caracteres por suas caixas delimitadoras em unidades de leitura razoáveis, mas sem garantir precisão total
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 truncada ou embaralhada, a camada de texto do documento provavelmente não foi criada pensando em uma ordem de leitura. A partir daí, a única correção segura repousa em reexportar a partir da aplicação de origem com os devidos recursos estruturais, ou na execução de um passo de pós-processamento que classifique as origens dos caracteres por Y e, a seguir, por X
O que CharacterOrigin e CharacterRectangle entregam a você
Ambas as propriedades retornam a posição de um caractere no espaço da página (em pontos, com a origem no canto inferior esquerdo e o eixo Y crescendo para cima). CharacterOrigin[i] é o ponto de ancoragem base (baseline) do glifo; CharacterRectangle[i] é a caixa delimitadora completa. Esses são os blocos construtivos para ir além do texto simples: detectar os limites das colunas, agrupar caracteres em linhas comparando coordenadas Y dentro de uma tolerância, ou construir um mapa de colisão (hit-test) para seleção de texto em um visualizador. Se você precisa achar qual caractere fica sob um clique do mouse, CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) faz essa busca diretamente sem que você precise iterar todos os retângulos
Configurando a DLL no lugar certo
O PDFium VCL delega todo o processo de parsing do PDF para uma DLL nativa, seja a pdfium32.dll ou pdfium64.dll, de acordo com sua plataforma-alvo. O componente entrega o script CopyDlls.bat para copiar o arquivo correto para o diretório do sistema Windows. Executá-lo como Administrador uma única vez na máquina de desenvolvimento é suficiente; para o estágio de implantação (deploy), você deve simplesmente copiar a DLL ao lado do executável da aplicação. As variantes compatíveis com o V8 (pdfium32v8.dll, pdfium64v8.dll) são expressivamente maiores em tamanho e são necessárias apenas caso os seus arquivos PDF carreguem JavaScript que precise executar. Para fins puramente associados a extração textual, a compilação padrão é a escolha exata e ideal
Se a DLL estiver ausente em tempo de execução, Active := True falhará silenciosamente, da mesma forma que ocorreria para um arquivo ausente, já que o componente retém o erro de carregamento de forma interna. Teste rigorosamente os documentos em uma máquina limpa antes de despachar e enviar o projeto aos seus usuários
Usando FontSize[] em conjunto com Character[] na análise de layout
Indo além do texto simples, a API em nível de caractere expõe FontSize[i], que retorna o tamanho de ponto renderizado de cada glifo. Combinado com CharacterOrigin[i] e CharacterRectangle[i], isso permite que você diferencie o texto do corpo das manchetes e títulos, sem precisar se apoiar à árvore de estruturas. Uma linha (run) de caractere onde o tamanho da fonte supera um limiar será sem dúvida um cabeçalho/título em um arquivo carente de tags de marcação (untagged). A mesma técnica é aplicável na detecção de legendas de imagens (texto pequeno abaixo de uma caixa delimitadora de imagem) ou notas de rodapé (texto pequeno nas imediações da parte inferior da página). Nada disso impõe uso de renderização visual; as três propriedades efetuam leitura a partir da camada do texto estruturada pelo PDFium de maneira interna, assim que se aciona Active := True
Um pequeno detalhe: a medição através de FontSize[i] ilustra a métrica pós-aplicação da CTM (matriz de transformação atual) na página, logo, em um documento onde o autor optou por dimensionar (scale) a folha de maneira abrangente, informará os tamanhos escalonados proporcionalmente. Ao comparar dimensões entre páginas portando larguras ou extensões diversas no enquadramento fático, você precisará providenciar a normalização do parâmetro adotando como lastro comparativo as alturas das caixas em MediaBox contidas na página antes de balizar por limites fixos (thresholds)
Gravando a saída em um arquivo
A classe TStringList do Delphi lida muito bem com saídas em UTF-8 desde a versão XE. Defina WriteBOM := False se precisar de um arquivo isento de BOM (pois muitos consumidores rejeitam a marcação e falham nesse trâmite de abertura de arquivo):
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 arquivos muito volumosos em que a conservação da memória torne-se uma pauta mandatória, grave em vias diretas num TStreamWriter aliado ao TEncoding.UTF8 de dentro do seu próprio loop associado às folhas, no lugar de acumular e condensar todo o conteúdo na lista textual primeiramente e apenas a posteriori efetivar a gravação
As APIs Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent, e CharacterIndexAtPos demonstradas aqui fazem parte do PDFium VCL Component para Delphi e C++Builder