A HotPDF a ExtractLoadedTypedTables Delphi API-val nyer ki táblázatokat meglévő PDF-ből: összeolvasztja a tördelési menet sorfragmentumait, minden táblához egy kanonikus oszloprácsot épít, a geometriát támogató oldaltörésen át folytatja a táblát, és minden cellát típusos értékként ad vissza oldalprovenienciával, oszloppal és határokkal. Az ExportLoadedTypedTables ugyanezt az eredményt közvetlenül CSV-be vagy JSON-ba írja. A problémát, ami miatt ez megéri, unalmasan gyakori. Egy negyvenoldalas számlanyilvántartás logikailag egyetlen tábla, minden oldal tetején megismételt fejléccel. Egy naiv olvasási sorrenddel negyven táblát, harminckilenc téves fejlécsort és olyan pénznemoszlopot kapsz, amely minden olyan sorban egy pozícióval balra csúszik, ahol a középső cella üres volt. Ennek utólagos rendbetétele, a hívó alkalmazásban, ott öli meg a dokumentumimport-projekteket
Miért adnak a PDF-oldalak tábla helyett fragmentumokat?
Azért, mert a PDF-oldal egyáltalán nem hordoz táblaszemantikát, hacsak a dokumentum nincs tagelve. A content stream csak szövegmegjelenítő operátorokat és pozicionáló mátrixokat tartalmaz (ISO 32000-1 §9.4.3), mást nem; a képernyőn látható keretezett doboz független path-rajzolás, amelyet semmilyen extractor nem köteles a szöveghez kapcsolni. A Table, TR, TH és TD struktúraelemtípusok csak egy tagelt PDF logikai struktúrahierarchiájában élnek (ISO 32000-1 §14.8.4), a forgalomban lévő üzleti dokumentumok túlnyomó része pedig nem tagelt. Minden, ami lentebb jön, geometriai helyreállítás, nem parsing, és ezt még azelőtt érdemes hangosan kimondani, hogy bárki reconciliation reportot építene rá
A HotPDF ezért először szemantikus layout-elemzést végez a kinyert glypheken, ugyanazt a menetet, amely a betöltött PDF struktúrasorrendű szövegkinyerését és a strukturált HTML- és XML-exportokat is ellátja. Ez a menet a baseline-okat olyan runokba csoportosítja, amelyek cellái függőlegesen igazodnak, és csak addig folytat egy runt, amíg az egymást követő soroknak azonos a cellaszáma. A layout-engine számára ez helyes és olcsó szabály. A hívó számára viszont rossz alak: egy üres belső cellát tartalmazó sor két source table-re bontja az egyetlen vizuális táblát. A typed-table réteg éppen azért ül e menet fölött, hogy újra összeillessze a darabokat
Kanonikus oszloprácsok és a ColumnTolerance kapcsoló
Az ExtractLoadedTypedTables bármi más előtt azonos oldalon lévő fragmentumokat egyesít, és sorok szövege helyett oszlopgeometriával dönt. Két szomszédos forrástábla akkor olvad össze, ha mindkettőnek legalább két oszlopa van, az első utolsó sora és a második első sora közötti függőleges rés a tűrési sávon belül marad, az oszlopkezdések pedig egy vonalba esnek. A ColumnTolerance értékén belüli oszlopkezdések egy kanonikus oszloppá olvadnak, az egyesítés során átlagolva. Az alapértelmezett tűrés 12 user-space egység, ami a szokásos üzleti tipográfiához illik, széles trackinggel vagy mélyen behúzott layouttal viszont emelni kell
Az igazán fontos rész az, mi történik egy belső értéket nélkülöző sorral. A HotPDF minden cellát a legközelebbi kanonikus oszlopkezdésre pattint, majd a ColumnSpan-t az adott oszloptól a következő foglaltig terjedő távolságra állítja, nem tolja balra a megmaradt cellákat. Egy ötoszlopos rács háromcellás sora így a helyes fejlécek alatt tartja meg az értékeket, és pontosan jelzi, hol vannak a hézagok. Ettől lesz a táblázat reconciliálható, ahelyett hogy csendben rossz összeghez rendelné a pénzt
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 egységek
Options.MinimumTableConfidence := 0.55; // ez alatt a táblák kiesnek
Options.DateOrder := ttdoDMY; // 03/04/2026 = április 3.
Options.DecimalSeparator := ',';
Options.ThousandsSeparator := '.';
if Pdf.ExtractLoadedTypedTables([0, 1, 2, 3], Options, Tables, Info) then
// Info.TableCount és Info.SourceTableCount mutatja, mennyi olvadt össze
ProcessTables(Tables)
else if Info.Status = ttesBudgetExceeded then
Log(string(Info.Diagnostic));
finally
Pdf.Free;
end;
end;
Mit garantál valójában az oldalak közötti egyesítés?
Szándékosan konzervativizmust garantál. A HotPDF csak akkor kapcsol össze két táblát oldaltörésen át, ha a MergeAcrossPages engedélyezve van, a második tábla pontosan az első utolsó oldala utáni oldalon indul, mindkettőnek legalább két oszlopa van, és legalább két kanonikus oszlopkezdés a ColumnTolerance-on belül igazodik. A szomszédos oldalak feltétele a teherhordó rész. A hívók a PageIndices-et tetszőleges sorrendű open arrayként adják át, és ezzel az ellenőrzéssel egy 3., 9. és 14. oldalt kérő hívás három egymáshoz nem tartozó táblát hegeszthetne össze egy teljesen hihető eredménnyé. Az ára az, hogy egy valódi, oldalt kihagyó folytatás, egy közbeékelt appendix vagy egy üres hátoldalt tartalmazó duplex scan két táblaként érkezik, és nincs olyan opció, amely ezt lazítaná. Ezek újraegyesítése csak a hívó alkalmazás policy-döntése lehet, ezért az API kiteszi a FirstPageIndex, LastPageIndex, SourceTableCount és soronkénti PageIndex értéket, a döntést pedig ott hagyja, ahová való
A megismételt fejléceket megjelöli, sosem törli
Az ExtractLoadedTypedTables soha nem töröl megismételt fejlécsort az eredményből. Amikor egy oldalak közötti egyesítés azt látja, hogy a beérkező tábla ugyanazzal a fejléccel indul, mint az addig összegyűjtött tábla — trimelés és case folding után összevetve —, a sorokat IsHeader és IsRepeatedHeader flaggel jelöli, de forrássorrendben így is hozzáfűzi. A törlés veszteséges és visszafordíthatatlan döntés, a különböző fogyasztók pedig mást akarnak: a CSV-import nem kéri az ismétléseket, az auditnyom szeretné őket az oldalszámukkal, egy diffeszköz pedig bájtról bájtra megőrzött forrássorrendet akar. A library ezért jelent, a hívó pedig dönt
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; // csak az első fejlécblokk maradjon
for C := 0 to High(Row.Cells) do
if Row.Cells[C].ValueKind = ttvkCurrency then
Total := Total + Row.Cells[C].NumberValue;
end;
end;
Típusos értékek és a megadandó elválasztók
A típuskövetkeztetés fix sorrendben fut, és az egyértelműtlenségeket az egyetlen józan irányban oldja fel: először boolean, aztán dátum, százalék, pénznem, sima szám, minden más pedig string marad. A sorrend akadályozza meg, hogy egy dátumoszlop 2026 értékét a számparser eldöntse, mielőtt a dátumparser meglátná. A pénznemet kezdő $, £, ¥ vagy €, illetve szóközt követő hárombetűs ISO 4217 kód alapján ismeri fel, a kódot pedig megőrzi a CurrencyCode-ban. A HotPDF döntő módon nem találgatja ki a locale-odat. A DecimalSeparator, ThousandsSeparator és DateOrder az opciókból jön, mert az 1.234 lehet egyetlen szám vagy ezerkettőszázharmincnégy attól függően, amit a PDF nem tartalmaz. A nyers Unicode Text minden cellában megmarad a típusos érték mellett, így egy rossz találgatás mindig visszafejthető új extraction menet nélkül
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;
A két exportformátum eltérő kérdésekre válaszol, és szándékosan nem ekvivalens. A CSV az összevont span folytatóoszlopait üres mezőként írja ki, ezt várja egy spreadsheet vagy bulk loader. A JSON mindent megőriz, amit az extraction tudott: a típusos értéket a saját kindja alatt, a columnSpan-t, cella- és soronkénti konfidenciát, a cellahatárokat, valamint az oldal- és forrástábla-provenienciát. Mindkét formátum a teljes dokumentumot korlátozott memóriapufferbe állítja össze, és csak ezután publikálja a célstreambe, hiba esetén visszaállítva az eredeti byte-okat, hosszt és pozíciót, így egy elbukott export soha nem hagy fél fájlt maga után. Az oldalak, oldalankénti glyphek, táblák, sorok, cellák, karakterek és kimeneti byte-ok budgetje külön számolódik, a sorokat pedig allokáció előtt számolja, mert a soronkénti SetLength jóval a milliós alapértelmezett plafon előtt kvadratikus másolásba fordulna
Hol adja fel a geometriai táblarekonstruálás?
A hibamódok explicitté tétele hasznosabb egy feature-listánál, mert mindegyik olyan pont, ahol a hívónak saját policyra van szüksége, nem egy jobb opcióértékre
- Függőleges összevonásokat nem állít helyre. A HotPDF a vízszintes spanekre
ColumnSpan-t jelent, aRowSpan-t pedig 1-en hagyja, ezért egy nyomtatott táblában három soron átívelő cella egy cellaként és két hézagként érkezik - A fejlécfelismerés adatvezérelt, nem vizuális. A fejlécblokk az első nem string típusos értéket tartalmazó sor előtti sorfutás, ezért egy teljesen szöveges törzsű tábla stílustól függetlenül nulla
HeaderRowCount-ot jelent - A
MinimumTableConfidencealatti táblák hiba nélkül kiesnek az eredményből. Hasonlítsd össze azInfo.TableCountésInfo.SourceTableCountértékét, ha tudni akarod, hogy valami el lett-e dobva - Egy runnak legalább két sor és legalább két oszlop kell, hogy a layout-pass egyáltalán táblának nevezze, így egy egysoros ál-tábla vagy egy hosszú prózát tartalmazó kétoszlopos layout helyesen, bár nem túl hasznosan nem lesz tábla
- A szkennelt oldalak nem tartalmaznak szövegoperátorokat, ezért geometriailag nincs mit helyreállítani, amíg nem kerül OCR text layer az oldalra
Ha a PDF-ek a saját reporting stackedből jönnek, mindezek legolcsóbb javítása upstream történik: állíts elő tagelt táblákat, vagy őrizd meg a forrásadatot, az extractiont pedig kezeld olyan dokumentumok fallbackjeként, amelyeket nem te gyártottál. Minden másnál érdemes ebben a sorrendben megismerni a pipeline-t, mert minden réteg az alatta lévőre épül: kezdd a betöltött PDF sima szövegkinyerésével, lépj fel a typed-table API-ra, amikor a geometriát is meg kell őrizni, és nézd meg az adattábla új PDF-be renderelését, amikor generáló oldalon vagy, és te döntöd el, mennyire legyen helyreállítható a kimenet
Az ExtractLoadedTypedTables és az ExportLoadedTypedTables a natív HotPDF Delphi PDF Component része Delphi és C++Builder számára, külső DLL és runtime-függőség nélkül; a termékoldal tartalmazza a typed-table API teljes opció-, státusz- és rekord-referenciáját