Τεχνικό Άρθρο

Πρόγραμμα προβολής PDF συνεχούς κύλισης στο Delphi με το PDFium Component

Μια μεμονωμένη σελίδα A4 που αποδίδεται σε ένα άνετο ζουμ ανάγνωσης είναι της τάξης μερικών megabyte bitmap 32-bit. Πολλαπλασιάστε το με ένα συμβόλαιο 400 σελίδων και η αριθμητική παύει να είναι αφηρημένη: αν αποδώσετε κάθε σελίδα εκ των προτέρων, ζητάτε από τα Windows πάνω από ένα gigabyte bitmap τα οποία ο χρήστης θα κοιτάζει μία οθόνη τη φορά. Η εφαρμογή είτε ξεμένει από χώρο διευθύνσεων σε μια έκδοση 32-bit είτε περνά τα πρώτα δευτερόλεπτά της παγωμένη ενώ η GPU και ο αναλυτής σελίδων επεξεργάζονται σελίδες στις οποίες κανείς δεν έχει κάνει κύλιση ακόμη. Ένας αναγνώστης συνεχούς κύλισης πρέπει να δίνει την αίσθηση μιας ψηλής κορδέλας σελίδων, αλλά δεν μπορεί στην πραγματικότητα να τις κρατήσει όλες στη μνήμη ταυτόχρονα

Αυτή η ένταση είναι όλο το πρόβλημα εδώ. Το PDFium Component το επιλύει αυτό μέσα στο TPdfView, οπότε το μεγαλύτερο μέρος της δουλειάς είναι η επιλογή της σωστής λειτουργίας εμφάνισης και η κατανόηση του τι κάνει το στοιχείο για λογαριασμό σας. Τα κομμάτια που δεν κάνει για εσάς, η προσαρμογή του μεγέθους των σελίδων για μια ροή ανάγνωσης και η διατήρηση της ανταπόκρισης στη γρήγορη κύλιση, είναι τα σημεία όπου λίγος κώδικας αξίζει τον κόπο. Εάν εξακολουθείτε να συναρμολογείτε το γύρω πλαίσιο (γραμμή εργαλείων, μικρογραφίες, πλαίσιο αναζήτησης), ο οδηγός προγράμματος προβολής πλούσιου σε δυνατότητες καλύπτει αυτό το έδαφος· εδώ το θέμα είναι η ίδια η κύλιση

Η διάταξη είναι μια λειτουργία εμφάνισης, όχι ένα πάνελ από bitmaps

Η ένστικτη αντίδραση από την εργασία με φόρμες VCL είναι να στραφούμε σε ένα πλαίσιο κύλισης (scroll box) και να τοποθετήσουμε στοιχεία εικόνας μέσα σε αυτό, ένα ανά σελίδα. Αποφύγετέ το. Αυτή η σχεδίαση σας αναγκάζει να διαχειριστείτε την τοποθέτηση της σελίδας, τα μαθηματικά της κύλισης και το ζήτημα της μνήμης ταυτόχρονα, και θα επανεφεύρετε κάθε ένα από αυτά με κακό τρόπο. Το TPdfView μοντελοποιεί ήδη το έγγραφο ως μια συνεχή σειρά σελίδων και εκθέτει τη διάταξη μέσω της ιδιότητάς του DisplayMode

Pdf := TPdf.Create(Self);
PdfView := TPdfView.Create(Self);
PdfView.Parent := Self;
PdfView.Align := alClient;
PdfView.Pdf := Pdf;

PdfView.DisplayMode := dmSingleContinuous;   // one page wide, scrolls vertically

Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
if not Pdf.Active then
  ShowMessage('Could not open the document');

Αυτή είναι ολόκληρη η ρύθμιση συνεχούς κύλισης. Η επιλογή dmSingleContinuous διατάσσει τις σελίδες σε μια ενιαία κατακόρυφη στήλη με τα κενά μεταξύ τους να αντιμετωπίζονται εσωτερικά, και η προβολή κυλά μέσα σε αυτήν τη στήλη ως μία επιφάνεια. Δεν υπάρχει έλεγχος ανά σελίδα για να συνδεθεί και κανένας χειριστής κύλισης για να γραφτεί για τη συνηθισμένη πλοήγηση. Σημειώστε τον έλεγχο στο Pdf.Active μετά την ανάθεση: το άνοιγμα ενός εγγράφου δεν προκαλεί ποτέ εξαίρεση, οπότε ένα κατεστραμμένο ή προστατευμένο με κωδικό πρόσβασης αρχείο αφήνει το Active στο False χωρίς εξαίρεση για σύλληψη, και ένα πρόγραμμα προβολής που παρακάμπτει αυτόν τον έλεγχο εμφανίζει ένα κενό πάνελ και κατηγορεί τον εαυτό του

Η ίδια ιδιότητα φέρει τις λειτουργίες spread (διπλό φύλλο). Η επιλογή dmTwoPageContinuous τοποθετεί τις σελίδες δίπλα-δίπλα, δύο ανά σειρά, για την ανάγνωση τύπου βιβλίου που θέλουν ορισμένα έγγραφα. Η επιλογή dmTwoPageContinuousWithCover κάνει το ίδιο, αλλά επιτρέπει στη σελίδα ένα να στέκεται μόνη της ως εξώφυλλο, ώστε τα υπόλοιπα διπλά φύλλα να εμπίπτουν στο φυσικό όριο ζυγών-μονών σελίδων. Και οι τρεις λειτουργίες κυλούν συνεχώς. Η εναλλαγή μεταξύ τους είναι μια απλή ανάθεση, γεγονός που καθιστά ένα σύνθετο πλαίσιο (combo box) λειτουργίας εμφάνισης εύκολο να προστεθεί αργότερα

Μόνο οι ορατές σελίδες ραστεροποιούνται

Ο λόγος που αυτό κλιμακώνεται σε ένα αρχείο 400 σελίδων είναι ότι η στήλη είναι εικονική. Το TPdfView γνωρίζει το ύψος κάθε σελίδας από το δέντρο σελίδων του εγγράφου, οπότε μπορεί να υπολογίσει τη συνολική έκταση κύλισης και τη θέση κάθε σελίδας χωρίς να ραστεροποιήσει τίποτα. Η ραστεροποίηση (rasterization), το δαπανηρό βήμα που μετατρέπει τη ροή περιεχομένου μιας σελίδας σε εικονοστοιχεία (pixels), συμβαίνει μόνο για τις σελίδες που τέμνουν επί του παρόντος τη θύρα προβολής (viewport), συν ένα μικρό περιθώριο, ώστε η σελίδα να είναι έτοιμη μέχρι να κυλήσει στην προβολή. Καθώς κυλάτε προς τα κάτω, οι σελίδες που εισέρχονται στη θύρα προβολής αποδίδονται και οι σελίδες που εξέρχονται από αυτήν απελευθερώνουν τα bitmaps τους. Η μνήμη παραμένει ανάλογη με ό,τι χωράει στην οθόνη, όχι με το μήκος του εγγράφου

Αυτό αξίζει να το εσωτερικεύσετε επειδή αλλάζει τον τρόπο που σκέφτεστε για το κόστος. Το άνοιγμα ενός εγγράφου 400 σελίδων είναι φθηνό: αναλύει τη δομή, όχι το περιεχόμενο. Το κόστος είναι ανά σελίδα και καταβάλλεται νωχελικά (lazily), τη στιγμή που γίνεται κύλιση κοντά σε μια σελίδα. Ένα πρόγραμμα προβολής που φαίνεται άμεσο στο άνοιγμα και ομαλό στην κύλιση δεν κάνει λιγότερη δουλειά συνολικά, αλλά απλώνει τη δουλειά κατά μήκος της πραγματικής διαδρομής ανάγνωσης του χρήστη και απορρίπτει ό,τι μένει πίσω. Η πρακτική συνέπεια είναι ότι σχεδόν ποτέ δεν θέλετε να επιβάλλετε την απόδοση σελίδων πριν από τον χρήστη. Αφήστε την προβολή να αποφασίσει τι είναι ορατό

Προσαρμόστε το μέγεθος των σελίδων στο πλάτος, και μετά αφήστε το ζουμ ήσυχο

Μια στήλη ανάγνωσης απαιτεί σελίδες προσαρμοσμένες στο πλάτος του πάνελ, και όχι καρφωμένες σε ένα απόλυτο ζουμ. Η λειτουργία FitMode το κάνει αυτό και συνεχίζει να το κάνει καθώς αλλάζει μέγεθος το παράθυρο

PdfView.FitMode := pfmFitWidth;   // each page fills the column width; height follows

Με το pfmFitWidth το στοιχείο επανυπολογίζει το ζουμ κάθε φορά που αλλάζει μέγεθος η προβολή, οπότε η στήλη γεμίζει πάντα το διαθέσιμο πλάτος και τα ύψη των σελίδων, και κατά συνέπεια η έκταση κύλισης, ακολουθούν από αυτό. Υπάρχει μια παγίδα που πιάνει τους προγραμματιστές: η απευθείας ανάθεση του Zoom επαναφέρει το FitMode στο pfmNone. Αυτό είναι εσκεμμένο, επειδή το χειροκίνητο ζουμ και η αυτόματη προσαρμογή είναι αντιφατικές προθέσεις, αλλά σημαίνει ότι ένα αδέσποτο PdfView.Zoom := 1.0 κάπου στον κώδικά σας απενεργοποιεί σιωπηλά την προσαρμογή στο πλάτος και η επόμενη αλλαγή μεγέθους σταματά να επαναρρέει (reflow). Εάν προσφέρετε τόσο έλεγχο ζουμ όσο και κουμπί προσαρμογής, αντιμετωπίστε τα ως διακόπτη λειτουργίας: η ρύθμιση του ενός καθαρίζει το άλλο, και εσείς αποφασίζετε ποιο κερδίζει

Για απόλυτους ελέγχους ζουμ που διαβάζονται φυσικά, η προβολή εκθέτει τα ζουμ προσαρμογής ως τιμές που μπορείτε να εφαρμόσετε ή να εμφανίσετε: το PageWidthZoom[PageNumber] επιστρέφει το ζουμ που θα προσάρμοζε αυτήν τη σελίδα στο πλάτος, και το αντίστοιχο PageZoom προσαρμόζει ολόκληρη τη σελίδα. Η ανάγνωση αυτών είναι ο τρόπος με τον οποίο συμπληρώνετε ένα μενού "Προσαρμογή στο Πλάτος" / "Προσαρμογή στη Σελίδα" χωρίς να κωδικοποιείτε σκληρά μαγικά ποσοστά που πηγαίνουν λάθος σε σελίδες οριζόντιου προσανατολισμού ή υπερμεγέθεις σελίδες

Διατηρήστε τη γρήγορη κύλιση ανταποκρίσιμη με προοδευτική απόδοση

Η προεπιλεγμένη διαδρομή απόδοσης σχεδιάζει μια σελίδα μέχρι την ολοκλήρωσή της πριν επιστρέψει. Για μια μεμονωμένη σελίδα αυτό είναι μια χαρά. Κατά τη διάρκεια μιας γρήγορης κύλισης (flick-scroll) σε ένα πυκνό έγγραφο δεν είναι: κάθε σελίδα που περνά φευγαλέα ξεκινά μια πλήρη ραστεροποίηση, και εάν ο χρήστης κυλά γρηγορότερα από ό,τι μπορούν να αποδοθούν οι σελίδες, αυτές οι αποδόσεις συσσωρεύονται και το πάνελ κολλάει επειδή γίνεται εργασία για σελίδες που βρίσκονται ήδη εκτός οθόνης μέχρι να τελειώσει. Η διόρθωση είναι να γίνει η απόδοση ακυρώσιμη και να εγκαταλείπεται τη στιγμή που ο χρήστης προχωρά

Η RenderPageProgressive αποδίδει σε κομμάτια (chunks) και ελέγχει ένα διακριτικό ακύρωσης σε κάθε όριο κομματιού, έτσι ώστε μια απόδοση σε εξέλιξη μιας σελίδας που μόλις απομακρύνθηκε με κύλιση να μπορεί να απορριφθεί αντί να εκτελεστεί μέχρι το τέλος

type
  TFormMain = class(TForm)
    // ...
  private
    FRenderCancel: IPdfCancellationTokenSource;
    procedure RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
  end;

procedure TFormMain.RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
var
  Status: TPdfProgressiveStatus;
begin
  // Cancel whatever was rendering; the old token is now signaled.
  if Assigned(FRenderCancel) then
    FRenderCancel.Cancel;
  FRenderCancel := TPdfCancellationTokenSource.New;

  Pdf.PageNumber := PageNo;
  Status := Pdf.RenderPageProgressive(Bmp, 0, 0, Bmp.Width, Bmp.Height,
    FRenderCancel.Token);

  case Status of
    prsDone:      ;                    // bitmap is complete, paint it
    prsCancelled: Exit;                // superseded, discard this result
    prsFailed:    ShowMessage('Render failed for page ' + IntToStr(PageNo));
  end;
end;

Το σχήμα που έχει σημασία είναι η τιμή επιστροφής. Η τιμή prsDone σημαίνει ότι το bitmap είναι πλήρως σχεδιασμένο και αξίζει να μεταφερθεί στην οθόνη. Η τιμή prsCancelled σημαίνει ότι μια νεότερη θέση κύλισης αντικατέστησε αυτήν τη σελίδα, οπότε πετάτε το μερικό αποτέλεσμα αντί να το εμφανίσετε. Η τιμή prsFailed είναι ένα πραγματικό σφάλμα σε αυτήν τη σελίδα. Η ακύρωση ελέγχεται στα όρια των κομματιών και όχι προληπτικά, οπότε αναμένετε μερικά δεκάδες χιλιοστά του δευτερολέπτου καθυστέρησης μεταξύ της κλήσης του Cancel και της πραγματικής διακοπής της απόδοσης. Αυτό είναι ακόμα πολύ φθηνότερο από το να αφήσετε μια μπαγιάτικη απόδοση ολόκληρης σελίδας να μπλοκάρει την ουρά. Η μεταβίβαση του nil ως διακριτικού αποδίδει απευθείας μέχρι την ολοκλήρωση, που είναι η σωστή επιλογή για μια εφάπαξ απόδοση, όπως μια προεπισκόπηση εκτύπωσης όπου δεν υπάρχει κάτι για να ακυρωθεί

Όταν καλείτε τη μορφή συνάρτησης της RenderPage, αυτήν που επιστρέφει ένα νέο TBitmap, να θυμάστε ότι ο καλών το κατέχει και πρέπει να το αποδεσμεύσει (Free). Σε έναν βρόχο κύλισης που εκχωρεί ένα bitmap ανά σελίδα, το να το ξεχάσετε αυτό είναι μια διαρροή μνήμης που μεγαλώνει με κάθε σελίδα που περνά ο χρήστης, που είναι ακριβώς η αποτυχία απεριόριστης μνήμης που υποτίθεται ότι θα απέφευγε η σχεδίαση συνεχούς κύλισης. Αποδώστε σε ένα επαναχρησιμοποιούμενο bitmap όπου μπορείτε

Τι σας απομένει

Ο αναγνώστης συνεχούς κύλισης είναι κυρίως ευθύνη του στοιχείου. Επιλέγετε dmSingleContinuous για τη διάταξη, ορίζετε pfmFitWidth ώστε η στήλη να επαναρρέει με το παράθυρο και ελέγχετε το Pdf.Active ώστε ένα κακό αρχείο να αποτυγχάνει ηχηρά. Το μόνο κομμάτι που αξίζει να γράψετε μόνοι σας είναι η ακυρώσιμη απόδοση, επειδή ένας αναγνώστης κρίνεται από τον τρόπο που συμπεριφέρεται όταν κάποιος σέρνει τη γραμμή κύλισης στο κάτω μέρος ενός μεγάλου εγγράφου και το πάνελ είτε προλαβαίνει είτε όχι. Όλα τα υπόλοιπα, επιλογή κειμένου μεταξύ σελίδων, επισήμανση αναζήτησης, δέντρο σελιδοδεικτών, είναι εργασίες διεπαφής που κάθονται πάνω σε αυτήν την επιφάνεια κύλισης και όχι μέσα της

Τα APIs TPdfView, DisplayMode και RenderPageProgressive που εμφανίζονται εδώ αποτελούν μέρος του PDFium Component για Delphi και Lazarus