有字段值却没有外观流的 PDF 表单,经 Linux 渲染作业出来可能是空白框、错误的字体或对齐,或者是一条读着 “Widget field requires a name field type” 的 EHPDFHeadlessRenderError。HotPDF 的无头渲染器为文本、按钮和选择框 Widget 绘制临时外观,沿字段树继承 /FT、/V、/DA 与 /Q
Widget 的字段设置存在哪里
麻烦大多始于字段与其 Widget 的分家。字段字典持有表单控件的逻辑部分:类型、值、标志和默认外观。Widget 注释持有页面需要的东西:/Rect、边框、指向其页面的指针。单 Widget 字段通常合并成单个字典。同一个值要出现在多页上时,Widget 变成字段的孩子,往往除了几何信息和 /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 里;它属于表单整体。只读 Widget 字典的渲染器,在下面的文件里找不到类型、值或 /DA,退回自己的默认字体
1 0 obj % catalog
<< /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 页的 Widget:只有几何信息,角点按右上角在前给出
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
/Rect [300 720 160 700] /F 4 /P 3 0 R >>
endobj
12 0 obj % 第 2 页的 Widget:只有几何信息
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
/Rect [160 60 300 80] /F 4 /P 4 0 R >>
endobj
HotPDF 在 Widget 上找每个字段条目,找不到就沿 /Parent 一层字典一层地爬;最近的值获胜,只有 /DA 与 /Q 在链条穷尽后才退到 AcroForm 字典。对上面文件的预期结果:两个 Widget 都以 10 磅的 Courier 右对齐显示 INV-2026-0417,深蓝色,文档级 /Helv 0 Tf 永远没机会出场,因为对象 10 有自己的 /DA。当字段以 12 磅黑色 Helvetica 出来时,显而易见的嫌疑是 Linux 机器上缺 Courier 字体。字号暴露了真相:Widget、任何祖先和 AcroForm 字典里都没有 /DA 时,HotPDF 画的就是这个。所以把 Widget 转储出来,手动沿 /Parent 走一遍,找到生产商把 /DA 放在了哪,比如放在一个 Widget 从不回指的字段字典上
/DA 里的字体名按这个顺序解析:Widget 或父字段上的 /DR、AcroForm 的 /DR、页面的字体资源。字段级 /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 值并重置字段时,应用同一套继承
无头渲染器如何决定画什么
HPDFRenderHeadlessPagePNG 及其文件变体只对 /AP 缺失或为 null 的可见 Widget 走兜底;ISO 32000-1 §7.3.7 说 null 字典值等同于缺席条目,渲染器也这样对待 /AP null。正常外观只要存在就永远获胜,/NeedAppearances 说什么都不算,所以早前填充留下的过期外观会一直显示,直到有什么东西重新生成它。已加载的文档绝不被修改。画什么取决于解析出的字段类型与标志:
| 字段 | 合成外观 |
|---|---|
单行文本,/FT /Tx | 字符串 /V,用解析出的 /DA 与 /Q 在 /Rect 内排版 |
复选框,/FT /Btn | 矩形边框,圆角半径取自 /Border,/MK /BG 填充,选中时描边矢量对勾 |
| 单选按钮,Btn 且第 16 位 | 圆形边框,选中时带实心圆点 |
| 按钮(pushbutton),Btn 且第 17 位 | 矩形边框,/MK /CA 标题居中 |
组合框,/FT /Ch 且第 18 位 | 所选 /Opt 条目的显示文本作居中标题 |
| 列表框,Ch 且第 18 位清零 | 从 /TI 起每选项一行,每个选中行背后有高亮带 |
对勾与单选圆点是矢量路径,而非许多生成器画它们的 ZapfDingbats 字形,因为瘦身的 Linux 容器可能根本没装 ZapfDingbats。按钮与组合框的表面与 HotPDF 的 VCL 外观生成器对同一字段画出的一致,表单无论哪侧出图看起来都一样。标题的字体、字号与颜色取自 /DA;标题排版不自动缩放,/DA 字号为 0 时取框高的 70%,上限 12 磅。复选框与单选状态取自 Widget 自己的 /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 标志的 Widget 跳过
try
// PageIndex 从 0 起算
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 重试,那会丢掉每个 Widget 及其值
Diagnostic := E.Message;
end;
finally
Doc.Free;
end;
end;
文本字段兜底只画持有字符串值的单行字段。更丰富的文本排版请在 Windows 侧填充时生成外观,EnsureLoadedFieldAppearanceStream 会在文件被扁平化或发货之前补建缺失的 /AP 流;Linux 作业收到的就是真实外观流,无头渲染器遇到仍缺一个的字段会指名道姓。但评判 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)。按位号本身移位,每个测试都上移一格:为 “radio,第 16 位” 写的 1 shl 16 实际测的是 pushbutton 标志,为 “combo,第 18 位” 写的 1 shl 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;
扫描读的是字段级标志——生产商习惯把标志放在那里;渲染器也尊重独立 Widget 上的 /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 流一视同仁,所以上面的 Widget 11 渲染起来如同角点是常规顺序。该错误只对这样一种注释抛出:它有外观流,而规范化后 /Rect 宽或高为零,或流的 /BBox 在其 /Matrix 之下塌缩成一条线或一个点。它出现时,把 Widget 的 /Rect 连同流的 /BBox 与 /Matrix 一起转储:三者之一有零尺寸
无 /AP 且 /Rect 零面积的 Widget 走另一条路:兜底跳过它,页面继续。ISO 32000-2 §12.5.2 只豁免 Popup、Projection 与 Link 注释以及塌缩为单点的矩形。仅仅是一条线的矩形在 PDF 2.0 之下仍欠一个外观;渲染器也跳过它,纯粹因为没有面积可涂
找不到字段类型时
“Widget field requires a name field type” 意味着渲染器从 Widget 沿 /Parent 爬到头也没找到 /FT。渲染器从页面的 /Annots 数组起步,向上的 /Parent 链是它通往字段的唯一道路。加载走的是另一头,从 AcroForm /Fields 数组顺 /Kids 向下,所以 LoadFromFile 能在其页面随后渲染失败的文件上成功。典型原因是字段 /Kids 里列了一个不回指的 Widget。ISO 32000-1 表 188 对此给了些掩护:它只要求 Widget 身为若干孩子之一时带 /Parent,并说“否则缺席”,照字面执行的写入器便把单独立 Widget 的那条链接丢了。ISO 32000-2 把同一条目放宽为“否则可选”。反正我们会在每个未与字段合并的 Widget 上写 /Parent。ISO 32000-2 允许这样做,而且这是页面驱动的读取器——无论我们的还是别人的——够到字段的唯一办法。修不了生产商时,把孤立 Widget 合并进其字段字典也能达到同样效果
回指父项、且沿途某处找得到 /DA 的 Widget,无需任何 /AP 就能在 HotPDF Delphi component 的无头渲染器里正确渲染——这让字段树,而非外观流,成为 Linux 表单流水线里第一件要做对的事