Tekninen artikkeli

AcroFormin perityt kenttäarvot ja nollaukset Delphissä

HotPDF Delphi Component käsittelee ladatun AcroForm-kentän /FT-, /Ff-, /V- ja /DV-merkinnät perittävinä attribuutteina, jotka ratkaistaan kävelemällä /Parent-ketjua. Versioista v2.754.3 ja v2.754.4 alkaen nimetty lapsi, jonka tyyppi tulee sen vanhemmalta, pysyy yksilöllisesti osoitettavana, RemoveFormField jättää sen sisarukset rauhaan, ja ResetLoadedFormField kopioi perityn oletuksen sen alkuperäisellä PDF-objektityypillä. Sitä ennen yllättävän moni tavallinen lomake luettiin väärin

Lomake, joka paljastaa kaiken tämän, ei ole eksoottinen. Kirjoitustyökalu rakentaa ryhmäsolmun group, joka kantaa kerran /FT /Ch -merkinnän, kenttäliput ja valintaluettelon, ja ripustaa sen alle kaksi nimettyä lasta a ja b, kumpikin yhdistetty kenttä- ja widget-sanakirja, jossa ei ole muuta kuin /T, /Parent, /Rect ja oma /V. Se on täysin laillinen tapa jakaa attribuutteja, ja se on täsmälleen se tapaus, jonka artikkelin lomakekenttien arvojen asettamisesta ladattuun PDF:ään Delphillä Rajat-osa merkitsi käsittelemättömäksi: painikkeiden sovitus katsoi vain paikallista /FT-merkintää. Tämä artikkeli poimii sen, mihin edellinen jäi, ja kattaa, miten kenttäpuu luokitellaan, miten perittyjä arvoja luetaan ja mitä yhden kentän nollaus saa kirjoittaa

Mitkä AcroForm-merkinnät kenttä voi periä vanhemmaltaan?

ISO 32000-1 §12.7.3.1, taulukko 220, merkitsee /FT-, /Ff-, /V- ja /DV-merkinnät perittäviksi, ja taulukko 229 kohdassa §12.7.4.3 tekee saman tekstikentän /MaxLenille, joten mikä tahansa lukija, joka katsoo vain paikallista sanakirjaa, raportoi täysin kelvolliselle lapselle väärän tyypin, väärät liput ja tyhjän arvon. HotPDF ohjaa kaikki nämä lukee yhden sisäisen ratkaisijan, HPDFLoadedInheritedFieldObjectin, läpi, joka tarkistaa sanakirjasta avaimen, ratkaisee epäsuoran viittauksen, jos sellaisen löytää, ja muussa tapauksessa seuraa /Parentia enintään 128 tasoa, koska vialliset tiedostot voivat rakentaa /Parent-silmukoita, joilla ei ole mitään tekemistä /Kidsin kanssa. Julkiset getterit istuvat sen päällä: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue sekä optioapurit GetLoadedFormFieldOptionCount ja GetLoadedFormFieldOptions, jotka poimivat myös vanhemmalle tallennetun /Opt-taulukon. Yksi sääntö ratkaisijassa on helppo saada väärin: kävely pysähtyy ensimmäiseen avaimen sisältävään sanakirjaan, vaikka arvo siellä olisi tyhjä merkkijono. Paikallinen /V () on tahallinen ohitus, joka peittää vanhemman, ei aukko, joka pitäisi täyttää ylempää puusta

HotPDF:n perittyjen AcroForm-attribuuttien kaavio: ryhmäsolmu kantaa /FT-, /Ff- ja /Opt-merkinnät kerran, kun taas nimetyt lapset group.a ja group.b pitävät vain /T-, /Parent-, /Rect-merkinnät ja paikallisen /V:n, ja kaavio näyttää HPDFLoadedInheritedFieldObjectin kävelemässä /Parentia enintään 128 tasoa, jossa ensimmäinen avaimen sisältävä sanakirja voittaa ja tyhjä paikallinen arvo peittää vanhemman
HotPDF ratkaisee /FT-, /Ff-, /V-, /DV- ja /Opt-merkinnät yhden vanhempaa kävelevän ratkaisijan kautta, joten nimetty lapsi pysyy osoitettavana, ja paikallinen tyhjä arvo ohittaa tahallaan kaiken sen yläpuolella olevan ryhmän kantaman
var
  Pdf: THotPDF;
  Field: THPDFLoadedFormField;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
    // 'group' kantaa /FT /Ch -merkinnän, /Ff 131078:n ja /Opt-taulukon; lapsi
    // 'group.b' kantaa vain /T:n, /Parentin, /Rectin ja oman /V-merkintänsä
    Field := Pdf.GetFormField('group.b');
    try
      if Pdf.GetFormFieldType(Field.Index) = lfftChoice then
      begin
        // 131078 = Combo (bitti 18) + NoExport (bitti 3) + Required (bitti 2)
        Writeln(Pdf.GetLoadedFormFieldFlags(Field.Index));
        Writeln(Pdf.IsFormFieldRequired(Field.Index));    // TRUE
        Writeln(Pdf.GetLoadedFormFieldOptionCount(Field.Index));
        Writeln(Pdf.GetFormFieldValue(Field.Index));       // paikallinen /V
      end;
    finally
      Field.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

Miksi paikallinen /FT on väärä testi päätekentälle?

Koska vanhempi voi toimittaa tyypin ja silti omistaa nimettyjä lapsikenttiä, /FT-merkinnän läsnäolo ei kerro mitään siitä, missä kenttäpuu päättyy. Vanha läpikäynti julisti solmun pääteksi aina, kun sillä oli oma /FT tai ei /Kidsia. Yllä olevassa lomakkeessa groupilla on sekä /FT /Ch että /Kids, joten se rekisteröitiin yhdeksi group-nimiseksi kentäksi kahdella widgetillä, ja täysin kvalifioidut nimet group.a ja group.b katosivat yksinkertaisesti. GetFormFieldCount palautti 1:n, haku lapsen nimellä epäonnistui, ja SetFormFieldValue pystyi kirjoittamaan vain jaettua vanhempaa. Korvaava testi, HPDFLoadedFieldHasChildFields, katsoo lapsia vanhemman sijaan: lapsi on lapsikenttä, jos sillä on oma /T, oma /Kids, tai jos se ei ole ylipäätään /Subtype /Widget-sanakirja. Vain kun mikään lapsi ei täytä ehtoja, solmu on pääte, ja sen lapset käsitellään sen widget-merkintähuomautuksina

Kaksi reunatapausta, jotka muovasivat kyseisen säännön, tulevat molemmat yhdistetyistä sanakirjoista, joita §12.7.3.1 sallii, kun kentällä on yksi widget. Nimetty yhdistetty sanakirja kantaa /Subtype /Widgetia ja on silti lapsikenttä, joten alatyyppi yksin ei voi lähettää sitä vanhemman anonyymien widgetien luetteloon; /T voittaa. Myös käänteinen tapahtuu: jotkin tuottajat toistavat vanhemman /FTin jokaisessa anonyymissä widgetissä, joten /FTia ei voi käyttääkään todisteena siitä, että widget aloittaa uuden kentän. Luokittelu on jaettu suhdevälimuistin, FormFieldExistsin ja RemoveFormFieldin kesken, ja jokainen kyseinen kävely kirjaa nyt käymänsä sanakirjat ja pysähtyy 128 tason jälkeen. Regressiotiedosto, jonka ryhmä luettelee itsensä kahdesti, /Kids [5 0 R 5 0 R 6 0 R 7 0 R], raportoi yhä täsmälleen kaksi kenttää sen sijaan että rekursioisi ikuisesti tai laskisi saman solmun kahdesti

Miten RemoveFormField välttää sisarkenttien poistamisen?

RemoveFormField poistaa nyt vain nimeämäsi lapsen, koska löytäminen ja poistaminen ovat vihdoin samaa mieltä siitä, mitä päätekenttä on. Se yhteisymmärrys on tärkeämpi kuin näyttää. Nimen perusteella toimiva ylikuormitus ratkaisee indeksin suhdevälimuistin kautta ja laskee sitten päätekentät toisella kävelyllä /AcroForm /Fieldsin yli. Kun välimuisti oli korjattu näkemään group.an ja group.bn, korjaamaton poistokävely olisi silti käsitellyt groupin yhtenä päätekenttänä, ja indeksi 0 olisi poistanut vanhemman yhdessä jokaisen sisaruksen ja kaikkien niiden widgetien kanssa. Poistokävely käyttää nyt samaa HPDFLoadedFieldHasChildFields-testiä ja samaa käyty-joukkoa, kerää vain poistetun lapsen widget-merkintähuomautukset, irrottaa ne jokaisen sivun /Annotsista ja poistaa vanhemman vain, kun sen /Kids-taulukko päätyy tyhjäksi. Regressio tarkistaa kaikki kolme paikkaa, joissa virhe näkyisi: vanhemman /Kidsin, sivun /Annotsin ja selvinneen sisaruksen arvon ja esityksen, sekä täyden uudelleenkirjoituksen että inkrementaalisen päivityksen jälkeen

HotPDF:n RemoveFormField-sisarusten selviämiskaavio: poistokävely käyttää uudelleen HPDFLoadedFieldHasChildFields-testiä ja löytämisen käyty-joukkoa, irrottaa vain nimetyn lapsen group.a AcroForm /Fieldsistä ja sivun /Annotsista ja pitää jaetun vanhemman, kun sen /Kids-taulukko yhä pitää selvinnyttä group.b:tä
Löytäminen ja poistaminen ovat vihdoin samaa mieltä siitä, mitä päätekenttä on, joten yhden nimetyn lapsen poistaminen jättää sisaruksen arvon ja esityksen ehjäksi täyden uudelleenkirjoituksen tai inkrementaalisen päivityksen jälkeenkin
// Poistetaan yksi nimetty lapsi; sen sisarus ja jaettu vanhempi selviävät
Pdf.RemoveFormField('group.a');

Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// Tyyppi, liput ja optiot ratkaistaan yhä vanhemman kautta
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');

Mitä ResetLoadedFormField kirjoittaa, kun oletus on peritty?

ResetLoadedFormField kirjoittaa paikallisen /Vin, joka on tuore kopio peritystä /DVistä samalla PDF-objektityypillä, ja se validoi koko oletuksen ennen kuin koskee kenttää. Objektityyppi on merkitsevä, koska skalaarigetterit litistävät kaiken tekstiksi. Valintaruudun oletus on nimi kuten /Yes, monivalintaluetteloruudun oletus on merkkijonojen taulukko, ja tekstin oletus voi olla heksadesimaalinen UTF-16-merkkijono; kumpaakin tahansa kopioimalla GetLoadedFormFieldDefaultValuein kautta nimi muuttuisi merkkijonoksi, taulukko tyhjäksi merkkijonoksi ja heksamerkkijono sen kirjaimellisiksi numeroiksi. Nollaus haarautuu siis perityn tyypin mukaan: teksti- ja valintakentät saavat uuden merkkijono-objektin, joka pitää IsHexadecimal-lipun, taulukko-oletuksiset valintakentät saavat uuden taulukon uusia merkkijonoja, ja muut kuin pushbutton-painikkeet saavat uuden nimiobjektin. Kopioiminen, ei osoittaminen vanhemman objekteihin, on tahallista: /V, joka jakoi vanhemman /DV-taulukon tai sen objektinumeron, muuttaisi oletusta seuraavan kerran, kun kuka tahansa muokkasi arvoa. Väärän tyypin oletus tai valintataulukko, joka sisältää mitään muuta kuin merkkijonoja, nostaa poikkeuksen ja jättää /Vin ja /Iin täsmälleen ennalleen. Pushbuttonit, joilla ei ole arvoa (taulukko 226, bitti 17), ja allekirjoituskentät palaavat vanhempaan vain-merkkijonopolkuun

HotPDF:n tyypitetyn nollauksen kaavio: ResetLoadedFormField haarautuu perityn /DV-objektityypin mukaan kirjoittaen tuoreen nimiobjektin valintaruudulle, uuden taulukon uusia merkkijonoja monivalintavalinnalle, merkkijonon, joka pitää IsHexadecimal-lipun heksatekstille, tyhjän merkkijonon tai /Off-arvon kun /DV:tä ei ole, ja nostaa poikkeuksen koskematta /V:hen tai /I:hen tyypin osuessa väärin
Kopioiminen vanhemman objekteihin osoittamisen sijaan estää myöhemmän arvomuokkauksen muuttamasta oletusta hiljaa, ja pushbuttonit sekä allekirjoituskentät palaavat vanhempaan vain-merkkijonopolkuun

Kun /DVia ei ole missään ketjua ylöspäin, metodi pitää tyhjentämislupauksensa kirjoittamalla paikallisen tyhjän merkkijonon tai /Offin valintaruutu- tai radiokentälle. Paikallisen /Vin poistaminen näyttäisi siistimmältä ja olisi väärin: vanhempi voi kantaa nykyistä arvoa, ja lapsen ohituksen poistaminen palauttaisi tuon arvon hiljaa. Tämä on myös syy, miksi yhden kentän nollaus ei ole §12.7.5.3:n ResetForm-toiminto, jonka katselin ajaa kenttäjoukon yli, kun käyttäjä napsauttaa painiketta, niin kuin kerrotaan artikkelissa AcroForm-kenttien ja -toimintojen rakentamisesta HotPDF:llä. ResetLoadedFormField on muokkausoperaatio yhdellä ladatulla kentällä, omalla säännöllään olettoman tapauksen varalta, ja se kirjaa kentän NoteLoadedFormFieldDirtyilla, jotta inkrementaalinen uudelleenlaskenta näkee muutoksen

var
  Field: THPDFLoadedFormField;
begin
  Field := Pdf.GetFormField('group.a');
  try
    // Vanhempi kantaa /DV [(b) (r)] -merkintää MultiSelect-luetteloruudussa: group.a
    // saa oman /V [(b) (r)] -merkintänsä ja tuoreen /I [0 2]:n; vanhempi pysyy ehjänä
    Pdf.ResetLoadedFormField(Field.Index);
    // Skalaarigetterit eivät voi esittää taulukko-oletusta
    Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // tyhjä
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('survey-reset.pdf');
end;

/V:n, /I:n ja /AS:n pitäminen yhdenmukaisina

Nollaus on oikein vain, jos valintaindeksi ja esitystila seuraavat arvoa, joten ResetLoadedFormField päättää samoihin kahteen sovittajaan kuin SetFormFieldValue. HPDFReconcileChoiceSelection hyväksyy nyt taulukkoarvon: se poistaa paikallisen /I-merkinnän mutatoimatta sitä, täsmää jokaisen arvon jokaisen /Opt-merkinnän vientipuoliskoon ja kirjoittaa yhden uuden lajitellun /I-taulukon, joten nollaus arvoon [(b) (r)] vaihtoehtoja b, g, r vasten tuottaa /I [0 2] -taulukon. ReconcileLoadedButtonAppearanceStates kysyy nyt perittyä tyyppiä, joten lapsen valintaruutu, jonka /FT /Btn asuu vanhemmalla, saa vihdoin /AS-arvonsa asetettua. Kirjoituspuolella SetFormFieldValue ja SetLoadedFormFieldDefaultValue tallentavat nimiobjektin peritylle ei-pushbutton-painikkeelle silloinkin, kun lapsella ei ole paikallista merkintää tyypin kopioimiseksi. Ja kun EnsureLoadedFieldAppearanceStream rakentaa painikkeiden esitykset uudelleen, se kirjoittaa /AS /Off-arvon, ellei arvo täsmää päällä-tilaan, ja antaa jokaiselle tilastreamille oikean /Type /XObject-, /Subtype /Form- ja /BBox-merkinnän; ennen v2.754.4:ää esityksen uudelleenluonti nollauksen jälkeen saattoi rastittaa ruudun uudelleen ennen tiedoston tallennusta

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

Skalaarigetterit pysyvät skalaareina. GetFormFieldValue ja GetLoadedFormFieldDefaultValue palauttavat tyhjän merkkijonon taulukkoarvolle, merkkijonoistuttavat luvut ja totuusarvot muotoon 42 tai true ja raportoivat heksakoodatun merkkijonon sen heksadesimaalisena kirjoitusasuna. /Parent-silmukka päättää kävelyn ilman poikkeusta, joten kenttä, jonka tyyppi katoaa silmukkaan, raportoi lfftUnknownin ja lipuiksi 0 sen sijaan että epäonnistuisi. SetFormFieldValue ja ResetLoadedFormField kirjoittavat aina lapsen, jonka osoitat, eivätkä koskaan ylennä arvoa jaettuun vanhempaan, mikä on oikein itsenäisille lapsille mutta tarkoittaa, että radioryhmät kannattaa osoittaa sen kentän kautta, joka omistaa valinnan. Ja jokainen kutsu käsittelee yhden kentän itsenäisesti; mikään tässä ei tee nollauserästä transaktiota

Tässä kuvattu perittyjen attribuuttien ratkaisu, yhtenäistetty kenttäpuun luokittelu ja tyypitetty nollaus kuuluvat ladatun lomakkeen API:in HotPDF Delphi Componentissa Delphille ja C++Builderille, yhdessä artikkelissa AcroForm-kenttien lisäämisestä ladattuun PDF:ään Delphissä käsitellyn kenttien luomisen kanssa