Tehnički članak

Tipizirane PDF tablice preko prijeloma stranice u Delphiju

HotPDF oporavlja tablice iz postojećeg PDF-a kroz ExtractLoadedTypedTables, Delphi API koji spaja fragmente redaka proizvedene prolazom rasporeda, gradi jednu kanonsku mrežu stupaca po tablici, nastavlja tablicu preko prijeloma stranice kada geometrija to podržava i vraća svaku ćeliju kao tipiziranu vrijednost s podrijetlom stranice, rasponom stupaca i granicama. ExportLoadedTypedTables isti rezultat zapisuje izravno u CSV ili JSON. Scenarij zbog kojeg se ovo isplati izgraditi dosadan je i vrlo čest. Registar računa od četrdeset stranica, logički jedna tablica, ispisan s ponovljenim zaglavljem na vrhu svake stranice. Pokrenite na njemu naivni prolaz redoslijeda čitanja i dobit ćete četrdeset tablica, trideset i devet lažnih redaka zaglavlja i stupac valute koji na svakom retku gdje je srednja ćelija slučajno prazna klizne za jedno mjesto ulijevo. Čišćenje toga nizvodno, unutar pozivateljske aplikacije, mjesto je na kojem projekti uvoza dokumenata umiru

Zašto vam PDF stranica predaje fragmente umjesto tablice?

Jer PDF stranica nema nikakvu semantiku tablice osim ako je dokument označen. Tok sadržaja drži operatore prikaza teksta i matrice pozicioniranja (ISO 32000-1 §9.4.3) i ništa više; iscrtani okvir koji vidite na zaslonu nepovezano je crtanje putanje koje nijedan ekstraktor nije dužan povezati s tekstom. Tipovi elemenata strukture Table, TR, TH i TD žive samo u hijerarhiji logičke strukture označenog PDF-a (ISO 32000-1 §14.8.4), a golema većina poslovnih dokumenata u opticaju nije označena. Sve što je opisano u nastavku geometrijski je oporavak, a ne parsiranje, i to vrijedi reći naglas prije nego što itko na tome izgradi izvještaj usklađivanja

HotPDF zato najprije pokreće semantičku analizu rasporeda nad izdvojenim glifovima, isti prolaz koji pokreće izdvajanje teksta prema redoslijedu strukture iz učitanog PDF-a i strukturirane izvoze HTML-a i XML-a. Taj prolaz grupira osnovne linije u nizove čije se ćelije okomito poravnavaju i nastavlja niz samo dok uzastopni retci imaju isti broj ćelija. Za mehanizam rasporeda to je pravilo ispravno i jeftino. Za pozivatelja je pogrešnog oblika: jedan redak s praznom unutarnjom ćelijom dijeli jednu vizualnu tablicu na dvije izvorne tablice. Tipizirani sloj tablice postoji upravo iznad tog prolaza kako bi dijelove spojio natrag

Kanonske mreže stupaca i postavka ColumnTolerance

ExtractLoadedTypedTables spaja fragmente iste stranice prije nego što učini bilo što drugo i spaja ih prema geometriji stupaca, a ne prema tekstu redaka. Dvije susjedne izvorne tablice na jednoj stranici spajaju se kada obje imaju barem dva stupca, kada okomiti razmak između posljednjeg retka prve i prvog retka druge ostane unutar pojasa tolerancije i kada se početni položaji njihovih stupaca poravnaju. Početci stupaca unutar ColumnTolerance jedan od drugoga stapaju se u jedan kanonski stupac i pri spajanju se usrednjuju. Zadana tolerancija iznosi 12 jedinica korisničkog prostora, što odgovara običnoj poslovnoj tipografiji, a traži povećanje za široko razmaknute ili duboko uvučene rasporede

Ono što se dogodi retku kojem nedostaje unutarnja vrijednost dio je koji je važan. HotPDF svaku ćeliju hvata za najbliži kanonski početak stupca i zatim ColumnSpan postavlja na udaljenost od tog stupca do sljedećeg zauzetog, umjesto da preostale ćelije pomakne ulijevo. Redak od tri ćelije u mreži od pet stupaca zadržava vrijednosti pod ispravnim naslovima i točno bilježi gdje su praznine. To je razlika između tablice koju možete uskladiti i one koja tiho pripiše novac pogrešnoj stavci

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;           // jedinice korisničkog prostora
    Options.MinimumTableConfidence := 0.55;  // ispod ovoga tablice se odbacuju
    Options.DateOrder := ttdoDMY;            // 03/04/2026 je 3. travnja
    Options.DecimalSeparator := ',';
    Options.ThousandsSeparator := '.';
    if Pdf.ExtractLoadedTypedTables([0, 1, 2, 3], Options, Tables, Info) then
      // Info.TableCount prema Info.SourceTableCount pokazuje koliko je spojeno
      ProcessTables(Tables)
    else if Info.Status = ttesBudgetExceeded then
      Log(string(Info.Diagnostic));
  finally
    Pdf.Free;
  end;
end;

Što spajanje preko stranica doista jamči?

Jamči konzervativnost, namjerno. HotPDF spaja dvije tablice preko granice stranice samo kada je MergeAcrossPages uključen, kada druga tablica počinje točno na indeksu stranice nakon one na kojoj prva završava, kada obje imaju barem dva stupca i kada se barem dva kanonska početka stupaca poravnaju unutar ColumnTolerance. Uvjet uzastopnih stranica nosivi je dio. Pozivatelji predaju PageIndices kao otvoreno polje u bilo kojem redoslijedu, a bez te provjere zahtjev za stranicama 3, 9 i 14 mogao bi zavariti tri nepovezane tablice u jedan rezultat koji izgleda posve uvjerljivo. Cijena je da stvarni nastavak koji preskače stranicu, umetnuti dodatak ili obostrani sken s praznom poleđinom dolazi natrag kao dvije tablice, a nijedna opcija to ne ublažava. Ponovno spajanje tih tablica pozivateljska je odluka koju može donijeti samo aplikacija, pa API izlaže FirstPageIndex, LastPageIndex, SourceTableCount i PageIndex po retku te odluku ostavlja ondje gdje pripada

Ponavljana zaglavlja označavaju se, nikad brišu

ExtractLoadedTypedTables nikad ne uklanja ponovljeni redak zaglavlja iz rezultata. Kada spajanje preko stranica otkrije da ulazna tablica počinje tekstom zaglavlja identičnim nakupljenoj tablici, uspoređenim nakon uklanjanja razmaka i izjednačavanja veličine slova, te retke označuje s IsHeader i IsRepeatedHeader te ih svejedno dodaje redoslijedom izvora. Brisanje je gubitni i nepovratan izbor, a različiti potrošači žele različite odgovore: uvoz CSV-a želi uklonjene ponavljane retke, revizijski trag želi ih s brojevima stranica, alat za usporedbu želi redoslijed izvora sačuvan bajt po bajt. Zato biblioteka prijavljuje, a pozivatelj odlučuje

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;                    // zadrži samo prvi blok zaglavlja
      for C := 0 to High(Row.Cells) do
        if Row.Cells[C].ValueKind = ttvkCurrency then
          Total := Total + Row.Cells[C].NumberValue;
    end;
end;

Tipizirane vrijednosti i razdjelnici koje morate zadati

Zaključivanje tipa pokreće se fiksnim redoslijedom koji dvosmislenosti razrješava u jedinom razumnom smjeru: najprije boolean, zatim datum, postotak, valuta pa običan broj, dok sve što se ne podudara ostaje string. Redoslijed je ono što sprječava da 2026 u stupcu datuma odredi parser brojeva prije nego što ga vidi parser datuma. Valuta se prepoznaje iz vodećeg $, £, ¥ ili , odnosno iz troslovnog koda ISO 4217 iza kojeg slijedi razmak, a kod se čuva u CurrencyCode. Ključno je da HotPDF ne pogađa vaš locale. DecimalSeparator, ThousandsSeparator i DateOrder dolaze iz opcija jer je 1.234 ili jedan broj ili tisuću dvjesto trideset i četiri, ovisno o činjenici koju PDF ne sadrži. Sirovi Unicode Text čuva se u svakoj ćeliji uz tipiziranu vrijednost, pa je pogrešnu pretpostavku uvijek moguće oporaviti bez drugog prolaza izdvajanja

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 izvozna formata odgovaraju na različita pitanja i namjerno nisu jednaka. CSV piše nastavne stupce spojenog raspona kao prazna polja, što očekuju proračunska tablica ili masovni učitavač. JSON čuva sve što je izdvajanje znalo: tipiziranu vrijednost pod vlastitom vrstom, columnSpan, pouzdanost po ćeliji i retku, granice ćelije te podrijetlo stranice i izvorne tablice. Oba formata cijeli dokument prvo smještaju u ograničeni memorijski međuspremnik i tek ga zatim objavljuju u odredišnom toku, a pri neuspjehu pisanja vraćaju izvorne bajtove, duljinu i položaj, pa neuspjeli izvoz nikad ne ostavlja napola zapisanu datoteku. Proračuni za stranice, glifove po stranici, tablice, retke, ćelije, znakove i izlazne bajtove vode se zasebno, a retci se broje prije alokacije jer se SetLength po retku degenerira u kvadratno kopiranje puno prije zadane granice od milijun redaka

Gdje geometrijski oporavak tablice odustaje

Izričito navođenje načina neuspjeha korisnije je od popisa značajki jer je svako od ovih mjesta situacija u kojoj pozivatelju treba vlastita politika, a ne bolja vrijednost opcije

  • Okomita spajanja ne oporavljaju se. HotPDF prijavljuje ColumnSpan za vodoravne raspone i ostavlja RowSpan na 1, pa ćelija koja u ispisanoj tablici obuhvaća tri retka dolazi kao jedna ćelija i dvije praznine
  • Otkrivanje zaglavlja pokreću podaci, a ne izgled. Blok zaglavlja niz je redaka prije prvog retka koji sadržava tipiziranu vrijednost koja nije string, pa tablica čije je tijelo u cijelosti tekst prijavljuje HeaderRowCount kao nulu bez obzira na stil
  • Tablice ispod MinimumTableConfidence odbacuju se iz rezultata bez pogreške. Usporedite Info.TableCount s Info.SourceTableCount kada trebate znati da je nešto odbačeno
  • Nizu trebaju barem dva retka i barem dva stupca prije nego što ga prolaz rasporeda uopće proglasi tablicom, pa jednoredna pseudo-tablica ili raspored od dvaju stupaca dugog proznog teksta ispravno, ali nekorisno, nije tablica
  • Skenirane stranice nemaju operatore teksta, pa se geometrijski nema što oporaviti dok na stranici ne postoji OCR tekstualni sloj

Ako vaši PDF-ovi izlaze iz vlastitog sustava izvještavanja, najjeftiniji popravak za sve ovo uzvodno je: emitirajte označene tablice ili zadržite izvorne podatke, a izdvajanje tretirajte kao fallback za dokumente koje niste proizveli. Za sve ostalo vrijedi učiti cjevovod ovim redoslijedom jer se svaki sloj nadograđuje na prethodni: počnite s običnim izdvajanjem teksta iz učitanog PDF-a, prijeđite na API tipizirane tablice kada geometriju morate sačuvati i pogledajte iscrtavanje podatkovne tablice u novi PDF kada ste na strani generiranja i možete odlučiti koliko će izlaz biti oporavljiv

ExtractLoadedTypedTables i ExportLoadedTypedTables isporučuju se kao dio izvorne HotPDF Delphi PDF Component za Delphi i C++Builder, bez vanjskog DLL-a i bez ovisnosti o runtimeu; stranica proizvoda sadrži potpunu referencu opcija, statusa i zapisa za API tipiziranih tablica