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

Интерактивни PDF форми в Delphi: Действия и JavaScript

Само по себе си полето във форма на PDF е просто кутия, която съдържа стойност. Това, което кара една форма да се държи като малко приложение, е действието, прикрепено към нея: щракване, което скрива секция, извлича записани стойности от файл, преминава към последната страница или изпълнява скрипт, който сумира колона. Нищо от това не живее в самото поле. То се намира в речник с действия (action dictionary), а ISO 32000-1 организира цялото семейство в §12.6. Тази статия разглежда действията, към които Delphi програма посяга най-често, и показва как PDF Library for Delphi свързва всяко едно от тях с поле или връзка

Мисленният модел, който си струва да запомните, е че полето и действието са отделни обекти, свързани чрез препратка. Анотация на графичен компонент (widget annotation) или анотация на връзка (link annotation) носи действие в своя запис /A. Действието посочва полето, върху което работи, по име (title), а не по индекс, така че името, което давате на полето, е манипулаторът, който всяко следващо действие използва, за да го намери. След като това разделение е ясно, API спира да изглежда как съвкупност от произволни извиквания и започва да изглежда как един шаблон, приложен към четири вида глаголи

PDF Library for Delphi диаграма на PDF линк или уиджет анотация, сочеща един речник с действия, разклоняващ се към глаголите на действия named, hide, import-data и JavaScript, с картата import-data, маркирана като изключена от PDF/A
Един речник с действия зад връзка или уиджет захранва четири различни глагола — придвижване на изгледа, видимост, импорт от диск и скриптиране

Именувани действия: навигация без номер на страница

Най-простите действия изобщо не носят параметри. ISO 32000-1 §12.6.4.11, Таблица 194, дефинира именувани действия: четецът интерпретира символно име по време на изпълнение, вместо да следва съхранена дестинация. Универсално се поддържат четири имена и те са точно тези, които читателят очаква от лентата с инструменти: NextPage, PrevPage, FirstPage и LastPage. Тъй като дестинацията е относителна спрямо страницата, която четецът показва в момента, бутонът "Следващ" (Next), създаден по този начин, работи на всяка страница, без да е необходимо да изчислявате цел

В PDF Library for Delphi именуваното действие е прикрепено към активен правоъгълник (hotspot rectangle) на текущата страница. Четвъртият и петият целочислени аргументи избират глагола и външния вид

// NamedActionType: 0 = NextPage, 1 = PrevPage, 2 = FirstPage, 3 = LastPage
// Бит 0 на Options (стойност 1) изчертава рамка около горещата зона
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);   // преминава към последната страница

Няма дестинация, която да поддържате в синхрон, което е основната цел. Именуваното действие оцелява при вмъкване и изтриване на страници, защото никога не назовава конкретна страница. Сравнете това с изрична връзка за преминаване (go-to link), която съхранява индекс на целевата страница, който трябва да преномерирате в момента, в който документът нарасне

Действието Hide и клопката с масива

Действието Hide, ISO 32000-1 §12.6.4.10, Таблица 196, превключва видимостта на едно или повече полета. Това е най-чистият начин за изграждане на поведение за показване и скриване без скриптове и е точно това, което искате за връзка "Покажи детайли" или за два взаимно изключващи се панела, където разкриването на единия скрива другия. Действието носи цел в своя запис /T и булева стойност /H, която решава посоката: скриване, когато е истина (true), показване, когато е лъжа (false)

Тънкостта е изцяло в това как се кодира тази цел и това е вид детайл, който създава форма, работеща на вашата машина, но неуспешна на машината на клиента. Когато действието посочва едно поле, /T се записва като един текстов низ. Когато посочва няколко, /T се записва като масив от текстови низове. По-старите четци не третират масив с един елемент по същия начин, по който третират чист низ, така че кодирането трябва да се разклонява според броя: едно единствено име трябва да се изведе като низ, а не като масив с дължина едно, за да бъде уважено от възможно най-широк спектър четци. PDF Library for Delphi взема това решение вместо вас. Вие предавате имена на полета, разделени с запетаи, точки и запетаи или прекъсвания на редове, а модулът за запис извежда единичен низ за едно име и масив за две или повече

PDF Library for Delphi диаграма на правилата за кодиране на PDF действие Hide, където точно едно име на поле става текстов низ /T, докато две или повече имена стават масив от низове, плюс бележки за посоката на флага hide, PDF/A безопасност и напълно квалифицирани заглавия на полета
Записващият се разклонява според броя на полетата, така че единично име отива като гол низ, който по-старите четци уважават, а две или повече стават масив, превключван общо
// HideFlag, различно от нула, скрива изброените полета (/H true); нула ги показва
// Едно име -> /T е текстов низ. Две или повече -> /T е масив от низове
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 пренарежда това, което вече е на страницата, действието за импортиране на данни (import-data action) внася стойности отвън. ISO 32000-1 §12.6.4.8, Таблица 198, го дефинира като действие, което попълва AcroForm от файл с формат за данни на форми (Forms Data Format - FDF) на диска. Това е действието зад контролите "Презареждане на примерни данни" (Reload sample data) или "Възстановяване по подразбиране" (Reset to defaults), при които FDF файлът се доставя заедно с PDF файла и съдържа каноничните стойности на полетата. Извикването отразява останалите, като приема правоъгълника на активната зона, пътя до FDF и битова маска за външен вид: Pdf.AddLinkToImportData(40, 660, 120, 18, 'defaults.fdf', 1). Файлът не е необходимо да съществува при създаването на PDF файла, но трябва да присъства, когато потребителят щракне, а всички обратни наклонени черти в пътя се пренаписват в каноничната за PDF наклонена черта

Едно ограничение си струва да се отбележи ясно, защото често е изненада. Действието за импортиране на данни сочи към външен файл, така че не е разрешено в PDF/A. Когато документът е в режим PDF/A, извикването връща нула и не добавя нищо, вместо да произведе файл, който не преминава валидация. Ако вашият конвейер е насочен към архивен изход, предварителното попълване трябва да се случи по време на генерирането чрез директно записване на стойностите на полетата, а не чрез отлагането им за щракване

JavaScript: глобални пакети и скриптове за отделни действия

За логика, която надхвърля показването, скриването и импортирането, семейството от действия посяга към JavaScript на ниво документ. Има две различни места, където може да живее един скрипт, и разликата е важна. Пакетът JavaScript на ниво документ се съхранява веднъж за целия файл и се изпълнява при отваряне на документа, което го прави подходящото място за дефиниции на функции и споделено състояние. Скриптът за конкретно действие е прикачен към една връзка или поле и се изпълнява само когато този обект се активира, което го прави подходящото място за единия ред, който извиква функция, вече дефинирана от пакета

PDF Library for Delphi разкрива и двете. AddGlobalJavaScript съхранява именуван пакет на ниво документ; повторното използване на име замества всичко, което е било съхранено под него. AddLinkToJavaScript прикачва скрипт към активна зона, така че щракването да го изпълни

PDF Library for Delphi диаграма на двуслойния модел на PDF JavaScript, където глобален пакет на ниво документ дефинира recalcTotal при отварянето на документа, а всеки линк носи едноредов скрипт на действие, извикващ го при клик
Дефинирайте recalcTotal веднъж в пакета на ниво документ и оставете всяка кликабелна гореща зона да го достигне с еднолинейно извикване на действие
// Пакет на ниво документ: дефинирайте функцията за повторна употреба веднъж
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);' +
  '}');

// Скрипт за конкретното действие върху връзка: просто извикайте споделената функция
Pdf.AddLinkToJavaScript(40, 620, 100, 18, 'recalcTotal();', 1);

Запазването на функцията в глобалния пакет и извикването във връзката не е въпрос на стилово предпочитание. Това избягва дублирането на едно и също тяло на всяка контрола, която се нуждае от него, и означава, че четец с деактивирани скриптове просто няма да направи нищо при щракване, вместо да блокира при лошо форматиран вграден блок. Освен това поддържа записите за отделните действия малки, което поддържа файла четим, когато го инспектирате по-късно

Полета, дъщерни полета и замразяване на резултата

Действията се нуждаят от полета, върху които да действат, така че е полезно да се види как се създава едно поле. NewFormField създава поле на текущата страница и връща неговия индекс; целочисленият тип избира вида, където 1 е Text, 2 е Pushbutton, 3 е Checkbox, 4 е Radiobutton, 5 е Choice, 6 е Signature и 7 е Parent (родител), който притежава дъщерни полета, но сам по себе си не рисува нищо. Името (title), което предавате, не може да съдържа точка, тъй като точката е разделител в напълно квалифицираните имена, които действията използват за адресиране на дъщерни полета

Радио групите и йерархичните форми се изграждат чрез даване на дъщерни полета на родителско поле. 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;

    // Текстово поле, към което действието Hide ще се насочи по заглавието му
    FldShip := Pdf.NewFormField('ShippingAddress', 1);
    Pdf.SetFormFieldBounds(FldShip, 40, 120, 240, 20);
    Pdf.SetFormFieldValue(FldShip, '');

    // Свържете връзка Hide и навигационна връзка към тази страница
    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

    // Скрипт на ниво документ, достъпен за всяко събитие във файла
    Pdf.AddGlobalJavaScript('OnOpen',
      'app.alert("Form ready", 3);');

    // Фиксирайте полето, ако изходът вече не трябва да може да се редактира
    // Pdf.FlattenFormField(FldShip);

    if Pdf.SaveToFile('form_actions.pdf') <> 1 then
      raise Exception.Create('Save failed');
  finally
    Pdf.Free;
  end;
end;

Извикването на flatten е коментирано нарочно. Оставете го изключено и документът се доставя като жива форма, чиито действия се задействат в четеца. Активирайте го и полето се рендерира до статични маркировки, което е точно това, което искате, когато формата е попълнена и резултатът трябва да пътува като фиксиран запис. Едно и също поле, един и същ код, два много различни документа в зависимост от това дали го замразявате

Избор на правилния глагол

Четирите действия се разделят чисто според това какво докосват. Именуваното действие премества прозореца за преглед (viewport) и не се нуждае от поле. Действието Hide променя видимостта и се нуждае от имена на полета, като кодирането низ-спрямо-масив се обработва автоматично. Действието за импортиране на данни достига до файл на диска и следователно е забранено в PDF/A. Действието на JavaScript изпълнява произволна логика и е най-добре да бъде разделено между глобален пакет от функции и малки извиквания за конкретни действия. Посегнете към най-простото, което върши работа: действието Hide е по-преносимо от скрипт, който задава флаг за скриване, а именуваното действие е по-издръжливо от съхранена дестинация на страница, защото няма номер на страница, който да поддържате

Оттук две съседни теми завършват картината. Ако формата е част от достъпен документ, дървото на структурата, по което се движат екранните четци, е разгледано в нашата статия за маркиран PDF и структура за достъпност. Когато попълнената форма трябва да бъде заключена и подписана, работният процес е описан в ръководството за съвместимост и подписване. И трите се изграждат върху една и съща машина, която се доставя като PDF библиотека за Delphi заедно с API за създаване, форми и подписи, разгледани на други места в този блог