losLab PDF Library leest en schrijft herhalende gegevensknooppunten in dynamische XFA-formulieren via XFA 3.3 SOM-padexpressies: GetXFAFormFieldValue en SetXFAFormFieldValue accepteren de roots $data, $record en !data, [*]-voorkomingswildcards, nakomelingen- en directekind-wildcardselectors, de eigenschap parent, de klasseselectors #dataGroup/#dataValue en eenvoudige predicaten in FormCalc-stijl, zodat een Delphi-programma elke factuurregel in een overheidsbelastingformulier met één aanroep kan bijwerken. Dit artikel is de referentie voor adressering; zie voor het verplaatsen van volledige gegevenssets tussen bestanden het bijbehorende artikel over FDF-, XFDF- en XFA-formuliergegevensuitwisseling
Het probleem doet zich voor zodra een formulier niet langer plat is. Een btw-aangifte of douaneverklaring die is gebouwd als een dynamisch XFA-formulier heeft geen twaalf velden met de namen Total_Price_1 tot en met Total_Price_12; het heeft één Detail-subformulier dat eenmaal in de template-DOM is gedeclareerd en zo vaak wordt geïnstantieerd als de gegevens vereisen. De waarden bevinden zich in een tweede boom, de Data-DOM in het datasets-pakket, waar elk regelitem een herhaald element <Detail> is onder <Receipt>. Platte veldnamen kunnen niet "de prijs op de derde regel" of "elke regel boven de 200" adresseren; dat is precies de taak die het hoofdstuk Scripting Object Model van de Adobe XFA 3.3-specificatie toekent aan SOM-expressies, en het is de syntaxis die de XFA-veldwaarde-API's van losLab PDF Library spreken
Hoe adresseert u XFA-gegevensknooppunten vanuit Delphi?
Elk datasetspad begint bij een root, en losLab PDF Library accepteert de standaardspellingen door elkaar: $data is de korte vorm van xfa.datasets.data in XFA 3.3, !data is de korte vorm met xfa.datasets als basis, en in pakketten die geen recordverwerking gebruiken, verwijst $record naar het buitenste gegevensrecord, het eerste element onder het gegevensknooppunt. API's aan de sjabloonzijde, zoals SetXFAFormFieldAccess, accepteren $template en xfa.template op dezelfde manier. Segmenten kunnen door punten of slashes worden gescheiden ($data.Receipt.Tax of $data/Receipt/Tax), wat van belang is omdat GetXFAFormFieldNames velden opsomt als door slashes gescheiden paden die direct weer in de waarde- en sjabloon-API's kunnen worden ingevoerd. Voorkomens-indexen zijn op nul gebaseerd volgens de specificatie, dus Detail[0] is de eerste rij. Twee ontsnappingsregels zorgen ervoor dat ongebruikelijke namen adresseerbaar blijven: in een door punten gescheiden pad staat \. voor een letterlijke punt (Line\.Item), terwijl door slashes gescheiden paden punten als gewone tekens behandelen. Bedrijfsgegevens die een eigen XML-naamruimtevoorvoegsel dragen, worden gekoppeld op basis van de lokale naam, dus <m:Receipt> reageert nog steeds op 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;
Hoe vult u herhalende rijen in een XFA-formulier programmatisch in?
De voorkomenswildcard [*] is het gereedschap voor batchverwerking. Waar een numerieke index één specifiek knooppunt selecteert, selecteert [*] alle knooppunten met dezelfde naam op hetzelfde niveau, zodat $data.Receipt.Detail[*].Total_Price de prijsvelden op alle regelitems tegelijk adresseert. Bij het lezen retourneert GetXFAFormFieldValue de waarden van alle overeenkomende knooppunten, samengevoegd met een scheidingsteken |; bij het schrijven werkt SetXFAFormFieldValue elk overeenkomend knooppunt bij met dezelfde waarde en retourneert 1 bij succes. Een pad dat geen overeenkomsten oplevert, wordt gelezen als een lege tekenreeks, wat de eenvoudige manier is om te controleren of een vertakking bestaat voordat u erin schrijft
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;
Nakomelingen-, kind-wildcard- en parentselectors
Drie structurele selectors dekken de situaties waarin u de veldnaam kent, maar niet de exacte diepte ervan. De nakomelingenselector .. zoekt op elke diepte onder het huidige knooppunt: $data..Total_Price vindt de eerste Total_Price ergens onder de gegevensroot, en een schrijfactie via een nakomelingenpad werkt dat eerste overeenkomende knooppunt bij. Let op de grens: een voorkomensindex na een nakomelingenovereenkomst stapt niet over naar andere vertakkingen, dus als $data..Total_Price[0] binnen de eerste Detail-rij wordt opgelost, retourneert $data..Total_Price[1] een lege waarde in plaats van naar de volgende rij te springen; gebruik Detail[*] als u ze allemaal wilt hebben. De kindwildcard .* komt overeen met elk direct kind en laat de resterende segmenten filteren: $data.Receipt.*.Total_Price bereikt de Total_Price binnen elke kindvertakking van Receipt, zonder ook een gelijknamig samenvattingsveld te selecteren dat direct op Receipt zelf staat. Ten slotte stijgt de eigenschap parent één niveau, wat XFA 3.3 bewust scheidt van ..: $data.Receipt.Detail[1].parent.Tax begint bij het tweede regelitem, stapt over naar Receipt en komt uit bij de bijbehorende waarde Tax
Klasseselectors en predicaten: #dataGroup, #dataValue, .[expressie]
De syntaxis #class adresseert Data-DOM-knooppunten op basis van hun objectklasse in plaats van hun naam, wat handig is wanneer XML-tags tekens bevatten die lastig te spellen zijn als scriptnamen. losLab PDF Library koppelt #dataGroup aan elementen met kind-elementen en #dataValue aan blad-elementen zonder kind-elementen, en beide kunnen worden gecombineerd met numerieke of [*]-voorkomens: $data.Receipt.#dataGroup[0].#dataValue[0] selecteert de eerste waarde binnen de eerste groep onder Receipt. De predicaatselector .[expressie] filtert gelijknamige knooppunten op basis van de inhoud. Ondersteunde predicaten zijn eenvoudige vergelijkingen van een kind-waarde met een letterlijke waarde, met relationele operatoren zoals >, < en <=; wanneer een predicaat met meerdere rijen overeenkomt, worden leesacties geretourneerd met | als scheidingsteken en werken schrijfacties elke overeenkomst bij
// 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]');
Welke XFA SOM-functies worden niet ondersteund?
De SOM-ondersteuning in losLab PDF Library is beperkt tot datasets en templateveldpaden, en de grenzen zijn expliciet in plaats van 'best-effort'. Het vooraf kennen van deze grenzen voorkomt dat u paden moet debuggen die stilletjes een lege tekenreeks retourneren
- Predicaten voeren geen willekeurige FormCalc of JavaScript uit: geen functie-aanroepen (een
contains(...)-predicaat retourneert een lege waarde), geen samengestelde booleaanse expressies, alleen een enkele vergelijking van het typechild op literal $recordwerkt alleen in pakketten die geen recordverwerking gebruiken; dataWindow-recordpaginering en recordgroeprotatie zijn niet geïmplementeerd- Resolutie vindt rechtstreeks plaats tegen de Data-DOM en de template-DOM; relatieve resolutie via de Form-DOM, het samengevoegde beeld dat een viewer tijdens runtime opbouwt, wordt niet uitgevoerd
- Gegevens-DOM-attribuutlaadsemantiek wordt niet toegepast; de bibliotheek leest het datasets-pakket als ruwe XML, dus paden adresseren elementen en geen attributen die zijn gepromoveerd tot knooppunten
Deze grenzen vormen zelden een probleem bij batch-invulwerk, omdat een invuller gegevens adresseert op basis van structuur in plaats van scriptlogica. Mocht dat wel het geval zijn, dan is het pakketniveau de ontsnappingsroute: lees de volledige datasets-XML, transformeer deze in Delphi en schrijf deze terug
Het hele pakket schrijven met SetXFAFromString
SetXFAFromString is dat ingangspunt op pakketniveau: het installeert een complete XDP-tekenreeks, inclusief sjabloon en datasets, en maakt de AcroForm-container in een nieuw document aan als deze nog niet bestaat. Het accepteert zowel UTF-16 gecodeerde pakketten met byte-order marks als UTF-8. Een veelvoorkomende productiemethode is om de door de instantie uitgegeven XDP als basis te bewaren, deze te laden met SetXFAFromString en vervolgens SOM-geadresseerde SetXFAFormFieldValue-aanroepen uit te voeren voor de volatiele factuurwaarden voordat u het bestand opslaat. Omdat XFA een van de twee formuliermodellen is die een PDF kan bevatten, heeft de klassieke AcroForm-zijde zijn eigen automatiseringsverhaal, dat wordt behandeld in het artikel over interactieve formulieracties en JavaScript
De XFA-veldwaarde-, opsommings- en pakket-API's die hier worden getoond, worden geleverd in de losLab PDF Library voor Delphi, C# en VB.NET; de productpagina bevat de volledige referentie voor formulierafhandeling