losLab PDF Library зчитує та записує повторювані вузли даних у динамічних формах XFA за допомогою шляхових виразів XFA 3.3 SOM: методи GetXFAFormFieldValue та SetXFAFormFieldValue підтримують корені $data, $record та !data, символи підстановки входжень [*], селектори нащадків та дочірніх елементів, властивість parent, селектори класів #dataGroup/#dataValue та прості предикати у стилі FormCalc, що дозволяє програмі на Delphi оновлювати кожен рядок інвойсу в державній податковій формі одним викликом. Ця стаття є довідником з адресації; про перенесення повних наборів даних між файлами читайте в супутній статті про обмін даними форм FDF, XFDF та XFA
Проблема виникає в той момент, коли форма перестає бути плоскою. Декларація з ПДВ або митна декларація, побудована як динамічна форма XFA, не має дванадцяти полів з іменами від Total_Price_1 до Total_Price_12; вона має одну субформу Detail, оголошену один раз у DOM шаблону та створену стільки разів, скільки вимагають дані. Значення живуть у другому дереві — Data DOM всередині пакета наборів даних, де кожен елемент рядка є повторюваним елементом <Detail> під <Receipt>. Плоскі імена полів не можуть адресувати «ціну в третьому рядку» або «кожен рядок із сумою понад 200» — це саме те завдання, яке розділ Scripting Object Model специфікації Adobe XFA 3.3 покладає на вирази SOM, і це синтаксис, яким оперують API значень полів XFA у losLab PDF Library
Як адресувати вузли даних XFA з Delphi?
Кожен шлях наборів даних починається з кореня, і losLab PDF Library приймає стандартні варіанти написання як взаємозамінні: $data — це коротка форма XFA 3.3 для xfa.datasets.data, !data — коротка форма з коренем у xfa.datasets, а в пакетах, які не використовують посимвольний запис, $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 — з кінцевими елементами без дочірніх елементів, і обидва типи можуть поєднуватися з числовими або [*] індексами: $data.Receipt.#dataGroup[0].#dataValue[0] вибирає перше значення всередині першої групи під Receipt. Селектор предикатів .[вираз] фільтрує сусідні одноіменні елементи за вмістом. Підтримуваними предикатами є прості порівняння дочірнього значення з літералом за допомогою операторів порівняння, таких як >, < та <=; коли предикат знаходить кілька рядків, зчитування повертає значення, об'єднані через |, а запис оновлює кожен знайдений збіг
// 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 обмежена наборами даних та шляхами полів шаблонів, і ці межі є фіксованими. Знання про них заздалегідь позбавить вас від налагодження шляху, який просто тихо повертає порожній рядок
- Предикати не виконують довільний FormCalc або JavaScript: немає викликів функцій (предикат із
contains(...)поверне порожній результат), немає складних логічних виразів, підтримується лише одне порівняння типуchild op literal - Кореневий селектор
$recordпрацює лише в пакетах без посимвольного запису; пейджинг записів dataWindow та ротація груп записів не реалізовані - Розпізнавання відбувається безпосередньо за Data DOM та DOM шаблону; відносне розпізнавання Form DOM, тобто об'єднаний вигляд, який переглядач будує під час виконання, не виконується
- Семантика завантаження атрибутів Data DOM не застосовується; бібліотека зчитує пакет наборів даних як звичайний XML, тому шляхи адресують елементи, а не атрибути, перетворені на вузли
Ці обмеження рідко заважають при пакетному заповненні, оскільки заповнювач адресує дані за структурою, а не за логікою сценарію. Якщо вони все ж заважають, рішенням є робота на рівні пакета: зчитайте весь XML наборів даних, трансформуйте його в Delphi та запишіть назад
Запис всього пакета за допомогою SetXFAFromString
Метод SetXFAFromString є точкою входу на рівні пакета: він встановлює повний рядок XDP (шаблон і набори даних разом), створюючи контейнер AcroForm у новому документі, якщо він ще не існує, і приймає пакети в кодуванні UTF-16 із міткою порядку байтів (BOM), а також у UTF-8. Типовий сценарій для продакшену — зберігати виданий податковим органом файл XDP як шаблон, завантажувати його за допомогою SetXFAFromString, а потім виконувати виклики SetXFAFormFieldValue з адресацією SOM для внесення динамічних даних рахунків перед збереженням. Оскільки XFA — це одна з двох моделей форм, які може містити PDF, класичні AcroForm мають власну історію автоматизації, описану в статті про інтерактивні дії форм та JavaScript
Описані тут API для роботи зі значеннями полів XFA, їх переліку та пакетної обробки постачаються у складі losLab PDF Library для Delphi, C# та VB.NET; сторінка продукту містить повну довідку щодо роботи з формами