Γράφετε έναν μικρό επικυρωτή (validator). Ανοίγει ένα PDF, αναζητά το τέλος, βρίσκει το startxref, διαβάζει τη μετατόπιση (offset) και αναμένει να προσγειωθεί στη λέξη-κλειδί xref με έναν πίνακα παραπομπών σταθερού πλάτους από κάτω. Από αυτόν τον πίνακα συλλέγει τις μετατοπίσεις αντικειμένων, και στη συνέχεια σαρώνει προς τα πίσω για τη λέξη-κλειδί trailer για να μάθει τα /Root και /Size. Λειτουργεί τέλεια σε κάθε αρχείο που δημιουργήσατε για να το δοκιμάσετε. Στη συνέχεια, φτάνει ένα αρχείο που παρήχθη από μια τρέχουσα έκδοση του Word, ή από μια βιβλιοθήκη που στοχεύει στο PDF 1.5, και ο επικυρωτής το δηλώνει κατεστραμμένο. Δεν υπάρχει λέξη-κλειδί xref εκεί που δείχνει η μετατόπιση, κανένα λεξικό trailer πουθενά, και ο πίνακας αντικειμένων που κατασκεύασε ο επικυρωτής είναι σχεδόν άδειος. Το αρχείο είναι έγκυρο. Ο επικυρωτής το διαβάζει μέσα από έναν φακό δεκαπέντε ετών.
Αυτός είναι ο πιο κοινός λόγος που ένας έλεγχος PDF σε επίπεδο byte γραμμένος για την κλασική διάταξη αποτυγχάνει σε σύγχρονα έγγραφα. Η δομή από την οποία εξαρτάται, ο plaintext πίνακας παραπομπών και η λέξη-κλειδί trailer, έγιναν προαιρετικά στο PDF 1.5 και συχνά απουσιάζουν. Δύο χαρακτηριστικά τα αντικατέστησαν: η ροή παραπομπών (cross-reference stream) και η συμπιεσμένη ροή αντικειμένων (compressed object stream). Και τα δύο περιγράφονται στο ISO 32000-1, και ένας επικυρωτής που δεν τα γνωρίζει βλέπει ένα υγιές αρχείο ως ένα σωρό από ελλείποντα αντικείμενα.
Τι άλλαξε το PDF 1.5 σχετικά με το τέλος του αρχείου
Το ISO 32000-1 §7.5.8 ορίζει τη ροή παραπομπών (cross-reference stream), και η §7.5.7 ορίζει τη ροή αντικειμένων (object stream) τύπου /ObjStm. Μαζί επιτρέπουν σε έναν writer να παραλείψει τις δύο δομές στις οποίες βασίζεται ένας κλασικός parser. Ένα αρχείο PDF 1.5 μπορεί να τελειώνει χωρίς καθόλου πίνακα xref. Στη θέση του, το αντικείμενο στο οποίο δείχνει το startxref είναι ένα κοινό αντικείμενο ροής του οποίου το λεξικό φέρει το /Type /XRef, και αυτή η ροή περιέχει τα δεδομένα παραπομπών σε μια συμπαγή δυαδική μορφή. Δεν υπάρχει ούτε λέξη-κλειδί trailer, επειδή το trailer είναι πλέον το ίδιο το λεξικό της ροής. Τα κλειδιά που αναζητούσε ένας κλασικός parser, τα /Root, /Size και /ID, ζουν μέσα σε αυτό το λεξικό.
Η δεύτερη αλλαγή μετακινεί τα ίδια τα αντικείμενα. Αντί να γράφει κάθε έμμεσο αντικείμενο (indirect object) στη δική του μετατόπιση byte, ένας writer μπορεί να πακετάρει πολλά μικρά αντικείμενα, τα λεξικά σελίδων, τα λεξικά σημειώσεων, το δέντρο δομής, σε μια ενιαία ροή αντικειμένων και να συμπιέσει ολόκληρο το κοντέινερ με Flate. Τα μεμονωμένα αντικείμενα δεν έχουν πλέον μετατόπιση byte στο αρχείο. Ένα συμπιεσμένο ωφέλιμο φορτίο, μόλις αποσυμπιεστεί, ξεκινά με μια μικρή κεφαλίδα από /N ζεύγη ακέραιων αριθμών. Ένας επικυρωτής που σαρώνει τα ακατέργαστα byte για 1 0 obj δεν τα βρίσκει ποτέ, επειδή αυτό το κείμενο υπάρχει μόνο μετά τον αποπληθωρισμό (inflation). Για έναν κλασικό parser, το μισό έγγραφο έχει απλώς εξαφανιστεί.
Τα κλειδιά του trailer είναι plaintext, ακόμη και σε συμπιεσμένο αρχείο
Ένα αντικείμενο ροής γράφεται ως λεξικό ακολουθούμενο από τη λέξη-κλειδί stream και στη συνέχεια τα συμπιεσμένα byte. Το λεξικό είναι plaintext. Έτσι, όταν το startxref δείχνει σε μια ροή παραπομπών, τα byte αμέσως μετά τον αριθμό του αντικειμένου μοιάζουν με κοινό λεξικό, και τα /Root, /Size και /ID βρίσκονται εκεί καθαρά, πριν ξεκινήσουν η λέξη-κλειδί stream και τα δεδομένα Flate.
Αυτό σημαίνει ότι ένας επικυρωτής μπορεί να μάθει τα τρία στοιχεία που χρειάζεται περισσότερο, πού βρίσκεται ο κατάλογος, πόσα αντικείμενα ισχυρίζεται ότι έχει το αρχείο και το αναγνωριστικό αρχείου, αναλύοντας μόνο το λεξικό της ροής. Δεν χρειάζεται να αποσυμπιέσει τα δεδομένα παραπομπών, ούτε να ερμηνεύσει τις δυαδικές καταχωρίσεις μέσα σε αυτά. Η εργασία που νικά έναν απλό parser δεν είναι η ανάγνωση του trailer. Είναι η εύρεση των αντικειμένων. Αυτά είναι δύο ξεχωριστά προβλήματα, και η επίλυση του πρώτου είναι φθηνή.
Ροές αντικειμένων: μια κεφαλίδα, μετά ένα Flate blob
Μια ροή αντικειμένων (object stream) είναι ένα κοντέινερ. Το λεξικό της φέρει το /Type /ObjStm, μια καταχώριση /N που δίνει τον αριθμό των αντικειμένων που είναι συσκευασμένα μέσα, και μια καταχώριση /First που δίνει τη μετατόπιση byte, εντός των αποσυμπιεσμένων δεδομένων, όπου ξεκινά το σώμα του πρώτου αντικειμένου. Το συμπιεσμένο ωφέλιμο φορτίο, μόλις αποσυμπιεστεί, ξεκινά με μια μικρή κεφαλίδα από /N ζεύγη ακέραιων αριθμών. Κάθε ζεύγος είναι ένας αριθμός αντικειμένου και η μετατόπιση του σώματος αυτού του αντικειμένου σε σχέση με το /First. Μετά την κεφαλίδα ακολουθούν τα ίδια τα σώματα των αντικειμένων, συνενωμένα.
Η επέκταση ενός είναι μηχανική μόλις αποσυμπιεστεί τα byte. Διαβάζετε το λεξικό για να λάβετε τα /N και /First, αποσυμπιέζετε τη ροή με έναν Flate decoder, περπατάτε τα αρχικά /N ζεύγη για να μάθετε ποιος αριθμός αντικειμένου ζει σε ποια μετατόπιση, και στη συνέχεια εξάγετε κάθε σώμα σαν να ήταν ένα συνηθισμένο έμμεσο αντικείμενο. Η μόνη πραγματική εξάρτηση είναι ο Flate decoder, και έχετε ήδη έναν: το Delphi αποστέλλει το System.ZLib, και η Free Pascal τη μονάδα zstream, τα οποία τυλίγουν το zlib και αποσυμπιέζουν μια ακατέργαστη ροή Flate χωρίς κώδικα τρίτων. Μια ρουτίνα που προσαρτά κάθε εξαγόμενο αντικείμενο στον πίνακα αντικειμένων του επικυρωτή κάνει τον υπόλοιπο επικυρωτή, το τμήμα που περπατά το /Root και ελέγχει το δέντρο σελίδων, να συμπεριφέρεται ακριβώς όπως θα έκανε σε ένα κλασικό αρχείο.
Τι δεν χρειάζεται να υλοποιήσετε
Είναι εύκολο να υπερεκτιμήσει κανείς την εργασία. Η ανάγνωση των κλειδιών του trailer από ένα συμπιεσμένο αρχείο δεν απαιτεί την αποκωδικοποίηση των δυαδικών καταχωρίσεων της ροής παραπομπών. Η ροή παραπομπών της §7.5.8 χρησιμοποιεί τρεις τύπους καταχωρίσεων, και η καταχώριση τύπου 2, αυτή που λέει αυτό το αντικείμενο ζει μέσα στη ροή αντικειμένων N στο δείκτη i
, είναι αυτή που θα αποκωδικοποιούσατε για να δημιουργήσετε έναν πλήρη χάρτη μετατοπίσεων. Χρειάζεστε αυτόν τον χάρτη για να επιλύσετε αυθαίρετα αντικείμενα κατά αριθμό. Δεν τον χρειάζεστε για να διαβάσετε τα /Root, /Size και /ID, τα οποία βρίσκονται στο plaintext λεξικό, και δεν τον χρειάζεστε για να επεκτείνετε τις ροές αντικειμένων, επειδή κάθε /ObjStm ανακοινώνει τα δικά του περιεχόμενα μέσω των /N και /First.
Επίσης, δεν χρειάζεται να χειριστείτε τις συναρτήσεις πρόβλεψης (predictor functions) PNG και TIFF που μπορεί να εφαρμόσει μια ροή παραπομπών μέσω του /DecodeParms της μόνο και μόνο για να λάβετε τα κλειδιά του trailer. Οι predictors φιλτράρουν τις δυαδικές σειρές παραπομπών για να τις κάνουν να συμπιέζονται καλύτερα. Δεν έχουν καμία σχέση με το λεξικό που προηγείται της ροής. Η ελάχιστη αναβάθμιση που καθιστά έναν κλασικό επικυρωτή ενήμερο για τα σύγχρονα PDF είναι επομένως μικρή: όταν το startxref προσγειώνεται σε μια ροή αντί για τη λέξη-κλειδί xref, αναλύστε το λεξικό της ροής για τα κλειδιά του trailer, και επεκτείνετε τυχόν αντικείμενα /ObjStm που συναντάτε ώστε τα περιεχομένα τους να εισέλθουν στον πίνακα αντικειμένων. Η αποκωδικοποίηση καταχωρίσεων τύπου 2 και predictors είναι μια ξεχωριστή, μεγαλύτερη εργασία που μπορείτε να αναβάλετε μέχρι να χρειαστείτε πραγματικά τυχαία επίλυση αντικειμένων.
Γιατί ένας έλεγχος συμμόρφωσης πρέπει να επεκτείνει τις ροές πρώτα
Αυτό παύει να είναι θεωρητικό τη στιγμή που εκτελείτε έναν έλεγχο προφίλ. Ένας επικυρωτής PDF/A ή PDF/X επιθεωρεί συγκεκριμένα αντικείμενα: τον κατάλογο εγγράφου για έναν πίνακα /OutputIntents, τη ροή /Metadata για ένα πακέτο XMP με το σωστό αναγνωριστικό, κάθε περιγραφέα γραμματοσειράς για ένα ενσωματωμένο αρχείο γραμματοσειράς, το trailer για ένα /ID. Σε ένα συμπιεσμένο αρχείο, τα περισσότερα από αυτά τα αντικείμενα βρίσκονται μέσα σε ροές αντικειμένων. Ένας επικυρωτής που δεν έχει επεκτείνει τις ροές αντικειμένων δεν μπορεί να δει τα κλειδιά του καταλόγου, δεν μπορεί να βρει τα μεταδεδομένα και δεν μπορεί να απαριθμήσει τις γραμματοσειρές. Θα αναφέρει ένα απόλυτα συμμορφούμενο έγγραφο ως ελλιπές σε πρόθεση εξόδου (output intent), XMP και δομή, επειδή τα στοιχεία που χρειάζεται κάθονται ακόμα σε ένα Flate blob που δεν αποσυμπίεσε ποτέ.
Η σειρά έχει σημασία. Η επέκταση πρέπει να συμβεί πριν εκτελεστούν οι έλεγχοι, όχι παράλληλα με αυτούς, επειδή κάθε έλεγχος προϋποθέτει ότι μπορεί να φτάσει σε ένα αντικείμενο κατά αριθμό. Εάν συνδέσετε έναν έλεγχο προφίλ απευθείας σε μια σάρωση ακατέργαστων byte, κληρονομεί την τυφλότητα του κλασικού parser και παράγει ψευδείς παραβιάσεις ακριβώς στα σύγχρονα αρχεία που είναι πιο πιθανό να είναι καλοσχηματισμένα, αφού προήλθαν από εργαλειοθήκες αρκετά νέες ώστε να γράφουν ροές παραπομπών εξ αρχής.
Αφήνοντας το PDFium να κάνει την ανάλυση για εσάς
Το στοιχείο PDFium αναλύει τις ροές παραπομπών και τις ροές αντικειμένων ως μέρος της φόρτωσης ενός εγγράφου, που είναι ο πρακτικός τρόπος για να αποφύγετε τη χειροκίνητη υλοποίηση του βήματος αποσυμπίεσης και επέκτασης. Όταν φορτώνετε ένα αρχείο με το στοιχείο TPdf, τα αντικείμενα που είναι συσκευασμένα σε κοντέινερ /ObjStm έχουν ήδη επιλυθεί, και τα σημεία εισόδου επικύρωσης βλέπουν το πλήρως ανεπτυγμένο έγγραφο. Η ValidatePdfA επιστρέφει μια εγγραφή TPdfAValidationResult της οποίας το πεδίο Conformance είναι μια τιμή TPdfAConformance όπως pac1b ή pacNone, της οποίας το πεδίο Issues είναι ένα σύνολο των συγκεκριμένων προβλημάτων που βρέθηκαν, και της οποίας η μέθοδος IsCompliant είναι αληθής μόνο όταν ανιχνεύθηκε επίπεδο συμμόρφωσης και το σύνολο προβλημάτων είναι άδειο. Επειδή τα αντικείμενα επεκτάθηκαν κατά τη φόρτωση, ένας πίνακας /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, έτσι ώστε ο κώδικας επικύρωσης να μην βλέπει ποτέ τη διαφορά ανάμεσα σε ένα κλασικό αρχείο και ένα πλήρως συμπιεσμένο. Και τα δύο φτάνουν στον επικυρωτή ως ένα επιλυμένο σύνολο αντικειμένων.
var
Pdf: TPdf;
R : TPdfXValidationResult;
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: ', Ord(R.Conformance))
else
Writeln('Not conformant; issue count = ', SizeOf(R.Issues));
finally
Pdf.Free;
end;
end;
Εάν τα byte βρίσκονται ήδη στη μνήμη και όχι στο δίσκο, η ίδια ακολουθία φόρτωσης και στη συνέχεια επικύρωσης λειτουργεί μέσω της υπερφόρτωσης LoadDocument(const Data: TBytes), η οποία λαμβάνει το ακατέργαστο περιεχόμενο του αρχείου και αναλύει τις ροές παραπομπών και αντικειμένων με τον ίδιο τρόπο που κάνει η διαδρομή του αρχείου. Το συμπεράσμα για έναν χειροκίνητο επικυρωτή είναι ο δομικός κανόνας, όχι το API: διαβάστε τα κλειδιά του trailer από το λεξικό της ροής σε plaintext, επεκτείνετε κάθε /ObjStm με έναν Flate decoder πριν περιηγηθείτε στο έγγραφο, και αντιμετωπίστε την αποκωδικοποίηση των δυαδικών καταχωρίσεων παραπομπών ως τη μεγαλύτερη, προαιρετική εργασία που είναι.
Μόλις επεκταθεί η δομή, ένας επικυρωτής μπορεί να οδηγήσει την υπόλοιπη ροή εργασίας πάνω της. Για μια εργαλειοθήκη γραμμής εντολών preflight που αναφέρει τη συμμόρφωση σε έναν φάκελο εισαγωγών, δείτε τον οδηγό μας για τη δημιουργία μιας αναφοράς preflight CLI σε παρτίδες. Όταν η επικύρωση είναι πύλη πριν από το σπάσιμο ενός μεγάλου εγγράφου, οι τεχνικές στον οδηγό μας για το διαχωρισμό εγγράφων PDF σε πολλαπλά αρχεία συνδυάζονται φυσικά με το μοτίβο φόρτωσης και ελέγχου που παρουσιάζεται εδώ. Και τα δύο βασίζονται στην επιφάνεια φόρτωσης και επικύρωσης του PDFium Component για Delphi και C++Builder.