Οι affine matrices του PDF χρησιμοποιούν τη σύμβαση row-vector του ISO 32000-1 §8.3.3, όπου ένα σημείο πολλαπλασιάζει τον πίνακα από αριστερά: point' = point * M. Στο PDFium Component για Delphi και C++Builder αυτό το ένα γεγονός καθορίζει ολόκληρη την επιφάνεια API του TPdfMatrix: το Multiply προσαρτά, οπότε M := M * Op, ενώ το PreMultiply προθέτει, οπότε M := Op * M
Κάθε κλασικό bug μετασχηματισμού ανάγεται σε αυτή τη φράση θυμημένη ανάποδα. Το υδατογράφημα που περιστρέφεται καθαρά στο αρχείο δοκιμής σου και προσγειώνεται μισό έξω από τη σελίδα στο αρχείο πελάτη. Η μικρογραφία που βγαίνει περιστραμμένη δύο φορές γιατί η σελίδα ήδη έφερε ένα τέταρτο στροφής. Η σφραγίδα της οποίας η μετατόπιση είναι τέλεια σε A4 και παρεκκλίνει σε Letter. Κανένα από αυτά δεν είναι bug απόδοσης· είναι bugs σειράς πολλαπλασιασμού, και όλα διορθώνονται μόλις μπορείς να πεις δυνατά σε ποιον χώρο είναι γραμμένη κάθε λειτουργία
Η σύμβαση row-vector που ορίζει τους κανόνες
Το TPdfMatrix αποθηκεύει τα έξι στοιχεία με ονόματα του spec και τα εφαρμόζει ακριβώς όπως τα ορίζει η μορφή, οπότε ο ίδιος ο μετασχηματισμός είναι εκεί που ξεκινά η συλλογιστική. Το TPdfMatrix.TransformPoint υπολογίζει x' = x*a + y*c + e και y' = x*b + y*d + f, που είναι η μορφή έξι στοιχείων που ορίζει το ISO 32000-1 §8.3.4 για τον τελεστή cm που συνθέτει έναν πίνακα πάνω στον τρέχοντα πίνακα μετασχηματισμού. Το ζεύγος (a, b) είναι η πρώτη γραμμή, το (c, d) η δεύτερη, και το (e, f) η γραμμή μετάθεσης. Οι συνήθειες column-vector που έχεις πάρει από το OpenGL ή από ένα μάθημα γραμμικής άλγεβρας θα σε παραπλανήσουν εδώ, και θα σε παραπλανήσουν σιωπηρά, γιατί ένας πίνακας λάθος σειράς εξακολουθεί να είναι απόλυτα έγκυρος πίνακας. Διάβασε ένα σύνθετο στη σύμβαση row από αριστερά προς τα δεξιά και η σειρά εφαρμογής προκύπτει δωρεάν: αφού το point * (M * Op) ισούται με (point * M) * Op, μια προσαρτημένη λειτουργία δρα πάνω σε συντεταγμένες που ο υπάρχων πίνακας έχει ήδη παράγει, δηλαδή στον χώρο σελίδας, ενώ μια προτεθειμένη λειτουργία δρα πριν τρέξει ο υπάρχων πίνακας, στον δικό του χώρο εισόδου του αντικειμένου
var
M: TPdfMatrix;
Pt: FS_POINTF;
begin
M := TPdfMatrix.Create; // identity
try
// Append order: each call acts on what the previous calls produced.
M.Scale(0.5, 0.5); // M := M * S half size
M.Rotate(90); // M := M * R clockwise, degrees
M.Translate(300, 400); // M := M * T then move on the page
Pt := M.TransformPoint(0, 0); // x*a + y*c + e, x*b + y*d + f
finally
M.Free;
end;
end;
Το TPdfMatrix.Rotate έχει προεπιλογή δεξιόστροφα και σε μοίρες, με τα ACounterClockwise και AAngleInRadians διαθέσιμα όταν τα δεδομένα πηγής σου έχουν το αντίθετο πρόσημο. Οι ιδιότητες μόνο-για-ανάγνωση a έως f και η ιδιότητα Handle σου δίνουν πίσω τον ακατέργαστο FS_MATRIX, που είναι αυτό που θέλει το FPDFPageObj_SetMatrix. Τίποτα στην κλάση δεν σου κρύβει τους έξι αριθμούς, και αυτό είναι σκόπιμο: όταν ένας μετασχηματισμός δεν συμπεριφέρεται σωστά, η εκτύπωση των a έως f είναι η ταχύτερη διάγνωση που έχεις
Γιατί η προθεσιμότητα μιας μετάθεσης χρειάζεται το γραμμικό μέρος;
Γιατί μια προτεθειμένη μετατόπιση είναι γραμμένη στον χώρο εισόδου του πίνακα, και πρέπει να περάσει μέσα από το τρέχον γραμμικό μέρος πριν μπορέσει να ενωθεί με τη γραμμή μετάθεσης. Το TPdfMatrix.PreTranslate επομένως υπολογίζει e := dx*a + dy*c + e και f := dx*b + dy*d + f. Η προσάρτηση είναι η εύκολη κατεύθυνση: το TPdfMatrix.Translate είναι γραμμένο στον χώρο σελίδας, όπου τίποτα δεν χρειάζεται μετατροπή, οπότε απλά προσθέτει dx στο e και dy στο f. Όποιος «βελτιστοποιήσει» το PreTranslate σε δύο προσθέσεις μόλις διέγραψε την περιστροφή και την κλίμακα από τη μετατόπιση
M := TPdfMatrix.Create;
try
M.Rotate(90); // a=0, b=-1, c=1, d=0
M.Translate(10, 0); // append: e := e + 10
// -> 10 points to the right on the page
M.Reset;
M.Rotate(90);
M.PreTranslate(10, 0); // prepend: e := 10*a + 0*c + e (unchanged)
// f := 10*b + 0*d + f (f - 10)
// -> 10 points along the stamp own x axis,
// which after the turn points down the page
finally
M.Free;
end;
Η ίδια ασυμμετρία διατρέχει το ζεύγος κλίμακας, και αξίζει να ξέρεις ποια στοιχεία αγγίζει το καθένα πριν το αποσφαλματώσεις στις τρεις τα ξημερώματα. Το TPdfMatrix.PreScale πολλαπλασιάζει γραμμές, κλιμακώνοντας τα a και b κατά scaleX και τα c και d κατά scaleY, και αφήνει τη μετάθεση ανέγγιχτη γιατί η μετατόπιση έχει ήδη συμβεί πιο κάτω στη ροή. Το προσαρτημένο TPdfMatrix.Scale πολλαπλασιάζει στήλες αντ' αυτού, παίρνοντας τα a, c, e κατά scaleX και τα b, d, f κατά scaleY, οπότε η υπάρχουσα μετατόπιση κλιμακώνεται μαζί με όλα τα υπόλοιπα. Και τα δύο είναι μονοπάτια μοναδικού σκοπού που παρακάμπτουν το γενικό γινόμενο έξι στοιχείων, και και τα δύο διατηρούν τη σημασιολογία σύνθεσης της γενικής μορφής ακριβώς
Πού πηγαίνουν οι δύο μεταθέσεις σε μια περιστροφή pivot;
Γύρω από τη λειτουργία, όχι γύρω από ολόκληρο τον πίνακα, και με αυτή τη σειρά. Το TPdfMatrix.RotateAt προσαρτά Translate(-pivot), μετά την περιστροφή, μετά Translate(+pivot), που κάτω από τη σύμβαση row-vector συντίθεται ως Translate(-pivot) * Op * Translate(pivot). Αυτή η ακολουθία είναι αυτό που κρατά το pivot σταθερό κάτω από τη νέα λειτουργία ενώ ακόμα επιτρέπει στον υπάρχοντα πίνακα να παράγει πρώτα τις συντεταγμένες του και να τις παραδώσει. Γράψε το ζεύγος με την άλλη σειρά, όπως θα ήταν σωστό σε μια βιβλιοθήκη column-vector, και το αντικείμενο περιφέρεται γύρω από την αρχή αντί να στριφογυρίζει επί τόπου, που είναι ακριβώς πώς ένα κεντραρισμένο υδατογράφημα καταλήγει έξω από το crop box
procedure RotateStampAboutPageCenter(AObj: FPDF_PAGEOBJECT;
const AAngleDegrees, APageWidth, APageHeight: Single);
var
M: TPdfMatrix;
Raw: FS_MATRIX;
begin
if not FPDFPageObj_GetMatrix(AObj, Raw) then
raise Exception.Create('Page object carries no matrix');
M := TPdfMatrix.Create(Raw);
try
// Appends Translate(-pivot) * Rotate * Translate(+pivot) in one call.
M.RotateAt(AAngleDegrees, APageWidth / 2, APageHeight / 2);
Raw := M.Handle;
FPDFPageObj_SetMatrix(AObj, Raw);
finally
M.Free;
end;
end;
Η ίδια σύνθεση υποστηρίζει τα ScaleAt, SkewAt, HorizontalFlipAt, VerticalFlipAt, και CentralFlipAt, οπότε μόλις εμπιστευτείς το μοτίβο για την περιστροφή μπορείς να το εμπιστευτείς και για τα υπόλοιπα. Το TPdfMatrix.CentralFlip αξίζει να ξεχωρίσει: αρνητικοποιεί και τα έξι στοιχεία για να σου δώσει μια στροφή 180 μοιρών χωρίς καθόλου τριγωνομετρία, που σημαίνει κανένα cos μιας τιμής που θα έπρεπε να είναι ακριβώς μηδέν και καμία συσσωρευόμενη παρέκκλιση όταν το εφαρμόζεις σε βρόχο. Αν τοποθετείς επαναλαμβανόμενα σημάδια αντί να γυρίζεις ένα, η μηχανική της ίδιας της τοποθέτησης καλύπτεται στα επαναχρησιμοποιήσιμα page stamps με Form XObjects, και η δουλειά πινάκων εδώ κάθεται ακριβώς από πάνω της
Τι σου λέει το TryDecompose για έναν πίνακα;
Το TPdfMatrix.TryDecompose αναφέρει μετάθεση, κλίμακα, περιστροφή, διάτμηση, ορίζουσα και μια σημαία ανάκλασης κάτω από μια σύμβαση κλίμακα-μετά-περιστροφή, και τα αναφέρει αρκετά ειλικρινά ώστε να είναι χρήσιμα για αποφάσεις παρά μόνο για καταγραφή. Το ScaleX προέρχεται από το μήκος της πρώτης γραμμής, Sqrt(a*a + b*b), οπότε είναι πάντα θετικό. Το ScaleY είναι μετά Determinant / ScaleX, που το κάνει προσημασμένο. Η περιστροφή προέρχεται από ArcTan2(-b, a) σε μοίρες, και η διάτμηση από το εσωτερικό γινόμενο των δύο γραμμών κανονικοποιημένο και από τις δύο κλίμακες
Εκείνο το πρόσημο στο ScaleY είναι το μέρος που ο κόσμος διαγράφει, και η διαγραφή του είναι πραγματικό bug παρά κοσμητικό. Μια αρνητική ορίζουσα σημαίνει ότι ο πίνακας περιέχει ανάκλαση. Ανάγκασε και τους δύο συντελεστές κλίμακας να είναι θετικοί για να κάνεις τους αριθμούς να φαίνονται πιο τακτοποιημένοι και έχεις πετάξει την ανάκλαση, οπότε ένας πίνακας ανακατασκευασμένος από την αποσύνθεση επιστρέφει καθρεφτισμένος: το κείμενο διαβάζεται ανάποδα, μια σαρωμένη σελίδα αναποδογυρίζει, ένα εισαγόμενο λογότυπο κοιτάζει προς τη λάθος κατεύθυνση. Το πεδίο IsReflected υπάρχει ώστε να μη χρειάζεται ποτέ να το συμπεράνεις. Αυτός είναι επίσης ο έλεγχος που εμποδίζει την κλασική διπλή περιστροφή, όπου ο κώδικας προσθέτει μια στροφή προβολής σε μια σελίδα που ήδη φέρει μία· η εκδοχή από την πλευρά του viewer αυτού του προβλήματος επεξεργάζεται στο προσαρμογή μικρογραφίας, zoom, και διπλή περιστροφή
var
D: TPdfMatrixDecomposition;
begin
if M.TryDecompose(D) then
begin
// D.ScaleX is always positive; D.ScaleY carries the determinant sign.
if D.IsReflected then
Log('mirrored, ScaleY = %.3f', [D.ScaleY]);
if Abs(D.RotationDegrees) > 0.5 then
SkipDisplayRotation; // the object already carries its own turn
end
else
UseIdentityFallback; // near-singular or non-finite: no answer
end;
Προσαρμογή ενός ορθογωνίου μέσα σε άλλο χωρίς μαντεψιά
Το TPdfMatrix.TryCreateRectMapping χτίζει τον πίνακα πηγής-προς-προορισμό για σένα και παίρνει ένα TPdfMatrixFitMode από pmfStretch, pmfContain, ή pmfCover. Κανονικοποιεί πρώτα και τα δύο ορθογώνια, γιατί τα ορθογώνια PDF δεν απαιτείται να φτάνουν με το αριστερό κάτω από το δεξί ή το κάτω κάτω από το πάνω, μετά παράγει ανεξάρτητες κλίμακες X και Y: το pmfStretch τις κρατά ανεξάρτητες, το pmfContain παίρνει τη μικρότερη και κεντράρει το letterbox, το pmfCover παίρνει τη μεγαλύτερη και κεντράρει την περικοπή. Η συνοδευτική MapRectToRect προσαρτά την ίδια αντιστοίχιση πάνω σε έναν υπάρχοντα πίνακα, και η NewRectMapping σηκώνει EPdfMatrixError εκεί όπου η μορφή Try επιστρέφει False. Αυτή είναι η πρωτόγονη λειτουργία κάτω από κάθε τοποθέτηση κελιού στην N-up imposition και αναδιάταξη σελίδων, όπου κάθε σελίδα πηγής πρέπει να προσγειωθεί μέσα σε ένα υπολογισμένο κελί χωρίς να ξαναπαράγεις την αριθμητική ανά διάταξη
Εκφυλισμένοι πίνακες και το ειλικρινές μονοπάτι αποτυχίας
Πεπερασμένες είσοδοι δεν εγγυώνται πεπερασμένο αποτέλεσμα, οπότε ο κώδικας προσαρμογής υπολογίζει σε Double και μετά ξανα-ελέγχει τον στενεμένο υποψήφιο Single για πεπερασμένο πριν τον δημοσιεύσει· μια αντιστοίχιση που περιέχει άπειρο δεν επιστρέφεται ποτέ σαν να ήταν έγκυρη. Η ίδια πειθαρχία διέπει την αντιστροφή. Το TPdfMatrix.TryGetInverse απορρίπτει έναν πίνακα χρησιμοποιώντας σχετικό κατώφλι, συγκρίνοντας την ορίζουσα έναντι του epsilon επί το τετράγωνο του μεγαλύτερου γραμμικού στοιχείου αντί έναντι μιας σταθερής σταθεράς, που είναι αυτό που κρατά τον έλεγχο ουσιαστικό είτε οι μονάδες σου είναι στιγμές είτε μικρόμετρα. Το TryDecompose εγκαταλείπει με τον ίδιο τρόπο, αρνούμενο όταν το μήκος της πρώτης γραμμής ή το παραγόμενο ScaleY πέφτει στο ή κάτω από το epsilon
Επίλεξε το στυλ αποτυχίας που ταιριάζει στο σημείο κλήσης αντί να τα τυλίγεις όλα σε try-except από συνήθεια. Τα TryInvert, TryGetInverse, TryInverseTransformPoint, TryTransformBounds και TryCreateRectMapping επιστρέφουν False και αφήνουν τους στόχους τους ανέγγιχτους, κάτι που ταιριάζει σε δοκιμή χτυπήματος και βρόχους ανά αντικείμενο όπου ένα εκφυλισμένο αντικείμενο πρέπει να παραλείπεται, όχι να είναι μοιραίο. Τα Invert, InverseCopy, InverseTransformPoint, MapRectToRect και TransformBounds σηκώνουν EPdfMatrixError αντ' αυτού, κάτι που ταιριάζει σε κώδικα ρύθμισης όπου ένας ενικός πίνακας σημαίνει ότι ο καλών υπολόγισε κάτι λάθος. Για δουλειά batch, τα TransformPoints και TransformRects εκχωρούν τον πίνακα αποτελέσματός τους ακριβώς μία φορά, τα TransformPointsInPlace και TransformRectsInPlace επαναχρησιμοποιούν τη δική σου αποθήκευση, και το TryTransformBounds συσσωρεύει το πλαίσιο οριοθέτησης σε ένα μοναδικό πέρασμα αντί να υλοποιεί πρώτα μετασχηματισμένα σημεία
Τίποτα από αυτά δεν είναι εξωτικά μαθηματικά. Είναι μία σύμβαση, εφαρμοσμένη με συνέπεια, με το API ονομασμένο ώστε η σύμβαση να είναι ορατή στο σημείο κλήσης: το Multiply και τα απλά ρήματα προσαρτούν, η οικογένεια Pre προθέτει, η οικογένεια At περικλείει τη λειτουργία με το ζεύγος pivot της. Γράψε τη σειρά σε ένα σχόλιο δίπλα σε κάθε σύνθετο που χτίζεις, γιατί ο κώδικας που διαβάζεται σωστά σήμερα είναι ο κώδικας που κάποιος θα αντιστρέψει σε έξι μήνες. Η πλήρης αναφορά TPdfMatrix, μαζί με τα APIs αντικειμένων σελίδας και απόδοσης στα οποία τροφοδοτούν αυτοί οι μετασχηματισμοί, βρίσκεται στο PDFium Component για Delphi και C++Builder