Tekninen artikkeli

PDF-lomakekenttien navigointi Delphissä (PDFium Component)

Paina koodisi rakentamassa PDF-lomakkeessa Sarkainta (Tab), ja kohdistin (cursor) laskeutuu kaksi kenttää kauemmas siitä, missä sen pitäisi, tai ohittaa toisen sarakkeen (column) kokonaan, tai hyppää takaisin alkuun kolmannen kentän jälkeen neljännen sijaan. Henkilö, joka täyttää laskua (invoice) katseluohjelmassasi, odottaa näppäimistön kävelevän (walk) lomakkeen läpi (form) samalla tavalla kuin se kävelee jokaisen hänen koskaan käyttämänsä verkkolomakkeen läpi. Kun se ei sitä tee, hän tarttuu hiireen, metsästää seuraavaa laatikkoa ja päättää hiljaa, että työkalusi on keskeneräinen. Ennustettava (predictable) kenttien läpikäynti (field traversal) on ero sen tiedonsyöttöohjelman välillä, jota ihmiset sietävät (tolerate), ja sen, johon he luottavat, ja se on lähes täysin kiinni oikean kohdistus-API:n (focus API) käyttämisestä (using) sen sijaan, että näppäimistönsyöttöä (keyboard input) väärennettäisiin (faking) simuloiduilla napsautuksilla

Alla olevat esimerkit käyttävät PDFium Component -komponenttia, PDFium-pohjaista VCL/LCL-komponenttia Delphiä, C++Builderia ja Lazarusta varten. Navigointi on yksi kolmesta asiasta, jotka lomakekatseluohjelman (form viewer) on tehtävä oikein; kaksi muuta, lomakkeen avaaminen oikein ja täytettyjen (filled) arvojen (values) tallentaminen siten, että ne todella näkyvät, ovat paikkoja, joissa useimmat yllätykset piilevät, joten kaikki kolme käsitellään alla

Lomakkeen avaaminen: FormFill, FormType ja XFA-kysymys

Kenttiin (fields) pääsy (access) edellyttää, että lomakkeentäyttö-alijärjestelmä (form-fill subsystem), jota ohjataan FormFill-ominaisuudella, on otettu käyttöön ennen asiakirjan avaamista. Kun se on aktiivinen, FormType kertoo sinulle, millaisen lomakkeen kohtaat (facing), ja vastaus muuttaa (changes) ominaisuusjoukkoa (feature set), jonka voit luvata:

Pdf.FileName := FormPath;
Pdf.FormFill := True;   // enable before Active; required for any field access
Pdf.Active := True;

case Pdf.FormType of
  ftNone:
    DisableFormPanel('This document has no interactive form');
  ftAcroForm:
    BuildFieldList;     // full field navigation and editing available
  ftXfaFull:
    ShowXfaNotice;      // XFA renders from its own XML template;
                        // treat field editing as limited
end;

Kaksi käytännön (practical) huomiota seuraa tuosta kytkimestä (switch). AcroForm on standardin ISO 32000 mukainen lomakemalli (form model), ja se on se, mihin jokainen täällä oleva API tähtää. XFA-asiakirjat (XFA documents) upottavat (embed) oman XML-lomakearkkitehtuurinsa, joten täyden XFA-muokkauksen lupaaminen asiakkaalle nopean AcroForm-demon jälkeen on sitoumus (commitment), jota tulet katumaan. Toinen huomio koskee sivuvaikutuksia (side effects): FormFill-arvon asettaminen True-tilaan alustaa myös asiakirjan JavaScriptin. Tiedonsyöttöohjelmassa se on täsmälleen oikein, koska laskentaskriptit pitävät juoksevan (running) summan (total) ajan tasalla (current) sillä aikaa, kun joku kirjoittaa. Tuntemattomasta (unknown) alkuperästä peräisin olevien tiedostojen (files of unknown origin) esikatseluikkunassa se on täsmälleen väärin. Artikkeli pdfium-component-secure-pdf-preview-delphi.html, turvallinen PDF-esikatselu (secure PDF preview), kattaa (covers) tuon kompromissin (trade-off) FormFill := False -puolen

Sarkain-näppäimen (Tab-key) läpikäynti (traversal), joka laskeutuu odotetusti

Takaisin alun näppäimistöongelmaan. Kiusaus (temptation) on väärentää Sarkainta (Tab) syntetisoimalla (synthesizing) hiiren napsautus (mouse click) seuraavan elementin (widget) suorakulmioon, mikä rikkoutuu sillä sekunnilla, kun kenttä on vieritetty ruudun (off-screen) ulkopuolelle tai kaksi elementtiä osuu päällekkäin (overlap). Kohdistus-API (focus API) siirtää lomakkeen omaa kohdistusta (focus) suoraan, ilman geometria-arvailuja (geometry guesswork). Viisi kutsua kattaa sen: FocusFormField indeksillä, FocusNextFormField ja FocusPreviousFormField askeltamiseen (stepping), FocusedFormFieldIndex lukeaksesi, missä olet, ja ClearFormFieldFocus pudottaaksesi kohdistuksen (focus) kokonaan

procedure TFormViewer.HandleTabKey(Shift: TShiftState);
begin
  if ssShift in Shift then
    PdfView.FocusPreviousFormField
  else
    PdfView.FocusNextFormField;
  UpdateFieldStatus;  // e.g. "Field 4 of 17: InvoiceDate"
end;

Yksi toiminnan osa, johon (that) ihmiset kompastuvat (trips people up), on kierto (wrap). Läpikäynti (traversal) toimii nykyisen sivun sarkainjärjestyksen (tab order) kautta ja silmukoi sen sisällä: askella ohi viimeisen kentän ja olet takaisin ensimmäisessä. Molemmat askellusfunktiot palauttavat uuden kenttäindeksin (field index) tai -1, kun sivulla ei ole lainkaan kenttiä. Tuo silmukointi on sivukohtaista, ei asiakirjakohtaista, mikä tarkoittaa, että seuraavalle sivulle siirtyminen on sinun työsi, ei kirjaston (library's). Vertaa palautettua indeksiä siihen, josta aloitit, huomaa (notice) milloin se on kiertänyt (wrapped), ja siirrä PageNumber-arvoa itse, jos lomakkeen (form) on tarkoitus olla yksi jatkuva (continuous) sekvenssi. Jätä tuo tarkistus (check) väliin ja kaksisivuinen (two-page) lomake ansoittaa (traps) kohdistimen äänettömästi sivulle yksi, mikä on oma makunsa (flavor) rikkinäinen Sarkain -valitukselle (broken-Tab complaint)

Läpikäynti (traversal) muuttuu hyödylliseksi (useful), kun käyttöliittymän (UI) loppuosa (rest) reagoi siihen. Tapahtuma (event) OnFormFieldEnter laukeaa, kun kohdistus (focus) saapuu, ja katseluohjelmassa OnFormFieldFocusChange raportoi uuden kenttäindeksin, joten sivupaneeli (side panel) voi pysyä tahdissa sen kanssa, mitä näppäimistö juuri valitsi. Kun tarvitset käänteisen kartoituksen (reverse mapping), näytön (screen) sijainnista kenttään, indeksoitu ominaisuus FormFieldAt tekee osumatestauksen (hit-testing) työkaluvihjeiden (tooltip) esikatseluita (previews) ja klikkaa-ja-muokkaa-paneeleja (click-to-edit panels) varten. Tässä kaikessa on hiljainen (quiet) saavutettavuuden (accessibility) palkkio (payoff): koska kohdistus (focus) seuraa (follows) asiakirjan omaa kenttäjärjestystä, reitti (path), jonka kytket Sarkain-näppäimelle (Tab key), on sama reitti, jonka ruudunlukija (screen reader) ilmoittaa, ilman (with no) ylimääräistä työtä

Kenttien nimien (field names) näyttäminen (showing) raakojen indeksinumeroiden sijaan (instead) vaatii yhden ominaisuuden lisää. FormFieldInfo[] palauttaa TPdfFormFieldInfo-tietueen indeksiä kohti kantaen (carrying) kentän nimeä (field name), tyyppiä (type), fonttikokoa (font size), valintatilaa (checked state), vientiarvoa (export value) ja ryhmäjäsenyyttä (group membership), joka (which is what a) navigointiluettelon (navigation list) pitäisi (should) näyttää ("Kenttä 4/17: InvoiceDate" pelkän "4" sijaan). Valintanappiryhmät (Radio groups) ovat se tapaus (case), joka on omistetun (dedicated) testitiedoston arvoinen. Useat elementit (widgets) voivat jakaa (share) yhden kentän nimen (single field name), joten elementeistä (from widgets) naiivisti koottu (assembled) luettelo (list) näyttää saman ryhmän useita kertoja ja hämmentää kaikkia, jotka lukevat sitä

Miksi täytetyt (filled) arvot (values) tulevat ulos tyhjinä (blank), ja (and the) kutsu (call), joka korjaa sen

Toinen valitus (complaint), joka täyttää tukijonot (support queues), on hälyttävämpi kuin huonosti käyttäytyvä Sarkain-näppäin (Tab key): lomake (form) täytetään ohjelmallisesti (programmatically), asiakas avaa sen Acrobatissa ja jokainen kenttä näyttää tyhjältä. Napsauta kenttää ja sen arvo näpsähtää (snaps) näkyviin. Data on tiedostossa koko ajan. Se mikä puuttuu, on kuva (picture) datasta, ja syy on ymmärtämisen (understanding) arvoinen kerran, koska (because) se selittää (explains) kokonaisen bugien perheen (family of bugs)

AcroForm-tekstikenttä (AcroForm text field) tallentaa (stores) arvonsa kenttä-sanakirjan (field dictionary) /V-merkintään (ISO 32000-1 §12.7.3.3). Se, mitä katseluohjelma (viewer) todellisuudessa maalaa (paints), on jotain erillistä: elementin ulkoasuvirta (widget's appearance stream) merkinnän /AP (§12.5.5) alla, pieni (small) esirenderöity (pre-rendered) pätkä sisältöä (snippet of content). Kirjoita (Write) /V ja jätä /AP rauhaan (alone), ja nämä kaksi ajautuvat (drift) erilleen. Arvo (value) on siellä; sen (its) renderöity versio (rendered version) on vanhentunut (stale) tai puuttuu (absent). Acrobat sattuu (happens to) rakentamaan uudelleen kentän ulkoasun, kun se saa (gains) kohdistuksen (focus), mikä on koko selitys arvoille, jotka (which) ilmestyvät vasta (only on) napsauttamalla. Vanha (old) NeedAppearances-lippu (flag), joka pyysi katseluohjelmia regeneroimaan ulkoasut (appearances) puolestasi (for you), ei koskaan toiminut (worked) yhdenmukaisesti (uniformly) ja on (and is) vanhentunut (deprecated) PDF 2.0:ssa, ja (and) tulostuspalvelimet (print servers) sekä (and) pikkukuvageneraattorit (thumbnail generators) jättävät sen täysin huomiotta. Ne maalaavat (paint) /AP-merkinnän eivätkä (and) mitään muuta, joten jos /AP on tyhjä, ne (they) tulostavat (print) tyhjän laatikon

Arvon määrittäminen (Assigning) FormField[i]-kutsun kautta kirjoittaa vain /V-merkinnän (only). Siksi lomakkeen (form) täyttäminen (filling) on kolmivaiheinen (three-step) sekvenssi (sequence), ja vaihe (step), jonka (which) tiimit (teams) pudottavat (drop), on (is the) keskimmäinen:

procedure TFormViewer.FillAndSave(const Values: array of WString;
  const OutputPath: string);
var
  i: Integer;
begin
  for i := 0 to Pdf.FormFieldCount - 1 do
    Pdf.FormField[i] := Values[i];   // writes /V only

  // Rebuild the /AP appearance streams; without this the form
  // looks blank in Acrobat until each field is clicked
  Pdf.GenerateFormAppearances;

  Pdf.SaveAs(OutputPath);
end;

GenerateFormAppearances on (is) koko korjaus (whole fix). Se (It) rakentaa uudelleen (rebuilds) jokaisen elementin (widget's) ulkoasuvirran nykyisistä arvoista (values), fonteista (fonts) ja tasauksesta (quadding), joten katseluohjelma, joka ei koskaan aja kohdistustapahtumaa (focus event), kuten tulostuspalvelin tai pikkukuvageneraattori (thumbnailer), maalaa (paints) täytetyn (filled) tilan (state) joka tapauksessa (anyway). Kutsu sitä (Call it) kerran asettamiserän (batch of assignments) jälkeen, ei kerran kenttää kohden (once per field). Ulkoasujen luominen (Appearance generation) tekee oikeaa (real) asettelutyötä (layout work), ja kenttäkohtaiset kutsut kertaavat sen turhaan laajassa lomakkeessa

Ulkoasujen (appearances) regenerointi (Regenerating) on myös (also) se hetki, jolloin fontit (fonts) ja tasaus (alignment) tuovat (assert) itsensä esiin, mikä on (is the) toisen asteen (second-order) yllätyksen (surprise) lähde (source). Uusi virta (stream) asettelee kunkin (each) arvon (value) elementin (widget) suorakulmion (rectangle) sisälle käyttäen kentän (field's) fonttia, kokoa (size) ja tasausta (quadding). Arvo, joka (which) istuu mukavasti (comfortably) testilomakkeessasi (test form), voi leikkautua (clip) tai kutistua (shrink) asiakkaan (customer's) kopiossa (copy), jossa (where) sama (same) kenttä (field) on (is) kapeampi (narrower). Automaattisesti kokoa muuttavat (Auto-sized) kentät (fonttikoko nolla) kutistavat (shrink) tekstin (text) sopimaan (to fit); kiinteäkokoiset (fixed-size) kentät vain (just) leikkaavat sen. Molemmat ovat laillisia (legal), ja ainoa rehellinen (honest) tapa (way) tietää (to know), kumpaa (which one) tietty lomake (given form) tekee (does), on (is) katsoa regeneroitua tulostetta (output) kirjoittamasi merkkijonon (string) sijaan. Kun (When) joku raportoi (reports) tekstin (text) leikkautuneen (cut off) laatikon reunassa, tämä (this) on melkein (almost) aina (always) syy (reason)

Käsittele (Treat) todentamista (verification) osana (part of) työn (work) viimeistelyä (finishing), ei jälkiajatuksena (afterthought). Avaa tallennettu tiedosto (saved file) Acrobatissa (in Acrobat) ja varmista (confirm), että (the) arvot (values) ovat näkyvissä ennen kuin kosket mihinkään kenttään. Tulosta se sitten (Then print it) PDF:ksi tai (or to an) kuvaksi (image) toisesta katseluohjelmasta, sellaisesta, joka sivuuttaa lomakelogiikan kokonaan, ja (and) varmista, että arvot (values) selviytyvät (survive) myös tuosta polusta (path too). Yhdessä (Between them) nuo kaksi (two) tarkistusta saavat kiinni (catch) jokaisen muunnelman (variant) /V-vastaan-/AP (versus) -ryöminnästä (drift)

Kenttäkokoonpanot (Field configurations), jotka läpäisevät (pass) demon ja epäonnistuvat (fail) kentällä (in the field)

Puhtaat demolomakkeet (Clean demo forms) piilottavat joukon reunaehtoja (edge cases), joita asiakastiedostot (customer files) eivät (do not). Neljä (Four) niistä (of them) muodostaa (account for) suurimman osan (most of the) "se toimi minun koneellani" -raporteista (reports)

  • Valintaruutujen (Checkbox) vientiarvot (export values). Päällä ("on") -tila ei ole aina Yes. Lomake voi vapaasti määritellä (define) oman vientiarvonsa, ja väärän (wrong) merkkijonon kirjoittaminen jättää laatikon visuaalisesti (visually) valitsemattomaksi (unchecked) samalla kun koodisi on vakuuttunut, että se asetti sen. Lue vientiarvo ominaisuudesta FormFieldInfo[] sen sijaan (rather than), että olettaisit sellaisen
  • Jaetun nimen (Shared-name) valintanappiryhmät (radio groups). Yksi (One) kenttä, useita elementtejä (widgets). Arvo, jonka (you) asetat, ratkaisee (decides), mikä elementti luetaan (reads as) valituksi (selected), joten käyttöliittymäkoodi (UI code), joka olettaa yhden nimen kartoittuvan (maps to) yhteen suorakulmioon, päätyy (ends up) piirtämään (drawing) kohdistusrenkaan (focus ring) väärälle painikkeelle (wrong button)
  • Lasketut kentät (Calculated fields). Summat (Totals), joita asiakirjan JavaScript ylläpitää, päivittyvät kenttätapahtumien vastauksena (in response to). Ohjelmallinen täyttö (programmatic fill), joka ohittaa (bypasses) nämä tapahtumat, joutuu (has to) joko laukaisemaan (trigger) uudelleenlaskennan (recalculation) tai ylikirjoittamaan (overwrite) lasketut kentät suoraan. Lomake, jossa (where the) rivikohteet (line items) ja summa (total) ovat eri mieltä (disagree), on pahempi kuin kumpikaan korjaus
  • Piilotetut (Hidden) pakolliset kentät (required fields). Ehdolliset (Conditional) lomakkeet piilottavat kenttiä, jotka on (are) yhä merkitty (flagged) pakollisiksi (required). Päätä (Decide) etukäteen (up front), kunnioittaako validointisi (validation) näkyvyyttä (visibility) vai (or the) raakaa (raw) required-lippua (flag), ja kirjoita tuo (that) päätös ylös jonnekin, mistä (somewhere) tuki (support) voi sen löytää (find it)

Yksi (One) ero (distinction) on arvoitus (worth settling) ennen kuin (before) se puraisee (bites) sinua: ulkoasujen (appearances) luominen (generating) ei ole (is not) litistämistä (flattening). GenerateFormAppearances tekee (makes) arvoista näkyviä kaikkialla (everywhere) jättäen kentät (leaving the fields) muokattaviksi (editable). Litistäminen (Flattening) leipoo (bakes) ulkoasun staattiseen sivun (page) sisältöön (content) ja poistaa (strips) interaktiivisuuden (interactivity) lopullisesti (for good), mikä on (which is) oikein (right for an) arkistokopiolle (archival copy) ja väärin lomakkeelle (form), joka (the) seuraavan (next) henkilön (person) täytyy yhä (still has to) täyttää. Jos FormType ilmoittaa ftXfaFull pelkän (rather than) ftAcroForm-arvon sijaan, mikään (none of the) muokkauspinnasta (editing surface) tässä ei päde (applies) puhtaasti (cleanly) muutenkaan (anyway), koska (since the) asiakirja renderöityy (renders) omasta (own) XML-mallistaan (template); tunnista tuo tapaus (case) ja kerro (tell) käyttäjälle sen sijaan, että (rather than) antaisit heidän (letting them) löytää rajan (limit) itse (on their own)

Tässä esitetyt lomakkeentäyttö-alijärjestelmä (form-fill subsystem), kohdistuksen läpikäynti (focus traversal) ja ulkoasun (appearance) luominen (generation) ovat osa PDFium Component -komponenttia Delphiä, C++Builderia ja Lazarusta/FPC:tä varten. Jos katseluohjelmasi (viewer) käsittelee (handles) myös (also) tarkastelijan (reviewer) merkintöjä (markup) lomaketietojen rinnalla, artikkeli pdfium-component-annotation-review-delphi.html (huomautusten tarkastelu) kattaa tuon viereisen (adjacent) mallin (model)