PDF 表单

用 LaTeX 制作 PDF 表单——可填写的文本框、复选框——时,最先踩到的雷总是同一个。写下 \TextField{Name} 再编译,得到的 PDF 没有报错、没有警告,也没有任何输入框。标签「Name」被排进了正文,pdfinfo 回答 Form: none,翻查文件内部也找不到一个输入部件(Widget 注释)。因为在 Form 环境之外,hyperref 的字段命令静悄悄地什么都不生成。本页从 \begin{Form} 讲起,用实测追踪每种字段究竟往 PDF 里写了什么、提交按钮默认把数据送到哪里、以及到什么程度就该放弃、改做网页表单。

没有 Form 环境就不会生成字段

所有交互字段都要放在 \begin{Form} … \end{Form} 内部。这不是书写习惯问题,而是真的会改变输出。把 \TextField\CheckBox 放在环境之外编译,pdflatex 会以退出码 0 正常结束、不给任何警告,但生成的 PDF 中 /Widget 注释是 0 个pdfinfoForm: 一栏仍是 none。把同样的命令移进环境内,pdfinfo 就会回答 Form: AcroForm。PDF 表单的结构是由一个 AcroForm 词典统管所有字段,而 Form 环境正是创建该词典的那一步。另外,只写 \usepackage{hyperref} 就够了,不需要 [pdftex] 之类的驱动选项——查看日志会发现 hpdftex.def 是自动载入的。

数一数 hyperref 的源码,Form 环境接受的选项只有 4 个action(送往何处)、methodencodingNeedAppearances。字段的外观与行为都在各自命令的 [...] 中设定,所以把环境的选项理解为「提交设置」就够了。

latex
\documentclass{article}
\usepackage{hyperref}          % no driver option needed
\begin{document}
\begin{Form}[action={https://example.org/collect},method=post]
  \TextField[name=fullname,width=6cm]{Name}\par
  \CheckBox[name=agree]{I agree}\par
  \ChoiceMenu[combo,name=affil]{Affiliation}{University,Company,Other}\par
  \Submit{Send}\quad\Reset{Clear}
\end{Form}
\end{document}

字段种类,以及各自往 PDF 里写了什么

字段命令共 5 个,落到 PDF 侧只剩 3 种字段类型:文本是 /Tx,选择是 /Ch,凡是按钮形态的——复选框、按钮、提交、重置——一律是 /Btn。把上面的例子编译后取出对象,看到的正是:/Tx 两个、/Ch 一个、/Btn 四个。把按钮归为一类是 PDF 规范本身的规定;复选框与按钮的差别不体现在类型上,而体现在标志位/Ff)的比特上。这种「由标志位决定性格」的结构,会在下一节发挥作用。

命令PDF 字段类型生成内容
\TextField/Tx文本输入框;multilinepasswordmaxlen 可改变其性质
\CheckBox/Btn复选框;默认值为 /Off,用 checked 可让它初始为选中
\ChoiceMenu/Ch/Btncombo 是可编辑下拉框,popdown 是列表框,radio 是单选组(会变成 /Btn
\PushButton/Btn按钮;在 onclick= 中写 JavaScript 会生成 /S /JavaScript 动作
\Submit / \Reset/Btn/S /SubmitForm/S /ResetForm字段名恒为 SubmitReset,参数只是显示的文字

省略 name= 时标签就成了字段名——以及单选组的陷阱

不写 name= 时,标签文字就直接成了字段名。查看由 \TextField{Your name} 生成的 PDF,字段名是 /T (Your name)——连空格一起。对接收数据的一方来说这名字很别扭,而写中文标签就会得到中文字段名。实务上的做法是:一定要用 name= 明确给出 ASCII 标识符。还有第二个后果:同一个 name= 写两次,PDF 会把同名字段视为同一个字段。放两个 name=dup\TextField,会生成两个都带 /T (dup) 的对象,在其中一个里输入,另一个也会变成相同的值。当你有意让同一个值在两处显示时这很方便,但若是不小心撞名,就会造成难以追查的故障。

单选按钮还藏着更深的问题。编译 \ChoiceMenu[radio,name=r1]{Pick}{a,b,c},会得到 3 个同名为 r1/Btn 对象,但 AcroForm 词典的 /Fields 数组里只有第一个。另外两个悬在半空,没有任何字段引用它们。把文件交给 qpdf,它会两次给出警告:WARNING: this widget annotation is not reachable from /AcroForm in the document catalog。PDF 规范要求单选组由一个父字段通过 /Kids 统管子项,而 hyperref 却把它们平铺开来。因为不少查看器照样能显示,这个问题很难被察觉——但它在严格的 PDF 处理程序或自动提取工具下可能崩坏。若选项是固定的,用 combopopdown 比单选更稳妥。

常用选项:哪些会变成标志位,哪些不会

每个字段都能在 [...] 中接受大量选项(hyperref 定义的键接近三十个)。常用的有 name=width=/height=default=(初始值)、bordercolor/backgroundcolorcharsize(文字大小)、align(0=左、1=中、2=右)、maxlen=(最大字符数)、menulength=(列表显示行数)。其中只有 multilinereadonlypassword 三个是不带值的开关,与 PDF 标志位一一对应:hyperref.sty 自 5283 行起把 ReadOnly 定为第 1 位、Multiline 为第 13 位、Password 为第 14 位;实际编译后读取 /Ff,得到的正是 1、4096、8192。相反,maxlen=5 根本不是标志位,而是写成独立的 /MaxLen 5 项。分清这一点,就知道当某个选项不按预期生效时该去看哪里。

latex
\begin{Form}
  \TextField[name=notes,multiline,width=8cm,height=3cm]{Notes}\par
  \TextField[name=locked,readonly,width=4cm,default={fixed}]{Locked}\par
  \TextField[name=short,maxlen=5,width=3cm]{Max 5}\par
  \TextField[name=email,width=5cm,align=0,
             bordercolor={0 0 0},backgroundcolor={1 1 0.9}]{Email}
\end{Form}

\Submit 默认发送 FDF——只写 method=post 是不够的

这是本页最要紧的一点。写下 \begin{Form}[action={https://example.org/collect},method=post] 并按下提交按钮,送到服务器的并不是 HTML 表单的 POST,而是 FDF —— Acrobat 自有的数据格式。 原因在 hyperref.sty 第 5371 行:\def\Fld@export{fdf} 把默认导出格式设为 FDF。实际编译后取出提交动作,会看到 /S /SubmitForm 中根本没有 /Flags 这一项,即所有标志位为 0,也就是 FDF。那 method=post 呢?读一下第 5378 行开始的 \HyField@FlagsSubmit 便知:method 所设置的 GetMethod 标志只在 HTML 与 PDF 分支中使用,在 FDF 分支里被完全忽略。也就是说,单写 method=post 毫无作用。

若想让普通 Web 服务器接收,就给 Form 环境加上 encoding=html。这是一个专用键,会在 hyperref.sty 第 5665 行附近执行 \def\Fld@export{html};加上它重新编译后,提交动作里会出现 /Flags 4——第 3 位 ExportFormat 被置位,也就是变成了 HTML 格式。顺带一提,若在 encoding 中写 html 以外的值,只会得到 Form 'encoding' key with unknown value 的警告,然后被悄悄忽略。可选的导出格式还有 xfdf(FDF 的 XML 版)和 pdf(把填好的整份 PDF 一并发送)。

latex
% FDF (the default) -- your endpoint receives an Acrobat-specific blob
\begin{Form}[action={https://example.org/collect},method=post]

% an ordinary HTML form post -- note encoding=html
\begin{Form}[action={https://example.org/collect},encoding=html,method=post]

真实查看器里会发生什么,以及何时该放弃

hyperref 生成的表单根本不把输入框的外观写进文件,而是在 AcroForm 词典里置 /NeedAppearances true,请查看器自行绘制控件。Acrobat Reader 会响应这个请求,但支持程度因查看器而异:在浏览器内置的 PDF 显示或轻量查看器里,可能看不到边框,或者有框却无法输入。至于 \PushButton[onclick=...] 里的 JavaScript,能运行它的查看器更是少数。同样的性质还会波及别处:依赖 /NeedAppearances 的结构无法符合 PDF/A。 只要放一个输入框,veraPDF 就会以 clause 6.3.3「An annotation does not contain an appearance dictionary」判定不合格(PDF/A 一页有详述)。

综合以上,选择 PDF 表单的理由相当有限。 若想做验证、脚本这类复杂功能,还有 insdljs 和 AcroTeX 的 eforms 可选,但堆到那一步也无法保证「在对方的查看器里能不能跑」。如果只是想在线收集回答,坦率的结论是:网页表单更可靠也更快。反过来,PDF 表单真正合适的场景是:以纸质分发为前提的表格,接收方恰好在电脑上填好后打印或存成 PDF——也就是根本不用提交功能的情形。这种用途下,光是「栏位可输入」就已经很有用;再配合 readonly 固定的栏位,就能作为稳定的模板使用。