Το HotPDF ανακτά tables από υπάρχον PDF μέσω του ExtractLoadedTypedTables, ενός Delphi API που ενώνει τα row fragments της layout pass, χτίζει ένα canonical column grid ανά table, συνεχίζει το table σε page break όταν το επιτρέπει η geometry και επιστρέφει κάθε cell ως typed value με page provenance, column span και bounds. Το ExportLoadedTypedTables γράφει το ίδιο αποτέλεσμα απευθείας σε CSV ή JSON. Το σενάριο που κάνει χρήσιμη αυτή τη δουλειά είναι βαρετό και εξαιρετικά συνηθισμένο. Ένα register τιμολογίων σαράντα σελίδων, ένα table λογικά, τυπωμένο με το header του να επαναλαμβάνεται στην κορυφή κάθε σελίδας. Τρέξε πάνω του naive reading-order pass και παίρνεις σαράντα tables, τριάντα εννέα ψεύτικες header rows και μια currency column που γλιστρά μία θέση αριστερά σε κάθε row όπου το μεσαίο cell έτυχε να είναι κενό. Το να καθαριστεί αυτό downstream, μέσα στην calling application, είναι το σημείο όπου πεθαίνουν τα document-import projects
Γιατί μια PDF page σου δίνει fragments αντί για table
Επειδή μια PDF page δεν φέρει table semantics εκτός αν το document είναι tagged. Το content stream περιέχει text-showing operators και positioning matrices (ISO 32000-1 §9.4.3) και τίποτε άλλο· το ruled box που βλέπεις στην οθόνη είναι άσχετο path painting που κανένας extractor δεν υποχρεούται να συσχετίσει με το text. Τα structure element types Table, TR, TH και TD ζουν μόνο στη logical structure hierarchy tagged PDF (ISO 32000-1 §14.8.4), και η συντριπτική πλειονότητα των business documents σε κυκλοφορία δεν είναι tagged. Όσα περιγράφονται παρακάτω είναι geometric recovery και όχι parsing, και πρέπει να ειπωθεί πριν κάποιος χτίσει reconciliation report πάνω τους
Γι’ αυτό το HotPDF τρέχει πρώτα semantic layout analysis πάνω στα extracted glyphs, το ίδιο pass που στηρίζει το structure-order text extraction από loaded PDF και τα structured HTML και XML exports. Αυτό το pass ομαδοποιεί baselines σε runs των οποίων τα cells ευθυγραμμίζονται κάθετα, και συνεχίζει ένα run μόνο όσο οι διαδοχικές rows έχουν ίδιο cell count. Για layout engine ο κανόνας είναι σωστός και φθηνός. Για caller έχει λάθος σχήμα: μία row με κενό interior cell χωρίζει ένα οπτικό table σε δύο source tables. Το typed table layer κάθεται πάνω από αυτό το pass ακριβώς για να ξαναβάλει τα κομμάτια στη θέση τους
Canonical column grids και το ColumnTolerance knob
Το ExtractLoadedTypedTables κάνει merge των same-page fragments πριν από οτιδήποτε άλλο, και κάνει merge με βάση τη column geometry και όχι το row text. Δύο adjacent source tables στην ίδια page ενώνονται όταν έχουν και τα δύο τουλάχιστον δύο columns, όταν το vertical gap ανάμεσα στην τελευταία row του πρώτου και την πρώτη row του δεύτερου μένει μέσα στο tolerance band και όταν οι column start positions τους ευθυγραμμίζονται. Column starts μέσα σε ColumnTolerance απόσταση η μία από την άλλη συμπτύσσονται σε ένα canonical column και γίνονται average κατά το merge. Το default tolerance είναι 12 user-space units, κατάλληλο για συνηθισμένη business typography, και χρειάζεται αύξηση σε wide-tracked ή βαθιά indented layouts
Αυτό που συμβαίνει σε row που λείπει interior value είναι το κρίσιμο σημείο. Το HotPDF κάνει snap κάθε cell στο κοντινότερο canonical column start και μετά ορίζει ColumnSpan ως την απόσταση από εκείνο το column μέχρι το επόμενο occupied, αντί να μετακινεί τα υπόλοιπα cells αριστερά. Μια row τριών cells σε grid πέντε columns κρατά τις τιμές της κάτω από τα σωστά headings και καταγράφει ακριβώς πού είναι τα gaps. Αυτή είναι η διαφορά ανάμεσα σε table που μπορείς να κάνεις reconcile και σε table που αποδίδει σιωπηρά λάθος ποσά
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; // user-space units
Options.MinimumTableConfidence := 0.55; // κάτω από αυτό, τα tables απορρίπτονται
Options.DateOrder := ttdoDMY; // 03/04/2026 είναι 3 April
Options.DecimalSeparator := ',';
Options.ThousandsSeparator := '.';
if Pdf.ExtractLoadedTypedTables([0, 1, 2, 3], Options, Tables, Info) then
// Info.TableCount έναντι Info.SourceTableCount δείχνει πόσα έγιναν merge
ProcessTables(Tables)
else if Info.Status = ttesBudgetExceeded then
Log(string(Info.Diagnostic));
finally
Pdf.Free;
end;
end;
Τι εγγυάται πραγματικά το cross-page merging
Εγγυάται συντηρητικότητα, σκόπιμα. Το HotPDF ενώνει δύο tables σε page boundary μόνο όταν είναι ενεργό το MergeAcrossPages, όταν το δεύτερο table αρχίζει ακριβώς στο page index μετά από εκείνο όπου τελειώνει το πρώτο, όταν και τα δύο έχουν τουλάχιστον δύο columns και όταν τουλάχιστον δύο canonical column starts ευθυγραμμίζονται μέσα στο ColumnTolerance. Η συνθήκη consecutive-page είναι αυτή που κρατά όλο το βάρος. Οι callers περνούν το PageIndices ως open array με όποια σειρά θέλουν, και χωρίς αυτόν τον έλεγχο ένα request για pages 3, 9 και 14 θα μπορούσε να συγκολλήσει τρία άσχετα tables σε ένα αποτέλεσμα που μοιάζει απολύτως πιθανό. Το κόστος είναι ότι genuine continuation που παραλείπει page, interleaved appendix ή duplex scan με blank verso επιστρέφει ως δύο tables και καμία option δεν το χαλαρώνει. Το rejoining είναι policy call που μόνο η calling application μπορεί να κάνει, γι’ αυτό η API εκθέτει τα FirstPageIndex, LastPageIndex, SourceTableCount και per-row PageIndex και αφήνει την απόφαση εκεί όπου ανήκει
Τα repeated headers σημειώνονται, δεν διαγράφονται
Το ExtractLoadedTypedTables δεν αφαιρεί ποτέ repeated header row από το αποτέλεσμα. Όταν ένα cross-page merge βρίσκει ότι το incoming table ανοίγει με header text ίδιο με το accumulated table, αφού γίνει trim και case folding, χαρακτηρίζει τις rows ως IsHeader και IsRepeatedHeader και τις προσθέτει και πάλι με τη source order. Η διαγραφή είναι lossy και irreversible επιλογή, και διαφορετικοί consumers θέλουν διαφορετικές απαντήσεις: ένα CSV import θέλει τα repeats εκτός, ένα audit trail θέλει να υπάρχουν με τα page numbers τους και ένα diffing tool θέλει τη source order διατηρημένη byte προς byte. Άρα η library αναφέρει και ο caller αποφασίζει
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; // κράτα μόνο το πρώτο header block
for C := 0 to High(Row.Cells) do
if Row.Cells[C].ValueKind = ttvkCurrency then
Total := Total + Row.Cells[C].NumberValue;
end;
end;
Typed values και οι separators που πρέπει να δώσεις
Το type inference τρέχει με fixed σειρά που επιλύει τις ambiguities προς τη μόνη λογική κατεύθυνση: πρώτα boolean, μετά date, μετά percentage, μετά currency και μετά plain number, ενώ οτιδήποτε δεν ταιριάξει παραμένει string. Η σειρά είναι αυτή που εμποδίζει το 2026 σε date column να κριθεί από number parser πριν το δει ο date parser. Currency αναγνωρίζεται από leading $, £, ¥ ή € ή από three-letter ISO 4217 code followed by a space, και το code διατηρείται στο CurrencyCode. Κρίσιμα, το HotPDF δεν μαντεύει το locale σου. Τα DecimalSeparator, ThousandsSeparator και DateOrder έρχονται από options, επειδή το 1.234 είναι είτε ένας αριθμός είτε χίλια διακόσια τριάντα τέσσερα ανάλογα με γεγονός που το PDF δεν περιέχει. Το raw Unicode Text διατηρείται σε κάθε cell μαζί με το typed value, οπότε ένα λάθος guess ανακτάται πάντα χωρίς δεύτερο extraction pass
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;
Τα δύο export formats απαντούν σε διαφορετικές ερωτήσεις και σκόπιμα δεν είναι ισοδύναμα. Το CSV γράφει τις continuation columns ενός merged span ως κενά fields, όπως περιμένει ένα spreadsheet ή bulk loader. Το JSON κρατά όσα γνώριζε η extraction: το typed value κάτω από το δικό του kind, columnSpan, per-cell και per-row confidence, τα cell bounds και το page και source-table provenance. Και τα δύο formats κάνουν stage ολόκληρο το document σε bounded in-memory buffer και μόνο μετά δημοσιεύουν στο destination stream, επαναφέροντας τα αρχικά bytes, length και position αν αποτύχει το write στη μέση, οπότε αποτυχημένο export δεν αφήνει μισογραμμένο file. Budgets για pages, glyphs ανά page, tables, rows, cells, characters και output bytes υπολογίζονται χωριστά, και οι rows μετρώνται πριν από την allocation επειδή ένα per-row SetLength εκφυλίζεται σε quadratic copying πολύ πριν από το default ceiling του ενός εκατομμυρίου rows
Πού παραδίδει η geometric table recovery
Το να δηλώσεις ρητά τα failure modes είναι χρησιμότερο από feature list, επειδή καθένα από αυτά είναι σημείο όπου ο caller χρειάζεται δική του policy και όχι καλύτερη option value
- Vertical merges δεν ανακτώνται. Το HotPDF αναφέρει
ColumnSpanγια horizontal spans και αφήνει τοRowSpanστο 1, οπότε cell που εκτείνεται σε τρεις rows στον τυπωμένο table φτάνει ως ένα cell και δύο gaps - Το header detection είναι data-driven και όχι visual. Το header block είναι το run των rows πριν από την πρώτη row που περιέχει non-string typed value, άρα table του οποίου το body είναι εξ ολοκλήρου text αναφέρει
HeaderRowCountίσο με zero ανεξάρτητα από το style - Tables κάτω από
MinimumTableConfidenceαπορρίπτονται από το αποτέλεσμα χωρίς error. ΣύγκρινεInfo.TableCountμεInfo.SourceTableCountόταν χρειάζεται να γνωρίζεις ότι κάτι discarded - Ένα run χρειάζεται τουλάχιστον δύο rows και δύο columns πριν το layout pass το χαρακτηρίσει table, οπότε one-line pseudo-table ή two-column layout με long prose δεν είναι, σωστά αλλά όχι βοηθητικά, table
- Scanned pages δεν περιέχουν text operators, άρα δεν υπάρχει τίποτε για geometric recovery μέχρι να υπάρχει OCR text layer στη σελίδα
Αν τα PDFs προέρχονται από το δικό σου reporting stack, η φθηνότερη διόρθωση όλων αυτών είναι upstream: εξέπεμπε tagged tables ή κράτα τα source data και αντιμετώπισε την extraction ως fallback για documents που δεν παρήγαγες εσύ. Για οτιδήποτε άλλο αξίζει να μάθεις το pipeline με αυτή τη σειρά, αφού κάθε layer χτίζει πάνω στο προηγούμενο: ξεκίνα με plain text extraction από loaded PDF, ανέβα στο typed table API όταν πρέπει να διατηρηθεί η geometry και δες το rendering data table σε νέο PDF όταν βρίσκεσαι στην πλευρά της παραγωγής και εσύ αποφασίζεις πόσο recoverable θα είναι το output
Τα ExtractLoadedTypedTables και ExportLoadedTypedTables διατίθενται ως μέρος του native HotPDF Delphi PDF Component για Delphi και C++Builder, χωρίς external DLL και χωρίς runtime dependency· η product page περιέχει την πλήρη reference των options, statuses και records του typed table API