跳转到内容
getnextpdf.com

从可填写表单到冻结记录:AcroForm 填写与扁平化

Spec: ISO 32000-2, §12.7

一份 PDF 表单有两段生命。它先是可填写的:一组人在里头键入、勾选,或从中选取的有类型字段。然后,当约定达成时,它变成一份冻结记录:那些值被印进页面本身,因此每一个查看器、在每一台设备上,都看见恰恰所约定的内容。NextPDF 构建第一者,并产生第二者,并带一个刻意的保证——它不会在跨越的途中悄悄地把那些值丢掉。

“我填了什么”与“你看见什么”之间的落差,正是表单出错的地方。

一个可填写字段,技术上是一个绘制在页面之上的小型交互式控件。不同的查看器可以把它渲染得各不相同。有些会兑现一个已保存的值,有些会用一个你没有的字体重新生成外观,有些则让读者再次编辑它。对一份你想要协作者持续编辑的草稿而言,那正是要点。对那份所约定内容的已签副本而言,它是一项负担:那份记录不应取决于是哪个应用程序打开它,且它在事后不应可被编辑。

扁平化弥合了这道落差。它取每个字段的当前值,并把它印进页面,作为普通的、不可变的图形——与一个标题或一个 logo 同一类的内容。在那之后,便没有字段可编辑,也没有外观可重新生成。文档在每一处都显示一样东西、同一样东西。

  • 一份 AcroForm 是文档的交互式表单:一棵声明在目录中的有类型字段树(Spec: ISO 32000-2, §12.7)。
  • 每个字段透过一个 widget 注解而变得可见——那个你点击或键入的页面上矩形(Spec: ISO 32000-2, §12.5)。
  • NextPDF 为每一种受支持的非签名表单控件——文本、复选框、单选按钮、列表框与组合框(choice),以及按钮——配备有类型构建器,外加一个把它们写为正规 PDF 对象的字段管理器。
  • 扁平化把每个字段的值渲染进页面内容流,并移除此时多余的交互式表单,留下一份冻结记录。
  • 如果你请求扁平化一份没有页面的文档,NextPDF 不会悄悄地销毁你的字段值。它会保留该表单、警告你,并让你添加一页之后正确地扁平化。

这个心智模型是两层。字段是数据:一个名称、一个类型、一个值,以及一组标志。widget 是图像:一个特定页面上、让查看器与该字段交互的矩形(Spec: ISO 32000-2, §12.5)。一个字段甚至能透过数个 widget 浮现出来——单选按钮组正是这么运作的,数个页面上的选项接到一个底层的值。

NextPDF 给每一种字段类型各自的有类型构建器,因此你永远不必亲手组装一份原始字典。类型替你携带了 PDF 正确的细节。一个复选框、一个单选按钮,以及一个按钮,在规格中全都共享同一种底层表单类型,并以它们的标志来彼此区分;引擎会从你所选的类型设定那些标志,而非要你记住哪一个比特表示“单选”。一个列表框与一个组合框都是 choice 字段;同样地,构建器会挑出正确的编码。你把字段类型用文字陈述一次,而字节随之而来。

扁平化是后半部。扁平化器按拥有它们的页面把 widget 注解分组,用每个 widget 的矩形在那一页上定位,然后把每个值渲染为一小段内容流运算子——设定一个颜色、设定一个文本位置、绘出字形——追加到那一页既有的内容上(Spec: ISO 32000-2, §8.4)。该值不再是一个活的字段,而成为印上的墨水。因为该表单不再携带任何交互式字段,引擎接着便移除 AcroForm 条目:已没有任何东西还需要可交互了。

  1. Declare typed fieldsAdd text, checkbox, radio, choice, and button fields through their typed builders; the engine sets the spec-correct PDF type and flags.
  2. Place the widgetsEach field is drawn as a widget annotation — a rectangle on a chosen page that a viewer can type into, tick, or pick from.
  3. Collect inputShip it fillable: a reader supplies values, or your code sets them, leaving a filled but still-editable document.
  4. Flatten the valuesRender each field's value into the page content stream as graphics; the painted value is now immutable.
  5. Drop the interactive formWith every value baked in, remove the AcroForm so nothing remains editable — a frozen record of what was agreed.
From a fillable form to a frozen record: declare typed fields, draw their on-page widgets, fill in values, then flatten those values into immutable page graphics and drop the now-empty interactive form.

一份小而有代表性的表单:构建几个有类型字段,然后把它扁平化成一份冻结记录。

<?php
declare(strict_types=1);
use NextPDF\Core\Document;
$document = Document::createStandalone();
$document->addPage();
// Typed builders, called straight on the document. You pick the field
// type by choosing its builder method — textField, checkBox, comboBox —
// and you pass the value to freeze at creation time. The engine writes
// the spec-correct PDF type and flags for you.
$document->textField('full_name', x: 40, y: 700, w: 220, h: 18, default: 'Ada Lovelace');
$document->checkBox('agree_terms', x: 40, y: 660, size: 14, checked: true);
$document->comboBox(
'plan',
x: 40,
y: 620,
w: 160,
h: 18,
items: ['Starter', 'Team', 'Enterprise'],
selected: 'Team',
);
// Flatten: the values become immutable page graphics and the
// interactive AcroForm is dropped. The result is a frozen record.
$document->flattenForms();
$bytes = $document->getPdfData();

flattenForms() 之前,这是一份可填写表单。在它之后,那些相同的值被印进页面,且已没有字段可供更改。你藉由选择字段的构建器方法——textFieldcheckBoxcomboBox——来挑选字段类型,因此一个错误的类型无法被编码为一个松散的字符串:一个拼写错误是一个对不存在之方法的调用,会在任何字段被写出之前被捕捉,而非一个悄悄出错的字段。那正是这个引擎其余部分所采取的同一套拒绝猜测的立场;参见一个拒绝猜测的 API

陷阱在于相信填写一个字段就冻结了它。它没有。一个已填写字段仍携带一个活的值,一个有能力的查看器能编辑它,以及一个某些查看器会重新生成的外观。“我设了值”与“文档现在是一份固定记录”是两个不同的状态。唯有扁平化才从一者跨越到另一者,因为唯有扁平化才把值变成不再像字段那样行事的页面图形。

镜像反面的错误,是扁平化一份你仍需要据以收集输入的草稿。一旦扁平化,那些字段就没了——那正是整个要点——因此请扁平化那份你打算让它成为最终版的副本,而非你仍在传阅的那一份。

NextPDF 的表单支持是完整的核心:为常见交互式字段控件配备的有类型构建器、一个表单扁平化器,以及一个把字段写为正规 PDF 对象的字段管理器。本页描述的就是那个核心面向。

AcroForm fields and flattening — edition availability
EditionAvailability
Core

Typed builders for text, checkbox, radio, choice (list box and combo box), and push-button fields; widget placement; a field manager; and a form flattener that bakes values into page graphics. Available in every edition.

ProNot in this edition
EnterpriseNot in this edition

扁平化在设计上是单向的。它移除交互式表单,好让记录固定下来;它不是一个“暂时锁定”的切换,而且没有一个能从被印上的图形重新推导出可编辑字段的“反扁平化”。如果你需要一份人们能继续编辑的副本,就保留未扁平化的表单,并把一份复本扁平化。

扁平化也不是签名。它让文档在普通查看器的意义上变得不可编辑,但它并不以密码学方式证明是谁产生了它、或它自此未曾变更。当记录必须可被证明就是当初所约定的那一份时,请先扁平化、再签名;参见签名在 PDF 中如何安置

最后,一份带标签的、无障碍的表单,与一份已扁平化的表单是不同的关注点。如果那份可填写的版本必须能与辅助技术配合使用,那么字段在仍可交互时就需要无障碍名称与结构;参见什么让 PDF 无障碍

  • AcroForm — 一份 PDF 的交互式表单:声明在文档目录中、让文件可填写的那棵有类型字段树(Spec: ISO 32000-2, §12.7)。
  • 字段 — 一个表单控件的数据侧:一个名称、一个类型、一个值,以及标志。独立于它在页面上看起来如何。
  • widget 注解 — 页面上那个查看器藉以与字段交互的可见、可点击矩形(Spec: ISO 32000-2, §12.5)。一个字段可以有数个。
  • choice 字段 — 一个提供一组选项的字段:列表框把它们展开显示,组合框则显示一个下拉。两者都是同一种 PDF 字段类型。
  • 扁平化 — 把每个字段的当前值渲染进页面、作为不可变图形,并移除交互式表单,产生一份冻结记录。
  • 冻结记录 — 一份已扁平化的文档:它在每一个查看器中都显示一样固定的东西,且已没有字段可供编辑。