Technical Article

Автовычисления в формах PDF в Delphi с помощью HotPDF

Для создания счета-фактуры или отчета о расходах в формате PDF, где строки автоматически суммируются, а дата форматируется автоматически, к форме AcroForm необходимо привязать три вещи, и HotPDF позволяет добавить их в загруженный файл в Delphi: ApplyLoadedFieldCalculation для итогов AFSimple_Calculate, ApplyLoadedFieldValidation для вспомогательных функций формата и валидации Acrobat AF, а также EnsureLoadedFieldAppearanceStream для создания внешнего вида поля, который фактически отрисовывается просмотрщиком

HotPDF — это нативный VCL-компонент PDF для Delphi и C++Builder, и эти три метода работают с документом, который вы открыли с помощью LoadFromFile, а не создаете с нуля. Каждый из них принимает отсчитываемый от нуля индекс поля в массиве AcroForm /Fields (тот же порядок, который возвращает метод GetLoadedFormFieldNames) и записывает данные непосредственно в словарь полей, так что весь процесс сводится к загрузке, индексации, привязке и вызову SaveLoadedDocument. Создание самого набора полей — это отдельная задача, описанная в статье о добавлении полей AcroForm в загруженный PDF в Delphi, в то время как эта страница посвящена добавлению вычислений и форматирования к существующему набору полей

Зачем форме PDF нужны скрипты для суммирования собственных полей?

Потому что в PDF нет табличного слоя. Текстовое поле хранит строку, а не формулу, и ничто в файле не пересчитывает итог при изменении строки данных, если для этого не подключен скрипт. Этот механизм реализуется через словарь дополнительных действий поля, /AA (ISO 32000-1 §12.7.5), который связывает триггерные события с действиями: событие ввода с клавиатуры (K) при вводе символов, событие форматирования при отображении, событие валидации (V) при подтверждении и событие вычислений (C), которое срабатывает при изменении любого поля в форме. Каждое действие представляет собой JavaScript-действие /S /JavaScript (ISO 32000-1 §12.6.4.14), и выполняемый им скрипт производит суммирование или форматирование. Acrobat поставляется с библиотекой готовых вспомогательных функций — AFSimple_Calculate, AFNumber_Format, AFDate_FormatEx и другими — поэтому привязываемый скрипт обычно представляет собой однострочный вызов этой библиотеки, а не уникальный код

Одна важная деталь определяет, дает ли многостраничная форма правильный ответ: порядок вычислений. Acrobat выполняет действия для каждого поля, содержащего вычисляемое действие, в последовательности, указанной в массиве AcroForm /CO, а не в порядке страниц. Метод ApplyLoadedFieldCalculation привязывает вычисляемое действие к указанному вами полю. Если одно вычисляемое поле зависит от другого (например, строка налога зависит от промежуточного итога, а общий итог зависит от обоих), вам необходимо убедиться, что порядок в /CO ставит каждое входное поле перед полем, которое его использует. В противном случае общий итог считает устаревший промежуточный итог и будет отставать на одно изменение

Как заставить форму PDF автоматически вычислять итоги?

Метод ApplyLoadedFieldCalculation принимает индекс целевого поля, математическую операцию и имена исходных полей, которые необходимо объединить. HotPDF записывает вызов AFSimple_Calculate("SUM", ["lineTotal.1", "lineTotal.2", ...]) в действие вычисления целевого поля, а перечисление THPDFFieldCalcOp указывает нужную функцию: fccSum, fccAverage, fccProduct, fccMinimum и fccMaximum соответствуют функциям Acrobat SUM, AVG, PRD, MIN и MAX. Исходными именами являются имена полей AcroForm — те же строки, которые возвращает GetLoadedFormFieldNames, так что вычисление итога по трем строкам товаров и стоимости доставки выполняется за один вызов

var
  Pdf: THotPDF;
  Names: THPDFAnsiStringArray;
  i, TotalIdx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('invoice.pdf');
    Names := Pdf.GetLoadedFormFieldNames;
    TotalIdx := -1;
    for i := 0 to High(Names) do
      if Names[i] = 'grandTotal' then TotalIdx := i;
    if TotalIdx >= 0 then
    begin
      Pdf.ApplyLoadedFieldCalculation(TotalIdx, fccSum,
        ['lineTotal.1', 'lineTotal.2', 'lineTotal.3', 'shipping']);
      Pdf.EnsureLoadedFieldAppearanceStream(TotalIdx);
    end;
    Pdf.SaveLoadedDocument('invoice-live.pdf');
  finally
    Pdf.Free;
  end;
end;

Вызов EnsureLoadedFieldAppearanceStream для итогового поля не является исключительно декоративным. Скрипт вычислений выполняется внутри просмотрщика, поэтому пока средство чтения с поддержкой скриптов не откроет файл, итоговое поле не будет иметь собственного сгенерированного содержимого. Запись внешнего вида (appearance stream) позволяет отобразить правильное значение в тех случаях, когда скрипт не выполняется (например, при отправке на печать или создании эскизов страниц)

Форматирование и валидация полей с помощью набора функций AF

Метод ApplyLoadedFieldValidation связывает скрипты форматирования, ввода с клавиатуры и валидации в один вызов, а передаваемый параметр THPDFFieldValidationKind определяет, какой именно обработчик Acrobat будет подключен. Передайте fvkCurrency, и HotPDF запишет AFNumber_Format(2,0,0,0,"$ ",true) в качестве скрипта форматирования с соответствующим вызовом AFNumber_Keystroke; fvkNumber использует "." для разделения аргументов, fvkPercentage генерирует AFPercent_Format, а fvkDate генерирует AFDate_FormatEx("dd/mm/yyyy"), если только вы не передадите свой шаблон в аргументе DateFormat. Специальные форматы — fvkZipCode, fvkSSN и fvkPhone — сопоставляются с AFSpecial_Format с кодом, который Acrobat резервирует для каждого из них, а fvkArbitraryMask принимает произвольную маску через аргумент Mask для сценариев, не покрываемых предустановками. Для числовых типов HotPDF также привязывает скрипт AFRange_Validate, чтобы отсекать некорректный ввод при подтверждении значения

// Format the invoice date as ISO, the amount as currency, and mask a tax id
Pdf.ApplyLoadedFieldValidation(DateIdx,   fvkDate,          'yyyy-mm-dd', '');
Pdf.ApplyLoadedFieldValidation(AmountIdx, fvkCurrency,      '', '');
Pdf.ApplyLoadedFieldValidation(TaxIdIdx,  fvkArbitraryMask, '', '999-99-9999');
Pdf.EnsureLoadedFieldAppearanceStream(AmountIdx);

Вспомогательные функции AF покрывают большинство стандартных сценариев, избавляя вас от необходимости писать JavaScript-код вручную. Если полю требуется логика, которую нельзя выразить стандартными шаблонами (например, контрольная сумма, перекрестное правило или регулярное выражение), привяжите исходный скрипт напрямую с помощью методов для работы с действиями полей, описанных в статье о создании полей AcroForm и действий в HotPDF, которые принимают строку JavaScript напрямую. Оба подхода пишут данные в один словарь /AA, так что стандартный формат валюты для одного поля и ручная валидация для другого могут сосуществовать в одном документе

Почему необходимо повторно генерировать внешний вид (appearance stream)?

Потому что текстовое значение поля и пиксели, которые рисует просмотрщик, — это разные вещи в спецификации PDF, и изменение одного не меняет другого автоматически. Поле сохраняет свое значение в /V, но то, что отображает средство чтения, берется из потока внешнего вида поля, /AP /N. Если изменить /V в структуре объектов, строгий просмотрщик все равно покажет старое визуальное состояние, пока внешний вид не будет сгенерирован заново. Словарь форм может содержать флаг /NeedAppearances, запрашивающий у просмотрщика перестроение внешнего вида всех полей при открытии, и Acrobat следует этому правилу. Однако встроенные в браузеры просмотрщики, мобильные приложения, серверы печати и генераторы эскизов часто игнорируют этот флаг и рисуют тот поток внешнего вида, который уже записан в файле, либо не рисуют ничего вовсе. EnsureLoadedFieldAppearanceStream устраняет эту проблему, самостоятельно создавая Form XObject /AP /N на основе параметров /DA, /V и /Rect поля, благодаря чему сгенерированное визуальное представление сохраняется в самом файле и не зависит от механизма отрисовки конкретного просмотрщика

// Bake an appearance for every field before saving, so values render everywhere
Names := Pdf.GetLoadedFormFieldNames;
for i := 0 to High(Names) do
  Pdf.EnsureLoadedFieldAppearanceStream(i);
Pdf.SaveLoadedDocument('invoice-flat-appearance.pdf');

Где скрипты AF перестают работать

Каждый скрипт семейства AF* зависит от наличия JavaScript-движка в просмотрщике, а большинство средств просмотра его не содержат. Acrobat и несколько настольных продуктов выполняют эти функции; средства просмотра PDF, встроенные в Chrome, Edge, Firefox и основные мобильные платформы, а также серверные растеризаторы и конвейеры печати, вообще не исполняют JavaScript в формах. В таких приложениях вычисления никогда не сработают, а форматирование не применится — поле просто покажет тот поток внешнего вида, с которым оно было сохранено в последний раз (именно поэтому запись /AP /N критически важна). Относитесь к функциям семейства AF* как к удобству для тех пользователей, которые работают с полнофункциональными просмотрщиками, но не полагайтесь на них как на гарантию. Любой итог, который должен быть гарантированно верным на сервере (например, сумма счета для оплаты или сумма налога для подачи), должен быть перепроверен на вашей стороне после получения заполненной формы, поскольку нельзя исходить из предположения, что средство просмотра выполнило хотя бы одну строку вашего скрипта

Стоит упомянуть еще один сценарий: формы, созданные в формате XFA. Динамическая форма XFA содержит собственный вычислительный движок в XML-структуре, о котором функции AF* ничего не знают, поэтому привязка AFSimple_Calculate к полю XFA не даст никакого результата. Сначала преобразуйте форму XFA в обычную форму AcroForm, как описано в статье о преобразовании XFA в AcroForm в Delphi, после чего три описанных здесь метода можно будет применить к результирующим полям AcroForm точно так же, как к любой другой форме

Создание автовычисляемых и форматируемых форм AcroForm сводится к трем вызовам для загруженного документа — привязке вычисления, привязке формата и валидации, генерации внешнего вида — с последующим вызовом SaveLoadedDocument. Описанное здесь API загруженных форм является частью компонента HotPDF для Delphi и C++Builder, который документирует полные перечисления THPDFFieldValidationKind и THPDFFieldCalcOp наряду со всеми возможностями AcroForm