Artykuł techniczny

Wyrażenia ścieżek XFA SOM w Delphi: dynamiczne dane formularzy

Biblioteka losLab PDF Library odczytuje i zapisuje powtarzające się węzły danych w dynamicznych formularzach XFA przy użyciu wyrażeń ścieżek XFA 3.3 SOM: funkcje GetXFAFormFieldValue oraz SetXFAFormFieldValue akceptują korzenie $data, $record i !data, symbole wieloznaczne wystąpień [*], selektory potomków (descendant) oraz bezpośrednich dzieci (child-wildcard), właściwość parent, selektory klas #dataGroup/#dataValue, a także proste predykaty w stylu FormCalc, dzięki czemu program w Delphi może zaktualizować każdą pozycję faktury w urzędowym formularzu podatkowym za pomocą jednego wywołania. Ten artykuł służy jako odnośnik do adresowania; informacje na temat przenoszenia całych zbiorów danych pomiędzy plikami znajdziesz w towarzyszącym artykule o wymianie danych formularzy FDF, XFDF oraz XFA

Problem pojawia się w momencie, gdy formularz przestaje być płaski. Deklaracja VAT czy zgłoszenie celne utworzone jako dynamiczny formularz XFA nie zawiera dwunastu pól o nazwach od Total_Price_1 do Total_Price_12; ma jeden podformularz Detail zadeklarowany raz w szablonie DOM i powielany tyle razy, ile wymagają tego dane. Wartości znajdują się w drugim drzewie — Data DOM wewnątrz pakietu datasets — gdzie każda pozycja wiersza to powtarzający się element <Detail> pod <Receipt>. Płaskie nazwy pól nie pozwalają odnieść się do „ceny w trzecim wierszu” ani „każdego wiersza o wartości powyżej 200”; to jest dokładnie zadanie, które rozdział Scripting Object Model specyfikacji Adobe XFA 3.3 powierza wyrażeniom SOM i jest to składnia, którą obsługują interfejsy API wartości pól XFA w losLab PDF Library

Jak adresować węzły danych XFA z poziomu Delphi?

Każda ścieżka zbiorów danych (datasets) rozpoczyna się od korzenia, a biblioteka losLab PDF Library akceptuje zamiennie standardowe zapisy: $data to skrócona forma standardu XFA 3.3 dla xfa.datasets.data, !data to skrócona forma zakorzeniona w xfa.datasets, a w pakietach, które nie korzystają z przetwarzania rekordów, $record odnosi się do zewnętrznego rekordu danych, czyli pierwszego elementu pod węzłem data. Interfejsy API po stronie szablonu, takie jak SetXFAFormFieldAccess, akceptują w ten sam sposób $template oraz xfa.template. Segmenty mogą być rozdzielane kropkami lub ukośnikami ($data.Receipt.Tax lub $data/Receipt/Tax), co ma znaczenie, ponieważ GetXFAFormFieldNames listuje pola jako ścieżki rozdzielane ukośnikami, które można bezpośrednio przekazać z powrotem do API wartości i szablonów. Indeksy wystąpień są indeksowane od zera zgodnie ze specyfikacją, stąd Detail[0] to pierwszy wiersz. Dwie reguły ucieczki (escaping) umożliwiają adresowanie nietypowych nazw: w ścieżce rozdzielanej kropkami zapis \. oznacza dosłowną kropkę (Line\.Item), podczas gdy ścieżki rozdzielane ukośnikami traktują kropki jak zwykłe znaki. Dane biznesowe zawierające własny prefiks przestrzeni nazw XML są dopasowywane po nazwie lokalnej, więc element <m:Receipt> nadal odpowiada na zapytanie o Receipt

var
  Lib: TPDFlib;
  Tax: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('vat-return.pdf', '');
    // Równoważne adresy dla tego samego węzła danych
    Tax := Lib.GetXFAFormFieldValue('Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('$data.Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('xfa.datasets.data.Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('$record.Tax');
    // Zapis z ukośnikami i indeksem wystąpienia liczonym od zera
    Lib.SetXFAFormFieldValue('$record/Detail[0]/Total_Price', '251.00');
    Lib.SaveToFile('vat-return-updated.pdf');
  finally
    Lib.Free;
  end;
end;

Jak programowo wypełniać powtarzające się wiersze w formularzu XFA?

Symbol wieloznaczny wystąpień [*] to narzędzie do przetwarzania wsadowego. Tam, gdzie indeks liczbowy wybiera jeden element siostrzany, zapis [*] wybiera wszystkie elementy o tej samej nazwie, więc ścieżka $data.Receipt.Detail[*].Total_Price odnosi się do pola ceny we wszystkich pozycjach jednocześnie. Przy odczycie funkcja GetXFAFormFieldValue zwraca wartości wszystkich pasujących węzłów połączone separatorem |; przy zapisie SetXFAFormFieldValue aktualizuje każdy pasujący węzeł tą samą wartością i zwraca 1 w przypadku powodzenia. Ścieżka, która nic nie dopasuje, jest odczytywana jako pusty ciąg znaków, co stanowi prosty sposób na sprawdzenie, czy dana gałąź istnieje przed podjęciem próby zapisu

var
  Lib: TPDFlib;
  Prices: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('invoice.pdf', '');
    // Wszystkie wiersze Detail jednocześnie: '250.00|60.00'
    Prices := Lib.GetXFAFormFieldValue('$data.Receipt.Detail[*].Total_Price');
    // Zresetuj jedną kolumnę we wszystkich powtórzonych wierszach w pojedynczym wywołaniu
    Lib.SetXFAFormFieldValue('$data.Receipt.Detail[*].Surcharge', '0.00');
    Lib.SaveToFile('invoice-updated.pdf');
  finally
    Lib.Free;
  end;
end;

Selektory potomków, bezpośrednich dzieci i rodzica

Trzy selektory strukturalne obsługują przypadki, gdy znasz nazwę pola, lecz nie jego dokładną głębokość. Selektor potomka .. dopasowuje elementy na dowolnej głębokości poniżej bieżącego węzła: zapis $data..Total_Price odnajduje pierwsze pole Total_Price w dowolnym miejscu pod korzeniem danych, a zapis pod ścieżką potomną aktualizuje ten pierwszy pasujący węzeł. Pamiętaj o ograniczeniu: indeks wystąpienia po dopasowaniu potomnym nie przechodzi przez gałęzie siostrzane, więc jeśli $data..Total_Price[0] wskazuje na komórkę w pierwszym wierszu Detail, to $data..Total_Price[1] zwróci wartość pustą zamiast przejścia do kolejnego wiersza; użyj Detail[*], aby pobrać je wszystkie. Symbol wieloznaczny dziecka .* dopasowuje każde bezpośrednie dziecko i pozwala na filtrowanie pozostałych segmentów: ścieżka $data.Receipt.*.Total_Price dociera do Total_Price wewnątrz każdej podrzędnej gałęzi Receipt, nie wybierając przy tym pola podsumowania o tej samej nazwie, które znajduje się bezpośrednio na poziomie Receipt. Na koniec właściwość parent pozwala przejść o jeden poziom w górę, co standard XFA 3.3 celowo oddziela od zapisu ..: ścieżka $data.Receipt.Detail[1].parent.Tax rozpoczyna się od drugiej pozycji wiersza, przechodzi do rodzica Receipt i trafia na jego siostrzaną wartość Tax

Selektory klas i predykaty: #dataGroup, #dataValue, .[expression]

Składnia #class adresuje węzły Data DOM na podstawie klasy obiektu, a nie nazwy, co jest przydatne, gdy znaczniki XML zawierają znaki trudne do zapisania w skryptach. Biblioteka losLab PDF Library mapuje #dataGroup na elementy posiadające dzieci będące elementami, a #dataValue na elementy liściowe (nieposiadające dzieci-elementów); oba te zapisy można łączyć z indeksami myślowymi lub [*]: ścieżka $data.Receipt.#dataGroup[0].#dataValue[0] wybiera pierwszą wartość wewnątrz pierwszej grupy pod Receipt. Selektor predykatu .[wyrażenie] filtruje rodzeństwo o tej samej nazwie na podstawie zawartości. Wspierane predykaty to proste porównania wartości podrzędnej z literałem za pomocą operatorów relacyjnych, takich jak >, < oraz <=. Gdy predykat pasuje do wielu wierszy, odczyty są zwracane z separatorem |, a zapisy aktualizują każde dopasowanie

// Oznacz każdy wiersz, w którym Total_Price przekracza 200
Lib.SetXFAFormFieldValue(
  '$data.Receipt.Detail.[Total_Price > 200].Review_Flag', '1');

// Opisy wszystkich linii o niskiej wartości, połączone separatorem '|', gdy dopasowań jest kilka
Desc := Lib.GetXFAFormFieldValue(
  '$data.Receipt.Detail.[Total_Price <= 200].Description');

// Adresowanie na podstawie klas, gdy nazwy tagów są trudne do zapisania w skryptach
Total := Lib.GetXFAFormFieldValue(
  '$data.Receipt.#dataGroup[0].#dataValue[0]');

Które funkcje XFA SOM nie są obsługiwane?

Obsługa SOM w losLab PDF Library ogranicza się do zbiorów danych (datasets) oraz ścieżek pól szablonów, a ograniczenia te są zdefiniowane wprost. Znajomość ich pozwala uniknąć szukania przyczyn w ścieżce, która bezgłośnie zwraca pusty ciąg znaków

  • Predykaty nie wykonują dowolnego kodu FormCalc ani JavaScript: brak wywołań funkcji (predykat z contains(...) zwróci pusty wynik), brak złożonych wyrażeń logicznych, dozwolone jest wyłącznie pojedyncze porównanie typu child operator literał
  • Zapis $record działa wyłącznie w pakietach bez przetwarzania rekordów; stronicowanie rekordów dataWindow oraz rotacja grup rekordów nie są zaimplementowane
  • Rozpoznawanie ścieżek odbywa się bezpośrednio na Data DOM i template DOM; względne rozpoznawanie Form DOM (połączony widok generowany dynamicznie przez przeglądarkę) nie jest wykonywane
  • Semantyka ładowania atrybutów Data DOM nie ma zastosowania; biblioteka odczytuje pakiet datasets jako czysty XML, stąd ścieżki odnoszą się do elementów, a nie do atrybutów promowanych do węzłów

Ograniczenia te rzadko przeszkadzają przy automatycznym wypełnianiu formularzy, ponieważ moduł wypełniający odwołuje się do struktury danych, a nie do logiki skryptowej. W razie potrzeby wyjściem awaryjnym jest praca na poziomie pakietu: odczytaj cały plik XML datasets, zmodyfikuj go w Delphi i zapisz z powrotem

Zapis całego pakietu za pomocą SetXFAFromString

Metoda SetXFAFromString to punkt wejścia na poziomie pakietu: instaluje kompletny ciąg znaków XDP, łącząc szablon i zbiory danych, tworząc kontener AcroForm w nowym dokumencie, jeśli ten jeszcze nie istnieje. Akceptuje pakiety kodowane w UTF-16 ze znacznikami kolejności bajtów (BOM), jak również w UTF-8. Typowy schemat produkcyjny polega na zachowaniu wzorcowego pliku XDP jako makiety, wczytaniu go przez SetXFAFromString, a następnie wykonaniu wywołań SetXFAFormFieldValue z adresami SOM dla zmiennych wartości przed zapisaniem dokumentu. Ponieważ XFA to jeden z dwóch modeli formularzy, które może zawierać plik PDF, klasyczna technologia AcroForm ma własną ścieżkę automatyzacji, opisaną w artykule o interaktywnych akcjach formularzy i języku JavaScript

Przedstawione tu interfejsy API wartości pól XFA, listowania i pakietów wchodzą w skład losLab PDF Library dla Delphi, C# i VB.NET; strona produktu zawiera pełną dokumentację obsługi formularzy