Technický článek

Zděděné hodnoty AcroForm polí a reset v Delphi

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ě

Diagram zděděných AcroForm atributů HotPDF: skupinový uzel ponese /FT, /Ff a /Opt jednou, zatímco pojmenovaná děti group.a a group.b drží jen /T, /Parent, /Rect a lokální /V, což ukazuje HPDFLoadedInheritedFieldObject projíždějící /Parent až 128 úrovní, kde vyhrává první slovník držící klíč a prázdná lokální hodnota překryje rodiče
HotPDF řeší /FT, /Ff, /V, /DV a /Opt jedním resolverem projíždějícím rodiče, takže pojmenované dítě zůstane adresovatelné, zatímco prázdná lokální hodnota záměrně překryje všechno, co nese skupina nad ní
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

Diagram přežití sourozenců v HotPDF RemoveFormField: mazací procházka znovu použije HPDFLoadedFieldHasChildFields a množinu navštívených z discovery, vypreparuje jen pojmenované dítě group.a z AcroForm /Fields a /Annots stránky a nechá sdíleného rodiče, dokud jeho pole /Kids pořád drží přeživší group.b
Discovery a mazání se konečně shodují na tom, co je terminální pole, takže smazání jednoho pojmenovaného dítěte nechá hodnotu i vzhled sourozence nedotčené 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

Diagram typovaného resetu HotPDF: ResetLoadedFormField se větví podle zděděného PDF typu objektu /DV, zapisuje čerstvý name objekt pro checkbox, nové pole nových řetězců pro multi-select choice, řetězec držící IsHexadecimal pro hex text, prázdný řetězec nebo /Off, když žádné /DV není, a vyhodí výjimku bez dotknutí /V nebo /I při nesouhlasu typů
Kopírování místo ukazování na objekty rodiče brání tomu, aby pozdější editace hodnoty potichu změnila default, a pushbuttony plus signature pole spadají 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