HotPDF Delphi Component третира /FT, /Ff, /V и /DV върху заредено AcroForm поле като наследяеми атрибути, разрешавани чрез изминаване на /Parent веригата. От v2.754.3 и v2.754.4 насам именувано child поле, чийто тип идва от родителя му, си остава индивидуално адресируемо, RemoveFormField не пипа събратята му, а ResetLoadedFormField копира наследеното подразбиране с оригиналния му PDF обектен тип. Преди това изумително много обикновени формуляри се четяха погрешно
Формулярът, който изважда всичко това наяве, не е екзотичен. Authoring инструмент строи group възел group, който носи /FT /Ch, field флаговете и списъка с опции по веднъж, и закачва под него две именувани деца a и b, всяко слепен речник поле-plus-widget, съдържащ нищо друго освен /T, /Parent, /Rect и собственото му /V. Това е напълно легален начин за споделяне на атрибути и е точно случаят, който Limits секцията на задаването на стойности на form полета в зареден PDF с Delphi отбеляза като необработен: button свързването гледаше само локалния /FT. Тази статия поема оттам, където онази спря, покривайки как се класифицира дървото с полета, как се четат наследените стойности и какво на едно поле reset-ът му е позволено да запише
Кои AcroForm записи може едно поле да наследи от родителя си?
ISO 32000-1 §12.7.3.1, Table 220, обявява /FT, /Ff, /V и /DV за наследяеми, а Table 229 в §12.7.4.3 прави същото за /MaxLen на текстово поле, така че четец, гледащ само локалния речник, ще докладва грешен тип, грешни флагове и празна стойност за напълно валидно дете. HotPDF провежда всичките тези четения през един вътрешен resolver, HPDFLoadedInheritedFieldObject, който проверява речника за ключа, разрешава indirect референция, ако намери такава, а иначе следва /Parent до най-много 128 нива, защото зле оформени файлове могат да строят /Parent цикли, които нямат нищо общо с /Kids. Публичните getter-и седят отгоре му: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue и option helper-ите GetLoadedFormFieldOptionCount и GetLoadedFormFieldOptions, които взимат и /Opt масив, съхраняван на родителя. Едно правило в resolver-а е лесно за сбъркване: обхождането спира на първия речник, съдържащ ключа, дори стойността там да е празен низ. Локално /V () е нарочна отмяна, която маскира родителя, а не дупка за попълване от по-нагоре в дървото
var
Pdf: THotPDF;
Field: THPDFLoadedFormField;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
// 'group' носи /FT /Ch, /Ff 131078 и /Opt; детето
// 'group.b' носи само /T, /Parent, /Rect и своето /V
Field := Pdf.GetFormField('group.b');
try
if Pdf.GetFormFieldType(Field.Index) = lfftChoice then
begin
// 131078 = Combo (бит 18) + NoExport (бит 3) + Required (бит 2)
Writeln(Pdf.GetLoadedFormFieldFlags(Field.Index));
Writeln(Pdf.IsFormFieldRequired(Field.Index)); // TRUE
Writeln(Pdf.GetLoadedFormFieldOptionCount(Field.Index));
Writeln(Pdf.GetFormFieldValue(Field.Index)); // локалното /V
end;
finally
Field.Free;
end;
finally
Pdf.Free;
end;
end;
Защо локален /FT е грешният тест за крайно поле?
Защото родител може да снабдя типа и пак да притежава именувани child полета, така че наличието на /FT не казва нищо за това къде свършва дървото с полета. Старото обхождане обявяваше възел за краен, щом имаше собствен /FT или нямаше /Kids. В горния формуляр group има и /FT /Ch, и /Kids, затова беше регистрирано като едно поле на име group с два widget-а, а напълно квалифицираните имена group.a и group.b просто изчезнаха. GetFormFieldCount връщаше 1, търсенето по child име проваляше, а SetFormFieldValue можеше да пише само споделения родител. Заместващият тест, HPDFLoadedFieldHasChildFields, гледа децата вместо родителя: kid е child поле, ако има собствен /T, има собствен /Kids или изобщо не е речник /Subtype /Widget. Само когато никое kid не се квалифицира, възелът е краен, а неговите kids се третират като неговите widget анотации
Двата крайни случая, оформили това правило, и двата идват от слепени речници, които §12.7.3.1 позволява, когато едно поле има един widget. Именуван слепен речник носи /Subtype /Widget и пак е child поле, така че самият subtype не може да го прати в анонимния widget списък на родителя; /T печели. Обратното също се случва: някои производители повтарят /FT на родителя върху всеки анонимен widget, така че /FT не може да служи за доказателство, че widget започва ново поле. Класификацията е споделена от relationship кеша, FormFieldExists и RemoveFormField, а всяко от тези обхождания вече записва речниците, които е посетило, и спира след 128 нива. Регресионен файл, чиято група изброява себе си два пъти, /Kids [5 0 R 5 0 R 6 0 R 7 0 R], и нататък докладва точно две полета, вместо да рекурсира вечно или да брои един и същ възел дваж
Как RemoveFormField избягва да изтрие полетата събратя?
RemoveFormField вече изтрива само детето, което назовете, защото откриването и триенето най-после се съгласиха какво е крайно поле. Това съгласие има повече значение, отколкото изглежда. Overload-ът по име разрешава индекс през relationship кеша и после брои крайни полета във второ обхождане на /AcroForm /Fields. Щом кешът беше оправен да вижда group.a и group.b, неотправеното обхождане за триене пак би третирало group като едно крайно поле, а индекс 0 би махнал родителя заедно с всяко събратя и всичките им widget-и. Обхождането за триене вече ползва същия тест HPDFLoadedFieldHasChildFields и същото множество от посетени, събира widget анотациите само на премахнатото дете, обира тези от /Annots на всяка страница и маха родителя само когато /Kids масивът му свърши празен. Регресията проверява и трите места, където грешка би се показала: /Kids на родителя, /Annots на страницата и стойността и appearance-а на оцелялото събратя, както след пълно пренаписване, така и след инкрементална актуализация
// Премахнете едно именувано дете; събратят му и споделеният родител оцеляват
Pdf.RemoveFormField('group.a');
Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// Типът, флаговете и опциите и нататък се разрешават през родителя
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');
Какво записва ResetLoadedFormField, когато подразбирането е наследено?
ResetLoadedFormField записва локално /V, което е свежо копие на наследеното /DV със същия PDF обектен тип, и валидира цялото подразбиране, преди да пипне полето. Обектният тип има значение, защото скаларните getter-и изравняват всичко до текст. Подразбиране на checkbox е име като /Yes, подразбиране на multi-select list box е масив от низове, а текстово подразбиране може да е hexadecimal UTF-16 низ; копирането на някое от тях през GetLoadedFormFieldDefaultValue би превърнало името в низ, масива в празен низ, а hex низа в неговите буквални цифри. Затова reset-ът се разклонява по наследения тип: текстови и choice полета получават нов string обект, който пази флага IsHexadecimal, choice полета с масиво подразбиране получават нов масив от нови низове, а non-pushbutton бутони получават нов name обект. Копирането, вместо соченето към обектите на родителя, е нарочено: /V, споделящ /DV масива на родителя или неговия object номер, би сменил подразбирането при следващата редакция на стойността. Подразбиране от грешен тип или choice масив, съдържащ нещо освен низове, вдига exception и оставя /V и /I точно каквито са били. Pushbutton-ите, които нямат стойност (Table 226, бит 17), и signature полетата се връщат на по-стария път само за низове
Когато няма /DV никъде нагоре по веригата, методът пази своя договор за изчистване, записвайки локален празен низ или /Off за checkbox или radio поле. Изтриването на локалното /V би изглеждало по-спретнато и би било грешно: родителят може да държи текуща стойност, а махането на отмяната на детето би върнало тази стойност тихо. Това е и причината reset на едно поле да не е ResetForm действието от §12.7.5.3, което viewer изпълнява върху множество полета, когато потребителят натисне бутон, както е описано в изграждането на AcroForm полета и действия с HotPDF. ResetLoadedFormField е операция по редакция върху едно заредено поле, със собственото си правило за случая без подразбиране, и записва полето чрез NoteLoadedFormFieldDirty, така че инкременталното преизчисление да види промяната
var
Field: THPDFLoadedFormField;
begin
Field := Pdf.GetFormField('group.a');
try
// Родителят държи /DV [(b) (r)] на MultiSelect list box: group.a получава
// собствено /V [(b) (r)] и свежо /I [0 2]; родителят е недокоснат
Pdf.ResetLoadedFormField(Field.Index);
// Скаларните getter-и не могат да представят масивното подразбиране
Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // празно
finally
Field.Free;
end;
Pdf.SaveLoadedDocument('survey-reset.pdf');
end;
Държане на /V, /I и /AS в съгласие
Reset е коректен само ако индексът на селекцията и appearance състоянието следват стойността, затова ResetLoadedFormField завършва със същите два reconciler-а като SetFormFieldValue. HPDFReconcileChoiceSelection вече приема стойност-масив: изтрива локалното /I, без да го мутира, съпоставя всяка стойност с export половината на всеки запис в /Opt и записва едно ново сортирано /I, така че reset до [(b) (r)] срещу опции b, g, r дава /I [0 2]. ReconcileLoadedButtonAppearanceStates вече пита за наследения тип, така че child checkbox, чийто /FT /Btn живее на родителя, най-после получава своето /AS зададено. На страната на записа SetFormFieldValue и SetLoadedFormFieldDefaultValue съхраняват name обект за наследен non-pushbutton бутон, дори детето да няма локален запис, от който да копира типа. А когато EnsureLoadedFieldAppearanceStream преизгражда button appearance-и, записва /AS /Off, освен ако стойността не съвпада с on състоянието, и дава на всеки state stream подобаващи /Type /XObject, /Subtype /Form и /BBox; преди v2.754.4 преизграждането на appearance след reset можеше да отметне квадратчето пак, преди файлът да е записан
Граници, които си струва да знаете, преди да градите върху това
Скаларните getter-и си остават скаларни. GetFormFieldValue и GetLoadedFormFieldDefaultValue връщат празен низ за стойност-масив, stringify-ват числа и булеви като 42 или true и докладват hex-кодиран низ в hex изписването му. /Parent цикъл приключва обхождането без exception, така че поле, чийто тип е изгубен в цикъл, докладва lfftUnknown и флагове 0, вместо да се провали. SetFormFieldValue и ResetLoadedFormField винаги записват детето, което адресирате, и никога не повишават стойност към споделения родител, което е вярно за независими деца, но значи, че radio групи трябва да се адресират през полето, притежаващо селекцията. И всяко извикване ангажира едно поле само за себе си; нищо тук не прави партида reset-и транзакционна
Разрешаването на наследени атрибути, единната класификация на дървото с полета и типизираният reset, описани тук, са част от loaded-form API-то на HotPDF Delphi Component за Delphi и C++Builder, редом със създаването на полета, покрито в добавянето на AcroForm полета към зареден PDF в Delphi