Un campo di modulo PDF da solo è solo una casella che contiene un valore. Ciò che fa comportare un modulo come una piccola applicazione è l'azione ad esso collegata: un clic che nasconde una sezione, recupera valori salvati da un file, salta all'ultima pagina o esegue uno script che calcola il totale di una colonna. Nulla di tutto questo risiede nel campo. Vive in un dizionario delle azioni, e lo standard ISO 32000-1 organizza l'intera famiglia nel §12.6. Questo articolo esamina le azioni a cui un programma Delphi fa ricorso più spesso e mostra come PDFlibPas collega ciascuna di esse a un campo o a un collegamento.
Il modello mentale da tenere a mente è che un campo e un'azione sono oggetti separati uniti da un riferimento. Un'annotazione di tipo widget o un'annotazione di tipo collegamento porta un'azione nella sua voce /A. L'azione identifica il campo su cui opera tramite il titolo, non tramite l'indice, quindi il titolo assegnato a un campo è il riferimento che ogni azione successiva utilizzerà per trovarlo. Una volta chiara questa divisione, l'API smette di sembrare un insieme disordinato di chiamate e inizia a presentarsi come un unico pattern applicato a quattro tipi di verbo.
Azioni con nome: navigazione senza numero di pagina
Le azioni più semplici non richiedono alcun parametro. Lo standard ISO 32000-1 §12.6.4.11, Tabella 194, definisce le azioni con nome: il visualizzatore interpreta un nome simbolico a runtime invece di seguire una destinazione memorizzata. Quattro nomi sono supportati universalmente, e sono esattamente quelli che un lettore si aspetta da una barra degli strumenti: NextPage, PrevPage, FirstPage e LastPage. Poiché la destinazione è relativa alla pagina che il visualizzatore sta mostrando in quel momento, un pulsante Successivo creato in questo modo funziona su ogni pagina senza dover calcolare un target.
In PDFlibPas un'azione con nome viene associata a un rettangolo hotspot sulla pagina corrente. Il quarto e il quinto argomento intero selezionano il verbo e l'aspetto.
// NamedActionType: 0 = NextPage, 1 = PrevPage, 2 = FirstPage, 3 = LastPage
// Options bit 0 (value 1) draws a border around the hotspot
Pdf.AddLinkToNamedAction(500, 560, 60, 18, 0, 1); // Next
Pdf.AddLinkToNamedAction(40, 560, 60, 18, 1, 1); // Previous
Pdf.AddLinkToNamedAction(110, 560, 60, 18, 3, 1); // jump to last page
Non c'è alcuna destinazione da mantenere sincronizzata, ed è proprio questo il punto. Un'azione con nome sopravvive all'inserimento e all'eliminazione delle pagine perché non fa mai riferimento a una pagina specifica. Questo contrasta con un collegamento di tipo go-to esplicito, che memorizza l'indice di una pagina di destinazione che deve essere ricalcolato non appena il documento cresce.
L'azione Hide e l'insidia dell'array
L'azione Hide, definita in ISO 32000-1 §12.6.4.10, Tabella 196, attiva o disattiva la visibilità di uno o più campi. È il modo più pulito per creare comportamenti di visualizzazione e occultamento senza ricorrere a script, ed è l'ideale per un collegamento Mostra dettagli o per due pannelli mutuamente esclusivi in cui la visualizzazione dell'uno nasconde l'altro. L'azione porta una destinazione nella sua voce /T e un booleano /H che decide la direzione: nascondi quando è vero, mostra quando è falso.
La sottigliezza sta interamente nel modo in care viene codificato il target, ed è il tipo di dettaglio che produce un modulo funzionante sulla propria macchina ma che fallisce su quella di un cliente. Quando l'azione fa riferimento a un singolo campo, la voce /T viene scritta come una stringa di testo singola. Quando fa riferimento a più campi, la voce /T viene scritta come un array di stringhe di testo. I visualizzatori più datati non gestiscono un array a elemento singolo nello stesso modo in cui gestiscono una stringa semplice, quindi la codifica deve variare in base al conteggio: un nome singolo deve essere emesso come stringa, non come array di lunghezza uno, affinché la più ampia gamma di lettori possa interpretarlo correttamente. PDFlibPas prende questa decisione al posto tuo. Passi i nomi dei campi separati da virgole, punti e virgola o interruzioni di riga, e il writer emetterà una stringa singola per un solo nome e un array per due o più nomi.
// HideFlag non-zero hides the listed fields (/H true); zero shows them.
// One name -> /T is a text string. Two or more -> /T is an array of strings.
Pdf.AddLinkToHideField(40, 700, 90, 18, 'ShippingAddress', 1, 1);
Pdf.AddLinkToHideField(140, 700, 90, 18,
'ShippingName,ShippingAddress,ShippingZip', 1, 1);
Poiché l'azione non fa riferimento ad alcuna risorsa esterna, rimane compatibile con lo standard PDF/A. I nomi passati sono i titoli dei campi completamente qualificati, ed è per questo che un campo figlio all'interno di un gruppo deve essere indirizzato tramite il suo percorso completo con i punti anziché solo con il suo nome terminale.
ImportData: precompilazione da FDF
Mentre l'azione Hide riorganizza ciò che si trova già sulla pagina, l'azione import-data introduce valori dall'esterno. Lo standard ISO 32000-1 §12.6.4.8, Tabella 198, la definisce come un'azione che popola l'AcroForm a partire da un file Forms Data Format sul disco. Questa è l'azione alla base dei controlli per ricaricare dati di esempio o ripristinare i valori predefiniti, in cui un file FDF viene distribuito insieme al PDF e contiene i valori dei campi canonici. La chiamata rispecchia le altre, accettando il rettangolo dell'hotspot, il percorso dell'FDF e una maschera di bit per l'aspetto: Pdf.AddLinkToImportData(40, 660, 120, 18, 'defaults.fdf', 1). Non è necessario che il file esista quando il PDF viene creato, ma deve essere presente quando l'utente fa clic, e gli eventuali backslash nel percorso vengono riscritti automaticamente nel formato con barra standard del PDF.
Un vincolo merita di essere esposto chiaramente poiché rappresenta spesso una sorpresa. Un'azione import-data fa riferimento a un file esterno, pertanto non è consentita in PDF/A. Quando il documento è in modalità PDF/A, la chiamata restituisce zero e non aggiunge nulla, per evitare di produrre un file che fallirebbe la validazione. Se il tuo flusso di lavoro è destinato all'archiviazione, la precompilazione deve avvenire al momento della generazione scrivendo direttamente i valori dei campi, senza rimandarli al momento del clic.
JavaScript: pacchetti globali e script per azione
Per logiche che vanno oltre la visualizzazione, l'occultamento e l'importazione, la famiglia di azioni ricorre al JavaScript a livello di documento. Ci sono due punti distinti in cui uno script può risiedere, e la differenza è importante. Un pacchetto JavaScript a livello di documento viene memorizzato una volta per l'intero file ed eseguito all'apertura del documento, il che lo rende il luogo ideale per definizioni di funzioni e stato condiviso. Uno script per azione viene associato a un singolo collegamento o campo ed eseguito solo quando quell'oggetto viene attivato, rendendolo la scelta ideale per quella singola riga che richiama una funzione già definita nel pacchetto globale.
PDFlibPas espone entrambi i meccanismi. AddGlobalJavaScript memorizza un pacchetto con nome a livello di documento; il riutilizzo di un nome sostituisce qualsiasi codice precedentemente memorizzato con lo stesso nome. AddLinkToJavaScript associa uno script a un hotspot in modo che un clic lo esegua.
// Document-level package: define a reusable function once.
Pdf.AddGlobalJavaScript('Totals',
'function recalcTotal() {' +
' var net = this.getField("Net").value;' +
' var tax = this.getField("Tax").value;' +
' this.getField("Gross").value = Number(net) + Number(tax);' +
'}');
// Per-action script on a link: just call the shared function.
Pdf.AddLinkToJavaScript(40, 620, 100, 18, 'recalcTotal();', 1);
Mantenere la funzione nel pacchetto globale e la chiamata nel collegamento non è una preferenza di stile. Evita di duplicare lo stesso codice su ogni controllo che ne ha bisogno, e fa sì che un visualizzatore con lo scripting disabilitato semplicemente non faccia nulla al clic, invece di bloccarsi su un blocco di codice incorporato non valido. Inoltre, mantiene piccole le voci delle singole azioni, rendendo il file più leggibile in caso di ispezione futura.
Campi, campi figli e consolidamento del risultato
Le azioni hanno bisogno di campi su cui agire, quindi è utile vedere come nasce un campo. NewFormField crea un campo sulla pagina corrente e ne restituisce l'indice; il tipo intero seleziona il tipo, dove 1 è Testo, 2 è Pulsante, 3 è Casella di controllo, 4 è Pulsante di opzione, 5 è Scelta, 6 è Firma e 7 è un Genitore che possiede figli ma non disegna nulla. Il titolo passato non può contenere punti, poiché il punto è il separatore nei nomi completamente qualificati che le azioni utilizzano per indirizzare i figli.
I gruppi di pulsanti di opzione e i moduli gerarchici vengono creati assegnando campi figli a un campo genitore. NewChildFormField aggiunge un figlio sotto un genitore con nome, e nei casi di pulsanti di opzione e scelte AddFormFieldSub aggiunge le singole opzioni e restituisce un indice temporaneo da utilizzare per posizionare ciascuna di esse. Quando la fase interattiva è terminata e desideri consolidare un campo in modo che il suo aspetto corrente dipenti contenuto permanente della pagina, FlattenFormField disegna il campo sulla pagina e lo rimuove dal modulo. Dopo un consolidamento, gli indici dei campi successivi si spostano verso il basso di uno, un aspetto importante da ricordare se si consolidano più campi all'interno di un ciclo.
var
Pdf: TPDFlib;
FldShip: Integer;
begin
Pdf := TPDFlib.Create;
try
Pdf.SetOrigin(1); // top-left origin
Pdf.SetPageSize('A4');
Pdf.NewPage;
// A text field the Hide action will target by its title.
FldShip := Pdf.NewFormField('ShippingAddress', 1);
Pdf.SetFormFieldBounds(FldShip, 40, 120, 240, 20);
Pdf.SetFormFieldValue(FldShip, '');
// Wire a Hide link and a navigation link to this page.
Pdf.DrawText(40, 110, 'Toggle shipping block:');
Pdf.AddLinkToHideField(220, 100, 70, 16, 'ShippingAddress', 1, 1);
Pdf.AddLinkToNamedAction(500, 800, 60, 18, 3, 1); // Last page
// A document-level script available to every event in the file.
Pdf.AddGlobalJavaScript('OnOpen',
'app.alert("Form ready", 3);');
// Freeze the field if the output should no longer be editable.
// Pdf.FlattenFormField(FldShip);
if Pdf.SaveToFile('form_actions.pdf') <> 1 then
raise Exception.Create('Save failed');
finally
Pdf.Free;
end;
end;
La chiamata a flatten è commentata intenzionalmente. Escludendola, il documento viene distribuito come un modulo attivo le cui azioni si attivano nel lettore. Abilitandola, il campo viene convertito in elementi statici, il che è ideale quando il modulo è stato completato e il risultato deve viaggiare come record fisso. Lo stesso campo, lo stesso codice, due documenti molto diversi a seconda che si scelga di consolidare o meno.
Scegliere il verbo corretto
Le quattro azioni si dividono chiaramente in base a ciò su cui intervengono. Un'azione con nome sposta la visualizzazione e non richiede alcun campo. Un'azione Hide modifica la visibilità e richiede i titoli dei campi, con la codifica stringa-contro-array gestita automaticamente. Un'azione import-data fa riferimento a un file sul disco ed è quindi vietata in PDF/A. Un'azione JavaScript esegue una logica arbitraria ed è preferibile suddividerla tra un pacchetto globale di funzioni e piccole chiamate per azione. Scegli l'opzione più semplice che svolge il compito: un'azione Hide è più portabile di uno script che imposta un flag nascosto, e un'azione con nome è più duratura di una destinazione di pagina memorizzata perché non richiede la manutenzione di numeri di pagina.
Da qui, due argomenti correlati completano il quadro. Se il modulo fa parte di un documento accessibile, l'albero di struttura per i lettori di schermo è trattato nel nostro articolo sui PDF taggati e la struttura di accessibilità. Quando il modulo completato deve essere bloccato e firmato, il flusso di lavoro è descritto nella guida all'ambiente di firma e conformità. Tutti e tre si basano sullo stesso motore, distribuito come la libreria PDF per Delphi insieme alle API di creazione, gestione moduli e firme trattate altrove in questo blog.