HotPDF obnovuje tabuľky z existujúceho PDF cez ExtractLoadedTypedTables, API pre Delphi, ktoré spája fragmenty riadkov vytvorené layout prechodom, zostaví pre každú tabuľku jednu kanonickú mriežku stĺpcov, pokračuje s tabuľkou cez zalomenie strany, keď to geometria podporuje, a každú bunku vráti ako typovanú hodnotu s pôvodom stránky, spanom stĺpcov a hranicami. ExportLoadedTypedTables zapíše ten istý výsledok priamo do CSV alebo JSON. Scenár, pre ktorý sa to oplatí postaviť, je nudný a nesmierne bežný. Štyridsaťstranový register faktúr, logicky jedna tabuľka, vytlačený s hlavičkou opakovanou hore na každej strane. Pustite nad ním naivný prechod v poradí čítania a dostanete štyridsať tabuliek, tridsaťdeväť falošných riadkov hlavičky a stĺpec meny, ktorý sa na každom riadku s náhodne prázdnou prostrednou bunkou posunie o jednu pozíciu doľava. Upratovanie tohto výsledku downstream v volajúcej aplikácii je miesto, kde projekty importu dokumentov chodia zomierať
Prečo vám stránka PDF odovzdá fragmenty namiesto tabuľky?
Pretože stránka PDF nenesie vôbec žiadnu sémantiku tabuľky, pokiaľ dokument nie je tagovaný. Content stream drží operátory zobrazujúce text a pozičné matice (ISO 32000-1 §9.4.3) a nič viac; orámovaný box, ktorý vidíte na obrazovke, je nesúvisiace kreslenie path, ktoré žiadny extractor nemusí korelovať s textom. Typy elementov štruktúry Table, TR, TH a TD žijú iba v hierarchii logickej štruktúry tagovaného PDF (ISO 32000-1 §14.8.4) a drvivá väčšina obchodných dokumentov v obehu tagovaná nie je. Všetko opísané nižšie je geometrická obnova, nie parsing, a to sa oplatí povedať nahlas skôr, než nad tým niekto postaví reconciliation report
HotPDF preto najprv vykoná sémantickú layout analýzu nad extrahovanými glyfmi, rovnaký prechod, ktorý stojí za extrakciou textu z načítaného PDF v poradí štruktúry aj štruktúrovanými exportmi HTML a XML. Tento prechod zoskupí baseline do behov, ktorých bunky sa zarovnávajú vertikálne, a beh bude pokračovať iba vtedy, kým majú po sebe idúce riadky rovnaký počet buniek. Pre layout engine je toto pravidlo správne a lacné. Pre volajúceho má nesprávny tvar: jediný riadok s prázdnou vnútornou bunkou rozdelí jednu vizuálnu tabuľku na dve zdrojové tabuľky. Vrstva typovaných tabuliek sedí nad týmto prechodom práve preto, aby kúsky zložila späť
Kanonické mriežky stĺpcov a gombík ColumnTolerance
ExtractLoadedTypedTables najprv spojí fragmenty na tej istej stránke a spája ich podľa geometrie stĺpcov, nie podľa textu riadkov. Dve susedné zdrojové tabuľky na jednej stránke sa spoja, keď obe majú aspoň dva stĺpce, keď vertikálna medzera medzi posledným riadkom prvej a prvým riadkom druhej zostane v pásme tolerancie a keď sa zarovnajú pozície začiatkov stĺpcov. Začiatky stĺpcov v rámci ColumnTolerance od seba splynú do jedného kanonického stĺpca a pri spájaní sa spriemerujú. Predvolená tolerancia je 12 jednotiek user-space, čo vyhovuje bežnej obchodnej typografii a pri širokom trackingu alebo hlboko odsadených layoutoch si žiada zvýšenie
Na riadku, ktorému chýba vnútorná hodnota, záleží najviac. HotPDF priradí každú bunku k najbližšiemu kanonickému začiatku stĺpca a potom nastaví ColumnSpan na vzdialenosť od tohto stĺpca k ďalšiemu obsadenému, namiesto posunutia zvyšných buniek doľava. Trojbunkový riadok v päťstĺpcovej mriežke si ponechá hodnoty pod správnymi hlavičkami a zaznamená presne, kde sú medzery. To je rozdiel medzi tabuľkou, ktorú možno zosúladiť, a tabuľkou, ktorá potichu priradí peniaze nesprávne
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; // jednotky user-space
Options.MinimumTableConfidence := 0.55; // pod touto hodnotou sa tabuľky zahodia
Options.DateOrder := ttdoDMY; // 03/04/2026 je 3. apríl
Options.DecimalSeparator := ',';
Options.ThousandsSeparator := '.';
if Pdf.ExtractLoadedTypedTables([0, 1, 2, 3], Options, Tables, Info) then
// Info.TableCount verzus Info.SourceTableCount ukazuje, koľko sa zlúčilo
ProcessTables(Tables)
else if Info.Status = ttesBudgetExceeded then
Log(string(Info.Diagnostic));
finally
Pdf.Free;
end;
end;
Čo v skutočnosti garantuje spájanie cez strany?
Garantuje konzervatívnosť, a to zámerne. HotPDF spojí dve tabuľky cez hranicu strany iba vtedy, keď je zapnuté MergeAcrossPages, keď druhá tabuľka začína presne na indexe strany nasledujúcom po poslednej strane prvej, keď obe majú aspoň dva stĺpce a keď sa aspoň dva kanonické začiatky stĺpcov zarovnajú v rámci ColumnTolerance. Podmienka po sebe idúcich strán je nosná. Volajúci odovzdávajú PageIndices ako open array v ľubovoľnom poradí a bez tejto kontroly by požiadavka na strany 3, 9 a 14 mohla zvariť tri nesúvisiace tabuľky do jedného úplne vierohodne vyzerajúceho výsledku. Cenou je, že skutočné pokračovanie preskakujúce stranu, interleaved appendix alebo duplexný sken s prázdnou zadnou stranou sa vrátia ako dve tabuľky a žiadna voľba to neuvoľní. Ich opätovné spojenie je policy call, ktorý môže urobiť iba volajúca aplikácia, preto API vystavuje FirstPageIndex, LastPageIndex, SourceTableCount a PageIndex pri každom riadku a rozhodnutie necháva tam, kam patrí
Opakované hlavičky sa označia, nikdy nevymažú
ExtractLoadedTypedTables opakovaný riadok hlavičky z výsledku nikdy neodstráni. Keď cross-page merge zistí, že prichádzajúca tabuľka sa otvára textom hlavičky identickým s akumulovanou tabuľkou, porovnaným po orezaní a case foldingu, označí tieto riadky IsHeader a IsRepeatedHeader a aj tak ich pridá v poradí zdroja. Mazanie je stratová, nevratná voľba a rôzni konzumenti chcú rôzne odpovede: CSV import chce opakovania preč, audit trail ich chce s číslami strán, diffing nástroj chce zachované poradie zdroja bajt za bajtom. Knižnica teda hlási a volajúci rozhoduje
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; // ponechaj iba prvý blok hlavičky
for C := 0 to High(Row.Cells) do
if Row.Cells[C].ValueKind = ttvkCurrency then
Total := Total + Row.Cells[C].NumberValue;
end;
end;
Typované hodnoty a oddeľovače, ktoré musíte dodať
Inferencia typu prebieha v pevnom poradí, ktoré rieši nejednoznačnosti jediným rozumným smerom: najprv boolean, potom dátum, percento, mena a obyčajné číslo, pričom všetko, čo sa nezhoduje, zostane stringom. Práve poradie zabráni tomu, aby 2026 v stĺpci dátumu vyhodnotil number parser skôr, než ho uvidí date parser. Mena sa rozpozná z úvodného $, £, ¥ alebo €, prípadne z trojpísmenového kódu ISO 4217 nasledovaného medzerou, a kód sa zachová v CurrencyCode. Dôležité je, že HotPDF nehádá váš locale. DecimalSeparator, ThousandsSeparator a DateOrder pochádzajú z options, pretože 1.234 je buď jedno číslo, alebo tisíc dvesto tridsaťštyri podľa faktu, ktorý PDF neobsahuje. Surový Unicode Text zostáva v každej bunke spolu s typovanou hodnotou, takže nesprávny odhad je vždy obnoviteľný bez druhého extrakčného prechodu
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;
Dva exportné formáty odpovedajú na rôzne otázky a zámerne nie sú ekvivalentné. CSV zapisuje pokračovacie stĺpce zlúčeného spanu ako prázdne polia, čo očakáva spreadsheet alebo bulk loader. JSON uchová všetko, čo extrakcia vedela: typovanú hodnotu pod vlastným druhom, columnSpan, confidence bunky aj riadku, hranice bunky a pôvod stránky a zdrojovej tabuľky. Oba formáty pripravia celý dokument do ohraničeného in-memory bufferu a až potom ho publikujú do cieľového streamu, pričom pri zlyhaní zápisu uprostred obnovia pôvodné bajty, dĺžku aj pozíciu, takže neúspešný export nikdy nenechá napoly zapísaný súbor. Rozpočty strán, glyfov na stránku, tabuliek, riadkov, buniek, znakov aj výstupných bajtov sa účtujú oddelene a riadky sa počítajú pred alokáciou, pretože SetLength po riadkoch degeneruje na kvadratické kopírovanie dávno pred predvoleným stropom milióna riadkov
Kde sa geometrická obnova tabuľky vzdá
Explicitne povedať, kde zlyháva, je užitočnejšie než zoznam funkcií, pretože každé z týchto miest potrebuje vlastnú policy volajúceho namiesto lepšej hodnoty option
- Vertikálne merge sa neobnovujú. HotPDF hlási
ColumnSpanpre horizontálne spany aRowSpannecháva na 1, takže bunka rozprestretá cez tri riadky tlačenej tabuľky príde ako jedna bunka plus dve medzery - Detekcia hlavičky je založená na dátach, nie na vizuále. Blok hlavičky je beh riadkov pred prvým riadkom obsahujúcim netextovú typovanú hodnotu, takže tabuľka, ktorej telo je celé textové, hlási
HeaderRowCountako nulu bez ohľadu na štýl - Tabuľky pod
MinimumTableConfidencesa z výsledku zahodia bez chyby. Ak potrebujete vedieť, že sa niečo zahodilo, porovnajteInfo.TableCountsInfo.SourceTableCount - Beh potrebuje aspoň dva riadky a aspoň dva stĺpce, kým ho layout prechod vôbec nazve tabuľkou, takže jednoriadková pseudo-tabuľka alebo dvojstĺpcový layout dlhého prózového textu správne, ale neužitočne nie je tabuľkou
- Naskenované stránky nemajú textové operátory, takže geometricky nie je čo obnovovať, kým na stránke neexistuje OCR textová vrstva
Ak vaše PDF vychádzajú z vlastného reporting stacku, najlacnejšia oprava tohto všetkého je upstream: emitujte tagované tabuľky alebo si ponechajte zdrojové dáta a extrakciu považujte za fallback pre dokumenty, ktoré ste nevytvorili. Pri ostatných sa oplatí naučiť pipeline v tomto poradí, pretože každá vrstva stavia na tej pod ňou: začnite obyčajnou extrakciou textu z načítaného PDF, prejdite na API typovaných tabuliek, keď treba zachovať geometriu, a pozrite si renderovanie dátovej tabuľky do nového PDF, keď ste na strane generovania a môžete rozhodnúť, ako obnoviteľný bude výstup
ExtractLoadedTypedTables a ExportLoadedTypedTables sa dodávajú ako súčasť natívneho HotPDF Delphi PDF Component pre Delphi a C++Builder bez externej DLL a runtime závislosti; produktová stránka obsahuje úplnú referenciu options, statusov a recordov pre API typovaných tabuliek