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

Σταδιακές ενημερώσεις PDF στο Delphi: Οδηγός AppendToStream

Οι σταδιακές ενημερώσεις PDF επιτρέπουν σε μια εφαρμογή Delphi να τροποποιεί ένα έγγραφο προσαρτώντας μόνο τα τροποποιημένα αντικείμενα, αφήνοντας κάθε αρχικό byte ανέπαφο. Η losLab PDF Library το υλοποιεί αυτό μέσω της AppendToStream, η οποία γράφει μόνο το σταδιακό τμήμα που ορίζεται από το ISO 32000-1 §7.5.6, έτσι ώστε η επεξεργασία ενός μόνο σελιδοδείκτη σε ένα αρχείο 2 GB να κοστίζει μερικά kilobytes εξόδου αντί για μια πλήρη επανεγγραφή. Ο ίδιος μηχανισμός είναι ο λόγος για τον οποίο τα υπογεγραμμένα έγγραφα μπορούν να ενημερωθούν χωρίς να ακυρωθούν οι υπογραφές τους

Το πρότυπο που λύνει αυτό είναι συγκεκριμένο. Μια πλήρης αποθήκευση ξαναγράφει ολόκληρο το αρχείο: κάθε αντικείμενο σειριοποιείται εκ νέου, κάθε μετατόπιση παραπομπής (cross-reference offset) υπολογίζεται ξανά, και το αρχείο εξόδου δεν έχει καμία σχέση σε επίπεδο byte με το αρχείο εισόδου. Για ένα τιμολόγιο 40 KB αυτό είναι μια χαρά. Για ένα σαρωμένο αρχείο 2 GB όπου διορθώσατε μόνο ένα τυπογραφικό σφάλμα στον τίτλο του εγγράφου, η επανεγγραφή δύο gigabytes για την αλλαγή είκοσι bytes είναι παράλογη — και αν το αρχείο έφερε ψηφιακή υπογραφή, η επανεγγραφή μόλις την κατέστρεψε

Γιατί η αποθήκευση ενός PDF καταστρέφει την ψηφιακή του υπογραφή;

Μια ψηφιακή υπογραφή PDF δεν υπογράφει το λογικό περιεχομένου του εγγράφου· υπογράφει εύρη byte (byte ranges) του φυσικού αρχείου. Η καταχώριση /ByteRange στο λεξικό υπογραφής καταγράφει ακριβώς ποια τμήματα του αρχείου καλύπτει η κρυπτογραφική σύνοψη. Οποιαδήποτε λειτουργία αποθήκευσης που σειριοποιεί εκ νέου αυτά τα bytes — ακόμη και μία που παράγει ένα σημασιολογικά πανομοιότυπο έγγραφο — αλλάζει τη σύνοψη, και κάθε επαληθευτής θα αναφέρει την υπογραφή ως κατεστραμμένη. Αυτό γίνεται από σχεδιασμό: η υπογραφή πιστοποιεί τα bytes που είδε ο υπογράφων, όχι κάποιο αφηρημένο μοντέλο εγγράφου

Οι σταδιακές ενημερώσεις είναι η δικλείδα ασφαλείας που παρέχει η προδιαγραφή PDF. Επειδή μια σταδιακή αποθήκευση προσαρτά νέα δεδομένα μετά το αρχικό %%EOF και δεν αγγίζει ποτέ τα υπογεγραμμένα εύρη byte, η υπάρχουσα υπογραφή συνεχίζει να επαληθεύεται με βάση τα bytes που καλύπτει. Στη συνέχεια, οι επαληθευτές ταξινομούν τις προσαρτημένες αλλαγές ξεχωριστά — μια δεύτερη υπογραφή, μια συμπλήρωση φόρμας, ένα σχόλιο — και αποφασίζουν εάν πρόκειται για επιτρεπόμενες τροποποιήσεις. Κάθε ροή εργασίας πολλαπλών υπογραφών βασίζεται σε αυτό: κάθε υπογράφων προσθέτει ένα σταδιακό τμήμα πάνω στο προηγούμενο. Εάν κατασκευάζετε ροές εργασίας υπογραφής, το συνοδευτικό άρθρο σχετικά με τις υπογραφές PAdES και την επαλήθευσή τους στο Delphi καλύπτει λεπτομερώς πώς αλληλεπιδρούν τα εύρη byte υπογραφής και τα σταδιακά τμήματα

Πώς λειτουργούν οι σταδιακές ενημερώσεις σύμφωνα με το ISO 32000-1 §7.5.6

Το ISO 32000-1 §7.5.6 ορίζει το μοντέλο σε τρεις κανόνες. Πρώτον, το αρχικό περιεχόμενο του αρχείου παραμένει εντελώς ανέπαφο — κανένα byte δεν μετακινείται. Δεύτερον, τα τροποποιημένα και τα πρόσφατα δημιουργημένα αντικείμενα προσαρτώνται μετά το τελευταίο %%EOF, το καθένα με τον ίδιο αριθμό αντικειμένου που είχε πριν (τα τροποποιημένα αντικείμενα απλώς λαμβάνουν έναν νεότερο ορισμό που επισκιάζει τον παλιό). Τρίτον, προσαρτάται μια νέα ενότητα παραπομπών (cross-reference section) και ένα trailer· η καταχώριση /Prev του trailer δείχνει πίσω στη μετατόπιση byte της προηγούμενης ενότητας παραπομπών, σχηματίζοντας μια αλυσίδα την οποία ο αναγνώστης διατρέχει από τη νεότερη προς την παλαιότερη για να επιλύσει κάθε αντικείμενο στον πιο πρόσφατο ορισμό του

Δύο χρήσιμες ιδιότητες προκύπτουν από αυτή τη δομή. Οι ενημερώσεις είναι οικονομικές σε σχέση με το τι άλλαξε, όχι με το μέγεθος του εγγράφου — το κόστος προσάρτησης είναι το μέγεθος των τροποποιημένων αντικειμένων συν μια μικρή επιβάρυνση xref/trailer. Και το αρχείο γίνεται το ιστορικό εκδόσεων του εαυτού του: κάθε προηγούμενη αναθεώρηση εξακολουθεί να είναι φυσικά παρούσα, επομένως ένας ελεγκτής μπορεί να περικόψει το αρχείο σε οποιοδήποτε προηγούμενο %%EOF και να ανακτήσει ακριβώς το έγγραφο που υπήρχε σε εκείνο το σημείο. Για ροές εργασίας συμμόρφωσης που πρέπει να αποδεικνύουν πώς έδειχνε ένα έγγραφο πριν από κάθε τροποποίηση, αυτή η ενσωματωμένη διαδρομή ελέγχου είναι συχνά το καθοριστικό επιχείρημα για σταδιακές αποθηκεύσεις

Εγγραφή μιας σταδιακής ενημέρωσης με το AppendToStream

Η losLab PDF Library εκθέτει τη σταδιακή έξοδο μέσω της AppendToStream(AppendMode: Integer; OutStream: TStream): Integer, η οποία επιστρέφει 1 σε περίπτωση επιτυχίας και 0 σε περίπτωση αποτυχίας. Η παράμετρος AppendMode επιλέγει τι θα καταλήξει στη ροή προορισμού. Η λειτουργία 0 (Mode 0) γράφει ένα πλήρες αρχείο: τα αρχικά bytes προέλευσης αντιγράφονται πρώτα στη ροή, και στη συνέχεια προσαρτάται το σταδιακό τμήμα. Η λειτουργία 1 (Mode 1) γράφει μόνο το ίδιο το σταδιακό τμήμα — το delta — και παρακάμπτει εντελώς τα bytes προέλευσης. Η λειτουργία 2 (Mode 2) γράφει πρώτα ένα πρόθεμα που παρέχεται από τον καλούντα και καταχωρίζεται μέσω της SetAppendInputFromString, και στη συνέχεια προσαρτά το τμήμα ενημέρωσης πάνω σε αυτό

var
  Doc: TPDFlib;
  Delta: TMemoryStream;
begin
  Doc := TPDFlib.Create;
  try
    if Doc.LoadFromFile('contract.pdf', '') <= 0 then
      Exit;

    // Small edit: the kind of change that should not
    // trigger a rewrite of the whole file
    Doc.SetInformation(3, 'Amended 2026-07-04');  // key 3 = /Subject

    Delta := TMemoryStream.Create;
    try
      // AppendMode = 1: write only the incremental section.
      // Original bytes + Delta = a complete, valid PDF.
      if Doc.AppendToStream(1, Delta) = 1 then
        Delta.SaveToFile('contract.delta.bin');
    finally
      Delta.Free;
    end;
  finally
    Doc.Free;
  end;
end;

Η λειτουργία 1 (Mode 1) είναι η ενδιαφέρουσα για τον σχεδιασμό συστημάτων. Επειδή το delta είναι αυτόνομο, μπορείτε να το στείλετε ανεξάρτητα από το αρχικό: να αποθηκεύσετε αναθεωρήσεις ως ξεχωριστά blobs σε χώρο αποθήκευσης αντικειμένων, να αναπαράγετε μόνο deltas σε μια απομακρυσμένη τοποθεσία ή να ανακατασκευάσετε οποιαδήποτε αναθεώρηση συνενώνοντας το βασικό αρχείο με την αλυσίδα των ενημερώσεών του. Ο κανόνας ανακατασκευής είναι η απλή συνένωση byte — πρώτα το αρχικό αρχείο, και στη συνέχεια κάθε delta με τη σειρά — επειδή αυτή είναι ακριβώς η διάταξη που προδιαγράφει η §7.5.6 για ένα σταδιακά ενημερωμένο αρχείο

Πώς υπολογίζει η βιβλιοθήκη τις μετατοπίσεις xref χωρίς να αντιγράφει το αρχικό αρχείο;

Οι καταχωρίσεις παραπομπών (cross-reference entries) μέσα σε ένα σταδιακό τμήμα πρέπει να περιέχουν απόλυτες μετατοπίσεις byte — θέσεις που μετρώνται από την αρχή του πλήρους αρχείου, όχι από την αρχή του delta. Αυτό δημιουργεί ένα πρόβλημα για τη λειτουργία 1: ο εγγραφέας δεν εκπέμπει ποτέ τα αρχικά bytes, ωστόσο κάθε μετατόπιση που καταγράφει πρέπει να προσποιείται ότι είναι εκεί. Η losLab PDF Library το επιλύει αυτό με έναν εσωτερικό προσαρμογέα ροής, τον TPDFAppendSectionStream, ο οποίος παρουσιάζει έναν εικονικό χώρο συντεταγμένων στον σειριοποιητή (serializer). Ο προσαρμογέας δημιουργείται με το μήκος byte του αρχικού αρχείου ως βασική μετατόπιση, αναφέρει τη θέση και το μέγεθός του ως αυτή τη βάση συν ό,τι έχει προσαρτηθεί μέχρι τώρα, και προωθεί μόνο τα πρόσφατα γραμμένα bytes στη ροή προορισμού του καλούντος

Το αποτέλεσμα είναι ότι η λειτουργία 1 δεν δημιουργεί ποτέ ένα αντίγραφο του εγγράφου προέλευσης — ούτε στο δίσκο, ούτε στη μνήμη. Η απλοϊκή υλοποίηση (εγγραφή ολόκληρου του αρχείου σε έναν προσωρινό buffer και στη συνέχεια αποκοπή της ουράς) θα μετέφερε ένα παροδικό αντίγραφο ολόκληρου του αρχικού PDF, το οποίο για εισόδους μεγέθους gigabyte είναι ακριβώς το κόστος που οι σταδιακές ενημερώσεις προσπαθούν να αποφύγουν. Αυτή η τεχνική εικονικοποίησης μετατόπισης είναι στενός συγγενής της μετατόπισης αναφοράς byte (byte-reference shifting) που χρησιμοποιείται αλλού στη βιβλιοθήκη· το άρθρο σχετικά με τη γρήγορη συγχώνευση PDF με μετατόπιση αναφοράς byte δείχνει την ίδια ιδέα να εφαρμόζεται στον συνδυασμό εγγράφων, και ο οδηγός για τη συγχώνευση και τον διαχωρισμό μεγάλων PDF με άμεση πρόσβαση αρχείων καλύπτει τη γύρω αρχιτεκτονική I/O για αρχεία που δεν χωρούν άνετα στη μνήμη RAM

Ροή πλήρους αποθήκευσης με το SaveToStream

Η σταδιακή έξοδος είναι το μισό της ιστορίας της ροής· το άλλο μισό είναι αυτό που συμβαίνει κατά την πλήρη αποθήκευση. Η SaveToStream στη losLab PDF Librarypercentage οδηγεί τον σειριοποιητή εγγράφων απευθείας στη ροή προορισμού, αντί να αποδίδει πρώτα ολόκληρο το έγγραφο σε μια ενδιάμεση AnsiString και στη συνέχεια να γράφει αυτόν τον buffer με μία κλήση. Η παλαιότερη προσέγγιση λειτουργούσε, αλλά σήμαινε ότι κάθε πλήρης αποθήκευση κρατούσε παροδικά ένα δεύτερο πλήρες αντίγραφο της εξόδου στη μνήμη — αβλαβές στα 10 MB, επίπονο στα 500 MB, και ένας σκληρός τοίχος για εξόδους πολλών gigabytes σε διεργασίες 32-bit. Η απευθείας σειριοποίηση κάνει τη μέγιστη μνήμη να ακολουθεί τις δομές αντικειμένων του εγγράφου αντί για το σειριοποιημένο μήκος του

var
  Doc: TPDFlib;
  Output: TFileStream;
begin
  Doc := TPDFlib.Create;
  try
    if Doc.LoadFromFile('archive.pdf', '') <= 0 then
      Exit;

    // ... edits that justify a full rewrite ...

    Output := TFileStream.Create('archive-rewritten.pdf', fmCreate);
    try
      if Doc.SaveToStream(Output) = 0 then
        Writeln('Save failed, error ', Doc.LastErrorCode);
    finally
      Output.Free;
    end;
  finally
    Doc.Free;
  end;
end;

Ένα μάθημα για τη λειτουργία κοινής χρήσης (share-mode): όταν το AppendToFile επέστρεφε 0

Μια παλινδρόμηση σε αυτόν τον τομέα αξίζει να αναφερθεί επειδή το μοτίβο αποτυχίας γενικεύεται. Η AppendToFile(FileName) προσαρτά μια σταδιακή ενημέρωση απευθείας σε ένα υπάρχον PDF στο δίσκο — η φυσική κλήση για μια ροή εργασίας επιτόπιου ελέγχου (in-place audit-trail): φορτώστε ένα αρχείο, κάντε μια αλλαγή, προσαρτήστε στην ίδια διαδρομή. Στην έκδοση v3.71.2 αυτή η ακριβής ακολουθία άρχισε να επιστρέφει 0. Η κύρια αιτία βρισκόταν στον φορτωτή (loader), όχι στον εγγραφέα: για την υποστήριξη της κατ' απαίτηση ανάγνωσης μεγάλων εγγράφων, η LoadFromFile διατηρεί τη λαβή του αρχείου προέλευσης ανοιχτή για τη διάρκεια ζωής του αντικειμένου του εγγράφου, και αυτή η λαβή ανοίχτηκε με fmShareDenyWrite. Όταν η AppendToFile προσπάθησε στη συνέχεια να ξανανοίξει το ίδιο αρχείο για εγγραφή, η ίδια η λειτουργία κοινής χρήσης του φορτωτή την απέρριψε, και το API απέτυχε πριν γράψει ούτε ένα byte

Η διόρθωση χαλάρωσε τη λειτουργία κοινής χρήσης του φορτωτή σε fmShareDenyNone, το οποίο είναι ασφαλές ακριβώς λόγω της φύσης της σταδιακής προσάρτησης: προσθέτει bytes αυστηρά μετά το τέλος του αρχείου και δεν ξαναγράφει ποτέ την περιοχή που εξυπηρετεί η μακροχρόνια λαβή του αναγνώστη. Το γενικό μάθημα για οποιονδήποτε δημιουργεί περιτυλίγματα γύρω από αυτή τη βιβλιοθήκη — ή κατασκευάζει παρόμοιους φορτωτές ροής — είναι ότι οι τεμπέληδες (lazy) αναγνώστες που κρατούν λαβές και οι εγγραφείς στο ίδιο αρχείο βρίσκονται σε ένταση, και η λειτουργία κοινής χρήσης που επιλέγετε κατά το άνοιγμα είναι ένα συμβόλαιο API, όχι μια λεπτομέρεια υλοποίησης. Εάν το AppendToFile επιστρέψει ποτέ 0 στον κώδικά σας, ελέγξτε πρώτα αν κάτι άλλο στη διεργασία σας εξακολουθεί να κρατά το αρχείο προορισμού με μια περιοριστική λειτουργία κοινής χρήσης

Το πραγματικό κόστος: πότε οι σταδιακές ενημερώσεις είναι το λάθος εργαλείο

Οι σταδιακές ενημερώσεις ανταλλάσσουν το μέγεθος του αρχείου με την αποτελεσματικότητα εγγραφής, και η ανταλλαγή αυτή δεν είναι πάντα ευνοϊκή. Κάθε αναθεώρηση προσαρτά τα τροποποιημένα αντικείμενά της, ενώ οι αντικατασταθέντες ορισμοί παραμένουν στο αρχείο, έτσι ένα έγγραφο που έχει υποστεί επεξεργασία εκατοντάδες φορές συσσωρεύει νεκρά αντικείμενα και μια μακριά αλυσίδα /Prev που κάθε αναγνώστης πρέπει να διατρέξει. Ακόμα χειρότερα, το "διαγραμμένο" περιεχόμενο δεν έχει χαθεί: το κείμενο που αφαιρέθηκε στην αναθεώρηση πέντε εξακολουθεί να είναι φυσικά παρόν στα bytes της αναθεώρησης τέσσερα, ανακτήσιμο από οποιονδήποτε περικόψει το αρχείο. Η απόκρυψη (redaction), η εξυγίανση ή οποιαδήποτε αφαίρεση ευαίσθητου περιεχομένου απαιτεί επομένως μια πλήρη επανεγγραφή — η σταδιακή αποθήκευση μιας απόκρυψης είναι μια διαρροή δεδομένων με επιπλέον βήματα

Μια πλήρης αποθήκευση είναι επίσης η σωστή επιλογή όταν ο στόχος είναι η συμπίεση (συμπίεση των συσσωρευμένων σταδιακών ενημερώσεων και των αχρησιμοποίητων αντικειμένων), όταν αλλάζουν ιδιότητες σε όλο το έγγραφο, όπως η κρυπτογράφηση — η εκ νέου κρυπτογράφηση αγγίζει κάθε συμβολοσειρά και ροή, οπότε δεν μένει τίποτα "σταδιακό" σχετικά με την αλλαγή — ή όταν παράγεται ένα καθαρό παραδοτέο όπου το ιστορικό επεξεργασίας δεν πρέπει να ταξιδεύει μαζί με το αρχείο. Ένας λογικός κανόνας: χρησιμοποιήστε το AppendToStream ή το AppendToFile όσο ένα έγγραφο είναι ενεργό και αλλάζει, ειδικά όταν φέρει υπογραφές· χρησιμοποιήστε μια πλήρη επανεγγραφή με SaveToStream στα όρια του κύκλου ζωής, όταν το έγγραφο φεύγει από το σύστημά σας ή το ιστορικό του πρέπει να επιπεδοποιηθεί

Οι σταδιακές ενημερώσεις, η έξοδος delta εικονικής μετατόπισης (virtual-offset) και η απευθείας σειριοποίηση σε ροή (direct-to-stream serialization) αποτελούν μέρος της τυπικής losLab PDF Library για Delphi, C# και VB.NET· η σελίδα του προϊόντος παραθέτει την πλήρη επιφάνεια API αποθήκευσης και προσάρτησης, μαζί με τα χαρακτηριστικά υπογραφής και μεγάλων αρχείων που συζητήθηκαν παραπάνω