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

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

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

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

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

Όταν δημιουργείτε ένα PDF από το μηδέν, το HotPDF ελέγχει ολόκληρο το object model. Κάθε σελίδα είναι ένα εγγράψιμο THPDFPage wrapper, και η προσθήκη ενός πεδίου κειμένου μέσω του AddTextField συνδέει το νέο widget στο annotation object της σελίδας, στο page object και στη συλλογή πεδίων της φόρμας, και μετά δημιουργεί ένα appearance stream από τους πόρους γραμματοσειρών του εγγράφου. Το appearance stream είναι η ορατή επιφάνεια του widget, το πλαίσιο και το περίγραμμα και οποιοδήποτε προεπιλεγμένο κείμενο, ζωγραφισμένα ως PDF drawing operators που ο viewer αποδίδει αυτούσια.

Ένα φορτωμένο έγγραφο δεν σας δίνει καμία από αυτή τη σκαλωσιά. Οι σελίδες εισέρχονται ως ακατέργαστα dictionaries, δεν υπάρχει εγγράψιμο THPDFPage wrapper για να κρεμάσετε πάνω του ένα widget, και το σημαντικότερο δεν υπάρχει έτοιμη font resource pipeline για να ζωγραφίσει appearance streams. Η διαδρομή του φορτωμένου εγγράφου ακολουθεί λοιπόν διαφορετικό μονοπάτι. Γράφει τα field dictionaries κατευθείαν πάνω στο αναλυμένο object graph και προσπελαύνει τις σελίδες με μηδενικής βάσης index αντί για page object. Οι τύποι πεδίων και τα flag bits ταιριάζουν ακριβώς με τη διαδρομή της δημιουργίας από το μηδέν, οπότε ένα Text field είναι Text field και στις δύο περιπτώσεις, αλλάζει όμως η υποδομή από κάτω και, κυρίως, ο τρόπος που σχεδιάζεται η επιφάνεια του widget.

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

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

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

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

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

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;

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

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

Τα ComboBox και ListBox είναι και τα δύο choice fields, τύπου πεδίου /Ch στο ISO 32000-1 §12.7.4. Η διαφορά ανάμεσα σε ένα dropdown και σε μια κυλιόμενη λίστα είναι ένα bit στο ακέραιο flags του πεδίου /Ff: το bit 18, το Combo flag, με τιμή $40000. Το HotPDF θέτει αυτό το bit για το AddLoadedComboBox και το αφήνει καθαρό για το AddLoadedListBox· κατά τα άλλα τα δύο είναι ίδια, και και τα δύο παίρνουν τις επιλογές τους ως ανοικτό array strings που γράφεται στην καταχώριση /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 ως απλό string, όπου το export value και η εμφανιζόμενη ετικέτα είναι το ίδιο κείμενο. Το ISO 32000-1 §12.7.4.4 επιτρέπει επίσης τη μορφή δύο στοιχείων [export display] όταν θέλετε η υποβληθείσα τιμή να διαφέρει από αυτό που διαβάζει ο χρήστης· οι μέθοδοι δημιουργίας σε φορτωμένο έγγραφο χρησιμοποιούν τη απλούστερη μορφή ενός string, οπότε αν χρειάζεστε διαφορετικά export και display values, θα τα ορίζατε εσείς στο παραγόμενο dictionary. Και η τιμή που δίνετε ως τρέχουσα επιλογή του πεδίου πρέπει να είναι μία από τις επιλογές που δώσατε, γιατί ο viewer τη συγκρίνει με τη λίστα.

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

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

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

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

Μερικοί ρεαλιστικοί περιορισμοί

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

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

Το loaded-document form API που παρουσιάζεται εδώ διατίθεται ως μέρος του τυπικού HotPDF Component για Delphi και C++Builder, μαζί με την πλήρη αναφορά για τα field flags, τον χειρισμό των appearance και όλο το υπόλοιπο AcroForm model.