HotPDF recupera tabelle da un PDF esistente tramite ExtractLoadedTypedTables, un'API Delphi che unisce i frammenti di riga prodotti dal passaggio di layout, costruisce una griglia di colonne canonica per ogni tabella, continua la tabella oltre un'interruzione di pagina quando la geometria lo consente e restituisce ogni cella come valore tipizzato con provenienza della pagina, span di colonna e bounds. ExportLoadedTypedTables scrive lo stesso risultato direttamente in CSV o JSON. Lo scenario che rende utile questo lavoro è banale e comunissimo. Un registro fatture di quaranta pagine, logicamente una sola tabella, stampato con l'intestazione ripetuta in cima a ogni pagina. Eseguite su di esso un passaggio ingenuo di reading order e ottenete quaranta tabelle, trentanove righe di intestazione spurie e una colonna valuta che scivola di una posizione a sinistra ogni volta che la cella centrale era vuota. Ripulire tutto downstream, nell'applicazione chiamante, è il punto in cui i progetti di importazione documentale vanno a morire
Perché una pagina PDF restituisce frammenti invece di una tabella?
Perché una pagina PDF non porta alcuna semantica tabellare, a meno che il documento non sia tagged. Il content stream contiene operatori text-showing e matrici di posizionamento (ISO 32000-1 §9.4.3) e nient'altro; il riquadro tracciato che vedete sullo schermo è un painting di path non correlato che nessun estrattore è obbligato a collegare al testo. I tipi di structure element Table, TR, TH e TD esistono solo nella gerarchia della struttura logica di un PDF tagged (ISO 32000-1 §14.8.4), e la maggior parte schiacciante dei documenti aziendali in circolazione non è tagged. Tutto ciò che segue è recupero geometrico, non parsing, ed è importante dirlo ad alta voce prima che qualcuno costruisca sopra il risultato un report di riconciliazione
HotPDF esegue quindi prima un'analisi semantica del layout sui glifi estratti, lo stesso passaggio alla base dell'estrazione del testo in ordine strutturale da un PDF caricato e delle esportazioni HTML e XML strutturate. Quel passaggio raggruppa le baseline in run le cui celle si allineano verticalmente e continua un run solo finché le righe consecutive hanno lo stesso numero di celle. Per un layout engine questa regola è corretta ed economica. Per un caller ha la forma sbagliata: una sola riga con una cella interna vuota divide una tabella visiva in due source table. Il layer delle tabelle tipizzate esiste precisamente per ricomporre i pezzi
Griglie canoniche delle colonne e il parametro ColumnTolerance
ExtractLoadedTypedTables unisce i frammenti della stessa pagina prima di fare qualsiasi altra cosa, e li unisce sulla geometria delle colonne, non sul testo delle righe. Due source table adiacenti nella stessa pagina vengono unite quando entrambe hanno almeno due colonne, quando il gap verticale tra l'ultima riga della prima e la prima della seconda resta nella fascia di tolleranza e quando le posizioni iniziali delle colonne si allineano. Gli inizi di colonna entro ColumnTolerance l'uno dall'altro collassano in una colonna canonica e vengono mediati durante l'unione. La tolleranza predefinita è di 12 unità nello user space, adatta alla tipografia aziendale ordinaria e da aumentare per layout molto spaziati o profondamente indentati
La parte importante è cosa succede a una riga che manca di un valore interno. HotPDF aggancia ogni cella all'inizio della colonna canonica più vicino e imposta ColumnSpan sulla distanza da quella colonna alla successiva occupata, invece di spostare verso sinistra le celle rimanenti. Una riga a tre celle in una griglia a cinque colonne mantiene i valori sotto le intestazioni corrette e registra esattamente dove sono i vuoti. È la differenza tra una tabella che si può riconciliare e una che attribuisce silenziosamente il denaro alla colonna sbagliata
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; // unità nello user space
Options.MinimumTableConfidence := 0.55; // sotto questo valore, le tabelle vengono scartate
Options.DateOrder := ttdoDMY; // 03/04/2026 significa 3 aprile
Options.DecimalSeparator := ',';
Options.ThousandsSeparator := '.';
if Pdf.ExtractLoadedTypedTables([0, 1, 2, 3], Options, Tables, Info) then
// Info.TableCount rispetto a Info.SourceTableCount mostra quanto è stato unito
ProcessTables(Tables)
else if Info.Status = ttesBudgetExceeded then
Log(string(Info.Diagnostic));
finally
Pdf.Free;
end;
end;
Cosa garantisce davvero l'unione oltre pagina?
Garantisce conservatorismo, volutamente. HotPDF unisce due tabelle oltre un confine di pagina solo quando MergeAcrossPages è abilitato, quando la seconda tabella inizia esattamente all'indice di pagina successivo alla fine della prima, quando entrambe hanno almeno due colonne e quando almeno due inizi di colonna canonici si allineano entro ColumnTolerance. La condizione sulle pagine consecutive è quella portante. I caller passano PageIndices come open array nell'ordine che preferiscono, e senza quel controllo una richiesta per le pagine 3, 9 e 14 potrebbe saldare tre tabelle non correlate in un risultato del tutto plausibile. Il costo è che una continuazione reale che salta una pagina, un'appendice intercalata o una scansione fronte-retro con il verso bianco torna come due tabelle e nessuna opzione allenta il vincolo. Riunirle è una decisione di policy che solo l'applicazione chiamante può prendere, quindi l'API espone FirstPageIndex, LastPageIndex, SourceTableCount e un PageIndex per riga e lascia la decisione dove deve stare
Le intestazioni ripetute vengono marcate, mai cancellate
ExtractLoadedTypedTables non rimuove mai una riga di intestazione ripetuta dal risultato. Quando un'unione tra pagine rileva che la tabella in ingresso si apre con un testo di intestazione identico a quello della tabella accumulata, dopo trim e case folding, marca quelle righe con IsHeader e IsRepeatedHeader e le aggiunge comunque nell'ordine della sorgente. La cancellazione è una scelta lossy e irreversibile, mentre consumer diversi vogliono risposte diverse: un import CSV vuole eliminare le ripetizioni, un audit trail vuole mantenerle con i numeri di pagina, uno strumento di diff vuole conservare l'ordine della sorgente byte per byte. La libreria segnala, il caller 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; // mantenere solo il primo blocco di intestazione
for C := 0 to High(Row.Cells) do
if Row.Cells[C].ValueKind = ttvkCurrency then
Total := Total + Row.Cells[C].NumberValue;
end;
end;
Valori tipizzati e separatori da fornire
L'inferenza del tipo segue un ordine fisso che risolve le ambiguità nell'unica direzione sensata: prima boolean, poi date, percentuale, valuta, numero semplice, mentre tutto ciò che non corrisponde resta una stringa. L'ordine impedisce che 2026 in una colonna data venga deciso dal parser numerico prima che il parser data lo veda. La valuta viene riconosciuta da un $, £, ¥ o € iniziale, oppure da un codice ISO 4217 di tre lettere seguito da uno spazio, e il codice viene conservato in CurrencyCode. Fondamentalmente, HotPDF non indovina la vostra locale. DecimalSeparator, ThousandsSeparator e DateOrder arrivano dalle opzioni, perché 1.234 è un numero oppure mille duecentotrentaquattro a seconda di un fatto che il PDF non contiene. Il testo Unicode raw Text viene conservato su ogni cella accanto al valore tipizzato, quindi un'ipotesi errata è sempre recuperabile senza un secondo passaggio di estrazione
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;
I due formati di export rispondono a domande diverse e deliberatamente non sono equivalenti. Il CSV scrive le colonne di continuazione di uno span unito come campi vuoti, che è ciò che si aspetta un foglio di calcolo o un bulk loader. Il JSON conserva tutto ciò che l'estrazione sapeva: il valore tipizzato nella propria kind, columnSpan, la confidence per cella e per riga, i bounds della cella e la provenienza della pagina e della source table. Entrambi i formati preparano l'intero documento in un buffer in-memory delimitato e pubblicano solo dopo sullo stream di destinazione, ripristinando byte, lunghezza e posizione originali se la scrittura fallisce a metà, così un export fallito non lascia mai un file scritto a metà. I budget per pagine, glifi per pagina, tabelle, righe, celle, caratteri e byte di output vengono conteggiati separatamente, e le righe vengono contate prima dell'allocazione perché un SetLength per riga degenera in copie quadratiche molto prima del limite predefinito di un milione di righe
Dove il recupero geometrico delle tabelle si arrende
Essere espliciti sui failure mode è più utile di un elenco di funzionalità, perché ciascuno di questi casi è un punto in cui al caller serve una propria policy, non un valore di opzione migliore
- Le unioni verticali non vengono recuperate. HotPDF riporta
ColumnSpanper gli span orizzontali e lasciaRowSpana 1, quindi una cella che copre tre righe nella tabella stampata arriva come una cella più due vuoti - Il rilevamento delle intestazioni è guidato dai dati, non dalla grafica. Il blocco di intestazione è la sequenza di righe prima della prima riga che contiene un valore tipizzato non stringa, quindi una tabella il cui body è tutto testo riporta
HeaderRowCountuguale a zero indipendentemente dallo stile - Le tabelle sotto
MinimumTableConfidencevengono eliminate dal risultato senza errore. ConfrontateInfo.TableCountconInfo.SourceTableCountquando volete sapere se qualcosa è stato scartato - Un run richiede almeno due righe e almeno due colonne prima che il passaggio di layout lo chiami tabella, quindi una pseudo-tabella su una riga o un layout a due colonne di prosa lunga non è correttamente, anche se poco utilmente, una tabella
- Le pagine scansionate non contengono operatori di testo, quindi non c'è nulla da recuperare geometricamente finché sulla pagina non esiste un OCR text layer
Se i vostri PDF escono dal vostro stack di reporting, la correzione più economica per tutto questo è upstream: emettete tabelle tagged oppure conservate i dati sorgente, e trattate l'estrazione come fallback per i documenti che non avete prodotto voi. Per tutto il resto vale la pena imparare la pipeline in quest'ordine, poiché ogni layer costruisce su quello sottostante: iniziate con l'estrazione del testo semplice da un PDF caricato, passate all'API delle tabelle tipizzate quando va preservata la geometria e guardate il rendering di una tabella dati in un nuovo PDF quando siete dal lato della generazione e potete decidere quanto l'output sarà recuperabile
ExtractLoadedTypedTables e ExportLoadedTypedTables sono inclusi nel componente PDF Delphi HotPDF nativo per Delphi e C++Builder, senza DLL esterne né dipendenze runtime; la pagina prodotto contiene il riferimento completo a opzioni, stati e record dell'API per le tabelle tipizzate