Tekninen artikkeli

Aseta lomakekenttien arvot ladattuun PDF:ään Delphissä

HotPDF Delphi Component täyttää ladatussa PDF:ssä olemassa olevan AcroForm-kentän THotPDF.SetFormFieldValue-kutsulla, jossa kenttä osoitetaan joko nollapohjaisella kenttäindeksillä tai täysin kvalifioidulla kenttänimellä. Uuden /V-merkinnän kirjoittaminen on helppo osa; sen, mikä tekee kutsusta luotettavan oikeiden lomakkeiden kanssa, muodostaa se, että sama metodi pitää johdonmukaisina myös kolmea tilatietoa, jotka ovat näkymättömiä kunnes ne menevät pieleen: kentän dekoodattu identiteetti, jotta ei-ASCII-nimi löytyy ylipäätään, valintaruutujen ja radiopainikkeiden /AS-esitystila sekä valintakenttien /I-valintaindeksitaulukko. Näkyvä esitysstream on erillinen, nimenomainen vaihe EnsureLoadedFieldAppearanceStream-kutsun kautta

Skenaario on arkinen: asiakas lähettää sinulle oman lomakkeensa, veroilmoituksen, vakuutushakemuksen tai tilauksen, jonka joku rakensi Acrobatissa vuosia sitten, ja Delphi-sovelluksesi on täytettävä se tietokannasta ja palautettava tiedosto, joka avautuu oikein kaikkialla. Et voi vaikuttaa siihen, miten lomake on laadittu. Kenttänimet voivat olla UTF-16-koodattuja, valintaruutujen vientiarvot voivat olla 2 eivätkä Yes, ja yhdistelmäruudut voivat käyttää [vienti näyttö]-vaihtoehtopareja. Jokaisella noista yksityiskohdista on sääntö ISO 32000-1:ssä, ja jokaisen säännön SetFormFieldValue hoitaa nyt puolestasi. Tämä artikkeli kertoo, mitä se tekee, miksi ja mihin se pysähtyy. Sisarongelmaa eli sellaisten kenttien luomista, joita ei vielä ole, käsitellään artikkelissa AcroForm-kenttien lisäämisestä ladattuun PDF:ään Delphissä

Miksi SetFormFieldValue ei löydä kenttää, jonka nimi ei ole ASCII?

Ennen versiota v2.752.1 vastaus oli koodaus: kenttä eli tiedostossa heksadesimaalisella UTF-16BE-nimellä, ja nimivälimuistiin tallentui heksamuoto tekstin sijaan. ISO 32000-1 §12.7.3.1 määrittelee osakenttänimen /T tekstimerkkijonoksi, ja §7.9.2.2 sanoo, että tekstimerkkijono voi olla UTF-16BE, jonka alussa on FE FF -tavujärjestysmerkki. Kirjoitustyökalut serialisoivat tällaiset nimet rutiininomaisesti heksadesimaalimerkkijonoiksi §7.3.4.3:n mukaan, joten kenttä nimeltä Straße saapuu muodossa <FEFF005300740072006100DF0065>. HotPDF:n sisällä THPDFStringObject.Value sisältää raa'an heksadesimaalitekstin aina kun IsHexadecimal on asetettu, mikä on täsmälleen sitä mitä haluat alkuperäisen sanakirjan häviöttömään edestakaiseen käsittelyyn ja täsmälleen sitä mitä et halua hakemistoavaimeksi. HPDFLoadedFormTextName erottaa nämä kaksi huolta toisistaan. Kun suhdevälimuisti rakennetaan, jokainen /T-arvo kulkee sen läpi: jos merkkijono-objekti on heksadesimaalinen, HPDFHexToBytes palauttaa tavujonon; jos tavut alkavat FE FF -merkeillä ja niiden pituus on parillinen, hyötykuorma puretaan UTF-16BE:nä ja koodataan uudelleen UTF-8:ksi; tulos liitetään sitten vanhemman nimeen pisteellä sen täysin kvalifioidun nimen muodostamiseksi, jonka §12.7.3.1 kuvaa, joten lapsi nimeltä City vanhemman Address alla rekisteröityy muodossa Address.City. Välimuistiavain normalisoidaan pieniksi kirjaimiksi, mikä saa myös SetFormFieldValue('address.city', ...) -kutsun onnistumaan; se on mukavuus yli standardin, sillä spesifikaatio kohtelee nimiä kirjainkoon erottavina. Ratkaisevaa on, että vain välimuistiavain muuttuu. Kenttäsanakirjan /T-objekti pitää heksadesimaalikoodauksensa, joten asiakirjan tallentaminen ei kirjoita uudelleen sellaisen kentän identiteettiä, jonka pelkästään täytit

Miten HotPDF ratkaisee ei-ASCII-AcroForm-nimet: HPDFHexToBytes palauttaa heksadesimaalisen /T-merkkijonon takaa UTF-16BE-hyötykuorman, FE FF -tavujärjestysmerkki puretaan ja koodataan uudelleen UTF-8:ksi, ja kvalifioitu nimi liitetään vanhempaansa, joten sekä Applicant.FullName että kenttä nimeltä Straße päätyvät hakuvälimuistiin
Vain välimuistiavain muuttuu: kenttäsanakirja pitää heksadesimaalikoodauksensa, haut normalisoidaan pieniksi kirjaimiksi mukavuutena yli standardin, eikä asiakirjan tallentaminen koskaan kirjoita uudelleen sellaisen kentän identiteettiä, jonka pelkästään täytit
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;

    // Kvalifioidut nimet puretaan UTF-16BE-muotoisista /T-merkkijonoista ja
    // liitetään pisteillä, joten sisäkkäiset ja ei-ASCII-nimet ratkeavat
    Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
    Pdf.SetFormFieldValue('Applicant.Straße', 'Hauptstraße 12');

    // Arvot, jotka eivät ole Latin-1:tä, kulkevat FEFF-etuliitteisenä UTF-16BE-heksana
    // ja kirjoitetaan PDF:n heksadesimaalimerkkijonona
    Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');

    Pdf.SaveLoadedDocument('claim-form-filled.pdf');
  finally
    Pdf.Free;
  end;
end;

Mitä SetFormFieldValue oikeastaan kirjoittaa?

Molemmat ylikuormitukset ajavat samat viisi vaihetta: paikanna kenttäsanakirja, kirjoita /V HPDFSetDictFormValue-kutsun kautta, sovita valintaindeksit yhteen, merkitse sanakirja likaiseksi, sovita painikkeiden esitystilat yhteen ja kirjaa lopuksi kenttäindeksi NoteLoadedFormFieldDirty-kutsulla. Tuo viimeinen vaihe on merkityksellinen, jos lomake kuljettaa laskentaskriptejä, koska likainen joukko on se, mitä parametriton RecalculateLoadedFormFieldsIncremental-ylikuormitus kuluttaa ajaakseen uudelleen vain ne laskennat, jotka lukevat muuttunutta kenttää transitiivisesti. HPDFSetDictFormValue itse on huolellinen sen objektityypin suhteen, jonka se korvaa. Jos olemassa oleva /V on nimiobjekti, jota valintaruudut ja radiopainikkeet käyttävät vientiarvonaan, uusi arvo kirjoitetaan nimenä eikä koskaan merkkijonona, koska PDF-nimet ovat rakenteellisesti pelkkää ASCII:ta. Muussa tapauksessa se kirjoittaa merkkijono-objektin ja tutkii välittämäsi arvon: merkkijono, joka alkaa FEFF-merkeillä, jonka pituus on parillinen ja joka koostuu pelkistä heksan numeroista, käsitellään §7.9.2.2:n UTF-16BE-siirtomuotona ja tallennetaan IsHexadecimal-lipun kanssa, joten se serialisoituu muodossa <FEFF...> eikä literaalina (FEFF...). Juuri tuohon mekanismiin yllä oleva City-rivi nojaa; mikä tahansa muu merkkijono tallennetaan literaalimerkkijonona antamillasi tavuilla, joten pelkälle latinalaiselle tekstille välität pelkkää tekstiä

Miksi valintaruutu pitää vanhan rastinsa arvon muuttumisen jälkeen?

Koska painikekentässä pelkkä arvo ei ratkaise sitä, mitä piirretään. ISO 32000-1 §12.7.4.2.3 määrittää, että valintaruudun widget kantaa /AS-esitystilaa, joka nimeää sen streamin /AP /N-sanakirjassa, joka kulloinkin näytetään, ja katselimet piirtävät /AS-arvosta eivätkä /V-arvosta. Jos muutat /V-arvoksi Yes mutta jätät /AS-arvon Off-tilaan, tiedosto on sisäisesti ristiriitainen, ja flattenointi polttaa mielellään vanhentuneen rastittamattoman esityksen sivuun samalla kun lomakedata sanoo rastitetuksi. ReconcileLoadedButtonAppearanceStates on olemassa tämän kuilun sulkemiseksi: kentälle, jonka /FT on Btn, se käy läpi kenttäsanakirjan itsensä ja jokaisen sen /Kids-taulukon merkinnän, lukee päällä-tilan nimen /AP /N-sanakirjasta ja kirjoittaa /AS-arvon uudelleen tuoksi nimeksi kun se täsmää kentän arvoon tai Off-arvoksi kun se ei täsmää

Miksi HotPDF:n valintaruutu pitää vanhan rastinsa kun vain /V muuttuu: katselimet piirtävät /AS-esitystilasta /AP /N -sanakirjaan, joten ReconcileLoadedButtonAppearanceStates käy läpi kentän ja jokaisen lapsen, lukee päällä-tilan nimen ensimmäisenä muuna kuin Off-avaimena ja kirjoittaa /AS-arvon uudelleen osumalla tai muussa tapauksessa Off-arvoksi
Radioryhmät vertaavat jokaista lasta siihen vanhemman arvoon, jonka InheritedButtonValue palauttaa /Parent-ketjua pitkin, joten ryhmän asettaminen yhteen vientiarvoon kytkee päälle täsmälleen tuon widgetin ja sammuttaa jokaisen sisaruksen

Kaksi oikeista lomakkeista löytynyttä yksityiskohtaa muovasi version v2.752.3 korjausta. Ensinnäkin normaali esityssanakirja saa sisältää vain päällä-tilan; §12.7.4.2.3 nimeää pois-tilan esityksen Off-nimiseksi, mutta kirjoitustyökalut jättävät sen streamin usein pois ja antavat katselimen piirtää tyhjää. Aiempi koodi luovutti, kun sanakirjassa oli vähemmän kuin kaksi merkintää, joten nuo yksitilaiset valintaruudut pitivät vanhan rastinsa hiljaa. Tarkistus on nyt yksinkertaisesti se, että sanakirja ei ole tyhjä, ja päällä-tilan nimi otetaan ensimmäisenä avaimena, joka ei ole Off. Toiseksi päällä-tilan nimi on mikä tahansa, minkä tekijä valitsi. Oikeat lomakkeet käyttävät arvoja 2, Yes, On tai paikallistettua sanaa, joten vertailu tehdään todelliseen avaimeen kirjainkoosta riippumatta eikä koskaan kovakoodattuun Yes-arvoon. Radiopainikkeet lisäävät vielä yhden mutkan, jota kuvataan §12.7.4.2.4:ssä: valinta elää vanhemman kentän /V-arvossa, kun taas yksittäiset lapset omistavat widgetit eivätkä yleensä sisällä omaa /V-arvoa. Sisäkkäinen InheritedButtonValue-apufunktio kulkeekin /Parent-ketjua ylöspäin enintään 64 tasoa kunnes se löytää ei-tyhjän arvon, joten jokaista lasta verrataan sen ryhmän arvoon, johon se kuuluu. Vanhemman asettaminen yhden lapsen vientiarvoon kytkee päälle täsmälleen tuon lapsen ja sammuttaa jokaisen sisaruksen

// Valintaruutu: vientiarvon on täsmättävä /AP /N -sanakirjan päällä-tilan avaimeen
// (usein 'Yes', mutta oikeat lomakkeet käyttävät arvoja '2', 'On' tai mitä tahansa muuta)
Pdf.SetFormFieldValue('Consent', 'Yes');

// Radioryhmä: /V kirjoitetaan vanhemmalle; jokainen lapsiwidget saa
// /AS-arvokseen oman vientinimensä tai Off-arvon
Pdf.SetFormFieldValue('PaymentMethod', 'Card');

// Valintaruudun tyhjennys: mikä tahansa arvo, joka ei täsmää mihinkään päällä-tilaan, tuottaa /AS Off -arvon
Pdf.SetFormFieldValue('Newsletter', 'Off');

Valintakentät: /I pidetään /V:n tahdissa

Yhdistelmä- tai luetteloruudussa /V ei ole ainoa paikka, johon valinta kirjataan. §12.7.4.4:n taulukko 231 määrittelee /I-arvon taulukoksi nollapohjaisia indeksejä /Opt-taulukkoon, ja se yksilöi valitut alkiot, ja katselin, joka löytää /I-arvon osoittavan vaihtoehtoon 0 samalla kun /V nimeää vaihtoehdon 3, voi korostaa väärän rivin. Versiosta v2.754.1 lähtien HPDFReconcileChoiceSelection ajetaan jokaisen SetFormFieldValue-kutsun sisällä ja rakentaa /I-arvon uudelleen uudesta arvosta, kun peritty /FT on Ch. Toimintojen järjestys on tarkoituksellinen. Paikallinen /I-merkintä poistetaan ensin sen sisältöön koskematta: jos vanha taulukko oli epäsuora objekti, joka oli jaettu toisen kentän kanssa, sen mutatoiminen paikan päällä turmelisi tuon toisen kentän valinnan, joten rutiini pudottaa viittauksen ja luo tilalle tuoreen suoran taulukon. Sitten se ratkaisee /Opt-arvon /Parent-ketjun kautta, koska valintavaihtoehdot voivat olla perittyjä, ja skannaa merkinnät. Paljas merkkijonovaihtoehto vertautuu suoraan; [vienti näyttö]-pari vertautuu vientialkionsa perusteella, ja pari, jossa on vähemmän kuin kaksi alkiota, ohitetaan. Molemmat puolet kulkevat HPDFLoadedFormTextName-funktion läpi, joten heksadesimaalinen UTF-16-vaihtoehto täsmää heksadesimaaliseen UTF-16-arvoon ilman että sinun pitää kirjoittaa ne identtisesti. Ensimmäisellä osumalla kirjoitetaan yhden alkion /I ja skannaus pysähtyy; skalaariarvo korvaa aina aiemman monivalinnan MultiSelect-lipusta riippumatta

Miten HotPDF pitää valintakentän johdonmukaisena: HPDFReconcileChoiceSelection poistaa paikallisen /I-taulukon ennen siihen koskemista, ratkaisee /Opt-arvon /Parent-ketjun kautta, vertaa jokaisen vaihtoehdon vientipuoliskoa HPDFLoadedFormTextName-funktion kautta, kirjoittaa yhden alkion /I:n ensimmäisellä osumalla eikä kirjoita mitään kun muokattavan yhdistelmäruudun arvolla ei ole indeksiä
Paljas merkkijonovaihtoehto vertautuu suoraan ja vienti- ja näyttöpari vientialkionsa perusteella, kun taas /Opt-arvon ulkopuolinen arvo jättää oikein indeksin kokonaan pois — väärälle riville osoittava vanhentunut /I olisi pahempi kuin ei indeksiä lainkaan

Kun mikään ei täsmää, /I-arvoa ei kirjoiteta lainkaan. Se on oikea lopputulos muokattavalle yhdistelmäruudulle, jossa §12.7.4.4 sallii käyttäjän kirjoittaa arvon vaihtoehtoluettelon ulkopuolelta; sellaisella arvolla ei ole indeksiä, ja vanhentunut indeksi olisi pahempi kuin ei indeksiä lainkaan. Sama tulos tulee myös, jos välität näyttötekstin vientiarvon sijaan parilliselle vaihtoehtoluettelolle, joten kun yhdistelmäruutu kieltäytyy näyttämästä valintaasi, tarkista kumman puoliskon parista annoit

// /Opt on [[US United States] [CA Canada] [MX Mexico]]:
// täsmää vientiarvoon, ja /I:stä tulee [1]
Pdf.SetFormFieldValue('Country', 'CA');

// Muokattava yhdistelmäruutu, jonka arvo on /Opt-arvon ulkopuolella: /V kirjoitetaan,
// /I poistetaan eikä indeksiä keksitä
Pdf.SetFormFieldValue('Title', 'Principal Engineer');

Arvo ja esitys ovat kaksi erillistä operaatiota

SetFormFieldValue ei koskaan koske teksti- tai valintakentän esitysstreamiin. Kutsun jälkeen /V sisältää uuden tekstin, kun taas /AP /N piirtää yhä vanhan, ja se kumman katselin näyttää, riippuu siitä, kuljettaako AcroForm-sanakirja /NeedAppearances true -arvoa §12.7.3.3:n mukaan ja noudattaako katselin sitä. Jos tarvitset, että tiedosto renderöi uuden arvon jokaisessa lukijassa, mukaan lukien flattenoijat ja esikatselukuvageneraattorit jotka jättävät lipun huomiotta, kutsu EnsureLoadedFieldAppearanceStream-metodia kenttäindeksillä. Se rakentaa Form XObjectin peritystä /DA-merkkijonosta, /Q-tasauksesta, /MaxLen-kampiasettelusta ja arvosta, ratkaisee nimetyn fontin AcroForm-sanakirjan /DR-resurssien kautta niin että Type0-fontti pitää oman jälkeläisfonttinsa eikä rappeudu Helveticaksi, ja palauttaa True, kun vähintään yksi widget sai streamin. SetFormFieldValue-metodin nimipohjainen ylikuormitus ei palauta sinulle indeksiä, joten hanki sellainen GetFormField-kutsulla, joka palauttaa THPDFLoadedFormField-olion, jonka omistat ja joka sinun on vapautettava. Version v2.752.1 muutoksen regressiosarja on tästä jaosta yksiselitteinen: se asettaa arvon, kutsuu EnsureLoadedFieldAppearanceStream-metodia, renderöi sitten sivun ja tarkistaa, että widgetin suorakulmion sisäpuoliset pikselit muuttuivat kun taas sen ulkopuoliset eivät. Sen todistaminen, että /V muuttui, ei todista mitään siitä, mitä käyttäjä tulee näkemään

var
  Field: THPDFLoadedFormField;
begin
  Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
  Field := Pdf.GetFormField('Applicant.FullName');
  try
    // Piirrä uusi arvo /AP-sanakirjaan, jotta sen ohittavat katselimet
    // näyttävät sen silti
    if not Pdf.EnsureLoadedFieldAppearanceStream(Field.Index) then
      raise Exception.Create('No widget rectangle to paint into');
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('claim-form-filled.pdf');
end;

Rajat, jotka kannattaa tietää ennen kuin rakennat tämän päälle

ReconcileLoadedButtonAppearanceStates testaa sen sanakirjan paikallisen /FT-arvon, jonka osoitit, joten se vaikuttaa radio-vanhempaan tai valintaruutuun, joka kantaa omaa /FT-arvoaan; lapsiwidget, jota osoitetaan erikseen ja jolla on /FT vain vanhemmassaan, ei tule sovitetuksi tuon polun kautta. HPDFReconcileChoiceSelection käsittelee yhtä skalaariarvoa ja kirjoittaa enintään yhden indeksin; monivalintaluetteloruudut, joissa on useita valittuja merkintöjä, ovat SetFormFieldValue-mallin ulkopuolella. Kumpikaan rutiini ei validoi välittämääsi arvoa /Opt-arvoa eikä päällä-tilan avaimia vasten, joten kirjoitusvirhe tuottaa Off-valintaruudun tai indeksittömän yhdistelmäruudun poikkeuksen sijaan. Ja GetFormFieldValue palauttaa tallennetun /V-tekstin sellaisena kuin se sanakirjassa istuu, mikä heksakoodatulle arvolle tarkoittaa heksadesimaalimuotoa eikä purettua tekstiä

Kun arvot ovat paikallaan ja esitykset piirretty, kaksi luontevaa seuraavaa askelta sijaitsevat tämän operaation kummallakin puolella. Kenttädatan vaihtaminen ulkoisten järjestelmien kanssa erissä yhden SetFormFieldValue-kutsun kerrallaan sijaan on se, mitä XFDF-tuonti ja -vienti Delphissä kattaa. Ja kun täytetty lomake on valmis eikä sen pitäisi enää olla muokattavissa, AcroForm- ja XFA-kenttien flattenointi Delphissä polttaa täsmälleen tässä kuvatut /AS-tilat ja esitysstreamit staattiseksi sivusisällöksi, minkä takia niiden saaminen johdonmukaisiksi ennen flattenointia ei ole valinnainen asia

Tässä artikkelissa kuvattu ladatun lomakkeen muokkausrajapinta, mukaan lukien SetFormFieldValue, EnsureLoadedFieldAppearanceStream ja inkrementaalinen uudelleenlaskentagraafi, toimitetaan osana HotPDF Delphi Componentia Delphille ja C++Builderille