Eigent PDF 表单填写技能实战:从可填写字段判定到注解式填充的完整技术路径
2026/9/14 16:06:49 网站建设 项目流程

Eigent PDF 表单填写技能实战:从可填写字段判定到注解式填充的完整技术路径

【免费下载链接】eigentEigent: The Open Source Cowork Desktop - Local and Free Alternative to Claude Cowork and Codex项目地址: https://gitcode.com/GitHub_Trending/ei/eigent

本文以 Eigent 仓库中 PDF 技能的核心文档 forms.md 为主体,完整讲解 Agent 填写 PDF 表单的两条技术路径:针对带 AcroForm 可填写字段的 PDF 走"字段提取—值校验—表单写入"流程,针对扫描件/图片型 PDF 走"结构提取或视觉估测—坐标校验—FreeText 注解写入"流程。读完后你能掌握 scripts 目录下全部 7 个配套脚本的用法、各中间 JSON 的字段含义,以及两套坐标系(PDF 坐标、图像像素坐标)的转换原理与源码实现细节。

1. forms.md 在 PDF 技能中的定位与总体流程

Eigent 仓库内置了一个示例 PDF 处理技能,入口文档 SKILL.md 在"Quick Reference"中明确将"填写 PDF 表单"这一任务指向 FORMS.md:Fill PDF forms | pdf-lib or pypdf (see FORMS.md)。因此 forms.md 是 Agent 在执行"帮我填这张 PDF 表单"类任务时的操作性手册,其核心设计是一个先判定、再分叉的流程:

  1. 第一步永远是运行 check_fillable_fields.py 检查 PDF 是否带有可填写(fillable)表单字段;
  2. 有可填写字段 → 走"Fillable fields"路径(结构化写入表单域);
  3. 无可填写字段 → 走"Non-fillable fields"路径(添加文本注解,先尝试从 PDF 结构提取坐标,失败再回退到视觉估测)。

forms.md 开篇特别强调"必须按顺序执行步骤,不要跳步直接写代码"(CRITICAL: You MUST complete these steps in order. Do not skip ahead to writing code),这是为了保证坐标来源可信、避免凭感觉写死坐标。

1.1 第一步:判定是否存在可填写字段

在 forms.md 所在目录执行:

python scripts/check_fillable_fields <file.pdf>

从源码看,check_fillable_fields.py 的实现极简:用 pypdf 的PdfReader读取文件后调用reader.get_fields(),有返回值则打印This PDF has fillable form fields,否则打印This PDF does not have fillable form fields; you will need to visually determine where to enter data。也就是说,判定依据就是 PDF 是否存在 AcroForm 字段字典,后续所有分支都以此输出为准。

2. 路径一:可填写表单字段(Fillable fields)

当 PDF 有可填写字段时,按以下四步执行,所有脚本均从 forms.md 所在目录(即resources/example-skills/pdf)运行。

2.1 提取字段信息

python scripts/extract_form_field_info <input.pdf> <field_info.json>

该命令生成一个 JSON 文件,列出所有字段。原文档定义的字段格式按类型分为四类:

[ { "field_id": "(字段的唯一 ID)", "page": "(页码,从 1 开始)", "rect": "([left, bottom, right, top] 边界框,PDF 坐标,y=0 为页面底部)", "type": "(\"text\"、\"checkbox\"、\"radio_group\" 或 \"choice\")" }, // 复选框额外带 checked_value 与 unchecked_value: { "field_id": "(字段的唯一 ID)", "page": "(页码,从 1 开始)", "type": "checkbox", "checked_value": "(把字段设为该值即勾选)", "unchecked_value": "(把字段设为该值即取消勾选)" }, // 单选组带 radio_options 列表: { "field_id": "(字段的唯一 ID)", "page": "(页码,从 1 开始)", "type": "radio_group", "radio_options": [ { "value": "(把字段设为该值即选中此单选项)", "rect": "(该单选按钮的边界框)" } // ...其他单选选项 ] }, // 下拉/多选字段带 choice_options 列表: { "field_id": "(字段的唯一 ID)", "page": "(页码,从 1 开始)", "type": "choice", "choice_options": [ { "value": "(把字段设为该值即选中此选项)", "text": "(选项的显示文本)" } // ...其他选项 ] } ]

结合 extract_form_field_info.py 源码,可以补充几个关键实现事实:

  • 类型映射:make_field_dict 将 PDF 字段类型/Tx映射为text/Btn映射为checkbox/Ch映射为choice,无法识别的类型会标记为unknown (…)
  • 复选框取值:复选框的checked_value/unchecked_value来自字段的状态表(/_States_);当两个状态中不含/Off时,脚本会打印提示"勾选/取消值可能不正确,建议对结果做视觉确认"——这是一个明确的降级告警,遇到时应核对输出 PDF。
  • 单选组的来源:radio_group 的组装逻辑 并不是直接读字段,而是遍历每一页的/Annots注释:凡是属于带/Kids/Btn父字段、且每个注释的外观字典/AP/N中恰有一个非/Off值的,就被归入同一radio_groupvalue取自该注释的 on 值,rect取自注释的/Rect
  • 排序规则:最终字段列表按"页码 → 自上而下 → 自左而右"排序(排序键为[page, -y, x]),方便 Agent 按阅读顺序理解表单。
  • 位置缺失的处理:无法从注释定位的字段会被跳过,并打印Unable to determine location for field id: …, ignoring;若字段信息中出现此类提示,说明该字段在 JSON 中缺失,需要人工确认是否影响填写。

2.2 将 PDF 转为 PNG 并分析字段用途

python scripts/convert_pdf_to_images <file.pdf> <output_directory>

该脚本把每一页渲染为一张 PNG。从 convert_pdf_to_images.py 源码看,渲染使用pdf2imageconvert_from_path(pdf_path, dpi=200),即200 DPI;若任一维度超过max_dim=1000像素,会等比缩放到 1000 像素以内再保存为page_1.pngpage_2.png……

原文档在此步的要求是:分析这些图像以确定每个表单字段的用途,务必把 bounding box 的 PDF 坐标转换为图像坐标。注意可填写路径的rect是标准 PDF 坐标(y=0在页面底部),而图像坐标y=0在页面顶部,直接照抄会导致字段上下颠倒,这是本流程中最常见的错误来源之一。

2.3 编写 field_values.json

确定每个字段的值后,创建field_values.json,原文档给出的格式如下:

[ { "field_id": "last_name", "description": "用户的姓氏", "page": 1, "value": "Simpson" }, { "field_id": "Checkbox12", "description": "用户年满 18 岁时勾选的复选框", "page": 1, "value": "/On" } // ...更多字段 ]

约束条件:

  • field_id必须与extract_form_field_info.py输出中的field_id完全一致;
  • page必须与字段信息 JSON 中该字段的page值一致;
  • 复选框要勾选时使用其checked_value的值(如/On);单选组则使用radio_options中某个选项的value

2.4 执行填充

python scripts/fill_fillable_fields.py <input.pdf> <field_values.json> <output.pdf>

原文档说明:该脚本会校验你提供的字段 ID 和值是否有效,若打印错误信息,应修正对应字段后重试。源码层面,fill_fillable_fields.py 的校验覆盖三类错误,任一命中即打印ERROR: …并以退出码 1 终止:

  1. 字段 ID 不存在ERROR: '…' is not a valid field ID
  2. 页码不匹配ERROR: Incorrect page number for '…' (got X, expected Y)
  3. 取值非法(validation_error_for_field_value):复选框的值必须等于checked_valueunchecked_value;单选组/下拉字段的值必须在其选项值列表内。

写入阶段(fill_pdf_fields)使用PdfWriter(clone_from=reader)克隆原文件,按页调用update_page_form_field_values(..., auto_regenerate=False)更新字段值,最后set_need_appearances_writer(True)设置 NeedAppearances 标志——含义是由 PDF 阅读器在打开时重新生成字段外观,因此建议填写完成后用阅读器打开输出 PDF 做一次视觉确认。此外脚本还包含一个对 pypdf 内部get_inherited的 monkey patch(L88-L101),用于修复/Opt选项数组以[值, 显示文本]二元组形式继承时的解析问题,属于 pypdf 兼容层补丁,使用时无需干预。

3. 路径二:非可填写 PDF(Non-fillable fields,注解式填充)

当 PDF 没有可填写表单字段时,填写方式变为在指定坐标叠加文本注解。原文档的策略是:先从 PDF 结构提取坐标(更精确),结构不可用时再回退到视觉估测。

3.1 Step 1:先尝试结构提取

python scripts/extract_form_structure <input.pdf> form_structure.json

该命令创建form_structure.json,包含四部分内容:

  • labels:每个文本元素及其精确坐标(x0, top, x1, bottom,单位 PDF point);
  • lines:定义行边界的水平线;
  • checkboxes:作为复选框的小方形矩形(带中心坐标);
  • row_boundaries:由水平线计算出的各行上/下边界。

从 extract_form_structure.py 源码可以确认三类元素的提取规则(基于 pdfplumber):

元素提取规则
labelspage.extract_words()的每个词,坐标四舍五入到 0.1 pt
linespage.lines中长度超过页宽50%的水平线
checkboxespage.rects中边长同时落在5–15 pt区间、且宽高中差异< 2 pt的近正方形矩形,输出含center_x/center_y
row_boundaries每页内所有水平线 y 值去重排序后,相邻两条线构成一行边界,并给出row_height

结果判定:如果form_structure.json里有有意义的 labels(对应表单字段的文本元素),使用Approach A:结构坐标;如果 PDF 是扫描/图片型、几乎没有标签(例如文本全部显示为(cid:X)之类的编码模式),则使用Approach B:视觉估测

3.2 Approach A:结构坐标(首选)

A.1 分析结构:读取form_structure.json,识别——

  1. 标签分组:相邻文本元素可能组成一个完整标签(如 "Last" + "Name");
  2. 行结构top值相近的标签位于同一行;
  3. 字段列:输入区从标签结束后开始(x0 = label.x1 + 间隙);
  4. 复选框:直接使用结构中的复选框坐标。

原文档在此处声明的坐标系是:y=0 位于页面顶部,y 向下增大(这正是 pdfplumber 的坐标系,与可填写路径中 PDF 注释的/Rect坐标系方向相反,两条路径不要混用)。

A.2 检查缺失元素:结构提取可能漏检部分表单元素,常见情况包括——圆形复选框(只有方形矩形会被识别为 checkbox)、复杂图形(装饰元素或非标准控件)、颜色较浅的元素。如果 PDF 图像中能看到结构 JSON 里没有的字段,对这些字段单独走视觉分析(见混合方案)。

A.3 创建使用 PDF 坐标的 fields.json

  • 文本字段entry x0 = 标签 x1 + 5(标签后的小间隙);entry x1 = 下一个标签的 x0,或行边界;entry top = 标签 topentry bottom = 标签下方行边界线,或标签 bottom + row_height
  • 复选框:直接使用form_structure.json中的矩形坐标,entry_bounding_box = [checkbox.x0, checkbox.top, checkbox.x1, checkbox.bottom]

pages中写入pdf_widthpdf_height(以此声明坐标是 PDF 坐标)。原文档的完整示例:

{ "pages": [ {"page_number": 1, "pdf_width": 612, "pdf_height": 792} ], "form_fields": [ { "page_number": 1, "description": "姓的输入字段", "field_label": "Last Name", "label_bounding_box": [43, 63, 87, 73], "entry_bounding_box": [92, 63, 260, 79], "entry_text": {"text": "Smith", "font_size": 10} }, { "page_number": 1, "description": "US Citizen Yes 复选框", "field_label": "Yes", "label_bounding_box": [260, 200, 280, 210], "entry_bounding_box": [285, 197, 292, 205], "entry_text": {"text": "X"} } ] }

要点:直接使用form_structure.jsonpdf_width/pdf_height与坐标;复选框通常用{"text": "X"}这样的短文本表达勾选。

A.4 校验边界框

python scripts/check_bounding_boxes fields.json

3.3 Approach B:视觉估测(回退方案)

B.1 PDF 转图

python scripts/convert_pdf_to_images <input.pdf> <images_dir/>

B.2 初步识别字段:逐页观察图像,识别表单分区并给出各字段的粗略估计位置——字段标签及其大致位置、输入区(横线、方框或留白)、复选框及其大致位置。每个字段记录近似像素坐标(此阶段不必精确)。

B.3 放大精修(精度关键步骤):对每个字段,裁剪估计位置附近的区域来精确化坐标。使用 ImageMagick 创建放大裁剪:

magick <page_image> -crop <width>x<height>+<x>+<y> +repage <crop_output.png>

其中<x>, <y>是裁剪区域左上角(取粗略估计值减去边距),<width>, <height>是裁剪区域大小(字段区域每侧加约 50px 边距)。原文档的示例——精修估计在 (100, 150) 附近的 "Name" 字段:

magick images_dir/page_1.png -crop 300x80+50+120 +repage crops/name_field.png

(若magick命令不可用,可用相同参数改用convert。)

观察裁剪图确定精确坐标:1) 输入区的确切起点(标签之后);2) 输入区终点(下一字段或边缘之前);3) 输入线/框的上下边界。然后把裁剪内坐标换算回整图坐标:

  • full_x = crop_x + crop_offset_x
  • full_y = crop_y + crop_offset_y

例如裁剪起点为 (50, 120)、裁剪图内输入框起点为 (52, 18),则entry_x0 = 52 + 50 = 102entry_top = 18 + 120 = 138。对每个字段重复此过程,相邻字段可合并到同一次裁剪中处理。

B.4 创建使用精修坐标的 fields.jsonpages中改用image_width/image_height(以此声明坐标是图像像素坐标),并填入放大分析得到的精修像素坐标:

{ "pages": [ {"page_number": 1, "image_width": 1700, "image_height": 2200} ], "form_fields": [ { "page_number": 1, "description": "姓的输入字段", "field_label": "Last Name", "label_bounding_box": [120, 175, 242, 198], "entry_bounding_box": [255, 175, 720, 218], "entry_text": {"text": "Smith", "font_size": 10} } ] }

要点:image_width/image_height必须与 convert_pdf_to_images.py 实际输出图像的像素尺寸一致(注意脚本会把超过 1000px 的图缩放,务必读实际保存后的尺寸,而非 200 DPI 原始渲染尺寸)。

B.5 校验边界框

python scripts/check_bounding_boxes fields.json

3.4 混合方案:结构 + 视觉

当结构提取对大多数字段有效、但漏掉个别元素(如圆形复选框、非常规控件)时使用:

  1. form_structure.json中已检测到的字段用Approach A
  2. 将 PDF 转为图像,对缺失字段做视觉分析;
  3. 对缺失字段使用放大精修(Approach B 的 B.3);
  4. 合并坐标:结构提取来的字段用pdf_width/pdf_height;视觉估测的字段必须把图像坐标换算为 PDF 坐标——
    • pdf_x = image_x * (pdf_width / image_width)
    • pdf_y = image_y * (pdf_height / image_height)
  5. fields.json 中只使用一套坐标系——全部换算为 PDF 坐标并声明pdf_width/pdf_height

4. 统一校验与收尾

4.1 Step 2:填充前必做校验

python scripts/check_bounding_boxes fields.json

check_bounding_boxes.py 做两类检查:

  1. 边界框相交:同页内任意两个label_bounding_boxentry_bounding_box(含同字段的 label 与 entry 之间)发生交集即报FAILURE: intersection between …——相交意味着填充后文本会重叠;
  2. 框高不足:entry 框高度(rect[3] - rect[1])小于entry_text.font_size(未指定时按 14 计算)即报FAILURE: entry bounding box height … is too short …

全部通过时输出SUCCESS: All bounding boxes are valid;错误信息累计到 20 条即中止后续检查,提示先修正再重试。有报错必须先修正fields.json再继续。

4.2 Step 3:执行填充

python scripts/fill_pdf_form_with_annotations.py <input.pdf> fields.json <output.pdf>

填充脚本会自动识别坐标系并完成换算。从 fill_pdf_form_with_annotations.py 源码看其换算逻辑:

  • 按页读取 PDF 实际 MediaBox 尺寸;若该页声明了pdf_width,调用 transform_from_pdf_coords——因为输入框是[x0, top, x1, bottom](y 向下),需翻转成 pypdf 的[left, bottom, right, top](y 向上):bottom = pdf_height - 输入bottomtop = pdf_height - 输入top
  • 若声明的是image_width,调用 transform_from_image_coords——先按pdf/image比例缩放 x、y,再做同样的 y 轴翻转;
  • 文本通过pypdf.annotations.FreeText注解写入,默认font="Arial"font_size=14(加pt后缀)、font_color="000000",且border_colorbackground_color均为None(即无描边无底色,视觉上不破坏原表单外观);没有entry_text.text或文本为空的字段会被静默跳过;
  • 成功后打印Successfully filled PDF form and saved to …与实际添加的注解数量。

4.3 Step 4:验证输出

python scripts/convert_pdf_to_images <output.pdf> <verify_images/>

将填充后的 PDF 转成图像,逐页核对文本落点。若文本位置偏移,按路径排查:

  • Approach A:确认使用的是form_structure.json的 PDF 坐标,且pages声明的是pdf_width/pdf_height
  • Approach B:确认image_width/image_height与实际图像像素尺寸一致、坐标是精修后的像素值;
  • 混合方案:确认视觉估测字段的图像→PDF 坐标换算正确。

5. 配套脚本与实现速查

脚本作用核心实现
check_fillable_fields.py判定是否有可填写字段pypdfget_fields()非空性检查
extract_form_field_info.py导出字段 ID/类型/页码/矩形解析/FT/States/Kids/Annots/AP/N
convert_pdf_to_images.pyPDF 每页转 PNGpdf2image,200 DPI,超 1000px 等比缩放
extract_form_structure.py导出标签/横线/复选框/行边界pdfplumber 的extract_words/lines/rects,含 5–15pt 方形与 50% 页宽横线规则
check_bounding_boxes.py校验边界框相交与框高同页两两相交检测 + 框高 ≥ 字号(默认 14)
fill_fillable_fields.py写入可填写表单域三重校验后update_page_form_field_values+ NeedAppearances
fill_pdf_form_with_annotations.py注解式填充坐标系统自动识别、y 轴翻转、FreeText 注解

6. 实战注意事项

  1. 两条路径的坐标系方向不同:可填写路径的rect是 PDF 原生坐标(y=0底部,来自注释/Rect);注解路径的结构坐标是y=0顶部(pdfplumber 坐标系),由填充脚本在写入时统一翻转。混用方向是文字上下颠倒的最常见原因。
  2. 所有脚本必须在resources/example-skills/pdf目录下运行:forms.md 反复强调 "run from this file's directory",且 fill_fillable_fields.py 通过相对导入复用extract_form_field_info.get_field_info,换目录执行会直接报 ImportError。
  3. 依赖:Python 侧需要pypdfpdfplumberpdf2image(后者依赖 poppler 渲染环境);Approach B 的放大精修依赖 ImageMagick(magick或旧版convert)。
  4. 字段用途必须靠图像确认field_id往往语义不明(如Checkbox12),forms.md 要求转图后"分析图像以确定每个字段的用途"——不要仅凭 ID 名称猜测。
  5. 降级告警要当回事:复选框状态异常、字段无法定位时,两个提取脚本都会打印提示;此时应人工核对输出 PDF 或改走视觉估测。

更广泛的 PDF 处理背景(合并、拆分、文本/表格提取、命令行工具)可继续参考同目录的 SKILL.md 与 reference.md,本文聚焦的表单填写流程以 forms.md 及其 scripts 目录下的实现为准。

【免费下载链接】eigentEigent: The Open Source Cowork Desktop - Local and Free Alternative to Claude Cowork and Codex项目地址: https://gitcode.com/GitHub_Trending/ei/eigent

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询