Ένας τοπογράφος ανοίγει ένα χωροταξικό σχέδιο και θέλει τις ισοϋψείς καμπύλες κρυφές ενώ τα δίκτυα κοινής ωφέλειας παραμένουν εμφανή. Ένας ελεγκτής θέλει τις σημειώσεις αναθεώρησης (redline annotations) ορατές στην οθόνη και εξαφανισμένες από την εκτύπωση. Ένα φυλλάδιο προϊόντος αποστέλλεται σε τρεις γλώσσες από ένα αρχείο, και ο αναγνώστης επιλέγει ποια γλώσσα θα εμφανίζεται. Και τα τρία είναι το ίδιο χαρακτηριστικό PDF, και το πάνελ που τα διαχειρίζεται στο Acrobat ονομάζεται Layers. Το χαρακτηριστικό κάτω από αυτό το πάνελ είναι το προαιρετικό περιεχόμενο (optional content), και είναι αυτό που επιτρέπει σε μια σελίδα να φέρει πολλά ανεξάρτητα οπτικά επίπεδα τα οποία ο χρήστης ενεργοποιεί και απενεργοποιεί.
Το προαιρετικό περιεχόμενο καθορίζεται στο ISO 32000-1 §8.11. Η μονάδα ορατότητας είναι μια προαιρετική ομάδα περιεχομένου (optional content group), OCG, ένα λεξικό τύπου /OCG που φέρει ένα όνομα. Το επισημασμένο περιεχόμενο (marked content) σε μια σελίδα συνδέεται με μια ομάδα, και το πρόγραμμα προβολής αποφασίζει αν αυτή η ομάδα εμφανίζεται αυτήν τη στιγμή. Μια σχετική κατασκευή, το λεξικό συμμετοχής προαιρετικού περιεχομένου ή OCMD, επιτρέπει στην ορατότητα να εξαρτάται από έναν συνδυασμό boolean πολλών ομάδων, αλλά η καθημερινή περίπτωση είναι μια ενιαία ονομαστική ομάδα που αντιπροσωπεύει ένα επίπεδο (layer). Το έγγραφο συνδέει ολόκληρο τον μηχανισμό μέσω μιας καταχώρισης καταλόγου, του /OCProperties, που περιγράφεται στη συνέχεια.
Τι πρέπει να φέρει ο κατάλογος
Ένα OCG από μόνο του είναι αδρανές. Για να καταγράψει ένα πρόγραμμα προβολής ένα επίπεδο και να θυμάται την κατάστασή του, ο κατάλογος εγγράφου χρειάζεται ένα λεξικό /OCProperties, και η §8.11.4 ορίζει ακριβώς τι περιλαμβάνεται σε αυτό. Υπάρχει ένας πίνακας /OCGs που ονομάζει κάθε ομάδα στο αρχείο, και υπάρχει μια καταχώριση /D που περιέχει την προεπιλεγμένη διαμόρφωση (default configuration). Η προεπιλεγμένη διαμόρφωση είναι το τμήμα που εφαρμόζει ένας αναγνώστης όταν ανοίγει για πρώτη φορά το αρχείο. Καταγράφει ποιες ομάδες ξεκινούν ως ενεργοποιημένες και ποιες ως απενεργοποιημένες, ποιες καταχωρίσεις είναι κλειδωμένες ώστε ο χρήστης να μην μπορεί να τις αλλάξει, και, μέσω ενός πίνακα /Order, πώς τα ονόματα των επιπέδων είναι διατεταγμένα και φωλιασμένα στο πάνελ.
Η πρακτική συνέπεια είναι ότι η δημιουργία ενός επιπέδου δεν είναι ποτέ μια καθαρά τοπική πράξη. Η ομάδα πρέπει να σχεδιαστεί στη σελίδα, αλλά πρέπει επίσης να καταχωριστεί σε μια δομή επιπέδου καταλόγου που δεν υπήρχε προηγουμένως. PDFlibPas κάνει και τα δύο για εσάς. Η πρώτη κλήση που δημιουργεί μια ομάδα προσθέτει την καταχώριση /OCProperties στον κατάλογο και τροφοδοτεί την προεπιλεγμένη διαμόρφωση, έτσι ώστε το επίπεδο να σχεδιάζεται και να καταγράφεται χωρίς ξεχωριστή τήρηση βιβλίων από την πλευρά σας.
Γιατί μια λειτουργία συμμόρφωσης μπορεί να παρακρατήσει το χαρακτηριστικό
Πριν εκτελεστεί οποιοσδήποτε κώδικας επιπέδου, ο στόχος συμμόρφωσης του εγγράφου αποφασίζει εάν το προαιρετικό περιεχόμενο είναι καν νόμιμο. Το PDF/A-1, το αρχειακό προφίλ που ορίζεται στο ISO 19005-1, απαγορεύει εντελώς την καταχώριση /OCProperties στην §6.1.13. Το σκεπτικό ταιριάζει με το σκοπό της μορφής. Ένα αρχειακό αρχείο πρέπει να αποδίδεται πανομοιότυπα για κάθε αναγνώστη στο μακρινό μέλλον, και το περιεχόμενο του οποίου την ορατότητα μπορεί να αλλάξει ο χρήστης είναι περιεχόμενο του οποίου η εμφάνιση δεν είναι σταθερή, επομένως το προφίλ απαγορεύει την κατασκευή αντί να επιτρέψει ένα αμφίβολο αρχείο. Τα PDF/A-2 and PDF/A-3, που ορίζονται στα ISO 19005-2 και ISO 19005-3, έχουν την αντίθετη άποψη στην §6.9 τους και επιτρέπουν το προαιρετικό περιεχόμενο, με κανόνες σχετικά με την προεπιλεγμένη ορατότητα.
Αυτή η διαφορά εμφανίζεται απευθείας στο API. Όταν το έγγραφο βρίσκεται σε λειτουργία PDF/A-1, η NewOptionalContentGroup αρνείται να δημιουργήσει την ομάδα και επιστρέφει μηδέν, επειδή η ικανοποίηση του αιτήματος θα παρήγαγε ένα αρχείο που αποτυγχάνει στη δηλωμένη συμμόρφωσή του. Σε λειτουργία PDF/A-2 ή PDF/A-3, καθώς και σε κανονικό μη περιορισμένο PDF, η ίδια κλήση πετυχαίνει και επιστρέφει ένα μη μηδενικό ID ομάδας. Ένα μηδενικό αποτέλεσμα δεν αποτελεί επομένως μια γενική αποτυχία για μεταγενέστερη επιθεώρηση. Είναι η βιβλιοθήκη που σας λέει ότι το ενεργό επίπεδο συμμόρφωσης δεν έχει χώρο για αυτό το χαρακτηριστικό.
var
Pdf: TPDFlib;
LayerID: Integer;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.NewDocument;
Pdf.SetPDFAMode(1); // PDF/A-1a: OCProperties forbidden
LayerID := Pdf.NewOptionalContentGroup('Utilities');
if LayerID = 0 then
// refused under PDF/A-1; not a transient error, the mode bans layers
ShowMessage('Optional content is not available in PDF/A-1 mode.');
finally
Pdf.Free;
end;
end;
Δύο καταστάσεις ανά επίπεδο, όχι μία
Ένα επίπεδο δεν είναι απλώς ορατό ή αόρατο. Η προεπιλεγμένη διαμόρφωση καταγράφει την κατάσταση στην οθόνη και μια ξεχωριστή κατάσταση εκτύπωσης, επειδή η §8.11.4 διακρίνει αυτό που εμφανίζει ένα πρόγραμμα προβολής από αυτό που εκπέμπει μια ροή εργασίας εκτύπωσης. Τα δύο είναι ανεξάρτητα επίτηδες. Ένα δοκιμαστικό υδατογράφημα (draft watermark) μπορεί να εμφανίζεται στην οθόνη και να παραλείπεται από το χαρτί, και ένα επίπεδο γραμμών κοπής (cut-line layer) μπορεί να είναι κρυφό στην οθόνη αλλά να αποστέλλεται σε έναν σχεδιογράφο (plotter). Η σύμπτυξη των δύο θα ανάγκαζε το ένα να ακολουθεί το άλλο και θα έχανε ακριβώς τον έλεγχο για τον οποίο υπάρχει το χαρακτηριστικό.
Το PDFlibPas εκθέτει το ζεύγος μέσω δύο setters. Η SetOptionalContentGroupVisible λαμβάνει το ID της ομάδας και μια σημαία, όπου το ένα σημαίνει ορατό και το μηδέν κρυφό, και ρυθμίζει την προεπιλεγμένη κατάσταση στην οθόνη. Η SetOptionalContentGroupPrintable λαμβάνει το ID της ομάδας και μια σημαία για το αν το επίπεδο εκπέμπεται κατά την εκτύπωση του εγγράφου. Οι αντίστοιχες getters, GetOptionalContentGroupVisible και GetOptionalContentGroupPrintable, επιστρέφουν η καθεμία ένα ή μηδέν, ώστε να μπορείτε να διαβάσετε την οθόνη και τη διάθεση εκτύπωσης ενός επιπέδου ξεχωριστά, αντί να συμπεράνετε το ένα από το άλλο.
Δημιουργία δύο επιπέδων σε μια σελίδα
Η δημιουργία ενός επιπέδου και η συμπλήρωσή του ακολουθεί μια σταθερή σειρά. Σχεδιάζετε το περιεχόμενο για το επίπεδο στην τρέχουσα σελίδα, στη συνέχεια καλείτε τη SetContentStreamOptional με το ID της ομάδας, η οποία τυλίγει την τρέχουσα ροή περιεχομένου της σελίδας (content stream), έτσι ώστε ό,τι έχει σχεδιαστεί μέχρι στιγμής να ανήκει σε αυτήν την ομάδα. Επειδή η κλήση καταγράφει ό,τι βρίσκεται στη ροή εκείνη τη στιγμή, η πειθαρχία είναι να τοποθετήσετε τα σημάδια ενός επιπέδου, να τα αντιστοιχίσετε και μόνο τότε να ξεκινήσετε το επόμενο επίπεδο. Το παρακάτω παράδειγμα τοποθετεί τα δίκτυα κοινής ωφέλειας στην πρώτη σελίδα και τις σημειώσεις αναθεώρησης σε μια δεύτερη σελίδα, ορίζει την κατάσταση οθόνης και εκτύπωσης κάθε επιπέδου και αποθηκεύει.
var
Pdf: TPDFlib;
FontID, UtilLayer, RedlineLayer: Integer;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.NewDocument; // unconstrained PDF: layers allowed
Pdf.SetPageDimensions(595, 842); // A4 in points
FontID := Pdf.AddStandardFont(0); // Helvetica
Pdf.SelectFont(FontID);
// Layer 1: utilities, drawn then assigned to its own group
Pdf.SetTextColor(0.10, 0.30, 0.65);
Pdf.DrawText(72, 770, 'Utilities: water main, valve chamber');
UtilLayer := Pdf.NewOptionalContentGroup('Utilities');
Pdf.SetContentStreamOptional(UtilLayer);
Pdf.SetOptionalContentGroupVisible(UtilLayer, 1); // shown on screen
Pdf.SetOptionalContentGroupPrintable(UtilLayer, 1); // and on paper
// Layer 2: reviewer redline on a fresh page
Pdf.InsertPages(2, 1); // append one page after page 1
Pdf.SetTextColor(0.80, 0.10, 0.10);
Pdf.DrawText(72, 770, 'REVIEW: revise valve spec before issue');
RedlineLayer := Pdf.NewOptionalContentGroup('Reviewer markup');
Pdf.SetContentStreamOptional(RedlineLayer);
Pdf.SetOptionalContentGroupVisible(RedlineLayer, 1); // visible while reviewing
Pdf.SetOptionalContentGroupPrintable(RedlineLayer, 0); // never printed
Pdf.SaveToFile('SitePlan_Layers.pdf');
finally
Pdf.Free;
end;
end;
Το επίπεδο αναθεώρησης (redline layer) is the case worth noting. Εμφανίζεται στην οθόνη ώστε ο αναθεωρητής να βλέπει τη σημείωση, και η σημαία εκτύπωσής του είναι μηδέν, ώστε ένα εκτυπωμένο αντίγραφο του ίδιου αρχείου να μην φέρει το κείμενο της αναθεώρησης. Αυτή η ασυμμετρία είναι όλο το νόημα της διατήρησης των δύο καταστάσεων χωριστά.
Ανάγνωση της διαμόρφωσης
Η ανάγνωση των επιπέδων είναι μια διαφορετική περιήγηση στην ίδια δομή. Αφού φορτωθεί ένα αρχείο, η GetOptionalContentConfigCount αναφέρει πόσα λεξικά διαμόρφωσης περιέχει το έγγραφο. Η πρώτη προεπιλεγμένη διαμόρφωση είναι το ID διαμόρφωσης 1. Μέσα σε μια διαμόρφωση, η GetOptionalContentConfigOrderCount δίνει τον αριθμό των καταχωρίσεων στο δέντρο σειράς, και τις ευρετηριάζετε από το 1. Για κάθε καταχώριση, η GetOptionalContentConfigOrderItemLabel επιστρέφει το κείμενο εμφάνισής της και η GetOptionalContentConfigOrderItemLevel επιστρέφει το βάθος φωλιάσματος της, έτσι ώστε ένα περίγραμμα πάνελ με υπο-επίπεδα σε εσοχή κάτω από επικεφαλίδες να μπορεί να ανακατασκευαστεί αυτολεξεί.
Κάθε καταχώριση έχει επίσης έναν τύπο. Η GetOptionalContentConfigOrderItemType διακρίνει μια πραγματική προαιρετική ομάδα περιεχομένου από μια ετικέτα απλού κειμένου που υπάρχει μόνο ως επικεφαλίδα μιας ενότητας του δέντρου. Αυτή η διάκριση έχει σημασία επειδή τα ερωτήματα κατάστασης ανά ομάδα έχουν νόημα μόνο για πραγματικές ομάδες. Για μια καταχώριση ομάδας, η GetOptionalContentConfigState αναφέρει εάν η διαμόρφωση την ξεκινά ως ενεργή, ανενεργή ή την αφήνει αμετάβλητη, και η GetOptionalContentConfigLocked αναφέρει εάν ο χρήστης εμποδίζεται να την εναλλάσσει. Ο παρακάτω βρόχος αποδίδει το δέντρο σειράς με την κατάσταση κάθε ομάδας και την κατάσταση κλειδώματος, με εσοχή ανάλογα με το επίπεδο.
var
Pdf: TPDFlib;
Cfg, Count, I, ItemType, GroupID, Indent: Integer;
Line: string;
begin
Pdf := TPDFlib.Create(nil);
try
if Pdf.LoadFromFile('SitePlan_Layers.pdf', '') = 0 then Exit;
if Pdf.GetOptionalContentConfigCount = 0 then Exit;
Cfg := 1; // the default configuration
Count := Pdf.GetOptionalContentConfigOrderCount(Cfg);
for I := 1 to Count do
begin
Indent := Pdf.GetOptionalContentConfigOrderItemLevel(Cfg, I);
Line := StringOfChar(' ', Indent * 2)
+ Pdf.GetOptionalContentConfigOrderItemLabel(Cfg, I);
ItemType := Pdf.GetOptionalContentConfigOrderItemType(Cfg, I);
if ItemType = 1 then // 1 = optional content group
begin
GroupID := Pdf.GetOptionalContentConfigOrderItemID(Cfg, I);
case Pdf.GetOptionalContentConfigState(Cfg, GroupID) of
1: Line := Line + ' [on]';
2: Line := Line + ' [off]';
3: Line := Line + ' [unchanged]';
end;
if Pdf.GetOptionalContentConfigLocked(Cfg, GroupID) = 1 then
Line := Line + ' (locked)';
end;
// ItemType = 2 is a text label heading; it has no per-group state
Writeln(Line);
end;
finally
Pdf.Free;
end;
end;
Δύο λεπτομέρειες διατηρούν αυτόν τον βρόχο σωστό. Ο δείκτης σειράς βασίζεται στο ένα, από το 1 έως το πλήθος, ταιριάζοντας με τον τρόπο που η βιβλιοθήκη αριθμεί το δέντρο εσωτερικά. Και οι κλήσεις ανά ομάδα εκτελούνται μόνο όταν ο τύπος στοιχείου είναι ομάδα, επειδή μια ετικέτα κειμένου είναι μια επικεφαλίδα με όνομα και επίπεδο, αλλά χωρίς κατάσταση on, off ή locked για ερώτημα. Παραλείψτε αυτόν τον έλεγχο προστασίας και θα ζητήσετε από μια ετικέτα μια κατάσταση που δεν διαθέτει.
Πού ταιριάζει αυτό
Τα επίπεδα είναι ένας μηχανισμός παρουσίασης, επομένως η μηχανή πρέπει να τα σέβεται σε κάθε διαδρομή που αποδίδει μια σελίδα, και η πλευρά της απόδοσης καλύπτεται στον οδηγό μας για την απόδοση πολλαπλών μηχανών στο Delphi. Διασταυρώνονται επίσης με τη δομή του εγγράφου, επειδή το όνομα ενός επιπέδου είναι κείμενο που απευθύνεται στον δημιουργό και ο αναγνώστης επωφελείται από ένα δομημένο περίγραμμα επιπέδων, το οποίο συνδέεται με την εργασία στο άρθρο μας για το tagged PDF και τη δομή προσβασιμότητας. Και τα δύο συνδυάζονται με τα API προαιρετικού περιεχομένου που περιγράφονται εδώ, τα οποία αποστέλλονται ως μέρος της Delphi PDF Library μαζί με τις δυνατότητες σελίδας, κειμένου, γραμματοσειράς και συμμόρφωσης που συζητούνται αλλού σε αυτό το ιστολόγιο.