Γράφετε έναν μικρό επικυρωτή (validator). Ανοίγει ένα PDF, αναζητά το τέλος, βρίσκει το startxref, διαβάζει το offset και περιμένει να προσγειωθεί στη λέξη-κλειδί xref με έναν πίνακα διασταυρούμενων αναφορών σταθερού πλάτους (fixed-width cross-reference table) από κάτω. Από αυτόν τον πίνακα συλλέγει τα offset των αντικειμένων, έπειτα σαρώνει προς τα πίσω για τη λέξη-κλειδί trailer για να μάθει τα /Root και /Size. Λειτουργεί τέλεια σε κάθε αρχείο που δημιουργήσατε για να το δοκιμάσετε. Στη συνέχεια φτάνει ένα αρχείο που παράγεται από μια τρέχουσα έκδοση του Word, ή από μια βιβλιοθήκη που στοχεύει σε PDF 1.5, και ο επικυρωτής το δηλώνει κατεστραμμένο. Δεν υπάρχει λέξη-κλειδί xref εκεί που δείχνει το offset, κανένα λεξικό trailer πουθενά, και ο πίνακας αντικειμένων που έφτιαξε ο επικυρωτής είναι σχεδόν άδειος. Το αρχείο είναι έγκυρο. Ο επικυρωτής το διαβάζει μέσα από έναν φακό δεκαπέντε ετών
Αυτός είναι ο πιο συνηθισμένος λόγος για τον οποίο ένας έλεγχος PDF σε επίπεδο byte που είναι γραμμένος για την κλασική διάταξη αποτυγχάνει σε σύγχρονα έγγραφα. Η δομή από την οποία εξαρτάται, ο πίνακας διασταυρούμενων αναφορών απλού κειμένου και η λέξη-κλειδί trailer, έγιναν προαιρετικά στο PDF 1.5 και συχνά απουσιάζουν. Αντικαταστάθηκαν από δύο χαρακτηριστικά: τη ροή διασταυρούμενων αναφορών (cross-reference stream) και τη συμπιεσμένη ροή αντικειμένων (compressed object stream). Και τα δύο περιγράφονται στο ISO 32000-1, και ένας επικυρωτής που δεν τα γνωρίζει βλέπει ένα υγιές αρχείο ως έναν σωρό αντικειμένων που λείπουν
Τι άλλαξε το PDF 1.5 σχετικά με την ουρά (tail) του αρχείου
Το ISO 32000-1 §7.5.8 ορίζει τη ροή διασταυρούμενων αναφορών, και το §7.5.7 ορίζει τη ροή αντικειμένων τύπου /ObjStm. Μαζί επιτρέπουν σε έναν εγγραφέα (writer) να παραλείψει τις δύο δομές στις οποίες βασίζεται ένας κλασικός αναλυτής (parser). Ένα αρχείο PDF 1.5 μπορεί να τελειώνει χωρίς κανέναν πίνακα xref. Στη θέση του, το αντικείμενο στο οποίο δείχνει το startxref είναι ένα συνηθισμένο αντικείμενο ροής (stream object) του οποίου το λεξικό φέρει /Type /XRef, και αυτή η ροή κρατά τα δεδομένα διασταυρούμενων αναφορών σε μια συμπαγή δυαδική (binary) μορφή. Δεν υπάρχει ούτε λέξη-κλειδί trailer, επειδή το trailer είναι πλέον το ίδιο το λεξικό της ροής. Τα κλειδιά που κυνηγούσε ένας κλασικός αναλυτής, /Root, /Size και /ID, ζουν μέσα σε αυτό το λεξικό
Η δεύτερη αλλαγή μετακινεί τα ίδια τα αντικείμενα. Αντί να γράφει κάθε έμμεσο αντικείμενο στο δικό του offset των byte, ένας εγγραφέας (writer) μπορεί να πακετάρει πολλά μικρά αντικείμενα, τα λεξικά σελίδων, τα λεξικά σχολιασμών, το δέντρο δομής, σε μια ενιαία ροή αντικειμένων και να συμπιέσει ολόκληρο το δοχείο με Flate. Τα μεμονωμένα αντικείμενα δεν έχουν πλέον offset byte στο αρχείο. Έχουν μια θέση μέσα σε ένα συμπιεσμένο blob. Ένας επικυρωτής (validator) που σαρώνει τα ακατέργαστα (raw) byte για 1 0 obj δεν τα βρίσκει ποτέ, επειδή αυτό το κείμενο υπάρχει μόνο μετά την αποσυμπίεση (inflation). Για έναν κλασικό αναλυτή, το μισό έγγραφο έχει απλώς εξαφανιστεί
Τα κλειδιά του trailer είναι απλό κείμενο, ακόμη και σε ένα συμπιεσμένο αρχείο
Το καθησυχαστικό μέρος είναι ότι η ανάγνωση του trailer μιας ροής διασταυρούμενων αναφορών δεν απαιτεί την αποσυμπίεση κανενός πράγματος. Ένα αντικείμενο ροής (stream object) γράφεται ως ένα λεξικό ακολουθούμενο από τη λέξη-κλειδί stream και έπειτα τα συμπιεσμένα byte. Το λεξικό είναι απλό κείμενο. Έτσι, όταν το startxref δείχνει σε μια ροή διασταυρούμενων αναφορών, τα byte αμέσως μετά τον αριθμό του αντικειμένου μοιάζουν με ένα συνηθισμένο λεξικό, και τα /Root, /Size και /ID κάθονται εκεί καθαρά, πριν ξεκινήσει η λέξη-κλειδί stream και τα δεδομένα Flate
Αυτό σημαίνει ότι ένας επικυρωτής (validator) μπορεί να μάθει τα τρία στοιχεία που χρειάζεται περισσότερο: πού βρίσκεται ο κατάλογος, πόσα αντικείμενα ισχυρίζεται το αρχείο και το αναγνωριστικό του αρχείου (file identifier), αναλύοντας μόνο το λεξικό της ροής. Δεν χρειάζεται να αποσυμπιέσει τα δεδομένα των διασταυρούμενων αναφορών, και δεν χρειάζεται να ερμηνεύσει τις δυαδικές εγγραφές μέσα σε αυτό. Η δουλειά που νικά έναν αφελή αναλυτή (parser) δεν είναι η ανάγνωση του trailer· είναι η εύρεση των αντικειμένων. Πρόκειται για δύο διαχωρίσιμα προβλήματα, και η επίλυση του πρώτου είναι φθηνή
Ροές αντικειμένων (Object streams): μια κεφαλίδα, έπειτα ένα blob Flate
Μια ροή αντικειμένων είναι ένα δοχείο. Το λεξικό της φέρει /Type /ObjStm, μια καταχώρηση /N που δίνει τον αριθμό των αντικειμένων που είναι πακεταρισμένα μέσα, και μια καταχώρηση /First που δίνει το offset των byte, εντός των αποσυμπιεσμένων δεδομένων, όπου ξεκινά το σώμα του πρώτου αντικειμένου. Το συμπιεσμένο ωφέλιμο φορτίο, μόλις αποσυμπιεστεί, ξεκινά με μια μικρή κεφαλίδα ζευγών ακεραίων /N. Κάθε ζεύγος είναι ένας αριθμός αντικειμένου και το offset του σώματος αυτού του αντικειμένου σε σχέση με το /First. Μετά την κεφαλίδα έρχονται τα ίδια τα σώματα των αντικειμένων, συνενωμένα (concatenated)
Η ανάπτυξη μιας ροής είναι μηχανική διαδικασία μόλις τα byte αποσυμπιεστούν. Διαβάζετε το λεξικό για να πάρετε τα /N και /First, αποσυμπιέζετε (inflate) τη ροή με έναν αποκωδικοποιητή Flate, διατρέχετε τα αρχικά ζεύγη /N για να μάθετε ποιος αριθμός αντικειμένου ζει σε ποιο offset, και στη συνέχεια ανασύρετε κάθε σώμα σαν να ήταν ένα συνηθισμένο έμμεσο αντικείμενο. Η μόνη πραγματική εξάρτηση είναι ο αποκωδικοποιητής Flate, και έχετε ήδη έναν: το Delphi παρέχει το System.ZLib, και το Free Pascal παρέχει τη μονάδα zstream, τα οποία και τα δύο τυλίγουν (wrap) τη zlib και αποσυμπιέζουν μια ακατέργαστη ροή Flate χωρίς κώδικα τρίτων. Μια ρουτίνα που προσαρτά κάθε εξαγόμενο αντικείμενο στον πίνακα αντικειμένων του επικυρωτή (validator) κάνει τον υπόλοιπο επικυρωτή, το μέρος που διατρέχει το /Root και ελέγχει το δέντρο σελίδων, να συμπεριφέρεται ακριβώς όπως θα έκανε σε ένα κλασικό αρχείο
Τι δεν χρειάζεται να υλοποιήσετε
Είναι εύκολο να υπερεκτιμήσετε τον όγκο της εργασίας. Η ανάγνωση των κλειδιών του trailer από ένα συμπιεσμένο αρχείο δεν απαιτεί την αποκωδικοποίηση των δυαδικών εγγραφών της ροής των διασταυρούμενων αναφορών (cross-reference stream). Η ροή διασταυρούμενων αναφορών της §7.5.8 χρησιμοποιεί τρεις τύπους εγγραφής, και η εγγραφή τύπου 2, αυτή που λέει αυτό το αντικείμενο ζει μέσα στη ροή αντικειμένων N στο ευρετήριο i
, είναι αυτό που θα αποκωδικοποιούσατε για να δημιουργήσετε έναν πλήρη χάρτη offset. Χρειάζεστε αυτόν τον χάρτη για την επίλυση (resolve) αυθαίρετων αντικειμένων βάσει αριθμού. Δεν τον χρειάζεστε για να διαβάσετε τα /Root, /Size και /ID, τα οποία βρίσκονται στο λεξικό απλού κειμένου, και δεν τον χρειάζεστε για να αναπτύξετε τις ροές αντικειμένων, επειδή κάθε /ObjStm ανακοινώνει τα δικά του περιεχόμενα μέσω των /N και /First
Επίσης, δεν χρειάζεται να χειριστείτε τις συναρτήσεις πρόβλεψης (predictor functions) PNG και TIFF που μπορεί να εφαρμόσει μια ροή διασταυρούμενων αναφορών μέσω των /DecodeParms της, μόνο και μόνο για να πάρετε τα κλειδιά του trailer. Οι προβλέψεις φιλτράρουν τις δυαδικές γραμμές διασταυρούμενων αναφορών για να τις κάνουν να συμπιέζονται καλύτερα· δεν έχουν καμία σχέση με το λεξικό που προηγείται της ροής. Επομένως, η ελάχιστη αναβάθμιση που κάνει έναν κλασικό επικυρωτή (validator) να έχει επίγνωση των σύγχρονων PDF είναι μικρή: όταν το startxref προσγειώνεται σε μια ροή αντί για τη λέξη-κλειδί xref, αναλύστε το λεξικό της ροής για τα κλειδιά του trailer και αναπτύξτε τυχόν αντικείμενα /ObjStm που συναντάτε, ώστε τα περιεχόμενά τους να εισαχθούν στον πίνακα αντικειμένων. Η αποκωδικοποίηση των εγγραφών τύπου 2 και των προβλέψεων (predictors) είναι μια ξεχωριστή, μεγαλύτερη εργασία που μπορείτε να αναβάλετε έως ότου χρειαστείτε πραγματικά την τυχαία επίλυση αντικειμένων (random object resolution)
Γιατί ένας έλεγχος συμμόρφωσης (compliance check) πρέπει να αναπτύσσει τις ροές πρώτα
Αυτό παύει να είναι θεωρητικό (academic) τη στιγμή που τρέχετε έναν έλεγχο προφίλ (profile check). Ένας επικυρωτής PDF/A ή PDF/X επιθεωρεί συγκεκριμένα αντικείμενα: τον κατάλογο του εγγράφου για έναν πίνακα /OutputIntents, τη ροή /Metadata για ένα πακέτο XMP με το σωστό αναγνωριστικό, κάθε περιγραφέα (descriptor) γραμματοσειράς για ένα ενσωματωμένο αρχείο γραμματοσειράς, το trailer για ένα /ID. Σε ένα συμπιεσμένο αρχείο, τα περισσότερα από αυτά τα αντικείμενα βρίσκονται μέσα σε ροές αντικειμένων (object streams). Ένας επικυρωτής που δεν έχει αναπτύξει τις ροές αντικειμένων δεν μπορεί να δει τα κλειδιά του καταλόγου, δεν μπορεί να βρει τα μεταδεδομένα, και δεν μπορεί να απαριθμήσει τις γραμματοσειρές. Θα αναφέρει ένα απόλυτα συμμορφούμενο έγγραφο (perfectly conformant document) ότι του λείπει το output intent, του λείπει το XMP, και του λείπει η μισή δομή του, επειδή τα στοιχεία που χρειάζεται εξακολουθούν να βρίσκονται σε ένα blob Flate που δεν αποσυμπίεσε ποτέ
Η σειρά έχει σημασία. Η ανάπτυξη πρέπει να συμβεί πριν τρέξουν οι έλεγχοι, όχι παράλληλα με αυτούς, επειδή κάθε έλεγχος υποθέτει ότι μπορεί να φτάσει σε ένα αντικείμενο βάσει του αριθμού του. Εάν συνδέσετε (wire) έναν έλεγχο προφίλ απευθείας σε μια ακατέργαστη (raw) σάρωση των byte, κληρονομεί την τύφλωση του κλασικού αναλυτή και παράγει ψευδείς παραβιάσεις ακριβώς στα σύγχρονα αρχεία που είναι πιο πιθανό να είναι καλοσχηματισμένα (well formed), αφού προήλθαν από toolchains αρκετά νέα ώστε να γράφουν ροές διασταυρούμενων αναφορών εξαρχής
Αφήνοντας το PDFium να κάνει την ανάλυση (parsing) για εσάς
Το PDFium Component αναλύει τις ροές διασταυρούμενων αναφορών και τις ροές αντικειμένων ως μέρος της φόρτωσης ενός εγγράφου, που είναι ο πρακτικός τρόπος για να αποφύγετε τη χειροκίνητη (hand-rolling) υλοποίηση του βήματος της αποσυμπίεσης και της ανάπτυξης. Όταν φορτώνετε ένα αρχείο με το εξάρτημα TPdf, τα αντικείμενα που είναι πακεταρισμένα σε δοχεία /ObjStm έχουν ήδη επιλυθεί (resolved) και τα σημεία εισόδου (entry points) της επικύρωσης βλέπουν το πλήρως ανεπτυγμένο έγγραφο. Το ValidatePdfA επιστρέφει μια εγγραφή TPdfAValidationResult της οποίας το πεδίο Conformance είναι μια τιμή TPdfAConformance όπως pac1b ή pacNone, το πεδίο της Issues είναι ένα σύνολο των συγκεκριμένων προβλημάτων που βρέθηκαν, και η μέθοδός της IsCompliant είναι αληθής (true) μόνο όταν ανιχνεύτηκε ένα επίπεδο συμμόρφωσης και το σύνολο των προβλημάτων είναι κενό. Επειδή τα αντικείμενα αναπτύχθηκαν κατά τη φόρτωση, ένας πίνακας /OutputIntents ή μια ενσωματωμένη γραμματοσειρά που ζούσε μέσα σε μια ροή αντικειμένων βρίσκεται, και δεν αναφέρεται ότι λείπει
uses
PDFium, FPdfPdfa;
function CheckPdfA(const FileName: string): TPdfAValidationResult;
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True; // parses xref/object streams on load
Result := Pdf.ValidatePdfA; // sees the expanded object table
finally
Pdf.Free;
end;
end;
Το ίδιο ισχύει για το ValidatePdfX, το οποίο επιστρέφει ένα TPdfXValidationResult με την ίδια μορφή. Το νόημα της δρομολόγησης μέσω του PDFium είναι ότι η δομική αποσυμπίεση που περιγράφεται παραπάνω συμβαίνει μία φορά, σωστά, μέσα στον φορτωτή (loader), ώστε ο κώδικας επικύρωσης σας να μην βλέπει ποτέ τη διαφορά μεταξύ ενός κλασικού αρχείου και ενός πλήρως συμπιεσμένου. Και τα δύο φτάνουν στον επικυρωτή ως ένα επιλυμένο σύνολο αντικειμένων
function PdfXConformanceName(C: TPdfXConformance): string;
begin
case C of
pxc1a: Result := 'PDF/X-1a';
pxc3 : Result := 'PDF/X-3';
pxc4 : Result := 'PDF/X-4';
else
Result := 'none';
end;
end;
var
Pdf: TPdf;
R : TPdfXValidationResult;
Issue: TPdfXValidationIssue;
IssueCount: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'Press_Ready.pdf';
Pdf.Active := True;
R := Pdf.ValidatePdfX;
if R.IsCompliant then
Writeln('PDF/X conformance: ', PdfXConformanceName(R.Conformance))
else
begin
IssueCount := 0;
for Issue in R.Issues do // Issues is a set: count its members
Inc(IssueCount);
Writeln('Not conformant; issue count = ', IssueCount);
end;
finally
Pdf.Free;
end;
end;
Αν τα byte βρίσκονται ήδη στη μνήμη αντί για τον δίσκο, η ίδια ακολουθία φόρτωσης-έπειτα-επικύρωσης λειτουργεί μέσω της υπερφόρτωσης (overload) LoadDocument(const Data: TBytes), η οποία παίρνει το ακατέργαστο περιεχόμενο του αρχείου και αναλύει τις ροές των διασταυρούμενων αναφορών και των αντικειμένων του με τον ίδιο τρόπο που κάνει και η διαδρομή (path) του αρχείου. Το συμπέρασμα για έναν χειρόγραφο επικυρωτή είναι ο δομικός κανόνας, όχι το API: διαβάστε τα κλειδιά του trailer από το λεξικό της ροής σε απλό κείμενο, αναπτύξτε κάθε /ObjStm με έναν αποκωδικοποιητή Flate πριν διατρέξετε το έγγραφο, και χειριστείτε την αποκωδικοποίηση των δυαδικών εγγραφών (binary entries) των διασταυρούμενων αναφορών ως τη μεγαλύτερη, προαιρετική δουλειά που είναι
Μόλις αναπτυχθεί η δομή, ένας επικυρωτής (validator) μπορεί να οδηγήσει την υπόλοιπη ροή εργασίας (workflow) πάνω σε αυτήν. Για ένα preflight εργαλείο γραμμής εντολών που αναφέρει τη συμμόρφωση σε έναν φάκελο εισόδων (inputs), ανατρέξτε στην αναλυτική μας παρουσίαση για τη δημιουργία ενός CLI αναφοράς preflight παρτίδας (batch preflight report CLI). Όταν η επικύρωση είναι μια πύλη πριν από τον διαχωρισμό ενός μεγάλου εγγράφου, οι τεχνικές στον οδηγό μας για τον διαχωρισμό εγγράφων PDF σε πολλαπλά αρχεία ταιριάζουν φυσικά με το μοτίβο φόρτωσης-και-ελέγχου που φαίνεται εδώ. Και τα δύο βασίζονται στην επιφάνεια φόρτωσης και επικύρωσης του PDFium Component για Delphi και C++Builder