HotPDF Delphi Component behandelt /FT, /Ff, /V en /DV op een geladen AcroForm-veld als overerfbare attributen, opgelost door de /Parent-keten af te lopen. Sinds v2.754.3 en v2.754.4 blijft een benoemd kind waarvan het type van zijn parent komt afzonderlijk adresseerbaar, laat RemoveFormField zijn broertjes met rust, en kopieert ResetLoadedFormField de geërfde default met zijn originele PDF-objecttype. Daarvoor werd een verrassend aantal gewone formulieren verkeerd gelezen
Het formulier dat dit alles aan het licht brengt is niet exotisch. Een authoringtool bouwt een groepsknooppunt group dat /FT /Ch, de veldflags en de optielijst één keer draagt, en hangt er twee benoemde kinderen a en b onder, elk een samengevoegd veld-plus-widget-dictionary met niets behalve /T, /Parent, /Rect en zijn eigen /V. Dat is een volstrekt legale manier om attributen te delen, en het is precies het geval dat de Limits-sectie van formulierveldwaarden zetten in een geladen PDF met Delphi als onbehandeld aanmerkte: de buttonreconciliatie keek alleen naar de lokale /FT. Dit artikel pakt op waar dat ophield: hoe de veldboom wordt geclassificeerd, hoe geërfde waarden worden gelezen, en wat een reset van één veld mag wegschrijven
Welke AcroForm-entries kan een veld van zijn parent erven?
ISO 32000-1 §12.7.3.1, Tabel 220, merkt /FT, /Ff, /V en /DV als overerfbaar aan, en Tabel 229 in §12.7.4.3 doet hetzelfde voor de /MaxLen van een tekstveld, dus elke reader die alleen naar de lokale dictionary kijkt meldt voor een volstrekt geldig kind het verkeerde type, de verkeerde flags en een lege waarde. HotPDF leidt al die reads door één interne resolver, HPDFLoadedInheritedFieldObject, die in de dictionary naar de sleutel zoekt, een indirecte referentie oplost als die er staat, en anders hoogstens 128 niveaus /Parent volgt, want misvormde bestanden kunnen /Parent-cycli bouwen die niets met /Kids te maken hebben. De publieke getters zitten er bovenop: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue en de optiehelpers GetLoadedFormFieldOptionCount en GetLoadedFormFieldOptions, die ook een op de parent opgeslagen /Opt-array oppakken. Eén regel in de resolver is makkelijk verkeerd te doen: de walk stopt bij de eerste dictionary die de sleutel bevat, zelfs als de waarde daar een lege string is. Een lokale /V () is een bewuste override die de parent maskeert, geen gat dat hogerop in de boom gevuld moet worden
var
Pdf: THotPDF;
Field: THPDFLoadedFormField;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
// 'group' draagt /FT /Ch, /Ff 131078 en /Opt; het kind
// 'group.b' draagt alleen /T, /Parent, /Rect en zijn eigen /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)); // de lokale /V
end;
finally
Field.Free;
end;
finally
Pdf.Free;
end;
end;
Waarom is een lokale /FT de verkeerde test voor een terminal veld?
Omdat een parent het type kan aanleveren en toch benoemde kindvelden bezit, dus de aanwezigheid van /FT zegt niets over waar de veldboom ophoudt. De oude traversatie riep een knooppunt terminal uit zodra het zijn eigen /FT had of geen /Kids. In het formulier hierboven heeft group allebei, /FT /Ch en /Kids, dus het werd geregistreerd als één veld met de naam group en twee widgets, en de volledig gekwalificeerde namen group.a en group.b verdwenen simpelweg. GetFormFieldCount gaf 1 terug, een opzoek op kindnaam faalde, en SetFormFieldValue kon alleen de gedeelde parent beschrijven. De vervangende test, HPDFLoadedFieldHasChildFields, kijkt naar de kids in plaats van naar de parent: een kid is een kindveld als het zijn eigen /T heeft, zijn eigen /Kids heeft, of helemaal geen /Subtype /Widget-dictionary is. Pas wanneer geen enkel kid kwalificeert is het knooppunt terminal, met zijn kids behandeld als zijn widgetannotaties
De twee randgevallen die die regel vormgaven komen allebei uit samengevoegde dictionaries, die §12.7.3.1 toestaat wanneer een veld één widget heeft. Een benoemde samengevoegde dictionary draagt /Subtype /Widget en is toch een kindveld, dus het subtype alleen kan hem niet naar de anonieme widgetlijst van de parent sturen; de /T wint. Het omgekeerde gebeurt ook: sommige producenten herhalen de /FT van de parent op elke anonieme widget, dus /FT kan evenmin als bewijs dienen dat een widget een nieuw veld opent. De classificatie wordt gedeeld door de relatiecache, FormFieldExists en RemoveFormField, en elk van die walks legt nu de dictionaries die hij al bezocht heeft vast en stopt voorbij 128 niveaus. Een regressiebestand waarvan de groep zichzelf twee keer opvoert, /Kids [5 0 R 5 0 R 6 0 R 7 0 R], meldt nog steeds precies twee velden in plaats van oneindig door te recursen of hetzelfde knooppunt twee keer te tellen
Hoe voorkomt RemoveFormField dat sibling-velden worden verwijderd?
RemoveFormField verwijdert nu alleen het kind dat u noemt, omdat ontdekking en verwijdering eindelijk hetzelfde verstaan onder een terminal veld. Die overeenkomst telt zwaarder dan hij lijkt. De overload op naam lost een index op via de relatiecache en telt daarna terminalvelden in een tweede walk over /AcroForm /Fields. Toen de cache eenmaal group.a en group.b zag, zou een niet-gefixte verwijderwalk group nog steeds als één terminal veld hebben behandeld, en zou index 0 de parent samen met elk sibling en al hun widgets hebben weggegooid. De verwijderwalk gebruikt nu dezelfde HPDFLoadedFieldHasChildFields-test en dezelfde visited-set, verzamelt alleen de widgetannotaties van het verwijderde kind, haalt die uit de /Annots van elke pagina, en verwijdert de parent pas wanneer zijn /Kids-array leeg eindigt. De regressie controleert alle drie de plekken waar een fout zou opduiken: de /Kids van de parent, de /Annots van de pagina, en de waarde en appearance van het overlevende sibling, zowel na een volledige herschrijving als na een incrementele update
// Eén benoemd kind verwijderen; zijn sibling en de gedeelde parent overleven
Pdf.RemoveFormField('group.a');
Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// Type, flags en opties worden nog steeds via de parent opgelost
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');
Wat schrijft ResetLoadedFormField wanneer de default geërfd is?
ResetLoadedFormField schrijft een lokale /V die een verse kopie is van de geërfde /DV met hetzelfde PDF-objecttype, en hij valideert de hele default voordat hij het veld aanraakt. Het objecttype telt omdat de scalaire getters alles platleggen als tekst. De default van een checkbox is een name zoals /Yes, die van een multi-select-listbox is een array van strings, en een tekstdefault kan een hexadecimale UTF-16-string zijn; ze allen kopiëren via GetLoadedFormFieldDefaultValue zou de name in een string veranderen, de array in een lege string en de hex-string in zijn letterlijke cijfers. De reset vertakt zich daarom op het geërfde type: tekst- en keuzevelden krijgen een nieuw stringobject dat de IsHexadecimal-flag vasthoudt, keuzevelden met een arraydefault krijgen een nieuwe array van nieuwe strings, en niet-pushbutton-knoppen krijgen een nieuw name object. Kopiëren, in plaats van wijzen naar de objecten van de parent, is bewust: een /V die de /DV-array van de parent of zijn objectnummer deelde zou de default veranderen zodra iemand de waarde weer bewerkt. Een default van het verkeerde type, of een keuzearray met iets anders dan strings, gooit een exception en laat /V en /I exact zoals ze waren. Pushbuttons, die geen waarde hebben (Tabel 226, bit 17), en signatuurvelden vallen terug op het oudere string-only-pad
Bestaat er nergens in de keten een /DV, dan houdt de methode zijn wiscontract vast door een lokale lege string weg te schrijven, of /Off bij een checkbox- of radioveld. De lokale /V verwijderen zou netter ogen en toch fout zijn: de parent kan een actuele waarde bevatten, en het weglaten van de override van het kind zou die waarde geruisloos terugbrengen. Dit is ook waarom een reset van één veld niet de ResetForm-action uit §12.7.5.3 is, die een viewer over een set velden draait wanneer de gebruiker op een knop klikt, zoals beschreven in AcroForm-velden en actions bouwen met HotPDF. ResetLoadedFormField is een bewerkingsoperatie op één geladen veld, met zijn eigen regel voor het geval zonder default, en hij registreert het veld via NoteLoadedFormFieldDirty zodat incrementele herberekening de verandering ziet
var
Field: THPDFLoadedFormField;
begin
Field := Pdf.GetFormField('group.a');
try
// De parent houdt /DV [(b) (r)] op een MultiSelect-listbox: group.a krijgt
// zijn eigen /V [(b) (r)] en een verse /I [0 2]; de parent blijft onaangetast
Pdf.ResetLoadedFormField(Field.Index);
// Scalaire getters kunnen de array-default niet representeren
Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // leeg
finally
Field.Free;
end;
Pdf.SaveLoadedDocument('survey-reset.pdf');
end;
/V, /I en /AS in overeenstemming houden
Een reset is pas correct als de selectie-index en de appearancestate de waarde volgen, dus ResetLoadedFormField eindigt met dezelfde twee reconciliatoren als SetFormFieldValue. HPDFReconcileChoiceSelection accepteert nu een arraywaarde: hij verwijdert de lokale /I zonder haar te muteren, matcht elke waarde tegen de exporthelft van elke /Opt-entry, en schrijft één nieuwe gesorteerde /I, dus een reset naar [(b) (r)] tegen opties b, g, r levert /I [0 2] op. ReconcileLoadedButtonAppearanceStates vraagt nu naar het geërfde type, dus een kindcheckbox waarvan /FT /Btn op de parent woort krijgt eindelijk zijn /AS gezet. Aan de schrijfkant slaan SetFormFieldValue en SetLoadedFormFieldDefaultValue een name object op voor een geërfde niet-pushbutton-knop, zelfs wanneer het kind geen lokale entry heeft om het type uit te kopiëren. En wanneer EnsureLoadedFieldAppearanceStream button-appearances herbouwt schrijft hij /AS /Off tenzij de waarde met de on-state matcht, en geeft hij elke state-stream een nette /Type /XObject, /Subtype /Form en /BBox; vóór v2.754.4 kon het hergenereren van de appearance na een reset het vinkje weer aanzetten voordat het bestand was opgeslagen
Grenzen die u wilt kennen voordat u hierop bouwt
De scalaire getters blijven scalair. GetFormFieldValue en GetLoadedFormFieldDefaultValue geven een lege string terug voor een arraywaarde, zetten getallen en booleans om naar 42 of true, en melden een hex-gecodeerde string in zijn hexadecimale spelling. Een /Parent-cyclus beëindigt de walk zonder exception, dus een veld waarvan het type in een cyclus verdwenen is meldt lfftUnknown en flags van 0 in plaats van te falen. SetFormFieldValue en ResetLoadedFormField schrijven altijd het kind dat u aanspreekt en promoveren nooit een waarde naar de gedeelde parent, wat klopt voor onafhankelijke kinderen maar betekent dat radiogroepen via het veld dat de selectie bezit moeten worden aangesproken. En elke aanroep legt één veld los vast; niets hier maakt een serie resets transactioneel
De geërfde-attributenresolutie, de verenigde veldboomclassificatie en de getypeerde reset die hier zijn beschreven horen bij de loaded-form-API in de HotPDF Delphi Component voor Delphi en C++Builder, naast het aanmaken van velden dat in AcroForm-velden toevoegen aan een geladen PDF in Delphi wordt behandeld