HotPDF recuperează tabele dintr-un PDF existent prin ExtractLoadedTypedTables, un API Delphi care unește fragmentele de rând produse de layout pass, construiește o grilă canonică de coloane per tabel, continuă tabelul peste un page break când geometria permite și returnează fiecare celulă ca valoare tipizată cu proveniența paginii, column span și bounds. ExportLoadedTypedTables scrie același rezultat direct în CSV sau JSON. Scenariul care justifică această funcționalitate este banal și extrem de comun. Un registru de facturi de patruzeci de pagini, un singur tabel logic, tipărit cu header-ul repetat în partea de sus a fiecărei pagini. Rulează peste el un pass naiv de reading order și primești patruzeci de tabele, treizeci și nouă de header rows false și o coloană de currency care alunecă cu o poziție la stânga pe fiecare rând în care celula din mijloc s-a întâmplat să fie goală. Curățarea asta downstream, în aplicația caller, este locul în care mor proiectele de document import
De ce îți dă pagina PDF fragmente în loc de un tabel?
Deoarece o pagină PDF nu poartă deloc semantics de tabel dacă documentul nu este tagged. Content stream-ul conține operatori text-showing și positioning matrices (ISO 32000-1 §9.4.3) și nimic mai mult; chenarul pe care îl vezi pe ecran este un path painting fără legătură pe care niciun extractor nu este obligat să îl coreleze cu textul. Tipurile de structure elements Table, TR, TH și TD există doar în ierarhia logical structure a unui PDF tagged (ISO 32000-1 §14.8.4), iar majoritatea covârșitoare a documentelor de business aflate în circulație nu sunt tagged. Tot ce urmează este recuperare geometrică, nu parsing, iar asta trebuie spus direct înainte ca cineva să construiască peste ea un reconciliation report
De aceea HotPDF rulează mai întâi o semantic layout analysis peste glifele extrase, același pass care susține extragerea textului în ordinea structurii dintr-un PDF încărcat și exporturile structurate HTML și XML. Pass-ul grupează baseline-uri în runs ale căror celule se aliniază vertical și va continua un run doar cât timp rândurile consecutive au același număr de celule. Pentru un layout engine regula este corectă și ieftină. Pentru un caller este forma greșită: un singur rând cu o celulă interioară goală desparte un tabel vizual în două source tables. Layer-ul typed table stă deasupra acelui pass tocmai pentru a pune piesele la loc
Grile canonice de coloane și reglajul ColumnTolerance
ExtractLoadedTypedTables unește fragmentele de pe aceeași pagină înainte să facă orice altceva și le unește după geometria coloanelor, nu după textul rândurilor. Două source tables adiacente pe o pagină se unesc când ambele au cel puțin două coloane, când spațiul vertical dintre ultimul rând al primei și primul rând al celei de-a doua rămâne în banda de toleranță și când pozițiile de start ale coloanelor se aliniază. Starturile de coloană aflate la cel mult ColumnTolerance unul de altul se reduc la o singură coloană canonică și sunt mediate pe măsură ce se unesc. Toleranța implicită este de 12 unități în user space, potrivită pentru tipografia obișnuită de business și care merită mărită pentru layout-uri cu tracking larg sau indentare adâncă
Ce se întâmplă cu un rând căruia îi lipsește o valoare interioară este partea importantă. HotPDF fixează fiecare celulă pe cel mai apropiat column start canonic și apoi setează ColumnSpan la distanța de la acea coloană la următoarea ocupată, în loc să deplaseze celulele rămase spre stânga. Un rând cu trei celule într-o grilă de cinci coloane își păstrează valorile sub heading-urile corecte și înregistrează exact unde sunt golurile. Aceasta este diferența dintre un tabel pe care îl poți reconcilia și unul care atribuie în tăcere banii greșit
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; // unități user-space
Options.MinimumTableConfidence := 0.55; // sub acest nivel, tabelele sunt eliminate
Options.DateOrder := ttdoDMY; // 03/04/2026 înseamnă 3 aprilie
Options.DecimalSeparator := ',';
Options.ThousandsSeparator := '.';
if Pdf.ExtractLoadedTypedTables([0, 1, 2, 3], Options, Tables, Info) then
// Info.TableCount față de Info.SourceTableCount arată cât s-a unit
ProcessTables(Tables)
else if Info.Status = ttesBudgetExceeded then
Log(string(Info.Diagnostic));
finally
Pdf.Free;
end;
end;
Ce garantează în realitate unirea între pagini?
Garantează conservatorismul, intenționat. HotPDF unește două tabele peste o limită de pagină doar când MergeAcrossPages este activat, când al doilea tabel începe exact pe indexul de pagină după care se termină primul, când ambele au cel puțin două coloane și când cel puțin două column starts canonice se aliniază în ColumnTolerance. Condiția de pagini consecutive este cea care poartă greutatea. Caller-ii transmit PageIndices ca open array în orice ordine doresc, iar fără acest check o cerere pentru paginile 3, 9 și 14 ar putea suda trei tabele fără legătură într-un rezultat complet plauzibil. Costul este că o continuare reală care sare o pagină, un appendix intercalat sau un duplex scan cu verso gol revine ca două tabele și nicio opțiune nu relaxează asta. Reunirea lor este o decizie de policy pe care doar aplicația caller o poate lua, așa că API-ul expune FirstPageIndex, LastPageIndex, SourceTableCount și un PageIndex per row și lasă decizia acolo unde îi este locul
Header-ele repetate sunt marcate, niciodată șterse
ExtractLoadedTypedTables nu elimină niciodată un repeated header row din rezultat. Când un cross-page merge găsește că tabelul incoming începe cu text de header identic cu cel al tabelului acumulat, comparat după trimming și case folding, marchează acele rows cu IsHeader și IsRepeatedHeader și le adaugă totuși în source order. Ștergerea este o alegere lossy și ireversibilă, iar consumatori diferiți vor răspunsuri diferite: un import CSV vrea repetările eliminate, un audit trail vrea să le păstreze cu numerele de pagină, un tool de diff vrea source order păstrat byte cu byte. Așa că biblioteca raportează, iar caller-ul decide
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; // păstrează doar primul bloc de header
for C := 0 to High(Row.Cells) do
if Row.Cells[C].ValueKind = ttvkCurrency then
Total := Total + Row.Cells[C].NumberValue;
end;
end;
Valori tipizate și separatorii pe care trebuie să îi furnizezi
Type inference rulează într-o ordine fixă ce rezolvă ambiguitățile în singura direcție sănătoasă: boolean mai întâi, apoi date, percentage, currency și plain number, iar orice nu se potrivește rămâne string. Ordinea împiedică 2026 dintr-o coloană de date să fie decis de un number parser înainte ca date parser-ul să îl vadă. Currency este recunoscut dintr-un $, £, ¥ sau € la început ori dintr-un cod ISO 4217 de trei litere urmat de un spațiu, iar codul este păstrat în CurrencyCode. Important, HotPDF nu îți ghicește locale-ul. DecimalSeparator, ThousandsSeparator și DateOrder vin din options, deoarece 1.234 este fie un număr, fie o mie două sute treizeci și patru, în funcție de un fapt pe care PDF-ul nu îl conține. Unicode Text raw este păstrat pe fiecare celulă alături de valoarea tipizată, așa că o presupunere greșită poate fi recuperată mereu fără un al doilea pass de extraction
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;
Cele două formate de export răspund la întrebări diferite și nu sunt echivalente intenționat. CSV scrie coloanele de continuare ale unui span unit ca field-uri goale, exact ce așteaptă un spreadsheet sau un bulk loader. JSON păstrează tot ce a știut extraction-ul: valoarea tipizată sub propriul kind, columnSpan, confidence per celulă și per row, bounds ale celulei și proveniența paginii și a source table-ului. Ambele formate pregătesc întregul document într-un buffer bounded în memorie și abia apoi publică în stream-ul destinație, restaurând bytes-ii, lungimea și poziția originale dacă write-ul eșuează la mijloc, astfel încât un export eșuat nu lasă niciodată un fișier scris pe jumătate. Bugetele pentru pagini, glife per pagină, tabele, rows, cells, caractere și output bytes sunt contabilizate separat, iar rows sunt numărate înainte de alocare, pentru că un SetLength per row ajunge la copiere quadratică cu mult înainte de plafonul implicit de un milion de rows
Unde renunță recuperarea geometrică a tabelelor
Este mai util să fii explicit cu failure modes decât să enumeri feature-uri, deoarece fiecare dintre acestea este un loc în care caller-ul are nevoie de propria policy, nu de o valoare mai bună pentru option
- Vertical merges nu sunt recuperate. HotPDF raportează
ColumnSpanpentru span-uri orizontale și lasăRowSpanla 1, așa că o celulă care acoperă trei rows în tabelul tipărit ajunge drept o celulă plus două goluri - Header detection este data-driven, nu vizual. Blocul de header este run-ul de rows dinaintea primului row care conține o valoare typed non-string, așa că un tabel al cărui body este complet text raportează
HeaderRowCountca zero indiferent de styling - Tabelele sub
MinimumTableConfidencesunt eliminate din rezultat fără eroare. ComparăInfo.TableCountcuInfo.SourceTableCountcând trebuie să știi că ceva a fost abandonat - Un run are nevoie de cel puțin două rows și cel puțin două coloane înainte ca layout pass-ul să îl numească tabel, așa că un pseudo-table cu un singur rând sau un layout cu două coloane de prose lungă nu este, corect și nefolositor, un tabel
- Paginile scanate nu conțin text operators, deci nu există nimic de recuperat geometric până când pe pagină nu există un OCR text layer
Dacă PDF-urile tale vin din propriul reporting stack, cea mai ieftină soluție pentru toate acestea este upstream: emite tabele tagged sau păstrează datele sursă și tratează extraction-ul ca fallback pentru documente pe care nu le-ai produs. Pentru restul, merită să înveți pipeline-ul în această ordine, deoarece fiecare layer se sprijină pe cel de sub el: începe cu plain text extraction dintr-un PDF încărcat, urcă la typed table API când geometria trebuie păstrată și uită-te la randarea unui tabel de date într-un PDF nou când ești pe partea de generare și poți decide cât de recuperabil va fi output-ul
ExtractLoadedTypedTables și ExportLoadedTypedTables sunt livrate ca parte din HotPDF Delphi PDF Component nativ pentru Delphi și C++Builder, fără DLL extern și fără runtime dependency; pagina produsului conține referința completă pentru options, status și records a API-ului typed table