Articolo tecnico

Moduli PDF interattivi in Delphi: Azioni e JavaScript

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 PDF Library for Delphi 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

Diagramma PDF Library for Delphi di un'annotazione link o widget PDF che riferisce un unico dizionario azione che si dirama nei verbi di azione named, hide, import-data e JavaScript, con la scheda import-data contrassegnata come esclusa dal PDF/A
Un solo dizionario azione dietro un link o un widget alimenta quattro verbi distinti — movimento della viewport, visibilità, importazione da disco e scripting

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 PDF Library for Delphi 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
// il bit 0 di Options (valore 1) disegna un bordo attorno all'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);   // salta all'ultima pagina

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. PDF Library for Delphi 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

Diagramma PDF Library for Delphi delle regole di codifica dell'azione Hide PDF in cui esattamente un nome di campo diventa una stringa di testo /T mentre due o più nomi diventano un array di stringhe, più note sulla direzione del flag hide, sulla sicurezza PDF/A e sui titoli di campo pienamente qualificati
Il writer dirama sul numero di campi così un nome singolo viaggia come stringa nuda che i lettori più vecchi onorano, mentre due o più diventano un array che si commuta in blocco
// HideFlag diverso da zero nasconde i campi elencati (/H true); zero li mostra
// Un nome solo -> /T è una stringa di testo. Due o più -> /T è un array di stringhe
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

PDF Library for Delphi 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

Diagramma PDF Library for Delphi del modello JavaScript PDF a due livelli in cui un pacchetto globale a livello documento definisce recalcTotal all'apertura del documento e ogni collegamento porta uno script di una riga per azione che lo chiama al clic
Definire recalcTotal una sola volta nel pacchetto a livello di documento e lasciare che ogni hotspot cliccabile lo raggiunga con una chiamata di una riga per azione
// Package a livello documento: definisci una volta sola una funzione riutilizzabile
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);' +
  '}');

// Script per singola azione su un link: basta chiamare la funzione condivisa
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;

    // Un campo di testo che l'azione Hide colpirà tramite il suo title
    FldShip := Pdf.NewFormField('ShippingAddress', 1);
    Pdf.SetFormFieldBounds(FldShip, 40, 120, 240, 20);
    Pdf.SetFormFieldValue(FldShip, '');

    // Collega a questa pagina un link Hide e un link di navigazione
    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

    // Uno script a livello documento disponibile a ogni evento del file
    Pdf.AddGlobalJavaScript('OnOpen',
      'app.alert("Form ready", 3);');

    // Blocca il campo se l'output non deve più essere modificabile
    // 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