Spire.PDF for Python 12.9.4 现已正式发布。该版本新增了对多种 PDF 注释类型的支持,包括 Caret、Circle、Square、FileAttachment、Movie、Screen、RichMedia 和 Widget 注释,并新增了通过 PdfCode128ABarcode 接口绘制 Code 128 条形码的支持。同时修复了 PDF 附件、文本替换、文本提取以及文本位置等方面的问题。详情如下。
新功能:
rect = RectangleF(100.0, 100.0, 10.0, 15.0)
annotation = PdfCaretAnnotation(page, rect)
annotation.Title = "CaretTest"
annotation.Contents = "A caret annotation with default (None) symbol."
annotation.Color = PdfRGBColor(0, 0, 255)
annotation.Border.Width = 1
annotation.Intent = "ParaCaret"
PdfAnnotationCollection.Create(page).Add(annotation)
annotation = PdfCircleAnnotation(page, rect)
annotation.Color = PdfRGBColor(128, 0, 128)
annotation.InteriorColor = PdfRGBColor(255, 255, 200)
annotation.Border.Width = 2
annotation.Border.Style = PdfBorderStyle.Solid
annotation.Border.Effect = PdfBorderEffect.Cloudy
annotation.Border.EffectIntensity = 2
PdfAnnotationCollection.Create(page).Add(annotation)
annotation = PdfSquareAnnotation(page, rect)
annotation.Color = PdfRGBColor(128, 0, 128)
annotation.InteriorColor = PdfRGBColor(255, 255, 200)
annotation.Border.Width = 2
annotation.Border.Style = PdfBorderStyle.Dashed
PdfAnnotationCollection.Create(page).Add(annotation)
fileSpecification = PdfFileSpecification("SalesReportChart.png", data)
annotation = PdfFileAttachmentAnnotation(page, bounds, fileSpecification)
annotation.Icon = PdfFileIcon.Graph
annotation.Color = PdfRGBColor(Color.get_Teal())
PdfAnnotationCollection.Create(page).Add(annotation)
annotation = PdfMovieAnnotation(page, rect, "sample.wmv")
annotation.Title = "My Movie Player"
annotation.Movie.Poster = False
annotation.Movie.Aspect = SizeF(16.0, 9.0)
PdfAnnotationCollection.Create(page).Add(annotation)
annotation = PdfScreenAnnotation(page, rect)
annotation.Title = "ScreenWithBorder"
annotation.Border.Width = 3.0
annotation.Border.Style = PdfBorderStyle.Dashed
PdfAnnotationCollection.Create(page).Add(annotation)
annotation = PdfRichMediaAnnotation(page, rect, content)
annotation.Content = PdfRichMediaContent()
annotation.Color = PdfRGBColor(Color.get_Green())
PdfAnnotationCollection.Create(page).Add(annotation)
doc.LoadFromFile("widget.pdf")
a = PdfAnnotationCollection.Create(page).get_Item(0)
a.Characteristics.BackgroundColor
a.Characteristics.BorderColor
a.Flatten()
message = '0123456789'
Code128 = PdfCode128ABarcode(message)
Code128.BarcodeToTextGapHeight = 1.0
Code128.TextDisplayLocation = TextLocation.Bottom
Code128.TextColor = PdfRGBColor(Color.get_Blue())
Code128.Draw(page, PointF(0.0, y))
问题修复:
合同、报价单、演示文稿这类附件经常被直接嵌入到 Excel 工作簿中:在工作表上看到的只是一个图标或缩略图,真正的文档数据藏在工作簿的 xl/embeddings 里。要把这些附件从工作簿中取出来单独归档,逐个双击另存显然不现实。Spire.XLS for JavaScript 基于 WebAssembly 在浏览器端直接完成此操作,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。
本文主要介绍以下两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.XLS for JavaScript。以下示例默认已安装 Spire.XLS 并完成 WebAssembly 模块初始化。
提取之前先要弄清工作簿里到底嵌了什么。一份附件清单能把嵌入对象的类型、原始名称、所在位置和体积一次列全,充当附件的登记记录,归档或交接时不必逐个双击确认。
具体操作步骤如下:
HasOleObjects 判断当前工作表是否含有嵌入对象,没有则提前结束OleObjects 集合遍历其中的每一个对象ObjectType:对象类型,如 WordDocument、PowerPointPresentationOleOriginName:嵌入前的原始文件名Location:对象所在的单元格位置OleData 的长度:数据大小ListOleObjects.txt下面的示例把这些信息汇总成一份文本清单:
function App() {
const listOleObjects = 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 = 'OLEObjects.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);
// 判断工作表中是否包含 OLE 对象
if (!sheet.HasOleObjects) {
alert('The worksheet contains no OLE objects.');
return;
}
// 把读取到的信息汇总成清单,首行为表头,字段之间用制表符分隔
const lines = [['序号', '对象类型', '原始文件名', '所在位置', '数据大小(字节)'].join('\t')];
// 遍历工作表中的 OLE 对象,每行记录一个对象
let index = 1;
for (const oleObject of sheet.OleObjects) {
lines.push([
index,
String(oleObject.ObjectType),
String(oleObject.OleOriginName),
String(oleObject.Location.RangeAddress),
oleObject.OleData.length,
].join('\t'));
index += 1;
}
// 释放 workbook 对象以释放资源
workbook.Dispose();
// 把清单写入 VFS 中的文本文件
const outputFileName = 'ListOleObjects.txt';
window.dotnetRuntime.Module.FS.writeFile(outputFileName, lines.join('\n'));
// 从 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>List OLE Objects</h1>
<button onClick={listOleObjects}>Start</button>
</div>
);
}
export default App;
运行后,读取工作表中的 OLE 对象信息的效果:

清单只能说明嵌了什么,真正要归档还得把附件本身取出来。OLE 对象的正文以字节数组的形式存放在对象内部,原样写出即可还原成嵌入前的文档,无需重新排版或转换格式。
具体操作步骤如下:
HasOleObjects 确认当前工作表含有嵌入对象OleObjects 集合中的每个对象,先由 ObjectType 判断它的类型,据此决定输出文件的扩展名与 MIME 类型OleData 取出对象的原始字节,写入虚拟文件系统并读回,包装成可下载的 BlobDispose 释放工作簿,再逐个触发下载下面的示例遍历工作表中的所有对象,把 Word、PowerPoint 与 PDF 三类附件分别写成 .docx、.pptx 与 .pdf 文件并提供下载:
function App() {
const extractOleObjects = 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 = 'OLEObjects.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 results = [];
// 遍历工作表中的 OLE 对象,按对象类型写出对应格式的文件
if (sheet.HasOleObjects) {
for (const oleObject of sheet.OleObjects) {
const type = oleObject.ObjectType;
let outputFileName = '';
let mimeType = '';
// Word 文档
if (type === xlsModule.OleObjectType.WordDocument) {
outputFileName = 'ExtractWord.docx';
mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
}
// PowerPoint 演示文稿,.pptx 与 .sldx 分属两个枚举成员
else if (
type === xlsModule.OleObjectType.PowerPointPresentation ||
type === xlsModule.OleObjectType.PowerPointSlide
) {
outputFileName = 'ExtractPowerPoint.pptx';
mimeType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
}
// PDF 文档
else if (type === xlsModule.OleObjectType.AdobeAcrobatDocument) {
outputFileName = 'ExtractPdf.pdf';
mimeType = 'application/pdf';
}
// 其余类型不作处理
if (!outputFileName) continue;
// 把对象的原始数据写入虚拟文件系统
window.dotnetRuntime.Module.FS.writeFile(outputFileName, oleObject.OleData);
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
results.push({
name: outputFileName,
url: URL.createObjectURL(new Blob([fileArray], { type: mimeType })),
});
}
}
// 释放 workbook 对象以释放资源
workbook.Dispose();
// 逐个触发下载
results.forEach(({ name, url }) => {
const a = document.createElement('a');
a.href = url;
a.download = name;
a.click();
URL.revokeObjectURL(url);
});
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Extract OLE Objects</h1>
<button onClick={extractOleObjects}>Start</button>
</div>
);
}
export default App;
运行后,提取出的附件文档的效果:

OleObjectType.PowerPointSlide 没有匹配上原因:OleObjectType 按嵌入文件的真实格式细分,.pptx 演示文稿的对象类型是 PowerPointPresentation,只有 .sldx、.ppt 这类放映格式才落到 PowerPointSlide,只判断后者会漏掉绝大多数 PowerPoint 附件。
解决:分支中同时匹配两个枚举成员,例如:
else if (
type === xlsModule.OleObjectType.PowerPointPresentation ||
type === xlsModule.OleObjectType.PowerPointSlide
) {
outputFileName = 'ExtractPowerPoint.pptx';
}
原因:OleObjects 是工作表级别的集合,示例先取 Worksheets.get(0) 拿到第一张表,再遍历这张表的 OleObjects,因此分布在其它工作表上的对象根本不在遍历范围内。附件跨表存放时,提取结果会缺少后面几张表的内容,而且不会有任何报错提示。
解决:改为逐张工作表遍历,对每张表分别取它的 OleObjects:
for (let i = 0; i < workbook.Worksheets.Count; i++) {
const worksheet = workbook.Worksheets.get(i);
for (const oleObject of worksheet.OleObjects) {
// 按类型写出附件
}
}
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。

光学字符识别(OCR)技术已成为处理扫描文档和基于图像的 PDF 的开发人员的必备工具。在本教程中,你将学习如何使用 Spire.PDF for .NET 和 Spire.OCR for .NET 库在 C# 中对 PDF 执行 OCR,从扫描文档或 PDF 中的图像里提取文本。通过将扫描版 PDF 转换为可编辑、可搜索的格式,你可以显著改善文档管理流程。
扫描版 PDF 本质上是图像文件——它们包含的是文字的图片,而不是实际可选择和可搜索的文本内容。当你扫描纸质文档或收到基于图像的 PDF 时,文本仅以像素形式存在,无法编辑、搜索或提取。这给需要以数字化方式处理这些文档的企业和个人带来了很大的限制。
OCR 技术通过分析扫描图像中字母和数字的形状,将其转换为机器可读的文本,从而解决了这一问题。这一过程将静态 PDF 转变为可使用、可搜索、可编辑的文档——实现文本提取、关键词搜索,以及与数据库和工作流自动化工具的无缝集成。
在法律、医疗和教育等经常需要处理大量扫描文档的领域中,OCR 在文档数字化过程中发挥着重要作用,可以让重要数据更容易访问和使用。
在开始编写代码之前,我们首先需要使用必要的组件配置开发环境:Spire.PDF 和 Spire.OCR。Spire.PDF 负责处理 PDF 操作,而 Spire.OCR 执行实际的文本识别。
首先,在 Visual Studio 中打开 NuGet 包管理器,搜索 “Spire.PDF” 和 “Spire.OCR” 并将其安装到你的项目中。或者,你也可以使用包管理器控制台:
Install-Package Spire.PDF
Install-Package Spire.OCR
Spire.OCR 需要预训练语言模型才能进行文本识别。根据你的操作系统(Windows、Linux 或 macOS)下载相应的模型文件,并将其解压到指定目录(例如 D:\win-x64)。
支持的平台包括:
重要提示:请确保项目的目标平台设置为 x64(项目属性 > 生成 > 平台目标),因为 Spire.OCR 仅支持 64 位系统。

安装好必要的库后,现在就可以对扫描版 PDF 执行 OCR 了。以下是一个演示此过程的示例代码片段。
using Spire.OCR;
using Spire.Pdf;
using Spire.Pdf.Graphics;
using System.Drawing;
namespace OCRPDF
{
class Program
{
static void Main(string[] args)
{
// 创建 OcrScanner 类的实例
OcrScanner scanner = new OcrScanner();
// 配置扫描器
ConfigureOptions configureOptions = new ConfigureOptions
{
ModelPath = @"D:\win-x64", // 设置模型路径
Language = "Chinese" // 设置语言
};
// 应用配置选项
scanner.ConfigureDependencies(configureOptions);
// 加载 PDF 文档
PdfDocument doc = new PdfDocument();
doc.LoadFromFile(@"C:\Users\Administrator\Desktop\Input.pdf");
// 遍历所有页面
for (int i = 0; i < doc.Pages.Count; i++)
{
// 将页面转换为图像
Image image = doc.SaveAsImage(i, PdfImageType.Bitmap);
// 将图像转换为 MemoryStream
using (MemoryStream stream = new MemoryStream())
{
image.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
stream.Position = 0; // 重置流的位置
// 对图像流执行 OCR
scanner.Scan(stream, OCRImageFormat.Png);
string pageText = scanner.Text.ToString();
// 将提取的文本保存到单独的文件中
string outputTxtPath = Path.Combine(@"C:\Users\Administrator\Desktop\Output", $"Page-{i + 1}.txt");
File.WriteAllText(outputTxtPath, pageText);
}
}
// 关闭文档
doc.Close();
}
}
}
关键组件说明:
输出:

如果需要从可搜索的 PDF 中提取文本,请参阅本指南:使用 C# 实现 PDF 文本自动化提取
除了处理整个 PDF 页面外,你还可以提取嵌入 PDF 中的图像里的文本。方法如下:
using Spire.Pdf;
using Spire.Pdf.Utilities;
using Spire.OCR;
using System.Drawing;
namespace OcrImageFromPdf
{
class Program
{
static void Main(string[] args)
{
// 创建 PdfDocument 对象
PdfDocument doc = new PdfDocument();
// 加载 PDF 文档
doc.LoadFromFile(@"C:\Users\Administrator\Desktop\Input.pdf");
// 创建 PdfImageHelper 对象
PdfImageHelper imageHelper = new PdfImageHelper();
// 创建 OcrScanner 类的实例
OcrScanner scanner = new OcrScanner();
// 配置扫描器
ConfigureOptions configureOptions = new ConfigureOptions
{
ModelPath = @"D:\win-x64", // 设置模型路径
Language = "Chinese" // 设置语言
};
scanner.ConfigureDependencies(configureOptions);
// 声明一个 int 变量
int m = 0;
// 遍历所有页面
for (int i = 0; i < doc.Pages.Count; i++)
{
// 获取指定页面
PdfPageBase page = doc.Pages[i];
// 获取页面中的所有图片信息
PdfImageInfo[] imageInfos = imageHelper.GetImagesInfo(page);
// 遍历图片信息
for (int j = 0; j < imageInfos.Length; j++)
{
// 获取指定的图片信息
PdfImageInfo imageInfo = imageInfos[j];
// 获取图片
Image image = imageInfo.Image;
// 将图片转换为 MemoryStream
using (MemoryStream stream = new MemoryStream())
{
image.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
stream.Position = 0; // 重置流的位置
// 对图片流执行 OCR
scanner.Scan(stream, OCRImageFormat.Png);
string imageText = scanner.Text.ToString();
// 将 OCR 结果保存到文本文件
string outputTxtPath = Path.Combine(@"C:\Users\Administrator\Desktop\Output", $"Image-Text-{m + 1}.txt");
File.WriteAllText(outputTxtPath, imageText);
m++;
}
}
}
// 释放资源
doc.Dispose();
}
}
}
关键组件说明:
输出:

通过将 Spire.PDF 与 Spire.OCR 结合使用,你可以将扫描版 PDF 和基于图像的文档无缝转换为完全可搜索、可编辑的文本。无论你需要处理整个页面还是从特定的嵌入图像中提取文本,方法都简单而灵活。
这种 OCR 集成不仅简化了文档数字化流程,还通过支持搜索、复制和自动化数据提取来提高生产力。对于经常需要处理大量扫描文档的行业而言,使用 C# 实现 OCR 可以显著提升信息的可访问性、合规性以及信息检索速度。
可以,Spire.OCR 支持多种语言。你可以在 ConfigureOptions 中将 Language 属性设置为所需语言。
请检查输入 PDF 中图像的质量。如果图像模糊或对比度低,OCR 可能难以准确识别文本。考虑在处理前增强图像质量。
可以。可以使用辅助类从每个页面中提取图像,然后应用 OCR 对图像中的文本进行识别。
Spire.OCR 主要针对印刷体文本进行了优化。对于手写文字,识别准确率通常会较低。
是的,Spire.OCR 需要预训练的语言模型文件。在执行 OCR 之前,请下载并配置适用于你目标语言的相应模型。
如需完整体验 Spire.PDF for .NET 和 Spire.OCR for .NET 的功能而不受评估限制,可以申请 30 天免费试用许可证。

JavaScript 可以通过编程方式生成 PDF 文件——创建文档、添加页面、绘制文本和其他元素,然后保存文档。在 React 应用中,借助 Spire.PDF for JavaScript 和 WebAssembly,这一流程完全在浏览器中完成,无需后端服务器。
从整体来看,JavaScript PDF 生成包含五个步骤:创建文档、添加页面、绘制内容、保存 PDF、下载生成的文件。本文以 React 作为示例环境,但 PDF 生成流程本身基于 JavaScript,适用于任何前端框架。
需要注意的是,本文所说的"生成 PDF"是指通过代码从零开始构建 PDF 文档,而非将现有 HTML 页面打印为 PDF。JavaScript 应用可以通过 HTML 转 PDF、基于 Canvas 的渲染或编程式 PDF 构建等方式生成 PDF。本文聚焦于使用 Spire.PDF for JavaScript 进行编程式 PDF 生成。
本文假设您已在 React 项目中安装并初始化了 Spire.PDF for JavaScript。有关配置详情,请参阅如何在 React 项目中集成 Spire.PDF for JavaScript。您还需要配置 WebAssembly 资源,使 PDF 模块可通过 window.spirepdf 访问(Spire.Office for JavaScript 11.7.0 或更高版本)。
内置标准字体不含中文字形,因此本文示例额外使用一份中文字体文件:把等线 Deng.ttf 与粗体 Dengb.ttf(或任何 .ttf)放进项目的 public/fonts/ 目录,页面在运行时读取它。
创建 PdfDocument -> 添加页面 -> 绘制内容 -> 保存 -> 下载
首先创建一个 PdfDocument 对象,它是即将构建的 PDF 文件的内存表示。
const pdf = window.spirepdf;
let doc = new pdf.PdfDocument();
PDF 文档至少需要一个页面。调用 Pages.Add() 创建一个默认大小的空白页面。
let page = doc.Pages.Add();
页面有一个 Canvas 属性,提供各种绘制方法。使用 DrawString 添加文本,DrawImage 添加图像,DrawLine 绘制线条,DrawRectangle 绘制填充或轮廓矩形。坐标以磅为单位(1 磅 = 1/72 英寸),从左上角开始计算。
PdfFontFamily 中的 Helvetica 等标准字体只有拉丁字形,绘制中文时要改用 PdfTrueTypeFont 引用一份中文字体文件:
// 把中文字体写进 WebAssembly 虚拟文件系统(VFS),再用路径引用它
const FS = window.dotnetRuntime.Module.FS;
FS.writeFile('/Deng.ttf', new Uint8Array(await (await fetch('/fonts/Deng.ttf')).arrayBuffer()));
let font = new pdf.PdfTrueTypeFont('/Deng.ttf', 12);
let brush = new pdf.PdfSolidBrush({ pdfRGBColor: new pdf.PdfRGBColor(0, 0, 0) });
page.Canvas.DrawString({ s: '你好,世界!', font: font, brush: brush, x: 50, y: 50 });
内容放置完毕后,调用 SaveToFile 将 PDF 写入 WebAssembly 虚拟文件系统(VFS)。VFS 是一个浏览器内的文件系统,Spire.PDF 通过它管理输入和输出文件,无需访问真实磁盘或服务器。
doc.SaveToFile({ fileName: 'Output.pdf' });
doc.Close();
保存到 VFS 后,将文件读回为字节数组,包装为 Blob 对象,然后通过创建临时锚点元素触发下载。这一模式在所有 Spire.PDF for JavaScript 示例中通用。
const fileArray = window.dotnetRuntime.Module.FS.readFile('Output.pdf');
const blob = new Blob([fileArray], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'Output.pdf';
a.click();
URL.revokeObjectURL(url);
当您需要精确控制文档结构、页面布局、文本、图形或元数据时,编程式 PDF 生成非常有用。如果您已有网页或 HTML 模板,只需将该布局转换为 PDF,则 JavaScript HTML 转 PDF 方案 可能更合适。
下一节将通过一个完整示例,将上述步骤组合起来,从 JavaScript 数据生成 PDF。
实际应用中,结构化文档——发票、报表、报价单——是从应用数据生成的,而非硬编码字符串。在 React 应用中,invoice 对象可能来自表单输入、组件状态或 API 响应;PDF 生成代码只需要文档布局所需的数据。本示例将发票定义为 JavaScript 对象,并从中渲染出可打印的 PDF:包含出血式页眉、购买方信息块、明细表格、金额汇总和付款详情。它将 DrawString、DrawRectangle、DrawLine 和 PdfGrid 组合为一个完整的可运行 React 组件。
function App() {
const generateInvoicePdf = async () => {
// spire.office 11.7.0 将引擎挂载到 window.spirepdf
const pdf = window.spirepdf;
if (!pdf) return alert('Spire.PDF 尚未就绪');
// 中文字形需要真实字体:把字体文件写进 WebAssembly 虚拟文件系统(VFS),
// 再用 PdfTrueTypeFont 按路径引用。把任意 .ttf 放进 public/fonts/ 即可替换。
const FS = window.dotnetRuntime.Module.FS;
const loadFont = async (name) => {
const bytes = new Uint8Array(await (await fetch(`/fonts/${name}`)).arrayBuffer());
FS.writeFile(`/${name}`, bytes);
return `/${name}`;
};
const regular = await loadFont('Deng.ttf');
const boldFace = await loadFont('Dengb.ttf');
// ── 发票数据。替换此对象,布局随之变化。 ────
const invoice = {
number: 'INV-2026-0148', issued: '2026年9月23日',
due: '2026年10月23日', po: 'PO-SZZN-88231', terms: '账期 30 天',
seller: ['云启软件科技(上海)有限公司', '面向制造企业的一体化云平台',
'上海市浦东新区世纪大道 1200 号 28 层'],
// [名称, 联系人, 税号, 地址, 城市] — 联系人行绘制在地址下方
buyer: ['苏州精智制造股份有限公司', '联系人:陈晓雯(采购部)',
'纳税人识别号 91320594MA1WQ9XT4K', '江苏省苏州市工业园区星湖街 328 号 5 号楼',
'苏州工业园区 215021 · 中国'],
// [描述, 数量, 单价] — 金额在下方计算
items: [
['CloudDesk Pro 云桌面年度订阅(25 席位)', 25, 860],
['实施与上线服务(远程,16 小时)', 16, 680],
['优先技术支持(高级版,12 个月)', 1, 17200],
['对象存储扩容包(500 GB / 年)', 1, 3400],
],
discountRate: 0.05, taxRate: 0.06,
bank: ['招商银行上海浦东分行 · 账号 1219 0688 7710 901',
'开户名 云启软件科技(上海)有限公司 · SWIFT CMBCCNBS'],
remark: ['请在汇款附言中注明发票号码。',
'逾期未付款项将按每月 1.5% 计收利息。'],
legal:
'云启软件科技(上海)有限公司 · 上海市浦东新区世纪大道 1200 号 28 层 · 税号 91310115MA1K3QX89T',
contact: 'billing@ yunqi-software.cn · 021-6888 1200 · yunqi-software.cn',
};
const round = (n) => Math.round(n * 100) / 100;
const subtotal = round(invoice.items.reduce((sum, [, qty, unit]) => sum + qty * unit, 0));
const discount = -round(subtotal * invoice.discountRate);
const tax = round((subtotal + discount) * invoice.taxRate);
const total = round(subtotal + discount + tax);
const money = (n) =>
'¥' + Math.abs(n).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const pct = (rate) => Math.round(rate * 100) + '%';
// ── 文档 ──────────────────────────────────────────────────────────
const doc = new pdf.PdfDocument();
doc.PageSettings.Margins.All = 0; // 必须在 Pages.Add() 之前设置,否则原点停留在边距处
const page = doc.Pages.Add();
// 超出客户区域的文本会被丢弃,因此基于 ClientSize 布局
const W = page.Canvas.ClientSize.Width;
const H = page.Canvas.ClientSize.Height;
const PAD = 28;
const EDGE = W - PAD;
// ── 绘制工具集 ───────────────────────────────────────────────────
// 对象式写法选择 PdfSolidBrush / PdfPen 的重载 ——
// 位置式构造函数在此处均报 "Ambiguous call"
const rgb = (c) => new pdf.PdfRGBColor(c[0], c[1], c[2]);
const paint = (c) => new pdf.PdfSolidBrush({ pdfRGBColor: rgb(c) });
// PdfTrueTypeFont 可以按路径重载,加粗用另一份粗体字体文件
const font = (size, bold) => new pdf.PdfTrueTypeFont(bold ? boldFace : regular, size);
// DrawString 的第一个参数是 `s`,按点绘制时没有对齐选项 ——
// 需要右对齐时先测量字符串宽度
const text = (s, f, colour, x, y, align) => {
const dx = align === 'right' ? f.MeasureString({ text: s }).Width : 0;
page.Canvas.DrawString({ s, font: f, brush: paint(colour), x: x - dx, y });
};
const box = (colour, x, y, w, h) =>
page.Canvas.DrawRectangle({ brush: paint(colour), x, y, width: w, height: h });
const INK = [26, 31, 43], NAVY = [23, 54, 93], ACCENT = [47, 111, 181];
const MUTED = [107, 114, 128], RULE = [220, 225, 232], ZEBRA = [246, 248, 251];
const SOFT = [238, 243, 249], WHITE = [255, 255, 255], ON_NAVY = [186, 200, 220];
const fHero = font(24, true), fBrand = font(15, true), fTitle = font(11, true);
const fBody = font(9), fBold = font(9, true), fSmall = font(8);
const fLabel = font(7.5, true), fFoot = font(7.5), fCell = font(8.5), fHead = font(8, true);
// ── 页眉色带,出血到纸张边缘 ──────────────────────────
box(NAVY, 0, 0, W, 106);
box(ACCENT, 0, 106, W, 3.5);
text(invoice.seller[0], fBrand, WHITE, PAD, 30);
text(invoice.seller[1], fSmall, ON_NAVY, PAD, 52);
text(invoice.seller[2], fFoot, ON_NAVY, PAD, 68);
text('服务费发票', fHero, WHITE, EDGE, 28, 'right');
text(invoice.number, fBody, ON_NAVY, EDGE, 58, 'right');
text(`开票日期 ${invoice.issued}`, fFoot, ON_NAVY, EDGE, 76, 'right');
// ── 购买方 / 发票详情 ───────────────────────────────────────
const top = 158;
text('购买方', fLabel, MUTED, PAD, top);
text(invoice.buyer[0], fBold, INK, PAD, top + 17);
invoice.buyer.slice(2).forEach((line, i) => text(line, fBody, MUTED, PAD, top + 35 + i * 14));
text(invoice.buyer[1], fSmall, MUTED, PAD, top + 83);
const details = [
['发票号码', invoice.number], ['开票日期', invoice.issued], ['付款期限', invoice.due],
['采购订单', invoice.po], ['付款条件', invoice.terms],
];
text('发票详情', fLabel, MUTED, EDGE, top, 'right');
details.forEach(([key, value], i) => {
text(key, fSmall, MUTED, EDGE - 128, top + 20 + i * 16);
text(value, fBold, INK, EDGE, top + 20 + i * 16, 'right');
});
const tableY = top + 106;
page.Canvas.DrawLine({
pen: new pdf.PdfPen({ pdfRGBColor: rgb(RULE), width: 0.75 }),
x1: PAD, y1: tableY, x2: EDGE, y2: tableY,
});
// ── 明细行 ────────────────────────────────────────────────────────
const grid = new pdf.PdfGrid();
grid.Columns.Add(4);
[255, 44, 112, 128].forEach((w, i) => (grid.Columns.get_Item(i).Width = w));
const alignRight = new pdf.PdfStringFormat({ alignment: pdf.PdfTextAlignment.Right });
[1, 2, 3].forEach((i) => (grid.Columns.get_Item(i).Format = alignRight));
// 单元格内边距从 row.Height 中扣除 —— 需留出一行高度
// 否则所有单元格渲染为空白,且无任何错误提示
const padding = new pdf.PdfPaddings();
padding.Left = padding.Right = 8;
padding.Top = padding.Bottom = 2;
grid.Style.CellPadding = padding;
grid.Style.Font = fCell;
const hairline = new pdf.PdfBorders();
hairline.All = new pdf.PdfPen({ pdfRGBColor: rgb(RULE), width: 0.5 });
try { grid.Headers.Add(1); } catch {}
const head = grid.Headers.get_Item(0);
head.Height = 26;
head.Style.BackgroundBrush = paint(NAVY);
head.Style.TextBrush = paint(WHITE);
head.Style.Font = fHead;
['服务内容', '数量', '单价', '金额'].forEach((label, i) => {
const cell = head.Cells.get_Item(i);
cell.Value = new pdf.String(label); // .Value 是 .NET 对象 —— 需用 pdf.String 装箱
if (i) cell.StringFormat = alignRight;
cell.Style.Borders = hairline;
});
invoice.items.forEach(([description, qty, unit], r) => {
const row = grid.Rows.Add();
row.Height = 26;
if (r % 2) row.Style.BackgroundBrush = paint(ZEBRA);
[description, String(qty), money(unit), money(qty * unit)].forEach((value, i) => {
const cell = row.Cells.get_Item(i);
cell.Value = new pdf.String(value);
cell.Style.Borders = hairline;
});
});
const layout = new pdf.PdfGridLayoutFormat();
layout.Layout = pdf.PdfLayoutType.Paginate;
// 参数名是 `format`,不是 `layout`
const tableBottom = grid.Draw({ page, x: PAD, y: tableY + 26, format: layout }).Bounds.Bottom;
// ── 金额汇总 ────────────────────────────────────────────────────
let y = tableBottom + 22;
const summary = [
['小计', money(subtotal)],
[`伙伴折扣 · ${pct(invoice.discountRate)}`, '-' + money(discount)],
[`增值税 · ${pct(invoice.taxRate)}`, money(tax)],
];
summary.forEach(([label, value], i) => {
text(label, fBody, MUTED, EDGE - 220, y + i * 18);
text(value, fBody, INK, EDGE, y + i * 18, 'right');
});
y += 58;
box(NAVY, EDGE - 220, y, 220, 34);
text('应付总额', fTitle, WHITE, EDGE - 204, y + 11);
text(money(total), font(14, true), WHITE, EDGE - 14, y + 8, 'right');
y += 34;
// ── 付款信息 ───────────────────────────────────────────────────
const cardTop = y + 34;
box(SOFT, PAD, cardTop, W - 2 * PAD, 104);
box(ACCENT, PAD, cardTop, 3, 104);
text('付款信息', fLabel, ACCENT, PAD + 18, cardTop + 16);
text(invoice.bank[0], fBody, INK, PAD + 18, cardTop + 34);
text(invoice.bank[1], fBody, INK, PAD + 18, cardTop + 50);
invoice.remark.forEach((line, i) => text(line, fSmall, MUTED, PAD + 18, cardTop + 72 + i * 14));
// ── 页脚色带,与页眉对称 ─────────────────────────────────
box(NAVY, 0, H - 58, W, 58);
text(invoice.legal, fFoot, ON_NAVY, PAD, H - 41);
text(invoice.contact, fFoot, ON_NAVY, PAD, H - 27);
text('第 1 页 / 共 1 页', fFoot, ON_NAVY, EDGE, H - 41, 'right');
text(invoice.number, fFoot, ON_NAVY, EDGE, H - 27, 'right');
// ── 保存并下载 ─────────────────────────────────────────────────
const fileName = 'Invoice-zh.pdf';
doc.SaveToFile({ fileName });
doc.Close();
const bytes = window.dotnetRuntime.Module.FS.readFile(fileName);
const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' }));
Object.assign(document.createElement('a'), { href: url, download: fileName }).click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>生成发票 PDF</h1>
<button onClick={generateInvoicePdf}>生成发票 PDF</button>
</div>
);
}
export default App;
从 JavaScript 数据生成的发票 PDF

代码说明:
PdfTrueTypeFont 按路径引用它。PdfFontFamily 里的 Helvetica 等标准字体只有拉丁字形,直接用它绘制中文会得到空白。invoice 对象,包含卖方、客户、发票元数据和 items 数组——这正是 React 应用从表单、API 响应或组件状态中获取的数据结构。reduce 从该数据派生 subtotal、discount、tax 和 total,确保 PDF 上打印的金额与应用中的金额始终一致。Pages.Add() 之前将 Margins.All 设为 0。画布原点位于页面边距原点,因此这一设置使 (0, 0) 成为纸张的左上角,让页眉和页脚色带出血到边缘。Canvas.ClientSize 而非 Size 进行页面布局,因为超出客户区域的文本会被丢弃而非裁剪。DrawRectangle、DrawLine 和 DrawString 绘制页眉色带、购买方和发票详情块、金额汇总和付款信息框。右对齐字符串先用 MeasureString 测量宽度,因为 DrawString 在按点绘制时没有对齐选项。PdfGrid 构建明细表格:固定列宽、带样式的表头行、斑马纹数据行,以及由 PdfStringFormat 驱动的右对齐数字列。同一模式适用于任何结构化文档——将 invoice 对象替换为报表数据、报价数据或收据数据,五步流程保持不变。
创建基础文档后,可以根据需要生成的文档类型,使用其他 PDF 元素扩展同一流程。
| 元素 | 典型用途 | 教程 |
|---|---|---|
| 文本 | 标题、标签、段落 | 本文已涵盖 |
| 图像 | Logo、签名、图表 | 在 PDF 中添加图像 |
| 表格 | 发票、报表、对账单 | 文档操作指南 |
| 形状 | 边框、分隔线、示意图 | 在 PDF 中绘制形状 |
| 页眉与页脚 | 页码、重复页眉 | 页面设置指南 |
| 表单域 | 交互式表单、可编辑文档 | 表单域指南 |
五步流程不变——只是第 3 步中绘制的内容不同。
Spire.PDF for JavaScript 通过 WebAssembly 运行其 PDF 处理引擎。WASM 模块加载后,您的应用可以在客户端创建、编辑和保存 PDF。文件通过浏览器内的虚拟文件系统(VFS)流转:
JavaScript -> Spire.PDF WebAssembly -> VFS -> Blob -> 下载

这意味着无需 PDF 生成后端。浏览器在本地处理文档创建、渲染和文件输出。生成的 PDF 从 VFS 中读为字节数组,并作为标准 Blob 下载。
代价是初始 WASM 下载大小,这是每次会话的一次性成本。对于频繁生成文档的应用,后续生成速度很快,因为模块已加载完毕。对于非常大或复杂的 PDF,客户端性能取决于用户设备和可用内存。
常见用例包括发票、报表、报价单、证书、收据以及其他从应用数据生成的结构化业务文档。具体布局会变化,但底层流程——创建、添加页面、绘制、保存、下载——保持不变。
如果 window.spirepdf 未定义,请确保在使用 API 之前完全初始化 WebAssembly 运行时:
const commonModule = await import('/node_modules/spire.office/spire.common.js');
await commonModule.initializeWasm();
await import('/node_modules/spire.office/spire.pdf.js');
如果下载未触发或文件为空,请验证在从 VFS 读取之前已调用 SaveToFile。文件必须先存在于 VFS 中,FS.readFile 才能读取:
doc.SaveToFile({ fileName: 'Output.pdf' });
doc.Close();
// 保存后才能读取
const fileArray = window.dotnetRuntime.Module.FS.readFile('Output.pdf');
Spire.PDF for JavaScript 提供了一种实用的方式,使用 JavaScript 和 WebAssembly 直接在浏览器中生成 PDF 文件。五步流程——创建、添加页面、绘制、保存、下载——涵盖了从简单文本文档到包含表格和金额汇总的结构化发票的所有场景。您可以申请 30 天免费许可证在购买前评估所有功能。
表单类 PDF 的价值都压在填写结果上,可文件一旦归档或转交,数据就被锁在版式里面。想知道某个字段填了什么,只能打开阅读器逐个复制;字段一多,人工誊抄既慢又容易看错。要拿这些值去做校验、入库或者跟单,前提是程序能先把它们读出来。
本文用 Spire.PDF for JavaScript 从已有 PDF 中提取表单域的值:遍历字段集合,逐个判断字段类型,再按类型读出文本框、列表框、下拉框、单选按钮与复选框各自的当前取值。Spire.PDF for JavaScript 基于 WebAssembly 在浏览器端打开并解析文档,读取全程在本地完成,通过虚拟文件系统(VFS)读写文件,不需要后端配合。
本文介绍一个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。
Spire.PDF for JavaScript 提供 PdfFormWidget,用于接管文档里已有的表单域;FieldsWidget 就是它的字段集合,可以按下标逐个取出。各类字段的取值属性并不统一:文本框在 Text 上,复选框要看 Checked,列表框与下拉框分选项集合和选中值两项,单选按钮则直接读 Value,所以取到字段后先按类型分派、再读对应的值,并把类型名一并写进结果,文档里有哪些字段不必事先知道。
function App() {
const getAllFieldValues = 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);
// 用文档的表单句柄构造 PdfFormWidget,FieldsWidget 是它的字段集合
const formWidget = new pdfModule.PdfFormWidget(doc.Form.H);
const fields = formWidget.FieldsWidget;
let report = '';
// 遍历字段集合,逐项判断类型并读取对应的值
for (let i = 0; i < fields.Count; i++) {
const field = fields.get_Item({ index: i });
// 类型名与取值都按字段类型分派填写
let type = 'Unknown';
let value = '(未识别的字段类型)';
if (field instanceof pdfModule.PdfTextBoxFieldWidget) {
// 文本框域:直接读 Text
type = 'TextBox';
value = field.Text;
} else if (field instanceof pdfModule.PdfListBoxWidgetFieldWidget) {
// 列表框域:Values 是全部选项,SelectedValue 是当前选中项
const options = [];
for (let j = 0; j < field.Values.Count; j++) {
options.push(field.Values.get_Item(j).Value);
}
type = 'ListBox';
value = `选中 ${field.SelectedValue},可选 ${options.join('、')}`;
} else if (field instanceof pdfModule.PdfComboBoxWidgetFieldWidget) {
// 下拉框域:与列表框一样有选项集合与选中值
const options = [];
for (let j = 0; j < field.Values.Count; j++) {
options.push(field.Values.get_Item(j).Value);
}
type = 'ComboBox';
value = `选中 ${field.SelectedValue},可选 ${options.join('、')}`;
} else if (field instanceof pdfModule.PdfRadioButtonListFieldWidget) {
// 单选按钮域:Value 是选中的那一项
type = 'RadioButton';
value = `选中 ${field.Value}`;
} else if (field instanceof pdfModule.PdfCheckBoxWidgetFieldWidget) {
// 复选框域:勾选状态由 Checked 给出,不是 Value
type = 'CheckBox';
value = field.Checked ? '已勾选' : '未勾选';
}
report += `字段 "${field.Name}"(${type}):${value}\n`;
}
const outputFileName = '所有字段的值.txt';
window.dotnetRuntime.Module.FS.writeFile(outputFileName, 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={getAllFieldValues}>
开始提取
</button>
</div>
);
}
export default App;
遍历全部表单域汇总出的取值:

Value 读不到勾选状态原因:复选框控件域(PdfCheckBoxWidgetFieldWidget)没有 Value 属性,读出来是 undefined。它在 PDF 里靠导出值(export value)表达状态:未勾选是 Off,勾选后是 Yes 或自定义的导出值,字符串形式的取值判断不了有没有勾上。
解决:勾选状态改用 Checked 判断:
// 是否勾选看 Checked,而不是 Value
const checked = field.Checked;
SelectedValue 还是 Values原因:这两个域的 Values 是选项全集,遍历它拿到的是全部可选项,而且每一项是 PdfListWidgetItem,还要再取 .Value 才是选项文本;用户实际选中的那一项在 SelectedValue 上。把 Values 当成取值来用,拿到的就不是填写结果。
解决:取当前值读 SelectedValue;需要展示可选范围时再遍历 Values:
// 当前选中项的文本
const selected = field.SelectedValue;
// 全部可选项
const options = [];
for (let j = 0; j < field.Values.Count; j++) {
options.push(field.Values.get_Item(j).Value);
}
原因:读取表单要先能把文档打开;文档受密码保护时,LoadFromFile 缺密码就会在加载阶段直接抛错,不会返回一个空文档。
解决:把打开密码作为 LoadFromFile 的第二个参数传入:
doc.LoadFromFile(inputFileName, 'spire123');
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
表格做成 PDF 之后,数据就跟着版式一起被封住了。同一份资料发给十个人,收回来的是十份各自填过的 PDF;想把其中一份的内容落到别的模板上,只能对着屏幕一个字段一个字段地抄。表单域本身是有名字的,值也挂在名字下面,可只要离开阅读器,这层结构就取不出来。
本文用 Spire.PDF for JavaScript 把表单域里的值导出成数据文件,再把数据文件导回空白表单。ExportData 与 ImportData 都支持 Xml、Fdf、XFdf 三种格式——三者只是 DataFormat 的枚举差异,调用方式完全相同,区别仅在写出的文件结构。下面的代码以 XML 跑通全流程,FDF、XFDF 的写法并列在注释里,去掉注释即可切换。Spire.PDF for JavaScript 基于 WebAssembly 在浏览器端读写文档,全过程在本地完成,通过虚拟文件系统(VFS)读写文件,不需要后端配合。
本文介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。
PdfFormWidget.ExportData 把表单域里的值写成一个数据文件,格式由第二个参数 DataFormat 指定。三种格式存的是同一份字段值,差别在文件结构:
| 数据格式 | 文件结构 |
|---|---|
DataFormat.Xml |
Adobe 表单数据 XML,字段名作元素名、取值作元素内容 |
DataFormat.Fdf |
表单数据格式(FDF),以 %FDF- 开头的文本结构,/T 存字段名、/V 存取值 |
DataFormat.XFdf |
XFDF,标准 XML,每个字段一个 <field name="…">,取值在 <value> 里 |
第三个参数是表单名,无名的 AcroForm 传空串即可。
function App() {
const exportFormData = 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);
// 用文档的表单句柄构造 PdfFormWidget,才能访问数据导出接口
const formWidget = new pdfModule.PdfFormWidget(doc.Form.H);
// 本次演示导出 XML
const dataFiles = [
{ fileName: '表单数据.xml', format: pdfModule.DataFormat.Xml },
// { fileName: '表单数据.fdf', format: pdfModule.DataFormat.Fdf },
// { fileName: '表单数据.xfdf', format: pdfModule.DataFormat.XFdf },
];
for (const item of dataFiles) {
// 第三个参数是表单名,无名表单传空串
formWidget.ExportData(item.fileName, item.format, '');
}
doc.Close();
// 从 VFS 读取生成的文件,触发下载
for (const item of dataFiles) {
const fileArray = window.dotnetRuntime.Module.FS.readFile(item.fileName);
const blob = new Blob([fileArray], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = item.fileName;
a.click();
URL.revokeObjectURL(url);
}
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>导出表单数据</h1>
<button onClick={exportFormData}>
开始导出
</button>
</div>
);
}
export default App;
导出的 XML 表单数据文件:

PdfFormWidget.ImportData 读入数据文件,按字段名把值写回表单域;第二个参数 DataFormat 只决定解析方式,与文件扩展名无关,三种格式通用。
导入的对象是空白表单。模板留空分发,回收到数据文件后再挨个填回去,字段多的时候不必重新录一遍。
function App() {
const importFormData = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 将待回填的空白表单载入 VFS
const inputFileName = '空白客户信息表.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// 本次演示回填 XML 数据文件
const dataFiles = [
{ fileName: '表单数据.xml', format: pdfModule.DataFormat.Xml, outputFileName: '导入XML数据.pdf' },
// { fileName: '表单数据.fdf', format: pdfModule.DataFormat.Fdf, outputFileName: '导入FDF数据.pdf' },
// { fileName: '表单数据.xfdf', format: pdfModule.DataFormat.XFdf, outputFileName: '导入XFDF数据.pdf' },
];
for (const item of dataFiles) {
// 数据文件同样要先载入 VFS
await window.spire.FetchFileToVFS(item.fileName, "", `${process.env.PUBLIC_URL}/data/`);
const doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// 读入数据文件,把值按字段名写回表单域
const formWidget = new pdfModule.PdfFormWidget(doc.Form.H);
formWidget.ImportData(item.fileName, item.format);
doc.SaveToFile(item.outputFileName);
doc.Close();
// 从 VFS 读取生成的文件,触发下载
const fileArray = window.dotnetRuntime.Module.FS.readFile(item.outputFileName);
const blob = new Blob([fileArray], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = item.outputFileName;
a.click();
URL.revokeObjectURL(url);
}
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>导入表单数据</h1>
<button onClick={importFormData}>
开始导入
</button>
</div>
);
}
export default App;
XML 数据导入后回填完成的表单:

原因:导入按字段名匹配,数据文件里的名字与表单域名称必须完全一致(区分大小写与空格)。对不上的字段会被直接跳过,既不报错也不返回提示,只有匹配上的字段被填值。
解决:先遍历字段集合打印出真实名称,再用它核对数据文件:
const fields = formWidget.FieldsWidget;
for (let i = 0; i < fields.Count; i++) {
console.log(fields.get_Item({ index: i }).Name);
}
原因:三者存的字段值相同,差别在结构与工具支持。Fdf 体积最小、以 %FDF- 开头,适合只在表单程序之间传递;XFdf 和 Xml 都是 XML,可以直接打开查看、也能用文本工具比对差异,跨工具交换更稳;Xml 的字段名就是元素名,结构最直白。
解决:程序内部往返用 Fdf;要进版本库、要人工检查或对接其他系统用 XFdf;只需要一份可读的字段名与取值清单,用 Xml。
原因:ImportData 按第二个参数指定的格式解析文件,不看扩展名。内容与格式不符时第一步就失败:XML 报 Xml_MessageWithErrorPosition, Xml_InvalidRootData,FDF 报 The source is not a valid FDF file because it does not start with "%FDF-"。
解决:按文件真实的格式传入对应的 DataFormat;数据文件用导出的原件,不要改完之后另存成别的格式。
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
Spire.PDF 12.9.18 现已正式发布。该版本支持以流式布局模式添加内容来创建 PDF,并调整了创建注释集合的方法。此外,还成功修复了一些在复制页面、创建表单字段和导入 FDF 时出现的问题。详情请查阅以下内容。
新功能:
// 创建 pdf 文档
PdfDocument pdf = new PdfDocument();
// 创建 PdfContentBuilder
PdfContentBuilder builder = new PdfContentBuilder(pdf);
// 添加第一节
var s1 = builder.AddSection();
s1.AddHeading1("Section One");
s1.AddParagraph("First flowing paragraph.");
// 添加第二节
var s2 = builder.AddSection();
s2.AddHeading1("Section Two");
s2.AddParagraph("Second flowing paragraph.");
// 添加第三节(只含段落)
var s3 = builder.AddSection();
s3.AddParagraph("Third section, only a paragraph.");
// 添加第四节,包含标题和导航条目
var s4 = builder.AddSection();
var h1 = s4.AddHeading(1, "Alpha Chapter");
var h2 = s4.AddHeading(2, "Beta Section");
var h3 = s4.AddHeading(3, "Gamma Subsection");
h1.AsNavigationEntry("Alpha Chapter", 1);
h2.AsNavigationEntry("Beta Section", 2);
h3.AsNavigationEntry("Gamma Subsection", 3);
s4.AddParagraph("Body copy under the headings.");
// 构建文档
var result = builder.Build();
// 保存文档
String outputFile = "Bug_1_MultipleSections.pdf";
pdf.SaveToFile(outputFile);
pdf.Close();
PdfDocument doc = new PdfDocument();
PdfPageBase page = doc.Pages.Add();
RectangleF rect = new RectangleF(140, 689, 80, 1);
PdfLineAnnotation lineAnnotation = new PdfLineAnnotation(page, rect, new PointF(140, 690), new PointF(220, 690));
lineAnnotation.Contents = "This is the first line annotation";
//新方式
PdfAnnotationCollection anmotationCollection = PdfAnnotationCollection.Create(page);
anmotationCollection.Add(lineAnnotation);
//原方式
//page.AnnotationsV2.Add(lineAnnotation);
问题修复:
Spire.Doc for Java 14.9.5 现已正式发布。该版本支持根据水印文本长度自适应字体大小。同时,一些在转换 Word 到 PDF、加载 DOC 文档保存为 DOCX 以及设置 setMimicWPSLayout(true) 时出现的问题也得以成功被修复。更多详情如下。
新功能:
TextWatermark wm = new TextWatermark(document);
wm.setText("水印文本");
// Automatic font size
wm.setFontSize(0);
document.setWatermark(wm);
问题修复:
上传前要校验页数、批量打印前要估纸张、按页拆分前要先知道文档规模——这类场景要的都只是一个数字:文档有几页。在浏览器里拿到这个数字并不顺手:渲染库只管把页面画出来,页数得自己数;绕开这一层,就只能把文件送到服务端去解析。
本文介绍用 Spire.PDF for JavaScript 获取 PDF 文档的页数。它基于 WebAssembly 在浏览器端直接加载与解析 PDF 文档,全过程在本地完成,通过虚拟文件系统(VFS)读写文件,无需后端配合。本文将向您详细介绍如何统计文档的总页数。
有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。
载入文档之后,doc.Pages 就是文档的页面集合,Count 给出集合里的页面数量,也就是文档的总页数——不必逐页遍历累加。
function App() {
const getPageCount = 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);
// Pages 是文档的页面集合,Count 即总页数
const pageCount = doc.Pages.Count;
// 统计结果写入 VFS
const outputFileName = '页数统计结果.txt';
const report = `文档:${inputFileName}\r\n总页数:${pageCount} 页`;
window.dotnetRuntime.Module.FS.writeFile(outputFileName, 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>获取 PDF 文档页数</h1>
<button onClick={getPageCount}>
开始统计
</button>
</div>
);
}
export default App;
统计结果输出为文本文件,记录文档的总页数:

原因:Pages.Count 数的是物理页数,即文档里实际有多少个页面。阅读器上那个数字来自页码标签(/PageLabels),可以另行设置:封面不计入编号、目录用罗马数字、正文再从 1 重新开始,都是常见做法。这样一来,第 5 个物理页在阅读器里可能显示成 iii 或 1。
解决:要拿的是显示编号,就读页面自身的 PageLabel,而不是 Count:
// 第 5 个物理页在阅读器里显示成什么编号
const page = doc.Pages.get_Item(4);
console.log(page.PageLabel);
文档没有设置页码标签时,PageLabel 返回空字符串——这时显示编号与物理页序一致,直接用 Count 即可。
原因:文档设了打开密码,LoadFromFile 不提供密码时连内容都读不到,页数自然也无从取得。这一层校验发生在加载阶段,读 Pages.Count 之前就已经抛出。
解决:把打开密码作为 LoadFromFile 的第二个参数传入,之后再读页数:
// 第二个参数即打开密码
doc.LoadFromFile(inputFileName, 'spire123');
const pageCount = doc.Pages.Count;
Count 当循环上界,最后一页报索引越界原因:Pages 的索引从 0 开始,合法范围是 0 到 Count - 1。循环写成 i <= doc.Pages.Count 时,最后一轮会去取第 Count 个页面,get_Item 抛出越界异常——底层是 .NET 的 ArgumentOutOfRangeException,经 WASM 运行时包装成一个 JS Error,消息为 ArgumentOutOfRange_IndexMustBeLess Arg_ParamName_Name, index。异常名是笼统的 Error,认这条消息即可。
解决:循环条件用 <:
// 上界是 Count - 1,条件用 < 而不是 <=
for (let i = 0; i < doc.Pages.Count; i++) {
const page = doc.Pages.get_Item(i);
}
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
公式、化学式、单位符号、脚注角标都带着上下标:a² + b² = c²、H₂SO₄、30m²。把这些排进 PDF 时,一行普通的文字画下去是不对的——上下标要比正文小一号,基线还要抬上去或沉下来,位置差一点整条式子就散了。
本文介绍用 Spire.PDF for JavaScript 在 PDF 页面中绘制上标和下标。它基于 WebAssembly 在浏览器端直接创建与保存 PDF 文档,全过程在本地完成,通过虚拟文件系统(VFS)读写文件,无需后端配合。
本文介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。
文字排成上标还是下标由 PdfStringFormat 的 SubSuperScript 决定:取 PdfSubSuperScript.SuperScript 时基线抬升并缩小字号,SubScript 则下沉。它作用的是一次 DrawString 画出的整段文字,所以基文本与上下标要分两次画,后一段的落点用 MeasureString 量出的宽度续接。
function App() {
const drawSuperAndSubScript = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 将宋体载入 VFS,供页面文字使用
await window.spire.FetchFileToVFS('SIMSUN.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 创建 PDF 文档并添加一个空白页面
const doc = new pdfModule.PdfDocument();
const page = doc.Pages.Add();
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 16 });
const brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Black() }) });
// 下标:基文本照常画
let text = '水的化学式是 H';
page.Canvas.DrawString({ s: text, font: font, brush: brush, x: 40, y: 110 });
// 量出基文本宽度,下标紧接在它右侧落笔
let x = 40 + font.MeasureString({ text: text }).Width;
// SubSuperScript 取 SubScript,这段文字自动缩小并下沉
const subFormat = new pdfModule.PdfStringFormat();
subFormat.SubSuperScript = pdfModule.PdfSubSuperScript.SubScript;
page.Canvas.DrawString({ s: '2', font: font, brush: brush, x: x, y: 110, format: subFormat });
// 上标同样分两次画,落点续在基文本右侧
text = '质能方程 E = mc';
page.Canvas.DrawString({ s: text, font: font, brush: brush, x: 40, y: 170 });
x = 40 + font.MeasureString({ text: text }).Width;
// SubSuperScript 取 SuperScript,这段文字自动缩小并抬升
const superFormat = new pdfModule.PdfStringFormat();
superFormat.SubSuperScript = pdfModule.PdfSubSuperScript.SuperScript;
page.Canvas.DrawString({ s: '2', font: font, brush: brush, x: x, y: 170, format: superFormat });
// 定义输出文件名并保存文档
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={drawSuperAndSubScript}>
开始绘制
</button>
</div>
);
}
export default App;
SubSuperScript 的两次取值分别画出的下标与上标:

SubSuperScript 一次只能把整段文字排成同一种形态,一行里与正文交替出现的上下标就得自己排:用 MeasureString 量出已绘文字的宽度推进 x,再换小一号的字体、按磅值偏移 y 落笔。字号取多大、抬升多少磅全由你给,不限于库内建的那套比例。
function App() {
const drawInlineSuperAndSubScript = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 将宋体载入 VFS,供页面文字使用
await window.spire.FetchFileToVFS('SIMSUN.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 创建 PDF 文档并添加一个空白页面
const doc = new pdfModule.PdfDocument();
const page = doc.Pages.Add();
const brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Black() }) });
// 正文 16 磅,上下标自己挑字号,这里取 10 磅
const BASE_SIZE = 16;
const MARK_SIZE = 10;
const baseFont = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: BASE_SIZE });
const markFont = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: MARK_SIZE });
// 上标抬升 8 磅、下标下沉 6 磅;y 轴向下,抬升取减法
const SUPER_RISE = 8;
const SUB_SINK = 6;
// 逐段落笔:每画完一段就用该段的字体量出宽度,把 x 推到它右端
const writeRuns = (runs, lineY) => {
let x = 40;
runs.forEach((run) => {
const runFont = run.kind === 'base' ? baseFont : markFont;
// 落点是文字的左上角,基线在其下方一个字号的长度处;小字号先补回这段落差
const baselineFix = run.kind === 'base' ? 0 : BASE_SIZE - MARK_SIZE;
const riseFix = run.kind === 'super' ? -SUPER_RISE : run.kind === 'sub' ? SUB_SINK : 0;
const runY = lineY + baselineFix + riseFix;
page.Canvas.DrawString({ s: run.s, font: runFont, brush: brush, x: x, y: runY });
x += runFont.MeasureString({ text: run.s }).Width;
});
};
// 公式:a² + b² = c²
writeRuns([
{ s: 'a', kind: 'base' }, { s: '2', kind: 'super' },
{ s: ' + b', kind: 'base' }, { s: '2', kind: 'super' },
{ s: ' = c', kind: 'base' }, { s: '2', kind: 'super' },
], 110);
// 化学式:H₂SO₄
writeRuns([
{ s: 'H', kind: 'base' }, { s: '2', kind: 'sub' },
{ s: 'SO', kind: 'base' }, { s: '4', kind: 'sub' },
], 170);
// 定义输出文件名并保存文档
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={drawInlineSuperAndSubScript}>
开始绘制
</button>
</div>
);
}
export default App;
正文与小一号的上下标交替衔接,一行公式与一行化学式:

原因:SubSuperScript 设在 PdfStringFormat 上,作用范围是这一次 DrawString 画出的全部字符,而不是其中的某几个字。把基文本和上标放进同一次 DrawString,整段都会一起缩小、一起抬升。
解决:拆成两次绘制,基文本不带格式,上标单独用带格式的那一次画,落点用 MeasureString 量出的宽度续接:
// 基文本不带格式
page.Canvas.DrawString({ s: '质能方程 E = mc', font: font, brush: brush, x: 40, y: 170 });
// 量出基文本宽度,只把上标这一次排成上标
const x = 40 + font.MeasureString({ text: '质能方程 E = mc' }).Width;
const superFormat = new pdfModule.PdfStringFormat();
superFormat.SubSuperScript = pdfModule.PdfSubSuperScript.SuperScript;
page.Canvas.DrawString({ s: '2', font: font, brush: brush, x: x, y: 170, format: superFormat });
原因:落点是这段文字的左上角,不是基线——基线在落点下方一个字号的长度处,字号一变小,这段距离也跟着变短。直接按基文本的落点给上标一个偏移量,得到的并不是想要的基线位移:小字号的上标会抬得比预期更高,下标则可能刚好压在基线上。x 推进若用了另一个字号量出的宽度,两段之间不是重叠就是留缝。
解决:按「落点 + 字号 = 基线」把想要的基线位移换算回落点,宽度用真正落笔的字体量:
// 基文本的基线在落点下方一个字号的长度处
const baselineY = lineY + BASE_SIZE;
// 上标要把基线抬高 8 磅,落点 = 目标基线 − 该段自己的字号
const superY = baselineY - 8 - MARK_SIZE;
page.Canvas.DrawString({ s: '2', font: markFont, brush: brush, x: x, y: superY });
// 宽度用实际落笔的字体量
x += markFont.MeasureString({ text: '2' }).Width;
SubSuperScript 没有对应的参数原因:PdfStringFormat 上只提供 SubSuperScript 这一个开关,缩放比例与升降量由库固定,没有公开可调的比例值。
解决:需要别的比例就自己排——换个小一号的字体对象画上下标,偏移量按需要的磅值给(写法见功能点 2)。
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。