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