Το PDF 1.5 εισήγαγε δύο δομές αποθήκευσης που η προηγούμενη μορφή αρχείου δεν είχε τρόπο να εκφράσει: το object stream και το cross-reference stream. Ένα object stream είναι ένας μοναδικός συμπιεσμένος με Flate περιέκτης, με ετικέτα /Type /ObjStm, που κρατά πολλά μικρά έμμεσα αντικείμενα πακεταρισμένα το ένα δίπλα στο άλλο αντί να τα σκορπίζει μέσα στο σώμα του αρχείου. Ένα cross-reference stream είναι ο πίνακας αναζήτησης του αρχείου ξαναγραμμένος ως συμπιεσμένο δυαδικό με πεδία μεταβλητού πλάτους, στη θέση του πίνακα ASCII σταθερού πλάτους που έκλεινε κάθε PDF μέχρι την έκδοση 1.4. Ταξιδεύουν μαζί. Μόλις τα αντικείμενα διπλωθούν μέσα σε ένα stream, ο παλιός πίνακας κειμένου δεν μπορεί πια να τα διευθυνσιοδοτήσει, οπότε το δυαδικό xref πρέπει να έρθει μαζί του
Συγκρίνετε αυτό με την κλασική διάταξη και το κόστος που αφαιρεί είναι εύκολο να το δείτε. Σε ένα αρχείο PDF 1.4 κάθε έμμεσο αντικείμενο κάθεται ασυμπίεστο πίσω από τη δική του κεφαλίδα obj, και ο πίνακας στο τέλος ξοδεύει ακριβώς 20 bytes ASCII ανά καταχώριση, χωρίς να επιτρέπεται συμπίεση. Ένα έγγραφο με 200.000 αντικείμενα κουβαλά περίπου 4 MB δεδομένων cross-reference προτού σχεδιαστεί ούτε ένα glyph, με όλα τα ασυμπίεστα σώματα λεξικών στοιβαγμένα από πάνω. Το PDF 1.5 επιτίθεται και στους δύο αριθμούς ταυτόχρονα: τα λεξικά διπλώνονται σε περιέκτες Flate, και ο πίνακας των 4 MB συρρικνώνεται σε μερικές εκατοντάδες kilobyte δυαδικού. Το ISO 32000-1 ορίζει τις δύο δομές στα §7.5.7 και §7.5.8
Πού πράγματι πέφτει η εξοικονόμηση
Τα object streams αγγίζουν μόνο αντικείμενα που δεν είναι streams, οπότε συμπιέζουν δομή, όχι pixel. Το περιεχόμενο σελίδας ήταν ήδη συμπιεσμένο με Flate πριν το 1.5, και τα δεδομένα εικόνας φέρουν τους δικούς τους codec, γι' αυτό μια εικονοβαρής διαφημιστική φυλλάδα σχεδόν δεν κουνιέται. Τα αρχεία που καταρρέουν είναι αυτά με βαριά δομή: AcroForms με χιλιάδες λεξικά πεδίων, βαθιά δέντρα περιγραμμάτων (outline), στοιχεία δομής tagged-PDF. Αυτά τα αντικείμενα είναι μικροσκοπικά, πολυάριθμα, και σχεδόν πανομοιότυπα μεταξύ τους, και αυτή η επανάληψη είναι ακριβώς αυτό που εκμεταλλεύεται το Flate μόλις καθίσουν σε ένα ενιαίο buffer αντί να είναι απλωμένα μέσα στο σώμα με κεφαλίδες σφηνωμένες ανάμεσά τους
Είναι εύκολο να υποτιμήσετε πόσο από ένα παλιό αρχείο είναι overhead. Ένα αρχείο φορμών που έχει απορροφήσει χρόνια επεξεργασιών μπορεί να ξοδεύει πάνω από τα μισά του bytes σε κεφαλίδες λεξικών, γέμισμα xref, και αναθεωρήσεις που κανένας αναγνώστης δεν θα κοιτάξει ποτέ. Τα δύο χαρακτηριστικά εδώ ανακτούν τα δύο πρώτα από αυτά. Το τρίτο, οι συσσωρευμένες αναθεωρήσεις, υποχωρεί μόνο στη συμπίεση (compaction), από τη στιγμή που το αρχείο δεν χρειάζεται πια να θυμάται το δικό του ιστορικό
Στο HotPDF ενεργοποιείτε και τα δύο μέσω ενός ζεύγους ιδιοτήτων, και το πώς εξαρτώνται η μία από την άλλη έχει μεγαλύτερη σημασία από τη σειρά με την οποία τις γράφετε:
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'catalog-2026.pdf';
Pdf.UseXRefStream := True; // δυαδικό xref, προαπαιτούμενο για το ObjStm
Pdf.UseObjectStreams := True; // πακετάρισμα αντικειμένων μέσα σε /Type /ObjStm
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Compressed structure demo');
Pdf.EndDoc; // εκπέμπει περιέκτες XRefStm + ObjStm
finally
Pdf.Free;
end;
end;
Το UseObjectStreams χρειάζεται το UseXRefStream ορισμένο σε True. Ένα συμπιεσμένο αντικείμενο προσεγγίζεται μέσω μιας καταχώρισης xref τύπου 2, η οποία καταγράφει έναν αριθμό object-stream συν έναν δείκτη, και μια κλασική γραμμή κειμένου 20 bytes δεν έχει χώρο να αποθηκεύσει αυτό το ζεύγος. Οπότε το UseObjectStreams από μόνο του δεν κάνει τίποτα ορατό· και οι δύο σημαίες, ορισμένες πριν το BeginDoc, είναι η διαμόρφωση που λειτουργεί. Ορίστε τις μετά το BeginDoc και το HotPDF έχει ήδη δεσμευτεί στην παλαιότερη διάταξη
Γιατί και τα δύο είναι απενεργοποιημένα εξ ορισμού
Το HotPDF αφήνει και τις δύο ιδιότητες False εξ αρχής, και ο λόγος εμφανίζεται σε ενσωματώσεις με παλιό κώδικα παρακάτω στη ροή. Ένας αναγνώστης που κατανοεί μόνο το PDF 1.4 δεν ανακοινώνει ότι δεν μπορεί να χειριστεί συμπιεσμένα αντικείμενα. Συναντά ένα xref stream, δεν βρίσκει καμία από τις λέξεις-κλειδιά trailer που περιμένει, και αναφέρει έναν κατεστραμμένο πίνακα cross-reference ή απλά αρνείται να ανοίξει το αρχείο. Αν η έξοδός σας ρέει προς μια γερασμένη πύλη fax, έναν εκτυπωτή υλικού που τρέχει έναν ενσωματωμένο interpreter, ή έναν parser που κάποιος έγραψε σύμφωνα με το spec 1.4 πριν μια δεκαετία, κρατήστε και τις δύο σημαίες απενεργοποιημένες για εκείνο το κανάλι και ζήστε με το μεγαλύτερο αρχείο. Για αρχειακή αποθήκευση και διανομή στον ιστό, όπου κάθε κύριο πρόγραμμα προβολής διαβάζει PDF 1.5 εδώ και είκοσι χρόνια, το να τις ενεργοποιήσετε είναι συμπίεση που παίρνετε σχεδόν δωρεάν
Υπάρχει ένα δευτερεύον φαινόμενο που αξίζει να πείτε στην ομάδα υποστήριξής σας. Μόλις τα λεξικά πακεταριστούν σε object streams, η σύγκριση δύο παραγόμενων αρχείων byte προς byte παύει να σημαίνει οτιδήποτε, επειδή η αλλαγή ενός μόνο πεδίου μπορεί να ξανασυμπιέσει με Flate ολόκληρο τον περιέκτη και να ανακατέψει όλα όσα ακολουθούν. Συγκρίνετε τέτοια αρχεία κατά περιεχόμενο αντικειμένου, όχι με δυαδική σύγκριση
Αυξητικές ενημερώσεις και τα byte offsets που προστατεύουν
Μια ψηφιακή υπογραφή καλύπτει ένα ρητό /ByteRange: δύο τμήματα του φυσικού αρχείου, δοσμένα ως απόλυτα byte offsets, πάνω στα οποία λήφθηκε το digest CMS. Ξαναγράψτε το αρχείο, ακόμη και σε κάτι που φαίνεται πανομοιότυπο στην οθόνη, και αυτά τα offsets μετακινούνται όλα. Το digest σταματά να ταιριάζει και η υπογραφή διαβάζεται ως κατεστραμμένη. Αυτό είναι το ακριβές πρόβλημα που λύνει το ISO 32000-1 §7.5.6 με τις αυξητικές ενημερώσεις. Νέα και τροποποιημένα αντικείμενα προσαρτώνται μετά το υπάρχον %%EOF, έπειτα γράφεται μια νέα ενότητα cross-reference της οποίας η καταχώριση /Prev δείχνει πίσω σε αυτήν πριν από αυτήν. Τα αρχικά bytes δεν διαταράσσονται ποτέ, οπότε μια υπογεγραμμένη αναθεώρηση παραμένει επαληθεύσιμη και το Acrobat μπορεί να παρουσιάσει κάθε υπογεγραμμένη αναθεώρηση ξεχωριστά στον πίνακα υπογραφών
Το HotPDF το εκθέτει αυτό μέσω του δικού του σημείου εισόδου:
Pdf.BeginIncrementalUpdate('contract-signed.pdf');
Pdf.AddPage;
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Addendum recorded 2026-06-11');
Pdf.SaveIncrementalUpdate('contract-updated.pdf'); // προσαρτά μόνο το delta
Δύο πράγματα μπερδεύουν τον κόσμο. Το BeginIncrementalUpdate πρέπει να λάβει το αρχικό όνομα αρχείου, επειδή η προσαρτημένη ενότητα xref καταγράφει offsets που έχουν νόημα μόνο σε σχέση με αυτά ακριβώς τα αρχικά bytes· δείξτε το σε ένα μετονομασμένο ή ξανααποθηκευμένο αντίγραφο και τα offsets περιγράφουν ένα αρχείο που δεν υπάρχει πια. Και η αποθήκευση είναι μόνο-για-προσάρτηση εκ κατασκευής, οπότε η έξοδος είναι πάντα μεγαλύτερη από την είσοδο. Αυτή η ανάπτυξη δεν είναι σπατάλη προς εξάλειψη. Είναι η ίδια ιδιότητα που αφήνει ανέπαφες τις προηγούμενες υπογεγραμμένες αναθεωρήσεις
Η τροποποίηση ενός φορτωμένου αρχείου περνά από το LoadFromFile
Οι προγραμματιστές που πρωτογνώρισαν το HotPDF μέσω του API δημιουργίας τείνουν να χτυπήσουν σε έναν συγκεκριμένο τοίχο. Το BeginDoc ανοίγει ένα ολοκαίνουριο έγγραφο, που είναι το λάθος εργαλείο όταν σκοπεύετε να αλλάξετε ένα που ήδη υπάρχει. Η επεξεργασία ενός υπάρχοντος αρχείου περνά αντίθετα από τις κλήσεις φορτωμένου εγγράφου:
PageCount := Pdf.LoadFromFile('base.pdf');
Pdf.InsertPagesFromDocument(OtherDoc, '1-3', 5); // σελίδες 1-3 μετά τη σελίδα 5
Pdf.MovePage(2, 5);
Pdf.SaveLoadedDocument('modified.pdf');
Ανακατέψτε τα δύο και το σύμπτωμα είναι ένα αρχείο εξόδου που κρατά το νέο σας περιεχόμενο και τίποτα από το αρχικό, επειδή το BeginDoc έχτισε πρόθυμα ένα καινούργιο έγγραφο δίπλα σε αυτό που πιστεύατε ότι επεξεργαζόσασταν. Διαβάστε το LoadFromFile με το SaveLoadedDocument ως ένα λεξιλόγιο και το BeginDoc με το EndDoc ως άλλο. Μια ρουτίνα που καταφεύγει και στα δύο ενάντια στο ίδιο αρχείο είναι σχεδόν πάντα λάθος
Πότε να συμπτύξετε ένα προσαρτημένο αρχείο
Η αποθήκευση μόνο-για-προσάρτηση κουβαλά ένα αργό κόστος. Μια νυχτερινή εργασία που σφραγίζει μία γραμμή κατάστασης στο ίδιο PDF παράγει 365 αναθεωρήσεις μέσα σε έναν χρόνο, και κάθε αναθεώρηση σέρνει πίσω της μια νέα ενότητα xref. Όταν αυτό το ιστορικό έχει ξεπεράσει τη χρησιμότητά του, και καμία υπογραφή στο αρχείο δεν χρειάζεται να επιβιώσει, μπορείτε να ισοπεδώσετε ολόκληρο το πράγμα επαναδιαδοχοποιώντας το μέσω της διαδρομής φορτωμένου εγγράφου:
Pdf.LoadFromFile('stamped.pdf');
Pdf.SaveLoadedDocument('compacted.pdf');
Αυτή η επανααποθήκευση είναι πλήρης επανεγγραφή. Πετάει σκόπιμα τις προηγούμενες αναθεωρήσεις και σπάει οποιαδήποτε υπογραφή παραμένει στο αρχείο, οπότε τοποθετήστε την πίσω από την ίδια πύλη πολιτικής που εφαρμόζετε σε κάθε άλλο καταστροφικό βήμα. Ένας κανόνας παραγωγής που αντέχει: συμπτύξτε όταν ο αριθμός αναθεωρήσεων ξεπεράσει ένα κατώφλι, ή όταν το προσαρτημένο overhead μεγαλώσει πέρα από ένα ποσοστό του βασικού αρχείου, και ποτέ μην συμπτύσσετε ένα έγγραφο του οποίου ο πίνακας υπογραφών έχει οτιδήποτε μέσα του
Έλεγχος της εξόδου πριν κυκλοφορήσει
Η επαλήθευση αυτού του ζεύγους χαρακτηριστικών είναι αναζωογονητικά συγκεκριμένη. Ανοίξτε το αποτέλεσμα στο Adobe Acrobat και επιβεβαιώστε τρία σημεία: οι ιδιότητες εγγράφου αναφέρουν PDF 1.5 ή νεότερο μόλις ενεργοποιηθούν τα object streams· ο πίνακας υπογραφών εξακολουθεί να επικυρώνει κάθε προηγουμένως υπογεγραμμένη αναθεώρηση μετά από μια αυξητική ενημέρωση· και η καταμέτρηση σελίδων και οι σελιδοδείκτες πέρασαν αλώβητοι από έναν κύκλο φόρτωσης, τροποποίησης και αποθήκευσης. Για αρχειακή έξοδο, περάστε το αρχείο και μέσα από το veraPDF, καθώς ένα συμπιεσμένο xref είναι ακριβώς το είδος δομής που ένας αυστηρός επικυρωτής εξετάζει πιο προσεκτικά απ' ό,τι θα το κάνει ποτέ ένα επιεικές πρόγραμμα προβολής. Αν η δουλειά σας εμπλέκει επίσης πολύ μεγάλες εισόδους, οι μέθοδοι επιθεώρησης στο οδηγό μας για το Direct File API για ροές εργασίας μεγάλων PDF ταιριάζουν φυσικά με την αυξητική αποθήκευση, και οι μηχανισμοί υπογραφής πίσω από τα byte range παραπάνω αναλύονται σε βάθος στο άρθρο για τις ψηφιακές υπογραφές HotPDF και το PAdES
Και τα δύο χαρακτηριστικά κυκλοφορούν ως μέρος του HotPDF Delphi Component για Delphi και C++Builder, δίπλα στα API δημιουργίας, φορμών, κρυπτογράφησης και υπογραφής που καλύπτονται αλλού σε αυτό το blog. Η σελίδα προϊόντος συνδέει την πλήρη αναφορά API αν θέλετε να αντιπαραβάλετε τις παραπάνω κλήσεις με τη δική σας ροή εγγράφων