Το PDFlibPas επιλύει αναδρομικές κλήσεις Form XObject σε PDF content streams σε Delphi παρακολουθώντας την ενεργή αλυσίδα κλήσεων, όχι ένα καθολικό σύνολο επισκεφθέντων, ώστε η TPDFlib.EnumPageContentStatesEx να μπορεί να διατρέξει την ίδια Form που καλείται πολλές φορές σε μία σελίδα χωρίς να μπερδέψει τη νόμιμη επαναχρησιμοποίηση με έναν κύκλο. Ένα Form XObject σφραγίδας σε πρότυπο τιμολογίου είναι η τυπική περίπτωση: το ίδιο αντικείμενο καλείται από την κεφαλίδα, το υποσέλιδο και ένα επίπεδο υδατογραφήματος στην ίδια σελίδα, και μόνο μια αλυσίδα κλήσεων που επιστρέφει σε εαυτήν είναι πραγματικός κύκλος
Το ISO 32000-1 §8.10 ορίζει μια Form XObject ως ένα αυτόνομο content stream που μια σελίδα, ή μια άλλη Form, καλεί με τον τελεστή Do, με το δικό της σύστημα συντεταγμένων στο /Matrix, ένα όριο clipping σε αυτό το σύστημα συντεταγμένων στο /BBox, και προαιρετικά το δικό της λεξικό πόρων. Τίποτα στην προδιαγραφή δεν θέτει όριο στο πόσες φορές μπορεί να κληθεί μια Form ή πόσο βαθιά μπορούν οι Forms να καλούν η μία την άλλη, οπότε ένας συμμορφούμενος parser πρέπει να δέχεται τη νόμιμη επαναχρησιμοποίηση και τη νόμιμη ένθεση διατηρώντας ταυτόχρονα άμυνα ενάντια στην μόνη διάταξη που απαγορεύει πράγματι η προδιαγραφή: μια Form της οποίας το content stream, άμεσα ή μεταβατικά, καλεί τον εαυτό της. Το PDFlibPas αναφέρει αυτή τη διάκριση μέσω των τιμών TPDFlibContentFormTraversalStatus που επισυνάπτονται σε κάθε στιγμιότυπο Do, κυρίως το ftsEnumerated για μια επιτυχημένη κατάβαση και το ftsCycle για τη μόνη περίπτωση που είναι πράγματι βρόχος
Γιατί η επαναχρησιμοποίηση της ίδιας Form XObject δεν προκαλεί ψευδή κύκλο;
Μια επαναλαμβανόμενη αναφορά Form XObject δεν αποτελεί από μόνη της ένδειξη κάποιου προβλήματος. Το ISO 32000-1 επιτρέπει το ίδιο αντικείμενο Form να καλείται από όσα σημεία ενός content stream επιθυμεί ο δημιουργός, κάτι που είναι ακριβώς ο τρόπος με τον οποίο μια σφραγίδα λογότυπου, ένα πρότυπο επιστολόχαρτου ή ένα υποσέλιδο αριθμού σελίδας επαναχρησιμοποιείται σε μια σελίδα χωρίς να διπλασιάζεται το content stream του πολλές φορές. Η αφελής προστασία από ανεξέλεγκτη αναδρομή είναι ένα ενιαίο σύνολο επισκεφθέντων με κλειδί τον αριθμό αντικειμένου: την πρώτη φορά που μια διαδρομή διάτρεσης βλέπει το αντικείμενο Form 12, το σημειώνει ως εωρακθέν και αρνείται να ξαναεισέλθει σε αυτό οπουδήποτε αλλού στο δέντρο. Αυτή η προσέγγιση καταρρέει τη στιγμή που η ίδια σφραγίδα εμφανίζεται σε δύο άσχετες γωνίες της ίδιας σελίδας, γιατί η δεύτερη, εντελώς νόμιμη κλήση φτάνει αφού ο αριθμός αντικειμένου έχει ήδη σημειωθεί ως εωρακθείς και απορρίπτεται σαν να επρόκειτο για βρόχο
Το PDFlibPas αποφεύγει αυτό το ψευδώς θετικό οριοθετώντας την ανίχνευση κύκλων στην τρέχουσα αλυσίδα κλήσεων αντί σε ολόκληρο το έγγραφο. Η EnumPageContentStatesEx ωθεί το επιλυμένο stream της Form στην ενεργή αλυσίδα κλήσεων αμέσως πριν κατέβει σε αυτό, και μετά αποσωρεύει την ίδια καταχώριση μόλις η κατάβαση επιστρέψει, με επιτυχία ή όχι. Μια αδελφή κλήση του ίδιου ακριβώς stream ξεκινά μόνο αφού η πρώτη έχει ήδη αποσωρευτεί, οπότε η αλυσίδα κλήσεων είναι καθαρή από εκείνο το stream τη στιγμή που η αδελφή κλήση το ελέγχει, και η διαδρομή διάτρεσης το απαριθμεί ακριβώς όπως θα έκανε με οποιαδήποτε άλλη Form. Ένας πραγματικός κύκλος φαίνεται διαφορετικός στην ίδια αλυσίδα: η Form A καλεί τη Form B, η B παραμένει ανοιχτή στην αλυσίδα όταν το δικό της περιεχόμενο καλεί πίσω την A, και η A εξακολουθεί να κάθεται στην αλυσίδα από την εξωτερική κλήση που δεν έχει ακόμα επιστρέψει — αυτό είναι το μόνο σχήμα που αναφέρει το ftsCycle, ένα stream Form ακόμα ανοιχτό κάπου νωρίτερα στην τρέχουσα αλυσίδα κλήσεων, όχι απλώς παρόν κάπου αλλού στη σελίδα
Πόσο βαθιά μπορεί να φτάσει η αναδρομή Form XObject πριν την σταματήσει το PDFlibPas;
Η ανίχνευση κύκλων και ο περιορισμός βάθους λύνουν δύο διαφορετικά προβλήματα, και το PDFlibPas τα διατηρεί ως δύο διαφορετικά αποτελέσματα TPDFlibContentFormTraversalStatus ακριβώς γι' αυτόν τον λόγο. Μια αλυσίδα είκοσι διακριτών Forms, καθεμία να καλεί την επόμενη και καμία να μην επαναλαμβάνεται, δεν είναι κύκλος με κανέναν ορισμό — ο έλεγχος ενεργής αλυσίδας ποτέ δεν βρίσκει επαναλαμβανόμενο stream — αλλά είκοσι έντιμα επίπεδα ένθεσης εξακολουθούν να είναι είκοσι επίπεδα ανάλυσης, συνένωσης πινάκων και επίλυσης πόρων που ένα κακοδιατυπωμένο ή εχθρικό PDF θα μπορούσε να ωθήσει αυθαίρετα ψηλότερα αν τίποτα άλλο δεν το σταματούσε. Η EnumPageContentStatesEx δέχεται μια παράμετρο MaxFormDepth ακριβώς γι' αυτόν τον λόγο και περιορίζει όποια τιμή δοθεί σε μέγιστο 64, ανεξάρτητα από το τι ζητά ο καλών. Ένα βάθος μηδέν είναι μια ειδική περίπτωση που αξίζει να γνωρίζετε από μόνη της: απενεργοποιεί εντελώς την αναδρομή Form και αναπαράγει την επίπεδη συμπεριφορά, μόνο-σελίδας, της παλαιότερης μεθόδου EnumPageContentStates, γι' αυτό κάθε στιγμιότυπο Do σε αυτή τη λειτουργία αναφέρει ftsNotRequested αντί να επιχειρήσει οτιδήποτε
var
Lib: TPDFlib;
States: array of TPDFlibContentGraphicsState;
Count, I: Integer;
begin
Lib:= TPDFlib.Create;
try
if Lib.LoadFromFile('invoice-batch.pdf', '')<> 1 then
Exit;
Lib.SelectPage(1);
Count:= Lib.EnumPageContentStatesEx(True, 8, States); // count only
SetLength(States, Count);
Lib.EnumPageContentStatesEx(True, 8, States); // fill
for I:= 0 to Count- 1 do
if States[I].FormTraversalStatus= ftsCycle then
LogSuspectForm(States[I].XObjectResource, States[I].ContentDepth);
finally
Lib.Free;
end;
end;
Ένας Sub-Tracker ανά Κλήση: Απομόνωση Κατάστασης Γραφικών
Κάθε κατάβαση σε μια Form XObject αποκτά τον δικό της tracker κατάστασης γραφικών αντί να μοιράζεται αυτόν που ήδη διατρέχει τη σελίδα, γιατί το content stream μιας Form οφείλει να αφήνει την κατάσταση γραφικών ακριβώς όπως τη βρήκε, και το PDFlibPas δεν μπορεί να υποθέσει ότι κάθε PDF που ανοίγει πράγματι τηρεί αυτή την απαίτηση. Ο θυγατρικός tracker ξεκινά από ένα στιγμιότυπο του CTM, της κατάστασης χρώματος και των παραμέτρων κειμένου που ήταν ενεργά στην καλούσα εντολή Do, και μετά μηδενίζει τη δική του στοίβα save-and-restore και την παρακολούθηση τρέχοντος μονοπατιού πριν εκτελέσει έστω και μία εντολή της Form. Ένα ανισοζυγισμένο q χωρίς αντίστοιχο Q μέσα σε μια απρόσεκτη ή κατεστραμμένη Form, όχι σπάνιο εύρημα σε PDF παραγμένα από παλαιότερα εργαλεία, παραμένει περιορισμένο μέσα στον tracker εκείνης της μίας κλήσης και ποτέ δεν διαρρέει στον tracker της σελίδας ούτε σε μια αδελφή κλήση της ίδιας σφραγίδας που βρίσκεται μία γραμμή παρακάτω στο content stream
Το /Matrix της Form συνδυάζεται με το CTM που ισχύει στο Do με τον ίδιο τρόπο που κάνει ένας τελεστής cm, πολλαπλασιαζόμενο αριστερά με τον τρέχοντα μετασχηματισμό αντί να τον αντικαθιστά, και το PDFlibPas επαναχρησιμοποιεί σκόπιμα αυτή τη μία διαδρομή κώδικα αντί να συντηρεί μια δεύτερη φόρμουλα, αφού δύο ανεξάρτητες υλοποιήσεις της ίδιας άλγεβρας πινάκων είναι ακριβώς το είδος διπλασιασμού που σιωπηλά αποκλίνει μετά από μερικούς γύρους σύνθεσης κλίμακας, περιστροφής και λόξωσης. Το /BBox κάνει στη συνέχεια clip στον δικό της χώρο συντεταγμένων της Form αφού έχει ήδη εφαρμοστεί ο πίνακας, και και οι τέσσερις γωνίες αυτού του πλαισίου μετασχηματίζονται ξεχωριστά αντί μόνο οι απέναντι γωνίες, αφού μια περιστραμμένη ή λοξωμένη Form μπορεί διαφορετικά να αναφέρει ένα πλαίσιο οριοθέτησης που χάνει πραγματικό περιεχόμενο που βρίσκεται σε αυτό που ήταν κάποτε μια ακραία γωνία πριν ο μετασχηματισμός το μετακινήσει αλλού. Επεκτείνοντας τον βρόχο από το προηγούμενο παράδειγμα πάνω στον ίδιο πίνακα States διαβάζονται απευθείας αυτά τα πεδία
for I:= 0 to Count- 1 do
if (States[I].OperatorName= 'Do')and (States[I].XObjectKind= cxkForm)and
States[I].FormBBoxKnown then
Writeln('Form ', States[I].XObjectResource, ' matrix ',
States[I].FormMatrix.M11:0:3, ',', States[I].FormMatrix.M12:0:3,
' bbox ', States[I].FormBBoxLeft:0:1, '..', States[I].FormBBoxRight:0:1);
Μοιράζονται Δύο Forms με το Ίδιο Όνομα Πόρου μία Γραμματοσειρά;
Όχι. Ένα όνομα πόρου όπως /F1 σημαίνει κάτι μόνο σε σχέση με το λεξικό πόρων που είναι ενεργό στο σημείο χρήσης του, και δύο διαφορετικές Form XObjects είναι ελεύθερες να ορίσουν δύο εντελώς διαφορετικές γραμματοσειρές κάτω από αυτό το ίδιο όνομα. Το PDFlibPas το επιλύει αυτό παρακολουθώντας ένα εύρος πόρων παράλληλα με κάθε όνομα πόρου: όταν μια Form φέρει το δικό της λεξικό /Resources, εκείνο το λεξικό γίνεται το πλήρες εύρος πόρων για οτιδήποτε βρίσκεται μέσα του, χωρίς επιστροφή ανά κλειδί στο λεξικό της σελίδας ή του καλούντος για ό,τι τυχόν παραλείπει το δικό της λεξικό της Form. Μόνο μια Form χωρίς καθόλου κλειδί /Resources, ένα μοτίβο που εξακολουθούν να παράγουν κάποιες παλαιότερες γεννήτριες PDF, κληρονομεί ολόκληρο το λεξικό του καλούντος, και αυτό είναι μια σκόπιμη εξαίρεση συμβατότητας παρά ένας γενικός κανόνας που αξίζει να στηριχθεί σε νέα έξοδο. Η ταυτότητα γραμματοσειράς σε ένα στιγμιότυπο TPDFlibContentGraphicsState είναι επομένως το ζεύγος FontResource και FontResourceScope, όχι μόνο το όνομα, με το FontObjectNumber διαθέσιμο για να επιβεβαιώσει ακριβώς σε ποιο έμμεσο αντικείμενο επιλύθηκε ένα δεδομένο /F1 σε εκείνο το συγκεκριμένο εύρος
Η ίδια οριοθέτηση εφαρμόζεται σε κάθε άλλο ονομασμένο πόρο που μπορεί να φέρει μια Form, συμπεριλαμβανομένων των καταχωρίσεων ExtGState και των ένθετων καταχωρίσεων XObject, αφού ο υποκείμενος μηχανισμός επίλυσης δεν αντιμετωπίζει τις γραμματοσειρές ως ειδική περίπτωση — απλώς η περίπτωση της γραμματοσειράς τυχαίνει να έχει τη μεγαλύτερη σημασία, γιατί μια αναντιστοιχούμενη ταυτότητα γραμματοσειράς παράγει σιωπηλά λάθος glyphs αντί για μια εμφανή αποτυχία. Κώδικας εξαγωγής που ομαδοποιεί runs κειμένου μόνο βάσει ονόματος γραμματοσειράς, χωρίς επίσης ομαδοποίηση βάσει εύρους πόρων, θα συγχωνεύσει δύο οπτικά διαφορετικές γραμματοσειρές που τυχαίνει να μοιράζονται όνομα, και το λάθος δεν θα εμφανιστεί μέχρι κάποιος να παρατηρήσει ψηφία από λάθος γραμματοσειρά μέσα σε αυτό που υποτίθεται ότι θα διαβαζόταν ως μία συνεπής γραμματοσειρά
for I:= 0 to Count- 1 do
if (States[I].OperatorName= 'Tj')and States[I].TextAdvanceResolved then
RecordGlyphRun(States[I].FontResource, States[I].FontResourceScope,
States[I].FontObjectNumber, States[I].ContentDepth);
Ανάγνωση του FormTraversalStatus στη Δική σας Ροή Επεξεργασίας
Το FormTraversalStatus μετατρέπει κάθε στιγμιότυπο Do σε μια μικρή διαγνωστική αναφορά από μόνο του, και μια ροή επεξεργασίας που το αγνοεί πετά ακριβώς την πληροφορία που θα εξηγούσε μια ημιτελή εξαγωγή. Το ftsNotApplicable σημαίνει ότι η εντολή ποτέ δεν ήταν επιλυμένη κλήση Form εξαρχής· το ftsNotRequested σημαίνει ότι η αναδρομή είχε απενεργοποιηθεί για αυτή την κλήση· το ftsEnumerated σημαίνει ότι η Form αναλύθηκε και διατρέχθηκε επιτυχώς· τα ftsDepthLimit και ftsCycle σημειώνουν τους δύο τρόπους με τους οποίους μια κατάβαση κόβεται σκόπιμα· και το ftsMalformed καλύπτει οτιδήποτε άλλο σταμάτησε τη διάτρεση — μια ανεπίλυτη αναφορά stream, ένα /Matrix ή /BBox που απέτυχε να αναλυθεί, ή μια εξαίρεση που εγέρθηκε κατά την εκτέλεση του περιεχομένου της ίδιας της Form. Αυτή η τελευταία περίπτωση έχει επιχειρησιακή σημασία, γιατί μια αποτυχημένη ένθετη διάτρεση αναιρεί ό,τι μερική έξοδο είχε ήδη παράγει για εκείνο τον κλάδο, οπότε ένας καλών ποτέ δεν χρειάζεται να μαντέψει αν μια Form ήταν πράγματι κενή ή απλώς κατέρρευσε δύο εντολές μέσα στο content stream της
var
Tally: array[TPDFlibContentFormTraversalStatus] of Integer;
Status: TPDFlibContentFormTraversalStatus;
begin
for Status:= Low(Tally) to High(Tally) do
Tally[Status]:= 0;
for I:= 0 to Count- 1 do
Inc(Tally[States[I].FormTraversalStatus]);
if (Tally[ftsCycle]> 0)or (Tally[ftsMalformed]> 0) then
FlagForManualReview(SourceFileName, Tally[ftsCycle], Tally[ftsMalformed]);
end;
Όρια, Κόστη και Πού Ταιριάζει Αυτό
Το content stream μιας Form αποκωδικοποιείται και αναλύεται ακριβώς μία φορά ανά κλήση απαρίθμησης ανεξάρτητα από το πόσες φορές καλείται η Form, γιατί το PDFlibPas αποθηκεύει σε cache τη λίστα αναλυμένων εντολών έναντι του υποκείμενου αντικειμένου stream αντί να την ξαναναλύει σε κάθε αδελφή κλήση — η σφραγίδα τριών γωνιών από το αρχικό παράδειγμα αποκωδικοποιείται μία φορά και διατρέχεται τρεις φορές, όχι αποκωδικοποιείται τρεις φορές. Αυτό που όντως ξαναχτίζεται σε κάθε μεμονωμένη κλήση είναι οτιδήποτε νομίμως διαφέρει από τη μια θέση κλήσης στην επόμενη: ο θυγατρικός tracker, το συνενωμένο CTM, το τεμνόμενο clip και το εύρος πόρων. Αυτή η λογιστική CTM και clip ανά κλήση είναι ο ίδιος μηχανισμός πίσω από τον tracker κατάστασης CTM και clipping content-stream του PDFlibPas, που αξίζει να διαβαστεί παράλληλα με αυτό το άρθρο για κάθε διάτρεση content-stream που πάει πέρα από την ίδια την αναδρομή Form
Αξίζει να τεθούν προσδοκίες γύρω από δύο όρια πριν αυτό το API μπει σε μεγαλύτερη ροή επεξεργασίας. Το ανώτατο όριο 64 επιπέδων δεν είναι ένα ρυθμιστικό κουμπί για νόμιμα βαθιά έγγραφα, αφού πραγματικά τιμολόγια, καταστάσεις και πρότυπα αναφορών ουσιαστικά ποτέ δεν ενθέτουν Forms πάνω από τρία ή τέσσερα επίπεδα βάθος — ένα έγγραφο που πράγματι χτυπά το ftsDepthLimit είναι πολύ πιο πιθανό να είναι κακοδιατυπωμένο ή εχθρικό παρά ασυνήθιστα περίπλοκο, και αξίζει να καταγράφεται ως σήμα ποιότητας δεδομένων παρά να επαναλαμβάνεται σιωπηλά με μεγαλύτερο αριθμό. Η EnumPageContentStatesEx είναι επίσης ένα API ανάλυσης πλευράς ανάγνωσης: αναφέρει τι κάνει ένα content stream, όχι αν μια Form θα έπρεπε καν να είναι ορατή, κάτι που είναι ξεχωριστό ερώτημα που απαντά η κατάσταση ορατότητας Optional Content Group όταν μια σφραγίδα ή υδατογράφημα Form βρίσκεται πίσω από ένα επίπεδο που ένας θεατής μπορεί να έχει απενεργοποιήσει. Η ανίχνευση κύκλων βάσει αλυσίδας κλήσεων, η απομόνωση ανά κλήση και η οριοθέτηση πόρων μαζί αποτελούν μία γωνία της επιφάνειας επιθεώρησης content-stream στο εξάρτημα PDFlibPas για Delphi και C++Builder