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

Spire.Cloud 纯前端文档控件

在销售报表、成绩单或指标看板中,数值的横向对比往往比数值本身更重要。若为每一列都插入图表,工作表会显得拥挤;而数据条、色阶与图标集可以在不占用额外行列的前提下,用条形长度、颜色深浅和图标形态,把单元格中的数值大小直接呈现出来。这三者同属 Excel 的条件格式(Conditional Formatting),在 Excel 界面中位于「开始 → 条件格式」之下。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端直接完成上述操作,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。

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

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


在单元格区域中应用数据条

数据条在单元格内绘制一条水平色带,色带长度与该单元格数值在所选区域内的相对大小成正比。Spire.XLS for JavaScript 通过 ConditionalFormats.Add 创建条件格式集合,用 AddRange 指定作用区域,再用 AddCondition 取得条件对象;将 FormatType 设为 ConditionalFormatType.DataBar 即可生成数据条,条形颜色由 DataBar.BarColor 控制。具体操作步骤如下:

  1. 将字体与测试数据文件载入 VFS。
  2. 加载工作簿并取得工作表。
  3. 用 ConditionalFormats.Add 新增条件格式,并通过 AddRange 绑定数据区域。
  4. 通过 AddCondition 添加条件,将 FormatType 设为 DataBar,并设置条形颜色。
  5. 保存工作簿。

以下为完整的代码示例,演示如何在 React 中为销量数据应用数据条:

function App() {
  const applyDataBars = 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 = 'SalesData.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 dataRange = sheet.Range.get("B2:E9");

    // 新增条件格式,并将其绑定到该区域
    const xcfs = sheet.ConditionalFormats.Add();
    xcfs.AddRange(dataRange);

    // 添加数据条条件,设置条形颜色
    const format = xcfs.AddCondition();
    format.FormatType = xlsModule.ConditionalFormatType.DataBar;
    format.DataBar.BarColor = xlsModule.Color.get_CadetBlue();

    // 保存工作簿
    const outputFileName = "ApplyDataBars.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={applyDataBars}>Start</button>
    </div>
  );
}

export default App;

运行后,在单元格区域中应用数据条的效果:

在单元格区域中应用数据条


在单元格区域中应用色阶

色阶用颜色的深浅表示数值的高低:单元格的数值在整个区域中越大,取到的颜色越靠近刻度的高端。与数据条相同,色阶同样由 ConditionalFormats 添加,区别仅在于将 FormatType 设为 ConditionalFormatType.ColorScale;未指定颜色时生成的是双色刻度,区域最小值取橙色,最大值取浅黄色,中间数值按比例在两端之间过渡。具体操作步骤如下:

  1. 将字体与测试数据文件载入 VFS。
  2. 加载工作簿并取得工作表。
  3. 用 ConditionalFormats.Add 新增条件格式,并通过 AddRange 绑定数据区域。
  4. 通过 AddCondition 添加条件,并将 FormatType 设为 ColorScale。
  5. 保存工作簿。

以下为完整的代码示例,演示如何在 React 中为销量数据应用色阶:

function App() {
  const applyColorScales = 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 = 'SalesData.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 dataRange = sheet.Range.get("B2:E9");

    // 新增条件格式,并将其绑定到该区域
    const xcfs = sheet.ConditionalFormats.Add();
    xcfs.AddRange(dataRange);

    // 添加色阶条件,颜色按数值高低自动过渡
    const format = xcfs.AddCondition();
    format.FormatType = xlsModule.ConditionalFormatType.ColorScale;

    // 保存工作簿
    const outputFileName = "ApplyColorScales.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={applyColorScales}>Start</button>
    </div>
  );
}

export default App;

运行后,在单元格区域中应用色阶的效果:

在单元格区域中应用色阶


在单元格区域中应用图标集

图标集依照数值所在的区间,在单元格中显示不同的图标,例如用红、黄、绿三色交通灯标记低、中、高三档。使用的仍是同一套 API:将 FormatType 设为 ConditionalFormatType.IconSet,再通过 IconSet.IconSetType 选定图标样式;示例采用 IconSetType.ThreeTrafficLights1(三色交通灯)。具体操作步骤如下:

  1. 将字体与测试数据文件载入 VFS。
  2. 加载工作簿并取得工作表。
  3. 用 ConditionalFormats.Add 新增条件格式,并通过 AddRange 绑定数据区域。
  4. 通过 AddCondition 添加条件,将 FormatType 设为 IconSet,并指定图标集类型。
  5. 保存工作簿。

以下为完整的代码示例,演示如何在 React 中为销量数据应用图标集:

function App() {
  const applyIconSets = 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 = 'SalesData.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 dataRange = sheet.Range.get("B2:E9");

    // 新增条件格式,并将其绑定到该区域
    const xcfs = sheet.ConditionalFormats.Add();
    xcfs.AddRange(dataRange);

    // 添加图标集条件,图标样式设为三色交通灯
    const format = xcfs.AddCondition();
    format.FormatType = xlsModule.ConditionalFormatType.IconSet;
    format.IconSet.IconSetType = xlsModule.IconSetType.ThreeTrafficLights1;

    // 保存工作簿
    const outputFileName = "ApplyIconSets.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={applyIconSets}>Start</button>
    </div>
  );
}

export default App;

图标集按区域内的取值区间划分档次,因此同一个图标在不同区域中对应的数值范围并不相同。

运行后,在单元格区域中应用图标集的效果:

在单元格区域中应用图标集


常见问题

为什么作用区域中的文字单元格没有出现数据条?

原因:数据条表达的是数值在区域内的相对大小,因此只有数值单元格会被着色,区域中的文本单元格会被自动跳过。即使作用区域包含产品名称列或表头行,这些单元格也不会显示数据条,得到的仍是只覆盖数值区域的条形。

解决:这是预期行为,无需额外处理;把作用区域限定为数值区域即可。若连数值区域也没有出现数据条,则应检查 AddRange 传入的区域是否与数据实际所在的区域一致。

数据条的颜色和边框可以自定义吗?

原因:数据条的外观由条件对象的 DataBar 属性控制。条形填充色通过 DataBar.BarColor 指定;若只设置 FormatType 而不设置 BarColor,得到的是默认的蓝色数据条。数据条默认没有边框,直接设置 BarBorder.Color 不会生效。

解决:先通过 DataBar.BarBorder.Type 指定边框类型,再设置边框颜色,两者需配合使用:

// 先指定边框类型,边框颜色才会生效
format.DataBar.BarBorder.Type = xlsModule.DataBarBorderType.DataBarBorderSolid;
format.DataBar.BarBorder.Color = xlsModule.Color.get_Red();

// 条形填充色
format.DataBar.BarColor = xlsModule.Color.get_GreenYellow();

获取免费许可证

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

将 Word 文档转换为图片,是在线预览、生成缩略图以及限制内容被随意复制时最常用的处理方式,转换后的图片在任何设备上都能以一致的排版呈现。Spire.Doc for JavaScript 基于 WebAssembly 在浏览器端直接完成此转换,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。

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

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


文档页面转图片

文档页面转图片的核心流程分为三个阶段:首先通过 FetchFileToVFS 将字体文件和目标 Word 文档载入 WASM 虚拟文件系统;然后实例化 Document 加载文档,调用 SaveImageToStreams 并传入 pageIndex 将指定页面渲染为图片,保存到 VFS 中;最后从 VFS 读取生成的图片文件,封装为 Blob 后生成下载链接。

import React from 'react';

function App() {
  const ToImage = async () => {
    // 获取 Spire.Doc WASM 模块
    const docModule = window.wasmModule?.spiredoc;

    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // 将字体文件载入虚拟文件系统 (VFS)
    await window.spire.FetchFileToVFS('ARIALUNI.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'ToImage.docx';
    // 将目标 Word 文档载入 VFS
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

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

    // 定义输出文件名
    const outputFileName = "ToImage-result.png";

    // 将第一页转换为图片流,并保存到 VFS
    let img = doc.SaveImageToStreams({ pageIndex: 0, type: docModule.ImageType.Bitmap });
    img.Save(outputFileName);

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

    // 从 VFS 读取生成的文件,封装为 Blob 对象
    const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([modifiedFileArray], {  type: 'image/png'});
    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={ToImage}>
        生成
      </button>
    </div>
  );
}

export default App;

文档页面通过 SaveImageToStreams 转换后生成的 PNG 图片

文档页面通过 SaveImageToStreams 转换后生成的 PNG 图片


文档对象转图片

除了整页转换,实际项目中还常需要将文档中的某个元素单独导出为图片,例如为表格生成预览图,或把文档中的形状提取出来作为独立素材。段落、表格、表格行、表格单元格和形状都可以通过 Clone 方法复制到新建的 Document 中,再调用 SaveImageToStreams 渲染为图片。转换结果统一写入 VFS 中的输出目录,最后使用 JSZip 打包为一个 ZIP 文件供用户下载。

需要注意的是,形状(Shape)无法直接加入新建文档的段落中,示例中先将文档保存到内存流再重新加载,让形状在新文档中获得完整的布局上下文后再渲染。

import React from 'react';
import JSZip from "jszip";

function App() {
  const ToImage = async () => {
    // 获取 Spire.Doc WASM 模块
    const docModule = window.wasmModule?.spiredoc;

    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // 将字体文件载入虚拟文件系统 (VFS)
    await window.spire.FetchFileToVFS('ARIALUNI.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
 // 将目标 Word 文档载入 VFS
 const inputFileName = "ConvertObjectToImage.docx";
 await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

 // 在 VFS 中定义并创建输出目录
 const outputDirectoryName = "outputFolder/";
 await window.dotnetRuntime.Module.FS.mkdirTree(outputDirectoryName);

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

 // 获取第一节及其正文
 let section = doc.Sections.get_Item(0);
 let body = section.Body;

 // 获取第一个段落并转换为图片
 let paragraph = body.Paragraphs.get_Item(0);
 let imageStream1 = ConvertParagraphToImage(paragraph, docModule);
 let imageFile1 = outputDirectoryName + "ConvertParagraphToImage.png";
 window.dotnetRuntime.Module.FS.writeFile(imageFile1, imageStream1.Save());

 // 获取第一个表格并转换为图片
 let table = body.Tables.get_Item(0);
 let imageStream2 = ConvertTableToImage(table, docModule);
 let imageFile2 = outputDirectoryName + "ConvertTableToImage.jpg";
 window.dotnetRuntime.Module.FS.writeFile(imageFile2, imageStream2.Save());

 // 获取第一个表格的第一行并转换为图片
 let row = table.Rows.get_Item(0);
 let imageStream3 = ConvertTableRowToImage(row, docModule);
 let imageFile3 = outputDirectoryName + "ConvertTableRowToImage.bmp";
 window.dotnetRuntime.Module.FS.writeFile(imageFile3, imageStream3.Save());

 // 获取第一行的第一个单元格并转换为图片
 let cell = row.Cells.get_Item(0);
 let imageStream4 = ConvertTableCellToImage(cell, docModule);
 let imageFile4 = outputDirectoryName + "ConvertTableCellToImage.png";
 window.dotnetRuntime.Module.FS.writeFile(imageFile4, imageStream4.Save());

 // 遍历段落,将其中的形状转换为图片
 for (let i = 0; i < section.Paragraphs.Count; i++) {
     let para = section.Body.Paragraphs.get_Item(i);
     for (let j = 0; j < para.ChildObjects.Count; j++) {
   let docObj = para.ChildObjects.get_Item(j);
   if (docObj.DocumentObjectType == docModule.DocumentObjectType.Shape) {
  let imageStream5 = ConvertShapeToImage(docObj, docModule);
  let imageFile5 = outputDirectoryName + "ConvertShapeToImage-" + j + ".png";

  window.dotnetRuntime.Module.FS.writeFile(imageFile5, imageStream5.Save());
  i++;
   }
     }
 }

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

 // 将输出目录下的所有图片打包为 ZIP 文件
 const zip = new JSZip();
 const addFilesToZip = async (folderPath, zipFolder) => {
     let items = await window.dotnetRuntime.Module.FS.readdir(folderPath);
     items = items.filter((item) => item !== "." && item !== "..");
     for (const item of items) {
   const itemPath = `${folderPath}/${item}`;
   try {
  const fileData = await window.dotnetRuntime.Module.FS.readFile(itemPath);
  zipFolder.file(item, fileData);
   } catch (error) {
  const zipSubFolder = zipFolder.folder(item);
  await addFilesToZip(itemPath, zipSubFolder);
   }
     }
  };

  await addFilesToZip(outputDirectoryName, zip);
  const zipBlob = await zip.generateAsync({ type: "blob" });
    // 从 VFS 读取生成的文件,封装为 Blob 对象
    const url = URL.createObjectURL(zipBlob);
    const a = document.createElement('a');
    a.href = url;
    a.download = "ConvertObjectToImage_out.zip";
    a.click();
    URL.revokeObjectURL(url);
  };
// 将段落转换为图片
    function ConvertParagraphToImage(paragraph, docModule) {
        let doc = new docModule.Document();
        let section = doc.AddSection();

        section.Body.ChildObjects.Add(paragraph.Clone());
        let imageStream = doc.SaveImageToStreams({ pageIndex: 0, type: docModule.ImageType.Bitmap });
        doc.Close();
        return imageStream;
    }

    // 将表格转换为图片
    function ConvertTableToImage(table, docModule) {
        let doc = new docModule.Document();
        let section = doc.AddSection();

        section.Body.ChildObjects.Add(table.Clone());

        let imageStream = doc.SaveImageToStreams({ pageIndex: 0, type: docModule.ImageType.Bitmap });
        doc.Close();
        return imageStream;
    }

    // 将表格行转换为图片
    function ConvertTableRowToImage(tableRow, docModule) {
        let doc = new docModule.Document();
        let section = doc.AddSection();
        let table = section.AddTable();
        table.Rows.Add(tableRow.Clone());
        let imageStream = doc.SaveImageToStreams({ pageIndex: 0, type: docModule.ImageType.Bitmap });
        doc.Close();
        return imageStream;
    }

    // 将表格单元格转换为图片
    function ConvertTableCellToImage(tableCell, docModule) {
        let doc = new docModule.Document();
        let section = doc.AddSection();
        let table = section.AddTable();
        table.AddRow().Cells.Add(tableCell.Clone());
        let imageStream = doc.SaveImageToStreams({ pageIndex: 0, type: docModule.ImageType.Bitmap });
        doc.Close();
        return imageStream;
    }

    // 将形状转换为图片
    function ConvertShapeToImage(shape, docModule) {
        let doc = new docModule.Document();
        let section = doc.AddSection();
        section.AddParagraph().ChildObjects.Add(shape.Clone());
        let memoryStream = new docModule.Stream();
        doc.SaveToStream({ stream: memoryStream, fileFormat: docModule.FileFormat.Docx });
        doc.LoadFromStream({ stream: memoryStream, fileFormat: docModule.FileFormat.Docx });
        let imageStream = doc.SaveImageToStreams({ pageIndex: 0, type: docModule.ImageType.Bitmap });
        memoryStream.Close();
        doc.Close();
        return imageStream;
    }

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>转换元素到图片</h1>
      <button onClick={ToImage}>
        生成
      </button>
    </div>
  );
}

export default App;

文档对象转换后打包生成的 ZIP 文件中的图片

文档对象转换后打包生成的 ZIP 文件中的图片


常见问题

生成的图片中文字缺失或乱码

原因:WASM 虚拟文件系统中缺少渲染所需的字体文件。SaveImageToStreams 渲染文本时需从 VFS 中读取字体,若未预加载则文字区域会留白或显示为乱码。

解决:转换前通过 FetchFileToVFS 将字体文件载入 VFS:

await window.spire.FetchFileToVFS(
  'ARIALUNI.TTF', '/Library/Fonts/', 
  `${process.env.PUBLIC_URL}/static/font/`
);

只生成了第一页的图片

原因:SaveImageToStreams 每次调用只渲染 pageIndex 指定的单个页面,示例中固定传入 0,因此多页文档只会输出第一页的图片。

解决:通过 PageCount 获取文档总页数并逐页遍历,为每一页生成独立的图片文件:

for (let i = 0; i < doc.PageCount; i++) {
  let img = doc.SaveImageToStreams({
    pageIndex: i, type: wasmModule.ImageType.Bitmap
  });
  img.Save(`ToImage-page-${i + 1}.png`);
}

获取免费许可证

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

实际工作中更常见的情况是:文档已经写好并带有完整的章节结构,只是当初没有生成目录。此时无需重排内容,只要在原有的标题样式基础上补一个目录域,即可得到带页码和跳转的完整目录。Spire.Doc for JavaScript 基于 WebAssembly 在浏览器端直接打开和编辑 Word 文档,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。

与新建文档相比,为已有文档添加目录多了两个关键步骤:一是通过 LoadFromFile 从 VFS 载入原有文档,二是通过 Paragraphs.Insert 把目录段落到文档最开头,而不是默认追加到末尾。

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

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


添加默认目录

为已有文档添加默认目录的核心流程分为三个阶段:首先通过 FetchFileToVFS 将字体文件和待处理的 Word 文档载入 WASM 虚拟文件系统;然后实例化 Document 并通过 LoadFromFile 载入文档,新建一个段落并用 AppendTOC 插入目录域,再通过 Paragraphs.Insert(0, tocPara) 将其移动到文档最开头;最后调用 UpdateTableOfContents 填充条目与页码,保存文档后从 VFS 读取生成的文件,封装为 Blob 后触发浏览器下载。

示例使用的输入文档 AddTocToExisting.docx 是一份带有三章十二个多级标题、但尚未生成目录的技术报告。

function App() {
  const AddTableOfContentsToExistingDocument = async () => {
    // 获取 Spire.Doc WASM 模块
    const docModule = window.wasmModule?.spiredoc;

    // 确保 WASM 模块完全加载完成
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // 将字体和已有 Word 文档载入 VFS
    await window.spire.FetchFileToVFS('msyh.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'AddTocToExisting.docx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // 创建文档实例并载入已有文档
    const doc = new docModule.Document();
    doc.LoadFromFile({ fileName: inputFileName });

    // 获取文档的第一节
    let section = doc.Sections.get_Item(0);

    // 新建段落并插入目录域,收集 Heading 1 至 Heading 3 的条目
    let tocPara = section.AddParagraph();
    tocPara.AppendTOC(1, 3);

    // 将目录段落移动到文档最开头
    section.Paragraphs.Insert(0, tocPara);

    // 更新目录,填充条目与页码
    doc.UpdateTableOfContents();

    // 定义输出文件名并保存
    const outputFileName = "为已有文档添加默认目录.docx";
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });

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

    // 从 VFS 读取生成的文件,触发下载
    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={AddTableOfContentsToExistingDocument}>
        生成
      </button>
    </div>
  );
}

export default App;

通过 LoadFromFile 载入已有文档并插入目录域后,目录被放置在文档最开头,原有章节内容与版式保持不变。

为已有文档添加默认目录后的效果


添加自定义目录

AppendTOC 生成的目录使用 Word 默认的域开关,当需要控制目录的具体行为时,可以改为直接构造 TableOfContent 对象并指定开关串。与上一个功能点的区别在于插入方式:需要手动将目录对象加入段落,并补齐域分隔符与域结束标记,同时将该对象赋给 document.TOC。常用的域开关及其含义如下:

开关 说明
\o "1-3" 按内置标题样式收集条目,此处表示收录 Heading 1 至 Heading 3
\h 将目录条目设为超链接,点击即可跳转到对应章节
\z 在 Web 版式视图中隐藏页码与制表符前导符
\u 按段落的大纲级别收集条目

如果希望目录单独占一页并与正文分开,可以在插入目录段落之后,再向同一节添加一个分页符:

tocPara.AppendBreak(docModule.BreakType.PageBreak);
function App() {
  const CustomizeTableOfContent = async () => {
    // 获取 Spire.Doc WASM 模块
    const docModule = window.wasmModule?.spiredoc;

    // 确保 WASM 模块完全加载完成
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // 将字体和已有 Word 文档载入 VFS
    await window.spire.FetchFileToVFS('msyh.ttc', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'AddTocToExisting.docx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // 创建文档实例并载入已有文档
    const doc = new docModule.Document();
    doc.LoadFromFile({ fileName: inputFileName });

    // 获取文档的第一节
    let section = doc.Sections.get_Item(0);

    // 构造带自定义域开关的目录对象
    let toc = new docModule.TableOfContent(doc, "{\\o \"1-3\" \\h \\z \\u}");

    // 将目录对象加入段落
    let tocPara = section.AddParagraph();
    tocPara.Items.Add(toc);

    // 补齐域分隔符与域结束标记
    tocPara.AppendFieldMark(docModule.FieldMarkType.FieldSeparator);
    tocPara.AppendText("TOC");
    tocPara.AppendFieldMark(docModule.FieldMarkType.FieldEnd);

    // 将该目录绑定到文档
    doc.TOC = toc;

    // 将目录段落移动到文档最开头
    section.Paragraphs.Insert(0, tocPara);

    // 更新目录,填充条目与页码
    doc.UpdateTableOfContents();

    // 定义输出文件名并保存
    const outputFileName = "为已有文档添加自定义目录.docx";
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });

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

    // 从 VFS 读取生成的文件,触发下载
    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={CustomizeTableOfContent}>
        生成
      </button>
    </div>
  );
}

export default App;

通过 TableOfContent 对象与自定义域开关生成的目录,条目层级、超链接与页码表现均由开关串决定。

为已有文档添加自定义目录后的效果


常见问题

目录出现在文档末尾而不是开头

原因:AddParagraph 默认把新段落追加到所在节的末尾,直接在其上插入目录域,目录自然也会出现在文末。已有文档的内容已经排好,因此必须显式指定插入位置。

解决:先创建目录段落,再用 Paragraphs.Insert 把它移动到文档最开头:

let tocPara = section.AddParagraph();
tocPara.AppendTOC(1, 3);
section.Paragraphs.Insert(0, tocPara);

目录为空或没有任何条目

原因:目录域按标题样式收集条目。如果原文档中的章节标题只是手动加粗、放大了字号,而没有应用 Heading1 至 Heading3 等内置标题样式,更新后目录中不会出现任何条目。

解决:先检查原文档的标题是否使用内置标题样式。若没有,可以在载入文档后重新为这些段落应用样式:

let heading = section.Paragraphs.get_Item(2);
heading.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });

目录页码为空或与实际页码不一致

原因:AppendTOC 只是插入了目录域本身,域内容需要显式更新。若在保存前未调用 UpdateTableOfContents,生成的目录只有域代码,没有条目和页码。

解决:在 SaveToFile 之前调用更新方法:

doc.UpdateTableOfContents();
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });

获取免费许可证

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

文档常常是拼出来的:封面、正文、附件分别来自不同环节,交付前才发现封面被排到了第二章后面,或者某几页的先后需要调换。要改的只是页面顺序,内容一个字都不用动,但身边没有 PDF 编辑器时,这件事就卡住了——把文件传到服务端处理,等于让它离开用户的设备。本文将介绍如何使用 Spire.PDF for JavaScript 在浏览器端按指定顺序重排 PDF 页面。它基于 WebAssembly 加载、修改与保存 PDF 文档,页面顺序的调整全部在本地完成,通过虚拟文件系统(VFS)读写文件,无需后端配合。

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


重排页面顺序

页面顺序由 PdfPageCollection.ReArrange() 一次性改写,传入的序号数组既决定每一页的新位置,也决定文档的页数——数组里放几个索引,输出就是几页,所以要保留全部页面,就得把 0 到页数减一的索引一个不落地排进去。

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

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

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

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

    // 新的页面顺序:把第 3 页提到最前,其余页面顺延;索引从 0 开始
    const newOrder = [2, 0, 1, 3, 4];
    doc.Pages.ReArrange(newOrder);

    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>重排 PDF 页面顺序</h1>
      <button onClick={rearrangePages}>
        开始重排
      </button>
    </div>
  );
}

export default App;

第 3 页被移到最前、其余页面依次顺延后的文档:

第 3 页被移到最前、其余页面依次顺延后的文档


常见问题

重排之后文档的页数变少了

原因:ReArrange() 是按传入的数组重建页面顺序,数组里没有出现的索引对应的页面不会进入结果文档。把 5 页文档写成 ReArrange([1, 0]),输出的就是 2 页。

解决:页数要保持不变,数组长度必须等于 doc.Pages.Count,且 0 到 doc.Pages.Count - 1 每个索引都出现一次:

// 索引从 0 开始,5 页文档对应 0、1、2、3、4
const pageCount = doc.Pages.Count;
const fullOrder = Array.from({ length: pageCount }, (_, i) => i);
doc.Pages.ReArrange(fullOrder);

调用 ReArrange() 时提示 "The page has existed."

原因:序号数组里出现了重复的索引,同一个原页被指定到了两个位置。一页无法同时占据两个位置,Spire.PDF 会直接抛错。

解决:确认数组是原页索引的一个排列——每个索引只出现一次,且不要超出页数范围,5 页文档的有效索引是 0 到 4。

只交换其中两页的位置该怎么写

原因:ReArrange() 处理的始终是整份文档的顺序,没有"只换两页"的简化重载。

解决:仍然按完整顺序数组来写,未涉及的位置保持原索引即可。下面这行把第 1 页与第 2 页对调:

// 交换前两页:0 与 1 互换,其余不动
const swappedOrder = [1, 0, 2, 3, 4];
doc.Pages.ReArrange(swappedOrder);

获取免费许可证

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

一批文档要看起来像同一套,最省事的做法是给每页铺上统一的品牌底色或信纸底纹。过去要么回到源文件逐份改版式,要么手工往每页叠一张图——前者要求手上还有可编辑的原始文档,后者稍不注意就压到正文。

在浏览器里直接改 PDF 就能绕开这两点:Spire.PDF for JavaScript 基于 WebAssembly 加载、修改与保存 PDF 文档,底色和底图都写进已有页面的 BackgroundColor、BackgroundImage 属性,绘制层级在正文下方;文件读写通过虚拟文件系统(VFS)完成,无需后端配合。

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

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


为 PDF 所有页面设置背景颜色

整份文档要统一底色,逐页给 BackgroundColor 赋一个 Color 就行,该属性默认只按 0.25 的不透明度叠加,需要实色时把 BackgroudOpacity 设为 1。

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

    // 自定义底色:ARGB 四通道,这里取浅蓝
    const backgroundColor = pdfModule.Color.FromArgb(255, 226, 240, 253);

    // 逐页设置背景颜色
    for (let i = 0; i < doc.Pages.Count; i++) {
      const page = doc.Pages.get_Item(i);
      page.BackgroundColor = backgroundColor;
      // 默认叠加不透明度为 0.25,置为 1 得到实色
      page.BackgroudOpacity = 1;
    }

    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>设置 PDF 背景颜色</h1>
      <button onClick={setBackgroundColor}>
        开始设置
      </button>
    </div>
  );
}

export default App;

设置浅蓝色背景后的两页文稿,底色前后保持一致:

设置浅蓝色背景后的两页文稿,底色前后保持一致


为 PDF 所有页面设置背景图片

整页铺一张底图则交给 BackgroundImage:它接收一个在虚拟文件系统里打开的图片流,并自动拉伸铺满页面内容区,浓淡同样由 BackgroudOpacity 控制。

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

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

    // 将待处理的 PDF 文件与背景图载入 VFS
    const inputFileName = '模板.pdf';
    const imageFileName = 'Background.png';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
    await window.spire.FetchFileToVFS(imageFileName, "", `${process.env.PUBLIC_URL}/data/`);

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

    // 从 VFS 打开背景图文件流,各页共用同一个流
    const imageStream = new window.spire.Stream(imageFileName);

    // 逐页铺上底图,图片会拉伸铺满页面内容区
    for (let i = 0; i < doc.Pages.Count; i++) {
      const page = doc.Pages.get_Item(i);
      page.BackgroundImage = imageStream;
      page.BackgroudOpacity = 1;
    }

    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>设置 PDF 背景图片</h1>
      <button onClick={setBackgroundImage}>
        开始设置
      </button>
    </div>
  );
}

export default App;

同一张底图铺满每一页,正文文字依旧清晰:

同一张底图铺满每一页,正文文字依旧清晰


常见问题

设置完背景色,颜色比预期的淡很多

原因:BackgroudOpacity 属性(拼写里少一个 n)默认是 0.25,背景会按 25% 的不透明度叠到页面上,深色也会被冲成浅色。

解决:需要实色就把 BackgroudOpacity 设为 1;想保留一点淡化效果,也可以取 0.3 到 0.8 之间的值。

page.BackgroundColor = backgroundColor;
// 置为 1 得到实色,调小则是淡化叠加
page.BackgroudOpacity = 1;

背景没有铺满整页,页面四周还是白的

原因:背景只绘制在页面的内容区(ClientSize)内。载入的 PDF 一般没有页边距,背景就是整页;而用 Pages.Add() 新建的页面默认带 40 磅页边距,这一圈不会被着色。

解决:新建页时把页边距设为 0,背景即可覆盖整页:

// 第二个参数传零页边距对象,页面内容区与整页等大
const page = doc.Pages.Add(pdfModule.PdfPageSize.A4(), new pdfModule.PdfMargins());

只想给其中某一页加背景

原因:BackgroundColor 与 BackgroundImage 都是页面级属性,只对赋值的那个页面生效,不会自动应用到整份文档。

解决:按页号单独取页设置即可,不必遍历:

// 只处理第 1 页,其余页面保持原样
const page = doc.Pages.get_Item(0);
page.BackgroundColor = pdfModule.Color.FromArgb(255, 226, 240, 253);
page.BackgroudOpacity = 1;

获取免费许可证

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

为长文档创建目录,既能让读者快速定位章节内容,也便于在文档结构变动后重新同步条目与页码。本文以一份《Spire.Doc 开发指南》为例,介绍如何从零新建一份带有多级标题的 Word 文档,并为其添加内容目录。Spire.Doc for JavaScript 基于 WebAssembly 在浏览器端直接构建和编辑 Word 文档,通过虚拟文件系统(VFS)管理字体资源,无需后端服务支持。

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

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


添加默认目录

在 Word 中,目录本质上是一个 TOC 域,其条目来自文档中应用了标题样式的段落。创建默认目录的核心流程分为三个阶段:首先通过 FetchFileToVFS 将字体文件载入 WASM 虚拟文件系统;然后实例化 Document 并通过 AddSection、AddParagraph 构建内容,对需要出现在目录中的段落调用 ApplyStyle 应用标题样式,再通过 AppendTOC 在文档开头插入目录域;最后调用 UpdateTableOfContents 填充条目与页码,保存文档后从 VFS 读取生成的文件,封装为 Blob 后触发浏览器下载。

示例文档共包含三章、十二个多级标题,涵盖 Heading 1 至 Heading 3 三个层级,便于观察目录对多级标题的收录效果。

function App() {
  const AddTableOfContentsToNewDocument = async () => {
    // 获取 Spire.Doc WASM 模块
    const docModule = window.wasmModule?.spiredoc;

    // 确保 WASM 模块完全加载完成
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

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

    // 创建文档实例并添加节
    const doc = new docModule.Document();
    let section = doc.AddSection();

    // 在文档开头插入目录域,收集 Heading 1 至 Heading 3 的条目
    let tocPara = section.AddParagraph();
    tocPara.AppendTOC(1, 3);

    // 添加文档标题
    let characterFormat = new docModule.CharacterFormat(doc);
    characterFormat.FontName ="微软雅黑";
    let title = section.AddParagraph();
    let titleRun = title.AppendText("Spire.Doc 开发指南");
    titleRun.ApplyCharacterFormat(characterFormat);
    titleRun.CharacterFormat.FontSize = 24;
    title.Format.HorizontalAlignment = docModule.HorizontalAlignment.Center;

    // 第一章 概述(一级标题)
    let p = section.AddParagraph();
    p.AppendText("第一章 概述").ApplyCharacterFormat(characterFormat);;
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });
    section.AddParagraph().AppendText("Spire.Doc for JavaScript 让开发者可以直接在浏览器中创建、编辑与保存 Word 文档,整个过程无需任何后端服务参与。").ApplyCharacterFormat(characterFormat);;

    // 1.1 什么是 Spire.Doc for JavaScript(二级标题)
    p = section.AddParagraph();
    p.AppendText("1.1 什么是 Spire.Doc for JavaScript").ApplyCharacterFormat(characterFormat);;
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
    section.AddParagraph().AppendText("它是一套基于 WebAssembly 的 Word 文档处理库,通过虚拟文件系统(VFS)管理字体与文档文件,并提供与 .NET 版一致的 API 形态。").ApplyCharacterFormat(characterFormat);;

    // 1.2 适用场景(二级标题)
    p = section.AddParagraph();
    p.AppendText("1.2 适用场景").ApplyCharacterFormat(characterFormat);;
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
    section.AddParagraph().AppendText("适用于在线合同签署、报表批量生成、简历模板填充等需要在客户端完成文档处理的场景。").ApplyCharacterFormat(characterFormat);;

    // 第二章 核心能力(一级标题)
    p = section.AddParagraph();
    p.AppendText("第二章 核心能力").ApplyCharacterFormat(characterFormat);;
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });
    section.AddParagraph().AppendText("Spire.Doc for JavaScript 覆盖了文档处理的完整链路,从内容构建、版式调整到格式导出均可在浏览器端完成。").ApplyCharacterFormat(characterFormat);;

    // 2.1 文档处理(二级标题)
    p = section.AddParagraph();
    p.AppendText("2.1 文档处理").ApplyCharacterFormat(characterFormat);;
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
    section.AddParagraph().AppendText("支持段落、样式、表格、图片、页眉页脚等常见文档元素的创建与修改,并保留原有的版式信息。").ApplyCharacterFormat(characterFormat);;

    // 2.1.1 段落与样式(三级标题)
    p = section.AddParagraph();
    p.AppendText("2.1.1 段落与样式").ApplyCharacterFormat(characterFormat);;
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading3 });
    section.AddParagraph().AppendText("通过 AddParagraph 新增段落,配合 ApplyStyle 应用内置样式,即可快速搭建结构清晰的文档骨架。").ApplyCharacterFormat(characterFormat);;

    // 2.1.2 表格与图片(三级标题)
    p = section.AddParagraph();
    p.AppendText("2.1.2 表格与图片").ApplyCharacterFormat(characterFormat);;
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading3 });
    section.AddParagraph().AppendText("表格与图片既可以直接写入指定段落,也可以嵌套在文本框内,满足复杂版式的排版需求。").ApplyCharacterFormat(characterFormat);;

    // 2.2 格式转换(二级标题)
    p = section.AddParagraph();
    p.AppendText("2.2 格式转换").ApplyCharacterFormat(characterFormat);;
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
    section.AddParagraph().AppendText("通过 SaveToFile 可以将文档转换为 PDF、HTML、Markdown 等多种格式,整个转换过程均在浏览器端完成。").ApplyCharacterFormat(characterFormat);;

    // 2.3 批量处理(二级标题)
    p = section.AddParagraph();
    p.AppendText("2.3 批量处理").ApplyCharacterFormat(characterFormat);;
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
    section.AddParagraph().AppendText("结合 WebAssembly 的运行效率,可以一次加载多份文档并顺序完成处理,避免频繁的文件上传与下载。").ApplyCharacterFormat(characterFormat);;

    // 第三章 快速上手(一级标题)
    p = section.AddParagraph();
    p.AppendText("第三章 快速上手").ApplyCharacterFormat(characterFormat);;
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });
    section.AddParagraph().AppendText("本章介绍在 React 项目中接入 Spire.Doc for JavaScript 并完成第一次文档生成所需的准备工作。").ApplyCharacterFormat(characterFormat);;

    // 3.1 环境准备(二级标题)
    p = section.AddParagraph();
    p.AppendText("3.1 环境准备").ApplyCharacterFormat(characterFormat);;
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
    section.AddParagraph().AppendText("在 React 项目中安装 Spire.Doc for JavaScript,并将字体文件与 WASM 资源放入 public 目录即可开始使用。").ApplyCharacterFormat(characterFormat);;

    // 3.2 第一个示例(二级标题)
    p = section.AddParagraph();
    p.AppendText("3.2 第一个示例").ApplyCharacterFormat(characterFormat);;
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
    section.AddParagraph().AppendText("初始化模块后创建 Document 实例,添加内容并保存,最后从 VFS 读取结果文件触发浏览器下载。").ApplyCharacterFormat(characterFormat);;

    // 更新目录,填充条目与页码
    doc.UpdateTableOfContents();

    // 定义输出文件名并保存
    const outputFileName = "在 Word 文档中创建默认目录.docx";
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });

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

    // 从 VFS 读取生成的文件,触发下载
    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={AddTableOfContentsToNewDocument}>
        生成
      </button>
    </div>
  );
}

export default App;

通过 AppendTOC 插入目录域并更新后,文档开头生成了收录三级标题、带页码与超链接的默认目录。

通过 AppendTOC 为新建文档添加默认目录后的效果


添加自定义目录

AppendTOC 生成的目录使用 Word 默认的域开关,当需要控制目录的具体行为时,可以改为直接构造 TableOfContent 对象并指定开关串。与上一个功能点的区别在于插入方式:需要手动将目录对象加入段落,并补齐域分隔符与域结束标记,同时将该对象赋给 document.TOC。常用的域开关及其含义如下:

开关 说明
\o "1-3" 按内置标题样式收集条目,此处表示收录 Heading 1 至 Heading 3
\h 将目录条目设为超链接,点击即可跳转到对应章节
\z 在 Web 版式视图中隐藏页码与制表符前导符
\u 按段落的大纲级别收集条目

例如将开关串改为 \o "1-2",目录就只收录一级和二级标题,三级标题不再出现在目录中;去掉 \h 则目录条目不再具备跳转能力。

function App() {
  const CustomizeTableOfContent = async () => {
    // 获取 Spire.Doc WASM 模块
    const docModule = window.wasmModule?.spiredoc;

    // 确保 WASM 模块完全加载完成
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

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

    // 创建文档实例并添加节
    const doc = new docModule.Document();
    let section = doc.AddSection();

    // 构造带自定义域开关的目录对象
    let toc = new docModule.TableOfContent(doc, "{\\o \"1-2\" \\h \\z \\u}");

    // 将目录对象加入段落
    let tocPara = section.AddParagraph();
    tocPara.Items.Add(toc);

    // 补齐域分隔符与域结束标记
    tocPara.AppendFieldMark(docModule.FieldMarkType.FieldSeparator);
    tocPara.AppendText("TOC");
    tocPara.AppendFieldMark(docModule.FieldMarkType.FieldEnd);

    // 将该目录绑定到文档
    doc.TOC = toc;

    // 添加文档标题
    let characterFormat = new docModule.CharacterFormat(doc);
    characterFormat.FontName ="微软雅黑";
    let title = section.AddParagraph();
    let titleRun = title.AppendText("Spire.Doc 开发指南");
    titleRun.ApplyCharacterFormat(characterFormat);
    titleRun.CharacterFormat.FontSize = 24;
    title.Format.HorizontalAlignment = docModule.HorizontalAlignment.Center;

    // 第一章 概述(一级标题)
    let p = section.AddParagraph();
    p.AppendText("第一章 概述").ApplyCharacterFormat(characterFormat);
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });
    section.AddParagraph().AppendText("Spire.Doc for JavaScript 让开发者可以直接在浏览器中创建、编辑与保存 Word 文档,整个过程无需任何后端服务参与。").ApplyCharacterFormat(characterFormat);
    
    // 1.1 什么是 Spire.Doc for JavaScript(二级标题)
    p = section.AddParagraph();
    p.AppendText("1.1 什么是 Spire.Doc for JavaScript").ApplyCharacterFormat(characterFormat);
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
    section.AddParagraph().AppendText("它是一套基于 WebAssembly 的 Word 文档处理库,通过虚拟文件系统(VFS)管理字体与文档文件,并提供与 .NET 版一致的 API 形态。").ApplyCharacterFormat(characterFormat);

    // 1.2 适用场景(二级标题)
    p = section.AddParagraph();
    p.AppendText("1.2 适用场景").ApplyCharacterFormat(characterFormat);
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
    section.AddParagraph().AppendText("适用于在线合同签署、报表批量生成、简历模板填充等需要在客户端完成文档处理的场景。").ApplyCharacterFormat(characterFormat);

    // 第二章 核心能力(一级标题)
    p = section.AddParagraph();
    p.AppendText("第二章 核心能力").ApplyCharacterFormat(characterFormat);
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });
    section.AddParagraph().AppendText("Spire.Doc for JavaScript 覆盖了文档处理的完整链路,从内容构建、版式调整到格式导出均可在浏览器端完成。").ApplyCharacterFormat(characterFormat);

    // 2.1 文档处理(二级标题)
    p = section.AddParagraph();
    p.AppendText("2.1 文档处理").ApplyCharacterFormat(characterFormat);
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
    section.AddParagraph().AppendText("支持段落、样式、表格、图片、页眉页脚等常见文档元素的创建与修改,并保留原有的版式信息。").ApplyCharacterFormat(characterFormat);

    // 2.1.1 段落与样式(三级标题)
    p = section.AddParagraph();
    p.AppendText("2.1.1 段落与样式").ApplyCharacterFormat(characterFormat);
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading3 });
    section.AddParagraph().AppendText("通过 AddParagraph 新增段落,配合 ApplyStyle 应用内置样式,即可快速搭建结构清晰的文档骨架。").ApplyCharacterFormat(characterFormat);

    // 2.1.2 表格与图片(三级标题)
    p = section.AddParagraph();
    p.AppendText("2.1.2 表格与图片").ApplyCharacterFormat(characterFormat);
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading3 });
    section.AddParagraph().AppendText("表格与图片既可以直接写入指定段落,也可以嵌套在文本框内,满足复杂版式的排版需求。").ApplyCharacterFormat(characterFormat);

    // 2.2 格式转换(二级标题)
    p = section.AddParagraph();
    p.AppendText("2.2 格式转换").ApplyCharacterFormat(characterFormat);
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
    section.AddParagraph().AppendText("通过 SaveToFile 可以将文档转换为 PDF、HTML、Markdown 等多种格式,整个转换过程均在浏览器端完成。").ApplyCharacterFormat(characterFormat);

    // 2.3 批量处理(二级标题)
    p = section.AddParagraph();
    p.AppendText("2.3 批量处理").ApplyCharacterFormat(characterFormat);
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
    section.AddParagraph().AppendText("结合 WebAssembly 的运行效率,可以一次加载多份文档并顺序完成处理,避免频繁的文件上传与下载。").ApplyCharacterFormat(characterFormat);

    // 第三章 快速上手(一级标题)
    p = section.AddParagraph();
    p.AppendText("第三章 快速上手").ApplyCharacterFormat(characterFormat);
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });
    section.AddParagraph().AppendText("本章介绍在 React 项目中接入 Spire.Doc for JavaScript 并完成第一次文档生成所需的准备工作。").ApplyCharacterFormat(characterFormat);

    // 3.1 环境准备(二级标题)
    p = section.AddParagraph();
    p.AppendText("3.1 环境准备").ApplyCharacterFormat(characterFormat);
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
    section.AddParagraph().AppendText("在 React 项目中安装 Spire.Doc for JavaScript,并将字体文件与 WASM 资源放入 public 目录即可开始使用。").ApplyCharacterFormat(characterFormat);

    // 3.2 第一个示例(二级标题)
    p = section.AddParagraph();
    p.AppendText("3.2 第一个示例").ApplyCharacterFormat(characterFormat);
    p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
    section.AddParagraph().AppendText("初始化模块后创建 Document 实例,添加内容并保存,最后从 VFS 读取结果文件触发浏览器下载。").ApplyCharacterFormat(characterFormat);

    // 更新目录,填充条目与页码
    doc.UpdateTableOfContents();

    // 定义输出文件名并保存
    const outputFileName = "在 Word 文档中创建自定义目录.docx";
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });

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

    // 从 VFS 读取生成的文件,触发下载
    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={CustomizeTableOfContent}>
        生成
      </button>
    </div>
  );
}

export default App;

通过 TableOfContent 对象与自定义域开关生成的目录,条目层级、超链接与页码表现均由开关串决定。

通过自定义域开关为新建文档添加目录后的效果


常见问题

生成的目录为空或没有任何条目

原因:目录域按标题样式收集条目。如果段落没有应用 Heading1 至 Heading3 等内置标题样式,即使成功插入了目录域,更新后目录中也不会出现任何条目。

解决:对需要出现在目录中的段落调用 ApplyStyle 应用标题样式:

p.AppendText("第一章 概述");
p.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });

目录的条目层级比预期少

原因:AppendTOC 的两个参数分别对应目录收录的起始与结束标题级别,若传入 AppendTOC(1, 2),三级标题将不会出现在目录中。使用自定义开关时,\o "1-2" 也会产生同样的结果。

解决:按需要收录的层级调整参数范围,例如收录一至三级标题:

tocPara.AppendTOC(1, 3);

目录页码为空或与实际页码不一致

原因:AppendTOC 只是插入了目录域本身,域内容需要显式更新。若在保存前未调用 UpdateTableOfContents,生成的目录只有域代码,没有条目和页码。

解决:在 SaveToFile 之前调用更新方法:

doc.UpdateTableOfContents();
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });

获取免费许可证

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

Excel 除用于存放单元格数据外,也常被用作文件容器:报价单中可嵌入 Word 版合同条款,产品表中可附带 PDF 规格书,双击对象即可直接打开源文件。此类嵌入工作表的文件称为 OLE 对象(Object Linking and Embedding)。在 Excel 界面中插入 OLE 对象仅需「插入 → 对象」两步操作,但若需在浏览器端由程序批量完成,则须借助专门的 API。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端直接完成上述操作,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。

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

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


在 Excel 中插入 OLE 对象

OleObjects.Add 用于将外部文件插入工作表,共接受三个参数:待嵌入的文件名、对象在工作表上显示的图标,以及链接方式——OleLinkType.Embed 表示将文件嵌入工作簿,OleLinkType.Link 表示以链接方式插入。对象插入后,用 Location 指定锚定到哪个单元格、用 ObjectType 声明嵌入的文件类型,Excel 据此确定双击时调用哪个程序打开。具体操作步骤如下:

  1. 新建工作簿,在单元格中写入说明文字。
  2. 打开被嵌入的工作簿,将其工作表渲染为图片,作为 OLE 对象的显示图标。
  3. 通过 OleObjects.Add 将该 Excel 文件嵌入工作表。
  4. 设置 Location 与 ObjectType。
  5. 保存工作簿。

以下为完整的代码示例,演示如何在 React 中将 Excel 文件作为 OLE 对象插入工作表:

function App() {
  const insertOleObject = 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 embeddedFileName = 'OLEObjects.xlsx';
    await window.spire.FetchFileToVFS(embeddedFileName, '', `${process.env.PUBLIC_URL}/static/data/`);

    // 新建工作簿,并写入说明文字
    const workbook = new xlsModule.Workbook();
    const sheet = workbook.Worksheets.get(0);
    sheet.Range.get("A1").Text = "这里是一个 OLE 对象。";

    // 打开被嵌入的工作簿,把它的工作表渲染成图片,作为 OLE 对象的显示图标
    const embeddedBook = new xlsModule.Workbook();
    embeddedBook.LoadFromFile(embeddedFileName);
    const embeddedSheet = embeddedBook.Worksheets.get(0);
    embeddedSheet.PageSetup.LeftMargin = 0;
    embeddedSheet.PageSetup.RightMargin = 0;
    embeddedSheet.PageSetup.TopMargin = 0;
    embeddedSheet.PageSetup.BottomMargin = 0;
    const image = embeddedSheet.ToImage(1, 1, 19, 5);
    embeddedBook.Dispose();

    // 把 Excel 文件嵌入工作表,文件数据随工作簿一起保存
    const oleObject = sheet.OleObjects.Add(
      embeddedFileName,
      image,
      xlsModule.OleLinkType.Embed
    );

    // 把对象锚定到 B4 单元格,并声明它是一份 Excel 工作表
    oleObject.Location = sheet.Range.get("B4");
    oleObject.ObjectType = xlsModule.OleObjectType.ExcelWorksheet;

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

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

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

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>插入 OLE 对象</h1>
      <button onClick={insertOleObject}>Start</button>
    </div>
  );
}

export default App;

示例中的图标直接取自被嵌入工作表的渲染结果,因此 OLE 对象在工作表中显示的是其自身内容。将 ObjectType 改为 OleObjectType.WordDocument、OleObjectType.AdobeAcrobatDocument 等值,即可声明其他类型的嵌入文件。

运行后,插入 Excel 工作簿作为 OLE 对象的效果:

在 Excel 中插入 OLE 对象


插入带自定义图标的 OLE 对象

ToImage 每次均需先打开工作簿、再按行列范围渲染,适用于需要让对象呈现自身内容的场景;若要为附件统一配图标,直接读取一张现成的图片更为简便,且同一张图片可复用于多个附件。OleObjects.Add 的第二个参数本身即同时接受这两种入参。具体操作步骤如下:

  1. 将图标图片与附件文件载入 VFS。
  2. 新建工作簿,用 new xlsModule.Stream 将图标图片读取为流。
  3. 通过 OleObjects.Add 将附件以嵌入方式插入,并传入图标流。
  4. 设置 Location 与 ObjectType。
  5. 保存工作簿。

以下为完整的代码示例,演示如何在 React 中插入带自定义图标的 PDF 附件:

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

    // 将图标图片与附件载入 VFS
    const iconFileName = 'OLEIcon.png';
    const attachmentFileName = 'Attachment.pdf';
    await window.spire.FetchFileToVFS(iconFileName, '', `${process.env.PUBLIC_URL}/static/data/`);
    await window.spire.FetchFileToVFS(attachmentFileName, '', `${process.env.PUBLIC_URL}/static/data/`);

    // 新建工作簿
    const workbook = new xlsModule.Workbook();
    const sheet = workbook.Worksheets.get(0);

    // 把图标图片读成流,作为 OLE 对象在工作表上的显示图标
    const iconStream = new xlsModule.Stream(iconFileName);

    // 把 PDF 附件嵌入工作表
    const oleObject = sheet.OleObjects.Add(
      attachmentFileName,
      iconStream,
      xlsModule.OleLinkType.Embed
    );

    // 把对象锚定到 B4 单元格,并声明它是一份 PDF 文档
    oleObject.Location = sheet.Range.get("B4");
    oleObject.ObjectType = xlsModule.OleObjectType.AdobeAcrobatDocument;

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

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

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

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>插入带自定义图标的 OLE 对象</h1>
      <button onClick={insertOleObjectWithIcon}>Start</button>
    </div>
  );
}

export default App;

运行后,插入带自定义图标的 PDF 附件作为 OLE 对象的效果:

插入带自定义图标的 OLE 对象


常见问题

插入后工作表中只显示一个空白图标?

原因:OleObjects.Add 的第二个参数决定 OLE 对象在工作表上的显示图标。使用 ToImage 取图时,若指定的区域落在工作表已用范围之外,得到的将是一张空白图片,插入的对象也只会显示空白。

解决:将取图区域限制在表格有内容的范围内,或改用一张现成的图片文件:

// 用固定图片作为图标,不依赖工作表内容
const iconStream = new xlsModule.Stream('OLEIcon.png');
const oleObject = sheet.OleObjects.Add('Attachment.pdf', iconStream, xlsModule.OleLinkType.Embed);
oleObject.Location = sheet.Range.get("B4");
oleObject.ObjectType = xlsModule.OleObjectType.AdobeAcrobatDocument;

ObjectType 设为其他值会有什么影响?

原因:ObjectType 并非注释,其取值会被写入工作簿的 progId 字段,Excel 双击对象时依据该标识查找对应的程序。同一份 PDF 附件,声明为 OleObjectType.AdobeAcrobatDocument 时写出的 progId 为 Acrobat Document;若声明为 OleObjectType.ExcelWorksheet,写出的则变为 Worksheet,Excel 会尝试用 Excel 本身打开该 PDF,对象将无法打开。

解决:按嵌入文件的实际类型设置 ObjectType。常用取值如下:

嵌入文件 ObjectType
Excel 工作簿 OleObjectType.ExcelWorksheet
Word 文档 OleObjectType.WordDocument
PowerPoint 演示文稿 OleObjectType.PowerPointSlide
PDF 文档 OleObjectType.AdobeAcrobatDocument
// 按文件真实类型声明,Excel 才能用正确的程序打开
oleObject.ObjectType = xlsModule.OleObjectType.AdobeAcrobatDocument;

获取免费许可证

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

带图的报表攒到一定数量,麻烦的往往不是文字而是图片:几百 KB 的产品图直接插进工作表,工作簿体积迅速膨胀,发送、归档都变得吃力;从别处复制过来的图片尺寸又不统一,大的盖住整块数据区,小的缩在角落里看不清。这些调整在 Excel 里逐张手动拖拽尚可应付,一旦换成程序批量处理就没有可用的入口。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端直接完成这些操作,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。

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

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


压缩 Excel 中的图片

Compress() 按指定的质量比例重新编码图片,传入 50 即把图片质量降到原来的 50%。质量越低,图片所占的数据量越小,工作簿也就越轻。压缩只作用于图片本身的数据,图片在工作表中的位置和显示尺寸都保持不变,因此适合在不改动版面的前提下给文件瘦身。具体操作步骤如下:

  1. 加载工作簿。
  2. 遍历所有工作表中的所有图片。
  3. 通过 Compress 将每张图片压缩到 50% 的质量。
  4. 保存工作簿。

下面是一个完整的代码示例,展示了在 React 中压缩 Excel 中的图片:

function App() {
  const compressPictures = 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 = 'ResizeAndMovePictures.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/static/data/`);

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

    // 遍历所有工作表中的所有图片
    for (const sheet of workbook.Worksheets) {
      for (const picture of sheet.Pictures) {
        // 将图片质量压缩到 50%
        picture.Compress(50);
      }
    }

    // 保存工作簿
    const outputFileName = "CompressPictures.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>压缩 Excel 中的图片</h1>
      <button onClick={compressPictures}>Start</button>
    </div>
  );
}

export default App;

运行后,压缩 Excel 中图片的效果:

压缩 Excel 中的图片


调整 Excel 中图片的大小

图片在工作表中的显示尺寸由 Width 和 Height 两个属性决定,单位是像素。给这两个属性重新赋值即可把图片改到指定大小——缩小的图不再压住旁边的数据区,放大的图也能填满预留的图片位。具体操作步骤如下:

  1. 加载工作簿,获取第一个工作表。
  2. 通过 sheet.Pictures.get(0) 获取工作表中的第一张图片。
  3. 设置 Width 和 Height 调整图片大小。
  4. 保存工作簿。

下面是一个完整的代码示例,展示了在 React 中调整 Excel 中图片的大小:

function App() {
  const resizePicture = 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 = 'ResizeAndMovePictures.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/static/data/`);

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

    // 获取第一个工作表
    const sheet = workbook.Worksheets.get(0);

    // 获取工作表中的第一张图片
    const picture = sheet.Pictures.get(0);

    // 调整图片大小
    picture.Width = 140;
    picture.Height = 140;

    // 保存工作簿
    const outputFileName = "ResizePicture.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>调整 Excel 中图片的大小</h1>
      <button onClick={resizePicture}>Start</button>
    </div>
  );
}

export default App;

Width 与 Height 分别对应图片的宽度和高度,两者独立生效,赋值后图片立即按新尺寸显示。需要注意的是,这里设置的是图片框的尺寸而非等比缩放——宽高与原图比例不一致时图片会被拉伸,处理办法见下文常见问题。

运行后,调整 Excel 中图片大小的效果:

调整 Excel 中图片的大小


移动 Excel 中图片的位置

图片在 Excel 中是被锚定在某个单元格上的浮动对象,仅设置尺寸并不能改变它出现的位置。Left 和 Top 两个属性以像素为单位,指定图片左上角相对于工作表左上角的距离,赋值后图片便会移动到新的坐标处,用于把图片挪出数据区、或统一对齐到同一列图片位。具体操作步骤如下:

  1. 加载工作簿,获取第一个工作表。
  2. 通过 sheet.Pictures.get(0) 获取工作表中的第一张图片。
  3. 设置 Left 和 Top 移动图片位置。
  4. 保存工作簿。

下面是一个完整的代码示例,展示了在 React 中移动 Excel 中图片的位置:

function App() {
  const movePicture = 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 = 'ResizeAndMovePictures.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/static/data/`);

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

    // 获取第一个工作表
    const sheet = workbook.Worksheets.get(0);

    // 获取工作表中的第一张图片
    const picture = sheet.Pictures.get(0);

    // 调整图片位置
    picture.Left = 360;
    picture.Top = 180;

    // 保存工作簿
    const outputFileName = "MovePicture.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>移动 Excel 中图片的位置</h1>
      <button onClick={movePicture}>Start</button>
    </div>
  );
}

export default App;

Left 与 Top 描述的是图片左上角在工作表中的绝对坐标,与图片原先锚定在哪一行、哪一列无关。

运行后,移动 Excel 中图片位置的效果:

移动 Excel 中图片的位置


常见问题

调整图片大小时图片被拉变形了?

原因:Width 和 Height 是两个彼此独立的属性,分别赋值时不会保留图片原有的宽高比。若把一张长方形图片的宽和高都设成同一个值,图片就会被强制拉成正方形。

解决:先读取图片当前的宽高算出比例,再按比例换算另一个方向的值:

// 读取图片当前的宽高,算出宽高比
const picture = sheet.Pictures.get(0);
const ratio = picture.Height / picture.Width;

// 固定宽度,按比例算出高度,图片不会被拉变形
picture.Width = 140;
picture.Height = Math.round(140 * ratio);

IsLockAspectRatio 能保证图片不变形吗?

原因:不能。这个属性默认就是 true,它设置的是图片的锁定标记,约束的是在 Excel 里手动拖拽时的行为;用代码给 Width / Height 赋值时,它不会替你换算另一条边。实测把 Width 改成 140,无论 IsLockAspectRatio 是 true 还是 false,Height 都停在原来的 300。

解决:等比缩放仍然要自己按比例算。这个属性可以读写,改完能随文件保存下来,需要与文件里的锁定状态对齐时再设置:

// 锁定标记:默认 true,改为 false 会写进文件,重新打开后读回仍是 false
picture.IsLockAspectRatio = false;

// 但它不参与宽高换算:只改 Width,Height 会停在原值
picture.Width = 140;

// 要等比缩放,还是先算比例再赋值
const ratio = picture.Height / picture.Width;
picture.Width = 140;
picture.Height = Math.round(140 * ratio);

获取免费许可证

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

数据写完之后,表格往往还差最后一步才能用:产品名称被窄列挤得只剩几个字,备注里的一整段话铺在一行上、末尾被右侧有内容的单元格截掉。手动拖列宽、拉行高不仅慢,列一多还容易漏掉几列。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端直接完成这些操作,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。

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

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


自动调整单行的行高和单列的列宽

行高由 AutoFitRow 按指定行中的内容自动算出,列宽由 AutoFitColumn 按指定列中的内容自动算出。两者都只作用于目标的那一行、那一列,表格其余部分保持原样,适合只修某一处最碍眼的溢出。具体操作步骤如下:

  1. 加载工作簿,获取第一个工作表。
  2. 通过 AutoFitRow 自动调整该行的行高。
  3. 通过 AutoFitColumn 自动调整该列的列宽。
  4. 保存工作簿。

下面是一个完整的代码示例,展示了在 React 中自动调整单行的行高和单列的列宽:

function App() {
  const autoFitSingleRowColumn = 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 = 'AutoFitRowsAndColumns.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/static/data/`);

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

    // 获取第一个工作表
    const sheet = workbook.Worksheets.get(0);

    // 自动调整第 2 行的行高
    sheet.AutoFitRow(2);

    // 自动调整第 4 列的列宽
    sheet.AutoFitColumn(4);

    // 保存工作簿
    const outputFileName = "AutoFitSingleRowColumn.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={autoFitSingleRowColumn}>Start</button>
    </div>
  );
}

export default App;

运行后,自动调整单行行高和单列列宽的效果:

自动调整单行行高和单列列宽


自动调整多行的行高和多列的列宽

整张表格都要收拾时,逐行逐列调用显然不现实。对一段单元格区域调用 AutoFitRows 或 AutoFitColumns,区域覆盖的每一行、每一列都会各自按内容重新计算尺寸,一次调用就能把整块数据排整齐,适合导出报表前的收尾。具体操作步骤如下:

  1. 加载工作簿,获取第一个工作表。
  2. 通过 AutoFitRows 自动调整范围内所有行的行高。
  3. 通过 AutoFitColumns 自动调整范围内所有列的列宽。
  4. 保存工作簿。

下面是一个完整的代码示例,展示了在 React 中自动调整多行的行高和多列的列宽:

function App() {
  const autoFitMultipleRowsColumns = 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 = 'AutoFitRowsAndColumns.xlsx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/static/data/`);

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

    // 获取第一个工作表
    const sheet = workbook.Worksheets.get(0);

    // 获取工作表中已使用的单元格范围
    const range = sheet.AllocatedRange;

    // 自动调整范围内所有行的行高
    range.AutoFitRows();

    // 自动调整范围内所有列的列宽
    range.AutoFitColumns();

    // 保存工作簿
    const outputFileName = "AutoFitMultipleRowsColumns.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={autoFitMultipleRowsColumns}>Start</button>
    </div>
  );
}

export default App;

运行后,自动调整多行行高和多列列宽的效果:

自动调整多行行高和多列列宽


常见问题

调用了 AutoFitRow(),行高却一点没变?

原因:行高自适应只对需要折行的内容起作用。单元格没开自动换行时,文字始终排在一行内,行高跟着字号走,自适应算出来的结果和原行高一致,看上去就像没生效。

解决:先把 WrapText 设为 true,再调用行高自适应:

// 没开自动换行,行高自适应不会带来变化
sheet.Range.get("D2").Style.WrapText = false;
sheet.AutoFitRow(2);

// 开启自动换行后,行高会按折行后的行数重新计算
sheet.Range.get("D2").Style.WrapText = true;
sheet.AutoFitRow(2);

AutoFitColumns() 对合并单元格不起作用?

原因:列宽自适应按区域内单个单元格的内容计算,合并单元格只有左上角那一格真正存放文字,区域内其余位置都是空的,算出来的宽度自然只够放下左上角的内容。

解决:合并单元格的列宽改用 ColumnWidth 手动指定:

// A7:D7 是合并单元格,自适应算不出合并后的总宽度
sheet.Range.get("A7:D7").Merge();
sheet.Range.get("A7:D7").AutoFitColumns();

// 改为手动指定列宽,让合并后的文字完整显示
sheet.Range.get("A7").ColumnWidth = 40;

获取免费许可证

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

扫描件方向不对、横版表格夹在一堆竖版页面里、打印前想把整份文档统一转个方向,这几种情况都不必重新排版。旋转角度是 PDF 页面自身的一个属性,改它只影响页面的显示方向,页面上的内容原样保留。

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

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

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


旋转新页面

整节页面要统一方向,在节上设一次就够:section.PageSettings.Rotate 取 PdfPageRotateAngle.RotateAngle90,该节所有页面顺时针转 90 度,RotateAngle180、RotateAngle270 可选,不设则不旋转。

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

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

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

    // 创建空白文档
    const doc = new pdfModule.PdfDocument();

    // 新建一个节,整节页面共用顺时针 90 度的旋转角度
    const section = doc.Sections.Add();
    section.PageSettings.Size = pdfModule.PdfPageSize.A4();
    section.PageSettings.Rotate = pdfModule.PdfPageRotateAngle.RotateAngle90;

    // 在该节中添加页面
    const page = section.Pages.Add();

    // 在页面上写一段说明文字
    const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL UNICODE MS.TTF', size: 14 });
    page.Canvas.DrawString({
      s: '本页在创建时即设置为旋转 90 度',
      font: font,
      brush: pdfModule.PdfBrushes.get_Black(),
      x: 40,
      y: 60,
      format: new pdfModule.PdfStringFormat({ alignment: pdfModule.PdfTextAlignment.Left })
    });

    // 保存并从 VFS 读回,触发下载
    const outputFileName = '旋转新文档.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    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={createRotatedPdf}>
        开始创建
      </button>
    </div>
  );
}

export default App;

若只是想在建页时顺带指定角度,doc.Pages.Add(size, margins, rotation) 这个重载同样可用。

创建时即设为旋转 90 度的 A4 页面:

创建时即设为旋转 90 度的 A4 页面


旋转已有页面

已有文档要改方向,角度存在页面自己的 Rotation 属性里,先读出当前值,再累加本次的旋转量。要注意写进去的是 PdfPageRotateAngle 的枚举数值(不旋转为 0,90 度对应 1),而不是角度本身。

function App() {
  const rotateExistingPage = 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 页,读出当前角度并在其基础上累加本次的旋转量
    const page = doc.Pages.get_Item(0);
    let rotation = page.Rotation.value + pdfModule.PdfPageRotateAngle.RotateAngle90.value;

    // 枚举数值只有 0~3,加到 4 表示转满一圈,回到不旋转
    if (rotation === 4) {
      rotation = 0;
    }
    page.Rotation = rotation;

    // 保存并从 VFS 读回,触发下载
    const outputFileName = '旋转指定页.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    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={rotateExistingPage}>
        开始旋转
      </button>
    </div>
  );
}

export default App;

只有第 1 页旋转 90 度,其余页面方向不变:

只有第 1 页旋转 90 度,其余页面方向不变


常见问题

给 Rotation 赋值时提示 "Value is not an integer"

原因:page.Rotation 的写入端只接受整数。直接传 PdfPageRotateAngle 的枚举对象会抛 Assert failed: Value is not an integer: PdfPageRotateAngle.RotateAngle90 (object)。

解决:取枚举的 value 再赋值:

page.Rotation = pdfModule.PdfPageRotateAngle.RotateAngle90.value;

为什么填 90 之后页面转成了 180 度

原因:PdfPageRotateAngle 的枚举数值是 0、1、2、3,分别对应 0、90、180、270 度,写入端要的是这个数值,不是角度。写成 page.Rotation = 90 不会报错,但得到的不是 90 度;用 PdfPageRotateAngle.fromValue(90) 反查会直接抛 Invalid value for spirepdfPdfPageRotateAngle。

解决:换算成枚举数值再写入:

// 90 度
page.Rotation = pdfModule.PdfPageRotateAngle.RotateAngle90.value;

// 180 度
page.Rotation = pdfModule.PdfPageRotateAngle.RotateAngle180.value;

在 section.Pages.Add(...) 上带旋转参数为什么没效果

原因:带旋转参数的重载 Add(size, margins, rotation) 只在 doc.Pages 上生效。写到 section.Pages 上时这个参数会被忽略,页面方向改由所在节的 PageSettings.Rotate 决定,结果是文件里的 /Rotate 仍是 0,页面没有旋转,也不会有任何报错。

解决:两种写法二选一,别把参数传给 section.Pages:

// 写法一:节级设置,作用于该节全部页面
section.PageSettings.Rotate = pdfModule.PdfPageRotateAngle.RotateAngle90;
section.Pages.Add();

// 写法二:在 doc.Pages 上建页并传入角度
const page = doc.Pages.Add(
  pdfModule.PdfPageSize.A4(),
  new pdfModule.PdfMargins(),
  pdfModule.PdfPageRotateAngle.RotateAngle90
);

页面建好之后仍可单独调整,page.Rotation 会覆盖节级设置:

// 只把第 2 页改成 180 度,其余页面不受影响
doc.Pages.get_Item(1).Rotation = pdfModule.PdfPageRotateAngle.RotateAngle180.value;

获取免费许可证

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