Technical Article

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

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

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

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

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

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

// 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

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

Действието 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 пренарежда това, което вече е на страницата, действието за импортиране на данни (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 на ниво документ се съхранява веднъж за целия файл и се изпълнява при отваряне на документа, което го прави подходящото място за дефиниции на функции и споделено състояние. Скриптът за конкретно действие е прикачен към една връзка или поле и се изпълнява само когато този обект се активира, което го прави подходящото място за единия ред, който извиква функция, вече дефинирана от пакета.

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 (родител), който притежава дъщерни полета, но сам по себе си не рисува нищо. Името (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;

    // 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 е коментирано нарочно. Оставете го изключено и документът се доставя като жива форма, чиито действия се задействат в четеца. Активирайте го и полето се рендерира до статични маркировки, което е точно това, което искате, когато формата е попълнена и резултатът трябва да пътува като фиксиран запис. Едно и също поле, един и същ код, два много различни документа в зависимост от това дали го замразявате.

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

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

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