Articol tehnic

Dreptunghiuri umplute ca linii de grilă de tabel în Delphi

Extragerea de tabele din PDFium Component, începând cu versiunea 3.117.0, tratează un dreptunghi subțire umplut ca o linie de grilă de tabel. Cu DetectFilledRulings activat, ceea ce este implicit, o casetă umplută aliniată pe axe și nu mai groasă de MaxRulingThickness (3 puncte) devine o singură linie de grilă de-a lungul axei ei lungi, o casetă umplută mai mare contribuie cu cele patru muchii ale ei, iar fiecare coordonată de linie este apropiată în limita RulingSnapTolerance (4 puncte) înainte de asamblarea grilei. Tabelele exportate din Word, Google Docs și browsere ajung astfel la detectorul de linii ca grile complete, în loc să cadă la detectarea pe spații albe ca fragmente

Articolul anterior despre detectarea și extragerea tabelelor afirma că detectarea pe linii folosește liniile desenate și că fiecare segment de traseu conturat este transformat în coordonate de pagină. Propoziția era adevărată și incompletă. Numărând obiectele de traseu pe un set de 13 documente eșantion reale s-a văzut că 9 dintre ele nu conțin niciun traseu conturat, dar fiecare dintre paginile lor poartă sute de dreptunghiuri umplute de 0.5 până la 1 punct grosime. Detectorul care se uita doar la contur nu vedea nimic, fiecare pagină cădea la detectarea pe spații albe, iar rezultatul era o risipă de fragmente mici, nu tabele. Presetul de coloane compacte adăugat în 3.116.4 a mai înmuiat asta la nivel de fragment; cauza rădăcină era că detectorul citea operatorul de desenare greșit

De ce nu are un tabel exportat din Word linii conturate?

Un procesor de text nu se gândește la o bordură ca la o linie; se gândește la ea ca la o casetă cu o lățime și pictează acea casetă cu o umplere. ISO 32000-1 §8.5.2.1 definește operatorul re ca adăugând un subtraseu dreptunghiular, iar §8.5.3 separă operatorii de desenare: S conturează traseul cu lățimea de linie curentă, f îi umple interiorul. O bordură de celulă de 0.5 puncte iese ca x y w 0.5 re f, iar mecanismul de conturare, cu lățimea liniei, îmbinările și modelul de linie întreruptă cu tot, nu rulează niciodată. Umplerea de celulă este aceeași construcție cu o casetă mai mare. O grilă conturată desenată cu m, l și S este ce aștepta detectorul original și este ce nu produce aproape nimic exportat dintr-o aplicație de birou:

% o bordură de celulă dintr-un export de procesor de text: o casetă umplută de 0.5 pt înălțime
72 700 468 0.5 re f
% umplerea celulei: o casetă umplută de dimensiunea celulei
72 676 117 24 re f
% linia de grilă conturată pentru care a fost scris detectorul original
72 700 m 540 700 l S

Pentru un detector care întreabă FPDFPath_GetDrawMode doar dacă flag-ul de conturare este setat, ambele casete umplute sunt invizibile. Cuvintele din interiorul celulelor ajung apoi la detectarea pe spații albe, unde coloanele separate de un culoar de 6 puncte stau sub MinColumnGap implicit de 12 puncte, iar ce se întoarce este acel subset de rânduri care se nimerește să fie aliniat destul ca să treacă de MinRows. Acesta este comportamentul de fragment, iar nicio reglare de parametri nu îl transformă în grila pe care a desenat-o autorul

Cum transformă PDFium Component o casetă umplută într-o linie de grilă?

TableCollectObjectRulings inspectează fiecare obiect de traseu, câte un subtraseu o dată. Modul de desenare vine de la FPDFPath_GetDrawMode; un traseu contează ca umplut când DetectFilledRulings este pornit și modul de umplere nu este none. Fiecare punct este transformat prin matricea obiectului și colectat, până la MaxSubpathPoints (8) per subtraseu, iar orice segment de curbă marchează subtraseul ca fiind curbat. Când subtraseul se închide sau începe un nou MoveTo, FlushSubpath decide ce a fost: un subtraseu curbat este aruncat, la fel și orice poligon închis ale cărui puncte nu stau toate în limita PointTolerance (0.05 puncte) față de muchiile casetei de încadrare pe cel puțin o axă. Un triunghi, un chevron sau o clapetă rotunjită nu devin niciodată linie de grilă, ceea ce ține arta decorativă în afara grilei

Diagramă PDFium Component despre felul în care TableCollectObjectRulings transformă subtraseele închise în linii de grilă de tabel în Delphi: FlushSubpath aruncă contururile curbate și poligoanele care nu stau pe muchiile casetei de încadrare, MaxRulingThickness împarte casetele subțiri într-o linie de grilă per axă lungă, celulele umbrite dau patru linii de muchie, iar DetectFilledRulings ține pătrățelele minuscule în afara grilei
Un subtraseu închis supraviețuiește doar dacă este aliniat pe axe, iar caseta de încadrare decide apoi dacă este o singură linie de grilă, patru muchii ale unei celule umbrite sau nimic

Ce supraviețuiește este un dreptunghi aliniat pe axe, clasificat după caseta lui de încadrare. Lățimea la sau sub MaxRulingThickness cu înălțimea peste acest prag dă o linie verticală în centrul orizontal, care acoperă caseta de jos în sus; cazul oglindit dă o linie orizontală. Ambele dimensiuni peste prag înseamnă o celulă umbrită, iar caseta contribuie cu patru linii, una per muchie. Ambele dimensiuni la sau sub prag nu contribuie cu nimic, așa că un punct de listă pătrat de 2 puncte nu este confundat cu o linie. Un traseu conturat ia calea mai veche prin AddLine, o linie per segment aliniat pe axe, așa că o grilă desenată cu S este tratată exact ca înainte, iar un traseu desenat și cu umplere, și cu contur produce piese suprapuse pe care trecerea de îmbinare le colapsează:

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;                     // indexat de la 1

    Options := TPdfTableExtractionOptions.Default;
    // acestea sunt valorile implicite din 3.117.0, scrise explicit pentru claritate
    Options.DetectFilledRulings := True;     // casetele subțiri umplute devin linii de grilă
    Options.MaxRulingThickness := 3.0;       // puncte; casetele mai groase contează ca umbrire
    Options.RulingSnapTolerance := 4.0;      // puncte; 0 dezactivează apropierea
    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;

Ce face RulingSnapTolerance pentru tabelele cu celule umbrite?

RulingSnapTolerance este ce face ca un tabel construit doar din umbrire să se lege într-o singură grilă. Unele exporturi nu desenează nicio bordură: fiecare celulă este o casetă umplută în propria culoare, iar casetele vecine sunt separate de un culoar alb de 1 până la 3 puncte. Fiecare casetă dă patru linii de muchie, dar muchia din dreapta a unei celule și cea din stânga a celei următoare stau la 2 puncte distanță, iar testul de conexitate folosește RulingTolerance, implicit 1 punct. Fără apropiere, fiecare celulă formează propria componentă conexă din patru linii, nicio componentă nu ajunge la MinRows, iar pagina nu raportează nimic. TableSnapRulings adună fiecare coordonată X în joc (poziția fiecărei linii verticale plus începutul și sfârșitul fiecăreia orizontale) și fiecare coordonată Y la fel, sortează fiecare listă, o grupează înlănțuind valorile al căror vecin diferă cu cel mult toleranța, înlocuiește fiecare grup cu media lui și apoi mută fiecare poziție, început și sfârșit la centrul de grup cel mai apropiat. Cele două laturi ale unui culoar devin aceeași linie, iar conexitatea se susține

Diagramă PDFium Component despre felul în care RulingSnapTolerance conectează un tabel cu celule umbrite în Delphi: celulele vecine lasă un culoar de 2 pt, liniile lor de muchie stau dincolo de RulingTolerance de 1 pt, iar TableSnapRulings înlănțuie cele două valori X într-o singură medie de grup, așa că testul de conexitate vede în sfârșit o linie de grilă comună
Apropierea rulează înainte de îmbinare și înaintea detectorului de linii, așa că cele două laturi ale unui culoar alb devin o singură linie, iar fiecare celulă nu mai este o insulă de patru linii

Apropierea rulează înainte de TableMergeRulings, care sortează liniile și unește piesele coliniare care se ating sau se suprapun în limita RulingTolerance, iar ambele rulează înainte ca TableDetectRuled să vadă vreodată datele, așa că verificarea de conexitate în perechi este proporțională cu numărul de linii de grilă, nu cu numărul de fragmente per celulă. Pe o grilă conturată trecerile sunt inofensive, pentru că coordonatele care erau deja identice se apropie de ele însele. Singurul lucru de reținut este că gruparea în lanț nu are proprie limită de lățime: un șir de coordonate aflate la 3 puncte una de alta se colapsează într-un singur centru. La valoarea implicită de 4 puncte asta afectează doar coloanele mai înguste decât un caracter, dar dacă un document are culoare reale de 3 puncte care trebuie să rămână separate, coborâți toleranța sau setați-o la 0 pentru a opri apropierea:

// Izolează strategia pe linii și compară ce vede fiecare setare pe o pagină
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;

// Un export din Word raportează de obicei 0, N și apoi mai puțin de N:
// doar conturul nu vede nimic, apropierea conectează celulele umbrite,
// iar oprirea apropierii lasă fiecare celulă umbrită ca insulă separată
Writeln(CountRuledTables(Pdf, False, 4.0));
Writeln(CountRuledTables(Pdf, True, 4.0));
Writeln(CountRuledTables(Pdf, True, 0.0));

Linii de grilă în interiorul form XObjects

Uneltele de așezare în pagină înfășoară des un tabel, sau tot corpul paginii, într-un form XObject și îl pictează cu Do. ISO 32000-1 §8.10.1 specifică faptul că matricea formularului este concatenată cu matricea de transformare curentă atunci când formularul este pictat, așa că un dreptunghi din interiorul formularului trăiește în spațiul formularului și ajunge pe pagină abia după două sau mai multe transformări. TableCollectObjectRulings coboară recursiv în obiectele de formular când IncludeFormXObjects este setat: citește matricea obiectului, o combină cu matricea părinte prin TableMultiplyMatrix, a cărui ordine a argumentelor înseamnă „mappează prin prima matrice, apoi prin a doua”, și enumeră copiii cu FPDFFormObj_CountObjects și FPDFFormObj_GetObject, transmitând mai departe matricea combinată. O imbricare mai adâncă de MaxFormDepth (8) este sărită în tăcere, ceea ce este o pază împotriva fișierelor patologice, nu o limită de care se apropie vreun export real. Motivul pentru care ordinea înmulțirii contează este același discutat în adăugarea la început față de adăugarea la final a matricelor: inversarea operanzilor mută termenul de translație, iar o linie care ar trebui să ajungă în partea de sus a paginii ajunge la origine

Diagramă PDFium Component despre liniile de grilă din interiorul unui form XObject în Delphi: un dreptunghi subțire scris ca 72 700 468 0.5 re f trăiește în spațiul formularului și ajunge pe pagină abia după ce TableMultiplyMatrix combină CTM-ul părinte cu matricea formularului, coborând recursiv prin FPDFFormObj_CountObjects până la MaxFormDepth
Dreptunghiul este scris în spațiul formularului și ajunge în partea de sus a paginii doar după ce matricele sunt înmulțite într-o ordine care păstrează termenul de translație acolo unde îi este locul

De ce s-a împătrătit bugetul de linii?

MaxRulingSegments implicit a crescut de la 4096 la 16384 în 3.117.0 pentru că bordurile per celulă sosesc într-un număr mult mai mare decât liniile de grilă conturate. Un tabel conturat de 30 de rânduri și 6 coloane înseamnă 38 de segmente de linie. Același tabel exportat ca dreptunghiuri umplute înseamnă până la patru borduri per celulă, 720 de piese înainte de îmbinare, iar un formular cu celule umbrite dublează asta. Două astfel de tabele pe o pagină ar fi epuizat vechiul buget. Bugetul este impus în TableAppendRuling prin Check, care ridică EPdfError cu mesajul „Table ruling-segment budget exceeded”; nu există rezultat degradat, nicio grilă parțială, iar trecerea pe spații albe nu rulează nici ea. Dacă vă setați un buget mai strâns pentru intrare neîncrezută, prindeți excepția și decideți, în loc să citiți un rezultat gol drept „nu există tabele”:

Options := TPdfTableExtractionOptions.Default;
Options.MaxRulingSegments := 2048;        // intenționat strâns pentru intrare neîncrezută
try
  Tables := Pdf.ExtractTables(Options);
except
  on E: EPdfError do
  begin
    Log(E.Message);                       // 'Table ruling-segment budget exceeded'
    Options.MaxRulingSegments := 16384;   // valoarea implicită din 3.117.0
    Tables := Pdf.ExtractTables(Options);
  end;
end;

Rezultate măsurate și unde se oprește abordarea

Pe aceleași 13 documente eșantion, extragerea a trecut de la 43 de tabele, 9 dintre ele pe linii și 34 de fragmente pe spații albe sau fals pozitive, la 41 de tabele pe linii și niciun fals pozitiv pe spații albe. O parte din curățenie se datorează a două modificări însoțitoare din 3.117.0: cuvintele deja revendicate de o grilă cu linii sunt eliminate înainte ca detectarea pe spații albe să ruleze, așa că un tabel nu este raportat niciodată de două ori, iar o limită de coloană pe spații albe trebuie să fie acum un culoar fără text pe fiecare rând pe care îl separă, ceea ce a oprit paragrafele aliniate să fie notate ca tabele 5x4. Cititorul de dreptunghiuri umplute este cel care a mutat tabelele însele din coloana de fragmente în cea de tabele pe linii

Limitele merită spuse răspicat. O pagină fără strat de text dă în continuare scheletul grilei, cu fiecare celulă goală, pentru că liniile vin din geometrie, iar textul vine din pagina de text; paginile scanate au nevoie mai întâi de OCR. Formele umplute cu curbe, colțuri rotunjite sau contururi care nu sunt dreptunghiulare sunt aruncate complet, așa că un tabel ale cărui borduri sunt desenate ca contururi de dreptunghi rotunjit are nevoie de detectarea pe spații albe ca înainte. Un tabel care nu are nici borduri, nici umbrire nu este schimbat de nimic din toate acestea și rămâne în domeniul strategiei pe spații albe descrise în articolul despre extragerea tabelelor; când nici asta nu este suficient, casetele de cuvinte și blocurile din textul structurat și ordinea de citire sunt materia primă pentru un cititor specific domeniului. Demo-ul TableExtractionLab care vine cu componenta expune DetectFilledRulings în panoul lui de opțiuni, ceea ce este cel mai rapid mod de a vedea cum arată un anumit export cu și fără el; API-ul complet este descris pe pagina PDFium Component pentru Delphi