Tekninen artikkeli

AcroForm-lomakekenttien lisääminen ladattuun PDF-tiedostoon Delphissä

Sinulla on kolmannen osapuolen laskupohja tai arkistoitu sopimus, jonka joku on luonut vuosia sitten ohjelmistolla, jota kukaan ei enää löydä, ja vaatimuksena on tehdä siitä interaktiivinen: pudottaa allekirjoituslaatikko kulmaan, lisätä muutama tekstikenttä ja muuttaa tasainen tarkistuslista todellisiksi valintaruuduiksi. Vaikeus on siinä, että et ole luomassa tätä PDF-tiedostoa alusta alkaen. Se on jo olemassa, siinä on jo sivuja, sisältövirtoja ja fontteja, joita et hallitse, ja sinun on siirrettävä AcroForm-widgetit tähän objektikaavioon ilman, että rakennat sitä uudelleen. Tämä on erilainen ongelma kuin lomakkeen luominen uuteen asiakirjaan, ja se osa, joka kompastuttaa kehittäjät, on näkymätön, kunnes avaat lopputuloksen katseluohjelmassa ja juuri kirjoittamasi kentät eivät näy missään

HotPDF on alkuperäinen VCL PDF -komponentti Delphille ja C++Builderille, ja versiosta v2.247.0 alkaen se tarjoaa oman metodiryhmänsä juuri tähän: kaikkien kuuden vakiomuotoisen kenttätyypin rakentamiseen suoraan dokumenttiin, joka on ladattu LoadFromFile-metodilla. Tässä artikkelissa käydään läpi, mitä nämä metodit tekevät, millaisen ISO 32000-1 -sanakirjan ne rakentavat ja minkä lipun puuttuminen johtaa siihen, että koko toimenpide tuottaa hiljaisesti tyhjältä näyttävän tiedoston

Miksi kenttien luonti ladattuun dokumenttiin on oma koodipolkunsa

Kun rakennat PDF:n tyhjästä, HotPDF omistaa koko objektimallin. Jokainen sivu on kirjoitettava THPDFPage-kääre, ja tekstikentän lisääminen AddTextField-metodilla kytkee uuden widgetin sivun huomautusobjektiin (annotation object), sivuobjektiin ja lomakkeen kenttäkokoelmaan ja luo sitten ulkoasuvirran (appearance stream) asiakirjan fonttiresursseista. Ulkoasuvirta on widgetin näkyvä pinta, laatikko ja reunus sekä mahdollinen oletusteksti, jotka piirretään PDF-piirto-operaattoreina, jotka katseluohjelma renderöi sellaisenaan

Ladattu asiakirja ei tarjoa mitään tästä kehikosta. Sivut tuotiin sisään raakoina sanakirjoina; ei ole olemassa kirjoitettavaa THPDFPage-käärettä, johon widgetin voisi ripustaa, ja mikä tärkeämpää, ei ole fonttiresurssiputkea valmiina maalaamaan ulkoasuvirtoja. Ladattu polku kulkee siksi toista reittiä. Se kirjoittaa kenttäsanakirjat suoraan jäsennellyn objektikaavion päälle ja viittaa sivuihin nollapohjaisella indeksillä sivuobjektin sijaan. Kenttätyypit ja lippubitit täsmäävät täsmälleen tyhjästä luodun polun kanssa, joten tekstikenttä on tekstikenttä molemmissa tapauksissa; muuttuva tekijä on taustalla oleva putkisto ja erityisesti se, miten widgetin pinta piirretään

Lippu /NeedAppearances ei ole tässä valinnainen

Tämä on se yksittäinen tekijä, joka ratkaisee, näkyykö työsi. Koska ladattu polku ei luo ulkoasuvirtoja, juuri lisätty widget saapuu katseluohjelmaan ilman /AP-merkintää: kenttänä ilman kuvattua pintaa. Monet katseluohjelmat eivät piirrä mitään, jos niitä pyydetään renderöimään widget, jolla ei ole ulkoasua eikä ohjetta sellaisen rakentamiseen. Kenttä on tiedostossa, rakenteellisesti pätevä, lomakkeen täyttötyökalun tavoitettavissa ja ihmiselle täysin näkymätön

Pakotie on määritelty ISO 32000-1 -standardin kohdassa §12.7.3: AcroForm-sanakirja sisältää /NeedAppearances-boolean-arvon, ja kun se on true, standardinmukaisen lukijan on itse rakennettava puuttuvat ulkoasuvirrat kunkin kentän oletusulkoasusta /DA (default appearance) ja arvosta. HotPDF asettaa tämän puolestasi. Kun lisäät minkä tahansa kentän ladattuun asiakirjaan ensimmäisen kerran, suoritetaan EnsureLoadedAcroForm: jos luettelossa (catalog) ei ole avainta /AcroForm, se luo sellaisen; jos /Fields-taulukkoa ei ole, se luodaan ja se pakottaa asetuksen /NeedAppearances true. Et kutsu sitä suoraan, mutta sen olemassaolon tietäminen selittää käyttäytymisen. Se selittää myös käyttöönottoon liittyvän varoituksen, joka on syytä sanoa suoraan: kourallinen yksinkertaisia tai standardista poikkeavia katseluohjelmia ohittaa /NeedAppearances-lipun ja jättää silti kaiken piirtämättä. Valtavirran lukijoille lippu tekee tehtävänsä, mutta jos kohdeyleisösi käyttää epätavallista upotettua renderöijää, testaa se siellä ennen kuin lupaat mitään

Kuuden eri kenttätyypin lisääminen

Jokainen metodi noudattaa samaa muotoa. Välität nollapohjaisen sivun indeksin, widget-suorakulmion neljä kulmaa PDF-käyttäjäavaruuden koordinaatteina (user-space coordinates), kentän nimen ja muut ylimääräiset argumentit, joita kenttätyyppi vaatii. Suorakulmio on muotoa X1, Y1, X2, Y2, jossa PDF-koordinaatiston origo on sivun vasemmassa alakulmassa, joten suuremmat Y-arvot sijaitsevat ylempänä; tämä on tiedostomuodon koordinaattikäytäntö, ei näytön vasemman yläkulman käytäntö, ja sen tekeminen väärinpäin on toiseksi yleisin virhe lipun unohtamisen jälkeen. Jokainen kutsu palauttaa uuden kentän nollapohjaisen indeksin tai arvon -1, jos sivun indeksi oli alueen ulkopuolella tai sivuobjektia ei voitu selvittää

var
  Pdf: THotPDF;
  Idx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('contract.pdf') <= 0 then Exit;

    // Text field: name, initial value, max length (0 = unlimited)
    Idx := Pdf.AddLoadedTextField(0, 72, 680, 320, 700, 'FullName', '', 0);

    // CheckBox: export value, initial checked state
    Pdf.AddLoadedCheckBox(0, 72, 640, 90, 658, 'AgreeTerms', 'Yes', False);

    // Signature field: just a name and a rectangle
    Pdf.AddLoadedSignatureField(0, 360, 72, 540, 132, 'ApproverSig');

    if Idx >= 0 then
      Pdf.SaveLoadedDocument('contract-interactive.pdf');
  finally
    Pdf.Free;
  end;
end;

Tekstikentän kolmas ja neljäs merkkijonoargumentti ovat kentän nimi and sen alkuarvo /V; kokonaisluku on /MaxLen, joka kirjoitetaan vain, kun se on suurempi kuin nolla. HotPDF antaa jokaiselle muokattavalle kentälle oletusulkoasun /Helv 12 Tf 0 0 0 rg, jonka /NeedAppearances-asetusta kunnioittava katseluohjelma lukee päättääkseen fontin ja värin, jolla se arvon piirtää. Valintaruutu ottaa vientiarvon (export value) eli merkkijonon, jonka lomake lähettää, kun valintaruutu on valittuna, sekä boolean-arvon alkutilalle. Sisäisesti se kirjoittaa vastaavat /V-, /AS- ja /DV-nimimerkinnät, jotta päällä/pois-tila on yhtenäinen heti tiedoston avautuessa. Tyhjä vientiarvo on oletuksena Yes, mikä on perinteinen valintaruudun "päällä"-nimi

Valintakentät ja /Ff-bittiliput

Yhdistelmäruutu (ComboBox) ja luetteloruutu (ListBox) ovat molemmat valintakenttiä (choice fields), kenttätyyppi /Ch standardissa ISO 32000-1 §12.7.4. Ero pudotusvalikon ja vieritettävän luettelon välillä on yksi bitti kentän lippukokonaisluvussa /Ff: bitti 18, Combo-lippu, arvo $40000. HotPDF asettaa tämän bitin metodille AddLoadedComboBox ja jättää sen tyhjäksi metodille AddLoadedListBox. Muuten nämä kaksi ovat identtisiä, ja molemmat ottavat valintansa merkkijonojen avoimena taulukkona (open array of strings), joka kirjoitetaan /Opt-merkintään

// Dropdown (Combo flag set internally) with an initial selection
Pdf.AddLoadedComboBox(0, 72, 600, 300, 620, 'Country', 'Canada',
  ['United States', 'Canada', 'Mexico']);

// Scrolling list, no initial value
Pdf.AddLoadedListBox(0, 72, 520, 300, 590, 'Priority', '',
  ['Low', 'Normal', 'High']);

// Push button with a caption drawn through /MK
Pdf.AddLoadedPushButton(0, 360, 600, 480, 626, 'SubmitBtn', 'Submit');

Kaksi huomautusta valintaluettelosta. HotPDF kirjoittaa jokaisen /Opt-merkinnän tavallisena merkkijonona, jossa vientiarvo ja näytettävä teksti ovat samaa tekstiä. ISO 32000-1 §12.7.4.4 sallii myös kaksiosaisen muodon [export display] silloin, kun lähetettävän arvon on poikettava siitä, mitä käyttäjä lukee. Ladatut luontimetodit käyttävät yksinkertaisempaa yhden merkkijonon muotoa, joten jos tarvitset toisistaan poikkeavat vienti- ja näyttöarvot, sinun on asetettava ne itse tuloksena olevaan sanakirjaan. Välittämäsi arvo kentän nykyiselle valinnalle tulisi olla yksi tarjoamistasi vaihtoehdoista, sillä katseluohjelma vertaa sitä luetteloon

Painike (push button) on toinen lippuohjattu tapaus: kenttätyyppi /Btn, jossa bitti 17 eli painikelippu on asetettu arvoon $10000. Tämä bitti erottaa klikattavan painikkeen valintaruudusta, joka on myös /Btn-kenttä, mutta ilman tätä bittiä. Välittämäsi teksti kirjoitetaan ulkoasun ominaisuussanakirjaan /MK normaaliksi otsikoksi /CA. On rehellistä todeta toimintapiiri tässä: painike luodaan sen tekstillä ja suorakulmiolla, mutta ladattu luontimetodi ei liitä siihen toimintoa, joten sellaisenaan se on painike, joka näyttää oikealta mutta ei tee mitään klikattaessa. Lähetys-, nollaus- tai JavaScript-toimintojen kytkeminen on erillinen asia; tyhjästä luotavan kirjoituspuolen osalta kenttä-plus-toiminto-työnkulku käsitellään artikkelissa AcroForm-kenttien ja toimintojen rakentaminen Delphissä, mikä on oikea vertailukohta sille, mitä ladattu polku jättää tarkoituksella pois

Sanakirja, jonka jokainen kenttä jakaa

Kaikkien kuuden metodin taustalla on yksi jaettu rakentaja, joka luo widget-huomautuksen ja rekisteröi sen kahteen paikkaan. Se kirjoittaa arvot /Type /Annot ja /Subtype /Widget, /Rect-taulukon neljästä koordinaatistasi, huomautusliput /F 4, joka asettaa tulostuslipun (Print bit) niin, että kenttä näkyy sekä paperilla että näytöllä, kentän nimen /T, kenttätyypin /FT, liput /Ff ja /P-takaisinviitteen sivuobjektiin. Tämän jälkeen se lisää uuden kentän AcroFormin /Fields-taulukkoon ja kyseisen sivun /Annots-taulukkoon selvittäen epäsuorat viitteet matkan varrella, jotta se laajentaa todellisia taulukoita widgetin orvoksi jättämisen sijaan

Tämä kaksoisrekisteröinti on tärkeää, koska widget, joka elää vain toisessa näistä listoista, on rikki hienoisella tavalla. Kenttä, joka on mukana /Fields-taulukossa mutta puuttuu sivun /Annots-taulukosta, on lomakkeen tiedossa mutta sitä ei koskaan piirretä; päinvastaisessa tapauksessa se piirretään, mutta se on tuntematon lomakelogiikalle. HotPDF pitää molemmat synkronoituna jokaisen lisäyksen yhteydessä, mikä on juuri sellaista kirjanpitoa, jonka joutuisit muuten tekemään käsin täysin oikein määritysten mukaan

Muutamia rehellisiä rajoituksia

Aseta odotukset oikein ennen kuin rakennat työnkulkua tämän varaan. Litistä-ja-luo-uudelleen-käyttäytyminen riippuu siitä, kunnioittaako katseluohjelma /NeedAppearances-lippua, mikä kattaa Acrobat-ohjelman, nykyaikaisten selainten PDF-moottorit ja yleiset työpöytälukijat, mutta ei ole ehdoton takuu kaikkien maailman renderöijien kohdalla. Jos sinun on tuotettava tiedosto, jonka kentät renderöityvät identtisesti kaikkialla – myös katseluohjelmissa, jotka ohittavat lipun – liikut ulkoasuvirtaterritoriossa (appearance-stream territory), ja tyhjästä luotava kirjoituspolku, joka piirtää /AP-merkinnän puolestasi, on parempi vaihtoehto. Allekirjoituskenttä luodaan vastaavasti tyhjänä allekirjoituswidgetinä valmiina allekirjoitettavaksi; kentän sijoittaminen ei ole sama asia kuin kryptografisen allekirjoituksen soveltaminen

Olemassa olevan muuttamiseksi pikemminkin kuin uuden lisäämiseksi liittyvä operaatio on lomakkeen litistäminen (flattening), jossa interaktiiviset kentät leivotaan takaisin staattiseksi sivusisällöksi, jolloin arvoista tulee pysyviä ja muuttamattomia. Tämä edestakainen kierros, mukaan lukien se, miten XFA-sanakirjoja sisältävät lomakkeet käsitellään, kuvataan artikkelissa XFA- ja AcroForm-kenttien litistäminen Delphissä. Kenttien lisääminen ja kenttien litistäminen ovat saman elinkaaren kaksi päätä: tämä artikkeli käsittelee interaktiivisuuden tuomista asiakirjaan, josta se puuttui, ja litistäminen on tapa ottaa se pois, kun lomake on palvellut tarkoitustaan

Tässä esitetty ladattujen asiakirjojen lomake-API toimitetaan osana vaatimuksenmukaista HotPDF-komponenttia Delphille ja C++Builderille yhdessä kenttälippujen, ulkoasun käsittelyn ja muun AcroForm-mallin täydellisen viitteen kanssa