Teknisk artikel

Udfyldte rektangel-tabelkanter som rulings i PDFium Delphi

PDFium Component tabel-ekstraktion behandler fra version 3.117.0 et tyndt udfyldt rektangel som en tabel-ruling. Med DetectFilledRulings slået til, hvilket er default, bliver en udfyldt akse-parallel boks ikke tykkere end MaxRulingThickness (3 point) til én ruling langs sin lange akse, en større udfyldt boks bidrager med sine fire kanter, og hver ruling-koordinat snaps inden for RulingSnapTolerance (4 point), før gitteret samles. Tabeller eksporteret fra Word, Google Docs og browsere når derfor den linjerede detektor som komplette gitre i stedet for at falde igennem til whitespace-detektering som fragmenter

Den tidligere artikel om tabeldetektering og -ekstraktion udtalte, at linjeret detektering bruger de tegnede linjer, og at hvert stroket path-segment transformeres til sidekoordinater. Den sætning var sand og ufuldstændig. Optælling af path-objekter hen over et sæt af 13 virkelige eksempeldokumenter viste, at 9 af dem slet ikke indeholder noget stroket path, og alligevel bærer hver af deres sider hundreder af udfyldte rektangler på 0,5 til 1 point i tykkelse. Stroke-only-detektoren så ingenting, hver side faldt til whitespace-detektering, og outputtet var en spredt hob af små fragmenter frem for tabeller. Compact-columns-præsettet tilføjet i 3.116.4 blødgjorde det på fragment-niveau; rodårsagen var, at detektoren læste den forkerte malingsoperator

Hvorfor har en Word-eksporteret tabel ingen strokede linjer?

Et tekstbehandlingsprogram tænker ikke på en kant som en linje; det tænker den som en boks med en bredde og maler den boks med en fill. ISO 32000-1 §8.5.2.1 definerer re-operatoren som at tilføje en rektangel-subpath, og §8.5.3 adskiller malingsoperatorerne: S stroker pathen med den aktuelle linjebredde, f udfylder dens indre. En 0,5-point cellekant kommer ud som x y w 0.5 re f, og stroke-maskineriet — linjebredde, joins og dash-mønster inkluderet — kører aldrig. Celle-shading er samme konstruktion med en større boks. Et stroket gitter tegnet med m, l og S er, hvad den oprindelige detektor forventede, og det er, hvad næsten intet, der eksporteres fra en office-applikation, producerer:

% Én cellekant fra en tekstbehandler-eksport: En 0,5 pt høj udfyldt boks
72 700 468 0.5 re f
% Celle-shading: En udfyldt boks i cellens størrelse
72 676 117 24 re f
% Den strokede gitterlinje, den oprindelige detektor blev skrevet til
72 700 m 540 700 l S

Til en detektor, der spørger FPDFPath_GetDrawMode kun om, hvorvidt stroke-flaget er sat, er begge udfyldte bokse usynlige. Ordene inde i cellerne når derefter whitespace-detektering, hvor kolonner adskilt af en 6-point gutter ligger under default-MinColumnGap på 12 point, og det, der kommer tilbage, er den delmængde af rækker, der tilfældigvis aligner godt nok til at bestå MinRows. Det er fragment-adfærden, og ingen mængde parameterjustering forvandler den til det gitter, forfatteren tegnede

Hvordan forvandler PDFium Component en udfyldt boks til en ruling?

TableCollectObjectRulings inspicerer hvert path-objekt én subpath ad gangen. Draw mode kommer fra FPDFPath_GetDrawMode; et path tæller som udfyldt, når DetectFilledRulings er slået til, og fill mode ikke er none. Hvert punkt transformeres gennem objekt-matrixen og samles op, op til MaxSubpathPoints (8) pr. subpath, og ethvert kurvesegment markerer subpathen som krummet. Når subpathen lukker, eller en ny MoveTo begynder, afgør FlushSubpath, hvad den var: En krummet subpath kasseres, og det gør enhver lukket polygon også, hvis punkter ikke alle sidder inden for PointTolerance (0,05 point) af bounding box-kanterne på mindst én akse. En trekant, en chevron eller en afrundet fane bliver aldrig til en ruling, hvilket er det, der holder dekorativt artwork ude af gitteret

PDFium Component-diagram over, hvordan TableCollectObjectRulings forvandler lukkede subpaths til tabel-rulings i Delphi: FlushSubpath kasserer krumme konturer og polygoner væk fra bounding box-kanterne, MaxRulingThickness splitter tynde bokse til én ruling pr. lang akse, shadede celler giver fire kant-rulings, og DetectFilledRulings holder småkvadrater ude
En lukket subpath overlever kun, når den er akse-parallel, og bounding box'en afgør derefter, om den er én ruling, fire kanter af en shaded celle eller slet ingenting

Hvad der overlever, er et akse-parallelt rektangel, klassificeret efter sin bounding box. Bredde på eller under MaxRulingThickness med højde over giver én vertikal ruling i den vandrette midte, der spænder over boksen fra bund til top; spejlfaldet giver én horisontal ruling. Begge dimensioner over tærsklen betyder en shaded celle, og boksen bidrager med fire rulings, én pr. kant. Begge dimensioner på eller under tærsklen bidrager med ingenting, så en 2-point kvadratisk bullet ikke tages for en linje. Et stroket path tager den ældre vej gennem AddLine, én ruling pr. akse-parallelt segment, så et gitter tegnet med S håndteres præcis som før, og et path malet med både fill og stroke producerer overlappende stykker, som merge-passet kollapser:

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

    Options := TPdfTableExtractionOptions.Default;
    // Dette er 3.117.0-defaults, stavet ud for tydelighedens skyld
    Options.DetectFilledRulings := True;     // tynde udfyldte bokse bliver rulings
    Options.MaxRulingThickness := 3.0;       // point; tykkere bokse tæller som shading
    Options.RulingSnapTolerance := 4.0;      // point; 0 deaktiverer snapping
    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;

Hvad gør RulingSnapTolerance for tabeller med shadede celler?

RulingSnapTolerance er det, der får en tabel bygget alene af shading til at forbinde sig til ét gitter. Nogle eksporter tegner slet ingen kant: Hver celle er en udfyldt boks i sin egen farve, og nabo-bokse er adskilt af en 1 til 3 point bred gutter af hvidt. Hver boks giver fire kant-rulings, men den højre kant af én celle og den venstre kant af den næste sidder 2 point fra hinanden, og connectivity-testen bruger RulingTolerance, som default er 1 point. Uden snapping danner hver celle sin egen forbundne komponent af fire rulings, ingen komponent når MinRows, og siden rapporterer ingenting. TableSnapRulings samler hver X-koordinat i spil (positionen af hver vertikal ruling plus start og slut af hver horisontal) og hver Y-koordinat ligeledes, sorterer hver liste, klynger den ved at kæde værdier, hvis nabo afviger med højst tolerancen, erstatter hver klynge med sit gennemsnit og flytter derefter hver position, start og slut til nærmeste klyngecenter. Gutterens to sider bliver samme linje, og connectivity holder

PDFium Component-diagram over RulingSnapTolerance, der forbinder en tabel med shadede celler i Delphi: Nabo-celler efterlader en 2 pt gutter, deres kant-rulings ligger ud over den 1 pt RulingTolerance, og TableSnapRulings kæder de to X-værdier til ét klyngemiddel, så connectivity-testen endelig ser en delt gitterlinje
Snapping kører før merging og før den linjerede detektor, så en hvid gutters to sider bliver én linje, og hver celle holder op med at være en ø af fire rulings

Snapping kører før TableMergeRulings, som sorterer rulings og forener kollineære stykker, der rører eller overlapper inden for RulingTolerance, og begge kører, før TableDetectRuled nogensinde ser dataene, så det parvise connectivity-tjek er proportionalt med antallet af gitterlinjer frem for antallet af per-celle-fragmenter. På et stroket gitter er passene harmløse, for koordinater, der allerede var identiske, snaps til sig selv. Den ene ting, man skal huske, er, at klynge-kædning ikke har nogen breddegrænse af sig selv: En række af koordinater hver 3 point fra hinanden kollapser til ét center. Ved 4-point-defaulten påvirker det kun kolonner smallere end et tegn, men hvis et dokument har rigtige 3-point gutters, der skal forblive adskilt, så sænk tolerancen eller sæt den til 0 for at slå snapping fra:

// Isolér den linjerede strategi og sammenlign, hvad hver indstilling ser på én side
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;

// En Word-eksport rapporterer typisk 0, N og derefter færre end N:
// stroke-only ser ingenting, snapping forbinder de shadede celler,
// og at deaktivere snap efterlader hver shaded celle som sin egen ø
Writeln(CountRuledTables(Pdf, False, 4.0));
Writeln(CountRuledTables(Pdf, True, 4.0));
Writeln(CountRuledTables(Pdf, True, 0.0));

Rulings inde i form XObjects

Side-layout-værktøjer pakker ofte en tabel eller hele sidekroppen ind i en form XObject og maler den med Do. ISO 32000-1 §8.10.1 specificerer, at form-matrixen konkateneres med den aktuelle transformationsmatrix, når formen males, så et rektangel inde i formen lever i form-rum og lander først på siden efter to eller flere transformationer. TableCollectObjectRulings rekurrerer ind i form-objekter, når IncludeFormXObjects er sat: Den læser objekt-matrixen, kombinerer den med forældrematrixen gennem TableMultiplyMatrix, hvis argumentrækkefølge betyder "afbild gennem første matrix, så den anden", og enumererer børnene med FPDFFormObj_CountObjects og FPDFFormObj_GetObject og giver den kombinerede matrix videre nedad. Nesting dybere end MaxFormDepth (8) springes lydløst over, hvilket er et værn mod patologiske filer frem for en grænse, nogen rigtig eksport nærmer sig. Grunden til, at multiplikationsordenen betyder noget, er den samme som diskuteret i matrix prepend versus append: At bytte om på operanderne flytter translation-leddet, og en ruling, der skulle lande i toppen af siden, lander i origo i stedet

PDFium Component-diagram over rulings inde i en form XObject i Delphi: Et tyndt rektangel, skrevet som 72 700 468 0.5 re f, lever i form-rum og lander først på siden, efter at TableMultiplyMatrix har kombineret forældre-CTM'en med form-matrixen, rekursivt gennem FPDFFormObj_CountObjects op til MaxFormDepth
Rektanglet er skrevet i form-rum og når toppen af siden først, efter at matrixerne er multipliceret i en orden, der holder translation-leddet, hvor det hører hjemme

Hvorfor firedobledes ruling-budgettet?

Default-MaxRulingSegments steg fra 4096 til 16384 i 3.117.0, fordi per-celle-kanter ankommer i langt større antal end strokede gitterlinjer. Et stroket 30-rækkers, 6-kolonners tabel er 38 linjesegmenter. Samme tabel eksporteret som udfyldte bokse er op til fire kanter pr. celle, 720 stykker før merging, og en form med shadede celler fordobler det. To sådanne tabeller på en side ville have udtømt det gamle budget. Budgettet håndhæves i TableAppendRuling gennem Check, som raiser EPdfError med beskeden "Table ruling-segment budget exceeded"; der findes intet degraderet resultat, intet delvist gitter, og whitespace-passet kører heller ikke. Sætter du et strammere budget af egen hånd for utroværdigt input, så fang exceptionen og afgør selv frem for at læse et tomt resultat som "ingen tabeller":

Options := TPdfTableExtractionOptions.Default;
Options.MaxRulingSegments := 2048;        // bevidst stram til utroværdigt input
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-defaulten
    Tables := Pdf.ExtractTables(Options);
  end;
end;

Målte resultater, og hvor tilgangen stopper

På de samme 13 eksempeldokumenter gik ekstraktionen fra 43 tabeller, 9 af dem linjerede og 34 whitespace-fragmenter eller falske positiver, til 41 linjerede tabeller og ingen whitespace-falske positiver. En del af den oprydning tilhører to ledsagende ændringer i 3.117.0: Ord, der allerede er claimet af et linjeret gitter, fjernes, før whitespace-detektering kører, så en tabel aldrig rapporteres to gange, og en whitespace-kolonnegrænse skal nu være en tekstfri korridor hen over hver række, den adskiller, hvilket er det, der stoppede justerede afsnit i at score som 5x4-tabeller. Udfyldt-rektangel-læseren er det, der flyttede tabellerne selv fra fragment-kolonnen til den linjerede kolonne

Grænserne er det værd at sige klart. En side uden tekstlag giver stadig gitterskelettet, hver celle tom, for rulings kommer fra geometrien, og tekst kommer fra tekst-siden; scannede sider behøver OCR først. Udfyldte former med kurver, afrundede hjørner eller ikke-rektangulære konturer kasseres fuldstændigt, så en tabel, hvis kanter er tegnet som afrundet-rektangel-konturer, behøver whitespace-detektering som før. En tabel uden hverken kanter eller shading er uændret af alt dette og forbliver whitespace-strategiens provins, som beskrevet i artiklen om tabel-ekstraktion; når selv det ikke er nok, er ord-boksene og blokkene fra struktureret tekst og læseretning råmaterialet til en domænespecifik læser. TableExtractionLab-demoen, der ships med komponenten, eksponerer DetectFilledRulings i sit options-panel, hvilket er den hurtigste måde at se, hvordan en given eksport ser ud med og uden den; den fulde API er beskrevet på siden PDFium Component til Delphi