Teknisk artikel

Fyllda rektanglar som tabellinjer i PDFium Delphi

PDFium Components tabellutvinning behandlar, från version 3.117.0, en tunn fylld rektangel som en tabellinje. Med DetectFilledRulings aktiverat, vilket är standard, blir en fylld axeljusterad box som inte är tjockare än MaxRulingThickness (3 punkter) en enda linje längs sin långa axel, en större fylld box bidrar med sina fyra kanter, och varje linjekoordinat snäpps inom RulingSnapTolerance (4 punkter) innan rutnätet sätts ihop. Tabeller exporterade från Word, Google Docs och webbläsare når därför den linjerade detektorn som kompletta rutnät i stället för att falla vidare till whitespace-detektering som fragment

Den tidigare artikeln om tabelldetektering och -utvinning slog fast att linjerad detektering använder de ritade linjerna och att varje sträckt bansegment transformeras till sidkoordinater. Den meningen var sann och ofullständig. Att räkna banobjekt över en uppsättning på 13 verkliga exempeldokument visade att 9 av dem inte innehåller någon sträckt bana alls, ändå bär var och en av deras sidor hundratals fyllda rektanglar 0,5 till 1 punkt tjocka. Den enbart sträckbaserade detektorn såg ingenting, varje sida föll till whitespace-detektering, och utdata blev en strössling av små fragment i stället för tabeller. Förvalet för kompakta kolumner som lades till i 3.116.4 mjukade upp det på fragmentnivå; grundorsaken var att detektorn läste fel målningsoperator

Varför har en Word-exporterad tabell inga sträckta linjer?

Ett ordbehandlingsprogram tänker inte på en ram som en linje; det tänker på den som en box med en bredd och målar boxen med en fyllning. ISO 32000-1 §8.5.2.1 definierar operatorn re som att den lägger till en rektangeldelbana, och §8.5.3 skiljer målningsoperatorerna åt: S sträcker banan med aktuell linjebredd, f fyller dess inre. En cellram på 0,5 punkt kommer ut som x y w 0.5 re f, och sträckmaskineriet, inklusive linjebredd, fogar och streckmönster, kör aldrig. Cellskuggning är samma konstruktion med en större box. Ett sträckt rutnät ritat med m, l och S är vad den ursprungliga detektorn förväntade sig, och det är vad nästan inget exporterat från ett kontorsprogram producerar:

% en cellram från en ordbehandlarexport: en fylld box 0,5 pt hög
72 700 468 0.5 re f
% cellskuggning: en fylld box i cellens storlek
72 676 117 24 re f
% den sträckta rutnätslinjen som den ursprungliga detektorn skrevs för
72 700 m 540 700 l S

För en detektor som bara frågar FPDFPath_GetDrawMode om sträckflaggan är satt är båda de fyllda boxarna osynliga. Orden inuti cellerna når då whitespace-detekteringen, där kolumner åtskilda av en springa på 6 punkter ligger under standardvärdet MinColumnGap på 12 punkter, och det som kommer tillbaka är vilken delmängd av rader som än råkar ligga tillräckligt linjerat för att passera MinRows. Det är fragmentbeteendet, och ingen mängd parametertrimning förvandlar det till det rutnät författaren ritade

Hur gör PDFium Component en fylld box till en tabellinje?

TableCollectObjectRulings inspekterar varje banobjekt en delbana i taget. Målningsläget kommer från FPDFPath_GetDrawMode; en bana räknas som fylld när DetectFilledRulings är på och fyllnadsläget inte är none. Varje punkt transformeras genom objektmatrisen och samlas in, upp till MaxSubpathPoints (8) per delbana, och varje kurvsegment märker delbanan som kurvig. När delbanan sluts eller en ny MoveTo börjar avgör FlushSubpath vad den var: en kurvig delbana kastas, och detsamma gäller varje sluten polygon vars punkter inte alla ligger inom PointTolerance (0,05 punkter) från avgränsningsboxens kanter på minst en axel. En triangel, en chevron eller en rundad flik blir aldrig en tabellinje, vilket är vad som håller dekorativ grafik borta från rutnätet

Diagram över hur TableCollectObjectRulings i PDFium Component gör slutna delbanor till tabellinjer i Delphi: FlushSubpath kastar kurviga konturer och polygoner som inte ligger på avgränsningsboxens kanter, MaxRulingThickness delar tunna boxar i en linje per lång axel, skuggade celler ger fyra kantlinjer och DetectFilledRulings håller små kvadrater borta
En sluten delbana överlever bara när den är axeljusterad, och avgränsningsboxen avgör sedan om den är en enda linje, fyra kanter på en skuggad cell, eller ingenting alls

Det som överlever är en axeljusterad rektangel, klassificerad efter sin avgränsningsbox. Bredd på eller under MaxRulingThickness med höjd över den ger en vertikal linje i den horisontella mitten, som spänner boxen från botten till topp; spegelvänt fall ger en horisontell linje. Båda dimensionerna över tröskeln betyder en skuggad cell, och boxen bidrar med fyra linjer, en per kant. Båda dimensionerna på eller under tröskeln bidrar med ingenting, så en fyrkantig punkt på 2 punkter misstas inte för en linje. En sträckt bana tar den äldre vägen genom AddLine, en linje per axeljusterat segment, så ett rutnät ritat med S hanteras precis som förut, och en bana målad med både fyllning och sträck ger överlappande bitar som sammanslagningspasset kollapsar:

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

    Options := TPdfTableExtractionOptions.Default;
    // detta är standardvärdena i 3.117.0, utskrivna för tydlighetens skull
    Options.DetectFilledRulings := True;     // tunna fyllda boxar blir tabellinjer
    Options.MaxRulingThickness := 3.0;       // punkter; tjockare boxar räknas som skuggning
    Options.RulingSnapTolerance := 4.0;      // punkter; 0 stänger av snäppningen
    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;

Vad gör RulingSnapTolerance för tabeller med skuggade celler?

RulingSnapTolerance är vad som får en tabell byggd enbart av skuggning att hänga samman till ett enda rutnät. Vissa exporter ritar ingen ram alls: varje cell är en fylld box i sin egen färg, och angränsande boxar skiljs av en vit springa på 1 till 3 punkter. Varje box ger fyra kantlinjer, men den ena cellens högerkant och nästa cells vänsterkant ligger 2 punkter isär, och konnektivitetsprovet använder RulingTolerance, som har standardvärdet 1 punkt. Utan snäppning bildar varje cell sin egen sammanhängande komponent av fyra linjer, ingen komponent når MinRows, och sidan rapporterar ingenting. TableSnapRulings samlar varje X-koordinat i spel (positionen för varje vertikal linje plus start och slut för varje horisontell) och varje Y-koordinat på samma sätt, sorterar varje lista, klustrar den genom att kedja värden vars granne skiljer sig med högst toleransen, ersätter varje kluster med sitt medelvärde och flyttar sedan varje position, start och slut till närmaste klustercentrum. Springans två sidor blir samma linje, och konnektiviteten håller

Diagram över hur RulingSnapTolerance i PDFium Component kopplar samman en tabell med skuggade celler i Delphi: angränsande celler lämnar en springa på 2 pt, deras kantlinjer ligger bortom RulingTolerance på 1 pt, och TableSnapRulings kedjar de två X-värdena till ett klustermedel så att konnektivitetsprovet till slut ser en delad rutnätslinje
Snäppningen kör före sammanslagningen och före den linjerade detektorn, så en vit springas två sidor blir en linje och varje cell slutar vara en ö av fyra linjer

Snäppningen kör före TableMergeRulings, som sorterar linjerna och fogar samman kollinejära bitar som rör vid eller överlappar varandra inom RulingTolerance, och båda kör innan TableDetectRuled alls ser datan, så det parvisa konnektivitetsprovet är proportionellt mot antalet rutnätslinjer i stället för mot antalet fragment per cell. På ett sträckt rutnät är passen ofarliga, eftersom koordinater som redan var identiska snäpps till sig själva. Det enda att ha i minnet är att klusterkedjning inte har någon egen breddgräns: en följd av koordinater med 3 punkters mellanrum kollapsar till ett enda centrum. Vid standardvärdet 4 punkter påverkar det bara kolumner smalare än ett tecken, men om ett dokument har verkliga springor på 3 punkter som måste förbli åtskilda, sänk toleransen eller sätt den till 0 för att stänga av snäppningen:

// Isolera den linjerade strategin och jämför vad varje inställning ser på en sida
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-export rapporterar typiskt 0, N och sedan färre än N:
// enbart sträck ser ingenting, snäppningen kopplar samman de skuggade cellerna,
// och att stänga av snäppningen lämnar varje skuggad cell som sin egen ö
Writeln(CountRuledTables(Pdf, False, 4.0));
Writeln(CountRuledTables(Pdf, True, 4.0));
Writeln(CountRuledTables(Pdf, True, 0.0));

Tabellinjer inuti form XObjects

Sidlayoutverktyg bäddar ofta in en tabell, eller hela sidkroppen, i en form XObject och målar den med Do. ISO 32000-1 §8.10.1 föreskriver att formmatrisen sammanfogas med den aktuella transformationsmatrisen när formen målas, så en rektangel inuti formen lever i formrymden och landar på sidan först efter två eller fler transformationer. TableCollectObjectRulings rekurserar in i formobjekt när IncludeFormXObjects är satt: den läser objektmatrisen, kombinerar den med föräldramatrisen genom TableMultiplyMatrix, vars argumentordning betyder "mappa genom den första matrisen, sedan den andra", och räknar upp barnen med FPDFFormObj_CountObjects och FPDFFormObj_GetObject och skickar den kombinerade matrisen vidare ned. Nästning djupare än MaxFormDepth (8) hoppas över tyst, vilket är ett skydd mot patologiska filer snarare än en gräns någon verklig export närmar sig. Anledningen till att multiplikationsordningen spelar roll är densamma som diskuteras i matriser före eller efter: att byta operander flyttar translationstermen, och en linje som borde landa högst upp på sidan landar i origo i stället

Diagram över tabellinjer inuti en form XObject i PDFium Component i Delphi: en tunn rektangel skriven som 72 700 468 0.5 re f lever i formrymden och landar på sidan först efter att TableMultiplyMatrix kombinerat föräldra-CTM:en med formmatrisen, rekurserande genom FPDFFormObj_CountObjects upp till MaxFormDepth
Rektangeln är skriven i formrymden och når sidans topp först efter att matriserna multiplicerats i en ordning som behåller translationstermen där den hör hemma

Varför fyrdubblades linjebudgeten?

Standardvärdet MaxRulingSegments höjdes från 4096 till 16384 i 3.117.0 eftersom ramar per cell anländer i långt större antal än sträckta rutnätslinjer. En sträckt tabell med 30 rader och 6 kolumner är 38 linjesegment. Samma tabell exporterad som fyllda boxar är upp till fyra ramar per cell, 720 bitar före sammanslagning, och ett formulär med skuggade celler fördubblar det. Två sådana tabeller på en sida skulle ha sprängt den gamla budgeten. Budgeten upprätthålls i TableAppendRuling genom Check, som kastar EPdfError med meddelandet "Table ruling-segment budget exceeded"; det finns inget försämrat resultat, inget partiellt rutnät, och whitespace-passet kör inte heller. Om du sätter en stramare egen budget för otillförlitlig indata, fånga undantaget och fatta ett beslut, i stället för att läsa ett tomt resultat som "inga tabeller":

Options := TPdfTableExtractionOptions.Default;
Options.MaxRulingSegments := 2048;        // avsiktligt stram för otillförlitlig indata
try
  Tables := Pdf.ExtractTables(Options);
except
  on E: EPdfError do
  begin
    Log(E.Message);                       // 'Table ruling-segment budget exceeded'
    Options.MaxRulingSegments := 16384;   // standardvärdet i 3.117.0
    Tables := Pdf.ExtractTables(Options);
  end;
end;

Uppmätta resultat och var metoden tar slut

På samma 13 exempeldokument gick utvinningen från 43 tabeller, varav 9 linjerade och 34 whitespace-fragment eller falska positiva, till 41 linjerade tabeller och inga falska positiva från whitespace. En del av den uppstädningen tillhör två följdändringar i 3.117.0: ord som redan tagits i anspråk av ett linjerat rutnät tas bort innan whitespace-detekteringen kör, så en tabell aldrig rapporteras två gånger, och en whitespace-kolumngräns måste nu vara en textfri korridor över varje rad den skiljer åt, vilket är vad som stoppade marginaljusterade stycken från att poängsättas som 5x4-tabeller. Läsaren för fyllda rektanglar är vad som flyttade själva tabellerna från fragmentkolumnen till den linjerade kolumnen

Gränserna är värda att säga rakt ut. En sida utan textlager ger fortfarande rutnätsskelettet, varje cell tom, eftersom linjer kommer från geometrin och text från textsidan; skannade sidor behöver OCR först. Fyllda former med kurvor, rundade hörn eller icke-rektangulära konturer kastas helt, så en tabell vars ramar är ritade som rundade rektangelkonturer behöver whitespace-detektering som förut. En tabell utan vare sig ramar eller skuggning påverkas inte av något av detta och förblir den whitespace-strategins domän som beskrivs i tabellutvinningsartikeln; när inte ens det räcker är ordfyrkanterna och blocken från strukturerad text och läsordning råmaterialet för en domänspecifik läsare. Demot TableExtractionLab som följer med komponenten exponerar DetectFilledRulings i sin inställningspanel, vilket är det snabbaste sättet att se hur en given export ser ut med och utan den; hela API:et beskrivs på sidan för PDFium Component for Delphi