Поле форми PDF саме по собі є просто полем, яке зберігає значення. Те, що змушує форму поводитися як невеликий додаток, це прикріплена до неї дія: клік, який приховує розділ, підтягує збережені значення з файлу, переходить на останню сторінку або запускає скрипт, який підсумовує стовпець. Ніщо з цього не живе в полі. Воно живе в словнику дій, і стандарт ISO 32000-1 організовує все це сімейство в §12.6. У цій статті ми розглянемо дії, до яких програма Delphi звертається найчастіше, і покажемо, як PDFlibPas прив'язує кожну з них до поля або посилання.
Основна ментальна модель полягає в тому, що поле та дія є окремими об'єктами, з'єднаними посиланням. Анотація віджета або посилання містить дію у своєму записі /A. Дія вказує ім'я поля, з яким она працює, за назвою, а не за індексом, тому назва, яку ви даєте полю, є ідентифікатором, який кожна наступна дія використовує для його пошуку. Як тільки цей поділ стає зрозумілим, API перестає виглядати як випадковий набір викликів і починає сприйматися як єдиний шаблон, застосований до чотирьох типів дієслів.
Іменовані дії: навігація без номера сторінки
Найпростіші дії взагалі не містять параметрів. Стандарт ISO 32000-1 §12.6.4.11, таблиця 194, визначає іменовані дії: засіб перегляду інтерпретує символічне ім'я під час виконання, замість того щоб слідувати збереженому призначенню. Універсально підтримуються чотири імені, і це саме ті дії, які користувач очікує від панелі інструментів: NextPage, PrevPage, FirstPage та LastPage. Оскільки призначення залежить від того, яку сторінку засіб перегляду показує в даний момент, створена таким чином кнопка 'Далі' працює на кожній сторінці без обчислення вами цільової сторінки.
У 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, перемикає видимість одного або кількох полів. Це найчистіший спосіб реалізувати поведінку показу та приховування без скриптів, і це саме те, що вам потрібно для посилання 'Показати деталі' або для двох взаємовиключних панелей, де розкриття однієї приховує іншу. Дія містить ціль у своєму записі /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 змінює розташування того, що вже є на сторінці, дія імпорту даних приносить значення ззовні. Стандарт ISO 32000-1 §12.6.4.8, таблиця 198, визначає її як дію, яка заповнює AcroForm з файлу Forms Data Format на диску. Це дія, що стоїть за елементами керування на кшталт 'Перезавантажити зразки даних' або 'Скинути до значень за замовчуванням', коли файл 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 для Delphi разом з API створення, форм та підписів, які описані в інших статтях цього блогу.