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

Spire.Cloud 纯前端文档控件

Spire.Presentation for Java 11.9.10 现已正式发布。该版本通过修复内容丢失的问题,优化了 PowerPoint 到 PDF 的转换功能。更多详情如下。

问题修复:


获取Spire.Presentation for Java 11.9.10 请点击:

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

Spire.Doc for C++ 14.9.11 现已发布。该版本新增支持 Windows、Linux 和 macOS 的 ARM64 平台,并新增支持 HarmonyOS(鸿蒙操作系统)的 ARM64 和 x86_64 架构,进一步扩展了 Spire.Doc for C++ 的跨平台支持能力。详情如下。

新增平台支持:


获取 Spire.Doc for C++ 14.9.11 请点击:

https://www.e-iceblue.cn/Downloads/Spire-Doc-CPP.html

PDF 的版本号决定了文档能用哪些特性,也决定了旧阅读器、打印系统和归档平台能不能正常打开它。一批文档从不同来源汇过来,版本参差不齐:有的出自新工具、声明到 1.7,有的产自多年未更新的系统、还停在 1.4。要按目标环境统一交付格式,就得把版本号改写过来,而桌面软件只能一份一份手工另存。

本文用 Spire.PDF for JavaScript 更改 PDF 文档的版本号。它基于 WebAssembly 在浏览器端直接加载、修改与保存 PDF 文档,全过程在本地完成,通过虚拟文件系统(VFS)读写文件,无需后端配合。

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


更改 PDF 版本

给 doc.FileInfo.Version 赋一个 PdfVersion 值即可改写版本号,保存后文件头随即写成对应的 %PDF-x.y。把它设成 Version1_4,一份 1.7 的文档就降到 1.4,可以交给只认旧规范的阅读器或打印系统。

function App() {
  const changePdfVersion = 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 文档
    const doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    // 把版本号改成目标值,这里降到 1.4
    doc.FileInfo.Version = pdfModule.PdfVersion.Version1_4;

    const outputFileName = '版本1.4.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>更改 PDF 版本</h1>
      <button onClick={changePdfVersion}>
        开始更改
      </button>
    </div>
  );
}

export default App;

版本号改为 1.4 后保存的文档,文件头已写成 %PDF-1.4:

版本号改为 1.4 后保存的文档,文件头已写成 %PDF-1.4


常见问题

设了版本号,重新打开仍是旧版本

原因:FileInfo.Version 改的是内存中文档对象的属性,只有保存才会写进文件。设完版本号直接关闭,或者仍去打开原来那份输入文件,看到的自然还是旧版本号。

解决:赋完值后必须调用 SaveToFile 另存为新的输出文件,再打开该产物查看:

// 先改版本号
doc.FileInfo.Version = pdfModule.PdfVersion.Version1_4;

// 再保存,版本号才会写入文件
doc.SaveToFile('版本1.4.pdf');

版本号该设成哪一个

原因:取决于交付对象的支持范围。旧打印系统与归档平台常要求 1.4 或更低,新工具大多能吃下 1.7;FileInfo.Version 取 PdfVersion 枚举,Version1_0 到 Version1_7 依次对应 PDF 1.0 到 1.7。

解决:按接收方能接受的最高版本设置,赋值时直接用枚举:

// 交付给只认旧规范的阅读器或打印系统
doc.FileInfo.Version = pdfModule.PdfVersion.Version1_4;

// 目标环境较新,保持或提到 1.7
doc.FileInfo.Version = pdfModule.PdfVersion.Version1_7;

把版本号降到低版本后,文档打不开或内容异常

原因:FileInfo.Version 只改声明,并不清理文档里超出该版本规范的对象。原文用了新版本才引入的特性——比如 PDF 1.5 起的交叉引用流、对象流或压缩透明——旧阅读器按低版本去解析就会失败。

解决:改声明之前先确认源文件没有依赖这些特性。源文件干净时,改版本号即可;否则应换用不依赖新特性的源文件,或先用能重建文档的方式(重新导出、打印为 PDF)把对象压回旧规范,再改版本号。


获取免费许可证

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

我们很高兴地宣布 Spire.Doc for Python 14.9.1 版本正式发布。本次更新修复了 Word 转 PDF 转换以及 Word 文档页面误删相关问题,详情如下:

问题修复:


获取 Spire.Doc for Python 14.9.1 请点击:

https://www.e-iceblue.cn/Downloads/Spire-Doc-Python.html

合同、标书这类多页文件外发前要盖骑缝章,为的是让每一页都带上印章残片——对方一旦抽换或重排过页,残片就对不上。纸质件可以整叠一盖,电子 PDF 没有对应的动作:把印章图片整枚贴上去,每页都是完整的章,起不到骑缝的作用。

本文介绍用 Spire.PDF for JavaScript 为多页合同添加骑缝章。它基于 WebAssembly 在浏览器端直接加载、修改与保存 PDF 文档,全过程在本地完成,通过虚拟文件系统(VFS)读写文件,无需后端配合。

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


添加骑缝章

骑缝章把一枚印章按页数切开,每页只落一片,翻页时各页残片能拼回完整印章。切片用 PdfTemplate 承载:模板的界框就是裁切范围,只要把印章在模板里逐页左移一个片宽,每页露出的就正好是自己那一片,再由 Canvas.DrawTemplate 落到页面右缘。

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

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

    // 将待盖章的合同与印章图片载入 VFS
    const inputFileName = 'Number.pdf';
    const sealFileName = 'Seal.png';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/data/`);
    await window.spire.FetchFileToVFS(sealFileName, '', `${process.env.PUBLIC_URL}/data/`);

    // 创建 PdfDocument 对象并加载合同
    const doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    // 载入印章图片;它的宽高是像素值,排版时按 1 像素 = 1 磅参与计算
    const seal = pdfModule.PdfImage.FromFile(sealFileName);

    // 印章整体缩放比例,以及缩放后的章宽、章高
    const scale = 0.55;
    const sealWidth = seal.Width * scale;
    const sealHeight = seal.Height * scale;

    // 按页数把印章等分,每页承载一片
    const pageCount = doc.Pages.Count;
    const stripWidth = sealWidth / pageCount;

    // 逐页把印章的第 i 片贴到页面右缘
    for (let i = 0; i < pageCount; i++) {
      const page = doc.Pages.get_Item(i);

      // 模板界框就是裁切范围:片宽 × 章高,刚好装下一片
      const template = new pdfModule.PdfTemplate({ width: stripWidth, height: sealHeight });
      template.Graphics.ScaleTransform(scale, scale);

      // 每往后一页就把印章多左移一个片宽,于是模板里露出的是下一片
      template.Graphics.DrawImage({ image: seal, x: -(i * seal.Width) / pageCount, y: 0 });

      // 每一页都落在同一位置:页面右缘向内留 24 磅,垂直居中
      const x = page.Canvas.ClientSize.Width - 24 - stripWidth;
      const y = (page.Canvas.ClientSize.Height - sealHeight) / 2;
      page.Canvas.DrawTemplate({ template: template, location: new pdfModule.PointF(x, y) });
    }

    // 保存到 VFS
    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={addCrossPageSeal}>
        开始盖章
      </button>
    </div>
  );
}

export default App;

加盖骑缝章后的合同,每页右缘各有一片印章残片,翻到最后一页即为印章的右半部分:

加盖骑缝章后的合同,每页右缘各有一片印章残片


常见问题

为什么每页只显示印章的一小段

原因:骑缝章本身就是按页等分切开的,每页只承载 印章宽度 / 页数 那一片。页数越多,每页上的残片越窄,这是设计使然——残片窄、位置固定,才能保证抽换单页后拼不回去。

解决:页数由文档决定,想让它更醒目,只能把整枚印章放大,残片会跟着变宽:

// 把 0.55 调大,例如 0.8,整章与每页残片一起变大
const scale = 0.8;
const sealWidth = seal.Width * scale;

印章盖得太大,把正文压住了

原因:PdfImage.Width 与 Height 给出的是像素值,不缩放就按 1 像素 = 1 磅落版。一张 400×400 像素的印章会占掉 400 磅宽,而 A4 页面只有 595 磅宽,自然会盖住正文。

解决:在模板里用 ScaleTransform 等比缩小,同时按同一个比例换算章宽、章高与片宽,三者必须同源,否则切片会与落点对不齐:

const scale = 0.55;
const sealWidth = seal.Width * scale;
const sealHeight = seal.Height * scale;
const stripWidth = sealWidth / pageCount;

获取免费许可证

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

由报表系统导出的 Excel 文件往往只带一个系统名作为作者,标题、主题、关键词等摘要条目一律留空;企业内部又常需要往工作簿里附加导出批次、责任人、审批状态等字段,供归档与检索使用。这些信息都不占用任何单元格,全部保存在工作簿的文档属性中,用 Excel 打开时可在「文件 > 信息 > 属性」里查看。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端写入这些属性,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。

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

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


设置工作簿的摘要属性

摘要属性是 Excel 文件在资源管理器和「文件 > 信息」中展示的一层描述,也是归档检索时最先被看到的信息。报表由程序生成时通常只有系统名作为作者,其余条目留空,补全这些条目可以让文件在流转过程中具备可读的上下文。文本条目直接赋值即可,时间条目则要传入日期对象。具体操作步骤如下:

  1. 加载工作簿,通过 workbook.DocumentProperties 取得摘要属性集合。
  2. 对 Title、Subject、Author、Keywords、Comments、Category 等文本条目直接赋值。
  3. 对 Company、Manager 这类由 Excel 扩展属性承载的条目同样直接赋值。
  4. 将 CreatedTime 与 LastSaveTime 设为 Date 对象。
  5. 保存工作簿。

下面是一个完整的代码示例,展示了在 React 中设置工作簿摘要属性:

function App() {
  const setSummaryProperties = 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 文件载入 VFS
    const inputFileName = 'WorkbookProperties.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // 加载工作簿
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile(inputFileName);

    // 设置摘要属性中的文本条目
    const summary = workbook.DocumentProperties;
    summary.Title = '2026 年第三季度销售报表';
    summary.Subject = '各销售部季度业绩汇总';
    summary.Author = 'E-iceblue';
    summary.Keywords = '销售, 报表, Excel';
    summary.Comments = '由报表系统导出后补充的摘要信息';
    summary.Category = '销售报表';

    // 设置由扩展属性承载的条目
    summary.Company = 'E-iceblue';
    summary.Manager = 'Sales Manager';

    // 设置文档时间,此处必须传入 Date 对象
    summary.CreatedTime = new Date(2026, 8, 1);
    summary.LastSaveTime = new Date(2026, 8, 20);

    // 保存工作簿
    const outputFileName = 'SetSummaryProperties.xlsx';
    workbook.SaveToFile(outputFileName);

    // 释放资源
    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={setSummaryProperties}>Start</button>
    </div>
  );
}

export default App;

运行后,设置工作簿摘要属性的效果:

设置工作簿摘要属性


添加自定义属性

自定义属性用于承载摘要属性容纳不下的业务字段,例如导出批次、责任人电话、版本号或审批日期。与摘要属性不同,自定义属性没有固定条目,名称与类型都由调用方决定,Excel 支持文本、整数、小数、布尔和日期时间五种取值。具体操作步骤如下:

  1. 加载工作簿,通过 workbook.CustomDocumentProperties 取得自定义属性集合。
  2. 调用 Add 添加属性,名称与值可以写成 { strName, boolValue } 这样的具名对象,也可以直接传入名称与值两个参数。
  3. 按值的类型选择对应的成员:整数用 intValue,小数用 dblValue。
  4. 日期时间值用 dtValue 传入 Date 对象。
  5. 保存工作簿。

下面是一个完整的代码示例,展示了在 React 中为工作簿添加自定义属性:

function App() {
  const addCustomProperties = 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 文件载入 VFS
    const inputFileName = 'WorkbookProperties.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // 加载工作簿
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile(inputFileName);

    // 添加布尔值属性,_MarkAsFinal 表示文档已定稿
    workbook.CustomDocumentProperties.Add({ strName: '_MarkAsFinal', boolValue: true });

    // 添加文本属性,也可直接传入名称与值两个参数
    workbook.CustomDocumentProperties.Add('The Editor', 'E-iceblue');

    // 添加整数属性
    workbook.CustomDocumentProperties.Add({ strName: 'Phone number', intValue: 81705109 });

    // 添加小数属性
    workbook.CustomDocumentProperties.Add({ strName: 'Revision number', dblValue: 7.12 });

    // 添加日期时间属性
    workbook.CustomDocumentProperties.Add({ strName: 'Revision date', dtValue: new Date(2026, 8, 1) });

    // 保存工作簿
    const outputFileName = 'AddCustomProperties.xlsx';
    workbook.SaveToFile(outputFileName);

    // 释放资源
    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={addCustomProperties}>Start</button>
    </div>
  );
}

export default App;

运行后,添加自定义属性的效果:

添加自定义属性


修改自定义属性的值

业务字段的值会随统计口径变化,例如导出记录数在补充数据后需要按新的口径重写。自定义属性没有提供直接改值的入口,但 Add 以名称为键,对同名属性再次调用时并不会新增重复条目,而是把原有条目的值替换掉。具体操作步骤如下:

  1. 加载工作簿,通过 workbook.CustomDocumentProperties 取得自定义属性集合。
  2. 再次调用 Add,传入与已有条目相同的名称和新的值。
  3. 保存工作簿。

下面是一个完整的代码示例,展示了在 React 中修改工作簿的自定义属性:

function App() {
  const updateCustomProperties = 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 文件载入 VFS
    const inputFileName = 'WorkbookProperties.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // 加载工作簿
    const workbook = new xlsModule.Workbook();
    workbook.LoadFromFile(inputFileName);

    // 按新的统计口径重写导出记录数,同名属性会被覆盖而不会重复添加
    workbook.CustomDocumentProperties.Add({ strName: 'ExportedRecords', intValue: 256 });

    // 保存工作簿
    const outputFileName = 'UpdateCustomProperties.xlsx';
    workbook.SaveToFile(outputFileName);

    // 释放资源
    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={updateCustomProperties}>Start</button>
    </div>
  );
}

export default App;

运行后,修改自定义属性的效果:

修改自定义属性的值


常见问题

为什么给 CreatedTime 赋值会报 Assert failed: Value is not a Date

原因:CreatedTime 与 LastSaveTime 只接受 JavaScript 的 Date 对象。写成日期字符串(如 '2026-09-01')、时间戳数字,或者自己拼一个只带 toISOString() 方法的普通对象,都会在这一步被类型校验挡下来,抛出 Assert failed: Value is not a Date。

解决:先用 new Date(...) 构造出 Date 对象,再赋值给它:

// 2026 年 9 月 1 日;月份从 0 开始计数,8 表示 9 月
workbook.DocumentProperties.CreatedTime = new Date(2026, 8, 1);

为什么给自定义属性的 Value 赋值会报 ArgumentNull_Generic

原因:Value 属性只提供读取,直接赋值会抛出 ArgumentNull_Generic Arg_ParamName_Name, value,原值不会改变。要更新一条已有属性,需要走 Add。

解决:用同名 Add 覆盖原值:

workbook.CustomDocumentProperties.Add({ strName: 'ExportedRecords', intValue: 256 });

获取免费许可证

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

表格里真正的分隔线不是空白,而是边框:财务报表用粗细不同的框线区分表头与合计行,导出给下游系统的数据又需要把框线一并去掉,只留干净的数据区。这些操作靠手工点选「设置单元格格式」逐个完成,工作量大且难以复用。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端直接完成此操作,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。

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

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


为选定的单元格或单元格区域添加边框

拿到一份没有框线的数据表时,最直接的做法是靠 BorderAround 与 BorderInside 两个方法把框线补上:前者负责外框,后者负责区域内部的网格线。用它们既能给整片数据区域一次框好,也能把某个单元格单独框出来,例如把表头所在的单元格加粗加重,让它从一整片同样的框线里跳出来。

具体操作步骤如下:

  1. 用 Range.get 按地址取到需要加边框的单元格区域
  2. 调用 BorderAround 为区域加上外框,线型取值来自 LineStyleType 枚举
  3. 调用 BorderInside 为区域内部补上单元格之间的分隔线
  4. 单独取到表头单元格,再对它调用一次 BorderAround,并改用中等粗细的线型

为区域与单元格设置边框时,线型通过对象写法 { borderLine } 传入。

function App() {
  const addBorderToCells = 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 = 'CellBorders.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);

    // 取 B2:E6 区域,先加细线外框,再加内部细线
    const dataRange = sheet.Range.get('B2:E6');
    dataRange.BorderAround({ borderLine: xlsModule.LineStyleType.Thin });
    dataRange.BorderInside({ borderLine: xlsModule.LineStyleType.Thin });

    // 单独给表头所在的 B2 单元格加中等粗细的四周框线
    sheet.Range.get('B2').BorderAround({ borderLine: xlsModule.LineStyleType.Medium });

    // 保存结果文件
    const outputFileName = 'AddBorderToCells.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={addBorderToCells}>为单元格或区域添加边框</button>
    </div>
  );
}

export default App;

运行后,数据区域与表头单元格加上边框的效果:

为选定的单元格或单元格区域添加边框


为包含数据的单元格区域添加边框

上一节按地址写死了 B2:E6,一旦表格增删行列就要跟着改地址。AllocatedRange 属性直接返回工作表中已分配的范围,也就是真正存放数据的矩形区域,用它取范围可以省去手工维护地址的麻烦,并且在表头之外再套一层样式不同的外框,让表格和正文区分得更清楚。

具体操作步骤如下:

  1. 通过 AllocatedRange 取到工作表中包含数据的单元格区域,无需手动指定地址
  2. 调用 BorderAround 给外圈换上中等粗细的虚线,与外框实线形成对比
  3. 调用 BorderInside 为区域内部补上细线,让每一行数据之间保持分隔
function App() {
  const addBorderToDataRange = async () => {
    const xlsModule = window.wasmModule?.spirexls;
    if (!xlsModule) {
      alert('Spire.Xls is not ready yet');
      return;
    }

    await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'CellBorders.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);

    // 用 AllocatedRange 直接取到含数据的单元格区域,不必手动指定地址
    const dataRange = sheet.AllocatedRange;

    // 外圈加中等粗细的虚线,内部加细线
    dataRange.BorderAround({ borderLine: xlsModule.LineStyleType.MediumDashed });
    dataRange.BorderInside({ borderLine: xlsModule.LineStyleType.Thin });

    const outputFileName = 'AddBorderToDataRange.xlsx';
    workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });
    workbook.Dispose();

    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={addBorderToDataRange}>为数据区域添加边框</button>
    </div>
  );
}

export default App;

运行后,数据区域加上虚线外框与内部细线的效果:

为包含数据的单元格区域添加边框


为单元格添加左侧、顶部、右侧、底部和对角线边框

BorderAround 一次给四条边设成同样的样式,遇到「左边加粗标红、下边用双线」这类需求就不够用了。这时改成逐条边处理:通过 Borders.get 按 BordersLineType 枚举取出其中的一条边,再分别设置它的 LineStyle 与 Color,每条边的线型和颜色都可以不同,对角线的两个方向同样能单独设置。

具体操作步骤如下:

  1. 用 Range.get 取到需要设置的单元格
  2. 用 Borders.get(BordersLineType.EdgeLeft) 等取出左侧、顶部、右侧、底部四条边
  3. 对每条边设置 LineStyle,分别取粗线、点线、斜划线点、双线四种线型
  4. 对每条边设置 Color,分别取红、棕、深灰、橙红四种颜色
  5. 另取一个单元格,用 BordersLineType.DiagonalDown 取出对角线下边框并设置线型
function App() {
  const addEdgeBorders = async () => {
    const xlsModule = window.wasmModule?.spirexls;
    if (!xlsModule) {
      alert('Spire.Xls is not ready yet');
      return;
    }

    await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'CellBorders.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);

    // 为 B4 单元格的四条边分别设置线型与颜色
    const cell = sheet.Range.get('B4');
    const edgeSpecs = [
      ['EdgeLeft', xlsModule.LineStyleType.Thick, xlsModule.Color.get_Red()],
      ['EdgeTop', xlsModule.LineStyleType.Dotted, xlsModule.Color.get_Brown()],
      ['EdgeRight', xlsModule.LineStyleType.SlantedDashDot, xlsModule.Color.get_DarkGray()],
      ['EdgeBottom', xlsModule.LineStyleType.Double, xlsModule.Color.get_OrangeRed()],
    ];
    for (const [edge, lineStyle, color] of edgeSpecs) {
      const border = cell.Borders.get(xlsModule.BordersLineType[edge]);
      border.LineStyle = lineStyle;
      border.Color = color;
    }

    // 为 E6 单元格添加对角线下边框
    const diagonal = sheet.Range.get('E6').Borders.get(xlsModule.BordersLineType.DiagonalDown);
    diagonal.LineStyle = xlsModule.LineStyleType.Thin;

    const outputFileName = 'AddEdgeBorders.xlsx';
    workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });
    workbook.Dispose();

    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={addEdgeBorders}>设置四条边与对角线边框</button>
    </div>
  );
}

export default App;

运行后,单元格四条边分别使用不同线型与颜色、并对角线加线的效果:

为单元格添加左侧、顶部、右侧、底部和对角线边框


删除单元格或单元格区域的边框

把内部报表导出给外部使用、或者把数据喂给下游系统时,框线往往属于多余信息:它会让数据看起来像一张已经定稿的报表,下游按区域解析时也可能把框线当成有效内容。删除的做法与设置一样简单,把 Borders.LineStyle 批量设为 LineStyleType.None 即可一次清空区域内的全部边框。

具体操作步骤如下:

  1. 取到保存着带框线报表的那张工作表
  2. 用 Range.get 取到需要清理的单元格区域
  3. 把区域的 Borders.LineStyle 设为 LineStyleType.None,清空其中的全部边框
function App() {
  const removeBorders = async () => {
    const xlsModule = window.wasmModule?.spirexls;
    if (!xlsModule) {
      alert('Spire.Xls is not ready yet');
      return;
    }

    await window.spire.FetchFileToVFS('simsun.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'CellBorders.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(1);

    // 把 B2:E6 区域内所有单元格的边框样式设为 None
    sheet.Range.get('B2:E6').Borders.LineStyle = xlsModule.LineStyleType.None;

    const outputFileName = 'RemoveBorders.xlsx';
    workbook.SaveToFile({ fileName: outputFileName, version: xlsModule.ExcelVersion.Version2010 });
    workbook.Dispose();

    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={removeBorders}>删除单元格边框</button>
    </div>
  );
}

export default App;

运行后,区域内的边框被清空的效果:

删除单元格或单元格区域的边框


常见问题

给整片区域设置上边框,每一行上方都多出一条线

原因:Borders.get(BordersLineType.EdgeTop) 取到的是「区域内每个单元格的上边」,而不是整个区域的外沿。把它直接用在 B2:E6 上,区域内的每一行都会各自得到一条上边线,表格内部因此凭空多出四条线。

解决:把区域收窄到一行或一列,取到的那条边才会落在表格外沿。上边取首行、下边取末行、左边取首列、右边取末列:

// 上边:只取首行
sheet.Range.get('B2:E2').Borders.get(xlsModule.BordersLineType.EdgeTop).LineStyle = xlsModule.LineStyleType.Thin;
// 下边:只取末行
sheet.Range.get('B6:E6').Borders.get(xlsModule.BordersLineType.EdgeBottom).LineStyle = xlsModule.LineStyleType.Thin;
// 左边:只取首列
sheet.Range.get('B2:B6').Borders.get(xlsModule.BordersLineType.EdgeLeft).LineStyle = xlsModule.LineStyleType.Thin;
// 右边:只取末列
sheet.Range.get('E2:E6').Borders.get(xlsModule.BordersLineType.EdgeRight).LineStyle = xlsModule.LineStyleType.Thin;

给单个单元格调用 BorderInside 报错

原因:BorderInside 表示「区域内部的分隔线」,至少要两个单元格才谈得上内部,因此传入单个单元格时会抛出 This method doesn't support for single cell.,结果文件同样不会生成。

解决:单个单元格改用 BorderAround 框住四周,或者按上一节的办法逐条边设置:

sheet.Range.get('B2').BorderAround({ borderLine: xlsModule.LineStyleType.Thin });

获取免费许可证

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

产品改名、术语调整、模板里的公司名写错,这类改动落到 PDF 上就很别扭:手上没有源文档时,只能把整页导成图片再压字,或者在阅读器里删掉原文字、手动补一个新的。合同、说明书这类成文文档尤其如此,同一个名字可能分散在正文、项目符号列表和文末说明里,逐处手改既慢又容易漏。

Spire.PDF for JavaScript 基于 WebAssembly 在浏览器端加载、修改与保存 PDF 文档,改名只需要定位文本再写回,文档通过虚拟文件系统(VFS)读写,不经过后端。本文的四节分别处理替换第一处、替换全部(附带换字色、匹配方式与替换范围三个开关)、用背景色覆盖原词,以及画矩形盖住原文再重绘新词;样例里新旧名字等长,替换后排版不变。

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

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


替换第一处匹配

Spire.PDF for JavaScript 提供 PdfTextReplacer 类,用于把页面上的文本换成另一段文本。它按页构造,ReplaceText 只替换找到的第一处,返回值是实际替换掉的处数(这里为 1),页面上剩下的同名文本保持原样。

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

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

    // 字体载入 VFS
    await window.spire.FetchFileToVFS('MSYH.TTC', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // 将待处理的 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 page = doc.Pages.get_Item(0);

    // 创建文本替换器,只替换第一处匹配
    const replacer = new pdfModule.PdfTextReplacer(page);
    replacer.ReplaceText('星辰云盘', '星云网盘');

    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={replaceFirstMatch}>
        开始替换
      </button>
    </div>
  );
}

export default App;

只替换了第一处:

只替换了第一处的文档,导语已换成新名,列表与文末说明里仍是旧名


替换全部匹配

Spire.PDF for JavaScript 还提供 PdfTextReplacer.ReplaceAllText(),用于把整页里所有匹配的文本一次换掉。它的入参与 ReplaceText 相同,同样返回替换掉的处数。它还带一个三参重载 ReplaceAllText(旧, 新, 颜色),替换的同时可以把字色一并改掉。

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

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

    // 字体载入 VFS
    await window.spire.FetchFileToVFS('MSYH.TTC', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // 将待处理的 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 page = doc.Pages.get_Item(0);

    // 第三个参数传颜色,替换的同时把新词改成红色
    const replacer = new pdfModule.PdfTextReplacer(page);
    replacer.ReplaceAllText('星辰云盘', '星云网盘', pdfModule.Color.get_Red());

    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={replaceAllMatches}>
        开始替换
      </button>
    </div>
  );
}

export default App;

旧名被全部替换、新名同时改成红色:

4 处旧名被全部替换、新名同时改成红色后的文档

除了换字色,PdfTextReplacer.Options 上还有两个开关。ReplaceType 决定「什么才算匹配」:默认只认完全相同的字符串,可以切到 IgnoreCase(忽略大小写)、WholeWord(整词)或 Regex(正则)。SetReplacementArea 则把替换限制在一个矩形里,矩形可以先让 PdfTextFinder 找出来:

const replacer = new pdfModule.PdfTextReplacer(page);
// 忽略大小写
replacer.Options.ReplaceType = pdfModule.ReplaceActionType.IgnoreCase;

// 把替换限制在矩形内
const finds = new pdfModule.PdfTextFinder(page).Find('星辰云盘');
replacer.Options.SetReplacementArea(finds.get(0).Bounds[0]);

// 设置完再调用
const count = replacer.ReplaceAllText('星辰云盘', '星云网盘');

覆盖式替换

Spire.PDF for JavaScript 还提供 PdfTextFragment.ApplyRecoverString(),用于把找到的文本就地覆盖掉:先用一个背景色盖住原文字,再把新文本写在同一个位置。它省掉了自己算坐标、自己重绘的步骤,代价是只能指定背景色,新字的字体与前景色沿用原文字;背景色取白色时看不出接缝。需要连字体和字色一起换时,见下一节自己重绘的写法。

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

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

    // 把文档用到的字体载入 VFS
    await window.spire.FetchFileToVFS('MSYH.TTC', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // 将待处理的 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 page = doc.Pages.get_Item(0);

    // 先用查找器拿到目标文本的位置
    const finder = new pdfModule.PdfTextFinder(page);
    const finds = finder.Find('星辰云盘');

    // 逐处覆盖:第二个参数是背景色,第三个参数 true 表示按 Unicode 写入新文本
    for (let i = 0; i < finds.length; i++) {
      finds.get(i).ApplyRecoverString('星云网盘', pdfModule.Color.get_White(), 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={replaceByCovering}>
        开始替换
      </button>
    </div>
  );
}

export default App;

原文被白色背景盖住、新词写在原位置的文档:

原文被白色背景盖住、新词写在原位置的文档


绘制矩形覆盖并重绘新文本

前面三节动的都是文本层的内容。这一节换个思路:用 PdfTextFinder 拿到关键字位置,先画一个白底矩形把原文字盖住,再在矩形里写上新的文字。相比 ApplyRecoverString,多出来的自由度是可以自己指定字体、字号和字色。

function App() {
  const replaceByDrawing = 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 文件载入 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 page = doc.Pages.get_Item(0);

    // 先用查找器拿到目标文本的位置
    const finder = new pdfModule.PdfTextFinder(page);
    const finds = finder.Find('星辰云盘');

    // 覆盖用的白底矩形,以及新词的字体与字色
    const white = pdfModule.PdfBrushes.get_White();
    const FONT_SIZE = 9;
    const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: FONT_SIZE });
    const brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_DarkBlue() }) });

    // LineLimit置false,避免文本在框内被裁切
    const format = new pdfModule.PdfStringFormat();
    format.LineLimit = false;
    // 设置文本的对齐方式
    format.Alignment = pdfModule.PdfTextAlignment.Center;
    format.LineAlignment = pdfModule.PdfVerticalAlignment.Middle;

    for (let i = 0; i < finds.length; i++) {
      const rec = finds.get(i).Bounds[0];

      // 先用白底矩形把原文字盖住
      page.Canvas.DrawRectangle({ brush: white, rectangle: rec });

      // 新词直接写进命中框
      page.Canvas.DrawString({ s: '星云网盘', font: font, brush: brush, layoutRectangle: rec, format: format });
    }

    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={replaceByDrawing}>
        开始替换
      </button>
    </div>
  );
}

export default App;

白底矩形盖住原文、新词直接写在命中框内的文档:

白底矩形盖住原文、新词直接写在命中框内的文档


常见问题

替换后旧名字还能被搜索、复制到

原因:ApplyRecoverString 属于视觉层操作——它在原文字上盖一层背景色再把新词画上去,原来的文本对象仍然留在文本层,所以阅读器里搜索、复制拿到的还是旧名字。实测覆盖式替换后旧词出现次数不变,新词多出 4 处。

解决:需要内容层面真正替换时改用 PdfTextReplacer,它的产物里旧词从文本层消失、新词进入文本层,实测旧词 4 处降到 0、新词由 1 处增至 5 处:

// 内容层替换:旧文本不再残留
const replacer = new pdfModule.PdfTextReplacer(page);
replacer.ReplaceAllText('星辰云盘', '星云网盘');

照官方示例写「白色矩形 + DrawString 重绘」,白底出来了但新文字不显示

原因:官方示例的思路和本文第四节一致——用 PdfTextFinder 找到位置,先画一个白色矩形盖住原文字,再用 Canvas.DrawString 把新文本画上去。白底矩形正常,但重绘这一步有一个坑:

  • DrawString 的矩形重载受 PdfStringFormat.LineLimit 控制,而它默认为 true,会把文本裁到框内;命中框又紧贴字形,高度如果有 11 磅(正好等于字号),装不下一整行行高,于是整行被裁光——页面和文本层都不会有新字。

解决:传一个关掉 LineLimit 的 PdfStringFormat,命中框就能原样当排版框用(第四节即此写法)。

const format = new pdfModule.PdfStringFormat();
format.LineLimit = false;

page.Canvas.DrawString({ s: '星云网盘', font: font, brush: brush, layoutRectangle: rec, format: format });

获取免费许可证

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

不少打印设备和打印服务只认 PCL(Printer Command Language,打印机命令语言)。手上的合同、手册是 PDF 时,过去要么装驱动用桌面软件另存,要么先把文件传到服务端转换——前者嵌不进 Web 流程,后者意味着文档离开了用户的设备。

本文介绍用 Spire.PDF for JavaScript 实现将 PDF 转换为 PCL。它基于 WebAssembly 在浏览器端直接加载与保存 PDF 文档,转换全过程在本地完成,通过虚拟文件系统(VFS)读写文件,无需后端配合。

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


将 PDF 转换为 PCL

要交给只认 PCL 的打印流程,可以用 PdfDocument.SaveToFile 把整份文档写成 PCL 文件——第二个参数传 FileFormat.PCL,输出为 HP-PCL XL(PCL6)。

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

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

    // 转换需要 Arial Unicode MS 字体,先放进虚拟文件系统
    await window.spire.FetchFileToVFS('ARIAL UNICODE MS.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // 将待转换的 PDF 文件载入 VFS
    const inputFileName = '业务数据概览.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // 定义输出文件名
    const outputFileName = '业务数据概览.pcl';

    // 加载 PDF 文档,并指定 FileFormat.PCL 写出 PCL 文件
    let doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);
    doc.SaveToFile(outputFileName, pdfModule.FileFormat.PCL);
    doc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/octet-stream' });
    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>将 PDF 转换为 PCL</h1>
      <button onClick={convertPdfToPcl}>
        开始转换
      </button>
    </div>
  );
}

export default App;

转换生成的 PCL 文件,文件头写明 PCL-XL 语言:

转换生成的 PCL 文件,文件头写明 PCL-XL 语言


常见问题

输出文件名带 .pcl,为什么存出来的还是 PDF

原因:SaveToFile(fileName) 这个单参数重载固定按 PDF 格式写出,扩展名不参与格式判断——文件名以 .pcl 结尾,得到的依然是一份内容为 PDF 的文件。

解决:把输出格式显式作为第二个参数传入:

// 单参数:按 PDF 写出
doc.SaveToFile(outputFileName);

// 指定 FileFormat.PCL:写出真正的 PCL
doc.SaveToFile(outputFileName, pdfModule.FileFormat.PCL);

报 Cannot found font(Arial) installed on the system. 怎么解决

原因:转 PCL 要把页面上的文字转成打印机能识别的字体子集指令,转换器在虚拟文件系统的 /Library/Fonts/ 里找字体,目录为空就会抛这个错。

解决:在加载 PDF 之前把字体送进 VFS:

// 放在 LoadFromFile 之前,转换时字体已在 VFS 里
await window.spire.FetchFileToVFS('ARIAL UNICODE MS.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

生成的 PCL 文件用什么打开

原因:输出是 PCL-XL(PCL6),文件以 ESC%-12345X@PJL ENTER LANGUAGE = PCLXL 开头,其后是二进制的 HP-PCL XL;2;0 数据,并不是文本,用编辑器打开只会看到乱码。

解决:直接交给支持 PCL 的打印机或打印服务,不需要人工打开;要确认格式是否正确,看文件开头那段 PJL 声明即可。


获取免费许可证

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

彩色文档打印时费墨,归档时那份颜色又用不上;PDF 的对象则按写入顺序排布,阅读器要等整份文件下载完才能渲染第一页,几十兆的手册点开先是空屏。这两件事过去都得靠桌面软件,一次处理一份,也进不了 Web 流程。

本文用 Spire.PDF for JavaScript 实现将 PDF 转换为灰度文档与线性化 PDF。灰度化改的是页面上的颜色,线性化改的只是文件内部的对象排列,两者互不影响。它基于 WebAssembly 在浏览器端直接加载、修改与保存 PDF 文档,全过程在本地完成,通过虚拟文件系统(VFS)读写文件,无需后端配合。

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

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


将 PDF 转换为灰度

打印或归档前先做一次灰度化,可以省下彩墨。构造 PdfGrayConverter 时传入输入文件,调用 ToGrayPdf 写出灰度文档——页面上的彩色图像会被重新编码为灰阶,文字仍是可检索的文本。

function App() {
  const convertToGrayscale = 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/`);

    // 定义输出文件名
    const outputFileName = '灰度文档.pdf';

    // 以输入文件构造灰度转换器,再写出灰度文档
    const converter = new pdfModule.PdfGrayConverter({ filePath: inputFileName });
    converter.ToGrayPdf({ filePath: outputFileName });
    converter.Dispose();

    // 从 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>将 PDF 转换为灰度</h1>
      <button onClick={convertToGrayscale}>
        开始转换
      </button>
    </div>
  );
}

export default App;

灰度后的示例文档,彩色图表已变为灰阶:

灰度后的示例文档,彩色图表已变为灰阶


将 PDF 线性化

线性化是为快速 Web 查看(Fast Web View)准备的:阅读器不必等整份文件传完,就能先渲染出第一页。它的用法和第 1 点一样——构造 PdfToLinearizedPdfConverter 时传入输入文件,调用 ToLinearizedPdf 写出结果,文档的外观与内容原样保留。

function App() {
  const convertToLinearized = 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/`);

    // 定义输出文件名
    const outputFileName = '线性化文档.pdf';

    // 以输入文件构造线性化转换器,再写出线性化文档
    const converter = new pdfModule.PdfToLinearizedPdfConverter({ filePath: inputFileName });
    converter.ToLinearizedPdf({ filePath: outputFileName });
    converter.Dispose();

    // 从 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>将 PDF 线性化</h1>
      <button onClick={convertToLinearized}>
        开始转换
      </button>
    </div>
  );
}

export default App;

线性化后的文档页面上看不出变化,改动在文件结构里:

线性化后的文档页面上看不出变化,改动在文件结构里


常见问题

转换后的文档里多出一行 Evaluation Warning

原因:未授权的 Spire.PDF 会在输出文档中插入一行 Evaluation Warning : The document was created with Spire.PDF for JavaScript.。它是按调用次数追加的——对同一份文件转换两次,这行警告就会出现两次。

解决:申请 30 天临时许可证后,输出文档不再带这行警告。详见文末「获取免费许可证」。

转灰度后文件的体积为什么变小了

原因:灰度化的做法是把彩色图像重新编码为单通道的 DeviceGray 图像,占用的空间随通道数一同下降。示例文档里的三个彩色图表原本是 3 通道 PNG、合计约 68 KB,转换后为 33 KB;整个文件由 119 KB 降到 82 KB。

解决:这是图像重编码的正常结果,不需要额外处理。文档的页数与文字内容都不变,文字仍可搜索、复制,只有颜色被改写。


获取免费许可证

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