HotPDF Delphi Component riempie un campo AcroForm esistente su un PDF caricato tramite THotPDF.SetFormFieldValue, indirizzato per indice di campo zero-based o per nome di campo completamente qualificato. Scrivere la nuova entry /V è la parte facile; a rendere affidabile la chiamata su form del mondo reale è il fatto che lo stesso metodo tiene coerenti anche tre pezzi di stato invisibili finché non vanno storti: l'identità decodificata del campo, così che un nome non ASCII possa essere trovato, lo stato di aspetto /AS sui widget checkbox e radio, e l'array di indici di selezione /I sui campi choice. L'appearance stream visibile è un passo separato ed esplicito tramite EnsureLoadedFieldAppearanceStream
Lo scenario è quello banale: un cliente ti manda il suo form, una dichiarazione dei redditi, una richiesta di rimborso assicurativo, un ordine di acquisto che qualcuno ha costruito in Acrobat anni fa, e la tua applicazione Delphi deve popolarlo da un database e restituire un file che si apra correttamente ovunque. Non hai alcun controllo su come il form è stato creato. I nomi dei campi possono essere codificati UTF-16, i valori di export delle checkbox possono essere 2 invece di Yes, e le combo box possono usare coppie di opzioni [export display]. Ognuno di questi dettagli ha una regola in ISO 32000-1, e ogni regola è qualcosa che SetFormFieldValue ora gestisce per te. Questo articolo parla di cosa fa, perché, e dove si ferma. Per il problema gemello di creare campi che non esistono ancora, vedi aggiungere campi AcroForm a un PDF caricato in Delphi
Perché SetFormFieldValue non trova un campo con un nome non ASCII?
Prima della v2.752.1 la risposta era la codifica: il campo viveva nel file sotto un nome UTF-16BE esadecimale, e la cache dei nomi memorizzava la grafia hex invece del testo. ISO 32000-1 §12.7.3.1 definisce il nome di campo parziale /T come text string, e la §7.9.2.2 dice che una text string può essere UTF-16BE con un byte order mark FE FF in testa. Gli strumenti di authoring serializzano abitualmente quei nomi come stringhe hex secondo la §7.3.4.3, quindi un campo chiamato Straße arriva come <FEFF005300740072006100DF0065>. Dentro HotPDF, THPDFStringObject.Value contiene il testo esadecimale grezzo ogni volta che IsHexadecimal è impostato, che è esattamente quello che vuoi per un round trip senza perdita del dizionario originale ed esattamente quello che non vuoi come chiave di ricerca. HPDFLoadedFormTextName separa le due cose. Quando la cache delle relazioni viene costruita, ogni valore /T ci passa attraverso: se l'oggetto stringa è esadecimale, HPDFHexToBytes ripristina la sequenza di byte; se i byte iniziano con FE FF e hanno lunghezza pari, il payload viene decodificato come UTF-16BE e ricodificato come UTF-8; il risultato viene poi unito al nome del parent con un punto per formare il nome completamente qualificato descritto nella §12.7.3.1, quindi un kid chiamato City sotto un parent chiamato Address viene registrato come Address.City. La chiave della cache è normalizzata in minuscolo, il che fa funzionare anche SetFormFieldValue('address.city', ...); è una comodità oltre lo standard, dato che la specifica tratta i nomi come case-sensitive. Fondamentale: cambia solo la chiave della cache. L'oggetto /T nel dizionario del campo mantiene la sua codifica esadecimale, quindi salvare il documento non riscrive l'identità di un campo che ti sei limitato a riempire
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;
// I nomi qualificati vengono decodificati dalle stringhe /T UTF-16BE e
// uniti con punti, quindi i nomi annidati e non ASCII si risolvono
Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
Pdf.SetFormFieldValue('Applicant.Straße', 'Hauptstraße 12');
// I valori che non sono Latin-1 viaggiano come hex UTF-16BE con prefisso FEFF
// e vengono scritti come stringa esadecimale PDF
Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');
Pdf.SaveLoadedDocument('claim-form-filled.pdf');
finally
Pdf.Free;
end;
end;
Cosa scrive davvero SetFormFieldValue?
Entrambi gli overload eseguono gli stessi cinque passi: individuano il dizionario del campo, scrivono /V tramite HPDFSetDictFormValue, riconciliano gli indici di selezione dei choice, marcano il dizionario dirty, riconciliano gli stati di aspetto dei bottoni, e infine registrano l'indice del campo tramite NoteLoadedFormFieldDirty. Quest'ultimo passo conta se il form porta script di calcolo, perché l'insieme dirty è ciò che l'overload senza parametri RecalculateLoadedFormFieldsIncremental consuma per rieseguire solo i calcoli che leggono transitivamente un campo modificato. HPDFSetDictFormValue in sé è attento al tipo di oggetto che sostituisce. Se il /V esistente è un oggetto name, che è quello che i campi checkbox e radio usano per il loro valore di export, il nuovo valore viene scritto come name, mai come stringa, perché i name PDF sono ASCII-only per costruzione. Altrimenti scrive un oggetto stringa e ispeziona il valore che hai passato: una stringa che inizia con FEFF, ha lunghezza pari e consiste solo di cifre esadecimali viene trattata come la forma wire UTF-16BE della §7.9.2.2 e memorizzata con IsHexadecimal impostato, quindi si serializza come <FEFF...> invece che come (FEFF...) letterale. È il meccanismo su cui conta la riga City qui sopra; qualunque altra stringa viene memorizzata come stringa literal con i byte che hai dato, quindi per testo latino semplice passi testo semplice
Perché una checkbox mantiene la vecchia spunta dopo il cambio di valore?
Perché per un campo button il solo valore non decide cosa viene disegnato. ISO 32000-1 §12.7.4.2.3 specifica che un widget checkbox porta uno stato di aspetto /AS che nomina quale stream in /AP /N è mostrato al momento, e i viewer dipingono da /AS, non da /V. Se cambi /V in Yes ma lasci /AS a Off, il file è internamente contraddittorio, e il flattening cuocerà volentieri nella pagina l'aspetto stale di non selezionato mentre i dati del form dicono selezionato. ReconcileLoadedButtonAppearanceStates esiste per chiudere quel divario: per un campo il cui /FT è Btn, visita il dizionario del campo stesso e ogni entry del suo array /Kids, legge il nome on-state da /AP /N, e riscrive /AS con quel nome quando corrisponde al valore del campo o con Off quando non corrisponde
Due dettagli dei form reali hanno dato forma alla correzione della v2.752.3. Primo, un dizionario di aspetto normale può contenere solo lo stato on; la §12.7.4.2.3 nomina l'aspetto off Off ma gli strumenti di authoring spesso omettono il suo stream e lasciano che il viewer non disegni nulla. Il codice precedente si tirava indietro quando il dizionario conteneva meno di due entry, quindi quelle checkbox a stato singolo mantenevano in silenzio la vecchia spunta. Il controllo ora è semplicemente che il dizionario non sia vuoto, e il nome on-state viene preso come la prima chiave che non è Off. Secondo, il nome on-state è quello che l'autore ha scelto. I form reali usano 2, Yes, On o una parola localizzata, quindi il confronto è contro la chiave effettiva, senza distinzione di maiuscole, mai contro un Yes hard-coded. I radio button aggiungono una piega in più, descritta nella §12.7.4.2.4: la selezione vive in /V sul campo parent, mentre i singoli kid possiedono i widget e tipicamente non hanno un /V proprio. L'helper annidato InheritedButtonValue risale quindi la catena /Parent, fino a 64 livelli, finché non trova un valore non vuoto, così ogni kid viene confrontato con il valore del gruppo a cui appartiene. Impostare il parent sul valore di export di un kid accende esattamente quel kid e spegne ogni fratello
// Checkbox: il valore di export deve corrispondere alla chiave on-state in /AP /N
// (spesso 'Yes', ma i form reali usano '2', 'On' o qualunque altra cosa)
Pdf.SetFormFieldValue('Consent', 'Yes');
// Gruppo radio: /V viene scritto sul parent; ogni widget kid riceve
// /AS impostato al proprio nome di export oppure a Off
Pdf.SetFormFieldValue('PaymentMethod', 'Card');
// Azzerare una checkbox: qualunque valore che non corrisponde a un on state dà /AS Off
Pdf.SetFormFieldValue('Newsletter', 'Off');
Campi choice: tenere /I allineato con /V
Per una combo box o una list box, /V non è l'unico posto in cui una selezione è registrata. La Table 231 della §12.7.4.4 definisce /I come array di indici zero-based dentro /Opt che identifica gli elementi selezionati, e un viewer che trova /I puntare all'opzione 0 mentre /V nomina l'opzione 3 può evidenziare la riga sbagliata. Dalla v2.754.1, HPDFReconcileChoiceSelection gira dentro ogni chiamata a SetFormFieldValue e, quando l'/FT ereditato è Ch, ricostruisce /I dal nuovo valore. L'ordine delle operazioni è voluto. L'entry locale /I viene cancellata per prima, senza toccarne il contenuto: se il vecchio array era un oggetto indiretto condiviso con un altro campo, mutarlo sul posto corromperebbe la selezione dell'altro campo, quindi la routine scarta il riferimento e crea invece un array diretto nuovo. Poi risolve /Opt attraverso la catena /Parent, dato che le opzioni di un choice possono essere ereditate, e scansiona le entry. Un'opzione stringa nuda viene confrontata direttamente; una coppia [export display] viene confrontata sul suo elemento export, e una coppia con meno di due elementi viene saltata. Entrambe le parti passano per HPDFLoadedFormTextName, quindi un'opzione hex UTF-16 corrisponde a un valore hex UTF-16 senza che tu debba scriverli allo stesso modo. Alla prima corrispondenza viene scritto un /I di un elemento e la scansione si ferma; un valore scalare sostituisce sempre qualsiasi multi-selezione precedente, indipendentemente dal flag MultiSelect
Quando non corrisponde nulla, non viene scritto alcun /I. È l'esito corretto per una combo box editabile, dove la §12.7.4.4 permette all'utente di digitare un valore fuori dalla lista delle opzioni; un valore così non ha indice, e un indice stale sarebbe peggio di nessuno. È anche quello che ottieni se passi un'etichetta di display invece di un valore di export a una lista di opzioni accoppiate, quindi quando una combo box si rifiuta di mostrare la tua selezione, controlla quale metà della coppia hai fornito
// /Opt è [[US United States] [CA Canada] [MX Mexico]]:
// si confronta sul valore di export, e /I diventa [1]
Pdf.SetFormFieldValue('Country', 'CA');
// Combo editabile con un valore fuori da /Opt: /V viene scritto,
// /I viene rimosso, e nessun indice viene inventato
Pdf.SetFormFieldValue('Title', 'Principal Engineer');
Valore e aspetto sono due operazioni separate
SetFormFieldValue non tocca mai l'appearance stream di un campo text o choice. Dopo la chiamata, /V contiene il nuovo testo mentre /AP /N disegna ancora quello vecchio, e quale dei due un viewer mostri dipende dal fatto che il dizionario AcroForm porti /NeedAppearances true secondo la §12.7.3.3 e dal fatto che il viewer lo rispetti. Se hai bisogno che il file renderizzi il nuovo valore in ogni reader, compresi flattener e generatori di thumbnail che ignorano il flag, chiama EnsureLoadedFieldAppearanceStream con l'indice del campo. Costruisce un Form XObject dalla stringa /DA ereditata, dal quadding /Q, dal layout comb /MaxLen e dal valore, risolve il font nominato attraverso le risorse /DR dell'AcroForm così un font Type0 mantiene il proprio descendant font invece di degradare a Helvetica, e restituisce True quando almeno un widget ha ricevuto uno stream. L'overload per nome di SetFormFieldValue non ti restituisce alcun indice, quindi procuratene uno tramite GetFormField, che restituisce un THPDFLoadedFormField di tua proprietà che devi liberare. La suite di regressione per la modifica della v2.752.1 è esplicita su questa separazione: imposta un valore, chiama EnsureLoadedFieldAppearanceStream, poi renderizza la pagina e verifica che i pixel dentro il rettangolo del widget siano cambiati mentre quelli fuori no. Verificare che /V è cambiato non prova niente su cosa vedrà un utente
var
Field: THPDFLoadedFormField;
begin
Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
Field := Pdf.GetFormField('Applicant.FullName');
try
// Dipinge il nuovo valore dentro /AP così i viewer che ignorano
// /NeedAppearances lo mostrano comunque
if not Pdf.EnsureLoadedFieldAppearanceStream(Field.Index) then
raise Exception.Create('No widget rectangle to paint into');
finally
Field.Free;
end;
Pdf.SaveLoadedDocument('claim-form-filled.pdf');
end;
Limiti da conoscere prima di costruirci sopra
ReconcileLoadedButtonAppearanceStates verifica l'/FT locale del dizionario che hai indirizzato, quindi agisce sul parent radio o su una checkbox che porta il proprio /FT; un widget kid indirizzato da solo, con /FT solo sul suo parent, non viene riconciliato attraverso quel percorso. HPDFReconcileChoiceSelection gestisce un singolo valore scalare e scrive al massimo un indice; le list box a selezione multipla con diverse entry scelte restano fuori da ciò che SetFormFieldValue modella. Nessuna delle due routine valida il valore che passi contro /Opt o contro le chiavi on-state, quindi un refuso produce una checkbox Off o una combo senza indice invece di un'eccezione. E GetFormFieldValue restituisce il testo /V memorizzato come sta nel dizionario, il che per un valore codificato in hex significa la grafia esadecimale, non il testo decodificato
Una volta che i valori sono dentro e gli aspetti dipinti, i due passi successivi naturali stanno ai due lati di questa operazione. Scambiare dati di campo con sistemi esterni in blocco, invece di una chiamata a SetFormFieldValue per volta, è ciò che copre import ed export XFDF in Delphi. E quando il form riempito è definitivo e non deve più essere modificabile, il flattening dei campi AcroForm e XFA in Delphi cuoce nella pagina esattamente gli stati /AS e gli appearance stream descritti qui, ed è per questo che renderli coerenti prima del flattening non è opzionale
L'API di editing dei form caricati di questo articolo, incluso SetFormFieldValue, EnsureLoadedFieldAppearanceStream e il grafo di ricalcolo incrementale, è distribuita come parte di HotPDF Delphi Component per Delphi e C++Builder