Odborný článok

Vyplnené obdĺžniky ako čiary tabuliek v PDFium

Extrakcia tabuliek v PDFium Component od verzie 3.117.0 berie tenký vyplnený obdĺžnik ako čiaru tabuľky. So zapnutým DetectFilledRulings, čo je default, sa vyplnený box zarovnaný s osami, ktorý nie je hrubší než MaxRulingThickness (3 body), stane jednou čiarou pozdĺž svojej dlhej osi, väčší vyplnený box prispeje svojimi štyrmi hranami a každá súradnica čiary sa pred zostavením mriežky snapne v rámci RulingSnapTolerance (4 body). Tabuľky exportované z Wordu, Google Docs a prehliadačov sa tak dostanú do ruled detektora ako kompletné mriežky namiesto toho, aby prepadli na whitespace detekciu ako fragmenty

Skorší článok o detekcii a extrakcii tabuliek uvádzal, že ruled detekcia používa nakreslené čiary a že každý segment ťahaného pathu sa transformuje do súradníc stránky. Tá veta bola pravdivá a neúplná. Spočítanie path objektov naprieč sadou 13 reálnych vzorových dokumentov ukázalo, že 9 z nich neobsahuje žiadny ťahaný path, a predsa každá ich stránka nesie stovky vyplnených obdĺžnikov hrubých 0,5 až 1 bod. Detektor postavený len na stroke nevidel nič, každá stránka padla na whitespace detekciu a výstupom bol rozsyp malých fragmentov namiesto tabuliek. Preset compact columns pridaný v 3.116.4 to zmiernil na úrovni fragmentov; koreňová príčina bola, že detektor čítal nesprávny painting operátor

Prečo tabuľka exportovaná z Wordu nemá ťahané čiary?

Textový procesor nemyslí na okraj ako na čiaru; myslí naň ako na box so šírkou a ten box vykresľuje výplňou. ISO 32000-1 §8.5.2.1 definuje operátor re ako pridanie obdĺžnikového subpathu a §8.5.3 oddeľuje painting operátory: S ťahá path aktuálnou šírkou čiary, f vyplní jeho vnútro. Okraj bunky s 0,5 bodu vyjde ako x y w 0.5 re f a stroke mašinéria, vrátane šírky čiary, spojov a dash patternu, sa nespustí nikdy. Tieňovanie bunky je tá istá konštrukcia s väčším boxom. Ťahaná mriežka kreslená cez m, l a S je to, čo pôvodný detektor očakával, a je to to, čo neprodukuje takmer nič exportované z kancelárskej aplikácie:

% jeden okraj bunky z exportu textového procesora: vyplnený box vysoký 0,5 pt
72 700 468 0.5 re f
% tieňovanie bunky: vyplnený box veľkosti bunky
72 676 117 24 re f
% ťahaná čiara mriežky, pre ktorú bol pôvodný detektor napísaný
72 700 m 540 700 l S

Pre detektor, ktorý sa FPDFPath_GetDrawMode pýta len na to, či je nastavený stroke flag, sú oba vyplnené boxy neviditeľné. Slová vnútri buniek sa potom dostanú na whitespace detekciu, kde stĺpce oddelené 6-bodovou medzerou sedia pod defaultným MinColumnGap 12 bodov, a vráti sa to, čo je tá podmnožina riadkov, ktorá sa náhodou zarovná dosť dobre na prejdenie MinRows. To je to fragmentové správanie a žiadne ladenie parametrov z neho nespraví mriežku, ktorú autor nakreslil

Ako PDFium Component spraví z vyplneného boxu čiaru?

TableCollectObjectRulings prezerá každý path objekt subpath po subpathe. Draw mode pochádza z FPDFPath_GetDrawMode; path sa počíta ako vyplnený, keď je zapnuté DetectFilledRulings a fill mode nie je none. Každý bod sa transformuje cez object matrix a pozbiera, najviac MaxSubpathPoints (8) na subpath, a každý krivkový segment označí subpath za krivkový. Keď sa subpath uzavrie alebo sa začína nové MoveTo, FlushSubpath rozhodne, čo to bolo: krivkový subpath sa zahodí a rovnako každý uzavretý polygón, ktorého body nesedia všetky v rámci PointTolerance (0,05 bodu) od hrán bounding boxu aspoň na jednej osi. Trojuholník, šípka ani zaoblený tab sa nikdy nestanú čiarou, a práve to drží dekoratívnu grafiku mimo mriežky

Diagram PDFium Component, ako TableCollectObjectRulings mení uzavreté subpathy na čiary tabuliek v Delphi: FlushSubpath zahodí krivkové obrysy a polygóny mimo hrán bounding boxu, MaxRulingThickness rozdelí tenké boxy na jednu čiaru na dlhú os, tieňované bunky dajú štyri okrajové čiary a DetectFilledRulings drží malé štvorce von
Uzavretý subpath prežije len vtedy, keď je zarovnaný s osami, a bounding box potom rozhodne, či je to jedna čiara, štyri hrany tieňovanej bunky, alebo vôbec nič

Čo prežije, je s osami zarovnaný obdĺžnik, klasifikovaný podľa svojho bounding boxu. Šírka na alebo pod MaxRulingThickness a výška nad ňou dá jednu zvislú čiaru v horizontálnom strede, pokrývajúcu box zdola nahor; zrkadlový prípad dá jednu vodorovnú čiaru. Obe dimenzie nad hranicou znamenajú tieňovanú bunku a box prispeje štyrmi čiarami, po jednej na hranu. Obe dimenzie na alebo pod hranicou neprispejú ničím, takže dvojbodový štvorcový bullet sa nepletie s čiarou. Ťahaný path ide staršou cestou cez AddLine, jedna čiara na segment zarovnaný s osami, takže mriežka kreslená cez S sa spracuje presne ako predtým, a path vykreslený aj výplňou aj strokeom produkuje prekrývajúce sa kúsky, ktoré merge prechod zbalí:

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

    Options := TPdfTableExtractionOptions.Default;
    // toto sú defaulty 3.117.0, vypísané pre názornosť
    Options.DetectFilledRulings := True;     // tenké vyplnené boxy sa stanú čiarami
    Options.MaxRulingThickness := 3.0;       // body; hrubšie boxy sa počítajú ako tieňovanie
    Options.RulingSnapTolerance := 4.0;      // body; 0 vypne snapovanie
    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;

Čo robí RulingSnapTolerance pre tabuľky s tieňovanými bunkami?

RulingSnapTolerance je to, čo spraví z tabuľky postavenej len na tieňovaní jednu prepojenú mriežku. Niektoré exporty nekreslia žiadny okraj: každá bunka je vyplnený box vo svojej farbe a susedné boxy oddeľuje 1 až 3 bodová medzera bielej. Každý box dá štyri okrajové čiary, ale pravý okraj jednej bunky a ľavý okraj nasledujúcej sedia 2 body od seba, a test konektivity používa RulingTolerance, ktorá má default 1 bod. Bez snapovania tvorí každá bunka vlastný connected component zo štyroch čiar, žiadny component nedosiahne MinRows a stránka nenahlási nič. TableSnapRulings pozbiera každú X súradnicu v hre (pozíciu každej zvislej čiary plus začiatok a koniec každej vodorovnej) a rovnako každú Y súradnicu, zoradí každý zoznam, naklastruje ho reťazením hodnôt, ktorých sused sa líši najviac o toleranciu, nahradí každý klaster jeho priemerom a potom presunie každú pozíciu, začiatok a koniec na najbližší stred klastra. Obe strany medzery sa stanú tou istou čiarou a konektivita drží

Diagram PDFium Component, ako RulingSnapTolerance prepojí tabuľku s tieňovanými bunkami v Delphi: susedné bunky nechávajú 2 pt medzeru, ich okrajové čiary sedia za 1 pt RulingTolerance a TableSnapRulings zreťazí obe X hodnoty do jedného stredu klastra, takže test konektivity konečne vidí zdieľanú čiaru mriežky
Snapovanie beží pred mergingom aj pred ruled detektorom, takže obe strany bielej medzery sa stanú jednou čiarou a každá bunka prestane byť ostrovom štyroch čiar

Snapovanie beží pred TableMergeRulings, ktorý zoradí čiary a spojí kolineárne kúsky, ktoré sa dotýkajú alebo prekrývajú v rámci RulingTolerance, a oba bežia skôr, než TableDetectRuled vôbec uvidí dáta, takže párová kontrola konektivity je úmerná počtu čiar mriežky a nie počtu per-cell fragmentov. Na ťahanej mriežke sú tie prechody neškodné, pretože súradnice, ktoré už boli identické, sa snapnú samy na seba. Jedna vec treba mať na pamäti: reťazové klasterovanie nemá vlastný limit šírky, séria súradníc vzdialených 3 body od seba sa zbalí do jediného stredu. Pri 4-bodovom defe to ovplyvní len stĺpce užšie než znak, ale ak má dokument skutočné 3-bodové medzery, ktoré musia zostať oddelené, znížte toleranciu alebo ju nastavte na 0 a snapovanie vypnete:

// Izoluj ruled stratégiu a porovnaj, čo na jednej stránke vidí každé nastavenie
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 export typicky nahlási 0, N a potom menej než N:
// detektor len na stroke nevidí nič, snapovanie prepojí tieňované bunky
// a vypnutie snapu nechá každú tieňovanú bunku ako vlastný ostrov
Writeln(CountRuledTables(Pdf, False, 4.0));
Writeln(CountRuledTables(Pdf, True, 4.0));
Writeln(CountRuledTables(Pdf, True, 0.0));

Čiary vnútri form XObjectov

Nástroje na page layout často zabalia tabuľku alebo celé telo stránky do form XObjectu a vykreslia ho cez Do. ISO 32000-1 §8.10.1 určuje, že form matrix sa pri vykreslení formu spája s aktuálnou transformation matrix, takže obdĺžnik vnútri formu žije vo form space a na stránku pristane až po dvoch alebo viacerých transformáciách. TableCollectObjectRulings rekurzívne vstupuje do form objektov, keď je nastavené IncludeFormXObjects: prečíta object matrix, spojí ho s rodičovskou maticou cez TableMultiplyMatrix, ktorého poradie argumentov znamená „zmapuj cez prvú maticu, potom cez druhú“, a deti vymenuje cez FPDFFormObj_CountObjects a FPDFFormObj_GetObject, pričom kombinovanú maticu posiela ďalej. Vnorenie hlbšie než MaxFormDepth (8) sa ticho preskočí, čo je poistka proti patologickým súborom a nie limit, ku ktorému by sa priblížil nejaký reálny export. Dôvod, prečo na poradí násobenia záleží, je ten istý, aký rozoberá matrix prepend verzus append: prehodenie operandov posunie translačný člen a čiara, ktorá mala pristáť na vrchu stránky, pristane v počiatku

Diagram PDFium Component, čiary vnútri form XObjectu v Delphi: tenký obdĺžnik napísaný ako 72 700 468 0.5 re f žije vo form space a na stránku pristane až po tom, čo TableMultiplyMatrix spojí rodičovskú CTM s form maticou, rekurzívne cez FPDFFormObj_CountObjects až po MaxFormDepth
Obdĺžnik je napísaný vo form space a na vrch stránky sa dostane až po vynásobení matíc v poradí, ktoré nechá translačný člen tam, kam patrí

Prečo sa rozpočet na čiary zoštvornásobil?

Defaultné MaxRulingSegments stúplo z 4096 na 16384 v 3.117.0, pretože okraje po bunkách prichádzajú v oveľa väčších počtoch než ťahané čiary mriežky. Ťahaná tabuľka s 30 riadkami a 6 stĺpcami je 38 segmentov čiar. Tá istá tabuľka exportovaná ako vyplnené boxy je až štyri okraje na bunku, teda 720 kúskov pred mergingom, a form s tieňovanými bunkami to zdvojnásobí. Dve také tabuľky na stránke by starý rozpočet vyčerpali. Rozpočet sa vynucuje v TableAppendRuling cez Check, ktorý vyhodí EPdfError s hláškou „Table ruling-segment budget exceeded“; neexistuje degradovaný výsledok, žiadna čiastočná mriežka a nespustí sa ani whitespace prechod. Ak si pre nedôveryhodný vstup nastavíte tesnejší rozpočet, výnimku odchytte a rozhodnite sa, namiesto toho, aby ste prázdny výsledok čítali ako „žiadne tabuľky“:

Options := TPdfTableExtractionOptions.Default;
Options.MaxRulingSegments := 2048;        // zámerne tesné pre nedôveryhodný vstup
try
  Tables := Pdf.ExtractTables(Options);
except
  on E: EPdfError do
  begin
    Log(E.Message);                       // 'Table ruling-segment budget exceeded'
    Options.MaxRulingSegments := 16384;   // default 3.117.0
    Tables := Pdf.ExtractTables(Options);
  end;
end;

Namerané výsledky a kde sa tento prístup zastaví

Na tých istých 13 vzorových dokumentoch sa extrakcia posunula zo 43 tabuliek, z toho 9 ruled a 34 whitespace fragmentov či false positives, na 41 ruled tabuliek a žiadne whitespace false positives. Časť toho upratovania patrí dvom sprievodným zmenám v 3.117.0: slová, ktoré už zabrala ruled mriežka, sa odstránia pred spustením whitespace detekcie, takže tabuľka sa nikdy nenahlási dvakrát, a hranica whitespace stĺpca musí byť teraz koridor bez textu naprieč každým riadkom, ktorý oddeľuje, a práve to zastavilo zarovnané odseky v tom, aby sa skórovali ako tabuľky 5x4. Práve čítanie vyplnených obdĺžnikov presunulo tabuľky samotné zo stĺpca fragmentov do stĺpca ruled

Hranice stoja za to povedať priamo. Stránka bez textovej vrstvy stále dá kostru mriežky, každú bunku prázdnu, pretože čiary pochádzajú z geometrie a text z textovej stránky; skenované stránky potrebujú najprv OCR. Vyplnené tvary s krivkami, zaoblenými rohmi alebo neobdĺžnikovými obrysmi sa zahadzujú úplne, takže tabuľka, ktorej okraje sú kreslené ako obrysy zaoblených obdĺžnikov, potrebuje whitespace detekciu ako predtým. Tabuľku bez okrajov aj bez tieňovania nič z tohto nemení a zostáva doménou whitespace stratégie opísanej v článku o extrakcii tabuliek; keď ani to nestačí, word boxy a bloky z structured text a reading order sú surovinou pre doménovo špecifický reader. Demo TableExtractionLab, ktoré vychádza s komponentom, vystavuje DetectFilledRulings vo svojom options paneli, čo je najrýchlejší spôsob, ako vidieť, ako daný export vyzerá s ním aj bez neho; plné API opisuje stránka PDFium Component pre Delphi