Το λεξικό (dictionary) Catalog του PDF έχει ακριβώς ένα απαιτούμενο κλειδί πλοήγησης: το /Pages. Αυτό το κλειδί πρέπει να δείχνει σε ένα έμμεσο (indirect) αντικείμενο τύπου /Pages, το οποίο με τη σειρά του περιέχει τον πίνακα /Kids και το συνολικό πλήθος σελίδων /Count. Πάρτε αυτόν τον δείκτη και κανένας συμβατός αναγνώστης (reader) δεν μπορεί να εντοπίσει ούτε μία σελίδα στο αρχείο. Το ISO 32000-1 §7.7.2 είναι σαφές σε αυτό το σημείο: το Catalog θα έχει μια καταχώρηση /Pages, και το αναφερόμενο αντικείμενο θα έχει τύπο /Pages. Τα αρχεία που παραβιάζουν αυτήν την απαίτηση δεν είναι απλώς μη συμμορφούμενα (non-conforming). Είναι δομικά σπασμένα με τρόπο που οι περισσότεροι αναλυτές (parsers) χειρίζονται άσχημα
Τι λένε πραγματικά οι προδιαγραφές
Ένα ελάχιστο συμβατό PDF έχει τουλάχιστον τρία αντικείμενα. Το Αντικείμενο 1 είναι το Catalog, το Αντικείμενο 2 είναι η ρίζα (root) Pages, και το Αντικείμενο 3 και μετά είναι μεμονωμένα λεξικά Page. Το Catalog δείχνει στη ρίζα Pages. Η ρίζα Pages παραθέτει τα παιδιά της στο /Kids. Κάθε Page φέρει μια αναφορά επιστροφής /Parent. Ολόκληρη η αλυσίδα είναι αμφίδρομη από τον σχεδιασμό της, ώστε ένας αναλυτής να μπορεί να ξεκινήσει από οποιοδήποτε άκρο και να διασχίσει (traverse) σε οποιαδήποτε σελίδα σε χρόνο O(log n) για ισορροπημένα δέντρα (balanced trees)
% Ελάχιστη συμβατή δομή (ISO 32000-1 §7.7.2)
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 5 0 R /Resources << >> >>
endobj
4 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 6 0 R /Resources << >> >>
endobj
Το page tree μπορεί να είναι ένθετο (nested). Ένα έγγραφο με χιλιάδες σελίδες συνήθως ομαδοποιεί τις σελίδες σε αντικείμενα ενδιάμεσων κόμβων που φέρουν επίσης τύπο /Pages, το καθένα με τα δικά του /Kids και ένα /Count που αντανακλά το υποδέντρο (subtree) κάτω από αυτό. Το /Count του ριζικού κόμβου (root node) ισούται πάντα με το συνολικό πλήθος σελίδων. Αυτός ο αριθμός είναι αυτό που εμφανίζουν τα προγράμματα προβολής στο πεδίο αριθμού σελίδας πριν αναλύσουν έστω και μία σελίδα, επειδή η ανάγνωση ενός ακέραιου από το αντικείμενο 2 είναι πολύ φθηνότερη από τη διάσχιση ολόκληρου του δέντρου
Πώς μοιάζει ένα αρχείο χωρίς Pages
Τα αρχεία που δεν διαθέτουν το λεξικό Pages προέρχονται συνήθως από γεννήτριες PDF που γράφουν τα αντικείμενα σελίδας απευθείας χωρίς να τα συναρμολογούν σε δέντρο, ή από καταστροφή (corruption) που αφαιρεί τον ριζικό κόμβο αφήνοντας ανέπαφα τα αντικείμενα φύλλων (leaf Page objects). Στο Catalog σε ένα τέτοιο αρχείο είτε λείπει εντελώς το κλειδί /Pages, είτε περιέχει μια αναφορά σε ένα αντικείμενο που δεν υπάρχει πλέον στον πίνακα cross-reference
% Μη συμμορφούμενο: Catalog χωρίς αναφορά /Pages
1 0 obj
<< /Type /Catalog >>
endobj
% Υπάρχουν αντικείμενα σελίδας (Page objects) αλλά δεν είναι προσβάσιμα από το Catalog
5 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 6 0 R /Resources << >> >>
endobj
15 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 16 0 R /Resources << >> >>
endobj
25 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 26 0 R /Resources << >> >>
endobj
Ένας αναλυτής που ακολουθεί τις προδιαγραφές θα διαβάσει το Catalog, θα προσπαθήσει να επιλύσει το /Pages, δεν θα βρει τίποτα (ή μια νεκρή αναφορά), και είτε θα εγείρει (raise) σφάλμα είτε θα αναφέρει μηδέν σελίδες. Αυτό που δεν πρέπει να κάνει είναι να συνεχίσει σαν να είχε το αρχείο μηδέν σελίδες και να πετύχει σιωπηλά. Αυτό παράγει μια κενή έξοδο που φαίνεται σωστή στα αυτοματοποιημένα εργαλεία και λάθος σε κάθε άνθρωπο που το ανοίγει
Γιατί καταρρέουν οι αναλυτές
Οι περισσότεροι αναλυτές PDF εκχωρούν (allocate) τον εσωτερικό πίνακα σελίδων τους κατά τον χρόνο φόρτωσης (load time) με βάση την τιμή /Count από τη ρίζα Pages. Όταν αυτή η ρίζα απουσιάζει, ο αναλυτής είτε διαβάζει μηδέν, δεν εκχωρεί τίποτα και στη συνέχεια αποαναφέρεται (dereferences) σε έναν null δείκτη την πρώτη φορά που οποιοσδήποτε κώδικας ζητά τη σελίδα 1, είτε διαβάζει σκουπίδια (garbage) και εκχωρεί ένα άκρως λανθασμένο buffer. Κανένα από τα δύο αποτελέσματα δεν είναι κομψό. Η παραβίαση πρόσβασης (access violation) στο 0x008E5D78 που εμφανίζεται στα αρχεία καταγραφής σφαλμάτων (crash logs) από την επεξεργασία ενός τέτοιου αρχείου είναι ακριβώς αυτό: μια αποαναφορά (dereference) ενός null-pointer μέσα στη διαδρομή πρόσβασης στη σελίδα (page-access path), που προκαλείται από την απουσία της δομής που ο αναλυτής υπέθεσε ότι θα ήταν πάντα εκεί
Η υποκείμενη υπόθεση σχεδιασμού είναι λογική. Η συντριπτική πλειοψηφία των PDF που υπάρχουν διαθέτουν λεξικό Pages. Οι αναλυτές που παραλείπουν τον έλεγχο ύπαρξης για να εξοικονομήσουν μερικές εντολές δεν είναι ριψοκίνδυνοι. Βελτιστοποιούν (optimizing) για τη συνήθη περίπτωση. Τα αρχεία που τιμωρούν αυτήν τη βελτιστοποίηση είναι αρκετά σπάνια ώστε ο κώδικας παραγωγής να μην συναντήσει ποτέ κάποιο μέχρι να το κάνει, οπότε η κατάρρευση (crash) είναι τόσο αναπαραγώγιμη όσο και ακατανόητη (baffling) εάν ο μηχανικός δεν έχει διαβάσει το §7.7.2
Ανάκτηση χωρίς page tree
Εάν ένας αναλυτής πρέπει να χειριστεί αυτά τα αρχεία αντί να τα απορρίψει, η ανάκτηση (recovery) ακολουθεί μια προβλέψιμη διαδρομή: σαρώστε (scan) κάθε έμμεσο αντικείμενο στον πίνακα cross-reference, συλλέξτε αυτά με /Type /Page, και ταξινομήστε τα με βάση τον αριθμό αντικειμένου. Η σειρά του αριθμού αντικειμένου δεν είναι εγγυημένο ότι θα ταιριάζει με τη σειρά ανάγνωσης στις προδιαγραφές, αλλά στην πράξη, οι γεννήτριες (generators) που παραλείπουν το page tree τείνουν να εκπέμπουν τις σελίδες διαδοχικά, οπότε η σειρά του αριθμού αντικειμένου είναι σωστή τις περισσότερες φορές
Ο ίδιος ο έλεγχος είναι φθηνός. Πριν διασχίσετε τον δείκτη /Pages του Catalog, επιβεβαιώστε ότι ο δείκτης υπάρχει, ότι επιλύεται σε ένα πραγματικό αντικείμενο, και ότι το /Type του επιλυμένου αντικειμένου ισούται με /Pages. Εάν οποιαδήποτε από αυτές τις τρεις συνθήκες αποτύχει, προχωρήστε στη γραμμική σάρωση (linear scan). Η σάρωση είναι πιο αργή από τη διάσχιση του δέντρου για μεγάλα έγγραφα, επειδή διαβάζει κάθε κεφαλίδα (header) αντικειμένου αντί να ακολουθεί μια ισορροπημένη διαδρομή (balanced path), αλλά λειτουργεί, και για ένα αρχείο που είναι ήδη κακοσχηματισμένο, η ορθότητα είναι πιο σημαντική από την ταχύτητα
Μια ακραία περίπτωση (edge case) που η γραμμική σάρωση δεν λύνει αυτόματα: τη σειρά των σελίδων. Χωρίς έναν πίνακα /Kids για να ορίσει την ακολουθία, η "σωστή" σειρά είναι απροσδιόριστη από τις προδιαγραφές. Η σειρά του αριθμού αντικειμένου είναι η πραγματιστική προεπιλογή. Εάν το αρχείο είναι αρκετά σημαντικό για να επεξεργαστεί προσεκτικά, ο έλεγχος για το εάν τα αντικείμενα Page φέρουν ένα ρητό /StructParents ή αναφορές σχολιασμών (annotation references) που υποδηλώνουν μια σειρά ανάγνωσης αξίζει την επιπλέον δουλειά
Επιπτώσεις για τις γεννήτριες PDF
Για όποιον γράφει μια γεννήτρια (generator) PDF και όχι έναν αναλυτή (parser), το μάθημα είναι στενό: εκπέμπετε πάντα τη ρίζα Pages πριν κλείσετε το αρχείο. Το Catalog χωρίς καταχώρηση /Pages δεν είναι έγκυρο PDF σύμφωνα με καμία αναθεώρηση (revision) των προδιαγραφών. Οι γεννήτριες που κατασκευάζουν τα αντικείμενα σελίδας on the fly (εν κινήσει) και συναρμολογούν το δέντρο κατά την οριστικοποίηση (finalization - η προσέγγιση που χρησιμοποιούν οι περισσότεροι streaming writers) είναι εντάξει αρκεί η οριστικοποίηση να εκτελείται πραγματικά. Η κοινή λειτουργία αποτυχίας (failure mode) είναι μια εξαίρεση (exception) ή πρώιμη επιστροφή (early return) που διακόπτει (aborts) την εγγραφή πριν ολοκληρωθεί το trailer, αφήνοντας πίσω ένα αρχείο που ανοίγει σε ορισμένα προγράμματα προβολής (τα οποία έχουν ευρετικές μεθόδους ανάκτησης - recovery heuristics) και αποτυγχάνει σε άλλα (που δεν έχουν)
Το PDF/A και το PDF/UA επιβάλλουν πρόσθετους περιορισμούς στο page tree πέρα από αυτούς που απαιτούν οι βασικές προδιαγραφές, αλλά κανένα από τα δύο δεν χαλαρώνει την απαίτηση /Pages. Ένας επικυρωτής (validator) που ελέγχει τη συμμόρφωση με το ISO 19005 ή το ISO 14289 θα εντοπίσει ένα λεξικό Pages που λείπει ως παραβίαση των βασικών προδιαγραφών πριν καν φτάσει στους κανόνες που αφορούν το προφίλ