Η συνάρτηση FPDF_RenderPageBitmap του PDFium Component δέχεται ένα όρισμα rotate που το PDFium πάντα προσθέτει πάνω σε όποια περιστροφή ήδη φέρει η σελίδα στη δική της καταχώριση /Rotate, οπότε η ανάγνωση της αποθηκευμένης περιστροφής μιας σελίδας και η επαναδιοχέτευση της ίδιας τιμής στην κλήση απόδοσης περιστρέφει τη σελίδα δύο φορές. Το πανομοιότυπο λάθος εμφανίζεται στα μαθηματικά fit-zoom: η προσαρμογή μεγέθους μιας μικρογραφίας από το μη περιστραμμένο πλάτος και ύψος της σελίδας παράγει τη λάθος αναλογία διαστάσεων όποτε το /Rotate είναι 90 ή 270 μοίρες, επειδή το αποδοθέν bitmap βγαίνει με το πλάτος και το ύψος εναλλαγμένα
Η αποτυχία είναι εύκολο να εντοπιστεί μόλις ξέρετε τι να αναζητήσετε, και εύκολο να διαφύγει μέχρι τότε. Μια παρτίδα σαρωμένων τιμολογίων φτάνει με ένα μείγμα πρωτοτύπων πορτραίτου και τοπίου, κάποιος ισιώνει τα μισά με μια περιστροφή 90 μοιρών στο Acrobat πριν την αρχειοθέτηση, και η λωρίδα μικρογραφιών σε έναν viewer Delphi χτισμένο πάνω στο PDFium αποδίδει εκείνες τις συγκεκριμένες σελίδες στο πλάι, ανάποδα, ή στριμωγμένες σε ένα κουτί σχεδιασμένο για τη λάθος προσανατολισμό. Τίποτα δεν εγείρει εξαίρεση. Τίποτα δεν καταγράφει σφάλμα. Τα pixel είναι απλώς λάθος, και μόνο για το υποσύνολο σελίδων που κάποιος περιέστρεψε εκ των υστέρων — ακριβώς το είδος σφάλματος που επιβιώνει από ένα πλήρες πέρασμα QA έναντι ενός μη περιστραμμένου δοκιμαστικού PDF και έπειτα εμφανίζεται στην παραγωγή στη σελίδα 47 ενός πραγματικού
Γιατί το PDFium περιστρέφει τη σελίδα δύο φορές;
Το PDFium εφαρμόζει τη δική της τιμή /Rotate μιας σελίδας αυτόματα κάθε φορά που αποδίδει ένα bitmap, ανεξάρτητα από το τι περνιέται στον αποδότη. Η παράμετρος rotate του FPDF_RenderPageBitmap, εκτεθειμένη στο PDFiumPas ως οι τιμές TRotation ro0, ro90, ro180 και ro270 στα TPdf.RenderPage, TPdf.RenderTile και TPdf.RenderPageThumbnail, δεν ορίζει τη γωνία στην οποία θα έπρεπε να καταλήξει μια σελίδα· η παράμετρος rotate ορίζει πόση επιπλέον περιστροφή να στρωθεί πάνω σε ό,τι ήδη προσδιορίζει το λεξικό σελίδας, γι' αυτό καθεμία από εκείνες τις μεθόδους την προεπιλέγει σε ro0
Το TPdf.PageRotation διαβάζει την ίδια τιμή /Rotate μέσω FPDFPage_GetRotation, και ο κώδικας εφαρμογής συχνά τη χρειάζεται για λόγους που δεν έχουν καμία σχέση με την απόδοση, όπως η απόφαση πώς να διατάξει μια annotation σε χώρο σελίδας. Η παγίδα είναι μία γραμμή: η μετάδοση του PageRotation στο όρισμα Rotation του RenderPage, αναμένοντας η κλήση να κανονικοποιήσει τη σελίδα σε όρθια. Μια σελίδα ήδη αποθηκευμένη με /Rotate 90 εμφανίζεται σωστά, περιστραμμένη, σε οποιονδήποτε συμμορφούμενο viewer, το PDFium συμπεριλαμβανομένου· προσθέστε ξανά ro90 πάνω από αυτό και η σελίδα στρέφεται στις 180 μοίρες αντί για τις σκοπούμενες 90, ενώ μια σελίδα χωρίς καθόλου περιστροφή περιστρέφεται κατά ένα ανεπιθύμητο τέταρτο χωρίς λόγο
// Wrong: PageRotation already reflects /Rotate, and PDFium applies
// it automatically on every render -- passing it again as Rotation
// doubles the angle
Bitmap := Pdf.RenderPage(0, 0, TargetW, TargetH, Pdf.PageRotation, []);
// Right: leave Rotation at its ro0 default and let PDFium apply the
// page's own /Rotate exactly once
Bitmap := Pdf.RenderPage(0, 0, TargetW, TargetH, ro0, []);
Για τι χρησιμεύει πράγματι η παράμετρος Rotation
Η παράμετρος Rotation κερδίζει τη θέση της στο API για μια γνησίως διαφορετική δουλειά: την προσθήκη μιας περιστροφής μόνο-για-προβολή που δεν έχει καμία σχέση με τον αποθηκευμένο προσανατολισμό μιας σελίδας, το είδος που εφαρμόζει ένα κουμπί εργαλειοθήκης περιστροφής-όψης χωρίς να αγγίζει το υποκείμενο αρχείο. Το TPdfView κρατά τις δύο έννοιες ως δύο ξεχωριστές ιδιότητες ακριβώς για αυτόν τον λόγο. Το TPdfView.PageRotation αντικατοπτρίζει τη δική της /Rotate της σελίδας και, μέσω FPDFPage_SetRotation, μπορεί να γράψει μια νέα τιμή πίσω στο έγγραφο· το TPdfView.Rotation είναι μια παροδική, μόνο-για-προβολή ιδιότητα που προεπιλέγει σε ro0 και ποτέ δεν αγγίζει το αρχείο. Το να διαβάσετε την πρώτη ιδιότητα και να τη γράψετε στη δεύτερη είναι ολόκληρο το σφάλμα σε μία πρόταση
// View-only: rotates what the user sees, changes nothing in the file
procedure TViewerForm.RotateViewClick(Sender: TObject);
begin
case PdfView.Rotation of
ro0: PdfView.Rotation := ro90;
ro90: PdfView.Rotation := ro180;
ro180: PdfView.Rotation := ro270;
ro270: PdfView.Rotation := ro0;
end;
end;
// Persistent: rewrites the page's own /Rotate entry in the document
procedure TViewerForm.RotatePageClick(Sender: TObject);
begin
case PdfView.PageRotation of
ro0: PdfView.PageRotation := ro90;
ro90: PdfView.PageRotation := ro180;
ro180: PdfView.PageRotation := ro270;
ro270: PdfView.PageRotation := ro0;
end;
end;
Γιατί σπάει με τον ίδιο τρόπο η προσαρμογή μεγέθους fit-zoom;
Η προσαρμογή μεγέθους fit-zoom σπάει για έναν κατοπτρικό λόγο: ο υπολογισμός ξεκινά από το λάθος ζεύγος αριθμών αντί από τη λάθος γωνία. Ένας τυπικός τρόπος να προσαρμοστεί το μέγεθος ενός κουτιού μικρογραφίας ζητά από το PDFium το πλάτος και το ύψος μιας σελίδας, συγκρίνει εκείνη την αναλογία διαστάσεων με το διαθέσιμο κουτί, και υπολογίζει το μεγαλύτερο ορθογώνιο που χωρά μέσα του — κάτι που λειτουργεί καθαρά για μια μη περιστραμμένη σελίδα. Ο ίδιος υπολογισμός αποτυγχάνει σιωπηλά για μια σελίδα /Rotate 90 ή /Rotate 270 όταν το πλάτος και το ύψος προήλθαν από μια κλήση που αναφέρει το εγγενές, μη περιστραμμένο μέγεθος της σελίδας: μια σελίδα πορτραίτου A4 που φέρει /Rotate 90 εξακολουθεί να αναφέρει περίπου 595 επί 842 σημεία, παρότι το PDFium την αποδίδει, σωστά, σε περίπου 842 επί 595 μόλις εφαρμοστεί η περιστροφή, και ένα κουτί fit υπολογισμένο από το μη περιστραμμένο ζεύγος καταλήγει σχηματισμένο εντελώς για λάθος προσανατολισμό
Το FPDF_GetPageSizeByIndex είναι ένα συγκεκριμένο παράδειγμα μιας κλήσης που αναφέρει εκείνο το εγγενές, μη περιστραμμένο μέγεθος εκ σχεδιασμού, κάτι που το κάνει βολικό για τη σάρωση διαστάσεων σελίδας χωρίς φόρτωση κάθε σελίδας και επικίνδυνο για μαθηματικά fit-zoom που ξεχνούν να το λάβουν υπόψη. Η διόρθωση ακολουθεί απευθείας από την ονομασία του προβλήματος: ελέγξτε την περιστροφή της σελίδας πριν κάνετε την αριθμητική fit, εναλλάξτε πλάτος και ύψος όποτε εκείνη η περιστροφή είναι 90 ή 270 μοίρες, υπολογίστε το κουτί fit από το εναλλαγμένο ζεύγος, και εξακολουθήστε να περνάτε ro0 στην πραγματική κλήση απόδοσης, επειδή το PDFium παραμένει αυτό που εφαρμόζει την πραγματική περιστροφή
Σωστές μικρογραφίες χωρίς επανεφεύρεση των μαθηματικών fit
Το TPdf.RenderPageThumbnail ήδη φέρει αυτή τη διόρθωση, οπότε η συντομότερη διαδρομή προς μια σωστή μικρογραφία είναι να το καλέσετε αντί να ξανασυναρμολογήσετε τη λογική fit-και-περιστροφή με το χέρι. Δεδομένου ενός δείκτη σελίδας βασισμένου στο 1 και ενός μέγιστου πλάτους και ύψους, το RenderPageThumbnail υπολογίζει ένα κουτί fit, το διορθώνει για /Rotate 90 ή 270 εσωτερικά, και επιστρέφει ένα bitmap ιδιοκτησίας του καλούντος χωρίς να διαταράξει την τρέχουσα σελίδα του εγγράφου ή να πυροδοτήσει ένα συμβάν OnPageChange — κάτι που έχει σημασία για μια λωρίδα μικρογραφιών χτισμένη δίπλα σε έναν ζωντανό viewer στο ίδιο στιγμιότυπο TPdf
// PageW, PageH are a page's own (unrotated) dimensions in points, for
// example from FPDF_GetPageSizeByIndex, which reports size before
// /Rotate is applied
function FitBox(PageW, PageH: Double; Rotation: TRotation;
MaxW, MaxH: Integer; out FitW, FitH: Integer): Boolean;
var
PgW, PgH, Swap: Integer;
begin
PgW := Round(PageW);
PgH := Round(PageH);
if PgW < 1 then PgW := 1;
if PgH < 1 then PgH := 1;
if Rotation in [ro90, ro270] then
begin
Swap := PgW;
PgW := PgH;
PgH := Swap;
end;
Result := (MaxW > 0) and (MaxH > 0);
if not Result then
Exit;
if PgW * MaxH > PgH * MaxW then
begin
FitW := MaxW;
FitH := (MaxW * PgH) div PgW;
end
else
begin
FitH := MaxH;
FitW := (MaxH * PgW) div PgH;
end;
end;
Ο βοηθός FitBox αξίζει να τον κρατήσετε ούτως ή άλλως, επειδή το RenderPageThumbnail καλύπτει μόνο την περίπτωση μεμονωμένου bitmap. Ένα προσαρμοσμένο πλέγμα μικρογραφιών, μια λωρίδα προεπισκόπησης εκτύπωσης, ή ένας διάλογος επιλογέα σελίδας που διατάσσει αρκετές σελίδες έναντι ανεξάρτητων κουτιών χρειάζεται τα ίδια μαθηματικά fit ενήμερα για περιστροφή χωρίς απαραίτητα να θέλει ένα φρέσκο bitmap για κάθε πλακίδιο, και οι δικές του λειτουργίες zoom fit-page και fit-width του TPdfView βασίζονται εσωτερικά στην ταυτόσημη ιδέα, επιλέγοντας μεταξύ του πλάτους και του ύψους μιας σελίδας για τον υπολογισμό λόγου zoom βάσει της τρέχουσας περιστροφής της όψης πριν τη συγκρίνει με τη διαθέσιμη περιοχή πελάτη. Αν η απόδοση zoom και κύλισης σε εκείνο το είδος viewer είναι το επόμενο πρόβλημα στη λίστα, το συνοδευτικό κείμενο για προσωρινή μνήμη απόδοσης και ομαλό zoom σε έναν viewer Delphi βασισμένο σε PDFium συνεχίζει ακριβώς από εκεί που σταματά η σωστή προσαρμογή μεγέθους
Εντοπίζοντας μια διπλή περιστροφή πριν το κάνει ένας πελάτης
Μια διπλή περιστροφή έχει μία αξιόπιστη οπτική υπογραφή: μια σελίδα που περιστράφηκε κατά 90 μοίρες στην είσοδο βγαίνει φαίνοντας περιστραμμένη κατά 180 σε σχέση με το υπόλοιπο του εγγράφου, όχι 90, επειδή το επιπλέον ro90 στοιβάχτηκε πάνω στη δική της ro90 της σελίδας αντί να την αντικαταστήσει. Ένα fixture δοκιμής χτισμένο μόνο από σελίδες /Rotate 0 ποτέ δεν θα το πιάσει αυτό, αφού η προσθήκη ro0 σε ro0 παραμένει ro0 και το σφάλμα παραμένει αόρατο· ένα fixture χρειάζεται τουλάχιστον μία σελίδα αποθηκευμένη με /Rotate 90 και μία με /Rotate 270 πριν μια διαδρομή κώδικα μικρογραφίας ή fit-zoom μπορεί να εμπιστευτεί
Η βασική διοχέτευση σελίδας-σε-bitmap που καλύπτεται στο απόδοση σελίδων PDF σε JPEG με το PDFium Component ήδη αποδίδει σωστά περιστραμμένες σελίδες χωρίς κανέναν κώδικα ειδικής περίπτωσης, ακριβώς επειδή αφήνει το Rotation στην προεπιλογή του ro0 και αφήνει το PDFium να εφαρμόσει το /Rotate μόνο του. Το σφάλμα διπλής περιστροφής εμφανίζεται μόνο μόλις ο κώδικας εφαρμογής αρχίσει να διαβάζει πίσω το PageRotation και να το τροφοδοτεί κάπου όπου δεν ανήκει
Οι κλήσεις απόδοσης ενήμερες για περιστροφή και η προσαρμογή μεγέθους μικρογραφίας που περιγράφονται εδώ είναι μέρος του εξαρτήματος PDFium για Delphi και C++Builder, μαζί με το υπόλοιπο των API απόδοσης, προβολής, και εξαγωγής κειμένου χτισμένα πάνω στις ίδιες κλάσεις TPdf και TPdfView