Tehnički članak

Content-aware nastavak tablica preko PDF stranica u Delphiju

PDFium Component verzija 3.117.0 povezuje tablicu koja se lomi preko granice stranice kada ili oba fragmenta dodiruju rubove stranice, ili ispod prvog fragmenta i iznad drugog ne stoji nikakav tekst tijela, pri čemu se tekuća zaglavlja i podnožja ignoriraju. ExtractDocumentTables primjenjuje taj content-aware test kao alternativu starijem testu po marginama stranice, odbija fragment sa sljedeće stranice čiji je prvi redak jedna ćelija natpisa pune širine, i zadržava pojedinačni redak koji se prelije na sljedeću stranicu kao dio svog lanca nastavka

Članak o detekciji i izvlačenju tablica predstavio je nastavak kao četiri stroge kapije i tretirao "dodiruje rub stranice" kao jednu od njih. Taj opis bio je točan za izdanje koje je pokrivao, a bio je i pogrešan za većinu tablica koje ljudi doista daju komponenti. Ovaj članak je ispravak: koje dokumente test po marginama ne može obraditi, što ga je zamijenilo, i dva rubna slučaja koja je popravak doveo sa sobom

Zašto test po marginama stranice pada na Word izvazima?

Test po marginama stranice pada zato što procesor teksta prestaje slagati retke na donjoj margini, a ne na rubu papira. Uz zadani ContinuationMargin od 36 točaka, izvorno pravilo zahtijevalo je da donji rub ranijeg fragmenta leži unutar 36 točaka od dna stranice, a gornji rub kasnijeg fragmenta unutar 36 točaka od vrha stranice. Dokument izvezen iz Worda sa zadanim marginama od jednog inča stavlja zadnji redak najmanje 72 točke iznad dna stranice, dalje ako je prisutno podnožje, pa uvjet nikad nije bio ispunjen. Svaka dugačka tablica u takvom dokumentu vraćena je kao neovisni fragmenti s ContinuationGroup na nuli, a pozivatelj se vratio ručnom spajanju. Test i dalje ima smisla za ono za što je osmišljen: izvještaji koje generiraju layout enginei koji pune stranicu do fiksnog okvira sadržaja i započinju sljedeću stranicu točno na vrhu. To nije loše pravilo, ono je nepotpuno, zato je verzija 3.117.0 zadržala i dodala drugu putanju umjesto da ga zamijeni

Što content-aware test provjerava umjesto toga?

Content-aware test provjerava zauzima li prostor između dvaju fragmenata bilo što osim tablice, koristeći word boxove svake stranice, a ne geometriju stranice. Dok ExtractDocumentTables prolazi dokumentom, bilježi po stranici najniži donji rub svake riječi čiji vrh leži iznad zone podnožja i najviši gornji rub svake riječi čije dno leži ispod zone zaglavlja. Obje zone duboke su ContinuationMargin točaka, pa ista opcija sada radi dvostruki posao: kao rezerva na rubu stranice i kao visina zona tekućeg zaglavlja i podnožja. Par fragmenata prolazi kada je donji rub ranijeg na razini najnižeg teksta tijela na njegovoj stranici ili ispod nje, a gornji rub kasnijeg na razini najvišeg teksta tijela na sljedećoj stranici ili iznad nje, svaki unutar AlignmentTolerance. Jednostavnije rečeno: tablica je bila zadnja stvar na stranici N i prva stvar na stranici N+1, a broj stranice ili naslov dokumenta u zoni margine se ne računaju. To isključivanje nije proizvoljno. ISO 32000-1 §14.8.2.2 klasificira tekuća zaglavlja i podnožja kao paginacijske artefakte, sadržaj koji postoji zbog prijeloma stranice, a ne uprkos njemu, i ista ideja koja pušta tagirani čitač da ih preskoči pušta i tablicu da nastavi preko njih. Članak o marked contentu pokriva kako tagirane datoteke izričito deklariraju te artefakte; ovdje se klasifikacija zaključuje iz položaja, jer većina izvezenih tablica uopće ne nosi tagove

Zašto nastavak tablica u PDFium Componentu treba dva testa: uz Word margine od jednog inča test po marginama zahtijeva rubove fragmenata unutar prozora od 36 pt do kojih layout nikad ne dolazi, dok content-aware test uspoređuje word boxove i povezuje kada je tablica zadnji sadržaj tijela na stranici N i prvi na stranici N+1, ignorirajući zone tekućeg zaglavlja i podnožja
Bilo koji od dvaju testova otvara kapiju, i tek tada se pokreću preostale provjere: susjedne stranice, nema retka natpisa pune širine na kasnijem fragmentu, i granice stupaca koje se poklapaju unutar dvostrukog AlignmentTolerance

Dva testa kombiniraju se s OR. Izvještaj layout enginea čije tablice idu do ruba papira prolazi prvi; Word izvaz čije se tablice zaustavljaju na margini prolazi drugi; dokument koji radi oboje prolazi dvaput. Tek nakon što jedan od njih uspije pokreću se preostale kapije, i to u fiksnom redoslijedu: brojevi stranica moraju biti susjedni, kasniji fragment ne smije započinjati retkom natpisa, a granice stupaca moraju se poklapati unutar dvostrukog AlignmentTolerance, što je 6 točaka uz zadane postavke. Enumeracija je TPdfTableContinuation s vrijednostima ptcNone, ptcStart, ptcMiddle i ptcEnd. Fragment koji je bio označen kao ptcEnd pa se poveže dalje na još jednu stranicu promovira se u ptcMiddle, pa se tablica na tri stranice čita kao start, middle, end u redoslijedu stranica. Brojevi grupa počinju od 1, a 0 znači nepovezano, i ToJson emitira istu informaciju kao članovi continuation i continuationGroup, što je oblik koji treba preferirati ako spajanje radi nizvodna usluga

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;     // zadano; prikazano radi jasnoće
    Options.ContinuationMargin := 54;        // podnožje od dva retka, dubine ~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 redak natpisa sprječava spajanje dviju tablica?

Fragment sa sljedeće stranice čiji je prvi redak jedna ćelija koja premošćuje sve stupce tretira se kao nova tablica, nikad kao ostatak prethodne. To pravilo postoji zato što content-aware test sam po sebi povezuje previše revno. Slučaj koji ga je razotkrio bio je obrazac u stilu transkripta: tablica završava blizu dna stranice 1, druga tablica s identičnim širinama stupaca počinje blizu vrha stranice 2, između njih ne stoji ništa osim podnožja, i stupci se poklapaju do točke. Pod testom po marginama njih dvije se nikad nisu susrele jer nijedna nije dotakla rub; pod testom sadržaja povezale su se odmah, i obrazac sa sekcijama postao je jedna nekoherentna mreža. Ono što ih razdvaja vidljivo je u strukturi ćelija. Druga tablica započinje natpisom sekcije poput "RECIPIENT INFORMATION" položenim kao jedna spojena ćelija preko cijele širine, a pravi nastavak to nikad ne radi, jer natpis pripada tablici koja je već započela na prethodnoj stranici. TableStartsWithCaptionRow kodira upravo to: fragment ima najmanje dva stupca i sadrži ćeliju s RowIndex = 0, ColumnIndex = 0 i ColumnSpan = ColumnCount. Provjera se izvršava samo na kasnijem fragmentu, pa tablica čiji vlastiti redak natpisa stoji na njenoj prvoj stranici nije pogođena; natpis je na stranici N, a pregledava se samo fragment sa stranice N+1

Kapija retka natpisa u PDFium Componentu: pravi nastavak započinje ćelijama s podacima i pridružuje se istoj ContinuationGroup, dok se kasniji fragment čiji nulti redak drži jednu spojenu ćeliju s RowIndex 0, ColumnIndex 0 i ColumnSpan jednakim ColumnCount odbija kao nastavak i prijavljuje kao nova tablica
Pregled dotiče samo kasniji fragment, pa tablica čiji vlastiti redak natpisa stoji na njenoj prvoj stranici nije pogođena, a kapija se izvršava nakon što je jedan od dvaju testova ruba već povezao par

Usporedba stupaca koja slijedi, TablesHaveMatchingColumns, stroža je od "isti broj stupaca". Ponovno gradi položaje granica svakog fragmenta iz pravokutnika ćelija, interpolira granice koje spojene ćelije skrivaju, i odbija par kada bilo koja granica odstupi više od tolerancije. Dvije tablice s četiri stupca različitih proporcija stoga ostaju odvojene čak i kada se sve ostalo poklapa

Što se događa s pojedinačnim retkom koji se prelije na sljedeću stranicu?

Mreža s crtama koja nosi jedan redak na sljedeću stranicu sada se detektira i povezuje, pod uvjetom da završi u lancu nastavka; sama za sebe biva odbačena. Zadani MinRows od 2 postoji da spriječi da se zalutali par crta prijavi kao tablica, ali zadnji redak gurnut preko prijeloma pravi je redak koji je tvrdi pod od 2 tiho odbacio, a ostatak tablice izgledao je potpun kad nije bio. Skeniranje na razini dokumenta rješava to u tri koraka. Kada su i DetectContinuations i DetectRuledTables postavljeni, prolaz po stranici pokreće detektor crtanih mreža s privremeno spuštenim podom na 1, zato ExtractTables sada prihvaća MinRows od 1 za crtane mreže, dok whitespace detekcija zadržava interni pod od 2. Nastavci se označavaju preko cijelog rezultata. Zatim se uklanja svaka tablica kraća od pozivateljevog MinRows koja nije dio nijednog lanca. Fragment od jednog retka preživljava samo zato što je bio povezan, a mreža od jednog retka nasred inače obične stranice filtrira se točno kao prije

Kako PDFium Component čuva crtani redak koji se prelije preko prijeloma stranice: prolaz po stranici za crtane mreže radi s podom od jednog retka kada su DetectContinuations i DetectRuledTables postavljeni, nastavci se označavaju preko cijelog rezultata, i uklanjaju se samo fragmenti kraći od MinRows koji stoje izvan svakog lanca
Preliveni redak preživljava jer ga njegov lanac povezuje, dok se samostalna mreža od jednog retka na običnoj stranici filtrira točno kao prije, a tablice detektirane po whitespaceu zadržavaju pod od dva retka bez takvog olakšanja
// Ponovno izgradi svaki lanac kao jedan CSV, odbacujući ponovljene retke 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 ponovio procesor teksta
      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 namjerna. Prelijevanje od jednog retka nikad se ne uklanja, jer ga čuva zaštita na RowCount, a procesor teksta koji ponavlja redak zaglavlja na svakoj stranici proizvodi fragment čiji je prvi redak opet zaglavlje, pa je odbacivanje nultog retka na srednjim i završnim fragmentima ispravno za taj slučaj, a pogrešno za generator koji ne ponavlja zaglavlja. Provjerite jedan dokument prije nego rutinu pustite na mapu

Gdje pravila još staju

Content-aware test dobar je onoliko koliko je dobar tekstualni sloj koji čita. Na skeniranoj stranici bez ikakvog teksta, zabilježeni ekstremi teksta tijela padaju natrag na granice stranice, uvjet "ništa između" ispunjen je vakuozno, i ostaju samo kapije retka natpisa i stupaca; crtana mreža na takvoj stranici i dalje se nalazi kao prazan kostur, pa se lanac može ispravno povezati, ali o okolnom tekstu ništa nije doista provjereno. Dodajte prvo tekstualni sloj ako vam je to važno. Podnožja renderirana kao slike, a ne kao tekst, nevidljiva su za logiku zona i bezopasna iz istog razloga

Zone su jedan broj. Podnožje dublje od ContinuationMargin ostavlja svoje donje retke unutar zone tijela, što čini da raniji fragment izgleda kao da ga slijedi tekst i blokira povezivanje; povisite opciju na stvarnu dubinu zone, kao što to čini prvi primjer. Povisite je previše i kratki završni odlomak blizu dna stranice sklizne u zonu i biva ignoriran, što povezuje tablicu s onim što god slijedi. Pravilo o natpisu ima zrcalnu pogrešku: generator koji piše spojeni natpis "continued" kao prvi redak svakog fragmenta nastavka imat će te fragmente odbijene kao nove tablice, a jedini je lijek danas da ih sami spojite po ContinuationGroup nakon što ništa niste olabavili, jer pravilo nema prekidač

Tablice detektirane po whitespaceu ne dobivaju nikakvo olakšanje za pojedinačni redak. Strategija whitespacea treba dva poravnata retka da uopće vidi tablicu, pa se tablica bez crta koja prelije jedan redak i dalje prijavljuje kratkom za taj redak. Kada naletite na to, word boxovi iza strukturiranih tekstualnih blokova i redoslijeda čitanja daju vam sirove pozicije da ga povratite. Na skupu uzoraka koji je pokrenuo ovaj rad, trinaest izvaza procesora teksta i preglednika, pet dokumenata sa stvarnim tablicama preko više stranica povezalo se u pojedinačne lance, a obrazac transkripta koji se prije spajao ostao je odvojen, što je mjerilo prema kojem je izdanje ocijenjeno, a ne obećanje o svakom layoutu

Označavanje nastavka, pravilo o natpisu i prolaz za pojedinačni redak žive u putanji na razini dokumenta koju dijele Delphi, C++Builder i Lazarus buildovi; puni API za izvlačenje tablica opisan je na stranici PDFium Component za Delphi