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

Выражения путей XFA SOM в Delphi: Динамические данные форм

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 внутри пакета 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 разрешается во внешнюю запись данных, первый элемент под узлом данных. 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 ограничена путями datasets и полей шаблона, и эти границы очерчены явно. Знание их заранее убережет от отладки путей, которые просто возвращают пустую строку

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

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

Запись всего пакета с помощью SetXFAFromString

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

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