HotPDF Delphi Component behandlar /FT, /Ff, /V och /DV på ett inläst AcroForm-fält som ärftliga attribut, upplösta genom att vandra /Parent-kedjan. Sedan v2.754.3 och v2.754.4 förblir ett namngivet barn vars typ kommer från sin förälder individuellt adresserbart, RemoveFormField lämnar syskonen i fred, och ResetLoadedFormField kopierar den ärvda defaulten med sin ursprungliga PDF-objekttyp. Dessförinnan misslästes förvånansvärt många helt vanliga formulär
Formuläret som exponerar allt detta är inte exotiskt. Ett författarverktyg bygger en gruppnod group som bär /FT /Ch, fältflaggorna och optionslistan en gång, och hänger två namngivna barn a och b under den, vart och ett en sammanslagen fält-plus-widget-ordbok med ingenting utöver /T, /Parent, /Rect och sin egen /V. Det är ett helt lagligt sätt att dela attribut, och det är exakt fallet som begränsningsavsnittet i att sätta formulärfältvärden i en inläst PDF med Delphi flaggade som ohanterat: knappavstämningen tittade bara på den lokala /FT. Den här artikeln tar vid där den förra slutade, och täcker hur fältträdet klassificeras, hur ärvda värden läses och vad en återställning av ett enskilt fält tillåts skriva
Vilka AcroForm-poster kan ett fält ärva från sin förälder?
ISO 32000-1 §12.7.3.1, tabell 220, märker /FT, /Ff, /V och /DV som ärftliga, och tabell 229 i §12.7.4.3 gör detsamma för ett textfälts /MaxLen, så vilken läsare som helst som bara tittar på den lokala ordboken kommer att rapportera fel typ, fel flaggor och ett tomt värde för ett helt giltigt barn. HotPDF leder alla dessa läsningar genom en intern upplösare, HPDFLoadedInheritedFieldObject, som kontrollerar ordboken för nyckeln, löser upp en indirekt referens om den hittar en, och annars följer /Parent i högst 128 nivåer, för att felformaterade filer kan bygga /Parent-cykler som inte har något med /Kids att göra. De publika getters vilar ovanpå den: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue samt options-hjälpfunktionerna GetLoadedFormFieldOptionCount och GetLoadedFormFieldOptions, som också plockar upp en /Opt-array lagrad på föräldern. En regel i upplösaren är lätt att få fel: vandringen stannar vid den första ordbok som innehåller nyckeln, även om värdet där är en tom sträng. En lokal /V () är en medveten överskrivning som maskerar föräldern, inte ett tomrum att fylla från högre upp i trädet
var
Pdf: THotPDF;
Field: THPDFLoadedFormField;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
// 'group' bär /FT /Ch, /Ff 131078 och /Opt; barnet
// 'group.b' bär bara /T, /Parent, /Rect och sin egen /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)); // den lokala /V
end;
finally
Field.Free;
end;
finally
Pdf.Free;
end;
end;
Varför är en lokal /FT fel test för ett terminalfält?
Eftersom en förälder kan leverera typen och ändå äga namngivna barnfält, säger närvaron av /FT ingenting om var fältträdet slutar. Den gamla genomgången förklarade en nod terminal när den hade sin egen /FT eller inga /Kids. I formuläret ovan har group både /FT /Ch och /Kids, så den registrerades som ett fält med namnet group och två widgets, och de fullt kvalificerade namnen group.a och group.b försvann bara. GetFormFieldCount returnerade 1, en uppslagning med barnnamn misslyckades, och SetFormFieldValue kunde bara skriva den delade föräldern. Ersättningstestet, HPDFLoadedFieldHasChildFields, tittar på barnen i stället för föräldern: en kid är ett barnfält om den har sin egen /T, har egna /Kids, eller inte är en /Subtype /Widget-ordbok alls. Först när ingen kid kvalificerar är noden terminal, med sina kids behandlade som sina widgetannoteringar
De två kantfall som formade den regeln kommer båda från sammanslagna ordböcker, vilket §12.7.3.1 tillåter när ett fält har en enda widget. En namngiven sammanslagen ordbok bär /Subtype /Widget och är ändå ett barnfält, så subtypen ensam kan inte skicka den in i förälderns anonyma widgetlista; /T vinner. Det omvända händer också: vissa producenter upprepar förälderns /FT på varje anonym widget, så /FT inte kan användas som bevis för att en widget startar ett nytt fält heller. Klassificeringen delas av relationscachen, FormFieldExists och RemoveFormField, och var och en av de genomgångarna registrerar nu de ordböcker den redan besökt och stannar efter 128 nivåer. En regressionsfil vars grupp listar sig själv två gånger, /Kids [5 0 R 5 0 R 6 0 R 7 0 R], rapporterar fortfarande exakt två fält i stället för att rekursera för evigt eller räkna samma nod två gånger
Hur undviker RemoveFormField att ta bort syskonfält?
RemoveFormField tar nu bara bort barnet du namnger, för att upptäckt och radering äntligen är överens om vad ett terminalfält är. Den överenskommelsen spelar större roll än den ser ut. By-name-overloaden löser upp ett index genom relationscachen och räknar sedan terminalfält i en andra genomgång av /AcroForm /Fields. Så snart cachen var fixad för att se group.a och group.b skulle en ofixerad raderingsgenomgång fortfarande ha behandlat group som ett enda terminalfält, och index 0 skulle ha tagit bort föräldern tillsammans med varje syskon och alla deras widgets. Raderingsgenomgången använder nu samma HPDFLoadedFieldHasChildFields-test och samma mängd besökta noder, samlar in widgetannoteringarna av bara det borttagna barnet, strippar dem från varje sidas /Annots och tar bort föräldern först när dess /Kids-array slutar tom. Regressionen kontrollerar alla tre ställen ett misstag skulle synas: förälderns /Kids, sidans /Annots och det överlevande syskonets värde och utseende, både efter en full omskrivning och efter en inkrementell uppdatering
// Ta bort ett namngivet barn; dess syskon och den delade föräldern överlever
Pdf.RemoveFormField('group.a');
Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// Typ, flaggor och options löses fortfarande upp genom föräldern
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');
Vad skriver ResetLoadedFormField när defaulten är ärvd?
ResetLoadedFormField skriver en lokal /V som är en färsk kopia av den ärvda /DV med samma PDF-objekttyp, och den validerar hela defaulten innan fältet rörs. Objekttypen spelar roll för att skalärgettern plattar ut allt till text. En kryssrutadefault är ett namn som /Yes, en multi-select-listrutas default är en array av strängar, och en textdefault kan vara en hexadecimal UTF-16-sträng; att kopiera någon av dem genom GetLoadedFormFieldDefaultValue skulle göra namnet till en sträng, arrayen till en tom sträng och hex-strängen till sina bokstavliga siffror. Återställningen förgrenar sig därför på den ärvda typen: text- och valfält får ett nytt strängobjekt som behåller IsHexadecimal-flaggan, valfält med en arraydefault får en ny array av nya strängar, och knappar som inte är pushbuttons får ett nytt namnobjekt. Att kopiera, i stället för att peka på förälderns objekt, är medvetet: en /V som delade förälderns /DV-array eller dess objektnummer skulle ändra defaulten nästa gång någon redigerade värdet. En default av fel typ, eller en valarray som innehåller något annat än strängar, kastar ett undantag och lämnar /V och /I exakt som de var. Pushbuttons, som inte har något värde (tabell 226, bit 17), och signaturfält faller tillbaka på den äldre strängbundna vägen
När ingen /DV finns någonstans uppåt i kedjan håller metoden sitt rensningskontrakt genom att skriva en lokal tom sträng, eller /Off för ett kryssruta- eller radiofält. Att radera den lokala /V skulle se snyggare ut och vara fel: föräldern kan hålla ett aktuellt värde, och att ta bort barnets överskrivning skulle tyst föra tillbaka det värdet. Det är också därför en återställning av ett enskilt fält inte är ResetForm-åtgärden i §12.7.5.3, som en viewer kör över en uppsättning fält när användaren klickar på en knapp, som beskrivs i att bygga AcroForm-fält och actions med HotPDF. ResetLoadedFormField är en redigeringsoperation på ett inläst fält, med sin egen regel för fallet utan default, och den registrerar fältet genom NoteLoadedFormFieldDirty så att inkrementell omräkning ser ändringen
var
Field: THPDFLoadedFormField;
begin
Field := Pdf.GetFormField('group.a');
try
// Föräldern håller /DV [(b) (r)] på en MultiSelect-listruta: group.a får
// sin egen /V [(b) (r)] och en färsk /I [0 2]; föräldern är orörd
Pdf.ResetLoadedFormField(Field.Index);
// Skalärgettern kan inte representera arraydefaulten
Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // tom
finally
Field.Free;
end;
Pdf.SaveLoadedDocument('survey-reset.pdf');
end;
Hålla /V, /I och /AS i samklang
En återställning är bara korrekt om urvalsindexet och utseendetillståndet följer värdet, så ResetLoadedFormField avslutar med samma två avstämmare som SetFormFieldValue. HPDFReconcileChoiceSelection accepterar nu ett arrayvärde: den raderar den lokala /I utan att mutera den, matchar varje värde mot exporthalvan av varje /Opt-post och skriver en ny sorterad /I, så en återställning till [(b) (r)] mot options b, g, r ger /I [0 2]. ReconcileLoadedButtonAppearanceStates frågar nu efter den ärvda typen, så en barnkryssruta vars /FT /Btn bor på föräldern får äntligen sin /AS satt. På skrivsidan lagrar SetFormFieldValue och SetLoadedFormFieldDefaultValue ett namnobjekt för en ärvd icke-pushbutton-knapp även när barnet inte har någon lokal post att kopiera typen från. Och när EnsureLoadedFieldAppearanceStream bygger om knapputseenden skriver den /AS /Off om inte värdet matchar på-läget, och ger varje tillståndsström en ordentlig /Type /XObject, /Subtype /Form och /BBox; före v2.754.4 kunde en regenerering av utseendet efter en återställning bocka i rutan igen innan filen sparades
Begränsningar värda att känna till innan du bygger på detta
Skalärgettern förblir skalära. GetFormFieldValue och GetLoadedFormFieldDefaultValue returnerar en tom sträng för ett arrayvärde, strängifierar tal och booleaner som 42 eller true och rapporterar en hex-kodad sträng i sin hexadecimala stavning. En /Parent-cykel avslutar vandringen utan undantag, så ett fält vars typ går förlorad i en cykel rapporterar lfftUnknown och flaggorna 0 i stället för att misslyckas. SetFormFieldValue och ResetLoadedFormField skriver alltid barnet du adresserar och befordrar aldrig ett värde till den delade föräldern, vilket är rätt för oberoende barn men betyder att radiogrupper bör adresseras genom fältet som äger urvalet. Och varje anrop committar ett fält på egen hand; ingenting här gör en batch av återställningar transaktionell
Upplösningen av ärvda attribut, den enhetliga fältträdsklassificeringen och den typade återställningen som beskrivs här är del av loaded-form-API:t i HotPDF Delphi Component för Delphi och C++Builder, jämte fältskapandet som täcks i att lägga till AcroForm-fält i en inläst PDF i Delphi