登记表、报名表、问卷这类 PDF 表单,发出去时是空白的,收回来要靠人工逐份填;表单本身也常常要改——缺一个输入框,多出一栏已经不需要的勾选项,都得动。用 Acrobat 一类桌面软件处理,一份两份还能忍,成批就只剩手工点。
本文介绍用 Spire.PDF for JavaScript 实现添加、填充和删除 PDF 表单域。它基于 WebAssembly 在浏览器端直接加载、修改与保存 PDF 文档,全过程在本地完成,通过虚拟文件系统(VFS)读写文件,无需后端配合。
本文介绍三个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。
添加表单域
Spire.PDF for JavaScript 提供了一整套表单字段类,覆盖文本框、复选框、单选按钮、下拉框、列表框、按钮与签名域。它们的用法一致:在页面上创建实例、用 Bounds 定位,再交给 doc.Form.Fields.Add() 登记;加载已有文档时,还要先把 doc.AllowCreateForm 设为 true。
| 类名 | 说明 |
|---|---|
PdfTextBoxField |
文本框域 |
PdfCheckBoxField |
复选框域 |
PdfRadioButtonListField |
单选按钮域 |
PdfComboBoxField |
下拉框域 |
PdfListBoxField |
列表框域 |
PdfButtonField |
按钮域 |
PdfSignatureField |
签名域 |
Bounds、BorderWidth、BorderStyle、Required、ReadOnly、Visible、ToolTip 这几项则由所有字段共有。
function App() {
const addFormFields = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 将待处理的 PDF 文件载入 VFS
const inputFileName = '空白登记表.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// 创建 PdfDocument 对象并加载 PDF 文档
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// 对已有文档,必须显式开启表单创建
doc.AllowCreateForm = true;
let page = doc.Pages.get_Item(0);
let uiFont = new pdfModule.PdfFont({
fontFamily: pdfModule.PdfFontFamily.Helvetica,
size: 10
});
const box = (x, y, width, height) => new pdfModule.RectangleF({ x, y, width, height });
const border = 0.75;
// 1. 文本框:姓名
let nameBox = new pdfModule.PdfTextBoxField(page, 'name');
nameBox.Bounds = box(178, 168, 210, 20);
nameBox.BorderWidth = border;
nameBox.BorderStyle = pdfModule.PdfBorderStyle.Solid;
nameBox.Font = uiFont;
doc.Form.Fields.Add(nameBox);
// 2. 文本框:邮箱
let emailBox = new pdfModule.PdfTextBoxField(page, 'email');
emailBox.Bounds = box(178, 206, 210, 20);
emailBox.BorderWidth = border;
emailBox.BorderStyle = pdfModule.PdfBorderStyle.Solid;
emailBox.Font = uiFont;
doc.Form.Fields.Add(emailBox);
// 3. 下拉框:部门
let departmentBox = new pdfModule.PdfComboBoxField(page, 'department');
departmentBox.Bounds = box(178, 244, 210, 20);
departmentBox.BorderWidth = border;
departmentBox.Font = uiFont;
['Engineering', 'Marketing', 'Sales', 'Support'].forEach(function (item) {
departmentBox.Items.Add(new pdfModule.PdfListFieldItem({ text: item, value: item.toLowerCase() }));
});
doc.Form.Fields.Add(departmentBox);
// 4. 单选按钮:性别,每个选项是一个 PdfRadioButtonListItem
let genderBox = new pdfModule.PdfRadioButtonListField(page, 'gender');
['male', 'female'].forEach(function (value, index) {
let item = new pdfModule.PdfRadioButtonListItem();
item.Bounds = box(185.5 + index * 110, 285.5, 13, 13);
item.BorderWidth = border;
item.Value = value;
genderBox.Items.Add(item);
});
doc.Form.Fields.Add(genderBox);
// 5. 列表框:学历
let educationBox = new pdfModule.PdfListBoxField(page, 'education');
educationBox.Bounds = box(178, 320, 210, 52);
educationBox.BorderWidth = border;
educationBox.Font = uiFont;
['Bachelor', 'Master', 'Doctor'].forEach(function (item) {
educationBox.Items.Add(new pdfModule.PdfListFieldItem({ text: item, value: item.toLowerCase() }));
});
doc.Form.Fields.Add(educationBox);
// 6. 复选框:同意条款
let agreeBox = new pdfModule.PdfCheckBoxField(page, 'agree_terms');
agreeBox.Bounds = box(178, 392, 15, 15);
agreeBox.BorderWidth = border;
agreeBox.Style = pdfModule.PdfCheckBoxStyle.Check;
agreeBox.Required = true;
doc.Form.Fields.Add(agreeBox);
// 7. 签名域:留出签名位置
let signatureBox = new pdfModule.PdfSignatureField(page, 'signature');
signatureBox.Bounds = box(178, 424, 210, 40);
doc.Form.Fields.Add(signatureBox);
// 8. 按钮:提交
let submitButton = new pdfModule.PdfButtonField(page, 'submit');
submitButton.Bounds = box(72, 478, 90, 26);
submitButton.Text = '提 交';
submitButton.HighlightMode = pdfModule.PdfHighlightMode.Push;
doc.Form.Fields.Add(submitButton);
const outputFileName = '添加表单域.pdf';
doc.SaveToFile(outputFileName);
doc.Close();
// 从 VFS 读取生成的文件,触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>添加表单域</h1>
<button onClick={addFormFields}>
开始添加
</button>
</div>
);
}
export default App;
空白登记表上补齐了文本框、复选框、单选按钮、下拉框、列表框、按钮与签名域:

填充表单域
填充现有表单要走 Widget 视角:PdfFormWidget 包住文档的表单,FieldsWidget 逐个给出字段实例,判断类型后转成对应子类再写值——文本用 Text,复选框用 Checked,下拉框用 SelectedIndex。字段之间靠 Name 区分,遍历一遍就能把整份表单填满。
function App() {
const fillFormFields = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 将待填写的 PDF 文件载入 VFS
const inputFileName = '员工登记表.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// 创建 PdfDocument 对象并加载 PDF 文档
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// 从表单拿到 Widget 集合
let formWidget = new pdfModule.PdfFormWidget(doc.Form.H);
for (let i = 0; i < formWidget.FieldsWidget.Count; i++) {
let field = formWidget.FieldsWidget.get_Item({ index: i });
// 文本框:直接写入 Text
if (field instanceof pdfModule.PdfTextBoxFieldWidget) {
switch (field.Name) {
case 'name':
field.Text = 'Chen Jing';
break;
case 'email':
field.Text = '该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。';break;}}// 下拉框:SelectedIndex 接收的是下标数组 if (field instanceof pdfModule.PdfComboBoxWidgetFieldWidget){if (field.Name === 'department'){field.SelectedIndex = [1];}}// 复选框:Checked 置为 true 即勾选 if (field instanceof pdfModule.PdfCheckBoxWidgetFieldWidget){if (field.Name === 'agree_terms'){field.Checked = true;}}}const outputFileName = '填写表单域.pdf';doc.SaveToFile(outputFileName);doc.Close();// 从 VFS 读取生成的文件,触发下载 const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);const blob = new Blob([fileArray],{type:'application/pdf'});const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = outputFileName;a.click();URL.revokeObjectURL(url);};return ( <div style={{textAlign:'center',height:'300px'}}><h1>填充表单域</h1><button onClick={fillFormFields}>开始填写 </button></div>);}export default App;文本框、下拉框与复选框都已写入对应内容:

删除表单域
删除同样从 FieldsWidget入手,先按 Name找到目标实例,再调用 Remove()把它从字段集合里摘掉。用字段名定位比按下标稳妥,文档改过版式也不会删错。
function App(){const deleteFormField = async () =>{// 获取 Spire.PDF WASM 模块 const pdfModule = window.wasmModule?.spirepdf;// 检查模块是否就绪 if (!pdfModule){alert('Spire.PDF is not ready yet');return;}// 将待处理的 PDF 文件载入 VFS const inputFileName = '员工登记表.pdf';await window.spire.FetchFileToVFS(inputFileName,"",`${process.env.PUBLIC_URL}/data/`);// 创建 PdfDocument 对象并加载 PDF 文档 let doc = new pdfModule.PdfDocument();doc.LoadFromFile(inputFileName);let form = doc.Form;if (form != null){let formWidget = new pdfModule.PdfFormWidget(form.H);// 按名称定位目标字段并移除 for (let i = 0;i <formWidget.FieldsWidget.Count;i++){let field = formWidget.FieldsWidget.get_Item({index:i});if (field.Name === 'name'){formWidget.FieldsWidget.Remove(field);break;}}}const outputFileName = '删除表单域.pdf';doc.SaveToFile(outputFileName);doc.Close();// 从 VFS 读取生成的文件,触发下载 const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);const blob = new Blob([fileArray],{type:'application/pdf'});const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = outputFileName;a.click();URL.revokeObjectURL(url);};return ( <div style={{textAlign:'center',height:'300px'}}><h1>删除表单域</h1><button onClick={deleteFormField}>开始删除 </button></div>);}export default App;「姓名」文本框已从表单中移除:

常见问题
新增的表单域在阅读器里点不动
原因:AllowCreateForm默认为 false。用 LoadFromFile打开已有文档时,Spire.PDF 会沿用文档原有的表单结构,不允许追加字段;此时调用 doc.Form.Fields.Add()不会报错,但保存出来的文档里看不到新字段。
解决:加载文档后、添加字段前,把该属性打开:
doc.LoadFromFile(inputFileName);doc.AllowCreateForm = true;填充后字段仍然是空的
原因:case分支里的字段名与文档中的实际名称不是逐字符相同。PDF 字段名区分大小写,也保留首尾空格,形如 company_name (末尾带空格)的名称在代码里写成 company_name就永远匹配不上。另一种常见写法错误是拿 doc.Form.Fields里的对象直接赋值,那里的字段没有 Text这类 Widget 属性。
解决:填充前先把所有字段名打印一遍,照着复制;赋值必须落在 PdfFormWidget给出的 *FieldWidget实例上:
let formWidget = new pdfModule.PdfFormWidget(doc.Form.H);for (let i = 0;i <formWidget.FieldsWidget.Count;i++){console.log(formWidget.FieldsWidget.get_Item({index:i}).Name);}删除一个字段,后面的字段也跟着不见了
原因:Remove()会立即改变 FieldsWidget.Count,被删元素之后的字段整体前移一位。如果正序遍历并在循环里连续删除,下一次迭代的 i已经跳过了前移过来的那个元素。
解决:一次只删一个就 break;要删多个时按名称收集目标后逐个处理,或者从末尾倒序遍历:
// 倒序遍历,逐个移除名称以 temp_ 开头的字段 for (let i = formWidget.FieldsWidget.Count - 1;i >= 0;i--){let field = formWidget.FieldsWidget.get_Item({index:i});if (field.Name.startsWith('temp_')){formWidget.FieldsWidget.Remove(field);}}获取免费许可证
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。









