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

Φόρτωση Εύρους Byte PDFium για Ενσωματωμένα PDF σε Delphi

Το PDFium Component μπορεί να ανοίξει ένα PDF που ζει μέσα σε ένα μεγαλύτερο buffer απευθείας από εύρος byte. Η υπερφόρτωση LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean) απευθύνεται σε ένα παράθυρο επί τόπου, οπότε δεν χρειάζεται προκαταρκτικό Copy. Σε αντάλλαγμα σας ζητά να καταλάβετε έναν κανόνα: όταν το Buffered είναι False, ο υποστηρικτικός πίνακας δανείζεται, δεν αντιγράφεται

Αυτός είναι διαφορετικός μηχανισμός από την προσέγγιση με callback που περιγράφεται στο streaming μεγάλων PDF κατά παραγγελία με PDFium VCL, που δίνει στο PDFium έναν reader FPDF_FILEACCESS και το αφήνει να τραβήξει μπλοκ από τον δίσκο όποτε τα χρειάζεται. Εκείνο είναι για έγγραφα υπερβολικά μεγάλα για να χωρέσουν στη RAM. Αυτό είναι για έγγραφα ήδη στη RAM, καθισμένα σε γνωστή θέση μέσα σε κάτι άλλο. Τα δύο είναι συμπληρωματικά, και η τελευταία ενότητα εξηγεί ποια κατάσταση ανήκει σε ποιο

Η αντιγραφή 40 MB που κανείς δεν ζήτησε

Το σενάριο εμφανίζεται όπου τα PDF ταξιδεύουν μέσα σε άλλες μορφές. Ένα mail store κρατά σώματα μηνυμάτων και συνημμένα σε μία εγγραφή. Ένα container αρχείου συνενώνει ένα manifest, μερικές εικόνες και ένα PDF. Ένα προσαρμοσμένο πρωτόκολλο καλωδίου πλαισιώνει ένα έγγραφο πίσω από μια κεφαλίδα με πρόθεμα μήκους. Σε κάθε περίπτωση καταλήγετε να κρατάτε ένα μεγάλο TBytes και να ξέρετε ότι το PDF ξεκινά στο byte 1.182.336 και τρέχει για 312 kilobytes

Πριν υπάρξει η υπερφόρτωση εύρους byte, η ιδιωματική απάντηση ήταν Copy(Data, Index, Count), που δεσμεύει έναν δεύτερο πίνακα και κάνει memcpy το παράθυρο μέσα του. Μετά δίνετε αυτό το τμήμα στην LoadDocument με Buffered = True, που το αντιγράφει ξανά στο ιδιωτικό buffer του component. Δύο αντίγραφα των ίδιων bytes, το ένα καθαρή τελετουργία, και σε μια μεγάλη σάρωση mailbox επαναλαμβανόμενο για κάθε μήνυμα. Η υπερφόρτωση εύρους byte αφαιρεί την πρώτη αντιγραφή άνευ όρων και τη δεύτερη προαιρετικά

Τι κάνει στην πραγματικότητα η υπερφόρτωση εύρους byte

Η υπερφόρτωση είναι λεπτή εκ σχεδιασμού: επικυρώνει, υπολογίζει έναν δείκτη, και αναθέτει στη μορφή δείκτη της LoadDocument στην οποία διοχετεύεται ήδη όλη η οικογένεια. Το Index είναι βασισμένο στο μηδέν, το Count είναι μήκος byte, και το Buffered έχει προεπιλογή True ακριβώς όπως στις άλλες υπερφορτώσεις. Η μονο-παραμετρική LoadDocument(const Data: TBytes; Buffered: Boolean) είναι η ίδια τώρα απλώς μια κλήση σε αυτήν με Index = 0 και Count = Length(Data), οπότε υπάρχει μία διαδρομή επικύρωσης αντί για δύο

Η κλήση της μοιάζει με τον κώδικα που ήδη γράφατε, μείον το τμήμα

var
  Frame: TBytes;          // whole container record, tens of megabytes
  Offset, Size: Integer;
begin
  Frame := LoadContainerRecord('mailbox.dat');
  LocateEmbeddedPdf(Frame, Offset, Size);   // your container parser

  // No Copy(Frame, Offset, Size) here - the window is addressed in place
  Pdf.LoadDocument(Frame, Offset, Size, True);
  try
    RenderPreview(Pdf);
  finally
    Pdf.UnloadDocument;
  end;
end;

Γιατί το Index συν Count υπερχειλίζει τον έλεγχο ορίων;

Επειδή τα Index και Count είναι και τα δύο Integer, και το άθροισμα δύο μεγάλων θετικών τιμών Integer δεν είναι απαραίτητα ένας μεγάλος θετικός Integer. Αυτός είναι ο τεχνικός πυρήνας της υπερφόρτωσης, και είναι το ένα σημείο όπου ένας φυσιολογικός έλεγχος είναι τρύπα ασφάλειας μνήμης. Η προφανής διατύπωση είναι λάθος

// WRONG: Index + Count is evaluated in Integer and can wrap negative
if Index + Count <= Length(Data) then
  DataPtr := @Data[Index];

// RIGHT: reject signs first, then bound each term separately,
// with the only arithmetic done as a subtraction that cannot wrap
Check(Index >= 0,  'PDF byte range index cannot be negative');
Check(Count >= 0,  'PDF byte range count cannot be negative');
Check(Index <= Length(Data), 'PDF byte range index exceeds data length');
Check(Count <= Length(Data) - Index, 'PDF byte range exceeds data length');

Δουλέψτε την περίπτωση αποτυχίας. Πάρτε Index = 2000000000 και Count = 2000000000. Το πραγματικό τους άθροισμα είναι τέσσερα δισεκατομμύρια, αλλά σε 32-bit αριθμητική με πρόσημο το αποτέλεσμα τυλίγεται σε ακριβώς μείον 294.967.296. Αυτή η τιμή είναι άνετα μικρότερη από το Length(Data), οπότε ο λάθος έλεγχος περνά, το @Data[Index] λαμβάνεται πολύ έξω από τον πίνακα, και στο PDFium δίνεται ένας άγριος δείκτης συν ένα μήκος δύο gigabyte. Αυτό που ακολουθεί είναι μια παραβίαση πρόσβασης σε καλή μέρα και σιωπηλή ανάλυση άσχετης μνήμης διεργασίας σε κακή

Η σωστή σειρά το διορθώνει μη προσθέτοντας ποτέ. Τα αρνητικά απορρίπτονται πριν δεικτοδοτηθεί οτιδήποτε, οπότε το @Data[Index] ποτέ δεν μπορεί να ληφθεί κάτω από τον πίνακα. Μετά το Index οριοθετείται μόνο του έναντι του Length(Data), που εγγυάται ότι το Length(Data) - Index είναι μη αρνητικό Integer. Μόνο τότε το Count συγκρίνεται με εκείνο το υπόλοιπο. Κάθε ενδιάμεση τιμή παραμένει μέσα στο αναπαραστάσιμο εύρος, οπότε καμία διαμόρφωση build δεν μπορεί να αλλάξει το αποτέλεσμα. Μην μπείτε στον πειρασμό να βασιστείτε στον έλεγχο υπερχείλισης {$Q+} ως δίχτυ ασφαλείας: τα builds κυκλοφορίας συνήθως αποστέλλονται με αυτόν απενεργοποιημένο, και ακόμη κι όταν είναι ενεργός έχετε μετατρέψει ένα σφάλμα ασφάλειας μνήμης σε ένα EIntOverflow που διαφεύγει από τη μέση μιας ρουτίνας επικύρωσης. Το PDFium Component αντιμετωπίζει τη μη αξιόπιστη αριθμητική μήκους με τον ίδιο τρόπο που αντιμετωπίζει το υπόλοιπο του ορίου, μια πειθαρχία που καλύπτεται ευρύτερα στην ενίσχυση του ABI PDFium VCL και ασφάλεια μνήμης σε Delphi

Γιατί πρέπει ένα παράθυρο μηδενικού μήκους να περνά nil;

Επειδή το @Data[Index] δεν είναι νόμιμη έκφραση για κάθε Index που αποδέχεται η επικύρωση. Το Index = Length(Data) με Count = 0 είναι ένα απόλυτα καλοσχηματισμένο κενό παράθυρο στην ουρά του buffer, και ένα κενό TBytes δίνει Index = 0 σε έναν πίνακα που δεν έχει καθόλου στοιχείο μηδέν. Η λήψη της διεύθυνσης σε οποιαδήποτε περίπτωση δεικτοδοτεί πέρα από το τέλος, ή απο-αναφέρει έναν nil δυναμικό πίνακα. Οπότε η υπερφόρτωση διακλαδίζεται: Count = 0 αποδίδει έναν δείκτη nil, οποιοδήποτε άλλο count αποδίδει @Data[Index]. Το nil τότε ρέει στην υπερφόρτωση δείκτη, της οποίας η δική της φρουρά δέχεται έναν δείκτη nil όταν το μέγεθος είναι μηδέν, και η φόρτωση καταλήγει στο συνηθισμένο σφάλμα «Cannot load PDF document» αντί για παραβίαση πρόσβασης. Ένας καλών που υπολόγισε ένα παράθυρο μηδέν byte από ένα κακοσχηματισμένο container παίρνει ένα καθαρό, πιάσιμο EPdfError όπως κάθε άλλη κακή είσοδο

Δανεισμένο ή αντιγραμμένο: τι αποφασίζει το Buffered

Το Buffered επιλέγει το συμβόλαιο κυριότητας, και είναι η μόνη παράμετρος εδώ με συνέπειες πέρα από την κλήση. Με Buffered = True, το PDFium Component αντιγράφει το επιλεγμένο παράθυρο, και μόνο το παράθυρο, στο εσωτερικό του buffer πριν τη φόρτωση. Το container 40 MB δεν αντιγράφεται· το PDF 312 KB αντιγράφεται. Μόλις επιστρέψει η LoadDocument μπορείτε να απελευθερώσετε, επαναχρησιμοποιήσετε ή αντικαταστήσετε το container αμέσως, επειδή το component δεν το αναφέρει πια. Αυτή είναι η προεπιλογή και η σωστή επιλογή για σχεδόν κάθε κώδικα

Το Buffered = False περνά το @Data[Index] απευθείας στην FPDF_LoadMemDocument64, και το PDFium κρατά αυτόν τον δείκτη για τη ζωή του εγγράφου αντί να αντιγράφει τα bytes. Αυτό κάνει τη φόρτωση χωρίς δεσμεύσεις, και κάνει ολόκληρο το υποστηρικτικό TBytes έναν δανεισμένο πόρο. Πρέπει να παραμείνει ζωντανό και μη τροποποιημένο μέχρι να τρέξει η UnloadDocument ή το Active να γίνει False. Όχι το παράθυρο, ολόκληρος ο πίνακας: ένας δυναμικός πίνακας μετρά αναφορές ως μονάδα, και η απελευθέρωση της τελευταίας αναφοράς οπουδήποτε στον κώδικά σας ελευθερώνει τη μνήμη που εξακολουθεί να διαβάζει το PDFium. Ο ορισμός του Length πάνω του είναι εξίσου μοιραίος, επειδή μια επαναδέσμευση μπορεί να μετακινήσει το μπλοκ. Δηλώστε αυτό στη δική σας τεκμηρίωση API όπου κι αν εκθέτετε μια τέτοια φόρτωση, στο ίδιο πνεύμα με κάθε άλλο όριο δανεισμού-έναντι-κυριότητας σε κώδικα Pascal· η κατάσταση αποτυχίας είναι πανομοιότυπη με τους κινδύνους aliasing που περιγράφονται στη διαρροή FillChar και result string σε Delphi, όπου ένα buffer φαίνεται κατεχόμενο και δεν είναι

type
  TFrameSession = class
  private
    FFrame: TBytes;   // owns the backing storage for as long as FPdf is loaded
    FPdf: TPdf;
  public
    procedure OpenEmbedded(Offset, Size: Integer);
    destructor Destroy; override;
  end;

procedure TFrameSession.OpenEmbedded(Offset, Size: Integer);
begin
  // Buffered = False: FFrame must outlive the loaded document
  FPdf.LoadDocument(FFrame, Offset, Size, False);
end;

destructor TFrameSession.Destroy;
begin
  FPdf.UnloadDocument;   // release the borrow first
  FFrame := nil;         // only now may the storage go
  inherited;
end;

Πότε το παράθυρο εύρους byte είναι το λάθος εργαλείο

Να είστε ειλικρινείς για το όριο. Η υπερφόρτωση εύρους byte υποθέτει ότι το container είναι ήδη πλήρως στη μνήμη, και το Count είναι Integer, οπότε ένα μεμονωμένο παράθυρο δεν μπορεί να υπερβεί τα δύο gigabyte. Αν το container είναι ένα αρχείο 6 GB στον δίσκο, ή φτάνει μέσω socket που δεν μπορείτε να επαναφέρετε, αυτή η υπερφόρτωση δεν μπορεί να σας βοηθήσει και η ανάγνωση ολόκληρου σε TBytes απλώς για να απευθυνθείτε σε ένα παράθυρο μέσα του ματαιώνει το νόημα. Εκεί ακριβώς ανήκει η διαδρομή FPDF_FILEACCESS, και το άρθρο streaming κατά παραγγελία δείχνει πώς να εκθέσετε μια προβολή μετατοπισμένη κατά offset ενός αρχείου ως προσαρμοσμένη πηγή εγγράφου. Εξίσου, αν τα ενσωματωμένα bytes χρειάζονται μετασχηματισμό πριν τα δει το PDFium, αποσυμπίεση, αποκρυπτογράφηση, ένα βήμα αποτύλιξης, τότε μια πραγματική αντιγραφή είναι αναπόφευκτη και το Buffered = True στον μετασχηματισμένο πίνακα είναι η ειλικρινής απάντηση. Το παράθυρο εύρους byte αποδίδει σε ακριβώς ένα σχήμα: συνεχόμενα, μη τροποποιημένα bytes PDF, ήδη κατοικημένα, σε γνωστή θέση

Αν το αξιολογείτε για έναν viewer, ένα panel προεπισκόπησης ή μια pipeline παραλαβής παρτίδων, η υπερφόρτωση εύρους byte και ο streaming loader είναι δύο από τις στρατηγικές φόρτωσης που διαθέτει το PDFium Component μαζί με φορτώσεις αρχείου, stream και ακατέργαστου δείκτη. Η πλήρης επιφάνεια API, η αδειοδότηση και η υποστήριξη εκδόσεων Delphi και C++Builder τεκμηριώνονται στη σελίδα προϊόντος PDFium Component