Artigo Técnico

Extração de texto PDF na ordem da estrutura com HotPDF

Todo extrator de texto geométrico está adivinhando. Ele lê os glifos que uma página desenha, os ordena por baseline e posição horizontal e espera que o arranjo visual bata com a ordem em que um humano leria. Em um relatório de coluna única esse palpite está certo. Em um artigo de periódico de duas colunas, um formulário com uma barra lateral, ou uma tabela cujas células foram emitidas coluna por coluna, está errado de jeitos difíceis de notar e caros de descobrir a jusante. O HotPDF responde a isso com o ExtractLoadedPageStructureText, que ignora a geometria por completo: percorre a árvore de estrutura do documento na ordem de autoria conforme definido pela ISO 32000-1 §14.8.4 e depois remonta os glifos da página por seu marked-content identifier. Para um PDF tagged isso não é uma heurística, é a ordem que o aplicativo produtor declarou

A função retorna False quando a página não tem árvore de estrutura utilizável, que é o sinal para recuar ao extrator geométrico em vez de falhar. Esse design de dois caminhos importa mais que o algoritmo: a admissão real de documentos vê formulários de governo tagged e saída de scanner na mesma pasta, e um pipeline que só lida com um deles não é um pipeline

Por que a extração geométrica erra a ordem de leitura?

Porque um content stream de PDF não carrega ordem de leitura alguma. É uma sequência de operadores de desenho, e um produtor é livre para emiti-los na sequência que convier ao seu próprio motor de layout. Processadores de texto geralmente emitem em ordem de fluxo e a ordenação geométrica parece boa. Ferramentas de layout, designers de formulários e geradores de relatórios frequentemente não: um rodapé pode ser emitido antes do corpo, uma tabela pode ser preenchida coluna a coluna, e uma página de duas colunas pode intercalar linhas das duas colunas porque o compositor as resolveu juntas

Comparação de página PDF de duas colunas mostrando a extração geométrica ordenada por baseline emendando colunas contra a extração por MCID na ordem da estrutura no HotPDF
Ordenar glifos por baseline intercala duas colunas em absurdo, enquanto a árvore de estrutura repete a ordem que o produtor declarou

O modo de falha é silencioso. Um extrator geométrico nunca reporta um erro, apenas devolve prosa cujas frases estão emendadas de duas colunas. Qualquer coisa que consuma esse texto, um índice de busca, um mapeador de campos de e-invoice, um pipeline de recuperação que alimenta um modelo de linguagem, herda o dano sem aviso. O HotPDF também distribui os extratores geométricos para documentos carregados, e eles continuam sendo a ferramenta certa para arquivos sem tags; o objetivo do caminho na ordem da estrutura é parar de adivinhar quando o documento já carrega a resposta

O que a árvore de estrutura realmente armazena

Um PDF tagged guarda uma segunda descrição paralela da página. O catálogo aponta para um /StructTreeRoot, cujos filhos /K formam uma árvore de elementos de estrutura: /Document, /Sect, /P, /Table, /TR, /TD e por aí vai. As folhas dessa árvore são marked-content references, inteiros que nomeiam um trecho do content stream da página. No lado do conteúdo, esses trechos são abertos com um operador BDC carregando um /MCID e fechados com EMC. Cada elemento de estrutura também carrega uma entrada /Pg nomeando a página a que pertence, que é o que torna possível a travessia por página em um documento cuja árvore de estrutura se estende por centenas de páginas

Anatomia da árvore de estrutura PDF ligando elementos StructTreeRoot como Sect, Table, TR e TD a trechos BDC MCID no content stream da página do HotPDF
As folhas da árvore são marked-content references, e cada elemento carrega uma entrada Pg que permite à caminhada filtrar pela página atual

O HotPDF percorre essa árvore com um teto de profundidade de 128 níveis e filtra por /Pg para que só a página atual contribua. A saída da travessia não é texto, é uma lista ordenada de valores MCID: a ordem de autoria dos trechos de marked-content desta página. Remontar o texto é então uma questão de repetir os glifos nessa ordem

O MCID é registrado durante a extração de glifos, não procurado depois

Este é o detalhe de implementação que torna o recurso barato. O HotPDF já registra o identificador de marked-content ativo em cada glifo que extrai, no campo MCID de THPDFGlyphRecord, porque o interpretador de content stream sabe qual escopo BDC está aberto no momento em que processa cada operador Tj ou TJ. A extração na ordem da estrutura portanto não precisa de uma segunda passada pelo content stream. Ela coleta a sequência de MCIDs da árvore de estrutura, então agrupa os glifos já extraídos por MCID e os emite nessa sequência

var
  Pdf: THotPDF;
  PageCount, I, Untagged: Integer;
  PageText, AllText: UnicodeString;
  Report: TStrings;   // coletor de diagnósticos de propriedade do chamador
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('accessible-form.pdf');
    AllText := '';
    for I := 0 to PageCount - 1 do
    begin
      if Pdf.ExtractLoadedPageStructureText(I, PageText, Untagged) then
      begin
        // Ordem de autoria direto da árvore de estrutura
        if Untagged > 0 then
          Report.Add(Format('page %d: %d glyphs outside the structure tree',
            [I, Untagged]));
      end
      else
        // Sem árvore de estrutura utilizável nesta página: fallback geométrico
        Pdf.ExtractLoadedPageText(I, PageText);
      AllText := AllText + PageText + #13#10;
    end;
  finally
    Pdf.Free;
  end;
end;

Glifos sem tags são contados, nunca descartados em silêncio

Uma página pode estar parcialmente tagged. Produtores adicionam uma régua decorativa, um número de página ou uma marca d'água tardia fora de qualquer escopo BDC, e esses glifos não pertencem a nenhum MCID. Descartá-los seria a implementação arrumada e a errada, porque a mesma lacuna também aparece quando um produtor taggeia o corpo mas esquece a tabela, e você perderia a tabela sem notar

O HotPDF anexa os glifos não reivindicados como uma cauda geométrica depois do texto na ordem da estrutura e reporta seu número pelo parâmetro de saída UntaggedGlyphCount. Esse número é um sinal de qualidade no qual você pode agir. Um punhado de glifos em uma página de dois mil é mobiliário de página e pode ser ignorado. Quarenta por cento da página fora da árvore de estrutura significa que a marcação é decorativa e o extrator geométrico é a resposta mais honesta para esse arquivo

Fluxo de decisão para extração de texto de estrutura do HotPDF com fallback geométrico quando uma página não tem árvore de estrutura utilizável ou marcação decorativa
True significa ordem da estrutura com a cauda sem tags anexada, e False encaminha a página ao extrator geométrico em vez de falhar
function ExtractPageBestEffort(Pdf: THotPDF; PageIndex: Integer;
  out AText: UnicodeString; out UsedStructure: Boolean): Boolean;
var
  Untagged, TotalGlyphs: Integer;
  Glyphs: THPDFGlyphArray;
begin
  UsedStructure := False;
  if Pdf.ExtractLoadedPageStructureText(PageIndex, AText, Untagged) then
  begin
    TotalGlyphs := 0;
    if Pdf.ExtractLoadedPageGlyphs(PageIndex, Glyphs) then
      TotalGlyphs := Length(Glyphs);
    // Confie na árvore de estrutura só quando ela reclama a maior parte da página
    if (TotalGlyphs = 0) or (Untagged * 4 <= TotalGlyphs) then
    begin
      UsedStructure := True;
      Result := True;
      Exit;
    end;
  end;
  Result := Pdf.ExtractLoadedPageText(PageIndex, AText);
end;

O que faz a função retornar False

Três casos, e valem a distinção porque só um deles é um defeito no documento. O primeiro é um PDF comum sem tags: sem /StructTreeRoot, nada a percorrer, e False é simplesmente a verdade. O segundo é uma página digitalizada cujo texto vem de uma camada de OCR que nunca foi taggeada. O terceiro é o interessante: conteúdo que carrega operadores BDC com valores /MCID mas cuja página não tem entrada /StructParents e cuja árvore de estrutura nunca referencia esses identificadores. O marked content existe, o lado da estrutura não, e não há ordem a recuperar. O HotPDF reporta False em vez de inventar uma

Esse último caso aparece em arquivos editados à mão e na saída de ferramentas que emitem marked content para fins de optional content ou artifact sem construir uma árvore de estrutura. Se você mesmo produz PDFs tagged, a mesma assimetria é o que a validação PDF/UA confere, e a contraparte do lado do writer é coberta em o layout DOM que emite saída tagged e paginada

Onde a ordem da estrutura se paga

Auditoria de acessibilidade é a óbvia: se você está certificando um documento contra PDF/UA, a ordem de leitura que um leitor de tela anunciará é exatamente a ordem da estrutura, então extraí-la é como você a revisa sem um leitor de tela. Captura de dados é o caso comercial maior. Formulários de governo tagged, divulgações reguladas e anexos de e-invoice carregam rótulos e valores de campo em ordem declarada, e lê-los nessa ordem remove uma classe inteira de bugs de mapeamento que a extração geométrica cria em layouts de múltiplas colunas

O consumidor mais novo é a recuperação para modelos de linguagem. Fatiar um documento para embedding só é tão bom quanto a ordem do texto, e um chunk que emenda duas colunas produz frases que nunca existiram. A extração na ordem da estrutura é a correção mais barata disponível para isso, porque para documentos tagged a ordem correta já está no arquivo e só precisa ser lida

O HotPDF é um componente VCL nativo para Delphi e C++Builder, então a travessia da árvore de estrutura e a repetição dos glifos rodam in-process contra um documento carregado sem renderizador externo envolvido. Os detalhes completos da API da família de extração de documentos carregados estão na página de produto do HotPDF Delphi PDF component