La libreria PDF losLab legge e scrive nodi di dati ripetitivi nei moduli dinamici XFA tramite le espressioni di percorso XFA 3.3 SOM: GetXFAFormFieldValue e SetXFAFormFieldValue accettano le radici $data, $record e !data, i caratteri jolly per le occorrenze [*], i selettori di discendenti e jolly per i figli, la proprietà parent, i selettori di classe #dataGroup/#dataValue e semplici predicati in stile FormCalc, consentendo a un programma Delphi di aggiornare ogni riga di fattura in un modulo fiscale governativo con un'unica chiamata. Questo articolo è il riferimento per l'indirizzamento; per lo spostamento di interi set di dati tra file, consulta l'articolo correlato sull'interscambio di dati di moduli FDF, XFDF e XFA
Il problema si presenta nel momento in cui un modulo cessa di essere piatto. Una dichiarazione IVA o una dichiarazione doganale strutturata come modulo XFA dinamico non ha dodici campi denominati da Total_Price_1 a Total_Price_12; ha un sotto-modulo Detail dichiarato una sola volta nel DOM del modello e istanziato tutte le volte richieste dai dati. I valori risiedono in un secondo albero, il Data DOM all'interno del pacchetto dei dataset, dove ogni riga è un elemento <Detail> ripetuto sotto <Receipt>. I nomi di campo piatti non possono fare riferimento a "il prezzo sulla terza riga" o a "ogni riga superiore a 200"; questo è esattamente il compito che il capitolo Scripting Object Model delle specifiche Adobe XFA 3.3 assegna alle espressioni SOM, ed è la sintassi utilizzata dalle API del valore dei campi XFA della libreria PDF losLab
Come si indirizzano i nodi dati XFA da Delphi?
Ogni percorso di dataset parte da una radice e la libreria PDF losLab accetta le diciture standard in modo intercambiabile: $data è la forma abbreviata XFA 3.3 di xfa.datasets.data, !data è la forma abbreviata con radice in xfa.datasets e, nei pacchetti che non utilizzano l'elaborazione dei record, $record si risolve nel record di dati esterno, the primo elemento sotto il nodo dati. Le API lato modello (template-side) come SetXFAFormFieldAccess accettano $template e xfa.template nello stesso modo. I segmenti possono essere delimitati da punti o da barre ($data.Receipt.Tax o $data/Receipt/Tax), un dettaglio importante perché GetXFAFormFieldNames elenca i campi como percorsi delimitati da barre che possono essere inseriti direttamente nelle API dei valori e del modello. Gli indici delle occorrenze sono a base zero in conformità con le specifiche, quindi Detail[0] rappresenta la prima riga. Due regole di escape mantengono indirizzabili i nomi insoliti: in un percorso delimitato da punti, \. indica un punto letterale (Line\.Item), mentre i percorsi delimitati da barre trattano i punti come caratteri ordinari. I dati aziendali che contengono un proprio prefisso di spazio dei nomi XML vengono associati in base al nome locale, quindi <m:Receipt> risponde comunque a Receipt
var
Lib: TPDFlib;
Tax: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('vat-return.pdf', '');
// Equivalent addresses for the same data node
Tax := Lib.GetXFAFormFieldValue('Receipt.Tax');
Tax := Lib.GetXFAFormFieldValue('$data.Receipt.Tax');
Tax := Lib.GetXFAFormFieldValue('xfa.datasets.data.Receipt.Tax');
Tax := Lib.GetXFAFormFieldValue('$record.Tax');
// Slash form with a zero-based occurrence index
Lib.SetXFAFormFieldValue('$record/Detail[0]/Total_Price', '251.00');
Lib.SaveToFile('vat-return-updated.pdf');
finally
Lib.Free;
end;
end;
Come si compilano le righe ripetitive in un modulo XFA a livello di programmazione?
Il carattere jolly per l'occorrenza [*] è lo strumento per le operazioni batch. Mentre un indice numerico seleziona un singolo nodo dello stesso livello (sibling), [*] seleziona tutti i nodi con lo stesso nome, per cui $data.Receipt.Detail[*].Total_Price indirizza contemporaneamente il campo del prezzo in ogni riga. In lettura, GetXFAFormFieldValue restituisce i valori di tutti i nodi corrispondenti uniti con un separatore |; in scrittura, SetXFAFormFieldValue aggiorna ogni nodo corrispondente con lo stesso valore e restituisce 1 in caso di successo. Un percorso che non trova corrispondenze viene letto come una stringa vuota, rappresentando un modo rapido per verificare se un ramo esiste prima di scrivervi
var
Lib: TPDFlib;
Prices: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('invoice.pdf', '');
// All Detail rows at once: '250.00|60.00'
Prices := Lib.GetXFAFormFieldValue('$data.Receipt.Detail[*].Total_Price');
// Reset one column across every repeated row in a single call
Lib.SetXFAFormFieldValue('$data.Receipt.Detail[*].Surcharge', '0.00');
Lib.SaveToFile('invoice-updated.pdf');
finally
Lib.Free;
end;
end;
Selettori di discendenti, jolly per i figli e parent
Tre selettori strutturali coprono i casi in cui si conosce il nome del campo ma non la sua esatta profondità. Il selettore di discendenti .. trova corrispondenze a qualsiasi profondità al di sotto del nodo corrente: $data..Total_Price trova il primo Total_Price ovunque si trovi sotto la radice dei dati, e una scrittura tramite un percorso di discendenti aggiorna quel primo nodo corrispondente. Nota il limite: un indice di occorrenza dopo una corrispondenza di discendenti non si sposta sui rami dello stesso livello, quindi se $data..Total_Price[0] si risolve all'interno della prima riga Detail, $data..Total_Price[1] restituisce vuoto invece di passare alla riga successiva; usa Detail[*] quando li desideri tutti. Il jolly per i figli .* corrisponde a ogni figlio diretto e consente di filtrare i restanti segmenti: $data.Receipt.*.Total_Price raggiunge il Total_Price all'interno di ciascun ramo figlio di Receipt senza selezionare anche un campo di riepilogo con lo stesso nome situato direttamente su Receipt stesso. Infine, la proprietà parent sale di un livello, cosa che XFA 3.3 separa deliberatamente da ..: $data.Receipt.Detail[1].parent.Tax parte dal secondo elemento, sale a Receipt e arriva al relativo valore Tax dello stesso livello
Selettori di classe e predicati: #dataGroup, #dataValue, .[espressione]
La sintassi #class indirizza i nodi del Data DOM per classe di oggetto invece che per nome, rappresentando la via più pratica quando i tag XML contengono caratteri complessi da scrivere come nomi di script. La libreria PDF losLab mappa #dataGroup sugli elementi che hanno elementi figli e #dataValue sugli elementi foglia senza elementi figli, ed entrambi si combinano con occorrenze numeriche o [*]: $data.Receipt.#dataGroup[0].#dataValue[0] seleziona il primo valore all'interno del primo gruppo sotto Receipt. Il selettore di predicati .[espressione] filtra per contenuto i nodi dello stesso livello con lo stesso nome. I predicati supportati sono semplici confronti tra un valore figlio e un valore letterale (literal), con operatori relazionali come >, < e <=; quando un predicato corrisponde a più righe, le letture vengono restituite unite con | e le scritture aggiornano ogni corrispondenza
// Flag every line whose Total_Price exceeds 200
Lib.SetXFAFormFieldValue(
'$data.Receipt.Detail.[Total_Price > 200].Review_Flag', '1');
// Descriptions of all low-value lines, '|'-joined when several match
Desc := Lib.GetXFAFormFieldValue(
'$data.Receipt.Detail.[Total_Price <= 200].Description');
// Class-based addressing when tag names resist script spelling
Total := Lib.GetXFAFormFieldValue(
'$data.Receipt.#dataGroup[0].#dataValue[0]');
Quali funzionalità XFA SOM non sono supportate?
Il supporto SOM nella libreria PDF losLab è limitato ai dataset e ai percorsi dei campi del modello, e i limiti sono espliciti e non di tipo best-effort. Conoscerli in anticipo evita di dover eseguire il debug di un percorso che restituisce silenziosamente una stringa vuota
- I predicati non eseguono FormCalc o JavaScript arbitrari: nessuna chiamata a funzioni (un predicato
contains(...)restituisce vuoto), nessuna espressione booleana composta, ma solo un singolo confrontochild op literal $recordfunziona solo nei pacchetti senza elaborazione dei record; il paging dei record di dataWindow e la rotazione dei gruppi di record non sono implementati- La risoluzione avviene direttamente sul Data DOM e sul DOM del modello; la risoluzione relativa del Form DOM, ovvero la visualizzazione unificata che un visualizzatore compila a runtime, non viene eseguita
- Le semantiche di caricamento degli attributi del Data DOM non vengono applicate; la libreria legge il pacchetto dei dataset come XML grezzo, quindi i percorsi indirizzano gli elementi, non gli attributi promossi a nodi
Questi limiti raramente rappresentano un problema nel lavoro di compilazione batch, poiché chi compila indirizza i dati per struttura piuttosto che per logica di script. Quando si verificano, l'alternativa consiste nel lavorare a livello di pacchetto: leggere l'intero XML del dataset, trasformarlo in Delphi e riscriverlo
Scrittura dell'intero pacchetto con SetXFAFromString
SetXFAFromString è il punto di accesso a livello di pacchetto: installa una stringa XDP completa, comprensiva di modello e dataset, creando il contenitore AcroForm in un nuovo documento qualora non ne esista ancora uno, e accetta sia pacchetti codificati in UTF-16 con marcatori dell'ordine dei byte (BOM) sia in UTF-8. Uno schema di produzione comune consiste nel mantenere l'XDP rilasciato dall'ente come elemento fisso, caricarlo con SetXFAFromString, quindi eseguire chiamate SetXFAFormFieldValue indirizzate tramite SOM per i valori volatili di ciascuna fattura prima di salvare. Poiché XFA è uno dei due modelli di moduli che un PDF può contenere, la controparte classica AcroForm ha una propria gestione dell'automazione, trattata nell'articolo sulle azioni interattive dei moduli e JavaScript
Le API dei valori dei campi, di enumerazione e dei pacchetti XFA mostrate qui sono incluse nella losLab PDF Library per Delphi, C# e VB.NET; la pagina del prodotto contiene il riferimento completo per la gestione dei moduli