HotPDF Delphi Component berie /FT, /Ff, /V a /DV na načítanom poli AcroForm ako dedičné atribúty, ktoré sa vyriešia prechodom reťazca /Parent. Od v2.754.3 a v2.754.4 pomenované dieťa, ktorého typ pochádza od rodiča, ostáva osobitne adresovateľné, RemoveFormField necháva súrodencom na pokoji a ResetLoadedFormField kopíruje zdedenú predvolenú hodnotu s jej pôvodným typom PDF objektu. Predtým sa prekvapujúco veľa úplne obyčajných formulárov čítalo zle
Formulár, ktorý to všetko vystaví, nie je nijaké exotikum. Autorský nástroj postaví skupinový uzol group, ktorý nesie /FT /Ch, flagy poľa a zoznam volieb raz, a zavesí pod neho dve pomenované deti a a b, každé ako zlúčený slovník pole plus widget bez ničoho iného než /T, /Parent, /Rect a vlastné /V. To je úplne legálny spôsob zdieľania atribútov a presne tento prípad označila sekcia Limits článku o nastavovaní hodnôt polí formulára v načítanom PDF v Delphi ako neobslúžený: zmierenie tlačidiel pozeralo len na lokálne /FT. Tento článok nadväzuje tam, kde ten skončil, a pokrýva, ako sa klasifikuje strom polí, ako sa čítajú zdedené hodnoty a čo smie reset jedného poľa zapísať
Ktoré položky AcroForm môže pole zdediť od rodiča?
ISO 32000-1 §12.7.3.1, Table 220, označuje /FT, /Ff, /V a /DV za dedičné a Table 229 v §12.7.4.3 robí to isté pre /MaxLen textového poľa, takže každý čítač pozerajúci len na lokálny slovník nahlási zlý typ, zlé flagy a prázdnu hodnotu pri úplne platnom dieťati. HotPDF smeruje všetky tieto čítania cez jeden interný resolver HPDFLoadedInheritedFieldObject, ktorý skontroluje kľúč v slovníku, vyrieši nepriamu referenciu, ak nejakú nájde, a inak nasleduje /Parent najviac 128 úrovní, lebo deformované súbory môžu stavať cykly /Parent, ktoré s /Kids nemajú nič spoločné. Verejné gettery stoja na ňom: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue a option helpery GetLoadedFormFieldOptionCount a GetLoadedFormFieldOptions, ktoré tiež zachytia pole /Opt uložené na rodičovi. Jedno pravidlo v resolveri sa dá pochopiť zle: prechod zastane na prvom slovníku obsahujúcom kľúč, aj keď je tam hodnota prázdny reťazec. Lokálne /V () je zámerné prepísanie, ktoré maskuje rodiča, nie medzera, ktorú treba doplniť vyššie v strome
var
Pdf: THotPDF;
Field: THPDFLoadedFormField;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
// 'group' nesie /FT /Ch, /Ff 131078 a /Opt; dieťa
// 'group.b' nesie len /T, /Parent, /Rect a vlastné /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)); // lokálne /V
end;
finally
Field.Free;
end;
finally
Pdf.Free;
end;
end;
Prečo je lokálne /FT zlý test na koncové pole?
Pretože rodič môže dodávať typ a pritom vlastniť pomenované dcérske polia, takže prítomnosť /FT nič nepovie o tom, kde strom polí končí. Starý prechod vyhlásil uzol za koncový vždy, keď mal vlastné /FT alebo žiadne /Kids. Vo formulári vyššie má group oboje, /FT /Ch aj /Kids, takže sa zaregistroval ako jedno pole menom group s dvomi widgetmi a úplné kvalifikované mená group.a a group.b prosto zmizli. GetFormFieldCount vrátilo 1, vyhľadávanie podľa mena dieťaťa zlyhalo a SetFormFieldValue mohlo písať len do zdieľaného rodiča. Nahrádzajúci test HPDFLoadedFieldHasChildFields pozerá na deti namiesto rodiča: dieťa je dcérske pole, ak má vlastné /T, vlastné /Kids, alebo vôbec nie je slovník /Subtype /Widget. Len keď žiadne dieťa nevyhovuje, je uzol koncový a jeho deti sa berú ako jeho widget anotácie
Dva okrajové prípady, ktoré dotvorili to pravidlo, obidva vychádzajú zo zlúčených slovníkov, ktoré §12.7.3.1 dovoľuje, keď má pole jediný widget. Pomenovaný zlúčený slovník nesie /Subtype /Widget a stále je dcérskym poľom, takže samotný subtype ho nemôže poslať do anonymného zoznamu widgetov rodiča; vyhráva /T. Nastáva aj opak: niektorí producenti opakujú /FT rodiča na každom anonymnom widgete, takže /FT nemôže slúžiť ako dôkaz, že widget začína nové pole. Klasifikáciu zdieľa relačná cache, FormFieldExists aj RemoveFormField a každý z tých prechodov si teraz zaznamenáva slovníky, ktoré už navštívil, a zastane za 128 úrovňami. Regresný súbor, ktorého skupina listuje samu seba dvakrát, /Kids [5 0 R 5 0 R 6 0 R 7 0 R], stále hlási presne dve polia namiesto večnej rekurzie alebo dvojitého počítania toho istého uzla
Ako RemoveFormField zabráni zmazaniu súrodencov?
RemoveFormField teraz maže len dieťa, ktoré pomenujete, lebo objavenie a mazanie sa konečne zhodujú v tom, čo je koncové pole. Tá zhoda má väčší význam, než vyzerá. Overload podľa mena vyrieši index cez relačnú cache a potom počíta koncové polia v druhom prechode cez /AcroForm /Fields. Keď sa raz cache opravila tak, aby videla group.a a group.b, neopravený prechod mazania by stále bral group ako jediné koncové pole a index 0 by odstránil rodiča spolu s každým súrodencom a všetkými ich widgetmi. Prechod mazania teraz používa rovnaký test HPDFLoadedFieldHasChildFields a rovnakú navštívenú množinu, posbiera widget anotácie len odstraňovaného dieťaťa, vyberie ich z /Annots každej strany a rodiča odstráni, len keď pole /Kids skončí prázdne. Regresia kontroluje všetky tri miesta, kde by sa chyba ukázala: /Kids rodiča, /Annots strany a hodnotu a vzhľad preživšieho súrodencu, tak po úplnom prepise, ako po inkrementálnej aktualizácii
// Odstráňte jedno pomenované dieťa; súrodenec a zdieľaný rodič prežijú
Pdf.RemoveFormField('group.a');
Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// Typ, flagy a voľby sa stále vyriešia cez rodiča
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');
Čo ResetLoadedFormField zapíše, keď je predvolená zdedená?
ResetLoadedFormField zapíše lokálne /V, ktoré je čerstvou kópiou zdedeného /DV s tým istým typom PDF objektu, a pred dotykom na pole validuje celú predvolenú hodnotu. Typ objektu má význam, lebo skalárne gettery splášia všetko na text. Predvolená hodnota checkboxu je name ako /Yes, predvolená hodnota multi-select list boxu je pole reťazcov a textová predvolená môže byť hexadecimálny reťazec UTF-16; skopírovanie ktoréhokoľvek z nich cez GetLoadedFormFieldDefaultValue by premenilo name na reťazec, pole na prázdny reťazec a hex reťazec na jeho doslovné číslice. Reset sa preto vetví podľa zdedeného typu: textové a choice polia dostanú nový objekt reťazca, ktorý drží flag IsHexadecimal, choice polia s poľovou predvolenou dostanú nové pole nových reťazcov a netlačidlové tlačidlá dostanú nový name objekt. Kopírovanie, nie ukazovanie na objekty rodiča, je zámerné: /V, ktoré by zdieľalo pole /DV rodiča alebo jeho číslo objektu, by zmenilo predvolenú hodnotu, keby ktokoľvek nabudúce upravil hodnotu. Predvolená zlého typu alebo choice pole obsahujúce čokoľvek iné než reťazce vyhodí výnimku a nechá /V aj /I presne ako boli. Pushbuttony, ktoré nemajú hodnotu (Table 226, bit 17), a signature polia spadnú späť na staršiu cestu len pre reťazce
Keď žiadne /DV neexistuje nikde vyššie v reťazci, metóda dodrží svoj čistiaci kontrakt tým, že zapíše lokálny prázdny reťazec, alebo /Off pri checkboxe či radio poli. Zmazanie lokálneho /V by vyzeralo úhľadnejšie a bolo by zlé: rodič môže držať aktuálnu hodnotu a odobratie prepísania dieťaťa by ju potichu priviedlo späť. Práve preto reset jedného poľa nie je akcia ResetForm z §12.7.5.3, ktorú prehliadač spustí cez sadu polí, keď používateľ klikne na tlačidlo, ako popisuje článok o stavbe polí a akcií AcroForm s HotPDF. ResetLoadedFormField je editačná operácia na jednom načítanom poli, s vlastným pravidlom pre prípad bez predvolenej hodnoty, a pole zaznamená cez NoteLoadedFormFieldDirty, aby inkrementálne prepočítanie zmenu zazrelo
var
Field: THPDFLoadedFormField;
begin
Field := Pdf.GetFormField('group.a');
try
// Rodič drží /DV [(b) (r)] na MultiSelect list boxe: group.a dostane
// vlastné /V [(b) (r)] a čerstvé /I [0 2]; rodič zostane nedotknutý
Pdf.ResetLoadedFormField(Field.Index);
// Skalárne gettery nedokážu zobraziť poľovú predvolenú
Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // prázdne
finally
Field.Free;
end;
Pdf.SaveLoadedDocument('survey-reset.pdf');
end;
Udržanie /V, /I a /AS v súlade
Reset je správny len vtedy, keď index výberu a stav vzhľadu nasledujú hodnotu, takže ResetLoadedFormField končí tými istými dvomi zmierovačmi ako SetFormFieldValue. HPDFReconcileChoiceSelection teraz prijíma poľovú hodnotu: zmaže lokálne /I bez jej mutovania, priradí každú hodnotu proti exportnej polovici každej položky /Opt a zapíše jedno nové triedené /I, takže reset na [(b) (r)] proti voľbám b, g, r dá /I [0 2]. ReconcileLoadedButtonAppearanceStates teraz žiada zdedený typ, takže dcérsky checkbox, ktorého /FT /Btn býva na rodičovi, konečne dostane nastavené /AS. Na zapisovacej strane SetFormFieldValue aj SetLoadedFormFieldDefaultValue uložia name objekt pre zdedené netlačidlové tlačidlo, aj keď dieťa nemá lokálnu položku, z ktorej by typ skopírovalo. A keď EnsureLoadedFieldAppearanceStream znovu stavia vzhľady tlaidiel, zapíše /AS /Off, pokiaľ hodnota nesedí so zapnutým stavom, a dá každému streamu stavu riadne /Type /XObject, /Subtype /Form a /BBox; pred v2.754.4 mohla regenerácia vzhľadu po resete začiarknuť políčko znovu, skôr než sa súbor uložil
Limity, ktoré stoja za poznanie, skôr než na tom postavíte
Skalárne gettery ostávajú skalárne. GetFormFieldValue a GetLoadedFormFieldDefaultValue vrátia prázdny reťazec pre poľovú hodnotu, premenia čísla a booleany na 42 alebo true a nahlásia hex-kódovaný reťazec v jeho hexadecimálnom zapísaní. Cyklus /Parent ukončí prechod bez výnimky, takže pole, ktorého typ sa stráca v cykle, hlási lfftUnknown a flagy 0 namiesto zlyhania. SetFormFieldValue a ResetLoadedFormField vždy zapisujú dieťa, ktoré adresujete, a nikdy nepovyšujú hodnotu do zdieľaného rodiča, čo je správne pre nezávislé deti, ale znamená to, že rádiové skupiny treba adresovať cez pole, ktoré vlastní výber. A každé volanie commitne jedno pole svojsky; nič z tohto nerobí zo dávky resetov transakciu
Riešenie dedičných atribútov, jednotná klasifikácia stromu polí a typovaný reset opísané tu sú súčasťou loaded-form API v HotPDF Delphi Component pre Delphi a C++Builder, vedľa tvorby polí pokrytej v článku o pridávaní polí AcroForm do načítaného PDF v Delphi