Ένα σαρωμένο αρχείο μπορεί να φτάσει τα αρκετά gigabyte σε ένα μόνο PDF. Ένα πρόγραμμα προβολής που ανοίγει ένα τέτοιο αρχείο συνήθως θέλει να εμφανίσει μία σελίδα, ίσως τον πίνακα περιεχομένων, ίσως μια σελίδα στην οποία μεταπήδησε ο χρήστης από έναν σελιδοδείκτη. Η ανάγνωση ολόκληρου του αρχείου στη μνήμη για την απόδοση δύο σελίδων είναι σπάταλη σε κάθε άξονα: καίει χώρο διευθύνσεων, καθυστερεί τον χρήστη πίσω από μια μακρά αρχική ανάγνωση και σε μια διεργασία 32-bit του Delphi μπορεί να αποτύχει εντελώς πριν εμφανιστεί έστω και μία σελίδα. Το PDFium δημιουργήθηκε με αυτό κατά νου. Μπορεί να φορτώσει ένα έγγραφο μέσω μιας επανάκλησης που ζητά τα συγκεκριμένα εύρη byte που χρειάζεται, όταν τα χρειάζεται, και δεν απαιτεί ποτέ ολόκληρο το αρχείο μεμιάς. Ένα όριο ανήκει στην αρχή: αυτό το κανάλι ροής περιγράφει το αρχείο με μήκος 32-bit, οπότε εξυπηρετεί ένα μόνο αρχείο έως 4 GiB, γεγονός που καλύπτει σχεδόν κάθε σαρωμένο αρχείο στην πράξη. Ένα αρχείο πέρα από αυτό το όριο δεν είναι περιοχή αυτού του άρθρου· θέλει να χωριστεί σε τόμους κατά τον χρόνο σάρωσης ή να ανοίξει μέσω μιας στρατηγικής άμεσης πρόσβασης (direct-access) αντ' αυτού, και ο φρουρός (guard) που επιβάλλει το ανώτατο όριο παίρνει ειλικρινά μια δική του ενότητα παρακάτω
Το εξάρτημα εκθέτει αυτή τη διαδρομή μέσω ενός προσαρμογέα ροής (stream adapter). Του παραδίδετε οποιοδήποτε TStream και το PDFium αντλεί μπλοκ από αυτήν τη ροή κατ' απαίτηση. Το αρχείο μπορεί να βρίσκεται στο δίσκο, σε ένα πεδίο blob βάσης δεδομένων ή πίσω από οποιονδήποτε άλλο απόγονο του TStream, και τίποτα από αυτό δεν αντιγράφεται εκ των προτέρων στη μνήμη
Πώς το PDFium ζητάει bytes
Το C API του PDFium φορτώνει ένα έγγραφο από ένα αντικείμενο παρεχόμενο από τον καλούντα που περιγράφεται από τη δομή FPDF_FILEACCESS. Η δομή έχει τρία μέρη που έχουν σημασία εδώ: ένα πεδίο μήκους, μια επανάκληση ανάγνωσης και μια αδιαφανή παράμετρο χρήστη. Το σημείο εισόδου που την καταναλώνει είναι το FPDF_LoadCustomDocument. Μόλις το PDFium κρατήσει αυτή τη δομή, αναλύει το trailer, εντοπίζει τον πίνακα διασταυρούμενων αναφορών (cross-reference table) και από εκεί και πέρα διαβάζει μόνο ό,τι απαιτεί μια δεδομένη λειτουργία. Το άνοιγμα του εγγράφου αγγίζει την ουρά του αρχείου και μια χούφτα αντικείμενα καταλόγου. Η απόδοση της σελίδας 400 διαβάζει τις ροές περιεχομένου και τους πόρους για αυτήν τη σελίδα και τίποτα άλλο
Αυτή είναι η διαφορά μεταξύ ενός φορτίου στην προσωρινή μνήμη (buffered load) και ενός φορτίου ροής (streaming load). Μια προσωρινή φόρτωση διαβάζει το αρχείο από άκρη σε άκρη πριν το PDFium δει το μηδενικό byte. Μια φόρτωση ροής αντιστρέφει τη σχέση: το PDFium οδηγεί τις αναγνώσεις και τα byte που δεν αγγίζονται ποτέ, δεν διαβάζονται ποτέ. Για ένα αρχείο πολλών gigabyte που προβάλλεται μία σελίδα τη φορά, αυτό είναι το χάσμα μεταξύ μιας άχρηστης φόρτωσης και μιας άμεσης φόρτωσης
Ο προσαρμογέας ροής (stream adapter)
Ο προσαρμογέας που γεφυρώνει ένα TStream του Delphi στο FPDF_FILEACCESS είναι το TPdfStreamAdapter. Ο κατασκευαστής (constructor) του παίρνει τη ροή και μια σημαία ιδιοκτησίας, καταγράφει το μήκος της ροής μία φορά, συμπληρώνει την εγγραφή FPDF_FILEACCESS και συνδέει την επανάκληση ανάγνωσης. Όταν το PDFium καλεί αργότερα με ένα offset και ένα μέγεθος, ο προσαρμογέας αναζητά στη ροή αυτό το offset και αντιγράφει ακριβώς αυτό το εύρος στο 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 και θα καλέσει πίσω σε οποιοδήποτε σημείο ενώ το έγγραφο είναι ανοιχτό, όχι μόνο κατά την αρχική φόρτωση
Γιατί η επανάκληση (callback) είναι μια στατική συνάρτηση
Η επανάκληση ανάγνωσης που αποθηκεύει το PDFium στο m_GetBlock είναι ένας απλός δείκτης συνάρτησης C με τη σύμβαση κλήσης cdecl. Μια μέθοδος Delphi δεν μπορεί να χρησιμοποιηθεί άμεσα, επειδή μια μέθοδος φέρει ένα κρυφό όρισμα Self για το οποίο ένας καλών της C δεν γνωρίζει τίποτα και δεν θα παρέχει ποτέ. Ο προσαρμογέας, επομένως, δηλώνει την επανάκληση ως class function με σήμανση cdecl; static, η οποία μεταγλωττίζεται σε μια αυτόνομη συνάρτηση με τη διάταξη πλαισίου C που περιμένει το PDFium και κανένα σιωπηρό Self
Αυτό λύνει τη σύμβαση κλήσης, αλλά εγείρει ένα δεύτερο ερώτημα: χωρίς το Self, πώς φτάνει η επανάκληση στη συγκεκριμένη ροή από την οποία υποτίθεται ότι διαβάζει; Η απάντηση είναι η αδιαφανής παράμετρος χρήστη. Όταν ο προσαρμογέας δημιουργεί την εγγραφή, αποθηκεύει τον δικό του δείκτη παρουσίας (instance pointer) στο m_Param. Το PDFium επιστρέφει τον ίδιο δείκτη πίσω ως το πρώτο όρισμα κάθε επανάκλησης. Η στατική συνάρτηση τον μετατρέπει ξανά σε ένα TPdfStreamAdapter και αποστέλλει την ανάγνωση έναντι της ροής αυτής της παρουσίας. Αυτό είναι το τυπικό τραμπολίνο για την παράδοση περιβάλλοντος αντικειμένου σε ένα όριο 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 και γιατί χρειάζεται προστασία (guard)
Από εδώ προέρχεται το όριο που αναφέρθηκε στην αρχή. Το πεδίο μήκους m_FileLen στο FPDF_FILEACCESS είναι μια τιμή 32-bit χωρίς πρόσημο. Το μεγαλύτερο αναπαραστάσιμο μήκος του είναι ένα byte λιγότερο από 4 GiB. Ένα TStream αναφέρει το μέγεθός του ως Int64, οπότε μια ροή μπορεί να περιγράψει πολύ περισσότερα byte από όσα μπορεί να χωρέσει το πεδίο. Τη στιγμή που το μέγεθος μιας ροής υπερβαίνει αυτό το ανώτατο όριο, δεν υπάρχει ειλικρινής τρόπος να πούμε στο PDFium πόσο μεγάλο είναι το αρχείο
Η λάθος απάντηση είναι να εκχωρήσετε το μέγεθος και να το αφήσετε να αναδιπλωθεί (wrap). Η περικοπή (truncating) ενός μήκους 5 GiB σε ένα πεδίο 32-bit παράγει έναν μικρό, αληθοφανή αριθμό, και το PDFium στη συνέχεια θα αναλύσει το αρχείο πιστεύοντας ότι τελειώνει περίπου στο ένα gigabyte. Το trailer και ο πίνακας διασταυρούμενων αναφορών βρίσκονται στο πραγματικό τέλος του αρχείου, πολύ μετά το περικομμένο μήκος, οπότε η ανάλυση αποτυγχάνει με τρόπο που δεν έχει καμία σχέση με την πραγματική αιτία. Θα διορθώνατε (debugging) ένα σφάλμα διασταυρούμενης αναφοράς σε ένα αρχείο που είναι απολύτως έγκυρο, χωρίς καμία ένδειξη ότι ένας ακέραιος έχει αναδιπλωθεί δύο επίπεδα πιο πάνω
Αντ' αυτού, ο προσαρμογέας αρνείται την είσοδο. Ο κατασκευαστής συγκρίνει το μέγεθος της ροής με το High(FPDF_DWORD) και εγείρει ένα EPdfError τη στιγμή που η ροή είναι πολύ μεγάλη για να περιγραφεί. Ένα ρητό, άμεσο σφάλμα κατονομάζει το πραγματικό πρόβλημα στο σημείο της κατασκευής. Μια σιωπηρή περικοπή το κρύβει πίσω από ένα παραπλανητικό σύμπτωμα που θα κυνηγούσατε πολύ αργότερα. Το όριο των 4 GiB είναι ένας γνήσιος περιορισμός αυτής της διαδρομής φόρτωσης, και το ειλικρινές είναι να το εμφανίσετε δυνατά αντί να το καλύψετε με αριθμητική που τυχαίνει να μεταγλωττίζεται. Όταν ένα αρχείο (archive) ξεπερνά πραγματικά τη γραμμή, οι θεραπείες που υποσχέθηκαν στην κορυφή ζουν εκτός αυτού του API: χωρίστε τη σάρωση σε αρχεία ανά τόμο που παραμένουν το καθένα κάτω από το ανώτατο όριο, ή αφήστε το έγγραφο στον δίσκο και εξυπηρετήστε το μέσω ενός σχεδιασμού άμεσης πρόσβασης (direct-access) που βασίζεται σε offset 64-bit αντί μέσω του FPDF_FILEACCESS
Οι αποτυχίες δεν πρέπει να διασχίζουν το όριο
Μια ανάγνωση μπορεί να αποτύχει. Η ροή (stream) μπορεί να είναι ένα αντικείμενο υποστηριζόμενο από δίκτυο που λήγει (times out), μια λαβή blob που έκλεισε κάτω από εσάς, ή ένα αρχείο που περικόπηκε μετά το άνοιγμα του εγγράφου. Το συμβόλαιο του PDFium για την επανάκληση ανάγνωσης είναι μια τιμή επιστροφής: μη μηδενική για επιτυχία, μηδέν για αποτυχία. Είναι ένα πλαίσιο C, και δεν έχει κανένα μηχανισμό για τη σύλληψη ή τη διάδοση μιας εξαίρεσης της Pascal
Αυτός είναι ο λόγος για τον οποίο το τραμπολίνο τυλίγει την αναζήτηση (seek) και την ανάγνωση σε ένα try/except που καταπίνει την εξαίρεση και επιστρέφει μηδέν. Εάν επιτρεπόταν να διαδοθεί μια εξαίρεση του Delphi εκτός της επανάκλησης, θα ξετυλιγόταν μέσω των πλαισίων στοίβας cdecl του PDFium, τα οποία δεν κατασκευάστηκαν ποτέ για να ξετυλιχτούν από τον μηχανισμό εξαιρέσεων της Pascal. Το αποτέλεσμα είναι μη καθορισμένη συμπεριφορά στην καλύτερη περίπτωση και σκληρή κατάρρευση στη χειρότερη, βαθιά μέσα στον αναλυτή (parser) PDF χωρίς χρησιμοποιήσιμη στοίβα. Η επιστροφή του μηδενός διατηρεί την αποτυχία μέσα στο συμβόλαιο. Το PDFium βλέπει μια αποτυχημένη ανάγνωση μπλοκ, ματαιώνει καθαρά τη λειτουργία, και το FPDF_LoadCustomDocument αναφέρει ότι το έγγραφο δεν μπόρεσε να φορτωθεί, το οποίο το εξάρτημα εμφανίζει ως EPdfError στην πλευρά της Pascal όπου ανήκει
Το άνοιγμα ενός εγγράφου με αυτόν τον τρόπο
Η μέθοδος του εξαρτήματος που οδηγεί τη διαδρομή ροής είναι το LoadCustomDocument, που δηλώνεται ως μια ξεχωριστή μέθοδος και όχι ως μια άλλη υπερφόρτωση του LoadDocument, έτσι ώστε η διέλευση ενός TMemoryStream να μην καταλήγει ποτέ κατά λάθος στη διαδρομή με προσωρινή μνήμη (buffered path). Δημιουργεί τον προσαρμογέα, καλεί το 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. Η φόρτωση κατ' απαίτηση αξίζει τον κόπο όταν το αρχείο είναι μεγάλο και μόνο ένα μέρος του θα διαβαστεί: ένα πρόγραμμα προβολής αρχειοθήκης, μια γεννήτρια μικρογραφιών που δειγματίζει μερικές σελίδες, ένα ευρετήριο αναζήτησης που αντλεί μία σελίδα τη φορά. Όταν το αρχείο είναι μικρό ή πρόκειται να το διαβάσετε ολόκληρο ούτως ή άλλως, μια φόρτωση στην προσωρινή μνήμη (buffered load) είναι απλούστερη και ο μηχανισμός ροής δεν σας προσφέρει τίποτα. Ο καθοριστικός παράγοντας είναι η αναλογία των byte που θα αγγίξετε πραγματικά προς τα byte που περιέχει το αρχείο
Μόλις οι σελίδες έρθουν με ροή κατ' απαίτηση, το επόμενο μέλημα είναι να διατηρηθούν οι αποδιδόμενες σελίδες ανταποκρίσιμες (responsive) καθώς ο χρήστης κάνει μεγέθυνση (zoom) και κύλιση (scroll), κάτι που καλύπτεται στο σημείωμά μας για την προσωρινή αποθήκευση απόδοσης και την απόδοση της μεγέθυνσης. Όταν το έγγραφο ροής είναι ένα έγγραφο που πρέπει να εμφανίσει ένα πρόγραμμα προβολής, αλλά όχι να επιτρέψει στον χρήστη να εξαγάγει ή να αλλάξει, οι τεχνικές στην αναλυτική παρουσίαση της ασφαλούς προεπισκόπησης PDF ταιριάζουν φυσικά με αυτήν τη διαδρομή φόρτωσης. Και τα δύο βασίζονται στη φόρτωση ροής που περιγράφεται εδώ, η οποία διατίθεται ως μέρος του PDFium Component για Delphi και C++Builder παράλληλα με τα API απόδοσης, εξαγωγής κειμένου και σχολιασμού που καλύπτονται αλλού σε αυτό το ιστολόγιο