导出一份订单明细表或对账单时,页面设置往往决定了打印出来的成品是否易读:表格要不要连网格线和行号列标一起打出来、以多高的精度输出、批注跟不跟着走,这些选项分散在 Excel「页面设置」对话框的多个选项卡里,逐个点选费时费力。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端直接完成页面设置,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。
本文主要介绍以下三个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.XLS for JavaScript。以下示例默认已安装 Spire.XLS 并完成 WebAssembly 模块初始化。
打印质量决定输出时每个点阵的墨滴数,取值是一个以 dpi 为单位的整数;草稿质量则让打印机以更省墨的方式快速输出,适合内部传阅的校样。Spire.XLS for JavaScript 通过 PageSetup 的 PrintQuality 与 Draft 属性设置这两项:
PrintQuality = 72 把打印质量设为 72 dpi,取值偏低,出纸更快、耗材更省Draft = true 开启草稿质量,打印速度优先于精细度
具体完整的示例代码如下:function App() {
const setPrintQuality = async () => {
// 获取 Spire.XLS WASM 模块
const xlsModule = window.wasmModule?.spirexls;
// 检查模块是否就绪
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// 将字体与输入文件载入 VFS
await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'OrderDetails.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// 加载工作簿,取第一个工作表
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
const sheet = workbook.Worksheets.get(0);
// 取得工作表的页面设置
const pageSetup = sheet.PageSetup;
// 把打印质量设为 72 dpi
pageSetup.PrintQuality = 72;
// 开启草稿质量,打印速度优先于精细度
pageSetup.Draft = true;
// 保存工作簿
const outputFileName = 'SetPrintQuality.xlsx';
workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });
// 释放 workbook 对象以释放资源
workbook.Dispose();
// 从 VFS 读取结果文件,触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
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={setPrintQuality}>设置打印质量与草稿质量</button>
</div>
);
}
export default App;
原文件打印效果:
运行后,打印质量与草稿质量的效果:

屏幕上的网格线不会跟着数据一起打印出来,如果表格本身没有设置边框,纸质件上就只剩一片浮空的数据,核对时很难定位单元格。行号列标同理。IsPrintGridlines 与 IsPrintHeadings 两个布尔属性分别控制这两项。具体完整的示例代码如下:
function App() {
const setGridlinesAndHeadings = async () => {
// 获取 Spire.XLS WASM 模块
const xlsModule = window.wasmModule?.spirexls;
// 检查模块是否就绪
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// 将字体与输入文件载入 VFS
await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'OrderDetails.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// 加载工作簿,取第一个工作表
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
const sheet = workbook.Worksheets.get(0);
// 取得工作表的页面设置
const pageSetup = sheet.PageSetup;
// 打印时输出网格线
pageSetup.IsPrintGridlines = true;
// 打印时输出行号与列标
pageSetup.IsPrintHeadings = true;
// 保存工作簿
const outputFileName = 'SetGridlinesAndHeadings.xlsx';
workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });
// 释放 workbook 对象以释放资源
workbook.Dispose();
// 从 VFS 读取结果文件,触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
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={setGridlinesAndHeadings}>打印网格线与行号列标</button>
</div>
);
}
export default App;
运行后,打印网格线与行号列标的效果:

余下的三项输出开关同样落在 PageSetup 上,分别决定纸质件呈现什么颜色、批注是否随表打印、错误值如何显示。三项属性与取值如下:
BlackAndWhite 设为 true 时以黑白模式打印,彩色内容转为灰度输出,只有黑白激光打印机时尤其适用PrintComments 取 InPlace 时,批注框随其在工作表上的位置一起打印;批注需先设为显示状态,未显示的批注不参与打印PrintErrors 取 NA 时,所有错误值(如 #DIV/0!)在纸质件上统一显示为 #N/A,内部公式错误对阅读者不可见具体完整的示例代码如下:
function App() {
const setOtherPrintOptions = async () => {
// 获取 Spire.XLS WASM 模块
const xlsModule = window.wasmModule?.spirexls;
// 检查模块是否就绪
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// 将字体与输入文件载入 VFS
await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'OrderDetails.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// 加载工作簿,取第一个工作表
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
const sheet = workbook.Worksheets.get(0);
// 取得工作表的页面设置
const pageSetup = sheet.PageSetup;
// 样本在 A14 单元格上有一条批注
// 先把批注设为显示状态,未显示的批注不参与打印
sheet.Range.get('A14').Comment.Visible = true;
// 以黑白模式打印工作表
pageSetup.BlackAndWhite = true;
// 批注按其在工作表上的位置打印
pageSetup.PrintComments = xlsModule.PrintCommentType.InPlace;
// 单元格错误值统一打印成 #N/A
pageSetup.PrintErrors = xlsModule.PrintErrorsType.NA;
// 保存工作簿
const outputFileName = 'SetOtherPrintOptions.xlsx';
workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });
// 释放 workbook 对象以释放资源
workbook.Dispose();
// 从 VFS 读取结果文件,触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
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={setOtherPrintOptions}>设置黑白打印、批注与错误值打印</button>
</div>
);
}
export default App;
运行后,黑白打印、批注与错误值打印的效果:

原因:打印质量与草稿质量只决定输出的精度与耗墨量,不改变内容的布局,因此再怎么调低,分页位置都不会移动。要把整张表收进一页,需要设置的是页面缩放,与打印质量无关;两者同时设置时并不冲突,只是各自管各自的事。
解决:用 FitToPagesWide 与 FitToPagesTall 把工作表压缩到指定页数,取 1 表示一页宽、一页高:
// 把工作表缩放打印到一页宽、一页高
pageSetup.FitToPagesWide = 1;
pageSetup.FitToPagesTall = 1;
PrintQuality 与 Draft,工作表看上去没有任何变化原因:这两项都只作用于打印机输出,不改变工作表本身的内容与显示,因此设置完成后再看文档,页面不会有任何不同——这属于正常现象,并不代表设置没有生效。
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
在浏览器端生成报表时,一张订单明细表往往要打印好几页。如果沿用默认的打印设置,第二页之后就会丢失表头和关键列,阅读时无从判断每一列的含义,页码也难以与整份报表的编排衔接。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端直接完成页面设置,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。
本文主要介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.XLS for JavaScript。以下示例默认已安装 Spire.XLS 并完成 WebAssembly 模块初始化。
打印标题对应 Excel「页面设置」对话框里的「顶端标题行」与「左端标题列」两项。设置之后,指定的行或列会在每一页打印输出的相同位置重复出现,表格翻到第二页时依然能看到表头。Spire.XLS for JavaScript 通过 PageSetup 的 PrintTitleRows 与 PrintTitleColumns 属性完成设置,取值是行列区间的引用字符串。具体代码示例如下:
function App() {
const setPrintTitles = async () => {
// 获取 Spire.XLS WASM 模块
const xlsModule = window.wasmModule?.spirexls;
// 检查模块是否就绪
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// 将字体与输入文件载入 VFS
await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'OrderDetails.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// 加载工作簿,取第一个工作表
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
const sheet = workbook.Worksheets.get(0);
// 取得工作表的页面设置
const pageSetup = sheet.PageSetup;
// 把第 1、2 行设为打印标题行,每一页的顶端都重复输出
pageSetup.PrintTitleRows = '$1:$2';
// 把 A、B 两列设为打印标题列,每一页的左端都重复输出
pageSetup.PrintTitleColumns = '$A:$B';
// 保存工作簿
const outputFileName = 'SetPrintTitles.xlsx';
workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });
// 释放 workbook 对象以释放资源
workbook.Dispose();
// 从 VFS 读取结果文件,触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
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={setPrintTitles}>设置打印标题行与打印标题列</button>
</div>
);
}
export default App;
原文件打印效果
运行后,打印标题行与打印标题列的效果:

当工作表同时超出一页宽和一页高时,Excel 需要在两种分页推进方向中做出选择:默认的「先行后列」先自上而下打满一页高,再换到右侧的下一组列;「先列后行」则先沿列方向打满一页宽,再向下推进。Spire.XLS for JavaScript 用 PageSetup 的 Order 属性在两者之间切换。具体代码示例如下:
function App() {
const setPrintOrder = async () => {
// 获取 Spire.XLS WASM 模块
const xlsModule = window.wasmModule?.spirexls;
// 检查模块是否就绪
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// 将字体与输入文件载入 VFS
await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'OrderDetails.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// 加载工作簿,取第一个工作表
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
const sheet = workbook.Worksheets.get(0);
// 取得工作表的页面设置
const pageSetup = sheet.PageSetup;
// 把打印顺序设为「先列后行」:先自上而下打满一列,再向右换到下一列
pageSetup.Order = xlsModule.OrderType.OverThenDown;
// 保存工作簿
const outputFileName = 'SetPrintOrder.xlsx';
workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });
// 释放 workbook 对象以释放资源
workbook.Dispose();
// 从 VFS 读取结果文件,触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
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={setPrintOrder}>设置打印顺序</button>
</div>
);
}
export default App;
运行后,打印顺序设为「先列后行」的效果:

原因:PrintTitleRows 与 PrintTitleColumns 是两个互不影响的属性,前者控制每页顶端重复的行,后者控制每页左端重复的列。只设置其中一个时,另一个保持未设置,不会因为缺了一项而被拒绝,也不会被补上默认值。
解决:按需要设置其中一个即可,例如只重复表头行:pageSetup.PrintTitleRows = '$1:$2'。
原因:打印标题属于页面设置,只决定打印时哪些行列重复输出,不改动单元格的内容,也不增删行列。
解决:数据不会受影响。实测设置前与设置后的结果文件同为 62 行 × 18 列,逐格文本完全一致,差别只在页面设置里多了一条打印标题定义;屏幕上看到的工作表与原来相同。
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
做一份演示文稿,真正花时间的往往不是排版,而是"讲什么"。幻灯片上只放得下要点:几个短句、一组数字、一张图表,真正要讲出口的内容却全在作者脑子里。等到上台前一天,作者还要再把这份 PPT 从头读一遍,逐页回忆"这一页当时想说什么",然后补出一份讲稿。
更麻烦的是团队协作的场景。市场部的发布会材料由产品经理出内容、市场同事做页面、再由发言人上台,最后接手的人拿到的是一份只有要点的 PPT——他既不知道每页想强调什么,也不知道数据背后的结论,只能凭猜测临场发挥。备注页(Speaker Notes)本就是为了解决这个问题而存在的:它不会出现在投影画面上,只在演讲者视图里可见,是"作者写给讲者的话"。但现实中,几乎没有人有耐心逐页去填。
Spire.Agent.Office 的 PowerPoint AI 能力可以直接用自然语言描述"备注要写什么、讲稿要什么口吻",AI 智能体读懂每一页的标题与要点后,自动完成两类工作:为每一页补写演讲备注,以及把整份演示文稿展开成一份可以直接照读的 Word 讲稿。
| 传统 Spire.Office for .NET API | Spire.Agent.Office 处理 | |
|---|---|---|
| 驱动方式 | 遍历幻灯片、取出文本框内容、再自行拼装文案并写回备注页,每一步都要编码 | 用自然语言描述备注与讲稿的写作要求,AI 理解后自动编排执行路径 |
| 代码量 | 需要 150-300 行 C# 代码(含幻灯片遍历、形状遍历、文本提取、备注页写入、字体与段落设置等) | 约 10 行调用代码 + 1 条自然语言指令 |
| 内容生成 | 需要自行接入大模型或手写文案模板,且难以兼顾每页的语境 | AI 直接理解每页要点,按页生成语境连贯的文案 |
| 版式处理 | 写入备注时容易破坏原有版式与占位符结构 | AI 自动保留原版式、字体与配色,只在备注栏追加内容 |
| 需求变更 | 调整语气、字数、讲稿格式 → 改代码 → 编译 → 重新部署 | 修改指令中的描述,即刻生效 |
本文介绍如何使用 Spire.Agent.Office 为 PowerPoint 演示文稿生成演讲备注与讲稿。全文通过两个案例展示"为已有 PPT 补写备注"与"把汇报内容整理成 Word 讲稿"两种典型用法:
有关产品安装和 SpireToken 配置,请参考 在 .NET 项目中集成 Spire.Agent.Office。以下示例默认已安装 Spire.Agent.Office 并完成 SpireToken 配置。
最常见的情况是:PPT 已经定稿,页面版式和内容都不需要再动,缺的只是一份备注。PPT 文件为了便于观看者理解,每页只有标题与几行要点——重点落在哪里、数字背后是什么、这一页怎么接到下一页,都还留在作者的脑子里。备注栏从头到尾都是空的,而演讲备注要做的,正是把这一层交接出去:它是作者留给登台者的交代,说清这一页真正想讲什么、哪里该着重、又该如何自然地引到下一页。发言人需要的是每页一小段提示——不必长篇大论,但要够他拿着资料讲,而不是照着幻灯片念。手工补备注的代价在于"逐页思考":一页一页读过去,重新组织语言,写少了没提示作用,写多了又会变成照本宣科。页数少尚可忍受,几十页的年度汇报就难免草草了事。
以下示例使用 Spire.Agent.Office 智能体,通过自然语言指令逐页阅读幻灯片内容,为每一页生成可直接照读的演讲备注并写入备注栏:
using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Presentation;
// PPT 处理相关配置
string inputPath = @"E:\product-launch.pptx"; // 待添加演讲备注的演示文稿路径
string savePath = @"E:\product_result.pptx"; // 结果文档路径
string key = "**************************"; // SpireToken Key
string instruction = "为这份输入PPT文件逐页生成演讲备注,写入每页的备注栏:" +
"1. 逐页阅读幻灯片标题与要点,理解该页要传达的信息;" +
"2. 为每一页撰写一段演讲备注,用演讲者口吻、可直接照读;" +
"3. 备注要说清这页的讲解重点,并自然过渡到下一页,备注需简繁有序,重要内容需详细说明,过渡页简单说明;" +
"4. 保持原有的幻灯片版式、字体、配色与页面顺序完全不变,不要增删任何幻灯片;" +
"最终输出保存为PPT文件";
// 调用PPT文档处理函数
AIResult result = ExecuteDemoPpt(instruction, inputPath, savePath, key);
// 执行PPT文档AI处理
static AIResult ExecuteDemoPpt(string instruction, string inputPath, string savePath, string key)
{
// 创建AIOptions选项配置对象
AIOptions options = new AIOptions();
options.SpireToken = key; // 设置SpireToken Key
// 使用Presentation对象处理PPT文档
using (Presentation ppt = new Presentation())
{
// 从文件加载PPT文档
if (!string.IsNullOrEmpty(inputPath) && File.Exists(inputPath))
{
ppt.LoadFromFile(inputPath);
}
// 创建AI文档处理器
AIDocumentProcessor processor = ppt.AI(options);
// 执行AI指令
return processor.ExecuteInstruction(ppt, instruction, savePath);
}
}
备注栏为空的原始演示文稿
逐页写入演讲备注后的演示文稿

生成后的演示文稿在投影画面上与原件完全一致,备注只出现在演讲者视图中。发言人打开演示者视图,就能在每一页下方看到该页的提示文案,按顺序念下来即可完成整场产品发布。
备注栏适合"提示",却不适合"照读"。备注以短句为主,字号小、位置偏,站在台上低头看备注既影响台风,也容易漏词。当场合更正式——例如季度经营汇报要向管理层逐项说明数据——讲者需要的是一份完整的、口语化的讲稿。讲稿的载体值得单独说明:演示文稿是给人看的,讲稿是给人读的。讲稿要控制字号、行距与分节,通常还要打印出来带进会场;用 PPT 承载每页几百字的正文,排版与阅读体验都不合适,投影出去更是把台词直接给了观众。所以本案例的输入仍是 PPT,输出是一份 Word 讲稿文档。
跨格式处理正是 Spire.Agent.Office 可以直接做的事:仍用 Presentation 加载演示文稿,由 AI 读取每页内容,再按指令把讲稿写成 Word 文件。因此本案例让 AI 先逐页"读进"数据,再按原页顺序分节写成讲稿:原演示文稿的每一页,对应讲稿文档中的一节,节标题沿用原标题,正文则是可以直接念出口的完整讲稿。
以下示例使用 Spire.Agent.Office 智能体,通过自然语言指令逐页解读数据并撰写讲稿,输出为 Word 文档:
using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Presentation;
// PPT 处理相关配置
string inputPath = @"E:\quarterly-review.pptx"; // 输入演示文稿路径
string savePath = null; // 结果文档路径(此处为null,将使用下面设置的输出文件夹路径)
string OutDir = @"E:\output-script"; // 输出目录
string key = "**************************"; // SpireToken Key
string instruction =
"读取输入 PPT 文件,为 PPT 演讲者撰写完整的讲稿,输出为一份Word讲稿文档:" +
"1. 逐页阅读幻灯片上的数据与要点,理解该页的核心结论;" +
"2. 为每一页撰写一段完整、口语化的讲稿,可直接照读,内容简繁有序,讲稿要引用该页的具体数据,并说明数字背后的经营含义;" +
"3. 讲稿按原幻灯片顺序分节编排,每节以'第N页 + 原幻灯片标题'作为小标题,正文为该页讲稿全文;" +
"4. 正文排版便于朗读:字体清晰,字号不小于14磅,行距1.5倍,段间距适中;";
// 调用PPT文档处理函数
AIResult result = ExecuteDemoPpt(instruction, inputPath, savePath, key, OutDir);
// 执行PPT文档AI处理
static AIResult ExecuteDemoPpt(string instruction, string inputPath, string savePath, string key, string output)
{
// 创建AIOptions选项配置对象
AIOptions options = new AIOptions();
options.WorkDir = output; // 设置工作目录为输出目录
options.SpireToken = key; // 设置SpireToken Key
// 使用Presentation对象处理PPT文档
using (Presentation ppt = new Presentation())
{
// 从文件加载PPT文档
if (!string.IsNullOrEmpty(inputPath) && File.Exists(inputPath))
{
ppt.LoadFromFile(inputPath);
}
// 创建AI文档处理器
AIDocumentProcessor processor = ppt.AI(options);
// 执行AI指令
return processor.ExecuteInstruction(ppt, instruction, savePath);
}
}
生成的 Word 版讲稿文档

说明:默认情况下 AI 请求走 Spire 的模型服务。AIOptions 另外提供了 Model、BaseUrl 与 ApiKey 三个属性,分别用于指定模型名称、服务地址与访问密钥,改走自建或第三方推理服务时配置这三项即可。
AIOptions options = new AIOptions();
options.BaseUrl = "baseUrl";
options.Model = "modelName";
options.SpireToken = "*************";
options.ApiKey = "*************";
原因:生成备注和讲稿是逐页进行的,AI 需要反复读写文档,每次都要把已有内容重新送进模型。页数越多、单页文字越密,消耗的 token 增长得越快,整体耗时也随之拉长,默认的超时时间往往不够用。
解决:在创建 AIOptions 时显式设置 TimeoutMs(单位毫秒),给长文档留出足够的处理时间。如 50 页的 PPT 文件,TimeoutMs 的值可以给到 10 分钟(600000)。
在代码中配置:
AIOptions options = new AIOptions();
options.SpireToken = key;
表单填完之后下一步通常是归档。可 PDF 表单域偏偏在这里留下口子:文件看着已经填好,控件却还在,收件人随手就能改掉金额、日期或者签字栏,有的阅读器打开时还会重新校验一遍。要把这份文件变成谁都改不动的成品,就得把控件连同值一起压进页面内容里。
本文用 Spire.PDF for JavaScript 来实现 PDF 表单域的扁平化。它基于 WebAssembly 在浏览器端加载、修改与保存文档,通过虚拟文件系统(VFS)读写文件,无需后端配合。
本文介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。
Spire.PDF for JavaScript 提供 PdfForm.IsFlatten 属性,用于把整份表单一次性扁平化。置为 true 后,文档里所有字段连同当前值一起转成静态页面内容,保存出来的 PDF 不再有可交互的控件;文字仍留在文本层,照样可以选中、复制和检索。
function App() {
const flattenWholeForm = 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.Form.IsFlatten = 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={flattenWholeForm}>
开始扁平化
</button>
</div>
);
}
export default App;
所有字段的输入框都消失,值留在页面上成为普通文本:

Spire.PDF for JavaScript 还提供 PdfField.Flatten 属性,用于按字段粒度扁平化。字段实例要从 PdfFormWidget 的 FieldsWidget 集合里按 Name 取,只有挑中的那个字段被固化,其余保持可编辑——比如把已经核对过的邮箱冻结,同时留出日期栏给收件人填。
function App() {
const flattenSelectedField = 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 });
if (field.Name === 'email') {
field.Flatten = 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={flattenSelectedField}>
开始扁平化
</button>
</div>
);
}
export default App;
只有邮箱的输入框消失,其余字段仍是可编辑的控件:

原因:代码里混用了两个字段集合。doc.Form.Fields 对由其它工具生成的 AcroForm 常常一个字段都读不到,本文样例文档的 Count 读回就是 0,对它调 get_Item() 会直接抛 ArgumentOutOfRange_IndexMustBeLess;就算能读到,那里返回的 PdfField 也没有 Text、Checked 这类控件属性,改它不会影响页面。
解决:整表扁平化只用 doc.Form.IsFlatten,与集合无关;只要涉及逐个字段——按名挑选、写值、单独固化——一律改走 PdfFormWidget:
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);
}
原因:PdfForm.IsFlatten 是写入指令,不是状态位。把已经扁平化的产物重新载入,doc.Form.IsFlatten 照旧读回 false——实测此时文档里的字段数已经是 0。
解决:按字段数量判断,FieldsWidget.Count 为 0 就是已经没有可交互的控件了:
let formWidget = new pdfModule.PdfFormWidget(doc.Form.H);
const hasFormFields = formWidget.FieldsWidget.Count > 0;
原因:Name 是逐字符比较,区分大小写,也保留首尾空格。写成 company_name 而文档里实际是 company_name (末尾带空格),循环里一次都不会命中,而且不报错、文件原样输出。
解决:先把所有字段名打印一遍再照着复制,判断与赋值都必须落在 FieldsWidget 给出的 *FieldWidget 实例上:
for (let i = 0; i < formWidget.FieldsWidget.Count; i++) {
console.log(formWidget.FieldsWidget.get_Item({ index: i }).Name);
}
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
登记表、报名表、问卷这类 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 天的临时许可证。
在 Word 文档中插入分页符,是控制章节起始位置、避免标题与正文被跨页割裂时最常用的排版手段;而清理文档中冗余的分页符,则是处理从其他文档复制内容后遗留空白页的关键步骤。Spire.Doc for JavaScript 基于 WebAssembly 在浏览器端直接完成这些操作,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。
本文介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.Doc for JavaScript。以下示例默认已安装 Spire.Doc 并完成 WebAssembly 模块初始化。
插入分页符的核心流程分为三个阶段:首先通过 FetchFileToVFS 将字体文件和目标 Word 文档载入 WASM 虚拟文件系统;然后实例化 Document 加载文档,定位到目标段落并调用 AppendBreak 传入 BreakType.PageBreak,将分页符作为该段落的子对象追加到末尾;最后从 VFS 读取保存后的文件,封装为 Blob 后生成下载链接。
function App() {
const InsertPageBreak = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// 将目标 Word 文档载入 VFS
const inputFileName = "Template_Docx_1.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// 创建 Document 实例并加载文档
const doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// 定位到第一节的第 4 个段落,在其末尾插入分页符
doc.Sections.get_Item(0).Paragraphs.get_Item(3).AppendBreak(docModule.BreakType.PageBreak);
// 定义输出文件名
const outputFileName = "InsertPageBreak_out.docx";
// 将文档保存到 VFS
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
doc.Dispose();
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
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>给Word文档插入分页符</h1>
<button onClick={InsertPageBreak}>开始</button>
</div>
);
}
export default App;
插入分页符后生成的文档效果

删除分页符的核心流程同样分为三个阶段:首先通过 FetchFileToVFS 将字体文件和包含分页符的 Word 文档载入 WASM 虚拟文件系统;然后实例化 Document 加载文档,依次遍历每一节中每一个段落的子对象,通过 DocumentObjectType 判断该对象是否为分页符,并用 ChildObjects.Remove 将其移除;最后从 VFS 读取保存后的文件,封装为 Blob 后生成下载链接。
function App() {
const RemovePageBreaks = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// 将目标 Word 文档载入 VFS
const inputFileName = "Template_Docx_4.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// 创建 Document 实例并加载文档
const doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// 获取第一节
const section = doc.Sections.get_Item(0);
// 遍历节中的每一个段落
for (let j = 0; j < section.Paragraphs.Count; j++) {
const p = section.Paragraphs.get_Item(j);
// 倒序遍历段落的子对象,避免移除元素后索引错位
for (let i = p.ChildObjects.Count - 1; i >= 0; i--) {
const obj = p.ChildObjects.get_Item(i);
// 判断对象是否为分页符
if (obj.DocumentObjectType == docModule.DocumentObjectType.Break
&& obj.BreakType == docModule.BreakType.PageBreak) {
// 将分页符从段落中移除
p.ChildObjects.Remove(obj);
}
}
}
// 定义输出文件名
const outputFileName = "RemovePageBreaks_out.docx";
// 将文档保存到 VFS
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
doc.Dispose();
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
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>移除Word文档中的分页符</h1>
<button onClick={RemovePageBreaks}>开始</button>
</div>
);
}
export default App;
删除分页符后生成的文档效果

原因:AppendBreak 是把分页符追加到目标段落的末尾,因此该段落自身的段后间距会随分页符一起被带到新页的开头,视觉上表现为新页顶部多出一段空白。若段落启用了自动间距,这段空白的高度还会随字体大小变化。
解决:插入分页符前先清除目标段落的段后间距:
const para = document.Sections.get_Item(0).Paragraphs.get_Item(3);
// 关闭自动段后间距,并将段后间距设为 0
para.Format.AfterAutoSpacing = false;
para.Format.AfterSpacing = 0;
// 再插入分页符
para.AppendBreak(wasmModule.BreakType.PageBreak);
原因:文档中的分页效果并不都由分页符对象产生。若段落本身设置了「段前分页」属性,即使分页符已被移除,该段落仍会从新的一页开始,只遍历 ChildObjects 删除 DocumentObjectType.Break 对象无法去除这类分页。
解决:在删除分页符对象的同时,重置段落的段前分页属性:
const section = document.Sections.get_Item(0);
for (let j = 0; j < section.Paragraphs.Count; j++) {
const p = section.Paragraphs.get_Item(j);
// 清除段落自带的「段前分页」属性
if (p.Format.PageBreakBefore) {
p.Format.PageBreakBefore = false;
}
}
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
装订线是页面为装订预留的额外留白,用于文档打印装订成册时避免正文被订书钉或胶装边遮挡。由于它属于页面设置的一部分,必须在生成文档时确定,无法在打印环节补上。Spire.Doc for JavaScript 基于 WebAssembly 在浏览器端直接完成页面设置,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。
本文介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.Doc for JavaScript。以下示例默认已安装 Spire.Doc 并完成 WebAssembly 模块初始化。
添加装订线的核心流程分为三个阶段:首先通过 FetchFileToVFS 将字体文件和目标 Word 文档载入 WASM 虚拟文件系统;然后实例化 Document 加载文档,获取目标节后通过 PageSetup.Gutter 设置装订线宽度,该值以磅为单位,默认加在页面左侧;最后从 VFS 读取保存后的文件,封装为 Blob 后生成下载链接。
function App() {
const AddGutter = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// 将目标 Word 文档载入 VFS
const inputFileName = "GutterSample.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// 创建 Document 实例并加载文档
const doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// 获取第一节
const section = doc.Sections.get_Item(0);
// 设置装订线宽度,单位为磅,默认作用于页面左侧
section.PageSetup.Gutter = 100;
// 定义输出文件名
const outputFileName = "AddGutter_out.docx";
// 将文档保存到 VFS
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
// 释放资源
doc.Dispose();
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
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={AddGutter}>开始</button>
</div>
);
}
export default App;
添加装订线后生成的文档效果

装订线位置决定这段留白加在页面的哪一侧:PageSetup.IsTopGutter 为 true 时表示顶部装订线,为 false(默认值)时表示左侧装订线。核心流程同样分为三个阶段:首先通过 FetchFileToVFS 将字体文件和目标 Word 文档载入 WASM 虚拟文件系统;然后实例化 Document 加载文档,获取目标节后将 PageSetup.IsTopGutter 设为 true 打开顶部装订线,再通过 PageSetup.Gutter 设置装订线宽度;最后从 VFS 读取保存后的文件,封装为 Blob 后生成下载链接。
function App() {
const SetGutterPosition = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// 将目标 Word 文档载入 VFS
const inputFileName = "GutterSample.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// 创建 Document 实例并加载文档
const doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// 获取第一节
const section = doc.Sections.get_Item(0);
// 将装订线位置设为顶部
section.PageSetup.IsTopGutter = true;
// 设置装订线宽度,单位为磅
section.PageSetup.Gutter = 100;
// 定义输出文件名
const outputFileName = "SetGutterPosition_out.docx";
// 将文档保存到 VFS
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
// 释放资源
doc.Dispose();
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
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>设置在Word文档中装订线的位置</h1>
<button onClick={SetGutterPosition}>开始</button>
</div>
);
}
export default App;
设置顶部装订线后生成的文档效果

原因:PageSetup.Gutter 的单位是磅(point),而 Word 页面设置对话框中默认以厘米或英寸显示。示例中直接传入 100,代表 100 磅,约合 3.53 厘米,在 A4 页面上会明显偏宽。
解决:按单位换算后再传入。1 磅等于 1/72 英寸,1 英寸等于 2.54 厘米,因此厘米转磅的公式为 磅 = 厘米 ÷ 2.54 × 72:
// 将 1.5 厘米的装订线宽度换算为磅
const gutterInPoints = (1.5 / 2.54) * 72;
section.PageSetup.Gutter = gutterInPoints;
原因:装订线是在页边距之外额外占用的空间,它不会自动扩大页面纸面,而是把正文区域向内压缩。若原有页边距已经较大,再加上装订线后正文可用宽度会进一步缩小,出现每行字数过少、段落频繁折行的情况。
解决:在设置装订线的同时,相应调小同一侧的页边距,保证正文可用宽度:
const pageSetup = section.PageSetup;
// 设置 1.5 厘米的装订线(厘米换算为磅)
pageSetup.Gutter = (1.5 / 2.54) * 72;
// 装订线占用左侧空间,相应调小左边距
pageSetup.Margins.Left = 60;
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
给 PDF 补文字往往排在生成流程的收尾:单据编号、审核意见、说明批注,或者压在图上一层浅色大字。几页文档手动敲一下还好,等文字要跟着数据走——编号逐份变化、说明斜排在页角、浅色字压在图表上——手工排版就吃力了。
本文介绍用 Spire.PDF for JavaScript 在 PDF 页面中绘制文本,包括渐变填充的文字、在矩形框内排版的文字,以及旋转变形与半透明的文字。它基于 WebAssembly 在浏览器端直接创建与保存 PDF 文档,全过程在本地完成,通过虚拟文件系统(VFS)读写文件,无需后端配合。
本文介绍四个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。
文字的颜色来自 DrawString 的填充刷子,换上一把渐变刷子 PdfLinearGradientBrush,字就会沿指定方向从一种颜色过渡到另一种——方向由 mode 决定,起止范围由刷子上的 rect 圈定。
function App() {
const drawGradientText = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 将宋体载入 VFS
await window.spire.FetchFileToVFS('SIMSUN.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 创建 PDF 文档并添加一个空白页面
const doc = new pdfModule.PdfDocument();
const page = doc.Pages.Add();
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 24 });
const text = '颜色渐变的文本';
// 量出这行文字的宽度,让渐变的起止范围与文字等宽
const textWidth = font.MeasureString({ text: text }).Width;
// 横向渐变:从红色过渡到蓝色
const gradient = new pdfModule.PdfLinearGradientBrush({
rect: new pdfModule.RectangleF({ x: 40, y: 90, width: textWidth, height: 40 }),
color1: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Red() }),
color2: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Blue() }),
mode: pdfModule.PdfLinearGradientMode.Horizontal,
});
// 文字落点与渐变矩形的左端对齐,红蓝两色完整扫过整行
page.Canvas.DrawString({ s: text, font: font, brush: gradient, x: 40, y: 110 });
// 定义输出文件名并保存文档
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={drawGradientText}>
开始绘制
</button>
</div>
);
}
export default App;
渐变矩形与文字等宽时,红色到蓝色完整扫过整行文字:

DrawString 的落点除了给一对坐标,也可以给一个矩形框 layoutRectangle:文字以框宽为准自动折行,不用自己算每一行在哪里断开。再配 PdfStringFormat 的 alignment 与 lineAlignment,可以更灵活的控制文本的对齐效果。
function App() {
const drawTextInRectangle = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 将宋体载入 VFS,供页面文字使用
await window.spire.FetchFileToVFS('SIMSUN.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 创建 PDF 文档并添加一个空白页面
const doc = new pdfModule.PdfDocument();
const page = doc.Pages.Add();
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 14 });
const brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Black() }) });
const borderPen = new pdfModule.PdfPen({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_LightGray() }), width: 1 });
const text = '这是一段比较长的说明文字,交给矩形框之后会按框的宽度自动折行,不需要自己计算每一行到哪里断开。';
// 左框:默认靠左折行,文字从框的左上角排起
const leftBox = new pdfModule.RectangleF({ x: 40, y: 80, width: 200, height: 100 });
page.Canvas.DrawRectangle({ pen: borderPen, rectangle: leftBox });
page.Canvas.DrawString({ s: text, font: font, brush: brush, layoutRectangle: leftBox });
// 右框:同样的文字,在框内水平居中并垂直居中
const rightBox = new pdfModule.RectangleF({ x: 300, y: 80, width: 200, height: 100 });
page.Canvas.DrawRectangle({ pen: borderPen, rectangle: rightBox });
const center = new pdfModule.PdfStringFormat({
alignment: pdfModule.PdfTextAlignment.Center,
lineAlignment: pdfModule.PdfVerticalAlignment.Middle,
});
page.Canvas.DrawString({ s: text, font: font, brush: brush, layoutRectangle: rightBox, format: center });
// 定义输出文件名并保存文档
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={drawTextInRectangle}>
开始绘制
</button>
</div>
);
}
export default App;
同一段文字在 200 磅宽的框内自动折行,右框另加了水平与垂直居中:

文字的旋转与变形不靠字体参数,而是先把画布转过去,画布变换常用的四个方法:
| 接口 | 作用 | 参数与单位 |
|---|---|---|
TranslateTransform(dx, dy) |
把画布原点平移到目标位置 | 位移量,单位磅 |
RotateTransform({ angle }) |
绕画布原点旋转 | 角度;正值在本画布上为顺时针 |
SkewTransform(angleX, angleY) |
让坐标轴倾斜,文字沿斜线排布 | 倾斜角度;(-20, 0) 时整行右端抬高 |
ScaleTransform(scaleX, scaleY) |
按倍数缩放画布 | 两个方向的缩放倍数;(1, 0.6) 纵向压到 0.6 倍 |
四种变换作用在同一段样例文字上的效果(灰为变换前、红为变换后,圆点为落点):

function App() {
const drawTransformedText = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 将宋体载入 VFS,供页面文字使用
await window.spire.FetchFileToVFS('SIMSUN.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 创建 PDF 文档并添加一个空白页面
const doc = new pdfModule.PdfDocument();
const page = doc.Pages.Add();
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 16 });
const brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_SteelBlue() }) });
// 平移:只把原点移到落点,文字保持水平地整体挪过去
let state = page.Canvas.Save();
page.Canvas.TranslateTransform(60, 110);
page.Canvas.DrawString({ s: '平移后的文本', font: font, brush: brush, x: 0, y: 0 });
page.Canvas.Restore({ state: state });
// 旋转:把原点挪到落点,再转过 30°
state = page.Canvas.Save();
page.Canvas.TranslateTransform(120, 210);
page.Canvas.RotateTransform({ angle: 30 });
page.Canvas.DrawString({ s: '旋转 30° 的文本', font: font, brush: brush, x: 0, y: 0 });
page.Canvas.Restore({ state: state });
// 倾斜:横向切变 -20°,整行右端抬高
state = page.Canvas.Save();
page.Canvas.TranslateTransform(60, 430);
page.Canvas.SkewTransform(-20, 0);
page.Canvas.DrawString({ s: '横向倾斜的文本', font: font, brush: brush, x: 0, y: 0 });
page.Canvas.Restore({ state: state });
// 变形:纵向缩到 0.6 倍,字被压扁
state = page.Canvas.Save();
page.Canvas.TranslateTransform(60, 560);
page.Canvas.ScaleTransform(1, 0.6);
page.Canvas.DrawString({ s: '纵向压缩的文本', font: font, brush: brush, x: 0, y: 0 });
page.Canvas.Restore({ state: state });
// 定义输出文件名并保存文档
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={drawTransformedText}>
开始变换
</button>
</div>
);
}
export default App;
平移、旋转、横向切变与纵向压缩四种画布变换绘制出的文字:

SetTransparency 设在画布上:alphaBrush 与 alphaPen 分别控制填充和描边的透明程度,取值是 0 到 1 之间的小数(0 全透明、1 不透明),blendMode 决定文字与下层内容如何叠加。它从设置的那一刻起对所有绘制生效,所以要用 Save 与 Restore 框在需要的范围内,否则后面的内容会一起变淡。
function App() {
const drawTransparentText = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 将宋体载入 VFS,供页面文字使用
await window.spire.FetchFileToVFS('SIMSUN.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 创建 PDF 文档并添加一个空白页面
const doc = new pdfModule.PdfDocument();
const page = doc.Pages.Add();
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 20 });
const brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_SeaGreen() }) });
const text = 'Spire.PDF for JavaScript 绘制半透明文本';
// 第一行:不透明,作为对照
page.Canvas.DrawString({ s: text, font: font, brush: brush, x: 40, y: 90 });
// 打开透明度设置:填充与描边的 alpha 都设为 0.3
const state = page.Canvas.Save();
page.Canvas.SetTransparency({ alphaPen: 0.3, alphaBrush: 0.3, blendMode: pdfModule.PdfBlendMode.Normal });
page.Canvas.DrawString({ s: text, font: font, brush: brush, x: 40, y: 140 });
// 恢复画布状态,这段之外的绘制回到不透明
page.Canvas.Restore({ state: state });
page.Canvas.DrawString({ s: text, font: font, brush: brush, x: 40, y: 190 });
// 定义输出文件名并保存文档
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={drawTransparentText}>
开始绘制
</button>
</div>
);
}
export default App;
同一行文字的三次绘制:不透明、alpha 0.3,以及 Restore 之后恢复不透明:

原因:画布坐标的原点在页面左上角(再内缩页边距),y 轴向下,单位是磅;落点是这行文字的左上角,不是文字的基线。按左下角为原点、y 轴向上的习惯换算,位置就会跑到相反的一侧,甚至落到页面可绘制区域之外——示例页面是 A4,上下页边距各 40 磅,可用高度只有 762 磅,落点 y 超过约 740 磅时,这行字就会被切掉。
解决:按左上角为原点、y 向下增大来摆放,需要从页面底部往上算时,取可用高度做减法:
// 页面可绘制区域的高度(A4 + 40 磅页边距时为 762 磅)
const height = page.Canvas.ClientSize.Height;
// 距离可绘制区域底部 100 磅处落笔
page.Canvas.DrawString({ s: '靠近页面底部的文字', font: font, brush: brush, x: 40, y: height - 100 });
原因:PdfLinearGradientBrush 的 rect 圈定的是渐变的起止范围,用的是画布上的绝对坐标,与文字落点相互独立。矩形没盖住整行文字时,文字只能落在渐变的一段上,看上去就像单色。实测把矩形放在 x 从 0 开始、文字落在 x 为 40 的位置,文字左端已经走过整段渐变的三分之一,红蓝过渡就不再完整。
解决:用 MeasureString 量出文字宽度,让矩形与文字等宽、起点与落点对齐,渐变就会完整扫过整行:
// 文字宽度作为渐变矩形的宽度
const textWidth = font.MeasureString({ text: text }).Width;
const gradient = new pdfModule.PdfLinearGradientBrush({
rect: new pdfModule.RectangleF({ x: 40, y: 90, width: textWidth, height: 40 }),
color1: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Red() }),
color2: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Blue() }),
mode: pdfModule.PdfLinearGradientMode.Horizontal,
});
// 落点与矩形左端对齐
page.Canvas.DrawString({ s: text, font: font, brush: gradient, x: 40, y: 110 });
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
Spire.XLS for Java 16.9.2 现已发布。该版本修复了读取单元格值与原文件中单元格格式不符的问题,优化了 Excel 转 PDF 时隐藏工作表的公式计算耗时,并修复了 Excel 转 PDF/A-1a、PDF/A-2a 和 PDF/A-3a 时验证失败的问题。详情如下。
问题修复:
一份几十页的合同或报告交到手上,要确认某条条款、某个金额出现过几次、都出现在哪里,靠眼睛翻页容易漏。把命中的文字标出来是最省事的做法,但桌面软件里的查找高亮很难嵌进 Web 流程,逐页截图再标注也不现实。
Spire.PDF for JavaScript 基于 WebAssembly 在浏览器端加载、处理与保存 PDF 文档,查找与高亮全部在本地完成,通过虚拟文件系统(VFS)读写文件,无需后端配合。本文用 PdfTextFinder 来实现三种查找高亮方式:全篇高亮、限定区域内高亮、按正则表达式高亮。
本文介绍三个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。
PdfTextFinder 用于在页面的文本层里定位指定文本。它按页工作,遍历文档的每一页各建一个 finder,就能把整份文档里的匹配项一次找全;命中的每一处调用 HighLight() 即完成高亮,默认是黄色,需要区分不同关键词时再传入颜色。
function App() {
const findAndHighlightAll = 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);
// 逐页查找,命中的每一处都加上高亮
for (let i = 0; i < doc.Pages.Count; i++) {
const finder = new pdfModule.PdfTextFinder(doc.Pages.get_Item(i));
finder.Options.Parameter = pdfModule.TextFindParameter.IgnoreCase;
const finds = finder.Find('观赏');
for (let j = 0; j < finds.length; j++) {
finds.get(j).HighLight();
}
}
// 保存文档
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={findAndHighlightAll}>
开始查找并高亮
</button>
</div>
);
}
export default App;
整份文档里的 观赏 全部被高亮:

页面上的同名文字往往只有一部分需要标注,PdfTextFinder 还提供 Options.Area,把查找范围收进一个矩形,落在框外的匹配不会返回,也就不会被高亮。矩形用页面坐标描述,原点在页面左上角、单位是磅。
function App() {
const findAndHighlightInArea = 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);
// 指定查找范围:页面坐标,原点在左上角,单位磅
const area = new pdfModule.RectangleF({ x: 60, y: 488, width: 420, height: 160 });
const finder = new pdfModule.PdfTextFinder(doc.Pages.get_Item(0));
finder.Options.Parameter = pdfModule.TextFindParameter.IgnoreCase;
finder.Options.Area = area;
// 只有落在矩形内的匹配会被返回
const finds = finder.Find('观赏');
for (let j = 0; j < finds.length; j++) {
finds.get(j).HighLight();
}
// 保存文档
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={findAndHighlightInArea}>
开始查找并高亮
</button>
</div>
);
}
export default App;
只有对照表区域内的 观赏 被高亮,正文与列表里的保持不变:

要找的目标未必是固定的几个字。Options.Parameter 决定匹配规则,取 Regex 时 Find() 的入参就是一个正则表达式,形态相同而内容各异的目标可以用一条模式一次圈出,默认取值按子串匹配,也就是前面两节的效果,同一枚举里还有 IgnoreCase、WholeWord 可用。
function App() {
const findAndHighlightByRegex = 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);
// 逐页按正则表达式匹配,命中的每一处都加上高亮
for (let i = 0; i < doc.Pages.Count; i++) {
const finder = new pdfModule.PdfTextFinder(doc.Pages.get_Item(i));
finder.Options.Parameter = pdfModule.TextFindParameter.Regex;
const finds = finder.Find('图\\s*\\d');
for (let j = 0; j < finds.length; j++) {
finds.get(j).HighLight({ color: pdfModule.Color.get_Orange() });
}
}
// 保存文档
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={findAndHighlightByRegex}>
开始查找并高亮
</button>
</div>
);
}
export default App;
三处图注被模式 图\s*\d 命中并高亮为橙色:

原因:HighLight() 走的是页面内容,不是 PDF 注释。高亮色块在保存时被写进页面的内容流,文件体积只增大约 2 KB 量级,产物里并不会多出注释对象——用 PyMuPDF 读回 page.annots() 得到的是空集。
解决:把高亮当作页面图形看待即可,显示效果与标注一致;只是它没有注释身份,不能像批注那样在阅读器里逐条选中、删除或改色。需要按注释管理高亮时,应在保存前记下命中的位置,由业务侧自行维护这份清单。
原因:Options.Area 用的是页面坐标(原点在页面左上角、单位磅),矩形写小了、位置偏了,匹配就全部落在框外。它也只在当前页生效——多页文档要把同一个矩形套到目标页的 finder 上。
解决:先按整页坐标量出目标区域,再往里收。下面这份样例里的对照表落在 x≈60–480、y≈488–648 之间,用 RectangleF({ x: 60, y: 488, width: 420, height: 160 }) 正好框住表格,矩形外的正文与列表都不会被匹配:
// 只查当前页,且只在这个矩形内匹配
const finder = new pdfModule.PdfTextFinder(doc.Pages.get_Item(0));
finder.Options.Area = new pdfModule.RectangleF({ x: 60, y: 488, width: 420, height: 160 });
拿不准坐标时,可以先不设 Area 查一遍,从命中的 finds.get(i).Bounds[0] 读出实际位置再反过来定矩形。
原因:正则匹配的是 PDF 文本层里的实际字符,不是语义。三份样例里表示区间的连字符并不一致:中文与英文用短横线 –(U+2013),日文用全角波浪线 ~(U+FF5E),只写一种模式就只能在一种文档上命中。
解决:把连字符写成字符组,一次兼容两种写法:
// 数字区间:6–9 月 / 1–3 m / 6~9月 都能匹配
finder.Options.Parameter = pdfModule.TextFindParameter.Regex;
const finds = finder.Find('[0-9]+\\s*[–-~]\\s*[0-9]+');
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。