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

Додавання полів AcroForm до завантаженого PDF у Delphi

У вас є сторонній шаблон рахунка або заархівований контракт, який хтось згенерував роки тому в програмі, що вже ніде не знайдеш, а вимога проста, зробити його інтерактивним: поставити поле підпису в кутку, додати кілька текстових полів, перетворити плоский чекліст на справжні прапорці. Проблема в тому, що ви не створюєте цей PDF з нуля. Він уже існує, у нього вже є сторінки, потоки вмісту та шрифти, якими ви не керуєте, і вам потрібно прищепити до цього графа об'єктів віджети AcroForm без перебудови документа. Це зовсім інша задача, ніж створення форми на новому файлі, і підступний момент не видно, доки ви не відкриєте результат у переглядачі та не виявите, що полів, які ви щойно створили, на сторінці немає

HotPDF це нативний VCL-компонент PDF для Delphi та C++Builder, і починаючи з v2.247.0 він надає окрему групу методів саме для цього, для створення всіх шести стандартних типів полів безпосередньо в документі, завантаженому через LoadFromFile. Ця стаття пояснює, що роблять ці методи, який словник ISO 32000-1 вони будують, і який один прапорець потрібен, інакше вся операція тихо створить файл, що виглядає порожнім

Чому створення полів у завантаженому документі йде окремим шляхом

Коли ви будуєте PDF з нуля, HotPDF контролює всю модель об'єктів. Кожна сторінка це доступна для запису обгортка THPDFPage, а додавання текстового поля через AddTextField під'єднує новий віджет до об'єкта анотацій сторінки, самого об'єкта сторінки та колекції полів форми, а потім генерує потік зовнішнього вигляду з ресурсів шрифтів документа. Потік зовнішнього вигляду це видима поверхня віджета, рамка, межа та будь-який текст за замовчуванням, намальовані як оператори PDF, які переглядач відтворює дослівно

Завантажений документ не дає вам жодного з цих допоміжних елементів. Сторінки приходять як сирі словники, немає доступної для запису обгортки THPDFPage, до якої можна причепити віджет, і що важливіше, немає готового конвеєра ресурсів шрифтів, який би намалював appearance streams. Тому шлях для завантаженого документа працює інакше. Він записує словники полів прямо у розібраний граф об'єктів і звертається до сторінок за нульовим індексом, а не через об'єкт сторінки. Типи полів і біти прапорців збігаються з шляхом створення з нуля, тож поле Text це поле Text у будь-якому разі, змінюється саме внутрішня механіка і, що найважливіше, спосіб малювання поверхні віджета

Прапорець /NeedAppearances тут не є необов'язковим

Це єдиний факт, який вирішує, чи буде видно вашу роботу. Оскільки шлях для завантаженого документа не генерує appearance streams, щойно доданий віджет потрапляє до переглядача без запису /AP, тобто як поле без описаної поверхні. Багато переглядачів, якщо їх попросити намалювати віджет без appearance і без вказівки згенерувати його, не показують нічого. Поле є у файлі, структурно коректне, доступне для інструментів заповнення форми, і повністю невидиме для людини

Вихід визначений в ISO 32000-1 §12.7.3: словник AcroForm містить булевий прапорець /NeedAppearances, і коли він true, сумісний читач зобов'язаний сам побудувати відсутні appearance streams з рядка /DA, тобто default appearance, для кожного поля та його значення. HotPDF робить це за вас. Перший раз, коли ви додаєте будь-яке поле до завантаженого документа, EnsureLoadedAcroForm спрацьовує: якщо в каталозі немає /AcroForm, він створює його, якщо немає масиву /Fields, він створює і його, а ще примусово встановлює /NeedAppearances true. Ви не викликаєте цей механізм напряму, але якщо знати, що він існує, поведінка стає зрозумілою. Це також пояснює важливе обмеження для розгортання: деякі мінімальні або несумісні переглядачі ігнорують /NeedAppearances і все одно не малюють нічого. Для основних читачів прапорець працює, але якщо ваша аудиторія використовує незвичайний вбудований рендерер, перевірте це там, перш ніж щось обіцяти

Додавання шести типів полів

Усі методи мають однакову форму. Ви передаєте нульовий індекс сторінки, чотири кути прямокутника віджета в координатах PDF user space, ім'я поля та будь-які додаткові аргументи, які потрібні конкретному типу. Прямокутник це X1, Y1, X2, Y2, а початок координат PDF розташований унизу ліворуч сторінки, тож більші значення Y знаходяться вище; це координатна система формату файлу, а не екранна система з верхнім лівим кутом, і друга найпоширеніша помилка після забутого прапорця саме тут. Кожен виклик повертає нульовий індекс нового поля або -1, якщо індекс сторінки виходить за межі або об'єкт сторінки не вдалося розв'язати

var
  Pdf: THotPDF;
  Idx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('contract.pdf') <= 0 then Exit;

    // Text field: name, initial value, max length (0 = unlimited)
    Idx := Pdf.AddLoadedTextField(0, 72, 680, 320, 700, 'FullName', '', 0);

    // CheckBox: export value, initial checked state
    Pdf.AddLoadedCheckBox(0, 72, 640, 90, 658, 'AgreeTerms', 'Yes', False);

    // Signature field: just a name and a rectangle
    Pdf.AddLoadedSignatureField(0, 360, 72, 540, 132, 'ApproverSig');

    if Idx >= 0 then
      Pdf.SaveLoadedDocument('contract-interactive.pdf');
  finally
    Pdf.Free;
  end;
end;

Третій і четвертий рядкові аргументи текстового поля це ім'я поля та його початкове значення /V; цілий аргумент це /MaxLen, який записується лише тоді, коли він більший за нуль. HotPDF задає для кожного редагованого поля рядок appearance за замовчуванням /Helv 12 Tf 0 0 0 rg, і саме його читає переглядач, що поважає /NeedAppearances, щоб вирішити, яким шрифтом і кольором малювати значення. Прапорець checkbox приймає export value, тобто рядок, який форма надсилає, коли прапорець увімкнений, а також булеве значення початкового стану; всередині він записує відповідні іменовані записи /V, /AS і /DV, щоб стан on/off був узгодженим у момент відкриття файлу. Якщо export value порожнє, за замовчуванням використовується Yes, традиційна назва увімкненого checkbox

Поля вибору і біти /Ff

ComboBox і ListBox це обидва поля вибору, тип поля /Ch в ISO 32000-1 §12.7.4. Різниця між випадаючим списком і прокручуваним списком це один біт у цілому прапорців поля /Ff: біт 18, прапорець Combo, значення $40000. HotPDF встановлює цей біт для AddLoadedComboBox і залишає його вимкненим для AddLoadedListBox; в іншому ці два варіанти ідентичні, і обидва приймають свої варіанти як відкритий масив рядків, записаний у запис /Opt

// Dropdown (Combo flag set internally) with an initial selection
Pdf.AddLoadedComboBox(0, 72, 600, 300, 620, 'Country', 'Canada',
  ['United States', 'Canada', 'Mexico']);

// Scrolling list, no initial value
Pdf.AddLoadedListBox(0, 72, 520, 300, 590, 'Priority', '',
  ['Low', 'Normal', 'High']);

// Push button with a caption drawn through /MK
Pdf.AddLoadedPushButton(0, 360, 600, 480, 626, 'SubmitBtn', 'Submit');

Дві примітки щодо списку опцій. HotPDF записує кожен запис /Opt як звичайний рядок, де export value і показана мітка це один і той самий текст. ISO 32000-1 §12.7.4.4 також дозволяє двоелементну форму [export display], коли потрібно, щоб надіслане значення відрізнялося від того, що читає користувач; методи створення для завантаженого документа використовують простішу однорядкову форму, тож якщо вам потрібні різні export і display значення, ви задаєте їх у вже створеному словнику самостійно. А значення, яке ви передаєте як поточний вибір поля, має бути одним із наданих варіантів, бо переглядач звіряє його зі списком

Push button це інший випадок, який керується прапорцем: тип поля /Btn з бітом 17, прапорцем PushButton, значення $10000. Саме цей біт відрізняє клікабельну кнопку від checkbox, який теж є полем /Btn, але без нього. Підпис, який ви передаєте, записується в словник appearance characteristics /MK як звичайний підпис /CA. Тут варто чесно сказати про межі можливого: кнопка створюється з міткою та прямокутником, але метод створення для завантаженого документа не приєднує дію, тож сама по собі це кнопка, яка виглядає правильно, але нічого не робить при натисканні. Підключення submit, reset або JavaScript actions це окрема задача; для авторингу з нуля відповідний сценарій полів плюс дій розглянуто в створенні полів і дій AcroForm у Delphi, і це правильна точка порівняння з тим, що шлях для завантаженого документа навмисно не робить

Словник, спільний для всіх полів

Під усіма шістьма методами стоїть один спільний builder, який створює widget annotation і реєструє її у двох місцях. Він записує /Type /Annot і /Subtype /Widget, масив /Rect з ваших чотирьох координат, прапорці анотації /F 4, що встановлює біт Print, аби поле відображалося і на папері, і на екрані, ім'я поля /T, тип поля /FT, прапорці /Ff, а також зворотне посилання /P на об'єкт сторінки. Потім він додає нове поле до масиву /Fields у AcroForm і до масиву /Annots цієї сторінки, розв'язуючи непрямі посилання по ходу, щоб розширювати справжні масиви, а не залишати віджет сиротою

Ця подвійна реєстрація важлива, бо віджет, який живе лише в одному зі списків, зламаний у тонкий спосіб. Поле, що є в /Fields, але відсутнє в /Annots сторінки, відоме формі, але ніколи не малюється; зворотний випадок малюється, але невідомий логіці форми. HotPDF тримає обидва списки синхронними при кожному додаванні, і саме таке ведення обліку вам інакше довелося б відтворювати вручну, буквально по специфікації

Кілька чесних меж

Поставте очікування до того, як будувати навколо цього робочий процес. Поведінка flatten-and-regenerate залежить від того, чи переглядач поважає /NeedAppearances, а це покриває Acrobat, сучасні браузерні PDF рушії та поширені настільні читачі, але не є жорсткою гарантією для кожного рендерера в дикій природі. Якщо вам треба отримати файл, у якому поля відображаються однаково всюди, включно з переглядачами, що ігнорують цей прапорець, ви вже в області appearance streams, і шлях авторингу з нуля, який сам малює /AP, буде кращим вибором. Поле підпису так само створюється як порожній віджет підпису, готовий до підписання, а розмістити поле це не те саме, що застосувати криптографічний підпис

Для зміни того, що вже існує, а не для додавання нового, пов'язаною операцією є flattening форми, коли інтерактивні поля вбудовуються назад у статичний вміст сторінки, щоб значення стали постійними і недоступними для редагування; цей зворотний прохід, зокрема й те, як обробляються форми з XFA, розглянуто в flattening XFA і полів AcroForm у Delphi. Додавання полів і flattening полів це два кінці одного життєвого циклу: ця стаття показує, як додати інтерактивність до документа, якому її бракувало, а flattening це спосіб прибрати її після того, як форма виконала своє завдання

Показаний тут API форм для завантаженого документа входить до стандартного HotPDF Component для Delphi та C++Builder, разом із повним довідником з прапорців полів, обробки appearance та решти моделі AcroForm