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

Створення полів та дій AcroForm за допомогою HotPDF у Delphi

Дія AcroForm — це словник, приєднаний до віджета, який повідомляє переглядачу, що робити, коли з цим віджетом щось відбувається. Натисніть кнопку — і переглядач читає її словник дії: дія URI відкриває веб-адресу, дія JavaScript виконує скрипт, дія SubmitForm надсилає зібрані значення полів на кінцеву точку, дія ResetForm скидає їх до значень за замовчуванням. Дія — це дані, а не поведінка, вбудована у файл. ISO 32000-1 §12.6 визначає форму словника; саме переглядач надає рушій, який його інтерпретує. Цей поділ важливий тому, що дія, бездоганно записана в PDF, усе одно нічого не робить, якщо у читача на іншому кінці немає рушія для неї, і значна частина болю з AcroForm випливає саме з цього розриву, а не з пошкодженого поля

HotPDF записує ці словники безпосередньо з Delphi та C++Builder поряд із віджетами полів, до яких вони прив'язані. У кожній інтерактивній формі задіяно дві структури: віджет, який користувач бачить на сторінці, і механізм поля та дії під ним, що несе дані та логіку зв'язків. Вони редагуються незалежно, і будь-яка з них може бути неправильною, тоді як інша виглядає бездоганно. Розділи нижче послідовно розглядають іменування полів, самі дії кнопок, JavaScript на рівні поля та клас дефектів, які переживають візуальну перевірку, бо повністю ховаються в другій структурі

Шар віджетів AcroForm у HotPDF, відображений на базові значення полів, словник дії submit і розбіжність export-значення згоди
Користувачі клацають шар віджетів, тоді як значення подорожують крізь шар поля та дій під ним, де невідповідності залишаються невидимими

Імена полів — це маршрутизаційні ключі, а не підписи

Кожне поле AcroForm має повністю кваліфіковане ім'я. ISO 32000-1 §12.7.3 робить саме це ім'я, а не видимий підпис, ключем, під яким значення поля передається під час експорту або надсилання форми. Розробники, які приходять зі світу VCL-проєктування, схильні сприймати ім'я елемента керування як приватний ідентифікатор коду — але тут це не так. Це формат передачі даних

Перший наслідок цього — два поля з однаковим повністю кваліфікованим ім'ям не є двома полями. PDF трактує їх як дві анотації-віджети одного поля із спільним значенням, тож введення в одне миттєво оновлює інше. Це саме те, що потрібно, коли ім'я клієнта має повторюватися на кожній сторінці договору. Але це вада, коли цикл генерації випадково повторно використовує 'Field1' на трьох сторінках. Жодна візуальна перевірка не виявить другий випадок. Кожна сторінка все ще малює власну рамку, а зв'язок проявляється лише тоді, коли хтось починає вводити текст

Імена з крапками, такі як applicant.email, вибудовують ієрархію. Батьківський вузол applicant групує своїх нащадків, що й дозволяє скиданню або надсиланню зачіпати лише частину форми. Іменувати поля так із самого початку не коштує нічого, а окупається вже першого разу, коли приймальна система запитує саме блок applicant

Перемикачі (radio button) мають власне правило. Кнопки, які мають перемикатися разом, повинні мати спільне ім'я групи. У HotPDF виклики AddRadioButton, що передають однакове ім'я групи, приєднують свої віджети до одного батьківського поля, а значення експорту кожної кнопки ('basic' або 'full') визначає обраний варіант. Дайте кожній кнопці окреме ім'я — і отримаєте ряд незалежних перемикачів увімкнено/вимкнено замість однієї взаємовиключної групи, яка виглядає ідентично, але поводиться неправильно

Створення набору полів сторінка за сторінкою

HotPDF розміщує поля через методи THPDFPage, тож кожне поле належить об'єкту сторінки, який його створив. Пастка з послідовністю викликів, на яку слід зважати, — це AddPage. Він перенаправляє CurrentPage на нову сторінку одразу після повернення, тож будь-який виклик поля після нього потрапляє на нову сторінку, навіть якщо логічно поле належало сторінці, яку ви щойно залишили. Завершуйте кожну сторінку — намальований вміст і поля разом — перш ніж викликати AddPage

procedure BuildClaimForm(Pdf: THotPDF);
begin
  // Сторінка 1: блок заявника
  Pdf.CurrentPage.AddTextField('applicant.name', '', Rect(50, 700, 300, 722));
  Pdf.CurrentPage.AddTextField('applicant.email', '', Rect(50, 660, 300, 682));
  Pdf.CurrentPage.AddCheckBox('consent', 'Y', Rect(50, 620, 70, 640), False);
  Pdf.CurrentPage.AddRadioButton('coverage', 'basic', Rect(50, 580, 70, 600), True);
  Pdf.CurrentPage.AddRadioButton('coverage', 'full', Rect(90, 580, 110, 600), False);
  Pdf.CurrentPage.AddComboBox('plan', 'Standard',
    ['Basic', 'Standard', 'Premium'], Rect(50, 540, 200, 565));

  Pdf.AddPage;  // CurrentPage тепер вказує на сторінку 2
  Pdf.CurrentPage.AddListBox('riders', 'None',
    ['None', 'Flood', 'Earthquake'], Rect(50, 500, 200, 600));
end;

Координати використовують угоду PDF, де початок відліку — у нижньому лівому куті сторінки. Це той самий початок відліку, який TextOut використовує для намальованого тексту, тож Rect(50, 100, 200, 120) опиняється біля низу сторінки Letter, а не вгорі. VCL розміщує Y угорі й нарощує його вниз, тому таблиця розмітки, перенесена без змін, виходить дзеркально перевернутою по вертикалі — кожне поле опиняється не на тому кінці сторінки. Виконайте перетворення один раз у спільному допоміжному методі замість кожного місця виклику, і одне виправлення полагодить всю форму

Прив'язка кнопок до дій URI, JavaScript і submit

Кнопка (push button) інертна, доки до неї не прив'язано дію. HotPDF відображає типи дій з ISO 32000-1 §12.6.4 через перелік THPDFButtonAction (baURI, baJavaScript, baSubmitURL, baResetForm, baHide, baShow, baNamed) і надає два методи, що створюють кнопку та прив'язують її дію одним викликом

Типи дій кнопки-натискання HotPDF у Delphi: посилання baURI, скрипти baJavaScript і SubmitForm з явними прапорцями формату
Один прив'язувальний виклик причіплює будь-який із трьох словників дій, і лише варіант submit несе контракт прапорців із приймаючим ендпоінтом
// Відкрити довідкову сторінку в системному браузері
Pdf.CurrentPage.AddPushButtonWithAction('btnHelp', 'Help',
  'https://www.example.com/claims-help', Rect(320, 700, 420, 730), baURI);

// Виконати JavaScript на стороні переглядача
Pdf.CurrentPage.AddPushButtonWithAction('btnRecalc', 'Recalculate',
  'app.alert("Totals updated.");', Rect(320, 660, 420, 690), baJavaScript);

// Надіслати як XFDF і залишити порожні поля в тілі запиту
Pdf.CurrentPage.AddPushButtonWithSubmitAction('btnSubmit', 'Submit claim',
  'https://api.example.com/claims', Rect(320, 620, 420, 650),
  [sffXFDF, sffIncludeNoValueFields]);

Прапорці надсилання заслуговують на більше уваги, ніж їм зазвичай приділяють. AddPushButtonWithSubmitAction приймає набір THPDFSubmitFormFlags, і порожній набір видає звичайний post з url-кодуванням — формат, який приймають чимало тестових кінцевих точок і відхиляють чимало промислових. Додавання sffXFDF перемикає тіло запиту на XFDF. sffGetMethod змінює HTTP-метод. sffIncludeNoValueFields залишає порожні поля в тілі запиту замість того, щоб мовчки їх відкидати, що важливо в момент, коли споживач розрізняє "відсутнє" і "порожнє". Набір прапорців — частина контракту інтерфейсу з приймальною кінцевою точкою, тож узгоджуйте його з командою, яка розбирає надсилання, а не після першої відхиленої партії

JavaScript на рівні поля: keystroke, format, validate

Кліки кнопок — не єдине місце, де живуть дії. HotPDF також прив'язує JavaScript до подій рівня поля, які викликають переглядачі з підтримкою скриптів під час введення даних користувачем. Є три тригери, і вони спрацьовують у різні моменти життєвого циклу введення. Дія keystroke виконується під час надходження кожного символу, а також повторно при фіксації (commit). Дія format переписує відображуване значення після фіксації зміни — суто для представлення. Дія validate має останнє слово: приймає або відхиляє зафіксоване значення, перш ніж воно стане значенням поля

Життєвий цикл подій JavaScript рівня поля в HotPDF: від натискання клавіші до validate і format, зі засторогою про серверну валідацію внизу
Скрипти keystroke і validate можуть відхиляти ввід, тоді як format лише підправляє показ, і жоден скрипт не переживе читача без рушія JavaScript
// Відхилити зафіксовані значення, що не схожі на правдоподібні email-адреси
Pdf.AttachFieldKeyStrokeAction('applicant.email',
  'if (event.willCommit && !/^[\w.-]+@[\w.-]+\.\w+$/.test(event.value)) event.rc = false;');

// Відобразити американські номери телефонів у форматі (NNN) NNN-NNNN
Pdf.AttachFieldFormatAction('applicant.phone',
  'event.value = event.value.replace(/(\d{3})(\d{3})(\d{4})/, "($1) $2-$3");');

// Відхилити заявників молодших 18 років у момент фіксації
Pdf.AttachFieldValidateAction('applicant.age',
  'if (parseInt(event.value) < 18) event.rc = false;');

Встановлення event.rc = false усередині скрипту keystroke або validate повідомляє переглядачу відхилити введення. Заковика в тому, що ніщо з цього не працює, якщо переглядач не має вбудованого рушія JavaScript. Acrobat і кілька десктопних продуктів його мають. Більшість мобільних читалок, вбудованих у браузер рендерерів і конвеєрів друку — ні, і вони мовчки відкидають скрипти. Тож скрипти полів покращують якість даних лише для тієї частини користувачів, чия читалка їх виконує, — і це все, на що вони здатні. Це не межа безпеки. Кожне надіслане значення все одно доведеться перевіряти на сервері після надходження, бо не можна вважати, що клієнт бодай щось перевірив

Дефекти, що проходять візуальну перевірку

Найважче виявити ті дефекти AcroForm, які живуть у структурі даних, а не у відображенні, бо відкриття файлу й погляд на нього нічого не скажуть. Чотири трапляються достатньо часто, щоб їх варто було назвати, і для кожного є механічний тест, який знаходить його ще до релізу

  • Розбіжність значення експорту. Прапорець, створений як AddCheckBox('consent', 'Yes', ...), надсилає Yes. Споживач, що звіряє за Y, відхиляє кожне надсилання, тоді як сторінка виглядає бездоганно. Заповніть форму, експортуйте її як XFDF з Acrobat і звірте значення зі схемою, яку споживач насправді очікує
  • Випадкове дзеркалювання значень. Два поля зі спільним повністю кваліфікованим ім'ям зливаються в одне. Симптом проявляється під час введення даних і ніколи — під час генерації, тож тест полягає у введенні тексту у форму, а не в її відображенні та огляді результату
  • Значення combo поза списком опцій. Коли поточне значення, передане в AddComboBox, не входить до перелічених опцій, переглядачі розходяться в думках: показувати його, очищати чи позначати як помилку. Тримайте значення за замовчуванням у межах списку — і розбіжність зникає
  • Поля, доступні для редагування після завершення робочого процесу. HotPDF не має виклику для сплощення зовнішнього вигляду (appearance-flattening) полів AcroForm. Підтримуваний спосіб заморозити заповнену форму — створювати поля з прапорцем ffReadOnly, який залишає значення видимим через власний потік зовнішнього вигляду поля, водночас відмовляючи в редагуванні. Поле залишається живим об'єктом форми, а саме це й очікують знайти подальші інструменти складання та підписання

Одна поведінка на боці переглядача заслуговує на нотатку про регресію, навіть якщо жодна зміна коду її не усуває. Корпоративні розгортання Acrobat можуть за політикою вимикати JavaScript або обмежувати цілі надсилання, тож дія, яка працювала в кожній збірці розробки, може виявитися мертвою на заблокованому робочому місці клієнта. Заплануйте видимий запасний варіант на випадок, коли кнопка нічого не робить, навіть якщо цей запасний варіант — лише надрукована інструкція, що робити натомість

Де робота з формою стикається з рештою документа

Поле підпису саме собою є типом поля AcroForm. Форму, яку пізніше сертифікуватимуть або контрпідписуватимуть, краще резервувати це поле під час генерації, ніж допатчовувати його згодом, а причини цього на рівні байтів наведено в суміжній статті про цифрові підписи та підписання PAdES у HotPDF. Вхідні дані, що надходять як пакети XFA, а не рідний AcroForm, — це інша ситуація: сплощення XFA у поля AcroForm — окремий робочий процес зі своєю моделлю втрат, бо дві технології форм не можуть співіснувати в одному файлі

Показані тут методи полів, дій і тригерів є частиною стандартного API HotPDF Delphi Component для Delphi та C++Builder; сторінка продукту містить посилання на повний довідник, зокрема перевантаження прапорців полів і повний перелік прапорців надсилання