У вас есть шаблон счета от стороннего поставщика или архивный контракт, который кто-то сгенерировал много лет назад в программе, которую уже не найти, и требуется сделать его интерактивным: вставить в угол поле для подписи, добавить пару текстовых полей, превратить плоский чек-лист в настоящие флажки. Загвоздка в том, что вы не создаете этот PDF с нуля. Он уже существует, у него уже есть страницы, потоки содержимого и шрифты, которыми вы не управляете, и вам нужно добавить к этому объектному графу виджеты AcroForm, не перестраивая его. Это совсем не та задача, что создание формы в новом документе, и та часть, на которой люди спотыкаются, не видна, пока вы не откроете результат в просмотрщике и не увидите, что полей, которые вы только что записали, на странице нет
HotPDF - нативный VCL-компонент PDF для Delphi и C++Builder, и начиная с v2.247.0 он предоставляет отдельное семейство методов именно для этого: создавать все шесть стандартных типов полей прямо в документе, загруженном с LoadFromFile. В этой статье разбирается, что делают эти методы, какой словарь ISO 32000-1 они строят, и тот единственный флаг, без которого вся операция тихо приводит к файлу, который выглядит пустым
Почему создание полей в загруженном документе идет отдельным путем
Когда вы создаете PDF с нуля, HotPDF владеет всей объектной моделью. Каждая страница - это редактируемая THPDFPage обертка, а добавление текстового поля через AddTextField встраивает новый виджет в объект аннотации страницы, объект страницы и коллекцию полей формы, а затем генерирует поток внешнего вида из шрифтовых ресурсов документа. Поток внешнего вида - это видимая поверхность виджета, рамка, граница и любой текст по умолчанию, отрисованные как операторы рисования PDF, которые просмотрщик выводит буквально
Загруженный документ не дает вам ничего из этой опоры. Страницы приходят как сырые словари; здесь нет редактируемой THPDFPage обертки, на которую можно повесить виджет, и, что еще важнее, нет готового конвейера шрифтовых ресурсов, чтобы рисовать потоки внешнего вида. Поэтому путь для загруженного документа идет другим маршрутом. Он записывает словари полей прямо в разобранный объектный граф и обращается к страницам по индексу с нуля, а не по объекту страницы. Типы полей и биты флагов совпадают с путем создания с нуля один в один, так что поле Text остается полем Text в любом случае; меняется то, что скрыто под капотом, и, что особенно важно, то, как рисуется поверхность виджета
Флаг /NeedAppearances здесь не является необязательным
Именно этот один факт решает, увидите ли вы результат. Поскольку путь для загруженного документа не генерирует потоки внешнего вида, только что добавленный виджет приходит к просмотрщику без /AP записи /AP: поле без описанной поверхности. Многие просмотрщики, если попросить их отрисовать виджет без внешнего вида и без указания, как его построить, не выводят ничего. Поле есть в файле, оно структурно корректно, к нему можно обратиться инструментом заполнения форм, и оно полностью невидимо для человека
Выход из этой ситуации определен в ISO 32000-1 §12.7.3: словарь AcroForm содержит /NeedAppearances логический параметр, и когда он равен true совместимый читатель обязан самостоятельно построить отсутствующие потоки внешнего вида из строки /DA по умолчанию и значения каждого поля. HotPDF делает это за вас. В первый раз, когда вы добавляете любое поле в загруженный документ, EnsureLoadedAcroForm выполняется: если в каталоге нет /AcroForm , он создает его, если нет /Fields массива, он создает и его, и принудительно ставит /NeedAppearances true. Вы не вызываете его напрямую, но знание о его существовании объясняет поведение. Это также объясняет важную оговорку для поставки: некоторые минимальные или не вполне соответствующие просмотрщики игнорируют /NeedAppearances и все равно ничего не выводят. Для обычных читателей флаг выполняет свою работу, но если ваша аудитория использует необычный встроенный рендерер, проверьте его до того, как что-то обещать
Добавление шести типов полей
У каждого метода одна и та же схема. Вы передаете индекс страницы с нуля, четыре угла прямоугольника виджета в координатах пользовательского пространства PDF, имя поля и любые дополнительные аргументы, которые нужны этому типу. Прямоугольник задается как 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 задает для каждого редактируемого поля строку внешнего вида по умолчанию /Helv 12 Tf 0 0 0 rg, и именно ее читает просмотрщик, /NeedAppearancesпросмотрщик, учитывающий /NeedAppearances, чтобы определить шрифт и цвет, которым он рисует значение. Флажок принимает экспортное значение, то есть строку, которую форма отправляет, когда флажок отмечен, а также логический параметр для начального состояния; внутри он записывает соответствующие /V, /AS, и /DV записи имени так, чтобы состояния включено и выключено были согласованы в момент открытия файла. Пустое экспортное значение по умолчанию равно Yes, традиционному имени флажка для состояния "вкл"
Поле выбора и битовые флаги /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 запись как обычную строку, где экспортное значение и отображаемая подпись - один и тот же текст. ISO 32000-1 §12.7.4.4 также допускает двухэлементную [export display] форму, когда нужно, чтобы отправляемое значение отличалось от того, что видит пользователь; методы создания для загруженного документа используют более простую одностроковую форму, так что если вам нужны разные экспортное и отображаемое значения, вы бы задали их в получившемся словаре вручную. А значение, которое вы передаете как текущий выбор поля, должно быть одним из заданных вами вариантов, поскольку просмотрщик сопоставляет его со списком
Кнопка PushButton - это еще один случай, управляемый флагом: тип поля /Btn с битом 17, флагом PushButton, значение $10000. Этот бит и отделяет нажимаемую кнопку от флажка, который тоже является /Btn полем, но без него. Передаваемая вами надпись записывается в словарь характеристик внешнего вида /MK как обычная надпись /CA. Здесь стоит честно обозначить границы: кнопка создается со своей надписью и прямоугольником, но метод для загруженного документа не прикрепляет действие, так что сама по себе это кнопка, которая выглядит правильно и ничего не делает при щелчке. Настройка действий submit, reset или JavaScript - отдельный вопрос; на стороне создания с нуля рабочий процесс поле плюс действие рассматривается в создании полей AcroForm и действий в Delphi, и именно это правильная точка сравнения для того, что путь для загруженного документа сознательно оставляет за скобками
Словарь, общий для каждого поля
Под всеми шестью методами лежит один общий строитель, который создает аннотацию виджета и регистрирует ее в двух местах. Он записывает /Type /Annot и /Subtype /Widget, /Rect массив из четырех ваших координат, флаги аннотации /F 4 которые устанавливают бит Print, чтобы поле появлялось и на бумаге, и на экране, имя поля /T, тип поля /FT, флаги /Ff, и /P обратную ссылку на объект страницы. Затем он добавляет новое поле в массив AcroForm /Fields и в массив /Annots этой страницы, разрешая косвенные ссылки по пути, чтобы расширять реальные массивы, а не оставлять виджет сиротой
Эта двойная регистрация важна, потому что виджет, который живет только в одном из двух списков, ломается тонким образом. Поле, присутствующее в /Fields но отсутствующее в массиве страницы /Annots известно форме, но никогда не рисуется; обратная ситуация рисуется, но неизвестна логике формы. HotPDF синхронизирует оба списка при каждом добавлении, и это именно тот учет, который иначе пришлось бы вручную выверять в точном соответствии со спецификацией
Несколько честных ограничений
Сразу задайте ожидания, прежде чем строить на этом рабочий процесс. Поведение сведения и пересоздания зависит от того, соблюдает ли просмотрщик /NeedAppearances, что охватывает Acrobat, современные PDF-движки браузеров и обычные настольные просмотрщики, но не дает жесткой гарантии для каждого рендерера в мире. Если вам нужно получить файл, где поля выглядят одинаково везде, включая просмотрщики, игнорирующие флаг, вы уже находитесь в области потоков внешнего вида, и путь создания с нуля, который рисует /AP за вас, подходит лучше. Поле подписи, в свою очередь, создается как пустой виджет подписи, готовый к подписанию; размещение поля - не то же самое, что применение криптографической подписи
Если нужно изменить уже существующее, а не добавлять новое, связанная операция - это сведение формы, когда вы запекаете интерактивные поля обратно в статический контент страницы, чтобы значения стали постоянными и неизменяемыми; этот круговой путь, включая то, как обрабатываются формы с XFA, разбирается в сведении полей XFA и AcroForm в Delphi. Добавление полей и их сведение - это два конца одного и того же жизненного цикла: эта статья о том, как добавить интерактивность в документ, которому ее не хватало, а сведение - о том, как снять ее обратно, когда форма выполнила свою задачу
Показанный здесь API форм для загруженного документа поставляется в составе стандартного HotPDF Component для Delphi и C++Builder вместе с полной справкой по флагам полей, обработке внешнего вида и остальной модели AcroForm