HotPDF Delphi Component попълва съществуващо AcroForm поле в зареден PDF чрез THotPDF.SetFormFieldValue, адресирано или чрез индекс на полето от нулата, или чрез напълно квалифицирано име на полето. Записването на новия /V е лесната част; това, което прави извикването надеждно върху реални формуляри, е, че същият метод поддържа съгласувани три парчета състояние, които остават невидими, докато нещо не се обърка: декодираната идентичност на полето, така че не-ASCII име изобщо да може да бъде намерено, /AS appearance състоянието върху checkbox и radio widget-ите, и /I масивът от индекси на селекцията върху choice полетата. Видимият appearance stream е отделна, изрична стъпка чрез EnsureLoadedFieldAppearanceStream
Сценарият е баналният: клиент ви праща свой формуляр — данъчна декларация, застрахователно искане, поръчка за покупка, която някой е направил в Acrobat преди години — и вашето Delphi приложение трябва да го попълни от база данни и да върне файл, който се отваря коректно навсякъде. Нямате никакъв контрол върху това как е създаден формулярът. Имената на полетата може да са UTF-16 кодирани, export стойностите на checkbox-ите може да са 2 вместо Yes, а combo полетата може да ползват [export display] двойки опции. Всяка от тези подробности има правило в ISO 32000-1, и всяко правило е нещо, което SetFormFieldValue вече оправя вместо вас. Тази статия е за това какво прави, защо и къде спира. За родствения проблем — създаване на полета, които все още не съществуват — вижте добавянето на AcroForm полета към зареден PDF в Delphi
Защо SetFormFieldValue не намира поле с не-ASCII име?
Преди v2.752.1 отговорът беше в кодировката: полето живееше във файла под hexadecimal UTF-16BE име, а name кешът пазеше hex изписването вместо текста. ISO 32000-1 §12.7.3.1 дефинира частичното име на поле /T като text string, а §7.9.2.2 казва, че text string може да е UTF-16BE с водещ byte order mark FE FF. Инструментите, с които се създават такива формуляри, редовно сериализират такива имена като hex низове съгласно §7.3.4.3, така че поле с име Straße пристига като <FEFF005300740072006100DF0065>. Вътре в HotPDF THPDFStringObject.Value държи суровия hexadecimal текст, когато IsHexadecimal е зададено, което е точно това, което искате за беззагубен round trip на оригиналния речник, и точно това, което не искате като ключ за търсене. HPDFLoadedFormTextName разделя двете грижи. Когато се гради relationship кешът, всяка /T стойност минава през него: ако string обектът е hexadecimal, HPDFHexToBytes възстановява байтовата последователност; ако байтовете започват с FE FF и дължината е четна, payload-ът се декодира като UTF-16BE и се прекодира като UTF-8; резултатът после се съединява с името на родителя с точка, за да се получи напълно квалифицираното име, което §12.7.3.1 описва, така че kid с име City под родител Address се регистрира като Address.City. Ключът на кеша се нормализира към малки букви, което прави SetFormFieldValue('address.city', ...) също успешен; това е удобство отвъд стандарта, тъй като спецификацията третира имената като case-sensitive. Решаващото е, че се сменя само ключът на кеша. /T обектът в речника на полето пази hex кодировката си, така че записването на документа не пренаписва идентичността на поле, което сте само попълнили
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;
// Квалифицираните имена се декодират от UTF-16BE /T низове и
// се съединяват с точки, така че вложени и не-ASCII имена се разрешават
Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
Pdf.SetFormFieldValue('Applicant.Straße', 'Hauptstraße 12');
// Стойности, които не са Latin-1, пътуват като UTF-16BE hex с FEFF префикс
// и се записват като PDF hexadecimal низ
Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');
Pdf.SaveLoadedDocument('claim-form-filled.pdf');
finally
Pdf.Free;
end;
end;
Какво всъщност записва SetFormFieldValue?
И двата overload-а минават през същите пет стъпки: намиране на речника на полето, запис на /V чрез HPDFSetDictFormValue, съгласуване на choice индексите на селекция, маркиране на речника като dirty, съгласуване на button appearance състоянията и накрая отбелязване на индекса на полето чрез NoteLoadedFormFieldDirty. Последната стъпка има значение, ако формулярът носи calculation скриптове, защото dirty множество е това, което parameterless overload-ът RecalculateLoadedFormFieldsIncremental консумира, за да преизпълни само изчисленията, които транзитивно четат променено поле. HPDFSetDictFormValue само по себе си е внимателен към типа обект, който заменя. Ако съществуващият /V е name обект — каквото checkbox и radio полетата ползват за своя export стойност — новата стойност се записва като name, никога като string, защото PDF имената са само ASCII по конструкция. Иначе се записва string обект и се оглежда подадената стойност: низ, който започва с FEFF, има четна дължина и се състои единствено от hex цифри, се третира като UTF-16BE wire формата от §7.9.2.2 и се съхранява с IsHexadecimal зададено, така че се сериализира като <FEFF...>, а не като литерален (FEFF...). Това е механизмът, на който разчита редът с City по-горе; всеки друг низ се съхранява като литерален string с байтовете, които сте му дали, така че за обикновен латински текст подавате обикновен текст
Защо checkbox-ът пази старата отметка след нова стойност?
Защото при button поле самата стойност не решава какво се рисува. ISO 32000-1 §12.7.4.2.3 предвижда checkbox widget-ът да носи /AS appearance състояние, което назовава кой stream в /AP /N се показва в момента, а viewer-ите рисуват от /AS, не от /V. Ако смените /V на Yes, но оставите /AS на Off, файлът е вътрешно противоречив, а flattening-ът с удоволствие ще запечата остарялата визия на неотметнато поле в страницата, докато данните на формуляра твърдят, че е отметнато. ReconcileLoadedButtonAppearanceStates съществува, за да затваря тази дупка: за поле, чийто /FT е Btn, то обхожда самия речник на полето и всеки елемент в /Kids масива му, прочита on-state името от /AP /N и пренаписва /AS на това име, когато съвпада със стойността на полето, или на Off, когато не съвпада
Две подробности от реални формуляри оформиха поправката в v2.752.3. Първо, normal appearance речникът има право да съдържа само on състоянието; §12.7.4.2.3 назовава off appearance-а Off, но authoring инструментите често пропускат неговия stream и оставят viewer-ът да не рисува нищо. По-старият код отпадаше, когато речникът държеше по-малко от два елемента, така че тези single-state checkbox-и тихо пазеха старата си отметка. Проверката сега е просто дали речникът не е празен, а on-state името се взима като първият ключ, който не е Off. Второ, on-state името е каквото авторът е избрал. Реални формуляри ползват 2, Yes, On или локализирана дума, така че сравнението е срещу действителния ключ, без значение от регистъра, никога срещу втвърдено Yes. Radio бутоните добавят още една особеност, описана в §12.7.4.2.4: селекцията живее в /V на родителското поле, докато отделните kids притежават widget-ите и обикновено нямат собствено /V. Вложеният helper InheritedButtonValue затова изминава /Parent веригата нагоре, до 64 нива, докато намери непразна стойност, така че всяко kid се сравнява със стойността на групата, към която принадлежи. Задаването на родителя на export стойността на едно kid включва точно това kid и изключва всичките му събратя
// Checkbox: export стойността трябва да съвпада с on-state ключа в /AP /N
// (обикновено 'Yes', но реални формуляри ползват '2', 'On' или каквото и да е)
Pdf.SetFormFieldValue('Consent', 'Yes');
// Radio група: /V се записва върху родителя; всяко kid widget получава
// /AS със собственото си export име или Off
Pdf.SetFormFieldValue('PaymentMethod', 'Card');
// Изчистване на checkbox: всяка стойност без съвпадение с on състояние дава /AS Off
Pdf.SetFormFieldValue('Newsletter', 'Off');
Choice полета: държете /I в крак с /V
При combo box или list box /V не е единственото място, където се записва селекция. Table 231 в §12.7.4.4 дефинира /I като масив от индекси от нулата в /Opt, който идентифицира избраните елементи, а viewer, който открие /I, сочещо опция 0, докато /V назовава опция 3, може да освети грешния ред. От v2.754.1 HPDFReconcileChoiceSelection се изпълнява вътре във всяко извикване на SetFormFieldValue и, когато наследеният /FT е Ch, преизгражда /I от новата стойност. Редът на операциите е умишлен. Локалният /I запис се изтрива първо, без съдържанието му да се пипа: ако старият масив е бил indirect object, споделен с друго поле, мутирането му на място би повредило селекцията на другото поле, така че рутината пуска референцията и създава свеж direct масив. После разрешава /Opt през /Parent веригата, тъй като choice опциите могат да се наследяват, и сканира елементите. Гол string опция се сравнява директно; [export display] двойка се сравнява по своя export елемент, а двойка с по-малко от два елемента се прескача. Двете страни минават през HPDFLoadedFormTextName, така че hex UTF-16 опция съвпада с hex UTF-16 стойност, без да ги изписвате еднакво. При първото съвпадение се записва /I с един елемент и сканирането спира; скаларна стойност винаги заменя всяка предишна мулти-селекция, без значение от флага MultiSelect
Когато нищо не съвпада, изобщо не се записва /I. Това е коректният изход за editable combo box, където §12.7.4.4 позволява на потребителя да въведе стойност извън списъка с опции; такава стойност няма индекс, а остарял индекс би бил по-зле от никакъв. Същото получавате и ако подадете display етикет вместо export стойност на списък с двойки опции, така че когато combo box откаже да покаже селекцията ви, проверете коя половина от двойката сте подали
// /Opt е [[US United States] [CA Canada] [MX Mexico]]:
// съвпадение по export стойността, и /I става [1]
Pdf.SetFormFieldValue('Country', 'CA');
// Editable combo със стойност извън /Opt: /V се записва,
// /I се премахва и никакъв индекс не се измисля
Pdf.SetFormFieldValue('Title', 'Principal Engineer');
Стойност и appearance са две отделни операции
SetFormFieldValue никога не пипа appearance stream на текстово или choice поле. След извикването /V държи новия текст, докато /AP /N все още рисува стария, а кое от двете показва даден viewer зависи от това дали AcroForm речникът носи /NeedAppearances true съгласно §12.7.3.3 и дали viewer-ът го уважава. Ако файлът трябва да рендира новата стойност във всеки четец, включително flattener-и и thumbnail генератори, които игнорират флага, извикайте EnsureLoadedFieldAppearanceStream с индекса на полето. Той построява Form XObject от наследения /DA низ, /Q quadding-а, /MaxLen comb подредбата и стойността, разрешава именувания шрифт през AcroForm /DR ресурсите, така че Type0 шрифт пази собствения си descendant шрифт, вместо да се деградира до Helvetica, и връща True, когато поне един widget е получил stream. Overload-ът по име на SetFormFieldValue не ви връща индекс, затова вземете такъв чрез GetFormField, който връща THPDFLoadedFormField, притежаван от вас и за освобождаване от вас. Регресионият набор за промяната в v2.752.1 е изричен относно този split: задава стойност, вика EnsureLoadedFieldAppearanceStream, после рендира страницата и проверява, че пикселите вътре в правоъгълника на widget-а са се сменили, докато пикселите извън него — не. Проверката, че /V се е сменил, не доказва нищо за това какво ще види потребител
var
Field: THPDFLoadedFormField;
begin
Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
Field := Pdf.GetFormField('Applicant.FullName');
try
// Нарисувайте новата стойност в /AP, така че viewer-и, които игнорират
// /NeedAppearances, пак да я показват
if not Pdf.EnsureLoadedFieldAppearanceStream(Field.Index) then
raise Exception.Create('No widget rectangle to paint into');
finally
Field.Free;
end;
Pdf.SaveLoadedDocument('claim-form-filled.pdf');
end;
Граници, които трябва да знаете преди да градите върху това
ReconcileLoadedButtonAppearanceStates тества локалния /FT на речника, който сте адресирали, така че действа върху radio родителя или върху checkbox, носещ собствен /FT; kid widget, адресиран самостоятелно, с /FT само на родителя си, не се съгласува по този път. HPDFReconcileChoiceSelection обработва единична скаларна стойност и записва най-много един индекс; multi-selection list box-ове с няколко избрани елемента са извън това, което SetFormFieldValue моделира. Нито една от рутините не валидира подадената стойност срещу /Opt или срещу on-state ключовете, така че печатна грешка произвежда Off checkbox или combo без индекс, вместо exception. А GetFormFieldValue връща съхранения /V текст такъв, какъвто седи в речника, което за hex-кодирана стойност значи hex изписването, не декодирания текст
Щом стойностите са въведени и appearance-ите са нарисувани, двете естествени следващи стъпки седат от двете страни на тази операция. Размяната на данни за полета с външни системи на едро, вместо по едно извикване на SetFormFieldValue наведнъж, е темата на XFDF импорт и експорт в Delphi. А когато попълненият формуляр е финален и не бива вече да се редактира, flattening-ът на AcroForm и XFA полета в Delphi запечатва точно описаните тук /AS състояния и appearance streams в статично съдържание на страницата, което е причината тяхното консистентиране преди flattening да не е по желание
API-то за редакция на заредени формуляри в тази статия, включително SetFormFieldValue, EnsureLoadedFieldAppearanceStream и инкременталният recalculation graph, се доставя като част от HotPDF Delphi Component за Delphi и C++Builder