Technický článek

Interaktivní PDF formuláře v Delphi: Akce a JavaScript

Samotné pole PDF formuláře je jen políčko pro hodnotu. To, co z formuláře dělá malou aplikaci, je akce k němu připojená: kliknutí, které skryje sekci, načte uložené hodnoty ze souboru, skočí na poslední stránku nebo spustí skript sčítající sloupec. Nic z toho nežije v samotném poli. Žije to v adresáři akcí (action dictionary) a ISO 32000-1 organizuje celou tuto rodinu v §12.6. Tento článek provází akcemi, po kterých program v Delphi sahá nejčastěji, a ukazuje, jak je PDF Library for Delphi propojuje s polem nebo odkazem

Důležitý mentální model spočívá v tom, že pole a akce jsou samostatné objekty spojené referencí. Anotace widgetu nebo anotace odkazu nese akci ve svém záznamu /A. Akce odkazuje na pole, se kterým pracuje, jeho názvem (title), nikoli indexem, takže název, který poli dáte, je identifikátorem, který každá pozdější akce používá k jeho vyhledání. Jakmile je toto rozdělení jasné, rozhraní API přestane vypadat jako náhodná směs volání a začne vypadat jako jeden vzorec aplikovaný na čtyři druhy operací

Diagram PDF Library for Delphi: anotace odkazu nebo widgetu PDF odkazuje na jeden slovník akcí, který se větví na slovesa named, hide, import-data a JavaScript, s kartou import-data označenou jako vyloučenou z PDF/A
Jeden slovník akcí za odkazem či widgetem živí čtyři odlišné slovesy — pohyb viewportu, viditelnost, import z disku a skriptování

Pojmenované akce: navigace bez čísla stránky

Nejjednodušší akce nenesou vůbec žádné parametry. ISO 32000-1 §12.6.4.11, tabulka 194, definuje pojmenované akce (named actions): prohlížeč interpretuje symbolický název za běhu namísto následování uloženého cíle. Univerzálně podporovány jsou čtyři názvy, což jsou přesně ty, které čtenář očekává na panelu nástrojů: NextPage, PrevPage, FirstPage a LastPage. Protože cíl je relativní k jakékoli stránce, kterou prohlížeč právě zobrazuje, tlačítko Další vytvořené tímto způsobem funguje na každé stránce, aniž byste museli počítat cíl

V PDF Library for Delphi je pojmenovaná akce připojena k aktivnímu obdélníku (hotspot rectangle) na aktuální stránce. Čtvrtý a pátý celočíselný argument vybírají operaci a vzhled

// NamedActionType: 0 = NextPage, 1 = PrevPage, 2 = FirstPage, 3 = LastPage
// bit 0 v Options (hodnota 1) vykreslí kolem aktivní oblasti rámeček
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);   // přejít na poslední stránku

Není zde žádný cíl, který by bylo nutné udržovat synchronizovaný, což je hlavní výhoda. Pojmenovaná akce přežije vložení i smazání stránky, protože na konkrétní stránku vůbec neodkazuje. Porovnejte to s explicitním odkazem typu go-to, který ukládá index cílové stránky, jejž musíte přečíslovat v momentě, kdy se dokument rozroste

Akce Hide a záludnost s polem

Akce Hide, ISO 32000-1 §12.6.4.10, tabulka 196, přepíná viditelnost jednoho nebo více polí. Je to nejčistší způsob, jak vytvořit chování pro zobrazení a skrytí bez skriptování, což je přesně to, co chcete pro odkaz Zobrazit podrobnosti nebo pro dva vzájemně se vylučující panely, kde odhalení jednoho skryje druhý. Akce nese cíl ve své položce /T a boolean /H, který určuje směr: skrýt při hodnotě true, zobrazit při false

Záludnost spočívá výhradně v tom, jak je tento cíl kódován, což je přesně ten detail, který způsobí, že formulář funguje na vašem počítači, ale selže u zákazníka. Když akce pojmenovává jediné pole, /T se zapíše jako jeden textový řetězec. Pokud jich pojmenovává několik, /T se zapíše jako pole textových řetězců. Starší prohlížeče nezacházejí s jednoprvkovým polem stejně jako s prostým řetězcem, takže kódování se musí větvit podle počtu: jediné jméno musí být zapsáno jako řetězec, nikoli jako pole o délce jedna, pokud jej má respektovat co nejširší škála čteček. PDF Library for Delphi toto rozhodnutí dělá za vás. Předáte názvy polí oddělené čárkami, středníky nebo zalomením řádků a zapisovač vygeneruje buď jeden řetězec pro jedno jméno, nebo pole pro dvě a více jmen

Diagram PDF Library for Delphi pravidel kódování akce Hide PDF: právě jeden název pole se stane textovým řetězcem /T, dva a více názvů polem řetězců, plus poznámky o směru příznaku hide, bezpečnosti PDF/A a plně kvalifikovaných názvech polí
Zapisovač se větví podle počtu polí, takže osamocený název odjede jako holý řetězec, který starší čtečky ctí, zatímco dva a více se stanou polem, jež se přepíná společně
// Nenulový HideFlag skryje uvedená pole (/H true); nula je zobrazí
// Jeden název -> /T je textový řetězec; dva nebo více -> /T je pole řetězců
Pdf.AddLinkToHideField(40, 700, 90, 18, 'ShippingAddress', 1, 1);
Pdf.AddLinkToHideField(140, 700, 90, 18,
  'ShippingName,ShippingAddress,ShippingZip', 1, 1);

Vzhledem k tomu, že akce neodkazuje na žádný externí zdroj, zůstává kompatibilní s PDF/A. Názvy, které předáváte, jsou plně kvalifikované názvy polí, což je důvod, proč podřízené pole v rámci skupiny musí být adresováno svou plnou tečkovanou cestou, nikoli pouze svým vlastním názvem

ImportData: předvyplnění z FDF

Zatímco akce Hide mění uspořádání toho, co již na stránce je, akce import-data přináší hodnoty zvenčí. ISO 32000-1 §12.6.4.8, tabulka 198, ji definuje jako akci, která naplní AcroForm ze souboru Forms Data Format (FDF) na disku. Jedná se o akci za ovládacími prvky jako Znovu načíst ukázková data nebo Obnovit výchozí, kdy se soubor FDF dodává vedle PDF a obsahuje kanonické hodnoty polí. Volání zrcadlí ostatní, přičemž přebírá aktivní obdélník, cestu k FDF a masku vzhledu: Pdf.AddLinkToImportData(40, 660, 120, 18, 'defaults.fdf', 1). Soubor nemusí existovat při sestavování PDF, ale musí být přítomen, když uživatel klikne, a případná zpětná lomítka v cestě jsou automaticky přepsána na kanonická lomítka PDF

Jedno omezení stojí za to uvést na rovinu, protože bývá častým překvapením. Akce import-data ukazuje na externí soubor, takže v PDF/A není povolena. Když je dokument v režimu PDF/A, volání vrátí nulu a nic nepřidá, namísto vytvoření souboru, který by selhal při validaci. Pokud vaše pipeline cílí na archivní výstup, předvyplnění musí proběhnout v době generování přímým zápisem hodnot polí, nikoli odkládáním na kliknutí

JavaScript: globální balíčky a skripty pro jednotlivé akce

Pro logiku, která přesahuje zobrazení, skrytí a import, sahá rodina akcí k JavaScriptu na úrovni dokumentu. Existují dvě odlišná místa, kde může skript žít, a na rozdílu záleží. Globální JavaScriptový balíček na úrovni dokumentu je uložen jednou pro celý soubor a spouští se při otevření dokumentu, což z něj dělá správné místo pro definice funkcí a sdílený stav. Skript pro konkrétní akci je připojen k jednomu odkazu nebo poli a spouští se pouze při aktivaci daného objektu, což z něj činí ideální domov pro řádek, který volá funkci, kterou balíček již definoval

PDF Library for Delphi zpřístupňuje obojí. AddGlobalJavaScript ukládá pojmenovaný balíček na úrovni dokumentu; opětovné použití názvu nahradí to, co pod ním bylo uloženo. AddLinkToJavaScript připojí skript k aktivní oblasti, takže jej kliknutí spustí

Diagram PDF Library for Delphi dvouvrstvého modelu JavaScriptu PDF: globální paket na úrovni dokumentu definuje recalcTotal při otevření dokumentu a každý odkaz nese jednořádkový skript volající jej při kliknutí
Definujte recalcTotal jednou v balíčku na úrovni dokumentu a nechte každé klikatelné hotspot dosáhnout ho jedním jednořádkovým voláním v rámci akce
// Balíček na úrovni dokumentu: jednou definujte znovupoužitelnou funkci
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);' +
  '}');

// Skript pro akci odkazu: pouze zavolejte sdílenou funkci
Pdf.AddLinkToJavaScript(40, 620, 100, 18, 'recalcTotal();', 1);

Ponechání funkce v globálním balíčku a volání v odkazu není otázkou stylu. Zabraňuje to duplikování stejného kódu u každého ovládacího prvku, který jej potřebuje, a znamená to, že prohlížeč s vypnutým skriptováním při kliknutí jednoduše neudělá nic, místo aby selhal na nesprávně formátovaném inline kódu. Také to udržuje položky jednotlivých akcí malé, což zachovává čitelnost souboru při jeho pozdější kontrole

Pole, podřízená pole a zmrazení výsledku

Akce potřebují pole, na která mohou působit, takže pomůže vidět, jak pole vzniká. NewFormField vytvoří pole na aktuální stránce a vrátí jeho index; celočíselný typ určuje druh, kde 1 je Text, 2 je Pushbutton, 3 is Checkbox, 4 is Radiobutton, 5 is Choice, 6 is Signature a 7 is Parent (rodič), který vlastní podřízená pole, ale sám nic nekreslí. Název, který předáváte, nesmí obsahovat tečku, protože tečka slouží jako oddělovač v plně kvalifikovaných názvech, které akce používají k adresování podřízených prvků

Skupiny přepínačů (radio groups) a hierarchické formuláře se sestavují tak, že rodičovskému poli přidělíte podřízená. NewChildFormField přidá podřízené pole pod pojmenovaného rodiče a v případě přepínačů a výběrů AddFormFieldSub přidá jednotlivé možnosti a vrátí dočasný index, který použijete k umístění každé z nich. Když je interaktivní fáze u konce a vy chcete pole zmrazit, aby se jeho aktuální vzhled stal trvalým obsahem stránky, FlattenFormField vykreslí pole na stránku a odebere jej z formuláře. Po zploštění se indexy následujících polí posunou o jeden dolů, což je jediná věc, kterou je třeba mít na paměti, pokud zplošťujete několik polí v cyklu

var
  Pdf: TPDFlib;
  FldShip: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    Pdf.SetOrigin(1);          // top-left origin
    Pdf.SetPageSize('A4');
    Pdf.NewPage;

    // Textové pole, na které akce Hide zacílí podle jeho názvu
    FldShip := Pdf.NewFormField('ShippingAddress', 1);
    Pdf.SetFormFieldBounds(FldShip, 40, 120, 240, 20);
    Pdf.SetFormFieldValue(FldShip, '');

    // Propojte odkaz Hide a navigační odkaz s touto stránkou
    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

    // Skript na úrovni dokumentu dostupný pro každou událost v souboru
    Pdf.AddGlobalJavaScript('OnOpen',
      'app.alert("Form ready", 3);');

    // Zmrazte pole, pokud výstup už nemá být upravitelný
    // Pdf.FlattenFormField(FldShip);

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

Volání flatten je záměrně zakomentováno. Pokud jej vynecháte, dokument se odešle jako živý formulář, jehož akce se spouštějí ve čtečce. Pokud jej povolíte, pole se vykreslí do statických prvků, což je to, co chcete, když byl formulář vyplněn a výsledek by měl putovat jako neměnný záznam. Stejné pole, stejný kód, dva velmi odlišné dokumenty v závislosti na tom, zda jej zmrazíte

Výběr správné operace

Tyto čtyři akce se jasně dělí podle toho, s čím pracují. Pojmenovaná akce posouvá zobrazení a nepotřebuje žádné pole. Akce Hide mění viditelnost a vyžaduje názvy polí, přičemž kódování řetězce versus pole je vyřešeno za vás. Akce import-data přistupuje k souboru na disku, a proto je v PDF/A zakázána. Akce JavaScriptu spouští libovolnou logiku a je nejlepší ji rozdělit mezi globální balíček funkcí a malá volání pro jednotlivé akce. Zvolte tu nejjednodušší, která splní daný úkol: akce Hide je přenosnější než skript nastavující příznak skrytí a pojmenovaná akce je odolnější než uložený cíl stránky, protože není třeba udržovat žádné číslo

Na toto téma navazují dvě oblasti, které doplňují celkový obraz. Pokud je formulář součástí přístupného dokumentu, strom struktury, který procházejí čtečky obrazovky, je popsán v našem článku o tagovaném PDF a struktuře přístupnosti. Pokud je nutné dokončený formulář uzamknout a podepsat, pracovní postup je popsán v našem průvodci pro shodu a podpisový workbench. Všechny tři staví na stejném enginu, který je dodáván jako PDF knihovna pro Delphi spolu s rozhraními API pro vytváření, formuláře a podpisy, o nichž se píše jinde na tomto blogu