PDF 图层是把同一页上的内容划分到多个“可选内容组”(Optional Content Group,OCG)中的机制,最常见的应用如 CAD 图纸里的墙体、家具、电气分层,地图里的道路、水系、注记分层,以及需要在一页里按需切换的多种方案或多种语言内容。与直接把内容擦掉不同,图层可以让内容“既能藏起来、又能随时再打开”,在不破坏文档结构的前提下大幅提升同一份 PDF 的复用价值。
Spire.PDF for JavaScript 基于 WebAssembly 在浏览器端直接完成 PDF 的加载、绘制与保存,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。
本文介绍三个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。
添加图层的过程是:先创建文档并添加页面,用 doc.Layers.AddLayer({ name, state }) 新建一个带名字的图层(state 可指定初始可见性),再调用该图层的 layer.CreateGraphics(page.Canvas) 拿到关联到该页画布的绘图上下文,之后便能用 DrawLine、DrawRectangle 等绘制方法把线条、色块等内容“画进”这个图层。下面的示例创建 red line、blue line、green line 三个图层,每个图层内各画一条对应颜色的横线与一个小色块,三条横线按不同高度错开排列。
function App() {
const addLayers = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 新建 PdfDocument 文档,并添加一页
let doc = new pdfModule.PdfDocument();
let page = doc.Pages.Add();
// 获取页面尺寸,用于在页面上相对定位
const width = page.Canvas.Size.Width;
const height = page.Canvas.Size.Height;
// 定义本地函数:把一条带同色小色块的横线绘制到指定名称的图层
const drawRow = (layerName, centerY, brush) => {
// 向文档添加一个图层,并设置其初始状态为可见
let layer = doc.Layers.AddLayer({ name: layerName, state: pdfModule.PdfVisibility.On });
// 获取该图层的绘图上下文
let g = layer.CreateGraphics(page.Canvas);
// 在图层内绘制一条彩色横线(横跨页面约 20%~80% 的宽度)
g.DrawLine({
pen: new pdfModule.PdfPen({ brush: brush, width: 2 }),
point1: new pdfModule.PointF(width * 0.2, centerY),
point2: new pdfModule.PointF(width * 0.8, centerY)
});
// 在横线左端绘制一个小色块,作为该图层颜色的标识
g.DrawRectangle({
brush: brush,
rectangle: new pdfModule.RectangleF({ x: width * 0.12, y: centerY - 6, width: 12, height: 12 })
});
};
// 三条横线自上而下排布,纵坐标分别取页面高度的 25%、50%、75%
drawRow('red line', height * 0.25, pdfModule.PdfBrushes.get_Red());
drawRow('blue line', height * 0.5, pdfModule.PdfBrushes.get_Blue());
drawRow('green line', height * 0.75, pdfModule.PdfBrushes.get_Green());
// 定义输出文件名并保存文档
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>Add Layers To PDF</h1>
<button onClick={addLayers}>
Generate
</button>
</div>
);
}
export default App;
添加 red line、blue line、green line 三个图层后的 PDF 页面

当某些图层的内容暂时不想显示、但之后还需要再次打开时,不必删除它们,只需把对应图层的 Visibility 属性设为 PdfVisibility.Off 即可“隐藏”。隐藏后内容不再显示,但图层与其中的对象仍保留在 PDF 里,阅读者可以在 PDF 查看器的图层面板中随时重新开启。下面的示例载入一份官方分层 PDF 样例(其中已含 red line、blue line、green line 三个图层),用 get_Item({ name }) 按名称获取其中两个图层并把它们的 Visibility 设为不可见,页面上便只保留 green line 图层的内容。
function App() {
const hideLayers = 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);
// 按名称获取“red line”与“blue line”图层,并设为不可见,页面上仅保留“green line”
doc.Layers.get_Item({ name: 'red line' }).Visibility = pdfModule.PdfVisibility.Off;
doc.Layers.get_Item({ name: 'blue line' }).Visibility = pdfModule.PdfVisibility.Off;
// 定义输出文件名并保存文档
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>Hide Layers In PDF</h1>
<button onClick={hideLayers}>
Generate
</button>
</div>
);
}
export default App;
隐藏 red line 与 blue line 图层后,页面仅保留 green line 图层内容

当某个图层及其内容不再需要时,可以用 doc.Layers.RemoveLayer 把它从文档的图层集合中删除:传入图层名称即可,例如 RemoveLayer({ name: 'red line' }) 会把名为 red line 的图层整个移除,之后该图层中的内容不再显示,也不能再通过图层面板恢复。
function App() {
const deleteLayer = 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);
// 按名称删除“red line”图层,页面将不再显示该图层内容
doc.Layers.RemoveLayer({ name: 'red line' });
// 定义输出文件名并保存文档
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>Delete PDF Layer</h1>
<button onClick={deleteLayer}>
Generate
</button>
</div>
);
}
export default App;
删除 red line 图层后,页面仅保留 blue line 与 green line 图层

原因:隐藏与删除都会让内容在页面上“看不见”,容易混淆两者在文档结构上的差别。
解决:隐藏是把图层 Visibility 设为 Off,图层与其中的对象仍保留在 PDF 里,之后用查看器的图层面板可随时再开启;删除则通过 RemoveLayer 把图层从 doc.Layers 中整个移除,其内容不再显示、也无法恢复。简单说:暂时不看用隐藏,永远不要用删除:
// 隐藏:内容仍在文档中,可随时再开启
doc.Layers.get_Item({ name: 'red line' }).Visibility = pdfModule.PdfVisibility.Off;
// 删除:从图层集合中移除,无法再开启
doc.Layers.RemoveLayer({ name: 'red line' });
原因:AddLayer 新建的图层默认可见,而有些图层希望一开始就处于隐藏状态(例如预置但暂不展示的备用方案)。
解决:AddLayer 的 state 参数可指定图层的初始可见性,传 PdfVisibility.Off 即创建为不可见,传 On(或省略)则创建后立即可见。创建后再用 Visibility 属性随时切换:
// 新建一个初始即不可见的图层
doc.Layers.AddLayer({ name: '备选方案', state: pdfModule.PdfVisibility.Off });
原因:对含多个图层的文档,常需要操作某个特定图层,而 Layers 集合里有多个元素,需要准确的定位方式。
解决:doc.Layers 是图层集合,Count 给出图层总数,get_Item({ name }) 按图层名取值,get_Item(i) 按下标取值。需要批量控制时遍历所有图层即可,例如把所有图层一并设为不可见:
// 遍历图层集合,把所有图层设为不可见
for (let i = 0; i < doc.Layers.Count; i++) {
doc.Layers.get_Item(i).Visibility = pdfModule.PdfVisibility.Off;
}
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
PDF 版式固定、跨端一致,适合分发合同、手册、报表等文档,不过一份资料往往由多份相互关联的 PDF 组成:产品手册之外还有报价单、技术规格书、常见问题等,逐个文件发送既零散又容易遗漏。PDF 的“文件包”(Portfolio)机制提供了标准的解决办法,把多份文档打包进同一个 PDF。接收方只需打开这一个文件,就能在查看器的文件包视图中看到、展开并另存各个成员文件,便于统一下发与归档。
Spire.PDF for JavaScript 基于 WebAssembly 在浏览器端直接完成 PDF 的加载、绘制与保存,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。围绕文件包有两种常用操作:创建,用 PdfDocument 加载一份主文档,再通过文件集合的根文件夹 doc.Collection.Folders 调用 AddFile 把已载入 VFS 的成员文件逐个加入,必要时用 CreateSubfolder 建子文件夹对成员归类;识别,直接读取 doc.IsPortfolio 属性,判断某份 PDF 是否为文件包。
本文介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。
文件包的成员不限于 PDF,Word、Excel、图片等文件都可以加入,也可以用子文件夹归类。打包的思路是:先用 PdfDocument 加载一份主文档作为文件包的载体;把要打包的成员文件通过 FetchFileToVFS 载入虚拟文件系统;再遍历成员,用文件集合的根文件夹 doc.Collection.Folders 调用 AddFile({ filePath }) 逐个加入。若希望部分文件单独归到某个子文件夹,可先用 CreateSubfolder 建好子文件夹,再对它调用 AddFile,把文件加进去。所有成员加入完成后保存,即得到把主文档与各成员文件打包到一起的 PDF 文件包。
function App() {
const createPortfolio = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 将作为文件包主文档的 PDF 载入 VFS
const mainFileName = '产品手册.pdf';
await window.spire.FetchFileToVFS(mainFileName, "", `${process.env.PUBLIC_URL}/data/`);
// 创建 PdfDocument 对象并加载主文档
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(mainFileName);
// 将放在文件包根目录下的成员文件逐个载入 VFS,并添加到文件集合的文件夹中
const rootFiles = ['报价单.pdf', '技术规格书.pdf', 'logo.png', '财务报表.xlsx'];
for (let i = 0; i < rootFiles.length; i++) {
await window.spire.FetchFileToVFS(rootFiles[i], "", `${process.env.PUBLIC_URL}/data/`);
doc.Collection.Folders.AddFile({ filePath: rootFiles[i] });
}
// 把要放进子文件夹的 Word 文档载入 VFS
await window.spire.FetchFileToVFS('test.docx', "", `${process.env.PUBLIC_URL}/data/`);
// 在文件集合中创建子文件夹“目录”,并把 Word 文档加入该子文件夹
const subFolder = doc.Collection.Folders.CreateSubfolder('目录');
subFolder.AddFile({ filePath: 'test.docx' });
// 定义输出文件名并保存文档
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>Create PDF Portfolio</h1>
<button onClick={createPortfolio}>
Generate
</button>
</div>
);
}
export default App;
创建后得到的产品资料文件包

判断一份 PDF 是否为文件包,用 PdfDocument.IsPortfolio 属性即可:LoadFromFile 载入文档后读取该布尔属性,返回 true 表示是文件包,false 表示是普通 PDF 文档。本示例加载一份文件包样例进行识别,把结论写入 txt 下载,并同步显示在页面下方,便于直接查看。
function App() {
const identifyPortfolio = 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);
// 判断该 PDF 是否为文件包
const isPortfolio = doc.IsPortfolio;
const message = isPortfolio ? '该 PDF 是一个文件包' : '该 PDF 不是文件包';
doc.Close();
// 在页面下方显示判定结果
const resultEl = document.getElementById('identify-result');
if (resultEl) resultEl.innerText = message;
// 把判定结果写入 txt 并触发下载
const outputFileName = '识别结果.txt';
window.dotnetRuntime.Module.FS.writeFile(outputFileName, message);
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'text/plain' });
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>Identify PDF Portfolio</h1>
<button onClick={identifyPortfolio}>
Check
</button>
<p id="identify-result" style={{ marginTop: '20px', fontWeight: 'bold' }}></p>
</div>
);
}
export default App;
识别结果为“该 PDF 是一个文件包”

原因:文件包只是把成员文件“打包”进了同一个 PDF,从外观上未必能一眼判断保存结果是否成功。
解决:用 doc.IsPortfolio 对保存结果做二次判断即可:重新载入生成的文件,若该属性返回 true,说明确实是文件包:
// 重新载入生成的文件并判断是否为文件包
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile('文件包.pdf');
const isPortfolio = doc.IsPortfolio;
原因:文件包和附件都会把文件“塞进”PDF,容易混淆两者的用途与判断方式。
解决:PDF 附件(Attachment)把文件作为嵌入文件挂在文档的附件面板里,正文本身通常是一份独立文档;而文件包(Portfolio)基于文件集合(Collection)组织成员文件,成员可以是多份文档,打开后在文件包视图中作为独立文件分别展开与另存。判断时可用 doc.Attachments 查看附件、用 doc.IsPortfolio 判断是否为文件包,二者互不替代。
原因:示例里先展示的是几个 PDF 成员,容易让人误以为文件包只能装 PDF。
解决:AddFile 加入的是虚拟文件系统里的任意文件,不限于 PDF。先通过 FetchFileToVFS 把目标文件载入 VFS,再以 { filePath: 文件名 } 加入即可;想让文件分组存放时,再用 CreateSubfolder 建一个子文件夹,把文件加入该子文件夹。Word、Excel、图片等文件都可以作为成员打包进文件包:
// 载入一份 Excel 文件并作为文件包成员加入
await window.spire.FetchFileToVFS('财务报表.xlsx', "", `${process.env.PUBLIC_URL}/data/`);
doc.Collection.Folders.AddFile({ filePath: '财务报表.xlsx' });
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
在人力资源、财务与法务场景中,PDF 常被用来承载薪酬单、工资条、劳动合同、对账单等敏感信息,而"把一批 PDF 分发给不同的人"是每个月都会发生的操作。直接发送明文 PDF 存在泄露风险——文件一旦被转发,任何收到的人都可随意打开查看。传统做法是借助专用工具逐份设置打开密码再分发,几十上百份文件要手动重复操作,既耗时又容易漏设密码;收到方需要查看时,又往往要另外寻求工具解除密码,来回切换效率很低。
Spire.Agent.Office 的 PDF AI 能力可以直接用自然语言描述需求,AI 智能体理解后自动完成"批量加密 → 分发清单 → 按需解密"的整个流程。
| 传统 Spire.Office for .NET API | Spire.Agent.Office 处理 | |
|---|---|---|
| 驱动方式 | 编写循环遍历文件 + 密码设置 + 权限配置代码,控制每一步 | 用自然语言描述目标,AI 理解后自动编排执行路径 |
| 代码量 | 批量加密/解密通常需要 200-400 行 C# 代码(含文件枚举、加密参数、异常处理等) | 约 10 行调用代码 + 1 条自然语言指令 |
| 密码策略 | 逐份硬编码密码或自行设计生成规则并写代码维护 | 在指令中用一句话说明密码规则(如按工号生成),AI 自动套用 |
| 分发清单 | 需额外编写生成清单/表格的逻辑 | 同一条指令可顺带输出 PDF 版分发密码对照表 |
| 格式处理 | 需手动处理加密算法、权限标志、文档结构等底层细节 | AI 自动识别并保留原文档的布局、样式和字体 |
| 需求变更 | 改动密码规则/目标路径 → 改代码 → 编译 → 重新部署 | 修改指令,即刻生效 |
本文介绍如何使用 Spire.Agent.Office PDF AI 能力,先为一批 PDF 批量设置打开密码并输出加密分发清单,再按需批量解除密码保护。全文通过两个案例展示加密分发与解密还原两种典型用法:
有关产品安装和 SpireToken 配置,请参考 在 .NET 项目中集成 Spire.Agent.Office。以下示例默认已安装 Spire.Agent.Office 并完成 SpireToken 配置。
该功能的核心思路是:以一份或多份待处理的 PDF 作为输入,让 AI 智能体按自然语言指令逐份设置打开密码加密保存,或在已知密码的情况下解除保护,并保持原文档版式不变。整个"文件读取 → 密码规则套用 → 加密/解密 → 结果输出"过程由 AI 自动完成,无需针对每份文件编写处理代码。下面通过案例一演示面向收件人的批量加密与分发,通过案例二演示管理员收回文件后的批量解密归档。
批量加密最常见的场景,是把多份敏感 PDF(如员工工资条)在发送前逐份加上打开密码。其特点是文件数量多、每份文件的收件人不同,密码如果全部相同则形同虚设,如果各不相同又难以记忆和传达。
以下示例使用 Spire.Agent.Office 智能体,通过自然语言指令把附件文件夹(即指定文件夹下的全部 PDF)作为待加密文档,按"员工生日+随机生成的四位数字+ 固定后缀"的规则为每一份设置独立打开密码,将加密后的文档逐份保存为 PDF,并同时生成一份 PDF 格式的加密分发清单,逐行列明原文件名、加密文件名与打开密码:
using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Pdf;
// 待批量加密的 PDF 所在文件夹:将该文件夹下的全部 PDF 文件作为附件处理(每份对应一位收件人)
string attachmentDir = @"E:\pdfs"; // 待加密 PDF 所在文件夹
string[] attachmentPaths = Directory.GetFiles(attachmentDir, "*.pdf");
// PDF 处理相关配置
string inputPath = null;
string savePath = null; // 结果文档路径(此处为null,将使用下面设置的输出文件夹路径)
string OutDir = @"E:\output"; // 输出目录(加密后文档与分发清单将保存到此目录)
string key = "**************************"; // SpireToken Key
string instruction =
"读取附件中的全部 PDF 文档,为每一份设置打开密码后加密保存:" +
"1. 密码生成规则为'员工生日+随机生成的四位数字@2026'(例如 salary-1001.pdf 对应密码 199805083354@2026),该打开密码仅用于限制文档的打开,不影响打印、复制、编辑等其他原有功能;" +
"2. 将加密后的每一份 PDF 保存到输出目录,文件名在原文件名后追加后缀'-加密',保持与原文档一致的布局、字体和页面设置;" +
"3. 在输出目录生成一份 excel格式的《加密分发清单》,逐行列出原文件名、加密文件名与对应的打开密码;" ;
// 调用PDF文档处理函数
AIResult result = ExecuteDemoPDF(instruction, inputPath, savePath, key, OutDir, attachmentPaths);
// 执行PDF文档AI处理
static AIResult ExecuteDemoPDF(string instruction, string inputPath, string savePath, string key, string output, string[] attachmentPaths)
{
// 创建AIOptions选项配置对象
AIOptions options = new AIOptions();
options.WorkDir = output; // 设置工作目录为输出目录
options.SpireToken = key; // 设置SpireToken Key
// 使用PdfDocument对象处理PDF文档
using (PdfDocument pdf = new PdfDocument())
{
// 从文件加载PDF文档
if (!string.IsNullOrEmpty(inputPath) && File.Exists(inputPath))
{
pdf.LoadFromFile(inputPath);
}
// 创建AI文档处理器
AIDocumentProcessor processor = pdf.AI(options);
// 执行AI指令
return processor.ExecuteInstruction(pdf, instruction, savePath, attachmentPaths);
}
}
待加密的原始 PDF 文档

批量加密后的 PDF 与加密分发清单

每一份 PDF 都被设置上独立的打开密码并原样保留了原文档的版式,加密分发清单则把每份文件与收件人密码一一对应,供发送方随附件安全送达收件人。后续新增收件人时,只需把新文件加入附件并调整指令中的文件清单,即可完成新一轮加密分发。
收件人完成查看后,管理员通常需要把文件收回并归档,此时需要批量解除之前设置的打开密码,还原为可检索、可合并处理的明文 PDF。本案例的输入文件为空,待解密的加密 PDF 与一张记录了"文件名、文件密码"映射的 Excel 对照表一并作为附件提供,解密的关键是 AI 从 Excel 中读取每份文件对应的打开密码,再用该密码打开文档。
以下示例使用 Spire.Agent.Office 智能体,输入文件为空,通过自然语言指令读取附件中的密码对照表 Excel 与已加密的 PDF,使用表中对应密码打开文档并解除密码保护,将无密码的文档逐份保存为 PDF:
using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Pdf;
// 附件为该文件夹下的全部文件(内含'文件名、文件密码'对照表Excel 与待解密的已加密PDF)
string attachmentDir = @"E:\pdfs"; // 存放密码对照表Excel与已加密PDF的文件夹
string[] attachmentPaths = Directory.GetFiles(attachmentDir);
// PDF 处理相关配置
string inputPath = ""; // 输入文件为空,所有待解密文件均位于附件中
string savePath = null; // 结果文档路径
string OutDir = @"E:\output-decrypted"; // 输出目录(解密后的文档将保存到此目录)
string key = "**************************"; // SpireToken Key
string instruction =
"读取附件中密码对照表Excel中'文件名、文件密码'的映射,批量解密附件中对应的加密PDF:" +
"1. 从Excel中读取每一行的文件名及其对应的打开密码;" +
"2. 在附件中定位与该行文件名相同的PDF文档,使用该行密码打开文档并解除密码保护;" +
"3. 将解密后的无密码PDF逐份保存到输出目录,文件名去除'-加密'后缀,保持与原文档一致的布局、字体和页面设置;" +
"最终输出保存为PDF文件";
// 调用PDF文档处理函数
AIResult result = ExecuteDemoPDF(instruction, inputPath, savePath, key, OutDir, attachmentPaths);
// 执行PDF文档AI处理
static AIResult ExecuteDemoPDF(string instruction, string inputPath, string savePath, string key, string output, string[] attachmentPaths)
{
// 创建AIOptions选项配置对象
AIOptions options = new AIOptions();
options.WorkDir = output; // 设置工作目录为输出目录
options.SpireToken = key; // 设置SpireToken Key
// 使用PdfDocument对象处理PDF文档
using (PdfDocument pdf = new PdfDocument())
{
// 输入文件为空时不加载文档,全部处理对象来自附件(密码对照表Excel + 已加密PDF)
if (!string.IsNullOrEmpty(inputPath) && File.Exists(inputPath))
{
pdf.LoadFromFile(inputPath);
}
// 创建AI文档处理器
AIDocumentProcessor processor = pdf.AI(options);
// 执行AI指令
return processor.ExecuteInstruction(pdf, instruction, savePath, attachmentPaths);
}
}
密码对照表 Excel 与待解密的加密 PDF 文档
解密还原后的无密码 PDF 文档

原因:打开密码与文档权限是两回事。本文示例仅为文档设置打开密码,不施加"禁止打印""禁止复制"等权限限制。
解决:加密后除打开文档需要密码外,打印、复制、编辑等其他原有功能均不受影响。若出于安全要求需要限制打印或复制,可在指令中追加权限说明(如"仅允许查看,禁止打印与复制"),AI 会按描述一并配置对应权限。
原因:加密本身只影响文档的打开与权限校验,不涉及页面内容的重排。
解决:Spire.Agent.Office 加密处理会自动保留原文档的布局、字体与页面设置。如担心个别样式变化,可在指令中补充"保持与原文档一致的布局、字体和页面设置"。
在代码中配置:
AIOptions options = new AIOptions();
options.SpireToken = key;
在管理项目计划、财务报表等行列较多的数据时,常常需要对部分行进行分组(Group / Outline),把它们折叠成一层层的结构,从而只查看汇总行或某些阶段的内容。当结构不再需要时,也可以随时取消分组,恢复行数据的平铺显示。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端直接完成此操作,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。
本文介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.XLS for JavaScript。以下示例默认已安装 Spire.XLS 并完成 WebAssembly 模块初始化。
多级分组由"外层分组 + 内层分组"组成。例如在一份项目计划中,把整个执行阶段(若干行)作为一级分组,再把其中每个子阶段的明细行作为二级分组。创建时,应先对外层的大范围调用 GroupByRows(),再对嵌套在内的小范围调用 GroupByRows(),这样 Excel 才能生成不同层级的折叠按钮。具体操作步骤如下:
Workbook 对象并获取第一个工作表。Worksheet.PageSetup.IsSummaryRowBelow = false 让汇总行显示在明细行的上方。GroupByRows(),再对嵌套的内层行区域(第 4-5 行、第 8-9 行)分别调用 GroupByRows()。Workbook.SaveToFile() 方法保存工作簿。下面是一个完整的代码示例,展示了在 React 中为工作表创建两级(嵌套)行分组:
function App() {
const createNestedGroup = 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 workbook = new xlsModule.Workbook();
const sheet = workbook.Worksheets.get(0);
// 添加一个命名样式,用于标题行
const style = workbook.Styles.Add("style");
style.Font.Color = xlsModule.Color.get_CadetBlue();
style.Font.IsBold = true;
// 让汇总行显示在明细行的上方
sheet.PageSetup.IsSummaryRowBelow = false;
// 写入示例数据
sheet.Range.get("A1").Value = "项目 X 实施计划";
sheet.Range.get("A1").CellStyleName = style.Name;
sheet.Range.get("A3").Value = "筹备";
sheet.Range.get("A3").CellStyleName = style.Name;
sheet.Range.get("A4").Value = "任务 1";
sheet.Range.get("A5").Value = "任务 2";
sheet.Range.get("A4:A5").BorderAround(xlsModule.LineStyleType.Thin);
sheet.Range.get("A4:A5").BorderInside(xlsModule.LineStyleType.Thin);
sheet.Range.get("A7").Value = "上线";
sheet.Range.get("A7").CellStyleName = style.Name;
sheet.Range.get("A8").Value = "任务 1";
sheet.Range.get("A9").Value = "任务 2";
sheet.Range.get("A8:A9").BorderAround(xlsModule.LineStyleType.Thin);
sheet.Range.get("A8:A9").BorderInside(xlsModule.LineStyleType.Thin);
// 先对外层行分组,再对嵌套的内层行分组,形成多级分组
sheet.GroupByRows(2, 9, false);
sheet.GroupByRows(4, 5, false);
sheet.GroupByRows(8, 9, false);
// 保存文档
const outputFileName = 'MultiLevelGroup.xlsx';
workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });
// 释放资源
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>Create Nested Group</h1>
<button onClick={createNestedGroup}>
Start
</button>
</div>
);
}
export default App;
创建多级分组的效果

如果工作簿中已经存在多层分组,需要把其中某个外层"大分组"或内层"小分组"取消时,可以直接加载文件后对相应范围调用 Worksheet.UngroupByRows() 方法。分组只是影响行的折叠显示,取消分组不会删除任何单元格内容;当外层大分组被取消后,原本嵌套在内的内层小分组会保留为独立的单级分组,可再单独取消。具体操作步骤如下:
Workbook 对象,并通过 Workbook.LoadFromFile() 方法加载已含多层分组的工作簿。Workbook.Worksheets.get() 方法获取该工作表。UngroupByRows(),取消外层大分组。UngroupByRows(),取消内层小分组。Workbook.SaveToFile() 方法保存工作簿。下面是一个完整的代码示例,展示了在 React 中加载已分组的 Excel 文件,并取消大分组与小分组的操作:
function App() {
const ungroupRows = 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/`);
// 加载已有多层分组的 Excel 文件
const inputFileName = 'MultiLevelGroup.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// 创建 Workbook 对象并加载该工作簿
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
// 获取第一个工作表
const sheet = workbook.Worksheets.get(0);
// 取消外层的大分组(第 2-9 行)
sheet.UngroupByRows(2, 9);
// 取消内层的小分组(第 4-5 行)
sheet.UngroupByRows(4, 5);
// 保存文档
const outputFileName = 'UngroupRows_output.xlsx';
workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });
// 释放资源
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>Ungroup Rows</h1>
<button onClick={ungroupRows}>
Start
</button>
</div>
);
}
export default App;
取消多级分组的效果

原因:多级分组要求内层分组的范围必须完全包含在外层分组范围内。如果两次分组的范围互不包含,Excel 会将其视为两个平级分组,而不是嵌套的多级分组。
解决:先对外层的大范围调用 GroupByRows(),再对其中嵌套的小范围调用 GroupByRows(),例如先 GroupByRows(2, 9, false),再 GroupByRows(4, 5, false)。
原因:GroupByRows(startRow, endRow, isCollapsed) 的第三个布尔参数决定分组创建后是否默认折叠明细。true 表示默认折叠,false 表示默认展开(本文示例使用 false)。打开保存后的文件时即按该状态显示。
解决:需要默认折叠时把第三参数改为 true,例如 sheet.GroupByRows(4, 5, true);如需在运行时折叠或展开,可对分组范围调用 CollapseGroup() / ExpandGroup()。
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
PDF 版式固定、跨端一致,是合同、报告、图册等文档分发的首选格式,但包含大量高分辨率图片或内嵌字体的 PDF 体积往往很大,既占用存储空间,也让邮件发送、网页上传与下载变得缓慢。若能在浏览器端直接给文档“瘦身”,在尽量保持可读性的前提下显著减小体积,就能明显改善分发与加载体验,而不必把文件上传到服务器再下载处理。
Spire.PDF for JavaScript 基于 WebAssembly 在浏览器端直接完成 PDF 的加载、压缩与保存,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。它提供专用的 PdfCompressor 压缩器,其 Options 可以从三个维度分别控制压缩策略:一是用 ImageCompressionOptions 对文档里的图片做缩放与重压缩,二是用 TextCompressionOptions 压缩字体数据甚至取消字体内嵌,三是用 CompressContents 重新压缩页面的内容流。三者在一次压缩中可以自由组合,兼顾画质与体积。
本文先从三个维度介绍 PdfCompressor 的压缩方式,再给出把它们组合起来的完整可运行示例:
有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。
页面中的高分辨率位图通常是 PDF 体积的主要来源。Options.ImageCompressionOptions 用三个属性共同控制图片的缩放与重编码:
| 选项 | 作用 | 效果与取舍 |
|---|---|---|
ResizeImages |
等比缩小图片后再重新编码 | 显著减小体积,适合分辨率过高的大图 |
CompressImage |
对图片做有损压缩 | 以少量画质损失换取体积下降 |
ImageQuality |
控制重编码的画质档位 | High 画质更高、体积略大;Low 体积更小、画质可能变模糊 |
对以图片为主的文档,通常先开启 ResizeImages 与 CompressImage,再按对画质的容忍度选择合适的 ImageQuality 档位:
// 压缩文档中的图片:缩放图片、重新压缩并降低画质
compressor.Options.ImageCompressionOptions.ResizeImages = true;
compressor.Options.ImageCompressionOptions.CompressImage = true;
compressor.Options.ImageCompressionOptions.ImageQuality = pdfModule.ImageQuality.Low;
PDF 会把正文用到的字体作为子集嵌入文件内,多个字体、多种字重叠加起来也占不少体积。Options.TextCompressionOptions 提供两种处理字体的方式:
| 选项 | 作用 | 效果与取舍 |
|---|---|---|
CompressFonts |
压缩内嵌字体数据,保留字体 | 显示不变,体积更小 |
UnembedFonts |
移除内嵌字体,改用系统字体渲染 | 体积更小;阅读端缺对应字体时字形可能被替换 |
UnembedFonts 能压得更小但有显示风险,是否开启需结合文档去向判断:
// 压缩字体数据;UnembedFonts 进一步取消字体内嵌
compressor.Options.TextCompressionOptions.CompressFonts = true;
compressor.Options.TextCompressionOptions.UnembedFonts = true;
PDF 页面中的文字与矢量绘制指令以“内容流(Content Stream)”的形式存储,生成时虽通常会压缩,但经过多次编辑或不同工具处理后仍可能存在冗余。Options.CompressContents 会重新压缩文档的内容流,开启后对文字较多、版式复杂的文档也能挤出一部分空间:
// 重新压缩文档的内容流
compressor.Options.CompressContents = true;
把上面三种方式组合起来即可得到兼顾效果与速度的完整压缩流程:加载 PDF 后一次开启图片、字体与内容流三类压缩,再调用 CompressToFile 把结果写入新文件。下图示例对一份同时包含高分辨率图片与文字的 PDF 同时启用了 ResizeImages、CompressImage(High 画质)、CompressFonts、UnembedFonts 与 CompressContents,得到体积明显减小的新文档:
function App() {
const compressPdfDocument = 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/`);
// 创建 PdfCompressor 对象并指定要压缩的 PDF 文件
let compressor = new pdfModule.PdfCompressor({ filePath: inputFileName });
// 1. 图片压缩:缩放图片并重新压缩,使用较高画质
compressor.Options.ImageCompressionOptions.ResizeImages = true;
compressor.Options.ImageCompressionOptions.CompressImage = true;
compressor.Options.ImageCompressionOptions.ImageQuality = pdfModule.ImageQuality.High;
// 2. 字体压缩:压缩字体数据并取消字体内嵌
compressor.Options.TextCompressionOptions.CompressFonts = true;
compressor.Options.TextCompressionOptions.UnembedFonts = true;
// 3. 内容压缩:重新压缩文档的内容流
compressor.Options.CompressContents = true;
// 定义输出文件名并压缩到该文件
const outputFileName = '压缩后的文档.pdf';
compressor.CompressToFile(outputFileName);
// 从 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>Compress PDF Document</h1>
<button onClick={compressPdfDocument}>
Generate
</button>
</div>
);
}
export default App;
综合三种方式压缩后的 PDF 文档

原因:文档体积未必都来自图片。当页面以文字为主时,体积更多来自内嵌字体与页面内容流,只开启图片压缩自然收效甚微。
解决:把压缩维度补齐——用 TextCompressionOptions 压缩字体数据(必要时 UnembedFonts 取消嵌入),用 CompressContents 重新压缩内容流,与图片压缩一起组合使用,才能在文字类文档上也明显减小体积。
原因:UnembedFonts 会从 PDF 中去掉字体程序,阅读端渲染时改用系统里已安装的字体;若目标环境缺少该字体,就可能出现字形替换或间距变化。
解决:文档若是分发给字体环境不确定的用户,建议保留内嵌,仅用 CompressFonts 压缩字体数据;只有当你能确认阅读端具备对应字体时,再开启 UnembedFonts:
// 保留内嵌但压缩字体数据,避免去嵌入带来的显示风险
compressor.Options.TextCompressionOptions.UnembedFonts = false;
compressor.Options.TextCompressionOptions.CompressFonts = true;
原因:ImageQuality 只影响图片重编码的画质与体积;文档构成不同,对体积贡献最大的部分也不同,单一维度难以达到理想的压缩率。
解决:图片为主时,先开 ResizeImages 与 CompressImage 并在 High/Low 之间选档;文字为主时,重点用字体压缩与内容压缩。需要保留字体内嵌的文档关掉 UnembedFonts 即可,不影响其余压缩项:
// 图片为主时选较低的画质档位以换更小体积
compressor.Options.ImageCompressionOptions.ImageQuality = pdfModule.ImageQuality.Low;
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
PDF 版式固定、便于分发,但单个 PDF 只能承载正文内容。业务中经常需要把主文档与配套资料一并交给对方:例如把合同正文与签章图片、报表正文与底稿数据、说明文档与相关图片打包在一起,方便归档与流转。PDF 的附件(嵌入文件)机制为此提供了标准做法——它允许在 PDF 的嵌入文件树中携带任意类型的文件,接收方打开一个 PDF,即可在查看器的“附件”面板中同时找到主文档与配套文件,无需再通过邮件或网盘二次索取。
Spire.PDF for JavaScript 基于 WebAssembly 在浏览器端直接完成 PDF 的加载、绘制与保存,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。处理附件有两种基本操作:一种是添加——用 PdfAttachment 把某个文件的名称、字节数据与说明封装成附件,再通过 doc.Attachments.Add 加入文档的附件集合;另一种是删除——访问 doc.Attachments 附件集合,用 RemoveAt(index) 按索引移除指定的附件。两者都围绕 PdfDocument.Attachments 这一集合展开。
本文介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。
给 PDF 添加附件时,先把主文档和要嵌入的文件加载进虚拟文件系统,再用 PdfAttachment 封装附件(名称、数据、说明、MIME 类型),最后 Add 进 doc.Attachments。附件不会画在页面上,而是存在 PDF 的嵌入文件树中,用查看器打开后可在“附件”面板看到并另存。本示例给合同文档嵌入一张 logo.png。
function App() {
const addAttachment = 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/`);
// 将待嵌入为附件的图片文件载入 VFS
const attachFileName = 'logo.png';
await window.spire.FetchFileToVFS(attachFileName, "", `${process.env.PUBLIC_URL}/data/`);
// 创建 PdfDocument 对象并加载 PDF 文档
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// 创建附件对象:设置附件名称、说明与 MIME 类型
let attachment = new pdfModule.PdfAttachment({ fileName: attachFileName });
attachment.Data = window.dotnetRuntime.Module.FS.readFile(attachFileName);
attachment.Description = '随合同一并分发的公司 logo';
attachment.MimeType = 'image/png';
// 将附件添加到文档的附件集合中
doc.Attachments.Add({ attachment: attachment });
// 定义输出文件名并保存文档
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>Add Attachment To PDF</h1>
<button onClick={addAttachment}>
Generate
</button>
</div>
);
}
export default App;
已嵌入 logo.png 附件的合同文档

删除某个指定的附件,用附件集合的 RemoveAt(index) 按索引移除,索引从 0 开始(删除前可用 Count 核对数量与索引范围);本示例加载一份带附件的样例文档,删除其中的第一个附件。如需删除文档中的全部附件,直接调用 attachments.Clear() 清空整个附件集合即可。
function App() {
const deleteAttachments = 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 attachments = doc.Attachments;
// 删除附件集合中指定索引处的附件(索引从 0 开始,此处删除第一个附件)
attachments.RemoveAt(0);
// 定义输出文件名并保存文档
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>Delete Attachments From PDF</h1>
<button onClick={deleteAttachments}>
Generate
</button>
</div>
);
}
export default App;
删除第一个附件后的 PDF 文档

原因:删除或读取附件前,通常需要先确认文档里是否存在附件、数量几何,避免对空集合做无效操作。
解决:文档的所有附件都存放在 doc.Attachments 集合中,其 Count 属性即附件个数,为 0 表示没有附件;需要逐个读取某个附件时,可用 get_Item(index) 按索引访问:
// 获取文档的附件集合与附件数量
let attachments = doc.Attachments;
let count = attachments.Count;
原因:只把文件字节塞给附件而不设置名称与说明,查看器“附件”面板里的展示会不完整,接收方难以识别文件内容。
解决:PdfAttachment 常用的几个属性是 fileName(接收方看到的附件文件名)、Description(一行说明文字)与 MimeType(内容类型),文件字节赋给 Data 即可。设置后再 Add 进集合,附件栏即可按名称、说明正确展示:
// 创建附件并填写名称、数据、说明与 MIME 类型
let attachment = new pdfModule.PdfAttachment({ fileName: 'logo.png' });
attachment.Data = window.dotnetRuntime.Module.FS.readFile('logo.png');
attachment.Description = '随合同一并分发的公司 logo';
attachment.MimeType = 'image/png';
doc.Attachments.Add({ attachment: attachment });
原因:示例多以图片演示附件,容易误以为 PDF 附件只支持图片。
解决:PDF 附件本质是携带任意字节的嵌入文件,不限类型。只要先把文件加载进虚拟文件系统,用 FS.readFile 读出字节赋给 Data,并把 MimeType 设为对应的内容类型,Word、Excel、PDF、压缩包等都能作为附件嵌入。以嵌入一份 PDF 附表为例:
// 载入要嵌入的 PDF 附表并作为附件添加
const attachName = '产品附表.pdf';
await window.spire.FetchFileToVFS(attachName, "", `${process.env.PUBLIC_URL}/data/`);
let attachment = new pdfModule.PdfAttachment({ fileName: attachName });
attachment.Data = window.dotnetRuntime.Module.FS.readFile(attachName);
attachment.MimeType = 'application/pdf';
doc.Attachments.Add({ attachment: attachment });
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
超链接是 Excel 中用于快速跳转到网页、邮件地址或其他资源的常用元素,经常出现在产品官网、联系方式、参考资料等表格中。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端直接完成超链接的添加、读取、修改与删除操作,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。
本文介绍几个常用功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.XLS for JavaScript。以下示例默认已安装 Spire.XLS 并完成 WebAssembly 模块初始化。
在含有公司名称、网站名称或邮箱地址等文本的单元格上,可以为这些文本添加超链接,使其可以直接点击跳转到网页或发送邮件。
function App() {
const addHyperlinkToText = async () => {
// 获取 Spire.XLS WASM 模块
const xlsModule = window.wasmModule?.spirexls;
// 检查模块是否就绪
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// 将字体和 Excel 文件载入 VFS
await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'HyperlinksSample.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);
// 为 D10 单元格中的文本添加网页超链接
const urlLink = sheet.HyperLinks.Add({ range: sheet.Range.get('D10') });
urlLink.TextToDisplay = sheet.Range.get('D10').Text;
urlLink.Type = xlsModule.HyperLinkType.Url;
urlLink.Address = 'https://www.e-iceblue.com/';
// 为 E10 单元格中的文本添加邮件超链接
const mailLink = sheet.HyperLinks.Add({ range: sheet.Range.get('E10') });
mailLink.TextToDisplay = sheet.Range.get('E10').Text;
mailLink.Type = xlsModule.HyperLinkType.Url;
mailLink.Address = 'mailto:该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。';
// 保存工作簿
const outputFileName = 'AddHyperlinkToText_output.xlsx';
workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });
// 释放资源
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>Add Hyperlink To Text</h1>
<button onClick={addHyperlinkToText}>Start</button>
</div>
);
}
export default App;
D10 单元格中的文本变为可点击的网页链接,E10 单元格中的邮箱地址变为可发送邮件的邮件链接。

通过 Worksheet.HyperLinks 集合可以获取工作表中所有超链接,并通过索引访问每个超链接的目标地址。
function App() {
const readHyperlinks = async () => {
// 获取 Spire.XLS WASM 模块
const xlsModule = window.wasmModule?.spirexls;
// 检查模块是否就绪
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// 将 Excel 文件载入 VFS
const inputFileName = 'HyperlinksSample.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 hyperlinkCount = sheet.HyperLinks.Count;
let allAddresses = '';
for (let i = 0; i < hyperlinkCount; i++) {
const address = sheet.HyperLinks.get(i).Address;
allAddresses += address + '\n';
}
// 将超链接地址保存为 txt 文件
const outputFileName = 'ReadHyperlinks_output.txt';
window.dotnetRuntime.Module.FS.writeFile(outputFileName, allAddresses);
workbook.Dispose();
// 从 VFS 读取生成的文件并触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'text/plain' });
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>Read Hyperlinks</h1>
<button onClick={readHyperlinks}>Start</button>
</div>
);
}
export default App;
通过 HyperLinks.Count 获取工作表中超链接的总数。

通过 HyperLinks.get(0) 按索引获取超链接后,可以重新设置其显示文本和目标地址,实现超链接的修改。
function App() {
const modifyHyperlink = async () => {
// 获取 Spire.XLS WASM 模块
const xlsModule = window.wasmModule?.spirexls;
// 检查模块是否就绪
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// 将字体和 Excel 文件载入 VFS
await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'HyperlinksSample.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 links = sheet.HyperLinks;
// 修改第一个超链接的显示文本与目标地址
links.get(0).TextToDisplay = 'E-iceblue';
links.get(0).Address = 'https://www.e-iceblue.com/';
// 保存工作簿
const outputFileName = 'ModifyHyperlink_output.xlsx';
workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });
// 释放资源
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>Modify Hyperlink</h1>
<button onClick={modifyHyperlink}>Start</button>
</div>
);
}
export default App;
修改后,第一个超链接的显示文本和目标地址均已更新。

通过 HyperLinks.RemoveAt(index) 方法只删除超链接保留文本,也可以通过 Range.ClearAll() 方法可以清除单元格中包含链接的所有内容。
function App() {
const removeHyperlinks = async () => {
// 获取 Spire.XLS WASM 模块
const xlsModule = window.wasmModule?.spirexls;
// 检查模块是否就绪
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// 将字体和 Excel 文件载入 VFS
await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'HyperlinksSample.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 links = sheet.HyperLinks;
// 清除链接单元格中的s所有内容
// sheet.Range.get('A1').ClearAll();
// sheet.Range.get('A2').ClearAll();
// sheet.Range.get('A3').ClearAll();
// 仅删除超链接,保留原文本
sheet.HyperLinks.RemoveAt(0);
// 保存工作簿
const outputFileName = 'RemoveHyperlinks_output.xlsx';
workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });
// 释放资源
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>Remove Hyperlinks</h1>
<button onClick={removeHyperlinks}>Start</button>
</div>
);
}
export default App;

原因:修改的是错误的超链接索引,或者目标单元格上不存在超链接。
解决:确认工作表中已存在超链接,并通过 sheet.HyperLinks.get(0) 等方式按正确索引访问后,再设置其 Address 属性。
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
在整理销售记录、统计报表等数据时,把一段普通数据区域转换成 Excel 的表格(Table / ListObject),可以让数据拥有独立的标题行、自动筛选下拉按钮、条纹外观和"汇总行"等能力,后续查阅和统计都更方便。而创建之后,还可以随时通过内置样式或各类显示选项来调整表格的外观。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端直接完成这些操作,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。
Spire.XLS for JavaScript 通过工作表对象的 ListObjects 集合来管理表格:调用 ListObjects.Create() 可以把指定单元格区域转换为表格;转换得到的 IListObject 对象支持设置内置样式(BuiltInTableStyle)、显示汇总行(DisplayTotalRow)、为汇总行各列设置计算方式(Columns[].TotalsCalculation)以及开启行条纹/列条纹(ShowTableStyleRowStripes / ShowTableStyleColumnStripes)等。
本文介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.XLS for JavaScript。以下示例默认已安装 Spire.XLS 并完成 WebAssembly 模块初始化。
将数据区域转换为表格,是快速获得"自带筛选按钮 + 条纹外观"结构化区域的方式。本例先在工作表中写入一份销售明细(产品、区域、月份、数量、销售额),再通过 ListObjects.Create() 方法把 A1:E13 区域转换为名为 "Table1" 的表格,最后应用内置浅色样式 TableStyleLight9。具体操作步骤如下:
Workbook 对象并获取第一个工作表。Worksheet.ListObjects.Create() 方法,把包含表头的数据区域转换为表格。IListObject.BuiltInTableStyle 属性为表格应用内置样式。Workbook.SaveToFile() 方法保存工作簿。下面是一个完整的代码示例,展示了在 React 中为工作表创建 Excel 表格:
function App() {
const createTable = 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 workbook = new xlsModule.Workbook();
const sheet = workbook.Worksheets.get(0);
// 写入表头
sheet.Range.get('A1').Value = '产品';
sheet.Range.get('B1').Value = '区域';
sheet.Range.get('C1').Value = '月份';
sheet.Range.get('D1').Value = '数量';
sheet.Range.get('E1').Value = '销售额';
// 写入示例数据
sheet.Range.get('A2').Value = '笔记本电脑';
sheet.Range.get('B2').Value = '华北';
sheet.Range.get('C2').Value = '一月';
sheet.Range.get('D2').NumberValue = 120;
sheet.Range.get('E2').NumberValue = 239760;
sheet.Range.get('A3').Value = '显示器';
sheet.Range.get('B3').Value = '华东';
sheet.Range.get('C3').Value = '一月';
sheet.Range.get('D3').NumberValue = 80;
sheet.Range.get('E3').NumberValue = 103920;
sheet.Range.get('A4').Value = '键盘';
sheet.Range.get('B4').Value = '华南';
sheet.Range.get('C4').Value = '一月';
sheet.Range.get('D4').NumberValue = 200;
sheet.Range.get('E4').NumberValue = 59800;
sheet.Range.get('A5').Value = '笔记本电脑';
sheet.Range.get('B5').Value = '华东';
sheet.Range.get('C5').Value = '二月';
sheet.Range.get('D5').NumberValue = 150;
sheet.Range.get('E5').NumberValue = 299700;
sheet.Range.get('A6').Value = '鼠标';
sheet.Range.get('B6').Value = '华北';
sheet.Range.get('C6').Value = '二月';
sheet.Range.get('D6').NumberValue = 300;
sheet.Range.get('E6').NumberValue = 26700;
sheet.Range.get('A7').Value = '打印机';
sheet.Range.get('B7').Value = '华南';
sheet.Range.get('C7').Value = '二月';
sheet.Range.get('D7').NumberValue = 60;
sheet.Range.get('E7').NumberValue = 65940;
sheet.Range.get('A8').Value = '显示器';
sheet.Range.get('B8').Value = '西部';
sheet.Range.get('C8').Value = '二月';
sheet.Range.get('D8').NumberValue = 90;
sheet.Range.get('E8').NumberValue = 116910;
sheet.Range.get('A9').Value = '键盘';
sheet.Range.get('B9').Value = '华北';
sheet.Range.get('C9').Value = '三月';
sheet.Range.get('D9').NumberValue = 180;
sheet.Range.get('E9').NumberValue = 53820;
sheet.Range.get('A10').Value = '路由器';
sheet.Range.get('B10').Value = '华东';
sheet.Range.get('C10').Value = '三月';
sheet.Range.get('D10').NumberValue = 70;
sheet.Range.get('E10').NumberValue = 27930;
sheet.Range.get('A11').Value = '笔记本电脑';
sheet.Range.get('B11').Value = '西部';
sheet.Range.get('C11').Value = '三月';
sheet.Range.get('D11').NumberValue = 140;
sheet.Range.get('E11').NumberValue = 279860;
sheet.Range.get('A12').Value = '打印机';
sheet.Range.get('B12').Value = '华北';
sheet.Range.get('C12').Value = '四月';
sheet.Range.get('D12').NumberValue = 110;
sheet.Range.get('E12').NumberValue = 120890;
sheet.Range.get('A13').Value = '鼠标';
sheet.Range.get('B13').Value = '华南';
sheet.Range.get('C13').Value = '四月';
sheet.Range.get('D13').NumberValue = 260;
sheet.Range.get('E13').NumberValue = 23140;
// 将 A1:E13 数据区域转换为 Excel 表格(ListObject)
const table = sheet.ListObjects.Create('Table1', sheet.Range.get({ row: 1, column: 1, lastRow: 13, lastColumn: 5 }));
// 应用内置浅色表格样式
table.BuiltInTableStyle = xlsModule.TableBuiltInStyles.TableStyleLight9;
// 自动调整列宽,使内容完整显示
sheet.AllocatedRange.AutoFitColumns();
// 保存文档
const outputFileName = 'CreateTable.xlsx';
workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });
// 释放资源
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>Create Table</h1>
<button onClick={createTable}>Start</button>
</div>
);
}
export default App;
创建表格效果:

创建好的表格可以随时重新设置外观:例如将浅色样式换成内置的 Medium 深色样式,在表格底部显示汇总行并让"数量""销售额"两列自动求和,同时开启行条纹与列条纹让数据更易阅读。具体操作步骤如下:
Workbook 对象,并通过 Workbook.LoadFromFile() 方法加载已含表格的工作簿。Workbook.Worksheets.get() 方法获取该工作表,再通过 ListObjects.get() 获取表格对象。BuiltInTableStyle 属性重新指定内置样式。DisplayTotalRow 设为 true 显示汇总行,并用 Columns[].TotalsRowLabel、Columns[].TotalsCalculation 设置汇总行的标签与求和方式。ShowTableStyleRowStripes、ShowTableStyleColumnStripes 开启行条纹与列条纹。Workbook.SaveToFile() 方法保存工作簿。下面是一个完整的代码示例,展示了在 React 中加载已创建的表格并重新设置样式与汇总行:
function App() {
const formatTable = 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/`);
// 加载上一节生成的、已包含表格的 Excel 文件
const inputFileName = 'CreateTable.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// 创建 Workbook 对象并加载该工作簿
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
// 获取第一个工作表及其中的表格
const sheet = workbook.Worksheets.get(0);
const table = sheet.ListObjects.get(0);
// 重新应用内置 Medium 表格样式
table.BuiltInTableStyle = xlsModule.TableBuiltInStyles.TableStyleMedium9;
// 显示汇总行
table.DisplayTotalRow = true;
// 在汇总行的首列输入"合计"标签
table.Columns.get(0).TotalsRowLabel = '合计';
// 对文本列不计算,对"数量""销售额"两列自动求和
table.Columns.get(1).TotalsCalculation = xlsModule.ExcelTotalsCalculation.None;
table.Columns.get(2).TotalsCalculation = xlsModule.ExcelTotalsCalculation.None;
table.Columns.get(3).TotalsCalculation = xlsModule.ExcelTotalsCalculation.Sum;
table.Columns.get(4).TotalsCalculation = xlsModule.ExcelTotalsCalculation.Sum;
// 显示行条纹与列条纹
table.ShowTableStyleRowStripes = true;
table.ShowTableStyleColumnStripes = true;
// 自动调整列宽,使内容完整显示
sheet.AllocatedRange.AutoFitColumns();
// 保存文档
const outputFileName = 'FormatTable_out.xlsx';
workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });
// 释放资源
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>Format Table</h1>
<button onClick={formatTable}>Start</button>
</div>
);
}
export default App;
设置表格样式效果:

原因:创建表格后未重新指定 BuiltInTableStyle,或对属性赋值时使用了错误的枚举类型。
解决:通过 IListObject.BuiltInTableStyle 属性重新赋值即可,取值来自 TableBuiltInStyles 枚举,它提供了浅色(TableStyleLight1 ~ TableStyleLight21)、中等(TableStyleMedium1 ~ TableStyleMedium28)和深色(TableStyleDark1 ~ TableStyleDark11)多套内置样式。例如本示例先应用 TableStyleLight9,之后切换为 TableStyleMedium9。
原因:ListObjects.Create() 的第一个参数就是表格名称,例如 Create("Table1", ...);在同一工作表中,表格名称不能重复,否则再次创建同名表格时会报错。
解决:创建时传入唯一的名称(如 "SalesTable1")。若需要修改已有表格的名称,可直接设置其 DisplayName 属性,例如 table.DisplayName = "SalesTable2025";。
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
在多人协作编辑 Excel 文档时,开启"跟踪修订"(Track Changes)后,每个用户对单元格的插入、修改和删除都会被记录下来。当审阅人员处理这些改动时,往往需要接受全部修订(将他人的改动正式合并进文档)或拒绝全部修订(撤销全部改动,恢复到修改前的状态)。手动在 Excel 中逐条处理既繁琐又容易遗漏,而在 Web 应用中通过代码批量处理则高效得多。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端直接完成此操作,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。
Spire.XLS for JavaScript 通过工作簿对象提供修订处理能力:加载包含修订记录的工作簿后,调用 AcceptAllTrackedChanges() 方法即可接受文档中的所有修订,调用 RejectAllTrackedChanges() 方法即可拒绝文档中的所有修订。
本文介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.XLS for JavaScript。以下示例默认已安装 Spire.XLS 并完成 WebAssembly 模块初始化。
当一个包含修订记录的工作簿经多人编辑、审阅通过后,需要把所有的改动正式合并进文档,也就是"接受修订"。接受修订后,改动内容成为文档的正式内容,修订记录随之清除。具体操作步骤如下:
Workbook 对象。Workbook.LoadFromFile() 方法加载包含修订记录的工作簿。Workbook.AcceptAllTrackedChanges() 方法接受文档中的所有修订。Workbook.SaveToFile() 方法保存工作簿。下面是一个完整的代码示例,展示了在 React 中接受 Excel 工作簿中的所有修订:
function App() {
const acceptTrackedChanges = async () => {
// 获取 Spire.XLS WASM 模块
const xlsModule = window.wasmModule?.spirexls;
// 检查模块是否就绪
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// 将字体和 Excel 文件载入 VFS
await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'TrackChanges.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// 创建 Workbook 对象并加载包含修订的工作簿
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
// 接受文档中的所有修订
workbook.AcceptAllTrackedChanges();
// 保存文档
const outputFileName = 'AcceptTrackedChanges_output.xlsx';
workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });
// 释放资源
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>Accept All Tracked Changes</h1>
<button onClick={acceptTrackedChanges}>
Start
</button>
</div>
);
}
export default App;
接受修订后的效果

当修订内容存在争议或不再需要时,审阅人员可以一次性拒绝全部修订,使文档恢复到修改前的状态。具体操作步骤如下:
Workbook 对象。Workbook.LoadFromFile() 方法加载包含修订记录的工作簿。Workbook.RejectAllTrackedChanges() 方法拒绝文档中的所有修订。Workbook.SaveToFile() 方法保存工作簿。下面是一个完整的代码示例,展示了在 React 中拒绝 Excel 工作簿中的所有修订:
function App() {
const rejectTrackedChanges = async () => {
// 获取 Spire.XLS WASM 模块
const xlsModule = window.wasmModule?.spirexls;
// 检查模块是否就绪
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// 将字体和 Excel 文件载入 VFS
await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'TrackChanges.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// 创建 Workbook 对象并加载包含修订的工作簿
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
// 拒绝文档中的所有修订
workbook.RejectAllTrackedChanges();
// 保存文档
const outputFileName = 'RejectTrackedChanges_output.xlsx';
workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });
// 释放资源
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>Reject All Tracked Changes</h1>
<button onClick={rejectTrackedChanges}>
Start
</button>
</div>
);
}
export default App;
拒绝修订后的效果

原因:RejectAllTrackedChanges() 拒绝的是被"跟踪修订"记录下来的单元格改动。如果某些改动发生在启用跟踪修订之前,或者通过其他方式写入且未被记录,它们不会成为可拒绝的修订,处理后仍会保留当前值,因此文档不会完全恢复到最初的基线。
原因:AcceptAllTrackedChanges() 与 RejectAllTrackedChanges() 对整个工作簿的修订进行整体处理,不提供按用户、时间或单元格区域筛选的单条处理接口。
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
在党政机关和事业单位的日常工作中,公文写作与排版是高频且高度标准化的任务。一份正式公文从拟稿、核稿到印发,既要保证语体庄重、行文规范、逻辑严密,又要严格符合《党政机关公文格式》(GB/T 9704—2012)对版式的明确要求——标题字号、正文字体、层次序数、页边距、行距、页码等均有统一标准。传统做法依赖文秘人员手工起草、反复校对和逐项排版,一份通知从成稿到符合版式往往要耗费半天以上,且不同人员排出的版式细节常有出入。
| 传统 Spire.Office for .NET API | Spire.Agent.Office | |
|---|---|---|
| 驱动方式 | 编写代码按模板拼装公文:加载文档→遍历段落→映射字段→套用版式,每步需代码控制 | 自然语言描述发文要素,AI 自动成文并按标准排版 |
| 代码量 | 需大量代码维护公文模板、字段映射和格式规则库 | 仅需配置代码 + 1 条自然语言指令 |
| 语体与措辞 | 只能替换占位符,难以把握公文称谓、惯用语与行文逻辑 | AI 基于语义生成庄重规范的公文语体,自动处理层次序数与衔接 |
| 版式规则 | 字体字号、页边距、行距等格式需硬编码到程序,改动需重新发版 | 指令中一句"按党政机关公文格式排版"即可套用标准版式 |
| 维护性 | 不同文种、不同单位要求需单独开发维护模板 | 文种、要素与版式要求可用自然语言随时调整 |
本文介绍如何使用 Spire.Agent.Office Word AI 能力实现公文的智能起草与版式规范化,二者构成公文从起草到定稿的完整链路:先用 AI 根据发文要点自动生成语体规范、结构完整的公文初稿,再对初稿或存量公文一键进行版式规范化,统一为党政机关公文格式。
有关产品安装和 SpireToken 配置,请参考 在 .NET 项目中集成 Spire.Agent.Office。以下示例默认已安装 Spire.Agent.Office 并完成 SpireToken 配置。
公文初稿智能生成是公文起草环节的起点,适合从零快速产出初稿。其核心思路是:用自然语言把发文单位、文种、主送机关、事由和正文要点告诉 AI,AI 按规范的公文语体与结构自动成文,并同步套用《党政机关公文格式》的版式要求,一次产出可直接进入审核流程的初稿。
using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Doc;
string savePath = "E:\\Output\\关于开展大气污染防治专项行动的通知.docx";
// SpireToken Key
string key = "**********************";
// 自然语言指令
string instruction =
"请以公文写作规范起草一份通知,发文单位:XX市生态环境局,文种:通知," +
"主送机关:各县(市、区)人民政府,事由:关于开展2026年秋冬季大气污染防治专项行动," +
"正文要点包括:一、总体要求;二、重点任务(1.扬尘污染整治 2.散煤治理 3.工业企业达标排放);三、工作要求。" +
"请使用规范的公文语体与称谓,层次清晰,语句庄重简洁。" +
"同时按《党政机关公文格式》(GB/T 9704—2012)规范排版:" +
"标题使用二号小标宋体居中排布,正文使用三号仿宋体、首行缩进2字符、行距固定值28磅," +
"一级标题(一、)使用三号黑体,二级标题((一))使用三号楷体," +
"页边距设置为上3.7cm、下3.5cm、左2.8cm、右2.6cm,并在版记区注明抄送机关与印发机关、印发日期。" +
"最终保存输出DOCX格式";
// 调用Word文档处理函数
AIResult result = ExecuteDemoWord(instruction, savePath, key, null);
// 执行Word文档AI处理
static AIResult ExecuteDemoWord(string instruction, string savePath, string key, string[] attachmentPaths)
{
// 创建AIOptions选项配置对象
AIOptions options = new AIOptions();
// 设置SpireToken Key
options.SpireToken = key;
// 使用Document对象处理Word文档
using (Document doc = new Document())
{
// 创建AI文档处理器
AIDocumentProcessor processor = doc.AI(options);
// 执行AI指令
return processor.ExecuteInstruction(doc, instruction, savePath, attachmentPaths);
}
}
AI 生成的公文初稿

生成的公文初稿语体庄重、层次清晰,标题、正文与各级标题的字体字号、页边距、行距等均符合公文格式要求。文秘人员只需核对事实数据、补充成文日期与印章信息即可送审,将撰写时间从数小时压缩到几分钟。对于同一机关的高频文种(通知、请示、报告、函),还可以把常用要素固定成统一指令,实现日常公文的一键起草。
对于存量公文、下级单位报送的稿件或 AI 生成的初稿,一键版式规范化可将其格式统一到 GB/T 9704—2012。其核心思路是:加载已有公文文档,让 AI 按标准逐项统一标题、正文、层次标题、页边距、行距与页码,同时顺带纠正错别字与语病,使不同来源的公文呈现一致的规范版式。
using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Doc;
// 待规范化公文文件路径
string inputPath = "E:\\Input\\XX单位工作汇报.docx";
// 保存路径
string savePath = "E:\\Output\\工作汇报-规范化.docx";
// SpireToken Key
string key = "**********************";
// 自然语言指令
string instruction =
"请对当前公文文档进行版式规范化处理,使其符合《党政机关公文格式》(GB/T 9704—2012):" +
"1. 标题统一为二号小标宋体、居中排布;" +
"2. 正文统一为三号仿宋体、首行缩进2字符、行距固定值28磅、两端对齐;" +
"3. 一级标题(一、)使用三号黑体,二级标题((一))使用三号楷体,三级标题(1.)使用三号仿宋体加粗;" +
"4. 页边距设置为上3.7cm、下3.5cm、左2.8cm、右2.6cm;" +
"5. 页码使用四号半角宋体阿拉伯数字;" +
"6. 纠正文中的错别字、语病与不当表述,保持公文原意,不得增删实质性内容。" +
"请严格按上述版式规则统一排版,最终保存输出DOCX格式";
// 调用Word文档处理函数
AIResult result = ExecuteDemoWord(instruction, inputPath, savePath, key, null);
// 执行Word文档AI处理
static AIResult ExecuteDemoWord(string instruction, string inputPath, string savePath, string key, string[] attachmentPaths)
{
// 创建AIOptions选项配置对象
AIOptions options = new AIOptions();
// 设置SpireToken Key
options.SpireToken = key;
// 使用Document对象处理Word文档
using (Document doc = new Document())
{
// 加载待规范化的公文文档
if (!string.IsNullOrEmpty(inputPath) && File.Exists(inputPath))
{
doc.LoadFromFile(inputPath);
}
// 创建AI文档处理器
AIDocumentProcessor processor = doc.AI(options);
// 执行AI指令
return processor.ExecuteInstruction(doc, instruction, savePath, attachmentPaths);
}
}
版式规范化后的公文

版式规范化后,公文的标题、正文、层级标题、页边距、行距与页码均符合标准,同一指令可批量应用于多份文档。对下级单位报送的稿件,可快速统一整个机关的公文外观;也可在规范化的同时要求 AI 修正明显语病,减少人工复核负担。
原因:AI 对公文语体的把握依赖指令中对文种、行文关系与称谓的描述,描述过泛时措辞可能偏口语化。
解决:在指令中明确文种、发文单位与主送机关,并补充"使用规范的公文语体与称谓、语句庄重简洁"等要求,必要时附上一份本机关公文范本作为附件参考。
原因:各机关单位可能对本机关公文版式有特殊要求(如发文机关标志样式、专用字体、成文日期编排方式),通用标准不完全覆盖。
解决:在指令中补充本单位的版式细则(页边距、字体字号、发文机关标志、印章与成文日期位置等),或将单位版式模板作为附件一并传入,让 AI 按模板套用。
原因:写作要点描述过简,AI 为补齐结构自行补写时间、指标数据等内容。
解决:将发文依据、时间节点、指标数据等关键要素写入指令,并明确要求"未提供的要素以空白或占位符标注,不得臆造"。
原因:每份指令描述细节不一致,或不同来源的文档基础样式差异较大,逐份处理后格式可能出现差异。
解决:对同一批次文档使用完全相同的版式描述,并固定"所有文档严格按同一条版式规则排版",在指令中重复强调关键格式项(如固定值行距、首行缩进2字符)。
在代码中配置:
AIOptions options = new AIOptions();
options.SpireToken = key;