Odborný článok

Typová extrakcia tabuliek PDF v Delphi cez zalomenia strán

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 ColumnSpan pre horizontálne spany a RowSpan nechá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 HeaderRowCount ako nulu bez ohľadu na štýl
  • Tabuľky pod MinimumTableConfidence sa z výsledku zahodia bez chyby. Ak potrebujete vedieť, že sa niečo zahodilo, porovnajte Info.TableCount s Info.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