O HotPDF recupera tabelas de um PDF existente através de ExtractLoadedTypedTables, uma API Delphi que funde os fragmentos de linhas produzidos pela passagem de layout, constrói uma grelha canónica de colunas por tabela, continua essa tabela através de uma quebra de página quando a geometria o suporta e devolve cada célula como um valor tipado com proveniência da página, extensão de colunas e limites. ExportLoadedTypedTables escreve o mesmo resultado diretamente em CSV ou JSON. O cenário que torna isto digno de ser construído é banal e extremamente comum. Um registo de faturas com quarenta páginas, uma única tabela do ponto de vista lógico, impresso com o cabeçalho repetido no topo de todas as páginas. Execute sobre ele uma passagem ingénua de ordem de leitura e obtém quarenta tabelas, trinta e nove linhas de cabeçalho espúrias e uma coluna de moeda que desliza uma posição para a esquerda em todas as linhas onde a célula do meio calhou estar vazia. Limpar isso a jusante, dentro da aplicação chamadora, é onde os projetos de importação de documentos vão morrer
Porque é que uma página PDF lhe entrega fragmentos em vez de uma tabela?
Porque uma página PDF não transporta semântica de tabela, a menos que o documento tenha tags. O content stream contém operadores de apresentação de texto e matrizes de posicionamento (ISO 32000-1 §9.4.3) e nada mais; a caixa desenhada que vê no ecrã é pintura de paths não relacionada, que nenhum extractor é obrigado a correlacionar com o texto. Os tipos de elementos de estrutura Table, TR, TH e TD vivem apenas na hierarquia de estrutura lógica de um PDF com tags (ISO 32000-1 §14.8.4), e a esmagadora maioria dos documentos empresariais em circulação não tem tags. Tudo o que é descrito abaixo é recuperação geométrica, não parsing, e vale a pena dizê-lo claramente antes de alguém construir um relatório de reconciliação sobre esta base
Por isso o HotPDF executa primeiro uma análise semântica de layout sobre os glifos extraídos, a mesma passagem que suporta a extração de texto por ordem estrutural de um PDF carregado e as exportações estruturadas HTML e XML. Essa passagem agrupa baselines em runs cujas células se alinham verticalmente e só continua um run enquanto as linhas consecutivas tiverem o mesmo número de células. Para um motor de layout, essa regra é correta e barata. Para um chamador, tem a forma errada: uma única linha com uma célula interior vazia divide uma tabela visual em duas tabelas de origem. A camada de tabelas tipadas fica precisamente acima dessa passagem para voltar a juntar as peças
Grelhas canónicas de colunas e o controlo ColumnTolerance
ExtractLoadedTypedTables funde primeiro os fragmentos da mesma página e só depois faz qualquer outra coisa, usando a geometria das colunas em vez do texto das linhas. Duas tabelas de origem adjacentes na mesma página juntam-se quando ambas têm pelo menos duas colunas, quando o intervalo vertical entre a última linha da primeira e a primeira linha da segunda permanece dentro da faixa de tolerância e quando as posições iniciais das colunas estão alinhadas. Inícios de colunas que estejam dentro de ColumnTolerance uns dos outros colapsam numa coluna canónica e são calculados em média à medida que se fundem. A tolerância predefinida é de 12 unidades do user space, adequada à tipografia empresarial normal e com tendência para precisar de ser aumentada em layouts muito espaçados ou profundamente indentados
O que acontece a uma linha sem um valor interior é a parte importante. O HotPDF ajusta cada célula ao início da coluna canónica mais próximo e define depois ColumnSpan como a distância dessa coluna até à próxima ocupada, em vez de deslocar as células restantes para a esquerda. Uma linha de três células numa grelha de cinco colunas mantém os seus valores sob os cabeçalhos corretos e regista exatamente onde estão as lacunas. Essa é a diferença entre uma tabela que pode reconciliar e uma que atribui dinheiro silenciosamente à coluna errada
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 deste valor, as tabelas são descartadas
Options.DateOrder := ttdoDMY; // 03/04/2026 é 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 fundido
ProcessTables(Tables)
else if Info.Status = ttesBudgetExceeded then
Log(string(Info.Diagnostic));
finally
Pdf.Free;
end;
end;
O que garante realmente a fusão entre páginas?
Garante conservadorismo, de propósito. O HotPDF junta duas tabelas através de uma fronteira de página apenas quando MergeAcrossPages está ativado, quando a segunda tabela começa exatamente no índice de página seguinte àquele onde a primeira termina, quando ambas têm pelo menos duas colunas e quando pelo menos dois inícios de colunas canónicas se alinham dentro de ColumnTolerance. A condição de páginas consecutivas é a que sustenta tudo. Os chamadores passam PageIndices como um array aberto na ordem que entenderem e, sem essa verificação, um pedido para as páginas 3, 9 e 14 poderia soldar três tabelas sem relação numa única saída totalmente plausível. O custo é que uma continuação genuína que salte uma página, um apêndice intercalado ou um scan duplex com um verso em branco regressa como duas tabelas e nenhuma opção relaxa isso. Voltar a juntá-las é uma decisão de política que só a aplicação chamadora pode tomar, por isso a API expõe FirstPageIndex, LastPageIndex, SourceTableCount e um PageIndex por linha, deixando a decisão no local certo
Os cabeçalhos repetidos são assinalados, nunca eliminados
ExtractLoadedTypedTables nunca remove do resultado uma linha de cabeçalho repetida. Quando uma fusão entre páginas descobre que a tabela recebida começa com texto de cabeçalho idêntico ao da tabela acumulada, comparado depois de remover espaços nas extremidades e fazer case folding, marca essas linhas com IsHeader e IsRepeatedHeader e acrescenta-as na mesma pela ordem de origem. A eliminação é uma escolha com perda e irreversível, e consumidores diferentes querem respostas diferentes: uma importação CSV quer os repetidos removidos, uma pista de auditoria quer tê-los presentes com os números de página e uma ferramenta de comparação quer a ordem de origem preservada byte a byte. Por isso a biblioteca comunica 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; // manter apenas o primeiro bloco de cabeçalho
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 tem de fornecer
A inferência de tipos executa-se numa ordem fixa que resolve as ambiguidades na única direção sensata: primeiro booleano, depois data, depois percentagem, depois moeda, depois número simples, ficando como string tudo o que não corresponder. A ordem é o que impede que 2026 numa coluna de datas seja decidido por um parser de números antes de o parser de datas o ver. A moeda é reconhecida a partir de um $, £, ¥ ou € inicial, ou de um código ISO 4217 de três letras seguido de um espaço, e o código é preservado em CurrencyCode. Fundamentalmente, o HotPDF não adivinha o seu locale. DecimalSeparator, ThousandsSeparator e DateOrder vêm das opções, porque 1.234 é um número ou mil duzentos e trinta e quatro consoante um facto que o PDF não contém. O Text Unicode bruto é mantido em todas as células ao lado do valor tipado, pelo que uma adivinha errada pode sempre ser recuperada 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. O CSV escreve as colunas de continuação de um span fundido como campos vazios, que é o que uma folha de cálculo ou um carregador em massa espera. O JSON conserva tudo o que a extração soube: o valor tipado sob o 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 colocam primeiro o documento inteiro num buffer limitado em memória e só depois publicam no seu stream de destino, restaurando os bytes, o comprimento e a posição originais se a escrita falhar a meio, pelo que uma exportação falhada nunca deixa um ficheiro escrito pela metade. Os orçamentos para páginas, glifos por página, tabelas, linhas, células, carateres e bytes de saída são todos contabilizados separadamente, e as linhas são contadas antes da alocação porque um SetLength por linha degenera em cópias quadráticas muito antes do teto predefinido 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 do que uma lista de funcionalidades, porque cada um destes pontos é um local onde o chamador precisa da sua própria política e não de um valor de opção melhor
- Fusões verticais não são recuperadas. O HotPDF comunica
ColumnSpanpara extensões horizontais e deixaRowSpanem 1, pelo que uma célula que atravesse três linhas na tabela impressa chega como uma célula mais duas lacunas - A deteção de cabeçalhos é orientada por dados, não visual. O bloco de cabeçalho é o run de linhas antes da primeira linha que contém um valor tipado não-string, pelo que uma tabela cujo corpo seja inteiramente texto comunica
HeaderRowCountcomo zero, independentemente do estilo - As tabelas abaixo de
MinimumTableConfidencesão descartadas do resultado sem erro. CompareInfo.TableCountcomInfo.SourceTableCountquando precisar de saber que algo foi descartado - Um run precisa de pelo menos duas linhas e pelo menos duas colunas antes de a passagem de layout o chamar tabela, pelo que uma pseudo-tabela de uma linha ou um layout de duas colunas de prosa longa não é corretamente, embora pouco utilmente, uma tabela
- Páginas digitalizadas não contêm operadores de texto, pelo que não há nada para recuperar geometricamente até existir uma camada de texto OCR na página
Se os seus PDFs saem da sua própria stack de reporting, a correção mais barata para tudo isto é a montante: emita tabelas com tags, ou conserve os dados de origem, e trate a extração como fallback para documentos que não produziu. Para tudo o resto, vale a pena aprender o pipeline por esta ordem, já que cada camada se apoia na anterior: comece pela extração de texto simples de um PDF carregado, suba para a API de tabelas tipadas quando a geometria tiver de ser preservada e veja renderizar uma tabela de dados num PDF novo quando está do lado da geração e pode decidir quão recuperável será a saída
ExtractLoadedTypedTables e ExportLoadedTypedTables são distribuídos como parte do HotPDF Delphi PDF Component nativo para Delphi e C++Builder, sem DLL externa nem dependência de runtime; a página do produto contém a referência completa das opções, estados e records da API de tabelas tipadas