Valintaruudut ja radiopainikkeet litistyvät valitsemattomina, koska ulkoasutila /AS ei koskaan synkronoitunut kentän arvon /V kanssa. PDFium Component, PDFium-pohjainen VCL- ja LCL-komponentti Delphille, C++Builderille ja Lazarukselle, lukee nyt tuon arvon FPDFAnnot_GetFormFieldValue:lla, joka ratkaisee ylemmän kenttäsanakirjan widget-huomautuksen sijaan
Bugiraportti, joka johti tähän, on sellainen, jota epäilee ensin. Asiakas litistää allekirjoitetun suostumuslomakkeen, avaa tuloksen, ja jokainen valintaruutu on tyhjä. Avaa lähdetiedosto Acrobatissa, ja ruudut on näkyvästi rastitettu. Lue lähdetiedosto takaisin saman komponentin kautta, ja kentän arvot ovat oikein. Vain litistetty tuloste menettää ne, ja vain valintaruuduille ja radiopainikkeille: samalla sivulla olevat tekstikentät tulevat ulos kunnossa
Miksi valintaruudut ovat valitsemattomia litistyksen jälkeen?
Koska litistys ei koskaan katso /V:tä. FPDFPage_Flatten paistaa widgetin ulkoasuvirran sivun sisältöön, ja ulkoasu, jonka se valitsee, on se, jonka /AS nimeää. Jos /AS sanoo yhä /Off samalla kun kentän arvo sanoo, että ruutu on päällä, litistys paistaa uskollisesti pois-ulkoasun. Arvoa ei koskaan menetetty; sitä ei koskaan konsultoitu
ISO 32000-1 §12.5.5 määrittelee ulkoasusanakirjan /AP kolmella mahdollisella merkinnällä, /N, /R ja /D. Valintaruudulle tai radiopainikkeelle /N-merkintä ei ole virta vaan alisanakirja, jonka avaimet ovat ulkoasutilan nimiä, ja §12.5.2 tekee /AS:sta pakollisen valitsijan, kun /N on alisanakirja. Valintaruutu siis kantaa kahta valmiiksi rakennettua ulkoasua ja yhtä osoitinta. Saa osoitin väärin, ja renderöinti on väärin tavalla, jota mikään määrä oikeaa /V:tä ei korjaa. Tämä on myös syy, miksi epäonnistumistila eroaa tekstikentistä, joilla ei ole lainkaan valmiiksi rakennettua ulkoasua valittavaksi: tekstikentän /N on yksittäinen virta, joka täytyy regeneroida alusta arvon muuttumisen jälkeen, joten GenerateFormAppearances käsittelee nämä kaksi tapausta täysin erillisten koodipolkujen kautta, ja vain painikepolku oli rikki
Missä valintaruudun arvo oikeasti elää?
Kenttäsanakirjassa, ei widgetissä. ISO 32000-1 §12.7.5.2 kuvaa valintaruudut ja radiopainikkeet painikekentiksi, joiden /V on nimiobjekti, joka nimeää nykyisen ulkoasutilan, ja §12.7.3.1 sijoittaa /V:n kaikille kenttäsanakirjoille yhteisten merkintöjen joukkoon. §12.5.6.19:ssa määritelty widget-huomautus tuottaa /AS:n ja /AP:n. Mikään spesifikaatiossa ei velvoita widgetiä kantamaan /V:tä
// Wrong: reads the widget annotation dictionary directly
buflen := FPDFAnnot_GetStringValue(Annot, 'V', nil, 0);
// For most real forms buflen comes back as 2 (an empty UTF-16 string),
// so /AS is never written and the box flattens as Off
{ What the two objects look like when the field has several widgets:
12 0 obj % field dictionary (the parent)
<< /FT /Btn /T (Consent) /V /On
/Kids [ 13 0 R 14 0 R ] >>
endobj
13 0 obj % widget annotation (a kid)
<< /Type /Annot /Subtype /Widget /Parent 12 0 R
/AS /Off
/AP << /N << /On 20 0 R /Off 21 0 R >> >> >>
endobj }
FPDFAnnot_GetStringValue ei ole viallinen. Sen sopimus on täsmälleen se, mitä sen nimi sanoo: hae merkkijonomerkintä huomautussanakirjasta, jonka sille annoit. Sen pyytäminen /V:tä objektille 13 palauttaa tyhjän, koska objektilla 13 aidosti ei ole /V:tä. Vika oli kutsujassa, joka oletti litteän objektimallin, jota ISO 32000-1 ei koskaan luvannut
Milloin kenttä ja widget jakavat yhden sanakirjan?
Aina kun kentällä on täsmälleen yksi widget. §12.5.6.19 sallii kenttäsanakirjan ja sen yksittäisen widget-huomautuksen yhdistämisen yhdeksi objektiksi, ja useimmat luontityökalut käyttävät tuota oikotietä. Yhdistetyssä objektissa /FT, /T, /V, /AS ja /AP kaikki istuvat vierekkäin, joten widget-tason luku /V:stä onnistuu, ja koko bugi pysyy näkymättömänä
Hetkellä, jolloin kenttä omistaa kaksi tai useampia widgetejä, yhdistäminen on mahdotonta, ja §12.7.3.1 vaatii widgetit muuttumaan erillisen kenttäsanakirjan /Kids-arvoiksi. Jokainen radioryhmä on tässä muodossa rakenteellisesti. Niin ovat suostumusvalintaruudut, jotka toistuvat otsikossa ja alatunnisteessa, ja mikä tahansa kenttä, jonka luontityökalu on kopioinut toiselle sivulle. Se on koko selitys sille, miksi vika selvisi regressiosarjasta: testikorpus oli täynnä yksittäisen widgetin lomakkeita, eivätkä asiakastiedostot olleet. Jos kuljet widgettejä läpi itse sen sijaan, että luottaisit komponenttiin, sama epäsymmetria näkyy luettelointijärjestyksessä, ja muistio PDF-lomakekentän navigoinnista PDFium Componentilla kattaa, miten sivutason huomautusten läpikäynti liittyy dokumenttitason kenttäpuuhun
Arvon lukeminen tavalla, jota PDFium tarkoittaa
FPDFAnnot_GetFormFieldValue on oikea API, ja se oli sidottu komponenttiin jonkin aikaa ilman, että valintaruutupolku käytti sitä. Se ottaa lomakekahvan huomautuksen lisäksi, mikä on merkki, joka on tärkeä: kun lomaketäyttöympäristö on saatavilla, PDFium ratkaisee huomautuksen sen lomakeohjaimeen ja lukee arvon kenttäobjektista, joten se palauttaa oikean vastauksen sekä yhdistetyille että jaetuille asetteluille
FPDF_FORMFIELD_CHECKBOX, FPDF_FORMFIELD_RADIOBUTTON:
begin
// /AP is prebuilt per state; only /AS has to be synchronised with /V.
// FPDFAnnot_GetFormFieldValue resolves the parent field dictionary,
// which is where ISO 32000-1 12.7.5.2 keeps the value.
buflen := FPDFAnnot_GetFormFieldValue(FFormHandle, Annot, nil, 0);
if buflen >= 4 then
begin
SetLength(OrigVal, buflen div 2 - 1);
FPDFAnnot_GetFormFieldValue(FFormHandle, Annot, PWideChar(OrigVal), buflen);
FPDFAnnot_SetStringValue(Annot, 'AS', Pointer(OrigVal));
end;
end;
Kaksi yksityiskohtaa tuossa katkelmassa on helppo saada väärin. Palautettu pituus on tavumäärä UTF-16-tekstille mukaan lukien päätin, joten merkkimäärä on buflen div 2 - 1, ja arvo 2 tarkoittaa tyhjää merkkijonoa. Vartija buflen >= 4 tarkoittaa siis vähintään yhtä todellista merkkiä, mikä on se, mikä estää kentän, jolla ei ole lainkaan /V:tä, saamasta /AS:aansa ylikirjoitettua tyhjällä nimellä
Mistä /AS ja /AP /N oikeasti sopivat keskenään?
Ne sopivat nimestä, ja nimen valitsee se, joka tuotti tiedoston. §12.7.5.2 vaatii, että pois-tilaa kutsutaan nimellä /Off, ja jättää päällä-tilan kokonaan tuottajan päätettäväksi. /Yes on käytäntö, ei sääntö. Acrobat kirjoittaa /Yes:n, mutta monet generaattorit kirjoittavat /On:n, /1:n, /Choice1:n tai lokalisoidun sanan, ja radioryhmä yleensä antaa jokaiselle lapselle erillisen päällä-tila-nimen, jotta ryhmä voi ilmaista, mikä painike on valittu. Tämä on juuri syy, miksi /V:n kopioiminen sanasta sanaan /AS:ään on oikea toimenpide eikä nikkarointi: valitulle ohjaimelle PDFium raportoi päällä-tilan nimen, jonka itse tiedosto määrittelee, ja valitsemattomalle se raportoi Off:n, joten arvo, jonka kirjoitat /AS:ään, on taatusti avain, joka on olemassa tuon widgetin /AP /N -alisanakirjassa. /Yes:n kovakoodaus toimisi Acrobat-tulosteella ja rikkoutuisi hiljaa kaikkialla muualla
Toimintojen järjestys, ja missä sitä yhä tarvitaan huolellisuutta
Sekvenssi on kiinteä ja anteeksiantamaton: ota käyttöön lomaketäyttö, aseta arvot, regeneroi ulkoasut, litistä, sitten tallenna. Ohita regenerointivaihe, ja FPDFPage_Flatten löytää tyhjiä tai vanhentuneita ulkoasuvirtoja ja paistaa ne ilman valitusta, mikä on hiljainen datan menetys eikä virhepaluu
Pdf.FileName := FormPath;
Pdf.FormFill := True; // required: FormHandle must exist
Pdf.Active := True;
Pdf.FormField[0] := 'On'; // writes /V only
Pdf.GenerateFormAppearances; // syncs /AS for buttons, rebuilds /AP for text
if Pdf.FlattenAllPages(FLAT_PRINT) then
Pdf.SaveAs('consent-flat.pdf');
Kaksi rehellistä rajaa jää jäljelle. Ensinnäkin, synkronointi kirjoittaa kentän arvon jokaisen tuon kentän widgetin /AS:ään, mikä on oikein valintaruuduille mutta likimääräistä radioryhmille, joiden jokainen lapsi määrittelee oman päällä-tila-nimensä; lapsi, jonka /AP /N:ssä ei ole merkintää, joka täsmää kirjoitettuun /AS:ään, ei omista ulkoasua valittavaksi §12.5.5:n alla, joten valitsematon painike voi litistyä ei-miksikään tyhjän ympyrän sijaan. Radioryhmän auditointi FPDFAnnot_GetFormControlIndex:lla ennen litistystä on muutaman rivin arvoinen. Toiseksi, mikään tästä ei koske XFA:ta, jossa arvo elää XML-datapaketissa AcroForm-sanakirjojen sijaan, erottelu käsitelty muistiossa XFA-kenttämuokkauksista, joita ei tallenneta pysyvästi. Yleinen oppi kannattaa pitää mielessä tämän yhden korjauksen ohella: aina kun API ottaa lomakekahvan huomautuksen lisäksi, se kertoo sinulle, että se ratkaisee kenttähierarkian puolestasi, ja aina kun se ottaa vain huomautuksen, se lukee täsmälleen sen objektin, jonka annoit. Tuo ero hallitsee myös datan vaihtoa, koska XFDF-lomaketiedon vienti ja tuonti toimii täysin pätevillä kenttänimillä, ei koskaan widget-sijainneilla
Lomakkeen litistys on yksi niistä ominaisuuksista, joka näyttää yksittäiseltä API-kutsulta ja osoittautuu sopimukseksi kolmen sanakirjan välillä. Jos haluaisit mieluummin työskennellä komponentin kanssa, joka jo koodaa tuon sopimuksen, PDFium Component Delphille ja C++Builderille toimittaa tässä kuvatun ulkoasun regeneroinnin, litistyksen ja lomakekentän käytön tavallisina ominaisuuksina ja metodeina