Ανοίξτε ένα PDF που παρήγαγε το Microsoft Word ή το Excel, ξεφυλλίστε το, και τίποτα δεν φαίνεται ασυνήθιστο. Φορτώστε το σε ένα πρόγραμμα Delphi, διαβάστε τον αριθμό σελίδων, και ο αριθμός είναι σωστός. Στη συνέχεια, αποθηκεύστε το ξανά (re-save) με ενεργοποιημένη την κρυπτογράφηση και η εργασία αποτυγχάνει με ένα EListError, ή η έξοδος ανοίγει με μια προειδοποίηση για κατεστραμμένη παραπομπή (damaged cross-reference warning). Το αρχείο δεν ήταν ποτέ κατεστραμμένο. Πρόκειται για ένα αρχείο υβριδικής αναφοράς (hybrid-reference file), και η ίδια δομή που επιτρέπει σε έναν προβολέα δεκαπέντε ετών να το ανοίξει είναι η δομή που νικά έναν φορτωτή (loader) ο οποίος σταματά να διαβάζει πολύ νωρίς
Αυτός είναι ένας από τους πιο συνηθισμένους τρόπους με τους οποίους ένας αγωγός PDF (PDF pipeline) που πέρασε κάθε εσωτερικό έλεγχο (internal test) συναντά ένα αρχείο στο οποίο δεν μπορεί να κάνει στρογγυλή διαδρομή (round-trip). Όλες οι είσοδοι δημιουργήθηκαν εσωτερικά (in-house), επομένως δεν ήταν ποτέ υβριδικές. Το πρώτο υβριδικό αρχείο φτάνει τη μέρα που ένας πελάτης προωθεί ένα τιμολόγιο που εξήχθη από ένα υπολογιστικό φύλλο
Τι γράφουν πραγματικά το Word και το Excel
Το ISO 32000-1 περιγράφει τη διάταξη υβριδικής αναφοράς (hybrid-reference layout) στην §7.5.8.4. Μια εφαρμογή που θέλει χαρακτηριστικά του PDF 1.5, όπως οι ροές αντικειμένων (object streams), ενώ εξακολουθεί να επιτρέπει σε έναν αναγνώστη PDF 1.4 να ανοίξει το αρχείο, γράφει τις πληροφορίες παραπομπής (cross-reference information) δύο φορές. Υπάρχει ένας κλασικός πίνακας παραπομπών, οι γραμμές ASCII σταθερού πλάτους (fixed-width ASCII rows) που τερμάτιζαν κάθε PDF μέχρι την έκδοση 1.4, και υπάρχει μια ροή παραπομπών (cross-reference stream) που ευρετηριάζει τα υπόλοιπα. Το τρέιλερ (trailer) της κλασικής ενότητας (classic section) φέρει μια καταχώριση /XRefStm της οποίας η τιμή είναι η μετατόπιση byte (byte offset) αυτής της ροής
Ο καταμερισμός εργασίας είναι σκόπιμος. Τα αντικείμενα που πρέπει να προσεγγίσει ένας παλιός αναγνώστης (old reader), μεταξύ των οποίων ο κατάλογος (catalog) και το δέντρο σελίδων (page tree), είναι διευθυνσιοδοτήσιμα από τον κλασικό πίνακα. Τα αντικείμενα που διπλώθηκαν σε συμπιεσμένες ροές αντικειμένων σημειώνονται ως ελεύθερα (free) στον κλασικό πίνακα, με μια καταχώριση τύπου f, ώστε ένας αναγνώστης 1.4 να τα προσπερνά κατευθείαν και να μην σκοντάφτει ποτέ σε μια δομή που δεν μπορεί να αναλύσει (parse). Οι πραγματικές τους τοποθεσίες ζουν μόνο στη ροή παραπομπών. Η υπογραφή ενός τέτοιου αρχείου είναι η ουρά (tail) του: μια σύντομη κλασική ενότητα, συχνά τίποτα περισσότερο από xref ακολουθούμενο από μια κεφαλίδα υποενότητας 0 0 (subsection header), της οποίας το τρέιλερ δείχνει στο /XRefStm όπου βρίσκονται τα πραγματικά δεδομένα ανάκτησης
Γιατί ο σωστός αριθμός σελίδων δεν αποδεικνύει τίποτα
Επειδή ο κατάλογος και το δέντρο σελίδων είναι προσβάσιμα από τον κλασικό πίνακα εσκεμμένα, ένας φορτωτής που διαβάζει μόνο αυτόν τον πίνακα βρίσκει το /Root, περπατά στο δέντρο σελίδων και αναφέρει τον σωστό αριθμό σελίδων. Όλα όσα χρειάζεται ένας παλιός αναγνώστης είναι παρόντα, οπότε το αρχείο φαίνεται υγιές. Τα αντικείμενα που έλειπαν είναι εκείνα που είναι συσκευασμένα σε ροές αντικειμένων: λεξικά πεδίων AcroForm, στοιχεία δομής tagged-PDF, η μακριά ουρά των μικρών λεξικών που ποτέ δεν χρειάστηκε να είναι ορατά σε έναν προβολέα παλαιού τύπου (legacy viewer)
Δεν παρατηρείτε το κενό μέχρι κάτι να αγγίξει αυτά τα αντικείμενα, και μια πλήρης επανεγγραφή τα αγγίζει όλα. Το περπάτημα του εγγράφου για επανεκρυπτογράφηση (re-encrypt) ή επανεγγραφή είναι ακριβώς η λειτουργία που ζητά κάθε αριθμό αντικειμένου με τη σειρά, γι' αυτό και το σύμπτωμα εμφανίζεται στον χρόνο αποθήκευσης (save time) αντί για τον χρόνο φόρτωσης (load time), μακριά από την αιτία του
Η παγίδα είναι ένας ανιχνευτής που βλέπει xref και σταματά
Ο φθηνός τρόπος για να αποφασίσετε πώς ευρετηριάζεται (indexed) ένα αρχείο είναι να ακολουθήσετε το startxref και να επιθεωρήσετε τα πρώτα byte στα οποία δείχνει. Η λέξη-κλειδί xref σημαίνει έναν κλασικό πίνακα. Ένα αντικείμενο ροής (stream object) σημαίνει μια ροή παραπομπών. Αυτός ο έλεγχος είναι σωστός για κάθε αρχείο που δεσμεύεται σε ένα σχήμα. Είναι λάθος για ένα υβριδικό αρχείο, του οποίου το startxref στοχεύει σε μια κλασική ενότητα με αποκλειστικό σκοπό να ικανοποιήσει παλιούς αναγνώστες, ενώ το /XRefStm στο τρέιλερ (trailer) αυτής της ενότητας είναι εκεί όπου ευρετηριάζεται πραγματικά το μεγαλύτερο μέρος του εγγράφου. Ένας ανιχνευτής (detector) που επιστρέφει "κλασικό" ("classic") στο πρώτο xref που συναντά δεν διαβάζει ποτέ το /XRefStm, και κάθε αντικείμενο που ζει μόνο στη ροή γίνεται αόρατο
var
Pdf: THotPDF;
PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('Invoice_XLS.pdf'); // count is correct
// inspect or edit the loaded document here
Pdf.SaveLoadedDocument('Invoice_secured.pdf'); // walks every object
finally
Pdf.Free;
end;
end;
Με τον ανιχνευτή πρόωρης εξόδου (early-exit detector) στη θέση του, το φορτίο (load) φαίνεται μια χαρά και η επανεγγραφή είναι το σημείο όπου ανακοινώνονται τα απόντα αντικείμενα. Η διόρθωση (fix) δεν είναι να διαβάσετε περισσότερα bytes στην αρχή. Είναι να αναγνωρίσετε το υβριδικό τρέιλερ και να ακολουθήσετε το /XRefStm προτού αποφασίσετε ότι το αρχείο έχει τελειώσει (is done)
Η σειρά συγχώνευσης δεν είναι διαπραγματεύσιμη
Μόλις διαβαστούν και τα δύο ευρετήρια (indexes), μπορούν να συνδυαστούν μόνο προς μία κατεύθυνση. Η ροή παραπομπών πρέπει να συγχωνευτεί (merged) πρώτη, με τις κλασικές καταχωρίσεις (classic entries) να συμπληρώνονται γύρω της. Ο λόγος είναι η μικρή απάτη στην καρδιά της μορφής (format). Ένα υβριδικό αρχείο (hybrid file) σημειώνει τα συμπιεσμένα αντικείμενά (compressed objects) του ως ελεύθερα (free) στον κλασικό πίνακα (classic table), ώστε οι παλιοί αναγνώστες να τα αγνοούν. Ένας φορτωτής που τιμά μια πολιτική «ο πρώτος που εμφανίζεται κερδίζει» (first-seen-wins) και διαβάζει πρώτα τον κλασικό πίνακα, θα καταγράψει αυτούς τους αριθμούς αντικειμένων ως ελεύθερους, και στη συνέχεια θα απορρίψει τις καταχωρίσεις της ροής που πραγματικά τους εντοπίζουν, επειδή οι θέσεις (slots) είναι ήδη κατειλημμένες. Αντιστρέψτε τη σειρά και οι καταχωρίσεις τύπου 2 (type 2) από τη ροή, η καθεμία ένας αριθμός ροής αντικειμένου (object-stream number) συν ένας δείκτης (index), κερδίζουν τις θέσεις που προορίζονται να κατέχουν, και οι κλασικές καταχωρίσεις εγκαθίστανται γύρω τους
Η ίδια πειθαρχία προφυλάσσει από μια παλαιότερη αναθεώρηση (revision) που ανασταίνει ένα διαγραμμένο αντικείμενο. Οι αυξητικές ενημερώσεις (incremental updates) αλυσιδώνονται προς τα πίσω μέσω του /Prev, και μια ελεύθερη καταχώριση τύπου 0 είναι ένας φρουρός (sentinel) ότι μια πιο πρόσφατη ενότητα έχει αποσύρει (retired) έναν αριθμό αντικειμένου. Μια μεταγενέστερη, παλαιότερη ενότητα στην αλυσίδα δεν πρέπει να επιτρέπεται να αντικαταστήσει αυτόν τον φρουρό με μια παρωχημένη (stale) τοποθεσία. Αντιμετωπίστε το first-seen ως έγκυρο (authoritative) για τους δείκτες ελεύθερου (free markers) και το διαγραμμένο αντικείμενο μένει διαγραμμένο. Αντιμετωπίστε το απρόσεκτα και η ίδια η ιστορία ενός αρχείου επαναζωντανεύει το περιεχόμενο που αφαίρεσε η τελευταία αναθεώρηση
Τι σημαίνει αυτό στην HotPDF
Η μηχανή επιλύει τα αρχεία υβριδικής αναφοράς για εσάς, και το κάνει σε κάθε διαδρομή (path) που πρέπει να αναλύσει (parse) τα δεδομένα παραπομπών. Φορτώστε ένα έγγραφο με την LoadFromFile ή την LoadFromStream, κάντε τις αλλαγές σας και καλέστε την SaveLoadedDocument· ή εκτελέστε μια λειτουργία one-shot (μία και έξω), όπως η EncryptFile, που διαβάζει μια είσοδο και γράφει μια έξοδο. Είτε έτσι είτε αλλιώς, η ανάκτηση (recovery) διαβάζει το /XRefStm, συγχωνεύει (merges) την ενότητα ροής (stream section) μπροστά από τις κλασικές καταχωρίσεις, και επιλύει τα αντικείμενα που ζουν σε ροές προτού η εγγραφή (write) τα απαριθμήσει. Η διαδρομή (path) της κρυπτογράφησης AES-256 είναι όπου το πρόβλημα εμφανίστηκε για πρώτη φορά, επειδή η κρυπτογράφηση ενός εγγράφου ξαναγράφει κάθε αντικείμενο και έτσι απαιτεί να έχει ήδη εντοπιστεί κάθε αντικείμενο
// One-shot: read the hybrid input, write an AES-256 encrypted copy
Pdf.EncryptFile('Letter_DOC.pdf', 'Letter_secured.pdf',
'owner-secret', '', aes256, [prPrint, prFillAnnotations]);
Η λεπτομέρεια που αξίζει να κρατήσετε βρίσκεται στην αρχή (upstream) του API. Αρχεία που φτάνουν από το Word, το Excel, το PowerPoint και μια μεγάλη λίστα αγωγών (pipelines) «Αποθήκευση ως PDF» ("Save as PDF") είναι συνήθως υβριδικά (hybrid), επομένως ένας φορτωτής που ασκείτε (exercise) μόνο έναντι της δικής σας εξόδου παραγωγής (generator output) μπορεί να μην συναντήσει ποτέ κανένα στις δοκιμές (testing). Δημιουργήστε τα δεδομένα των δοκιμών σας (seed your fixtures) με έγγραφα που εξάγονται από πραγματικές εφαρμογές Office, όχι μόνο με αρχεία που παρήγαγε ο δικός σας κώδικας
Έλεγχος ενός αρχείου που υποπτεύεστε
Δύο επιθεωρήσεις διευθετούν (settle) το ερώτημα γρήγορα. Ανοίξτε το αρχείο σε μια προβολή δεκαεξαδικών (hex view) και διαβάστε τα bytes μετά το τελικό startxref. Ένα υβριδικό αρχείο (hybrid file) δείχνει μια σύντομη κλασική ενότητα, της οποίας το λεξικό του τρέιλερ (trailer dictionary) περιέχει το /XRefStm. Ή συγκρίνετε την καταμέτρηση αντικειμένων (object count) που αναφέρει μια πλήρης ανάλυση (full parse) έναντι του υψηλότερου αριθμού αντικειμένου που δηλώνει το /Size στο τρέιλερ. Ένα μεγάλο χάσμα σημαίνει ότι αντικείμενα κρύβονται σε ροές που δεν έχει ανοίξει ο φορτωτής (loader), το οποίο είναι η ίδια έλλειψη (shortfall) που μετατρέπεται αργότερα σε αποτυχία κατά την αποθήκευση (save-time failure)
Η πλευρά του συγγραφέα (writer's side) αυτής της ιστορίας, πώς δημιουργούνται εξ αρχής οι ροές αντικειμένων και οι συμπιεσμένες παραπομπές, καλύπτεται στο άρθρο μας για τις ροές αντικειμένων και τις αυξητικές ενημερώσεις. Όταν το υβριδικό αρχείο για το οποίο μιλάμε είναι επίσης πολύ μεγάλο, οι τεχνικές φόρτωσης στον οδηγό (walkthrough) Direct File API για ροές εργασίας μεγάλων PDF σάς επιτρέπουν να το επιθεωρήσετε χωρίς να διαβάσετε ολόκληρο το αρχείο στη μνήμη. Και τα δύο συνδυάζονται (pair) φυσικά με την ανάκτηση (recovery) που περιγράφεται εδώ, η οποία διατίθεται ως μέρος του HotPDF Component για Delphi και C++Builder μαζί με τα API φόρτωσης, επεξεργασίας, κρυπτογράφησης και υπογραφής που καλύπτονται αλλού σε αυτό το blog