Τεχνικό Άρθρο

Προσθήκη Πεδίων AcroForm σε Φορτωμένο PDF στο Delphi

Έχετε ένα πρότυπο τιμολογίου τρίτου ή ένα αρχειοθετημένο συμβόλαιο που κάποιος δημιούργησε πριν από χρόνια σε λογισμικό που δεν μπορεί πια να βρει κανείς, και η απαίτηση είναι να γίνει διαδραστικό: να βάλετε ένα πλαίσιο υπογραφής στη γωνία, να προσθέσετε μερικά πεδία κειμένου, να μετατρέψετε μια επίπεδη λίστα ελέγχου σε κανονικά checkbox. Το δύσκολο σημείο είναι ότι δεν δημιουργείτε αυτό το PDF από την αρχή. Υπάρχει ήδη, έχει ήδη σελίδες, ροές περιεχομένου και γραμματοσειρές που δεν ελέγχετε, και πρέπει να εμφυτεύσετε widgets AcroForm σε αυτό το γράφημα αντικειμένων χωρίς να το ξαναχτίσετε. Αυτό είναι διαφορετικό πρόβλημα από τη δημιουργία μιας φόρμας σε ένα καινούργιο έγγραφο, και το σημείο που μπερδεύει τους περισσότερους είναι αόρατο μέχρι να ανοίξετε το αποτέλεσμα σε ένα πρόγραμμα προβολής και τα πεδία που μόλις γράψατε να μην φαίνονται πουθενά στη σελίδα

Το HotPDF είναι ένα εγγενές στοιχείο VCL PDF για Delphi και C++Builder, και από την έκδοση v2.247.0 εκθέτει μια ειδική οικογένεια μεθόδων ακριβώς για αυτό: να δημιουργείτε και τους έξι τυπικούς τύπους πεδίων απευθείας σε ένα έγγραφο φορτωμένο με LoadFromFile. Αυτό το άρθρο εξηγεί τι κάνουν αυτές οι μέθοδοι, το λεξικό ISO 32000-1 που συνθέτουν και τη μία σημαία χωρίς την οποία όλη η διαδικασία παράγει σιωπηλά ένα αρχείο που δείχνει άδειο

Γιατί η δημιουργία πεδίων σε φορτωμένο έγγραφο ακολουθεί ξεχωριστή διαδρομή κώδικα

Όταν χτίζετε ένα PDF από το μηδέν, το HotPDF κατέχει ολόκληρο το μοντέλο αντικειμένων. Κάθε σελίδα είναι ένα εγγράψιμο THPDFPage wrapper, και η προσθήκη ενός πεδίου κειμένου μέσω AddTextField συνδέει το νέο widget στο αντικείμενο σχολιασμών της σελίδας, στο αντικείμενο της σελίδας και στη συλλογή πεδίων της φόρμας, και έπειτα δημιουργεί μια ροή εμφάνισης από τους πόρους γραμματοσειρών του εγγράφου. Η ροή εμφάνισης είναι η ορατή επιφάνεια του widget, το πλαίσιο, το περίγραμμα και οποιοδήποτε προεπιλεγμένο κείμενο, ζωγραφισμένα ως εντολές σχεδίασης PDF που ο viewer αποδίδει κατά γράμμα

Ένα φορτωμένο έγγραφο δεν σας δίνει τίποτα από αυτά τα στηρίγματα. Οι σελίδες ήρθαν ως ακατέργαστα λεξικά· δεν υπάρχει εγγράψιμο THPDFPage wrapper για να κρεμάσετε πάνω του ένα widget, και το σημαντικότερο δεν υπάρχει έτοιμη γραμμή πόρων γραμματοσειρών για να ζωγραφίσει ροές εμφάνισης. Η διαδρομή του φορτωμένου εγγράφου ακολουθεί λοιπόν άλλη πορεία. Γράφει τα λεξικά πεδίων απευθείας στο αναλυμένο γράφημα αντικειμένων και απευθύνεται στις σελίδες με δείκτη που ξεκινά από το μηδέν αντί για αντικείμενο σελίδας. Οι τύποι πεδίων και τα bits των σημαιών ταιριάζουν ακριβώς με τη διαδρομή από το μηδέν, άρα ένα πεδίο Text παραμένει πεδίο Text με κάθε τρόπο· αυτό που αλλάζει είναι τα εσωτερικά σωληνώματα και, κυρίως, το πώς σχεδιάζεται η επιφάνεια του widget

Η σημαία /NeedAppearances δεν είναι προαιρετική εδώ

Αυτό είναι το μοναδικό γεγονός που αποφασίζει αν η δουλειά σας θα εμφανιστεί. Επειδή η διαδρομή του φορτωμένου εγγράφου δεν δημιουργεί ροές εμφάνισης, ένα widget που μόλις προστέθηκε φτάνει στο πρόγραμμα προβολής χωρίς καμία /APκαταχώριση: ένα πεδίο χωρίς περιγεγραμμένη επιφάνεια. Πολλά προγράμματα προβολής, όταν τους ζητηθεί να αποδώσουν ένα widget που δεν έχει εμφάνιση και καμία οδηγία να τη δημιουργήσουν, δεν σχεδιάζουν τίποτα. Το πεδίο υπάρχει στο αρχείο, είναι δομικά έγκυρο, μπορεί να το προσπελάσει ένα εργαλείο συμπλήρωσης φόρμας και είναι εντελώς αόρατο για έναν άνθρωπο

Το σημείο διαφυγής ορίζεται στο ISO 32000-1 §12.7.3: το λεξικό AcroForm φέρει μια /NeedAppearances boolean, και όταν είναι trueένας συμβατός αναγνώστης οφείλει να κατασκευάσει μόνος του τις χαμένες ροές εμφάνισης από το /DA (default appearance) string και την τιμή κάθε πεδίου. Το HotPDF το ρυθμίζει αυτό για εσάς. Την πρώτη φορά που προσθέτετε οποιοδήποτε πεδίο σε ένα φορτωμένο έγγραφο, EnsureLoadedAcroForm τρέχει: αν ο κατάλογος δεν έχει /AcroForm το δημιουργεί, αν δεν υπάρχει /Fields array το δημιουργεί, και επιβάλλει /NeedAppearances true. Δεν το καλείτε απευθείας, αλλά το ότι υπάρχει εξηγεί τη συμπεριφορά. Εξηγεί επίσης μια λεπτομέρεια ανάπτυξης που αξίζει να ειπωθεί καθαρά: μερικά ελαφριά ή μη συμβατά προγράμματα προβολής αγνοούν /NeedAppearances και πάλι δεν εμφανίζουν τίποτα. Για τα συνηθισμένα προγράμματα ανάγνωσης η σημαία κάνει τη δουλειά της, αλλά αν το κοινό σας χρησιμοποιεί έναν ασυνήθιστο ενσωματωμένο renderer, δοκιμάστε εκεί πριν υποσχεθείτε οτιδήποτε

Προσθήκη των έξι τύπων πεδίων

Κάθε μέθοδος ακολουθεί το ίδιο σχήμα. Δίνετε τον δείκτη σελίδας που ξεκινά από το μηδέν, τις τέσσερις γωνίες του ορθογωνίου του widget σε συντεταγμένες user-space του PDF, το όνομα του πεδίου και όσα επιπλέον ορίσματα χρειάζεται ο τύπος. Το ορθογώνιο είναι X1, Y1, X2, Y2 με την αρχή του PDF στο κάτω αριστερό μέρος της σελίδας, άρα οι μεγαλύτερες τιμές Y βρίσκονται ψηλότερα· αυτή είναι η σύμβαση συντεταγμένων του μορφότυπου αρχείου, όχι η σύμβαση της οθόνης με την αρχή επάνω αριστερά, και το να τα μπερδέψετε είναι το δεύτερο πιο συνηθισμένο λάθος μετά το να ξεχάσετε τη σημαία. Κάθε κλήση επιστρέφει τον νέο δείκτη του πεδίου που ξεκινά από το μηδέν ή -1 αν ο δείκτης της σελίδας ήταν εκτός εύρους ή το αντικείμενο της σελίδας δεν μπορούσε να επιλυθεί

var
  Pdf: THotPDF;
  Idx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('contract.pdf') <= 0 then Exit;

    // Text field: name, initial value, max length (0 = unlimited)
    Idx := Pdf.AddLoadedTextField(0, 72, 680, 320, 700, 'FullName', '', 0);

    // CheckBox: export value, initial checked state
    Pdf.AddLoadedCheckBox(0, 72, 640, 90, 658, 'AgreeTerms', 'Yes', False);

    // Signature field: just a name and a rectangle
    Pdf.AddLoadedSignatureField(0, 360, 72, 540, 132, 'ApproverSig');

    if Idx >= 0 then
      Pdf.SaveLoadedDocument('contract-interactive.pdf');
  finally
    Pdf.Free;
  end;
end;

Τα τρίτο και τέταρτο ορίσματα συμβολοσειράς του πεδίου κειμένου είναι το όνομα του πεδίου και η αρχική του /V τιμή· ο ακέραιος είναι /MaxLen, και γράφεται μόνο όταν είναι μεγαλύτερος από μηδέν. Το HotPDF δίνει σε κάθε επεξεργάσιμο πεδίο μια προεπιλεγμένη συμβολοσειρά εμφάνισης /Helv 12 Tf 0 0 0 rg, η οποία είναι αυτό που διαβάζει ένα /NeedAppearancesπρόγραμμα προβολής που σέβεται το /DA για να αποφασίσει τη γραμματοσειρά και το χρώμα με τα οποία αποδίδει την τιμή. Το checkbox δέχεται μια export value, τη συμβολοσειρά που υποβάλλει η φόρμα όταν το πλαίσιο επισημαίνεται, συν μια boolean για την αρχική κατάσταση· εσωτερικά γράφει τις αντίστοιχες /V, /AS, και /DV καταχωρίσεις ονομάτων, ώστε η κατάσταση on/off να είναι συνεπής τη στιγμή που ανοίγει το αρχείο. Μια κενή export value προεπιλέγει το Yes, το καθιερωμένο όνομα "on" του checkbox

Πεδία επιλογής και τα bit flags του /Ff

Το ComboBox και το ListBox είναι και τα δύο πεδία επιλογής, τύπος πεδίου /Ch στο ISO 32000-1 §12.7.4. Η διαφορά ανάμεσα σε ένα αναπτυσσόμενο μενού και μια λίστα κύλισης είναι ένα bit στον ακέραιο flags του πεδίου /Ff: bit 18, η σημαία Combo, τιμή $40000. Το HotPDF ορίζει αυτό το bit για AddLoadedComboBox και το αφήνει μη ορισμένο για AddLoadedListBox; διαφορετικά τα δύο είναι ταυτόσημα και και τα δύο παίρνουν τις επιλογές τους ως ανοικτό πίνακα συμβολοσειρών που γράφεται στο /Opt καταχώριση

// Dropdown (Combo flag set internally) with an initial selection
Pdf.AddLoadedComboBox(0, 72, 600, 300, 620, 'Country', 'Canada',
  ['United States', 'Canada', 'Mexico']);

// Scrolling list, no initial value
Pdf.AddLoadedListBox(0, 72, 520, 300, 590, 'Priority', '',
  ['Low', 'Normal', 'High']);

// Push button with a caption drawn through /MK
Pdf.AddLoadedPushButton(0, 360, 600, 480, 626, 'SubmitBtn', 'Submit');

Δύο σημειώσεις για τη λίστα επιλογών. Το HotPDF γράφει κάθε /Opt καταχώριση ως απλή συμβολοσειρά, όπου η τιμή εξαγωγής και η εμφανιζόμενη ετικέτα είναι το ίδιο κείμενο. Το ISO 32000-1 §12.7.4.4 επιτρέπει επίσης τη μορφή δύο στοιχείων [export display] όταν χρειάζεστε η τιμή που υποβάλλεται να διαφέρει από αυτό που διαβάζει ο χρήστης· οι μέθοδοι δημιουργίας σε φορτωμένο έγγραφο χρησιμοποιούν την απλούστερη μορφή μίας συμβολοσειράς, οπότε αν χρειάζεστε διαφορετικές τιμές εξαγωγής και εμφάνισης θα τις ορίσετε μόνοι σας στο λεξικό που προκύπτει. Και η τιμή που περνάτε ως τρέχουσα επιλογή του πεδίου θα πρέπει να είναι μία από τις επιλογές που δώσατε, επειδή το πρόγραμμα προβολής τη συγκρίνει με τη λίστα

Το push button είναι η άλλη περίπτωση που καθορίζεται από σημαία: τύπος πεδίου /Btn με bit 17, η σημαία PushButton, τιμή $10000. Αυτό το bit είναι που ξεχωρίζει ένα κουμπί με δυνατότητα κλικ από ένα checkbox, το οποίο είναι επίσης πεδίο /Btn αλλά χωρίς αυτό. Η λεζάντα που περνάτε γράφεται στο λεξικό appearance characteristics /MK ως κανονική λεζάντα /CA. Αξίζει να είμαστε ειλικρινείς για το εύρος εδώ: το κουμπί δημιουργείται με την ετικέτα και το ορθογώνιό του, αλλά η μέθοδος δημιουργίας σε φορτωμένο έγγραφο δεν συνδέει ενέργεια, οπότε από μόνο του είναι ένα κουμπί που δείχνει σωστό και δεν κάνει τίποτα όταν γίνει κλικ. Η σύνδεση ενεργειών submit, reset ή JavaScript είναι ξεχωριστό ζήτημα· για τη δημιουργία από το μηδέν, η ροή εργασίας πεδίου και ενέργειας καλύπτεται στο τη δημιουργία πεδίων και ενεργειών AcroForm στο Delphi, που είναι το σωστό σημείο σύγκρισης για όσα η διαδρομή του φορτωμένου εγγράφου αφήνει σκόπιμα εκτός

Το λεξικό που μοιράζονται όλα τα πεδία

Κάτω από και τις έξι μεθόδους υπάρχει ένας κοινός builder που κατασκευάζει το widget annotation και το καταχωρίζει σε δύο σημεία. Γράφει /Type /Annot και /Subtype /Widget, τον /Rect πίνακα από τις τέσσερις συντεταγμένες σας, τις σημαίες του annotation /F 4 που ορίζει το Print bit ώστε το πεδίο να εμφανίζεται τόσο σε χαρτί όσο και στην οθόνη, το όνομα του πεδίου /T, τον τύπο πεδίου /FT, τις σημαίες /Ff, και μια /P αντίστροφη αναφορά στο αντικείμενο της σελίδας. Έπειτα προσθέτει το νέο πεδίο στον πίνακα του AcroForm /Fields πίνακα και στον /Annots πίνακα της σελίδας, επιλύοντας τις έμμεσες αναφορές στην πορεία ώστε να επεκτείνει τους πραγματικούς πίνακες αντί να αφήνει το widget ορφανό

Αυτή η διπλή καταχώριση έχει σημασία επειδή ένα widget που υπάρχει μόνο σε μία από τις δύο λίστες είναι σπασμένο με τρόπο ύπουλο. Ένα πεδίο που υπάρχει στο /Fields αλλά λείπει από το /Annots είναι γνωστό στη φόρμα αλλά δεν ζωγραφίζεται ποτέ· το αντίστροφο ζωγραφίζεται αλλά είναι άγνωστο στη λογική της φόρμας. Το HotPDF τα κρατά συγχρονισμένα σε κάθε προσθήκη, και αυτό είναι το είδος εσωτερικής τήρησης που αλλιώς θα έπρεπε να πετύχετε με απόλυτη ακρίβεια με το χέρι απέναντι στο spec

Μερικοί ειλικρινείς περιορισμοί

Βάλτε τις προσδοκίες στη σωστή θέση πριν χτίσετε μια ροή εργασίας πάνω σε αυτό. Η συμπεριφορά flatten-and-regenerate εξαρτάται από το αν το πρόγραμμα προβολής τηρεί το /NeedAppearances, η οποία καλύπτει το Acrobat, τις σύγχρονες μηχανές PDF των browsers και τα συνηθισμένα desktop readers, αλλά δεν αποτελεί απόλυτη εγγύηση σε κάθε renderer που κυκλοφορεί. Αν πρέπει να παραγάγετε ένα αρχείο του οποίου τα πεδία αποδίδονται ίδια παντού, ακόμη και σε viewers που αγνοούν τη σημαία, βρίσκεστε στην περιοχή των appearance streams και η διαδρομή δημιουργίας από το μηδέν που ζωγραφίζει /AP για εσάς είναι η καλύτερη επιλογή. Το πεδίο υπογραφής, επίσης, δημιουργείται ως κενό widget υπογραφής έτοιμο να υπογραφεί· η τοποθέτηση του πεδίου δεν είναι το ίδιο με την εφαρμογή μιας κρυπτογραφικής υπογραφής

Για την αλλαγή όσων ήδη υπάρχουν αντί για την προσθήκη σε αυτά, η σχετική λειτουργία είναι το form flattening, όπου ενσωματώνετε ξανά τα διαδραστικά πεδία στο στατικό περιεχόμενο της σελίδας ώστε οι τιμές να γίνουν μόνιμες και μη επεξεργάσιμες· αυτή η διαδρομή επιστροφής, μαζί με τον τρόπο χειρισμού των φορμών που περιέχουν XFA, συζητείται στο το flattening των πεδίων XFA και AcroForm στο Delphi. Η προσθήκη πεδίων και το flattening πεδίων είναι δύο άκρα του ίδιου κύκλου ζωής: αυτό το άρθρο δείχνει πώς βάζετε διαδραστικότητα σε ένα έγγραφο που δεν την είχε, και το flattening είναι πώς την αφαιρείτε ξανά όταν η φόρμα έχει εξυπηρετήσει τον σκοπό της

Το API φορμών για φορτωμένα έγγραφα που παρουσιάζεται εδώ περιλαμβάνεται ως μέρος του τυπικού HotPDF Component για Delphi και C++Builder, μαζί με την πλήρη αναφορά για τις σημαίες πεδίων, τον χειρισμό της εμφάνισης και το υπόλοιπο μοντέλο AcroForm