Technický článek

Cesty XFA SOM v Delphi: Dynamická data formulářů

losLab PDF Library čte a zapisuje opakující se datové uzly v dynamických formulářích XFA prostřednictvím cest XFA 3.3 SOM (Scripting Object Model): metody GetXFAFormFieldValue and SetXFAFormFieldValue akceptují kořeny $data, $record a !data, zástupné znaky výskytu [*], selektory potomků a zástupných znaků dětí, vlastnost parent, selektory tříd #dataGroup/#dataValue a jednoduché predikáty ve stylu FormCalc, takže program v Delphi může jedním voláním aktualizovat každý řádek faktury ve státním daňovém formuláři. Tento článek slouží jako referenční příručka pro adresování; informace o přenosu celých datových sad mezi soubory naleznete v doprovodném článku o výměně dat formulářů FDF, XFDF a XFA

Problém se objevuje v okamžiku, kdy formulář přestane být plochý. Přiznání k DPH nebo celní prohlášení vytvořené jako dynamický formulář XFA nemá dvanáct polí s názvy Total_Price_1 až Total_Price_12; má jeden dílčí formulář (subform) Detail deklarovaný jednou v šabloně DOM a vytvořený v tolika instancích, kolik data vyžadují. Hodnoty žijí ve druhém stromu, Data DOM, uvnitř balíčku datasets, kde každá položka řádku představuje opakovaný element <Detail> pod <Receipt>. Ploché názvy polí nedokážou adresovat „cenu na třetím řádku“ nebo „každý řádek nad 200“; to je přesně úkol, který kapitola Scripting Object Model specifikace Adobe XFA 3.3 přiděluje výrazům SOM, a je to syntaxe, kterou hovoří rozhraní API XFA pro hodnoty polí losLab PDF Library

PDF Library for Delphi: Diagram kontrastuje DOM šablony XFA deklarující subform Detail jednou s Data DOM opakujícím jej pro každý příchozí datový záznam
Dynamický formulář XFA deklaruje každý subform jednou v šabloně a opakuje ho v paketu datasets, jakmile přicházejí data

Jak v Delphi adresovat datové uzly XFA?

Každá cesta v datových sadách začíná od kořene a losLab PDF Library přijímá standardní zápisy zaměnitelně: $data je zkrácená forma XFA 3.3 pro xfa.datasets.data, !data je zkrácená forma s kořenem v xfa.datasets a v balíčcích, které nepoužívají zpracování záznamů, se $record vyhodnocuje jako vnější datový záznam, první prvek pod uzlem data. Rozhraní API na straně šablony, jako je SetXFAFormFieldAccess, přijímají $template a xfa.template stejným způsobem. Segmenty mohou být odděleny tečkou nebo lomítkem ($data.Receipt.Tax nebo $data/Receipt/Tax), což je důležité, protože GetXFAFormFieldNames vyjmenovává pole jako cesty oddělené lomítkem, které se předávají přímo zpět do API pro hodnoty a šablony. Indexy výskytů jsou podle specifikace od nuly, takže Detail[0] je první řádek. Dvě pravidla pro uvozování (escaping) udržují adresovatelné i neobvyklé názvy: v cestě oddělené tečkami označuje \. doslovnou tečku (Line\.Item), zatímco cesty oddělené lomítky považují tečky za běžné znaky. Podniková data, která nesou vlastní předponu jmenného prostoru XML, se porovnávají podle lokálního názvu, takže <m:Receipt> stále odpovídá Receipt

var
  Lib: TPDFlib;
  Tax: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('vat-return.pdf', '');
    // Ekvivalentní adresy stejného datového uzlu
    Tax := Lib.GetXFAFormFieldValue('Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('$data.Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('xfa.datasets.data.Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('$record.Tax');
    // Tvar s lomítky a indexem výskytu od nuly
    Lib.SetXFAFormFieldValue('$record/Detail[0]/Total_Price', '251.00');
    Lib.SaveToFile('vat-return-updated.pdf');
  finally
    Lib.Free;
  end;
end;

Jak programově vyplnit opakující se řádky ve formuláři XFA?

Zástupný znak výskytu [*] je nástroj pro hromadné operace. Tam, kde číselný index vybírá jednoho sourozence, [*] vybírá všechny sourozence se stejným názvem, takže $data.Receipt.Detail[*].Total_Price adresuje pole ceny na každé položce řádku najednou. Při čtení vrací GetXFAFormFieldValue hodnoty všech odpovídajících uzlů spojené oddělovačem |; při zápisu aktualizuje SetXFAFormFieldValue každý odpovídající uzel stejnou hodnotou a při úspěchu vrátí 1. Cesta, která neodpovídá ničemu, se přečte jako prázdný řetězec, což představuje snadný způsob, jak před zápisem ověřit, zda větev vůbec existuje

var
  Lib: TPDFlib;
  Prices: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('invoice.pdf', '');
    // Všechny řádky Detail najednou: '250.00|60.00'
    Prices := Lib.GetXFAFormFieldValue('$data.Receipt.Detail[*].Total_Price');
    // Resetujte jeden sloupec ve všech opakovaných řádcích jediným voláním
    Lib.SetXFAFormFieldValue('$data.Receipt.Detail[*].Surcharge', '0.00');
    Lib.SaveToFile('invoice-updated.pdf');
  finally
    Lib.Free;
  end;
end;

Selektory potomků, dětských zástupných znaků a rodičů

Tři strukturální selektory pokrývají případy, kdy znáte název pole, ale ne jeho přesnou hloubku. Selektor potomků .. odpovídá v jakékoli hloubce pod aktuálním uzlem: $data..Total_Price najde první Total_Price kdekoli pod kořenem dat a zápis přes cestu potomka tento první odpovídající uzel aktualizuje. Všimněte si omezení: index výskytu po shodě s potomkem nepřechází přes sourozenecké větve, takže pokud se $data..Total_Price[0] vyhodnotí uvnitř prvního řádku Detail, $data..Total_Price[1] vrátí prázdnou hodnotu, místo aby přeskočil na další řádek; chcete-li všechny, použijte Detail[*]. Zástupný znak pro děti .* odpovídá každému přímému potomkovi a umožňuje zbývajícím segmentům filtrovat: $data.Receipt.*.Total_Price dosáhne na Total_Price uvnitř každé podřízené větve Receipt, aniž by zároveň vybral souhrnné pole se stejným názvem umístěné přímo na samotném Receipt. Nakonec vlastnost parent stoupá o jednu úroveň výše, což specifikace XFA 3.3 záměrně odděluje od ..: cesta $data.Receipt.Detail[1].parent.Tax začíná u druhé položky řádku, vystoupá k Receipt a skončí na jeho sourozenecké hodnotě Tax

Selektory tříd a predikáty: #dataGroup, #dataValue, .[expression]

Syntaxe #class adresuje uzly Data DOM podle třídy objektu namísto názvu, což je praktická cesta, když tagy XML obsahují znaky, které se obtížně zapisují jako názvy skriptů. losLab PDF Library mapuje #dataGroup na prvky, které mají podřízené prvky (elementy), a #dataValue na koncové prvky (listy) bez podřízených prvků, přičemž oba lze kombinovat s číselnými výskyty nebo [*]: $data.Receipt.#dataGroup[0].#dataValue[0] vybere první hodnotu uvnitř první skupiny pod Receipt. Selektor predikátu .[expression] filtruje sourozence se stejným názvem podle obsahu. Podporovány jsou jednoduché predikáty porovnání podřízené hodnoty s literálem pomocí relačních operátorů jako >, < a <=; pokud predikát odpovídá více řádkům, vrátí čtení hodnoty spojené symbolem | a zápis aktualizuje všechny shody

PDF Library for Delphi: Diagram rozebere výraz SOM cesty nad dataset XFA a označí kořen datasetu, oddělené segmenty, index výskytu od nuly a cílovou listovou hodnotu
Kořeny, oddělovače a indexy výskytů od nuly se skládají do cest SOM, přičemž několik zápisů se rozřeší na stejný uzel
// Označte každý řádek, jehož Total_Price překračuje 200
Lib.SetXFAFormFieldValue(
  '$data.Receipt.Detail.[Total_Price > 200].Review_Flag', '1');

// Popisy všech řádků s nízkou hodnotou, při více shodách spojené pomocí '|'
Desc := Lib.GetXFAFormFieldValue(
  '$data.Receipt.Detail.[Total_Price <= 200].Description');

// Adresování podle třídy, když názvy tagů odolávají zápisu ve skriptu
Total := Lib.GetXFAFormFieldValue(
  '$data.Receipt.#dataGroup[0].#dataValue[0]');

Které funkce XFA SOM nejsou podporovány?

Podpora SOM v losLab PDF Library je omezena na sady dat a cesty polí šablony, přičemž hranice jsou definovány explicitně a nikoli s nejlepším úsilím. Jejich znalost předem vám ušetří ladění cesty, která tiše vrací prázdný řetězec

  • Predikáty nespouštějí libovolný FormCalc ani JavaScript: žádná volání funkcí (predikát contains(...) vrací prázdnou hodnotu), no compound boolean expressions, pouze jediné porovnání child op literal
  • $record funguje pouze v balíčcích bez zpracování záznamů; stránkování záznamů dataWindow a rotace skupin záznamů nejsou implementovány
  • Vyhodnocování probíhá přímo proti Data DOM a template DOM; relativní vyhodnocování Form DOM, tedy sloučený pohled, který prohlížeč sestavuje za běhu, se neprovádí
  • Sémantika načítání atributů Data DOM se nepoužije; knihovna čte balíček datasets jako surové XML, takže cesty adresují elementy, nikoli atributy povýšené na uzly

Tato omezení se při hromadném vyplňování projevují málokdy, protože plnič adresuje data podle struktury, nikoli podle naskriptované logiky. Pokud na ně narazíte, únikovou cestou je úroveň balíčku: načtěte celé XML datasets, transformujte jej v Delphi a zapište zpět

PDF Library for Delphi: Diagram dává přehled o šesti strukturálních SOM selektorech pro opakovaná data XFA: zástupný znak výskytu, vyhledání potomka, zástupný znak dítěte, krok rodiče, selektory tříd a obsahové predikáty
Zástupné znaky, potomci, kroky k rodiči, selektory třídy a predikáty obsahu pokrývají každá jinou mezeru v adresování

Zápis celého balíčku pomocí SetXFAFromString

Metoda SetXFAFromString je oním přístupovým bodem na úrovni balíčku: instaluje kompletní řetězec XDP, šablonu a datové sady dohromady, přičemž v novém dokumentu vytvoří kontejner AcroForm, pokud ještě neexistuje, a přijímá balíčky kódované v UTF-16 se značkami pořadí bajtů (BOM) i UTF-8. Běžným produkčním postupem je uchovat úředně vydané XDP jako šablonu, načíst jej pomocí SetXFAFromString a před uložením provést volání SetXFAFormFieldValue adresovaná pomocí SOM pro proměnlivé hodnoty jednotlivých faktur. Vzhledem k tomu, že XFA je jedním ze dvou modelů formulářů, které může PDF obsahovat, klasická strana AcroForm má svůj vlastní příběh automatizace, popsaný v článku o interaktivních akcích formulářů a JavaScriptu

Zde uvedená rozhraní API XFA pro hodnoty polí, výčty a balíčky se dodávají v knihovně losLab PDF Library pro Delphi, C# a VB.NET; produktová stránka obsahuje kompletní referenční příručku pro práci s formuláři