HotPDF Delphi Component bere /FT, /Ff, /V a /DV na načteném AcroForm poli jako dědičné atributy, řešené projetím řetězce /Parent. Od v2.754.3 a v2.754.4 pojmenované dítě, jehož typ pochází od rodiče, zůstává individuálně adresovatelné, RemoveFormField nechá jeho sourozence na pokoji a ResetLoadedFormField zkopíruje zděděný default s původním PDF typem objektu. Předtím se překvapivé množství obyčejných formulářů četlo špatně
Formulář, který tohle všechno vystaví, není exotický. Autorský nástroj postaví skupinový uzel group, který ponese /FT /Ch, field flags a seznam voleb jednou, a pověsí pod něj dvě pojmenovaná děti a a b, každé jako sloučený slovník pole plus widget s ničím jiným než /T, /Parent, /Rect a vlastním /V. To je úplně legální způsob sdílení atributů a je to přesně ten případ, který sekce Limits článku o nastavování hodnot formulářových polí v načteném PDF v Delphi označila za neobsloužené: reconciliation tlačítek se dívala jen na lokální /FT. Tenhle článek navazuje tam, kde ten předchozí skončil, a pokrývá, jak se klasifikuje strom polí, jak se čtou zděděné hodnoty a co smí reset jednoho pole zapsat
Které AcroForm položky může pole zdědit od rodiče?
ISO 32000-1 §12.7.3.1, Table 220, značí /FT, /Ff, /V a /DV jako dědičné a Table 229 v §12.7.4.3 dělá totéž pro /MaxLen textového pole, takže každá čtečka dívající se jen do lokálního slovníku ohlásí špatný typ, špatné příznaky a prázdnou hodnotu pro úplně validní dítě. HotPDF svádí všechna tahle čtení do jednoho interního resolveru HPDFLoadedInheritedFieldObject, který zkontroluje slovník na klíč, resolveuje nepřímou referenci, když na ni narazí, a jinak následuje /Parent nejvýš 128 úrovní, protože deformované soubory umí postavit cykly /Parent, které s /Kids nemají nic společného. Veřejné gettery na něm sedí: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue a option helpery GetLoadedFormFieldOptionCount a GetLoadedFormFieldOptions, které taky sesbírají pole /Opt uložené na rodiči. Jedno pravidlo v resolveru se snadno pokazí: procházka se zastaví na prvním slovníku, který klíč obsahuje, i když je tam hodnota prázdný řetězec. Lokální /V () je záměrný override, který překryje rodiče, ne díra k zaplnění o úroveň výš ve stromě
var
Pdf: THotPDF;
Field: THPDFLoadedFormField;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
// 'group' nese /FT /Ch, /Ff 131078 a /Opt; dítě
// 'group.b' nese jen /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ální /V
end;
finally
Field.Free;
end;
finally
Pdf.Free;
end;
end;
Proč je lokální /FT špatný test na terminální pole?
Protože rodič může dodávat typ a pořád vlastnit pojmenovaná dětská pole, takže přítomnost /FT neříká nic o tom, kde strom polí končí. Starý traversal prohlásil uzel za terminální, kdykoli měl vlastní /FT nebo žádné /Kids. Ve formuláři výše má group obojí, /FT /Ch i /Kids, takže se registroval jako jedno pole jménem group se dvěma widgety a plně kvalifikovaná jména group.a a group.b prostě zmizela. GetFormFieldCount vrátilo 1, vyhledání podle jména dítěte selhalo a SetFormFieldValue umělo zapsat jen sdíleného rodiče. Náhradní test HPDFLoadedFieldHasChildFields se dívá na děti místo rodiče: dítě je dětské pole, pokud má vlastní /T, má vlastní /Kids, nebo vůbec není slovník /Subtype /Widget. Jen když žádné dítě nekvalifikuje, je uzel terminální a jeho děti se berou jako jeho widget anotace
Dva edge case, které tenhle test formovaly, oba vycházejí ze sloučených slovníků, které §12.7.3.1 dovoluje, když má pole jediný widget. Pojmenovaný sloučený slovník nese /Subtype /Widget a pořád je dětským polem, takže samotný subtype ho nemůže poslat do anonymního seznamu widgetů rodiče; /T vyhrává. Opačný případ se taky stává: někteří producenti opakují /FT rodiče na každém anonymním widgetu, takže /FT nemůže sloužit jako důkaz, že widget otvírá nové pole. Klasifikaci sdílí relationship cache, FormFieldExists a RemoveFormField a každá z těch procházek teď zaznamenává slovníky, které už navštívila, a končí za 128 úrovněmi. Regresní soubor, jehož skupina vypíše sama sebe dvakrát, /Kids [5 0 R 5 0 R 6 0 R 7 0 R], pořád hlásí přesně dvě pole místo rekurze navěky nebo počítání téhož uzlu dvakrát
Jak RemoveFormField brání smazání sourozeneckých polí?
RemoveFormField teď maže jen dítě, které pojmenujete, protože discovery a mazání se konečně shodují na tom, co je terminální pole. Ta shoda má větší váhu, než vypadá. Overload podle jména resolveuje index přes relationship cache a pak počítá terminální pole ve druhé procházce přes /AcroForm /Fields. Jakmile se cache opravila, aby viděla group.a a group.b, neopravená mazací procházka by pořád brala group jako jedno terminální pole a index 0 by smazal rodiče spolu s každým sourozencem a všemi jejich widgety. Mazací procházka teď používá tentýž test HPDFLoadedFieldHasChildFields a tutéž množinu navštívených, sesbírá widget anotace jen mazaného dítěte, vypreparuje je z /Annots každé stránky a rodiče maže jen když jeho pole /Kids skončí prázdné. Regrese kontroluje všechna tři místa, kde by se chyba ukázala: /Kids rodiče, /Annots stránky a hodnotu i vzhled přeživšího sourozence, a to po plném přepisu i po inkrementálním updatu
// Smazat jedno pojmenované dítě; sourozenec i sdílený rodič přežijí
Pdf.RemoveFormField('group.a');
Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// Typ, příznaky a volby se pořád resolveují přes rodiče
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');
Co zapíše ResetLoadedFormField, když je default zděděný?
ResetLoadedFormField zapisuje lokální /V, který je čerstvou kopií zděděného /DV s tímtéž PDF typem objektu, a validuje celý default, než na pole sáhne. Typ objektu záleží, protože skalární gettery všechno splácnou na text. Default checkboxu je jméno jako /Yes, default multi-select list boxu je pole řetězců a textový default může být hexadecimální UTF-16 řetězec; zkopírování kterékoli z nich přes GetLoadedFormFieldDefaultValue by z jména udělalo řetězec, z pole prázdný řetězec a z hex řetězce jeho literální číslice. Reset se proto větví podle zděděného typu: textová a choice pole dostanou nový string objekt, který si drží příznak IsHexadecimal, choice pole s polem defaultů dostanou nové pole nových řetězců a non-pushbutton tlačítka dostanou nový name objekt. Kopírování místo ukazování na objekty rodiče je záměrné: /V, který by sdílel pole /DV rodiče nebo jeho object number, by změnil default, jakkoli kdokoli příště editoval hodnotu. Default špatného typu nebo choice pole obsahující cokoli jiného než řetězce vyhodí výjimku a nechají /V a /I přesně tak, jak byly. Pushbuttony, které nemají hodnotu (Table 226, bit 17), a signature pole spadnou zpátky na starší cestu jen s řetězci
Když žádné /DV nikde ve směru nahoru neexistuje, metoda drží svůj clearing kontrakt tak, že zapíše lokální prázdný řetězec, nebo /Off u checkboxu či radio pole. Smazání lokálního /V by vypadalo úhledněji a bylo by špatně: rodič může držet aktuální hodnotu a odstranění override dítěte by ji potichu vrátilo zpátky. Proto taky reset jednoho pole není akce ResetForm z §12.7.5.3, kterou prohlížeč pustí přes sadu polí, když uživatel klikne na tlačítko, jak popisuje článek o stavbě AcroForm polí a akcí s HotPDF. ResetLoadedFormField je editační operace na jednom načteném poli, s vlastním pravidlem pro případ bez defaultu, a zaznamenává pole přes NoteLoadedFormFieldDirty, aby inkrementální přepočet změnu viděl
var
Field: THPDFLoadedFormField;
begin
Field := Pdf.GetFormField('group.a');
try
// Rodič drží /DV [(b) (r)] na MultiSelect list boxu: group.a dostane
// vlastní /V [(b) (r)] a čerstvé /I [0 2]; rodič zůstává nedotčen
Pdf.ResetLoadedFormField(Field.Index);
// Skalární gettery nedokážou reprezentovat pole defaultu
Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // prázdný
finally
Field.Free;
end;
Pdf.SaveLoadedDocument('survey-reset.pdf');
end;
Jak udržet /V, /I a /AS v souladu
Reset je správný jen tehdy, když selection index i appearance state následují hodnotu, takže ResetLoadedFormField končí stejnými dvěma reconcilery jako SetFormFieldValue. HPDFReconcileChoiceSelection teď akceptuje pole hodnotu: smaže lokální /I bez mutace, porovná každou hodnotu s exportní polovinou každé položky /Opt a zapíše jedno nové setříděné /I, takže reset na [(b) (r)] proti volbám b, g, r dá /I [0 2]. ReconcileLoadedButtonAppearanceStates teď žádá zděděný typ, takže dětský checkbox, jehož /FT /Btn bydlí na rodiči, konečně dostane své /AS. Na straně zápisu ukládají SetFormFieldValue a SetLoadedFormFieldDefaultValue name objekt pro zděděné non-pushbutton tlačítko, i když dítě nemá žádnou lokální položku, ze které by typ zkopírovalo. A když EnsureLoadedFieldAppearanceStream staví button vzhledy znovu, zapíše /AS /Off, pokud hodnota nesedí na on stav, a dá každému stavovému streamu pořádné /Type /XObject, /Subtype /Form a /BBox; před v2.754.4 mohla regenerace vzhledu po resetu zaškrtnout box znovu, než se soubor uložil
Limity, které stojí za znalost, než na tom postavíte
Skalární gettery zůstávají skalární. GetFormFieldValue a GetLoadedFormFieldDefaultValue vrátí prázdný řetězec pro pole hodnotu, převedou čísla a booleany na 42 nebo true a hlásí hex zakódovaný řetězec v jeho hexadecimálním zápisu. Cyklus /Parent ukončí procházku bez výjimky, takže pole, jehož typ se ztratil v cyklu, hlásí lfftUnknown a příznaky 0 místo selhání. SetFormFieldValue a ResetLoadedFormField vždy zapisují dítě, které adresujete, a nikdy nepovýší hodnotu na sdíleného rodiče, což sedí pro nezávislé děti, ale znamená, že radio skupiny se mají adresovat přes pole, které vlastní výběr. A každé volání commituje jedno pole své vlastní silou; nic tady nedělá ze dávky resetů transakci
Resolver zděděných atributů, jednotná klasifikace stromu polí a typovaný reset popsané tady jsou součástí loaded-form API v HotPDF Delphi Component pro Delphi a C++Builder, po boku tvorby polí pokryté v článku o přidávání AcroForm polí do načteného PDF v Delphi