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

Φόρτωση Εύρους 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 αφαιρεί την πρώτη αντιγραφή άνευ όρων και τη δεύτερη προαιρετικά

Διάγραμμα σύγκρισης που δείχνει την παλιά διαδρομή διπλής αντιγραφής για ενσωματωμένο PDF μέσα σε δοχείο σε Delphi έναντι του overload LoadDocument byte-range του PDFium Component που απευθύνει το παράθυρο επί τόπου
Χωρίς το overload το ίδιο παράθυρο 312 KB αντιγραφόταν memcpy δύο φορές· η κλήση byte-range το προσφωνεί επιτόπου και το Buffered διαλέγει αν τίποτα αντιγράφεται καθόλου

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

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

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

var
  Frame: TBytes;          // ολόκληρη η εγγραφή του περιέκτη, δεκάδες megabytes
  Offset, Size: Integer;
begin
  Frame := LoadContainerRecord('mailbox.dat');
  LocateEmbeddedPdf(Frame, Offset, Size);   // ο αναλυτής του δικού σας περιέκτη

  // Όχι Copy(Frame, Offset, Size) εδώ - το παράθυρο διευθυνοποιείται επί τόπου
  Pdf.LoadDocument(Frame, Offset, Size, True);
  try
    RenderPreview(Pdf);
  finally
    Pdf.UnloadDocument;
  end;
end;

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

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

// ΛΑΘΟΣ: το Index + Count αξιολογείται σε Integer και μπορεί να υποστεί περιτύλιξη σε αρνητικό
if Index + Count <= Length(Data) then
  DataPtr := @Data[Index];

// ΣΩΣΤΟ: απορρίψτε πρώτα τα πρόσημα, μετά φράξτε κάθε όρο ξεχωριστά,
// με τη μόνη αριθμητική να γίνεται ως αφαίρεση που δεν μπορεί να περιτυλιχθεί
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

Διάγραμμα ροής που αντιπαραθέτει τον έλεγχο ορίων Index συν Count που υπερχειλίζει με τη σειρά επικύρωσης μόνο-αφαιρέσεων που κρατά τη φόρτωση byte-range PDFium ασφαλή στη μνήμη σε Delphi
Η πρόσθεση πρώτα αναδιπλώνει πέρα από MaxInt και νικά τον φρουρό· η απόρριψη προσήμων και ο ορισμός ορίων όρων πριν από μία α-αναδίπλωτη αφαίρεση κρατούν κάθε ενδιάμεσο αναπαραστάσιμο

Γιατί πρέπει ένα παράθυρο μηδενικού μήκους να περνά 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;   // κατέχει τον αποθηκευτικό χώρο όσο το FPdf είναι φορτωμένο
    FPdf: TPdf;
  public
    procedure OpenEmbedded(Offset, Size: Integer);
    destructor Destroy; override;
  end;

procedure TFrameSession.OpenEmbedded(Offset, Size: Integer);
begin
  // Buffered = False: το FFrame πρέπει να επιβιώσει μετά το φορτωμένο έγγραφο
  FPdf.LoadDocument(FFrame, Offset, Size, False);
end;

destructor TFrameSession.Destroy;
begin
  FPdf.UnloadDocument;   // απελευθερώστε πρώτα τον δανεισμό
  FFrame := nil;         // μόνο τώρα μπορεί να φύγει ο αποθηκευτικός χώρος
  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

Διάγραμμα χρονογραμμής των δύο λειτουργιών Buffered κατά τη φόρτωση ενσωματωμένου PDF από byte range σε Delphi: αντιγραφή μόνο του παραθύρου PDF και άμεση απελευθέρωση του δοχείου έναντι δανεισμού ολόκληρων των backing TBytes μέχρι να τρέξει το UnloadDocument
Με Buffered = True αντιγράφεται μόνο το παράθυρο ώστε ο περιέκτης να είναι μίας χρήσης, ενώ με Buffered = False ολόκληρος ο υποκείμενος πίνακας μένει δανεικός μέχρι την εκφόρτωση