Articolo tecnico

Creare campi e azioni AcroForm con HotPDF in Delphi

Un team sinistri distribuisce un modulo di consenso generato in Delphi che in Acrobat sembra impeccabile: ogni checkbox viene renderizzata, ogni didascalia è allineata, il layout coincide con l'originale cartaceo. Tre settimane dopo, il servizio di intake comincia a respingere un terzo delle sottomissioni. Il backend si aspetta che la checkbox del consenso invii il valore Y; il modulo esporta Yes, perché chi lo ha costruito ha dato per scontato che didascalia e valore di esportazione fossero la stessa cosa. Nella pagina renderizzata nulla suggerisce la differenza. Questa è la proprietà decisiva dello sviluppo AcroForm: il widget visibile e il contratto dati sottostante sono strutture separate, e solo una delle due viene verificata quando qualcuno apre il file e lo guarda

HotPDF è un componente VCL nativo che scrive i dizionari dei campi AcroForm direttamente da codice Delphi e C++Builder, quindi entrambe le strutture sono sotto controllo esplicito del programma — ed entrambe possono essere sbagliate in modo indipendente. Di seguito vediamo creazione dei campi, azioni dei pulsanti e JavaScript a livello di campo, con attenzione ai punti in cui l'anteprima della pagina e i dati del modulo divergono in silenzio

Strato di widget AcroForm in HotPDF mappato ai valori dei campi sottostanti, al dizionario azione submit, e a una mancata corrispondenza del valore di export del consenso
Gli utenti cliccano il layer dei widget mentre i valori viaggiano attraverso il layer di campo e azione sottostante, dove le discrepanze restano invisibili

I nomi dei campi sono chiavi di routing, non didascalie

Ogni campo AcroForm ha un nome completamente qualificato, e ISO 32000-1 §12.7.3 definisce quel nome — mai la didascalia visibile — come la chiave con cui il valore del campo viaggia in sottomissioni FDF, XFDF e HTTP. Ne derivano due conseguenze che sorprendono spesso gli sviluppatori abituati al design di form VCL, dove il nome di un controllo è poco più di un identificatore nel codice

Primo: due campi con lo stesso nome completamente qualificato non sono due campi. Il modello PDF li tratta come due annotazioni widget di un unico campo, che condividono un unico valore: scrivi in uno e l'altro si aggiorna immediatamente. Ripetere il nome del cliente in ogni pagina di un contratto è un uso legittimo di questo comportamento. Ottenerlo per errore, perché un ciclo di generazione ha riutilizzato 'Field1' su tre pagine, è un bug che nessuna ispezione visiva intercetta: ogni pagina mostra comunque il proprio widget, e il mirroring appare solo quando un utente inizia a digitare

Secondo: i nomi puntati come applicant.email costruiscono una gerarchia: il nodo padre applicant raggruppa i figli per operazioni di reset e invio che riguardano solo una parte del modulo. Nominare i campi in modo gerarchico fin dall'inizio non costa nulla e ripaga appena il sistema ricevente chiede "solo il blocco applicant"

I pulsanti radio aggiungono una terza regola: i pulsanti che devono alternarsi come un unico gruppo devono condividere un nome di gruppo. In HotPDF, le chiamate AddRadioButton che usano lo stesso nome di gruppo collegano i rispettivi widget a un unico campo padre, e il valore di esportazione di ogni pulsante — 'basic', 'full' — identifica l'opzione selezionata. Assegnare invece un nome univoco a ogni pulsante produce interruttori on/off indipendenti, non un gruppo a scelta esclusiva

Creare il set di campi pagina per pagina

HotPDF inserisce i campi tramite metodi di THPDFPage, quindi ogni campo appartiene all'oggetto pagina che lo ha creato. Qui domina una trappola di sequenza: AddPage ripunta subito CurrentPage alla nuova pagina, perciò ogni chiamata campo emessa dopo finisce sulla nuova pagina anche se logicamente appartiene alla precedente. Completa ogni pagina — contenuto disegnato e campi insieme — prima di chiamare AddPage

procedure BuildClaimForm(Pdf: THotPDF);
begin
  // Page 1: applicant block
  Pdf.CurrentPage.AddTextField('applicant.name', '', Rect(50, 70, 300, 92));
  Pdf.CurrentPage.AddTextField('applicant.email', '', Rect(50, 110, 300, 132));
  Pdf.CurrentPage.AddCheckBox('consent', 'Y', Rect(50, 152, 70, 172), False);
  Pdf.CurrentPage.AddRadioButton('coverage', 'basic', Rect(50, 192, 70, 212), True);
  Pdf.CurrentPage.AddRadioButton('coverage', 'full', Rect(90, 192, 110, 212), False);
  Pdf.CurrentPage.AddComboBox('plan', 'Standard',
    ['Basic', 'Standard', 'Premium'], Rect(50, 227, 200, 252));

  Pdf.AddPage;  // CurrentPage ora punta alla pagina 2
  Pdf.CurrentPage.AddListBox('riders', 'None',
    ['None', 'Flood', 'Earthquake'], Rect(50, 192, 200, 292));
end;

Le coordinate usano lo stesso sistema in alto a sinistra di TextOut e delle altre chiamate di disegno della pagina: X cresce verso destra, Y cresce verso il basso dal bordo superiore, in punti, e ogni TRect si legge alla maniera VCL, con Top come Y minore e Bottom come Y maggiore. HotPDF converte il rettangolo nello spazio utente PDF bottom-up quando scrive il /Rect del widget, quindi Rect(50, 100, 200, 120) si trova vicino all'alto di una pagina Letter, non al fondo. Una tabella di layout importata da coordinate PDF pure, dove Y già conta dal bordo inferiore verso l'alto, esce specchiata verticalmente, con ogni campo rovesciato all'estremità sbagliata della pagina. Tieni tutto il modulo in un unico sistema di riferimento e lascia fare la conversione a HotPDF. L'eccezione è la famiglia AddLoaded* che aggiunge campi a un file esistente: quei metodi accettano angoli nello spazio utente PDF, con l'origine in basso a sinistra

Collegare pulsanti ad azioni URI, JavaScript e submit

Un pulsante push non fa nulla finché non gli viene associata un'azione. HotPDF espone i tipi di azione di ISO 32000-1 §12.6.4 tramite l'enumerazione THPDFButtonAction — baURI, baJavaScript, baSubmitURL, baResetForm, baHide, baShow, baNamed — più due metodi che creano il pulsante e legano l'azione in un unico passaggio

Tipi di azione dei pulsanti HotPDF in Delphi: collegamenti baURI, script baJavaScript, e invio SubmitForm con flag di formato espliciti
Una sola chiamata di binding aggancia uno qualsiasi dei tre dizionari azione, e solo la variante submit porta un contratto di flag con l'endpoint ricevente
// Apre una pagina di aiuto nel browser di sistema
Pdf.CurrentPage.AddPushButtonWithAction('btnHelp', 'Help',
  'https://www.example.com/claims-help', Rect(320, 62, 420, 92), baURI);

// Run viewer-side JavaScript
Pdf.CurrentPage.AddPushButtonWithAction('btnRecalc', 'Recalculate',
  'app.alert("Totals updated.");', Rect(320, 102, 420, 132), baJavaScript);

// Invia come XFDF tenendo i campi vuoti nel payload
Pdf.CurrentPage.AddPushButtonWithSubmitAction('btnSubmit', 'Submit claim',
  'https://api.example.com/claims', Rect(320, 142, 420, 172),
  [sffXFDF, sffIncludeNoValueFields]);

I flag di invio meritano più attenzione progettuale di quanta ne ricevano di solito. AddPushButtonWithSubmitAction accetta un set THPDFSubmitFormFlags, e un set vuoto produce un normale post url-encoded: il formato che molti endpoint di esempio accettano e molti endpoint di produzione no. Aggiungere sffXFDF passa il payload a XFDF; sffGetMethod cambia il verbo HTTP; sffIncludeNoValueFields fa comparire nel payload anche i campi vuoti invece di eliminarli in silenzio, cosa importante quando il consumer distingue "assente" da "vuoto". Il set di flag è di fatto parte del contratto di interfaccia con l'endpoint ricevente, quindi sceglilo insieme al team che interpreta la sottomissione, non dopo il primo lotto respinto

JavaScript a livello di campo: keystroke, format, validate

Oltre ai click sui pulsanti, HotPDF collega JavaScript agli eventi per-campo che i viewer con supporto script generano durante l'immissione dei dati. I tre trigger corrispondono a momenti diversi del ciclo di input: le azioni keystroke girano mentre arrivano i caratteri, le azioni format riscrivono il valore visualizzato dopo il commit di una modifica, e le azioni validate accettano o rifiutano il valore confermato

Ciclo di vita degli eventi JavaScript a livello di campo HotPDF da tasto premuto a validate a format, con l'avvertenza della validazione lato server sotto
Gli script keystroke e validate possono rifiutare l'input mentre format ritocca soltanto la visualizzazione, e nessuno script sopravvive a un lettore senza motore JavaScript
// Rifiuta i valori confermati che non sembrano indirizzi email plausibili
Pdf.AttachFieldKeyStrokeAction('applicant.email',
  'if (event.willCommit && !/^[\w.-]+@[\w.-]+\.\w+$/.test(event.value)) event.rc = false;');

// Mostra i numeri di telefono USA come (NNN) NNN-NNNN
Pdf.AttachFieldFormatAction('applicant.phone',
  'event.value = event.value.replace(/(\d{3})(\d{3})(\d{4})/, "($1) $2-$3");');

// Rifiuta i richiedenti sotto i 18 anni al commit
Pdf.AttachFieldValidateAction('applicant.age',
  'if (parseInt(event.value) < 18) event.rc = false;');

Impostare event.rc = false dentro uno script keystroke o validate fa rifiutare l'input al viewer. Il limite da tenere presente: questo codice viene eseguito solo nei viewer che includono un motore JavaScript. Acrobat e pochi prodotti desktop lo eseguono; la maggior parte dei reader mobili, dei renderer incorporati nei browser e delle pipeline di stampa lo ignora completamente. Gli script di campo migliorano la qualità dei dati per gli utenti che li ricevono: non sono un confine di sicurezza, e ogni valore inviato deve comunque essere validato dal server quando arriva

Difetti che superano la revisione visiva

Alcuni difetti dei moduli sono strutturalmente invisibili a un controllo "apri e guarda". Questi quattro spiegano la maggior parte delle escalation AcroForm che arrivano al supporto, e ciascuno può essere intercettato meccanicamente prima del rilascio:

  • Deriva del valore di esportazione. Una checkbox creata come AddCheckBox('consent', 'Yes', ...) invia Yes; un consumer che fa match su Y respinge ogni sottomissione mentre la pagina appare perfetta. Esporta da Acrobat il modulo compilato come XFDF e confronta i valori con lo schema del consumer
  • Mirroring accidentale dei valori. Nomi completamente qualificati duplicati fondono i campi in uno solo. Il sintomo compare durante l'immissione dati, mai durante la generazione, quindi testa digitando nel modulo, non solo renderizzandolo
  • Valori combo fuori dall'elenco delle opzioni. Se il valore corrente passato a AddComboBox non è tra le opzioni, i viewer non concordano se mostrarlo, svuotarlo o segnalarlo. Mantieni il default dentro l'elenco
  • Campi ancora modificabili dopo la chiusura del workflow. HotPDF non ha una chiamata di appearance-flattening per i campi AcroForm; il modo supportato per congelare un modulo completato è creare i campi con il flag ffReadOnly, che mantiene il valore renderizzato tramite lo stream di appearance del campo bloccando le modifiche. Un campo read-only resta un oggetto form vivo, che gli strumenti downstream di assemblaggio e firma gestiscono in modo prevedibile

Un comportamento lato viewer merita una nota di regressione anche se nessuna modifica al codice lo risolve: le distribuzioni enterprise di Acrobat possono disabilitare JavaScript o limitare i target di submit per policy, quindi un'azione che funzionava in sviluppo può essere inerte sul desktop del cliente. Prevedi un fallback visibile per l'invio — almeno un'istruzione stampata — nel caso in cui il pulsante non faccia nulla

Dove il lavoro sui form incontra il resto del documento

Un campo firma è a sua volta un tipo di campo AcroForm, quindi un modulo che verrà certificato o controfirmato più tardi dovrebbe riservare quel campo durante la generazione invece di applicarlo a posteriori; i dettagli byte-level sono trattati nell'articolo collegato su firme digitali e firma PAdES con HotPDF. Se gli input arrivano come pacchetti XFA invece che come AcroForm nativi, appiattire XFA in campi AcroForm è un workflow separato con un proprio modello di perdita, perché le due tecnologie di modulo sono mutuamente esclusive dentro un singolo file

I metodi per campi, azioni e trigger mostrati qui fanno parte dell'API standard HotPDF Delphi Component per Delphi e C++Builder; la pagina prodotto collega il riferimento completo, inclusi gli overload dei flag di campo e l'enumerazione completa dei flag di submit