Технічна стаття

Автоматично обчислювані форми PDF в Delphi за допомогою HotPDF

Для створення рахунку-фактури або PDF-звіту про витрати, де позиції підсумовуються автоматично, а дата форматується сама, потрібно додати три речі до форми AcroForm. HotPDF дозволяє додати всі три до завантаженого файлу в Delphi: ApplyLoadedFieldCalculation для розрахунку підсумків через AFSimple_Calculate, ApplyLoadedFieldValidation для форматування та перевірки Acrobat AF, а також EnsureLoadedFieldAppearanceStream для формування потоку вигляду (appearance stream), який безпосередньо відображає переглядач.

HotPDF — це нативний VCL-компонент PDF для Delphi та C++Builder. Ці три методи працюють із документом, який ви відкрили за допомогою LoadFromFile, а не створюєте з нуля. Кожен метод приймає індекс поля (починаючи з 0) у масиві 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);

Чому потрібно регенерувати потік вигляду (appearance stream)?

Тому що в PDF рядок значення та пікселі, які відображає переглядач, є абсолютно різними речами, і зміна одного не змінює іншого. Поле зберігає своє значення у /V, але те, що рендерить програма перегляду, береться з потоку вигляду поля /AP /N. Якщо ви зміните /V через граф об'єктів, строгий переглядач все одно показуватиме старий вигляд, поки щось його не регенерує. Словник форми містить прапорець /NeedAppearances, який просить переглядач відновити вигляд усіх полів при відкритті, і Acrobat виконує це. Проте вбудовані в браузери переглядачі, мобільні додатки, сервери друку та генератори ескізів часто ігнорують цей прапорець і малюють той потік вигляду, який уже присутній, або взагалі нічого. Метод EnsureLoadedFieldAppearanceStream усуває цю проблему, створюючи об'єкт /AP /N Form XObject самостійно на основі полів /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 Component для Delphi та C++Builder, документація якого містить опис усіх перерахувань THPDFFieldValidationKind та THPDFFieldCalcOp разом із рештою інтерфейсу AcroForm.