Техническа статия

XFA SOM пътища в Delphi: Динамични данни от формуляри

losLab PDF Library чете и записва повтарящи се възли от данни в динамични XFA формуляри чрез XFA 3.3 SOM (Scripting Object Model) изрази за пътища: GetXFAFormFieldValue и SetXFAFormFieldValue приемат корените $data, $record и !data, заместващи знаци за повторение [*], селектори за наследници и заместващи знаци за деца, свойството parent, класови селектори #dataGroup/#dataValue и прости предикати в стил FormCalc, така че програма на Delphi може да актуализира всеки ред от фактура в правителствен данъчен формуляр с едно извикване. Тази статия е справочник за адресирането; за преместване на цели набори от данни между файлове вижте придружаващата статия за обмен на данни от формуляри във формати FDF, XFDF и XFA

Проблемът възниква в момента, в който формулярът спре да бъде плосък. Декларация по ДДС или митническа декларация, изградена като динамичен XFA формуляр, няма дванадесет полета, наречени от Total_Price_1 до Total_Price_12; тя има една подобласт Detail, декларирана веднъж в шаблона DOM и инстанцирана толкова пъти, колкото изискват данните. Стойностите живеят във второ дърво – Data DOM вътре в пакета datasets, където всеки артикул е повтарящ се елемент <Detail> под <Receipt>. Плоските имена на полета не могат да адресират „цената на третия ред“ или „всеки ред над 200“; това е точно задачата, която разделът Scripting Object Model на спецификацията Adobe XFA 3.3 дава на SOM изразите, и това е синтаксисът, когото говорят приложните програмни интерфейсове (API) за стойности на XFA полета в losLab PDF Library

Как се адресират XFA възли от данни в Delphi?

Всеки път в datasets започва от корен, като losLab PDF Library приема стандартните изписвания взаимозаменяемо: $data е кратката форма в XFA 3.3 за xfa.datasets.data, !data е кратката форма с корен в xfa.datasets, а в пакети, които не използват обработка на записи (record processing), $record съвпада с външния запис от данни – първия елемент под възела за данни. Приложните програмни интерфейсове (API) от страната на шаблона като SetXFAFormFieldAccess приемат $template и xfa.template по същия начин. Сегментите могат да бъдат разделени с точки или наклонени черти ($data.Receipt.Tax или $data/Receipt/Tax), което е важно, тъй като GetXFAFormFieldNames изброява полетата като пътища, разделени с наклонени черти, които се подават директно обратно в API-тата за стойности и шаблони. Индексите за повторение са нулево-базирани според спецификацията, така че Detail[0] е първият ред. Две правила за екраниране поддържат необичайните имена адресируеми: в път, разделен с точка, \. означава буквална точка (Line\.Item), докато пътищата с наклонени черти третират точките като обикновени символи. Бизнес данните, които носят собствен префикс на XML пространство от имена, се съпоставят по локално име, така че <m:Receipt> все още съответства на 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;

Как се попълват програмно повтарящи се редове в XFA формуляр?

Заместващият знак за повторение [*] е инструментът за групова обработка. Докато числовият индекс избира един съседен елемент, [*] избира всички съседи с едно и също име, така че $data.Receipt.Detail[*].Total_Price адресира полето за цена във всеки един ред наведнъж. При четене, GetXFAFormFieldValue връща стойностите на всички съвпадащи възли, обединени с разделител |; при запис, SetXFAFormFieldValue актуализира всеки съвпадащ възел с една и съща стойност и връща 1 при успех. Път, който не съвпада с нищо, се чете като празен низ, което е лесният начин да проверите дали даден клон съществува, преди да пишете в него

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;

Селектори за наследници, заместващи знаци за деца и родителски селектори

Три структурни селектора покриват случаите, в които знаете името на полето, но не и точната му дълбочина. Селекторът за наследници .. намира съвпадение на всякаква дълбочина под текущия възел: $data..Total_Price намира първото Total_Price навсякъде под корена за данни, а записът през път за наследник актуализира този първи съвпадащ възел. Обърнете внимание на ограничението: индексът за повторение след съвпадение за наследник не преминава през съседни клонове, така че ако $data..Total_Price[0] сочи към първия ред на Detail, $data..Total_Price[1] връща празен низ, вместо да прескочи на следващия ред; използвайте Detail[*], ако искате всички тях. Заместващият знак за дете .* съвпада с всяко пряко дете и позволява филтриране по следващите сегменти: $data.Receipt.*.Total_Price достига до Total_Price във всеки детски клон на Receipt, без да избира обобщаващо поле със същото име, намиращо се директно в самия Receipt. И накрая, свойството parent се изкачва с едно ниво нагоре, което XFA 3.3 умишлено разделя от ..: $data.Receipt.Detail[1].parent.Tax започва от втория артикул, изкачва се до Receipt и попада върху съседната стойност Tax

Класови селектори и предикати: #dataGroup, #dataValue, .[израз]

Синтаксисът #class адресира Data DOM възли по клас на обекта вместо по име, което е практическият път, когато XML таговете съдържат символи, които са неудобни за изписване като имена в скрипт. losLab PDF Library съпоставя #dataGroup към елементи, които имат деца-елементи, и #dataValue към крайни (leaf) елементи без деца-елементи, като и двата вида се комбинират с числови или [*] повторения: $data.Receipt.#dataGroup[0].#dataValue[0] избира първата стойност в първата група под Receipt. Селекторът с предикат .[expression] филтрира съседни елементи с едно и също име по тяхното съдържание. Поддържаните предикати са прости сравнения на стойност на дете с литерал, с релационни оператори като >, < и <=; когато даден предикат намери съвпадение в няколко реда, четенето връща резултати, разделени с |, а записът актуализира всяко съвпадение

// 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]');

Кои функции на XFA SOM не се поддържат?

Поддръжката на SOM в losLab PDF Library е ограничена до datasets и пътища на полета в шаблони, като границите са ясни, а не на принципа на най-добрите усилия. Знанието за тях предварително спестява дебъгването на път, който безшумно връща празен низ

  • Предикатите не изпълняват произволен FormCalc или JavaScript: без извиквания на функции (предикат contains(...) връща празен низ), без сложни булеви изрази, а само единично сравнение child op literal
  • $record работи само в пакети без обработка на записи; странирането на записи в dataWindow и ротацията на групи от записи не са внедрени
  • Разделителната способност е директно спрямо Data DOM и шаблонния DOM; относителното разрешаване на Form DOM (смесеният изглед, който четецът изгражда при изпълнение) не се извършва
  • Семантиката за зареждане на атрибути в Data DOM не се прилага; библиотеката чете пакета datasets какъвто е, като суров XML, така че пътищата адресират елементи, а не атрибути, превърнати във възли

Тези ограничения рядко пречат при задачи по групово попълване, тъй като попълващият код адресира данните по структура, а не по логика на скрипт. Когато обаче възпрепятстват работата, спасителният изход е на ниво пакет: прочетете целия datasets XML, трансформирайте го в Delphi и го запишете обратно

Записване на целия пакет с SetXFAFromString

Методът SetXFAFromString е входната точка на ниво пакет: той инсталира пълен XDP низ (шаблон и datasets заедно), създавайки контейнера AcroForm в нов документ, ако такъв все още не съществува, и приема както UTF-16 кодирани пакети с маркери за подредба на байтовете (BOM), така и UTF-8. Общ производствен шаблон е да запазите издадения от агенцията XDP файл като шаблон, да го заредите с SetXFAFromString, след което да изпълните SOM-адресирани повиквания на SetXFAFormFieldValue за променливите стойности на конкретната фактура преди запис. Тъй като XFA е един от двата модела на формуляри, които PDF може да носи, класическата страна на AcroForm има своя собствена история на автоматизация, разгледана в статията за интерактивни действия с формуляри и JavaScript

Приложните програмни интерфейсове (API) за стойности на XFA полета, изброяване и пакети, показани тук, се доставят в losLab PDF Library за Delphi, C# и VB.NET; продуктовата страница съдържа пълното справочно ръководство за работа с формуляри