Ένα PDF είναι στην ουσία του ένα δοχείο απλού κειμένου. Ανοίξτε τα περισσότερα αρχεία σε έναν hex editor και το πάνω μέρος είναι αναγνώσιμο: ένα σχόλιο έκδοσης, στη συνέχεια μια σειρά αριθμημένων αντικειμένων, έπειτα ένα μικρό ευρετήριο και ένας δείκτης στο κάτω μέρος που λέει σε έναν αναγνώστη από πού να ξεκινήσει. Αφαιρέστε τη συμπίεση και η μορφή είναι αρκετά προσιτή ώστε να μπορείτε να πληκτρολογήσετε ένα λειτουργικό έγγραφο σε έναν επεξεργαστή κειμένου και να το ανοίξετε σε ένα πρόγραμμα προβολής. Κάνοντας αυτό μία φορά μαθαίνετε περισσότερα για το πώς συγκρατείται ένα PDF από οποιαδήποτε ανάγνωση των προδιαγραφών, επειδή πρέπει να συνδέσετε τα αντικείμενα μεταξύ τους με το χέρι και το αρχείο αρνείται να ανοίξει μέχρι να κάνετε σωστά τη σύνδεση
Αυτός ο οδηγός κατασκευάζει το μικρότερο PDF που πραγματικά αποδίδει κάτι: μία σελίδα, τις λέξεις "Hello, World!" σε μια ενσωματωμένη γραμματοσειρά, σε χαρτί US Letter. Το τελικό αρχείο χρειάζεται ακριβώς πέντε αντικείμενα και μερικές γραμμές τήρησης βιβλίων γύρω τους. Θα γράψουμε πρώτα τα αντικείμενα, στη συνέχεια θα συναρμολογήσουμε την κεφαλίδα, τον πίνακα cross-reference (xref) και το trailer που τα συνδέουν σε ένα αρχείο που θα αποδεχτεί ένας αναγνώστης
Τα πέντε αντικείμενα που απαιτεί ένα πρόγραμμα προβολής
Ένας αναγνώστης δεν σαρώνει ένα PDF από πάνω προς τα κάτω αναζητώντας περιεχόμενο. Ξεκινά από το trailer, ακολουθεί μια αναφορά στο document catalog και διασχίζει μια αλυσίδα αντικειμένων από εκεί. Κάθε αντικείμενο σε αυτήν την αλυσίδα πρέπει να υπάρχει διαφορετικά το άνοιγμα αποτυγχάνει. Για ένα έγγραφο μίας σελίδας η αλυσίδα είναι μικρή και κάθε κρίκος έχει μια μόνο δουλειά:
- Το Catalog είναι η ρίζα. Είναι το αντικείμενο στο οποίο δείχνει το trailer, και η μόνη απαιτούμενη καταχώρηση εδώ είναι μια αναφορά στο page tree
- Το Pages είναι ο κόμβος του page tree. Παραθέτει τις σελίδες στο έγγραφο και αναφέρει πόσες υπάρχουν
- Η Page περιγράφει μία φυσική σελίδα: το μέγεθός της, τους πόρους (resources) με τους οποίους σχεδιάζει και ποιο content stream τη ζωγραφίζει
- Το Content stream διατηρεί τους τελεστές σχεδίασης, τις εντολές postfix που τοποθετούν κείμενο και γραφικά σε αυτήν τη σελίδα
- Η Font δηλώνει τη γραμματοσειρά στην οποία αναφέρεται το content stream. Χρησιμοποιήστε μία από τις 14 standard γραμματοσειρές και δεν χρειάζεται να ενσωματώσετε τίποτα
Κάθε αντικείμενο είναι αριθμημένο και διευθυνσιοδοτήσιμο. Ένα έμμεσο (indirect) αντικείμενο γράφεται ως N 0 obj ... endobj, όπου N είναι ο αριθμός του αντικειμένου και 0 είναι ο αριθμός γενιάς (generation number) του (πάντα 0 σε ένα αρχείο που γράφετε από την αρχή). Οπουδήποτε αλλού στο αρχείο δείχνετε σε αυτό το αντικείμενο με μια αναφορά: 5 0 R σημαίνει "αντικείμενο 5". Αυτές οι αναφορές είναι η καλωδίωση. Το catalog περιέχει 2 0 R στην αρίθμησή μας για να φτάσει στο page tree, το page tree περιέχει μια αναφορά πίσω κάτω στη σελίδα (page), και ούτω καθεξής. Κάντε λάθος σε έναν αριθμό και ο αναγνώστης ακολουθεί έναν αιωρούμενο δείκτη (dangling pointer) στο πουθενά
Ονόματα, λεξικά και streams
Τρία κομμάτια σύνταξης μεταφέρουν σχεδόν τα πάντα. Ένα όνομα (name) ξεκινά με μια κάθετο: /Type, /Page, /F0. Τα ονόματα είναι αναγνωριστικά (identifiers) με διάκριση πεζών-κεφαλαίων, όχι συμβολοσειρές (strings), και το PDF τα χρησιμοποιεί για κλειδιά λεξικού (dictionary) και για την προσθήκη ετικετών σχετικά με το τι είναι ένα αντικείμενο. Ένα λεξικό (dictionary) είναι ένα σύνολο ζευγών κλειδιού-τιμής τυλιγμένο σε διπλές γωνιακές αγκύλες, όπου κάθε κλειδί είναι ένα όνομα: << /Type /Page /MediaBox [0 0 612 792] >>. Οι τιμές μπορούν να είναι αριθμοί, ονόματα, πίνακες (arrays) σε αγκύλες, αναφορές (references) ή ένθετα λεξικά (nested dictionaries). Τα περισσότερα αντικείμενα PDF είναι λεξικά
Ένα stream είναι ένα λεξικό ακολουθούμενο από ένα μπλοκ bytes μεταξύ των λέξεων-κλειδιών stream και endstream. Εκεί ζουν οι τελεστές σχεδίασης σελίδας, και σε πραγματικά αρχεία εκεί ζουν επίσης συμπιεσμένες εικόνες και ενσωματωμένες γραμματοσειρές. Το λεξικό του stream περιγράφει τα bytes. Σε ένα αρχείο παραγωγής πρέπει να φέρει μια καταχώρηση /Length δίνοντας τον ακριβή αριθμό byte, και συχνά ένα /Filter όπως /FlateDecode όταν τα δεδομένα είναι συμπιεσμένα. Εμείς πρόκειται να βασιστούμε σε ένα εργαλείο για να συμπληρώσει το /Length, επειδή η μέτρηση των bytes με το χέρι είναι το μέρος αυτής της άσκησης χωρίς εκπαιδευτικό όφελος και με μεγάλη πιθανότητα ενός σφάλματος off-by-one που σπάει το αρχείο
Γράφοντας τα αντικείμενα
Εδώ είναι τα πέντε αντικείμενα με τη σειρά. Η λεπτομέρεια των συντεταγμένων που πρέπει να θυμάστε πριν διαβάσετε το content stream: Το PDF μετράει από την κάτω αριστερή γωνία της σελίδας σε σημεία (points), όπου ένα σημείο είναι 1/72 της ίντσας, και ο άξονας Y αυξάνεται προς τα πάνω. Μια σελίδα US Letter είναι 612 επί 792 σημεία, οπότε το 50 700 βρίσκεται κοντά στο πάνω αριστερό μέρος, όχι στο κάτω
1 0 obj
<< /Type /Catalog
/Pages 2 0 R
>>
endobj
2 0 obj
<< /Type /Pages
/Kids [3 0 R]
/Count 1
>>
endobj
3 0 obj
<< /Type /Page
/Parent 2 0 R
/MediaBox [0 0 612 792]
/Resources << /Font << /F0 4 0 R >> >>
/Contents 5 0 R
>>
endobj
4 0 obj
<< /Type /Font
/Subtype /Type1
/BaseFont /Helvetica
>>
endobj
5 0 obj
<< /Length 44 >>
stream
BT
/F0 36 Tf
50 700 Td
(Hello, World!) Tj
ET
endstream
endobj
Διαβάστε τις αναφορές και η δομή αναδύεται. Το Αντικείμενο 1, το catalog, δείχνει την καταχώρηση /Pages στο αντικείμενο 2. Το Αντικείμενο 2, το page tree, παραθέτει το αντικείμενο 3 στο /Kids και δηλώνει /Count 1. Το Αντικείμενο 3, η σελίδα (page), δείχνει το /Parent πίσω πάνω στο αντικείμενο 2 (το δέντρο και η σελίδα αναφέρονται το ένα στο άλλο, το οποίο απαιτείται), ορίζει το μέγεθός του με το /MediaBox, εκθέτει τη γραμματοσειρά με το τοπικό όνομα /F0 στο /Resources του, και ονομάζει το αντικείμενο 5 ως περιεχόμενό του (content). Το Αντικείμενο 4 είναι η γραμματοσειρά (font): το /BaseFont /Helvetica επιλέγει μία από τις 14 standard γραμματοσειρές που έχει ήδη κάθε συμβατός αναγνώστης, οπότε δεν υπάρχει τίποτα για ενσωμάτωση. Το Αντικείμενο 5 είναι το content stream
Τι λέει πραγματικά το content stream
Το σώμα του stream είναι ένα μικροσκοπικό πρόγραμμα στη γλώσσα περιγραφής σελίδας του PDF, η οποία είναι postfix: οι τελεσταίοι (operands) έρχονται πρώτοι, στη συνέχεια ο τελεστής (operator) που τους καταναλώνει. Πέντε γραμμές κάνουν τη δουλειά. Τα BT και ET ανοίγουν και κλείνουν ένα αντικείμενο κειμένου. Οτιδήποτε τοποθετεί ή εμφανίζει κείμενο πρέπει να βρίσκεται ανάμεσά τους. Το /F0 36 Tf ορίζει την τρέχουσα γραμματοσειρά στον πόρο με το όνομα /F0 στα 36 σημεία (το Tf είναι "ορισμός γραμματοσειράς και μεγέθους κειμένου"). Το 50 700 Td μετακινεί τη θέση του κειμένου στο (50, 700) σε συντεταγμένες σελίδας. Το (Hello, World!) Tj εμφανίζει τη συμβολοσειρά (string), την οποία το PDF γράφει ως literal κείμενο σε παρενθέσεις, χρησιμοποιώντας το Tj για να τη ζωγραφίσει στην τρέχουσα θέση. Αφήστε έξω τα BT/ET και ένας αυστηρός αναγνώστης απορρίπτει τους τελεστές κειμένου. Ξεχάστε να ορίσετε μια γραμματοσειρά πριν από το Tj και δεν υπάρχει τρέχουσα γραμματοσειρά για σχεδίαση
Το /Length 44 στο λεξικό του stream είναι ο αριθμός των bytes μεταξύ του stream και του endstream, και πρέπει να είναι ακριβής. Αυτή είναι η τιμή που αξίζει να παραδώσετε σε ένα εργαλείο αντί να μετράτε τις αλλαγές γραμμής (newlines) με το χέρι, ειδικά επειδή το αν ο επεξεργαστής σας γράφει τα τέλη γραμμής ως LF ή CRLF αλλάζει το σύνολο
Κεφαλίδα, xref και trailer
Τα αντικείμενα είναι το περιεχόμενο. Τρία δομικά κομμάτια τα μετατρέπουν σε αρχείο. Το πρώτο είναι η κεφαλίδα, η πρώτη γραμμή, που ονομάζει τη μορφή και την έκδοση:
%PDF-1.7
Το % ξεκινά ένα σχόλιο στη σύνταξη του PDF, αλλά ένας αναγνώστης αντιμετωπίζει αυτό το συγκεκριμένο σχόλιο ως υπογραφή της μορφής και διαβάζει την έκδοση από αυτό. Ένας πραγματικός writer το ακολουθεί αμέσως με μια δεύτερη γραμμή σχολίου από bytes υψηλών bit (high-bit bytes), μια υπόδειξη στα εργαλεία μεταφοράς αρχείων ότι το αρχείο είναι δυαδικό (binary) και δεν πρέπει να παραμορφωθεί ως κείμενο
Στο τέλος του αρχείου έρχεται ο πίνακας cross-reference, το ευρετήριο που καθιστά δυνατή την τυχαία πρόσβαση. Καταγράφει το byte offset κάθε αντικειμένου από την αρχή του αρχείου, ώστε ένας αναγνώστης να μπορεί να αναζητήσει κατευθείαν το αντικείμενο 3 χωρίς να αναλύσει τα αντικείμενα 1 και 2 πρώτα. Ο πίνακας είναι άκαμπτος: οι καταχωρήσεις έχουν σταθερό πλάτος, 20 bytes η καθεμία, συμπεριλαμβανομένου του τέλους γραμμής, μορφοποιημένες ως ένα 10-ψήφιο offset, μια 5-ψήφια γενιά, μια λέξη-κλειδί (n για in-use (σε χρήση), f για free (ελεύθερο)), και έναν τερματιστή δύο byte. Ένας σωστός πίνακας για τις έξι καταχωρήσεις μας (το αντικείμενο 0 είναι πάντα η κεφαλή της ελεύθερης λίστας (free-list head)) μοιάζει με αυτό:
xref
0 6
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000235 00000 n
0000000308 00000 n
trailer
<< /Size 6
/Root 1 0 R
>>
startxref
408
%%EOF
Αυτά τα offsets είναι το εύθραυστο μέρος της γραφής ενός PDF με το χέρι. Το καθένα είναι η ακριβής θέση byte όπου αρχίζει το αντίστοιχο N 0 obj, και κάθε offset μετατοπίζεται τη στιγμή που προσθέτετε έναν χαρακτήρα οπουδήποτε από πάνω του. Το trailer είναι το σημείο εισόδου που χρησιμοποιεί ένας αναγνώστης τελευταίο και πρώτο: το /Root 1 0 R ονομάζει το catalog, το /Size 6 δηλώνει το πλήθος των αντικειμένων και το startxref 408 δίνει το byte offset της ίδιας της λέξης xref. Ένας αναγνώστης ανοίγει το αρχείο, πηδάει στο τέλος, διαβάζει το startxref, αναζητά τον πίνακα cross-reference και από εκεί φτάνει στο catalog και σε οτιδήποτε βρίσκεται κάτω από αυτό. Το %%EOF επισημαίνει το τελευταίο byte
Αφήστε ένα εργαλείο να διορθώσει τις μετρήσεις των bytes
Τα παραπάνω offsets είναι ενδεικτικά. Στην πράξη θα είναι λάθος μέχρι να τελειώσετε την πληκτρολόγηση, επειδή εξαρτώνται από την ακριβή διάταξη των byte του αρχείου σας. Αντί να τα υπολογίσετε ξανά, γράψτε τη δομή με τιμές placeholder (δεσμευτικά θέσης) και αφήστε ένα βοηθητικό πρόγραμμα να δημιουργήσει ξανά τον πίνακα cross-reference και τα μήκη των stream. Το δωρεάν, διαπλατφορμικό pdftk το κάνει αυτό με ένα πέρασμα:
pdftk hello-draft.pdf output hello.pdf
Αναλύει τα αντικείμενά σας, υπολογίζει ξανά κάθε byte offset, συμπληρώνει τις σωστές τιμές /Length, γράφει έναν έγκυρο πίνακα xref και trailer, και παράγει το hello.pdf. Ανοίξτε το σε οποιοδήποτε πρόγραμμα προβολής και παίρνετε μία σελίδα με "Hello, World!" σε Helvetica 36-σημείων κοντά στην κορυφή. Το Qpdf κάνει την ίδια δουλειά, και πολλά προγράμματα προβολής θα επιδιορθώσουν επίσης ένα ελαφρώς κακοσχηματισμένο αρχείο εν κινήσει. Ο λόγος που βασιζόμαστε σε ένα εργαλείο εδώ δεν είναι η τεμπελιά. Είναι ότι η αριθμητική των offset είναι το μόνο μέρος της μορφής με μηδενικό εννοιολογικό περιεχόμενο και το υψηλότερο ποσοστό σφαλμάτων, οπότε η αυτοματοποίησή της αφήνει τη δομή να παραμείνει το πράγμα που μαθαίνετε
Γιατί αυτό κλιμακώνεται σε πραγματικά έγγραφα
Τίποτα σχετικά με μια αναφορά εκατό σελίδων δεν αλλάζει το σχήμα που μόλις κατασκευάσατε. Το catalog εξακολουθεί να κάθεται στη ρίζα, το page tree εξακολουθεί να συγκεντρώνει τις σελίδες, και κάθε σελίδα εξακολουθεί να δείχνει στους πόρους της και σε ένα content stream. Αυτό που αυξάνεται είναι το εύρος, όχι η σπονδυλική στήλη: το page tree διακλαδίζεται έτσι ώστε ένας αναγνώστης να μπορεί να παραλείψει ολόκληρα υποδέντρα (subtrees), τα content streams φέρουν εκατοντάδες τελεστές αντί για πέντε, οι γραμματοσειρές ενσωματώνονται ως δικά τους αντικείμενα stream με πίνακες πλάτους και κωδικοποιήσεις, και οι εικόνες φτάνουν ως streams με φίλτρα ειδικά για εικόνες. Τα σύγχρονα αρχεία τείνουν επίσης να συσκευάζουν πολλά αντικείμενα σε συμπιεσμένα object streams και να αντικαθιστούν τον απλό πίνακα xref με ένα cross-reference stream, πράγμα που εξηγεί γιατί το άνοιγμα ενός πραγματικού PDF σε έναν επεξεργαστή κειμένου δείχνει συνήθως έναν τοίχο από δυαδικά (binary) δεδομένα. Το μοντέλο από κάτω είναι πανομοιότυπο με αυτό στο χειροποίητο αρχείο σας. Για το ευρύτερο γράφημα αντικειμένων και πώς το catalog, το page tree και τα λεξικά πόρων σχετίζονται σε ένα μεγαλύτερο έγγραφο, η εμπεριστατωμένη περιήγηση στη δομή του εγγράφου PDF συνεχίζει από εκεί που σταματά αυτό, και η επισκόπηση της δομής του αρχείου καλύπτει τις αυξητικές ενημερώσεις (incremental updates) και το πώς το trailer συνδέεται αλυσιδωτά μεταξύ των αναθεωρήσεων
Από τη χειρόγραφη γραφή σε μια βιβλιοθήκη
Η πληκτρολόγηση αντικειμένων με το χέρι είναι μια άσκηση μάθησης, όχι μια τεχνική παραγωγής. Τη στιγμή που χρειάζεστε πραγματικές γραμματοσειρές, αναδιπλούμενο (wrapped) κείμενο, εικόνες ή περισσότερα από μια ασήμαντη σελίδα, η τήρηση των bytes που επιδιόρθωσε το pdftk για εσάς γίνεται ολόκληρη η δουλειά, και θέλετε μια βιβλιοθήκη που να την αναλαμβάνει. Τα ίδια πέντε αντικείμενα εξακολουθούν να γράφονται, αλλά μια βιβλιοθήκη υπολογίζει κάθε offset, διαχειρίζεται τα λεξικά γραμματοσειρών και πόρων, και συμπιέζει τα content streams χωρίς να παρακολουθείτε ούτε ένα byte. Στο Delphi και στο C++Builder, το HotPDF Component μειώνει ολόκληρο αυτό το αρχείο σε μια χούφτα κλήσεις: ρυθμίστε το έγγραφο, καλέστε BeginDoc, SetFont και TextOut για να τοποθετήσετε τον ίδιο χαιρετισμό, στη συνέχεια EndDoc για να γράψετε ένα σωστό catalog, page tree, xref και trailer. Η κατανόηση των αντικειμένων από κάτω είναι αυτό που σας επιτρέπει να συλλογιστείτε για την έξοδο όταν ένα έγγραφο δεν αποδίδεται με τον τρόπο που περιμένατε