Οι περισσότερες σελίδες PDF ραστεροποιούνται σε λίγα χιλιοστά του δευτερολέπτου και ποτέ δεν το σκέφτεστε. Έπειτα κάποιος χρήστης ανοίγει ένα αρχιτεκτονικό σχέδιο A1, μια σελίδα γεμάτη δεκάδες χιλιάδες διανυσματικές πινελιές, ή μια αφίσα φορτωμένη με ομάδες διαφάνειας και απαλές μάσκες, και η μοναδική κλήση που τη ζωγραφίζει χρειάζεται δύο ή τρία δευτερόλεπτα. Αν αυτή η κλήση εκτελείται στο νήμα του UI, το παράθυρο σταματά να ανανεώνεται, η γραμμή τίτλου γκριζάρει, και το λειτουργικό σύστημα προτείνει τον τερματισμό της εφαρμογής. Η εργασία είναι νόμιμη. Η σελίδα πράγματι χρειάζεται τόσο χρόνο. Το ελάττωμα είναι ότι η απόδοση είναι μία αδιαίρετη κλήση αποκλεισμού χωρίς κανέναν τρόπο να πάρει ανάσα και κανέναν τρόπο να διακοπεί
Αυτό το άρθρο αφορά ακριβώς το ένα από αυτά τα δύο προβλήματα: την ακύρωση μιας μεγάλης απόδοσης μίας σελίδας χωρίς να παγώσει το UI. Ο χρήστης έκανε κλικ στην επόμενη σελίδα, ή έκανε ζουμ, ή έκλεισε το έγγραφο, και η απόδοση που βρίσκεται σε εξέλιξη είναι πλέον χαμένη εργασία που θα έπρεπε να τερματιστεί στην πρώτη ευκαιρία αντί να τρέξει μέχρι το τέλος. Η εξομάλυνση της κύλισης και του ζουμ με προσωρινή αποθήκευση όσων έχουν ήδη ραστεροποιηθεί είναι ένα ξεχωριστό ζήτημα με δικό του σχεδιασμό, που καλύπτεται στο συνοδευτικό άρθρο που παραπέμπεται στο τέλος. Εδώ το μόνο ερώτημα είναι πώς μία προοδευτική απόδοση μπορεί να ανταποκριθεί σε ένα αίτημα ακύρωσης γρήγορα και καθαρά
Το API προοδευτικής απόδοσης που το PDFium ήδη διαθέτει
Το PDFium προέβλεψε το μισό του προβλήματος που αφορά το πάγωμα. Παράλληλα με τη μονοκόμματη FPDF_RenderPageBitmap, εκθέτει μια προοδευτική παραλλαγή που χωρίζει μια σελίδα σε τμήματα εργασίας. Καλείτε την FPDF_RenderPageBitmap_Start μία φορά για να ρυθμίσετε την απόδοση πάνω σε ένα bitmap προορισμού, και έπειτα καλείτε επανειλημμένα την FPDF_RenderPage_Continue. Κάθε Continue ραστεροποιεί ένα οριοθετημένο τμήμα και επιστρέφει μια κατάσταση. Το FPDF_RENDER_TOBECONTINUED σημαίνει ότι απομένει ακόμη δουλειά, το FPDF_RENDER_DONE σημαίνει ότι η σελίδα ολοκληρώθηκε, και το FPDF_RENDER_FAILED σημαίνει ότι σταμάτησε λόγω σφάλματος. Όταν ο βρόχος τελειώσει, καλείτε την FPDF_RenderPage_Close για να απελευθερώσετε την ανά σελίδα προοδευτική κατάσταση. Επειδή ο έλεγχος επιστρέφει στον κώδικά σας ανάμεσα στα τμήματα, μπορείτε να επεξεργαστείτε μηνύματα, να ενημερώσετε μια ένδειξη προόδου, ή να ελέγξετε αν η εργασία εξακολουθεί να είναι επιθυμητή
Ο μηχανισμός που παρέχει το PDFium για να αποφασίζει πότε να παραχωρήσει τον έλεγχο είναι μια δομή επανάκλησης με όνομα IFSDK_PAUSE. Την παραδίδετε στην Start και σε κάθε Continue. Μετά από κάθε τμήμα, το PDFium καλεί τον δείκτη συνάρτησης NeedToPauseNow, και αν αυτός επιστρέψει μη μηδενική τιμή, η τρέχουσα Continue σταματά νωρίτερα και επιστρέφει τον έλεγχο με FPDF_RENDER_TOBECONTINUED. Η δομή διαθέτει επίσης ένα πεδίο version, το οποίο πρέπει να οριστεί σε 1, και έναν ελεύθερης μορφής δείκτη user τον οποίο το PDFium ποτέ δεν αγγίζει και τον περνάει ανέπαφο. Αυτός ο ανέπαφος δείκτης είναι ο πυρήνας ολόκληρου του σχεδιασμού που ακολουθεί
Επαναχρησιμοποίηση της παύσης ως ακύρωση
Η αρχική πρόθεση της NeedToPauseNow είναι ο επιμερισμός χρόνου. Επιστρέφετε μη μηδενική τιμή όταν έχει εξαντληθεί το χρονικό σας περιθώριο, επιστρέφετε μηδέν για να συνεχιστεί η απόδοση, και το PDFium κάνει παύση ώστε να μπορείτε να κάνετε κάτι άλλο πριν συνεχίσετε την ίδια απόδοση. Το PDFium Component επαναχρησιμοποιεί το ίδιο ακριβώς σήμα για διαφορετικό σκοπό. Αντί να απαντά «πρέπει να κάνω παύση και να σας αφήσω να συνεχίσετε», η επανάκληση απαντά «έχει ακυρωθεί αυτή η εργασία». Τα δύο αντιστοιχίζονται καθαρά λόγω του τι κάνει ο βρόχος όταν δει τη σημαία. Μια γνήσια παύση αναμένει μια μεταγενέστερη Continue· μια ακύρωση όχι. Μόλις ο βρόχος κλήσης παρατηρήσει ότι το token έχει ακυρωθεί, κλείνει το πλαίσιο απόδοσης και δεν καλεί ποτέ ξανά την Continue, οπότε η ίδια μη μηδενική τιμή επιστροφής που το PDFium διαβάζει ως «σταμάτησε αυτό το τμήμα» γίνεται, ουσιαστικά, «σταμάτησε οριστικά.»
Η ακύρωση εκφράζεται μέσω μιας διεπαφής, IPdfCancellationToken, της οποίας η ιδιότητα IsCancelled μεταβαίνει από false σε true όταν κάποιο άλλο τμήμα του προγράμματος ζητά τη διακοπή της απόδοσης. Η γέφυρα ανάμεσα σε αυτή τη διεπαφή Pascal και την C επανάκληση του PDFium είναι ένας μοναδικός δείκτης. Η αναφορά διεπαφής του token γράφεται στο IFSDK_PAUSE.user, και μια στατική cdecl επανάκληση τη διαβάζει πίσω και την ερωτά. Αυτό είναι το κλασικό πρόβλημα του να αφήνεις μια βιβλιοθήκη C να καλεί πίσω σε Pascal: η επανάκληση πρέπει να είναι μια απλή συνάρτηση με σύμβαση κλήσης C, όχι μέθοδος, επειδή το PDFium αποθηκεύει και καλεί έναν γυμνό δείκτη συνάρτησης που δεν γνωρίζει τίποτα για αντικείμενα Pascal ή για το Self
type
TPdfProgressivePause = record
Pause: IFSDK_PAUSE; // Το PDFium το διαβάζει· το .user κρατά το token
Token: IPdfCancellationToken; // ισχυρή αναφορά που κρατά το token ζωντανό
end;
function ProgressivePauseCallback(pThis: PIFSDK_PAUSE): FPDF_BOOL; cdecl;
var
Token: IPdfCancellationToken;
begin
Result := 0;
if (pThis = nil) or (pThis^.user = nil) then
Exit;
Token := IPdfCancellationToken(pThis^.user);
if Token.IsCancelled then
Result := 1; // μη μηδενικό: το PDFium σταματά αυτό το τμήμα
end;
Η επανάκληση ανακτά το token μετατρέποντας το pThis^.user πίσω στον τύπο διεπαφής και διαβάζει το IsCancelled. Τίποτα μέσα της δεν εκχωρεί μνήμη, δεν κλειδώνει, ούτε μπλοκάρει, κάτι που έχει σημασία επειδή το PDFium την καλεί στο νήμα απόδοσης μετά από κάθε τμήμα και κάθε εργασία που γίνεται εδώ προστίθεται στο κόστος της ίδιας της απόδοσης. Η προστασία έναντι μιας μηδενικής δομής ή ενός μηδενικού πεδίου user σημαίνει ότι η ίδια συνάρτηση είναι ασφαλής προς εγκατάσταση ακόμη και σε μια απόδοση στην οποία δεν δόθηκε ποτέ πραγματικό token
Διατηρώντας το token ζωντανό σε όλη τη διάρκεια του βρόχου
Η μετατροπή ενός δείκτη διεπαφής μέσω ενός ακατέργαστου Pointer και πίσω είναι εκεί όπου γεννιούνται τα σφάλματα διάρκειας ζωής. Μια IInterface στη Delphi μετράει αναφορές, και ο μετρητής μετακινείται μόνο όταν ο μεταγλωττιστής μπορεί να δει μια μεταβλητή τύπου διεπαφής να ανατίθεται. Η αποθήκευση του token αποκλειστικά ως γυμνού δείκτη μέσα στο IFSDK_PAUSE.user θα το έκρυβε εντελώς από τον μετρητή αναφορών. Αν η μοναδική άλλη αναφορά σε εκείνο το token έβγαινε εκτός εμβέλειας ενώ ο βρόχος Continue εξακολουθούσε να εκτελείται, το αντικείμενο θα απελευθερωνόταν κάτω από την επανάκληση, και το επόμενο τμήμα θα αναφερόταν σε έναν κρεμασμένο δείκτη
Γι' αυτό ο περιγραφέας είναι μια εγγραφή που κρατά δύο πράγματα, όχι ένα. Το πεδίο Pause είναι η δομή που διαβάζει το PDFium. Το πεδίο Token είναι μια πραγματική αναφορά τύπου διεπαφής που μετράει ο μεταγλωττιστής, και υπάρχει για κανέναν άλλο λόγο παρά για να καθηλώνει το token στη μνήμη για όσο διάστημα ζει η εγγραφή. Η εγγραφή είναι μια τοπική μεταβλητή στη στοίβα της ρουτίνας απόδοσης, άρα παραμένει έγκυρη για ολόκληρη τη διάρκεια του βρόχου και καταστρέφεται μόνο όταν η ρουτίνα ολοκληρωθεί. Ο γυμνός δείκτης στο user και η μετρημένη αναφορά στο Token αναφέρονται στο ίδιο αντικείμενο· ο ένας είναι αυτό που το PDFium μπορεί να διαβάσει, ο άλλος είναι αυτό που εμποδίζει τη συλλογή εκείνου του αντικειμένου
var
Pause: TPdfProgressivePause;
EffectiveToken: IPdfCancellationToken;
begin
// ... επιλογή του EffectiveToken ...
// Πρώτα η ισχυρή αναφορά, μετά δημοσίευση του ίδιου αντικειμένου στο PDFium μέσω του .user.
Pause.Token := EffectiveToken;
Pause.Pause.version := 1;
Pause.Pause.NeedToPauseNow := ProgressivePauseCallback;
Pause.Pause.user := Pointer(EffectiveToken);
Κλείνοντας το πλαίσιο απόδοσης ανεξάρτητα από το πώς τελειώνει ο βρόχος
Κάθε κλήση στην FPDF_RenderPageBitmap_Start εκχωρεί προοδευτική κατάσταση που το PDFium συσχετίζει με τη σελίδα, και αυτή η κατάσταση απελευθερώνεται μόνο από την FPDF_RenderPage_Close. Υπάρχουν τρεις τρόποι εξόδου από τον βρόχο οδήγησης. Η σελίδα ολοκληρώνεται και η τελευταία κατάσταση είναι FPDF_RENDER_DONE. Το token ενεργοποιείται και ο βρόχος βγαίνει νωρίς αναφέροντας ακύρωση. Κάτι αποτυγχάνει και η κατάσταση είναι FPDF_RENDER_FAILED. Και οι τρεις περιπτώσεις πρέπει να καλούν την Close, και η διαδρομή ακύρωσης είναι εκείνη όπου είναι πιο εύκολο να γίνει λάθος, επειδή το φυσικό σχήμα του «είδα ακύρωση, βγαίνω» τείνει να παρακάμπτει τον καθαρισμό στον δρόμο προς την έξοδο. Αν η Close δεν προσεγγιστεί ποτέ, διαρρέει η ανά σελίδα κατάσταση, και ένας viewer που επιτρέπει στον χρήστη να ακυρώνει απόδοση μετά από απόδοση θα συσσώρευε αυτή τη διαρροή σε κάθε ματαιωμένη σελίδα
Το εύρωστο σχήμα τοποθετεί τον βρόχο και την ταξινόμηση του αποτελέσματος μέσα σε ένα try και την FPDF_RenderPage_Close στο αντίστοιχο finally. Το bitmap προορισμού καταστρέφεται στο ίδιο μπλοκ. Η ακύρωση μπορεί να βγει από τον βρόχο μέσω πρόωρης Exit και το finally εξακολουθεί να εκτελείται, οπότε υπάρχει ακριβώς ένα σημείο που απελευθερώνει την προοδευτική κατάσταση και δεν μπορεί να παρακαμφθεί
Status := FPDF_RenderPageBitmap_Start(PdfBmp, FPage, Left, Top,
Width, Height, Ord(Rotation), EncodeRenderOptions(Options), Pause.Pause);
try
while Status = FPDF_RENDER_TOBECONTINUED do
begin
if EffectiveToken.IsCancelled then
begin
Result := prsCancelled;
Exit;
end;
Status := FPDF_RenderPage_Continue(FPage, Pause.Pause);
end;
if EffectiveToken.IsCancelled then
Result := prsCancelled
else if Status = FPDF_RENDER_DONE then
Result := prsDone
else
Result := prsFailed;
finally
// Απελευθερώνει την προοδευτική κατάσταση που εκχώρησε η Start· υποχρεωτικό σε κάθε διαδρομή.
FPDF_RenderPage_Close(FPage);
FPDFBitmap_Destroy(PdfBmp);
end;
Ο βρόχος ελέγχει το token πριν από κάθε Continue, επιπλέον της επανάκλησης μέσα σε αυτήν. Η επανάκληση συντομεύει το τρέχον τμήμα· ο έλεγχος του βρόχου εμποδίζει την έναρξη του επόμενου. Μαζί οριοθετούν πόσο χρόνο χρειάζεται μια ακύρωση για να αποδώσει αποτέλεσμα, περίπου στη διάρκεια ενός τμήματος
Τρία αποτελέσματα, και τι περιέχει το bitmap μετά από μια ακύρωση
Το δημόσιο σημείο εισόδου είναι η TPdf.RenderPageProgressive, και επιστρέφει μια TPdfProgressiveStatus που είναι μία από τις prsDone, prsCancelled, ή prsFailed. Οι τιμές αντικατοπτρίζουν τις σταθερές FPDF_RENDER_* του PDFium στο ιδίωμα της Pascal, αλλά εντάσσουν την περίπτωση ακύρωσης ως αποτέλεσμα πρώτης τάξης αντί για σφάλμα
Το σημείο που μπερδεύει τον κόσμο είναι τι περιέχει το bitmap προορισμού μετά την prsCancelled. Δεν είναι κενό. Το PDFium αποδίδει προοδευτικά στο ίδιο bitmap, τμήμα προς τμήμα, οπότε όταν μια ακύρωση σταματά τον βρόχο, το bitmap κρατά ό,τι είχε ζωγραφιστεί μέχρι εκείνη τη στιγμή, δηλαδή μια μερική εικόνα: κάποιες ζώνες ολοκληρωμένες, οι υπόλοιπες ακόμη να δείχνουν το χρώμα γεμίσματος. Το αν αυτό το μερικό αποτέλεσμα είναι χρήσιμο εξαρτάται από τον καλούντα. Ένας viewer που πρόκειται να πετάξει το bitmap επειδή ο χρήστης πλοηγήθηκε αλλού μπορεί απλώς να το αγνοήσει. Ένας viewer που θέλει να δείξει μια προεπισκόπηση χαμηλού κόστους μπορεί να το κρατήσει. Αυτό που δεν πρέπει να κάνετε είναι να υποθέσετε ότι η prsCancelled συνεπάγεται ένα κενό ή απροσδιόριστο bitmap· συνεπάγεται μια αληθινή στιγμιαία εικόνα μιας ημιτελούς απόδοσης
var
Bmp: TBitmap;
Token: IPdfCancellationToken;
Status: TPdfProgressiveStatus;
begin
Bmp := TBitmap.Create;
try
// Το token ξεκινά μη ακυρωμένο· αλλάξτε το Token.IsCancelled από αλλού
// (μια ενέργεια UI, ένα συμβάν πλοήγησης) για να ματαιώσετε την απόδοση σε εξέλιξη.
Status := Pdf.RenderPageProgressive(Bmp, 0, 0, PageW, PageH, Token);
case Status of
prsDone: Image1.Picture.Assign(Bmp); // πλήρης απόδοση
prsCancelled: ; // μερικό bitmap, συνήθως απορρίπτεται
prsFailed: ShowMessage('Render failed');
end;
finally
Bmp.Free;
end;
end;
Το μηδενικό token και μια διαδρομή επανάκλησης χωρίς διακλαδώσεις
Η ακύρωση είναι προαιρετική (opt-in). Ένας καλών που απλώς θέλει προοδευτική απόδοση για το όφελος της επεξεργασίας μηνυμάτων, χωρίς πρόθεση ματαίωσης, θα πρέπει να μπορεί να περάσει nil για το token. Ο αφελής τρόπος για να υποστηριχθεί αυτό είναι να διασπαρούν έλεγχοι «αν δόθηκε token» μέσα στην επανάκληση και τον βρόχο, κάτι που σημαίνει μια διακλάδωση σε κάθε τμήμα και μια επανάκληση που πρέπει να χειρίζεται τόσο ένα πραγματικό token όσο και την απουσία του
Η υλοποίηση το αποφεύγει αντικαθιστώντας με ένα singleton όταν ο καλών δεν περνά τίποτα. Ένα nil token αντικαθίσταται με το PdfNoCancellationToken, μια διεπαφή της οποίας το IsCancelled είναι πάντα false. Από εκείνο το σημείο, η επανάκληση και ο βρόχος έχουν πάντα ένα token να ερωτήσουν, οπότε κανένας από τους δύο δεν χρειάζεται έλεγχο για nil ούτε ειδική διαδρομή. Το token που ποτέ δεν ακυρώνει απλώς απαντά πάντα false, η επανάκληση επιστρέφει πάντα μηδέν, και η απόδοση τρέχει μέχρι το τέλος ακριβώς όπως θα έτρεχε μια μη ακυρώσιμη. Η προαιρετική συμπεριφορά μοντελοποιείται ως ένα token που ποτέ δεν ενεργοποιείται, αντί ως απουσία token, κάτι που διατηρεί τη βασική διαδρομή ομοιόμορφη
// nil -> singleton που ποτέ δεν ακυρώνει, ώστε η διαδρομή επανάκλησης να είναι ίδια
// είτε ο καλών επέλεξε ακύρωση είτε όχι.
if AToken <> nil then
EffectiveToken := AToken
else
EffectiveToken := PdfNoCancellationToken;
Το σχήμα που προκύπτει είναι μικρό και αξίζει να επαναδιατυπωθεί, επειδή είναι το επαναχρησιμοποιήσιμο κομμάτι. Μια βιβλιοθήκη C που υποστηρίζει επανάκληση σάς δίνει ακριβώς ένα κανάλι για να περάσετε κατάσταση μέσα σε αυτή την επανάκληση, τον αδιαφανή δείκτη user. Τοποθετήστε μια μετρημένη αναφορά διεπαφής Pascal πίσω από αυτόν τον δείκτη, κρατήστε μια δεύτερη πραγματική αναφορά ζωντανή δίπλα στη δομή ώστε το αντικείμενο να μην μπορεί να συλλεχθεί στη μέση μιας κλήσης, και διαβάστε τη διεπαφή πίσω μέσα σε μια στατική συνάρτηση cdecl. Τυλίξτε ολόκληρο τον βρόχο οδήγησης σε ένα try και απελευθερώστε το εγγενές πλαίσιο στο finally. Το ίδιο πρότυπο μεταφέρεται σε οποιαδήποτε προοδευτική ή βασισμένη σε επανάκληση λειτουργία PDFium όπου ο κώδικας Pascal πρέπει να παραμείνει σε έλεγχο της διάρκειας ζωής ενώ η C κρατά έναν δείκτη
Η ακύρωση είναι μόνο το ένα μισό ενός responsive viewer. Το άλλο μισό είναι να μην αποδίδονται εκ νέου σελίδες που έχουν ήδη σχεδιαστεί, και να διατηρούνται η κύλιση και το ζουμ ομαλά εξυπηρετώντας αποθηκευμένα bitmap, κάτι που καλύπτεται στο άρθρο μας για την προσωρινή αποθήκευση απόδοσης και την απόδοση του ζουμ. Για το πώς η ακυρώσιμη απόδοση εντάσσεται σε έναν πλήρη viewer μαζί με την πλοήγηση, την επιλογή και την αναζήτηση, δείτε το δημιουργία ενός πλούσιου σε λειτουργίες PDF viewer με το PDFium Component. Η προοδευτική απόδοση που περιγράφεται εδώ διατίθεται ως μέρος του PDFium Component για Delphi και Lazarus μαζί με τα APIs φόρτωσης, απόδοσης, και φορμών που καλύπτονται αλλού σε αυτό το blog