Ένα πρόγραμμα ανάγνωσης PDF δεν ξεκινά από την αρχή του αρχείου. Ξεκινά στο τέλος. Τα τελευταία λίγα byte περιέχουν τη διεύθυνση όλων των υπολοίπων, και ένας αναλυτής που δεν κατανοεί αυτή τη σειρά θα παρερμηνεύσει τη μορφή από την πρώτη γραμμή. Επομένως, ο πιο χρήσιμος τρόπος για να μάθετε το PDF στον δίσκο είναι να το μάθετε με τον τρόπο που το κάνει ένας αναγνώστης: πρώτα η ουρά, μετά άλμα προς τα πίσω στον χάρτη, στη συνέχεια ανάλυση των αντικειμένων στα οποία δείχνει ο χάρτης
Τα ίδια τα byte είναι αρκετά απλά για να διαβαστούν σε ένα πρόγραμμα επεξεργασίας κειμένου όταν τίποτα δεν είναι συμπιεσμένο. Ένα ελάχιστο έγγραφο μίας σελίδας που σχεδιάζει "Hello, World!" χωράει σε λιγότερα από πεντακόσια byte, και κάθε δομικό στοιχείο της μορφής είναι ορατό σε αυτό. Εδώ είναι ολόκληρο το αρχείο, με τα τέσσερα μέρη σημειωμένα:
%PDF-1.0 % Header
%âãÏÓ
1 0 obj % Body: the object sequence
<<
/Kids [2 0 R]
/Count 1
/Type /Pages
>>
endobj
2 0 obj
<<
/Rotate 0
/Parent 1 0 R
/Resources 3 0 R
/MediaBox [0 0 612 792]
/Contents [4 0 R]
/Type /Page
>>
endobj
3 0 obj
<< /Font << /F0 << /BaseFont /Times-Italic /Subtype /Type1 /Type /Font >> >> >>
endobj
4 0 obj
<< /Length 65 >>
stream
1. 0. 0. 1. 50. 700. cm BT
/F0 36. Tf
(Hello, World!) Tj
ET
endstream
endobj
5 0 obj
<< /Pages 1 0 R /Type /Catalog >>
endobj
xref % Cross-reference table
0 6
0000000000 65535 f
0000000015 00000 n
0000000074 00000 n
0000000192 00000 n
0000000291 00000 n
0000000409 00000 n
trailer % Trailer
<<
/Root 5 0 R
/Size 6
>>
startxref
459
%%EOF
Τέσσερα μέρη, πάντα με αυτήν τη σειρά προς τα κάτω στο αρχείο: μια κεφαλίδα, ένα σώμα αντικειμένων, ένας πίνακας διασταυρούμενων αναφορών και ένα τρέιλερ (trailer). Η παγίδα είναι ότι τα διαβάζετε σχεδόν με την αντίστροφη σειρά. Το ISO 32000-2 §7.5.1 παραθέτει την ίδια ανατομία τεσσάρων μερών, και ο λόγος για την πρόσβαση από πίσω προς τα εμπρός είναι καθαρά πρακτικός: ένας αναγνώστης που πηδά κατευθείαν στο αντικείμενο που χρειάζεται είναι πολύ πιο γρήγορος από αυτόν που σαρώνει κάθε byte από την κορυφή, και αυτή η τυχαία πρόσβαση είναι ακριβώς αυτό που υπάρχουν το τρέιλερ και ο πίνακας διασταυρούμενων αναφορών για να παρέχουν
Η κεφαλίδα είναι δύο γραμμές και η δεύτερη έχει σημασία
Η πρώτη γραμμή είναι %PDF-1.0. Το σύμβολο του ποσοστού το καθιστά σχόλιο όσον αφορά τη σύνταξη, αλλά οι αναγνώστες το χειρίζονται ως υπογραφή αρχείου και τραβούν τον αριθμό έκδοσης από αυτό. Ο χειρισμός εκδόσεων είναι χαλαρός στην πράξη. Ένας αναγνώστης φτιαγμένος για PDF 2.0 θα ανοίξει με χαρά ένα αρχείο που ισχυρίζεται 1.0, και οι περισσότεροι αναγνώστες θα επιχειρήσουν να ανοίξουν ένα αρχείο του οποίου η δηλωμένη έκδοση είναι λάθος ή του οποίου η γραμμή έκδοσης είναι θαμμένη λίγο πιο μέσα στο αρχείο αντί στο μηδενικό byte. Ο αριθμός είναι μια υπόδειξη για το ποιες δυνατότητες να περιμένουμε, όχι μια πύλη
Η δεύτερη γραμμή είναι αυτή που οι άνθρωποι διαγράφουν κατά λάθος και μετά περνούν ένα απόγευμα προσπαθώντας να διορθώσουν σφάλματα. Είναι επίσης ένα σχόλιο, αλλά το ωφέλιμο φορτίο του είναι τέσσερα byte πάνω από το ASCII 127. Υπάρχουν ώστε οτιδήποτε μετακινεί το αρχείο σε "λειτουργία κειμένου" να το αναγνωρίζει ως δυαδικό (binary) και να σταματά να ξαναγράφει τις καταλήξεις γραμμών (line endings). Ένα PDF φέρει συμπιεσμένες ροές των οποίων τα byte μπορούν να ταιριάζουν με επιστροφή φορείου (carriage return) ή αλλαγή γραμμής (line feed) από σύμπτωση· εάν ένα εργαλείο μεταφοράς τα ξαναγράψει, το μήκος της ροής που καταγράφεται στο λεξικό δεν ταιριάζει πλέον με τα byte στον δίσκο και το αρχείο καταστρέφεται. Το σχόλιο high-byte είναι μια άμυνα σαράντα ετών ενάντια στο FTP σε λειτουργία ASCII, και εξακολουθεί να βρίσκεται σε κάθε αρχείο που γράφει ένα σοβαρό εργαλείο, επειδή η αποτυχία που αποτρέπει είναι σιωπηλή και ολική
Το σώμα κρατά τα αντικείμενα, καθένα αριθμημένο
Ό,τι συνθέτει το έγγραφο ζει στο σώμα ως μια επίπεδη ακολουθία έμμεσων αντικειμένων. Καθένα ανοίγει με δύο ακέραιους και τη λέξη-κλειδί obj, κρατά το περιεχόμενό του και κλείνει με το endobj. Το Αντικείμενο 1 στο παραπάνω δείγμα είναι ο κόμβος δέντρου σελίδων: 1 0 obj, μετά ένα λεξικό, μετά endobj. Ο πρώτος ακέραιος είναι ο αριθμός αντικειμένου, ο δεύτερος είναι ο αριθμός γενιάς (generation number). Η γενιά είναι σχεδόν πάντα μηδέν σε ένα πρόσφατα γραμμένο αρχείο· αυξάνεται μόνο όταν ένας αριθμός αντικειμένου επαναχρησιμοποιείται σε επεξεργασίες, κάτι που είναι αρκετά σπάνιο ώστε να μπορείτε να αντιμετωπίζετε μια μη μηδενική γενιά ως σημάδι ότι το αρχείο έχει περάσει από σταδιακές ενημερώσεις. Το περιεχόμενο μεταξύ των λέξεων-κλειδιών είναι ένα λεξικό εδώ, γραμμένο μεταξύ << και >>, αλλά θα μπορούσε εξίσου καλά να είναι ένας αριθμός, μια συμβολοσειρά, ένας πίνακας ή μια ροή
Αυτό που το κάνει γράφημα (graph) και όχι λίστα είναι το διακριτικό αναφοράς (reference token) 2 0 R. Αυτό σημαίνει "αντικείμενο 2, γενιά 0, όπου κι αν τυχαίνει να ζει στο αρχείο". Ο κόμβος του δέντρου σελίδων παραπάνω δεν περιέχει τη σελίδα του· δείχνει στο αντικείμενο 2, το οποίο δείχνει στους πόρους του και στη ροή περιεχομένου με τον ίδιο μηχανισμό. Το σώμα είναι διατεταγμένο με όποια σειρά ο συγγραφέας βρήκε βολική, και οι αναφορές το ράβουν σε ένα δέντρο ριζωμένο στον κατάλογο (catalog). Η θέση στο αρχείο δεν έχει κανένα νόημα. Η ταυτότητα προέρχεται από τον αριθμό αντικειμένου και η τοποθεσία προέρχεται από τον πίνακα διασταυρούμενων αναφορών
Ο πίνακας διασταυρούμενων αναφορών είναι ένα ευρετήριο μετατοπίσεων byte
Ο πίνακας xref είναι αυτό που μετατρέπει τους αριθμούς αντικειμένων σε θέσεις αρχείου. Είναι ο λόγος που ένας αναγνώστης μπορεί να ανοίξει ένα έγγραφο χιλίων σελίδων και να αποδώσει τη σελίδα 850 χωρίς να αναλύσει τις 849 σελίδες πριν από αυτήν. Κάθε καταχώριση καταγράφει ακριβώς πού ξεκινά το αντικείμενό της, μετρημένη σε byte από την αρχή του αρχείου:
xref
0 6 % 6 entries, starting at object 0
0000000000 65535 f % entry 0: head of the free list
0000000015 00000 n % object 1 begins at byte 15
0000000074 00000 n % object 2 begins at byte 74
0000000192 00000 n % object 3 begins at byte 192
0000000291 00000 n % object 4 begins at byte 291
0000000409 00000 n % object 5 begins at byte 409
Το σταθερό πλάτος είναι σκόπιμο. Κάθε καταχώριση είναι ακριβώς είκοσι byte: μια δεκαψήφια μετατόπιση, ένα κενό, μια πενταψήφια γενιά, ένα κενό, ένας τύπος ενός χαρακτήρα και ένα τέλος γραμμής δύο byte. Επειδή οι σειρές είναι ομοιόμορφες, ένας αναγνώστης μπορεί να προσπελάσει απευθείας την καταχώριση για το αντικείμενο n με αριθμητική (arithmetic) αντί για σάρωση, επομένως ο πίνακας που δίνει τυχαία πρόσβαση στο σώμα είναι ο ίδιος τυχαία προσβάσιμος. Η γραμμή 0 6 είναι μια κεφαλίδα υποενότητας: λέει ότι οι επόμενες καταχωρίσεις περιγράφουν έξι αντικείμενα ξεκινώντας από τον αριθμό 0
Το Αντικείμενο 0 είναι ειδικό και πάντα παρόν. Ο τύπος του είναι f για ελεύθερο (free), η γενιά του είναι 65535 και είναι επικεφαλής της συνδεδεμένης λίστας των ελεύθερων αριθμών αντικειμένων. Σε ένα αρχείο που δεν έχει ποτέ υποστεί επεξεργασία, η ελεύθερη λίστα είναι μόνο αυτή η μία καταχώριση, μια τυπικότητα. Κερδίζει την αξία της κατά τη διάρκεια σταδιακών ενημερώσεων, όταν η διαγραφή ενός αντικειμένου προσθέτει τον αριθμό του σε αυτήν τη λίστα, ώστε μια μεταγενέστερη επεξεργασία να μπορεί να τον ανακτήσει. Οι άλλες καταχωρίσεις είναι τύπου n για σε-χρήση (in-use), και ο δεκαψήφιος αριθμός τους είναι η μετατόπιση που θα αναζητούσατε για να διαβάσετε τον ορισμό αυτού του αντικειμένου
Το τρέιλερ είναι το σημείο εισόδου, και βρίσκεται στο τέλος
Το τρέιλερ είναι το πρώτο πράγμα που πραγματικά καταναλώνει ένας αναγνώστης, παρόλο που γράφεται τελευταίο. Ένας αναλυτής ανοίγει το αρχείο, αναζητά το τέλος και περπατά προς τα πίσω αναζητώντας το %%EOF. Ακριβώς από πάνω του βρίσκεται το startxref ακολουθούμενο από έναν μόνο αριθμό, και αυτός ο αριθμός είναι η μετατόπιση byte της λέξης-κλειδιού xref. Με αυτό, ο αναγνώστης πηδά απευθείας στον πίνακα διασταυρούμενων αναφορών χωρίς να έχει σαρώσει ούτε ένα αντικείμενο:
trailer
<<
/Root 5 0 R % the document catalog
/Size 6 % one more than the highest object number
>>
startxref
459 % byte offset of the xref table
%%EOF
Το λεξικό του τρέιλερ φέρει τις δύο τιμές που χρειάζεται ένας αναγνώστης πριν μπορέσει να κάνει οτιδήποτε άλλο. Το /Root δείχνει στον κατάλογο του εγγράφου, στο αντικείμενο 5 εδώ, που είναι η κορυφή του γραφήματος αντικειμένων και η διαδρομή προς το δέντρο σελίδων. Το /Size είναι το πλήθος των καταχωρίσεων που θα έπρεπε να περιέχει ο πίνακας διασταυρούμενων αναφορών, το οποίο είναι ένα περισσότερο από τον υψηλότερο αριθμό αντικειμένου, λόγω της ελεύθερης καταχώρισης στη θέση μηδέν. Από το %%EOF προκύπτει ολόκληρη η ακολουθία ανάγνωσης: βρείτε τον δείκτη, διαβάστε το startxref για να εντοπίσετε τον πίνακα, φορτώστε τον πίνακα για να μάθετε πού ζει κάθε αντικείμενο, διαβάστε το /Root για να βρείτε τον κατάλογο, και αναλύστε (resolve) τα αντικείμενα κατά παραγγελία από εκεί. Η κεφαλίδα, που βρίσκεται στην κορυφή, ελάχιστα συμβουλεύεται μέχρι αργά. Ο χάρτης στο κάτω μέρος είναι αυτό που χρειάζεται πρώτα ο αναγνώστης
Η σταδιακή ενημέρωση προσαρτά έναν δεύτερο χάρτη αντί να ξαναγράφει
Αυτός ο σχεδιασμός (από την ουρά πρώτα) αποδίδει όταν ένα αρχείο αλλάζει. Ένα PDF μπορεί να επεξεργαστεί χωρίς να ξαναγραφτεί κανένα από τα byte που βρίσκονται ήδη στο δίσκο. Τα νέα και τροποποιημένα αντικείμενα προσαρτώνται στο τέλος, ακολουθούμενα από μια φρέσκια ενότητα διασταυρούμενων αναφορών και ένα φρέσκο τρέιλερ, και το αρχικό αρχείο από κάτω παραμένει ανέγγιχτο. Το μόνο νέο κομμάτι τήρησης βιβλίων είναι μια καταχώριση /Prev στο νέο τρέιλερ, η οποία περιέχει τη μετατόπιση byte του προηγούμενου πίνακα διασταυρούμενων αναφορών:
% ... original file, unchanged, ends here ...
6 0 obj % an object added by this edit
<< /Type /Annot /Subtype /Text /Rect [100 700 120 720] >>
endobj
xref % a second xref section, for the new object only
6 1
0000000612 00000 n
trailer
<<
/Root 5 0 R
/Size 7
/Prev 459 % byte offset of the earlier xref table
>>
startxref
680 % offset of this new xref section
%%EOF
Ένας αναγνώστης εξακολουθεί να ξεκινά από το τελικό %%EOF, εξακολουθεί να ακολουθεί το startxref στον πιο πρόσφατο πίνακα, αλλά τώρα ακολουθεί την αλυσίδα /Prev προς τα πίσω στους παλαιότερους πίνακες, συγχωνεύοντάς τους έτσι ώστε η νεότερη καταχώριση για οποιονδήποτε αριθμό αντικειμένου να κερδίζει. Οι ενότητες διασταυρούμενων αναφορών σχηματίζουν μια συνδεδεμένη λίστα προς τα κάτω στο αρχείο, καθεμία παρακάμπτοντας την προηγούμενη για τα αντικείμενα που αγγίζει. Ένα αντικείμενο που αντικαταστάθηκε από μια επεξεργασία εξακολουθεί να υπάρχει φυσικά στην παλιά του μετατόπιση· απλώς δεν είναι πλέον προσβάσιμο, επειδή μια μεταγενέστερη καταχώριση xref δείχνει κάπου νεότερα
Αυτός είναι ο μηχανισμός που κάνει τα υπογεγραμμένα PDF επαληθεύσιμα. Μια ψηφιακή υπογραφή καλύπτει ένα εύρος byte του αρχείου, και επειδή μια σταδιακή ενημέρωση μόνο προσαρτά, τα υπογεγραμμένα byte δεν μετακινούνται ποτέ. Η υπογραφή εξακολουθεί να επικυρώνεται έναντι του αρχικού εύρους, ενώ οι μεταγενέστερες αναθεωρήσεις βρίσκονται πέρα από αυτό, καθεμία με το δικό της xref και τρέιλερ. Είναι επίσης ο λόγος για τον οποίο ένα PDF μπορεί να φέρει ανακτήσιμο ιστορικό: κάθε αντικατασταθέν (superseded) αντικείμενο βρίσκεται ακόμα στο δίσκο κάτω από μια προηγούμενη ενότητα διασταυρούμενων αναφορών, το οποίο είναι χαρακτηριστικό για την παρακολούθηση εκδόσεων και μειονέκτημα για όποιον πίστευε ότι η "διαγραφή" σήμαινε ότι τα byte είχαν φύγει
Το κόστος είναι η ανάπτυξη. Κάθε επεξεργασία προσαρτά· τίποτα δεν ανακτάται επιτόπου, επομένως ένα αρχείο που αναθεωρήθηκε πολλές φορές συσσωρεύει νεκρά αντικείμενα και μια μακριά αλυσίδα από ενότητες xref. Η θεραπεία είναι μια πλήρης επανεγγραφή: φορτώστε το έγγραφο και αποθηκεύστε το φρέσκο, γεγονός που επαναριθμεί τα επιζώντα αντικείμενα, απορρίπτει τα μη προσβάσιμα και εκπέμπει έναν ενιαίο, καθαρό πίνακα διασταυρούμενων αναφορών. Οι δύο στρατηγικές ανταλλάσσονται άμεσα. Η προσάρτηση είναι γρήγορη και διατηρεί τις υπογραφές και το ιστορικό· η επανεγγραφή είναι πιο αργή και απορρίπτει και τα δύο, με αντάλλαγμα ένα συμπαγές αρχείο
Η ανάγνωση των τεσσάρων μερών στην πράξη
Η γνώση της διάταξης είναι αρκετή για τον εντοπισμό σφαλμάτων των περισσότερων προβλημάτων "αυτό το αρχείο δεν ανοίγει" με το χέρι. Εάν ένας αναγνώστης απορρίψει ένα PDF, οι συνήθεις ένοχοι βρίσκονται στα δύο άκρα, όχι στη μέση. Μια κολοβή (truncated) λήψη χάνει το τρέιλερ, οπότε λείπει το startxref ή το %%EOF και ο αναγνώστης δεν έχει σημείο εισόδου· οι ανεκτικοί αναγνώστες καταφεύγουν στη σάρωση ολόκληρου του αρχείου για την αναδόμηση του xref, που είναι ακριβώς η αργή διαδρομή που ο πίνακας προοριζόταν να αποφύγει. Μια κακή μεταφορά σε λειτουργία κειμένου καταστρέφει τα byte ροής ή οι μετατοπίσεις παύουν να ταιριάζουν με την πραγματικότητα, και τα αντικείμενα φορτώνονται από λάθος θέση. Όταν οι μετατοπίσεις στον πίνακα δεν δείχνουν πλέον σε πραγματικές λέξεις-κλειδιά obj, το αρχείο έχει καταστραφεί δομικά, ακόμη και αν κάθε αντικείμενο είναι μεμονωμένα εντάξει
Για νέο κώδικα, το μάθημα από τη διάταξη είναι να αφήσετε μια βιβλιοθήκη να έχει την ιδιοκτησία της τήρησης των βιβλίων για τα byte. Οι μετατοπίσεις στον πίνακα διασταυρούμενων αναφορών πρέπει να συμφωνούν με τις πραγματικές θέσεις κάθε αντικειμένου στο byte, το τρέιλερ πρέπει να δείχνει στον σωστό πίνακα, και οι σταδιακές ενημερώσεις πρέπει να αλυσοδένονται σωστά μέσω του /Prev. Ένα εγγενές συστατικό (native component) όπως το HotPDF Component για Delphi και C++Builder χειρίζεται όλα αυτά όταν γράφει ένα αρχείο, συμπεριλαμβανομένης της επιλογής μεταξύ της προσάρτησης μιας σταδιακής αναθεώρησης και της επανεγγραφής ενός συμπαγούς. Αν θέλετε να δείτε την ίδια δομή να χτίζεται από το μηδέν αντί να ανατέμνεται, το συνοδευτικό άρθρο για τη δημιουργία ενός εγγράφου PDF από το μηδέν παρουσιάζει βήμα-βήμα την εκπομπή της κεφαλίδας, των αντικειμένων, του xref και του τρέιλερ με τη σειρά