L'estrazione delle tabelle di PDFium Component, dalla versione 3.117.0, tratta un rettangolo pieno sottile come un righello di tabella. Con DetectFilledRulings attivo, che è il default, un box pieno allineato agli assi non più spesso di MaxRulingThickness (3 punti) diventa un righello lungo il suo asse maggiore, un box pieno più grande contribuisce con i suoi quattro bordi, e ogni coordinata di righello viene agganciata entro RulingSnapTolerance (4 punti) prima che la griglia venga assemblata. Le tabelle esportate da Word, Google Docs e browser arrivano quindi al rilevatore di griglie come griglie complete invece di cadere nel rilevamento su spaziature come frammenti
L'articolo precedente su rilevamento ed estrazione delle tabelle affermava che il rilevamento con griglia usa le linee disegnate e che ogni segmento di percorso tracciato viene trasformato in coordinate di pagina. Quella frase era vera e incompleta. Contare gli oggetti path su un insieme di 13 documenti campione reali ha mostrato che 9 di essi non contengono alcun percorso tracciato, eppure ognuna delle loro pagine porta centinaia di rettangoli pieni spessi da 0.5 a 1 punto. Il rilevatore di soli tratti non vedeva nulla, ogni pagina cadeva nel rilevamento su spaziature, e l'output era una dispersione di piccoli frammenti invece di tabelle. Il preset compact-columns aggiunto nella 3.116.4 ammorbidiva la cosa a livello di frammento; la causa radice era che il rilevatore leggeva l'operatore di pittura sbagliato
Perché una tabella esportata da Word non ha linee tracciate?
Un word processor non pensa a un bordo come a una linea; lo pensa come a un box con una larghezza, e dipinge quel box con un riempimento. ISO 32000-1 §8.5.2.1 definisce l'operatore re come l'aggiunta di un sottopercorso rettangolare, e §8.5.3 separa gli operatori di pittura: S traccia il percorso con la larghezza di linea corrente, f ne riempie l'interno. Un bordo di cella da 0.5 punti esce come x y w 0.5 re f, e la macchina dei tratti, larghezza di linea, giunzioni e pattern di tratteggio compresi, non entra mai in funzione. L'ombreggiatura delle celle è la stessa costruzione con un box più grande. Una griglia tracciata con m, l e S è quello che il rilevatore originale si aspettava, ed è quello che quasi nulla di esportato da un'applicazione office produce:
% un bordo di cella da un export di word processor: un box pieno alto 0.5 pt
72 700 468 0.5 re f
% ombreggiatura di cella: un box pieno grande quanto la cella
72 676 117 24 re f
% la linea di griglia tracciata per cui il rilevatore originale era scritto
72 700 m 540 700 l S
Per un rilevatore che chiede a FPDFPath_GetDrawMode solo se il flag di tratto è impostato, entrambi i box pieni sono invisibili. Le parole dentro le celle arrivano allora al rilevamento su spaziature, dove colonne separate da un gutter di 6 punti stanno sotto il MinColumnGap di default di 12 punti, e quello che torna indietro è il sottoinsieme di righe che per caso si allinea abbastanza bene da passare MinRows. È il comportamento a frammenti, e nessuna taratura dei parametri lo trasforma nella griglia che l'autore ha disegnato
Come fa PDFium Component a trasformare un box pieno in un righello?
TableCollectObjectRulings ispeziona ogni oggetto path un sottopercorso alla volta. Il draw mode arriva da FPDFPath_GetDrawMode; un percorso conta come riempito quando DetectFilledRulings è attivo e la modalità di riempimento non è none. Ogni punto viene trasformato tramite la matrice dell'oggetto e raccolto, fino a MaxSubpathPoints (8) per sottopercorso, e qualsiasi segmento di curva marca il sottopercorso come curvo. Quando il sottopercorso si chiude o inizia un nuovo MoveTo, FlushSubpath decide che cosa era: un sottopercorso curvo viene scartato, e così pure qualsiasi poligono chiuso i cui punti non stiano tutti entro PointTolerance (0.05 punti) dai bordi del bounding box su almeno un asse. Un triangolo, una punta di freccia o una linguetta arrotondata non diventano mai un righello, ed è questo che tiene la grafica decorativa fuori dalla griglia
Ciò che sopravvive è un rettangolo allineato agli assi, classificato dal suo bounding box. Una larghezza pari o sotto MaxRulingThickness con un'altezza sopra la soglia produce un righello verticale al centro orizzontale, che copre il box dal basso in alto; il caso speculare produce un righello orizzontale. Entrambe le dimensioni sopra la soglia significano cella ombreggiata, e il box contribuisce con quattro righelli, uno per bordo. Entrambe le dimensioni pari o sotto la soglia non contribuiscono nulla, quindi un punto elenco quadrato da 2 punti non viene scambiato per una linea. Un percorso tracciato prende la vecchia strada attraverso AddLine, un righello per ogni segmento allineato agli assi, quindi una griglia disegnata con S viene gestita esattamente come prima, e un percorso dipinto sia con riempimento sia con tratto produce pezzi sovrapposti che il passaggio di merge fa collassare:
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; // base 1
Options := TPdfTableExtractionOptions.Default;
// questi sono i default della 3.117.0, esplicitati per chiarezza
Options.DetectFilledRulings := True; // i box pieni sottili diventano righelli
Options.MaxRulingThickness := 3.0; // punti; i box più spessi contano come ombreggiatura
Options.RulingSnapTolerance := 4.0; // punti; 0 disattiva lo 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;
Che cosa fa RulingSnapTolerance per le tabelle con celle ombreggiate?
RulingSnapTolerance è ciò che fa connettere in un'unica griglia una tabella costruita solo da ombreggiature. Alcuni export non disegnano alcun bordo: ogni cella è un box pieno del suo colore, e i box vicini sono separati da un gutter di bianco da 1 a 3 punti. Ogni box produce quattro righelli di bordo, ma il bordo destro di una cella e quello sinistro della successiva stanno a 2 punti di distanza, e il test di connettività usa RulingTolerance, che per default vale 1 punto. Senza snapping ogni cella forma la propria componente connessa di quattro righelli, nessuna componente raggiunge MinRows, e la pagina non riporta nulla. TableSnapRulings raccoglie ogni coordinata X in gioco (la posizione di ogni righello verticale più l'inizio e la fine di ogni righello orizzontale) e ogni coordinata Y allo stesso modo, ordina ciascuna lista, la raggruppa concatenando i valori il cui vicino differisce non più della tolleranza, sostituisce ogni gruppo con la sua media e poi sposta ogni posizione, inizio e fine al centro di gruppo più vicino. I due lati di un gutter diventano la stessa linea, e la connettività tiene
Lo snapping gira prima di TableMergeRulings, che ordina i righelli e unisce i pezzi collineari che si toccano o si sovrappongono entro RulingTolerance, ed entrambi girano prima che TableDetectRuled veda mai i dati, quindi il controllo di connettività a coppie è proporzionale al numero di linee di griglia e non al numero di frammenti per cella. Su una griglia tracciata i passaggi sono innocui, perché coordinate già identiche si agganciano a se stesse. L'unica cosa da tenere a mente è che il raggruppamento a catena non ha un limite di ampiezza proprio: una sequenza di coordinate distanti 3 punti l'una dall'altra collassa in un unico centro. Con il default di 4 punti la cosa tocca solo colonne più strette di un carattere, ma se un documento ha gutter reali da 3 punti che devono restare separati, abbassa la tolleranza o portala a 0 per disattivare lo snapping:
// Isola la strategia delle griglie e confronta cosa vede ogni impostazione su una pagina
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 di Word riporta di solito 0, N e poi meno di N:
// i soli tratti non vedono nulla, lo snapping connette le celle ombreggiate,
// e disattivare lo snap lascia ogni cella ombreggiata come isola a sé
Writeln(CountRuledTables(Pdf, False, 4.0));
Writeln(CountRuledTables(Pdf, True, 4.0));
Writeln(CountRuledTables(Pdf, True, 0.0));
I righelli dentro i form XObject
Gli strumenti di impaginazione avvolgono spesso una tabella, o l'intero corpo della pagina, in un form XObject e lo dipingono con Do. ISO 32000-1 §8.10.1 specifica che la matrice del form viene concatenata con la matrice di trasformazione corrente quando il form viene dipinto, quindi un rettangolo dentro il form vive nello spazio del form e arriva sulla pagina solo dopo due o più trasformazioni. TableCollectObjectRulings ricorre dentro gli oggetti form quando IncludeFormXObjects è impostato: legge la matrice dell'oggetto, la combina con la matrice genitore tramite TableMultiplyMatrix, il cui ordine degli argomenti significa "mappa attraverso la prima matrice, poi la seconda", ed enumera i figli con FPDFFormObj_CountObjects e FPDFFormObj_GetObject, passando verso il basso la matrice combinata. Una nidificazione più profonda di MaxFormDepth (8) viene saltata in silenzio, il che è una difesa contro file patologici più che un limite che qualche export reale si avvicini a toccare. Il motivo per cui l'ordine di moltiplicazione conta è lo stesso discusso in matrice prepend e append a confronto: scambiare gli operandi sposta il termine di traslazione, e un righello che dovrebbe finire in cima alla pagina finisce nell'origine
Perché il budget dei righelli è quadruplicato?
Il valore di default di MaxRulingSegments è salito da 4096 a 16384 nella 3.117.0 perché i bordi per cella arrivano in numeri molto maggiori delle linee di griglia tracciate. Una tabella tracciata di 30 righe e 6 colonne è fatta di 38 segmenti di linea. La stessa tabella esportata come box pieni arriva fino a quattro bordi per cella, 720 pezzi prima del merge, e un form con celle ombreggiate raddoppia quel numero. Due tabelle così su una pagina avrebbero esaurito il vecchio budget. Il budget viene applicato in TableAppendRuling tramite Check, che solleva EPdfError con il messaggio "Table ruling-segment budget exceeded"; non c'è alcun risultato degradato, nessuna griglia parziale, e nemmeno il passaggio sulle spaziature gira. Se imposti un budget più stretto per input non fidati, intercetta l'eccezione e decidi, invece di leggere un risultato vuoto come "nessuna tabella":
Options := TPdfTableExtractionOptions.Default;
Options.MaxRulingSegments := 2048; // volutamente stretto per input non fidati
try
Tables := Pdf.ExtractTables(Options);
except
on E: EPdfError do
begin
Log(E.Message); // 'Table ruling-segment budget exceeded'
Options.MaxRulingSegments := 16384; // il default della 3.117.0
Tables := Pdf.ExtractTables(Options);
end;
end;
Risultati misurati e dove l'approccio si ferma
Sugli stessi 13 documenti campione, l'estrazione è passata da 43 tabelle, 9 delle quali con griglia e 34 frammenti o falsi positivi da spaziature, a 41 tabelle con griglia e nessun falso positivo da spaziature. Parte di quella pulizia appartiene a due modifiche compagne nella 3.117.0: le parole già rivendicate da una griglia vengono rimosse prima che giri il rilevamento su spaziature, così una tabella non viene mai segnalata due volte, e un confine di colonna per spaziature deve ora essere un corridoio libero da testo attraverso ogni riga che separa, ed è questo che ha smesso di far valutare paragrafi giustificati come tabelle 5x4. È il lettore di rettangoli pieni ad aver spostato le tabelle stesse dalla colonna dei frammenti a quella delle griglie
Vale la pena dichiarare chiaramente i confini. Una pagina senza livello di testo produce comunque lo scheletro della griglia, con ogni cella vuota, perché i righelli vengono dalla geometria e il testo viene dalla text page; le pagine scansionate richiedono prima un OCR. Le forme piene con curve, angoli arrotondati o contorni non rettangolari vengono scartate del tutto, quindi una tabella i cui bordi sono disegnati come contorni di rettangoli arrotondati ha bisogno del rilevamento su spaziature come prima. Una tabella senza bordi né ombreggiature non è toccata da nulla di tutto questo e resta territorio della strategia sulle spaziature descritta nell'articolo sull'estrazione delle tabelle; quando anche quella non basta, i box delle parole e i blocchi di testo strutturato e ordine di lettura sono la materia prima per un reader specifico del dominio. La demo TableExtractionLab che accompagna il componente espone DetectFilledRulings nel suo pannello delle opzioni, che è il modo più rapido per vedere come appare un dato export con e senza; l'API completa è descritta nella pagina di PDFium Component per Delphi