Εξάγετε ένα έγγραφο από το Microsoft Word ή το Excel με το "Αποθήκευση ως PDF" (Save as PDF) και το αρχείο στον δίσκο είναι, τις περισσότερες φορές, ένα αρχείο hybrid-reference (υβριδικής αναφοράς). Φέρει τις πληροφορίες του για τις διασταυρούμενες αναφορές (cross-reference) δύο φορές: μία φορά ως τον κλασικό πίνακα σταθερού πλάτους (fixed-width table) με τον οποίο τελείωνε κάθε PDF μέχρι την έκδοση 1.4, και μία φορά ως μια συμπιεσμένη cross-reference ροή (stream) από την οποία το μεγαλύτερο μέρος του εγγράφου εξαρτάται πραγματικά. Ένα μεμονωμένο κλειδί (key) στο trailer, το /XRefStm, ράβει τις δύο οπτικές (views) μαζί, και το αν ένα εργαλείο "βλέπει" ολόκληρο το έγγραφο εξαρτάται από το αν ακολουθεί αυτό το κλειδί
Αυτό το άρθρο εξετάζει τα hybrid αρχεία από την πλευρά της κατανάλωσης (consuming side): πώς μοιάζουν τα bytes στο τέλος του αρχείου, πώς οι δύο οπτικές (views) αποκλίνουν (drift apart) κατά την επεξεργασία, και πώς μια γραμμή επεξεργασίας (pipeline) στο Delphi μπορεί να εντοπίζει και να δρομολογεί υβριδικές εισόδους. Το πώς ένας φορτωτής (loader) συγχωνεύει (merges) τις δύο οπτικές, και γιατί η σειρά δεν είναι διαπραγματεύσιμη, αποτελεί αντικείμενο του άρθρου μας στο HotPDF για τη φόρτωση αρχείων hybrid-reference· το παρόν αφορά την αναγνώριση της ίδιας της διάταξης (layout) αρχικά
Γιατί οι εξαγωγές του Office γράφουν το ευρετήριο (index) δύο φορές
Το PDF 1.5 εισήγαγε δύο λειτουργίες που άλλαξαν τη μορφή του αρχείου: cross-reference streams, οι οποίες αποθηκεύουν το ευρετήριο αντικειμένων ως συμπιεσμένα δυαδικά (binary) δεδομένα αντί για έναν πίνακα απλού κειμένου, και object streams, οι οποίες πακετάρουν πολλά μικρά αντικείμεπα (objects) σε έναν Flate-συμπιεσμένο περιέκτη (container). Ένας εγγραφέας (writer) που τα χρησιμοποιεί παράγει μικρότερα αρχεία, αλλά ένας αναγνώστης (reader) του PDF 1.4 δεν μπορεί να ανοίξει το αποτέλεσμα, επειδή οι δομές στις οποίες βασίζεται, η λέξη-κλειδί xref και το λεξικό trailer, έχουν εξαφανιστεί
Η ενότητα ISO 32000-1 §7.5.8.4 ορίζει τον συμβιβασμό. Ένα hybrid-reference αρχείο γράφει και τα δύο: έναν κλασικό πίνακα cross-reference που παραπέμπει στα αντικείμενα που ένας παλιός αναγνώστης πρέπει να μπορεί να φτάσει (τον κατάλογο/catalog και το δέντρο σελίδων μεταξύ αυτών), και ένα cross-reference stream που ευρετηριάζει (indexes) όλα τα υπόλοιπα. Αντικείμενα που έχουν διπλωθεί (folded) μέσα σε object streams σημειώνονται ως ελεύθερα (free) στον κλασικό πίνακα, οπότε ένας 1.4 αναγνώστης τα παραλείπει χωρίς παράπονο· οι πραγματικές τους θέσεις υπάρχουν μόνο στο stream. Το κλασικό trailer φέρει στη συνέχεια ένα κλειδί /XRefStm που κρατάει το byte offset (μετατόπιση byte) αυτού του stream. Ένας παλιός προβολέας (viewer) δεν διαβάζει ποτέ το κλειδί και αποδίδει το αρχείο από την οπτική του πίνακα. Ένας σύγχρονος προβολέας το ακολουθεί και βλέπει το πλήρες έγγραφο. Τα Word και Excel εκπέμπουν (emit) ακριβώς αυτή τη διάταξη εδώ και χρόνια, γι' αυτό και τα hybrid αρχεία δεν είναι μια εξωτική (exotic) και σπάνια περίπτωση, αλλά ένα μεγάλο μερίδιο αυτών που παραλαμβάνουν οι επιχειρησιακές ροές (business pipelines)
Πώς μοιάζει η ουρά (tail) ενός hybrid αρχείου
Η διάταξη (layout) είναι πιο εύκολο να γίνει κατανοητή από τα bytes. Εδώ είναι η "ουρά" (tail) ενός μικρού hybrid αρχείου, με συντομευμένες (shortened) μετατοπίσεις· σε μια πραγματική εξαγωγή του Office η τιμή /XRefStm είναι συνήθως μια μεγάλη μετατόπιση (offset) κοντά στο τέλος του αρχείου. Η σειρά ανάγνωσης (reading order) είναι η περιήγηση ξεκινώντας από το τέλος (tail-first walk) που περιγράφεται στην επισκόπησή μας για τη δομή αρχείων PDF: βρείτε το %%EOF, διαβάστε το startxref, πηδήξτε στον πίνακα
% ... body objects, including object streams and, at byte 116,
% the cross-reference stream (a stream object with /Type /XRef) ...
xref % classic section: what startxref points at
0 4
0000000000 65535 f % slot 0: head of the free list, always present
0000000017 00000 n % object 1: the catalog, visible to any reader
0000000000 65535 f % object 2: marked free -- lives in an object stream
0000000000 65535 f % object 3: same; only the stream view locates it
trailer
<<
/Size 4
/Root 1 0 R
/XRefStm 116 % byte offset of the cross-reference stream
>>
startxref
7164 % byte offset of the 'xref' keyword above
%%EOF
Δύο λεπτομέρειες σε αυτή την "αποτύπωση" (dump) μεταφέρουν ολόκληρο τον μηχανισμό. Πρώτον, το startxref δείχνει στην κλασική ενότητα, επίτηδες: αυτή είναι η διεύθυνση στην οποία πρέπει να προσγειωθεί ένας παλιός αναγνώστης (reader). Η cross-reference stream είναι προσβάσιμη (reachable) μόνο μέσω του κλειδιού /XRefStm μέσα στο λεξικό (dictionary) του trailer, επομένως ένας αναλυτής (parser) που ποτέ δεν ψάχνει για αυτό το κλειδί δεν μαθαίνει ποτέ ότι η ροή (stream) υπάρχει. Δεύτερον, τα αντικείμενα 2 και 3 είναι καλοήθεις "μεταμφιέσεις". Ο κλασικός πίνακας τα δηλώνει ως ελεύθερα (free), αλλά πρόκειται για πραγματικά αντικείμενα που κάθονται (sitting) μέσα σε έναν συμπιεσμένο περιέκτη (compressed container)· η σήμανση 'ελεύθερο' (free marking) είναι αυτή που εμποδίζει έναν 1.4 αναγνώστη από το να σκοντάψει πάνω σε καταχωρίσεις (entries) που δεν μπορεί να χρησιμοποιήσει. Ένας καταναλωτής (consumer) που εμπιστεύεται αποκλειστικά την κλασική οπτική (view) καταλήγει στο συμπέρασμα ότι το μεγαλύτερο μέρος αυτού του εγγράφου δεν υπάρχει
Πώς οι δύο οπτικές (views) αποκλίνουν (drift apart)
Ένα hybrid αρχείο φρέσκο βγαλμένο από το Word έχει εσωτερική συνέπεια (internally consistent): και οι δύο όψεις (views) περιγράφουν το ίδιο έγγραφο, η καθεμία εντός του δηλωμένου σκοπού της (scope). Το πρόβλημα ξεκινά όταν το αρχείο υποβάλλεται σε επεξεργασία (edited) από ένα εργαλείο που κατανοεί μόνο τη μία από τις δύο. Εξετάστε ένα βοηθητικό πρόγραμμα σφράγισης (stamping utility) που προσαρτά μια αυξητική ενημέρωση (incremental update) κλασικού τύπου: νέα αντικείμενα, νέα ενότητα xref, αλυσίδα /Prev προς την προηγούμενη ενότητα, και ένα νέο trailer. Αν αυτό το trailer απορρίψει το κλειδί /XRefStm, η οπτική (view) της ροής ορφανεύει· αν αντιγράψει την παλιά τιμή προς τα εμπρός (forward), η οπτική της ροής (stream view) εξακολουθεί να περιγράφει το έγγραφο όπως ήταν πριν από την επεξεργασία. Και στις δύο περιπτώσεις, τα δύο ευρετήρια (indexes) πλέον διαφωνούν ως προς το τι περιέχει το αρχείο
Το αρχείο που προκύπτει έχει μια χαρακτηριστική υπογραφή αποτυχίας (failure signature): αντικείμενα ορατά (visible) σε μια οπτική είναι αγνοούμενα (missing) ή παρωχημένα (stale) στην άλλη. Ένας αναγνώστης που επιλύει μέσω του stream view βρίσκει την έκδοση (version) ενός ενημερωμένου (updated) αντικειμένου προ της επεξεργασίας, ή καμία καταχώριση για ένα πρόσφατα προσαρτημένο. Ένας αναγνώστης μέσω του table view βλέπει την επεξεργασία (edit) αλλά χάνει τα ίχνη των συμπιεσμένων αντικειμένων που μόνο η ροή (stream) εντοπίζει (locates). Στην πράξη, αυτό εμφανίζεται ως πεδία φορμών (form fields) που επιβιώνουν σε έναν προβολέα και εξαφανίζονται σε άλλον, σημειώσεις (annotations) που μοιάζει να έχει διαγράψει ένα βήμα σφράγισης (stamping pass), ή αναζητήσεις (lookups) που προσγειώνονται σε εντελώς λάθος αντικείμενο
Αυτό που κάνει αυτά τα αρχεία ακριβά για διόρθωση σφαλμάτων (debugging) είναι ότι το Adobe Acrobat συνήθως τα ανοίγει χωρίς παράπονο: όταν το ευρετήριο (index) διαφωνεί με τα bytes, αθόρυβα ξαναχτίζει τα cross-reference δεδομένα ανιχνεύοντας επικεφαλίδες αντικειμένων (object headers), οπότε όποιος παρήγαγε το χαλασμένο αρχείο δεν βλέπει τίποτα λάθος. Η αποτυχία αναδύεται (surfaces) αργότερα, όταν το αρχείο φτάσει σε έναν αυστηρό (strict) καταναλωτή, σε έναν επικυρωτή προελέγχου (preflight validator), σε μια υπηρεσία υπογραφής, σε μια εργασία εισαγωγής σε αρχείο (archival ingest job), ο οποίος εμπιστεύεται τη δηλωμένη δομή και αναφέρει αγνοούμενα αντικείμενα (missing objects) ή αναντιστοιχία διασταυρούμενων αναφορών (cross-reference mismatch). Το "Ανοίγει μια χαρά στο Acrobat" (It opens fine in Acrobat) είναι ο τρόπος με τον οποίο ξεκινά σχεδόν κάθε δελτίο προβλήματος (ticket) αποσυγχρονισμού hybrid αρχείων
Εντοπισμός ενός hybrid αρχείου με απλό Delphi
Η ταξινόμηση των εισόδων (inputs) δεν απαιτεί βιβλιοθήκη PDF. Το κλειδί /XRefStm μπορεί να εμφανιστεί μόνο μέσα σε ένα κλασικό λεξικό (dictionary) του trailer, και το ενεργό (active) trailer βρίσκεται στα τελευταία λίγα kilobytes (kilobytes) του αρχείου, επειδή η προδιαγραφή (specification) απαιτεί το %%EOF να εμφανίζεται κοντά στο φυσικό τέλος (physical end). Η ανάγνωση ενός οριοθετημένου παραθύρου (bounded window) στην "ουρά" (tail) του αρχείου και η αναζήτηση εντός αυτού είναι αρκετή για τη διαλογή (triage):
uses
System.SysUtils, System.Classes, System.StrUtils, System.Math;
function IsHybridReferencePdf(const FileName: string): Boolean;
const
TailWindow = 2048;
var
Stream: TFileStream;
Buf: TBytes;
Tail: string;
Len, TrailerPos, NextPos, KeyPos, StartXrefPos: Integer;
begin
Result := False;
Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
if Stream.Size < 48 then
Exit;
Len := Min(TailWindow, Integer(Stream.Size));
SetLength(Buf, Len);
Stream.Position := Stream.Size - Len;
Stream.ReadBuffer(Buf[0], Len);
finally
Stream.Free;
end;
// Every keyword involved is 7-bit ASCII, so a byte-wise decode is safe
Tail := TEncoding.ANSI.GetString(Buf);
// Find the LAST 'trailer' keyword: with incremental updates,
// the newest trailer is the one that governs the file
TrailerPos := 0;
NextPos := Pos('trailer', Tail);
while NextPos > 0 do
begin
TrailerPos := NextPos;
NextPos := PosEx('trailer', Tail, NextPos + 1);
end;
if TrailerPos = 0 then
Exit; // no classic trailer: a pure xref-stream file, not hybrid
// A hybrid trailer carries /XRefStm between 'trailer' and 'startxref'
KeyPos := PosEx('/XRefStm', Tail, TrailerPos);
StartXrefPos := PosEx('startxref', Tail, TrailerPos);
Result := (KeyPos > 0) and
((StartXrefPos = 0) or (KeyPos < StartXrefPos));
end;
Τα τρία αποτελέσματα (outcomes) ευθυγραμμίζονται (line up) με τις τρεις διατάξεις (layouts). Ένα αποκλειστικά-κλασικό (classic-only) αρχείο έχει ένα trailer αλλά όχι /XRefStm: False. Ένα αρχείο που δεσμεύεται (commits) πλήρως σε cross-reference streams δεν έχει καθόλου τη λέξη-κλειδί trailer· τα κλειδιά (keys) του trailer ζουν στο λεξικό (dictionary) του stream: επίσης False, σωστά, επειδή ένα τέτοιο αρχείο είναι συμπιεσμένο, όχι hybrid. Μόνο η διάταξη με το διπλό ευρετήριο (double-indexed layout) επιστρέφει True
Για χρήση στην παραγωγή (production use), δύο θωρακίσεις (hardenings) αξίζουν τις επιπλέον γραμμές κώδικα (extra lines). Αναλύστε (Parse) τον ακέραιο (integer) μετά το /XRefStm, πηδήξτε (seek) σε αυτή τη μετατόπιση (offset), και επιβεβαιώστε ότι ένα αντικείμενο ροής (stream object) με /Type /XRef όντως κάθεται εκεί· ένα αποκομμένο (truncated) αρχείο μπορεί να φέρει το κλειδί ενώ η ροή έχει χαθεί, κάτι που ανήκει σε διαφορετικό κουβά (different bucket) από ένα υγιές (healthy) hybrid. Και χειριστείτε το μέγεθος του παραθύρου (window size) ως παράμετρο: τα 2 KB καλύπτουν τη συνηθισμένη έξοδο (output) του Office, αλλά ένα ασυνήθιστα μεγάλο λεξικό (dictionary) trailer μπορεί να σπρώξει τη λέξη-κλειδί (keyword) εκτός εμβέλειας (out of range), και η διεύρυνση (widening) του παραθύρου είναι προτιμότερη (beats) από το να δηλώσετε το αρχείο κατά λάθος ως "κλασικό"
Δρομολόγηση hybrid αρχείων (hybrid files) μέσα σε μια γραμμή επεξεργασίας (pipeline) στο Delphi
Η ανίχνευση (Detection) σας αγοράζει μια απόφαση δρομολόγησης (routing decision). Για αρχεία που προορίζονται μόνο για ανάγνωση, απόδοση (rendering), ή επικύρωση (validation), χρησιμοποιήστε έναν φορτωτή (loader) που επιλύει (resolves) και τις δύο όψεις (views), και στη συνέχεια επαληθεύστε τη συμπεριφορά (verify behavior) και όχι τα bytes (bytes). Το PDFium Component αναλύει (parses) την αλυσίδα /XRefStm κατά τη φόρτωση, οπότε ο πίνακας αντικειμένων που βλέπει ο κώδικάς σας (your code sees) είναι ο συγχωνευμένος (merged), και οι έλεγχοι που περιγράφονται στο άρθρο μας για την επικύρωση object και cross-reference streams ισχύουν χωρίς καμία αλλαγή. Αν ένα αποσυγχρονισμένο hybrid αρχείο είναι τόσο πολύ κατεστραμμένο ώστε να αρνείται τη φόρτωση, η μηχανή (engine) το αναφέρει μέσω του σετ σφαλμάτων της (error set): FPDF_ERR_SUCCESS, FPDF_ERR_UNKNOWN, FPDF_ERR_FILE, FPDF_ERR_FORMAT, FPDF_ERR_PASSWORD, FPDF_ERR_SECURITY και FPDF_ERR_PAGE, με το FPDF_ERR_FORMAT να είναι αυτό που παράγει (produces) η δομική ζημιά (structural damage). Ωστόσο, μην στηρίζεστε σε αυτό το σήμα (signal): το PDFium είναι εκ κατασκευής (by design) επιεικές (lenient) και ξαναχτίζει (rebuilds) σιωπηλά τα περισσότερα ασυνεπή αρχεία, οπότε μια επιτυχημένη φόρτωση (successful load) αποδεικνύει (proves) ότι το αρχείο ήταν ανακτήσιμο (recoverable), όχι ότι οι δύο όψεις του συμφωνούν (agree). Ο πραγματικά ουσιαστικός έλεγχος συνέπειας (consistency check) είναι να συγκρίνετε (comparing) τι ανακαλύπτει μια πλήρης περιήγηση αντικειμένων (full object walk) σε σχέση με αυτό που δηλώνει (declares) το /Size του trailer
Για τα αρχεία που τροποποιεί (modifies) η γραμμή επεξεργασίας σας (your pipeline), η πιο ασφαλής πολιτική είναι να τα σταματήσετε (stop them) από το να είναι καν hybrid (υβριδικά). Μια φόρτωση (load) που ακολουθείται από μια πλήρη αποθήκευση (full save) μέσω του HotPDF ξαναγράφει το έγγραφο (rewrites the document) με μια μοναδική (single), εσωτερικά συνεπή (self-consistent) διασταυρούμενη αναφορά (cross-reference) σε μία μορφή: χωρίς /XRefStm, χωρίς δεύτερη όψη (second view) που να βγαίνει εκτός συγχρονισμού (fall out of sync), και κάθε αντικείμενο να ανήκει (owned) ακριβώς σε μία (exactly one) καταχώριση (entry) στο ευρετήριο (index). Αυτή η κανονικοποίηση (normalization) είναι αυτό που θέλετε πριν από μια εισαγωγή σε αρχείο (archival ingest), πριν από έναν αυστηρό RIP επόμενου σταδίου (downstream RIP) ή υπηρεσία υπογραφής, και μετά από οποιαδήποτε επεξεργασία (edit) εφαρμόζεται (applied) σε μια υβριδική είσοδο (hybrid input). Λειτουργεί (It works) επειδή ο φορτωτής (loader) συγχώνευσε σωστά (merged correctly) τις όψεις κατά την είσοδο (on the way in), τον ίδιο τον μηχανισμό (mechanism) τον οποίο το άρθρο του HotPDF για τις υβριδικές αναφορές διατρέχει με κάθε λεπτομέρεια
Η μοναδική κατηγορία αρχείων (class of files) που πρέπει να αφήσετε στην ησυχία τους (leave alone) είναι τα ψηφιακά υπογεγραμμένα (digitally signed) έγγραφα. Μια πλήρης επανεγγραφή (full rewrite) μετακινεί κάθε byte, κάτι που ακυρώνει (invalidates) οποιαδήποτε υπογραφή έχει υπολογιστεί πάνω στα αρχικά εύρη (original ranges). Μια αλλαγή σε ένα υπογεγραμμένο hybrid πρέπει να μπει ως σωστή αυξητική ενημέρωση (incremental update) που διατηρεί (maintains) και τις δύο όψεις· ένα αρχείο που χρειάζεται μόνο ανάγνωση θα πρέπει να περάσει μέσα (pass through) χωρίς να αγγιχτεί. Η κανονικοποίηση (Normalization) είναι για τα αρχεία που σας ανήκουν (files you own)· τα υπογεγραμμένα αρχεία τα προσαρτάτε (append to) μόνο (only ever)
Τα Hybrid-reference PDFs δεν είναι κακοσχηματισμένα (malformed)· αποτελούν (they are) τη γέφυρα συμβατότητας (compatibility bridge) του ίδιου του (its own) format, και οι εφαρμογές του Office θα συνεχίσουν να τα παράγουν (producing them) για όσο διάστημα επιβιώνουν (survive) αναγνώστες (readers) του PDF 1.4 στην εγκατεστημένη βάση (install base). Μια γραμμή επεξεργασίας (pipeline) που μπορεί να εντοπίσει το κλειδί /XRefStm, να επικυρώσει (validate) το συγχωνευμένο έγγραφο με το PDFium Component, και να αναπαραγάγει (regenerate) μια καθαρή (clean) έξοδο ενός-ευρετηρίου (single-index output) με το HotPDF Component τα αντιμετωπίζει για (treats them as) αυτό που πραγματικά είναι (what they are): συνηθισμένες είσοδοι (ordinary inputs) με μία επιπλέον πινακίδα (signpost) στο trailer