Odborný článok

Dedičné hodnoty polí AcroForm a resety v Delphi

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

Diagram dedičných atribútov AcroForm v HotPDF: skupinový uzol nesie /FT, /Ff a /Opt raz, zatiaľ čo pomenované deti group.a a group.b držia len /T, /Parent, /Rect a lokálne /V, s HPDFLoadedInheritedFieldObject prechádzajúcim /Parent po 128 úrovní, kde vyhráva prvý slovník držiaci kľúč a prázdna lokálna hodnota maskuje rodiča
HotPDF vyrieši /FT, /Ff, /V, /DV a /Opt jedným resolverom prechádzajúcim rodičov, takže pomenované dieťa ostáva adresovateľné, kým lokálna prázdna hodnota zámerný prekryje všetko, čo nesie skupina nad ňou
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

Diagram prežitia súrodencov v HotPDF RemoveFormField: prechod mazania znovu používa HPDFLoadedFieldHasChildFields a navštívenú množinu z objavenia, vyberie len pomenované dieťa group.a z AcroForm /Fields a /Annots strany a ponechá zdieľaného rodiča, kým pole /Kids stále drží preživšie group.b
Objavenie a mazanie sa konečne zhodujú v tom, čo je koncové pole, takže odstránenie jedného pomenovaného dieťaťa nechá hodnotu a vzhľad súrodencu nedotknuté po úplnom prepise aj 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

Diagram typovaného resetu v HotPDF: ResetLoadedFormField sa vetví podľa typu objektu zdedeného /DV a zapíše čerstvý name objekt pre checkbox, nové pole nových reťazcov pre multi-select choice, reťazec držiaci IsHexadecimal pre hex text, prázdny reťazec alebo /Off, keď /DV neexistuje, a vyhodí výnimku bez dotyku /V či /I pri nezhode typov
Kopírovanie namiesto ukazovania na objekty rodiča zabráni, aby neskoršia úprava hodnoty potichu zmenila predvolenú hodnotu, a pushbuttony aj signature polia spadnú 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