Η εξαγωγή tables του PDFium Component, από την έκδοση 3.117.0, μεταχειρίζεται ένα λεπτό γεμάτο ορθογώνιο ως table ruling. Με DetectFilledRulings ενεργό, που είναι το default, ένα γεμάτο axis-aligned box όχι πιο χοντρό από MaxRulingThickness (3 πόντους) γίνεται ένα ruling πάνω στον μακρύ άξονά του, ένα μεγαλύτερο γεμάτο box συνεισφέρει τις τέσσερις ακμές του, και κάθε συντεταγμένη ruling κουμπώνει εντός RulingSnapTolerance (4 πόντων) πριν συναρμολογηθεί το grid. Πίνακες εξαγμένοι από Word, Google Docs και browsers φτάνουν επομένως στον ruled detector ως πλήρη grids αντί να πέφτουν στην ανίχνευση whitespace ως τμήματα
Το προηγούμενο άρθρο για την ανίχνευση και εξαγωγή tables έλεγε ότι η ruled ανίχνευση χρησιμοποιεί τις σχεδιασμένες γραμμές και ότι κάθε stroked τμήμα path μετασχηματίζεται σε συντεταγμένες σελίδας. Εκείνη η πρόταση ήταν αληθής και ελλιπής. Η καταμέτρηση path objects πάνω σε σύνολο 13 πραγματικών δειγμάτων εγγράφων έδειξε ότι 9 από αυτά δεν περιέχουν καθόλου stroked path, και όμως κάθε σελίδα τους κουβαλάει εκατοντάδες γεμάτα ορθογώνια πάχους 0.5 έως 1 πόντου. Ο detector μόνο-για-stroke δεν έβλεπε τίποτα, κάθε σελίδα έπεφτε στην ανίχνευση whitespace, και η έξοδος ήταν διασπορά μικρών τμημάτων αντί για πίνακες. Το preset compact-columns που προστέθηκε στο 3.116.4 μαλάκωσε αυτό στο επίπεδο τμημάτων· η ρίζα ήταν ότι ο detector διάβαζε τον λάθος painting operator
Γιατί ένας πίνακας από export Word δεν έχει stroked γραμμές;
Ένας word processor δεν σκέφτεται ένα border ως γραμμή· το σκέφτεται ως box με πάχος, και το ζωγραφίζει με fill. Το ISO 32000-1 §8.5.2.1 ορίζει τον operator re ως προσάρτηση rectangle subpath, και το §8.5.3 χωρίζει τους painting operators: το S κάνει stroke το path με το τρέχον line width, το f γεμίζει το εσωτερικό του. Ένα border κελιού 0.5 πόντου βγαίνει ως x y w 0.5 re f, και η μηχανή stroke, line width, joins και dash pattern συμπεριλαμβανομένων, δεν τρέχει ποτέ. Το shading κελιού είναι η ίδια κατασκευή με μεγαλύτερο box. Ένα stroked grid σχεδιασμένο με m, l και S είναι ό,τι περίμενε ο αρχικός detector, και είναι ό,τι σχεδόν τίποτα εξαγμένο από office εφαρμογή παράγει:
% ένα border κελιού από export word processor: γεμάτο box ύψους 0.5 pt
72 700 468 0.5 re f
% cell shading: ένα γεμάτο box στο μέγεθος του κελιού
72 676 117 24 re f
% η stroked γραμμή grid για την οποία γράφτηκε ο αρχικός detector
72 700 m 540 700 l S
Σε έναν detector που ρωτάει το FPDFPath_GetDrawMode μόνο αν το stroke flag είναι στραμμένο, και τα δύο γεμάτα boxes είναι αόρατα. Οι λέξεις μέσα στα κελιά φτάνουν τότε στην ανίχνευση whitespace, όπου στήλες χωρισμένες με αυλάκι 6 πόντων κάθονται κάτω από το default MinColumnGap των 12 πόντων, και αυτό που γυρνά είναι όποιο υποσύνολο γραμμών τύχει να ευθυγραμμίζεται αρκετά για να περάσει το MinRows. Αυτή είναι η συμπεριφορά τμημάτων, και καμία ποσότητα ρύθμισης παραμέτρων δεν τη γυρνά στο grid που έζωγράφισε ο συγγραφέας
Πώς γυρνά το PDFium Component ένα γεμάτο box σε ruling;
Το TableCollectObjectRulings εξετάζει κάθε path object ένα subpath τη φορά. Το draw mode έρχεται από το FPDFPath_GetDrawMode· ένα path μετράει ως γεμάτο όταν το DetectFilledRulings είναι ανοιχτό και το fill mode δεν είναι none. Κάθε σημείο μετασχηματίζεται μέσω του object matrix και συλλέγεται, έως MaxSubpathPoints (8) ανά subpath, και οποιοδήποτε τμήμα καμπύλης μαρκάρει το subpath ως καμπύλο. Όταν το subpath κλείνει ή ξεκινά νέο MoveTo, το FlushSubpath αποφασίζει τι ήταν: ένα καμπύλο subpath απορρίπτεται, και το ίδιο κάθε κλειστό πολύγωνο του οποίου τα σημεία δεν κάθονται όλα εντός PointTolerance (0.05 πόντων) από τις ακμές του bounding box σε τουλάχιστον έναν άξονα. Ένα τρίγωνο, ένα chevron ή ένα στρογγυλεμένο tab δεν γίνεται ποτέ ruling, που είναι αυτό που κρατά διακοσμητικά γραφικά έξω από το grid
Αυτό που επιβιώνει είναι axis-aligned ορθογώνιο, ταξινομημένο από το bounding box του. Πλάτος στο ή κάτω από MaxRulingThickness με ύψος πάνω από αυτό δίνει ένα κατακόρυφο ruling στο οριζόντιο κέντρο, απλωμένο πάνω στο box από πάτο ως κορυφή· η κατοπτρική περίπτωση δίνει ένα οριζόντιο ruling. Και οι δύο διαστάσεις πάνω από το κατώφλι σημαίνει shaded κελί, και το box συνεισφέρει τέσσερα rulings, ένα ανά ακμή. Και οι δύο διαστάσεις στο ή κάτω από το κατώφλι δεν συνεισφέρουν τίποτα, οπότε ένα τετράγωνο bullet 2 πόντων δεν μπερδεύεται με γραμμή. Ένα stroked path παίρνει την παλαιότερη διαδρομή μέσω AddLine, ένα ruling ανά axis-aligned τμήμα, οπότε ένα grid σχεδιασμένο με S χειρίζεται ακριβώς όπως πριν, και ένα path ζωγραφισμένο με fill και stroke μαζί παράγει επικαλυπτόμενα κομμάτια που το πέρασμα συγχώνευσης καταρρίπτει:
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
Options := TPdfTableExtractionOptions.Default;
// αυτά είναι τα defaults του 3.117.0, γραμμένα για σαφήνεια
Options.DetectFilledRulings := True; // λεπτά γεμάτα boxes γίνονται rulings
Options.MaxRulingThickness := 3.0; // points· πιο χοντρά boxes μετρούν ως shading
Options.RulingSnapTolerance := 4.0; // points· το 0 απενεργοποιεί 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;
Τι κάνει το RulingSnapTolerance για tables από shaded κελιά;
Το RulingSnapTolerance είναι αυτό που κάνει έναν πίνακα χτισμένο μόνο από shading να συνδεθεί σε ένα grid. Κάποια exports δεν σχεδιάζουν καθόλου border: κάθε κελί είναι γεμάτο box στο δικό του χρώμα, και γειτονικά boxes χωρίζονται με λευκό αυλάκι 1 έως 3 πόντων. Κάθε box δίνει τέσσερα edge rulings, αλλά η δεξιά ακμή ενός κελιού και η αριστερή ακμή του επόμενου κάθονται 2 πόντους μεταξύ τους, και το test συνεκτικότητας χρησιμοποιεί RulingTolerance, που προεπιλέγεται σε 1 πόντο. Χωρίς snapping, κάθε κελί σχηματίζει το δικό του connected component από τέσσερα rulings, κανένα component δεν φτάνει το MinRows, και η σελίδα δεν αναφέρει τίποτα. Το TableSnapRulings μάζεψε κάθε X συντεταγμένη σε παιχνίδι (τη θέση κάθε κατακόρυφου ruling συν την αρχή και το τέλος κάθε οριζόντιου) και κάθε Y συντεταγμένη αντίστοιχα, ταξινομεί κάθε λίστα, την ομαδοποιεί αλυσιδώνοντας τιμές των οποίων ο γείτονας διαφέρει κατά όχι περισσότερο από την ανοχή, αντικαθιστά κάθε ομάδα με τον μέσο όρο της, και μετά μετακινεί κάθε θέση, αρχή και τέλος στο πλησιέστερο κέντρο ομάδας. Οι δύο πλευρές ενός αυλακιού γίνονται η ίδια γραμμή, και η συνεκτικότητα κρατάει
Το snapping τρέχει πριν το TableMergeRulings, που ταξινομεί τα rulings και ενώνει collinear κομμάτια που αγγίζουν ή επικαλύπτονται εντός RulingTolerance, και τα δύο τρέχουν πριν το TableDetectRuled δει ποτέ τα δεδομένα, οπότε ο pairwise έλεγχος συνεκτικότητας είναι αναλογικός στον αριθμό γραμμών grid κι όχι στον αριθμό τμημάτων ανά κελί. Σε stroked grid τα περάσματα είναι αβλαβή, γιατί συντεταγμένες που ήταν ήδη πανομοιότυπες κουμπώνουν στον εαυτό τους. Το ένα πράγμα που πρέπει να θυμάσαι είναι ότι η αλυσιδωτή ομαδοποίηση δεν έχει δικό της όριο πλάτους: μια σειρά συντεταγμένων η καθεμία 3 πόντων μεταξύ τους καταρρέει σε ένα μοναδικό κέντρο. Στο default 4 πόντων αυτό επηρεάζει μόνο στήλες στενότερες από χαρακτήρα, αλλά αν ένα έγγραφο έχει πραγματικά αυλάκια 3 πόντων που πρέπει να μείνουν χωριστά, χαμήλωσε την ανοχή ή θέσε την στο 0 για να κλείσεις το snapping:
// Απομόνωσε τη ruled στρατηγική και σύγκρινε τι βλέπει κάθε ρύθμιση σε μία σελίδα
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;
// Ένα export Word τυπικά αναφέρει 0, N και μετά λιγότερα από N:
// το stroke-only δεν βλέπει τίποτα, το snapping συνδέει τα shaded κελιά,
// και το κλείσιμο του snap αφήνει κάθε shaded κελί νησί από μόνο του
Writeln(CountRuledTables(Pdf, False, 4.0));
Writeln(CountRuledTables(Pdf, True, 4.0));
Writeln(CountRuledTables(Pdf, True, 0.0));
Rulings μέσα σε form XObjects
Εργαλεία page-layout τυλίγουν συχνά έναν πίνακα, ή όλο το σώμα της σελίδας, μέσα σε form XObject και τον ζωγραφίζουν με Do. Το ISO 32000-1 §8.10.1 προδιαγράφει ότι ο form matrix συνενώνεται με τον τρέχον transformation matrix όταν ζωγραφίζεται η form, οπότε ένα ορθογώνιο μέσα στη form ζει στον χώρο της form και προσγειώνεται στη σελίδα μόνο μετά από δύο ή περισσότερους μετασχηματισμούς. Το TableCollectObjectRulings κάνει recursion μέσα σε form objects όταν το IncludeFormXObjects είναι στραμμένο: διαβάζει το object matrix, το συνδυάζει με τον parent matrix μέσω TableMultiplyMatrix, της οποίας η σειρά ορισμάτων σημαίνει «χαρτογράφησε μέσω του πρώτου matrix, μετά του δεύτερου», και απαριθμεί τα παιδιά με FPDFFormObj_CountObjects και FPDFFormObj_GetObject, περνώντας τον συνδυασμένο matrix προς τα κάτω. Φωλιάσματος βαθύτερο από MaxFormDepth (8) παραλείπεται σιωπηλά, που είναι φύλακας απέναντι σε παθολογικά αρχεία και όχι όριο που οποιοδήποτε πραγματικό export προσεγγίζει. Η αιτία που η σειρά πολλαπλασιασμού μετράει είναι η ίδια που συζητιέται στο matrix prepend απέναντι σε append: η εναλλαγή των operands μετακινεί τον όρο μετατόπισης, και ένα ruling που έπρεπε να προσγειωθεί στην κορυφή της σελίδας προσγειώνεται στην αρχή των αξόνων αντ' αυτού
Γιατί τετραπλασιάστηκε ο προϋπολογισμός rulings;
Το default MaxRulingSegments ανέβηκε από 4096 σε 16384 στο 3.117.0 επειδή τα borders ανά κελί φτάνουν σε πολύ μεγαλύτερους αριθμούς από stroked γραμμές grid. Ένα stroked πίνακας 30 γραμμών, 6 στηλών είναι 38 line segments. Ο ίδιος πίνακας εξαγμένος ως γεμάτα boxes είναι έως τέσσερα borders ανά κελί, 720 κομμάτια πριν τη συγχώνευση, και μια form με shaded κελιά το διπλασιάζει. Δύο τέτοιοι πίνακες σε μια σελίδα θα είχαν εξαντλήσει τον παλιό προϋπολογισμό. Ο προϋπολογισμός επιβάλλεται στο TableAppendRuling μέσω Check, που πετάει EPdfError με το μήνυμα "Table ruling-segment budget exceeded"· δεν υπάρχει degraded αποτέλεσμα, ούτε μερικό grid, και το πέρασμα whitespace δεν τρέχει ούτε αυτό. Αν θέσεις δικό σου πιο σφιχτό προϋπολογισμό για μη έμπιστη είσοδο, πιάσε το exception και αποφάσισε, αντί να διαβάσεις κενό αποτέλεσμα ως «κανένας πίνακας»:
Options := TPdfTableExtractionOptions.Default;
Options.MaxRulingSegments := 2048; // σκόπιμα σφιχτό για μη έμπιστη είσοδο
try
Tables := Pdf.ExtractTables(Options);
except
on E: EPdfError do
begin
Log(E.Message); // 'Table ruling-segment budget exceeded'
Options.MaxRulingSegments := 16384; // το default του 3.117.0
Tables := Pdf.ExtractTables(Options);
end;
end;
Μετρημένα αποτελέσματα και πού σταματά η προσέγγιση
Στα ίδια 13 δείγματα εγγράφων, η εξαγωγή πήγε από 43 tables, 9 εξ αυτών ruled και 34 whitespace τμήματα ή false positives, σε 41 ruled tables και μηδέν whitespace false positives. Μέρος εκείνου του καθαρισμού ανήκει σε δύο συνοδές αλλαγές του 3.117.0: λέξεις που διεκδικούνται ήδη από ruled grid αφαιρούνται πριν τρέξει η ανίχνευση whitespace, οπότε ένας πίνακας δεν αναφέρεται ποτέ δύο φορές, και ένα όριο whitespace στήλης πρέπει πλέον να είναι διάδρομος χωρίς text σε κάθε γραμμή που χωρίζει, που είναι αυτό που σταμάτησε justified παραγράφους από το να σκοράρουν ως tables 5x4. Ο reader γεμάτων ορθογωνίων είναι αυτό που μετακίνησε τους ίδιους τους πίνακες από τη στήλη τμημάτων στη στήλη ruled
Τα όρια αξίζει να δηλωθούν ξερά. Μια σελίδα χωρίς text layer εξακολουθεί να δίνει τον σκελετό grid, κάθε κελί κενό, γιατί τα rulings έρχονται από τη γεωμετρία και το text από τη text page· οι σκαναρισμένες σελίδες χρειάζονται πρώτα OCR. Γεμάτα σχήματα με καμπύλες, στρογγυλεμένες γωνίες ή μη ορθογώνια περιγράμματα απορρίπτονται ολότελα, οπότε πίνακας του οποίου τα borders σχεδιάζονται ως περιγράμματα στρογγυλεμένων ορθογωνίων χρειάζεται ανίχνευση whitespace όπως πριν. Ένας πίνακας χωρίς ούτε borders ούτε shading δεν αλλάζει από τίποτα από αυτά και παραμένει επαρχία της στρατηγικής whitespace που περιγράφεται στο άρθρο εξαγωγής tables· όταν ούτε εκείνο φτάνει, τα word boxes και blocks από το structured text και reading order είναι η πρώτη ύλη για reader ειδικό πεδίου. Το demo TableExtractionLab που κυκλοφορεί μαζί με το component εκθέτει το DetectFilledRulings στο panel επιλογών του, που είναι ο γρηγορότερος τρόπος να δεις πώς φαίνεται ένα δοσμένο export με και χωρίς αυτό· το πλήρες API περιγράφεται στη σελίδα PDFium Component for Delphi