Tehnični članak

Nadaljevanje tabel čez strani PDF glede na vsebino

PDFium Component v različici 3.117.0 poveže tabelo, ki se zlomi čez mejo strani, kadar se oba fragmenta dotikata robov strani ali kadar pod prvim fragmentom in nad drugim ne sedi nobeno besedilo telesa, pri čemer se glave in noge, ki tečejo, prezrejo. ExtractDocumentTables ta preskus, ki gleda vsebino, uporabi kot alternativo starejšemu preskusu po robovih strani, zavrne fragment na naslednji strani, katerega prva vrstica je ena sama celica napisa čez celotno širino, in ohrani posamezno vrstico, ki se prelije na naslednjo stran, kot del njene verige nadaljevanja

Članek o zaznavanju in izluščevanju tabel je nadaljevanje predstavil kot štiri stroga vrata in "dotika se roba strani" štel za ena od njih. Ta opis je bil točen za različico, ki jo je pokrival, in je bil hkrati napačen za večino tabel, ki jih ljudje komponenti dejansko podajajo. Ta članek je popravek: katere dokumente preskus po robovih ne zmore, kaj ga je nadomestilo in dva stranska primera, ki ju je popravek prinesel s sabo

Zakaj preskus po robovih strani odpove pri izvozih iz Worda?

Preskus po robovih strani odpove, ker urejevalnik besedil preneha zlagati vrstice pri spodnjem robu in ne pri robu papirja. S privzetim ContinuationMargin 36 točk je izvirno pravilo zahtevalo, da spodnji rob zgodnejšega fragmenta leži znotraj 36 točk od dna strani, zgornji rob poznejšega fragmenta pa znotraj 36 točk od vrha strani. Dokument, izvožen iz Worda s privzetimi enopalčnimi robovi, postavi zadnjo vrstico vsaj 72 točk nad dnom strani, še več, če je prisotna noga, zato pogoj ni bil nikoli izpolnjen. Vsaka dolga tabela v takem dokumentu se je vrnila kot neodvisni fragmenti s ContinuationGroup na nič in klicatelj je bil spet pri ročnem sestavljanju. Preskus je še vedno smiseln za tisto, za kar je bil zasnovan: poročila, ki jih ustvarijo motorji za postavitev in napolnijo stran do fiksnega vsebinskega okvirja ter začnejo naslednjo stran poravnano na vrhu. Ni slabo pravilo, je nepopolno pravilo, zato ga je različica 3.117.0 obdržala in namesto zamenjave dodala drugo pot

Kaj preskus, ki gleda vsebino, preverja namesto tega?

Preskus, ki gleda vsebino, preveri, ali prostor med obema fragmentoma zaseda kar koli drugega razen tabele, pri čemer uporabi okvire besed vsake strani in ne geometrije strani. Medtem ko ExtractDocumentTables hodi po dokumentu, za vsako stran zabeleži najnižji spodnji rob vsake besede, katere vrh leži nad pasom noge, in najvišji zgornji rob vsake besede, katere dno leži pod pasom glave. Oba pasova sta globoka ContinuationMargin točk, zato ista možnost zdaj služi dvojno: kot popust pri robu strani in kot višina območij glave in noge, ki tečeta. Par fragmentov gre skozi, kadar je spodnji rob zgodnejšega na ravni najnižjega besedila telesa na njegovi strani ali nižje od njega in je zgornji rob poznejšega na ravni najvišjega besedila telesa na naslednji strani ali višje od njega, vsak znotraj AlignmentTolerance. Povedano preprosto: tabela je bila zadnja stvar na strani N in prva stvar na strani N+1, številka strani ali naslov dokumenta v pasu roba pa se ne štejeta. Ta izločitev ni samovoljna. ISO 32000-1 §14.8.2.2 glave in noge, ki tečejo, razvršča med artefakte paginacije, vsebino, ki obstaja zaradi preloma strani in ne kljub njemu, in ista ideja, ki označenemu bralniku omogoči, da jih preskoči, omogoči tabeli, da se nadaljuje onkraj njih. Članek o označeni vsebini pokriva, kako označene datoteke te artefakte razglasijo izrecno; tu se razvrstitev sklepa iz položaja, ker večina izvoženih tabel nima nobenih oznak

Zakaj nadaljevanje tabel v PDFium Component potrebuje dva preskusa: pri enopalčnih robovih Worda preskus po robovih strani zahteva robove fragmentov znotraj 36-točkovnih oken, ki jih postavitev nikoli ne doseže, medtem ko preskus, ki gleda vsebino, primerja okvire besed in poveže, kadar je tabela zadnja vsebina telesa na strani N in prva na strani N+1, pri čemer pasova glave in noge prezre
Vsak od obeh preskusov odpre vrata in šele nato se izvedejo ostala preverjanja: sosednji strani, brez vrstice napisa čez celotno širino na poznejšem fragmentu in ujemanje meja stolpcev znotraj dvakratnega AlignmentTolerance

Preskusa se združita z OR. Poročilo motorja za postavitev, katerega tabele segajo do roba papirja, gre skozi prvega; izvoz iz Worda, katerega tabele se ustavijo pri robu, gre skozi drugega; dokument, ki izpolni oboje, gre skozi dvakrat. Šele ko eden od njiju uspe, se izvedejo ostala vrata in to v fiksnem vrstnem redu: številki strani morata biti sosednji, poznejši fragment se ne sme začeti z vrstico napisa, meje stolpcev pa se morajo ujemati znotraj dvakratnega AlignmentTolerance, kar je pri privzetih vrednostih 6 točk. Naštevanje je TPdfTableContinuation z vrednostmi ptcNone, ptcStart, ptcMiddle in ptcEnd. Fragment, ki je bil označen kot ptcEnd in se nato poveže še z drugo stranjo, se povzdigne v ptcMiddle, zato se tristranska tabela bere kot začetek, sredina, konec v vrstnem redu strani. Številke skupin se začnejo pri 1, 0 pa pomeni nepovezano, in ToJson odda iste podatke kot člana continuation in continuationGroup, kar je oblika, ki ji velja dati prednost, če sestavljanje opravi storitev dolvodno

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;     // privzeto; prikazano zaradi jasnosti
    Options.ContinuationMargin := 54;        // dvovrstična noga, globoka ~50 točk

    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 vrstica napisa prepreči, da bi se dve tabeli zlili?

Fragment na naslednji strani, katerega prva vrstica je ena celica čez vse stolpce, se obravnava kot nova tabela in nikoli kot preostanek prejšnje. To pravilo obstaja, ker preskus, ki gleda vsebino, sam po sebi povezuje preveč vneto. Primer, ki ga je razkril, je bil obrazec v obliki zapisnika: tabela se konča blizu dna strani 1, druga tabela z enakimi širinami stolpcev se začne blizu vrha strani 2, med njima ni nič razen noge, stolpci pa se ujemajo do točke. Pri preskusu po robovih se ta dva nista nikoli srečala, ker se nobeden ni dotaknil roba; pri preskusu po vsebini sta se povezala takoj in obrazec z razdelki je postal ena neskladna mreža. Kar ju loči, je vidno v strukturi celic. Druga tabela se začne z napisom razdelka, kot je "RECIPIENT INFORMATION", postavljenim kot ena združena celica čez celotno širino, resnično nadaljevanje pa tega nikoli ne naredi, ker napis pripada tabeli, ki se je že začela na prejšnji strani. TableStartsWithCaptionRow zapisuje natanko to: fragment ima vsaj dva stolpca in vsebuje celico z RowIndex = 0, ColumnIndex = 0 in ColumnSpan = ColumnCount. Preverjanje se izvede le na poznejšem fragmentu, zato tabela, katere lastna vrstica napisa sedi na njeni prvi strani, ostane nedotaknjena; napis je na strani N in pregleda se le fragment strani N+1

Vrata vrstice napisa v PDFium Component: resnično nadaljevanje se začne s podatkovnimi celicami in se pridruži isti ContinuationGroup, poznejši fragment, katerega ničta vrstica drži eno združeno celico z RowIndex 0, ColumnIndex 0 in ColumnSpan, enakim ColumnCount, pa se kot nadaljevanje zavrne in prijavi kot nova tabela
Pregled se dotakne le poznejšega fragmenta, zato tabela, katere lastna vrstica napisa sedi na njeni prvi strani, ostane nedotaknjena, vrata pa se izvedejo potem, ko je eden od obeh preskusov robov par že povezal

Primerjava stolpcev, ki sledi, TablesHaveMatchingColumns, je strožja od "enako število stolpcev". Meje vsakega fragmenta znova zgradi iz pravokotnikov celic, interpolira meje, ki jih združene celice skrijejo, in par zavrne, kadar se katera koli meja premakne za več kot toleranca. Dve tabeli s štirimi stolpci in različnimi razmerji zato ostaneta ločeni, tudi kadar se vse drugo ujema

Kaj se zgodi z eno samo vrstico, ki se prelije na naslednjo stran?

Mreža z obrobami, ki eno vrstico ponese na naslednjo stran, se zdaj zazna in poveže, če le konča v verigi nadaljevanja; sama zase se zavrže. Privzeti MinRows 2 obstaja zato, da se zalutali par črt ne prijavi kot tabela, zadnja vrstica, potisnjena čez prelom, pa je resnična vrstica, ki jo je trda meja 2 tiho spustila, in preostanek tabele je bil videti popoln, čeprav ni bil. Pregled na ravni dokumenta to reši v treh korakih. Ko sta nastavljena DetectContinuations in DetectRuledTables, prehod po posamezni strani požene zaznavanje mrež z obrobami z začasno spuščeno mejo vrstic na 1, zato ExtractTables zdaj za mreže z obrobami sprejme MinRows 1, medtem ko zaznavanje po belini obdrži notranjo mejo 2. Nadaljevanja se označijo čez celoten rezultat. Nato se odstrani vsaka tabela, ki je krajša od klicateljevega MinRows in ni del nobene verige. Enovrstični fragment preživi le zato, ker je bil povezan, enovrstična mreža sredi sicer običajne strani pa se izloči natanko kot prej

Kako PDFium Component ohrani vrstico mreže z obrobami, ki se prelije čez prelom strani: prehod po posamezni strani za mreže z obrobami teče z mejo vrstic pri ena, kadar sta nastavljena DetectContinuations in DetectRuledTables, nadaljevanja se označijo čez celoten rezultat, odstranijo pa se le fragmenti, krajši od MinRows, ki so zunaj vsake verige
Prelita vrstica preživi, ker jo njena veriga poveže, samostojna enovrstična mreža na običajni strani pa se izloči natanko kot prej, tabele, zaznane po belini, pa obdržijo svojo dvovrstično mejo brez takšne olajšave
// Vsako verigo znova zgradi kot en CSV in spusti ponovljene
// vrstice glave na fragmentih nadaljevanja
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);            // glavo je ponovil urejevalnik besedil
      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;

Dve podrobnosti v tej rutini sta namerni. Enovrstična prelita vrstica ni nikoli odstranjena, ker jo varovalka na RowCount obdrži, urejevalnik besedil, ki ponovi vrstico glave na vsaki strani, pa ustvari fragment, katerega prva vrstica je spet glava, zato je spust ničte vrstice na srednjih in končnih fragmentih pravilen za ta primer in napačen za generator, ki glav ne ponavlja. Preden rutino spustite na mapo, preverite en dokument

Kje se pravila še ustavijo

Preskus, ki gleda vsebino, je toliko dober, kolikor dober je besedilni sloj, ki ga bere. Na skenirani strani brez vsakršnega besedila se zabeleženi skrajnosti besedila telesa vrnejo na meje strani, pogoj "nič vmes" je izpolnjen prazno in ostaneta le vrata vrstice napisa in stolpcev; mreža z obrobami na taki strani se še vedno najde kot prazno ogrodje, zato se veriga lahko poveže pravilno, a o okoliškem besedilu ni bilo dejansko preverjeno nič. Če je to pomembno, najprej dodajte besedilni sloj. Noge, upodobljene kot slike in ne kot besedilo, so za logiko pasov nevidne in iz istega razloga neškodljive

Pasova sta ena sama številka. Noga, globlja od ContinuationMargin, pusti svoje spodnje vrstice znotraj območja telesa, zaradi česar je videti, kakor da zgodnejšemu fragmentu sledi besedilo, in povezava se prepreči; možnost dvignite na resnično globino pasu, kot to naredi prvi primer. Dvignite jo previsoko in kratek zaključni odstavek blizu dna strani zdrsne v pas ter se prezre, kar poveže tabelo s tem, kar ji sledi. Pravilo o napisu ima zrcalno napako: generator, ki kot prvo vrstico vsakega fragmenta nadaljevanja zapiše združen napis "se nadaljuje", bo imel te fragmente zavrnjene kot nove tabele, edino zdravilo danes pa je, da sami sestavite po ContinuationGroup, ne da bi kar koli popustili, ker pravilo nima stikala

Tabele, zaznane po belini, ne dobijo nobene olajšave za posamezno vrstico. Strategija po belini potrebuje dve poravnani vrstici, da tabelo sploh vidi, zato je neobrobljena tabela, ki prelije eno vrstico, še vedno prijavljena krajša za to vrstico. Ko na to naletite, vam okviri besed, na katerih slonita strukturirani besedilni bloki in vrstni red branja, dajo surove položaje, s katerimi jo izterjate. Na vzorčnem nizu, ki je pognal to delo, trinajst izvozov urejevalnikov besedil in brskalnikov, se je pet dokumentov z resničnimi večstranskimi tabelami povezalo v ene same verige, obrazec v obliki zapisnika, ki se je prej zlil, pa je ostal ločen — to je merilo, po katerem je bila različica merjena, ne obljuba o vsaki postavitvi

Označevanje nadaljevanj, pravilo o napisu in prehod za posamezno vrstico živijo v poti na ravni dokumenta, ki si jo delijo različice za Delphi, C++Builder in Lazarus; celoten API za izluščevanje tabel je opisan na strani izdelka PDFium Component za Delphi