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

Навигация по полям форм PDF в Delphi (PDFium Component)

Нажмите клавишу Tab в созданной вашим кодом форме PDF, и курсор перескочит на два поля вперед, пропустит второй столбец или вернется в начало после третьего поля вместо четвертого. Пользователь, заполняющий счет в окне просмотра, ожидает, что клавиатура переключает поля формы точно так же, как в любом веб-приложении. Если этого не происходит, пользователь берет мышь, ищет нужное поле и делает вывод о незавершенности вашего решения. Предсказуемый обход полей — разница между формой ввода, с которой пользователи мирятся, и той, которой доверяют. И это полностью зависит от использования правильного API фокуса, а не от имитации кликов мыши

Приведенные ниже примеры используют PDFium Component — VCL/LCL-компонент на базе PDFium для Delphi, C++Builder и Lazarus. Навигация — одна из трех задач, которые средство просмотра форм должно решать корректно; две другие (правильное открытие форм и сохранение заполненных значений так, чтобы они действительно отображались) таят больше сюрпризов, поэтому мы рассмотрим все три

Открытие формы: FormFill, FormType и архитектура XFA

Доступ к полям требует активации подсистемы заполнения форм (управляемой свойством FormFill) до открытия документа. После активации свойство FormType сообщает тип формы, определяя набор доступных функций:

Pdf.FileName := FormPath;
Pdf.FormFill := True;   // enable before Active; required for any field access
Pdf.Active := True;

case Pdf.FormType of
  ftNone:
    DisableFormPanel('This document has no interactive form');
  ftAcroForm:
    BuildFieldList;     // full field navigation and editing available
  ftXfaFull:
    ShowXfaNotice;      // XFA renders from its own XML template;
                        // treat field editing as limited
end;

Из этого кода следуют два практических замечания. Модель AcroForm является стандартным форматом форм по спецификации ISO 32000, и именно на нее ориентирован описываемый здесь API. Документы XFA используют собственную XML-архитектуру форм, поэтому обещание полной поддержки редактирования XFA после простой демонстрации AcroForm станет проблемой при поддержке. Второе замечание касается побочных эффектов: установка FormFill := True также инициализирует JavaScript документа. В окне заполнения форм это правильно, так как скрипты расчетов автоматически обновляют итоги при вводе. В окне предварительного просмотра файлов из неизвестных источников это небезопасно. В статье об безопасном просмотре PDF подробно описан этот баланс при FormFill := False

Переход по клавише Tab, предсказуемый для пользователей

Вернемся к проблеме навигации с клавиатуры. Часто возникает желание сымитировать клавишу Tab путем симуляции клика мыши по прямоугольнику следующего интерактивного элемента. Это решение ломается, как только поле прокручивается за экран или элементы перекрывают друг друга. API фокуса перемещает фокус самой формы напрямую, без расчета геометрии. За это отвечают пять методов: FocusFormField по индексу, FocusNextFormField и FocusPreviousFormField для перемещения, FocusedFormFieldIndex для чтения текущей позиции и ClearFormFieldFocus для сброса фокуса

procedure TFormViewer.HandleTabKey(Shift: TShiftState);
begin
  if ssShift in Shift then
    PdfView.FocusPreviousFormField
  else
    PdfView.FocusNextFormField;
  UpdateFieldStatus;  // e.g. "Field 4 of 17: InvoiceDate"
end;

Единственная особенность навигации — зацикливание. Переход работает в рамках порядка табуляции текущей страницы: при попытке выйти за последнее поле фокус возвращается на первое. Обе функции перехода возвращают новый индекс поля или -1, если страница вообще не содержит интерактивных элементов. Это зацикливание работает в пределах одной страницы, поэтому переход на следующую страницу — задача приложения, а не библиотеки. Сравните возвращенный индекс с исходным, зафиксируйте факт зацикливания и измените PageNumber самостоятельно, если форма должна восприниматься как единый документ. Без этого двухстраничная форма заблокирует курсор на первой странице

Навигация становится удобной, когда остальной интерфейс реагирует на нее. Событие OnFormFieldEnter срабатывает при получении фокуса, а событие OnFormFieldFocusChange сообщает новый индекс поля в средстве просмотра, позволяя боковой панели синхронизировать свое состояние с выбранным полем. Если вам нужно обратное сопоставление (координат экрана с полем), индексируемое свойство FormFieldAt выполняет проверку попадания (hit-testing) для подсказок и панелей редактирования кликом. Это дает важный плюс для доступности: поскольку фокус следует внутреннему порядку полей документа, путь обхода Tab совпадает с путем чтения экранного диктора без дополнительных усилий

Отображение имен полей вместо индексов требует еще одного свойства. FormFieldInfo[] возвращает запись TPdfFormFieldInfo для индекса, содержащую имя поля, тип, размер шрифта, состояние выбора, экспортируемое значение и принадлежность к группе. Это именно то, что нужно выводить в списке навигации (например, «Поле 4 из 17: InvoiceDate» вместо «4»). Группы переключателей (Radio groups) заслуживают отдельного тестирования. Несколько элементов управления могут использовать одинаковое имя поля, поэтому список, собранный напрямую из элементов управления, покажет одну группу несколько раз, запутывая пользователя

Почему заполненные значения отображаются пустыми, и спасительный метод

Вторая популярная проблема критичнее неверного порядка Tab: форма заполняется программно, клиент открывает ее в Acrobat, а все поля выглядят пустыми. Но стоит кликнуть в поле, как значение мгновенно появляется. Сами данные присутствуют в файле все время. Отсутствует графическое представление данных, и причину этого важно понять

Текстовое поле AcroForm сохраняет значение в параметре /V словаря поля (ISO 32000-1 §12.7.3.3). Но на экране программа просмотра отрисовывает другой объект: поток внешнего вида (/AP) под ключом §12.5.5 — небольшой предварительно срендеренный фрагмент содержимого. Если записать значение в /V и не обновить /AP, они начнут расходиться. Данные есть, но их визуальное представление отсутствует. Acrobat перестраивает внешний вид поля при получении фокуса, что и объясняет появление данных при клике. Устаревший флаг NeedAppearances, запрашивающий перерисовку у программ просмотра, никогда не работал стабильно и объявлен устаревшим в PDF 2.0. Серверы печати и генераторы миниатюр полностью игнорируют его: они выводят только поток /AP, поэтому при его отсутствии отображают пустые поля

Запись значения через FormField[i] изменяет только /V. Заполнение формы должно быть трехэтапным процессом, но разработчики часто забывают второй шаг:

procedure TFormViewer.FillAndSave(const Values: array of WString;
  const OutputPath: string);
var
  i: Integer;
begin
  for i := 0 to Pdf.FormFieldCount - 1 do
    Pdf.FormField[i] := Values[i];   // writes /V only

  // Rebuild the /AP appearance streams; without this the form
  // looks blank in Acrobat until each field is clicked
  Pdf.GenerateFormAppearances;

  Pdf.SaveAs(OutputPath);
end;

Метод GenerateFormAppearances решает проблему. Он перестраивает поток внешнего вида каждого элемента управления на основе текущих значений, шрифтов и выравнивания, благодаря чему серверы печати или средства генерации миниатюр отображают заполненное состояние сразу. Вызывайте этот метод один раз после заполнения всех полей, а не после каждого поля отдельно. Генерация внешнего вида выполняет реальную работу по верстке текста, и вызов после каждого поля создаст лишнюю нагрузку на больших формах

Генерация внешнего вида также задействует шрифты и выравнивание, что может преподнести сюрпризы. Новый поток верстает текст внутри прямоугольника поля с использованием заданного шрифта, размера и выравнивания. Текст, который свободно умещался на этапе проектирования, может обрезаться или сильно уменьшиться в файле клиента, если границы поля окажутся уже. Поля с автоподбором размера (размер шрифта равен нулю) сжимают текст; поля фиксированного размера просто обрезают его. Оба варианта соответствуют стандарту, и единственный способ проверить верстку — посмотреть на сгенерированный результат, а не на записываемую строку. В этом кроется причина большинства жалоб на обрезанный текст

Считайте проверку частью процесса разработки, а не постобработки. Откройте сохраненный файл в Acrobat и убедитесь, что значения видны до клика по полям. Затем экспортируйте его в изображение или отправьте на печать из средства просмотра, полностью игнорирующего логику форм, и убедитесь, что данные сохранились. Эти две проверки выявляют любые расхождения между /V и /AP

Нюансы настройки полей на практике

Реальные файлы клиентов содержат много нюансов, с которыми вы не столкнетесь на тестовых формах. Вот четыре основные причины жалоб:

  • Экспортируемые значения флажков. Состояние «включено» не всегда равно строке Yes. Форма может задавать собственное значение экспорта, и запись неверного значения оставит флажок пустым, хотя ваш код будет уверен в его установке. Считывайте значение экспорта из FormFieldInfo[] вместо предположений
  • Группы переключателей с общим именем. Одно поле — несколько элементов управления. Записываемое значение определяет, какой переключатель будет выбран, поэтому логика UI, предполагающая однозначное соответствие имени прямоугольнику, может нарисовать рамку фокуса не на той кнопке
  • Вычисляемые поля. Формулы расчетов на JavaScript обновляются в ответ на события полей. Программное заполнение в обход событий интерфейса требует либо вызова пересчета, либо ручной записи значений в расчетные поля. Форма, в которой сумма по позициям не сходится с итогом, неприемлема
  • Скрытые обязательные поля. Интерактивные формы часто скрывают поля, сохраняя у них флаг обязательного заполнения. Решите заранее, учитывает ли ваша валидация видимость полей или опирается только на флаг обязательности, и задокументируйте это решение

Важно помнить разницу: генерация внешнего вида не является слиянием (flattening). Метод GenerateFormAppearances делает значения видимыми везде, сохраняя интерактивность полей. Слияние же встраивает изображение полей в статичное содержимое страницы и навсегда удаляет интерактивность, что подходит для архивной копии, но делает форму непригодной для дальнейшего заполнения. Если свойство FormType возвращает ftXfaFull вместо ftAcroForm, описанная логика редактирования не применима, так как документ рендерится из своего XML-шаблона; зафиксируйте этот случай в кодовой базе и сообщите об этом пользователю

Подсистема заполнения форм, навигация фокуса и генерация внешнего вида входят в состав PDFium Component для Delphi, C++Builder и Lazarus/FPC. Если ваше приложение также работает с пометками рецензентов наряду с данными форм, статья о рецензировании аннотаций подробно описывает эту смежную модель