Technical Article

Interaktív PDF űrlapok Delphi-ben: Műveletek és JavaScript

Egy PDF űrlapmező önmagában csak egy doboz, amely egy értéket tárol. Ami egy űrlapot kis alkalmazássá tesz, az a hozzá csatolt művelet: egy kattintás, amely elrejt egy szakaszt, beolvassa a mentett értékeket egy fájlból, az utolsó oldalra ugrik, vagy futtat egy szkriptet, amely összesít egy oszlopot. Mindez nem a mezőben él, hanem egy műveletszótárban (action dictionary), és az ISO 32000-1 a 12.6. szakaszban rendszerezi a teljes családot. Ez a cikk azokat a műveleteket mutatja be, amelyeket egy Delphi program a leggyakrabban használ, és szemlélteti, hogyan kapcsolja össze őket a PDFlibPas egy mezővel vagy hivatkozással.

A fejünkben érdemes azt a modellt megtartani, hogy a mező és a művelet különálló objektumok, amelyeket egy hivatkozás köt össze. Egy widget annotáció vagy egy link annotáció a /A bejegyzésében hordozza a műveletet. A művelet név alapján azonosítja a mezőt, amelyen operál, nem pedig index szerint, így a mezőnek adott név az a fogantyú (handle), amelyet minden későbbi művelet használ a megtalálásához. Ha ez a különválás világos, az API már nem hívások véletlenszerű gyűjteményének tűnik, hanem egyetlen mintának, amelyet négyféle műveletre alkalmazunk.

Nevesített műveletek: navigáció oldalszám nélkül

A legegyszerűbb műveletek egyáltalán nem hordoznak paramétereket. Az ISO 32000-1 12.6.4.11. szakasza (194. táblázat) definiálja a nevesített műveleteket (named actions): a megjelenítő futásidőben értelmez egy szimbolikus nevet ahelyett, hogy egy tárolt célállomást követne. Négy név univerzálisan támogatott, és pontosan ezek azok, amelyeket az olvasó az eszköztárról vár: NextPage, PrevPage, FirstPage és LastPage. Mivel a célállomás relatív ahhoz az oldalhoz képest, amelyet a megjelenítő éppen mutat, az így felépített Next gomb minden oldalon működik anélkül, hogy ki kellene számítanunk a célt.

A PDFlibPas-ban a nevesített művelet az aktuális oldalon lévő hotspot téglalaphoz van csatolva. A negyedik és ötödik egész szám típusú argumentum választja ki a műveletet és a megjelenést.

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

Nincs szinkronban tartandó célállomás, és ez a lényeg. A nevesített művelet túléli a lapok beszúrását és törlését, mivel eleve nem nevez meg konkrét oldalt. Ezzel szemben egy explicit ugrási (go-to) link egy céloldal-indexet tárol, amelyet újra kell számozni, amint a dokumentum növekszik.

A Hide művelet és a tömbökkel kapcsolatos csapda

A Hide művelet (ISO 32000-1 12.6.4.10. szakasz, 196. táblázat) egy vagy több mező láthatóságát kapcsolja. Ez a legtisztább módja a megjelenítési és elrejtési viselkedés megvalósításának szkriptek nélkül, és ez az, amit egy Show details (Részletek megjelenítése) hivatkozáshoz vagy két egymást kölcsönösen kizáró panelhez szeretne használni, ahol az egyik felfedése elrejti a másikat. A művelet a /T bejegyzésében hordozza a célt, a /H logikai érték pedig a döntést: elrejtés, ha true, megjelenítés, ha false.

A finomság teljesen abban rejlik, hogyan van kódolva ez a cél, és ez az a fajta részlet, amely olyan űrlapot eredményezhet, amely működik az Ön gépén, de megbukik az ügyfélnél. Ha a művelet egyetlen mezőt nevez meg, a /T egyetlen szöveges karakterláncként (string) íródik le. Ha többet nevez meg, a /T karakterláncok tömbjeként íródik le. A régebbi megjelenítők nem kezelik ugyanúgy az egyelemű tömböt, mint a sima karakterláncot, így a kódolásnak el kell ágaznia a darabszám alapján: az egyetlen nevet karakterláncként kell kiadni, nem pedig egyelemű tömbként, ha azt szeretnénk, hogy az olvasók legszélesebb köre elfogadja. A PDFlibPas meghozza ezt a döntést Ön helyett. A mezőneveket vesszővel, pontosvesszővel vagy sortöréssel elválasztva adhatja át, és az író egyetlen karakterláncot ad ki egy név esetén, és többet kettő vagy több esetén.

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

Mivel a művelet nem hivatkozik külső erőforrásra, kompatibilis marad a PDF/A szabvánnyal. Az átadott nevek teljesen minősített mezőnevek (fully qualified field titles), ezért kell a csoporton belüli gyermekmezőt a teljes pontozott elérési útjával megcímezni a puszta mezőneve helyett.

ImportData: előkitöltés FDF fájlból

Ahol a Hide művelet átrendezi azt, ami már az oldalon van, ott az adatimportálási művelet kívülről hoz be értékeket. Az ISO 32000-1 12.6.4.8. szakasza (198. táblázat) olyan műveletként definiálja ezt, amely feltölti az AcroForm-ot egy lemezen lévő Forms Data Format (FDF) fájlból. Ez a művelet áll a Reload sample data (Mintaadatok újratöltése) vagy a Reset to defaults (Alapértelmezések visszaállítása) vezérlők mögött, ahol egy FDF fájl a PDF mellett található, és a kanonikus mezőértékeket hordozza. A hívás tükrözi a többit, átveszi a hotspot téglalapot, az FDF útvonalát és a megjelenési bitmaszkot: Pdf.AddLinkToImportData(40, 660, 120, 18, 'defaults.fdf', 1). A fájlnak nem kell léteznie a PDF összeállításakor, de jelen kell lennie, amikor a felhasználó rákattint, és az elérési útban lévő visszaperjelek (backslash) automatikusan átíródnak a PDF-kanonikus perjel formátumra.

Egy korlátozást érdemes egyértelműen kimondani, mert gyakran okoz meglepetést. Az adatimportálási művelet külső fájlra mutat, így nem engedélyezett a PDF/A szabványban. Ha a dokumentum PDF/A módban van, a hívás nullát ad vissza és nem ad hozzá semmit, ahelyett, hogy olyan fájlt hozna létre, amely megbukik a hitelesítésen. Ha a munkafolyamata archív kimenetet céloz meg, az előkitöltésnek a generálás idején kell megtörténnie a mezőértékek közvetlen beírásával, nem pedig egy kattintásra halasztva.

JavaScript: globális csomagok és műveletenkénti szkriptek

A megjelenítésen, elrejtésen és importáláson túlmutató logikához a műveletcsalád a dokumentumszintű JavaScript felé nyúl. Két különböző helyen élhet egy szkript, és a különbség számít. A dokumentumszintű JavaScript-csomag a teljes fájlhoz egyszer van tárolva, és a dokumentum megnyitásakor fut le, így ez a megfelelő hely a függvénydefiníciók és a megosztott állapot számára. A műveletenkénti szkript egyetlen linkhez vagy mezőhöz van csatolva, és csak akkor fut le, amikor az objektum aktiválódik, így ez a megfelelő hely annak az egy sornak, amely meghívja a csomag által már definiált függvényt.

A PDFlibPas mindkettőt elérhetővé teszi. Az AddGlobalJavaScript egy nevesített csomagot tárol dokumentumszinten; egy név újrahasználata felülírja a korábban alatta tárolt tartalmat. Az AddLinkToJavaScript egy hotspot-hoz csatol egy szkriptet, így a kattintás végrehajtja azt.

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

A függvény globális csomagban tartása és a hívás elhelyezése a linkben nem stílusbeli preferencia. Ezzel elkerülhető ugyanazon törzs duplikálása minden vezérlőn, amelynek szüksége van rá, és azt jelenti, hogy a letiltott szkripteléssel rendelkező megjelenítő egyszerűen semmit sem tesz kattintásra, ahelyett, hogy összeomlana egy hibás formátumú beágyazott objektumon. Ezenkívül a műveletenkénti bejegyzéseket kicsinek tartja, ami megőrzi a fájl olvashatóságát, amikor később ellenőrzi.

Mezők, gyermekmezők és az eredmény rögzítése

A műveleteknek mezőkre van szükségük, amelyeken dolgozhatnak, így hasznos látni, hogyan jön létre egy mező. A NewFormField létrehoz egy mezőt az aktuális oldalon és visszaadja annak indexét; az egész szám típus választja ki a fajtát, ahol az 1 a Text (szöveg), 2 a Pushbutton (nyomógomb), 3 a Checkbox (jelölőnégyzet), 4 a Radiobutton (rádiógomb), 5 a Choice (választó), 6 a Signature (aláírás) és 7 a Parent (szülő), amelynek vannak gyermekei, de maga nem rajzol ki semmit. Az átadott név nem tartalmazhat pontot, mert a pont az elválasztó karakter azokban a teljesen minősített nevekben, amelyeket a műveletek használnak a gyermekek eléréséhez.

A rádiócsoportok és a hierarchikus űrlapok úgy épülnek fel, hogy egy szülőmezőnek gyermekeket adunk. A NewChildFormField hozzáad egy gyermeket egy nevesített szülő alatt, a rádió és választó esetekben pedig az AddFormFieldSub hozzáadja az egyes opciókat, és visszaad egy ideiglenes indexet, amelyet az egyes elemek pozicionálására használhat. Amikor az interaktív szakasz véget ér, és le szeretne fagyasztani (freeze) egy mezőt, hogy a jelenlegi megjelenése permanent oldaltartalommá váljon, a FlattenFormField rárajzolja a mezőt az oldalra, és eltávolítja az űrlapról. A lapítás (flatten) után a későbbi mezők indexei eggyel lejjebb csúsznak, amit érdemes észben tartani, ha több mezőt lapít le egy ciklusban.

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;

A lapítási (flatten) hívás szándékosan van kikommentezve. Ha elhagyja, a dokumentum élő űrlapként kerül kiadásra, amelynek műveletei lefutnak az olvasóban. Ha engedélyezi, a mező statikus jelekké alakul át, ami akkor hasznos, ha az űrlap ki van töltve, és az eredményt rögzített rekordként kell továbbítani. Ugyanaz a mező, ugyanaz a kód, két teljesen különböző dokumentumot eredményez attól függően, hogy lefagyasztja-e.

A megfelelő művelet kiválasztása

A négy művelet egyértelműen elválik aszerint, hogy mit érintenek. Egy nevesített művelet elmozdítja a nézetet (viewport), és nincs szüksége mezőre. Egy Hide művelet megváltoztatja a láthatóságot, és mezőneveket igényel, a karakterlánc kontra tömb kódolást pedig a rendszer kezeli Ön helyett. Az adatimportálási művelet eléri a lemezen lévő fájlt, ezért nem engedélyezett a PDF/A-in. Egy JavaScript művelet tetszőleges logikát futtat, és a legcélszerűbb megosztani a függvények globális csomagja és a kis, műveletenkénti hívások között. Nyúljon a legegyszerűbbhöz, amely elvégzi a feladatot: egy Hide művelet hordozhatóbb, mint egy rejtett jelzőt beállító szkript, egy nevesített művelet pedig tartósabb, mint egy tárolt oldal-célállomás, mivel nincs karbantartandó oldalszám.

Innen két kapcsolódó téma teszi teljessé a képet. Ha az űrlap egy akadálymentes dokumentum része, a képernyőolvasók által bejárt struktúrafát a címkézett PDF-ről és akadálymentesítési struktúráról szóló cikkünk tárgyalja. Amikor a kitöltött űrlapot zárolni és aláírni kell, a munkafolyamatot az aláírási és megfelelőségi munkaasztal bemutatója írja le. Mindhárom ugyanarra a motorra épül, amely Delphi PDF-könyvtárként érhető el, a blogunkon máshol ismertetett dokumentumkészítő, űrlap- és aláírás-kezelő API-kkal együtt.