Techninis straipsnis

AcroForm paveldimos laukų reikšmės ir atstatymai Delphi

HotPDF Delphi Component /FT, /Ff, /V ir /DV įkeltame AcroForm lauke traktuoja kaip paveldimus atributus, išsprendžiamus apeinant /Parent grandinę. Nuo v2.754.3 ir v2.754.4 pavadintas vaikas, kurio tipas ateina iš tėvo, lieka atskirai pasiekiamas, RemoveFormField palieka jo brolius ramybėje, o ResetLoadedFormField nukopijuoja paveldėtą numatytąją reikšmę su originaliu PDF objekto tipu. Iki tol stebėtinai daug paprastų formų buvo skaitomos neteisingai

Forma, atverianti visa tai, nėra egzotiška. Autorinis įrankis sukonstruoja grupės mazgą group, kuris vienąkart neša /FT /Ch, lauko vėliavėles ir parinkčių sąrašą, ir po juo pakabina du pavadintus vaikus a ir b, kiekvieną – sulietą lauko ir valdiklio žodyną, neturintį nieko, tik /T, /Parent, /Rect ir savą /V. Tai visiškai teisėtas būdas dalintis atributais, ir būtent tą atvejį formos laukų reikšmių nustatymas įkeltame PDF su Delphi straipsnio Limits skyrius pažymėjo kaip neapdorojamą: mygtukų suderinimas žiūrėjo tik į vietinį /FT. Šis straipsnis pasiima ten, kur tas sustojo, ir aprėpia, kaip klasifikuojamas laukų medis, kaip skaitomos paveldėtos reikšmės ir ką vieno lauko atstatymui leidžiama įrašyti

Kuriuos AcroForm įrašus laukas gali paveldėti iš tėvo?

ISO 32000-1 §12.7.3.1, 220 lentelė, pažymi /FT, /Ff, /V ir /DV kaip paveldimus, o 229 lentelė §12.7.4.3 tą patį daro tekstinio lauko /MaxLen atžvilgiu, tad bet kuris skaitytuvas, žiūrintis tik į vietinį žodyną, visiškai teisėtam vaikui praneš neteisingą tipą, neteisingas vėliavėles ir tuščią reikšmę. HotPDF visus šiuos skaitymus supila pro vieną vidinį išsprendėją HPDFLoadedInheritedFieldObject, kuris žodyne patikrina raktą, išsprendžia netiesioginę nuorodą, jei ją randa, ir kitu atveju seka /Parent ne daugiau kaip 128 lygius, nes netinkamai suformuoti failai gali sukurti /Parent ciklus, su /Kids neturinčius nieko bendra. Viešieji getteriai sėdi ant jo viršaus: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue ir parinkčių pagalbinės GetLoadedFormFieldOptionCount bei GetLoadedFormFieldOptions, kurios taip pat pasiima tėve saugomą /Opt masyvą. Viena išsprendėjo taisyklė lengvai suklaidina: apeiga sustoja ties pirmuoju žodynu, turinčiu raktą, net jei ten esanti reikšmė yra tuščia eilutė. Vietinis /V () yra sąmoningas perrašymas, uždengiantis tėvą, o ne tuštuma, kurią reikia užpildyti iš aukštesnių medžio pakopų

HotPDF paveldimų AcroForm atributų diagrama: grupės mazgas vienąkart neša /FT, /Ff ir /Opt, o pavadinti vaikai group.a ir group.b laiko tik /T, /Parent, /Rect ir vietinį /V; matyti, kaip HPDFLoadedInheritedFieldObject apeina /Parent iki 128 lygių, kur pirmasis raktą turintis žodynas laimi, o tuščia vietinė reikšmė uždengia tėvą
HotPDF /FT, /Ff, /V, /DV ir /Opt išsprendžia pro vieną tėvo grandine einantį išsprendėją, tad pavadintas vaikas lieka pasiekiamas, o vietinė tuščia reikšmė sąmoningai perrašo viską, ką neša virš jo esanti grupė
var
  Pdf: THotPDF;
  Field: THPDFLoadedFormField;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
    // „group“ neša /FT /Ch, /Ff 131078 ir /Opt; vaikas
    // „group.b“ laiko tik /T, /Parent, /Rect ir savą /V
    Field := Pdf.GetFormField('group.b');
    try
      if Pdf.GetFormFieldType(Field.Index) = lfftChoice then
      begin
        // 131078 = Combo (bitas 18) + NoExport (bitas 3) + Required (bitas 2)
        Writeln(Pdf.GetLoadedFormFieldFlags(Field.Index));
        Writeln(Pdf.IsFormFieldRequired(Field.Index));    // TRUE
        Writeln(Pdf.GetLoadedFormFieldOptionCount(Field.Index));
        Writeln(Pdf.GetFormFieldValue(Field.Index));       // vietinis /V
      end;
    finally
      Field.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

Kodėl vietinis /FT yra netinkamas terminalinio lauko testas?

Nes tėvas gali duoti tipą ir vis tiek turėti pavadintus vaiko laukus, tad /FT buvimas nesako nieko apie tai, kur baigiasi laukų medis. Senoji apeiga mazgą skelbdavo terminaliniu, kai tik jis turėdavo savą /FT arba neturėdavo /Kids. Aukščiau esančioje formoje group turi ir /FT /Ch, ir /Kids, tad jis buvo užregistruojamas kaip vienas laukas group su dviem valdikliais, o pilnieji vardai group.a ir group.b tiesiog dingdavo. GetFormFieldCount grąžindavo 1, paieška pagal vaiko vardą žlugdavo, o SetFormFieldValue galėdavo rašyti tik į bendrąjį tėvą. Pakaitinis testas HPDFLoadedFieldHasChildFields žiūri į vaikus, o ne į tėvą: vaikas yra vaiko laukas, jei turi savą /T, turi savą /Kids arba apskritai nėra /Subtype /Widget žodynas. Tik kai nė vienas vaikas neatitinka, mazgas terminalinis, o jo vaikai traktuojami kaip jo valdiklio anotacijos

Abu kraštiniai atvejai, suformavę tą taisyklę, ateina iš sulietų žodynų, kuriuos §12.7.3.1 leidžia, kai laukas turi vieną valdiklį. Pavadintas sulietas žodynas neša /Subtype /Widget ir vis tiek yra vaiko laukas, tad vien subtype negali jo nusiųsti į tėvo anonominių valdiklių sąrašą; laimi /T. Atvirkščiai irgi nutinka: kai kurie generatoriai tėvo /FT pakartoja ant kiekvieno anonominio valdiklio, tad /FT negali būti įrodymu, kad valdiklis pradeda naują lauką. Klasifikaciją dalijasi santykių podėlis, FormFieldExists ir RemoveFormField, ir kiekviena tų apeigų dabar užsirašo jau aplankytus žodynus ir sustoja praėjusi 128 lygius. Regresinis failas, kurio grupė save išvardija dukart, /Kids [5 0 R 5 0 R 6 0 R 7 0 R], vis tiek praneša lygiai du laukus, vietoj to, kad rekursuotų amžinai arba skaičiuotų tą patį mazgą dukart

Kaip RemoveFormField išvengia brolio laukų ištrynimo?

RemoveFormField dabar ištrina tik tą vaiką, kurį pavadinate, nes atradimas ir trynimas galiausiai sutaria, kas yra terminalinis laukas. Ta sutartis svarbiau, nei atrodo. Overloadas pagal vardą išsprendžia indeksą pro santykių podėlį ir tada antra apeiga per /AcroForm /Fields suskaičiuoja terminalinius laukus. Kai podėlis buvo pataisytas matyti group.a ir group.b, nepataisyta trynimo apeiga vis tiek būtų group laikiusi vienu terminaliniu lauku, ir indeksas 0 būtų pašalinęs tėvą kartu su kiekvienu broliu ir visais jų valdikliais. Trynimo apeiga dabar naudoja tą patį HPDFLoadedFieldHasChildFields testą ir tą patį aplankytų rinkinį, surenka tik pašalinto vaiko valdiklio anotacijas, jas išplėšia iš kiekvieno puslapio /Annots, o tėvą šalina tik tada, kai jo /Kids masyvas lieka tuščias. Regresija tikrina visas tris vietas, kur klaida išryškėtų: tėvo /Kids, puslapio /Annots ir išgyvenusio brolio reikšmę bei išvaizdą – ir po pilno perrašymo, ir po inkrementinio atnaujinimo

HotPDF RemoveFormField brolio išlikimo diagrama: trynimo apeiga pakartotinai naudoja HPDFLoadedFieldHasChildFields ir aplankytų rinkinį iš atradimo, iš AcroForm /Fields ir puslapio /Annots išplėšia tik pavadintą vaiką group.a, o bendrąjį tėvą palieka, kol jo /Kids masyvas vis dar laiko išgyvenusį group.b
Atradimas ir trynimas galiausiai sutaria, kas yra terminalinis laukas, tad vieno pavadinto vaiko pašalinimas palieka brolio reikšmę ir išvaizdą nepaliestus ir po pilno perrašymo, ir po inkrementinio atnaujinimo
// Pašalinkite vieną pavadintą vaiką; jo brolis ir bendrasis tėvas išgyvena
Pdf.RemoveFormField('group.a');

Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// Tipas, vėliavėlės ir parinktys vis dar išsprendžiamos pro tėvą
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');

Ką ResetLoadedFormField įrašo, kai numatytoji reikšmė paveldėta?

ResetLoadedFormField įrašo vietinę /V, kuri yra šviežia paveldėtos /DV kopija su tuo pačiu PDF objekto tipu, ir patvirtina visą numatytąją reikšmę, dar neliesdamas lauko. Objekto tipas svarbus, nes skaliariniai getteriai viską suploja į tekstą. Žymimojo langelio numatytoji reikšmė yra vardas, toks kaip /Yes, kelių pasirinkimų sąrašo dėžutės numatytoji reikšmė yra eilučių masyvas, o tekstinė numatytoji reikšmė gali būti šešioliktainė UTF-16 eilutė; bet kurios jų kopijavimas pro GetLoadedFormFieldDefaultValue pavestų vardą eilute, masyvą – tuščia eilute, o šešioliktainę eilutę – jos pačios skaitmenimis. Todėl atstatymas šakojasi pagal paveldėtą tipą: tekstiniai ir choice laukai gauna naują eilutės objektą, išlaikantį IsHexadecimal vėliavėlę, choice laukai su masyvine numatytąja reikšme gauna naują naujų eilučių masyvą, o nepushbutton mygtukai gauna naują vardo objektą. Kopijavimas, o ne rodyklė į tėvo objektus, sąmoningas: /V, dalijanti tėvo /DV masyvą arba jo objekto numerį, pakeistų numatytąją reikšmę kitąkart, kas nors redaguojant reikšmę. Netinkamo tipo numatytoji reikšmė arba choice masyvas, kuriame yra kas nors kitą nei eilutės, kelia išimtį ir palieka /V bei /I lygiai tokias, kokios buvo. Pushbutton, kurie neturi reikšmės (226 lentelė, 17 bitas), ir parašo laukai grįžta į senąjį kelią, skirtą tik eilutėms

HotPDF tipizuoto atstatymo diagrama: ResetLoadedFormField šakoja pagal paveldėto /DV objekto tipą, rašydamas šviežią vardo objektą žymimajam langeliui, naują naujų eilučių masyvą kelių pasirinkimų choice, eilutę, išlaikančią IsHexadecimal, šešioliktainiam tekstui, tuščią eilutę arba /Off, kai /DV nėra, ir keldamas išimtį neliesdamas /V ar /I esant tipo nesutapimui
Kopijavimas, o ne rodyklės į tėvo objektus, neleidžia vėlesniam reikšmės redagavimui tyliai pakeisti numatytosios, o pushbutton ir parašo laukai grįžta į senąjį kelią, skirtą tik eilutėms

Kai /DV niekur grandinėje nėra, metodas išlaiko savo išvalymo sutartį įrašydamas vietinę tuščią eilutę arba /Off žymimajam langeliui arba radijo laukui. Vietinės /V ištrynimas atrodytų tvarkingiau ir būtų neteisingas: tėvas gali laikyti dabartinę reikšmę, ir vaiko perrašymo pašalinimas tyliai sugrąžintų tą reikšmę. Todėl vieno lauko atstatymas nėra §12.7.5.3 ResetForm veiksmas, kurį peržiūros programa paleidžia per laukų rinkinį, vartotojui paspaudus mygtuką, kaip aprašyta AcroForm laukų ir veiksmų kūrime su HotPDF. ResetLoadedFormField yra redagavimo operacija ant vieno įkelto lauko, su savo taisykle be-numatytosios-reikšmės atvejui, ir jis užregistruoja lauką per NoteLoadedFormFieldDirty, kad inkrementinis perskaičiavimas pamatytų pokytį

var
  Field: THPDFLoadedFormField;
begin
  Field := Pdf.GetFormField('group.a');
  try
    // Tėvas laiko /DV [(b) (r)] ant MultiSelect sąrašo dėžutės: group.a gauna
    // savą /V [(b) (r)] ir šviežią /I [0 2]; tėvas neliečiamas
    Pdf.ResetLoadedFormField(Field.Index);
    // Skaliariniai getteriai negali atvaizduoti masyvinės numatytosios
    Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // tuščia
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('survey-reset.pdf');
end;

Kaip /V, /I ir /AS laikomi suderinti?

Atstatymas teisingas tik tada, kai pasirinkimo indeksas ir išvaizdos būsena seka reikšmę, todėl ResetLoadedFormField baigiasi tais pačiais dviem suderinintojais kaip SetFormFieldValue. HPDFReconcileChoiceSelection dabar priima masyvinę reikšmę: jis ištrina vietinę /I jos nekeisdamas, kiekvieną reikšmę sugretina su kiekvieno /Opt įrašo eksportine puse ir rašo vieną naują surūšiuotą /I, tad atstatymas į [(b) (r)] prieš parinktis b, g, r duoda /I [0 2]. ReconcileLoadedButtonAppearanceStates dabar prašo paveldėto tipo, tad vaiko žymimasis langelis, kurio /FT /Btn gyvena tėve, galiausiai gauna savą /AS. Rašymo pusėje SetFormFieldValue ir SetLoadedFormFieldDefaultValue saugo vardo objektą paveldėtam nepushbutton mygtukui net tada, kai vaikas neturi vietinio įrašo, iš kurio nukopijuoti tipą. O kai EnsureLoadedFieldAppearanceStream perstatinėja mygtukų išvaizdas, jis rašo /AS /Off, nebent reikšmė sutampa su įjungta būsena, ir kiekvienam būsenos srautui duoda tvarkingą /Type /XObject, /Subtype /Form ir /BBox; iki v2.754.4 išvaizdos atkūrimas po atstatymo galėjo vėl pažymėti langelį dar prieš failą įrašant

Ribos, vertos žinoti, prieš statant ant to

Skaliariniai getteriai lieka skaliariniai. GetFormFieldValue ir GetLoadedFormFieldDefaultValue masyvinei reikšmei grąžina tuščią eilutę, skaičius ir boolean suveda į 42 ar true, o šešioliktaine koduotą eilutę praneša jos šešioliktaine rašyba. /Parent ciklas baigia apeigą be išimties, tad laukas, kurio tipas pamestas cikle, praneša lfftUnknown ir nulines vėliavėles, o ne žlunga. SetFormFieldValue ir ResetLoadedFormField visada rašo tą vaiką, kurį adresuojate, ir niekada nepakelia reikšmės į bendrąjį tėvą – tai teisinga nepriklausomiems vaikams, bet reiškia, kad radio grupes reikia adresuoti pro lauką, valdantį pasirinkimą. Ir kiekvienas iškvietimas patvirtina po vieną lauką savarankiškai; niekas čia neatveria atstatymų paketo kaip transakcijos

Čia aprašytas paveldimų atributų išsprendimas, vieninga laukų medžio klasifikacija ir tipizuotas atstatymas yra įkeltų formų API dalis HotPDF Delphi Component Delphi ir C++Builder aplinkose, greta AcroForm laukų pridėjimo į įkeltą PDF Delphi aprašyto laukų kūrimo