Tehnički članak

AcroForm nasleđene vrednosti polja i reseti u Delphi-ju

HotPDF Delphi Component tretira /FT, /Ff, /V i /DV na učitanom AcroForm polju kao nasledive atribute, koje razrešava hodom kroz /Parent lanac. Od v2.754.3 i v2.754.4, imenovano dete čiji tip dolazi od roditelja ostaje pojedinačno adresabilno, RemoveFormField ostavlja njegovu braću na miru, a ResetLoadedFormField kopira nasleđeni default sa originalnim PDF tipom objekta. Pre toga, iznenađujuće mnogo običnih formulara je pogrešno čitano

Formular koji sve ovo izvlači na videlo nije egzotičan. Alat za izradu formulara napravi group čvor koji jednom nosi /FT /Ch, field flagove i listu opcija, i okači ispod njega dva imenovana deteta a i b, svako spojen field-plus-widget rečnik sa ničim osim /T, /Parent, /Rect i sopstvenim /V. To je sasvim legalan način deljenja atributa, i to je tačno slučaj koji je odeljak Limits u tekstu postavljanja vrednosti polja formulara u učitanom PDF-u uz Delphi označio kao neriješen: usaglašavanje dugmadi gledalo je samo na lokalni /FT. Ovaj članak nastavlja tamo gde je onaj stao, pokrivajući kako se klasifikuje drvo polja, kako se čitaju nasleđene vrednosti i šta reset jednog polja sme da upiše

Koji AcroForm unosi polje može da nasledi od roditelja?

ISO 32000-1 §12.7.3.1, Tabela 220, označava /FT, /Ff, /V i /DV kao nasledive, a Tabela 229 u §12.7.4.3 isto radi za /MaxLen tekstualnog polja, pa će svaki čitač koji gleda samo u lokalni rečnik prijaviti pogrešan tip, pogrešne flagove i praznu vrednost za sasvim validno dete. HotPDF sva ova čitanja sprovodi kroz jedan interni resolver, HPDFLoadedInheritedFieldObject, koji proverava rečnik za ključ, razrešava indirektnu referencu ako je nađe, a inače prati /Parent najviše 128 nivoa, jer neispravni fajlovi mogu da naprave /Parent cikluse koji nemaju veze sa /Kids. Javni getteri sede na njemu: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue i option helperi GetLoadedFormFieldOptionCount i GetLoadedFormFieldOptions, koji hvataju i /Opt niz sačuvan na roditelju. Jedno pravilo u resolveru je lako pogrešiti: obilazak staje na prvom rečniku koji sadrži ključ, čak i ako je vrednost tamo prazan string. Lokalni /V () je namerna zamena koja zaklanja roditelja, a ne rupa koju treba popuniti odozgo

Dijagram nasleđenih AcroForm atributa u HotPDF-u: group čvor jednom nosi /FT, /Ff i /Opt dok imenovana deca group.a i group.b drže samo /T, /Parent, /Rect i lokalni /V, prikazujući HPDFLoadedInheritedFieldObject kako hoda /Parent do 128 nivoa gde prvi rečnik sa ključem pobeđuje a prazna lokalna vrednost zaklanja roditelja
HotPDF razrešava /FT, /Ff, /V, /DV i /Opt kroz jedan resolver koji hoda roditeljima, pa imenovano dete ostaje adresabilno dok lokalna prazna vrednost namerno nadjačava sve što group iznad nje nosi
var
  Pdf: THotPDF;
  Field: THPDFLoadedFormField;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
    // 'group' nosi /FT /Ch, /Ff 131078 i /Opt; dete
    // 'group.b' nosi samo /T, /Parent, /Rect i svoj /V
    Field := Pdf.GetFormField('group.b');
    try
      if Pdf.GetFormFieldType(Field.Index) = lfftChoice then
      begin
        // 131078 = Combo (bit 18) + NoExport (bit 3) + Required (bit 2)
        Writeln(Pdf.GetLoadedFormFieldFlags(Field.Index));
        Writeln(Pdf.IsFormFieldRequired(Field.Index));    // TRUE
        Writeln(Pdf.GetLoadedFormFieldOptionCount(Field.Index));
        Writeln(Pdf.GetFormFieldValue(Field.Index));       // lokalni /V
      end;
    finally
      Field.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

Zašto je lokalni /FT pogrešan test za završno polje?

Zato što roditelj može da snabdeva tip, a i dalje poseduje svoja imenovana child polja, pa prisustvo /FT ne kaže ništa o tome gde se drvo polja završava. Stari obilazak je proglasio čvor završnim svaki put kad je imao sopstveni /FT ili nije imao /Kids. U formularu gore, group ima i /FT /Ch i /Kids, pa je registrovan kao jedno polje po imenu group sa dva widget-a, a potpuno kvalifikovana imena group.a i group.b su jednostavno nestala. GetFormFieldCount je vratio 1, pretraga po imenu deteta je pala, a SetFormFieldValue je mogao da upiše samo deljenog roditelja. Zamenski test, HPDFLoadedFieldHasChildFields, gleda decu umesto roditelja: dete je child polje ako ima sopstveni /T, ima sopstveni /Kids, ili uopšte nije /Subtype /Widget rečnik. Tek kad nijedno dete ne ispunjava uslov, čvor je završan, sa svojom decom tretiranom kao njegove widget anotacije

Dva granična slučaja koja su oblikovala to pravilo oba dolaze iz spojenih rečnika, koje §12.7.3.1 dozvoljava kad polje ima jedan widget. Imenovan spojen rečnik nosi /Subtype /Widget i i dalje je child polje, pa ga sam podtip ne može poslati u anonimni widget spisak roditelja; /T pobeđuje. Obrnuti slučaj se takođe dešava: neki proizvođači ponavljaju roditeljev /FT na svakom anonimnom widget-u, pa /FT ne može da služi ni kao dokaz da widget započinje novo polje. Klasifikaciju dele cache odnosa, FormFieldExists i RemoveFormField, i svaki od tih obilazaka sada beleži rečnike koje je već posetio i staje posle 128 nivoa. Regresioni fajl čiji se group navodi dva puta, /Kids [5 0 R 5 0 R 6 0 R 7 0 R], i dalje prijavljuje tačno dva polja umesto da se rekurzira zauvek ili da broji isti čvor dvaput

Kako RemoveFormField izbegava brisanje braće polja?

RemoveFormField sada briše samo dete koje imenujete, jer otkrivanje i brisanje konačno slažu oko toga šta je završno polje. To slaganje je važnije nego što izgleda. Overload po imenu razrešava indeks kroz cache odnosa, pa broji završna polja u drugom obilasku /AcroForm /Fields-a. Kada je cache popravljen da vidi group.a i group.b, nepopravljen obilazak brisanja i dalje bi tretirao group kao jedno završno polje, i indeks 0 bi uklonio roditelja zajedno sa svakim bratom i svim njihovim widget-ima. Obilazak brisanja sada koristi isti HPDFLoadedFieldHasChildFields test i isti skup posećenih, skuplja widget anotacije samo uklonjenog deteta, skida ih sa /Annots svake stranice, i roditelja uklanja tek kad se njegov /Kids niz isprazni. Regresija proverava sva tri mesta gde bi se greška pokazala: roditeljeve /Kids, stranične /Annots, i vrednost i izgled preživele braće, i posle potpunog prepisa i posle inkrementalnog ažuriranja

Dijagram preživljavanja braće u HotPDF RemoveFormField: obilazak brisanja koristi HPDFLoadedFieldHasChildFields i skup posećenih iz otkrivanja, skida samo imenovano dete group.a iz AcroForm /Fields i straničnih /Annots, i zadržava deljenog roditelja dok njegov /Kids niz još drži preživelo group.b
Otkrivanje i brisanje konačno se slažu oko toga šta je završno polje, pa uklanjanje jednog imenovanog deteta ostavlja vrednost i izgled njegovog brata netaknutim posle potpunog prepisa ili inkrementalnog ažuriranja
// Uklonite jedno imenovano dete; njegov brat i deljeni roditelj prežive
Pdf.RemoveFormField('group.a');

Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// Tip, flagovi i opcije se i dalje razrešavaju kroz roditelja
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');

Šta ResetLoadedFormField upisuje kad je default nasleđen?

ResetLoadedFormField upisuje lokalni /V koji je sveža kopija nasleđenog /DV sa istim PDF tipom objekta, i validira ceo default pre nego što dira polje. Tip objekta je bitan jer skalarni getteri sve sploščuju u tekst. Default checkbox-a je name poput /Yes, default multi-select list box-a je niz stringova, a tekstualni default može biti heksadecimalni UTF-16 string; kopiranje bilo kog od njih kroz GetLoadedFormFieldDefaultValue pretvorilo bi name u string, niz u prazan string i heks string u njegove literalne cifre. Reset se zato grana po nasleđenom tipu: tekstualna i choice polja dobijaju novi string objekat koji čuva IsHexadecimal flag, choice polja sa nizom kao default dobijaju novi niz novih stringova, a nepushbutton dugmad dobijaju novi name objekat. Kopiranje, umesto usmeravanja na objekte roditelja, je namerno: /V koji bi delio roditeljev /DV niz ili njegov broj objekta menjao bi default sledeći put kad bilo ko izmeni vrednost. Default pogrešnog tipa, ili choice niz koji sadrži išta osim stringova, podiže izuzetak i ostavlja /V i /I tačno onakvima kakvi su bili. Pushbutton-i, koji nemaju vrednost (Tabela 226, bit 17), i signature polja vraćaju se na stariju putanju samo sa stringovima

Dijagram tipiziranog reseta u HotPDF-u: ResetLoadedFormField se grana po nasleđenom /DV tipu objekta, upisujući svež name objekat za checkbox, novi niz novih stringova za multi-select choice, string koji čuva IsHexadecimal za heks tekst, prazan string ili /Off kad /DV ne postoji, i podiže izuzetak bez diranja /V ili /I pri nepoklapanju tipa
Kopiranje umesto usmeravanja na objekte roditelja sprečava da kasnija izmena vrednosti tiho promeni default, a pushbutton-i i signature polja vraćaju se na stariju putanju samo sa stringovima

Kada /DV ne postoji nigde uz lanac, metoda zadržava svoj ugovor čišćenja upisom lokalnog praznog stringa, ili /Off za checkbox ili radio polje. Brisanje lokalnog /V bi izgledalo urednije i bilo bi pogrešno: roditelj može držati tekuću vrednost, i skidanje detetove zamene bi tu vrednost tiho vratilo. Zato reset jednog polja nije ni ResetForm akcija iz §12.7.5.3, koju pregledač pokreće nad skupom polja kad korisnik klikne dugme, kao što je opisano u tekstu izrade AcroForm polja i akcija sa HotPDF-om. ResetLoadedFormField je operacija uređivanja na jednom učitanom polju, sa sopstvenim pravilom za slučaj bez defaulta, i beleži polje kroz NoteLoadedFormFieldDirty da inkrementalno preračunavanje vidi promenu

var
  Field: THPDFLoadedFormField;
begin
  Field := Pdf.GetFormField('group.a');
  try
    // Roditelj drži /DV [(b) (r)] na MultiSelect list boxu: group.a dobija
    // svoj /V [(b) (r)] i svež /I [0 2]; roditelj je netaknut
    Pdf.ResetLoadedFormField(Field.Index);
    // Skalarni getteri ne mogu da predstave niz kao default
    Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // prazno
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('survey-reset.pdf');
end;

Držanje /V, /I i /AS u slozi

Reset je ispravan tek ako indeks izbora i stanje izgleda prate vrednost, pa ResetLoadedFormField završava sa ista dva usaglašivača kao SetFormFieldValue. HPDFReconcileChoiceSelection sada prihvata niz kao vrednost: briše lokalni /I bez mutiranja, poklapa svaku vrednost sa izvoznom polovinom svakog /Opt unosa, i upisuje jedan novi sortiran /I, pa reset na [(b) (r)] naspram opcija b, g, r daje /I [0 2]. ReconcileLoadedButtonAppearanceStates sada traži nasleđeni tip, pa child checkbox čiji se /FT /Btn nalazi na roditelju konačno dobija svoje /AS. Na strani upisa, SetFormFieldValue i SetLoadedFormFieldDefaultValue čuvaju name objekat za nasleđeno nepushbutton dugme čak i kad dete nema lokalni unos sa koga bi kopiralo tip. A kada EnsureLoadedFieldAppearanceStream ponovo gradi izglede dugmadi, upisuje /AS /Off osim ako vrednost poklapa sa uključenim stanjem, i daje svakom state stream-u pravi /Type /XObject, /Subtype /Form i /BBox; pre v2.754.4, obnavljanje izgleda posle reseta moglo je ponovo štiklirati kutijicu pre nego što se fajl sačuva

Ograničenja koja vredi znati pre nego što gradite na ovome

Skalarni getteri ostaju skalarni. GetFormFieldValue i GetLoadedFormFieldDefaultValue vraćaju prazan string za niz kao vrednost, pretvaraju brojeve i boolean-e u 42 ili true, a heks-kodiran string prijavljuju u njegovom heksadecimalnom pisanju. /Parent ciklus završava obilazak bez izuzetka, pa polje čiji se tip izgubi u ciklusu prijavljuje lfftUnknown i flagove 0 umesto da padne. SetFormFieldValue i ResetLoadedFormField uvek upisuju dete koje adresirate i nikada ne promovišu vrednost na deljenog roditelja, što je ispravno za nezavisna deca ali znači da radio grupe treba adresirati kroz polje koje poseduje izbor. I svaki poziv sam za sebe završava jedno polje; ništa ovde ne čini seriju resetova transakcijom

Razrešavanje nasleđenih atributa, objedinjena klasifikacija drveta polja i tipizirani reset opisani ovde deo su API-ja za učitane formulare u HotPDF Delphi Component-u za Delphi i C++Builder, uz izradu polja pokrivenu u tekstu dodavanja AcroForm polja u učitani PDF u Delphi-ju