Το PDFlibPas, η εγγενής βιβλιοθήκη VCL για PDF σε Delphi και C++Builder, αναπαράγει το content stream μιας σελίδας μέσω της κλάσης TPDFContentStateTracker χωρίς να αγγίζει καθόλου κάποιο canvas απόδοσης. Τροφοδοτώντας τον tracker με έναν αναλυμένο τελεστή τη φορά, διατηρείται μια συνεχής καταγραφή της κατάστασης γραφικών — τρέχων πίνακας μετασχηματισμού (CTM), πίνακας κειμένου, όρια clip και η στοίβα αποθήκευσης q/Q — διαθέσιμη για στιγμιότυπο πριν ή μετά την εκτέλεση κάθε τελεστή
Αν αναρωτηθείτε πού ακριβώς προσγειώνεται μια σειρά κειμένου στην τυπωμένη σελίδα, οι ακατέργαστοι αριθμοί του content stream από μόνοι τους θα σας παραπλανήσουν κάθε φορά. Η TPDFContentProgram.GetTextRuns ήδη επιστρέφει το σημείο αγκύρωσης κάθε εντολής εμφάνισης κειμένου μέσω των πεδίων OriginX και OriginY στο TPDFTextRun, και τα σχόλια των πεδίων είναι ρητά ότι αυτό το σημείο βρίσκεται στον χώρο κειμένου, ήδη διαμορφωμένο μέσω των Tm, Td, TD και T*. Αυτό που λείπει ακόμα, και που τα σχόλια αυτά αναφέρουν ότι πρέπει να παρέχει ο καλών, είναι το CTM που ισχύει σε αυτή ακριβώς την εντολή — το γινόμενο κάθε cm που έχει συνενωθεί μέχρι εκείνο το σημείο, ένθετο μέσα σε όσα ζεύγη q/Q τυχαίνει να είναι ανοιχτά εκείνη τη στιγμή στο stream
Γιατί αναπαραγωγή ενός content stream αντί για απόδοση;
Το PDFlibPas διατηρεί δύο ξεχωριστές έννοιες κατάστασης γραφικών για δύο ξεχωριστές δουλειές, και ο διαχωρισμός είναι σκόπιμος. Η εσωτερική καταγραφή κατάστασης του renderer φέρει μια ενεργή λαβή canvas συσκευής, μια λαβή περιοχής clipping και caches εξομάλυνσης γραμματοσειράς — πραγματικούς πόρους δεμένους με όποια επιφάνεια ζωγραφίζεται εκείνη τη στιγμή, χωρίς νόημα μόλις αυτή η επιφάνεια εξαφανιστεί. Το TPDFContentGraphicsState δεν φέρει τίποτα από αυτά: είναι μια απλή εγγραφή περιορισμένη στις τιμές που το ISO 32000-1 §8.4 ορίζει ως προσβάσιμες αποκλειστικά από τελεστές content-stream — το CTM, το στυλ γραμμής, το χρώμα, την κατάσταση κειμένου, και τα παράγωγα όρια clip και μονοπατιού. Επειδή η εγγραφή δεν κρατά καμία αναφορά canvas ούτε κανένα ανοιχτό handle αρχείου, ένας καλών μπορεί να αναλύσει ένα content stream, να το διατρέξει με το TPDFContentStateTracker, και να συνεχίσει να χρησιμοποιεί τα στιγμιότυπα που προκύπτουν πολύ αφότου έχει εξαφανιστεί ό,τι παρήγαγε τα bytes
Πώς το TPDFContentStateTracker χτίζει το CTM
Η TPDFContentStateTracker.Apply συνενώνει τους έξι τελεστέους ενός τελεστή cm στο CTM του tracker χρησιμοποιώντας τον ίδιο κανόνα προπολλαπλασιασμού που ορίζει το ίδιο το PDF: ο νέος πίνακας M2 συνδυάζεται με το τρέχον CTM ως M2 × CTM, στη σύμβαση διανύσματος-γραμμής όπου ένα σημείο μετασχηματίζεται ως P′ = P × M (ISO 32000-1 §8.4). Το σημείο που είναι εύκολο να γίνει λάθος βρίσκεται στον όρο μετάθεσης, όχι στο γραμμικό μέρος: η δική του μετάθεση του M2 πρέπει να περάσει μέσα από τη συνιστώσα περιστροφής-και-κλίμακας του τρέχοντος CTM πριν προστεθεί από πάνω η μετάθεση του τρέχοντος CTM. Αν παραλείψετε αυτό το βήμα και κωδικοποιήσετε σκληρά αντ' αυτού έναν αφελή συνδυασμό ανά συνιστώσα, το πρώτο μεμονωμένο cm που θα δοκιμάσετε θα φαίνεται σωστό ενώ κάθε συντεταγμένη μετά από ένα δεύτερο ή τρίτο ένθετο cm θα παρεκκλίνει σιωπηλά — ακριβώς το είδος σφάλματος που επιβιώνει από code review, γιατί το unit test που θα το έπιανε χρειάζεται τουλάχιστον δύο αλυσιδωτούς μετασχηματισμούς για να αποτύχει
var
Prog: TPDFContentProgram;
Runs: TPDFTextRunArray;
States: TPDFContentGraphicsStateArray;
DeviceX, DeviceY: Double;
I: Integer;
begin
Prog := TPDFContentProgram.Create;
try
Prog.Parse(ContentBytes);
Runs := Prog.GetTextRuns;
// One before-instruction snapshot per operator, computed in a single pass
States := Prog.TraceGraphicsStates(nil, False);
for I := 0 to High(Runs) do
begin
// OriginX/OriginY already fold in Tm/Td/TD/T*; only the CTM active
// at this instruction is still missing (ISO 32000-1 8.4)
with States[Runs[I].InstructionIndex].CTM do
begin
DeviceX := Runs[I].OriginX * M11 + Runs[I].OriginY * M21 + DX;
DeviceY := Runs[I].OriginX * M12 + Runs[I].OriginY * M22 + DY;
end;
LogTextOrigin(Runs[I].Text, DeviceX, DeviceY); // caller-supplied handler
end;
finally
Prog.Free;
end;
end;
Ο παραπάνω βρόχος απαντά στο πρόβλημα από την αρχή: η TPDFContentProgram.GetTextRuns επιστρέφει τα OriginX και OriginY ήδη διαμορφωμένα μέσω των Tm, Td, TD και T*, ενώ η TraceGraphicsStates(nil, False) παρέχει το ένα κομμάτι που λείπει, το CTM πριν από την εντολή, ακριβώς στον δείκτη όπου καταγράφηκε κάθε run, σε ένα ενιαίο γραμμικό πέρασμα πάνω από ολόκληρο το πρόγραμμα. Περνώντας nil, η μέθοδος αναλαμβάνει έναν ιδιωτικό tracker για την κλήση και τον απελευθερώνει εσωτερικά, η σωστή επιλογή για μια εφάπαξ σάρωση· περνώντας αντ' αυτού μια υπάρχουσα παρουσία TPDFContentStateTracker διατηρείται η κατάσταση συνεχής σε μια σελίδα που συναρμολογείται από περισσότερα από ένα content streams, καθώς το ISO 32000-1 αντιμετωπίζει τον πίνακα /Contents μιας σελίδας ως ένα ενιαίο λογικό stream και η στοίβα q/Q πρέπει να συμφωνεί
Ο πίνακας κειμένου επιβιώνει του Q· η κατάσταση γραφικών όχι
Το ISO 32000-1 §9.4.2 ορίζει τα Td, TD, Tm και T* ως τους τελεστές που χτίζουν τον πίνακα κειμένου και τον πίνακα γραμμής κειμένου μέσα σε ένα μπλοκ BT/ET, και το PDFlibPas κρατά αυτή τη διάκριση ξεκάθαρη: τα Td και TD συνενώνουν μια καθαρή μετάθεση στον πίνακα γραμμής κειμένου, το T* κάνει το ίδιο χρησιμοποιώντας το αρνητικό του τρέχοντος leading, και μόνο το Tm αντικαθιστά εξ ολοκλήρου και τους δύο πίνακες με τους έξι αριθμούς που του δίνονται. Το BT μηδενίζει και τους δύο πίνακες στην ταυτότητα, ακριβώς μία φορά, στην αρχή του αντικειμένου κειμένου — αλλά τα q και Q δεν τους αγγίζουν καθόλου. Η TPDFContentStateTracker.Apply χειρίζεται το coRestoreState ως ειδική περίπτωση ακριβώς γι' αυτόν τον λόγο: πριν αποσωρεύσει την αποθηκευμένη κατάσταση από τη στοίβα, καταγράφει τον τρέχοντα πίνακα κειμένου, τον πίνακα γραμμής κειμένου και τη σημαία BT/ET, και τα επανεφαρμόζει πάνω σε ό,τι τυχόν κρατούσε η αποσωρευμένη κατάσταση, γιατί ένα ζεύγος q/Q που τυλίγει γύρω από ένα run κειμένου δεν πρέπει να μετακινεί πίσω τη θέση του κειμένου
var
Tracker: TPDFContentStateTracker;
Prog: TPDFContentProgram;
I: Integer;
begin
Prog := TPDFContentProgram.Create;
Tracker := TPDFContentStateTracker.Create;
try
Prog.Parse('BT 100 700 Td q 2 0 0 2 0 0 cm (A) Tj Q (B) Tj ET');
for I := 0 to Prog.Count - 1 do
begin
Tracker.Apply(Prog[I]);
if Prog[I].Op in [coShowText, coRestoreState] then
LogState(Prog[I].OpName, Tracker.Snapshot); // caller-supplied handler
end;
finally
Tracker.Free;
Prog.Free;
end;
end;
Εκτελέστε αυτή την ακολουθία και το CTM που αναφέρεται στο δεύτερο Tj έχει επιστρέψει στην κλίμακα ταυτότητας που είχε πριν το q — το 2 0 0 2 0 0 cm μέσα στο ζεύγος save/restore έχει εξαφανιστεί, όπως απαιτεί το q/Q. Το TextMatrix.DX στην ίδια εντολή, όμως, είναι ακόμα 100: το Td που το όρισε εκτελέστηκε πριν το q, άρα δεν είναι κατάσταση γραφικών που το Q είχε ποτέ το δικαίωμα να αγγίξει, και ένα εργαλείο που θεωρούσε το αντίθετο θα ανέφερε το δεύτερο run γλυφών να ξεκινά από λάθος οριζόντια θέση στη σελίδα
Τι συμβαίνει όταν εκτελείται ένας τελεστής μονοπατιού clipping
Ένας τελεστής W ή W* δεν συρρικνώνει αμέσως το clip· απλώς καταγράφει ποιος κανόνας πλήρωσης θα χρησιμοποιηθεί, ενώ η πραγματική τομή περιμένει όποιον τελεστή ζωγραφικής μονοπατιού ακολουθήσει, συμπεριλαμβανομένου του no-op ζωγράφου n που οι δημιουργοί PDF χρησιμοποιούν τακτικά ακριβώς για να κάνουν clip χωρίς να σχεδιάσουν τίποτα. Το TPDFContentStateTracker αντικατοπτρίζει ακριβώς αυτή τη διβηματική χρονική ρύθμιση: τα coClip και coClipEvenOdd απλώς θέτουν μια σημαία εκκρεμούς κανόνα clip, και η EndCurrentPath — που καλείται από κάθε τελεστή ζωγραφικής μονοπατιού — είναι αυτή που πραγματικά τέμνει τα όρια του εκκρεμούς μονοπατιού στα ClipMinX, ClipMinY, ClipMaxX και ClipMaxY. Το να γίνει σωστά αυτή η σταδιοποίηση έχει σημασία για το ίδιο το συμβόλαιο του στιγμιότυπου πριν/μετά: ένα στιγμιότυπο πριν, ληφθέν ακριβώς στην εντολή W, πρέπει ακόμα να δείχνει το παλιό, ευρύτερο clip, γιατί το clip δεν έχει ακόμα ισχύσει σε εκείνο το σημείο του stream, και η συγχώνευση των δύο βημάτων σε ένα θα χαλούσε σιωπηλά κάθε καλούντα που βασίζεται στο ότι η κατάσταση-πριν σημαίνει αυτό που λέει
Το ClipBoundsExact ενημερώνει έναν καλούντα ποια από τις δύο καταστάσεις αντιμετωπίζει, και είναι Αληθές μόνο για ένα μοναδικό ορθογώνιο ευθυγραμμισμένο με τους άξονες, χτισμένο από re πάνω σε κατά τα άλλα κενό μονοπάτι — το μόνο σχήμα που το PDFlibPas μπορεί να αναπαραστήσει ακριβώς ως τέσσερις αριθμούς. Οτιδήποτε άλλο — ένα περιστραμμένο ορθογώνιο, ένα καμπύλο περίγραμμα, ένα σύνθετο μονοπάτι με πολλά υπο-μονοπάτια, ή ένα clip χτισμένο από λειτουργία απόδοσης κειμένου — παράγει ακόμα ClipMinX έως ClipMaxY, αλλά με το ClipBoundsExact μηδενισμένο σε False, ένα ειλικρινές σήμα ότι οι τέσσερις αριθμοί είναι ένα ασφαλές εξωτερικό όριο και όχι το πραγματικό σχήμα clip· καλούντες που χρειάζονται μόνο αυτό το όριο, όπως η απομόνωση μιας ορθογώνιας υπο-περιοχής πριν από τη μετατροπή GDI halftone που περιγράφεται στο απόδοση σελίδων PDF σε μονόχρωμο 1-bit, μπορούν να το διαβάσουν απευθείας αντί να το ξαναϋπολογίσουν από τη γεωμετρία της σελίδας
Καμπύλες Bézier: ένα ακριβές όριο ή ένα ασφαλές
Ο φθηνότερος τρόπος να οριοθετηθεί ένα κυβικό τμήμα Bézier είναι να ληφθεί το κυρτό περίβλημα των τεσσάρων σημείων ελέγχου του, και αυτό είναι πάντα ασφαλές γιατί η καμπύλη ποτέ δεν το ξεπερνά — αλλά μια ρηχή, πλατιά καμπύλη μπορεί να αναφέρει ένα πλαίσιο οριοθέτησης πολύ μεγαλύτερο από αυτό που πραγματικά καταλαμβάνει η καμπύλη, κάτι που αποδυναμώνει το φιλτράρισμα βάσει clip ακριβώς όταν έχει τη μεγαλύτερη σημασία, σε μεγάλα διακοσμητικά μονοπάτια. Το PDFlibPas λύνει αντίθετα το πιο σφιχτό πρόβλημα: για κάθε άξονα, λύνει την παράγωγο της κυβικής καμπύλης για ρίζες μέσα στο ανοιχτό διάστημα (0, 1) και αξιολογεί την καμπύλη σε όποιες ρίζες βρει, μαζί και με τα δύο άκρα, που είναι ο τυπικός κλειστός τρόπος να βρεθεί η πραγματική έκταση μιας καμπύλης ευθυγραμμισμένη με τους άξονες αντί για μια υπερεκτίμηση. Η ακρίβεια ανά καμπύλη όμως δεν μεταφέρεται στο ίδιο το clip: μόλις ένα καμπύλο περίγραμμα γίνει μονοπάτι clipping, το ClipBoundsExact πέφτει και πάλι σε False γι' αυτό, γιατί ένα πλαίσιο οριοθέτησης, όσο σφιχτό κι αν είναι, εξακολουθεί να μην είναι το ίδιο σχήμα με την καμπύλη που οριοθετεί, και ο state tracker προτιμά να το πει παρά να αφήσει έναν καλούντα να υποθέσει ορθογώνιο εκεί όπου στην πραγματικότητα υπάρχει καμπύλη
Ανάγνωση κατάστασης πριν και μετά από κάθε τελεστή
Το αν ένας καλών θέλει την κατάσταση πριν ή μετά εξαρτάται εξ ολοκλήρου από το τι κάνει ο τελεστής: μια ερώτηση σχεδίασης ή hit-testing για ένα μονοπάτι ή run κειμένου θέλει την κατάσταση όπως ήταν τη στιγμή ακριβώς πριν εκτελεστεί εκείνος ο τελεστής, αφού αυτή καθόρισε πραγματικά πώς ζωγράφισε ο τελεστής, ενώ μια διαγνωστική ερώτηση για έναν τελεστή που θέτει κατάσταση όπως το gs συνήθως θέλει να δει τι μόλις άλλαξε. Η TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) εκθέτει ακριβώς αυτή την επιλογή ως ένα μόνο Boolean, υπολογίζοντας ένα TPDFContentGraphicsState ανά εντολή σε ένα γραμμικό πέρασμα πάνω από ολόκληρο το πρόγραμμα, ανεξάρτητα από το ποια στιγμή ζητείται. Η GetGraphicsState(InstructionIndex, AfterInstruction, State) προσφέρει την ίδια επιλογή πριν/μετά για μία μόνο εντολή αντί για ολόκληρο το πρόγραμμα, αλλά αναπαράγει από την εντολή μηδέν σε κάθε κλήση για να φτάσει εκεί, οπότε η σάρωση πολλών δεικτών καλώντας την σε βρόχο κοστίζει O(n²) έναντι μιας μόνο κλήσης O(n) στην TraceGraphicsStates πάνω στο ίδιο πρόγραμμα
var
Before, After: TPDFContentGraphicsState;
begin
// Same instruction index, two different instants: before vs. after it runs
Prog.GetGraphicsState(CmIndex, False, Before);
Prog.GetGraphicsState(CmIndex, True, After);
// Before.CTM reflects every earlier cm; After.CTM already folds in
// this instruction's own concatenation as well
end;
Αντιμετώπιση κακοδιατυπωμένων content streams
Δύο είδη κακοδιατυπωμένης εισόδου είναι αρκετά συχνά σε πραγματικούς παραγωγούς PDF ώστε το TPDFContentStateTracker να πρέπει να τα ανέχεται αντί να αποτυγχάνει σε αυτά. Το πρώτο είναι ένα μονοπάτι που εκτείνεται πέρα από ένα όριο q/Q: το τρέχον μονοπάτι, το τρέχον σημείο και ο αριθμός υπο-μονοπατιών δεν είναι παράμετροι κατάστασης γραφικών — το ISO 32000-1 §8.4 καλύπτει τι αποθηκεύουν και επαναφέρουν τα q και Q, και το υπό κατασκευή τρέχον μονοπάτι δεν περιλαμβάνεται σε αυτά — οπότε το TPDFContentStateTracker παρακολουθεί αυτά τα δεδομένα εντελώς εκτός της αποθηκευμένης κατάστασης, και ένα υπο-μονοπάτι που ξεκίνησε πριν από ένα q εξακολουθεί να υπάρχει, αβαφτο, αμέσως μετά το αντίστοιχο Q. Το δεύτερο είναι ένα γυμνό Q χωρίς αντίστοιχο q πουθενά πριν από αυτό στο stream, όχι σπάνιο σε έξοδο από γεννήτριες που συναρμολογούν θραύσματα content-stream με συνένωση και κάνουν λάθος στη λογιστική. Η TPDFContentStateTracker.RestoreUnderflowCount μετράει κάθε τέτοιο συμβάν αντί να προκαλεί εξαίρεση ή να καταστρέφει την κατάσταση: ένα αταίριαστο Q απλώς αφήνει την τρέχουσα κατάσταση γραφικών ακριβώς όπως ήταν, σαν εκείνη η εντολή να ήταν no-op, ώστε το υπόλοιπο του stream να συνεχίζει να αναπαράγεται πάνω σε λογική κατάσταση και ένας καλών να μπορεί ακόμα να αποφασίσει μετά, από τον μετρητή, αν η είσοδος αξίζει να επισημανθεί πίσω σε όποιον την παρήγαγε
Η σύνθεση CTM, η ανεξαρτησία του πίνακα κειμένου από τα q/Q, και η σταδιακή πραγματοποίηση ενός μονοπατιού clipping δεν εξαρτώνται από το πώς ή αν το content stream ζωγραφίζεται ποτέ, κάτι που είναι ακριβώς το ζητούμενο: το ίδιο στιγμιότυπο του TPDFContentStateTracker είναι σωστό είτε η σελίδα δεν αποδοθεί ποτέ είτε πρόκειται να παραδοθεί σε όποιον backend επιλέξει το PDFlibPas για εκείνο το αρχείο, συμπεριλαμβανομένης της εναλλαγής μηχανών χρόνου εκτέλεσης που καλύπτεται στον οδηγό για απόδοση PDF πολλαπλών μηχανών στο PDFlibPas. Ανάλυση περιεχομένου, χαρτογράφηση συντεταγμένων και εργαλεία redaction μπορούν όλα να τρέξουν εξ ολοκλήρου πάνω στην έξοδο του tracker, πολύ πριν ή εντελώς χωρίς ποτέ να ζητηθεί από κάποιον renderer να εμπλακεί
Η αναπαραγωγή content-stream μέσω του TPDFContentStateTracker είναι μέρος του δομημένου πλαισίου επεξεργασίας περιεχομένου ενσωματωμένου στο PDFlibPas, την εγγενή βιβλιοθήκη VCL για PDF σε Delphi και C++Builder