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

Spire.Cloud 纯前端文档控件

Spire.Presentation 11.9.1 现已正式发布。该版本新增了 Paragraphs.AppendOfficeMathML() 方法,用于添加 Office 公式,并修复了 PPT 转 PDF 时文字模糊不清的问题。详情如下。

新功能:

问题修复:


获取 Spire.Presentation 11.9.1 请点击:

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

给 PDF 加批注、按区域抽取数据、给图片套一圈边框,都要先知道目标元素落在页面的什么位置。PDF 内部并没有一张现成的坐标表:文字是一段段绘制指令,图片是页面资源里的对象,位置信息分散在各自的矩阵和矩形里。过去要么自己写解析器把这些数值拆出来,要么把文件传回服务端处理。

Spire.PDF for JavaScript 基于 WebAssembly 在浏览器端加载并解析 PDF 文档,通过虚拟文件系统(VFS)读写文件,不需要后端参与。

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

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


坐标体系

使用 Spire.PDF 处理现有的 PDF 文档时,坐标的原点位于页面的左上角。X 轴从原点水平向右延伸,Y 轴从原点垂直向下延伸(如下图所示)。数值的单位是磅(1 磅 = 1/72 英寸),下面两个功能点里 Positions 与 Bounds 给出的都是这套口径下的数值。

Spire.PDF 的坐标体系


获取指定文本的坐标

Spire.PDF for JavaScript 提供 PdfTextFinder,用于按内容查找文本在页面上的位置,每一处匹配都会给出落点坐标。查找以页为单位,多页文档需要逐页处理。

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

    // 取第 1 页
    let page = doc.Pages.get_Item(0);

    // 创建文本查找器,忽略大小写地搜索指定文本
    let finder = new pdfModule.PdfTextFinder(page);
    finder.Options.Parameter = pdfModule.TextFindParameter.IgnoreCase;
    let results = finder.Find('玫瑰');

    // 汇总每处匹配的坐标
    let report = '';
    for (let i = 0; i < results.length; i++) {
      let find = results.get(i);
      let position = find.Positions[0];

      report += '第 ' + (i + 1) + ' 处匹配:' + find.Text + '\n';
      report += '  坐标:X = ' + position.X + ',Y = ' + position.Y + '\n';
    }

    // 将报告写入 VFS
    const outputFileName = '文本坐标.txt';
    window.dotnetRuntime.Module.FS.writeFile(outputFileName, new TextEncoder().encode(report));
    doc.Close();

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

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>获取文本坐标</h1>
      <button onClick={getTextCoordinates}>
        开始获取
      </button>
    </div>
  );
}

export default App;

两处匹配各自的坐标

两处匹配各自的坐标


获取页面中图片的坐标

Spire.PDF for JavaScript 还提供 PdfImageHelper,用于读取页面中每张图片的位置。图片在页面资源里本来就有登记,取到的 Bounds 直接给出左上角坐标。同样按页取。

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

    // 取第 1 页
    let page = doc.Pages.get_Item(0);

    // 创建图片助手,取出该页的图片信息
    let helper = new pdfModule.PdfImageHelper();
    let images = helper.GetImagesInfo(page);

    // 汇总每张图片的坐标
    let report = '';
    for (let i = 0; i < images.length; i++) {
      let bounds = images[i].Bounds;

      report += '第 ' + (i + 1) + ' 张图片:' + '\n';
      report += '  坐标:X = ' + bounds.X + ',Y = ' + bounds.Y + '\n';
    }

    // 将报告写入 VFS
    const outputFileName = '图片坐标.txt';
    window.dotnetRuntime.Module.FS.writeFile(outputFileName, new TextEncoder().encode(report));
    doc.Close();

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

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>获取图片坐标</h1>
      <button onClick={getImageCoordinates}>
        开始获取
      </button>
    </div>
  );
}

export default App;

页面上三张花卉图片的坐标

页面上三张花卉图片的坐标


常见问题

大小写不同就匹配不上

原因:Find() 的匹配行为由 Options.Parameter 决定,默认取值 TextFindParameter.None 按子串查找并区分大小写,Rose 与 rose 会被当成两回事。

解决:换成需要的取值即可,TextFindParameter 是标志枚举,能用按位或组合:

// 忽略大小写
finder.Options.Parameter = pdfModule.TextFindParameter.IgnoreCase;

// 只匹配整词,同时忽略大小写
finder.Options.Parameter = pdfModule.TextFindParameter.WholeWord | pdfModule.TextFindParameter.IgnoreCase;

// 用正则表达式查找:一次匹配 Rosa 或 Tulipa
finder.Options.Parameter = pdfModule.TextFindParameter.Regex;
let results = finder.Find('Rosa|Tulipa');

坐标数值和 PDF 阅读器里显示的对不上

原因:Positions 与 Bounds 用的是上面那套页面坐标系;而 PDF 文件内部(/MediaBox、内容流)用的是左下角原点、Y 轴向上,两套口径差着一个页面高度,直接对照就会差出整页的数值。

解决:统一按左上角原点处理,换算成像素时乘以 dpi / 72——96 dpi 下系数是 1.333。取到的数值本身就是浮点数,需要精确比对时保留两位小数再比较。

图片信息里没有页面上看到的那些图形

原因:GetImagesInfo 返回的是页面资源中的位图对象。用矢量指令画出来的线条、表格框和色块不属于图片;反过来,整页扫描件只有一张铺满页面的图片,图里的文字也搜不到。

解决:先用 Bounds 的 X、Y 确认每张图片实际落在页面的哪个位置。页面上用矢量指令画的线条与色块,GetImagesInfo 不会返回,扫描件里的文字也搜不到——这两类内容要定位,得走文本提取(PdfTextExtractor)或另行接入 OCR。


获取免费许可证

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

在企业培训、在线课程与学术分享场景中,把一份教案做成"图文并茂、带语音讲解"的讲解视频,通常需要动用演示文稿、图片处理、音频合成、视频编码等多个工具链。使用 Spire.Agent.Office 的 AI 能力,您只需上传 Word、PDF、Excel、Markdown 等格式教案文档,通过简单的自然指令即可获得一段完整的讲解视频。

对比传统SDK API处理

传统 Spire.Office for .NET API Spire.Agent.Office 处理
驱动方式 分别解析教案文档、逐页排版生成Presentation、使用api和ffmpeg 转Presentation 到 MP4 自然语言描述目标,AI 理解自动编排执行
代码量 需要大量代码处理,并且不能添加语音 简单自然语言指令
多格式解析 每种格式(Word/PDF/Excel/Markdown)需单独编写解析逻辑 AI 自动识别文档格式并读取内容
需求变更 调整主题、页数、语种或讲解风格 → 改代码 → 编译 → 重新部署 修改指令,即刻生效

从教案文档到讲解视频:

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


第一步:从教案文档到Presentation

使用 Spire.Agent.Office 自动提炼核心要点生成排版精美的Presentation。


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

// 加载 Word、PDF、Excel、Markdown 4种教案文档
List<string> inputPaths = new List<string>
{
    @"AI应用类教案.docx",
    @"学术论文类.pdf",
    @"金融分析类.xlsx",
    @"医疗政策类.md"
};

string outputDirectory = @"E:\output\";
string key = "**************************";
// 提炼教案核心要点生成教学Presentation。
string instruction =
    "将附带的教程文档视为处理目标:提炼人工智能应用教程的核心要点,涵盖基础知识、典型应用领域、实施步骤、真实案例和前景。主题:使用冷静的专业灰蓝色作为主题色(干净、极简、现代AI技术感)。1.确保高质量的布局和排版。包括适当的说明性图表/图3。保持干净、简约的风格。生成12张幻灯片;";

// 依次根据Word、PDF、Excel、Markdown 4种教案文档生成Presentation文档
foreach (string inputPath in inputPaths)
{
    if (!System.IO.File.Exists(inputPath)) continue;

    string savePath = System.IO.Path.Combine(outputDirectory,
        $"{System.IO.Path.GetFileNameWithoutExtension(inputPath)}.pptx");

    GeneratePPT(inputPath, instruction, savePath, key);
}

// AI 生成PPT处理
static PPTGenerationResult GeneratePPT(string input, string instruction, string savePath, string key)
{
    //AIOptions选项配置
    AIOptions options = new AIOptions();
    //配置SpireToken
    options.SpireToken = key;
    //配置超时时间
    options.TimeoutMs = 1000000;
    //如果处理文档内容较长,可自定义配置token 熔断,默认是300w 熔断
    options.TokenCircuitBreakerLimit = 5000000;
    using (Presentation ppt = new Presentation())
    {
        AIDocumentProcessor processor = ppt.AI(options);
        return processor.GeneratePresentation(input, instruction, savePath);
    }
}

第二步:从Presentation到视频

基于第一步Presentation文档,用一条"配上语音讲解"的指令,自动生成带语音旁白的 MP4 讲解视频。

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

// 定义上一步生成的4个PPT源文件路径
List<string> inputPaths = new List<string>
{
    @"AI应用类教案.pptx",
    @"学术论文类.pptx",
    @"金融分析类.pptx",
    @"医疗政策类.pptx"
};

string outputDirectory = @"E:\output\";
string key = "**************************";
string instruction = "生成视频同时配上语音讲解";

// 循环处理每个PPT生成视频
foreach (string inputPath in inputPaths)
{
    if (!File.Exists(inputPath)) continue;
    
    string savePath = Path.Combine(outputDirectory, 
        $"{Path.GetFileNameWithoutExtension(inputPath)}.mp4");
    
    ExecuteDemoPPT(instruction, inputPath, savePath, key, null);
}

// 执行PPT文档AI处理(生成带语音讲解的视频)
static AIResult ExecuteDemoPPT(string instruction, string inputPath, string savePath, string key, string[] attachmentPaths)
{
    //AIOptions选项配置
    AIOptions options = new AIOptions();
    //配置SpireToken
    options.SpireToken = key;
    //配置超时时间
    options.TimeoutMs = 1000000;
    //配置token 熔断
    options.TokenCircuitBreakerLimit = 5000000;
    using (Presentation ppt = new Presentation())
    {
        if (!string.IsNullOrEmpty(inputPath) && File.Exists(inputPath))
        {
            ppt.LoadFromFile(inputPath);
        }
        AIDocumentProcessor processor = ppt.AI(options);
        return processor.ExecuteInstruction(ppt, instruction, savePath, attachmentPaths);
    }
}

Word格式文档---->Presentation文档------>.mp4语音视频

生成的教学 PPT Pdf格式文档---->Presentation文档------>.mp4语音视频

生成的教学 PPT Excel格式文档---->Presentation文档------>.mp4语音视频

生成的教学 PPT markdown 格式文档---->Presentation文档------>.mp4语音视频

生成的教学 PPT


常见问题

生成的 PPT 页数与教案内容不匹配

原因:教案内容较多,AI 在提炼要点与分页时的取舍与预期不同。

解决:在指令中明确页数、如"生成9页、每页一个教学环节"。

结果生成失败

原因:源文档数据较多,分析处理消耗token大,超过默认token熔断限制。

解决:使用TokenCircuitBreakerLimit自定配置,如下:

AIOptions options = new AIOptions();
options.TokenCircuitBreakerLimit = 5000000;

多格式教案(Word/PDF/Excel/Markdown)都能转吗

原因:不同格式解析方式不同,用户不确定是否可用。

解决:Spire.Agent.Office 可自动识别附件文档格式并读取内容;直接传入对应格式文件并在指令中说明来源即可。


获取SpireToken Key

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

在代码中配置:

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

在金融分析与投资研究领域,上市公司财报解读是最基础也最耗时的工作之一。一份完整的年度财报篇幅较长,不仅包含资产负债表、利润表、现金流量表、股东权益变动表等核心报表,还涵盖附注中大量明细数据。分析师需从中提取关键数据并生成可视化图表,方能形成投资判断。传统做法通常需要人工翻阅 PDF、手动录入 Excel、手工绘制图表并撰写分析结论,整套流程耗时 4-8 小时,且极易因数据录入错误导致结论偏差。

但解析只是第一步。在真实的投研与财务建模流程里,数据最终要落到分析师自己的估值模型里。而估值模型恰恰是整个工作流中最"娇贵"的部分:它通常由分析师经年累月打磨而成,内含大量自定义宏(VBA)、数据透视表以及层层嵌套的复杂公式。传统做法只能靠人工把数据一格一格敲进去——之所以不敢用脚本批量写入,是因为绝大多数第三方 Excel 库在读写文件时会重建工作簿结构,结果是宏指令丢失、数据透视表失效、嵌套公式被改成静态值,一个精心维护的模型就此报废。

本文用两个前后衔接的案例,借助 Spire.Agent.Office 的 Excel AI 能力,串起从「PDF 财报」到「估值结论」的完整链路:

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


对比传统SDK API处理

传统 Spire.Office for .NET API Spire.Agent.Office 处理
驱动方式 编写 PDF 解析 + 单元格写入 + 图表创建 + 公式计算代码 用自然语言描述目标,AI 理解后自动编排执行路径
代码量 两个案例合计通常需要 800-1500 行 C# 代码(含 PDF 表格定位、行列解析、图表配置等) 约 20 行调用代码 + 两条自然语言指令
表格识别 需手动定位 PDF 中表格位置,处理跨页表格拆分、合并单元格等逻辑 AI 自动识别文档中的表格结构,理解表头和层级关系
图表生成 需手动创建 Chart 对象、配置数据区域、设定图表类型和样式 AI 根据数据语义自动选择最合适的图表类型
数据注入 需硬编码"源表第 N 行 → 模板第 M 行"的映射表,报表口径一变就要改代码 AI 按科目名称语义匹配,源报表行列顺序变化不影响结果
宏与透视表 需自行处理 VBA 工程与透视缓存的保留逻辑,稍有不慎即损坏 原样保留宏指令、数据透视表与图表,AI 只写入目标单元格
需求变更 新增分析维度需改代码 → 编译 → 部署 修改指令中的描述,即刻生效

案例一:长文本财报数据提取与分析

AI 读取 PDF 财报文件,自动识别其中的财务报表表格并提取至 Excel 文件中,同时自动生成可视化图表与财务分析。整个过程仅需一份代码、一条指令。

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

// 需要处理的 PDF 财报文件(作为附件传入)
string[] attachmentPaths = new string[] { @"C:\FinancialReport\上市公司2025年度报告.pdf" };
// 结果 Excel 文档的保存路径
string savePath = @"C:\FinancialReport\分析结果.xlsx";
// SpireToken Key(需在官网页面申请)
string key = "sk-***************************";

// 自然语言指令
string instruction =
    "请按以下步骤处理附件文件: " +
    "1. 提取财务报告板块中的所有表格,每张表格独立为一个工作表,工作表名与原始表格名保持一致。 " +
    "2. 严格保留原始数据,不添加、不修改任何数值。 " +
    "3. 应用适当格式提升表格可读性。 " +
    "4. 将跨页表格合并到同一张工作表中。 " +
    "5. 为每张表格的数据生成合适的图表。 " +
    "6. 直接使用提取的原始数据制图,不添加、不修改任何数值。 " +
    "7. 基于提供的数据,分析并总结公司的财务状况与变化趋势。";

// AI 生成
AIResult result = AnalyzeFinancialReport(instruction, savePath, key, attachmentPaths);

// AI 辅助的财报分析处理
static AIResult AnalyzeFinancialReport(string instruction, string savePath, string key, string[] attachmentPaths)
{
    // 配置 AI 处理选项
    AIOptions options = new AIOptions();
    options.SpireToken = key;
    options.TimeoutMs = 10000000;

    using (Workbook wb = new Workbook())
    {
        AIDocumentProcessor processor = wb.AI(options);
        return processor.ExecuteInstruction(wb, instruction, savePath, attachmentPaths);
    }
}

财报数据处理与图表分析结果:

输入文件:PDF 财报 说明:原输入文件

输出结果包含两部分:

提取的财报数据与图表1 提取的财报数据与图表2 说明:从 PDF 财报中提取的表格数据,并生成对应的可视化图表。

财报分析结果 说明:基于提取的数据生成的财务分析结论。


案例二:财报数据注入预设估值模型

案例一产出的是"数据",案例二要做的是"建模"。整个过程的输入有两份:预设的估值模型模板(.xlsm,内含宏、数据透视表与嵌套公式)与案例一产出的财报数据工作簿(.xlsx)。AI 读取财报数据,按科目名称对应关系注入模板的「数据录入」工作表,模型内的公式随即重算,估值曲线自动更新。

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

// 预设的估值模型模板(含宏 / 数据透视表 / 嵌套公式 / 估值曲线图)
string inputPath = @"C:\FinancialReport\估值模型模板.xlsm";
// 案例一产出的财报数据工作簿(作为附件传入)
string[] attachmentPaths = new string[] { @"C:\FinancialReport\分析结果.xlsx" };
// 结果文档路径(此处为 null,将使用下面设置的输出文件夹路径)
string savePath = null;
// 输出目录
string OutDir = @"C:\FinancialReport\output";
// SpireToken Key(需在官网页面申请)
string key = "sk-***************************";

// 自然语言指令
string instruction =
    "读取附件中的财报数据,按科目名称对应填入当前估值模型模板的「数据录入」工作表,本期与上年同期分别填入对应两列;" +
    "只填写浅黄色底纹的单元格,不要改动已有公式;" +
    "保留模板中已有的宏、数据透视表、公式与图表;" +
    "填入后重算模型、刷新数据透视表,并更新「估值曲线」;" +
    "最终保存为启用宏的 Excel 文件";

// AI 生成
AIResult result = InjectDataIntoModel(instruction, inputPath, savePath, key, OutDir, attachmentPaths);

// AI 辅助的估值模型数据注入处理
static AIResult InjectDataIntoModel(string instruction, string inputPath, string savePath,
                                    string key, string output, string[] attachmentPaths)
{
    // 配置 AI 处理选项
    AIOptions options = new AIOptions();
    options.WorkDir = output;      // 设置工作目录为输出目录
    options.SpireToken = key;      // 设置 SpireToken Key

    using (Workbook workbook = new Workbook())
    {
        // 从文件加载估值模型模板
        if (!string.IsNullOrEmpty(inputPath) && File.Exists(inputPath))
        {
            workbook.LoadFromFile(inputPath);
        }

        // 创建 AI 文档处理器
        AIDocumentProcessor processor = workbook.AI(options);

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

注入前的估值模型模板:

预设的估值模型模板 说明:预设的估值模型模板,浅黄色单元格为待注入的数据区。

案例一产出的财报数据工作簿,作为本次注入的数据来源填充到估值模板的数据录入区:

填充后的数据 说明:填充合并资产负债表、合并利润表、合并现金流量表等其中的数据。

注入后自动重算得到的估值曲线:

注入后自动重算的估值曲线 说明:数据注入完成后,模型自动重算并刷新的估值曲线。


常见问题

提取的表格数据与 PDF 不一致

原因:PDF 中的表格可能包含跨页拆分、合并单元格、旋转文本等复杂布局,AI 在识别时可能出现偏差。

解决:在指令中添加"仔细核对数据准确性,特别注意合并单元格和跨页表格的拼接";或限定具体的页码范围分批次提取,审核后再整合。

注入数据后估值曲线不更新

原因:模型重算与数据透视表刷新是两步独立动作。仅写入数据不会自动刷新透视缓存;部分阅读器也不会主动重算整条公式链。

解决:在指令中显式要求"填入后重算模型、刷新数据透视表"。此外,模板中可设置打开时强制重算,确保曲线始终为最新值。


获取SpireToken Key

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

在代码中配置:

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

同一份 PDF 交给不同的人打开,看到的界面可能完全不同:有的窗口一打开就居中,有的把工具栏和菜单栏顶在最上面,有的默认一次只显示一页,有的一屏并排两页。文档是给别人看的说明书、宣传册或报告时,制作者往往希望这些默认表现可控——打开即居中、界面尽量干净、直接进入双栏视图。这类行为由文档自身的查看器首选项(Viewer Preferences)描述,写在 PDF 的目录字典里,跟着文件一起分发,不需要阅读器做任何配置。

Spire.PDF for JavaScript 基于 WebAssembly 在浏览器端加载、修改与保存 PDF 文档,查看器首选项通过 PdfDocument.ViewerPreferences 读写,文件进出走虚拟文件系统(VFS),无需后端配合。

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

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


设置窗口与界面元素相关的首选项

ViewerPreferences 上的几个布尔开关决定阅读器打开文档时的外观,默认都是 false,也就是不干预阅读器自身的界面:

  • CenterWindow 让阅读器窗口居中显示
  • DisplayTitle 决定标题栏是否使用文档标题(文档没写标题元数据时,阅读器回退显示文件名)
  • FitWindow 决定窗口是否缩放到首页大小
  • HideMenubar 隐藏菜单栏
  • HideToolbar 隐藏工具栏
  • HideWindowUI 隐藏滚动条一类界面元素,只留页面内容
function App() {
  const setWindowPreferences = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

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

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

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

    // 让阅读器窗口居中
    doc.ViewerPreferences.CenterWindow = true;
    // 标题栏不使用文档标题(保持显示文件名)
    doc.ViewerPreferences.DisplayTitle = false;
    // 不把窗口缩放到首页大小
    doc.ViewerPreferences.FitWindow = false;
    // 隐藏菜单栏
    doc.ViewerPreferences.HideMenubar = true;
    // 隐藏工具栏
    doc.ViewerPreferences.HideToolbar = true;
    // 隐藏滚动条等界面元素,只留页面内容
    doc.ViewerPreferences.HideWindowUI = 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={setWindowPreferences}>
        开始设置
      </button>
    </div>
  );
}

export default App;

窗口居中、工具栏与菜单栏已隐藏的 PDF 文档

窗口居中、工具栏与菜单栏已隐藏的 PDF 文档


设置页面布局与打开时的显示模式

页面怎么排列、打开时先看到什么,同样由 ViewerPreferences 上的枚举属性决定,两者的默认值分别是 SinglePage 与 UseNone:

  • PageLayout 控制页面的排列方式,SinglePage 一次只显示一页,TwoColumnLeft 是双栏并排、奇数页在左
  • PageMode 控制打开文档时先呈现什么,UseNone 直接显示页面内容,UseThumbs 会在侧边栏展开缩略图面板
function App() {
  const setPageLayout = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

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

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

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

    // 双栏并排显示,奇数页在左
    doc.ViewerPreferences.PageLayout = pdfModule.PdfPageLayout.TwoColumnLeft;
    // 打开文档时展开缩略图面板
    doc.ViewerPreferences.PageMode = pdfModule.PdfPageMode.UseThumbs;

    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={setPageLayout}>
        开始设置
      </button>
    </div>
  );
}

export default App;

打开时双栏并排、左侧展开缩略图面板的 PDF 文档

打开时双栏并排、左侧展开缩略图面板的 PDF 文档


常见问题

设置了首选项,打开文档却没看到变化

原因:按 PDF 规范,/ViewerPreferences 是给阅读器的偏好提示,不是强制要求。浏览器内置的 PDF 阅读器与一些轻量阅读器只实现其中一小部分,HideToolbar、HideMenubar、PageLayout 这类界面项常被直接忽略;Adobe Acrobat 若开启了“恢复上次查看设置”,也会用它记住的视图状态覆盖文档里的值。

解决:用 Adobe Acrobat Reader 打开验证,并在“首选项 → 文档”里确认“恢复上次查看设置”未勾选。排查时先排除阅读器本身的原因,再回头检查设置是否写进了文件——首选项要 SaveToFile 之后才落盘,保存前调用 Close() 会丢掉改动。

PageLayout 和 PageMode 有什么区别

原因:两个属性名字接近,又都影响文档打开时的样子,容易混。它们管的是两件事:PageLayout 决定页面怎么排列,PageMode 决定打开时先显示哪块面板。

解决:按需要各取一个值即可,两者互不影响:

// 页面排列:一次一页、单向连续、双栏、双页
doc.ViewerPreferences.PageLayout = pdfModule.PdfPageLayout.TwoColumnLeft;

// 打开时先显示的面板:纯页面、缩略图、书签大纲
doc.ViewerPreferences.PageMode = pdfModule.PdfPageMode.UseThumbs;

PdfPageLayout 可选 SinglePage、OneColumn、TwoColumnLeft、TwoColumnRight、TwoPageLeft、TwoPageRight;PdfPageMode 可选 UseNone、UseOutlines、UseThumbs、FullScreen、UseOC、UseAttachments。

隐藏工具栏能阻止用户打印或另存吗

原因:不能。HideToolbar、HideMenubar、HideWindowUI 只作用于阅读器界面的显示,与打印、复制、另存这些操作权限无关;换一个不理会该设置的阅读器打开,或者直接从菜单里操作,照样能打印和另存。

解决:要限制操作得设权限密码——用 PdfPasswordSecurityPolicy 配合 PdfDocumentPrivilege 关闭打印、复制等项,这类限制由文档的加密字典强制,阅读器会执行。查看器首选项管的是“看起来怎么样”,权限设置管的是“允许做什么”。


获取免费许可证

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

订单号、运单号、资产编号这类信息,一旦由人工录入就难免出错。把它们印成条形码贴在单据上,扫码枪一次读取即可完成录入,出错率也随之下降。要在 Web 应用里把条形码落到 PDF 上,过去通常先在服务端生成条码图片,再拼回文档,文件往返一趟,链路并不短。

Spire.PDF for JavaScript 基于 WebAssembly 在浏览器端直接创建、修改和保存 PDF 文档,条形码由 PdfCode128BBarcode、PdfCode39Barcode 这类一维条码对象直接绘制到页面上,借助虚拟文件系统(VFS)读写文件,不需要把文档上传到服务器。

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

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


支持的条形码类型

十个一维条码类都从 PdfBarcode 派生,字符集与编码密度各不相同,选型可对照下表:

类 条码符号 支持的字符
PdfCodabarBarcode Codabar 数字与 - $ : / . +
PdfCode11Barcode Code 11 数字与 -
PdfCode32Barcode Code 32 数字,常见于药品编码
PdfCode39Barcode Code 39 数字、大写字母、空格与 - . $ / + %
PdfCode39ExtendedBarcode Code 39 Extended 完整 ASCII,由多字符组合表示
PdfCode93Barcode Code 93 与 Code 39 相同的字符集,密度更高
PdfCode93ExtendedBarcode Code 93 Extended 完整 ASCII
PdfCode128ABarcode Code 128A 数字、大写字母、符号与控制字符
PdfCode128BBarcode Code 128B 数字、大小写字母与符号
PdfCode128CBarcode Code 128C 成对出现的数字,密度最高
function App() {
  const drawBarcodeTypes = 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.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

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

    // 类型名的字体、颜色与对齐方式,绿色与黑色条码区分开
    const labelFont = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL.TTF', size: 12 });
    const labelBrush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Green() }) });
    const labelFormat = new pdfModule.PdfStringFormat({ alignment: pdfModule.PdfTextAlignment.Left });

    // 待编码的数据,以及各自使用的条码类与标注在条码上方的类型名
    const items = [
      { Barcode: pdfModule.PdfCodabarBarcode, label: 'Codabar', text: '00:12-3456/7890' },
      { Barcode: pdfModule.PdfCode11Barcode, label: 'Code 11', text: '123-4567890' },
      { Barcode: pdfModule.PdfCode32Barcode, label: 'Code 32', text: '16273849' },
      { Barcode: pdfModule.PdfCode39Barcode, label: 'Code 39', text: 'ORDER-2026-0007' },
      { Barcode: pdfModule.PdfCode39ExtendedBarcode, label: 'Code 39 Extended', text: 'Order 2026-0007' },
      { Barcode: pdfModule.PdfCode93Barcode, label: 'Code 93', text: 'ORDER-2026-0007' },
      { Barcode: pdfModule.PdfCode93ExtendedBarcode, label: 'Code 93 Extended', text: 'Order 2026-0007' },
      { Barcode: pdfModule.PdfCode128ABarcode, label: 'Code 128A', text: 'INVOICE 2026' },
      { Barcode: pdfModule.PdfCode128BBarcode, label: 'Code 128B', text: 'Order-2026-0001' },
      { Barcode: pdfModule.PdfCode128CBarcode, label: 'Code 128C', text: '20260001' },
    ];

    // 从页面上方开始依次绘制
    let y = 20;
    for (const item of items) {
      // 在条码上方标出它所属的类型
      page.Canvas.DrawString({ s: `${item.label}:`, font: labelFont, brush: labelBrush, x: 20, y: y, format: labelFormat });

      // 用待编码的文本创建条形码对象
      const barcode = new item.Barcode({ text: item.text });

      // 可读文本显示在条形码下方
      barcode.TextDisplayLocation = pdfModule.TextLocation.Bottom;

      // 收窄条高,让十种条码排进同一页
      barcode.BarHeight = 25;

      // 绘制到类型名下方,该坐标是条形码左上角的位置
      barcode.Draw({ page: page, location: new pdfModule.PointF(20, y + labelFont.Size + 4) });

      // 用实际占用的下边界确定下一枚的位置
      y = barcode.Bounds.Bottom + 10;
    }

    // 保存文档
    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={drawBarcodeTypes}>
        开始绘制
      </button>
    </div>
  );
}

export default App;

十种条形码在同一页面依次绘制,类型名标在条码上方,可读文本排在条码下方

十种条形码在同一页面依次绘制,类型名标在条码上方,可读文本排在条码下方


自定义条形码的外观

默认外观是黑条白底、文字排在下方,需要时可以从颜色、条高、留白到文字字体逐项覆盖。条和文字分别由 BarColor 与 TextColor 着色,BackColor 铺在条码下面当底色;BarHeight、NarrowBarWidth 与 BarcodeToTextGapHeight 控制尺寸细节,QuietZone 是条码四周需要留出的空白。

function App() {
  const customizeBarcode = 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.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

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

    // 第一枚保持默认外观,用于对比
    const plain = new pdfModule.PdfCode39Barcode({ text: 'ORDER-2026-0007' });
    plain.TextDisplayLocation = pdfModule.TextLocation.Bottom;
    plain.Draw({ page: page, location: new pdfModule.PointF(20, 20) });

    // 第二枚逐项设置外观
    const barcode = new pdfModule.PdfCode39Barcode({ text: 'ORDER-2026-0007' });
    barcode.TextDisplayLocation = pdfModule.TextLocation.Bottom;

    // 条、文字与底色分别设置
    barcode.BarColor = new pdfModule.PdfRGBColor({ color: pdfModule.Color.FromArgb(0, 60, 160) });
    barcode.TextColor = new pdfModule.PdfRGBColor({ color: pdfModule.Color.FromArgb(200, 0, 0) });
    barcode.BackColor = new pdfModule.PdfRGBColor({ color: pdfModule.Color.FromArgb(255, 255, 0) });

    // 条高、窄条宽度、条到文字的间距
    barcode.BarHeight = 45;
    barcode.NarrowBarWidth = 1.6;
    barcode.BarcodeToTextGapHeight = 4;

    // 文字居中,四周各留 5 磅空白
    barcode.TextAlignment = pdfModule.PdfBarcodeTextAlignment.Center;
    barcode.QuietZone.Top = 5;
    barcode.QuietZone.Bottom = 5;
    barcode.QuietZone.Left = 5;
    barcode.QuietZone.Right = 5;

    // 文字改用 12 磅的 Arial
    barcode.Font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL.TTF', size: 12 });

    // 排在第一枚下方
    barcode.Draw({ page: page, location: new pdfModule.PointF(20, plain.Bounds.Bottom + 30) });

    // 保存文档
    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={customizeBarcode}>
        开始设置
      </button>
    </div>
  );
}

export default App;

下方条形码更换了条的颜色、高度、留白与文字字体

下方条形码更换了条的颜色、高度、留白与文字字体


常见问题

构造 PdfRGBColor 时提示 Ambiguous call

原因:PdfRGBColor 的构造函数只接受 Color 对象,直接按 RGB 三个分量传参(如 new pdfModule.PdfRGBColor(0, 60, 160) 或 { r: 0, g: 60, b: 160 })会命中多个重载而报错:Ambiguous call: arguments (object) match multiple overloads.。

解决:先用 Color.FromArgb 生成 Color,再交给 PdfRGBColor:

// 正确写法:先得到 Color 对象
barcode.BarColor = new pdfModule.PdfRGBColor({ color: pdfModule.Color.FromArgb(0, 60, 160) });

// 也可以直接取预设颜色
barcode.TextColor = new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Blue() });

同一段数据,条形码的长度却不一样

原因:不同符号的编码密度不同,同样的文本用 Code128 画出来比用 Code39 短,Code128C 则还要更短;此外 NarrowBarWidth 调大后整条条码会同步变宽。

解决:需要固定版面时,可以用接收 RectangleF 的 Draw 重载限定条码占用的区域,或统一调整 NarrowBarWidth:

// 无论数据多长,条形码都落在同一个矩形内
barcode.Draw({ page: page, rect: new pdfModule.RectangleF(20, 20, 250, 80) });

可以在 PDF 中添加二维码吗

原因:Spire.PDF for JavaScript 提供的一维条码类是 PdfCodabarBarcode、PdfCode39Barcode、PdfCode128BBarcode 等,没有二维码类,用 PdfQRCodeBarcode 这样的类名取到的是 undefined。

解决:二维码在前端生成图片,再通过 PdfImage.FromStream 与 page.Canvas.DrawImage 绘入页面,定位与缩放交给画布:

// qrBytes 是前端生成的二维码 PNG 字节
const image = pdfModule.PdfImage.FromStream(new pdfModule.Stream(qrBytes));
page.Canvas.DrawImage({ image: image, x: 20, y: 20, width: 100, height: 100 });

获取免费许可证

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

在法院、律所与合规部门中,电子卷宗归档是一项高频却繁琐的工作。一个案件的材料往往分散成几十份 PDF——起诉状、证据清单、庭审笔录、判决书等,来源不同、页数各异,归档时需要把这些零散材料整理成一份符合规范的卷宗;整理完成后,还要再填一份归档登记表,逐项写明卷内材料名称、形成日期与页数,供入库和日后检索使用。

传统做法是在多个工具之间来回切换:先逐份打开文件判断材料类型、按卷内常规顺序手工拖拽合并,再手工敲一份封面和目录,最后用图像工具分别加页码、盖水印、设密码;等到要出登记表时,又得把每份材料再打开一遍,手工抄录材料名称和页数。几十份材料下来,既耗时又容易抄错或漏项。

Spire.Agent.Office 的 PDF AI 能力可以直接用自然语言描述归档要求,AI 智能体理解后自动完成两类工作:把散材料整理成规范卷宗,以及把卷宗信息提取成归档登记表。

对比传统 SDK API 处理

传统 Spire.Office for .NET API Spire.Agent.Office 处理
驱动方式 编写循环遍历文件 + 编排卷内顺序 + 合并 + 盖章 + 加密的完整代码,控制每一步 用自然语言描述归档要求,AI 理解后自动编排执行路径
代码量 整卷归档通常需要 300-500 行 C# 代码(含文件枚举、顺序编排、页面合并、封面目录生成、页码绘制、水印生成、加密参数、登记表读取等) 约 10 行调用代码 + 1 条自然语言指令
需求变更 改动编排顺序/水印样式/登记表字段 → 改代码 → 编译 → 重新部署 修改指令,即刻生效

本文介绍如何使用 Spire.Agent.Office PDF AI 能力完成电子卷宗归档。全文通过两个案例展示"整理卷宗"与"登记卷宗"两种典型用法:

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


案例一:卷宗整卷合成归档

一个案件的材料种类相对固定,但份数和页数都不确定:起诉状、证据清单、庭审笔录、判决书等,每类材料可能有多份,页数也各不相同。归档时不仅要按卷内约定俗成的顺序把它们拼成一卷,还要为卷宗补上封面与目录、让整卷具备贯通全卷的连续页码和统一的案号标识,并在移交或保存时加密,避免材料外泄。手工完成这条链路需要在多个工具间反复切换,材料一多就极易出现顺序错乱、页码接不上或漏加密的情况。

以下示例使用 Spire.Agent.Office 智能体,通过自然语言指令读取附件中的全部 PDF 材料,按卷宗文件的一般顺序合并为一份卷宗,自动生成封面页与目录页,删除各材料原有的页码并统一添加连续页脚,再加盖案号水印、设置打开密码:

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

// 待整卷的 PDF 材料所在文件夹:将该文件夹下的全部 PDF 文件作为附件处理
string attachmentDir = @"E:\case_files";  // 同一案件的 PDF 材料所在文件夹
string[] attachmentPaths = Directory.GetFiles(attachmentDir, "*.pdf");

// PDF 处理相关配置
string inputPath = "";  // 输入文件为空,全部待归档材料均位于附件中
string savePath = null;  // 结果文档路径
string OutDir = @"E:\output";  // 输出目录(合并加密后的卷宗将保存到此目录)
string key = "**************************";  // SpireToken Key
string instruction =
     "读取附件中的全部 PDF 卷宗材料,完成整卷归档:" +
 "1. 按照卷宗文件的一般顺序将全部材料合并为一份卷宗 PDF,并根据内容生成封面页与目录页;" +
 "2. 删除原文件页码,并为合并后的卷宗统一添加连续页脚,页脚居中显示'第 X 页 / 共 Y 页',页码字号小于正文且不遮挡原有内容;" +
 "3. 在卷宗每一页添加案号水印'CASE-2026-0001',水印呈45度斜向、浅灰色,不影响正文阅读;" +
 "4. 为该卷宗 PDF 设置打开密码'2026@Case0001',该密码仅用于限制文档打开,不影响打印、复制等原有功能;" +
 "5. 添加目录页并保持各份材料原有的布局、字体和页面设置;" +
 "最终输出保存为PDF文件";

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

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

    // 使用PdfDocument对象处理PDF文档
    using (PdfDocument pdf = new PdfDocument())
    {
        // 输入文件为空时不加载文档,全部处理对象来自附件
        if (!string.IsNullOrEmpty(inputPath) && File.Exists(inputPath))
        {
            pdf.LoadFromFile(inputPath);
        }
        // 创建AI文档处理器
        AIDocumentProcessor processor = pdf.AI(options);

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

待整卷的 PDF 卷宗材料 待整卷的 PDF 卷宗材料 按顺序合并并添加页码水印后的加密卷宗 按顺序合并并添加页码水印后的加密卷宗


案例二:卷宗材料信息提取与归档登记表

卷宗整理完成后,还需要一份《归档登记表》随卷入库。登记表要逐项列明卷内每份材料的名称、形成日期和页数,并在表头写明案号、当事人与案由。这些信息其实都藏在材料本身里,但传统做法只能靠人工逐份打开、逐项抄录——材料一多,抄错页码或写错日期几乎难以避免。

与案例一不同,本案例不改动任何页面,只做信息读取:输入同样是附件中的 PDF 材料,产物则是一份新生成的登记表 PDF。

以下示例使用 Spire.Agent.Office 智能体,通过自然语言指令读取附件中的全部 PDF 材料,逐份提取案件基本信息与材料信息,按形成日期排序后汇总生成《归档登记表》:

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

// 待登记的 PDF 材料所在文件夹:将该文件夹下的全部 PDF 文件作为附件处理
string attachmentDir = @"E:\case_files";  // 同一案件的 PDF 材料所在文件夹
string[] attachmentPaths = Directory.GetFiles(attachmentDir, "*.pdf");

// PDF 处理相关配置
string inputPath = "";  // 输入文件为空,全部待登记材料均位于附件中
string savePath = null;  // 结果文档路径
string OutDir = @"E:\output-register";  // 输出目录(归档登记表将保存到此目录)
string key = "**************************";  // SpireToken Key
string instruction =
    "读取附件中的全部 PDF 卷宗材料,提取信息并汇总生成'归档登记表':" +
    "1. 从材料中提取案件基本信息:案号、原告、被告、案由;" +
    "2. 逐份提取每份材料的材料名称、形成日期与页数;" +
    "3. 按形成日期从早到晚对材料排序;" +
    "4. 在输出目录生成一份 PDF 格式的'归档登记表',表格包含以下列:序号、材料名称、形成日期、页数、备注;" +
    "5. 在表格上方列明案号、原告、被告、案由,在表格下方统计卷内材料份数与总页数;" ;

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

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

    // 使用PdfDocument对象处理PDF文档
    using (PdfDocument pdf = new PdfDocument())
    {
        // 输入文件为空时不加载文档,全部处理对象来自附件
        if (!string.IsNullOrEmpty(inputPath) && File.Exists(inputPath))
        {
            pdf.LoadFromFile(inputPath);
        }
        // 创建AI文档处理器
        AIDocumentProcessor processor = pdf.AI(options);

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

生成的《归档登记表》PDF 生成的《归档登记表》PDF


常见问题

添加页码和水印后,会遮挡正文内容吗?

原因:页码与水印都是叠加在页面上的绘制层,位置或透明度设置不当确实可能压住正文。

解决:在指令中明确描述位置与样式即可(如"页脚居中显示页码""水印45度斜向、浅灰色"),AI 会按描述绘制。Spire.Agent.Office 在添加页码和水印时不会改动正文原有的布局、字体与页面设置。

卷内材料的合并顺序依据什么确定?

原因:案例一按卷内约定俗成的材料顺序编排(起诉状、证据清单、笔录、判决书等),案例二则按材料的形成日期排序。若材料类型不常见、或缺少可识别的形成日期,编排结果可能不符合预期。

解决:案例一可在指令中直接写明卷内顺序(如"按起诉状、证据清单、庭审笔录、判决书的顺序编排");案例二需确保材料中载明形成日期,对于确实缺项的材料补充兜底规则(如"未标注日期的材料排在末尾,备注列标注'日期待补'")。


获取 SpireToken Key

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

在代码中配置:

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

一份表格好不好读,往往不取决于数据本身,而取决于文字在单元格里怎么摆放。标题需要居中、金额需要靠右、多行说明需要缩进、窄列里的长句需要折行,斜着排列的表头则能在有限的列宽里塞下更多信息。这些都属于单元格文字的排版设置。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端直接完成这些操作,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。

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

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


设置文字的对齐方式

对齐方式分为两个方向:垂直对齐决定文字在单元格高度上的位置,通过 VerticalAlignment 属性设置,可选 Top、Center、Bottom 等;水平对齐决定文字在单元格宽度上的位置,通过 HorizontalAlignment 属性设置,可选 General、Left、Center、Right 等。两者相互独立,可以任意组合。具体操作步骤如下:

  1. 新建工作簿,获取第一个工作表。
  2. 写入示例文字。
  3. 通过 VerticalAlignment 设置垂直对齐方式。
  4. 通过 HorizontalAlignment 设置水平对齐方式。
  5. 保存工作簿。

下面是一个完整的代码示例,展示了在 React 中设置单元格文字的对齐方式:

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

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

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

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

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

    // 写入垂直对齐的示例文字
    sheet.Range.get("A1").Text = "对齐方式";
    sheet.Range.get("B1").Text = "示例文字";
    sheet.Range.get("A2").Text = "垂直靠上";
    sheet.Range.get("B2").Text = "VerticalAlignType.Top";
    sheet.Range.get("A3").Text = "垂直居中";
    sheet.Range.get("B3").Text = "VerticalAlignType.Center";
    sheet.Range.get("A4").Text = "垂直靠下";
    sheet.Range.get("B4").Text = "VerticalAlignType.Bottom";

    // 写入水平对齐的示例文字
    sheet.Range.get("A6").Text = "水平常规";
    sheet.Range.get("B6").Text = "HorizontalAlignType.General";
    sheet.Range.get("A7").Text = "水平靠左";
    sheet.Range.get("B7").Text = "HorizontalAlignType.Left";
    sheet.Range.get("A8").Text = "水平居中";
    sheet.Range.get("B8").Text = "HorizontalAlignType.Center";
    sheet.Range.get("A9").Text = "水平靠右";
    sheet.Range.get("B9").Text = "HorizontalAlignType.Right";

    // 设置垂直对齐方式
    sheet.Range.get("B2").Style.VerticalAlignment = xlsModule.VerticalAlignType.Top;
    sheet.Range.get("B3").Style.VerticalAlignment = xlsModule.VerticalAlignType.Center;
    sheet.Range.get("B4").Style.VerticalAlignment = xlsModule.VerticalAlignType.Bottom;

    // 设置水平对齐方式
    sheet.Range.get("B6").Style.HorizontalAlignment = xlsModule.HorizontalAlignType.General;
    sheet.Range.get("B7").Style.HorizontalAlignment = xlsModule.HorizontalAlignType.Left;
    sheet.Range.get("B8").Style.HorizontalAlignment = xlsModule.HorizontalAlignType.Center;
    sheet.Range.get("B9").Style.HorizontalAlignment = xlsModule.HorizontalAlignType.Right;

    // 加宽 B 列、加高第 2~4 行,对齐差异才看得出来
    sheet.Range.get("B1:B9").ColumnWidth = 32;
    sheet.Range.get("A2:B4").RowHeight = 40;

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

export default App;

运行后,设置文字对齐方式的效果:

设置文字对齐方式


设置文字的缩进

缩进用于在单元格内部为文字留出左侧(或右侧)的空白,适合「地区 → 城市」这类有层级关系的数据。通过 IndentLevel 属性设置缩进级别,一个级别大约相当于一个字符的宽度。需要注意的是,缩进只在水平对齐为 Left、Right 等非 General 的对齐方式下才会显示,所以通常要和 HorizontalAlignment 一起使用。具体操作步骤如下:

  1. 新建工作簿,获取第一个工作表。
  2. 写入示例文字。
  3. 将水平对齐设为靠左,让缩进生效。
  4. 通过 IndentLevel 设置逐级递增的缩进级别。
  5. 保存工作簿。

下面是一个完整的代码示例,展示了在 React 中设置单元格文字的缩进:

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

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

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

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

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

    // 写入示例文字
    sheet.Range.get("A1").Text = "缩进级别";
    sheet.Range.get("B1").Text = "示例文字";
    sheet.Range.get("A2").Text = "0";
    sheet.Range.get("B2").Text = "全国";
    sheet.Range.get("A3").Text = "1";
    sheet.Range.get("B3").Text = "华北地区";
    sheet.Range.get("A4").Text = "2";
    sheet.Range.get("B4").Text = "北京市";
    sheet.Range.get("A5").Text = "3";
    sheet.Range.get("B5").Text = "海淀区";

    // 缩进需要配合靠左对齐才会生效
    sheet.Range.get("B2:B5").Style.HorizontalAlignment = xlsModule.HorizontalAlignType.Left;

    // 设置文字的缩进级别
    sheet.Range.get("B2").Style.IndentLevel = 0;
    sheet.Range.get("B3").Style.IndentLevel = 1;
    sheet.Range.get("B4").Style.IndentLevel = 2;
    sheet.Range.get("B5").Style.IndentLevel = 3;

    // 加宽 B 列,缩进差异才看得出来
    sheet.Range.get("B1:B5").ColumnWidth = 32;

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

export default App;

运行后,设置文字缩进的效果: 设置文字缩进


设置文字的方向

文字方向包含两个彼此独立的设置。一是旋转角度,通过 Rotation 属性设置,取值 0 到 90 表示逆时针旋转的角度,-1 到 -90 表示顺时针旋转的角度;旋转常用于在窄列中放置较长的表头。二是阅读方向,通过 ReadingOrder 属性设置,可选 LeftToRight、RightToLeft、Context,它决定单元格内混排文字以哪个方向为基准排列,用于阿拉伯语、希伯来语等从右到左书写的语言。旋转或竖排之后文字占用的高度比原来大得多,因此要同时调大行高,让文字完整落在单元格内。具体操作步骤如下:

  1. 新建工作簿,获取第一个工作表。
  2. 写入示例文字。
  3. 通过 Rotation 设置文字的旋转角度。
  4. 通过 ReadingOrder 设置文字的阅读方向。
  5. 保存工作簿。

下面是一个完整的代码示例,展示了在 React 中设置单元格文字的方向:

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

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

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

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

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

    // 写入旋转角度的示例文字
    sheet.Range.get("A1").Text = "文字方向";
    sheet.Range.get("B1").Text = "示例文字";
    sheet.Range.get("A2").Text = "逆时针 45 度";
    sheet.Range.get("B2").Text = "Rotation = 45";
    sheet.Range.get("A3").Text = "逆时针 90 度";
    sheet.Range.get("B3").Text = "Rotation = 90";
    sheet.Range.get("A4").Text = "顺时针 45 度";
    sheet.Range.get("B4").Text = "Rotation = -45";
    sheet.Range.get("A5").Text = "竖排文字";
    sheet.Range.get("B5").Text = "Spire";

    // 写入阅读方向的示例文字:拉丁字母与希伯来文混排,方向差异才看得出来
    sheet.Range.get("A7").Text = "从左到右";
    sheet.Range.get("B7").Text = "Spire.XLS שלום";
    sheet.Range.get("A8").Text = "从右到左";
    sheet.Range.get("B8").Text = "Spire.XLS שלום";

    // 设置文字的旋转角度,255 表示竖排(文字自上而下逐字排列)
    sheet.Range.get("B2").Style.Rotation = 45;
    sheet.Range.get("B3").Style.Rotation = 90;
    sheet.Range.get("B4").Style.Rotation = -45;
    sheet.Range.get("B5").Style.Rotation = 255;

    // 设置文字的阅读方向
    sheet.Range.get("B7").Style.ReadingOrder = xlsModule.ReadingOrderType.LeftToRight;
    sheet.Range.get("B8").Style.ReadingOrder = xlsModule.ReadingOrderType.RightToLeft;

    // 加宽 B 列、加高第 2~5 行,旋转与竖排的文字才放得下
    sheet.Range.get("B1:B8").ColumnWidth = 20;
    sheet.Range.get("A2:B5").RowHeight = 60;

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

export default App;

运行后,设置文字方向的效果: 设置文字方向


设置文字的换行

当一段文字比列宽更长时,默认会溢出到相邻的空单元格上,一旦相邻单元格有内容就会被截断。把 WrapText 属性设为 true,文字就会在单元格内部自动折行;设为 false 则恢复为不折行。和旋转一样,换行只改变文字的排列方式,不会自动调整行高,所以折行之后一般还要把行高调大,让折行出来的几行都完整显示。具体操作步骤如下:

  1. 新建工作簿,获取第一个工作表。
  2. 写入一段长文字。
  3. 通过 WrapText 开启或关闭自动换行。
  4. 调整列宽与行高,让折行效果完整显示。
  5. 保存工作簿。

下面是一个完整的代码示例,展示了在 React 中设置单元格文字的换行:

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

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

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

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

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

    // 写入示例文字
    sheet.Range.get("A1").Text = "自动换行";
    sheet.Range.get("B1").Text = "示例文字";
    sheet.Range.get("A2").Text = "开启";
    sheet.Range.get("B2").Text = "Spire.XLS for JavaScript 可以在浏览器端为单元格文字设置自动换行。";
    sheet.Range.get("A3").Text = "关闭";
    sheet.Range.get("B3").Text = "Spire.XLS for JavaScript 可以在浏览器端为单元格文字设置自动换行。";

    // 开启自动换行,文字超出列宽时在单元格内折行
    sheet.Range.get("B2").Style.WrapText = true;

    // 关闭自动换行,文字保持在一行内
    sheet.Range.get("B3").Style.WrapText = false;

    // 收窄 B 列并加高第 2~3 行,折行效果才看得出来
    sheet.Range.get("B1:B3").ColumnWidth = 24;
    sheet.Range.get("A2:B3").RowHeight = 60;

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

export default App;

运行后,设置文字换行的效果:

设置文字换行


常见问题

设置了 IndentLevel,文字却完全没有缩进?

原因:缩进只在水平对齐为 Left、Right 等非 General 的对齐方式下才会显示。单元格默认是 General 对齐,此时 IndentLevel 的值会被直接忽略,只设缩进自然看不出任何变化。

解决:先设置 HorizontalAlignment,再设置 IndentLevel:

// 缩进需要配合靠左对齐才会生效
sheet.Range.get("B2").Style.HorizontalAlignment = xlsModule.HorizontalAlignType.Left;
sheet.Range.get("B3").Style.HorizontalAlignment = xlsModule.HorizontalAlignType.Left;

// 设置文字的缩进级别
sheet.Range.get("B2").Style.IndentLevel = 1;
sheet.Range.get("B3").Style.IndentLevel = 2;

想让文字竖排(一个字占一行),把 Rotation 设为 90 度为什么不行?

原因:Rotation 的 0 到 90 与 -1 到 -90 两段只在旋转角度上做文章,90 度也只是让文字整个躺倒,并不会把它拆成自上而下的单字排列。

解决:竖排文字要用 255 这个特殊取值:

// 90 度只是把文字旋转 90 度
sheet.Range.get("B2").Style.Rotation = 90;

// 255 表示竖排,文字自上而下逐字排列
sheet.Range.get("B3").Style.Rotation = 255;

获取免费许可证

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

PDF 版式稳定、便于分发,代价是正文被锁在页面结构里,想改一个字往往要从头排版。Markdown 是另一条路:纯文本、层级清楚,能直接进 Git 仓库和知识库,也方便交给大语言模型做摘要或问答。把 PDF 转成 Markdown,本质上就是让内容重新变回可编辑的结构化文本。

Spire.PDF for JavaScript 基于 WebAssembly,在浏览器里就能完成 PDF 的加载、转换与保存,输入输出交给虚拟文件系统(VFS)管理,全过程不需要后端服务。转换有两个入口:PdfDocument.SaveToFile 把输出格式指定为 FileFormat.Markdown 即可一步到位;PdfToMarkdownConverter 则提供了 MarkdownOptions,可以按需要调整转换时的细节。

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

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


将 PDF 转换为 Markdown

PdfDocument.SaveToFile 同样可以输出 Markdown:把 fileFormat 指定为 FileFormat.Markdown,PDF 里的标题、段落与列表便会按原有层级整理成 Markdown 文本。文档中的图片默认也会一并提取出来。

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

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

    // 将待转换的 PDF 文件载入虚拟文件系统
    const inputFileName = '花卉.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // 载入字体到虚拟文件系统(WebAssembly 环境没有系统字体)
    await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

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

    // 定义输出文件名并转换为 Markdown 格式
    const outputFileName = '转换结果.md';
    doc.SaveToFile({ fileName: outputFileName, fileFormat: pdfModule.FileFormat.Markdown });
    doc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'text/markdown' });
    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>Convert PDF to Markdown</h1>
      <button onClick={convertPdfToMarkdown}>
        Convert
      </button>
    </div>
  );
}

export default App;

转换得到的 Markdown 文件

转换得到的 Markdown 文件


跳过图片只转换文字

只要文字的话,用 PdfToMarkdownConverter 更直接。它默认会把文档里的图片一并提取,把 MarkdownOptions.IgnoreImage 设为 true 后,输出就只剩纯文本。

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

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

    // 将待转换的 PDF 文件载入虚拟文件系统
    const inputFileName = '花卉.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // 载入字体到虚拟文件系统(WebAssembly 环境没有系统字体)
    await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // 创建转换器,并设置忽略图片
    const converter = new pdfModule.PdfToMarkdownConverter(inputFileName);
    converter.MarkdownOptions.IgnoreImage = true;

    // 定义输出文件名并执行转换
    const outputFileName = '纯文本转换结果.md';
    converter.ConvertToMarkdown(outputFileName);
    converter.Dispose();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'text/markdown' });
    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>Convert PDF to Text-only Markdown</h1>
      <button onClick={convertPdfToTextOnlyMarkdown}>
        Convert
      </button>
    </div>
  );
}

export default App;

只保留文字、跳过图片的转换结果

只保留文字、跳过图片的转换结果


常见问题

转换后标题和列表的层级是怎么来的

原因:PDF 里并不存在“标题”“列表”这类语义标记,只有一个个带坐标的文本片段。转换器需要根据字号、字重、缩进和行间距等版式线索,反推出原本的结构。

解决:Spire.PDF 会按上述线索判断层级并输出对应的 Markdown 标记。如果源文档的版式比较随意(例如用空格而不是真正的字号差异来区分标题),反推结果可能不理想,此时建议先调整源 PDF 的版式,或对转换结果做一次人工校对:

// 转换后读取 Markdown 文本进行检查
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
doc.SaveToFile({ fileName: outputFileName, fileFormat: pdfModule.FileFormat.Markdown });
doc.Close();

设置了 IgnoreImage 为什么图片不见了

原因:MarkdownOptions.IgnoreImage 的含义就是“转换时跳过图片”,设为 true 后,图片既不会被提取成独立文件,也不会在 Markdown 里留下图片引用。

解决:如果既要文字又要图片,把这个属性去掉或设为 false 即可,转换器会照常处理图片:

// 保留图片(默认行为)
const converter = new pdfModule.PdfToMarkdownConverter(inputFileName);
converter.MarkdownOptions.IgnoreImage = false;
converter.ConvertToMarkdown(outputFileName);
converter.Dispose();

扫描件转换出来是空白的

原因:扫描件是整页图片,没有文字层,转换器从中提取不到任何文本,所以输出几乎为空。

解决:这类 PDF 需要先做 OCR,把图片识别成文字层,再交给 Spire.PDF 转换。判断方法很简单,用阅读器打开试着选中文字,如果选不中,就说明是扫描件:

// 从 VFS 读回结果,确认是否真的产生了文本
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
console.log('输出大小:', fileArray.length, 'bytes');

获取免费许可证

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

用户上传的 PDF 里,总有一部分是带着密码的。程序若不先问一句就直接处理,轻则抛异常中断,重则生成一份内容残缺的输出。与其等失败之后再回头排查,不如在入口处把文档查清楚:它加密了没有、需要哪种密码、手上的候选密码对不对。

Spire.PDF for JavaScript 基于 WebAssembly 在浏览器端直接读取 PDF 文档,通过虚拟文件系统(VFS)管理输入文件,无需后端配合。PdfDocument.IsPasswordProtected() 是一个静态方法,不用打开文档、也不用提供密码,直接读文件即可判断;文档载入后,PdfDocument.Security 还会记录本次使用的密码落在 UserPassword 还是 OwnerPassword,据此可以确认密码的角色。

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

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


判断 PDF 是否受密码保护

PdfDocument.IsPasswordProtected() 接收虚拟文件系统中的文件名,返回布尔值,判断的是文档里有没有加密字典,因此不需要打开文档,也不需要密码。它读的是虚拟文件系统里的路径,必须先执行 window.spire.FetchFileToVFS() 把文件读进去,否则会抛 File doesn't exist。

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

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

    // 待检测的文件名
    const plainFileName = '合同模板.pdf';
    const lockedFileName = '加密合同.pdf';

    // 静态方法读的是虚拟文件系统,先把文件载入
    await window.spire.FetchFileToVFS(plainFileName, "", `${process.env.PUBLIC_URL}/data/`);
    await window.spire.FetchFileToVFS(lockedFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // 逐个判断,不需要打开文档,也不需要密码
    const reportLines = [];
    for (const fileName of [plainFileName, lockedFileName]) {
      const isProtected = pdfModule.PdfDocument.IsPasswordProtected(fileName);
      reportLines.push(`${fileName}:${isProtected ? '受密码保护' : '未加密'}`);
    }

    // 结果写入虚拟文件系统后导出
    const outputFileName = '加密检测结果.txt';
    const report = reportLines.join('\r\n');
    window.dotnetRuntime.Module.FS.writeFile(outputFileName, new TextEncoder().encode(report));

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'text/plain;charset=utf-8' });
    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={checkPasswordProtection}>
        开始检测
      </button>
    </div>
  );
}

export default App;

导出的检测报告:合同模板未加密,加密合同受密码保护

导出的检测报告:合同模板未加密,加密合同受密码保护


判断文档是否需要打开密码

IsPasswordProtected() 返回 true 只说明文档带加密字典,并不等于必须输密码才能打开——只设了权限密码的文档依然可以免密阅读。要区分这两种情况,直接免密调用一次 LoadFromFile():抛异常说明缺打开密码,能正常载入则说明文档只是受限。

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

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

    // 待判定的文件名
    const fileNames = ['合同模板.pdf', '加密合同.pdf'];
    for (const fileName of fileNames) {
      await window.spire.FetchFileToVFS(fileName, "", `${process.env.PUBLIC_URL}/data/`);
    }

    // 免密载入:抛异常即说明需要打开密码
    const reportLines = [];
    for (const fileName of fileNames) {
      try {
        const doc = new pdfModule.PdfDocument();
        doc.LoadFromFile(fileName);
        reportLines.push(`${fileName}:${doc.IsEncrypted ? '可直接打开,但文档仍受权限限制' : '未加密,可直接打开'}`);
        doc.Close();
      } catch (error) {
        reportLines.push(`${fileName}:需要打开密码`);
      }
    }

    // 结果写入虚拟文件系统后导出
    const outputFileName = '打开密码判定.txt';
    const report = reportLines.join('\r\n');
    window.dotnetRuntime.Module.FS.writeFile(outputFileName, new TextEncoder().encode(report));

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'text/plain;charset=utf-8' });
    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={checkOpenPassword}>
        开始判定
      </button>
    </div>
  );
}

export default App;

导出的判定结果:合同模板可直接打开,加密合同需要打开密码

导出的判定结果:合同模板可直接打开,加密合同需要打开密码


校验候选密码并确认密码角色

密码对不对,由 LoadFromFile() 能否成功载入来回答,失败时一律抛 Can not open an encrypted document. The password is invalid.,不区分“没给密码”还是“密码错误”。所以先看文档是否加密——未加密的直接跳过,加密的逐个试密码,再从 PdfDocument.Security 读出这次用的是 UserPassword 还是 OwnerPassword,确认它属于打开密码还是权限密码。

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

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

    // 待校验的文件与候选密码
    const fileNames = ['合同模板.pdf', '加密合同.pdf'];
    const candidates = ['wrong123', 'spire123', 'owner123'];

    // 静态方法与载入都读虚拟文件系统,先把文件载入
    for (const fileName of fileNames) {
      await window.spire.FetchFileToVFS(fileName, "", `${process.env.PUBLIC_URL}/data/`);
    }

    const reportLines = [];
    for (const fileName of fileNames) {
      // 未加密的文档无需验证密码
      if (!pdfModule.PdfDocument.IsPasswordProtected(fileName)) {
        reportLines.push(`${fileName}:该PDF文档没有加密,无需验证密码`);
        continue;
      }

      // 已加密:逐个尝试候选密码,能载入即为正确密码
      for (const password of candidates) {
        try {
          const doc = new pdfModule.PdfDocument();
          doc.LoadFromFile(fileName, password);

          // Security 记录的是本次使用的密码,据此判断密码角色
          const role = doc.Security.UserPassword ? '打开密码' : '权限密码';
          reportLines.push(`${fileName}:密码 "${password}" 是正确的(${role})`);
          doc.Close();
        } catch (error) {
          reportLines.push(`${fileName}:密码 "${password}" 不正确`);
        }
      }
    }

    // 结果写入虚拟文件系统后导出
    const outputFileName = '密码校验结果.txt';
    const report = reportLines.join('\r\n');
    window.dotnetRuntime.Module.FS.writeFile(outputFileName, new TextEncoder().encode(report));

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'text/plain;charset=utf-8' });
    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={verifyPassword}>
        开始校验
      </button>
    </div>
  );
}

export default App;

导出的校验结果:错误密码被拒绝,spire123 是打开密码,owner123 是权限密码

导出的校验结果:错误密码被拒绝,spire123 是打开密码,owner123 是权限密码


常见问题

IsPasswordProtected() 抛 File doesn't exist

原因:该方法读的是虚拟文件系统里的路径,不是浏览器能直接访问的地址。文件没有先载入虚拟文件系统时,Spire.PDF 找不到目标,直接抛出 File doesn't exist Arg_ParamName_Name, fileName。

解决:调用前先用 FetchFileToVFS 把文件读进虚拟文件系统,文件名与后续传入的名字保持一致:

// 先载入虚拟文件系统,再判断
await window.spire.FetchFileToVFS('加密合同.pdf', "", `${process.env.PUBLIC_URL}/data/`);
const isProtected = pdfModule.PdfDocument.IsPasswordProtected('加密合同.pdf');

文档明明需要密码,报错却说“密码无效”

原因:Spire.PDF 对“没有提供密码”和“密码填错”返回同一条提示 Can not open an encrypted document. The password is invalid.,单看报错无法区分。

解决:按三步走——先用 IsPasswordProtected() 判断文档是否带加密字典,再免密载入区分“需要打开密码”与“仅权限受限”,最后才用候选密码逐个尝试,由是否抛出异常来判定密码是否正确:

// 免密失败 → 需要打开密码;换密码再试 → 是否抛错即是否命中
try {
  const doc = new pdfModule.PdfDocument();
  doc.LoadFromFile(fileName, password);
  // 载入成功:读 Security 确认角色
} catch (error) {
  // 载入失败:该密码不可用
}

载入后读 Security.Permissions 报错

原因:权限位是多个标志位的组合值,不一定落在 PdfPermissionsFlags 枚举成员上,读取时会抛 Invalid value for spirepdfPdfPermissionsFlags。同一版本上 HasExtendedRight() 也会抛 ArgumentNullException。

解决:改用 Security.UserPassword 与 Security.OwnerPassword 判断本次密码的角色——权限密码载入时拥有全部权限,可以直接调用 Decrypt() 移除保护;打开密码载入时需另提供权限密码。需要精确控制权限时,改在加密环节用 PdfDocumentPrivilege 设置并自行记录:

const role = doc.Security.UserPassword ? '打开密码' : '权限密码';

// 权限密码载入的情况下,可直接移除保护
if (role === '权限密码') {
  doc.Decrypt();
}

获取免费许可证

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