Tekninen artikkeli

Interaktiiviset PDF-lomakkeet Delphissä: Toiminnot ja JavaScript

PDF-lomakekenttä on yksinään vain laatikko, joka pitää sisällään arvon. Se, mikä saa lomakkeen käyttäytymään kuin pieni sovellus, on siihen liitetty toiminto (action): napsautus, joka piilottaa osion, vetää tallennetut arvot takaisin tiedostosta, hyppää viimeiselle sivulle tai suorittaa skriptin, joka laskee sarakkeen summan. Mikään tästä ei elä itse kentässä. Se elää toimintasanakirjassa (action dictionary), ja ISO 32000-1 järjestää koko perheen luvussa 12.6. Tämä artikkeli käy läpi toiminnot, joihin Delphi-ohjelma tarttuu useimmin, ja näyttää kuinka PDFlibPas kytkee jokaisen kenttään tai linkkiin

Käyttökelpoinen mentaalimalli on se, että kenttä ja toiminto ovat erillisiä objekteja, joita yhdistää viittaus (reference). Widget-annotaatio tai linkkiannotaatio kantaa toimintoa sen /A-merkinnässä. Toiminto nimeää kentän, jossa se toimii, otsikon (title) perusteella, ei indeksin, joten kentälle antamasi otsikko on kahva (handle), jota jokainen myöhempi toiminto käyttää sen löytämiseen. Kun tämä jako on selvä, API (ohjelmointirajapinta) lakkaa näyttämästä kutsujen sekamelskalta ja alkaa näyttää yhdeltä kuviolta, jota sovelletaan neljään eri verbityyppiin

Nimetyt toiminnot: navigointi ilman sivunumeroa

Yksinkertaisimmat toiminnot eivät sisällä lainkaan parametreja. ISO 32000-1 §12.6.4.11, taulukko 194, määrittelee nimetyt toiminnot: katseluohjelma tulkitsee symbolisen nimen ajon aikana sen sijaan, että se seuraisi tallennettua määränpäätä (destination). Neljää nimeä tuetaan yleisesti, ja ne ovat täsmälleen ne, joita lukija odottaa työkalupalkilta: NextPage, PrevPage, FirstPage ja LastPage. Koska määränpää on suhteellinen siihen sivuun, jota katseluohjelma kulloinkin näyttää, tällä tavalla rakennettu Seuraava-painike toimii jokaisella sivulla ilman, että lasket kohdetta

PDFlibPas-kirjastossa nimetty toiminto liitetään hotspot-suorakulmioon nykyisellä sivulla. Neljäs ja viides kokonaislukuargumentti valitsevat verbin ja ulkoasun

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

Tässä ei ole synkronoitavaa määränpäätä, mikä on koko pointti. Nimetty toiminto selviää sivun lisäämisestä ja poistamisesta, koska se ei alun perinkään nimeä sivua. Vertaa tätä eksplisiittiseen go-to-linkkiin (siirry-linkki), joka tallentaa kohdesivun indeksin, joka sinun on numeroitava uudelleen heti, kun asiakirja kasvaa

Hide-toiminto ja sen taulukon (array) kompastuskivi

Hide (Piilota) -toiminto, ISO 32000-1 §12.6.4.10, taulukko 196, vaihtaa yhden tai useamman kentän näkyvyyttä. Se on siistein tapa rakentaa näytä ja piilota -käyttäytymistä ilman skriptausta, ja juuri sen haluat Näytä tiedot -linkille (Show details) tai kahdelle toisensa poissulkevalle paneelille, joissa toisen paljastaminen piilottaa toisen. Toiminto sisältää kohteen (target) sen /T-merkinnässä ja boolean-arvon /H, joka päättää suunnan: piilota (hide) kun true, näytä (show) kun false

Hienous piilee täysin siinä, kuinka tämä kohde on koodattu, ja se on juuri sellainen yksityiskohta, joka tuottaa lomakkeen, joka toimii sinun koneellasi ja epäonnistuu asiakkaan koneella. Kun toiminto nimeää yksittäisen kentän, /T kirjoitetaan yhtenä tekstimerkkijonona. Kun se nimeää useita, /T kirjoitetaan tekstimerkkijonojen taulukkona. Vanhemmat katseluohjelmat eivät käsittele yhden elementin taulukkoa samalla tavalla kuin pelkkää merkkijonoa, joten koodauksen on haaraduttava määrän perusteella: yksittäinen nimi on lähetettävä merkkijonona, ei taulukkona, jonka pituus on yksi, jos halutaan, että laajin valikoima lukijoita kunnioittaa sitä. PDFlibPas tekee tämän päätöksen puolestasi. Välität kenttien nimet erotettuna pilkuilla, puolipisteillä tai rivinvaihdoilla, ja kirjoittaja lähettää yhden merkkijonon yhdelle nimelle ja taulukon kahdelle tai useammalle

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

Koska toiminto ei viittaa ulkoiseen resurssiin, se pysyy yhteensopivana PDF/A:n kanssa. Välittämäsi nimet ovat täysin määriteltyjä (fully qualified) kenttäotsikoita, minkä vuoksi ryhmän sisällä olevaan lapsikenttään (child field) on viitattava sen koko polulla pisteytettynä pikemminkin kuin pelkällä lehtinimellä (leaf name)

ImportData: esitäyttö FDF:stä

Kun Hide-toiminto järjestelee sitä, mitä sivulla jo on, import-data-toiminto tuo arvot sen ulkopuolelta. ISO 32000-1 §12.6.4.8, taulukko 198, määrittelee sen toiminnoksi, joka täyttää AcroFormin levyllä olevasta Forms Data Format -tiedostosta. Tämä on toiminto, joka on Lataa näytedata (Reload sample data)- tai Palauta oletusasetukset (Reset to defaults) -kontrollin takana, jossa FDF-tiedosto toimitetaan PDF-tiedoston vieressä ja sisältää kanoniset kenttien arvot. Kutsu peilaa muita, ottaen hotspot-suorakulmion, polun FDF:ään ja ulkoasubittikartan: Pdf.AddLinkToImportData(40, 660, 120, 18, 'defaults.fdf', 1). Tiedoston ei tarvitse olla olemassa, kun PDF rakennetaan, mutta sen on oltava läsnä, kun käyttäjä napsauttaa, ja kaikki kenoviivat (backslashes) polussa kirjoitetaan uudelleen PDF-kanoniseen kauttaviivamuotoon puolestasi

Yksi rajoitus on syytä sanoa selvästi, koska se on yleinen yllätys. Import-data-toiminto osoittaa ulkoiseen tiedostoon, joten sitä ei sallita PDF/A:ssa. Kun asiakirja on PDF/A-tilassa, kutsu palauttaa nollan eikä lisää mitään sen sijaan, että se tuottaisi tiedoston, joka epäonnistuu validoinnissa. Jos putkesi (pipeline) tähtää arkistotulostukseen, esitäytön on tapahduttava generointivaiheessa kirjoittamalla kenttien arvot suoraan, ei siirtämällä niitä napsautukseen

JavaScript: globaalit paketit ja toimintokohtaiset skriptit

Logiikkaa varten, joka menee näyttämisen (show), piilottamisen (hide) ja tuonnin (import) yli, toimintoperhe ulottuu dokumenttitason JavaScriptiin. Skripti voi elää kahdessa eri paikassa, ja erolla on merkitystä. Dokumenttitason JavaScript-paketti tallennetaan kerran koko tiedostolle ja suoritetaan, kun asiakirja avataan, mikä tekee siitä oikean kodin funktiomäärityksille (function definitions) ja jaetulle tilalle (shared state). Toimintokohtainen skripti (per-action script) liitetään yhteen linkkiin tai kenttään ja suoritetaan vain, kun kyseinen objekti aktivoidaan, mikä tekee siitä oikean kodin sille yhdelle riville, joka kutsuu paketin jo määrittelemää funktiota

PDFlibPas paljastaa molemmat. AddGlobalJavaScript tallentaa nimetyn paketin dokumenttitasolla; nimen uudelleenkäyttö korvaa kaiken sen alle tallennetun. AddLinkToJavaScript kiinnittää skriptin hotspotiin, joten napsautus suorittaa sen

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

Funktion pitäminen globaalissa paketissa ja kutsun pitäminen linkissä ei ole tyylimieltymys. Se välttää saman rungon kopioimisen jokaiseen kontrolliin, joka tarvitsee sitä, ja se tarkoittaa, että katseluohjelma, jossa komentosarjat on poistettu käytöstä, ei yksinkertaisesti tee mitään napsautuksella sen sijaan, että se tukehtuisi epämuodostuneeseen inline-blobiin. Se pitää myös toimintokohtaiset merkinnät pieninä, mikä pitää tiedoston luettavana, kun tarkastelet sitä myöhemmin

Kentät, lapsikentät ja tuloksen jäädyttäminen

Toiminnot tarvitsevat kenttiä, joihin ne voivat vaikuttaa, joten auttaa näkemään, kuinka kenttä syntyy. NewFormField luo kentän nykyiselle sivulle ja palauttaa sen indeksin; kokonaislukutyyppi valitsee laadun, jossa 1 on Text, 2 on Pushbutton, 3 on Checkbox, 4 on Radiobutton, 5 on Choice, 6 on Signature ja 7 on Parent, joka omistaa lapsia (children), mutta ei piirrä mitään itse. Välittämäsi otsikko ei voi sisältää pistettä, koska piste on erotin (separator) täysin määritellyissä nimissä (fully qualified names), joita toiminnot käyttävät lapsiin (children) viittaamiseen

Radioryhmät ja hierarkkiset lomakkeet rakennetaan antamalla vanhempi-kentälle (parent field) lapsia. NewChildFormField lisää lapsen nimetyn vanhemman alle, ja radion ja valinnan (choice) tapauksissa AddFormFieldSub lisää yksittäiset vaihtoehdot ja palauttaa tilapäisen indeksin, jota käytät kunkin sijoittamiseen. Kun interaktiivinen vaihe on ohi ja haluat jäädyttää (freeze) kentän, jotta sen nykyisestä ulkoasusta tulee pysyvää sivun sisältöä, FlattenFormField piirtää kentän sivulle ja poistaa sen lomakkeesta. Flattening-toiminnon (litistämisen) jälkeen myöhempien kenttien indeksit siirtyvät yhdellä alaspäin, mikä on ainoa asia, joka on muistettava, jos litistät (flatten) useita kenttiä silmukassa

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-kutsu (litistä) on kommentoitu ulos tarkoituksella. Jätä se pois, ja asiakirja toimitetaan elävänä lomakkeena, jonka toiminnot laukeavat lukijassa. Ota se käyttöön, ja kenttä renderöidään alas staattisiin merkkeihin, mitä haluat silloin, kun lomake on täytetty ja tuloksen on kuljettava kiinteänä tietueena. Sama kenttä, sama koodi, kaksi hyvin erilaista asiakirjaa riippuen siitä, jäädytätkö sen

Oikean verbin valinta

Neljä toimintoa (action) jakautuvat siististi sen mukaan, mihin ne koskevat. Nimetty toiminto siirtää näkymää (viewport) eikä tarvitse kenttää. Hide-toiminto muuttaa näkyvyyttä ja tarvitsee kenttäotsikoita (field titles), jolloin merkkijono vastaan taulukko (string-versus-array) -koodaus hoidetaan puolestasi. Import-data-toiminto (tuo tietoja) tavoittaa levyllä olevan tiedoston, ja on siksi kielletty PDF/A:ssa. JavaScript-toiminto suorittaa mielivaltaista logiikkaa, ja se on parasta jakaa globaalin funktiipaketin ja pienten toimintokohtaisten (per-action) kutsujen kesken. Tartu yksinkertaisimpaan vaihtoehtoon, joka tekee työn: Hide-toiminto on siirrettävämpi (portable) kuin skripti, joka asettaa piilotetun lipun (hidden flag), ja nimetty toiminto on kestävämpi kuin tallennettu sivun määränpää (page destination), koska siinä ei ole ylläpidettävää numeroa

Tästä kaksi viereistä aihetta täydentävät kuvan. Jos lomake on osa saavutettavaa (accessible) asiakirjaa, rakennepuu (structure tree), jota ruudunlukuohjelmat käyvät läpi, on käsitelty artikkelissamme pdflibpas-tagged-pdf-accessibility-structure.html tagatuista PDF-tiedostoista ja saavutettavuusrakenteesta (tagged PDF and accessibility structure). Kun täytetty lomake on lukittava ja allekirjoitettava, työnkulku on kuvattu läpikäynnissä (walkthrough) pdflibpas-compliance-signing-workbench.html, vaatimustenmukaisuus ja allekirjoittaminen -työpöytä (compliance and signing workbench). Kaikki kolme rakentuvat samalle moottorille, joka toimitetaan PDF library for Delphi -kirjastona luonti- (creation), lomake- (form) ja allekirjoitus-API:den (signature APIs) ohella, joita on käsitelty muualla tässä blogissa