与 PDF 表单的交互

读取表单字段

from pypdf import PdfReader

reader = PdfReader("form.pdf")
fields = reader.get_form_text_fields()
fields == {"key": "value", "key2": "value2"}

# 你也可以获取所有字段:
fields = reader.get_fields()

填充表单

from pypdf import PdfReader, PdfWriter

reader = PdfReader("form.pdf")
writer = PdfWriter()

page = reader.pages[0]
fields = reader.get_fields()

writer.append(reader)

writer.update_page_form_field_values(
    writer.pages[0],
    {"fieldname": "some filled in text"},
    auto_regenerate=False,
)

with open("filled-out.pdf", "wb") as output_stream:
    writer.write(output_stream)

一般来说,你总是需要使用 auto_regenerate=False。该参数默认为 True,为了兼容旧版本,但这会标记 PDF 处理程序重新计算字段的渲染,并可能在用户打开生成的 PDF 时触发“保存更改”对话框。

关于表单字段和注释的一些说明

PDF 表单字段具有双重性质:

  • 在根对象中,存在一个 /AcroForm 结构。 其中可能包含(可选):

    • 一些全局元素(字体、资源等)

    • 一些全局标志(例如 /NeedAppearances(通过 update_page_form_field_values() 中的 auto_regenerate 参数设置/清除),指示读取程序是否应在文档启动时重新渲染视觉字段)

    • /XFA,包含一个 XDP 格式的表单(描述由某些查看器呈现的表单的特定 XML);/XFA 表单会覆盖页面内容

    • /Fields,包含一个间接引用数组,引用上级 Field 对象(根对象)

  • 在页面的 /Annots 中,你会发现 /Widget 注释,用于定义视觉呈现。

为了补充此概述:

  • 字段的核心特定属性是:

    • /FT:字段类型(按钮、文本、选择或签名)。

    • /T:字段名称的一部分。

    • /V:字段的值,格式根据字段类型而变化。

    • /DV:当执行重置表单操作时,字段恢复的默认值。

  • 为了简化可读性,Field 对象和 Widget 对象可以合并,包含所有属性。

  • 字段可以进行层次组织,即一个字段可以放置在另一个字段下。在这种情况下,/Parent 将包含一个间接对象,提供自底向上的链接,而 /Kids 是一个包含间接对象的数组,用于自顶向下的导航; Widget 对象仍然需要进行视觉呈现。调用它们时,使用 完全限定的字段名称(所有父对象的名称用 . 分隔)

    例如,假设有两个(视觉)字段都叫 city,但分别附加在 senderreceiver 下;相应的完整名称将是 sender.cityreceiver.city

  • 当一个字段在多个页面上重复时,字段对象将有多个 Widget 对象在 /Kids 中。这些对象是纯粹的 widget,不包含任何 field 特定的数据。

  • 如果字段仅存储隐藏值,则不需要 Widgets

pypdf 中,字段是从 /Fields 数组中提取的:

from pypdf import PdfReader

reader = PdfReader("form.pdf")
fields = reader.get_fields()
from pypdf import PdfReader
from pypdf.constants import AnnotationDictionaryAttributes

reader = PdfReader("form.pdf")
fields = []
for page in reader.pages:
    for annot in page.annotations:
        annot = annot.get_object()
        if annot[AnnotationDictionaryAttributes.Subtype] == "/Widget":
            fields.append(annot)

然而,虽然两段代码相似,但它们之间有一些非常重要的区别。最重要的是,第一段代码会返回一个字段对象的列表,而第二段则返回更通用的类似字典的对象。两个对象列表大多数情况下会引用相同的底层 PDF 对象,这意味着你会发现 obj_taken_from_first_list.indirect_reference == obj_taken_from_second_list.indirect_reference。字段对象通常更为便捷,因为暴露的数据可以通过明确命名的属性进行访问。然而,更通用的类似字典的对象会包含字段对象没有暴露的数据,例如 Rect(widget 在页面上的位置)。因此,正确的方法取决于你的使用场景。

此外,还需要注意的是,这两个列表并不总是指向相同的底层 PDF 对象。例如,如果表单包含单选按钮,你会发现 reader.get_fields() 会获取父对象(单选按钮组),而 page.annotations 会返回所有子对象(单独的单选按钮)。

备注

请记住,字段不存储在页面中;如果你使用 add_page(),字段结构不会被复制。建议使用 .append() 并提供适当的参数。

/Fields 中缺少 field 对象时,writer.reattach_fields() 将解析页面的注释并重新附加它们。此修复不能猜测中间字段,并且不会报告使用相同 name 的字段。

确定字段使用的页面

为了方便定位页面字段,你可以使用 PdfReaderPdfWriterget_pages_showing_field 方法。此方法接受一个字段对象,一个代表字段的 PdfObject(从 _root_object["/AcroForm"]["/Fields"] 提取)。该方法返回一个页面列表,因为一个字段可以有多个小部件,如前所述(例如单选按钮或显示在多个页面上的文本)。

然后,可以像往常一样通过 page.page_number 获取页面号。