Artigo Técnico

Extração tipada de tabelas PDF no Delphi

O HotPDF recupera tabelas de um PDF existente por meio de ExtractLoadedTypedTables, uma API Delphi que mescla os fragmentos de linha produzidos pela passagem de layout, cria uma grade canônica de colunas por tabela, continua essa tabela depois de uma quebra de página quando a geometria permite e retorna cada célula como um valor tipado com proveniência da página, span de coluna e limites. ExportLoadedTypedTables grava o mesmo resultado diretamente em CSV ou JSON. O cenário que torna isso útil é banal e extremamente comum. Um registro de faturas de quarenta páginas, uma única tabela lógica, impresso com o cabeçalho repetido no topo de cada página. Execute uma passagem ingênua de ordem de leitura e obtenha quarenta tabelas, 39 linhas de cabeçalho espúrias e uma coluna de moeda que desliza uma posição para a esquerda em cada linha cujo meio por acaso estava vazio. Limpar isso downstream, dentro da aplicação chamadora, é onde projetos de importação de documentos costumam morrer

Por que uma página PDF entrega fragmentos em vez de uma tabela?

Porque uma página PDF não carrega semântica de tabela alguma, a menos que o documento esteja marcado. O content stream contém operadores de exibição de texto e matrizes de posicionamento (ISO 32000-1 §9.4.3), e nada além disso; a caixa desenhada que você vê na tela é pintura de path sem relação que nenhum extrator é obrigado a correlacionar com o texto. Os tipos de elemento estrutural Table, TR, TH e TD vivem apenas na hierarquia de estrutura lógica de um PDF marcado (ISO 32000-1 §14.8.4), e a esmagadora maioria dos documentos empresariais em circulação não é marcada. Tudo o que segue é recuperação geométrica, não parsing, e vale dizer isso em voz alta antes que alguém construa um relatório de reconciliação sobre ela

Por isso o HotPDF executa primeiro uma análise semântica de layout sobre os glifos extraídos, a mesma passagem que sustenta a extração de texto em ordem estrutural de um PDF carregado e as exportações estruturadas em HTML e XML. Essa passagem agrupa linhas de base em runs cujas células se alinham verticalmente, e só continua um run enquanto as linhas consecutivas têm a mesma quantidade de células. Para um engine de layout, essa regra é correta e barata. Para um chamador, é o formato errado: uma única linha com uma célula interior vazia divide uma tabela visual em duas tabelas de origem. A camada de tabela tipada existe justamente para juntar as peças de volta

Grades canônicas de colunas e o ajuste ColumnTolerance

ExtractLoadedTypedTables mescla os fragmentos da mesma página antes de fazer qualquer outra coisa, e mescla pela geometria das colunas, não pelo texto das linhas. Duas tabelas de origem adjacentes em uma página são unidas quando ambas têm pelo menos duas colunas, quando o espaço vertical entre a última linha da primeira e a primeira linha da segunda permanece dentro da faixa de tolerância e quando suas posições iniciais de coluna se alinham. Inícios de coluna dentro de ColumnTolerance uns dos outros colapsam em uma coluna canônica e são calculados em média durante a fusão. A tolerância padrão é de 12 unidades de user space, adequada para tipografia empresarial comum, e deve ser aumentada para layouts com tracking amplo ou indentação profunda

O que acontece com uma linha sem valor interior é a parte importante. O HotPDF encaixa cada célula no início de coluna canônico mais próximo e então define ColumnSpan como a distância dessa coluna até a próxima ocupada, em vez de deslocar as células restantes para a esquerda. Uma linha de três células em uma grade de cinco colunas mantém seus valores sob os cabeçalhos corretos e registra exatamente onde estão as lacunas. Essa é a diferença entre uma tabela que você consegue reconciliar e uma que atribui dinheiro à coluna errada em silêncio

var
  Pdf: THotPDF;
  Options: THPDFTypedTableExtractionOptions;
  Tables: THPDFTypedTables;
  Info: THPDFTypedTableExtractionInfo;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('register.pdf', '') <= 0 then
      Exit;
    Options := THPDFTypedTableExtractionOptions.Default;
    Options.ColumnTolerance := 12;           // unidades do user space
    Options.MinimumTableConfidence := 0.55;  // abaixo disso, as tabelas sao descartadas
    Options.DateOrder := ttdoDMY;            // 03/04/2026 e 3 de abril
    Options.DecimalSeparator := ',';
    Options.ThousandsSeparator := '.';
    if Pdf.ExtractLoadedTypedTables([0, 1, 2, 3], Options, Tables, Info) then
      // Info.TableCount versus Info.SourceTableCount mostra quanto foi mesclado
      ProcessTables(Tables)
    else if Info.Status = ttesBudgetExceeded then
      Log(string(Info.Diagnostic));
  finally
    Pdf.Free;
  end;
end;

O que a mesclagem entre páginas realmente garante?

Ela garante conservadorismo, de propósito. O HotPDF une duas tabelas através de uma quebra de página somente quando MergeAcrossPages está habilitado, quando a segunda tabela começa exatamente no índice de página seguinte ao fim da primeira, quando ambas têm pelo menos duas colunas e quando pelo menos dois inícios de coluna canônicos se alinham dentro de ColumnTolerance. A condição de páginas consecutivas é a que sustenta tudo. Os chamadores passam PageIndices como um open array na ordem que quiserem e, sem essa verificação, uma solicitação pelas páginas 3, 9 e 14 poderia soldar três tabelas sem relação em um resultado completamente plausível. O custo é que uma continuação genuína que pule uma página, um apêndice intercalado ou um scan duplex com verso em branco volta como duas tabelas, sem opção para relaxar a regra. Reuni-las novamente é uma decisão de política que somente a aplicação chamadora pode tomar, então a API expõe FirstPageIndex, LastPageIndex, SourceTableCount e um PageIndex por linha, deixando a decisão onde ela deve ficar

Cabeçalhos repetidos são rotulados, nunca apagados

ExtractLoadedTypedTables nunca remove uma linha de cabeçalho repetida do resultado. Quando uma mesclagem entre páginas descobre que a tabela recebida começa com um texto de cabeçalho idêntico ao da tabela acumulada, comparado depois de trim e case folding, ela marca essas linhas com IsHeader e IsRepeatedHeader e ainda as anexa na ordem da fonte. A exclusão é uma escolha com perda e irreversível, e consumidores diferentes querem respostas diferentes: uma importação CSV quer os repetidos removidos, uma trilha de auditoria quer que apareçam com seus números de página e uma ferramenta de diff quer a ordem da fonte preservada byte a byte. Por isso a biblioteca informa e o chamador decide

var
  T, R, C: Integer;
  Row: THPDFTypedTableRow;
  Total: Double;
begin
  Total := 0;
  for T := 0 to High(Tables) do
    for R := 0 to High(Tables[T].Rows) do
    begin
      Row := Tables[T].Rows[R];
      if Row.IsRepeatedHeader then
        Continue;                    // mantenha apenas o primeiro bloco de cabecalho
      for C := 0 to High(Row.Cells) do
        if Row.Cells[C].ValueKind = ttvkCurrency then
          Total := Total + Row.Cells[C].NumberValue;
    end;
end;

Valores tipados e os separadores que você precisa fornecer

A inferência de tipos roda em uma ordem fixa que resolve as ambiguidades na única direção sensata: booleano primeiro, depois data, porcentagem, moeda e número simples, com tudo que não corresponder permanecendo string. A ordem impede que 2026 em uma coluna de data seja decidido por um parser numérico antes que o parser de data o veja. Moeda é reconhecida por um $, £, ¥ ou inicial, ou por um código ISO 4217 de três letras seguido de um espaço, e o código é preservado em CurrencyCode. Crucialmente, o HotPDF não adivinha sua locale. DecimalSeparator, ThousandsSeparator e DateOrder vêm das opções, porque 1.234 pode ser um número ou mil duzentos e trinta e quatro, dependendo de um fato que o PDF não contém. O Text Unicode bruto é mantido em cada célula junto ao valor tipado, então um palpite errado sempre pode ser recuperado sem uma segunda passagem de extração

var
  Stream: TFileStream;
  Info: THPDFTypedTableExtractionInfo;
begin
  Stream := TFileStream.Create('tables.json', fmCreate);
  try
    if not Pdf.ExportLoadedTypedTables([0, 1, 2], ttefJSON,
      Stream, Options, Info) then
      case Info.Status of
        ttesInvalidOptions:   ReportBadConfiguration;
        ttesBudgetExceeded:   ReportOversizedDocument;
        ttesCancelled:        ReportUserCancelled;
        ttesWriteFailed:      ReportDestinationProblem;
      else
        ReportExtractionFailure;
      end;
  finally
    Stream.Free;
  end;
end;

Os dois formatos de exportação respondem a perguntas diferentes e deliberadamente não são equivalentes. CSV grava as colunas de continuação de um span mesclado como campos vazios, que é o que uma planilha ou um bulk loader espera. JSON mantém tudo que a extração soube: o valor tipado sob seu próprio tipo, columnSpan, a confiança por célula e por linha, os limites da célula e a proveniência da página e da tabela de origem. Ambos os formatos montam o documento inteiro em um buffer limitado na memória e só então publicam no stream de destino, restaurando os bytes, o comprimento e a posição originais se a escrita falhar no meio, portanto uma exportação falha nunca deixa um arquivo pela metade. Os orçamentos de páginas, glifos por página, tabelas, linhas, células, caracteres e bytes de saída são contabilizados separadamente, e as linhas são contadas antes da alocação porque um SetLength por linha degenera em cópia quadrática muito antes do teto padrão de um milhão de linhas

Onde a recuperação geométrica de tabelas desiste

Ser explícito sobre os modos de falha é mais útil que uma lista de recursos, porque cada um destes pontos é um lugar em que o chamador precisa de sua própria política, não de um valor de opção melhor

  • Mesclas verticais não são recuperadas. O HotPDF informa ColumnSpan para spans horizontais e mantém RowSpan em 1, então uma célula que ocupa três linhas na tabela impressa chega como uma célula mais duas lacunas
  • A detecção de cabeçalho é orientada por dados, não visual. O bloco de cabeçalho é a sequência de linhas antes da primeira linha que contém um valor tipado não string, então uma tabela cujo corpo seja todo texto informa HeaderRowCount como zero, independentemente do estilo
  • Tabelas abaixo de MinimumTableConfidence são descartadas do resultado sem erro. Compare Info.TableCount com Info.SourceTableCount quando precisar saber se algo foi descartado
  • Um run precisa de pelo menos duas linhas e duas colunas antes que a passagem de layout o chame de tabela, então uma pseudo-tabela de uma linha ou um layout de duas colunas de prosa longa é corretamente, embora sem utilidade, não considerado tabela
  • Páginas digitalizadas não contêm operadores de texto, então não há nada a recuperar geometricamente até existir uma camada de texto OCR na página

Se seus PDFs saem da sua própria stack de relatórios, a correção mais barata para tudo isso é upstream: emita tabelas marcadas, ou mantenha os dados de origem, e trate a extração como fallback para documentos que você não produziu. Para o restante, vale aprender o pipeline nesta ordem, pois cada camada se apoia na abaixo: comece com a extração de texto simples de um PDF carregado, suba para a API de tabelas tipadas quando a geometria precisar ser preservada e veja renderizar uma tabela de dados em um novo PDF quando estiver do lado da geração e puder decidir quão recuperável a saída será

ExtractLoadedTypedTables e ExportLoadedTypedTables fazem parte do HotPDF Delphi PDF Component nativo para Delphi e C++Builder, sem DLL externa e sem dependência de runtime; a página do produto traz a referência completa das opções, status e records da API de tabelas tipadas