O HotPDF Delphi Component extrai texto Unicode de qualquer PDF que você carregue no Delphi por meio de duas chamadas: ExtractLoadedPageText devolve o texto da página em ordem de leitura, e ExtractLoadedPageTextLayout (incluída na v2.263.0) reconstrói a disposição visual da página como texto puro, de modo que colunas, recuos e alinhamento de tabela sobrevivem na saída. As duas funcionam em documentos que o HotPDF não criou, que é o caso que realmente importa: a nota fiscal que um cliente mandou por e-mail, o relatório que o birô de digitalização entregou, o contrato gerado por um software que ninguém consegue mais nomear
Chegar até aí exigiu mais maquinaria do que as duas assinaturas sugerem, porque um PDF não guarda texto como um arquivo de texto guarda. Este artigo percorre os dois modos de extração e depois abre o capô das três peças que ficam embaixo — o leitor de CMap, o interpretador de content stream e a cadeia de fallback de decodificação de fontes — porque saber como o mapeamento funciona é a diferença entre dar de ombros para uma saída ilegível e diagnosticá-la
Por que extrair texto é mais difícil do que ler strings do arquivo?
Um content stream de PDF registra códigos de caractere, não caracteres. Os operadores Tj e TJ (ISO 32000-1 §9.4.3) carregam sequências de bytes cujo significado depende inteiramente da fonte selecionada pelo Tf anterior: 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 extração de texto exatamente como esse problema de decodificação — mapear cada código de volta para Unicode usando as informações que o dicionário da fonte fornecer — e a norma é explícita ao dizer que um arquivo em conformidade não é obrigado a fornecer informação suficiente para isso
Essa última cláusula explica todo relatório de bug do tipo "por que copiar e colar deste PDF produz caracteres sem sentido" que você já viu. Um produtor que incorpora uma fonte de subconjunto sem tabela /ToUnicode escreveu um arquivo que renderiza perfeitamente e extrai como lixo, porque o mapeamento de código para glifo existe, mas o mapeamento de código para Unicode nunca foi enviado. Toda API de extração honesta é, portanto, uma cadeia de fallbacks de melhor esforço, e a pergunta útil é até onde essa cadeia vai
Extração em ordem de leitura com ExtractLoadedPageText
Para indexação de busca, correspondência de palavras-chave ou alimentar um pipeline de análise, ExtractLoadedPageText é a chamada que você quer. A assinatura é function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean — os índices de página começam em zero, o resultado chega como uma UnicodeString nativa do Delphi, e a função devolve False quando a página não tem um content stream legível, em vez de levantar 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 agora contém o texto do documento em ordem de leitura
finally
Pdf.Free;
end;
end;
As quebras de linha na saída vêm de uma regra geométrica deliberadamente simples. Desde o HotPDF v2.766.79, uma nova linha é inserida quando a origem do glifo se move na direção da escrita mais do que metade da altura do texto, tomada como o maior entre a altura da caixa de ascendente a descendente deste glifo e a do glifo anterior, em unidades de página. Antes dessa release, o deslocamento vertical era comparado com metade do tamanho de fonte dado ao Tf, que costuma ser 1 em documentos que dimensionam o texto pela matriz de texto, então um superscrito elevado começava uma nova linha e texto rotacionado colocava cada caractere em sua própria linha. Os espaços entre palavras seguem uma regra correspondente desde a v2.766.76: quando um produtor separa palavras movendo a posição do texto, com um ajuste TJ ou um Td, em vez de exibir um caractere de espaço, um espaço é acrescentado quando a lacuna ao longo da linha de base depois da largura do próprio glifo anterior excede 0.15 da altura da caixa do glifo, e nunca entre dois caracteres chineses, japoneses ou coreanos. Os caracteres que o decodificador não consegue resolver viram espaços em vez de sumirem, então os limites de palavra sobrevivem mesmo quando glifos individuais não sobrevivem. O que esse modo não tenta fazer é agrupamento por ordem de leitura nem detecção de múltiplas colunas: uma página de duas colunas sai intercalada na ordem do content stream, que costuma ser, mas nem sempre é, a ordem visual
Quando você deve usar a extração que preserva o layout?
ExtractLoadedPageTextLayout é a chamada certa sempre que a posição carrega significado: tabelas, formulários, listagens de código, qualquer coisa que você pretenda comparar com diff, filtrar com grep ou analisar por coluna. Em vez de achatar os glifos em um fluxo, ela os agrupa em linhas de base, ordena cada linha de base por X e reproduz o espaço em branco horizontal e vertical em uma grade monoespaçada de caracteres dimensionada a partir do avanço mediano dos glifos e do tamanho da fonte. Intervalos largos entre trechos na mesma linha de base viram sequências de espaços; intervalos grandes entre linhas de base viram linhas em branco. O resultado se lê como a página se parece
var
Grid: UnicodeString;
begin
if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
TFile.WriteAllText('page1.txt', Grid, TEncoding.UTF8);
// Colunas, recuos e alinhamento de tabela sobrevivem como
// espaços e linhas em branco em uma grade de caracteres
end;
Os dois modos compartilham cada byte da maquinaria de decodificação e diferem apenas em como organizam os glifos decodificados, então a escolha não custa nada em fidelidade. Escolha ExtractLoadedPageText quando só as palavras importam e ExtractLoadedPageTextLayout quando a disposição importa. A detecção de ordem de leitura em múltiplas colunas continua fora de escopo para os dois — uma renderização em grade de uma página de duas colunas mostra as duas colunas lado a lado, fielmente, o que para diff é exatamente o certo e para refluir a prosa não é
Como o HotPDF decodifica códigos de caractere para Unicode?
O HotPDF Delphi Component resolve cada código de caractere por uma cadeia de fallback ordenada por prioridade: primeiro o CMap /ToUnicode incorporado na fonte, depois a entrada /Encoding (stream ou CMap nomeado), depois — para fontes compostas — os arquivos de CMap padrão da Adobe para coleções de caracteres como Adobe-GB1, Adobe-CNS1, Adobe-Japan1 e Adobe-KR, e por fim as tabelas internas WinAnsi e MacRoman para fontes simples. Uma estratégia que não consegue entregar uma resposta degrada silenciosamente para a seguinte, em vez de levantar exceção, e um código que esgota a cadeia inteira resolve para 0, de modo que quem chama pode contar as falhas em vez de adivinhar
O CMap /ToUnicode (ISO 32000-1 §9.10.3) vem primeiro porque é o único mapeamento que o produtor escreveu especificamente para extração. O caminho dos CMaps padrão da Adobe importa para documentos CJK que usam CMaps predefinidos como UniGB-UTF16-H em vez de incorporar qualquer coisa: o HotPDF distribui os arquivos das coleções no diretório resources\CMap, localiza-os em tempo de execução em relação ao executável e mantém em cache cada mapa analisado por processo — vale saber, porque o maior deles, o mapa Adobe-GB1, tem cerca de 2 MB de texto-fonte que você não quer reanalisar a cada página. Se o diretório não existir, o decodificador simplesmente pula os CMaps em disco e trabalha com as tabelas incorporadas mais as codificações internas. Este é o espelho, do lado da leitura, do problema de shaping tratado em shaping de texto de escritas complexas com HotPDF, onde a mesma distinção entre código e glifo aparece na hora da escrita
Duas armadilhas da sintaxe de CMap que vale conhecer
Arquivos de CMap parecem trivialmente analisáveis e não são, e dois detalhes respondem pela maioria das falhas de parser na primeira tentativa. A primeira é que a contagem de registros vem antes da palavra-chave da seção: uma seção se lê 2 beginbfchar, não beginbfchar 2. Um parser que espera a contagem depois da palavra-chave consome o número como um token perdido e então encontra zero entradas em todas as seções. A abordagem robusta — a que o leitor do HotPDF adotou — é ignorar a contagem por completo e iterar até a palavra-chave endbfchar / endbfrange correspondente, o que tem o bônus de tolerar arquivos do mundo real cujas contagens estão simplesmente erradas
A segunda armadilha é que os destinos de bfchar e bfrange são strings UTF-16BE, não inteiros. O destino <D83DDE00> significa U+1F600 — um par substituto que precisa ser recombinado em um único ponto de código — e ler esses quatro bytes como um inteiro big-endian produz um valor sem sentido em todo ponto de código fora do Plano Multilíngue Básico. Emoji em PDFs já não são exóticos, então um decodificador que pula a recombinação de pares substitutos falha em arquivos que os seus usuários realmente têm. O HotPDF analisa o literal hexadecimal em bytes brutos primeiro e depois recombina as unidades de código UTF-16BE, o que também cobre os destinos de múltiplos caracteres que os mapeamentos de ligadura produzem
Descendo ao nível do glifo com ExtractLoadedPageGlyphs
As duas chamadas de texto são construídas sobre ExtractLoadedPageGlyphs, e o THPDFGlyphArray subjacente também está disponível para o seu código. Cada THPDFGlyphRecord carrega o ponto de código Unicode resolvido junto do código de caractere bruto, a largura em bytes do código (1, 2 ou 4, decidida pelo codespacerange do CMap), a chave e o tamanho do recurso de fonte ativo, a origem X e Y no espaço do usuário e o avanço horizontal; desde a v2.766.76 ele também carrega GlyphEndX e GlyphEndY, o ponto final em espaço de página da largura do próprio glifo, sem espaçamento de caractere nem de palavra, de onde a regra de lacuna entre palavras acima tira sua medida. Isso basta para construir detecção de limites de palavra, realce posicionado ou um algoritmo de layout próprio sem você mesmo tocar no content stream
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;
Contar os registros com Unicode = 0, como acima, é a forma honesta de medir a qualidade da extração em um dado documento antes de confiar no texto rio abaixo. Os registros de glifo também ancoram cada caractere ao operando de origem no content stream, e é isso que torna possível a busca e substituição de texto do HotPDF em documentos carregados sobre a mesma base
Quais PDFs não vão entregar o texto deles?
Alguns arquivos derrotam qualquer extrator, e é melhor detectá-los do que publicar a saída deles. Documentos digitalizados são o caso mais evidente: uma página que é uma única imagem grande não contém nenhum operador de texto, então a extração corretamente devolve uma string vazia — a correção é OCR, e extrair as imagens da página do PDF carregado é o primeiro passo desse pipeline. Fontes de subconjunto sem tabela /ToUnicode são o caso mais difícil: se o caminho de /Encoding e os CMaps padrão também não derem nada, esses glifos resolvem para 0 e aparecem como espaços nas chamadas de texto. Documentos criptografados extraem normalmente desde que você os carregue com a senha deles pela sobrecarga de LoadFromFile, de modo que os streams sejam descriptografados antes de o interpretador vê-los
Vale enunciar com clareza um limite mais estreito: a cadeia de decodificação lê CMaps e content streams pelo caminho Flate do HotPDF, então uma fonte cujo stream ToUnicode use um filtro incomum degrada para a estratégia seguinte em vez de derrubar a página. Na prática, o FlateDecode cobre quase tudo que foi produzido nas duas últimas décadas, e a degradação é silenciosa por projeto — você recebe o melhor texto que o arquivo permite, e não uma exceção. A mesma maquinaria de objetos do lado da leitura que resolve dicionários de fonte aqui também move a edição de metadados em documentos carregados, então um pipeline de recepção de documentos pode extrair, inspecionar e anotar em uma só passagem
A extração de texto, a renderização que preserva o layout, o acesso em nível de glifo e os recursos de busca e substituição construídos sobre eles fazem parte do HotPDF Delphi Component padrão para Delphi e C++Builder — sem DLLs externas, sem serviços de texto do sistema operacional, só Object Pascal que você pode depurar passo a passo quando um arquivo estranho cai na sua fila