Technický článek

Vyplněné obdélníky jako řídící čáry tabulek v PDFium Delphi

Extrakce tabulek PDFium Component od verze 3.117.0 bere tenký vyplněný obdélník jako řídící čáru tabulky. S zapnutým DetectFilledRulings, což je default, se vyplněný box zarovnaný na osy, tenčí než MaxRulingThickness (3 body), stane jednou řídící čarou podél své dlouhé osy, větší vyplněný box přispěje svými čtyřmi hranami a každá souřadnice řídící čáry se zasnapuje v rámci RulingSnapTolerance (4 body), než se mřížka složí. Tabulky exportované z Wordu, Google Docs a prohlížečů tak dorazí k ruled detektoru jako kompletní mřížky místo propadnutí k detekci whitespace jako fragmenty

Dřívější článek o detekci a extrakci tabulek tvrdil, že ruled detekce používá nakreslené čáry a že každý strokovaný segment cesty se transformuje do souřadnic stránky. Ta věta byla pravdivá a neúplná. Počítání path objektů napříč sadou 13 reálných vzorkových dokumentů ukázalo, že 9 z nich neobsahuje žádnou strokovanou cestu, přesto každá z jejich stránek nese stovky vyplněných obdélníků tlustých 0,5 až 1 bod. Detector pracující jen se strokem neviděl nic, každá stránka propadla k detekci whitespace a výstupem byl rozptyl malých fragmentů místo tabulek. Preset compact-columns přidaný ve 3.116.4 to na úrovni fragmentů zmírnil; kořenová příčina byla v tom, že detector četl špatný malířský operátor

Proč tabulka exportovaná z Wordu nemá strokované čáry?

Textový procesor nebere okraj jako čáru; bere ho jako box se šířkou a maluje ten box výplní. ISO 32000-1 §8.5.2.1 definuje operátor re jako přidávání obdélníkového subpathu a §8.5.3 odděluje malířské operátory: S strokuje cestu aktuální šířkou čáry, f vyplní její vnitřek. Okraj buňky 0,5 bodu vychází jako x y w 0.5 re f a strokový stroj, včetně šířky čáry, spojů a vzoru přerušení, nikdy neseběhne. Stínování buňky je tatáž konstrukce s větším boxem. Strokovaná mřížka kreslená m, l a S je to, co původní detector očekával, a je to to, co skoro nic exportované z kancelářské aplikace neprodukuje:

% okraj jedné buňky z exportu textového procesoru: vyplněný box vysoký 0,5 pt
72 700 468 0.5 re f
% stínování buňky: vyplněný box o velikosti buňky
72 676 117 24 re f
% strokovaná mřížková čára, pro kterou se původní detector psal
72 700 m 540 700 l S

Detectoru, který se ptá FPDFPath_GetDrawMode jen na to, zda je nastavený stroke příznak, jsou oba vyplněné boxy neviditelné. Slova uvnitř buněk pak dorazí k detekci whitespace, kde sloupce oddělené 6bodovým gutrem sedí pod defaultním MinColumnGap 12 bodů a co se vrátí, je jakákoli podmnožina řádků, které se zrovna zarovnají dost dobře na projití MinRows. To je fragmentové chování a žádné doladění parametrů z toho neudělá mřížku, kterou autor nakreslil

Jak PDFium Component mění vyplněný box na řídící čáru?

TableCollectObjectRulings prohlíží každý path objekt po jednom subpathu. Draw mód přichází z FPDFPath_GetDrawMode; cesta počítá jako vyplněná, když je DetectFilledRulings zapnuté a fill mód není none. Každý bod se transformuje přes matici objektu a sbírá, do MaxSubpathPoints (8) na subpath a jakýkoli segment křivky označí subpath jako zakřivený. Když se subpath zavře nebo začne nový MoveTo, rozhodne FlushSubpath, co to bylo: zakřivený subpath se zahazuje a taky jakýkoli uzavřený polygon, jehož body nesedí všechny v rámci PointTolerance (0,05 bodu) od hran bounding boxu aspoň na jedné ose. Trojúhelník, šipka nebo zaoblený tab se nikdy nestane řídící čarou, což drží dekorativní grafiku mimo mřížku

Diagram PDFium Component, jak TableCollectObjectRulings mění uzavřené subpathy na řídící čáry tabulek v Delphi: FlushSubpath zahazuje zakřivené obrysy a polygony mimo hrany bounding boxu, MaxRulingThickness štěpí tenké boxy na jednu řídící čáru na dlouhou osu, stínované buňky dávají čtyři hranové řídící čáry a DetectFilledRulings drží maličké čtverečky venku
Uzavřený subpath přežije jen tehdy, když je zarovnaný na osy, a bounding box pak rozhoduje, zda je to jedna řídící čára, čtyři hrany stínované buňky, nebo nic

Co přežije, je obdélník zarovnaný na osy, klasifikovaný podle svého bounding boxu. Šířka na nebo pod MaxRulingThickness s výškou nad ní dává jednu svislou řídící čáru na horizontálním středu, táhnoucí boxem od spodku k vršku; zrcadlový případ dává jednu vodorovnou řídící čáru. Obě rozměry nad prahem znamenají stínovanou buňku a box přispívá čtyřmi řídícími čarami, jednou na hranu. Obě rozměry na nebo pod prahem nepřispívají ničím, takže čtverečková odrážka 2 bodů se nevezme za čáru. Strokovaná cesta jde starší cestou přes AddLine, jedna řídící čára na segment zarovnaný na osy, takže mřížka kreslená S se obsluhuje přesně jako dřív a cesta malovaná výplní i strokem produkuje překrývající se kusy, které sloučovací průchod slepí:

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;                     // od 1

    Options := TPdfTableExtractionOptions.Default;
    // tohle jsou defaulty 3.117.0, vyhláskované pro jasnost
    Options.DetectFilledRulings := True;     // tenké vyplněné boxy se stanou řídícími čarami
    Options.MaxRulingThickness := 3.0;       // body; silnější boxy se počítají jako stínování
    Options.RulingSnapTolerance := 4.0;      // body; 0 vypíná snapování
    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;

Co dělá RulingSnapTolerance pro tabulky se stínovanými buňkami?

RulingSnapTolerance je to, co spojí tabulku postavenou jen ze stínování do jedné mřížky. Některé exporty nekreslí vůbec žádný okraj: každá buňka je vyplněný box ve vlastní barvě a sousední boxy odděluje 1 až 3bodový bílý gutter. Každý box dá čtyři hranové řídící čáry, ale pravá hrana jedné buňky a levá hrana další sedí 2 body od sebe a test spojitosti používá RulingTolerance, který defaultuje na 1 bod. Bez snapování si každá buňka tvoří vlastní spojitou komponentu čtyř řídících čar, žádná komponenta nedosáhne MinRows a stránka nenahlásí nic. TableSnapRulings sebere každou souřadnici X ve hře (pozici každé svislé řídící čáry plus start a konec každé vodorovné) a každou souřadnici Y obdobně, sortuje každý seznam, shlukuje ho řetězením hodnot, jejichž soused se liší nejvýše o toleranci, vymění každý shluk za jeho průměr a pak přesune každou pozici, start i konec k nejbližšímu středu shluku. Dvě strany gutru se stanou jednou čarou a spojitost drží

Diagram PDFium Component, jak RulingSnapTolerance spojuje tabulku se stínovanými buňkami v Delphi: sousední buňky nechávají 2pt gutter, jejich hranové řídící čáry sedí za 1pt RulingTolerance a TableSnapRulings řetězí obě hodnoty X do jednoho průměru shluku, takže test spojitosti konečně vidí sdílenou mřížkovou čáru
Snapování běží před sloučováním i před ruled detektorem, takže dvě strany bílého gutru se stanou jednou čarou a každá buňka přestane být ostrovem čtyř řídících čar

Snapování běží před TableMergeRulings, která sortuje řídící čáry a spojuje kolineární kusy, které se dotýkají nebo překrývají v rámci RulingTolerance, a obě běží, než TableDetectRuled vůbec uvidí data, takže párová kontrola spojitosti je úměrná počtu mřížkových čar, ne počtu fragmentů na buňku. Na strokované mřížce jsou průchody neškodné, protože souřadnice, které už byly identické, se snapnou na sebe. Jedna věc, kterou držet na paměti, je, že řetězové shlukování nemá vlastní limit šířky: běh souřadnic každých 3 body od sebe se slepí do jediného středu. Na 4bodovém defaultu se to dotkne jen sloupců užších než znak, ale pokud má dokument reálné 3bodové gutry, které musí zůstat oddělené, snižte toleranci nebo ji nastavte na 0, čímž snapování vypnete:

// Izoluj ruled strategii a srovnej, co která volba vidí na jedné stránce
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;

// Export z Wordu typicky hlásí 0, N a pak méně než N:
// stroke-only nevidí nic, snapování spojí stínované buňky,
// a vypnutí snapu nechá každou stínovanou buňku jako vlastní ostrov
Writeln(CountRuledTables(Pdf, False, 4.0));
Writeln(CountRuledTables(Pdf, True, 4.0));
Writeln(CountRuledTables(Pdf, True, 0.0));

Řídící čáry uvnitř form XObjects

Nástroje page layoutu často balí tabulku, nebo celé tělo stránky, do form XObjectu a malují ji přes Do. ISO 32000-1 §8.10.1 specifikuje, že form matice se konkatenuje s aktuální transformační maticí v momentě malování formy, takže obdélník uvnitř formy bydlí ve formovém prostoru a na stránku přistane až po dvou a více transformacích. TableCollectObjectRulings rekurzuje do form objektů, když je nastavené IncludeFormXObjects: přečte matici objektu, zkombinuje ji s rodičovskou maticí přes TableMultiplyMatrix, jejíž pořadí argumentů znamená „mapuj přes první matici, pak druhou", a enumeruje děti přes FPDFFormObj_CountObjects a FPDFFormObj_GetObject, předávaje zkombinovanou matici dál. Zanoření hlubší než MaxFormDepth (8) se potichu přeskočí, což je hlídka proti patologickým souborům, ne limit, ke kterému se cokoliv reálného blíží. Důvod, proč pořadí násobení záleží, je týž, jaký rozebírá prepend versus append matic: vyměnění operandů přemístí translační člen a řídící čára, která má přistát u vršku stránky, přistane v počátku

Diagram PDFium Component řídících čar uvnitř form XObjectu v Delphi: tenký obdélník napsaný jako 72 700 468 0.5 re f bydlí ve formovém prostoru a na stránku přistane až poté, co TableMultiplyMatrix zkombinuje rodičovskou CTM s form maticí, rekurzivně skrz FPDFFormObj_CountObjects až do MaxFormDepth
Obdélník se píše ve formovém prostoru a k vršku stránky se dostane jen poté, co se matice vynásobí v pořadí, které drží translační člen tam, kam patří

Proč se budget řídících čar zčtyřnásobil?

Defaultní MaxRulingSegments vzrostl ve 3.117.0 z 4096 na 16384, protože okraje na buňku přicházejí v daleko větších počtech než strokované mřížkové čáry. Strokovaná tabulka 30 řádků a 6 sloupců je 38 úseček. Tatáž tabulka exportovaná jako vyplněné boxy je až čtyři okraje na buňku, 720 kusů před sloučením, a forma se stínovanými buňkami to zdvojnásobí. Dvě takové tabulky na stránce by vyčerpaly starý budget. Budget se vynucuje v TableAppendRuling přes Check, který hodí EPdfError se zprávou „Table ruling-segment budget exceeded"; žádný degradovaný výsledek, žádná částečná mřížka a whitespace průchod neseběhne taky. Nastavíte-li si vlastní těsnější budget pro nedůvěryhodný vstup, chytejte výjimku a rozhodněte, místo čtení prázdného výsledku jako „žádné tabulky":

Options := TPdfTableExtractionOptions.Default;
Options.MaxRulingSegments := 2048;        // záměrně těsné pro nedůvěryhodný 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;

Změřené výsledky a kde přístup končí

Na téže sadě 13 vzorkových dokumentů šla extrakce z 43 tabulek, z nichž 9 ruled a 34 whitespace fragmentů nebo falešných pozitiv, na 41 ruled tabulek a žádné whitespace falešné pozitivy. Část toho úklidu patří dvěma doprovodným změnám ve 3.117.0: slova už zabraná ruled mřížkou se odstraňují, než běží detekce whitespace, takže tabulka se nikdy nenahlásí dvakrát a whitespace sloupcová hranice musí teď být koridor bez textu přes každý řádek, který odděluje, což zastavilo zarovnané odstavce, aby se skórovaly jako tabulky 5x4. Reader vyplněných obdélníků je to, co přesunulo samotné tabulky ze sloupce fragmentů do sloupce ruled

Hranice stojí za výslovnou řeč. Stránka bez textové vrstvy pořád dá kostru mřížky, každou buňku prázdnou, protože řídící čáry přicházejí z geometrie a text z textové stránky; skenované stránky potřebují nejdřív OCR. Vyplněné tvary s křivkami, zaoblenými rohy nebo neobdélníkovými obrysy se zahazují celé, takže tabulka, jejíž okraje jsou kreslené jako obrysy zaoblených obdélníků, potřebuje detekci whitespace jako dřív. Tabulka bez okrajů i bez stínování se tímhle vším nemění a zůstává územím strategie whitespace popsané v článku o extrakci tabulek; když nestačí ani ta, word boxy a bloky z strukturovaného textu a reading orderu jsou surovina pro doménově specifický reader. Demo TableExtractionLab, které se dodává s komponentou, vystavuje DetectFilledRulings ve svém panelu voleb, což je nejrychlejší způsob, jak vidět, jak daný export vypadá s ním i bez něj; plné API je popsáno na stránce PDFium Component for Delphi