Tehnički članak

AcroForm naslijeđene vrijednosti polja i reseti u Delphiju

HotPDF Delphi Component tretira /FT, /Ff, /V i /DV na učitanom AcroForm polju kao naslijedive atribute, razriješene obilaskom lanca /Parent. Od v2.754.3 i v2.754.4, imenovano dijete čiji tip dolazi od roditelja ostaje pojedinačno adresabilno, RemoveFormField ostavlja brata-sestru na miru, a ResetLoadedFormField kopira naslijeđenu zadanu vrijednost s njezinim izvornim PDF tipom objekta. Prije toga, iznenađujuće mnogo običnih obrazaca čitalo se pogrešno

Obrazac koji sve ovo izbacuje na vidjelo nije egzotičan. Authoring alat sagradi group čvor group koji jednom nosi /FT /Ch, flagove polja i popis opcija, i objesi pod njega dva imenovana djeteta a i b, svako spojeni field-plus-widget rječnik s ničim osim /T, /Parent, /Rect i vlastitim /V. To je posve legalan način dijeljenja atributa, i točno je to slučaj koji je Limits sekcija članka o postavljanju vrijednosti polja obrasca u učitanom PDF-u u Delphiju označila kao neriješeno: usklađivanje gumba gledalo je samo lokalni /FT. Ovaj članak nastavlja tamo gdje je on stao, pokrivajući kako se field tree klasificira, kako se čitaju naslijeđene vrijednosti i što reset pojedinačnog polja smije zapisati

Koje AcroForm unose polje može naslijediti od roditelja?

ISO 32000-1 §12.7.3.1, Tablica 220, označava /FT, /Ff, /V i /DV kao naslijedive, a Tablica 229 u §12.7.4.3 isto čini za /MaxLen tekstualnog polja, pa će svaki čitač koji gleda samo lokalni rječnik javiti krivi tip, krive flagove i praznu vrijednost za posve valjano dijete. HotPDF sva ova čitanja provlači kroz jedan interni resolver, HPDFLoadedInheritedFieldObject, koji u rječniku provjerava ključ, razriješi indirektnu referencu ako je nađe, a inače slijedi /Parent najviše 128 razina, jer pokvarene datoteke mogu sagraditi /Parent cikluse koji s /Kids nemaju ništa. Javni getteri sjede na njemu: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue i option helperi GetLoadedFormFieldOptionCount i GetLoadedFormFieldOptions, koji pokupi i /Opt polje spremljeno na roditelju. Jedno pravilo u resolveru lako je pokvariti: obilazak staje na prvom rječniku koji sadrži ključ, i kad je vrijednost tamo prazan string. Lokalni /V () namjerna je zamjena koja prikriva roditelja, a ne praznina koju treba popuniti s viših razina treea

Dijagram naslijeđenih AcroForm atributa u HotPDF-u: group čvor jednom nosi /FT, /Ff i /Opt dok imenovana djeca group.a i group.b drže samo /T, /Parent, /Rect i lokalni /V, prikazujući HPDFLoadedInheritedFieldObject kako obilazi /Parent do 128 razina gdje prvi rječnik s ključem pobjeđuje, a prazna lokalna vrijednost prikriva roditelja
HotPDF razrješava /FT, /Ff, /V, /DV i /Opt kroz jedan resolver koji hoda po roditeljima, pa imenovano dijete ostaje adresabilno dok prazna lokalna vrijednost namjerno nadjačava sve što grupa 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; dijete
    // 'group.b' nosi samo /T, /Parent, /Rect i vlastito /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 krivi test za terminalno polje?

Zato što roditelj može dovoditi tip a i dalje posjedovati imenovana dijete-polja, pa prisutnost /FT ne govori ništa o tome gdje se field tree završava. Stari obilazak proglašavao je čvor terminalnim čim ima vlastiti /FT ili nema /Kids. U gore opisanom obrascu group ima i /FT /Ch i /Kids, pa je registriran kao jedno polje imena group s dva widgeta, a potpuna imena group.a i group.b jednostavno su nestala. GetFormFieldCount vratio je 1, upit po imenu djeteta pao, a SetFormFieldValue mogao je pisati samo u zajedničkog roditelja. Zamjenski test, HPDFLoadedFieldHasChildFields, gleda djecu umjesto roditelja: kid je dijete-polje ako ima vlastito /T, ima vlastite /Kids ili uopće nije rječnik /Subtype /Widget. Tek kad nijedan kid ne kvalificira, čvor je terminalan, a njegova se djeca tretiraju kao njegove widget anotacije

Dva rubna slučaja koja su oblikovala to pravilo oba dolaze iz spojenih rječnika, koje §12.7.3.1 dopušta kad polje ima jedan widget. Imenovani spojeni rječnik nosi /Subtype /Widget i i dalje je dijete-polje, pa sam subtype ne može poslati u anonimni widget popis roditelja; /T pobjeđuje. Dogodi se i obratno: neki producenti ponavljaju roditeljev /FT na svakom anonimnom widgetu, pa /FT ne može služiti kao dokaz da widget počinje novo polje. Klasifikaciju dijele cache odnosa, FormFieldExists i RemoveFormField, i svaki od tih obilazaka sada bilježi rječnike koje je već posjetio i staje nakon 128 razina. Regresijska datoteka čija se group dva puta nabraja samu sebe, /Kids [5 0 R 5 0 R 6 0 R 7 0 R], i dalje javlja točno dva polja umjesto da se vrti u krug ili broji isti čvor dvaput

Kako RemoveFormField izbjegava brisanje sestrinskih polja?

RemoveFormField sada briše samo dijete koje imenujete, jer se otkrivanje i brisanje napokon slažu oko toga što je terminalno polje. To se slaganje važnije je nego što izgleda. Overload po imenu razriješi indeks kroz cache odnosa, pa u drugom obilasku nad /AcroForm /Fields broji terminalna polja. Kad se cache jednom popravio da vidi group.a i group.b, nepopravljen bi obilazak brisanja i dalje tretirao group kao jedno terminalno polje, i indeks 0 bi uklonio roditelja zajedno sa svakim bratom-sestrom i svim njihovim widgetima. Obilazak brisanja sada koristi isti test HPDFLoadedFieldHasChildFields i isti skup posjećenih, skuplja widget anotacije samo uklonjenog djeteta, skine ih s /Annots svake stranice, a roditelja uklanja samo kad se njegovo /Kids polje isprazni. Regresija provjerava sva tri mjesta gdje bi se greška pokazala: roditeljeve /Kids, stranične /Annots te vrijednost i izgled preživjelog brata-sestre, i nakon potpunog prepisivanja i nakon inkrementalnog updatea

Dijagram preživljavanja brata-sestre u HotPDF RemoveFormField: obilazak brisanja ponovno koristi HPDFLoadedFieldHasChildFields i skup posjećenih iz otkrivanja, skida samo imenovano dijete group.a iz AcroForm /Fields i straničnih /Annots, i zadržava zajedničkog roditelja dok njegovo /Kids polje još drži preživjeli group.b
Otkrivanje i brisanje napokon se slažu što je terminalno polje, pa uklanjanje jednog imenovanog djeteta ostavlja vrijednost i izgled njegova brata-sestre netaknutima i nakon potpunog prepisivanja i nakon inkrementalnog updatea
// Ukloni jedno imenovano dijete; njegov brat-sestra i zajednički roditelj prežive
Pdf.RemoveFormField('group.a');

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

Što ResetLoadedFormField zapiše kad je zadana vrijednost naslijeđena?

ResetLoadedFormField zapisuje lokalni /V koji je svježa kopija naslijeđenog /DV istog PDF tipa objekta, i validira cijelu zadanu vrijednost prije nego dira polje. Tip objekta važan je jer skalarski getteri sve izravnavaju u tekst. Zadana vrijednost checkboxa ime je poput /Yes, zadana vrijednost multi-select list boxa polje je stringova, a tekstualna zadana vrijednost može biti heksadecimalni UTF-16 string; kopiranje bilo kojeg kroz GetLoadedFormFieldDefaultValue pretvorilo bi ime u string, polje u prazan string, a hex string u njegove doslovne znamenke. Reset se zato grana po naslijeđenom tipu: text i choice polja dobivaju novi string objekt koji čuva flag IsHexadecimal, choice polja s poljnom zadanom vrijednošću dobivaju novo polje novih stringova, a nepushbutton gumbi novi name objekt. Kopiranje, umjesto upućivanja na roditeljeve objekte, namjerno je: /V koji bi dijelio roditeljevo /DV polje ili njegov broj objekta promijenio bi zadanu vrijednost sljedeći put kad itko uredi vrijednost. Zadana vrijednost krivog tipa, ili choice polje koje sadrži išta osim stringova, baca iznimku i ostavlja /V i /I točno kakvi su bili. Pushbuttoni, koji nemaju vrijednost (Tablica 226, bit 17), i signature polja vraćaju se na stariji put samo sa stringovima

Dijagram tipiziranog reseta u HotPDF-u: ResetLoadedFormField grana se po tipu naslijeđenog /DV objekta, pišući svježi name objekt za checkbox, novo polje novih stringova za multi-select choice, string koji čuva IsHexadecimal za hex tekst, prazan string ili /Off kad /DV ne postoji, i baca iznimku bez diranja /V ili /I pri nepodudaranju tipa
Kopiranje umjesto upućivanja na roditeljeve objekte sprječava da kasnija izmjena vrijednosti tiho promijeni zadanu vrijednost, a pushbuttoni i signature polja vraćaju se na stariji put samo sa stringovima

Kad nigdje uz lanac ne postoji /DV, metoda zadržava svoj ugovor čišćenja zapisujući lokalni prazan string, ili /Off za checkbox ili radio polje. Brisanje lokalnog /V izgledalo bi urednije i bilo bi pogrešno: roditelj može držati trenutačnu vrijednost, i uklanjanje djetetove zamjene tiho bi tu vrijednost vratilo. Zato i reset pojedinačnog polja nije ResetForm akcija iz §12.7.5.3, koju preglednik izvodi nad skupom polja kad korisnik klikne gumb, kako je opisano u članku o izradi AcroForm polja i akcija pomoću HotPDF-a. ResetLoadedFormField je operacija uređivanja nad jednim učitanim poljem, sa svojim pravilom za slučaj bez zadane vrijednosti, i bilježi polje kroz NoteLoadedFormFieldDirty da inkrementalni preračun vidi promjenu

var
  Field: THPDFLoadedFormField;
begin
  Field := Pdf.GetFormField('group.a');
  try
    // Roditelj drži /DV [(b) (r)] na MultiSelect list boxu: group.a dobiva
    // vlastito /V [(b) (r)] i svježi /I [0 2]; roditelj je netaknut
    Pdf.ResetLoadedFormField(Field.Index);
    // Skalarski getteri ne mogu prikazati poljnu zadanu vrijednost
    Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // prazno
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('survey-reset.pdf');
end;

Da /V, /I i /AS ostanu usklađeni

Reset je ispravan samo ako indeks selekcije i stanje izgleda prate vrijednost, pa ResetLoadedFormField završava s ista dva usklađivača kao SetFormFieldValue. HPDFReconcileChoiceSelection sada prima poljnu vrijednost: briše lokalni /I bez mutiranja, svaku vrijednost poklopi s export polovicom svakog /Opt unosa, i zapiše jedno novo sortirano /I, pa reset na [(b) (r)] uz opcije b, g, r daje /I [0 2]. ReconcileLoadedButtonAppearanceStates sada traži naslijeđeni tip, pa dijete-checkbox čiji se /FT /Btn nalazi na roditelju napokon dobiva postavljen /AS. Na strani pisanja, SetFormFieldValue i SetLoadedFormFieldDefaultValue spremaju name objekt za naslijeđeni nepushbutton gumb čak i kad dijete nema lokalni unos s kojeg bi kopiralo tip. A kad EnsureLoadedFieldAppearanceStream ponovno gradi izglede gumba, zapisuje /AS /Off osim ako vrijednost odgovara uključenom stanju, i svakom state streamu daje ispravne /Type /XObject, /Subtype /Form i /BBox; prije v2.754.4, regeneracija izgleda nakon reseta mogla je ponovno staviti kvačicu prije nego se datoteka spremi

Ograničenja vrijedna poznavanja prije nego na tome gradite

Skalarski getteri ostaju skalarski. GetFormFieldValue i GetLoadedFormFieldDefaultValue vraćaju prazan string za poljnu vrijednost, brojeve i booleane pretvaraju u tekst kao 42 odnosno true, a hex-kodirani string javljaju u njegovu heksadecimalnom zapisu. /Parent ciklus završava obilazak bez iznimke, pa polje čiji se tip izgubi u ciklusu javlja lfftUnknown i flagove 0 umjesto da padne. SetFormFieldValue i ResetLoadedFormField uvijek pišu dijete koje adresirate i nikad ne promiču vrijednost zajedničkom roditelju, što je pravo za nezavisnu djecu, ali znači da radio grupe treba adresirati kroz polje koje posjeduje selekciju. I svaki poziv sam za sebe potvrđuje jedno polje; ništa ovdje ne čini seriju reseta transakcijom

Razrješavanje naslijeđenih atributa, ujedinjena klasifikacija field treea i tipizirani reset opisani ovdje dio su loaded-form API-ja u HotPDF Delphi Component za Delphi i C++Builder, uz izradu polja pokrivenu u članku o dodavanju AcroForm polja u učitani PDF u Delphiju