Tabela wyeksportowana z Worda albo Dokumentów Google może nieść setki kawałków obramowania na jednej stronie i ani jednej linii obrysowanej. Zdekompresuj stronę przez qpdf --qdf, a strumień zawartości czyta się jak kolumnę par re f, z których każda opisuje pudełko o wysokości pół punktu. Detektor tabel szukający wyłącznie obrysowanych odcinków nie znajdzie niczego, wróci do układania słów po białej przestrzeni między nimi i odda rozsypankę fragmentów po dwa-trzy wiersze tam, gdzie autor narysował jedną czystą siatkę. Obramowania tam wszystkie są. Są malowane wypełnieniem, więc detektor musi czytać wypełnione prostokąty jako linie, a potem ściągać prawie dotykające się krawędzie na wspólne linie siatki
Czemu tabela z Worda nie ma obrysowanych linii
Procesor tekstu traktuje obramowanie komórki jako pudełko o szerokości i maluje to pudełko operatorem wypełnienia. ISO 32000-1 §8.5.2.1 definiuje re jako doklejenie zamkniętej podścieżki prostokąta, a §8.5.3 rozdziela operatory malowania: S obrysa ścieżkę bieżącą szerokością linii, f wypełnia jej wnętrze. Obramowanie 0.5 punktu wychodzi jako x y w 0.5 re f, a machineria obrysu (szerokość linii, łączenia, wzór kreski) nigdy nie rusza. Cieniowanie komórki to ta sama konstrukcja z większym pudełkiem. Obrysowana forma, której oczekuje detektor zorientowany na linie — m, l, S — to ta, której eksporty biurowe rzadko używają:
% obramowanie jednej komórki z eksportu procesora tekstu: wypełnione pudełko 0.5 pt wysokości
72 700 468 0.5 re f
% cieniowanie komórki: wypełnione pudełko wielkości komórki
72 676 117 24 re f
% obrysowana linia siatki, której szuka detektor tylko-obrysowy
72 700 m 540 700 l S
Pisząc własny detektor na surowym API PDFium, pierwszy szkic zwykle woła FPDFPath_GetDrawMode, trzyma ścieżkę, gdy flaga obrysu jest ustawiona, i idzie dalej. Oba wypełnione pudełka powyżej oblatują ten test. Słowa wewnątrz komórek docierają do wykrywania białej przestrzeni, a na kompaktowym eksporcie kolumny siedzą może 6 punktów od siebie. W PDFium Component domyślny MinColumnGap to 12 punktów, więc te kolumny się nie rejestrują. Obniżanie MinColumnGap aż się zarejestrują sprawia, że rejestrują się też rozciągnięte odstępy między słowami akapitów justowanych, a siatka, którą narysował autor, i tak nie wraca, bo informacja ją definiująca mieszka w wypełnionych pudełkach
Jak PDFium Component czyta wypełnione pudełko jako linię
PDFium Component czyta geometrię tabel z każdego obiektu ścieżki na stronie, po jednej podścieżce, i ponieważ TPdfTableExtractionOptions.DetectFilledRulings ma domyślnie True, ścieżka, której tryb wypełnienia jest czymkolwiek innym niż none, jest badana pod kątem linii obok ścieżek obrysowanych. Filtr, który przychodzi potem, jest celowo surowy. Wypełniona podścieżka kwalifikuje się tylko wtedy, gdy ma od czterech do ośmiu punktów, nie zawiera segmentu krzywej i każdy punkt siedzi na narożniku jej pudełka otaczającego z dokładnością do 0.05 punktu. Prostokąt narysowany przez re przechodzi, i taki narysowany jako pięć jawnych punktów z powtórzonym narożnikiem też. Trójkąt, szewron, kontur w kształcie L albo zaokrąglona zakładka oblatują test i nie wnoszą niczego
Przetrwały prostokąt jest klasyfikowany swoim pudełkiem otaczającym względem MaxRulingThickness, domyślnie 3 punkty. Szerokość na progu lub poniżej z wysokością powyżej daje jedną pionową linię na poziomym środku, biegnącą po całej wysokości pudełka; przypadek lustrzany daje jedną poziomą linię. Kiedy oba wymiary przekraczają próg, pudełko jest traktowane jako cieniowana komórka i wnosi cztery linie, po jednej na krawędź. Kiedy oba wymiary są na progu lub poniżej, pudełko nie wnosi niczego, więc kwadratowy punktor 2 punktów w liście nigdy nie staje się kikutem siatki tabeli. Kolor wypełnienia nie gra w tym żadnej roli. Białe pudełko na białej stronie produkuje te same linie co czarne — dlatego panel tła wielkości tabeli może wyskoczyć jako zewnętrzna rama
Obrysowane ścieżki trzymają własną drogę. Każdy prosty segment, którego końce różnią się o nie więcej niż RulingTolerance (domyślnie 1 punkt) na jednej osi, staje się linią wzdłuż drugiej, segmenty krzywych są przeskakiwane, i segment zamykający zamkniętej podścieżki też się liczy. Ścieżka malowana i wypełnieniem, i obrysem wnosi przez obie drogi, a nachodzące kawałki są zwijane później przez krok scalania. Obrysowany zaokrąglony prostokąt daje wciąż linie dla swoich prostych boków; tylko łuki na narożnikach wypadają. Samo wywołanie ekstrakcji nie potrzebuje żadnego specjalnego ustawienia:
uses
System.SysUtils, PDFium;
var
Pdf: TPdf;
Options: TPdfTableExtractionOptions;
Tables: TPdfTables;
Mode: string;
I, J, WithText: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'itinerary-from-word.pdf';
Pdf.LoadDocument;
Pdf.PageNumber := 1; // liczony od 1
Options := TPdfTableExtractionOptions.Default;
// pola wypełnionych linii, wypisane na wartościach domyślnych
Options.DetectFilledRulings := True;
Options.MaxRulingThickness := 3.0; // punkty; musi być większe od 0
Options.RulingSnapTolerance := 4.0; // punkty; 0 wyłącza przyciąganie
Options.IncludeFormXObjects := True;
Tables := Pdf.ExtractTables(Options);
for I := 0 to High(Tables) do
begin
if Tables[I].DetectionMode = ptdmRuled then
Mode := 'ruled'
else
Mode := 'whitespace';
WithText := 0;
for J := 0 to High(Tables[I].Cells) do
if Tables[I].Cells[J].Text <> '' then
Inc(WithText);
Writeln(Format('%dx%d %s, confidence %.2f, %d of %d cells hold text',
[Tables[I].RowCount, Tables[I].ColumnCount, Mode,
Tables[I].Confidence, WithText, Length(Tables[I].Cells)]));
end;
finally
Pdf.Free;
end;
end;
Ustawienie MaxRulingThickness na 0 nie wyłącza funkcji. ExtractTables waliduje opcje geometrii z góry i podnosi EPdfError z "Table extraction geometry options are invalid" dla progu zero albo ujemnego. Zachowanie tylko-obrysowe dostajesz, ustawiając DetectFilledRulings na False. Zostaw włączone dla czegokolwiek z pakietu biurowego albo z okna drukowania przeglądarki; surowy filtr prostokątów jest tym, co trzyma koszt niskim, bo dekoracyjne wypełnienia prawie nigdy go nie przechodzą
Co RulingSnapTolerance robi dla tabel z cieniowanych komórek?
RulingSnapTolerance jest tym, co pozwala tabeli zbudowanej z samego cieniowania złączyć się w jedną siatkę. Niektóre eksporty nie rysują żadnego obramowania: każda komórka to wypełnione pudełko oddzielone od sąsiadów białą przeprawką 1 do 3 punktów. Każde pudełko daje swoje cztery linie krawędziowe, ale prawa krawędź jednej komórki i lewa krawędź następnej siedzą, powiedzmy, 2 punkty od siebie, a test rozstrzygający, czy dwie linie należą do tej samej siatki, pozwala tylko RulingTolerance, 1 punkt. Bez przyciągania każda komórka jest wyspą czterech linii opisujących siatkę 1x1, nic nie dociera do MinColumns ani MinRows (obu domyślnie 2), a przebieg rządzony nie raportuje tabeli na stronie, która jak najbardziej jaką ma
Krok przyciągania zbiera każdą współrzędną X w grze — pozycję każdej pionowej linii plus początek i koniec każdej poziomej — i każdą współrzędną Y tak samo. Każda lista jest sortowana i cięta w klastry wszędzie tam, gdzie dwaj sąsiedzi różnią się o więcej niż tolerancja; każdy klaster jest zastępowany swoją średnią, a potem każda pozycja, początek i koniec przechodzi do najbliższego środka. Krawędzie 230.0 i 232.0 stają się obie 231.0, dwie komórki dzielą linię, a poziome linie rozciągają się z nimi, więc rogi się schodzą
Kolinearne kawałki, które się dotykają albo nakładają w ramach RulingTolerance, są scalane w pojedyncze linie. Dopiero wtedy rusza detektor siatki, porównując linie parami; 30-wierszowa tabela, która przyjechała jako 720 kawałków obramowania, dociera do tego porównania jako kilkadziesiąt linii
Na obrysowanej siatce krok nie zmienia niczego, bo współrzędne już pasujące przyciągają się do samych siebie. Haczyk: łańcuchowanie nie ma własnego limitu szerokości. Przebieg wartości co 3 punkty — 100, 103, 106 i 109 — zapada się w jeden środek na 104.5, mimo że skrajne wartości dzielą 9 punktów. Przy domyślnych 4 punktach ugryzie to tylko na kolumnach węższych niż znak, więc wyjście biurowe może zostać przy domyślnych 4. Obniżaj w stronę 1 albo 2, gdy formularz ma celowo wąskie kolumny (pola zaznaczania, pola numeru konta po cyfrze na komórkę), a ustaw 0, żeby wyłączyć przyciąganie. Puszczenie strategii rządzonej samotnie pod trzema ustawieniami pokazuje, od jakiego mechanizmu strona zależy:
// Tylko strategia rządzona, żeby wykrywanie białej przestrzeni nie mogło zamaskować wyniku
function CountRuledTables(Pdf: TPdf; FilledRulings: Boolean;
SnapTolerance: Double): Integer;
var
Options: TPdfTableExtractionOptions;
begin
Options := TPdfTableExtractionOptions.Default;
Options.DetectWhitespaceTables := False;
Options.DetectFilledRulings := FilledRulings;
Options.RulingSnapTolerance := SnapTolerance;
Result := Length(Pdf.ExtractTables(Options));
end;
// Oczekiwane dla tabeli narysowanej wyłącznie jako cieniowane komórki z przeprawkami 2 pt:
// tylko obrys -> 0 (w ogóle żadnej geometrii obrysowanej)
// wypełnione pudełka, snap 4 -> 1 (krawędzie przeprawki dzielą jedną linię)
// wypełnione pudełka, snap 0 -> 0 (każda komórka zostaje wyspą 1x1)
Writeln('stroke only: ', CountRuledTables(Pdf, False, 4.0));
Writeln('filled boxes, snap 4: ', CountRuledTables(Pdf, True, 4.0));
Writeln('filled boxes, snap 0: ', CountRuledTables(Pdf, True, 0.0));
Kiedy trzeci licznik spadnie poniżej drugiego, strona polega na przyciąganiu, i to jest strona do sprawdzenia okiem przed obniżeniem tolerancji
Linie wewnątrz form XObjects
Narzędzia składu strony często owijają tabelę, albo całe ciało strony, w form XObject i malują go przez Do. ISO 32000-1 §8.10.1 mówi, że macierz formularza jest konkatenowana z bieżącą macierzą transformacji w chwili malowania formularza, więc prostokąt wewnątrz formularza jest zapisany w przestrzeni formularza i dociera do przestrzeni strony dopiero po dwóch albo więcej transformacjach. Z ustawionym IncludeFormXObjects — a to domyślne — PDFium Component wchodzi w każdy obiekt formularza, łączy macierz obiektu z macierzą jego rodzica i podaje macierz połączoną w dół każdemu dziecku, więc obramowanie narysowane jako 72 700 468 0.5 re f wewnątrz przesuniętego formularza ląduje tam, gdzie pokazuje je podgląd. Przejście staje na ośmiu poziomach zagnieżdżonych formularzy
Kolejność tego łączenia ma znaczenie. Punkt wewnątrz formularza idzie najpierw przez macierz obiektu, a macierz rodzica druga; zamień operandy, a człon przesunięcia zostanie zastosowany w złej przestrzeni, więc linia należąca na górę strony wyląduje przy początku układu albo poza stroną. Ta sama pułapka wraca za każdym razem, gdy budujesz macierze samodzielnie, a artykuł o prepend versus append macierzy przechodzi przez nią z osią obrotu
Czemu domyślny budżet linii to 16384?
Domyślne MaxRulingSegments równe 16384 odzwierciedla, ile kawałków produkuje eksport obramowania per komórka. Obrysowana tabela 30 wierszy na 6 kolumn potrzebuje 38 segmentów linii: 31 poziomych i 7 pionowych. Ta sama tabela wyeksportowana jako wypełnione pudełka może nieść cztery kawałki obramowania na komórkę — 720 przed scalaniem — a cieniowanie każdej komórki dokłada kolejne cztery linie krawędziowe na komórkę, 1440 razem. Dwie takie tabele na jednej stronie już potrzebują blisko 3000
Przekroczenie budżetu jest błędem i jako błąd się objawia. ExtractTables podnosi EPdfError z komunikatem "Table ruling-segment budget exceeded", nie zwraca żadnej częściowej siatki, a przebieg białej przestrzeni dla tej strony też nie rusza. Ciasny budżet jest rozsądny dla wejścia niezaufanego, gdzie sfabrykowana strona mogłaby trzymać miliony malutkich pudełek, ale wtedy wywołujący musi odróżnić „za złożone na ten budżet” od „brak tabel na tej stronie”:
function TryExtractTables(Pdf: TPdf; Budget: Integer;
out Tables: TPdfTables): Boolean;
var
Options: TPdfTableExtractionOptions;
begin
Options := TPdfTableExtractionOptions.Default;
Options.MaxRulingSegments := Budget; // domyślnie 16384
try
Tables := Pdf.ExtractTables(Options);
Result := True;
except
on E: EPdfError do
begin
// LogWarning to twoja własna rutyna logująca, nie część biblioteki
LogWarning(Format('Page %d: %s', [Pdf.PageNumber, E.Message]));
Tables := nil;
Result := False; // ponad budżet albo złe opcje
end;
end;
end;
// Wynik False znaczy „nierozstrzygnięte”, co różni się od pustej tablicy
if not TryExtractTables(Pdf, 4096, Tables) then
QueueForReview(Pdf.FileName, Pdf.PageNumber); // również twoja własna rutyna
Ten sam typ wyjątku pokrywa też budżet słów i złe opcje, więc to zalogowany komunikat rozstrzyga je w kolejce do przeglądu
Gdzie droga rządzona przekazuje pałeczkę
Wykrywanie rządzone działa pierwsze, wykrywanie białej przestrzeni drugie, i dwie reguły trzymają je z dala od deptania po sobie. Słowa, których środki wpadają wewnątrz siatki już zaraportowanej przez przebieg rządzony, są usuwane, zanim wykrywanie białej przestrzeni ruszy, więc tabela nigdy nie jest raportowana dwa razy. Granica kolumny białej przestrzeni musi też być korytarzem pionowym wolnym od tekstu przez każdy wiersz, który rozdziela, co trzyma akapity justowane z dala od miana tabel; artykuł o fałszywych pozytywach tabel białej przestrzeni pokrywa tę regułę. Czytanie wypełnionych prostokątów przenosi tabele z eksportów biurowych ze ścieżki białej przestrzeni na rządzoną
Ponieważ linie przychodzą z obiektów ścieżek, a tekst ze strony tekstowej, strona bez warstwy tekstowej wciąż produkuje szkielet siatki z każdą komórką pustą. Pewność rządzonej siatki to 0.88 plus 0.12 razy ułamek komórek trzymających tekst, więc pusty szkielet raportuje dokładnie 0.88, a licznik komórek w pierwszym zestawieniu pokazuje 0 komórek z tekstem. Ta kombinacja to sygnał, żeby najpierw puścić OCR, jak opisano w dodawaniu przeszukiwalnej warstwy tekstu przez OCR. Wypełnione kształty z krzywymi albo nieprostokątnymi konturami nie wnoszą niczego, więc tabela, której komórki to wypełnione zaokrąglone prostokąty, spada na wykrywanie białą przestrzenią. Tabela bez obramowań i bez cieniowania należy do strategii białej przestrzeni opisanej w artykule o wykrywaniu i ekstrakcji tabel, a kiedy nawet to nie wystarcza, pudełka słów i bloki z tekstu strukturalnego i kolejności czytania to surowiec dla czytnika napisanego pod jedną rodzinę dokumentów
Demo TableExtractionLab przychodzące z komponentem ma pole wyboru dla DetectFilledRulings, więc możesz załadować eksport i zobaczyć siatkę pojawiającą się i znikającą bez pisania kodu. Opcje, typy rekordów i reszta API ekstrakcji są wypisane na stronie produktu PDFium Component for Delphi