Articolo tecnico

Valori dei campi modulo in un PDF caricato con Delphi

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

Come HotPDF risolve i nomi AcroForm non ASCII: la stringa /T esadecimale viene riportata a byte, il byte order mark FE FF viene decodificato come UTF-16BE e ricodificato come UTF-8, e il nome qualificato unisce il suo parent così Applicant.FullName e un campo chiamato Straße atterrano entrambi nella tabella di ricerca
Cambia solo la chiave della cache: il dizionario del campo mantiene la sua codifica esadecimale, le ricerche normalizzano in minuscolo come comodità oltre lo standard, e salvare il documento non riscrive mai l'identità di un campo che vi siete limitati a riempire

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

Perché una checkbox HotPDF conserva la vecchia spunta quando cambia solo /V: i viewer dipingono dallo stato di aspetto /AS dentro /AP /N, quindi SetFormFieldValue visita il campo e ogni kid, legge il nome dello stato on come prima chiave diversa da Off, e riscrive /AS in caso di corrispondenza o altrimenti a Off
I gruppi radio confrontano ogni kid contro il valore ereditato dal campo parent attraverso la catena /Parent, così impostare il gruppo su un valore di export accende esattamente quel widget e spegne ogni fratello

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

Come HotPDF tiene coerente un campo choice: SetFormFieldValue cancella l'array /I locale prima di toccarlo, risolve /Opt attraverso la catena /Parent, confronta la metà export di ogni opzione come testo decodificato, scrive un /I di un elemento alla prima corrispondenza e non scrive nulla quando un valore di combo editabile non ha indice
Un'opzione stringa nuda viene confrontata direttamente e una coppia export display sul suo elemento export, mentre un valore fuori da /Opt correttamente non lascia alcun indice — un /I stale che punta alla riga sbagliata sarebbe peggio di nessuno

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