HotPDF odzyskuje tabele z istniejącego PDF-a przez ExtractLoadedTypedTables, API Delphi, które scala fragmenty wierszy utworzone przez przebieg układu, buduje jedną kanoniczną siatkę kolumn dla każdej tabeli, kontynuuje tabelę przez podział strony, gdy pozwala na to geometria, i zwraca każdą komórkę jako wartość typowaną z pochodzeniem strony, rozpiętością kolumn i granicami. ExportLoadedTypedTables zapisuje ten sam wynik bezpośrednio do CSV albo JSON. Scenariusz uzasadniający budowę tej funkcji jest zwyczajny i bardzo częsty. Czterdziestostronicowy rejestr faktur, logicznie jedna tabela, został wydrukowany z nagłówkiem powtarzanym u góry każdej strony. Uruchom na nim naiwny przebieg kolejności czytania, a otrzymasz czterdzieści tabel, trzydzieści dziewięć fałszywych wierszy nagłówkowych i kolumnę walutową przesuniętą o jedną pozycję w lewo w każdym wierszu, w którym środkowa komórka była pusta. Porządkowanie tego downstream, w aplikacji wywołującej, jest miejscem, w którym umierają projekty importu dokumentów
Dlaczego strona PDF przekazuje fragmenty zamiast tabeli?
Bo strona PDF nie ma żadnej semantyki tabeli, chyba że dokument jest otagowany. Strumień treści zawiera operatory wyświetlania tekstu i macierze pozycjonowania (ISO 32000-1 §9.4.3) i nic więcej; obramowane pole widoczne na ekranie jest niezależnym malowaniem ścieżki, którego żaden ekstraktor nie musi korelować z tekstem. Typy elementów struktury Table, TR, TH i TD istnieją wyłącznie w hierarchii struktury logicznej otagowanego PDF-a (ISO 32000-1 §14.8.4), a przytłaczająca większość krążących dokumentów biznesowych nie ma tagów. Wszystko, co opisano poniżej, jest odzyskiwaniem geometrycznym, a nie parsowaniem, i warto powiedzieć to głośno, zanim ktoś zbuduje na tym raport uzgodnieniowy
Dlatego HotPDF najpierw wykonuje semantyczną analizę układu na wyodrębnionych glifach, w tym samym przebiegu, który zasila ekstrakcję tekstu w kolejności struktury z załadowanego PDF-a oraz eksporty ustrukturyzowanego HTML i XML. Ten przebieg grupuje linie bazowe w odcinki, których komórki są wyrównane w pionie, i kontynuuje odcinek tylko dopóty, dopóki kolejne wiersze mają tę samą liczbę komórek. Dla silnika układu ta zasada jest poprawna i tania. Dla wywołującego ma niewłaściwy kształt: pojedynczy wiersz z pustą komórką wewnętrzną dzieli jedną wizualną tabelę na dwie tabele źródłowe. Warstwa typowanych tabel znajduje się nad tym przebiegiem właśnie po to, by złożyć elementy z powrotem
Kanoniczne siatki kolumn i pokrętło ColumnTolerance
ExtractLoadedTypedTables najpierw scala fragmenty z tej samej strony, a dopiero potem robi cokolwiek innego, i scala je na podstawie geometrii kolumn, a nie tekstu wierszy. Dwie sąsiednie tabele źródłowe na jednej stronie zostają połączone, gdy obie mają co najmniej dwie kolumny, pionowa przerwa między ostatnim wierszem pierwszej i pierwszym wierszem drugiej pozostaje w paśmie tolerancji, a początki kolumn są wyrównane. Początki kolumn oddalone od siebie o nie więcej niż ColumnTolerance zapadają się do jednej kolumny kanonicznej i są uśredniane podczas scalania. Domyślna tolerancja wynosi 12 jednostek przestrzeni użytkownika, co pasuje do zwykłej typografii biznesowej, ale dla szerokiego trackingu albo głęboko wciętych układów warto ją podnieść
Najważniejsza jest sytuacja wiersza z brakującą wartością wewnętrzną. HotPDF przypina każdą komórkę do najbliższego kanonicznego początku kolumny, a następnie ustawia ColumnSpan jako odległość od tej kolumny do następnej zajętej, zamiast przesuwać pozostałe komórki w lewo. Wiersz z trzema komórkami w siatce pięciokolumnowej zachowuje wartości pod właściwymi nagłówkami i dokładnie zapisuje miejsca przerw. To różnica między tabelą, którą można uzgodnić, a tabelą, która po cichu przypisuje pieniądze do niewłaściwych kolumn
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; // jednostki przestrzeni użytkownika
Options.MinimumTableConfidence := 0.55; // poniżej tego tabele są odrzucane
Options.DateOrder := ttdoDMY; // 03/04/2026 to 3 kwietnia
Options.DecimalSeparator := ',';
Options.ThousandsSeparator := '.';
if Pdf.ExtractLoadedTypedTables([0, 1, 2, 3], Options, Tables, Info) then
// Info.TableCount kontra Info.SourceTableCount pokazuje, ile scalono
ProcessTables(Tables)
else if Info.Status = ttesBudgetExceeded then
Log(string(Info.Diagnostic));
finally
Pdf.Free;
end;
end;
Co naprawdę gwarantuje scalanie przez strony?
Gwarantuje konserwatywność, i to celowo. HotPDF łączy dwie tabele przez granicę strony tylko wtedy, gdy MergeAcrossPages jest włączone, druga tabela zaczyna się dokładnie na indeksie strony następującym po końcu pierwszej, obie mają co najmniej dwie kolumny, a co najmniej dwa kanoniczne początki kolumn są wyrównane w ramach ColumnTolerance. Warunek kolejnych stron jest kluczowy. Wywołujący przekazuje PageIndices jako tablicę otwartą w dowolnej kolejności, a bez tego sprawdzenia żądanie dla stron 3, 9 i 14 mogłoby zespawać trzy niezwiązane tabele w jeden wynik wyglądający całkowicie wiarygodnie. Koszt polega na tym, że prawdziwa kontynuacja z pominiętą stroną, przeplatanym dodatkiem albo skanem dwustronnym z pustym odwrociem wraca jako dwie tabele i żadna opcja tego nie rozluźnia. Ponowne połączenie jest decyzją polityczną, którą może podjąć tylko aplikacja wywołująca, więc API udostępnia FirstPageIndex, LastPageIndex, SourceTableCount oraz PageIndex dla każdego wiersza i pozostawia decyzję tam, gdzie należy
Powtarzane nagłówki są oznaczane, nigdy usuwane
ExtractLoadedTypedTables nigdy nie usuwa powtarzanego wiersza nagłówkowego z wyniku. Gdy scalanie przez strony znajdzie, że napływająca tabela zaczyna się tekstem nagłówka identycznym z nagłówkiem tabeli już zgromadzonej, po przycięciu i złożeniu wielkości liter, oznacza te wiersze jako IsHeader i IsRepeatedHeader, a mimo to dołącza je w kolejności źródłowej. Usunięcie jest decyzją stratną i nieodwracalną, a różni konsumenci chcą różnych odpowiedzi: import CSV chce usunięcia powtórzeń, ślad audytowy chce ich obecności z numerami stron, a narzędzie diff chce zachowania kolejności źródłowej bajt po bajcie. Biblioteka raportuje więc stan, a decyzję podejmuje wywołujący
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; // zachowaj tylko pierwszy blok nagłówka
for C := 0 to High(Row.Cells) do
if Row.Cells[C].ValueKind = ttvkCurrency then
Total := Total + Row.Cells[C].NumberValue;
end;
end;
Wartości typowane i separatory, które musisz podać
Wnioskowanie typu działa w stałej kolejności, która rozwiązuje niejednoznaczności w jedynym rozsądnym kierunku: najpierw wartość logiczna, potem data, procent, waluta, zwykła liczba, a wszystko niedopasowane pozostaje łańcuchem. To kolejność sprawia, że 2026 w kolumnie dat nie zostanie rozstrzygnięte przez parser liczb, zanim parser dat zobaczy wartość. Waluta jest rozpoznawana po początkowym $, £, ¥ albo € lub po trzyznakowym kodzie ISO 4217 następującym przed spacją, a kod jest zachowywany w CurrencyCode. Co ważne, HotPDF nie zgaduje twojej lokalizacji. DecimalSeparator, ThousandsSeparator i DateOrder pochodzą z opcji, ponieważ 1.234 może oznaczać jedną liczbę albo tysiąc dwieście trzydzieści cztery, zależnie od faktu, którego PDF nie zawiera. Surowy Unicode Text jest zachowywany w każdej komórce obok wartości typowanej, więc błędne zgadywanie zawsze można odzyskać bez drugiej ekstrakcji
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;
Dwa formaty eksportu odpowiadają na różne pytania i celowo nie są równoważne. CSV zapisuje kolumny kontynuacji scalonego zakresu jako puste pola, czego oczekuje arkusz albo loader wsadowy. JSON zachowuje wszystko, co wiedziała ekstrakcja: wartość typowaną pod własnym rodzajem, columnSpan, pewność dla każdej komórki i wiersza, granice komórek oraz pochodzenie strony i tabeli źródłowej. Oba formaty najpierw układają cały dokument w ograniczonym buforze pamięci, a dopiero potem publikują go w docelowym strumieniu, przywracając oryginalne bajty, długość i pozycję, jeśli zapis nie powiedzie się w połowie, więc nieudany eksport nigdy nie pozostawia częściowo zapisanego pliku. Budżety stron, glifów na stronę, tabel, wierszy, komórek, znaków i bajtów wyjściowych są rozliczane osobno, a wiersze są zliczane przed alokacją, ponieważ SetLength dla każdego wiersza degraduje się do kopiowania kwadratowego na długo przed domyślnym limitem miliona wierszy
Gdzie kończy się geometryczne odzyskiwanie tabel?
Jasne opisanie trybów niepowodzenia jest użyteczniejsze niż lista funkcji, bo każdy z tych przypadków jest miejscem, w którym wywołujący potrzebuje własnej polityki, a nie lepszej wartości opcji
- Scalanie pionowe nie jest odzyskiwane. HotPDF raportuje
ColumnSpandla zakresów poziomych i pozostawiaRowSpanrówne 1, więc komórka obejmująca trzy wiersze drukowanej tabeli przychodzi jako jedna komórka i dwie przerwy - Wykrywanie nagłówka opiera się na danych, a nie na wyglądzie. Blok nagłówka to ciąg wierszy przed pierwszym wierszem zawierającym nietypowaną jako łańcuch wartość, więc tabela, której ciało składa się wyłącznie z tekstu, raportuje
HeaderRowCountjako zero niezależnie od stylu - Tabele poniżej
MinimumTableConfidencesą usuwane z wyniku bez błędu. PorównajInfo.TableCountzInfo.SourceTableCount, gdy chcesz wiedzieć, że coś odrzucono - Odcinek musi mieć co najmniej dwa wiersze i co najmniej dwie kolumny, zanim przebieg układu w ogóle uzna go za tabelę, więc jednowierszowa pseudotabela albo dwukolumnowy układ długiej prozy poprawnie, choć niepraktycznie, nie jest tabelą
- Skanowane strony nie zawierają operatorów tekstowych, więc nie ma czego odzyskiwać geometrycznie, dopóki na stronie nie istnieje warstwa tekstowa OCR
Jeśli twoje PDF-y pochodzą z własnego stosu raportowego, najtańsza poprawka tego wszystkiego leży wcześniej: emituj otagowane tabele albo zachowuj dane źródłowe i traktuj ekstrakcję jako fallback dla dokumentów, których nie wytworzyłeś. W pozostałych przypadkach warto poznać pipeline w tej kolejności, ponieważ każda warstwa buduje na poprzedniej: zacznij od zwykłej ekstrakcji tekstu z załadowanego PDF-a, przejdź do API typowanych tabel, gdy geometria musi zostać zachowana, i spójrz na renderowanie tabeli danych do nowego PDF-a, gdy jesteś po stronie generowania i możesz zdecydować, jak łatwo odzyskać wynik
ExtractLoadedTypedTables i ExportLoadedTypedTables są częścią natywnego komponentu PDF HotPDF dla Delphi dla Delphi i C++Builder, bez zewnętrznego DLL i zależności runtime; strona produktu zawiera pełny opis opcji, statusów i rekordów API typowanych tabel