O HotPDF Component extrai texto Unicode de qualquer PDF que você carregar no Delphi por meio de duas chamadas: o ExtractLoadedPageText retorna o texto no fluxo de leitura de uma página, e o ExtractLoadedPageTextLayout (adicionado na v2.263.0) reconstrói a disposição visual da página como texto simples, de modo que colunas, recuos e alinhamentos de tabelas sobrevivam na saída. Ambos funcionam em documentos que o HotPDF não criou, que é o cenário que realmente importa: a fatura que um cliente lhe enviou por e-mail, o relatório que um escritório de digitalização entregou, ou o contrato gerado por um software que ninguém mais sabe o nome
Chegar lá exigiu mais estrutura do que as duas assinaturas sugerem, porque um PDF não armazena texto como um arquivo de texto. Este artigo descreve ambos os modos de extração e, em seguida, abre o capô sobre as três partes subjacentes — o leitor CMap, o intérprete de fluxo de conteúdo (content stream interpreter) e a cadeia de alternativas de decodificação de fontes (font decode fallback chain) — pois saber como o mapeamento funciona é a diferença entre dar de ombros para uma saída distorcida e diagnosticá-la
Por que a extração de texto é mais difícil do que ler strings diretamente do arquivo?
Um fluxo de conteúdo PDF (PDF content stream) registra códigos de caracteres, e não caracteres. Os operadores Tj e TJ (ISO 32000-1 §9.4.3) carregam cadeias de bytes cujo significado depende inteiramente da fonte selecionada pelo Tf precedente: o byte 0x41 pode ser a letra A sob WinAnsi, um glifo arbitrário em uma fonte de subconjunto ou a metade de um CID de dois bytes em uma fonte CJK composta. A ISO 32000-1 §9.10 define a extração de texto exatamente como esse problema de decodificação — mapeando cada código de volta para Unicode usando qualquer informação que o dicionário de fontes forneça — e a norma é explícita ao dizer que um arquivo em conformidade não é obrigado a fornecer informações suficientes para fazer isso
Essa última cláusula explica todo relatório de bug do tipo "por que copiar e colar deste PDF produz caracteres ilegíveis" que você já viu. Um gerador que incorpora uma fonte de subconjunto sem uma tabela /ToUnicode grava um arquivo que é renderizado perfeitamente, mas cuja extração resulta em bobagem, pois o mapeamento de código para glifo existe, mas o mapeamento de código para Unicode nunca foi enviado. Qualquer API de extração honesta é, portanto, uma cadeia de alternativas (fallbacks) de melhor esforço, e a pergunta útil é quão profunda é essa cadeia
Extração de fluxo de leitura com ExtractLoadedPageText
Para indexação de busca, correspondência de palavras-chave ou para alimentar de texto um pipeline de análise, o ExtractLoadedPageText é a chamada ideal. A assinatura é function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean — os índices de página começam do zero, o resultado chega como uma UnicodeString nativa do Delphi e a função retorna False quando a página não possui um fluxo de conteúdo legível, em vez de gerar uma exceção
var
Pdf: THotPDF;
PageCount, I: Integer;
PageText, AllText: UnicodeString;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('invoice.pdf');
AllText := '';
for I := 0 to PageCount - 1 do
if Pdf.ExtractLoadedPageText(I, PageText) then
AllText := AllText + PageText + #13#10;
// AllText now holds the reading-flow text of the document
finally
Pdf.Free;
end;
end;
As quebras de linha na saída vêm de uma heurística deliberadamente simples: quando a origem vertical de um glifo se move mais do que a metade do tamanho da fonte atual — a assinatura de uma instrução Td ou T* no fluxo de conteúdo — uma nova linha é inserida. Caracteres que o decodificador não consegue resolver tornam-se espaços em vez de desaparecer, de modo que os limites das palavras sobrevivam mesmo quando glifos individuais não o fazem. O que este modo não tenta realizar é o agrupamento de ordem de leitura ou a detecção de múltiplas colunas: uma página de duas colunas sai intercalada na ordem do fluxo de conteúdo, que é geralmente, mas nem sempre, a ordem visual
Quando você deve usar a extração que preserva o layout?
O ExtractLoadedPageTextLayout é a chamada correta sempre que a posição carrega significado: tabelas, formulários, listagens de código, qualquer coisa que você pretenda comparar (diff), pesquisar (grep) ou analisar por coluna. Em vez de achatar os glifos em um fluxo contínuo, ele os agrupa em linhas de base (baselines), ordena cada linha de base pelo eixo X e reproduz espaços em branco horizontais e verticais em uma grade de caracteres monoespaçados dimensionada a partir do avanço mediano do glifo e do tamanho da fonte. Grandes lacunas entre trechos na mesma linha de base tornam-se sequências de espaços; grandes lacunas entre linhas de base tornam-se linhas em branco. O resultado é lido da mesma forma que a página aparenta
var
Grid: UnicodeString;
begin
if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
TFile.WriteAllText('page1.txt', Grid, TEncoding.UTF8);
// Columns, indentation and table alignment survive as
// spaces and blank lines on a character grid
end;
Os dois modos compartilham cada byte do mecanismo de decodificação e diferem apenas em como organizam os glifos decodificados, portanto a escolha não custa nada em fidelidade. Escolha o ExtractLoadedPageText quando apenas as palavras importarem e o ExtractLoadedPageTextLayout quando a organização for relevante. A detecção de ordem de leitura de múltiplas colunas permanece fora do escopo para ambos — uma renderização em grade de uma página de duas colunas mostra ambas as colunas lado a lado, fielmente, o que para comparação (diff) é perfeitamente correto e para o refluxo de prosa não é
Como o HotPDF decodifica códigos de caracteres para Unicode?
O HotPDF Component resolve cada código de caractere por meio de uma cadeia de alternativas (fallback chain) ordenada por prioridade: a tabela /ToUnicode CMap incorporada da fonte primeiro, depois a entrada /Encoding (fluxo ou CMap nomeada), depois — para fontes compostas — os arquivos CMap padrão da Adobe para coleções de caracteres como Adobe-GB1, Adobe-CNS1, Adobe-Japan1 e Adobe-KR e, finalmente, as tabelas WinAnsi e MacRoman integradas para fontes simples. Uma estratégia que não consegue entregar uma resposta degrada-se silenciosamente para a próxima em vez de gerar uma exceção, e um código que esgota toda a cadeia é resolvido como 0 para que o chamador possa contar as falhas em vez de adivinhar
O /ToUnicode CMap (ISO 32000-1 §9.10.3) fica primeiro porque é o mapeamento que o gerador escreveu especificamente para extração. O caminho do CMap padrão da Adobe é importante para documentos CJK que usam CMaps predefinidos como o UniGB-UTF16-H em vez de incorporar qualquer coisa: o HotPDF fornece os arquivos de coleção em seu diretório resources\CMap, localiza-os em relação ao executável em tempo de execução e armazena em cache cada mapa analisado por processo — detalhe que vale a pena saber porque o maior deles, o mapa Adobe-GB1, tem cerca de 2 MB de texto de origem que você não deseja analisar novamente a cada página. Se o diretório estiver ausente, o decodificador simplesmente ignora os CMaps baseados em disco e trabalha com as tabelas incorporadas mais as codificações integradas. Este é o espelho do lado da leitura do problema de modelagem abordado na modelagem de texto de escrita complexa com o HotPDF, onde a mesma distinção entre código e glifo é enfrentada no momento da gravação
Duas armadilhas de sintaxe do CMap que vale a pena conhecer
Os arquivos CMap parecem trivialmente fáceis de analisar e não são, e dois detalhes respondem pela maioria das falhas do analisador (parser) na primeira tentativa. O primeiro é que a contagem de registros vem antes da palavra-chave da seção: uma seção lê 2 beginbfchar, não beginbfchar 2. Um analisador que espera a contagem depois da palavra-chave consome o número como um token perdido e depois encontra zero entradas em cada seção. A abordagem robusta — aquela que o leitor do HotPDF adotou — é ignorar a contagem inteiramente e fazer o loop até a palavra-chave correspondente endbfchar / endbfrange, o que tem a vantagem de tolerar arquivos do mundo real cujas contagens estão simplesmente incorretas
Duas armadilhas de sintaxe do CMap que vale a pena conhecer
A segunda armadilha é que os alvos de bfchar e bfrange são strings UTF-16BE, e não inteiros. O destino <D83DDE00> significa U+1F600 — um par substituto (surrogate pair) que deve ser recombinado em um único ponto de código — e a leitura desses quatro bytes como um inteiro big-endian produz um valor sem sentido em todos os pontos de código fora do Plano Multilíngue Básico. Emojis em PDFs não são mais exóticos, portanto um decodificador que ignora a recombinação de substitutos falha em arquivos que seus usuários realmente possuem. O HotPDF analisa o literal hexadecimal em bytes brutos primeiro e, em seguida, recombina as unidades de código UTF-16BE, o que também cobre os alvos de múltiplos caracteres que mapeamentos de ligaduras produzem
Descendo ao nível do glifo com o ExtractLoadedPageGlyphs
Ambas as chamadas de texto são baseadas no ExtractLoadedPageGlyphs, e a THPDFGlyphArray subjacente também está disponível para o seu código. Cada THPDFGlyphRecord carrega o ponto de código Unicode resolvido ao lado do código de caractere bruto, a largura de byte do código (1, 2 ou 4, decidida pelo codespacerange do CMap), a chave e o tamanho do recurso de fonte ativa, a origem X e Y no espaço do usuário e o avanço horizontal. Isso é suficiente para criar detecção de limite de palavras, destaque posicionado ou um algoritmo de layout personalizado sem você mesmo tocar no fluxo de conteúdo
var
Glyphs: THPDFGlyphArray;
I, Unresolved: Integer;
begin
if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
begin
Unresolved := 0;
for I := 0 to High(Glyphs) do
if Glyphs[I].Unicode = 0 then
Inc(Unresolved);
if Unresolved > 0 then
ShowMessageFmt('%d of %d glyphs have no Unicode mapping',
[Unresolved, Length(Glyphs)]);
end;
end;
Counting Unicode = 0 records, as above, is the honest way to measure extraction quality on a given document before you trust the text downstream. The glyph records also anchor each character to the source operand in the content stream, which is what makes HotPDF's loaded-document text search and replace possible on top of the same foundation
Quais PDFs não entregarão seu texto?
Alguns arquivos derrotam qualquer extrator, e é melhor detectá-los do que entregar sua saída. Documentos digitalizados são o caso mais evidente: uma página que é uma imagem grande não contém nenhum operador de texto, de modo que a extração retorna corretamente uma string vazia — a solução é OCR, e a extração de imagens de página do PDF carregado é a primeira etapa desse pipeline. Fontes de subconjunto sem uma tabela /ToUnicode são o caso mais difícil: se o caminho do /Encoding e os CMaps padrão também retornarem vazios, esses glifos se resolvem como 0 e surgem como espaços nas chamadas de texto. Documentos criptografados são extraídos normalmente, desde que você os carregue com sua senha por meio da sobrecarga do LoadFromFile, de modo que os fluxos sejam descriptografados antes que o intérprete chegue a vê-los
Um limite mais estreito vale ser exposto claramente: a cadeia de decodificação lê fluxos CMap e de conteúdo por meio do caminho Flate do HotPDF, de modo que uma fonte cujo fluxo ToUnicode use um fluxo incomum se degrade para a próxima estratégia em vez de falhar na página. Na prática, o FlateDecode cobre quase tudo o que foi produzido nas últimas duas décadas, e a degradação é silenciosa por design — você obtém o melhor texto que o arquivo permite, em vez de uma exceção. O mesmo mecanismo de objetos no lado da leitura que resolve dicionários de fontes aqui também viabiliza a edição de metadados em documentos carregados, permitindo que um pipeline de ingestão de documentos extraia, inspecione e faça anotações em uma única passagem
A extração de texto, renderização que preserva layout, acesso a nível de glifo e os recursos de busca e substituição baseados neles fazem parte do HotPDF Component padrão para Delphi e C++Builder — sem DLLs externas, sem serviços de texto do sistema operacional, apenas Object Pascal que você pode depurar linha por linha quando um arquivo estranho chegar na sua fila