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

Κληρονομημένες τιμές πεδίων AcroForm και resets στο Delphi

Το HotPDF Delphi Component μεταχειρίζεται τα /FT, /Ff, /V και /DV σε φορτωμένο πεδίο AcroForm ως κληρονομήσιμα attributes, λυμένα περπατώντας την αλυσίδα /Parent. Από το v2.754.3 και το v2.754.4, ένα επώνυμο child του οποίου ο τύπος έρχεται από τον γονέα του μένει ατομικά προσβάσιμο, το RemoveFormField αφήνει τα αδέλφια του ήσυχα, και το ResetLoadedFormField αντιγράφει το κληρονομημένο default με τον πρωτότυπο PDF object τύπο του. Πριν από αυτό, ένας εκπληκτικός αριθμός συνηθισμένων φορμών διαβαζόταν λάθος

Η φόρμα που ξεσκεπάζει όλα αυτά δεν είναι εξωτική. Ένα εργαλείο συγγραφής χτίζει group node group που κουβαλά /FT /Ch, τις σημαίες πεδίου και τη λίστα επιλογών μία φορά, και κρεμά κάτω του δύο επώνυμα children a και b, το καθένα merged dictionary πεδίου-συν-widget με τίποτα παρά /T, /Parent, /Rect και το δικό του /V. Είναι απόλυτα νόμιμος τρόπος μοιράσματος attributes, και είναι ακριβώς η περίπτωση που η ενότητα Limits του ορισμός τιμών πεδίων φόρμας σε φορτωμένο PDF με Delphi σημείωνε ως ανεπεξέργαστη: η συμφιλίωση κουμπιών κοίταζε μόνο το τοπικό /FT. Αυτό το άρθρο παίρνει από εκεί που εκείνο σταμάτησε, καλύπτοντας πώς ταξινομείται το δέντρο πεδίων, πώς διαβάζονται οι κληρονομημένες τιμές, και τι επιτρέπεται να γράψει ένα reset ενός μόνο πεδίου

Ποιες εγγραφές AcroForm μπορεί να κληρονομήσει ένα πεδίο από τον γονέα του;

Το ISO 32000-1 §12.7.3.1, Πίνακας 220, σημειώνει τα /FT, /Ff, /V και /DV ως κληρονομήσιμα, και ο Πίνακας 229 στο §12.7.4.3 κάνει το ίδιο για το /MaxLen ενός πεδίου text, οπότε οποιοσδήποτε reader κοιτάζει μόνο το τοπικό dictionary θα αναφέρει λάθος τύπο, λάθος σημαίες και κενή τιμή για ένα απόλυτα έγκυρο child. Το HotPDF διοχετεύει όλες αυτές τις αναγνώσεις μέσω ενός εσωτερικού resolver, του HPDFLoadedInheritedFieldObject, που ελέγχει το dictionary για το κλειδί, λύνει indirect reference αν βρει, και αλλιώς ακολουθεί /Parent για το πολύ 128 επίπεδα, επειδή κακοσχηματισμένα αρχεία μπορούν να χτίσουν κύκλους /Parent που δεν έχουν καμία σχέση με /Kids. Τα δημόσια getters κάθονται από πάνω του: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue και οι option helpers GetLoadedFormFieldOptionCount και GetLoadedFormFieldOptions, που επίσης σηκώνουν πίνακα /Opt αποθηκευμένο στον γονέα. Ένας κανόνας στον resolver είναι εύκολο να πάει στραβά: το πέρασμα σταματά στο πρώτο dictionary που περιέχει το κλειδί, ακόμα κι αν η τιμή εκεί είναι κενό string. Ένα τοπικό /V () είναι σκόπιμο override που κρύβει τον γονέα, όχι κενό προς γέμισμα από ψηλότερα στο δέντρο

Διάγραμμα κληρονομημένων attributes AcroForm του HotPDF: ένα group node κουβαλά /FT, /Ff και /Opt μία φορά ενώ τα επώνυμα children group.a και group.b κρατούν μόνο /T, /Parent, /Rect και τοπικό /V, δείχνοντας το HPDFLoadedInheritedFieldObject να περπατά /Parent έως 128 επίπεδα όπου το πρώτο dictionary που κρατά κλειδί κερδίζει και κενή τοπική τιμή κρύβει τον γονέα
Το HotPDF λύνει /FT, /Ff, /V, /DV και /Opt μέσω ενός resolver που περπατά γονείς, οπότε επώνυμο child μένει προσβάσιμο ενώ κενή τοπική τιμή σκόπιμα υπερισχύει ό,τι κουβαλά η ομάδα από πάνω του
var
  Pdf: THotPDF;
  Field: THPDFLoadedFormField;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
    // Το 'group' κουβαλά /FT /Ch, /Ff 131078 και /Opt· το child
    // 'group.b' κουβαλά μόνο /T, /Parent, /Rect και το δικό του /V
    Field := Pdf.GetFormField('group.b');
    try
      if Pdf.GetFormFieldType(Field.Index) = lfftChoice then
      begin
        // 131078 = Combo (bit 18) + NoExport (bit 3) + Required (bit 2)
        Writeln(Pdf.GetLoadedFormFieldFlags(Field.Index));
        Writeln(Pdf.IsFormFieldRequired(Field.Index));    // TRUE
        Writeln(Pdf.GetLoadedFormFieldOptionCount(Field.Index));
        Writeln(Pdf.GetFormFieldValue(Field.Index));       // το τοπικό /V
      end;
    finally
      Field.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

Γιατί το τοπικό /FT είναι λάθος τεστ για πεδίο terminal;

Επειδή ένας γονέας μπορεί να προμηθεύει τον τύπο και να κατέχει ταυτόχρονα επώνυμα child πεδία, οπότε η παρουσία /FT δεν λέει τίποτα για το πού τελειώνει το δέντρο πεδίων. Ο παλιός traversal δήλωνε node terminal όποτε είχε δικό του /FT ή ήταν χωρίς /Kids. Στη φόρμα παραπάνω, το group έχει και /FT /Ch και /Kids, οπότε καταχωριόταν ως ένα πεδίο με όνομα group με δύο widgets, και τα πλήρως προσδιορισμένα ονόματα group.a και group.b απλώς εξαφανίζονταν. Το GetFormFieldCount επέστρεφε 1, μια αναζήτηση με όνομα child αποτύγχανε, και το SetFormFieldValue μπορούσε να γράψει μόνο τον κοινό γονέα. Το αντικαταστατικό τεστ, HPDFLoadedFieldHasChildFields, κοιτάζει τα kids αντί για τον γονέα: kid είναι child πεδίο αν έχει δικό του /T, έχει δικό του /Kids, ή δεν είναι καθόλου dictionary /Subtype /Widget. Μόνο όταν κανένα kid δεν πληροί τα κριτήρια η node είναι terminal, με τα kids της να μεταχειρίζονται ως widget annotations της

Οι δύο οριακές περιπτώσεις που διαμόρφωσαν εκείνο τον κανόνα έρχονται και οι δύο από merged dictionaries, που το §12.7.3.1 επιτρέπει όταν ένα πεδίο έχει ένα widget. Ένα επώνυμο merged dictionary κουβαλά /Subtype /Widget και εξακολουθεί να είναι child πεδίο, οπότε το subtype μόνο του δεν μπορεί να το στείλει στην ανώνυμη λίστα widgets του γονέα· το /T κερδίζει. Το αντίστροφο επίσης συμβαίνει: κάποιοι παραγωγοί επαναλαμβάνουν το /FT του γονέα σε κάθε ανώνυμο widget, οπότε το /FT δεν μπορεί να χρησιμοποιηθεί ως απόδειξη ότι ένα widget ξεκινά νέο πεδίο. Η ταξινόμηση μοιράζεται από τη relationship cache, το FormFieldExists και το RemoveFormField, και κάθε ένα από εκείνα τα περάσματα τώρα καταγράφει τα dictionaries που έχει ήδη επισκεφτεί και σταματά πέρα από 128 επίπεδα. Ένα regression αρχείο του οποίου το group αναφέρει τον εαυτό του δύο φορές, /Kids [5 0 R 5 0 R 6 0 R 7 0 R], αναφέρει ακόμα ακριβώς δύο πεδία αντί να αναδύεται ατελείωτα ή να μετρά την ίδια node δύο φορές

Πώς αποφεύγει το RemoveFormField να σβήσει αδελφικά πεδία;

Το RemoveFormField τώρα σβήνει μόνο το child που κατονομάζετε, επειδή η ανεύρεση και η διαγραφή επιτέλους συμφωνούν για το τι είναι πεδίο terminal. Εκείνη η συμφωνία έχει μεγαλύτερη σημασία απ' όση φαίνεται. Το overload με όνομα λύνει δείκτη μέσω της relationship cache και μετά μετρά πεδία terminal σε δεύτερο πέρασμα πάνω από /AcroForm /Fields. Μόλις η cache διορθώθηκε να βλέπει group.a και group.b, ένα αδιόρθωτο πέρασμα διαγραφής θα εξακολουθούσε να μεταχειρίζεται το group ως μοναδικό πεδίο terminal, και ο δείκτης 0 θα είχε σβήσει τον γονέα μαζί με κάθε αδελφό και όλα τα widgets τους. Το πέρασμα διαγραφής τώρα χρησιμοποιεί το ίδιο τεστ HPDFLoadedFieldHasChildFields και το ίδιο visited set, μαζεύει τα widget annotations μόνο του αφαιρεθέντος child, τα ξεσκίζει από τα /Annots κάθε σελίδας, και σβήνει τον γονέα μόνο όταν ο πίνακας /Kids του καταλήξει άδειος. Το regression ελέγχει και τα τρία σημεία όπου ένα λάθος θα φαινόταν: τα /Kids του γονέα, τα /Annots της σελίδας, και την τιμή και appearance του επιζώντος αδελφού, τόσο μετά από πλήρες ξαναγράψιμο όσο και μετά από incremental update

Διάγραμμα επιβίωσης αδελφών του HotPDF RemoveFormField: το πέρασμα διαγραφής ξαναχρησιμοποιεί HPDFLoadedFieldHasChildFields και το visited set της ανεύρεσης, ξεσκίζει μόνο το επώνυμο child group.a από AcroForm /Fields και τα /Annots της σελίδας, και κρατά τον κοινό γονέα ενώ ο πίνακας /Kids του κρατά ακόμα το επιζών group.b
Ανεύρεση και διαγραφή επιτέλους συμφωνούν για το τι είναι πεδίο terminal, οπότε η αφαίρεση ενός επώνυμου child αφήνει την τιμή και appearance του αδελφού του άθικτες μετά από πλήρες ξαναγράψιμο ή incremental update
// Αφαίρεση ενός επώνυμου child· το αδελφό του και ο κοινός γονέας επιζητούν
Pdf.RemoveFormField('group.a');

Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// Τύπος, σημαίες και επιλογές εξακολουθούν να λύνονται μέσω του γονέα
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');

Τι γράφει το ResetLoadedFormField όταν το default είναι κληρονομημένο;

Το ResetLoadedFormField γράφει τοπικό /V που είναι φρέσκο αντίγραφο του κληρονομημένου /DV με τον ίδιο PDF object τύπο, και επικυρώνει ολόκληρο το default πριν αγγίξει το πεδίο. Ο object τύπος έχει σημασία επειδή τα scalar getters ισοπεδώνουν τα πάντα σε κείμενο. Default checkbox είναι name όπως /Yes, default multi-select list box είναι πίνακας strings, και default text μπορεί να είναι hexadecimal string UTF-16· η αντιγραφή οποιουδήποτε από αυτά μέσω GetLoadedFormFieldDefaultValue θα μετέτρεπε το name σε string, τον πίνακα σε κενό string και το hex string στα literal ψηφία του. Το reset λοιπόν διακλαδώνεται στον κληρονομημένο τύπο: πεδία text και choice παίρνουν νέο string object που κρατά τη σημαία IsHexadecimal, πεδία choice με πίνακα default παίρνουν νέο πίνακα από νέα strings, και κουμπιά μη-pushbutton παίρνουν νέο name object. Η αντιγραφή, αντί να δείχνει στα objects του γονέα, είναι σκόπιμη: ένα /V που μοιραζόταν τον πίνακα /DV του γονέα ή τον object αριθμό του θα άλλαζε το default την επόμενη φορά που κάποιος θα επεξεργαζόταν την τιμή. Default λάθος τύπου, ή πίνακας choice που περιέχει οτιδήποτε άλλο εκτός strings, πετάει εξαίρεση και αφήνει /V και /I ακριβώς όπως ήταν. Τα pushbuttons, που δεν έχουν τιμή (Πίνακας 226, bit 17), και τα signature fields γυρνάνε στο παλιότερο μονοπάτι μόνο-string

Διάγραμμα typed reset του HotPDF: το ResetLoadedFormField διακλαδώνεται στον κληρονομημένο /DV object τύπο, γράφοντας φρέσκο name object για checkbox, νέο πίνακα από νέα strings για multi-select choice, string που κρατά IsHexadecimal για hex text, κενό string ή /Off όταν δεν υπάρχει /DV, και πετώντας χωρίς να αγγίξει /V ή /I σε mismatch τύπου
Η αντιγραφή αντί να δείχνει στα objects του γονέα εμποδίζει μελλοντική επεξεργασία τιμής να αλλάξει σιωπηλά το default, και pushbuttons συν signature fields γυρνάνε στο παλιότερο μονοπάτι μόνο-string

Όταν δεν υπάρχει /DV πουθενά στην αλυσίδα, η μέθοδος κρατά το συμβόλαιο καθαρίσματος της γράφοντας τοπικό κενό string, ή /Off για πεδίο checkbox ή radio. Το σβήσιμο του τοπικού /V θα φαινόταν πιο καθαρό και θα ήταν λάθος: ο γονέας μπορεί να κρατά τρέχουσα τιμή, και η αφαίρεση του override του child θα έφερνε σιωπηλά πίσω εκείνη την τιμή. Αυτό είναι και ο λόγος που ένα reset ενός πεδίου δεν είναι η ενέργεια ResetForm του §12.7.5.3, που ένας viewer τρέχει πάνω από σύνολο πεδίων όταν ο χρήστης πατά κουμπί, όπως περιγράφεται στο χτίσιμο πεδίων και ενεργειών AcroForm με HotPDF. Το ResetLoadedFormField είναι λειτουργία επεξεργασίας σε ένα φορτωμένο πεδίο, με δικό του κανόνα για την περίπτωση χωρίς default, και καταγράφει το πεδίο μέσω NoteLoadedFormFieldDirty ώστε ο incremental επανυπολογισμός να βλέπει την αλλαγή

var
  Field: THPDFLoadedFormField;
begin
  Field := Pdf.GetFormField('group.a');
  try
    // Ο γονέας κρατά /DV [(b) (r)] σε MultiSelect list box: το group.a παίρνει
    // δικό του /V [(b) (r)] και φρέσκο /I [0 2]· ο γονέας μένει άθικτος
    Pdf.ResetLoadedFormField(Field.Index);
    // Τα scalar getters δεν μπορούν να αναπαραστήσουν τον πίνακα default
    Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // κενό
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('survey-reset.pdf');
end;

Κρατώντας /V, /I και /AS σε συμφωνία

Ένα reset είναι σωστό μόνο αν ο δείκτης επιλογής και η κατάσταση appearance ακολουθούν την τιμή, οπότε το ResetLoadedFormField τελειώνει με τους ίδιους δύο reconcilers με το SetFormFieldValue. Το HPDFReconcileChoiceSelection τώρα δέχεται τιμή πίνακα: σβήνει το τοπικό /I χωρίς να το μεταλλάξει, ταιριάζει κάθε τιμή απέναντι στο μισό εξαγωγής κάθε εγγραφής /Opt, και γράφει έναν νέο ταξινομημένο /I, οπότε reset σε [(b) (r)] απέναντι σε επιλογές b, g, r δίνει /I [0 2]. Το ReconcileLoadedButtonAppearanceStates τώρα ζητά τον κληρονομημένο τύπο, οπότε child checkbox του οποίου το /FT /Btn ζει στον γονέα επιτέλους παίρνει set το /AS του. Στην πλευρά εγγραφής, το SetFormFieldValue και το SetLoadedFormFieldDefaultValue αποθηκεύουν name object για κληρονομημένο κουμπί μη-pushbutton ακόμα κι όταν το child δεν έχει τοπική εγγραφή από την οποία να αντιγράψει τον τύπο. Και όταν το EnsureLoadedFieldAppearanceStream ξαναχτίζει appearances κουμπιών, γράφει /AS /Off εκτός αν η τιμή ταιριάζει την κατάσταση on, και δίνει σε κάθε state stream σωστό /Type /XObject, /Subtype /Form και /BBox· πριν το v2.754.4, η αναγέννηση του appearance μετά από reset μπορούσε να τικάρει ξανά το box πριν αποθηκευτεί το αρχείο

Όρια που αξίζει να ξέρετε πριν χτίσετε πάνω σε αυτό

Τα scalar getters μένουν scalar. Το GetFormFieldValue και το GetLoadedFormFieldDefaultValue επιστρέφουν κενό string για τιμή πίνακα, μετατρέπουν αριθμούς και booleans σε 42 ή true, και αναφέρουν hex-κωδικοποιημένο string στη hexadecimal ορθογραφία του. Κύκλος /Parent τελειώνει το πέρασμα χωρίς εξαίρεση, οπότε πεδίο του οποίου ο τύπος χάθηκε σε κύκλο αναφέρει lfftUnknown και σημαίες 0 αντί να αποτύχει. Το SetFormFieldValue και το ResetLoadedFormField γράφουν πάντα το child που διευθύνετε και ποτέ δεν προβιβάζουν τιμή στον κοινό γονέα, που είναι σωστό για ανεξάρτητα children αλλά σημαίνει ότι radio groups πρέπει να διευθύνονται μέσω του πεδίου που κατέχει την επιλογή. Και κάθε κλήση δεσμεύει ένα πεδίο από μόνη της· τίποτα εδώ δεν κάνει μια παρτίδα resets συναλλακτική

Η λύση κληρονομημένων attributes, η ενοποιημένη ταξινόμηση δέντρου πεδίων και το typed reset που περιγράφονται εδώ είναι μέρος του loaded-form API στο HotPDF Delphi Component για Delphi και C++Builder, δίπλα στη δημιουργία πεδίων που καλύπτει το προσθήκη πεδίων AcroForm σε φορτωμένο PDF στο Delphi