Само по себе pole формы PDF представляет собой просто область, содержащую значение. Форма начинает вести себя как небольшое приложение благодаря привязанному к ней действию: клик, который скрывает раздел, загружает сохраненные значения из файла, переходит на последнюю страницу или запускает скрипт для расчета суммы столбца. Ничего из этого не находится внутри самого поля. Это хранится в словаре действий, и стандарт ISO 32000-1 классифицирует все это семейство в §12.6. В этой статье рассматриваются действия, к которым чаще всего обращаются программы на Delphi, и показывается, как PDFlibPas связывает каждое из них с полем или ссылкой.
Мысленная модель, которую стоит усвоить, заключается в том, что поле и действие представляют собой отдельные объекты, связанные ссылкой. Аннотация виджета или ссылки содержит действие в своей записи /A. Действие идентифицирует поле, с которым оно работает, по его имени, а не по индексу, поэтому имя поля является дескриптором, используемым всеми последующими действиями для его поиска. Как только это разделение становится понятным, API перестает казаться набором разрозненных вызовов и предстает как единый шаблон, применяемый к четырем типам операций.
Именованные действия: навигация без номера страницы
Самые простые действия вообще не имеют параметров. Раздел §12.6.4.11 стандарта ISO 32000-1, таблица 194, определяет именованные действия: программа просмотра интерпретирует символическое имя во время выполнения, а не следует по сохраненному пути назначения. Универсально поддерживаются четыре имени, и это именно те действия, которые читатель ожидает увидеть на панели инструментов: NextPage, PrevPage, FirstPage и LastPage. Поскольку назначение является относительным по отношению к странице, которую в данный момент просматривает пользователь, кнопка Next, созданная таким образом, работает на любой странице без необходимости вычислять целевой индекс.
В PDFlibPas именованное действие привязывается к активной прямоугольной области на текущей странице. Четвертый и пятый целочисленные аргументы выбирают тип действия и внешний вид.
// NamedActionType: 0 = NextPage, 1 = PrevPage, 2 = FirstPage, 3 = LastPage
// Options bit 0 (value 1) draws a border around the hotspot
Pdf.AddLinkToNamedAction(500, 560, 60, 18, 0, 1); // Next
Pdf.AddLinkToNamedAction(40, 560, 60, 18, 1, 1); // Previous
Pdf.AddLinkToNamedAction(110, 560, 60, 18, 3, 1); // jump to last page
Здесь нет целевой точки, которую нужно синхронизировать, и в этом весь смысл. Именованное действие сохраняет работоспособность при добавлении или удалении страниц, поскольку оно изначально не указывает на конкретную страницу. Сравните это с явной ссылкой перехода, которая хранит индекс целевой страницы, требующий обновления, как только размер документа изменится.
Действие Hide и ловушка с массивом
Действие Hide, ISO 32000-1 §12.6.4.10, таблица 196, переключает видимость одного или нескольких полей. Это самый простой способ реализовать логику показа и скрытия без использования скриптов, и это именно то, что нужно для создания ссылки Show details или двух взаимоисключающих панелей, где отображение одной скрывает другую. Действие содержит цель в записи /T и логическое значение /H, которое определяет направление: скрывать при значении true, показывать при значении false.
Тонкость заключается в том, как именно кодируется эта цель, и именно такая деталь может привести к тому, что форма будет работать у вас, но не сработает у клиента. Если действие ссылается на одно поле, /T записывается как текстовая строка. Если оно ссылается на несколько полей, /T записывается как массив текстовых строк. Старые программы просмотра не обрабатывают массив из одного элемента так же, как обычную строку, поэтому логика кодирования должна зависеть от количества: одно имя должно выводиться в виде строки, а не массива единичной длины, чтобы его поддерживал максимально широкий спектр программ. PDFlibPas принимает это решение за вас. Вы передаете имена полей через запятую, точку с запятой или перевод строки, а библиотека сама генерирует одну строку для одного имени и массив для двух или более полей.
// HideFlag non-zero hides the listed fields (/H true); zero shows them.
// One name -> /T is a text string. Two or more -> /T is an array of strings.
Pdf.AddLinkToHideField(40, 700, 90, 18, 'ShippingAddress', 1, 1);
Pdf.AddLinkToHideField(140, 700, 90, 18,
'ShippingName,ShippingAddress,ShippingZip', 1, 1);
Поскольку действие не ссылается на внешние ресурсы, оно остается совместимым с форматом PDF/A. Передаваемые имена должны быть полными именами полей, поэтому к дочернему полю внутри группы нужно обращаться через его полный путь с точками, а не просто по имени самого поля.
ImportData: предварительное заполнение из FDF
В то время как действие Hide управляет элементами непосредственно на странице, действие импорта данных загружает значения извне. Раздел §12.6.4.8 стандарта ISO 32000-1, таблица 198, определяет его как действие, заполняющее AcroForm из файла формата FDF (Forms Data Format) на диске. Это действие лежит в основе элементов управления вроде Reload sample data или Reset to defaults, когда файл FDF поставляется вместе с PDF и содержит эталонные значения полей. Этот вызов похож на остальные: он принимает прямоугольник активной зоны, путь к FDF и битовую маску внешнего вида: Pdf.AddLinkToImportData(40, 660, 120, 18, 'defaults.fdf', 1). Файл не обязательно должен существововать в момент создания PDF, но он должен быть на месте, когда пользователь нажимает кнопку, а любые обратные косые черты в пути автоматически преобразуются в каноническую для PDF прямую косую черту.
Одно ограничение стоит упомянуть особо, поскольку оно часто вызывает удивление. Действие импорта данных указывает на внешний файл, поэтому оно запрещено в PDF/A. Если документ находится в режиме PDF/A, вызов возвращает ноль и ничего не добавляет, чтобы не создавать файл, который не пройдет валидацию. Если ваш процесс ориентирован на архивный вывод, предварительное заполнение должно происходить на этапе генерации путем прямой записи значений полей, а не откладываться до клика пользователя.
JavaScript: глобальные пакеты и скрипты для конкретных действий
Для реализации логики, выходящей за рамки простого переключения видимости или импорта, семейство действий обращается к JavaScript на уровне документа. Скрипты могут храниться в двух разных местах, и эта разница важна. Глобальный пакет JavaScript уровня документа сохраняется один раз для всего файла и выполняется при открытии документа, что делает его идеальным местом для определения функций и общего состояния. Скрипт конкретного действия привязывается к одной ссылке или полю и запускается только при активации этого объекта, что делает его отличным решением для вызова функций, уже определенных в глобальном пакете.
PDFlibPas поддерживает оба варианта. Функция AddGlobalJavaScript сохраняет именованный пакет на уровне документа; повторное использование имени перезаписывает ранее сохраненный скрипт. Функция AddLinkToJavaScript привязывает скрипт к активной зоне на странице, чтобы клик приводил к его выполнению.
// Document-level package: define a reusable function once.
Pdf.AddGlobalJavaScript('Totals',
'function recalcTotal() {' +
' var net = this.getField("Net").value;' +
' var tax = this.getField("Tax").value;' +
' this.getField("Gross").value = Number(net) + Number(tax);' +
'}');
// Per-action script on a link: just call the shared function.
Pdf.AddLinkToJavaScript(40, 620, 100, 18, 'recalcTotal();', 1);
Разделение на глобальное определение функции и вызов внутри ссылки представляет собой не просто предпочтение стиля. Это помогает избежать дублирования кода в каждом элементе управления, а в программах просмотра с отключенным выполнением скриптов клик просто не вызовет никаких действий вместо сбоя из-за некорректного встроенного блока кода. Кроме того, это уменьшает объем записей для конкретных действий, сохраняя структуру файла читаемой при последующем анализе.
Поля, дочерние поля и фиксация результата
Действиям нужны поля, с которыми они будут работать, поэтому полезно знать, как создаются эти поля. Функция NewFormField создает поле на текущей странице и возвращает его индекс. Целочисленный тип задает вид поля: 1 - Text, 2 - Pushbutton, 3 - Checkbox, 4 - Radiobutton, 5 - Choice, 6 - Signature и 7 - Parent (родительское поле, которое содержит дочерние элементы, но само ничего не отображает). Передаваемое имя поля не должно содержать точку, поскольку точка используется как разделитель в полных именах, с помощью которых действия обращаются к дочерним элементам.
Группы переключателей и иерархические формы создаются путем добавления дочерних элементов к родительскому полю. Функция NewChildFormField добавляет дочерное поле под указанным родительским элементом, а для переключателей и списков функция AddFormFieldSub добавляет отдельные опции и возвращает временный индекс, используемый для позиционирования каждого элемента. Когда интерактивный этап завершен и вы хотите зафиксировать поле, чтобы его текущий вид стал постоянным содержимым страницы, функция FlattenFormField отрисовывает поле на странице и удаляет его из формы. После вызова flatten индексы последующих полей сдвигаются на единицу вниз, что важно помнить при циклическом сведении нескольких полей.
var
Pdf: TPDFlib;
FldShip: Integer;
begin
Pdf := TPDFlib.Create;
try
Pdf.SetOrigin(1); // top-left origin
Pdf.SetPageSize('A4');
Pdf.NewPage;
// A text field the Hide action will target by its title.
FldShip := Pdf.NewFormField('ShippingAddress', 1);
Pdf.SetFormFieldBounds(FldShip, 40, 120, 240, 20);
Pdf.SetFormFieldValue(FldShip, '');
// Wire a Hide link and a navigation link to this page.
Pdf.DrawText(40, 110, 'Toggle shipping block:');
Pdf.AddLinkToHideField(220, 100, 70, 16, 'ShippingAddress', 1, 1);
Pdf.AddLinkToNamedAction(500, 800, 60, 18, 3, 1); // Last page
// A document-level script available to every event in the file.
Pdf.AddGlobalJavaScript('OnOpen',
'app.alert("Form ready", 3);');
// Freeze the field if the output should no longer be editable.
// Pdf.FlattenFormField(FldShip);
if Pdf.SaveToFile('form_actions.pdf') <> 1 then
raise Exception.Create('Save failed');
finally
Pdf.Free;
end;
end;
Вызов flatten закомментирован намеренно. Если оставить его закомментированным, документ будет передан как активная форма, действия в которой будут выполняться в программе просмотра. Если его активировать, поле преобразуется в статичные графические элементы на странице, что и требуется после заполнения формы, когда результат должен быть отправлен как неизменяемый документ. Одно и то же поле, один и тот же код приводят к созданию двух совершенно разных документов в зависимости от того, фиксируете вы его или нет.
Выбор правильного действия
Эти четыре типа действий четко разделяются по объектам их работы. Именованное действие управляет окном просмотра и не требует наличия поля. Действие Hide изменяет видимость и требует имен полей, а выбор между кодированием в виде строки или массива берет на себя библиотека. Действие импорта данных обращается к файлу на диске и поэтому запрещено в PDF/A. Действие JavaScript выполняет произвольную логику, и его лучше всего разделять на глобальный пакет функций и небольшие вызовы на уровне отдельных элементов. Выбирайте самое простое решение, решающее задачу: действие Hide более переносимо, чем скрипт, устанавливающий флаг видимости, а именованное действие надежнее жестко прописанного перехода на страницу, так как вам не приходится отслеживать номера страниц.
Две смежные темы дополняют общую картину. Если форма является частью доступного документа, структура, используемая программами чтения с экрана, описана в нашей статье о тегированных PDF и структуре доступности. Если заполненную форму необходимо заблокировать и подписать, этот рабочий процесс описан в руководстве по подготовке к подписанию и проверке соответствия. Все эти возможности базируются на одном движке, который поставляется в составе библиотеки PDF library для Delphi вместе с API для создания документов, форм и подписей, описанными в других статьях блога.