Technisch artikel

Gevulde rechthoeken als tabelranden in PDFium Delphi

De tabelextractie van PDFium Component behandelt sinds versie 3.117.0 een dunne gevulde rechthoek als een tabelrand. Met DetectFilledRulings aan, wat de standaard is, wordt een gevulde asparallelle box die niet dikker is dan MaxRulingThickness (3 punten) één rand langs zijn lange as, levert een grotere gevulde box zijn vier randen, en wordt elke randcoördinaat binnen RulingSnapTolerance (4 punten) samengevoegd voordat het raster wordt opgebouwd. Tabellen die uit Word, Google Docs en browsers zijn geëxporteerd bereiken de gelijnde detector dus als complete rasters in plaats van als fragmenten door te vallen naar whitespace-detectie

Het eerdere artikel over tabledetectie en -extractie stelde dat gelijnde detectie de getekende lijnen gebruikt en dat elk padsegment naar paginacoördinaten wordt getransformeerd. Die zin was waar en onvolledig. Het tellen van padobjecten over een set van 13 echte voorbeelddocumenten liet zien dat 9 ervan helemaal geen gestreken pad bevatten, terwijl elke pagina honderden gevulde rechthoeken van 0.5 tot 1 punt dik draagt. De detector die alleen naar streken keek zag niets, elke pagina viel terug op whitespace-detectie, en de uitvoer was een verstrooiing van kleine fragmenten in plaats van tabellen. De preset voor compacte kolommen die in 3.116.4 is toegevoegd verzachtte dat op fragmentniveau; de grondoorzaak was dat de detector de verkeerde schilderoperator las

Waarom heeft een uit Word geëxporteerde tabel geen gestreken lijnen?

Een tekstverwerker ziet een rand niet als een lijn; hij ziet hem als een box met een breedte, en die box schildert hij met een vulling. ISO 32000-1 §8.5.2.1 definieert de operator re als het toevoegen van een rechthoeksubpad, en §8.5.3 scheidt de schilderoperatoren: S strekt het pad met de huidige lijnbreedte, f vult zijn binnengebied. Een celrand van 0.5 punt komt eruit als x y w 0.5 re f, en het strekenmechanisme, lijnbreedte, verbindingen en streeppatroon inbegrepen, loopt nooit. Celarcering is dezelfde constructie met een grotere box. Een gestreken raster getekend met m, l en S is wat de oorspronkelijke detector verwachtte, en het is wat bijna niets dat uit een kantoorapplicatie komt oplevert:

% één celrand uit een tekstverwerker-export: een gevulde box van 0.5 pt hoog
72 700 468 0.5 re f
% celarcering: een gevulde box zo groot als de cel
72 676 117 24 re f
% de gestreken rasterlijn waarvoor de oorspronkelijke detector was geschreven
72 700 m 540 700 l S

Voor een detector die FPDFPath_GetDrawMode alleen vraagt of de strekenflag gezet is, zijn beide gevulde boxen onzichtbaar. De woorden in de cellen bereiken dan whitespace-detectie, waar kolommen die door een tussenruimte van 6 punten gescheiden zijn onder de standaard MinColumnGap van 12 punten blijven, en wat terugkomt is welke subset van rijen dan ook die toevallig goed genoeg uitlijnt om MinRows te halen. Dat is het fragmentgedrag, en geen enkele hoeveelheid parametertuning maakt er het raster van dat de auteur tekende

Hoe maakt PDFium Component van een gevulde box een rand?

TableCollectObjectRulings bekijkt elk padobject subpad voor subpad. De tekenmodus komt van FPDFPath_GetDrawMode; een pad telt als gevuld wanneer DetectFilledRulings aan staat en de vulmodus niet none is. Elk punt wordt door de objectmatrix getransformeerd en verzameld, tot MaxSubpathPoints (8) per subpad, en elk curvesegment markeert het subpad als gebogen. Wanneer het subpad sluit of een nieuwe MoveTo begint, beslist FlushSubpath wat het was: een gebogen subpad wordt weggegooid, en zo ook elke gesloten polygoon waarvan de punten niet allemaal binnen PointTolerance (0.05 punten) van de randen van de begrenzende box liggen op minstens één as. Een driehoek, een chevron of een afgerond tabje wordt nooit een rand, en dat is wat decoratieve grafiek uit het raster houdt

Diagram van PDFium Component: hoe TableCollectObjectRulings gesloten subpaden in Delphi omzet in tabelranden, waarbij FlushSubpath gebogen contouren en polygonen buiten de randen van de begrenzende box weggooit, MaxRulingThickness dunne boxen splitst in één rand per lange as, gearceerde cellen vier randstukken geven en DetectFilledRulings kleine vierkantjes eruit houdt
Een gesloten subpad overleeft alleen als het asparallel is, en de begrenzende box beslist daarna of het één rand is, vier randen van een gearceerde cel, of helemaal niets

Wat overblijft is een asparallelle rechthoek, geclassificeerd op zijn begrenzende box. Breedte op of onder MaxRulingThickness met een hoogte erboven levert één verticale rand op het horizontale midden, die de box van onder naar boven omspant; het spiegelgeval levert één horizontale rand. Beide dimensies boven de drempel betekent een gearceerde cel, en de box levert vier randen, één per zijde. Beide dimensies op of onder de drempel leveren niets, dus een vierkant opsommingsteken van 2 punten wordt niet voor een lijn aangezien. Een gestreken pad neemt de oudere route via AddLine, één rand per asparallel segment, dus een raster dat met S is getekend wordt precies zoals voorheen behandeld, en een pad dat zowel gevuld als gestreken is levert overlappende stukken op die de merge-ronde samenvouwt:

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;
    // dit zijn de standaardwaarden van 3.117.0, voor de duidelijkheid uitgeschreven
    Options.DetectFilledRulings := True;     // dunne gevulde boxen worden randen
    Options.MaxRulingThickness := 3.0;       // punten; dikkere boxen gelden als arcering
    Options.RulingSnapTolerance := 4.0;      // punten; 0 schakelt samenvoegen uit
    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;

Wat doet RulingSnapTolerance voor tabellen met gearceerde cellen?

RulingSnapTolerance is wat een tabel die alleen uit arcering is opgebouwd tot één raster laat aaneensluiten. Sommige exports tekenen helemaal geen rand: elke cel is een gevulde box in zijn eigen kleur, en naburige boxen worden gescheiden door een tussenruimte van 1 tot 3 punten wit. Elke box levert vier randstukken, maar de rechterrand van de ene cel en de linkerrand van de volgende liggen 2 punten uit elkaar, en de connectiviteitstest gebruikt RulingTolerance, die standaard 1 punt is. Zonder samenvoegen vormt elke cel zijn eigen samenhangende component van vier randen, geen component haalt MinRows, en de pagina meldt niets. TableSnapRulings verzamelt elke X-coördinaat in het spel (de positie van elke verticale rand plus het begin en eind van elke horizontale) en elke Y-coördinaat op dezelfde manier, sorteert elke lijst, clustert die door waarden aan elkaar te rijgen waarvan de buurwaarde niet meer dan de tolerantie verschilt, vervangt elk cluster door zijn gemiddelde, en verplaatst daarna elke positie, elk begin en elk eind naar het dichtstbijzijnde clustercentrum. De twee zijden van een tussenruimte worden dezelfde lijn, en de connectiviteit houdt stand

Diagram van PDFium Component: hoe RulingSnapTolerance een tabel met gearceerde cellen in Delphi verbindt, waarbij naburige cellen een tussenruimte van 2 pt laten, hun randstukken buiten de RulingTolerance van 1 pt liggen, en TableSnapRulings de twee X-waarden tot één clustergemiddelde rijgt zodat de connectiviteitstest eindelijk een gedeelde rasterlijn ziet
Het samenvoegen loopt vóór het mergen en vóór de gelijnde detector, zodat twee zijden van een witte tussenruimte één lijn worden en elke cel ophoudt een eiland van vier randen te zijn

Het samenvoegen loopt vóór TableMergeRulings, dat de randen sorteert en collineaire stukken samenvoegt die elkaar binnen RulingTolerance raken of overlappen, en beide lopen voordat TableDetectRuled de data ooit ziet, zodat de paarsgewijze connectiviteitscontrole evenredig is met het aantal rasterlijnen in plaats van met het aantal fragmenten per cel. Op een gestreken raster zijn de rondes onschadelijk, want coördinaten die al identiek waren voegen naar zichzelf samen. Het enige om in gedachten te houden is dat het koppelen van clusters geen eigen breedtelimiet heeft: een reeks coördinaten die elk 3 punten uit elkaar liggen valt samen tot één centrum. Met de standaard van 4 punten raakt dat alleen kolommen smaller dan een teken, maar heeft een document echte tussenruimtes van 3 punten die apart moeten blijven, verlaag dan de tolerantie of zet hem op 0 om het samenvoegen uit te schakelen:

// Isoleer de gelijnde strategie en vergelijk wat elke instelling op één pagina ziet
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;

// Een Word-export meldt doorgaans 0, N en dan minder dan N:
// alleen-streken ziet niets, samenvoegen verbindt de gearceerde cellen,
// en het samenvoegen uitzetten laat elke gearceerde cel zijn eigen eiland blijven
Writeln(CountRuledTables(Pdf, False, 4.0));
Writeln(CountRuledTables(Pdf, True, 4.0));
Writeln(CountRuledTables(Pdf, True, 0.0));

Randen binnen form XObjects

Pagineringtools stoppen een tabel, of de hele paginabody, vaak in een form XObject en schilderen die met Do. ISO 32000-1 §8.10.1 bepaalt dat de formmatrix wordt samengevoegd met de huidige transformatiematrix wanneer de form wordt geschilderd, dus een rechthoek binnen de form leeft in formruimte en landt pas op de pagina na twee of meer transformaties. TableCollectObjectRulings daalt af in formobjecten wanneer IncludeFormXObjects aan staat: het leest de objectmatrix, combineert die met de bovenliggende matrix via TableMultiplyMatrix, waarvan de argumentorde "map door de eerste matrix, dan door de tweede" betekent, en enumereert de kinderen met FPDFFormObj_CountObjects en FPDFFormObj_GetObject, waarbij de gecombineerde matrix wordt doorgegeven. Dieper nesten dan MaxFormDepth (8) wordt stilzwijgend overgeslagen, wat een vangnet tegen pathologische bestanden is en geen limiet die een echte export benadert. De reden dat de vermenigvuldigingsorde ertoe doet is dezelfde als besproken in matrix prepend versus append: de operanden omwisselen verplaatst de translatieterm, en een rand die bovenaan de pagina hoort te landen komt in de oorsprong terecht

Diagram van PDFium Component: randen binnen een form XObject in Delphi, waarbij een dunne rechthoek geschreven als 72 700 468 0.5 re f in formruimte leeft en pas op de pagina landt nadat TableMultiplyMatrix de bovenliggende CTM met de formmatrix combineert, recursief via FPDFFormObj_CountObjects tot MaxFormDepth
De rechthoek is in formruimte geschreven en bereikt de bovenkant van de pagina pas nadat de matrices zijn vermenigvuldigd in een orde die de translatieterm laat waar hij hoort

Waarom is het randenbudget verviervoudigd?

De standaard MaxRulingSegments steeg in 3.117.0 van 4096 naar 16384, omdat celranden in veel grotere aantallen binnenkomen dan gestreken rasterlijnen. Een gestreken tabel van 30 rijen en 6 kolommen is 38 lijnsegmenten. Dezelfde tabel als gevulde boxen is tot vier randen per cel, 720 stukken vóór het samenvoegen, en een formulier met gearceerde cellen verdubbelt dat. Twee zulke tabellen op één pagina zouden het oude budget hebben uitgeput. Het budget wordt gehandhaafd in TableAppendRuling via Check, dat EPdfError opgooit met de melding "Table ruling-segment budget exceeded"; er is geen afgezwakt resultaat, geen gedeeltelijk raster, en de whitespace-ronde loopt ook niet. Zet je voor niet-vertrouwde invoer een strakker budget van jezelf, vang dan de exception en beslis, in plaats van een leeg resultaat te lezen als "geen tabellen":

Options := TPdfTableExtractionOptions.Default;
Options.MaxRulingSegments := 2048;        // bewust strak voor niet-vertrouwde invoer
try
  Tables := Pdf.ExtractTables(Options);
except
  on E: EPdfError do
  begin
    Log(E.Message);                       // 'Table ruling-segment budget exceeded'
    Options.MaxRulingSegments := 16384;   // de standaardwaarde van 3.117.0
    Tables := Pdf.ExtractTables(Options);
  end;
end;

Gemeten resultaten en waar de aanpak ophoudt

Op dezelfde 13 voorbeelddocumenten ging de extractie van 43 tabellen, waarvan 9 gelijnd en 34 whitespace-fragmenten of valse positieven, naar 41 gelijnde tabellen zonder whitespace-valse-positieven. Een deel van die opruiming hoort bij twee begeleidende wijzigingen in 3.117.0: woorden die al door een gelijnd raster zijn opgeëist worden verwijderd voordat whitespace-detectie loopt, zodat een tabel nooit twee keer wordt gemeld, en een whitespace-kolomgrens moet nu een tekstvrije corridor zijn over elke rij die hij scheidt, en dat is wat uitgelijnde alinea's ervan weerhield als 5x4-tabellen te scoren. De gevulde-rechthoeklezer is wat de tabellen zelf van de fragmentkolom naar de gelijnde kolom verplaatste

De grenzen zijn het benoemen waard. Een pagina zonder tekstlaag levert nog steeds het rasterskelet op, met elke cel leeg, omdat randen uit geometrie komen en tekst uit de tekstpagina; gescande pagina's hebben eerst OCR nodig. Gevulde vormen met curven, afgeronde hoeken of niet-rechthoekige contouren worden volledig weggegooid, dus een tabel waarvan de randen als afgeronde-rechthoekcontouren zijn getekend heeft zoals voorheen whitespace-detectie nodig. Een tabel zonder randen en zonder arcering verandert door dit alles niet en blijft het domein van de whitespace-strategie die in het tabelextractie-artikel wordt beschreven; wanneer zelfs dat niet genoeg is, zijn de woordboxen en blokken uit gestructureerde tekst en leesorde het ruwe materiaal voor een domeinspecifieke lezer. De demo TableExtractionLab die met de component meekomt stelt DetectFilledRulings beschikbaar in zijn optiepaneel, wat de snelste manier is om te zien hoe een gegeven export er met en zonder uitziet; de volledige API staat beschreven op de PDFium Component for Delphi-pagina