Nella lista dei campi di quasi ogni viewer PDF, il campo via di un modulo di reclamo in lingua tedesca legge Straße. I byte dietro la sua voce /T raccontano un'altra storia: <FEFF005300740072006100DF0065>, una stringa esadecimale UTF-16BE con un byte order mark davanti. Codice che confronta i nomi dei campi contro quei byte grezzi riferisce il campo come assente, anche se un utente può cliccarci dentro direttamente. Lo stesso modulo può conservare lo stato on di una checkbox come 2 invece di Yes e la sua lista paesi come coppie [export display], il che vi dà tre modi separati per cui il valore che scrivete e il valore che un viewer dipinge discordano
Tutte e tre le varianti vengono dallo stesso posto: un form costruito anni fa con lo strumento di authoring che il cliente aveva, uno che la vostra applicazione Delphi ora deve riempire da un database e restituire come file che si renderizza uguale in ogni reader. Non potete riscrivere il form, così il codice di riempimento deve seguire le regole sotto cui il form è stato scritto. ISO 32000-1 ha una regola per ciascuno dei tre casi: §7.9.2.2 per le text string, §12.7.4.2.3 per gli stati di aspetto dei button, Table 231 in §12.7.4.4 per le selezioni choice. Creare campi che non esistono ancora è un lavoro diverso, coperto in aggiungere campi AcroForm a un PDF caricato in Delphi
Trovare il campo: HotPDF decodifica /T prima di confrontare i nomi
THotPDF.SetFormFieldValue ha due overload, uno che prende un indice di campo zero-based e uno che prende un nome completamente qualificato, ed entrambi si risolvono contro una tabella dei nomi che il componente costruisce dall'albero AcroForm al primo uso. Mentre quella tabella viene costruita, ogni nome parziale passa per un decoder. Una stringa /T esadecimale viene riportata a byte; se quei byte iniziano con FE FF e hanno lunghezza pari, il resto viene letto come UTF-16BE e ricodificato come UTF-8. Ogni nome parziale decodificato viene poi unito al suo parent con un punto, esattamente come descrive §12.7.3.1, così un campo kid chiamato City sotto un parent chiamato Address viene registrato come Address.City. Solo la chiave di ricerca viene decodificata. L'oggetto /T nel file conserva la sua grafia hex, e salvare il documento non tocca l'identità di un campo che vi siete limitati a riempire
Le chiavi vengono portate in minuscolo prima di entrare nella tabella, così 'address.city' trova Address.City. Va oltre lo standard, che tratta i nomi di campo come case-sensitive, e significa che due campi che differiscono solo per maiuscole colliderebbero nella ricerca. Per codice che legge nomi di campo da un file di configurazione digitato a mano, una maiuscola mancata è un fallimento molto più comune di due campi in un form che differiscono solo per maiuscole
I nomi decodificati sono byte UTF-8 portati in un AnsiString, ed è lì che le conversioni di stringa di Delphi stessa si mettono di traverso. Un literal tipo 'Applicant.Straße' passato a un parametro AnsiString viene convertito attraverso la code page ANSI di sistema, così ß arriva come il byte singolo DF invece che come la coppia UTF-8 C3 9F, e la ricerca manca. I nomi ASCII non ne risentono. Per qualunque altra cosa, enumerate i campi una volta, decodificate ogni nome con UTF8ToString, e scrivete attraverso l'overload per indice:
// FindFieldIndex è un helper che aggiungete voi: confronta i
// nomi UTF-8 decodificati con una stringa Unicode, così nessuna code
// page ANSI si mette fra il vostro literal e la ricerca
function FindFieldIndex(Pdf: THotPDF; const Name: string): Integer;
var
I: Integer;
begin
for I := 0 to Pdf.GetFormFieldCount - 1 do
if SameText(UTF8ToString(Pdf.GetFormFieldName(I)), Name) then
Exit(I);
Result := -1;
end;
var
Pdf: THotPDF;
Street: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;
// I nomi ASCII possono andare dritti all'overload per nome
Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
Street := FindFieldIndex(Pdf, 'Applicant.Straße');
// Anche il valore è un AnsiString, quindi un literal 'ß' dipenderebbe dalla
// code page di sistema; l'hex FEFF (Hauptstraße 12) lo evita
if Street >= 0 then
Pdf.SetFormFieldValue(Street,
'FEFF00480061007500700074007300740072006100DF0065002000310032');
// Lo stesso hex UTF-16BE con prefisso FEFF funziona con l'overload
// per nome (questo è München); viene scritto come stringa hex
Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');
Pdf.SaveLoadedDocument('claim-form-filled.pdf');
finally
Pdf.Free;
end;
end;
Ciò che SetFormFieldValue scrive, passo per passo
Entrambi gli overload eseguono la stessa sequenza dopo che il campo è stato trovato: scrivono /V, ricostruiscono l'indice di selezione choice, marcano il campo come modificato, commutano gli stati di aspetto dei button, rigenerano l'aspetto text o choice, e registrano il campo in un insieme dirty. Quell'ultimo passo alimenta la RecalculateLoadedFormFieldsIncremental senza parametri, che riesegue solo gli script di calcolo che leggono un campo modificato, direttamente o attraverso una catena tipo riga d'ordine, subtotale, totale
Il tipo di oggetto del nuovo /V dipende dal campo. Per una checkbox o un radio button, SetFormFieldValue scrive sempre un oggetto name, perché §12.7.4.2.3 conserva lo stato selezionato come name e i name PDF sono byte semplici senza codifica da text string. Ogni altro campo riceve un oggetto stringa, e il valore passato viene prima ispezionato. Una stringa che inizia con FEFF, ha lunghezza pari e contiene solo cifre esadecimali viene presa come la forma UTF-16BE di §7.9.2.2 e serializzata come <FEFF...>. È ciò su cui conta la riga City qui sopra. Qualunque altra cosa viene conservata come stringa literal con esattamente i byte forniti, così il testo latino semplice entra come testo semplice
Perché una checkbox conserva la vecchia spunta dopo il cambio di valore?
Per un campo button il valore non decide ciò che viene disegnato. Un widget checkbox porta una voce /AS che nomina quale stream in /AP /N è attualmente in mostra, e i viewer dipingono da /AS, non da /V. Cambiate /V in Yes mentre /AS dice ancora Off e il file si contraddice; un flattener cuocerà nella pagina la casella non selezionata mentre i dati del form affermano che lo è. SetFormFieldValue visita quindi il campo button e ogni voce del suo array /Kids, legge il nome dello stato on da /AP /N di ogni widget, e imposta /AS a quel nome quando corrisponde al valore del campo o a Off quando non corrisponde
I form reali piegano quella regola in due modi. L'aspetto off si chiama Off nello standard, eppure gli strumenti di authoring spesso lasciano fuori del tutto il suo stream e lasciano che il viewer non disegni nulla, così un dizionario di aspetto normale con una sola voce è perfettamente legale. Il nome dello stato on è ciò che l'autore ha scelto: Yes, On, 2, una parola nella lingua del form. Così lo stato on viene preso come la prima chiave in /AP /N che non sia Off, e il confronto col vostro valore ignora le maiuscole. I radio button aggiungono l'assetto di §12.7.4.2.4: il valore selezionato vive in /V sul campo parent, mentre ogni widget kid di solito non ha un /V proprio. Ogni kid viene confrontato col valore ereditato attraverso la sua catena /Parent
// Checkbox: il valore deve eguagliare la chiave on-state in /AP /N
// («Yes» qui; chi ha disegnato il form può aver scelto «2» o «On»)
Pdf.SetFormFieldValue('Consent', 'Yes');
// Gruppo radio: /V va sul parent come name; ogni widget kid riceve
// /AS impostato al proprio nome on-state oppure a Off
Pdf.SetFormFieldValue('PaymentMethod', 'Card');
// Azzerare una checkbox: un valore che non corrisponde a alcuno stato on dà /AS Off
Pdf.SetFormFieldValue('Newsletter', 'Off');
Il rovescio della medaglia è che un refuso non produce eccezioni. 'yes ' con uno spazio finale non corrisponde a nulla, così la casella finisce Off e il file resta perfettamente valido. Se i vostri valori arrivano da una colonna di database, ripuliteli dagli spazi prima della chiamata
Campi choice: tenere /I allineato con /V
Una combo box o una list box registra la sua selezione in due posti. Accanto a /V, la Table 231 definisce /I come array di indici zero-based dentro /Opt, e un viewer che trova /I puntare all'opzione 0 mentre /V nomina l'opzione 3 può evidenziare la riga sbagliata. SetFormFieldValue ricostruisce /I a ogni chiamata a un campo choice. Cancella la voce /I del campo prima di ogni altra cosa e non modifica mai il vecchio array sul posto: un array indiretto può essere condiviso con un altro campo, e mutarlo cambierebbe anche la selezione di quell'altro campo. Risolve poi /Opt attraverso la catena /Parent, dato che le opzioni possono essere ereditate, e percorre le voci. Un'opzione stringa nuda viene confrontata direttamente, una coppia [export display] sul suo elemento export, e una coppia con meno di due elementi viene saltata
Il confronto gira su testo decodificato, così un'opzione conservata in PDFDocEncoding corrisponde a un valore scritto come hex UTF-16BE e viceversa. Un valore scalare produce al massimo un indice; qualunque multi-selezione che il campo avesse prima viene sostituita. Quando non corrisponde nulla, non viene scritto alcun /I. Per una combo box editabile è l'esito corretto, perché §12.7.4.4 permette all'utente di digitare un valore fuori dalla lista e un valore così non ha indice da puntare
La stessa regola sta dietro al lamento più comune sulle combo box: la chiamata riesce, /V contiene il nuovo testo, e il viewer non mostra nulla di selezionato. Di solito chi chiamava ha passato l'etichetta di display (Canada) a una lista costruita da coppie [CA Canada]. Il componente non può trattarlo come errore senza rompere le combo editabili, così il controllo appartiene al vostro codice. GetFormFieldOptionValue restituisce l'elemento export di ogni opzione, il che rende una guardia breve:
// SetChoiceByExport è una guardia vostra, non parte del componente:
// rifiuta i valori che non corrispondono ad alcun elemento export in /Opt
procedure SetChoiceByExport(Pdf: THotPDF; const FieldName,
ExportValue: AnsiString);
var
I: Integer;
begin
for I := 0 to Pdf.GetFormFieldOptionCount(FieldName) - 1 do
if Pdf.GetFormFieldOptionValue(FieldName, I) = ExportValue then
begin
// /Opt è [[US United States] [CA Canada] [MX Mexico]]:
// «CA» corrisponde all'elemento export e /I diventa [1]
Pdf.SetFormFieldValue(FieldName, ExportValue);
Exit;
end;
raise Exception.CreateFmt('%s is not an export value of %s',
[string(ExportValue), string(FieldName)]);
end;
// Combo editabile con testo libero: scrivetelo direttamente. /V viene impostato,
// /I viene rimosso, e nessun indice viene inventato
Pdf.SetFormFieldValue('Title', 'Principal Engineer');
Il nuovo valore compare senza /NeedAppearances?
Sì, per i campi text e choice. Un viewer disegna lo stream /AP conservato e non guarda /V, così un valore nuovo con un aspetto vecchio lascia il vecchio testo su schermo e in stampa a meno che il dizionario AcroForm porti /NeedAppearances true (§12.7.3.3) e il viewer scelga di onorarlo. I flattener e i generatori di thumbnail ignorano spesso quel flag. SetFormFieldValue rigenera l'appearance stream per i campi text e choice come parte della chiamata, usando lo stesso builder del pubblico EnsureLoadedFieldAppearanceStream. Lo stream viene costruito dalla stringa /DA ereditata, dall'allineamento /Q, dal /MaxLen per i campi comb e dal valore stesso. Il font nominato in /DA viene risolto attraverso le risorse /DR dell'AcroForm, così un font Type0 conserva i propri glifi invece di ricadere su Helvetica. I button conservano gli stati di aspetto disegnati dal loro designer; per loro si muove solo /AS
SetFormFieldValue è una procedure e non vi dice se qualcosa è stato dipinto. Un widget senza un /Rect utilizzabile non riceve stream, e form rotti con widget orfani o senza rettangolo esistono davvero. Quando il risultato renderizzato conta, chiamate voi EnsureLoadedFieldAppearanceStream dopo aver impostato il valore. Restituisce True quando almeno un widget ha ricevuto uno stream. GetFormField vi consegna un THPDFLoadedFormField la cui proprietà Index è ciò che la chiamata si aspetta; quell'oggetto è vostro e va liberato
var
Field: THPDFLoadedFormField;
begin
Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
Field := Pdf.GetFormField('Applicant.FullName');
try
// False significa che nessun widget di questo campo aveva un rettangolo dove
// dipungere; il valore è conservato, ma nulla lo mostrerà
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;
Controllare che /V sia cambiato prova ben poco su ciò che un utente vedrà. Un test che significa qualcosa renderizza la pagina prima e dopo e conferma che i pixel dentro il rettangolo del widget sono cambiati mentre quelli fuori no. Teniamo quel controllo perché i soli byte di aspetto ci hanno già ingannato: uno stream conteneva ogni byte giusto mentre un viewer che onorava /NeedAppearances true ricostruiva il display da /V e non ne mostrava niente
Rileggere i valori, e cosa viene di solito dopo
GetFormFieldValue restituisce la /V ereditata del campo. Un valore conservato come hex con prefisso FEFF torna decodificato in UTF-8, così il campo City qui sopra si rilegge come i byte UTF-8 di München e UTF8ToString lo trasforma in una stringa Delphi. Una stringa literal torna con i byte con cui è stata conservata. Per i controlli di round trip, confrontate il testo decodificato da entrambi i lati anziché la grafia che avete passato
Spostare dati di campo in blocco fra il form e un sistema esterno, invece di una chiamata per campo, è il soggetto di import ed export XFDF in Delphi. Bloccare un form finito così nessuno può modificarlo è il flattening dei campi AcroForm e XFA in Delphi, che trasforma in contenuto statico di pagina esattamente gli stati /AS e gli appearance stream descritti qui. Ciò che è incoerente in quel punto resta incoerente per sempre, quindi riempite, controllate, poi flattenate
SetFormFieldValue, EnsureLoadedFieldAppearanceStream e la API di ricalcolo incrementale fanno parte dell'HotPDF Delphi Component, accanto alle chiamate che rinominano, rimuovono e flattenano i campi in un documento caricato da Delphi o C++Builder