Το PDFium Component ανοίγει ένα PDF που εξακολουθεί να κατεβαίνει μέσω του TPdfProgressiveDocument, μια υποκλάση TPdf που τυλίγει το API διαθεσιμότητας FPDFAvail_* του PDFium. Το BeginProgressiveLoad ξεκινά τη session, το CheckDocumentAvailability αναφέρει ποια εύρη bytes χρειάζεται ακόμα το PDFium, το OpenProgressiveDocument ανοίγει το αρχείο μόλις υπάρχουν αρκετά bytes, και το CancelProgressiveLoad εγκαταλείπει ένα διακοπμένο κατέβασμα χωρίς να διαρρέει native handles. Το δύσκολο δεν είναι το μονοπάτι της ευτυχίας. Ένας viewer σε ασταθή σύνδεση θα δει χρήστες να κλείνουν το tab στο 25 τοις εκατό, να αλλάζουν γνώμη, και να ξανανοίγουν τον ίδιο σύνδεσμο, και κάθε μια από εκείνες τις ματαιωμένες sessions έχει ένα native availability handle, δύο εγγραφές C callbacks, έναν adapter stream και ένα σύνολο requests εύρους εν πτήσει που πρέπει να απελευθερωθούν με ακριβώς τη σωστή σειρά
Πώς φορτώνει το TPdfProgressiveDocument ένα PDF που εξακολουθεί να κατεβαίνει;
Το TPdfProgressiveDocument κρατά έναν provider διαθεσιμότητας του PDFium ζωντανό όσο ένα stream τυχαίας προσπέλασης γεμίζει, και ρωτά εκείνον τον provider πριν από κάθε βήμα parse αν τα bytes που θέλει υπάρχουν. Το BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) παίρνει το backing stream συν το λογικό μέγεθος του απομακρυσμένου αρχείου, συνδέει ένα callback IsDataAvail και ένα callback AddSegment σε δύο εγγραφές, και καλεί το FPDFAvail_Create. Όταν το PDFium ρωτά αν υπάρχει ένα εύρος, το component απαντά ναι αν το εύρος κείτεται μέσα στο συνεχές πρόθεμα που περιγράφει το AvailableByteCount ή μέσα σε εύρος ήδη ολοκληρωμένο μέσω του scheduler RangeRequests, και το event OnDataAvailable μπορεί να ανατρέψει την ετυμηγορία για αραιά stores. Κάθε κλήση στο CheckDocumentAvailability επιστρέφει μία από τις τρεις τιμές TPdfDataAvailability (pdaAvailable, pdaNotAvailable, pdaError) και παραδίδει πίσω τα εύρη που ζήτησε το PDFium ως ταξινομημένο, συγχωνευμένο πίνακα TPdfDownloadRanges, ήδη σε ουρά στον scheduler με προτεραιότητα rrpImmediate
// Το FetchRange είναι η μεταφορά σας (HTTP Range GET, socket, blob reader):
// γράφει Size bytes στην Offset μέσα στο Store και επιστρέφει πόσα έφτασαν
function FetchRange(Store: TStream; Offset, Size: UInt64): UInt64; forward;
procedure OpenWhileDownloading(Pdf: TPdfProgressiveDocument; Store: TStream;
RemoteSize: UInt64);
const
MaxRounds = 64;
var
Hints: TPdfDownloadRanges;
State: TPdfDataAvailability;
Request: TPdfRangeRequest;
Round: Integer;
begin
Pdf.BeginProgressiveLoad(Store, RemoteSize, False);
State := pdaNotAvailable;
for Round := 1 to MaxRounds do
begin
State := Pdf.CheckDocumentAvailability(Hints);
if State <> pdaNotAvailable then
Break;
// Τα hints είναι ήδη σε ουρά· γράψτε πρώτα τα bytes, μετά ολοκληρώστε
while Pdf.RangeRequests.TryDequeue(Request) do
Pdf.RangeRequests.CompleteRequest(Request,
FetchRange(Store, Request.Offset, Request.Size));
end;
if State <> pdaAvailable then
raise EPdfError.Create('The document could not be discovered');
Pdf.OpenProgressiveDocument;
end;
Δύο λεπτομέρειες σε εκείνη τη βρόχο είναι φέρουσες φορτίου. Το ταβάνι γύρων μετράει επειδή ένας νεκρός σύνδεσμος κάνει το CheckDocumentAvailability να ζητά τα ίδια εύρη για πάντα, και μια αχανής βρόχος μετατρέπει μια αποτυχία δικτύου σε κρεμασμένο UI. Η σειρά μετράει επειδή ο scheduler σειριοποιεί την κατάστασή του με critical section αλλά δεν κάνει τίποτα για το TStream.Position στο backing store: ένα νήμα μεταφοράς πρέπει να γράψει τα bytes της απάντησης στο stream πριν καλέσει το CompleteRequest, αφού τη στιγμή που δημοσιεύεται ένα completion το PDFium μπορεί να διαβάσει εκείνο το εύρος, και οι ταυτόχρονοι writers χρειάζονται positioned I/O ή δικό τους lock
Γιατί το AvailableByteCount αρνείται να κινηθεί προς τα πίσω;
Το AvailableByteCount μόνο μεγαλώνει, και ο setter σηκώνει EPdfError με «Available byte count cannot move backwards» όταν προσπαθήσετε να το συρρικνώσετε. Από τη στιγμή που το callback IsDataAvail είπε στο PDFium ότι υπάρχει ένα εύρος, ο parser μπορεί ήδη να έχει διαβάσει και κρατήσει σε cache objects από εκεί, οπότε η ανάκληση εκείνων των bytes μετά θα έκανε τις απαντήσεις διαθεσιμότητας ασυνεπείς με ό,τι έχει ήδη καταναλώσει το PDFium. Ο ίδιος setter απορρίπτει τιμές μεγαλύτερες από το LogicalFileSize και σηκώνει «No progressive load is active» εκτός session, γι' αυτό τα bytes που κρατάτε ήδη πριν ξεκινήσει το load ανήκουν στο όρισμα AInitialAvailableByteCount του BeginProgressiveLoad και όχι σε μια ανάθεση ιδιότητας που έγινε πολύ νωρίς. Αν το store κατεβάσματος σας γεμίζει εκτός σειράς, μην προσπαθήσετε να το εκφράσετε μέσω του prefix καθόλου: ολοκληρώστε τα εύρη μέσω του scheduler ή απαντήστε μέσω OnDataAvailable
Πότε μπορεί πραγματικά να ανοίξει ένα μερικώς κατεβασμένο PDF;
Μόνο ένα linearized PDF (Παράρτημα F του ISO 32000-1, η διάταξη «Fast Web View») ανοίγει πριν έχει φτάσει ολόκληρο το αρχείο· ένα μη linearized PDF χρειάζεται ακόμα κάθε byte. Το OpenProgressiveDocument ελέγχει την ιδιότητα Linearization (plnUnknown, plnNotLinearized, plnLinearized) και δρομολογεί αναλόγως: ένα linearized αρχείο ανοίγει μέσω FPDFAvail_GetDocument μόλις υπάρχουν η ενότητα πρώτης σελίδας και οι πίνακες hints, ενώ ένα μη linearized αρχείο ανοίγει μέσω FPDF_LoadCustomDocument πάνω στην ίδια εγγραφή πρόσβασης αρχείου και μεταχειρίζεται ως αναγνώσιμο μόνο ως σύνολο. Ο δρομολογητής υπάρχει για συγκεκριμένο λόγο. Το να καλέσετε FPDFAvail_GetDocument σε μη linearized αρχείο μπορεί να επιστρέψει handle μη μηδενικό του οποίου το πλήθος σελίδων είναι μηδέν, ένα έγγραφο που φαίνεται ανοιχτό και είναι άδειο. Στη δική του test suite του component ένα linearized fixture 51 σελίδων φτάνει pdaAvailable και ανοίγει με το πλήρες page tree του ενώ το αραιό store κατεβάσματος εξακολουθεί να μην καλύπτει το αρχείο
function WaitForPage(Pdf: TPdfProgressiveDocument; Store: TStream;
PageNumber: Integer): Boolean;
var
Hints: TPdfDownloadRanges;
Request: TPdfRangeRequest;
Round: Integer;
begin
Result := False;
for Round := 1 to 64 do
case Pdf.LoadAvailablePage(PageNumber, Hints) of
pdaAvailable:
Exit(True); // Το PageNumber είναι τώρα η ενεργή σελίδα
pdaError:
Exit(False);
pdaNotAvailable:
while Pdf.RangeRequests.TryDequeue(Request) do
Pdf.RangeRequests.CompleteRequest(Request,
FetchRange(Store, Request.Offset, Request.Size));
end;
end;
Το LoadAvailablePage παίρνει αριθμό σελίδας 1-based και επιβάλλει τη σειρά που περιμένει το PDFium: πριν από τον πρώτο έλεγχο σελίδας τρέχει το CheckFormAvailability, που τυλίγει το FPDFAvail_IsFormAvail, και μόνο μετά καλεί το FPDFAvail_IsPageAvail. Ένα αποτέλεσμα pfaNotPresent είναι η φυσιολογική απάντηση για έγγραφο χωρίς AcroForm και δεν μπλοκάρει τίποτα. Όταν η σελίδα είναι έτοιμη, το LoadAvailablePage την κάνει ενεργή σελίδα, οπότε ένας viewer μπορεί να απεικονίσει τη σελίδα 1 ενός linearized φυλλαδίου ενώ οι υπόλοιπες σελίδες είναι ακόμα εν διαδρομή· το FirstAvailablePageNumber σας λέει ποια σελίδα ορίζει το dictionary γραμμικοποίησης ως πρώτη, ήδη μετατραπμένο από το μηδενικό δείκτη του PDFium
Τι απελευθερώνει το CancelProgressiveLoad, και με τι σειρά;
Το CancelProgressiveLoad ξεδιπλώνει μια session σε τέσσερα βήματα που δεν μπορούν να αναδιαταχθούν: ακυρώστε τον scheduler εύρων, κλείστε το έγγραφο, καταστρέψτε το availability handle με FPDFAvail_Destroy, μετά απορρίψτε τις εγγραφές callbacks και ελευθερώστε τον adapter stream. Η ακύρωση του scheduler πρώτο ανεβάζει τον μετρητή γενιάς του, πετάει κάθε εκκρεμές και εν πτήσει request, και ανάβει το OnCancelRequest για καθένα από τα εν πτήσει, οπότε ένα completion μεταφοράς που προσγειώνεται αργότερα κουβαλά την παλιά γενιά και το CompleteRequest επιστρέφει False χωρίς να αγγίξει τίποτα. Το έγγραφο πρέπει να κλείσει πριν φύγουν το availability handle και ο adapter επειδή το PDFium μπορεί να καλέσει πίσω στον provider πρόσβασης αρχείου όσο κλείνει ένα έγγραφο, και αν ο adapter έχει ήδη φύγει εκείνο το callback διαβάζει ελεύθερη μνήμη
procedure TDownloadForm.FormCreate(Sender: TObject);
begin
FPdf := TPdfProgressiveDocument.Create(nil);
// Ο scheduler ζει όσο το FPdf, οπότε συνδέστε τον μία φορά
FPdf.RangeRequests.OnCancelRequest := RangeCancelled;
end;
procedure TDownloadForm.RangeCancelled(Sender: TObject; RequestId: UInt64;
Attempt: Cardinal);
begin
FTransport.Abort(RequestId); // ο κώδικάς σας: κλείστε εκείνο το socket ή request
end;
procedure TDownloadForm.CancelButtonClick(Sender: TObject);
begin
FPdf.CancelProgressiveLoad;
// ProgressiveLoading = False, Active = False, AvailableByteCount = 0
end;
Η μέθοδος είναι idempotent και είναι το ενιαίο μονοπάτι καθαρισμού για τρεις καταστάσεις: ένα BeginProgressiveLoad που αποτυγχάνει στη μέση της κατασκευής, μια ρητή ακύρωση χρήστη, και τον destructor. Το BeginProgressiveLoad το καλεί επίσης πριν ξεκινήσει, οπότε η επανεκκίνηση του ίδιου object σε νέο URL είναι ασφαλής χωρίς ρητή ακύρωση. Μία απόφαση ιδιοκτησίας είναι δική σας να την πάτε σωστά: αν ένα νήμα worker γράφει στο backing stream, περάστε AOwnsStream = False και ελευθερώστε το stream μόνοι σας αφού το worker έχει σταματήσει, επειδή με την ιδιοκτησία παραχωρημένη η ακύρωση ελευθερώνει το stream ενώ ένα καθυστερημένο write μπορεί ακόμα να είναι στον δρόμο. Εξαιρέσεις που σηκώνονται μέσα στο OnCancelRequest καταπίνονται ανά request ώστε μια μεταφορά που αποτυγχάνει να μην μπλοκάρει τις υπόλοιπες ακυρώσεις
Πώς αποδεικνύει η σουίτα lifecycle ότι το μονοπάτι cancel δεν διαρρέει;
Η σουίτα stress lifecycle του PDFium Component εξασκεί ένα διακοπμένο κατέβασμα τύπου δικτύου σε κάθε μικτό κύκλο. Κάθε κύκλος ξεκινά progressive load του οποίου το store κρατά μόνο ένα τέταρτο των bytes του fixture, απαιτεί pdaNotAvailable με μη κενή λίστα hints, καλεί το CancelProgressiveLoad, και κάνει assert ότι το object δεν αναφέρει ούτε ProgressiveLoading ούτε Active· μετά τρέχει το ίδιο μονοπάτι streaming ως ολοκλήρωση με πλήρη διαθεσιμότητα, OpenProgressiveDocument, μια απεικόνιση και ένα κλείσιμο. Η default μικτή εκτέλεση καλύπτει 100 μετρημένους κύκλους με 600 ανοίγματα, 2300 απεικονίσεις και 100 progressive ακυρώσεις, και η δειγματοληπτική ιδιωτική μνήμη μεγάλωσε κατά 8,21 MiB απέναντι σε budget 32 MiB. Η σουίτα μετρά τις progressive ακυρώσεις ξεχωριστά από τις ακυρώσεις render-callback, επειδή ένα ματαιωμένο κατέβασμα και μια βρόχος απεικόνισης που σταματά νωρίς είναι διαφορετικά γεγονότα με διαφορετικά κριτήρια αποδοχής
Εκεί όπου το progressive μονοπάτι παύει να βοηθά
Λίγα όρια αξίζει να ξέρετε πριν χτίσετε viewer πάνω σε αυτό. Χαρακτηριστικά που χρειάζονται τα bytes του πρωτότυπου αρχείου αρνούνται μια ημιτελή progressive πηγή αντί να μαντέψουν: το ReadXmpPacket αποτυγχάνει ρητά και η επικύρωση υπογραφής αναφέρει Indeterminate μέχρι να είναι παρών ολόκληρο το αρχείο. Το default τεστ διαθεσιμότητας υποθέτει συνεχές πρόθεμα, οπότε μια μεταφορά που ανακτά εύρη εκτός σειράς πρέπει να τα ολοκληρώσει μέσω RangeRequests ή να απαντήσει μέσω OnDataAvailable, αλλιώς το PDFium θα εξακολουθεί να ζητά bytes που κρατάτε ήδη. Ένα μη linearized αρχείο δεν κερδίζει τίποτα σε χρόνο έως πρώτη σελίδα, οπότε αν η γρήγορη πρώτη απεικόνιση μετράει, γραμικοποιήστε το αρχείο στην πλευρά του server. Και το CancelProgressiveLoad δεν κλείνει μόνο του τα sockets σας· το OnCancelRequest είναι το hook όπου συμβαίνει αυτό
Για το σκέτο μονοπάτι του adapter stream που φορτώνει ένα πλήρες τοπικό αρχείο κατ' απαίτηση, δείτε το streaming μεγάλων PDF κατ' απαίτηση με PDFium· για άνοιγμα PDF που κάθεται μέσα σε μεγαλύτερο buffer, δείτε το byte range loading για ενσωματωμένα PDF. Η ακύρωση μιας αργής απεικόνισης σελίδας που είναι ήδη φορτωμένη είναι ξεχωριστός μηχανισμός, που καλύπτει το ματαιώσιμη progressive απεικόνιση σελίδας. Το TPdfProgressiveDocument και ο scheduler εύρων του έρχονται με το PDFium Component for Delphi and C++Builder