Техническа статия

Наследени AcroForm стойности на полета и reset в Delphi

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 () е нарочна отмяна, която маскира родителя, а не дупка за попълване от по-нагоре в дървото

Диаграма на наследените AcroForm атрибути в HotPDF: group възел носи /FT, /Ff и /Opt по веднъж, докато именуваните деца group.a и group.b държат само /T, /Parent, /Rect и локално /V, показвайки HPDFLoadedInheritedFieldObject, изминаващ /Parent до 128 нива, при който първият речник с ключа печели, а празна локална стойност маскира родителя
HotPDF разрешава /FT, /Ff, /V, /DV и /Opt през един resolver, обикалящ родителите, така че именувано дете си остава адресируемо, докато локална празна стойност нарочно отменя всичко, което групата отгоре носи
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-а на оцелялото събратя, както след пълно пренаписване, така и след инкрементална актуализация

Диаграма на оцеляването на събратята при HotPDF RemoveFormField: обхождането за триене преизползва HPDFLoadedFieldHasChildFields и множеството посетени от откриването, обира само именуваното дете group.a от AcroForm /Fields и /Annots на страницата и пази споделения родител, докато /Kids масивът му все още държи оцелялото group.b
Откриването и триенето най-после се съгласиха какво е крайно поле, така че премахването на едно именувано дете оставя стойността и 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 полетата се връщат на по-стария път само за низове

Диаграма на типизирания reset в HotPDF: ResetLoadedFormField се разклонява по обектния тип на наследеното /DV, записвайки свеж name обект за checkbox, нов масив от нови низове за multi-select choice, низ, пазещ IsHexadecimal, за hex текст, празен низ или /Off, когато няма /DV, и вдига грешка, без да пипа /V или /I при разминаване на типа
Копирането, вместо соченето към обектите на родителя, пази подразбирането от тиха промяна при следваща редакция на стойността, а pushbutton-и и 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