PDF-форма со значениями полей, но без потоков внешнего вида может выйти из задачи рендеринга на Linux пустыми рамками, не тем шрифтом или выравниванием, либо EHPDFHeadlessRenderError с текстом «Widget field requires a name field type». Headless-рендерер HotPDF рисует временные внешние виды для текстовых, кнопочных и выборочных виджетов, наследуя /FT, /V, /DA и /Q по дереву полей
Где виджет хранит настройки поля
Большинство неприятностей начинается с разделения поля и его виджетов. Словарь поля держит логическую часть элемента управления формы: тип, значение, флаги и внешний вид по умолчанию. Аннотация виджета держит то, что нужно странице: /Rect, рамку, указатель на свою страницу. Поле с одним виджетом обычно слито в единый словарь. Когда одно и то же значение должно появиться на нескольких страницах, виджеты становятся потомками поля и часто не несут ничего, кроме геометрии и ссылки /Parent. ISO 32000-1 §12.7.3.1 делает /FT, /Ff, /V и /DV наследуемыми по этой цепочке (таблица 220), а §12.7.3.3 добавляет к наследуемому набору из таблицы 222 всего два атрибута переменного текста: /DA, который обязателен, но может прийти от предка, и /Q. Словарь интерактивной формы (таблица 218) поставляет общие для документа значения по умолчанию для /DA и /Q, плюс /DR — словарь ресурсов по умолчанию, в котором ищутся имена шрифтов из /DA. /DR в таблице 222 не входит: он принадлежит форме как целому. Рендерер, читающий только словарь виджета, не найдёт в файле ниже ни типа, ни значения, ни /DA и откатится к собственному шрифту по умолчанию
1 0 obj % каталог
<< /Type /Catalog /Pages 2 0 R
/AcroForm << /Fields [10 0 R] /DA (/Helv 0 Tf 0 g) /Q 0
/DR << /Font << /Helv 20 0 R /Cour 21 0 R >> >> >> >>
endobj
10 0 obj % терминальное поле: тип, значение и внешний вид по умолчанию живут здесь
<< /FT /Tx /T (invoice_no) /V (INV-2026-0417)
/DA (/Cour 10 Tf 0 0 0.6 rg) /Q 2
/Kids [11 0 R 12 0 R] >>
endobj
11 0 obj % виджет на странице 1: только геометрия, углы даны начиная с правого верхнего
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
/Rect [300 720 160 700] /F 4 /P 3 0 R >>
endobj
12 0 obj % виджет на странице 2: только геометрия
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
/Rect [160 60 300 80] /F 4 /P 4 0 R >>
endobj
HotPDF ищет каждую запись поля на виджете, а не найдя, поднимается по /Parent на один словарь за раз; побеждает ближайшее значение, и только /DA и /Q откатываются к словарю AcroForm, когда цепочка исчерпана. Ожидаемо для файла выше: оба виджета показывают INV-2026-0417 с выравниванием по правому краю, 10-пунктовым Courier, тёмно-синим, а общесловный /Helv 0 Tf никогда не вступает в игру, потому что объект 10 имеет собственную /DA. Когда поле выходит 12-пунктовым чёрным Helvetica, очевидный подозреваемый — шрифт Courier, отсутствующий на машине Linux. Размер выдаёт с головой: именно это рисует HotPDF, когда /DA не нашлась ни на виджете, ни на каком-либо предке, ни в словаре AcroForm. Так что выгрузите виджет, вручную пройдите его /Parent и найдите, куда производитель положил /DA — например, на словарь поля, на который виджет никогда не ссылается назад
Имена шрифтов из /DA разрешаются по /DR на виджете или родительском поле, затем по /DR AcroForm и по шрифтовым ресурсам страницы, в этом порядке. /DR на уровне поля вне таблицы 222, но он однозначен, когда присутствует, поэтому HotPDF его принимает. Подъём ограничен: цикл в дереве останавливается с «Widget field inheritance contains a cycle», /Parent, не являющийся словарём, — с «Widget Parent requires a dictionary», а цепочка глубже MaxDepth в опциях рендеринга (32 по умолчанию) — с «Widget field inheritance depth budget exceeded». VCL-компонент применяет то же наследование, когда читает унаследованные значения AcroForm и сбрасывает поля
Как headless-рендерер решает, что рисовать?
HPDFRenderHeadlessPagePNG и его файловый вариант запускают резервный синтез только для видимого виджета, у которого /AP отсутствует или равен null; ISO 32000-1 §7.3.7 говорит, что null-значение словаря — то же, что отсутствующая запись, и рендерер обращается с /AP null именно так. Присутствующий нормальный внешний вид всегда побеждает, что бы ни говорил /NeedAppearances, поэтому устаревший внешний вид от раннего заполнения продолжает показываться, пока что-то не перегенерирует его. Загруженный документ не изменяется никогда. Что будет нарисовано, зависит от разрешённых типа поля и флагов:
| Поле | Синтезированный внешний вид |
|---|---|
Однострочный текст, /FT /Tx | Строка /V, выложенная с разрешёнными /DA и /Q внутри /Rect |
Флажок, /FT /Btn | Прямоугольная рамка с радиусами углов из /Border, заливкой /MK /BG и обведённой векторной галочкой во включённом состоянии |
| Переключатель, Btn с битом 16 | Круглая рамка с залитой точкой во включённом состоянии |
| Кнопка, Btn с битом 17 | Прямоугольная рамка с центрированной подписью /MK /CA |
Комбинированный список, /FT /Ch с битом 18 | Отображаемый текст выбранной записи /Opt в виде центрированной подписи |
| Список, Ch с очищенным битом 18 | Строка на опцию начиная с /TI, с подсвечивающей полосой за каждой выбранной строкой |
Галочка и точка переключателя — векторные пути, а не глифы ZapfDingbats, как рисуют их многие генераторы, потому что в тощем контейнере Linux может не оказаться установленного ZapfDingbats. Лица кнопки и комбинированного списка совпадают с тем, что для того же поля рисует VCL-генератор внешних видов HotPDF, поэтому форма выглядит одинаково, с какой стороны ни вышло изображение. Подпись берёт шрифт, размер и цвет из /DA; поскольку компоновка подписи не подгоняется автоматически, размер 0 в /DA становится 70 процентами высоты бокса с потолком в 12 пунктов. Состояние флажка и переключателя берётся из собственного /AS виджета с откатом к унаследованному /V, и любое состояние помимо Off рисует отметку. Давайте каждому потомку-переключателю собственный /AS: /V родителя называет выбранное состояние для всей группы и не отличит одного потомка от другого
uses
SysUtils, HPDFHeadlessDocument, HPDFHeadlessCairoDevice;
function RenderFormPage(const FileName, PngName: string; PageIndex: Integer;
out Diagnostic: string): Boolean;
var
Doc: THPDFHeadlessDocument;
Options: THPDFHeadlessRenderOptions;
Info: THPDFHeadlessRenderResult;
begin
Result := False;
Diagnostic := '';
Doc := THPDFHeadlessDocument.Create;
try
Doc.LoadFromFile(FileName);
Options := THPDFHeadlessRenderOptions.Default; // RenderAnnotations = True, MaxDepth = 32
Options.Usage := hruPrint; // как печать в просмотрщике: виджеты без флага Print пропускаются
try
// PageIndex отсчитывается с нуля
Info := HPDFRenderHeadlessPagePNGFile(Doc.GetRenderAccess(PageIndex),
PngName, Options);
Diagnostic := Format('%d appearances drawn', [Info.Appearances]);
Result := True;
except
on E: EHPDFHeadlessRenderError do
// например 'Widget field requires a name field type'; для заполненной формы не
// перезапускайте с RenderAnnotations := False — он выбросит все виджеты и их значения
Diagnostic := E.Message;
end;
finally
Doc.Free;
end;
end;
Резервный путь текстовых полей рисует однострочные поля, держащие строковое значение. Для любой более богатой текстовой компоновки генерируйте внешний вид в момент заполнения на стороне Windows, где EnsureLoadedFieldAppearanceStream строит недостающий поток /AP до того, как файл будет сведён или отправлен; тогда задача Linux получает настоящие потоки внешнего вида, а headless-рендерер называет любое встреченное поле, которому такой всё ещё нужен. Но судите о выводе Linux по рендереру Linux. RenderLoadedPageToBitmap рисует через бэкенд Windows GDI, который никогда не запускает этот резервный путь, поэтому страница, выглядящая правильно там, не скажет ничего о том, что устройство Cairo выдаст из того же файла
Биты флагов полей нумеруются с единицы
ISO 32000-1 §12.7.3.1 утверждает, что позиции битов в слове /Ff идут с 1 на младшем конце до 32, и таблицы 226, 228 и 230 пользуются этой нумерацией. В Delphi бит N — значит 1 shl (N - 1). Сдвиньте на сам номер бита, и каждая проверка переедет на одну позицию вверх: 1 shl 16, написанный для «переключателя, бит 16», на деле проверяет флаг кнопки, а 1 shl 18, написанный для «комбинированного списка, бит 18», проверяет Edit. Обе неверные маски — настоящие флаги. Код компилируется, работает и раскладывает поля не по тем видам
| Флаг | Таблица | Бит | Маска Delphi |
|---|---|---|---|
| Multiline | 228 (текст) | 13 | 1 shl 12 = 4096 |
| Password | 228 (текст) | 14 | 1 shl 13 = 8192 |
| Comb | 228 (текст) | 25 | 1 shl 24 = 16777216 |
| RichText | 228 (текст) | 26 | 1 shl 25 = 33554432 |
| Radio | 226 (кнопка) | 16 | 1 shl 15 = 32768 |
| Pushbutton | 226 (кнопка) | 17 | 1 shl 16 = 65536 |
| Combo | 230 (выбор) | 18 | 1 shl 17 = 131072 |
uses
Classes, SysUtils, HPDFHeadlessDocument;
const
// ISO 32000-1 нумерует биты Ff с 1 (младший конец), поэтому бит N — 1 shl (N - 1)
TxMultiline = 1 shl 12; // бит 13
TxPassword = 1 shl 13; // бит 14
TxFileSelect = 1 shl 20; // бит 21
TxDoNotScroll = 1 shl 23; // бит 24
TxComb = 1 shl 24; // бит 25
TxRichText = 1 shl 25; // бит 26
TxProducerLayout = TxMultiline or TxPassword or TxFileSelect or
TxDoNotScroll or TxComb or TxRichText;
procedure ListTextFieldsForProducerAppearance(const FileName: string;
Report: TStrings);
var
Doc: THPDFHeadlessDocument;
Field: THPDFHeadlessFormField;
I: Integer;
begin
Doc := THPDFHeadlessDocument.Create;
try
Doc.LoadFromFile(FileName);
for I := 0 to Doc.FormFieldCount - 1 do
begin
// FieldType, Flags и MaxLength приходят уже унаследованными от предков
Field := Doc.GetFormField(I);
if Field.FieldType <> 'Tx' then
Continue;
if (Field.Flags and TxProducerLayout) <> 0 then
Report.Add(Format('%s: flags $%.8x, needs a producer /AP',
[Field.Name, Field.Flags]))
else if Field.MaxLength > 0 then
Report.Add(Format('%s: /MaxLen %d, needs a producer /AP',
[Field.Name, Field.MaxLength]))
else if Field.Value = '' then
Report.Add(Format('%s: blank, fill it or give it an /AP', [Field.Name]));
end;
finally
Doc.Free;
end;
end;
Скан читает флаги уровня поля — именно туда их кладут производители; рендерер также уважает /Ff на отдельном виджете, так что считайте список подспорьем для планирования, а диагностику рендеринга — последним словом. Проверка на пустоту важна, потому что текстовому полю без /V и без /AP нечего синтезировать: рендеринг останавливается с «A text-field widget without a value has no appearance to synthesize», а лекарство — значение или внешний вид прежде, чем файл дойдёт до рендерера
Что значит «Degenerate appearance geometry»?
Это значит, что у прямоугольника или бокса внешнего вида нет площади, и перевёрнутый /Rect — не причина. ISO 32000-1 §7.9.5 позволяет прямоугольнику называть любые два диагонально противоположных угла, и его примечание велит обрабатывающему софту быть готовым их нормализовать. Наивный рендерер, считающий ширину как x2 - x1, получает отрицательное число от [300 720 160 700] и либо отвергает аннотацию, либо зеркалит её внешний вид. HotPDF меняет координаты местами в порядок «левый нижний — правый верхний» до любой проверки размера, одинаково для синтезированных лиц и существующих потоков /AP, поэтому виджет 11 выше отрисовывается так, как будто его углы стоят канонически. Ошибка поднимается только для аннотации с потоком внешнего вида, когда после нормализации /Rect имеет нулевую ширину или высоту, либо /BBox потока схлопывается в линию или точку под своей /Matrix. Когда она появляется, выгрузите /Rect виджета вместе с /BBox и /Matrix потока: у одного из трёх нулевая протяжённость
Виджет без /AP и с /Rect нулевой площади идёт другим маршрутом: резервный синтез его пропускает и продолжает страницу. ISO 32000-2 §12.5.2 освобождает от требования внешнего вида только аннотации Popup, Projection и Link и прямоугольник, схлопнувшийся в единственную точку. Прямоугольник, представляющий собой просто линию, по PDF 2.0 всё ещё обязан иметь внешний вид; рендерер пропускает и его — просто потому, что красить нечего: нет площади
Когда тип поля найти не удаётся
«Widget field requires a name field type» значит, что рендерер поднялся от виджета настолько, насколько дотянулись ссылки /Parent, и не нашёл /FT. Рендерер стартует с массива /Annots страницы, поэтому восходящая ссылка /Parent — его единственная дорога к полю. Загрузка идёт в другую сторону, вниз от массива /Fields AcroForm через /Kids, оттого LoadFromFile может успешно завершиться на файле, чья страница позже откажется рендериться. Типичная причина — виджет, перечисленный в /Kids поля, но не указывающий назад. Таблица 188 ISO 32000-1 даёт здесь некоторое прикрытие: она требует /Parent на виджете, только когда виджет — один из нескольких детей, и говорит «absent otherwise», поэтому писатель, читающий её буквально, роняет ссылку для поля с одним отдельным виджетом. ISO 32000-2 смягчил ту же запись до «optional otherwise». Мы бы всё равно писали /Parent на каждом виджете, не слитом со своим полем. ISO 32000-2 это позволяет, и это единственный способ, которым ридер, ведомый страницей, — наш или чужой — может дотянуться до поля. Если производителя починить нельзя, слияние одинокого виджета в словарь его поля даёт тот же результат
Виджет, ссылающийся на родителя и находящий /DA где-то по пути вверх, отрисовывается правильно без всякого /AP в headless-рендерере компонента HotPDF для Delphi, что делает дерево полей, а не потоки внешнего вида, первым, что нужно выстроить правильно в Linux-конвейере форм