Techninis straipsnis

Tipinių PDF lentelių išgavimas Delphi per puslapių lūžius

HotPDF atkuria lenteles iš esamo PDF per ExtractLoadedTypedTables – Delphi API, kuri sujungia maketo etapo sukurtus eilučių fragmentus, kiekvienai lentelei sudaro vieną kanoninį stulpelių tinklelį, tęsia lentelę per puslapio lūžį, kai geometrija tai leidžia, ir grąžina kiekvieną langelį kaip tipinę reikšmę su puslapio kilme, stulpelio aprėptimi ir ribomis. ExportLoadedTypedTables tą patį rezultatą tiesiai įrašo į CSV arba JSON. Situacija, dėl kurios verta tai kurti, nuobodi ir labai dažna. Keturiasdešimties puslapių sąskaitų registras logiškai yra viena lentelė, tačiau spausdinant jos antraštė kartojama kiekvieno puslapio viršuje. Paleidus naivų skaitymo tvarkos etapą gaunate keturiasdešimt lentelių, trisdešimt devynias netikras antraštės eilutes ir valiutos stulpelį, kuris kiekvienoje eilutėje, kur vidurinis langelis tuščias, pasislenka viena pozicija į kairę. Tolesnis tokio rezultato valymas iškvietėjo programoje yra vieta, kur dokumentų importo projektai miršta

Kodėl PDF puslapis pateikia fragmentus, o ne lentelę?

Nes PDF puslapis neturi jokios lentelės semantikos, nebent dokumentas pažymėtas. Turinio sraute yra teksto rodymo operatoriai ir pozicionavimo matricos (ISO 32000-1 §9.4.3), ir nieko daugiau; ekrane matomas langelių rėmelis yra nesusijęs kelio piešimas, kurio joks ekstraktorius neprivalo susieti su tekstu. Struktūros elementų tipai Table, TR, TH ir TD gyvena tik pažymėto PDF loginės struktūros hierarchijoje (ISO 32000-1 §14.8.4), o didžioji dalis apyvartoje esančių verslo dokumentų nėra pažymėti. Visa, kas aprašyta toliau, yra geometrinis atkūrimas, o ne analizė, ir tai verta pasakyti garsiai prieš kuriant suderinimo ataskaitą ant tokio rezultato

Todėl HotPDF pirmiausia atlieka semantinę maketo analizę išgautiems glifams, tuo pačiu etapu, kuris maitina struktūros tvarka paremtą teksto išgavimą iš įkelto PDF ir struktūruotus HTML bei XML eksportus. Šis etapas grupuoja bazines linijas į eilučių sekas, kurių langeliai lygiuojasi vertikaliai, ir tęsia seką tik tol, kol gretimos eilutės turi tiek pat langelių. Maketo varikliui ši taisyklė teisinga ir pigi. Iškvietėjui ji netinka: viena eilutė su tuščiu vidiniu langeliu suskaldo vizualiai vieną lentelę į dvi šaltinio lenteles. Tipinių lentelių sluoksnis yra būtent virš šio etapo, kad dalys būtų vėl sujungtos

Kanoniniai stulpelių tinkleliai ir ColumnTolerance valdiklis

ExtractLoadedTypedTables pirmiausia sujungia to paties puslapio fragmentus, o tik tada imasi ko nors kito, ir jungia pagal stulpelių geometriją, o ne eilučių tekstą. Dvi gretimos šaltinio lentelės viename puslapyje sujungiamos, kai abi turi bent du stulpelius, vertikalus tarpas tarp pirmosios paskutinės ir antrosios pirmos eilutės telpa į tolerancijos juostą, o jų stulpelių pradžios sutampa. Pradžios taškai, vienas nuo kito nutolę ne daugiau nei ColumnTolerance, suliejami į vieną kanoninį stulpelį ir sujungimo metu suvidurkinami. Numatytoji tolerancija yra 12 naudotojo erdvės vienetų, tinkanti įprastai verslo tipografikai, tačiau plačiai išretintam ar giliai įtrauktam maketui ją verta didinti

Svarbiausia yra tai, kas nutinka eilutei, kurioje trūksta vidinės reikšmės. HotPDF kiekvieną langelį pritraukia prie artimiausios kanoninio stulpelio pradžios, tada ColumnSpan nustato pagal atstumą nuo to stulpelio iki kito užimto stulpelio, užuot pastūmėjusi likusius langelius į kairę. Trijų langelių eilutė penkių stulpelių tinklelyje išlaiko reikšmes po teisingomis antraštėmis ir tiksliai užrašo, kur yra tarpai. Tuo lentelė, kurią galima suderinti, skiriasi nuo tos, kuri tyliai priskiria pinigus ne tam stulpeliui

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;           // naudotojo erdvės vienetai
    Options.MinimumTableConfidence := 0.55;  // žemiau šios reikšmės lentelės išmetamos
    Options.DateOrder := ttdoDMY;            // 03/04/2026 yra balandžio 3 d.
    Options.DecimalSeparator := ',';
    Options.ThousandsSeparator := '.';
    if Pdf.ExtractLoadedTypedTables([0, 1, 2, 3], Options, Tables, Info) then
      // Info.TableCount ir Info.SourceTableCount rodo, kiek buvo sulieta
      ProcessTables(Tables)
    else if Info.Status = ttesBudgetExceeded then
      Log(string(Info.Diagnostic));
  finally
    Pdf.Free;
  end;
end;

Ką iš tikrųjų garantuoja suliejimas per puslapius?

Jis tyčia garantuoja konservatyvumą. HotPDF sujungia dvi lenteles per puslapio ribą tik kai įjungtas MergeAcrossPages, antroji lentelė prasideda tiksliai po to einančiame puslapyje po to, kuriame baigiasi pirmoji, abi turi bent du stulpelius ir bent dvi kanoninių stulpelių pradžios sutampa pagal ColumnTolerance. Gretimų puslapių sąlyga čia yra esminė. Iškvietėjai PageIndices perduoda kaip atvirą masyvą bet kokia norima tvarka, o be šio tikrinimo 3, 9 ir 14 puslapių prašymas galėtų suvirinti tris nesusijusias lenteles į vieną visiškai įtikinamai atrodančią išvestį. Kaina tokia, kad tikras tęsinys, praleidžiantis puslapį, įterptas priedas ar dvipusis skenavimas su tuščia antroji puse grįžta kaip dvi lentelės, ir jokia parinktis šios taisyklės nesušvelnina. Jų sujungimas iš naujo yra tik kviečiančios programos politikos sprendimas, todėl API pateikia FirstPageIndex, LastPageIndex, SourceTableCount ir kiekvienos eilutės PageIndex, o sprendimą palieka ten, kur jam vieta

Pasikartojančios antraštės pažymimos, bet niekada neištrinamos

ExtractLoadedTypedTables niekada nepašalina pasikartojančios antraštės eilutės iš rezultato. Kai jungiant per puslapius nustatoma, kad įeinanti lentelė prasideda antraštės tekstu, identišku sukauptos lentelės tekstui po tarpų apkirpimo ir raidžių registro suvienodinimo, tos eilutės pažymimos IsHeader ir IsRepeatedHeader, bet vis tiek pridedamos šaltinio tvarka. Trinimas yra informacijos praradimas ir negrįžtamas sprendimas, o skirtingiems vartotojams reikia skirtingų atsakymų: CSV importui pasikartojimų nereikia, audito pėdsakui jie reikalingi su puslapių numeriais, o skirtumų įrankiui reikia išlaikyti šaltinio tvarką baitas po baito. Todėl biblioteka praneša, o iškvietėjas nusprendžia

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;                    // palikti tik pirmą antraščių bloką
      for C := 0 to High(Row.Cells) do
        if Row.Cells[C].ValueKind = ttvkCurrency then
          Total := Total + Row.Cells[C].NumberValue;
    end;
end;

Tipinės reikšmės ir skyrikliai, kuriuos turite pateikti

Tipas nustatomas fiksuota tvarka, kuri dviprasmybes išsprendžia vienintele sveika kryptimi: pirmiausia boolean, tada data, procentas, valiuta, paprastas skaičius, o niekur netinkantis turinys lieka eilute. Tvarka neleidžia 2026 datos stulpelyje būti atpažintam skaičių parserio dar prieš jį pasiekiant datos parseriui. Valiuta atpažįstama iš pradžioje esančio $, £, ¥ arba , arba iš trijų raidžių ISO 4217 kodo, po kurio eina tarpas, o kodas išsaugomas CurrencyCode. Svarbiausia, HotPDF neatspėja jūsų lokalės. DecimalSeparator, ThousandsSeparator ir DateOrder ateina iš parinkčių, nes 1.234 gali būti vienas skaičius arba vienas tūkstantis du šimtai trisdešimt keturi, priklausomai nuo fakto, kurio PDF neturi. Neapdorotas Unicode Text išsaugomas kiekviename langelyje kartu su tipine reikšme, todėl neteisingą spėjimą visada galima atkurti be antro išgavimo etapo

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;

Du eksporto formatai atsako į skirtingus klausimus ir sąmoningai nėra lygiaverčiai. CSV sulietos aprėpties tęstinius stulpelius rašo kaip tuščius laukus, ko tikisi skaičiuoklė arba masinio įkėlimo įrankis. JSON išsaugo viską, ką žinojo išgavimas: tipinę reikšmę pagal jos rūšį, columnSpan, langelio ir eilutės pasitikėjimą, langelio ribas, puslapio ir šaltinio lentelės kilmę. Abu formatai visą dokumentą pirmiausia sukaupia ribotame atminties buferyje, tik tada publikuoja į paskirties srautą ir, jei rašymas nutrūksta, grąžina pradinius baitus, ilgį ir poziciją, todėl nesėkmingas eksportas niekada nepalieka pusiau įrašyto failo. Puslapių, glifų puslapyje, lentelių, eilučių, langelių, simbolių ir išvesties baitų biudžetai skaičiuojami atskirai, o eilutės suskaičiuojamos prieš paskirstymą, nes kiekvienos eilutės SetLength ilgai prieš milijono eilučių numatytąją ribą virsta kvadratiniu kopijavimu

Kur geometrinis lentelių atkūrimas pasiduoda

Aiškiai įvardyti gedimo režimus naudingiau nei funkcijų sąrašas, nes kiekvienas iš jų yra vieta, kur iškvietėjui reikia savo politikos, o ne geresnės parinkties reikšmės

  • Vertikalūs sujungimai neatkuriami. HotPDF horizontalioms aprėptims praneša ColumnSpan, o RowSpan palieka 1, todėl spausdintoje lentelėje per tris eilutes einantis langelis gaunamas kaip vienas langelis su dviem tarpais
  • Antraščių aptikimas paremtas duomenimis, o ne vaizdu. Antraštės blokas yra eilučių seka iki pirmos eilutės, kurioje pasirodo ne eilutės tipo reikšmė, todėl lentelė, kurios turinys vien tekstas, praneša HeaderRowCount kaip nulį, kad ir kaip būtų suformatuota
  • Lentelės, kurių rezultatas mažesnis už MinimumTableConfidence, išmetamos be klaidos. Lyginkite Info.TableCount su Info.SourceTableCount, kai reikia žinoti, kad kažkas buvo atmesta
  • Sekai reikia bent dviejų eilučių ir bent dviejų stulpelių, kad maketo etapas ją apskritai pavadintų lentele, todėl vienos eilutės pseudo-lentelė arba dviejų stulpelių ilgos prozos maketas teisingai, nors ir nepatogiai, nelaikomas lentele
  • Skenuotuose puslapiuose nėra teksto operatorių, todėl geometriškai nėra ko atkurti, kol puslapyje neatsiranda OCR teksto sluoksnis

Jei jūsų PDF išeina iš jūsų pačių ataskaitų konvejerio, pigiausias sprendimas viskam yra ankstesnėje grandies dalyje: išveskite pažymėtas lenteles arba išsaugokite šaltinio duomenis, o išgavimą laikykite atsarginiu keliu dokumentams, kurių negaminote jūs. Visa kita konvejerį verta mokytis tokia tvarka, nes kiekvienas sluoksnis statomas ant žemesnio: pradėkite nuo paprasto teksto išgavimo iš įkelto PDF, pereikite prie tipinių lentelių API, kai reikia išsaugoti geometriją, ir peržiūrėkite duomenų lentelės atvaizdavimą į naują PDF, kai esate generavimo pusėje ir galite nuspręsti, kiek atkuriama bus išvestis

ExtractLoadedTypedTables ir ExportLoadedTypedTables pateikiami vietiniame HotPDF Delphi PDF komponente, skirtame Delphi ir C++Builder, be išorinės DLL ir vykdymo priklausomybės; produkto puslapyje pateikta visa tipinių lentelių API parinkčių, būsenų ir įrašų nuoroda