Articolo tecnico

Moduli PDF a calcolo automatico in Delphi con HotPDF

Una fattura o una nota spese in PDF in cui le singole voci si sommano da sole e la data si formatta automaticamente richiedono tre elementi collegati al relativo AcroForm, e HotPDF li aggiunge tutti e tre a un file caricato in Delphi: ApplyLoadedFieldCalculation per i totali di AFSimple_Calculate, ApplyLoadedFieldValidation per gli helper Acrobat AF di formattazione e convalida, e EnsureLoadedFieldAppearanceStream per generare il valore che un visualizzatore effettivamente disegna

HotPDF è un componente PDF VCL nativo per Delphi e C++Builder, e questi tre metodi operano su un documento aperto con LoadFromFile anziché su uno creato da zero. Ciascuno accetta un indice di campo a base 0 nell'array /Fields dell'AcroForm — lo stesso ordine restituito da GetLoadedFormFieldNames — e scrive direttamente nel dizionario del campo, quindi l'intera procedura consiste nel caricare, indicizzare, associare e chiamare SaveLoadedDocument. La creazione del set di campi in sé è un'attività separata, trattata nella sezione aggiungere campi AcroForm a un PDF caricato in Delphi, mentre questa pagina riguarda l'applicazione di calcoli e formattazione a un set di campi esistente

Perché un modulo PDF ha bisogno di script per sommare i propri campi?

Perché il formato PDF non ha un livello foglio di calcolo. Un campo di testo contiene una stringa, non una formula, e nulla nel file ricalcola un totale al variare di una voce a meno che non vi sia associato uno script per farlo. Il meccanismo è il dizionario delle azioni aggiuntive del campo, /AA (ISO 32000-1 §12.7.5), che mappa gli eventi di attivazione sulle azioni: un trigger di digitazione tasti (K) all'arrivo dei caratteri, un trigger di formattazione per la visualizzazione, un trigger di convalida (V) al commit e un trigger di calcolo (C) che si attiva ogni volta che cambia un qualsiasi campo del modulo. Ciascuna azione è un'azione JavaScript, /S /JavaScript (ISO 32000-1 §12.6.4.14), e lo script che esegue è ciò che effettivamente esegue la somma o la riformattazione. Acrobat fornisce una libreria di helper pronti all'uso — AFSimple_Calculate, AFNumber_Format, AFDate_FormatEx e gli altri — quindi lo script associato è solitamente una chiamata di una sola riga a quella libreria anziché codice personalizzato

Un dettaglio decide se un modulo a più passaggi fornisce la risposta corretta: l'ordine di calcolo. Acrobat esegue ogni campo che contiene un'azione di calcolo e li esegue nella sequenza elencata dall'array /CO dell'AcroForm, non nell'ordine delle pagine. ApplyLoadedFieldCalculation associa l'azione di calcolo al campo indicato; quando un campo calcolato ne alimenta un altro — ad esempio un subtotale letto da una riga fiscale e poi un totale generale che legge entrambi — devi verificare che l'ordine in /CO elenchi ciascun input prima del campo che lo consuma, altrimenti il totale generale leggerà un subtotale obsoleto e rimarrà indietro di una modifica

Come si fa a far calcolare automaticamente i totali in un modulo PDF?

ApplyLoadedFieldCalculation accetta l'indice del campo di destinazione, un'operazione e i nomi dei campi di origine da aggregare. HotPDF scrive AFSimple_Calculate("SUM", ["lineTotal.1", "lineTotal.2", ...]) nell'azione di calcolo del campo di destinazione, e l'enumerazione THPDFFieldCalcOp seleziona la funzione: fccSum, fccAverage, fccProduct, fccMinimum e fccMaximum mappano le funzioni Acrobat SUM, AVG, PRD, MIN e MAX. I nomi di origine sono nomi di campi AcroForm, le stesse stringhe restituite da GetLoadedFormFieldNames, quindi un totale su tre voci e una spesa di spedizione richiede una sola chiamata

var
  Pdf: THotPDF;
  Names: THPDFAnsiStringArray;
  i, TotalIdx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('invoice.pdf');
    Names := Pdf.GetLoadedFormFieldNames;
    TotalIdx := -1;
    for i := 0 to High(Names) do
      if Names[i] = 'grandTotal' then TotalIdx := i;
    if TotalIdx >= 0 then
    begin
      Pdf.ApplyLoadedFieldCalculation(TotalIdx, fccSum,
        ['lineTotal.1', 'lineTotal.2', 'lineTotal.3', 'shipping']);
      Pdf.EnsureLoadedFieldAppearanceStream(TotalIdx);
    end;
    Pdf.SaveLoadedDocument('invoice-live.pdf');
  finally
    Pdf.Free;
  end;
end;

La chiamata a EnsureLoadedFieldAppearanceStream sul totale non è cosmetica. Il calcolo viene eseguito all'interno del visualizzatore, quindi fino a quando un lettore compatibile con gli script non apre il file, il campo del totale non ha un proprio contenuto renderizzato. La generazione di un flusso di aspetto (appearance stream) gli fornisce una cifra da mostrare nel frattempo, che è ciò che un processo de stampa o un generatore di miniature rileverà quando non esegue lo script

Formattazione e convalida dei campi con la suite di helper AF

Formattazione e convalida dei campi con la suite di helper AF

ApplyLoadedFieldValidation associa gli script di formattazione, digitazione tasti e convalida in un'unica chiamata, e il tipo THPDFFieldValidationKind passato decide quale helper Acrobat collegare. Passando fvkCurrency, HotPDF scrive AFNumber_Format(2,0,0,0,"$ ",true) come script di formattazione con la corrispondente AFNumber_Keystroke; fvkNumber utilizza "." come argomento separatore, fvkPercentage emette AFPercent_Format e fvkDate emette AFDate_FormatEx("dd/mm/yyyy") a meno che non si passi una propria maschera nell'argomento DateFormat. I formati speciali — fvkZipCode, fvkSSN e fvkPhone — mappano su AFSpecial_Format con il codice che Acrobat riserva a ciascuno, e fvkArbitraryMask accetta una stringa di maschera tramite l'argomento Mask per tutto ciò che questi preset non coprono. Per i tipi numerici, HotPDF associa anche uno script AFRange_Validate in modo che l'input fuori intervallo venga rifiutato al momento del commit

// Formatta la data della fattura come ISO, l'importo come valuta e maschera un ID fiscale
Pdf.ApplyLoadedFieldValidation(DateIdx,   fvkDate,          'yyyy-mm-dd', '');
Pdf.ApplyLoadedFieldValidation(AmountIdx, fvkCurrency,      '', '');
Pdf.ApplyLoadedFieldValidation(TaxIdIdx,  fvkArbitraryMask, '', '999-99-9999');
Pdf.EnsureLoadedFieldAppearanceStream(AmountIdx);

Gli helper AF coprono le forme comuni in modo da non dover scrivere codice JavaScript manualmente per esse. Quando un campo richiede una logica non espressa da alcun preset — un checksum, una regola tra campi, una regex personalizzata — associa direttamente lo script non elaborato con i metodi delle azioni a livello di campo descritti in creare campi e azioni AcroForm con HotPDF, che accettano direttamente una stringa JavaScript. Entrambe le strade scrivono nello stesso dizionario /AA, quindi un formato di valuta predefinito su un campo e una convalida personalizzata su un altro coesistono nello stesso documento

Perché devi rigenerare l'appearance stream?

Perché la stringa del valore e i pixel disegnati da un visualizzatore sono due cose diverse nel PDF, e modificare l'una non cambia l'altra. Un campo memorizza il suo valore in /V, ma ciò che un lettore renderizza proviene dall'appearance stream del campo, /AP /N. Modificando /V attraverso il grafo degli oggetti, un visualizzatore rigoroso mostrerà comunque il vecchio aspetto fino a quando qualcosa non lo rigenererà. Il dizionario del modulo contiene un flag /NeedAppearances che richiede al visualizzatore di ricostruire ogni aspetto all'apertura, e Acrobat lo rispetta — ma i renderer incorporati nei browser, i lettori mobili, i server di stampa e i generatori di miniature spesso lo ignorano e disegnano qualsiasi stream sia già presente, o nulla del tutto. EnsureLoadedFieldAppearanceStream colma questa lacuna compilando direttamente il Form XObject /AP /N, a partire dai campi /DA, /V e /Rect, in modo che il valore renderizzato viaggi all'interno del file anziché dipendere dal visualizzatore per ridisegnarlo

// Genera un aspetto per ogni campo prima di salvare, in modo che i valori vengano visualizzati ovunque
Names := Pdf.GetLoadedFormFieldNames;
for i := 0 to High(Names) do
  Pdf.EnsureLoadedFieldAppearanceStream(i);
Pdf.SaveLoadedDocument('invoice-flat-appearance.pdf');

Dove gli script AF smettono di funzionare

Ogni script AF* dipende dal fatto che il visualizzatore disponga di un motore JavaScript, e la maggior parte dei visualizzatori non ne ha uno. Acrobat e una manciata di prodotti desktop eseguono questi helper; i visualizzatori PDF integrati in Chrome, Edge, Firefox e nelle principali piattaforme mobili, insieme ai rasterizzatori lato server e alle pipeline di stampa, non eseguono alcun JavaScript per moduli. In questi lettori il calcolo non si attiva mai e la formattazione non si applica mai — il campo mostra semplicemente qualsiasi appearance stream con cui è stato salvato l'ultima volta, ed è esattamente per questo che la generazione di /AP /N è fondamentale. Considera il livello AF* come una comodità per la cerchia di utenti che utilizzano un lettore in grado di eseguire script, mai come una garanzia. Qualsiasi totale che debba essere corretto sul server — un importo di fatturazione o una cifra fiscale da dichiarare — deve essere ricalcolato da parte tua dopo il ritorno del modulo, poiché non puoi presumere che il lettore che lo ha compilato abbia eseguito una sola riga del tuo script

Vale la pena nominare un altro caso: i moduli che arrivano in formato XFA. Un modulo XFA dinamico trasporta il proprio motore di calcolo in un payload XML di cui gli helper AF* non sanno nulla, quindi associare AFSimple_Calculate a un campo XFA non produce alcun effetto. Appiattisci prima il modulo XFA in un semplice AcroForm, come trattato nella sezione appiattimento da XFA ad AcroForm in Delphi, e i tre metodi qui descritti si applicheranno quindi ai campi AcroForm risultanti esattamente come farebbero per qualsiasi altro modulo caricato

Gli AcroForm a calcolo automatico e autoconvalidanti si riducono a tre chiamate su un documento caricato — associa il calcolo, associa la formattazione e la convalida, genera l'aspetto — seguite da SaveLoadedDocument. L'API per moduli caricati mostrata qui fa parte di HotPDF Component per Delphi e C++Builder, che documenta le enumerazioni complete THPDFFieldValidationKind e THPDFFieldCalcOp insieme al resto dell'interfaccia AcroForm