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

Spire.Cloud 纯前端文档控件

导出一份订单明细表或对账单时,页面设置往往决定了打印出来的成品是否易读:表格要不要连网格线和行号列标一起打出来、以多高的精度输出、批注跟不跟着走,这些选项分散在 Excel「页面设置」对话框的多个选项卡里,逐个点选费时费力。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端直接完成页面设置,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。

本文主要介绍以下三个核心功能点:

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


设置打印质量与草稿质量

打印质量决定输出时每个点阵的墨滴数,取值是一个以 dpi 为单位的整数;草稿质量则让打印机以更省墨的方式快速输出,适合内部传阅的校样。Spire.XLS for JavaScript 通过 PageSetup 的 PrintQuality 与 Draft 属性设置这两项:

  • PrintQuality = 72 把打印质量设为 72 dpi,取值偏低,出纸更快、耗材更省
  • Draft = true 开启草稿质量,打印速度优先于精细度 具体完整的示例代码如下:
function App() {
  const setPrintQuality = async () => {
    // 获取 Spire.XLS WASM 模块
    const xlsModule = window.wasmModule?.spirexls;

    // 检查模块是否就绪
    if (!xlsModule) {
      alert('Spire.Xls is not ready yet');
      return;
    }

    // 将字体与输入文件载入 VFS
    await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'OrderDetails.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // 加载工作簿,取第一个工作表
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile({ fileName: inputFileName });
    const sheet = workbook.Worksheets.get(0);

    // 取得工作表的页面设置
    const pageSetup = sheet.PageSetup;

    // 把打印质量设为 72 dpi
    pageSetup.PrintQuality = 72;

    // 开启草稿质量,打印速度优先于精细度
    pageSetup.Draft = true;

    // 保存工作簿
    const outputFileName = 'SetPrintQuality.xlsx';
    workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });

    // 释放 workbook 对象以释放资源
    workbook.Dispose();

    // 从 VFS 读取结果文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>设置打印质量与草稿质量</h1>
      <button onClick={setPrintQuality}>设置打印质量与草稿质量</button>
    </div>
  );
}

export default App;

原文件打印效果: 原文件打印效果 运行后,打印质量与草稿质量的效果: 设置打印质量与草稿质量


打印网格线与行号列标

屏幕上的网格线不会跟着数据一起打印出来,如果表格本身没有设置边框,纸质件上就只剩一片浮空的数据,核对时很难定位单元格。行号列标同理。IsPrintGridlines 与 IsPrintHeadings 两个布尔属性分别控制这两项。具体完整的示例代码如下:

function App() {
  const setGridlinesAndHeadings = async () => {
    // 获取 Spire.XLS WASM 模块
    const xlsModule = window.wasmModule?.spirexls;

    // 检查模块是否就绪
    if (!xlsModule) {
      alert('Spire.Xls is not ready yet');
      return;
    }

    // 将字体与输入文件载入 VFS
    await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'OrderDetails.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // 加载工作簿,取第一个工作表
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile({ fileName: inputFileName });
    const sheet = workbook.Worksheets.get(0);

    // 取得工作表的页面设置
    const pageSetup = sheet.PageSetup;

    // 打印时输出网格线
    pageSetup.IsPrintGridlines = true;

    // 打印时输出行号与列标
    pageSetup.IsPrintHeadings = true;

    // 保存工作簿
    const outputFileName = 'SetGridlinesAndHeadings.xlsx';
    workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });

    // 释放 workbook 对象以释放资源
    workbook.Dispose();

    // 从 VFS 读取结果文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>打印网格线与行号列标</h1>
      <button onClick={setGridlinesAndHeadings}>打印网格线与行号列标</button>
    </div>
  );
}

export default App;

运行后,打印网格线与行号列标的效果: 打印网格线与行号列标


设置黑白打印、批注与错误值打印

余下的三项输出开关同样落在 PageSetup 上,分别决定纸质件呈现什么颜色、批注是否随表打印、错误值如何显示。三项属性与取值如下:

  • BlackAndWhite 设为 true 时以黑白模式打印,彩色内容转为灰度输出,只有黑白激光打印机时尤其适用
  • PrintComments 取 InPlace 时,批注框随其在工作表上的位置一起打印;批注需先设为显示状态,未显示的批注不参与打印
  • PrintErrors 取 NA 时,所有错误值(如 #DIV/0!)在纸质件上统一显示为 #N/A,内部公式错误对阅读者不可见

具体完整的示例代码如下:

function App() {
  const setOtherPrintOptions = async () => {
    // 获取 Spire.XLS WASM 模块
    const xlsModule = window.wasmModule?.spirexls;

    // 检查模块是否就绪
    if (!xlsModule) {
      alert('Spire.Xls is not ready yet');
      return;
    }

    // 将字体与输入文件载入 VFS
    await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'OrderDetails.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // 加载工作簿,取第一个工作表
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile({ fileName: inputFileName });
    const sheet = workbook.Worksheets.get(0);

    // 取得工作表的页面设置
    const pageSetup = sheet.PageSetup;

    // 样本在 A14 单元格上有一条批注
    // 先把批注设为显示状态,未显示的批注不参与打印
    sheet.Range.get('A14').Comment.Visible = true;

    // 以黑白模式打印工作表
    pageSetup.BlackAndWhite = true;

    // 批注按其在工作表上的位置打印
    pageSetup.PrintComments = xlsModule.PrintCommentType.InPlace;

    // 单元格错误值统一打印成 #N/A
    pageSetup.PrintErrors = xlsModule.PrintErrorsType.NA;

    // 保存工作簿
    const outputFileName = 'SetOtherPrintOptions.xlsx';
    workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });

    // 释放 workbook 对象以释放资源
    workbook.Dispose();

    // 从 VFS 读取结果文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>设置黑白打印、批注与错误值打印</h1>
      <button onClick={setOtherPrintOptions}>设置黑白打印、批注与错误值打印</button>
    </div>
  );
}

export default App;

运行后,黑白打印、批注与错误值打印的效果: 设置黑白打印、批注与错误值打印


常见问题

表格分了好几页,调低打印质量也没能打印在一页上

原因:打印质量与草稿质量只决定输出的精度与耗墨量,不改变内容的布局,因此再怎么调低,分页位置都不会移动。要把整张表收进一页,需要设置的是页面缩放,与打印质量无关;两者同时设置时并不冲突,只是各自管各自的事。

解决:用 FitToPagesWide 与 FitToPagesTall 把工作表压缩到指定页数,取 1 表示一页宽、一页高:

// 把工作表缩放打印到一页宽、一页高
pageSetup.FitToPagesWide = 1;
pageSetup.FitToPagesTall = 1;

设置了 PrintQuality 与 Draft,工作表看上去没有任何变化

原因:这两项都只作用于打印机输出,不改变工作表本身的内容与显示,因此设置完成后再看文档,页面不会有任何不同——这属于正常现象,并不代表设置没有生效。


获取免费许可证

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

在浏览器端生成报表时,一张订单明细表往往要打印好几页。如果沿用默认的打印设置,第二页之后就会丢失表头和关键列,阅读时无从判断每一列的含义,页码也难以与整份报表的编排衔接。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端直接完成页面设置,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。

本文主要介绍两个核心功能点:

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


设置打印标题行与打印标题列

打印标题对应 Excel「页面设置」对话框里的「顶端标题行」与「左端标题列」两项。设置之后,指定的行或列会在每一页打印输出的相同位置重复出现,表格翻到第二页时依然能看到表头。Spire.XLS for JavaScript 通过 PageSetup 的 PrintTitleRows 与 PrintTitleColumns 属性完成设置,取值是行列区间的引用字符串。具体代码示例如下:

function App() {
  const setPrintTitles = async () => {
    // 获取 Spire.XLS WASM 模块
    const xlsModule = window.wasmModule?.spirexls;

    // 检查模块是否就绪
    if (!xlsModule) {
      alert('Spire.Xls is not ready yet');
      return;
    }

    // 将字体与输入文件载入 VFS
    await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'OrderDetails.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // 加载工作簿,取第一个工作表
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile({ fileName: inputFileName });
    const sheet = workbook.Worksheets.get(0);

    // 取得工作表的页面设置
    const pageSetup = sheet.PageSetup;

    // 把第 1、2 行设为打印标题行,每一页的顶端都重复输出
    pageSetup.PrintTitleRows = '$1:$2';

    // 把 A、B 两列设为打印标题列,每一页的左端都重复输出
    pageSetup.PrintTitleColumns = '$A:$B';

    // 保存工作簿
    const outputFileName = 'SetPrintTitles.xlsx';
    workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });

    // 释放 workbook 对象以释放资源
    workbook.Dispose();

    // 从 VFS 读取结果文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>设置打印标题行与打印标题列</h1>
      <button onClick={setPrintTitles}>设置打印标题行与打印标题列</button>
    </div>
  );
}

export default App;

原文件打印效果 原文件打印效果 运行后,打印标题行与打印标题列的效果: 设置打印标题行与打印标题列


设置打印顺序

当工作表同时超出一页宽和一页高时,Excel 需要在两种分页推进方向中做出选择:默认的「先行后列」先自上而下打满一页高,再换到右侧的下一组列;「先列后行」则先沿列方向打满一页宽,再向下推进。Spire.XLS for JavaScript 用 PageSetup 的 Order 属性在两者之间切换。具体代码示例如下:

function App() {
  const setPrintOrder = async () => {
    // 获取 Spire.XLS WASM 模块
    const xlsModule = window.wasmModule?.spirexls;

    // 检查模块是否就绪
    if (!xlsModule) {
      alert('Spire.Xls is not ready yet');
      return;
    }

    // 将字体与输入文件载入 VFS
    await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'OrderDetails.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // 加载工作簿,取第一个工作表
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile({ fileName: inputFileName });
    const sheet = workbook.Worksheets.get(0);

    // 取得工作表的页面设置
    const pageSetup = sheet.PageSetup;

    // 把打印顺序设为「先列后行」:先自上而下打满一列,再向右换到下一列
    pageSetup.Order = xlsModule.OrderType.OverThenDown;

    // 保存工作簿
    const outputFileName = 'SetPrintOrder.xlsx';
    workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });

    // 释放 workbook 对象以释放资源
    workbook.Dispose();

    // 从 VFS 读取结果文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>设置打印顺序</h1>
      <button onClick={setPrintOrder}>设置打印顺序</button>
    </div>
  );
}

export default App;

运行后,打印顺序设为「先列后行」的效果:

设置打印顺序


常见问题

只设置打印标题行、不设置打印标题列,可以吗

原因:PrintTitleRows 与 PrintTitleColumns 是两个互不影响的属性,前者控制每页顶端重复的行,后者控制每页左端重复的列。只设置其中一个时,另一个保持未设置,不会因为缺了一项而被拒绝,也不会被补上默认值。

解决:按需要设置其中一个即可,例如只重复表头行:pageSetup.PrintTitleRows = '$1:$2'。

设置打印标题后,工作表里的数据会变吗

原因:打印标题属于页面设置,只决定打印时哪些行列重复输出,不改动单元格的内容,也不增删行列。

解决:数据不会受影响。实测设置前与设置后的结果文件同为 62 行 × 18 列,逐格文本完全一致,差别只在页面设置里多了一条打印标题定义;屏幕上看到的工作表与原来相同。


获取免费许可证

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

做一份演示文稿,真正花时间的往往不是排版,而是"讲什么"。幻灯片上只放得下要点:几个短句、一组数字、一张图表,真正要讲出口的内容却全在作者脑子里。等到上台前一天,作者还要再把这份 PPT 从头读一遍,逐页回忆"这一页当时想说什么",然后补出一份讲稿。

更麻烦的是团队协作的场景。市场部的发布会材料由产品经理出内容、市场同事做页面、再由发言人上台,最后接手的人拿到的是一份只有要点的 PPT——他既不知道每页想强调什么,也不知道数据背后的结论,只能凭猜测临场发挥。备注页(Speaker Notes)本就是为了解决这个问题而存在的:它不会出现在投影画面上,只在演讲者视图里可见,是"作者写给讲者的话"。但现实中,几乎没有人有耐心逐页去填。

Spire.Agent.Office 的 PowerPoint AI 能力可以直接用自然语言描述"备注要写什么、讲稿要什么口吻",AI 智能体读懂每一页的标题与要点后,自动完成两类工作:为每一页补写演讲备注,以及把整份演示文稿展开成一份可以直接照读的 Word 讲稿。

对比传统 SDK API 处理

传统 Spire.Office for .NET API Spire.Agent.Office 处理
驱动方式 遍历幻灯片、取出文本框内容、再自行拼装文案并写回备注页,每一步都要编码 用自然语言描述备注与讲稿的写作要求,AI 理解后自动编排执行路径
代码量 需要 150-300 行 C# 代码(含幻灯片遍历、形状遍历、文本提取、备注页写入、字体与段落设置等) 约 10 行调用代码 + 1 条自然语言指令
内容生成 需要自行接入大模型或手写文案模板,且难以兼顾每页的语境 AI 直接理解每页要点,按页生成语境连贯的文案
版式处理 写入备注时容易破坏原有版式与占位符结构 AI 自动保留原版式、字体与配色,只在备注栏追加内容
需求变更 调整语气、字数、讲稿格式 → 改代码 → 编译 → 重新部署 修改指令中的描述,即刻生效

本文介绍如何使用 Spire.Agent.Office 为 PowerPoint 演示文稿生成演讲备注与讲稿。全文通过两个案例展示"为已有 PPT 补写备注"与"把汇报内容整理成 Word 讲稿"两种典型用法:

有关产品安装和 SpireToken 配置,请参考 在 .NET 项目中集成 Spire.Agent.Office。以下示例默认已安装 Spire.Agent.Office 并完成 SpireToken 配置。


案例一:为演示文稿逐页生成演讲备注

最常见的情况是:PPT 已经定稿,页面版式和内容都不需要再动,缺的只是一份备注。PPT 文件为了便于观看者理解,每页只有标题与几行要点——重点落在哪里、数字背后是什么、这一页怎么接到下一页,都还留在作者的脑子里。备注栏从头到尾都是空的,而演讲备注要做的,正是把这一层交接出去:它是作者留给登台者的交代,说清这一页真正想讲什么、哪里该着重、又该如何自然地引到下一页。发言人需要的是每页一小段提示——不必长篇大论,但要够他拿着资料讲,而不是照着幻灯片念。手工补备注的代价在于"逐页思考":一页一页读过去,重新组织语言,写少了没提示作用,写多了又会变成照本宣科。页数少尚可忍受,几十页的年度汇报就难免草草了事。

以下示例使用 Spire.Agent.Office 智能体,通过自然语言指令逐页阅读幻灯片内容,为每一页生成可直接照读的演讲备注并写入备注栏:

using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Presentation;

// PPT 处理相关配置
string inputPath = @"E:\product-launch.pptx";  // 待添加演讲备注的演示文稿路径
string savePath = @"E:\product_result.pptx";  // 结果文档路径

string key = "**************************";  // SpireToken Key
string instruction = "为这份输入PPT文件逐页生成演讲备注,写入每页的备注栏:" +
"1. 逐页阅读幻灯片标题与要点,理解该页要传达的信息;" +
"2. 为每一页撰写一段演讲备注,用演讲者口吻、可直接照读;" +
"3. 备注要说清这页的讲解重点,并自然过渡到下一页,备注需简繁有序,重要内容需详细说明,过渡页简单说明;" +
"4. 保持原有的幻灯片版式、字体、配色与页面顺序完全不变,不要增删任何幻灯片;" +
"最终输出保存为PPT文件";

// 调用PPT文档处理函数
AIResult result = ExecuteDemoPpt(instruction, inputPath, savePath, key);

// 执行PPT文档AI处理
static AIResult ExecuteDemoPpt(string instruction, string inputPath, string savePath, string key)
{
    // 创建AIOptions选项配置对象
    AIOptions options = new AIOptions();
    options.SpireToken = key;  // 设置SpireToken Key

    // 使用Presentation对象处理PPT文档
    using (Presentation ppt = new Presentation())
    {
        // 从文件加载PPT文档
        if (!string.IsNullOrEmpty(inputPath) && File.Exists(inputPath))
        {
            ppt.LoadFromFile(inputPath);
        }
        // 创建AI文档处理器
        AIDocumentProcessor processor = ppt.AI(options);

        // 执行AI指令
        return processor.ExecuteInstruction(ppt, instruction, savePath);
    }
}

备注栏为空的原始演示文稿 备注栏为空的原始演示文稿 逐页写入演讲备注后的演示文稿 逐页写入演讲备注后的演示文稿

生成后的演示文稿在投影画面上与原件完全一致,备注只出现在演讲者视图中。发言人打开演示者视图,就能在每一页下方看到该页的提示文案,按顺序念下来即可完成整场产品发布。


案例二:把汇报内容整理成可照读的 Word 讲稿

备注栏适合"提示",却不适合"照读"。备注以短句为主,字号小、位置偏,站在台上低头看备注既影响台风,也容易漏词。当场合更正式——例如季度经营汇报要向管理层逐项说明数据——讲者需要的是一份完整的、口语化的讲稿。讲稿的载体值得单独说明:演示文稿是给人看的,讲稿是给人读的。讲稿要控制字号、行距与分节,通常还要打印出来带进会场;用 PPT 承载每页几百字的正文,排版与阅读体验都不合适,投影出去更是把台词直接给了观众。所以本案例的输入仍是 PPT,输出是一份 Word 讲稿文档。

跨格式处理正是 Spire.Agent.Office 可以直接做的事:仍用 Presentation 加载演示文稿,由 AI 读取每页内容,再按指令把讲稿写成 Word 文件。因此本案例让 AI 先逐页"读进"数据,再按原页顺序分节写成讲稿:原演示文稿的每一页,对应讲稿文档中的一节,节标题沿用原标题,正文则是可以直接念出口的完整讲稿。

以下示例使用 Spire.Agent.Office 智能体,通过自然语言指令逐页解读数据并撰写讲稿,输出为 Word 文档:

using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Presentation;

// PPT 处理相关配置
string inputPath = @"E:\quarterly-review.pptx";  // 输入演示文稿路径
string savePath = null;  // 结果文档路径(此处为null,将使用下面设置的输出文件夹路径)
string OutDir = @"E:\output-script";  // 输出目录
string key = "**************************";  // SpireToken Key
string instruction =
    "读取输入 PPT 文件,为 PPT 演讲者撰写完整的讲稿,输出为一份Word讲稿文档:" +
    "1. 逐页阅读幻灯片上的数据与要点,理解该页的核心结论;" +
    "2. 为每一页撰写一段完整、口语化的讲稿,可直接照读,内容简繁有序,讲稿要引用该页的具体数据,并说明数字背后的经营含义;" +
    "3. 讲稿按原幻灯片顺序分节编排,每节以'第N页 + 原幻灯片标题'作为小标题,正文为该页讲稿全文;" +
    "4. 正文排版便于朗读:字体清晰,字号不小于14磅,行距1.5倍,段间距适中;";

// 调用PPT文档处理函数
AIResult result = ExecuteDemoPpt(instruction, inputPath, savePath, key, OutDir);

// 执行PPT文档AI处理
static AIResult ExecuteDemoPpt(string instruction, string inputPath, string savePath, string key, string output)
{
    // 创建AIOptions选项配置对象
    AIOptions options = new AIOptions();
    options.WorkDir = output;  // 设置工作目录为输出目录
    options.SpireToken = key;  // 设置SpireToken Key

    // 使用Presentation对象处理PPT文档
    using (Presentation ppt = new Presentation())
    {
        // 从文件加载PPT文档
        if (!string.IsNullOrEmpty(inputPath) && File.Exists(inputPath))
        {
            ppt.LoadFromFile(inputPath);
        }
        // 创建AI文档处理器
        AIDocumentProcessor processor = ppt.AI(options);

        // 执行AI指令
        return processor.ExecuteInstruction(ppt, instruction, savePath);
    }
}

生成的 Word 版讲稿文档 生成的 Word 版讲稿文档


常见问题

想接入自己的模型或私有化部署,该改哪里

说明:默认情况下 AI 请求走 Spire 的模型服务。AIOptions 另外提供了 Model、BaseUrl 与 ApiKey 三个属性,分别用于指定模型名称、服务地址与访问密钥,改走自建或第三方推理服务时配置这三项即可。

AIOptions options = new AIOptions();
options.BaseUrl = "baseUrl";
options.Model = "modelName";
options.SpireToken = "*************";
options.ApiKey = "*************";

演示文稿页数一多,处理就很慢,甚至会超时

原因:生成备注和讲稿是逐页进行的,AI 需要反复读写文档,每次都要把已有内容重新送进模型。页数越多、单页文字越密,消耗的 token 增长得越快,整体耗时也随之拉长,默认的超时时间往往不够用。

解决:在创建 AIOptions 时显式设置 TimeoutMs(单位毫秒),给长文档留出足够的处理时间。如 50 页的 PPT 文件,TimeoutMs 的值可以给到 10 分钟(600000)。


获取 SpireToken Key

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

在代码中配置:

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

表单填完之后下一步通常是归档。可 PDF 表单域偏偏在这里留下口子:文件看着已经填好,控件却还在,收件人随手就能改掉金额、日期或者签字栏,有的阅读器打开时还会重新校验一遍。要把这份文件变成谁都改不动的成品,就得把控件连同值一起压进页面内容里。

本文用 Spire.PDF for JavaScript 来实现 PDF 表单域的扁平化。它基于 WebAssembly 在浏览器端加载、修改与保存文档,通过虚拟文件系统(VFS)读写文件,无需后端配合。

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

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


扁平化整个表单

Spire.PDF for JavaScript 提供 PdfForm.IsFlatten 属性,用于把整份表单一次性扁平化。置为 true 后,文档里所有字段连同当前值一起转成静态页面内容,保存出来的 PDF 不再有可交互的控件;文字仍留在文本层,照样可以选中、复制和检索。

function App() {
  const flattenWholeForm = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

    // 检查模块是否就绪
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // 将待处理的 PDF 文件载入 VFS
    const inputFileName = '员工登记表.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // 创建 PdfDocument 对象并加载 PDF 文档
    let doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    // 一次性扁平化整份表单
    doc.Form.IsFlatten = true;

    const outputFileName = '扁平化表单.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>扁平化整个表单</h1>
      <button onClick={flattenWholeForm}>
        开始扁平化
      </button>
    </div>
  );
}

export default App;

所有字段的输入框都消失,值留在页面上成为普通文本:

所有字段的输入框都消失,值留在页面上成为普通文本


扁平化指定字段

Spire.PDF for JavaScript 还提供 PdfField.Flatten 属性,用于按字段粒度扁平化。字段实例要从 PdfFormWidget 的 FieldsWidget 集合里按 Name 取,只有挑中的那个字段被固化,其余保持可编辑——比如把已经核对过的邮箱冻结,同时留出日期栏给收件人填。

function App() {
  const flattenSelectedField = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

    // 检查模块是否就绪
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // 将待处理的 PDF 文件载入 VFS
    const inputFileName = '员工登记表.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // 创建 PdfDocument 对象并加载 PDF 文档
    let doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    // 从表单拿到 Widget 集合
    let formWidget = new pdfModule.PdfFormWidget(doc.Form.H);

    // 按名称挑出目标字段,只扁平化它
    for (let i = 0; i < formWidget.FieldsWidget.Count; i++) {
      let field = formWidget.FieldsWidget.get_Item({ index: i });
      if (field.Name === 'email') {
        field.Flatten = true;
      }
    }

    const outputFileName = '扁平化指定字段.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>扁平化指定字段</h1>
      <button onClick={flattenSelectedField}>
        开始扁平化
      </button>
    </div>
  );
}

export default App;

只有邮箱的输入框消失,其余字段仍是可编辑的控件:

只有邮箱的输入框消失,其余字段仍是可编辑的控件


常见问题

设置了 IsFlatten,字段在阅读器里还是能点

原因:代码里混用了两个字段集合。doc.Form.Fields 对由其它工具生成的 AcroForm 常常一个字段都读不到,本文样例文档的 Count 读回就是 0,对它调 get_Item() 会直接抛 ArgumentOutOfRange_IndexMustBeLess;就算能读到,那里返回的 PdfField 也没有 Text、Checked 这类控件属性,改它不会影响页面。

解决:整表扁平化只用 doc.Form.IsFlatten,与集合无关;只要涉及逐个字段——按名挑选、写值、单独固化——一律改走 PdfFormWidget:

let formWidget = new pdfModule.PdfFormWidget(doc.Form.H);
for (let i = 0; i < formWidget.FieldsWidget.Count; i++) {
  console.log(formWidget.FieldsWidget.get_Item({ index: i }).Name);
}

怎么判断一份 PDF 是否已经扁平化

原因:PdfForm.IsFlatten 是写入指令,不是状态位。把已经扁平化的产物重新载入,doc.Form.IsFlatten 照旧读回 false——实测此时文档里的字段数已经是 0。

解决:按字段数量判断,FieldsWidget.Count 为 0 就是已经没有可交互的控件了:

let formWidget = new pdfModule.PdfFormWidget(doc.Form.H);
const hasFormFields = formWidget.FieldsWidget.Count > 0;

只扁平化了指定字段,值却没有跟着固化

原因:Name 是逐字符比较,区分大小写,也保留首尾空格。写成 company_name 而文档里实际是 company_name (末尾带空格),循环里一次都不会命中,而且不报错、文件原样输出。

解决:先把所有字段名打印一遍再照着复制,判断与赋值都必须落在 FieldsWidget 给出的 *FieldWidget 实例上:

for (let i = 0; i < formWidget.FieldsWidget.Count; i++) {
  console.log(formWidget.FieldsWidget.get_Item({ index: i }).Name);
}

获取免费许可证

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

登记表、报名表、问卷这类 PDF 表单,发出去时是空白的,收回来要靠人工逐份填;表单本身也常常要改——缺一个输入框,多出一栏已经不需要的勾选项,都得动。用 Acrobat 一类桌面软件处理,一份两份还能忍,成批就只剩手工点。

本文介绍用 Spire.PDF for JavaScript 实现添加、填充和删除 PDF 表单域。它基于 WebAssembly 在浏览器端直接加载、修改与保存 PDF 文档,全过程在本地完成,通过虚拟文件系统(VFS)读写文件,无需后端配合。

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

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


添加表单域

Spire.PDF for JavaScript 提供了一整套表单字段类,覆盖文本框、复选框、单选按钮、下拉框、列表框、按钮与签名域。它们的用法一致:在页面上创建实例、用 Bounds 定位,再交给 doc.Form.Fields.Add() 登记;加载已有文档时,还要先把 doc.AllowCreateForm 设为 true。

类名 说明
PdfTextBoxField 文本框域
PdfCheckBoxField 复选框域
PdfRadioButtonListField 单选按钮域
PdfComboBoxField 下拉框域
PdfListBoxField 列表框域
PdfButtonField 按钮域
PdfSignatureField 签名域

Bounds、BorderWidth、BorderStyle、Required、ReadOnly、Visible、ToolTip 这几项则由所有字段共有。

function App() {
  const addFormFields = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

    // 检查模块是否就绪
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // 将待处理的 PDF 文件载入 VFS
    const inputFileName = '空白登记表.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // 创建 PdfDocument 对象并加载 PDF 文档
    let doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    // 对已有文档,必须显式开启表单创建
    doc.AllowCreateForm = true;

    let page = doc.Pages.get_Item(0);
    let uiFont = new pdfModule.PdfFont({
      fontFamily: pdfModule.PdfFontFamily.Helvetica,
      size: 10
    });
    const box = (x, y, width, height) => new pdfModule.RectangleF({ x, y, width, height });
    const border = 0.75;

    // 1. 文本框:姓名
    let nameBox = new pdfModule.PdfTextBoxField(page, 'name');
    nameBox.Bounds = box(178, 168, 210, 20);
    nameBox.BorderWidth = border;
    nameBox.BorderStyle = pdfModule.PdfBorderStyle.Solid;
    nameBox.Font = uiFont;
    doc.Form.Fields.Add(nameBox);

    // 2. 文本框:邮箱
    let emailBox = new pdfModule.PdfTextBoxField(page, 'email');
    emailBox.Bounds = box(178, 206, 210, 20);
    emailBox.BorderWidth = border;
    emailBox.BorderStyle = pdfModule.PdfBorderStyle.Solid;
    emailBox.Font = uiFont;
    doc.Form.Fields.Add(emailBox);

    // 3. 下拉框:部门
    let departmentBox = new pdfModule.PdfComboBoxField(page, 'department');
    departmentBox.Bounds = box(178, 244, 210, 20);
    departmentBox.BorderWidth = border;
    departmentBox.Font = uiFont;
    ['Engineering', 'Marketing', 'Sales', 'Support'].forEach(function (item) {
      departmentBox.Items.Add(new pdfModule.PdfListFieldItem({ text: item, value: item.toLowerCase() }));
    });
    doc.Form.Fields.Add(departmentBox);

    // 4. 单选按钮:性别,每个选项是一个 PdfRadioButtonListItem
    let genderBox = new pdfModule.PdfRadioButtonListField(page, 'gender');
    ['male', 'female'].forEach(function (value, index) {
      let item = new pdfModule.PdfRadioButtonListItem();
      item.Bounds = box(185.5 + index * 110, 285.5, 13, 13);
      item.BorderWidth = border;
      item.Value = value;
      genderBox.Items.Add(item);
    });
    doc.Form.Fields.Add(genderBox);

    // 5. 列表框:学历
    let educationBox = new pdfModule.PdfListBoxField(page, 'education');
    educationBox.Bounds = box(178, 320, 210, 52);
    educationBox.BorderWidth = border;
    educationBox.Font = uiFont;
    ['Bachelor', 'Master', 'Doctor'].forEach(function (item) {
      educationBox.Items.Add(new pdfModule.PdfListFieldItem({ text: item, value: item.toLowerCase() }));
    });
    doc.Form.Fields.Add(educationBox);

    // 6. 复选框:同意条款
    let agreeBox = new pdfModule.PdfCheckBoxField(page, 'agree_terms');
    agreeBox.Bounds = box(178, 392, 15, 15);
    agreeBox.BorderWidth = border;
    agreeBox.Style = pdfModule.PdfCheckBoxStyle.Check;
    agreeBox.Required = true;
    doc.Form.Fields.Add(agreeBox);

    // 7. 签名域:留出签名位置
    let signatureBox = new pdfModule.PdfSignatureField(page, 'signature');
    signatureBox.Bounds = box(178, 424, 210, 40);
    doc.Form.Fields.Add(signatureBox);

    // 8. 按钮:提交
    let submitButton = new pdfModule.PdfButtonField(page, 'submit');
    submitButton.Bounds = box(72, 478, 90, 26);
    submitButton.Text = '提 交';
    submitButton.HighlightMode = pdfModule.PdfHighlightMode.Push;
    doc.Form.Fields.Add(submitButton);

    const outputFileName = '添加表单域.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>添加表单域</h1>
      <button onClick={addFormFields}>
        开始添加
      </button>
    </div>
  );
}

export default App;

空白登记表上补齐了文本框、复选框、单选按钮、下拉框、列表框、按钮与签名域:

空白登记表上补齐了文本框、复选框、单选按钮、下拉框、列表框、按钮与签名域


填充表单域

填充现有表单要走 Widget 视角:PdfFormWidget 包住文档的表单,FieldsWidget 逐个给出字段实例,判断类型后转成对应子类再写值——文本用 Text,复选框用 Checked,下拉框用 SelectedIndex。字段之间靠 Name 区分,遍历一遍就能把整份表单填满。

function App() {
  const fillFormFields = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

    // 检查模块是否就绪
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // 将待填写的 PDF 文件载入 VFS
    const inputFileName = '员工登记表.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // 创建 PdfDocument 对象并加载 PDF 文档
    let doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    // 从表单拿到 Widget 集合
    let formWidget = new pdfModule.PdfFormWidget(doc.Form.H);

    for (let i = 0; i < formWidget.FieldsWidget.Count; i++) {
      let field = formWidget.FieldsWidget.get_Item({ index: i });

      // 文本框:直接写入 Text
      if (field instanceof pdfModule.PdfTextBoxFieldWidget) {
        switch (field.Name) {
          case 'name':
            field.Text = 'Chen Jing';
            break;
          case 'email':
            field.Text = '该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。';
            break;
        }
      }

      // 下拉框:SelectedIndex 接收的是下标数组
      if (field instanceof pdfModule.PdfComboBoxWidgetFieldWidget) {
        if (field.Name === 'department') {
          field.SelectedIndex = [1];
        }
      }

      // 复选框:Checked 置为 true 即勾选
      if (field instanceof pdfModule.PdfCheckBoxWidgetFieldWidget) {
        if (field.Name === 'agree_terms') {
          field.Checked = true;
        }
      }
    }

    const outputFileName = '填写表单域.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>填充表单域</h1>
      <button onClick={fillFormFields}>
        开始填写
      </button>
    </div>
  );
}

export default App;

文本框、下拉框与复选框都已写入对应内容:

文本框、下拉框与复选框都已写入对应内容


删除表单域

删除同样从 FieldsWidget 入手,先按 Name 找到目标实例,再调用 Remove() 把它从字段集合里摘掉。用字段名定位比按下标稳妥,文档改过版式也不会删错。

function App() {
  const deleteFormField = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

    // 检查模块是否就绪
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // 将待处理的 PDF 文件载入 VFS
    const inputFileName = '员工登记表.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // 创建 PdfDocument 对象并加载 PDF 文档
    let doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    let form = doc.Form;
    if (form != null) {
      let formWidget = new pdfModule.PdfFormWidget(form.H);

      // 按名称定位目标字段并移除
      for (let i = 0; i < formWidget.FieldsWidget.Count; i++) {
        let field = formWidget.FieldsWidget.get_Item({ index: i });
        if (field.Name === 'name') {
          formWidget.FieldsWidget.Remove(field);
          break;
        }
      }
    }

    const outputFileName = '删除表单域.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>删除表单域</h1>
      <button onClick={deleteFormField}>
        开始删除
      </button>
    </div>
  );
}

export default App;

「姓名」文本框已从表单中移除:

「姓名」文本框已从表单中移除


常见问题

新增的表单域在阅读器里点不动

原因:AllowCreateForm 默认为 false。用 LoadFromFile 打开已有文档时,Spire.PDF 会沿用文档原有的表单结构,不允许追加字段;此时调用 doc.Form.Fields.Add() 不会报错,但保存出来的文档里看不到新字段。

解决:加载文档后、添加字段前,把该属性打开:

doc.LoadFromFile(inputFileName);
doc.AllowCreateForm = true;

填充后字段仍然是空的

原因:case 分支里的字段名与文档中的实际名称不是逐字符相同。PDF 字段名区分大小写,也保留首尾空格,形如 company_name (末尾带空格)的名称在代码里写成 company_name 就永远匹配不上。另一种常见写法错误是拿 doc.Form.Fields 里的对象直接赋值,那里的字段没有 Text 这类 Widget 属性。

解决:填充前先把所有字段名打印一遍,照着复制;赋值必须落在 PdfFormWidget 给出的 *FieldWidget 实例上:

let formWidget = new pdfModule.PdfFormWidget(doc.Form.H);
for (let i = 0; i < formWidget.FieldsWidget.Count; i++) {
  console.log(formWidget.FieldsWidget.get_Item({ index: i }).Name);
}

删除一个字段,后面的字段也跟着不见了

原因:Remove() 会立即改变 FieldsWidget.Count,被删元素之后的字段整体前移一位。如果正序遍历并在循环里连续删除,下一次迭代的 i 已经跳过了前移过来的那个元素。

解决:一次只删一个就 break;要删多个时按名称收集目标后逐个处理,或者从末尾倒序遍历:

// 倒序遍历,逐个移除名称以 temp_ 开头的字段
for (let i = formWidget.FieldsWidget.Count - 1; i >= 0; i--) {
  let field = formWidget.FieldsWidget.get_Item({ index: i });
  if (field.Name.startsWith('temp_')) {
    formWidget.FieldsWidget.Remove(field);
  }
}

获取免费许可证

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

在 Word 文档中插入分页符,是控制章节起始位置、避免标题与正文被跨页割裂时最常用的排版手段;而清理文档中冗余的分页符,则是处理从其他文档复制内容后遗留空白页的关键步骤。Spire.Doc for JavaScript 基于 WebAssembly 在浏览器端直接完成这些操作,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。

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

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


插入分页符

插入分页符的核心流程分为三个阶段:首先通过 FetchFileToVFS 将字体文件和目标 Word 文档载入 WASM 虚拟文件系统;然后实例化 Document 加载文档,定位到目标段落并调用 AppendBreak 传入 BreakType.PageBreak,将分页符作为该段落的子对象追加到末尾;最后从 VFS 读取保存后的文件,封装为 Blob 后生成下载链接。

function App() {
  const InsertPageBreak = async () => {
    const docModule = window.wasmModule?.spiredoc;
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // 将目标 Word 文档载入 VFS
    const inputFileName = "Template_Docx_1.docx";
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);

    // 创建 Document 实例并加载文档
    const doc = new docModule.Document();
    doc.LoadFromFile(inputFileName);

    // 定位到第一节的第 4 个段落,在其末尾插入分页符
    doc.Sections.get_Item(0).Paragraphs.get_Item(3).AppendBreak(docModule.BreakType.PageBreak);

    // 定义输出文件名
    const outputFileName = "InsertPageBreak_out.docx";

    // 将文档保存到 VFS
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });

    doc.Dispose();

    const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

 return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>给Word文档插入分页符</h1>
      <button onClick={InsertPageBreak}>开始</button>
    </div>
  );
}
export default App;

插入分页符后生成的文档效果

插入分页符后生成的文档效果


删除分页符

删除分页符的核心流程同样分为三个阶段:首先通过 FetchFileToVFS 将字体文件和包含分页符的 Word 文档载入 WASM 虚拟文件系统;然后实例化 Document 加载文档,依次遍历每一节中每一个段落的子对象,通过 DocumentObjectType 判断该对象是否为分页符,并用 ChildObjects.Remove 将其移除;最后从 VFS 读取保存后的文件,封装为 Blob 后生成下载链接。

function App() {
  const RemovePageBreaks = async () => {
    const docModule = window.wasmModule?.spiredoc;
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // 将目标 Word 文档载入 VFS
    const inputFileName = "Template_Docx_4.docx";
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);

    // 创建 Document 实例并加载文档
    const doc = new docModule.Document();
    doc.LoadFromFile(inputFileName);

    // 获取第一节
    const section = doc.Sections.get_Item(0);

    // 遍历节中的每一个段落
    for (let j = 0; j < section.Paragraphs.Count; j++) {
        const p = section.Paragraphs.get_Item(j);

        // 倒序遍历段落的子对象,避免移除元素后索引错位
        for (let i = p.ChildObjects.Count - 1; i >= 0; i--) {
            const obj = p.ChildObjects.get_Item(i);

            // 判断对象是否为分页符
            if (obj.DocumentObjectType == docModule.DocumentObjectType.Break
                && obj.BreakType == docModule.BreakType.PageBreak) {
                // 将分页符从段落中移除
                p.ChildObjects.Remove(obj);
            }
        }
    }

    // 定义输出文件名
    const outputFileName = "RemovePageBreaks_out.docx";

    // 将文档保存到 VFS
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
    doc.Dispose();

    const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

 return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>移除Word文档中的分页符</h1>
      <button onClick={RemovePageBreaks}>开始</button>
    </div>
  );
}
export default App;

删除分页符后生成的文档效果

删除分页符后生成的文档效果


常见问题

插入分页符后新页顶部出现多余空行

原因:AppendBreak 是把分页符追加到目标段落的末尾,因此该段落自身的段后间距会随分页符一起被带到新页的开头,视觉上表现为新页顶部多出一段空白。若段落启用了自动间距,这段空白的高度还会随字体大小变化。

解决:插入分页符前先清除目标段落的段后间距:

const para = document.Sections.get_Item(0).Paragraphs.get_Item(3);

// 关闭自动段后间距,并将段后间距设为 0
para.Format.AfterAutoSpacing = false;
para.Format.AfterSpacing = 0;

// 再插入分页符
para.AppendBreak(wasmModule.BreakType.PageBreak);

删除分页符后文档仍然分页

原因:文档中的分页效果并不都由分页符对象产生。若段落本身设置了「段前分页」属性,即使分页符已被移除,该段落仍会从新的一页开始,只遍历 ChildObjects 删除 DocumentObjectType.Break 对象无法去除这类分页。

解决:在删除分页符对象的同时,重置段落的段前分页属性:

const section = document.Sections.get_Item(0);

for (let j = 0; j < section.Paragraphs.Count; j++) {
  const p = section.Paragraphs.get_Item(j);

  // 清除段落自带的「段前分页」属性
  if (p.Format.PageBreakBefore) {
    p.Format.PageBreakBefore = false;
  }
}

获取免费许可证

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

装订线是页面为装订预留的额外留白,用于文档打印装订成册时避免正文被订书钉或胶装边遮挡。由于它属于页面设置的一部分,必须在生成文档时确定,无法在打印环节补上。Spire.Doc for JavaScript 基于 WebAssembly 在浏览器端直接完成页面设置,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。

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

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


添加装订线

添加装订线的核心流程分为三个阶段:首先通过 FetchFileToVFS 将字体文件和目标 Word 文档载入 WASM 虚拟文件系统;然后实例化 Document 加载文档,获取目标节后通过 PageSetup.Gutter 设置装订线宽度,该值以磅为单位,默认加在页面左侧;最后从 VFS 读取保存后的文件,封装为 Blob 后生成下载链接。

function App() {
  const AddGutter = async () => {
    const docModule = window.wasmModule?.spiredoc;
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // 将目标 Word 文档载入 VFS
    const inputFileName = "GutterSample.docx";
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);

    // 创建 Document 实例并加载文档
    const doc = new docModule.Document();
    doc.LoadFromFile(inputFileName);

    // 获取第一节
    const section = doc.Sections.get_Item(0);

    // 设置装订线宽度,单位为磅,默认作用于页面左侧
    section.PageSetup.Gutter = 100;

    // 定义输出文件名
    const outputFileName = "AddGutter_out.docx";

    // 将文档保存到 VFS
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });

    // 释放资源
    doc.Dispose();

    const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

 return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>添加装订线</h1>
      <button onClick={AddGutter}>开始</button>
    </div>
  );
}
export default App;

添加装订线后生成的文档效果

添加装订线后生成的文档效果


设置装订线位置

装订线位置决定这段留白加在页面的哪一侧:PageSetup.IsTopGutter 为 true 时表示顶部装订线,为 false(默认值)时表示左侧装订线。核心流程同样分为三个阶段:首先通过 FetchFileToVFS 将字体文件和目标 Word 文档载入 WASM 虚拟文件系统;然后实例化 Document 加载文档,获取目标节后将 PageSetup.IsTopGutter 设为 true 打开顶部装订线,再通过 PageSetup.Gutter 设置装订线宽度;最后从 VFS 读取保存后的文件,封装为 Blob 后生成下载链接。

function App() {
  const SetGutterPosition = async () => {
    const docModule = window.wasmModule?.spiredoc;
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // 将目标 Word 文档载入 VFS
    const inputFileName = "GutterSample.docx";
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);

    // 创建 Document 实例并加载文档
    const doc = new docModule.Document();
    doc.LoadFromFile(inputFileName);

    // 获取第一节
    const section = doc.Sections.get_Item(0);

    // 将装订线位置设为顶部
    section.PageSetup.IsTopGutter = true;

    // 设置装订线宽度,单位为磅
    section.PageSetup.Gutter = 100;

    // 定义输出文件名
    const outputFileName = "SetGutterPosition_out.docx";

    // 将文档保存到 VFS
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });

    // 释放资源
    doc.Dispose();

    const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([modifiedFileArray], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

 return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>设置在Word文档中装订线的位置</h1>
      <button onClick={SetGutterPosition}>开始</button>
    </div>
  );
}
export default App;

设置顶部装订线后生成的文档效果

设置顶部装订线后生成的文档效果


常见问题

装订线宽度与 Word 界面显示的数值不一致

原因:PageSetup.Gutter 的单位是磅(point),而 Word 页面设置对话框中默认以厘米或英寸显示。示例中直接传入 100,代表 100 磅,约合 3.53 厘米,在 A4 页面上会明显偏宽。

解决:按单位换算后再传入。1 磅等于 1/72 英寸,1 英寸等于 2.54 厘米,因此厘米转磅的公式为 磅 = 厘米 ÷ 2.54 × 72:

// 将 1.5 厘米的装订线宽度换算为磅
const gutterInPoints = (1.5 / 2.54) * 72;
section.PageSetup.Gutter = gutterInPoints;

设置装订线后正文可用宽度过窄

原因:装订线是在页边距之外额外占用的空间,它不会自动扩大页面纸面,而是把正文区域向内压缩。若原有页边距已经较大,再加上装订线后正文可用宽度会进一步缩小,出现每行字数过少、段落频繁折行的情况。

解决:在设置装订线的同时,相应调小同一侧的页边距,保证正文可用宽度:

const pageSetup = section.PageSetup;

// 设置 1.5 厘米的装订线(厘米换算为磅)
pageSetup.Gutter = (1.5 / 2.54) * 72;

// 装订线占用左侧空间,相应调小左边距
pageSetup.Margins.Left = 60;

获取免费许可证

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

给 PDF 补文字往往排在生成流程的收尾:单据编号、审核意见、说明批注,或者压在图上一层浅色大字。几页文档手动敲一下还好,等文字要跟着数据走——编号逐份变化、说明斜排在页角、浅色字压在图表上——手工排版就吃力了。

本文介绍用 Spire.PDF for JavaScript 在 PDF 页面中绘制文本,包括渐变填充的文字、在矩形框内排版的文字,以及旋转变形与半透明的文字。它基于 WebAssembly 在浏览器端直接创建与保存 PDF 文档,全过程在本地完成,通过虚拟文件系统(VFS)读写文件,无需后端配合。

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

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


绘制颜色渐变的文本

文字的颜色来自 DrawString 的填充刷子,换上一把渐变刷子 PdfLinearGradientBrush,字就会沿指定方向从一种颜色过渡到另一种——方向由 mode 决定,起止范围由刷子上的 rect 圈定。

function App() {
  const drawGradientText = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

    // 检查模块是否就绪
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // 将宋体载入 VFS
    await window.spire.FetchFileToVFS('SIMSUN.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // 创建 PDF 文档并添加一个空白页面
    const doc = new pdfModule.PdfDocument();
    const page = doc.Pages.Add();

    const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 24 });
    const text = '颜色渐变的文本';

    // 量出这行文字的宽度,让渐变的起止范围与文字等宽
    const textWidth = font.MeasureString({ text: text }).Width;

    // 横向渐变:从红色过渡到蓝色
    const gradient = new pdfModule.PdfLinearGradientBrush({
      rect: new pdfModule.RectangleF({ x: 40, y: 90, width: textWidth, height: 40 }),
      color1: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Red() }),
      color2: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Blue() }),
      mode: pdfModule.PdfLinearGradientMode.Horizontal,
    });

    // 文字落点与渐变矩形的左端对齐,红蓝两色完整扫过整行
    page.Canvas.DrawString({ s: text, font: font, brush: gradient, x: 40, y: 110 });

    // 定义输出文件名并保存文档
    const outputFileName = '渐变文本.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>绘制颜色渐变的文本</h1>
      <button onClick={drawGradientText}>
        开始绘制
      </button>
    </div>
  );
}

export default App;

渐变矩形与文字等宽时,红色到蓝色完整扫过整行文字:

渐变矩形与文字等宽时,红色到蓝色完整扫过整行文字


绘制在矩形框内排版的文本

DrawString 的落点除了给一对坐标,也可以给一个矩形框 layoutRectangle:文字以框宽为准自动折行,不用自己算每一行在哪里断开。再配 PdfStringFormat 的 alignment 与 lineAlignment,可以更灵活的控制文本的对齐效果。

function App() {
  const drawTextInRectangle = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

    // 检查模块是否就绪
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // 将宋体载入 VFS,供页面文字使用
    await window.spire.FetchFileToVFS('SIMSUN.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // 创建 PDF 文档并添加一个空白页面
    const doc = new pdfModule.PdfDocument();
    const page = doc.Pages.Add();

    const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 14 });
    const brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Black() }) });
    const borderPen = new pdfModule.PdfPen({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_LightGray() }), width: 1 });

    const text = '这是一段比较长的说明文字,交给矩形框之后会按框的宽度自动折行,不需要自己计算每一行到哪里断开。';

    // 左框:默认靠左折行,文字从框的左上角排起
    const leftBox = new pdfModule.RectangleF({ x: 40, y: 80, width: 200, height: 100 });
    page.Canvas.DrawRectangle({ pen: borderPen, rectangle: leftBox });
    page.Canvas.DrawString({ s: text, font: font, brush: brush, layoutRectangle: leftBox });

    // 右框:同样的文字,在框内水平居中并垂直居中
    const rightBox = new pdfModule.RectangleF({ x: 300, y: 80, width: 200, height: 100 });
    page.Canvas.DrawRectangle({ pen: borderPen, rectangle: rightBox });
    const center = new pdfModule.PdfStringFormat({
      alignment: pdfModule.PdfTextAlignment.Center,
      lineAlignment: pdfModule.PdfVerticalAlignment.Middle,
    });
    page.Canvas.DrawString({ s: text, font: font, brush: brush, layoutRectangle: rightBox, format: center });

    // 定义输出文件名并保存文档
    const outputFileName = '矩形框排版文本.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>绘制在矩形框内排版的文本</h1>
      <button onClick={drawTextInRectangle}>
        开始绘制
      </button>
    </div>
  );
}

export default App;

同一段文字在 200 磅宽的框内自动折行,右框另加了水平与垂直居中:

同一段文字在 200 磅宽的框内自动折行,右框另加了水平与垂直居中


绘制旋转与变形的文本

文字的旋转与变形不靠字体参数,而是先把画布转过去,画布变换常用的四个方法:

接口 作用 参数与单位
TranslateTransform(dx, dy) 把画布原点平移到目标位置 位移量,单位磅
RotateTransform({ angle }) 绕画布原点旋转 角度;正值在本画布上为顺时针
SkewTransform(angleX, angleY) 让坐标轴倾斜,文字沿斜线排布 倾斜角度;(-20, 0) 时整行右端抬高
ScaleTransform(scaleX, scaleY) 按倍数缩放画布 两个方向的缩放倍数;(1, 0.6) 纵向压到 0.6 倍

四种变换作用在同一段样例文字上的效果(灰为变换前、红为变换后,圆点为落点):

平移、旋转、切变与缩放四种画布变换作用在同一段样例文字上的效果

function App() {
  const drawTransformedText = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

    // 检查模块是否就绪
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // 将宋体载入 VFS,供页面文字使用
    await window.spire.FetchFileToVFS('SIMSUN.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // 创建 PDF 文档并添加一个空白页面
    const doc = new pdfModule.PdfDocument();
    const page = doc.Pages.Add();

    const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 16 });
    const brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_SteelBlue() }) });

    // 平移:只把原点移到落点,文字保持水平地整体挪过去
    let state = page.Canvas.Save();
    page.Canvas.TranslateTransform(60, 110);
    page.Canvas.DrawString({ s: '平移后的文本', font: font, brush: brush, x: 0, y: 0 });
    page.Canvas.Restore({ state: state });

    // 旋转:把原点挪到落点,再转过 30°
    state = page.Canvas.Save();
    page.Canvas.TranslateTransform(120, 210);
    page.Canvas.RotateTransform({ angle: 30 });
    page.Canvas.DrawString({ s: '旋转 30° 的文本', font: font, brush: brush, x: 0, y: 0 });
    page.Canvas.Restore({ state: state });

    // 倾斜:横向切变 -20°,整行右端抬高
    state = page.Canvas.Save();
    page.Canvas.TranslateTransform(60, 430);
    page.Canvas.SkewTransform(-20, 0);
    page.Canvas.DrawString({ s: '横向倾斜的文本', font: font, brush: brush, x: 0, y: 0 });
    page.Canvas.Restore({ state: state });

    // 变形:纵向缩到 0.6 倍,字被压扁
    state = page.Canvas.Save();
    page.Canvas.TranslateTransform(60, 560);
    page.Canvas.ScaleTransform(1, 0.6);
    page.Canvas.DrawString({ s: '纵向压缩的文本', font: font, brush: brush, x: 0, y: 0 });
    page.Canvas.Restore({ state: state });

    // 定义输出文件名并保存文档
    const outputFileName = '旋转变形文本.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>绘制旋转与变形的文本</h1>
      <button onClick={drawTransformedText}>
        开始变换
      </button>
    </div>
  );
}

export default App;

平移、旋转、横向切变与纵向压缩四种画布变换绘制出的文字:

平移、旋转、横向切变与纵向压缩四种画布变换绘制出的文字


绘制半透明文本

SetTransparency 设在画布上:alphaBrush 与 alphaPen 分别控制填充和描边的透明程度,取值是 0 到 1 之间的小数(0 全透明、1 不透明),blendMode 决定文字与下层内容如何叠加。它从设置的那一刻起对所有绘制生效,所以要用 Save 与 Restore 框在需要的范围内,否则后面的内容会一起变淡。

function App() {
  const drawTransparentText = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

    // 检查模块是否就绪
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // 将宋体载入 VFS,供页面文字使用
    await window.spire.FetchFileToVFS('SIMSUN.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // 创建 PDF 文档并添加一个空白页面
    const doc = new pdfModule.PdfDocument();
    const page = doc.Pages.Add();

    const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 20 });
    const brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_SeaGreen() }) });
    const text = 'Spire.PDF for JavaScript 绘制半透明文本';

    // 第一行:不透明,作为对照
    page.Canvas.DrawString({ s: text, font: font, brush: brush, x: 40, y: 90 });

    // 打开透明度设置:填充与描边的 alpha 都设为 0.3
    const state = page.Canvas.Save();
    page.Canvas.SetTransparency({ alphaPen: 0.3, alphaBrush: 0.3, blendMode: pdfModule.PdfBlendMode.Normal });
    page.Canvas.DrawString({ s: text, font: font, brush: brush, x: 40, y: 140 });

    // 恢复画布状态,这段之外的绘制回到不透明
    page.Canvas.Restore({ state: state });
    page.Canvas.DrawString({ s: text, font: font, brush: brush, x: 40, y: 190 });

    // 定义输出文件名并保存文档
    const outputFileName = '半透明文本.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>绘制半透明文本</h1>
      <button onClick={drawTransparentText}>
        开始绘制
      </button>
    </div>
  );
}

export default App;

同一行文字的三次绘制:不透明、alpha 0.3,以及 Restore 之后恢复不透明:

同一行文字的三次绘制:不透明、alpha 0.3,以及 Restore 之后恢复不透明


常见问题

文字画到了页面外面,或者上下位置和预期相反

原因:画布坐标的原点在页面左上角(再内缩页边距),y 轴向下,单位是磅;落点是这行文字的左上角,不是文字的基线。按左下角为原点、y 轴向上的习惯换算,位置就会跑到相反的一侧,甚至落到页面可绘制区域之外——示例页面是 A4,上下页边距各 40 磅,可用高度只有 762 磅,落点 y 超过约 740 磅时,这行字就会被切掉。

解决:按左上角为原点、y 向下增大来摆放,需要从页面底部往上算时,取可用高度做减法:

// 页面可绘制区域的高度(A4 + 40 磅页边距时为 762 磅)
const height = page.Canvas.ClientSize.Height;

// 距离可绘制区域底部 100 磅处落笔
page.Canvas.DrawString({ s: '靠近页面底部的文字', font: font, brush: brush, x: 40, y: height - 100 });

渐变文字只显示一种颜色,或者颜色过渡不完整

原因:PdfLinearGradientBrush 的 rect 圈定的是渐变的起止范围,用的是画布上的绝对坐标,与文字落点相互独立。矩形没盖住整行文字时,文字只能落在渐变的一段上,看上去就像单色。实测把矩形放在 x 从 0 开始、文字落在 x 为 40 的位置,文字左端已经走过整段渐变的三分之一,红蓝过渡就不再完整。

解决:用 MeasureString 量出文字宽度,让矩形与文字等宽、起点与落点对齐,渐变就会完整扫过整行:

// 文字宽度作为渐变矩形的宽度
const textWidth = font.MeasureString({ text: text }).Width;

const gradient = new pdfModule.PdfLinearGradientBrush({
  rect: new pdfModule.RectangleF({ x: 40, y: 90, width: textWidth, height: 40 }),
  color1: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Red() }),
  color2: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Blue() }),
  mode: pdfModule.PdfLinearGradientMode.Horizontal,
});

// 落点与矩形左端对齐
page.Canvas.DrawString({ s: text, font: font, brush: gradient, x: 40, y: 110 });

获取免费许可证

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

Spire.XLS for Java 16.9.2 现已发布。该版本修复了读取单元格值与原文件中单元格格式不符的问题,优化了 Excel 转 PDF 时隐藏工作表的公式计算耗时,并修复了 Excel 转 PDF/A-1a、PDF/A-2a 和 PDF/A-3a 时验证失败的问题。详情如下。

问题修复:


获取 Spire.XLS for Java 16.9.2 请点击:

https://www.e-iceblue.cn/Downloads/Spire-XLS-JAVA.html

一份几十页的合同或报告交到手上,要确认某条条款、某个金额出现过几次、都出现在哪里,靠眼睛翻页容易漏。把命中的文字标出来是最省事的做法,但桌面软件里的查找高亮很难嵌进 Web 流程,逐页截图再标注也不现实。

Spire.PDF for JavaScript 基于 WebAssembly 在浏览器端加载、处理与保存 PDF 文档,查找与高亮全部在本地完成,通过虚拟文件系统(VFS)读写文件,无需后端配合。本文用 PdfTextFinder 来实现三种查找高亮方式:全篇高亮、限定区域内高亮、按正则表达式高亮。

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

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


查找并高亮全部匹配文本

PdfTextFinder 用于在页面的文本层里定位指定文本。它按页工作,遍历文档的每一页各建一个 finder,就能把整份文档里的匹配项一次找全;命中的每一处调用 HighLight() 即完成高亮,默认是黄色,需要区分不同关键词时再传入颜色。

function App() {
  const findAndHighlightAll = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

    // 检查模块是否就绪
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // 将待处理的 PDF 文件载入 VFS
    const inputFileName = '花卉.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // 创建 PdfDocument 对象并加载 PDF 文档
    let doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    // 逐页查找,命中的每一处都加上高亮
    for (let i = 0; i < doc.Pages.Count; i++) {
      const finder = new pdfModule.PdfTextFinder(doc.Pages.get_Item(i));
      finder.Options.Parameter = pdfModule.TextFindParameter.IgnoreCase;
      const finds = finder.Find('观赏');
      for (let j = 0; j < finds.length; j++) {
        finds.get(j).HighLight();
      }
    }

    // 保存文档
    const outputFileName = '查找高亮.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>查找并高亮全部匹配文本</h1>
      <button onClick={findAndHighlightAll}>
        开始查找并高亮
      </button>
    </div>
  );
}

export default App;

整份文档里的 观赏 全部被高亮:

整份文档里的观赏全部被高亮


在指定区域内查找并高亮

页面上的同名文字往往只有一部分需要标注,PdfTextFinder 还提供 Options.Area,把查找范围收进一个矩形,落在框外的匹配不会返回,也就不会被高亮。矩形用页面坐标描述,原点在页面左上角、单位是磅。

function App() {
  const findAndHighlightInArea = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

    // 检查模块是否就绪
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // 将待处理的 PDF 文件载入 VFS
    const inputFileName = '花卉.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // 创建 PdfDocument 对象并加载 PDF 文档
    let doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    // 指定查找范围:页面坐标,原点在左上角,单位磅
    const area = new pdfModule.RectangleF({ x: 60, y: 488, width: 420, height: 160 });

    const finder = new pdfModule.PdfTextFinder(doc.Pages.get_Item(0));
    finder.Options.Parameter = pdfModule.TextFindParameter.IgnoreCase;
    finder.Options.Area = area;

    // 只有落在矩形内的匹配会被返回
    const finds = finder.Find('观赏');
    for (let j = 0; j < finds.length; j++) {
      finds.get(j).HighLight();
    }

    // 保存文档
    const outputFileName = '区域高亮.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>在指定区域内查找并高亮</h1>
      <button onClick={findAndHighlightInArea}>
        开始查找并高亮
      </button>
    </div>
  );
}

export default App;

只有对照表区域内的 观赏 被高亮,正文与列表里的保持不变:

只有对照表区域内的观赏被高亮


用正则表达式查找并高亮

要找的目标未必是固定的几个字。Options.Parameter 决定匹配规则,取 Regex 时 Find() 的入参就是一个正则表达式,形态相同而内容各异的目标可以用一条模式一次圈出,默认取值按子串匹配,也就是前面两节的效果,同一枚举里还有 IgnoreCase、WholeWord 可用。

function App() {
  const findAndHighlightByRegex = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

    // 检查模块是否就绪
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // 将待处理的 PDF 文件载入 VFS
    const inputFileName = '花卉.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // 创建 PdfDocument 对象并加载 PDF 文档
    let doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    // 逐页按正则表达式匹配,命中的每一处都加上高亮
    for (let i = 0; i < doc.Pages.Count; i++) {
      const finder = new pdfModule.PdfTextFinder(doc.Pages.get_Item(i));
      finder.Options.Parameter = pdfModule.TextFindParameter.Regex;
      const finds = finder.Find('图\\s*\\d');
      for (let j = 0; j < finds.length; j++) {
        finds.get(j).HighLight({ color: pdfModule.Color.get_Orange() });
      }
    }

    // 保存文档
    const outputFileName = '正则高亮.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>用正则表达式查找并高亮</h1>
      <button onClick={findAndHighlightByRegex}>
        开始查找并高亮
      </button>
    </div>
  );
}

export default App;

三处图注被模式 图\s*\d 命中并高亮为橙色:

三处图注被正则模式命中并高亮


常见问题

高亮生效了,但在阅读器的注释面板里找不到它

原因:HighLight() 走的是页面内容,不是 PDF 注释。高亮色块在保存时被写进页面的内容流,文件体积只增大约 2 KB 量级,产物里并不会多出注释对象——用 PyMuPDF 读回 page.annots() 得到的是空集。

解决:把高亮当作页面图形看待即可,显示效果与标注一致;只是它没有注释身份,不能像批注那样在阅读器里逐条选中、删除或改色。需要按注释管理高亮时,应在保存前记下命中的位置,由业务侧自行维护这份清单。

设置了查找区域,却一处都没高亮

原因:Options.Area 用的是页面坐标(原点在页面左上角、单位磅),矩形写小了、位置偏了,匹配就全部落在框外。它也只在当前页生效——多页文档要把同一个矩形套到目标页的 finder 上。

解决:先按整页坐标量出目标区域,再往里收。下面这份样例里的对照表落在 x≈60–480、y≈488–648 之间,用 RectangleF({ x: 60, y: 488, width: 420, height: 160 }) 正好框住表格,矩形外的正文与列表都不会被匹配:

// 只查当前页,且只在这个矩形内匹配
const finder = new pdfModule.PdfTextFinder(doc.Pages.get_Item(0));
finder.Options.Area = new pdfModule.RectangleF({ x: 60, y: 488, width: 420, height: 160 });

拿不准坐标时,可以先不设 Area 查一遍,从命中的 finds.get(i).Bounds[0] 读出实际位置再反过来定矩形。

同一个正则,中文文档能匹配,换日文文档就失效

原因:正则匹配的是 PDF 文本层里的实际字符,不是语义。三份样例里表示区间的连字符并不一致:中文与英文用短横线 –(U+2013),日文用全角波浪线 ~(U+FF5E),只写一种模式就只能在一种文档上命中。

解决:把连字符写成字符组,一次兼容两种写法:

// 数字区间:6–9 月 / 1–3 m / 6~9月 都能匹配
finder.Options.Parameter = pdfModule.TextFindParameter.Regex;
const finds = finder.Find('[0-9]+\\s*[–-~]\\s*[0-9]+');

获取免费许可证

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