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
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
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
// 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