冰蓝科技
|
028-81705109
|
|
微信扫一扫
|

Spire.Cloud 纯前端文档控件

PDF 图层是把同一页上的内容划分到多个“可选内容组”(Optional Content Group,OCG)中的机制,最常见的应用如 CAD 图纸里的墙体、家具、电气分层,地图里的道路、水系、注记分层,以及需要在一页里按需切换的多种方案或多种语言内容。与直接把内容擦掉不同,图层可以让内容“既能藏起来、又能随时再打开”,在不破坏文档结构的前提下大幅提升同一份 PDF 的复用价值。

Spire.PDF for JavaScript 基于 WebAssembly 在浏览器端直接完成 PDF 的加载、绘制与保存,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。

本文介绍三个核心功能点:

有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。


添加图层到 PDF

添加图层的过程是:先创建文档并添加页面,用 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 页面

添加 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 图层内容

隐藏 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 图层

删除 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 文件包

文件包的成员不限于 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 文件包

判断一份 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 是一个文件包”


常见问题

创建文件包后如何确认它确实是文件包

原因:文件包只是把成员文件“打包”进了同一个 PDF,从外观上未必能一眼判断保存结果是否成功。

解决:用 doc.IsPortfolio 对保存结果做二次判断即可:重新载入生成的文件,若该属性返回 true,说明确实是文件包:

// 重新载入生成的文件并判断是否为文件包
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile('文件包.pdf');
const isPortfolio = doc.IsPortfolio;

文件包与普通 PDF 附件有什么区别

原因:文件包和附件都会把文件“塞进”PDF,容易混淆两者的用途与判断方式。

解决:PDF 附件(Attachment)把文件作为嵌入文件挂在文档的附件面板里,正文本身通常是一份独立文档;而文件包(Portfolio)基于文件集合(Collection)组织成员文件,成员可以是多份文档,打开后在文件包视图中作为独立文件分别展开与另存。判断时可用 doc.Attachments 查看附件、用 doc.IsPortfolio 判断是否为文件包,二者互不替代。

是不是只能把 PDF 文件加入文件包

原因:示例里先展示的是几个 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 智能体理解后自动完成"批量加密 → 分发清单 → 按需解密"的整个流程。

对比传统SDK API处理

传统 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 批量加密分发与解密

该功能的核心思路是:以一份或多份待处理的 PDF 作为输入,让 AI 智能体按自然语言指令逐份设置打开密码加密保存,或在已知密码的情况下解除保护,并保持原文档版式不变。整个"文件读取 → 密码规则套用 → 加密/解密 → 结果输出"过程由 AI 自动完成,无需针对每份文件编写处理代码。下面通过案例一演示面向收件人的批量加密与分发,通过案例二演示管理员收回文件后的批量解密归档。


案例一:批量加密 PDF 并生成分发清单

批量加密最常见的场景,是把多份敏感 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 都被设置上独立的打开密码并原样保留了原文档的版式,加密分发清单则把每份文件与收件人密码一一对应,供发送方随附件安全送达收件人。后续新增收件人时,只需把新文件加入附件并调整指令中的文件清单,即可完成新一轮加密分发。


案例二:批量解密受密码保护的 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 文档 密码对照表 Excel 与待解密的加密 PDF 文档 解密还原后的无密码 PDF 文档 解密还原后的无密码 PDF 文档


常见问题

设置"打开密码"后,除了打开需要密码,打印/复制/编辑会受影响吗?

原因:打开密码与文档权限是两回事。本文示例仅为文档设置打开密码,不施加"禁止打印""禁止复制"等权限限制。

解决:加密后除打开文档需要密码外,打印、复制、编辑等其他原有功能均不受影响。若出于安全要求需要限制打印或复制,可在指令中追加权限说明(如"仅允许查看,禁止打印与复制"),AI 会按描述一并配置对应权限。

设置打开密码后,加密 PDF 的版式会改变吗?

原因:加密本身只影响文档的打开与权限校验,不涉及页面内容的重排。

解决:Spire.Agent.Office 加密处理会自动保留原文档的布局、字体与页面设置。如担心个别样式变化,可在指令中补充"保持与原文档一致的布局、字体和页面设置"。


获取SpireToken Key

  • 联系 该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。 或访问 https://www.e-iceblue.com/TemLicense.html 获得试用/商业 API 密钥

在代码中配置:

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 才能生成不同层级的折叠按钮。具体操作步骤如下:

  1. 创建一个 Workbook 对象并获取第一个工作表。
  2. 添加一个命名样式并设置字体(用于标题等单元格)。
  3. 通过 Worksheet.PageSetup.IsSummaryRowBelow = false 让汇总行显示在明细行的上方。
  4. 将示例数据写入单元格。
  5. 先对外层行区域(第 2-9 行)调用 GroupByRows(),再对嵌套的内层行区域(第 4-5 行、第 8-9 行)分别调用 GroupByRows()。
  6. 通过 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() 方法。分组只是影响行的折叠显示,取消分组不会删除任何单元格内容;当外层大分组被取消后,原本嵌套在内的内层小分组会保留为独立的单级分组,可再单独取消。具体操作步骤如下:

  1. 创建一个 Workbook 对象,并通过 Workbook.LoadFromFile() 方法加载已含多层分组的工作簿。
  2. 通过 Workbook.Worksheets.get() 方法获取该工作表。
  3. 对外层行区域(第 2-9 行)调用 UngroupByRows(),取消外层大分组。
  4. 对内层行区域(第 4-5 行)调用 UngroupByRows(),取消内层小分组。
  5. 通过 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;

取消多级分组的效果

取消(删除)分组


常见问题

为什么多次调用 GroupByRows 后没有形成多级分组

原因:多级分组要求内层分组的范围必须完全包含在外层分组范围内。如果两次分组的范围互不包含,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 中的图片

页面中的高分辨率位图通常是 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 中的字体并取消字体嵌入

PDF 会把正文用到的字体作为子集嵌入文件内,多个字体、多种字重叠加起来也占不少体积。Options.TextCompressionOptions 提供两种处理字体的方式:

选项 作用 效果与取舍
CompressFonts 压缩内嵌字体数据,保留字体 显示不变,体积更小
UnembedFonts 移除内嵌字体,改用系统字体渲染 体积更小;阅读端缺对应字体时字形可能被替换

UnembedFonts 能压得更小但有显示风险,是否开启需结合文档去向判断:

// 压缩字体数据;UnembedFonts 进一步取消字体内嵌
compressor.Options.TextCompressionOptions.CompressFonts = true;
compressor.Options.TextCompressionOptions.UnembedFonts = true;

压缩 PDF 的内容流

PDF 页面中的文字与矢量绘制指令以“内容流(Content Stream)”的形式存储,生成时虽通常会压缩,但经过多次编辑或不同工具处理后仍可能存在冗余。Options.CompressContents 会重新压缩文档的内容流,开启后对文字较多、版式复杂的文档也能挤出一部分空间:

// 重新压缩文档的内容流
compressor.Options.CompressContents = true;

综合三种方式压缩 PDF 文档

把上面三种方式组合起来即可得到兼顾效果与速度的完整压缩流程:加载 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 文档

综合三种方式压缩后的 PDF 文档


常见问题

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 文档

给 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 附件的合同文档

已嵌入 logo.png 附件的合同文档


删除 PDF 文档中的附件

删除某个指定的附件,用附件集合的 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 文档

删除第一个附件后的 PDF 文档


常见问题

如何判断 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 模块初始化。


在 Excel 中创建表格(Table)

将数据区域转换为表格,是快速获得"自带筛选按钮 + 条纹外观"结构化区域的方式。本例先在工作表中写入一份销售明细(产品、区域、月份、数量、销售额),再通过 ListObjects.Create() 方法把 A1:E13 区域转换为名为 "Table1" 的表格,最后应用内置浅色样式 TableStyleLight9。具体操作步骤如下:

  1. 创建一个 Workbook 对象并获取第一个工作表。
  2. 将表头和示例数据写入单元格。
  3. 调用 Worksheet.ListObjects.Create() 方法,把包含表头的数据区域转换为表格。
  4. 通过 IListObject.BuiltInTableStyle 属性为表格应用内置样式。
  5. 通过 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;

创建表格效果:

在 Excel 中创建表格(Table)


设置表格样式、汇总行与条纹

创建好的表格可以随时重新设置外观:例如将浅色样式换成内置的 Medium 深色样式,在表格底部显示汇总行并让"数量""销售额"两列自动求和,同时开启行条纹与列条纹让数据更易阅读。具体操作步骤如下:

  1. 创建一个 Workbook 对象,并通过 Workbook.LoadFromFile() 方法加载已含表格的工作簿。
  2. 通过 Workbook.Worksheets.get() 方法获取该工作表,再通过 ListObjects.get() 获取表格对象。
  3. 通过 BuiltInTableStyle 属性重新指定内置样式。
  4. 将 DisplayTotalRow 设为 true 显示汇总行,并用 Columns[].TotalsRowLabel、Columns[].TotalsCalculation 设置汇总行的标签与求和方式。
  5. 通过 ShowTableStyleRowStripes、ShowTableStyleColumnStripes 开启行条纹与列条纹。
  6. 通过 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 模块初始化。


接受 Excel 中的所有修订

当一个包含修订记录的工作簿经多人编辑、审阅通过后,需要把所有的改动正式合并进文档,也就是"接受修订"。接受修订后,改动内容成为文档的正式内容,修订记录随之清除。具体操作步骤如下:

  1. 创建一个 Workbook 对象。
  2. 通过 Workbook.LoadFromFile() 方法加载包含修订记录的工作簿。
  3. 调用 Workbook.AcceptAllTrackedChanges() 方法接受文档中的所有修订。
  4. 通过 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;

接受修订后的效果

接受 Excel 中的所有修订


拒绝 Excel 中的所有修订

当修订内容存在争议或不再需要时,审阅人员可以一次性拒绝全部修订,使文档恢复到修改前的状态。具体操作步骤如下:

  1. 创建一个 Workbook 对象。
  2. 通过 Workbook.LoadFromFile() 方法加载包含修订记录的工作簿。
  3. 调用 Workbook.RejectAllTrackedChanges() 方法拒绝文档中的所有修订。
  4. 通过 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;

拒绝修订后的效果

拒绝 Excel 中的所有修订


常见问题

拒绝修订后内容没有恢复到修改前的状态

原因:RejectAllTrackedChanges() 拒绝的是被"跟踪修订"记录下来的单元格改动。如果某些改动发生在启用跟踪修订之前,或者通过其他方式写入且未被记录,它们不会成为可拒绝的修订,处理后仍会保留当前值,因此文档不会完全恢复到最初的基线。

能否只接受或拒绝部分修订,而不是全部

原因:AcceptAllTrackedChanges() 与 RejectAllTrackedChanges() 对整个工作簿的修订进行整体处理,不提供按用户、时间或单元格区域筛选的单条处理接口。


获取免费许可证

如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。

在党政机关和事业单位的日常工作中,公文写作与排版是高频且高度标准化的任务。一份正式公文从拟稿、核稿到印发,既要保证语体庄重、行文规范、逻辑严密,又要严格符合《党政机关公文格式》(GB/T 9704—2012)对版式的明确要求——标题字号、正文字体、层次序数、页边距、行距、页码等均有统一标准。传统做法依赖文秘人员手工起草、反复校对和逐项排版,一份通知从成稿到符合版式往往要耗费半天以上,且不同人员排出的版式细节常有出入。

对比传统SDK API处理

传统 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生成的公文初稿

生成的公文初稿语体庄重、层次清晰,标题、正文与各级标题的字体字号、页边距、行距等均符合公文格式要求。文秘人员只需核对事实数据、补充成文日期与印章信息即可送审,将撰写时间从数小时压缩到几分钟。对于同一机关的高频文种(通知、请示、报告、函),还可以把常用要素固定成统一指令,实现日常公文的一键起草。


公文版式规范化

对于存量公文、下级单位报送的稿件或 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字符)。


获取SpireToken Key

  • 联系 该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。 或访问 https://www.e-iceblue.com/TemLicense.html 获得试用/商业 API 密钥

在代码中配置:

AIOptions options = new AIOptions();
options.SpireToken = key;