Όταν η FPDFPage_TransFormWithClip ξαναγράφει μια σελίδα, κάθε χειριστής FPDF_PAGEOBJECT που ήδη κρατάτε εξακολουθεί να περιγράφει την ανάλυση από πριν το transform. Το PDFium Component για Delphi και C++Builder το λύνει αυτό μέσα στην TransformPageContent, που ξεφορτώνει τη σελίδα κειμένου, αναγεννά περιεχόμενο, μετά επαναφορτώνει τη σελίδα ώστε μεταγενέστερα ερωτήματα να βλέπουν τις νέες συντεταγμένες
Το σύμπτωμα είναι ήσυχο. Εφαρμόζετε μια κλίμακα 0,9 για να προσθέσετε ένα περιθώριο εκτύπωσης, μετά διαβάζετε το PageObjectInfo και παίρνετε ακριβώς τους αριθμούς που πήρατε πριν την κλήση. Καμία εξαίρεση, κανένας κωδικός σφάλματος, τίποτα σε ένα log. Αυτή είναι διαφορετική αποτυχία από την cache σελίδας κειμένου που περιγράφεται στο άρθρο για μπαγιάτικες σελίδες κειμένου μετά από επεξεργασία: εκεί η cache είναι ένας μεμονωμένος χειριστής FPDF_TEXTPAGE που μπορείτε να αφήσετε και να ξαναχτίσετε, εδώ το πρόβλημα είναι κάθε χειριστής αντικειμένου σελίδας στις δικές σας μεταβλητές, συν μια κατηγορία getters που αναφέρουν αποτυχία μέσω κωδικού επιστροφής που οι περισσότεροι καλούντες πετούν
Γιατί τα όρια αντικειμένου σελίδας γίνονται μπαγιάτικα χωρίς σφάλμα;
Επειδή ένας χειριστής αντικειμένου σελίδας είναι ένας δείκτης σε μια αναλυμένη αναπαράσταση ενός συγκεκριμένου ρεύματος περιεχομένου, και ένα transform ολόκληρης σελίδας αντικαθιστά εκείνο το ρεύμα περιεχομένου με νέο. Το PDFium δεν περπατά τη στοίβα κλήσεών σας ψάχνοντας χειριστές για να επιδιορθώσει. Χτίζει ένα φρέσκο γράφημα αντικειμένων και αφήνει το παλιό ακριβώς όπως ήταν, οπότε μια ανάγνωση έναντι του παλιού χειριστή είναι μια απόλυτα έγκυρη ανάγνωση μιας δομής που δεν αντιστοιχεί πια σε αυτό που λέει το αρχείο
Το ISO 32000-1 §7.8.2 ορίζει το ρεύμα περιεχομένου ως την ακολουθία τελεστών που σχεδιάζει μια σελίδα, και το §8.3.3 ορίζει πώς ο τρέχων πίνακας μετασχηματισμού απεικονίζει τον χώρο χρήστη στον χώρο συσκευής. Ένα transform επιπέδου σελίδας εκφράζεται τυλίγοντας και ξαναγράφοντας αυτούς τους τελεστές, όχι επεξεργαζόμενο συντεταγμένες ανά αντικείμενο επί τόπου. Οπότε οι συντεταγμένες που φέρουν τα αντικείμενα μπορεί να μην αλλάζουν καθόλου· αυτό που αλλάζει είναι ο πίνακας σε ισχύ όταν σχεδιάζονται. Οποιοσδήποτε χειριστής αναλύθηκε κάτω από τον παλιό πίνακα απαντά ερωτήματα γεωμετρίας κάτω από τον παλιό πίνακα, και τα απαντά χωρίς παράπονο
Τι ξαναγράφει στην πραγματικότητα η FPDFPage_TransFormWithClip
Ξαναγράφει τη σελίδα, όχι τις στιγμιαίες εικόνες σας. Η FPDFPage_TransFormWithClip παίρνει έναν FS_MATRIX και ένα ορθογώνιο clip FS_RECTF και εφαρμόζει και τα δύο σε ολόκληρο το περιεχόμενο σελίδας. Είναι η σωστή κλήση για περιθώρια, κλιμάκωση imposition, και κανονικοποίηση μιας παράξενα μεγεθυμένης σελίδας έναντι ενός στοχευμένου κουτιού. Είναι η λάθος κλήση στην οποία να απλωθείτε αν περιμένετε τους υπάρχοντες χειριστές να ακολουθήσουν, και αξίζει επίσης να θυμάστε ότι αγγίζει μόνο το περιεχόμενο σελίδας: οι σημειώσεις είναι ξεχωριστό στρώμα και χρειάζονται TransformPageAnnotations, που προωθεί τους ίδιους έξι συντελεστές πίνακα στην FPDFPage_TransformAnnots
var
Info: TPdfPageObjectInfo;
Scale: FS_MATRIX;
Clip: TPdfRectangle;
begin
Pdf.PageNumber:= 1;
Info:= Pdf.PageObjectInfo(0); // snapshot taken before the transform
Scale.a:= 0.9; Scale.b:= 0.0;
Scale.c:= 0.0; Scale.d:= 0.9;
Scale.e:= 29.7; Scale.f:= 42.0; // 5% margin, A4 in points
Clip:= Pdf.GetPageBox(pbMedia);
Pdf.TransformPageContent(Scale, Clip);
// Info.Bounds still holds pre-transform geometry, and Info.Handle now
// points into a page that TransformPageContent has already replaced
end;
Η σειρά ανανέωσης που χρησιμοποιεί η TransformPageContent
Τέσσερα βήματα, με αυτή τη σειρά: ξεφόρτωση της σελίδας κειμένου, transform, δημιουργία περιεχομένου, επαναφόρτωση της σελίδας. Η TPdf.TransformPageContent τρέχει ακριβώς αυτή την ακολουθία. Καλεί την CheckPageActive, αντιγράφει τον πίνακα και το clip στις εγγενείς μορφές εγγραφών τους, καλεί την UnloadTextPage, μετά την FPDFPage_TransFormWithClip, μετά την UpdatePage, που είναι ο wrapper γύρω από την FPDFPage_GenerateContent, και τέλος την ReloadPage
Κάθε βήμα κερδίζει τη θέση του. Η UnloadTextPage πάει πρώτη επειδή η cache FPDF_TEXTPAGE κρατά κουτιά χαρακτήρων υπολογισμένα κάτω από τον παλιό πίνακα, και επίσης αφαιρεί την παράγωγη λίστα web-link και οποιαδήποτε συνεδρία αναζήτησης σε εξέλιξη που χτίστηκαν από αυτήν. Η FPDFPage_GenerateContent πρέπει να τρέξει πριν την επαναφόρτωση, επειδή το transform ζει στη σελίδα στη μνήμη μέχρι να σειριοποιηθεί πίσω στο ρεύμα περιεχομένου, και μια επαναφόρτωση διαφορετικά θα ξανα-ανάλυε το μη τροποποιημένο ρεύμα. Η ReloadPage κλείνει με FPDF_LoadPage έναντι του τρέχοντος δείκτη σελίδας, που είναι το μόνο πράγμα που στην πραγματικότητα σας δίνει ένα φρέσκο γράφημα αντικειμένων
// After the transform, re-enumerate. Do not reuse anything captured earlier.
var
I: Integer;
Info: TPdfPageObjectInfo;
begin
Pdf.TransformPageContent(Scale, Clip); // unload text page, transform,
// generate content, reload page
for I:= 0 to Pdf.ObjectCount- 1 do
begin
Info:= Pdf.PageObjectInfo(I); // handle and bounds from the new parse
if Info.Bounds.Right> PageWidth then
Log('object '+ IntToStr(I)+ ' still overflows after scaling');
end;
end;
Μια λεπτομέρεια στην ReloadPage αξίζει να αντιγραφεί αν ποτέ γράψετε αυτή την ακολουθία μόνοι σας. Φορτώνει πρώτα τη νέα σελίδα και μόνο μετά τη δεσμεύει στο πεδίο, οπότε μια φόρτωση σελίδας που αποτυγχάνει αφήνει την τρέχουσα εγγενή σελίδα και όλες τις παράγωγες caches της άθικτες αντί να σας ρίχνει σε μια μισό-κατεδαφισμένη κατάσταση. Η επαναφόρτωση δεν είναι δωρεάν — πληρώνετε για μια πλήρη επαναανάλυση της σελίδας — αλλά πληρώνεται μία φορά ανά transform, όχι μία φορά ανά ερώτημα, και δεν υπάρχει φθηνότερη σωστή εναλλακτική
Μη μεταφέρετε χειριστές μέσα από την επαναφόρτωση
Μετά την επαναφόρτωση, οι παλιοί χειριστές δεν είναι απλώς μπαγιάτικοι, είναι κρεμασμένοι. Το προηγούμενο FPDF_PAGE έχει κλείσει, και οι τιμές FPDF_PAGEOBJECT που του ανήκαν είναι δείκτες σε απελευθερωμένη μνήμη. Το TPdfPageObjectInfo εκθέτει τον εγγενή χειριστή στο πεδίο Handle του, που είναι γνησίως χρήσιμο για να περάσετε ένα αντικείμενο απευθείας σε μια κλήση χαμηλότερου επιπέδου, και εξίσου γνησίως επικίνδυνο να κρατήσετε σε ένα πεδίο φόρμας ή μια λίστα κατά μήκος μιας λειτουργίας που επαναφορτώνει τη σελίδα. Αντιμετωπίστε μια εγγραφή στιγμιαίας εικόνας ως έγκυρη μόνο μέχρι την επόμενη κλήση που αναγεννά περιεχόμενο, στο ίδιο πνεύμα με τους κανόνες κυριότητας που συζητούνται στις σημειώσεις για ABI και ασφάλεια μνήμης στο όριο PDFium
Μπορεί ένα getter να αποτύχει και ακόμη να μοιάζει με έγκυρα δεδομένα;
Ναι, και αυτό είναι το δεύτερο μισό του ίδιου προβλήματος. Οι FPDFPageObj_GetRotatedBounds και FPDFPageObj_GetIsActive είναι getters με out-παράμετρο: επιστρέφουν μια σημαία επιτυχίας int και γράφουν την πραγματική απάντηση σε ένα όρισμα αναφοράς. Και οι δύο μπορούν να επιστρέψουν FALSE για ένα αντικείμενο που δημιουργήθηκε αλλά του οποίου η σελίδα δεν έχει ξανα-αναλυθεί ακόμη. Όταν αυτό συμβαίνει η out παράμετρος μένει ανέγγιχτη, και μια εγγραφή Pascal αρχικοποιημένη με Default(TPdfPageObjectInfo) είναι όλα μηδενικά, οπότε ο καλών βλέπει ένα τετράπλευρο με τέσσερα σημεία στην αρχή και μια σημαία Active False. Μια αποτυχημένη κλήση έχει προαχθεί σιωπηλά σε ευλογοφανή δεδομένα
Το TPdfPageObjectInfo απαντά σε αυτό με ρητά sentinels. Το HasRotatedBounds φέρει το αποτέλεσμα της κλήσης FPDFPageObj_GetRotatedBounds, το HasActiveState φέρει το αποτέλεσμα της FPDFPageObj_GetIsActive, και τα πεδία γεωμετρίας και κατάστασης γράφονται μόνο όταν το αντίστοιχο sentinel είναι True. Το ίδιο σχήμα επαναλαμβάνεται κατά μήκος της εγγραφής για τα άλλα getters με out-παράμετρο, οπότε τα HasMatrix, HasFillColor, HasStrokeColor, και HasStrokeWidth όλα σημαίνουν το ίδιο πράγμα: η εγγενής κλήση πέτυχε και το γειτονικό πεδίο έχει νόημα
Info:= Pdf.PageObjectInfo(I);
if Info.HasRotatedBounds then
// RotatedBounds is array [1..4] of TPdfPoint, in draw order
UseQuad(Info.RotatedBounds[1], Info.RotatedBounds[2],
Info.RotatedBounds[3], Info.RotatedBounds[4])
else
// the native call failed; fall back to the axis-aligned rectangle
UseRect(Info.Bounds);
if Info.HasActiveState and (not Info.Active) then
SkipObject(I); // genuinely inactive
// if HasActiveState is False, the object state is unknown, not inactive
Το μοτίβο γενικεύεται σε κάθε getter PDFium που ακολουθεί τη σύμβαση κωδικού-επιστροφής-συν-out-παραμέτρου, και υπάρχουν πολλά από αυτά. Αν ένας wrapper συμπτύσσει αυτή τη σύμβαση σε ένα απλό αποτέλεσμα συνάρτησης, έχει πετάξει το μόνο σήμα που διακρίνει «η απάντηση είναι μηδέν» από «δεν υπάρχει απάντηση». Η μεταφορά ενός επιπλέον boolean ανά πεδίο κοστίζει ένα byte και αφαιρεί μια ολόκληρη κατηγορία σφάλματος όπου μια εγγραφή με προεπιλεγμένες τιμές μπερδεύεται με μια μέτρηση
Πού εξακολουθεί να δαγκώνει αυτό
Τρία ειλικρινή όρια. Πρώτον, η ανανέωση είναι ανά σελίδα: το transform της σελίδας δύο και όσοι χειριστές κρατάτε για τη σελίδα ένα δεν επηρεάζονται, αλλά τώρα έχετε δύο σελίδες αναλυμένες σε διαφορετικές στιγμές και είναι δική σας δουλειά να θυμάστε ποιες στιγμιαίες εικόνες προήλθαν από ποια. Δεύτερον, η σταθερότητα δείκτη δεν εγγυάται κατά μήκος μιας αναγέννησης περιεχομένου — μετά την επαναφόρτωση, ο δείκτης 3 είναι ό,τι είναι ο δείκτης 3 στη νέα ανάλυση, οπότε επαναταυτίστε αντικείμενα κατά τύπο και γεωμετρία τους αντί να υποθέτετε ότι κρατήθηκαν θέσεις. Τρίτον, το ορθογώνιο clip στην FPDFPage_TransFormWithClip εφαρμόζεται στο περιεχόμενο σελίδας και δεν αλλάζει μέγεθος σε κανένα από τα κουτιά σελίδας· αν κλιμακώσετε το περιεχόμενο προς τα κάτω για να δημιουργήσετε ένα περιθώριο, το MediaBox εξακολουθεί να είναι το μέγεθος που πάντα ήταν, και ένας viewer θα δείξει το αρχικό φύλλο με το σχέδιο συρρικνωμένο μέσα του. Τίποτα από αυτά δεν είναι εξωτικό — είναι η συνηθισμένη συνέπεια ενός API C που δίνει δείκτες σε αναλυμένη κατάσταση και αφήνει τη διάρκεια ζωής στον καλούντα. Η διόρθωση είναι αυτή που δουλεύει παντού αλλού: ορίστε ακριβώς πότε λήγει μια στιγμιαία εικόνα, ανανεώστε σε εκείνο το όριο, και ποτέ μην αφήνετε μια αποτυχημένη κλήση να παρουσιάζεται ως τιμή
Αν δουλεύετε τη συμπεριφορά πινάκων πιο γενικά, η σειρά πολλαπλασιασμού που αποφασίζει πού καταλήγει ένα transform καλύπτεται στο άρθρο για prepend, append, και pivot με πίνακες. Τα API transform και αντικειμένου σελίδας που περιγράφονται εδώ διατίθενται με το PDFium Component για Delphi και C++Builder, του οποίου η σελίδα προϊόντος φέρει την πλήρη αναφορά για την εγγραφή στιγμιαίας εικόνας αντικειμένου σελίδας και τα πεδία sentinel της