Ανοίξτε ένα PDF που παρήγαγε το Microsoft Word ή το Excel, περιηγηθείτε στις σελίδες του και τίποτα δεν φαίνεται ασυνήθιστο. Φορτώστε το σε ένα πρόγραμμα Delphi, διαβάστε τον αριθμό σελίδων και ο αριθμός είναι σωστός. Στη συνέχεια, αποθηκεύστε το ξανά με ενεργοποιημένη την κρυπτογράφηση και η εργασία αποτυγχάνει με ένα σφάλμα EListError, ή η έξοδος ανοίγει με προειδοποίηση κατεστραμμένης διασταυρούμενης αναφοράς (cross-reference). Το αρχείο δεν ήταν ποτέ κατεστραμμένο. Πρόκειται για ένα αρχείο υβριδικής αναφοράς (hybrid-reference), και η ίδια η δομή που επιτρέπει σε ένα πρόγραμμα προβολής δεκαπέντε ετών να το ανοίξει είναι η δομή που νικά έναν φορτωτή (loader) που σταματά να διαβάζει πολύ νωρίς.
Αυτός είναι ένας από τους πιο συνηθισμένους τρόπους με τους οποίους μια ροή εργασίας PDF που πέρασε κάθε εσωτερικό έλεγχο συναντά ένα αρχείο που δεν μπορεί να επεξεργαστεί πλήρως (round-trip). Τα αρχεία εισόδου δημιουργήθηκαν όλα εσωτερικά, επομένως δεν ήταν ποτέ υβριδικά. Το πρώτο υβριδικό αρχείο φτάνει την ημέρα που ένας πελάτης προωθεί ένα τιμολόγιο που εξήχθη από ένα υπολογιστικό φύλλο.
Τι γράφουν στην πραγματικότητα το Word και το Excel
Το πρότυπο ISO 32000-1 περιγράφει τη διάταξη υβριδικής αναφοράς στην §7.5.8.4. Μια εφαρμογή που θέλει δυνατότητες PDF 1.5, όπως ροές αντικειμένων (object streams), επιτρέποντας ταυτόχρονα σε έναν αναγνώστη PDF 1.4 να ανοίξει το αρχείο, γράφει τις πληροφορίες διασταυρούμενης αναφοράς δύο φορές. Υπάρχει ένας κλασικός πίνακας διασταυρούμενων αναφορών, οι σειρές ASCII σταθερού πλάτους που ολοκλήρωναν κάθε PDF μέχρι την έκδοση 1.4, και υπάρχει μια ροή διασταυρούμενων αναφορών που ευρετηριάζει τα υπόλοιπα. Το trailer του κλασικού τμήματος φέρει μια καταχώριση /XRefStm της οποίας η τιμή είναι η μετατόπιση byte (byte offset) αυτής της ροής.
Ο καταμερισμός της εργασίας είναι σκόπιμος. Αντικείμενα στα οποία πρέπει να έχει πρόσβαση ένας παλιός αναγνώστης, συμπεριλαμβανομένου του καταλόγου και του δέντρου σελίδων, είναι προσβάσιμα από τον κλασικό πίνακα. Αντικείμενα που ενσωματωθηκαν σε συμπιεσμένες ροές αντικειμένων επισημαίνονται ως ελεύθερα στον κλασικό πίνακα, με μια καταχώριση τύπου f, έτσι ώστε ένας αναγνώστης 1.4 να τα προσπερνά απευθείας και να μην σκοντάφτει ποτέ σε μια δομή που δεν μπορεί να αναλύσει. Οι πραγματικές τους τοποθεσίες υπάρχουν μόνο στη ροή διασταυρούμενων αναφορών. Η υπογραφή ενός τέτοιου αρχείου είναι το τέλος του: ένα σύντομο κλασικό τμήμα, συχνά τίποτα περισσότερο από xref ακολουθούμενο από μια κεφαλίδα υποτμήματος 0 0, του οποίου το trailer δείχνει στο /XRefStm όπου βρίσκονται τα πραγματικά δεδομένα ανάκτησης.
Γιατί ο σωστός αριθμός σελίδων δεν αποδεικνύει τίποτα
Επειδή ο κατάλογος και το δέντρο σελίδων είναι προσβάσιμα από τον κλασικό πίνακα επίτηδες, ένας φορτωτής που διαβάζει μόνο αυτόν τον πίνακα βρίσκει το /Root, διασχίζει το δέντρο σελίδων και αναφέρει τον σωστό αριθμό σελίδων. Όλα όσα χρειάζεται ένας παλιός αναγνώστης είναι παρόντα, επομένως το αρχείο φαίνεται υγιές. Τα αντικείμενα που χάθηκαν είναι αυτά που είναι συσκευασμένα σε ροές αντικειμένων: λεξικά πεδίων AcroForm, στοιχεία δομής tagged-PDF και η μεγάλη ουρά μικρών λεξικών που δεν χρειαζόταν ποτέ να είναι ορατά σε ένα παλό πρόγραμμα προβολής.
Δεν παρατηρείτε το κενό μέχρι κάτι να αγγίξει αυτά τα αντικείμενα, και μια πλήρης εκ νέου αποθήκευση τα αγγίζει όλα. Η περιήγηση στο έγγραφο για την εκ νέου κρυπτογράφηση ή την επανεγγραφή του είναι ακριβώς η λειτουργία που ζητά με τη σειρά κάθε αριθμό αντικειμένου, γι' αυτό και το σύμπτωμα εμφανίζεται κατά τον χρόνο αποθήκευσης και όχι κατά τον χρόνο φόρτωσης, μακριά από την αιτία του.
Η παγίδα είναι ένας ανιχνευτής που βλέπει το xref και σταματά
Ο οικονομικός τρόπος για να αποφασίσετε πώς ευρετηριάζεται ένα αρχείο είναι να ακολουθήσετε το startxref και να επιθεωρήσετε τα πρώτα byte στα οποία δείχνει. Η λέξη-κλειδί xref σημαίνει έναν κλασικό πίνακα. Ένα αντικείμενο ροής (stream object) σημαίνει μια ροή διασταυρούμενων αναφορών. Αυτή η δοκιμή είναι σωστή για κάθε αρχείο που δεσμεύεται σε ένα σχήμα. Είναι λάθος για ένα υβριδικό αρχείο, του οποίου το startxref στοχεύει σε ένα κλασικό τμήμα με αποκλειστικό σκοπό να ικανοποιήσει τους παλιούς αναγνώστες, ενώ το /XRefStm στο trailer αυτού του τμήματος είναι το σημείο όπου ευρετηριάζεται στην πραγματικότητα το μεγαλύτερο μέρος του εγγράφου. Ένας ανιχνευτής που επιστρέφει "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;
Με τον ανιχνευτή πρόωρης εξόδου στη θέση του, η φόρτωση φαίνεται εντάξει και η εκ νέου αποθήκευση είναι το σημείο όπου τα απόντα αντικείμενα κάνουν αισθητή την παρουσία τους. Η διόρθωση δεν είναι να διαβάσετε περισσότερα byte στην αρχή. Είναι να αναγνωρίσετε το υβριδικό trailer και να ακολουθήσετε το /XRefStm προτού αποφασίσετε ότι η ανάγνωση του αρχείου ολοκληρώθηκε.
Η σειρά συγχώνευσης δεν είναι διαπραγματεύσιμη
Μόλις αναγνωστούν και τα δύο ευρετήρια, μπορούν να συνδυαστούν προς μία κατεύθυνση μόνο. Η ροή διασταυρούμενων αναφορών πρέπει να συγχωνευθεί πρώτη, με τις κλασικές καταχωρίσεις να συμπληρώνονται γύρω της. Ο λόγος είναι η μικρή εξαπάτηση στην καρδιά της μορφής. Ένα υβριδικό αρχείο επισημαίνει τα συμπιεσμένα αντικείμενά του ως ελεύθερα στον κλασικό πίνακα, ώστε οι παλιοί αναγνώστες να τα αγνοούν. Ένας φορτωτής που τιμά την πολιτική "το πρώτο που εμφανίζεται κερδίζει" και διαβάζει πρώτα τον κλασικό πίνακα θα καταγράψει αυτούς τους αριθμούς αντικειμένων ως ελεύθερους, και στη συνέχεια θα απορρίψει τις καταχωρίσεις ροής που πραγματικά τα εντοπίζουν, επειδή οι θέσεις είναι ήδη κατειλημμένες. Αντιστρέψτε τη σειρά και οι καταχωρίσεις τύπου 2 από τη ροή, η καθεμία αποτελούμενη από έναν αριθμό ροής αντικειμένου συν ένα ευρετήριο, κερδίζουν τις θέσεις που προορίζονται να κατέχουν, και οι κλασικές καταχωρίσεις εγκαθίστανται γύρω τους.
Η ίδια πειθαρχία προστατεύει από το να επαναφέρει μια παλαιότερη αναθεώρηση ένα διαγραμμένο αντικείμενο. Οι σταδιακές ενημερώσεις (incremental updates) συνδέονται προς τα πίσω μέσω του /Prev, και μια ελεύθερη καταχώριση τύπου 0 είναι ένας φρουρός (sentinel) που δείχνει ότι ένα πιο πρόσφατο τμήμα έχει αποσύρει έναν αριθμό αντικειμένου. Ένα μεταγενέστερο, παλαιότερο τμήμα στην αλυσίδα δεν πρέπει να επιτρέπεται να αντικαταστήσει αυτόν τον φρουρό με μια παλιά τοποθεσία. Αντιμετωπίστε το πρώτο εμφανιζόμενο ως αυθεντικό για τους ελεύθερους δείκτες και το διαγραμμένο αντικείμενο παραμένει διαγραμμένο. Αν το χειριστείτε απρόσεκτα, το ίδιο το ιστορικό του αρχείου θα επαναφέρει περιεχόμενο που η τελευταία αναθεώρηση αφαίρεσε.
Τι σημαίνει αυτό στο HotPDF
Η μηχανή επιλύει τα αρχεία υβριδικής αναφοράς για εσάς, και το κάνει σε κάθε διαδρομή που πρέπει να αναλύσει τα δεδομένα διασταυρούμενης αναφοράς. Φορτώστε ένα έγγραφο με την LoadFromFile ή την LoadFromStream, κάντε τις αλλαγές σας και καλέστε την SaveLoadedDocument. Ή εκτελέστε μια λειτουργία μίας φάσης, όπως η EncryptFile, που διαβάζει μια είσοδο και γράφει μια έξοδο. Σε κάθε περίπτωση, η ανάκτηση διαβάζει το /XRefStm, συγχωνεύει το τμήμα ροής πριν από τις κλασικές καταχωρίσεις και επιλύει τα αντικείμενα που υπάρχουν στις ροές πριν η εγγραφή τα απαριθμήσει. Η διαδρομή κρυπτογράφησης 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]);
Η λεπτομέρεια που αξίζει να κρατήσετε βρίσκεται πριν από το API. Αρχεία που προέρχονται από Word, Excel, PowerPoint και μια μακρά λίστα ροών "Αποθήκευση ως PDF" είναι συνήθως υβριδικά, επομένως ένας φορτωτής που δοκιμάζετε μόνο έναντι της δικής σας εξόδου παραγωγής ενδέχεται να μην συναντήσει ποτέ κανένα κατά τις δοκιμές. Εμπλουτίστε τα δείγματα δοκιμών σας με έγγραφα που εξήχθησαν από πραγματικές εφαρμογές Office, όχι μόνο με αρχεία που παρήγαγε ο δικός σας κώδικας.
Έλεγχος ενός αρχείου που υποψιάζεστε
Δύο επιθεωρήσεις λύνουν την απορία γρήγορα. Ανοίξτε το αρχείο σε προβολή δεκαεξαδικού (hex view) και διαβάστε τα byte μετά το τελικό startxref. Ένα υβριδικό αρχείο δείχνει ένα σύντομο κλασικό τμήμα του οποίου το λεξικό trailer περιέχει το /XRefStm. Ή συγκρίνετε τον αριθμό αντικειμένων που αναφέρει μια πλήρης ανάλυση με τον υψηλότερο αριθμό αντικειμένου που δηλώνει το /Size στο trailer. Ένα μεγάλο κενό σημαίνει ότι αντικείμενα κρύβονται σε ροές που ο φορτωτής δεν έχει ανοίξει, η οποία είναι η ίδια έλλειψη που μετατρέπεται σε αποτυχία κατά τον χρόνο αποθήκευσης αργότερα.
Η πλευρά του συγγραφέα αυτής της ιστορίας, ο τρόπος με τον οποίο παράγονται εξ αρχής οι ροές αντικειμένων και οι συμπιεσμένες διασταυρούμενες αναφορές, καλύπτεται στο άρθρο μας σχετικά με τις ροές αντικειμένων και τις σταδιακές ενημερώσεις. Όταν το εν λόγω υβριδικό αρχείο είναι επίσης πολύ μεγάλο, οι τεχνικές φόρτωσης στον οδηγό Direct File API για μεγάλες ροές εργασίας PDF σάς επιτρέπουν να το επιθεωρήσετε χωρίς να το διαβάσετε ολόκληρο στη μνήμη. Και τα δύο συνδυάζονται φυσικά με την ανάκτηση που περιγράφεται εδώ, η οποία παρέχεται ως μέρος του HotPDF Component για Delphi και C++Builder μαζί με τα API φόρτωσης, επεξεργασίας, κρυπτογράφησης και υπογραφής που καλύπτονται σε άλλα σημεία αυτού του ιστολογίου.