Tehnički članak

Content-aware nastavak tabela preko PDF stranica u Delphiju

PDFium Component verzija 3.117.0 povezuje tabelu koja se prelama preko granice stranice kada ili oba fragmenta dodiruju ivice stranice ili ispod prvog fragmenta i iznad drugog ne stoji nikakav tekst tela, pri čemu se tekuća zaglavlja i podnožja ignorišu. ExtractDocumentTables primenjuje taj test zasnovan na sadržaju kao alternativu starijem testu ivica stranice, odbija fragment sa sledeće stranice čiji je prvi red jedna naslovna ćelija preko cele širine, i čuva jedan red koji se prelije na sledeću stranicu kao deo svog lanca nastavka

Članak o detekciji i izvlačenju tabela predstavio je nastavak kao četiri stroge kapije i tretirao „dodiruje ivicu stranice" kao jednu od njih. Taj opis bio je tačan za izdanje koje je pokrivao, i bio je takođe pogrešan za većinu tabela koje ljudi zaista daju komponenti. Ovaj članak je ispravka: koje dokumente test ivica ne može da obradi, šta ga je zamenilo, i dva sporedna slučaja koja je ispravka dovukla sa sobom

Zašto test ivica stranice pada na izvozima iz Word-a?

Test ivica stranice pada jer word processor prestaje da slaže redove na donjoj margini, ne na ivici papira. Sa podrazumevanim ContinuationMargin od 36 poena, originalno pravilo zahtevalo je da donja ivica ranijeg fragmenta leži unutar 36 poena od dna stranice, a gornja ivica kasnijeg fragmenta unutar 36 poena od vrha stranice. Dokument izvezen iz Word-a sa svojim podrazumevanim marginama od jednog inča stavlja poslednji red najmanje 72 poena iznad dna stranice, više ako postoji podnožje, pa taj uslov nikad nije važio. Svaka dugačka tabela u takvom dokumentu vraćena je kao nezavisni fragmenti sa ContinuationGroup na nuli, a pozivalac se vratio ručnom spajanju. Test i dalje ima smisla za ono za šta je napravljen: izveštaji koje generišu layout engine-i koji pune stranicu do fiksnog okvira sadržaja i sledeću stranicu počinju tačno na vrhu. To nije loše pravilo, ono je nepotpuno, i zato je verzija 3.117.0 zadržala i dodala drugu putanju umesto da ga zameni

Šta test zasnovan na sadržaju proverava umesto toga?

Test zasnovan na sadržaju proverava da li bilo šta osim tabele zauzima prostor između dva fragmenta, koristeći box-ove reči sa svake stranice a ne geometriju stranice. Dok ExtractDocumentTables prolazi kroz dokument, beleži po stranici najnižu donju ivicu bilo koje reči čiji vrh leži iznad pojasa podnožja i najvišu gornju ivicu bilo koje reči čije dno leži ispod pojasa zaglavlja. Oba pojasa su duboka ContinuationMargin poena, pa ista opcija sada radi dvostruki posao: popust prema ivici stranice i visina zona tekućeg zaglavlja i podnožja. Par fragmenata prolazi kada je donja ivica ranijeg na ili ispod najnižeg teksta tela na njegovoj stranici i gornja ivica kasnijeg na ili iznad najvišeg teksta tela na sledećoj stranici, svaka u granicama AlignmentTolerance. Jednostavnije rečeno: tabela je bila poslednja stvar na stranici N i prva stvar na stranici N+1, a broj stranice ili naslov dokumenta u pojasu margine se ne računaju. To izuzimanje nije proizvoljno. ISO 32000-1 §14.8.2.2 klasifikuje tekuća zaglavlja i podnožja kao paginacijske artefakte, sadržaj koji postoji zbog preloma stranice a ne uprkos njemu, i ista ideja koja pušta tagovanog čitača da ih preskoči pušta i tabelu da se nastavi preko njih. Članak o marked content-u pokriva kako tagovani fajlovi eksplicitno deklarišu te artefakte; ovde se klasifikacija izvodi iz pozicije, jer većina izvezenih tabela uopšte ne nosi tagove

Zašto nastavak tabela u PDFium Component-u treba dva testa: sa Word marginama od jednog inča test ivica stranice zahteva ivice fragmenata unutar prozora od 36 pt do kojih raspored nikad ne stiže, dok test sadržaja poredi box-ove reči i povezuje kada je tabela poslednji sadržaj tela na stranici N i prvi na stranici N+1, ignorišući pojaseve tekućeg zaglavlja i podnožja
Bilo koji od dva testa otvara kapiju, i tek tada se izvršavaju ostale provere: susedne stranice, bez reda naslova preko cele širine na kasnijem fragmentu, i granice kolona koje se poklapaju unutar dvostrukog AlignmentTolerance

Dva testa se kombinuju sa OR. Izveštaj layout engine-a čije tabele idu do ivice papira prolazi prvi; Word izvoz čije tabele staju na margini prolazi drugi; dokument koji radi i jedno i drugo prolazi dvaput. Tek pošto jedan od njih uspe, izvršavaju se ostale kapije, i to u fiksnom redu: brojevi stranica moraju biti susedni, kasniji fragment ne sme da počinje redom naslova, i granice kolona moraju da se poklapaju unutar dvostrukog AlignmentTolerance, što je 6 poena pri podrazumevanim vrednostima. Enumeracija je TPdfTableContinuation sa vrednostima ptcNone, ptcStart, ptcMiddle i ptcEnd. Fragment koji je označen kao ptcEnd a zatim se povezuje dalje na još jednu stranicu promoviše se u ptcMiddle, pa tabela na tri stranice čita start, middle, end po redu stranica. Brojevi grupa počinju od 1 a 0 znači nepovezano, a ToJson emituje istu informaciju kao članove continuation i continuationGroup, što je oblik koji vredi preferirati ako spajanje radi usluga nizvodno

uses
  PDFium;

var
  Pdf: TPdf;
  Options: TPdfTableExtractionOptions;
  Tables: TPdfTables;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'itinerary-from-word.pdf';
    Pdf.LoadDocument;

    Options := TPdfTableExtractionOptions.Default;
    Options.DetectContinuations := True;     // podrazumevano; prikazano radi jasnoće
    Options.ContinuationMargin := 54;        // footer od dva reda, dubok ~50 pt

    Tables := Pdf.ExtractDocumentTables(Options);
    for I := 0 to High(Tables) do
      case Tables[I].Continuation of
        ptcStart:
          Writeln(Format('group %d starts on page %d (%d rows)',
            [Tables[I].ContinuationGroup, Tables[I].PageNumber,
             Tables[I].RowCount]));
        ptcMiddle, ptcEnd:
          Writeln(Format('group %d continues on page %d (%d rows)',
            [Tables[I].ContinuationGroup, Tables[I].PageNumber,
             Tables[I].RowCount]));
      else
        Writeln(Format('standalone table on page %d (%d rows)',
          [Tables[I].PageNumber, Tables[I].RowCount]));
      end;
  finally
    Pdf.Free;
  end;
end;

Kako red naslova sprečava da se dve tabele spoje?

Fragment sa sledeće stranice čiji je prvi red jedna ćelija preko svih kolona tretira se kao nova tabela, nikad kao ostatak prethodne. To pravilo postoji jer test zasnovan na sadržaju sam po sebi povezuje previše rado. Slučaj koji ga je razotkrio bio je formular u stilu transkripta: tabela se završava blizu dna stranice 1, druga tabela sa identičnim širinama kolona počinje blizu vrha stranice 2, između njih ne stoji ništa osim podnožja, i kolone se poklapaju do poena. Pod testom ivica njih dvoje se nikad nisu sreli jer nijedna nije dodirivala ivicu; pod testom sadržaja povezali su se odmah, i formular sa sekcijama postao je jedna nekoherentna mreža. Ono što ih razdvaja vidljivo je u strukturi ćelija. Druga tabela počinje naslovom sekcije kao što je „RECIPIENT INFORMATION" postavljenim kao jedna spojena ćelija preko cele širine, a pravi nastavak to nikad ne radi, jer naslov pripada tabeli koja je već počela na prethodnoj stranici. TableStartsWithCaptionRow kodira tačno to: fragment ima najmanje dve kolone i sadrži ćeliju sa RowIndex = 0, ColumnIndex = 0 i ColumnSpan = ColumnCount. Provera se izvršava samo na kasnijem fragmentu, pa tabela čiji sopstveni red naslova stoji na njenoj prvoj stranici nije pogođena; naslov je na stranici N, a pregleda se samo fragment sa stranice N+1

Kapija reda naslova u PDFium Component-u: pravi nastavak počinje ćelijama sa podacima i pridružuje se istoj ContinuationGroup, dok se kasniji fragment čiji red nula drži jednu spojenu ćeliju sa RowIndex 0, ColumnIndex 0 i ColumnSpan jednakim ColumnCount odbija kao nastavak i prijavljuje kao nova tabela
Pregled dira samo kasniji fragment, pa tabela čiji sopstveni red naslova stoji na njenoj prvoj stranici nije pogođena, a kapija se izvršava pošto je jedan od dva testa ivica već povezao par

Poređenje kolona koje sledi, TablesHaveMatchingColumns, strože je od „isti broj kolona". Ponovo gradi pozicije granica svakog fragmenta iz pravougaonika ćelija, interpolira granice koje spojene ćelije kriju, i odbija par kada bilo koja granica odluta više od tolerancije. Dve tabele sa četiri kolone različitih proporcija zato ostaju razdvojene čak i kada se sve ostalo poklapa

Šta se dešava sa jednim redom koji se prelije na sledeću stranicu?

Mreža sa linijama koja nosi jedan red na sledeću stranicu sada se detektuje i povezuje, pod uslovom da završi u lancu nastavka; sama za sebe biva odbačena. Podrazumevani MinRows od 2 postoji da spreči da se zalutali par linija prijavi kao tabela, ali poslednji red gurnut preko preloma je pravi red koji je tvrdi pod od 2 tiho odbacio, a ostatak tabele je izgledao potpun kada nije bio. Sken na nivou dokumenta obrađuje to u tri koraka. Kada su i DetectContinuations i DetectRuledTables postavljeni, prolaz po stranici pokreće detektor mreža sa privremeno spuštenim podom redova na 1, i zato ExtractTables sada prihvata MinRows od 1 za mreže sa linijama dok detekcija praznina zadržava interni pod od 2. Nastavci se označavaju nad celim rezultatom. Zatim se uklanja svaka tabela koja je kraća od pozivalačevog MinRows i nije deo nijednog lanca. Fragment sa jednim redom preživljava samo zato što je bio povezan, a mreža sa jednim redom nasred inače obične stranice filtrira se tačno kao pre

Kako PDFium Component čuva red mreže koji se prelije preko preloma stranice: prolaz po stranici za mreže radi sa podom redova na jedan kada su DetectContinuations i DetectRuledTables postavljeni, nastavci se označavaju nad celim rezultatom, i uklanjaju se samo fragmenti kraći od MinRows koji stoje izvan svakog lanca
Red koji se prelio preživljava jer ga njegov lanac povezuje, dok se samostalna mreža sa jednim redom na običnoj stranici filtrira tačno kao pre, a tabele detektovane prazninama zadržavaju svoj pod od dva reda bez takvog olakšanja
// Ponovo izgradi svaki lanac kao jedan CSV, odbacujući ponovljene redove zaglavlja
// na fragmentima nastavka
procedure ExportChains(const Tables: TPdfTables; const Folder: string);
var
  I, R: Integer;
  Lines: TStringList;
  Csv: TStringList;
begin
  Csv := TStringList.Create;
  Lines := TStringList.Create;
  try
    for I := 0 to High(Tables) do
    begin
      if Tables[I].Continuation in [ptcNone, ptcStart] then
        Csv.Clear;
      Lines.Text := string(Tables[I].ToCsv);
      if (Tables[I].Continuation in [ptcMiddle, ptcEnd]) and
         (Lines.Count > 1) and (Tables[I].RowCount > 1) then
        Lines.Delete(0);            // zaglavlje koje je word processor ponovio
      for R := 0 to Lines.Count - 1 do
        Csv.Add(Lines[R]);
      if Tables[I].Continuation in [ptcNone, ptcEnd] then
        Csv.SaveToFile(Format('%s\page%d-group%d.csv',
          [Folder, Tables[I].PageNumber, Tables[I].ContinuationGroup]));
    end;
  finally
    Lines.Free;
    Csv.Free;
  end;
end;

Dva detalja u toj rutini su namerna. Preliv od jednog reda nikad se ne skida, jer ga čuvar na RowCount zadržava, a word processor koji ponavlja red zaglavlja na svakoj stranici proizvodi fragment čija je prva linija ponovo zaglavlje, pa je odbacivanje nulte linije na srednjim i završnim fragmentima tačno za taj slučaj a pogrešno za generator koji ne ponavlja zaglavlja. Proverite jedan dokument pre nego što rutinu pustite na celu fasciklu

Gde pravila i dalje staju

Test zasnovan na sadržaju vredi onoliko koliko vredi tekstualni sloj koji čita. Na skeniranoj stranici bez ikakvog teksta, zabeleženi ekstremi teksta tela padaju na granice stranice, uslov „ništa između" zadovoljen je prazno, i ostaju samo kapije reda naslova i kolona; mreža sa linijama na takvoj stranici i dalje se nalazi kao prazan skelet, pa se lanac može ispravno povezati, ali ništa o okolnom tekstu nije zaista verifikovano. Dodajte prvo tekstualni sloj ako to ima značaja. Podnožja renderovana kao slike a ne kao tekst nevidljiva su za logiku pojasa i bezopasna iz istog razloga

Pojasevi su jedan broj. Podnožje dublje od ContinuationMargin ostavlja svoje donje linije unutar zone tela, što čini da raniji fragment izgleda kao da ga prati tekst i blokira vezu; podignite opciju na stvarnu dubinu pojasa, kao što prvi primer i radi. Podignite je previše i kratak završni pasus blizu dna stranice uklizava u pojas i biva ignorisan, što povezuje tabelu sa bilo čim što sledi. Pravilo naslova ima otkaz u zrcalu: generator koji piše spojeni baner „continued" kao prvi red svakog fragmenta nastavka imaće te fragmente odbijene kao nove tabele, a jedini lek danas je da sami spojite po ContinuationGroup nakon što ništa ne olabavite, jer pravilo nema prekidač

Tabele detektovane prazninama ne dobijaju nikakvo olakšanje za jedan red. Strategija praznina treba dva poravnata reda da bi uopšte videla tabelu, pa se tabela bez linija koja prelije jedan red i dalje prijavljuje kratkom za taj red. Kada udarite u to, box-ovi reči iza strukturiranih tekstualnih blokova i redosleda čitanja daju vam sirove pozicije da ga povratite. Na skupu uzoraka koji je pokrenuo ovaj rad, trinaest izvoza iz word processora i browser-a, pet dokumenata sa pravim tabelama na više stranica svi su se povezali u pojedinačne lance a formular transkripta koji se ranije spajao ostao je razdvojen, što je mera prema kojoj je izdanje ocenjeno, a ne obećanje o svakom rasporedu

Označavanje nastavka, pravilo naslova i prolaz za jedan red svi žive u putanji na nivou dokumenta koju dele Delphi, C++Builder i Lazarus buildovi; pun API za izvlačenje tabela opisan je na stranici PDFium Component za Delphi