Ένα σαρωμένο αρχείο μπορεί να φτάσει σε μέγεθος αρκετών gigabyte σε ένα μόνο PDF. Ένα πρόγραμμα προβολής που ανοίγει ένα τέτοιο αρχείο συνήθως θέλει να δείξει μία σελίδα, ίσως τον πίνακα περιεχομένων, ίσως μια σελίδα στην οποία μετέβη ο χρήστης από έναν σελιδοδείκτη. Η ανάγνωση ολόκληρου του αρχείου στη μνήμη για την απόδοση δύο σελίδων είναι σπατάλη σε κάθε άξονα: καταναλώνει χώρο διευθύνσεων, καθυστερεί τον χρήστη πίσω από μια μεγάλη αρχική ανάγνωση, και σε μια διεργασία Delphi 32 bit μπορεί να αποτύχει εντελώς πριν εμφανιστεί έστω και μία σελίδα. Το PDFium κατασκευάστηκε με αυτό κατά νου. Μπορεί να φορτώσει ένα έγγραφο μέσω μιας ανάκλησης (callback) που ζητά τα συγκεκριμένα εύρη byte που χρειάζεται, όταν τα χρειάζεται, και δεν απαιτεί ποτέ ολόκληρο το αρχείο ταυτόχρονα.
Το στοιχείο (component) εκθέτει αυτή τη διαδρομή μέσω ενός προσαρμογέα ροής (stream adapter). Του παραδίδετε οποιοδήποτε TStream, και το PDFium τραβάει μπλοκ από αυτή τη ροή κατά παραγγελία (on demand). Το αρχείο μπορεί να βρίσκεται στο δίσκο, σε ένα πεδίο blob βάσης δεδομένων ή πίσω από οποιονδήποτε άλλο απόγονο του TStream, και τίποτα από αυτά δεν αντιγράφεται εκ των προτέρων στη μνήμη.
Πώς το PDFium ζητάει byte
Το C API του PDFium φορτώνει ένα έγγραφο από ένα αντικείμενο που παρέχει ο καλών και περιγράφεται από τη δομή FPDF_FILEACCESS. Η δομή έχει τρία μέρη που έχουν σημασία εδώ: ένα πεδίο μήκους, μια ανάκληση ανάγνωσης και μια αδιαφανή παράμετρο χρήστη. Το σημείο εισόδου που την καταναλώνει είναι η FPDF_LoadCustomDocument. Μόλις το PDFium λάβει αυτή τη δομή, αναλύει το trailer, εντοπίζει τον πίνακα διασταυρούμενων αναφορών και από εκεί και πέρα διαβάζει μόνο ό,τι απαιτεί μια δεδομένη λειτουργία. Το άνοιγμα του εγγράφου αγγίζει το τέλος του αρχείου και μερικά αντικείμενα καταλόγου. Η απόδοση της σελίδας 400 διαβάζει τις ροές περιεχομένου και τους πόρους για αυτήν τη σελίδα και τίποτα άλλο.
Αυτή είναι η διαφορά μεταξύ μιας buffer-φορτωμένης φόρτωσης (buffered load) και μιας ροϊκής φόρτωσης (streaming load). Μια buffered φόρτωση διαβάζει το αρχείο από άκρη σε άκρη πριν το PDFium δει το byte μηδέν. Μια streaming φόρτωση αντιστρέφει τη σχέση: το PDFium κατευθύνει τις αναγνώσεις, και τα byte που δεν αγγίζονται ποτέ δεν διαβάζονται. Για ένα αρχείο πολλών gigabyte που προβάλλεται μία σελίδα τη φορά, αυτή είναι η διαφορά μεταξύ μιας άχρηστης φόρτωσης και μιας στιγμιαίας.
Ο προσαρμογέας ροής
Ο προσαρμογέας που γεφυρώνει ένα Delphi TStream με το FPDF_FILEACCESS είναι ο TPdfStreamAdapter. Ο κατασκευαστής του λαμβάνει τη ροή και μια σημαία ιδιοκτησίας, καταγράφει το μήκος της ροής μία φορά, συμπληρώνει την εγγραφή FPDF_FILEACCESS και συνδέει την ανάκληση ανάγνωσης. Όταν το PDFium καλέσει αργότερα πίσω με μια μετατόπιση και ένα μέγεθος, ο προσαρμογέας μετακινεί τη ροή σε αυτήν τη μετατόπιση και αντιγράφει ακριβώς αυτό το εύρος στο buffer που παρείχε το PDFium.
// Verbatim from the component: the stream-to-FPDF_FILEACCESS bridge
constructor TPdfStreamAdapter.Create(AStream: TStream; AOwnsStream: Boolean);
begin
inherited Create;
if AStream = nil then
raise EPdfError.Create('TPdfStreamAdapter: AStream is nil');
FStream := AStream;
FOwnsStream := AOwnsStream;
// FPDF_FILEACCESS.m_FileLen is a 32-bit unsigned long. Refuse a stream
// that would silently truncate past 4 GiB.
if AStream.Size > High(FPDF_DWORD) then
raise EPdfError.Create('TPdfStreamAdapter: stream exceeds the 4 GiB limit');
FillChar(FFileAccess, SizeOf(FFileAccess), 0);
FFileAccess.m_FileLen := FPDF_DWORD(AStream.Size);
FFileAccess.m_GetBlock := GetBlockCallback;
FFileAccess.m_Param := Self;
end;
Η σημαία ιδιοκτησίας αποφασίζει ποιος ελευθερώνει τη ροή. Περάστε την τιμή False και ο καλών διατηρεί τη ροή και πρέπει να την κρατήσει ζωντανή για όλη τη διάρκεια ζωής του εγγράφου. Περάστε την τιμή True και ο προσαρμογέας αναλαμβάνει τον έλεγχο, ελευθερώνοντας τη ροή όταν κλείνει το έγγραφο. Σε κάθε περίπτωση, η ροή πρέπει να επιβιώσει από κάθε ανάγνωση που θα εκτελέσει το PDFium, επειδή το PDFium κρατά τον δείκτη FPDF_FILEACCESS και θα καλέσει πίσω σε οποιοδήποτε σημείο ενώ το έγγραφο είναι ανοιχτό, όχι μόνο κατά την αρχική φόρτωση.
Γιατί η ανάκληση είναι μια στατική συνάρτηση
Η ανάκληση ανάγνωσης που αποθηκεύει το PDFium στο m_GetBlock είναι ένας απλός δείκτης συνάρτησης C με τη σύμβαση κλήσης cdecl. Μια μέθοδος Delphi δεν μπορεί να χρησιμοποιηθεί απευθείας, επειδή μια μέθοδος μεταφέρει ένα κρυφό όρισμα Self για το οποίο ένας καλών C δεν γνωρίζει τίποτα και δεν θα παρέχει ποτέ. Επομένως, ο προσαρμογέας δηλώνει την ανάκληση ως class function με την ένδειξη cdecl; static, η οποία μεταγλωττίζεται σε μια ελεύθερη συνάρτηση με τη διάταξη πλαισίου C που αναμένει το PDFium και χωρίς έμμεσο Self.
Αυτό λύνει το θέμα της σύμβασης κλήσης αλλά εγείρει ένα δεύτερο ερώτημα: χωρίς το Self, πώς φτάνει η ανάκληση στη συγκεκριμένη ροή από την οποία υποτίθεται ότι πρέπει να διαβάσει; Η απάντηση είναι η αδιαφανής παράμετρος χρήστη. Όταν ο προσαρμογέας δημιουργεί την εγγραφή, αποθηκεύει τον δικό του δείκτη στιγμιοτύπου (instance pointer) στο m_Param. Το PDFium επιστρέφει αυτόν τον ίδιο δείκτη ως το πρώτο όρισμα κάθε ανάκλησης. Η στατική συνάρτηση τον μετατρέπει ξανά σε TPdfStreamAdapter και αποστέλλει την ανάγνωση έναντι της ροής αυτού του στιγμιοτύπου. Αυτή είναι η τυπική μέθοδος αναπήδησης (trampoline) για τη μεταβίβαση του πλαισίου αντικειμένου σε ένα όριο C που δεν έχει καμία έννοια αντικειμένων.
// Verbatim from the component: the cdecl trampoline back to the instance
class function TPdfStreamAdapter.GetBlockCallback(
param : Pointer;
position: FPDF_DWORD;
pBuf : PByte;
size : FPDF_DWORD): Integer; cdecl;
var
Adapter: TPdfStreamAdapter;
begin
Result := 0;
if (param = nil) or (pBuf = nil) or (size = 0) then
Exit;
Adapter := TPdfStreamAdapter(param); // recover the instance from m_Param
if Adapter.FStream = nil then
Exit;
try
Adapter.FStream.Position := Int64(position);
Adapter.FStream.ReadBuffer(pBuf^, Int64(size));
Result := 1;
except
Result := 0; // report failure by return value, never by raising
end;
end;
Η οροφή των 4 GiB και γιατί χρειάζεται φύλακα
Το πεδίο μήκους m_FileLen στο FPDF_FILEACCESS είναι μια μη προσημασμένη τιμή 32 bit. Το μεγαλύτερο αναπαραστάσιμο μήκος του είναι ένα byte λιγότερο από 4 GiB. Ένα TStream αναφέρει το μέγεθός του ως Int64, επομένως μια ροή μπορεί να περιγράψει πολύ περισσότερα byte από όσα μπορεί να χωρέσει το πεδίο. Τη στιγμή που το μέγεθος μιας ροής υπερβαίνει αυτήν την οροφή, δεν υπάρχει ειλικρινής τρόπος να πείτε στο PDFium πόσο μεγάλο είναι το αρχείο.
Η λάθος αντίδραση είναι να εκχωρήσετε το μέγεθος και να το αφήσετε να αναδιπλωθεί. Η περικοπή ενός μήκους 5 GiB σε ένα πεδίο 32 bit παράγει έναν μικρό, εύλογο αριθμό, και το PDFium θα αναλύσει στη συνέχεια το αρχείο πιστεύοντας ότι τελειώνει περίπου στο ένα gigabyte. Το trailer και ο πίνακας διασταυρούμενων αναφορών βρίσκονται στο πραγματικό τέλος του αρχείου, πολύ πέρα από το περικομμένο μήκος, οπότε η ανάλυση αποτυγχάνει με τρόπο που δεν έχει καμία σχέση με την πραγματική αιτία. Θα αποσφαλματώνατε ένα σφάλμα διασταυρούμενης αναφοράς σε ένα αρχείο που είναι απόλυτα έγκυρο, χωρίς καμία ένδειξη ότι ένας ακέραιος αναδιπλώθηκε δύο επίπεδα πιο πάνω.
Αντίθετα, ο προσαρμογέας απορρίπτει την είσοδο. Ο κατασκευαστής συγκρίνει το μέγεθος της ροής με το High(FPDF_DWORD) και εγείρει EPdfError τη στιγμή που η ροή είναι πολύ μεγάλη για να περιγραφεί. Ένα ρητό, άμεσο σφάλμα κατονομάζει το πραγματικό πρόβλημα στο σημείο της κατασκευής. Μια σιωπηλή περικοπή το κρύβει πίσω από ένα παραπλανητικό σύμπτωμα που θα κυνηγούσατε πολύ αργότερα. Το όριο των 4 GiB είναι ένας πραγματικός περιορισμός αυτής της διαδρομής φόρτωσης, και το ειλικρινές είναι να το φέρετε στην επιφάνεια με έντονο τρόπο αντί να το συγκαλύψετε με αριθμητική που τυχαίνει να μεταγλωττίζεται.
Οι αποτυχίες δεν πρέπει να ξεπερνούν το όριο
Μια ανάγνωση μπορεί να αποτύχει. Η ροή μπορεί να είναι ένα αντικείμενο που υποστηρίζεται από δίκτυο και λήγει το χρονικό του όριο, μια λαβή blob που έκλεισε από κάτω σας ή ένα αρχείο που περικόπηκε μετά το άνοιγμα του εγγράφου. Το συμβόλαιο του PDFium για την ανάκληση ανάγνωσης είναι μια τιμή επιστροφής: μη μηδενική για επιτυχία, μηδέν για αποτυχία. Είναι ένα πλαίσιο C και δεν διαθέτει μηχανισμό για τη σύλληψη ή τη διάδοση μιας εξαίρεσης Pascal.
Αυτός είναι ο λόγος για τον οποίο η μέθοδος αναπήδησης (trampoline) τυλίγει την αναζήτηση και την ανάγνωση σε ένα try/except που καταπίνει την εξαίρεση και επιστρέφει μηδέν. Εάν επιτρεπόταν σε μια εξαίρεση Delphi να διαδοθεί έξω από την ανάκληση, θα εκτελούσε unwind μέσω των πλαισίων στοίβας cdecl του PDFium, τα οποία δεν κατασκευάστηκαν ποτέ για να εκτελούν unwind από τον μηχανισμό εξαιρέσεων της Pascal. Το αποτέλεσμα είναι απροσδιόριστη συμπεριφορά στην καλύτερη περίπτωση και σκληρή κατάρρευση στη χειρότερη, βαθιά μέσα στον αναλυτή PDF χωρίς χρήσιμη στοίβα. Η επιστροφή μηδενός διατηρεί την αποτυχία εντός του συμβολαίου. Το PDFium βλέπει μια αποτυχημένη ανάγνωση μπλοκ, ματαιώνει τη λειτουργία καθαρά, και η FPDF_LoadCustomDocument αναφέρει ότι το έγγραφο δεν μπόρεσε να φορτωθεί, το οποίο το στοιχείο εμφανίζει ως EPdfError στην πλευρά της Pascal όπου ανήκει.
Άνοιγμα εγγράφου με αυτόν τον τρόπο
Η μέθοδος του στοιχείου που οδηγεί τη διαδρομή streaming είναι η LoadCustomDocument, η οποία δηλώνεται ως ξεχωριστή μέθοδος και όχι ως άλλη μια υπερφόρτωση της LoadDocument, ώστε η μεταβίβαση ενός TMemoryStream να μην προσγειώνεται ποτέ κατά λάθος στη buffered διαδρομή. Δημιουργεί τον προσαρμογέα, καλεί την FPDF_LoadCustomDocument και διατηρεί τον προσαρμογέα ζωντανό για τη διάρκεια ζωής του φορτωμένου εγγράφου.
var
Pdf: TPdf;
FileStream: TFileStream;
begin
Pdf := TPdf.Create(nil);
FileStream := TFileStream.Create('Archive_4GB.pdf', fmOpenRead or fmShareDenyWrite);
try
// Hand stream ownership to Pdf: it frees FileStream when the document closes.
Pdf.LoadCustomDocument(FileStream, True);
// PDFium has read only the trailer and catalog so far.
// Rendering a page pulls just that page's bytes through the callback.
// ... render or inspect pages here ...
finally
Pdf.Free; // closes the document, which frees the adapter and the stream
end;
end;
Η ίδια κλήση λειτουργεί για ένα TMemoryStream, μια ροή blob από ένα σύνολο δεδομένων βάσης δεδομένων ή έναν προσαρμοσμένο απόγονο του TStream. Η φόρτωση κατά παραγγελία (on-demand) δικαιολογεί την ύπαρξή της όταν το αρχείο είναι μεγάλο και μόνο ένα μέρος του θα διαβαστεί: ένα πρόγραμμα προβολής αρχείων, μια γεννήτρια μικρογραφιών που δειγματίζει μερικές σελίδες, ένα ευρετήριο αναζήτησης που τραβάει μία σελίδα τη φορά. Όταν το αρχείο είναι μικρό ή πρόκειται να το διαβάσετε ολόκληρο ούτως ή άλλως, μια buffered φόρτωση είναι απλώς απλούστερη και ο μηχανισμός streaming δεν σας προσφέρει τίποτα. Ο καθοριστικός παράγοντας είναι η αναλογία των byte που θα αγγίξετε πραγματικά προς τα byte που περιέχει το αρχείο.
Μόλις οι σελίδες ρέουν κατά παραγγελία, το επόμενο μέλημα είναι η διατήρηση της ανταπόκρισης των αποδιδόμενων σελίδων καθώς ο χρήστης εστιάζει (zooms) και κυλάει (scrolls), θέμα που καλύπτεται στη σημείωσή μας για την προσωρινή μνήμη απόδοσης (render caching) και την απόδοση του zoom. Όταν το έγγραφο ροής είναι ένα έγγραφο που ένα πρόγραμμα προβολής πρέπει να εμφανίσει αλλά να μην επιτρέψει στον χρήστη να το εξαγάγει ή να το τροποποιήσει, οι τεχνικές στον οδηγό για την ασφαλή προεπισκόπηση PDF συνδυάζονται φυσικά με αυτήν τη διαδρομή φόρτωσης. Και τα δύο βασίζονται στη streaming φόρτωση που περιγράφεται εδώ, η οποία παρέχεται ως μέρος του PDFium Component για Delphi και C++Builder, μαζί με τα API απόδοσης, εξαγωγής κειμένου και σχολιασμών που καλύπτονται σε άλλα σημεία αυτού του ιστολογίου.