Ένα πεδίο φόρμας PDF από μόνο του είναι απλώς ένα πλαίσιο που περιέχει μια τιμή. Αυτό που κάνει μια φόρμα να συμπεριφέρεται σαν μια μικρή εφαρμογή είναι η ενέργεια που είναι συνδεδεμένη με αυτήν: ένα κλικ που κρύβει μια ενότητα, ανακτά αποθηκευμένες τιμές από ένα αρχείο, μεταβαίνει στην τελευταία σελίδα ή εκτελεί ένα σενάριο που αθροίζει μια στήλη. Τίποτα από αυτά δεν ζει στο πεδίο. Ζει σε ένα λεξικό ενεργειών (action dictionary), και το πρότυπο ISO 32000-1 οργανώνει ολόκληρη την οικογένεια στην §12.6. Αυτό το άρθρο εξετάζει τις ενέργειες που χρησιμοποιεί συχνότερα ένα πρόγραμμα Delphi και δείχνει πώς το PDFlibPas συνδέει καθεμία από αυτές με ένα πεδίο ή έναν σύνδεσμο.
Το νοητικό μοντέλο που αξίζει να θυμάστε είναι ότι ένα πεδίο και μια ενέργεια είναι ξεχωριστά αντικείμενα που ενώνονται με μια αναφορά. Μια σημείωση γραφικού στοιχείου (widget annotation) ή μια σημείωση συνδέσμου (link annotation) φέρει μια ενέργεια στην καταχώρισή της /A. Η ενέργεια ονομάζει το πεδίο στο οποίο λειτουργεί με βάση τον τίτλο, όχι τον δείκτη, επομένως ο τίτλος που δίνετε σε ένα πεδίο είναι η λαβή που χρησιμοποιεί κάθε μεταγενέστερη ενέργεια για να το βρει. Μόλις γίνει σαφής αυτός ο διαχωρισμός, το API παύει να μοιάζει με μια τυχαία συλλογή κλήσεων και αρχίζει να φαίνεται ως ένα ενιαίο μοτίβο που εφαρμόζεται σε τέσσερα είδη ενεργειών.
Named actions: πλοήγηση χωρίς αριθμό σελίδας
Οι απλούστερες ενέργειες δεν φέρουν καθόλου παραμέτρους. Το ISO 32000-1 §12.6.4.11, Πίνακας 194, ορίζει τις ονομαστικές ενέργειες (named actions): το πρόγραμμα προβολής ερμηνεύει ένα συμβολικό όνομα κατά το χρόνο εκτέλεσης αντί να ακολουθεί έναν αποθηκευμένο προορισμό. Τέτοια ονόματα υποστηρίζονται καθολικά και είναι ακριβώς αυτά που περιμένει ένας αναγνώστης από μια εργαλειοθήκη: NextPage, PrevPage, FirstPage και LastPage. Επειδή ο προορισμός είναι σχετικός με οποιαδήποτε σελίδα εμφανίζει αυτήν τη στιγμή το πρόγραμμα προβολής, ένα κουμπί Next που έχει κατασκευαστεί με αυτόν τον τρόπο λειτουργεί σε κάθε σελίδα χωρίς να χρειάζεται να υπολογίσετε έναν στόχο.
Στο PDFlibPas, μια ονομαστική ενέργεια επισυνάπτεται σε ένα ορθογώνιο ενεργού σημείου (hotspot) στην τρέχουσα σελίδα. Το τέταρτο και το πέμπτο όρισμα ακέραιου αριθμού επιλέγουν την ενέργεια και την εμφάνιση.
// NamedActionType: 0 = NextPage, 1 = PrevPage, 2 = FirstPage, 3 = LastPage
// Options bit 0 (value 1) draws a border around the hotspot
Pdf.AddLinkToNamedAction(500, 560, 60, 18, 0, 1); // Next
Pdf.AddLinkToNamedAction(40, 560, 60, 18, 1, 1); // Previous
Pdf.AddLinkToNamedAction(110, 560, 60, 18, 3, 1); // jump to last page
Δεν υπάρχει προορισμός που πρέπει να διατηρείται συγχρονισμένος, κάτι που είναι και το ζητούμενο. Μια ονομαστική ενέργεια επιβιώνει από την εισαγωγή και τη διαγραφή σελίδων επειδή δεν κατονομάζει ποτέ μια σελίδα εξ αρχής. Αντιπαραβάλετε αυτό με έναν ρητό σύνδεσμο μετάβασης (go-to link), ο οποίος αποθηκεύει έναν δείκτη σελίδας-στόχου που πρέπει να επαναριθμήσετε τη στιγμή που το έγγραφο μεγαλώνει.
Η ενέργεια Hide και η παγίδα του πίνακα
Η ενέργεια Hide, ISO 32000-1 §12.6.4.10, Πίνακας 196, εναλλάσσει την ορατότητα ενός ή περισσότερων πεδίων. Είναι ο πιο καθαρός τρόπος για να δημιουργήσετε συμπεριφορά εμφάνισης και απόκρυψης χωρίς scripting, και είναι αυτό που θέλετε για έναν σύνδεσμο Show details ή για δύο αμοιβαία αποκλειόμενα πάνελ όπου η αποκάλυψη του ενός κρύβει το άλλο. Η ενέργεια φέρει έναν στόχο στην καταχώρισή της /T και μια τιμή boolean /H που καθορίζει την κατεύθυνση: απόκρυψη όταν είναι true, εμφάνιση όταν είναι false.
Η λεπτομέρεια βρίσκεται εξ ολοκλήρου στον τρόπο κωδικοποίησης αυτού του στόχου, και είναι το είδος της λεπτομέρειας που παράγει μια φόρμα που λειτουργεί στον υπολογιστή σας αλλά αποτυγχάνει σε αυτόν του πελάτη. Όταν η ενέργεια ονομάζει ένα μόνο πεδίο, το /T γράφεται ως μία συμβολοσειρά κειμένου. Όταν ονομάζει πολλά, το /T γράφεται ως ένας πίνακας από συμβολοσειρές κειμένου. Τα παλαιότερα προγράμματα προβολής δεν αντιμετωπίζουν έναν πίνακα ενός στοιχείου με τον ίδιο τρόπο που αντιμετωπίζουν μια απλή συμβολοσειρά, επομένως η κωδικοποίηση πρέπει να διακλαδίζεται ανάλογα με το πλήθος: ένα μεμονωμένο όνομα πρέπει να εκπέμπεται ως συμβολοσειρά, όχι ως πίνακας μήκους ένα, εάν θέλουμε να το υποστηρίζει το ευρύτερο φάσμα αναγνωστών. Το PDFlibPas παίρνει αυτήν την απόφαση για εσάς. Περνάτε ονόματα πεδίων διαχωρισμένα με κόμματα, ελληνικά ερωτηματικά (semicolons) ή αλλαγές γραμμής, και ο εγγραφέας εκπέμπει μια ενιαία συμβολοσειρά για ένα όνομα και έναν πίνακα για δύο ή περισσότερα.
// HideFlag non-zero hides the listed fields (/H true); zero shows them.
// One name -> /T is a text string. Two or more -> /T is an array of strings.
Pdf.AddLinkToHideField(40, 700, 90, 18, 'ShippingAddress', 1, 1);
Pdf.AddLinkToHideField(140, 700, 90, 18,
'ShippingName,ShippingAddress,ShippingZip', 1, 1);
Επειδή η ενέργεια δεν αναφέρεται σε εξωτερικό πόρο, παραμένει συμβατή με το PDF/A. Τα ονόματα που περνάτε είναι πλήρως προσδιορισμένοι τίτλοι πεδίων, γι' αυτό και ένα θυγατρικό πεδίο μέσα σε μια ομάδα πρέπει να απευθύνεται μέσω της πλήρους διαδρομής του με τελείες και όχι με το απλό όνομα φύλλου του.
ImportData: προσυμπλήρωση από FDF
Εκεί που η ενέργεια Hide αναδιατάσσει όσα βρίσκονται ήδη στη σελίδα, η ενέργεια εισαγωγής δεδομένων (import-data action) φέρνει τιμές από έξω. Το ISO 32000-1 §12.6.4.8, Πίνακας 198, την ορίζει ως μια ενέργεια που συμπληρώνει το AcroForm από ένα αρχείο Forms Data Format στο δίσκο. Αυτή είναι η ενέργεια πίσω από ένα στοιχείο ελέγχου Reload sample data ή Reset to defaults, όπου ένα αρχείο FDF αποστέλλεται δίπλα στο PDF και περιέχει τις κανονικές τιμές των πεδίων. Η κλήση αντικατοπτρίζει τις άλλες, λαμβάνοντας το ορθογώνιο του ενεργού σημείου, τη διαδρομή προς το FDF και μια μάσκα εμφάνισης: Pdf.AddLinkToImportData(40, 660, 120, 18, 'defaults.fdf', 1). Το αρχείο δεν χρειάζεται να υπάρχει όταν δημιουργείται το PDF, αλλά πρέπει να είναι παρόν όταν ο χρήστης κάνει κλικ, και οποιεσδήποτε ανάστροφες κάθετοι στη διαδρομή ξαναγράφονται στην κανονική μορφή κάθετου του PDF για εσάς.
Ένας περιορισμός αξίζει να αναφερθεί ξεκάθαρα, επειδή αποτελεί συχνή έκπληξη. Μια ενέργεια εισαγωγής δεδομένων δείχνει σε ένα εξωτερικό αρχείο, επομένως δεν επιτρέπεται στο PDF/A. Όταν το έγγραφο βρίσκεται σε λειτουργία PDF/A, η κλήση επιστρέφει μηδέν και δεν προσθέτει τίποτα, αντί να παράγει ένα αρχείο που αποτυγχάνει στην επικύρωση. Εάν η ροή εργασίας σας στοχεύει σε αρχειακό αποτέλεσμα, η προσυμπλήρωση πρέπει να γίνει κατά το χρόνο δημιουργίας γράφοντας τις τιμές των πεδίων απευθείας, και όχι αναβάλλοντάς τες για ένα κλικ.
JavaScript: καθολικά πακέτα και σενάρια ανά ενέργεια
Για λογική που υπερβαίνει την εμφάνιση, την απόκρυψη και την εισαγωγή, η οικογένεια ενεργειών επεκτείνεται σε επίπεδο εγγράφου JavaScript. Υπάρχουν δύο διαφορετικά μέρη όπου μπορεί να ζει ένα σενάριο (script), και η διαφορά έχει σημασία. Ένα πακέτο JavaScript σε επίπεδο εγγράφου αποθηκεύεται μία φορά για ολόκληρο το αρχείο και εκτελείται όταν ανοίγει το έγγραφο, γεγονός που το καθιστά την κατάλληλη έδρα για ορισμούς συναρτήσεων και κοινόχρηστη κατάσταση (shared state). Ένα σενάριο ανά ενέργεια επισυνάπτεται σε έναν σύνδεσμο ή πεδίο και εκτελείται μόνο όταν ενεργοποιείται αυτό το αντικείμενο, γεγονός που το καθιστά την κατάλληλη έδρα για τη μία γραμμή που καλεί μια συνάρτηση την οποία έχει ήδη ορίσει το πακέτο.
Το PDFlibPas εκθέτει και τα δύο. Η AddGlobalJavaScript αποθηκεύει ένα ονομαστικό πακέτο σε επίπεδο εγγράφου. Η επαναχρησιμοποίηση ενός ονόματος αντικαθιστά ό,τι είχε αποθηκευτεί κάτω από αυτό. Η AddLinkToJavaScript συνδέει ένα σενάριο σε ένα ενεργό σημείο, ώστε ένα κλικ να το εκτελεί.
// Document-level package: define a reusable function once.
Pdf.AddGlobalJavaScript('Totals',
'function recalcTotal() {' +
' var net = this.getField("Net").value;' +
' var tax = this.getField("Tax").value;' +
' this.getField("Gross").value = Number(net) + Number(tax);' +
'}');
// Per-action script on a link: just call the shared function.
Pdf.AddLinkToJavaScript(40, 620, 100, 18, 'recalcTotal();', 1);
Η διατήρηση της συνάρτησης στο καθολικό πακέτο και της κλήσης στον σύνδεσμο δεν είναι θέμα στυλιστικής προτίμησης. Αποφεύγει την επανάληψη του ίδιου κώδικα σε κάθε στοιχείο ελέγχου που τον χρειάζεται, και σημαίνει ότι ένα πρόγραμμα προβολής με απενεργοποιημένο το scripting απλώς δεν κάνει τίποτα στο κλικ αντί να κολλάει σε ένα κακοσχηματισμένο ενσωματωμένο blob. Διατηρεί επίσης τις καταχωρίσεις ανά ενέργεια μικρές, γεγονός που κρατά το αρχείο αναγνώσιμο όταν το επιθεωρήσετε αργότερα.
Πεδία, θυγατρικά πεδία και πάγωμα του αποτελέσματος
Οι ενέργειες χρειάζονται πεδία για να δράσουν, επομένως βοηθάει να δούμε πώς δημιουργείται ένα πεδίο. Η NewFormField δημιουργεί ένα πεδίο στην τρέχουσα σελίδα και επιστρέφει το δείκτη του. Ο τύπος ακέραιου αριθμού επιλέγει το είδος, όπου 1 είναι Text, 2 είναι Pushbutton, 3 είναι Checkbox, 4 είναι Radiobutton, 5 είναι Choice, 6 είναι Signature και 7 είναι ένα Parent που έχει θυγατρικά πεδία αλλά δεν σχεδιάζει τίποτα το ίδιο. Ο τίτλος που περνάτε δεν μπορεί να περιέχει τελεία, επειδή η τελεία είναι ο διαχωριστής στα πλήρως προσδιορισμένα ονόματα που χρησιμοποιούν οι ενέργειες για να απευθυνθούν στα θυγατρικά πεδία.
Οι ομάδες ραδιοπλήκτρων (radio groups) και οι ιεραρχικές φόρμες δημιουργούνται δίνοντας θυγατρικά πεδία σε ένα γονικό πεδίο. Η NewChildFormField προσθέτει ένα θυγατρικό πεδίο κάτω από έναν ονομασμένο γονέα, και για τις περιπτώσεις ραδιοπλήκτρων και επιλογών, η AddFormFieldSub προσθέτει τις επιμέρους επιλογές και επιστρέφει έναν προσωρινό δείκτη που χρησιμοποιείτε για να τοποθετήσετε την καθεμία. Όταν η διαδραστική φάση τελειώσει και θέλετε να παγώσετε ένα πεδίο ώστε η τρέχουσα εμφάνισή του να γίνει μόνιμο περιεχόμενο της σελίδας, η FlattenFormField σχεδιάζει το πεδίο πάνω στη σελίδα και το αφαιρεί από τη φόρμα. Μετά από ένα flatten, οι δείκτες των μεταγενέστερων πεδίων μετατοπίζονται προς τα κάτω κατά ένα, κάτι που είναι το μόνο πράγμα που πρέπει να θυμάστε εάν κάνετε flatten σε πολλά πεδία σε έναν βρόχο.
var
Pdf: TPDFlib;
FldShip: Integer;
begin
Pdf := TPDFlib.Create;
try
Pdf.SetOrigin(1); // top-left origin
Pdf.SetPageSize('A4');
Pdf.NewPage;
// A text field the Hide action will target by its title.
FldShip := Pdf.NewFormField('ShippingAddress', 1);
Pdf.SetFormFieldBounds(FldShip, 40, 120, 240, 20);
Pdf.SetFormFieldValue(FldShip, '');
// Wire a Hide link and a navigation link to this page.
Pdf.DrawText(40, 110, 'Toggle shipping block:');
Pdf.AddLinkToHideField(220, 100, 70, 16, 'ShippingAddress', 1, 1);
Pdf.AddLinkToNamedAction(500, 800, 60, 18, 3, 1); // Last page
// A document-level script available to every event in the file.
Pdf.AddGlobalJavaScript('OnOpen',
'app.alert("Form ready", 3);');
// Freeze the field if the output should no longer be editable.
// Pdf.FlattenFormField(FldShip);
if Pdf.SaveToFile('form_actions.pdf') <> 1 then
raise Exception.Create('Save failed');
finally
Pdf.Free;
end;
end;
Η κλήση flatten είναι σκόπιμα σε σχόλιο. Παραλείψτε την και το έγγραφο αποστέλλεται ως ζωντανή φόρμα της οποίας οι ενέργειες ενεργοποιούνται στον αναγνώστη. Ενεργοποιήστε την και το πεδίο αποδίδεται ως στατικά σημάδια, κάτι που είναι αυτό που θέλετε όταν η φόρμα έχει συμπληρωθεί και το αποτέλεσμα πρέπει να ταξιδέψει ως σταθερό αρχείο. Το ίδιο πεδίο, ο ίδιος κώδικας, δύο πολύ διαφορετικά έγγραφα ανάλογα με το αν το παγώνετε.
Επιλέγοντας τη σωστή ενέργεια
Οι τέσσερις ενέργειες χωρίζονται ξεκάθαρα από το τι αγγίζουν. Μια ονομαστική ενέργεια (named action) μετακινεί το viewport και δεν χρειάζεται πεδίο. Μια ενέργεια Hide αλλάζει την ορατότητα και χρειάζεται τίτλους πεδίων, με την κωδικοποίηση string-έναντι-πίνακα να διεκπεραιώνεται αυτόματα για εσάς. Μια ενέργεια εισαγωγής δεδομένων φτάνει σε ένα αρχείο στο δίσκο και επομένως είναι απαγορευμένη στο PDF/A. Μια ενέργεια JavaScript εκτελεί αυθαίρετη λογική και είναι προτιμότερο να χωριστεί μεταξύ ενός καθολικού πακέτου συναρτήσεων και μικρών κλήσεων ανά ενέργεια. Επιλέξτε την απλούστερη που κάνει τη δουλειά: μια ενέργεια Hide είναι πιο φορητή από ένα σενάριο που ορίζει μια σημαία απόκρυψης, και μια ονομαστική ενέργεια είναι πιο ανθεκτική από έναν αποθηκευμένο προορισμό σελίδας επειδή δεν υπάρχει αριθμός για συντήρηση.
Από εδώ, δύο γειτονικά θέματα ολοκληρώνουν την εικόνα. Εάν η φόρμα είναι μέρος ενός προσβάσιμου εγγράφου, το δέντρο δομής που διαβάζουν οι αναγνώστες οθόνης καλύπτεται στο άρθρο μας για το tagged PDF και τη δομή προσβασιμότητας. Όταν η συμπληρωμένη φόρμα πρέπει να κλειδωθεί και να υπογραφεί, η ροή εργασίας περιγράφεται στον οδηγό για τον πάγκο εργασίας συμμόρφωσης και υπογραφής. Και τα τρία βασίζονται στην ίδια μηχανή, η οποία διατίθεται ως η PDF library για Delphi μαζί με τα API δημιουργίας, φόρμας και υπογραφής που καλύπτονται αλλού σε αυτό το ιστολόγιο.