HotPDF Delphi Component tratta /FT, /Ff, /V e /DV di un campo AcroForm caricato come attributi ereditabili, risolti percorrendo la catena dei /Parent. Dalla v2.754.3 e v2.754.4, un figlio con nome il cui tipo viene dal genitore resta indirizzabile individualmente, RemoveFormField lascia in pace i suoi fratelli, e ResetLoadedFormField copia il default ereditato con il suo tipo di oggetto PDF originale. Prima di allora, un numero sorprendente di form ordinari veniva letto male
Il form che mette in luce tutto questo non è esotico. Uno strumento di authoring costruisce un nodo di gruppo group che porta /FT /Ch, i field flag e la lista di opzioni una volta sola, e vi appende sotto due figli con nome a e b, ciascuno un dizionario campo più widget fuso che non ha altro se non /T, /Parent, /Rect e il suo /V. È un modo perfettamente legale di condividere attributi, ed è esattamente il caso che la sezione Limits di impostare i valori dei campi form in un PDF caricato con Delphi segnava come non gestito: la riconciliazione dei button guardava solo il /FT locale. Questo articolo riprende da dove quello si fermò, coprendo come l'albero dei campi viene classificato, come i valori ereditati vengono letti, e che cosa un reset di campo singolo ha il permesso di scrivere
Quali voci AcroForm può ereditare un campo dal suo genitore?
ISO 32000-1 §12.7.3.1, Table 220, segna /FT, /Ff, /V e /DV come ereditabili, e la Table 229 in §12.7.4.3 fa lo stesso per /MaxLen di un campo testo, quindi qualunque reader che guardi solo il dizionario locale riporterà il tipo sbagliato, i flag sbagliati e un valore vuoto per un figlio perfettamente valido. HotPDF incanala tutte queste letture in un resolver interno, HPDFLoadedInheritedFieldObject, che controlla se il dizionario ha la chiave, risolve un riferimento indiretto se lo trova, e altrimenti segue /Parent per al massimo 128 livelli, perché file malformati possono costruire cicli di /Parent che non c'entrano niente con /Kids. I getter pubblici poggiano su di lui: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue e gli helper di opzioni GetLoadedFormFieldOptionCount e GetLoadedFormFieldOptions, che raccolgono anche un array /Opt memorizzato sul genitore. Una regola del resolver è facile sbagliare: la camminata si ferma al primo dizionario che contiene la chiave, anche se il valore lì è una stringa vuota. Un /V () locale è un override deliberato che maschera il genitore, non un buco da riempire risalendo l'albero
var
Pdf: THotPDF;
Field: THPDFLoadedFormField;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
// 'group' porta /FT /Ch, /Ff 131078 e /Opt; il figlio
// 'group.b' porta solo /T, /Parent, /Rect e il suo /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)); // il /V locale
end;
finally
Field.Free;
end;
finally
Pdf.Free;
end;
end;
Perché un /FT locale è il test sbagliato per un campo terminale?
Perché un genitore può fornire il tipo e possedere comunque campi figli con nome, quindi la presenza di /FT non dice nulla su dove finisce l'albero dei campi. La vecchia traversata dichiarava terminale un nodo ogni volta che aveva un suo /FT o nessun /Kids. Nel form qui sopra, group ha sia /FT /Ch sia /Kids, quindi veniva registrato come un unico campo di nome group con due widget, e i nomi pienamente qualificati group.a e group.b sparivano e basta. GetFormFieldCount restituiva 1, una ricerca per nome del figlio falliva, e SetFormFieldValue poteva scrivere soltanto il genitore condiviso. Il test sostitutivo, HPDFLoadedFieldHasChildFields, guarda i kid anziché il genitore: un kid è un campo figlio se ha un suo /T, ha un suo /Kids, o non è per niente un dizionario /Subtype /Widget. Solo quando nessun kid si qualifica il nodo è terminale, con i suoi kid trattati come annotazioni widget
I due casi limite che hanno plasmato quella regola vengono entrambi dai dizionari fusi, che §12.7.3.1 consente quando un campo ha un widget singolo. Un dizionario fuso con nome porta /Subtype /Widget e resta comunque un campo figlio, quindi il sottotipo da solo non può mandarlo nella lista dei widget anonimi del genitore; vince il /T. Vale anche il contrario: alcuni producer ripetono il /FT del genitore su ogni widget anonimo, quindi /FT non può servire come prova che un widget apra un campo nuovo. La classificazione è condivisa dalla cache delle relazioni, da FormFieldExists e da RemoveFormField, e ognuna di quelle camminate ora registra i dizionari già visitati e si ferma oltre i 128 livelli. Un file di regression il cui gruppo elenca se stesso due volte, /Kids [5 0 R 5 0 R 6 0 R 7 0 R], riporta ancora esattamente due campi invece di ricorrere all'infinito o contare due volte lo stesso nodo
Come fa RemoveFormField a non cancellare i campi fratelli?
RemoveFormField ora cancella solo il figlio che nomini, perché scoperta e cancellazione finalmente concordano su che cos'è un campo terminale. Quell'accordo conta più di quanto sembri. L'overload per nome risolve un indice attraverso la cache delle relazioni e poi conta i campi terminali in una seconda camminata su /AcroForm /Fields. Una volta sistemata la cache per vedere group.a e group.b, una camminata di cancellazione non sistemata avrebbe comunque trattato group come un unico campo terminale, e l'indice 0 avrebbe rimosso il genitore insieme a ogni fratello e a tutti i loro widget. La camminata di cancellazione ora usa lo stesso test HPDFLoadedFieldHasChildFields e lo stesso insieme dei visitati, raccoglie le annotazioni widget del solo figlio rimosso, le togli dagli /Annots di ogni pagina, e rimuove il genitore solo quando il suo array /Kids finisce vuoto. La regression controlla tutti e tre i posti dove un errore si vedrebbe: i /Kids del genitore, gli /Annots della pagina, e il valore e l'appearance del fratello sopravvissuto, sia dopo una riscrittura completa sia dopo un aggiornamento incrementale
// Rimuovi un figlio per nome; il fratello e il genitore condiviso sopravvivono
Pdf.RemoveFormField('group.a');
Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// Tipo, flag e opzioni vengono risolti ancora attraverso il genitore
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');
Che cosa scrive ResetLoadedFormField quando il default è ereditato?
ResetLoadedFormField scrive un /V locale che è una copia fresca del /DV ereditato con lo stesso tipo di oggetto PDF, e valida l'intero default prima di toccare il campo. Il tipo di oggetto conta perché i getter scalari appiattiscono tutto a testo. Il default di una checkbox è un name come /Yes, quello di una list box multi-select è un array di stringhe, e quello di un testo può essere una stringa UTF-16 esadecimale; copiare uno qualsiasi di essi attraverso GetLoadedFormFieldDefaultValue trasformerebbe il name in una stringa, l'array in una stringa vuota e la stringa esadecimale nelle sue cifre letterali. Quindi il reset si ramifica sul tipo ereditato: i campi testo e choice ricevono un nuovo oggetto stringa che mantiene il flag IsHexadecimal, i campi choice con default array ricevono un nuovo array di nuove stringhe, e i button non pushbutton ricevono un nuovo oggetto name. Copiare, anziché puntare agli oggetti del genitore, è voluto: un /V che condividesse l'array /DV del genitore o il suo numero di oggetto cambierebbe il default la prossima volta che qualcuno modifica il valore. Un default di tipo sbagliato, o un array choice che contiene altro che stringhe, alza un'eccezione e lascia /V e /I esattamente come erano. I pushbutton, che non hanno valore (Table 226, bit 17), e i campi signature ripiegano sul vecchio percorso solo-stringa
Quando non esiste /DV da nessuna parte lungo la catena, il metodo mantiene il suo contratto di pulizia scrivendo una stringa vuota locale, o /Off per un campo checkbox o radio. Cancellare il /V locale sembrerebbe più ordinato e sarebbe sbagliato: il genitore può tenere un valore corrente, e togliere l'override del figlio riporterebbe quel valore in silenzio. È anche per questo che un reset di campo singolo non è l'azione ResetForm di §12.7.5.3, che un viewer esegue su un insieme di campi quando l'utente preme un button, come descritto in costruire campi e azioni AcroForm con HotPDF. ResetLoadedFormField è un'operazione di editing su un campo caricato, con la sua regola propria per il caso senza default, e registra il campo attraverso NoteLoadedFormFieldDirty così il ricalcolo incrementale vede il cambiamento
var
Field: THPDFLoadedFormField;
begin
Field := Pdf.GetFormField('group.a');
try
// Il genitore tiene /DV [(b) (r)] su una list box MultiSelect: group.a riceve
// il suo /V [(b) (r)] e un nuovo /I [0 2]; il genitore resta intoccato
Pdf.ResetLoadedFormField(Field.Index);
// I getter scalari non possono rappresentare il default array
Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // vuoto
finally
Field.Free;
end;
Pdf.SaveLoadedDocument('survey-reset.pdf');
end;
Tenere /V, /I e /AS d'accordo
Un reset è corretto solo se l'indice di selezione e lo stato di appearance seguono il valore, quindi ResetLoadedFormField chiude con gli stessi due reconciler di SetFormFieldValue. HPDFReconcileChoiceSelection ora accetta un valore array: cancella il /I locale senza mutarlo, incrocia ogni valore con la metà di export di ogni voce /Opt, e scrive un nuovo /I ordinato, quindi un reset a [(b) (r)] contro opzioni b, g, r produce /I [0 2]. ReconcileLoadedButtonAppearanceStates ora chiede il tipo ereditato, così una checkbox figlia il cui /FT /Btn sta sul genitore finalmente riceve il suo /AS. Sul lato scrittura, SetFormFieldValue e SetLoadedFormFieldDefaultValue memorizzano un oggetto name per un button non pushbutton ereditato anche quando il figlio non ha nessuna voce locale da cui copiare il tipo. E quando EnsureLoadedFieldAppearanceStream ricostruisce le appearance dei button, scrive /AS /Off salvo che il valore combaci con lo stato on, e dà a ogni state stream un proprio /Type /XObject, /Subtype /Form e /BBox; prima della v2.754.4, rigenerare l'appearance dopo un reset poteva ri-spuntare la casella prima che il file fosse salvato
Limiti da conoscere prima di costruire su questo
I getter scalari restano scalari. GetFormFieldValue e GetLoadedFormFieldDefaultValue restituiscono una stringa vuota per un valore array, stringificano numeri e booleani come 42 o true, e riportano una stringa codificata es nella sua grafia esadecimale. Un ciclo di /Parent termina la camminata senza eccezione, quindi un campo il cui tipo si perde in un ciclo riporta lfftUnknown e flag a 0 anziché fallire. SetFormFieldValue e ResetLoadedFormField scrivono sempre il figlio che indirizzi e non promuovono mai un valore al genitore condiviso, che è giusto per figli indipendenti ma significa che i gruppi radio vanno indirizzati attraverso il campo che possiede la selezione. E ogni chiamata commette un campo per conto suo; niente di tutto ciò rende transazionale un batch di reset
La risoluzione degli attributi ereditati, la classificazione unificata dell'albero dei campi e il reset tipizzato descritti qui fanno parte della API dei form caricati in HotPDF Delphi Component per Delphi e C++Builder, accanto alla creazione dei campi coperta in aggiungere campi AcroForm a un PDF caricato in Delphi