Tekninen artikkeli

Täytetyt suorakaiteet taulukon viivoina PDFium Delphissä

PDFium Componentin taulukonpoiminta käsittelee ohutta täytettyä suorakaidetta taulukkoviivana versiosta 3.117.0 lähtien. Kun DetectFilledRulings on käytössä, mikä on oletus, akselien suuntainen täytetty laatikko, joka ei ole MaxRulingThickness-arvoa (3 pistettä) paksumpi, muuttuu yhdeksi taulukkoviivaksi pitkittäisakseliaan pitkin, suurempi täytetty laatikko antaa neljä reunaviivaansa, ja jokainen taulukkoviivan koordinaatti kohdistetaan RulingSnapTolerance-arvon (4 pistettä) sisällä ennen kuin ruudukko kootaan. Wordista, Google Docsista ja selaimista viedyt taulukot saapuvat siksi viivailmaisimelle täydellisinä ruudukoina sen sijaan, että ne putoaisivat tyhjän tilan tunnistukseen palasina

Aiempi artikkeli taulukoiden tunnistuksesta ja poiminnasta totesi, että viivojen tunnistus käyttää piirrettyjä viivoja ja että jokainen viivapiirretty polun segmentti muunnetaan sivukoordinaateiksi. Tuo lause oli totta ja puutteellinen. Polkuobjektien laskeminen kolmentoista todellisen näyteasiakirjan joukosta osoitti, että yhdeksässä niistä ei ole yhtäkään viivapiirrettyä polkua, mutta jokaisella niiden sivulla on satoja täytettyjä suorakaiteita, jotka ovat 0.5–1 pisteen paksuisia. Pelkän viivapiirron ilmaisin ei nähnyt mitään, jokainen sivu putosi tyhjän tilan tunnistukseen, ja tuloste oli pienten palojen sirpale taulukoiden sijaan. Versiossa 3.116.4 lisätty kapeiden sarakkeiden esiasetus pehmensi tuota palojen tasolla; perimmäinen syy oli, että ilmaisin luki väärää piirto-operaattoria

Miksi Wordista viedyssä taulukossa ei ole viivapiirrettyjä viivoja?

Tekstinkäsittelyohjelma ei ajattele reunusta viivana; se ajattelee sitä laatikkona, jolla on leveys, ja maalaa tuon laatikon täytöllä. ISO 32000-1 §8.5.2.1 määrittelee re-operaattorin liittävän suorakaiteen alapolun, ja §8.5.3 erottaa piirto-operaattorit: S viivapiirtää polun nykyisellä viivanleveydellä, f täyttää sen sisäosan. 0.5 pisteen solun reunus tulee ulos muodossa x y w 0.5 re f, eikä viivapiirtokoneisto, viivanleveys, liitokset ja katkoviivakuvio mukaan lukien, koskaan käynnisty. Solun varjostus on sama rakenne isommalla laatikolla. Viivapiirretty ruudukko, joka on piirretty operaattoreilla m, l ja S, on se mitä alkuperäinen ilmaisin odotti, ja sitä ei tuota juuri mikään toimistosovelluksesta viety tiedosto:

% yksi solun reunus tekstinkäsittelyohjelman viennistä: 0.5 pt korkea täytetty laatikko
72 700 468 0.5 re f
% solun varjostus: solun kokoinen täytetty laatikko
72 676 117 24 re f
% viivapiirretty ruudukkoviiva, jota varten alkuperäinen ilmaisin kirjoitettiin
72 700 m 540 700 l S

Ilmaisimelle, joka kysyy FPDFPath_GetDrawMode-funktiolta vain sitä, onko viivapiirtolippu asetettu, molemmat täytetyt laatikot ovat näkymättömiä. Solujen sisällä olevat sanat päätyvät sitten tyhjän tilan tunnistukseen, jossa 6 pisteen raolla erotetut sarakkeet jäävät oletusarvoisen MinColumnGap-arvon 12 pistettä alapuolelle, ja takaisin tulee mikä tahansa rivien osajoukko, joka sattuu kohdistumaan tarpeeksi hyvin läpäistäkseen MinRows-arvon. Se on sitä palakäyttäytymistä, eikä mikään parametrien virittäminen muuta sitä ruudukoksi, jonka tekijä piirsi

Miten PDFium Component muuttaa täytetyn laatikon taulukkoviivaksi?

TableCollectObjectRulings tutkii jokaisen polkuobjektin yksi alapolku kerrallaan. Piirtotila tulee FPDFPath_GetDrawMode-funktiosta; polku lasketaan täytetyksi kun DetectFilledRulings on päällä eikä täyttötila ole none. Jokainen piste muunnetaan objektimatriisin läpi ja kerätään, enintään MaxSubpathPoints (8) per alapolku, ja mikä tahansa käyräsegmentti merkitsee alapolun käyräksi. Kun alapolku sulkeutuu tai uusi MoveTo alkaa, FlushSubpath päättää mikä se oli: käyrä alapolku hylätään, ja niin hylätään mikä tahansa suljettu monikulmio, jonka pisteet eivät kaikki istu PointTolerance-arvon (0.05 pistettä) sisällä rajaavan laatikon reunoista ainakin yhdellä akselilla. Kolmiosta, nuolesta tai pyöristetystä välilehdestä ei koskaan tule taulukkoviivaa, mikä pitää koristekuvituksen poissa ruudukosta

PDFium Componentin kaavio siitä, miten TableCollectObjectRulings muuttaa suljetut alapolut taulukkoviivoiksi Delphissä: FlushSubpath hylkää käyrät ääriviivat ja rajaavan laatikon reunoilta poikkeavat monikulmiot, MaxRulingThickness jakaa ohuet laatikot yhdeksi taulukkoviivaksi pitkittäisakseliaan pitkin, varjostetut solut antavat neljä reunaviivaa ja DetectFilledRulings pitää pienet neliöt poissa
Suljettu alapolku säilyy vain kun se on akselien suuntainen, ja rajaava laatikko ratkaisee sitten, onko se yksi taulukkoviiva, varjostetun solun neljä reunaa vai ei mitään

Se mikä selviää, on akselien suuntainen suorakaide, joka luokitellaan rajaavan laatikkonsa mukaan. Leveys enintään MaxRulingThickness-arvon suuruinen ja korkeus sitä suurempi antaa yhden pystysuoran taulukkoviivan vaakasuoran keskikohdan kohdalle, ulottuen laatikon alaosasta yläosaan; peilikuvatapaus antaa yhden vaakasuoran taulukkoviivan. Molemmat mitat kynnyksen yläpuolella tarkoittaa varjostettua solua, ja laatikko antaa neljä taulukkoviivaa, yhden per reuna. Molemmat mitat kynnyksen alapuolella eivät anna mitään, joten 2 pisteen neliömäistä luotia ei sekoiteta viivaksi. Viivapiirretty polku kulkee vanhempaa reittiä AddLine-funktion kautta, yksi taulukkoviiva per akselien suuntainen segmentti, joten operaatiolla S piirretty ruudukko käsitellään täsmälleen kuten ennenkin, ja sekä täytöllä että viivapiirrolla maalattu polku tuottaa päällekkäisiä paloja, jotka yhdistämisvaihe tiivistää:

uses
  PDFium;

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

    Options := TPdfTableExtractionOptions.Default;
    // nämä ovat 3.117.0:n oletukset, kirjoitettuna auki selvyyden vuoksi
    Options.DetectFilledRulings := True;     // ohuet täytetyt laatikot muuttuvat taulukkoviivoiksi
    Options.MaxRulingThickness := 3.0;       // pistettä; paksummat laatikot lasketaan varjostukseksi
    Options.RulingSnapTolerance := 4.0;      // pistettä; 0 poistaa kohdistuksen käytöstä
    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';
      Writeln(Format('%dx%d %s, confidence %.2f',
        [Tables[I].RowCount, Tables[I].ColumnCount, Mode,
         Tables[I].Confidence]));
    end;
  finally
    Pdf.Free;
  end;
end;

Mitä RulingSnapTolerance tekee varjostetuista soluista koostuville taulukoille?

RulingSnapTolerance on se, mikä saa pelkästä varjostuksesta rakennetun taulukon liittymään yhdeksi ruudukoksi. Jotkin viennit eivät piirrä reunusta lainkaan: jokainen solu on täytetty laatikko omassa värissään, ja vierekkäiset laatikot erottaa 1–3 pisteen valkoinen rako. Jokainen laatikko antaa neljä reunaviivaa, mutta yhden solun oikea reuna ja seuraavan vasen reuna istuvat 2 pisteen päässä toisistaan, ja yhtenäisyystesti käyttää RulingTolerance-arvoa, jonka oletus on 1 piste. Ilman kohdistusta jokainen solu muodostaa oman yhtenäisen komponenttinsa neljästä taulukkoviivasta, mikään komponentti ei yllä MinRows-arvoon, eikä sivu raportoi mitään. TableSnapRulings kerää kaikki pelissä olevat X-koordinaatit (kunkin pystysuoran taulukkoviivan sijainti sekä kunkin vaakasuoran alun ja lopun) ja samoin kaikki Y-koordinaatit, lajittelee kunkin listan, klusteroi sen ketjuttamalla arvot joiden naapuri ei eroa toleranssia enempää, korvaa jokaisen klusterin sen keskiarvolla ja siirtää sitten jokaisen sijainnin, alun ja lopun lähimpään klusterin keskikohtaan. Raon kaksi puolta muuttuvat samaksi viivaksi, ja yhtenäisyys pitää

PDFium Componentin kaavio siitä, miten RulingSnapTolerance yhdistää varjostetuista soluista koostuvan taulukon Delphissä: vierekkäiset solut jättävät 2 pt raon, niiden reunaviivat istuvat 1 pt RulingTolerancen ulkopuolella, ja TableSnapRulings ketjuttaa kaksi X-arvoa yhdeksi klusterin keskiarvoksi jolloin yhtenäisyystesti viimein näkee yhteisen ruudukkoviivan
Kohdistus ajetaan ennen yhdistämistä ja ennen viivailmaisinta, joten valkoisen raon kaksi puolta muuttuvat yhdeksi viivaksi ja jokainen solu lakkaa olemasta neljän taulukkoviivan saareke

Kohdistus ajetaan ennen TableMergeRulings-funktiota, joka lajittelee taulukkoviivat ja liittää samalla suoralla olevat palat, jotka koskettavat tai menevät päällekkäin RulingTolerance-arvon sisällä, ja molemmat ajetaan ennen kuin TableDetectRuled koskaan näkee dataa, joten pareittainen yhtenäisyystarkistus on verrannollinen ruudukkoviivojen määrään eikä solukohtaisten palojen määrään. Viivapiirretyllä ruudukolla kierrokset ovat vaarattomia, koska jo valmiiksi identtiset koordinaatit kohdistuvat itseensä. Yksi asia kannattaa pitää mielessä: ketjuklusteroinnilla ei ole omaa leveysrajaa, joten jonossa olevat koordinaatit, jotka ovat 3 pisteen päässä toisistaan, tiivistyvät yhdeksi keskipisteeksi. 4 pisteen oletusarvolla se vaikuttaa vain merkkiä kapeampiin sarakkeisiin, mutta jos asiakirjassa on aitoja 3 pisteen rakoja joiden on pysyttävä erillään, laske toleranssia tai aseta se nollaan kohdistuksen poistamiseksi käytöstä:

// Eristä viivastrategia ja vertaa, mitä kukin asetus näkee yhdellä sivulla
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;

// Word-vienti raportoi tyypillisesti 0, N ja sitten vähemmän kuin N:
// pelkkä viivapiirto ei näe mitään, kohdistus yhdistää varjostetut solut,
// ja kohdistuksen poistaminen jättää jokaisen varjostetun solun omaksi saarekseen
Writeln(CountRuledTables(Pdf, False, 4.0));
Writeln(CountRuledTables(Pdf, True, 4.0));
Writeln(CountRuledTables(Pdf, True, 0.0));

Taulukkoviivat lomake-XObjectien sisällä

Sivujen asettelutyökalut kääriyvät usein taulukon tai koko sivun rungon lomake-XObjectiin ja maalaavat sen operaatiolla Do. ISO 32000-1 §8.10.1 määrittelee, että lomakematriisi ketjutetaan nykyisen muunnosmatriisin kanssa kun lomake maalataan, joten lomakkeen sisällä oleva suorakaide elää lomakeavaruudessa ja laskeutuu sivulle vasta kahden tai useamman muunnoksen jälkeen. TableCollectObjectRulings laskeutuu rekursiivisesti lomakeobjekteihin kun IncludeFormXObjects on asetettu: se lukee objektimatriisin, yhdistää sen ylemmän tason matriisiin TableMultiplyMatrix-funktiolla, jonka argumenttijärjestys tarkoittaa että kuvataan ensimmäisen matriisin läpi ja sitten toisen, ja luettelee lapset FPDFFormObj_CountObjects- ja FPDFFormObj_GetObject -funktioilla välittäen yhdistetyn matriisin alaspäin. Yli MaxFormDepth-arvon (8) menevä sisäkkäisyys ohitetaan hiljaisesti, mikä on suoja patologisia tiedostoja vasten eikä raja, jota mikään oikea vienti lähestyy. Kertolaskujärjestyksen merkitys on sama, jota käsitellään artikkelissa matriisin prepend- vai append-operaatiosta: operandien vaihtaminen siirtää siirtotermiä, ja taulukkoviiva, jonka pitäisi laskeutua sivun yläosaan, laskeutuu origoon sen sijaan

PDFium Componentin kaavio taulukkoviivoista lomake-XObjectin sisällä Delphissä: muodossa 72 700 468 0.5 re f kirjoitettu ohut suorakaide elää lomakeavaruudessa ja laskeutuu sivulle vasta kun TableMultiplyMatrix yhdistää ylemmän tason CTM:n lomakematriisiin, rekursiivisesti FPDFFormObj_CountObjects-funktion kautta MaxFormDepth-arvoon asti
Suorakaide on kirjoitettu lomakeavaruudessa ja saavuttaa sivun yläosan vasta kun matriisit kerrotaan järjestyksessä, joka pitää siirtotermin siellä mihin se kuuluu

Miksi taulukkoviivabudjetti nelinkertaistui?

Oletusarvoinen MaxRulingSegments nousi 4096:sta 16384:ään versiossa 3.117.0, koska solukohtaisia reunuksia saapuu paljon suurempia määriä kuin viivapiirrettyjä ruudukkoviivoja. Viivapiirretty 30 rivin ja 6 sarakkeen taulukko on 38 viivan segmenttiä. Sama taulukko täytettyinä laatikoina vietyinä on enintään neljä reunusta per solu, 720 palaa ennen yhdistämistä, ja lomake jossa on varjostettuja soluja kaksinkertaistaa sen. Kaksi sellaista taulukkoa sivulla olisi kuluttanut vanhan budjetin loppuun. Budjettia valvotaan TableAppendRuling-funktiossa Check-kutsun kautta, joka nostaa EPdfError-poikkeuksen viestillä "Table ruling-segment budget exceeded"; tuloksena ei ole heikennettyä tulosta, ei osittaista ruudukkoa, eikä tyhjän tilan kierros aja myöskään. Jos asetat oman tiukemman budjetin epäluotettavalle syötteelle, nappaa poikkeus ja päätä itse sen sijaan, että lukisit tyhjän tuloksen merkityksessä ei taulukoita:

Options := TPdfTableExtractionOptions.Default;
Options.MaxRulingSegments := 2048;        // tarkoituksella tiukka epäluotettavalle syötteelle
try
  Tables := Pdf.ExtractTables(Options);
except
  on E: EPdfError do
  begin
    Log(E.Message);                       // 'Table ruling-segment budget exceeded'
    Options.MaxRulingSegments := 16384;   // 3.117.0:n oletus
    Tables := Pdf.ExtractTables(Options);
  end;
end;

Mitatut tulokset ja mihin menetelmä pysähtyy

Samoilla kolmellatoista näyteasiakirjalla poiminta meni 43 taulukosta, joista 9 oli viivapohjaisia ja 34 tyhjän tilan paloja tai vääriä positiivisia, 41 viivapohjaiseen taulukkoon ilman tyhjän tilan vääriä positiivisia. Osa tuosta siistiytymisestä kuuluu kahdelle rinnakkaiselle muutokselle versiossa 3.117.0: sanat, jotka viivaruudukko on jo vallannut, poistetaan ennen kuin tyhjän tilan tunnistus ajetaan, joten taulukkoa ei koskaan raportoida kahdesti, ja tyhjän tilan sarakerajan on nyt oltava tekstistä vapaa käytävä jokaisen erottamansa rivin yli, mikä pysäytti tasattujen kappaleiden pisteytymisen 5x4-taulukoiksi. Täytettyjen suorakaiteiden lukija on se, mikä siirsi taulukot itse palasarakkeesta viivapohjaisten sarakkeeseen

Rajat kannattaa sanoa selvästi. Sivu, jolla ei ole tekstitasoa, tuottaa yhä ruudukon kehyksen, jokainen solu tyhjänä, koska taulukkoviivat tulevat geometriasta ja teksti tekstisivulta; skannatut sivut tarvitsevat OCR:n ensin. Täytetyt muodot, joissa on käyriä, pyöristettyjä kulmia tai ei-suorakaiteisia ääriviivoja, pudotetaan kokonaan, joten taulukko, jonka reunukset on piirretty pyöristettyjen suorakaiteiden ääriviivoina, tarvitsee tyhjän tilan tunnistuksen kuten ennenkin. Taulukko, jossa ei ole reunuksia eikä varjostusta, ei muutu tästä mihinkään ja pysyy tyhjän tilan strategian alana, jota kuvataan taulukonpoiminta-artikkelissa; kun sekään ei riitä, jäsennellyn tekstin ja lukujärjestyksen sanalaatikot ja lohkot ovat raaka-ainetta toimialakohtaiselle lukijalle. Komponentin mukana toimitettava TableExtractionLab-demo tuo DetectFilledRulings-asetuksen asetuspaneliinsa, mikä on nopein tapa nähdä, miltä tietty vienti näyttää sen kanssa ja ilman; täysi API kuvataan PDFium Component for Delphi -sivulla