Технічна стаття

Заповнення полів форми в завантаженому PDF у Delphi

HotPDF Delphi Component заповнює наявне поле AcroForm у завантаженому PDF через THotPDF.SetFormFieldValue, адресуючи його або за нульовим індексом поля, або за повністю кваліфікованим іменем поля. Записати новий запис /V — це легка частина; надійним на реальних формах виклик робить те, що той самий метод тримає узгодженими три речі, які невидимі, доки не зламаються: декодовану ідентичність поля, щоб не-ASCII ім'я взагалі можна було знайти, стан вигляду /AS на віджетах прапорців і радіокнопок та масив індексів вибору /I на полях вибору. Видимий потік вигляду — це окремий, явний крок через EnsureLoadedFieldAppearanceStream

Сценарій буденний: клієнт надсилає вам власну форму — податкову декларацію, заяву на страховку, замовлення, яке хтось зробив в Acrobat роки тому, — і ваш застосунок Delphi мусить заповнити її з бази даних і повернути файл, який правильно відкривається всюди. Ви не контролюєте, як форму створювали. Імена полів можуть бути в UTF-16, значення експорту прапорця може бути 2, а не Yes, а combo box може використовувати пари опцій [export display]. У кожної з цих деталей є правило в ISO 32000-1, і кожне правило — це те, що SetFormFieldValue тепер обробляє за вас. Ця стаття про те, що він робить, чому і де зупиняється. Сусідню задачу — створення полів, яких ще не існує, — розглядає додавання полів AcroForm до завантаженого PDF у Delphi

Чому SetFormFieldValue не знаходить поле з не-ASCII іменем?

До v2.752.1 відповідь була в кодуванні: поле жило у файлі під шістнадцятковим іменем UTF-16BE, а кеш імен зберігав шістнадцяткове написання замість тексту. ISO 32000-1 §12.7.3.1 визначає часткове ім'я поля /T як рядок тексту, а §7.9.2.2 каже, що рядок тексту може бути UTF-16BE з провідною міткою порядку байтів FE FF. Інструменти авторингу регулярно серіалізують такі імена як hex-рядки за §7.3.4.3, тож поле з іменем Straße надходить як <FEFF005300740072006100DF0065>. Усередині HotPDF THPDFStringObject.Value тримає сирий шістнадцятковий текст щоразу, коли встановлено IsHexadecimal, а це саме те, що потрібно для дослівної подорожі оригінального словника туди й назад, і саме те, чого не потрібно як ключ пошуку. HPDFLoadedFormTextName розділяє ці дві турботи. Коли будується кеш зв'язків, кожне значення /T проходить через нього: якщо рядковий об'єкт шістнадцятковий, HPDFHexToBytes відновлює послідовність байтів; якщо байти починаються з FE FF і мають парну довжину, вміст декодується як UTF-16BE і перекодовується в UTF-8; результат далі з'єднується з батьківським іменем через крапку, утворюючи повністю кваліфіковане ім'я, яке описує §12.7.3.1, тож нащадок із іменем City під батьком із іменем Address реєструється як Address.City. Ключ кешу нормалізується до нижнього регістру, завдяки чому SetFormFieldValue('address.city', ...) теж спрацьовує; це зручність понад стандарт, бо специфікація трактує імена як чутливі до регістру. Найважливіше те, що змінюється лише ключ кешу. Об'єкт /T у словнику поля зберігає своє шістнадцяткове кодування, тож збереження документа не переписує ідентичність поля, яке ви лише заповнили

Як HotPDF розв'язує не-ASCII імена AcroForm: HPDFHexToBytes відновлює вміст UTF-16BE за шістнадцятковим рядком /T, мітка порядку байтів FE FF декодується й перекодовується в UTF-8, а кваліфіковане ім'я приєднує батька, тож і Applicant.FullName, і поле з іменем Straße потрапляють у кеш пошуку
Змінюється лише ключ кешу: словник поля зберігає своє шістнадцяткове кодування, пошук нормалізується до нижнього регістру як зручність понад стандарт, а збереження документа ніколи не переписує ідентичність поля, яке ви лише заповнили
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;

    // Кваліфіковані імена декодуються з рядків /T у UTF-16BE і
    // з'єднуються крапками, тож вкладені й не-ASCII імена резолвляться
    Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
    Pdf.SetFormFieldValue('Applicant.Straße', 'Hauptstraße 12');

    // Значення, що не є Latin-1, їдуть як UTF-16BE hex із префіксом FEFF
    // і записуються як шістнадцятковий рядок PDF
    Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');

    Pdf.SaveLoadedDocument('claim-form-filled.pdf');
  finally
    Pdf.Free;
  end;
end;

Що насправді записує SetFormFieldValue?

Обидва перевантаження виконують ті самі п'ять кроків: знайти словник поля, записати /V через HPDFSetDictFormValue, узгодити індекси вибору поля вибору, позначити словник брудним, узгодити стани вигляду кнопок і нарешті зареєструвати індекс поля через NoteLoadedFormFieldDirty. Цей останній крок важливий, якщо форма несе скрипти обчислень, бо саме множину брудних споживає перевантаження RecalculateLoadedFormFieldsIncremental без параметрів, щоб перезапустити лише ті обчислення, які транзитивно читають змінене поле. Сам HPDFSetDictFormValue уважний до типу об'єкта, який замінює. Якщо наявний /V — це об'єкт-ім'я, а саме його використовують прапорці й радіокнопки для свого значення експорту, нове значення записується як ім'я, ніколи як рядок, бо імена PDF за побудовою лише ASCII. Інакше він пише рядковий об'єкт і оглядає передане вами значення: рядок, що починається з FEFF, має парну довжину і складається виключно з шістнадцяткових цифр, трактується як дротова форма UTF-16BE з §7.9.2.2 і зберігається зі встановленим IsHexadecimal, тож серіалізується як <FEFF...>, а не як літерал (FEFF...). Саме на цьому механізмі тримається рядок City вище; будь-який інший рядок зберігається як літеральний рядок із тими байтами, які ви дали, тож для простого латинського тексту передавайте простий текст

Чому прапорець зберігає стару галочку після зміни значення?

Бо для поля-кнопки саме значення не вирішує, що намальовано. ISO 32000-1 §12.7.4.2.3 визначає, що віджет прапорця несе стан вигляду /AS, який називає, який потік у /AP /N показано зараз, і переглядачі малюють із /AS, а не з /V. Якщо ви зміните /V на Yes, але лишите /AS як Off, файл стане внутрішньо суперечливим, і вирівнювання радо запече застарілий вигляд непоставленої галочки в сторінку, поки дані форми кажуть, що галочка стоїть. ReconcileLoadedButtonAppearanceStates існує, щоб закрити цю прогалину: для поля, чий /FT дорівнює Btn, він відвідує і сам словник поля, і кожен запис у його масиві /Kids, читає ім'я стану «увімкнено» з /AP /N і переписує /AS на це ім'я, коли воно збігається зі значенням поля, або на Off, коли ні

Чому прапорець у HotPDF зберігає стару галочку, коли змінюється лише /V: переглядачі малюють зі стану вигляду /AS у /AP /N, тож ReconcileLoadedButtonAppearanceStates відвідує поле і кожного нащадка, читає ім'я стану «увімкнено» як перший ключ, відмінний від Off, і переписує /AS при збігу або на Off в іншому разі
Групи радіокнопок порівнюють кожного нащадка зі значенням батька, яке InheritedButtonValue відновлює проходом ланцюгом /Parent, тож встановлення групи в одне значення експорту вмикає рівно той віджет і вимикає кожного сусіда

Дві деталі з реальних форм визначили виправлення у v2.752.3. Перша: звичайному словнику вигляду дозволено містити лише стан «увімкнено»; §12.7.4.2.3 називає вигляд «вимкнено» як Off, але інструменти авторингу часто опускають його потік і дозволяють переглядачу не малювати нічого. Раніший код здавався, коли словник містив менше двох записів, тож ті одностанові прапорці тихо зберігали свою стару галочку. Тепер перевірка проста: словник непорожній, а ім'я стану «увімкнено» береться як перший ключ, відмінний від Off. Друга: ім'я стану «увімкнено» — те, що обрав автор. Реальні форми використовують 2, Yes, On або локалізоване слово, тож порівняння йде з фактичним ключем, без урахування регістру, а не з зашитим Yes. Радіокнопки додають ще один нюанс, описаний у §12.7.4.2.4: вибір живе в /V батьківського поля, тоді як окремі нащадки володіють віджетами й зазвичай не мають власного /V. Тож вкладений хелпер InheritedButtonValue піднімається ланцюгом /Parent до 64 рівнів, доки не знайде непорожнє значення, і кожен нащадок порівнюється зі значенням тієї групи, до якої належить. Встановлення батька в значення експорту одного нащадка вмикає рівно цього нащадка й вимикає кожного сусіда

// Прапорець: значення експорту мусить збігатися з ключем стану «увімкнено» в /AP /N
// (часто це 'Yes', але реальні форми використовують '2', 'On' чи будь-що інше)
Pdf.SetFormFieldValue('Consent', 'Yes');

// Група радіокнопок: /V пишеться на батьку; кожен віджет-нащадок отримує
// /AS зі своїм іменем експорту або Off
Pdf.SetFormFieldValue('PaymentMethod', 'Card');

// Зняття галочки: будь-яке значення, що не збігається зі станом «увімкнено», дає /AS Off
Pdf.SetFormFieldValue('Newsletter', 'Off');

Поля вибору: тримаємо /I в ногу з /V

Для combo box або list box /V — не єдине місце, де записано вибір. Table 231 у §12.7.4.4 визначає /I як масив нульових індексів у /Opt, який ідентифікує вибрані елементи, і переглядач, який бачить /I, що вказує на опцію 0, поки /V називає опцію 3, може підсвітити не той рядок. Від v2.754.1 HPDFReconcileChoiceSelection виконується всередині кожного виклику SetFormFieldValue і, коли успадкований /FT дорівнює Ch, перебудовує /I з нового значення. Порядок операцій навмисний. Локальний запис /I спершу видаляється, не торкаючись його вмісту: якби старий масив був непрямим об'єктом, спільним з іншим полем, мутація його на місці зіпсувала б вибір того іншого поля, тож рутина відкидає посилання й створює свіжий прямий масив. Далі вона розв'язує /Opt ланцюгом /Parent, бо опції вибору можуть успадковуватися, і сканує записи. Голий рядок опції порівнюється напряму; пара [export display] порівнюється за своїм елементом експорту, а пара з менш ніж двома елементами пропускається. Обидві сторони проходять через HPDFLoadedFormTextName, тож hex-опція UTF-16 збігається з hex-значенням UTF-16 без потреби писати їх однаково. На першому збігу записується одноелементний /I і скан зупиняється; скалярне значення завжди замінює будь-який попередній множинний вибір, незалежно від прапорця MultiSelect

Як HotPDF тримає поле вибору узгодженим: HPDFReconcileChoiceSelection видаляє локальний масив /I до будь-якого дотику до нього, розв'язує /Opt ланцюгом /Parent, порівнює половину експорту кожної опції через HPDFLoadedFormTextName, пише одноелементний /I на першому збігу й не пише нічого, коли редаговане значення combo не має індексу
Голий рядок опції порівнюється напряму, а пара export display — за своїм елементом експорту, тоді як значення поза /Opt правильно не лишає індексу — застарілий /I, що вказує на не той рядок, був би гіршим за жоден

Коли нічого не збігається, /I не записується взагалі. Це правильний результат для редагованого combo box, де §12.7.4.4 дозволяє користувачу ввести значення поза списком опцій; таке значення не має індексу, і застарілий індекс був би гіршим за жоден. Це ж ви отримаєте, якщо передасте мітку для показу замість значення експорту у спареному списку опцій, тож коли combo box відмовляється показувати ваш вибір, перевірте, яку половину пари ви дали

// /Opt це [[US United States] [CA Canada] [MX Mexico]]:
// збіг за значенням експорту, і /I стає [1]
Pdf.SetFormFieldValue('Country', 'CA');

// Редагований combo зі значенням поза /Opt: /V записано,
// /I вилучено, і жоден індекс не вигадано
Pdf.SetFormFieldValue('Title', 'Principal Engineer');

Значення і вигляд — дві окремі операції

SetFormFieldValue ніколи не торкається потоку вигляду текстового поля чи поля вибору. Після виклику /V містить новий текст, поки /AP /N усе ще малює старий, і що з цих двох покаже переглядач, залежить від того, чи несе словник AcroForm /NeedAppearances true за §12.7.3.3 і чи шанує це переглядач. Якщо вам потрібно, щоб файл рендерив нове значення в кожному читачі, включно з вирівнювачами й генераторами мініатюр, які ігнорують прапорець, викличте EnsureLoadedFieldAppearanceStream з індексом поля. Він будує Form XObject зі успадкованого рядка /DA, вирівнювання /Q, компонування comb /MaxLen і значення, розв'язує названий шрифт через ресурси /DR AcroForm, щоб шрифт Type0 зберіг свій власний descendant-шрифт замість деградації до Helvetica, і повертає True, коли бодай один віджет отримав потік. Перевантаження SetFormFieldValue за іменем не повертає індекс, тож візьміть його через GetFormField, який повертає THPDFLoadedFormField, що належить вам і який треба звільнити. Регресійний набір для зміни у v2.752.1 прямо каже про цей поділ: він встановлює значення, викликає EnsureLoadedFieldAppearanceStream, потім рендерить сторінку й перевіряє, що пікселі всередині прямокутника віджета змінилися, а поза ним — ні. Перевірка, що /V змінився, не доводить нічого про те, що побачить користувач

var
  Field: THPDFLoadedFormField;
begin
  Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
  Field := Pdf.GetFormField('Applicant.FullName');
  try
    // Намалювати нове значення в /AP, щоб переглядачі, які ігнорують
    // /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 того словника, який ви адресували, тож він діє на батька радіогрупи або на прапорець, що несе власний /FT; віджет-нащадок, адресований сам по собі, з /FT лише на батьку, цим шляхом не узгоджується. HPDFReconcileChoiceSelection обробляє одне скалярне значення й пише щонайбільше один індекс; list box із множинним вибором і кількома вибраними записами поза межами того, що моделює SetFormFieldValue. Жодна з рутин не перевіряє передане значення проти /Opt чи проти ключів стану «увімкнено», тож одруківка дає прапорець у стані Off або combo без індексу замість винятку. А GetFormFieldValue повертає збережений текст /V таким, як він лежить у словнику, що для шістнадцятково закодованого значення означає шістнадцяткове написання, а не декодований текст

Коли значення вже вставлені й вигляд намальовано, два природні наступні кроки лежать по різні боки від цієї операції. Обмін даними полів із зовнішніми системами масово, а не по одному виклику SetFormFieldValue, — це те, що покриває імпорт і експорт XFDF у Delphi. А коли заповнена форма готова й більше не повинна бути редагованою, вирівнювання полів AcroForm і XFA в Delphi запечатує саме ті стани /AS і потоки вигляду, що описані тут, у статичний вміст сторінок — саме тому привести їх до узгодженості до вирівнювання не є опцією

API редагування завантажених форм із цієї статті, включно з SetFormFieldValue, EnsureLoadedFieldAppearanceStream і графом інкрементального перерахунку, постачається як частина HotPDF Delphi Component для Delphi і C++Builder