

PDF 文档默认只是一幅静态画面:读者能翻能看,文档本身却不会对任何操作作出反应。要让读者点一下图片就播放一段声音,或者点一下某块区域就跳到指定页、翻开内嵌的附件,就得给文档挂上「动作」(action)。这类交互过去要靠专门的排版软件逐项设置,或干脆放到服务端生成——前者门槛不低,后者意味着文件要离开本地。
Spire.PDF for JavaScript 基于 WebAssembly 在浏览器端加载、修改与保存 PDF 文档,动作全部在本地生成,通过虚拟文件系统(VFS)读写,无需后端配合。本文用 PdfActionAnnotation 承载四种动作:PdfGoToAction 跳到文档内的页、PdfSoundAction 在点击图片时播放音频、PdfEmbeddedGoToAction 打开内嵌的附件、PdfLaunchAction 打开文档外部的文件。
本文介绍四个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。
跳转动作把阅读位置从当前页带到文档里的另一处。
function App() {
const addGoToAction = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 将含中文字形的字体载入 VFS 的字体目录
await window.spire.FetchFileToVFS('SIMSUN.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 将待处理的 PDF 载入 VFS
const inputFileName = '示例文档.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// 创建 PdfDocument 对象并加载文档
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// 第一页放跳转入口,第二页当落点
let firstPage = doc.Pages.get_Item(0);
let secondPage = doc.Pages.get_Item(1);
// 用目标页构造落点,页面顶部、缩放 100%
let destination = new pdfModule.PdfDestination({ page: secondPage });
destination.Mode = pdfModule.PdfDestinationMode.Location;
destination.Location = new pdfModule.PointF(0, 0);
destination.Zoom = 1;
// 把落点包成跳转动作
let action = new pdfModule.PdfGoToAction({ destination: destination });
// 在第一页画一个按钮,矩形即点击热区
let bounds = new pdfModule.RectangleF({ x: 72, y: 720, width: 200, height: 28 });
let font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 12 });
let format = new pdfModule.PdfStringFormat({ alignment: pdfModule.PdfTextAlignment.Center, lineAlignment: pdfModule.PdfVerticalAlignment.Middle });
firstPage.Canvas.DrawRectangle({ brush: pdfModule.PdfBrushes.get_LightGray(), rectangle: bounds });
firstPage.Canvas.DrawString({ s: '跳转到第 2 页', font: font, brush: pdfModule.PdfBrushes.get_Black(), layoutRectangle: bounds, format: format });
// 动作注解覆盖按钮区域,点击即触发跳转
let annotation = new pdfModule.PdfActionAnnotation(bounds, action);
firstPage.Annotations.Add(annotation);
// 保存文档
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={addGoToAction}>
开始创建
</button>
</div>
);
}
export default App;
第一页底部出现一个「跳转到第 2 页」按钮,点击后阅读位置跳到第二页顶部:

声音动作把音频随文档一起分发,点击页面上的图片即可播放。
function App() {
const addSoundAction = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 将待处理的 PDF、提示音与图标载入 VFS
const inputFileName = '示例文档.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
const soundFileName = '提示音.wav';
await window.spire.FetchFileToVFS(soundFileName, "", `${process.env.PUBLIC_URL}/data/`);
const iconFileName = '声音图标.png';
await window.spire.FetchFileToVFS(iconFileName, "", `${process.env.PUBLIC_URL}/data/`);
// 创建 PdfDocument 对象并加载文档
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// 获取第一页
let firstPage = doc.Pages.get_Item(0);
// 用音频文件构造声音动作
let soundAction = new pdfModule.PdfSoundAction(soundFileName);
// 采样格式与音频一致:16 位、单声道、有符号 PCM、22050 Hz
soundAction.Sound.Bits = 16;
soundAction.Sound.Channels = pdfModule.PdfSoundChannels.Mono;
soundAction.Sound.Encoding = pdfModule.PdfSoundEncoding.Signed;
soundAction.Sound.Rate = 22050;
// 播放参数:音量 0.5、循环、与其它声音混音、非同步
soundAction.Volume = 0.5;
soundAction.Repeat = true;
soundAction.Mix = true;
soundAction.Synchronous = false;
// 把图标画进页面上的矩形,矩形即点击热区
let icon = pdfModule.PdfImage.FromFile(iconFileName);
let bounds = new pdfModule.RectangleF({ x: 72, y: 600, width: 160, height: 160 });
firstPage.Canvas.DrawImage({ image: icon, rectangle: bounds });
// 动作注解覆盖图标,点击图片即播放音频
let annotation = new pdfModule.PdfActionAnnotation(bounds, soundAction);
firstPage.Annotations.Add(annotation);
// 保存文档
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={addSoundAction}>
开始创建
</button>
</div>
);
}
export default App;
第一页出现一张扬声器图标,点击它时会播放内嵌音频。若希望音频改为在文档打开时自动播放,把同一个动作赋给 doc.AfterOpenAction 即可,但能否自动播放取决于阅读器,点击触发通常更稳妥:

内嵌文件跳转动作面向「文档夹带附件」的场景:附件先用 PdfAttachment 加进 Attachments 集合,再用 PdfEmbeddedGoToAction 指明打开哪个附件。
function App() {
const addEmbeddedGoToAction = 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/`);
// 将主文档与要内嵌的附件载入 VFS
const inputFileName = '示例文档.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
const attachmentFileName = '附件.pdf';
await window.spire.FetchFileToVFS(attachmentFileName, "", `${process.env.PUBLIC_URL}/data/`);
// 创建 PdfDocument 对象并加载文档
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// 获取第一页
let firstPage = doc.Pages.get_Item(0);
// 把附件 PDF 内嵌进文档
let attachment = new pdfModule.PdfAttachment(attachmentFileName);
doc.Attachments.Add({ attachment: attachment });
// 目标:内嵌文件的首页
let destination = new pdfModule.PdfDestination({ page: firstPage });
destination.Location = new pdfModule.PointF(0, 842);
destination.Zoom = 1;
// 用附件名 + 目标构造内嵌跳转动作,true 表示在新窗口中打开
let action = new pdfModule.PdfEmbeddedGoToAction(attachment.FileName, destination, true);
// 在第一页画一个按钮,矩形即点击热区
let bounds = new pdfModule.RectangleF({ x: 72, y: 720, width: 200, height: 28 });
let font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 12 });
let format = new pdfModule.PdfStringFormat({ alignment: pdfModule.PdfTextAlignment.Center, lineAlignment: pdfModule.PdfVerticalAlignment.Middle });
firstPage.Canvas.DrawRectangle({ brush: pdfModule.PdfBrushes.get_LightGray(), rectangle: bounds });
firstPage.Canvas.DrawString({ s: '打开内嵌附件', font: font, brush: pdfModule.PdfBrushes.get_Black(), layoutRectangle: bounds, format: format });
// 动作注解覆盖按钮区域,点击即打开附件
let annotation = new pdfModule.PdfActionAnnotation(bounds, action);
firstPage.Annotations.Add(annotation);
// 保存文档
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={addEmbeddedGoToAction}>
开始创建
</button>
</div>
);
}
export default App;
第一页出现一个「打开内嵌附件」按钮,点击后在新窗口打开内嵌的附件 PDF:

启动动作把点击交给操作系统:文档里只记一条文件路径,点击时由系统用关联程序打开该文件,文件本身不在 PDF 内。
与上一节的内嵌文件跳转相比,关键差别在「文件在哪」:内嵌跳转把文件装进 PDF、落到附件里的某一页,文件随文档一起分发;启动动作只在文档里留下一条路径,PDF 本身不含该文件,目标文件必须放在同一目录(或可解析的路径)下,点击才有反应。不少阅读器出于安全考虑还会直接拦截启动动作。
function App() {
const addLaunchAction = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 将含中文字形的字体载入 VFS 的字体目录
await window.spire.FetchFileToVFS('SIMSUN.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 将待处理的 PDF 载入 VFS
const inputFileName = '示例文档.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// 创建 PdfDocument 对象并加载文档
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// 获取第一页
let firstPage = doc.Pages.get_Item(0);
// 用外部文件路径构造启动动作;Relative 表示相对文档所在目录
const targetFileName = '附件.pdf';
let action = new pdfModule.PdfLaunchAction(targetFileName, pdfModule.PdfFilePathType.Relative);
action.IsNewWindow = true;
// 在第一页画一个按钮,矩形即点击热区
let bounds = new pdfModule.RectangleF({ x: 72, y: 720, width: 200, height: 28 });
let font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 12 });
let format = new pdfModule.PdfStringFormat({ alignment: pdfModule.PdfTextAlignment.Center, lineAlignment: pdfModule.PdfVerticalAlignment.Middle });
firstPage.Canvas.DrawRectangle({ brush: pdfModule.PdfBrushes.get_LightGray(), rectangle: bounds });
firstPage.Canvas.DrawString({ s: '打开外部文件', font: font, brush: pdfModule.PdfBrushes.get_Black(), layoutRectangle: bounds, format: format });
// 动作注解覆盖按钮区域,点击即交给系统打开该文件
let annotation = new pdfModule.PdfActionAnnotation(bounds, action);
firstPage.Annotations.Add(annotation);
// 保存文档
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={addLaunchAction}>
开始创建
</button>
</div>
);
}
export default App;
第一页出现一个「打开外部文件」按钮,点击后由系统打开与 PDF 放在同一目录的 附件.pdf:

原因:动作由 PdfActionAnnotation 承载,它只框定热区,不绘制任何外观——落盘后注解字典里只有 /Rect 与 /A,没有 /AP 外观流。页面渲染器不会为它画边框或下划线,光挂一个注解,读者无从知道哪里能点。
解决:让热区落在自己画出来的图形上,用 Canvas 画底、写文字,注解与图形共用同一组坐标:
let bounds = new pdfModule.RectangleF({ x: 72, y: 720, width: 200, height: 28 });
firstPage.Canvas.DrawRectangle({ brush: pdfModule.PdfBrushes.get_LightGray(), rectangle: bounds });
firstPage.Canvas.DrawString({ s: '跳转到第 2 页', font: font, brush: pdfModule.PdfBrushes.get_Black(), layoutRectangle: bounds, format: format });
firstPage.Annotations.Add(new pdfModule.PdfActionAnnotation(bounds, action));
原因:动作与音频都已写入文档——注解字典里记着 /A /S /Sound,音频也以流的形式内嵌——但阅读器是否播放内嵌声音由它自己决定,部分阅读器(尤其是浏览器内置的 PDF 查看器)默认不播放内嵌音频。
解决:换用支持声音动作的阅读器(如 Adobe Acrobat),点击图片即会播放。这段音频始终随文档一起分发,不依赖任何外部文件。
原因:PdfDestination.Location 与注解矩形一样,用的是左上角原点、y 向下的坐标,写进 PDF 后会换算成 PDF 原生的左下角坐标。按左下角的习惯给 y 值,读出来的落点就会和预期相反。
解决:一律按左上角原点给值,Location = new PointF(0, 0) 就表示页面顶部:
let destination = new pdfModule.PdfDestination({ page: secondPage });
destination.Mode = pdfModule.PdfDestinationMode.Location;
destination.Location = new pdfModule.PointF(0, 0);
destination.Zoom = 1;
原因:启动动作只在文档里留下一条文件路径(落盘为 /A << /S /Launch /F ... >>),目标文件并不在 PDF 内。文件不在文档同目录(相对路径)或路径无法解析时,点击自然没有反应;此外,出于安全考虑,许多阅读器会把启动动作整个拦下。
解决:把目标文件与 PDF 放在同一目录再打开;若希望文件随文档一起分发,改用内嵌文件跳转,把文件装进 PDF,而不是引用外部路径。
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。
合同、审批单定稿之后常要盖一枚「已核准」的图章,或在页角留一行时间戳,标记这份文件由谁在什么时候确认过。桌面软件做这件事要一枚枚手动盖,文件一多就很吃力;把文档传到服务端处理,又等于让文件离开了本地。
Spire.PDF for JavaScript基于 WebAssembly 在浏览器端加载、修改与保存 PDF 文档,图章在本地生成,通过虚拟文件系统(VFS)读写,无需后端配合。它把图章当作一类注释处理:PdfRubberStampAnnotation决定图章在页面上的位置与大小,外观则由 PdfAppearance挂上的 PdfTemplate决定——在模板上画图片就是图像章,画文字就是文字章。
本文介绍三个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。
Spire.PDF for JavaScript 提供 PdfRubberStampAnnotation类,用于在页面的指定位置放置一枚图章。它只框定图章的位置与大小,外观由 PdfAppearance决定。
function App(){const addImageStamp = async () =>{// 获取 Spire.PDF WASM 模块 const pdfModule = window.wasmModule?.spirepdf;// 检查模块是否就绪 if (!pdfModule){alert('Spire.PDF is not ready yet');return;}// 将待处理的 PDF 与印章图片载入 VFS const inputFileName = '示例文档.pdf';await window.spire.FetchFileToVFS(inputFileName,"",`${process.env.PUBLIC_URL}/data/`);const stampImageName = '印章.png';await window.spire.FetchFileToVFS(stampImageName,"",`${process.env.PUBLIC_URL}/data/`);// 创建 PdfDocument 对象并加载文档 let doc = new pdfModule.PdfDocument();doc.LoadFromFile(inputFileName);// 获取第一页 let page = doc.Pages.get_Item(0);// 载入印章图片 let image = pdfModule.PdfImage.FromFile(stampImageName);// 把图片缩到 120 磅 const maxSize = 120;const scale = Math.min(maxSize / image.Width,maxSize / image.Height,1);const stampWidth = image.Width * scale;const stampHeight = image.Height * scale;// 创建与图章等大的模板,并把图片铺满模板 let template = new pdfModule.PdfTemplate({width:stampWidth,height:stampHeight});template.Graphics.DrawImage({image:image,x:0,y:0,width:stampWidth,height:stampHeight});// 在页面右下角框出图章的位置,矩形与模板等大 let rect = new pdfModule.RectangleF({location:new pdfModule.PointF(page.ActualSize.Width - stampWidth - 40,page.ActualSize.Height - stampHeight - 40),size:new pdfModule.SizeF({width:stampWidth,height:stampHeight})});// 创建图章注释,并把模板设成它的外观 let stamp = new pdfModule.PdfRubberStampAnnotation({rectangle:rect});let appearance = new pdfModule.PdfAppearance(stamp);appearance.Normal = template;stamp.Appearance = appearance;// 将图章注释添加到页面的注释集合中 page.AnnotationsWidget.Add(stamp);// 保存文档 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={addImageStamp}>开始添加 </button></div>);}export default App;在页面右下角盖上图像图章后的 PDF 文档:

Spire.PDF for JavaScript 还提供 PdfPath、PdfPen与 PdfTrueTypeFont,用于把文字画成图章。PdfPath用四段圆弧拼出圆角矩形,DrawPath把它描边成外框,再用 DrawString在框内写上文字,得到的是一枚带边框的文字图章。
function App(){const addTextStamp = async () =>{// 获取 Spire.PDF WASM 模块 const pdfModule = window.wasmModule?.spirepdf;// 检查模块是否就绪 if (!pdfModule){alert('Spire.PDF is not ready yet');return;}// 将含中文字形的字体载入 VFS 的字体目录 await window.spire.FetchFileToVFS('SIMSUN.TTF','/Library/Fonts/',`${process.env.PUBLIC_URL}/font/`);// 将待处理的 PDF 载入 VFS const inputFileName = '示例文档.pdf';await window.spire.FetchFileToVFS(inputFileName,"",`${process.env.PUBLIC_URL}/data/`);// 创建 PdfDocument 对象并加载文档 let doc = new pdfModule.PdfDocument();doc.LoadFromFile(inputFileName);// 获取第一页 let page = doc.Pages.get_Item(0);// 创建承载图章的模板 let template = new pdfModule.PdfTemplate({width:125,height:55});// 用 VFS 中的字体文件构造字体,并准备画笔 let font = new pdfModule.PdfTrueTypeFont({fontFile:'/Library/Fonts/SIMSUN.TTF',size:12,style:pdfModule.PdfFontStyle.Bold});let brush = new pdfModule.PdfSolidBrush({pdfRGBColor:new pdfModule.PdfRGBColor({color:pdfModule.Color.get_DarkRed()})});let pen = new pdfModule.PdfPen({brush:brush});// 用四段圆弧加一条直线拼出圆角矩形路径 const radius = 20;let path = new pdfModule.PdfPath();path.AddArc({x:0,y:0,width:radius,height:radius,startAngle:180,sweepAngle:90});path.AddArc({x:template.Width - radius,y:0,width:radius,height:radius,startAngle:270,sweepAngle:90});path.AddArc({x:template.Width - radius,y:template.Height - radius,width:radius,height:radius,startAngle:0,sweepAngle:90});path.AddArc({x:0,y:template.Height - radius,width:radius,height:radius,startAngle:90,sweepAngle:90});path.AddLine({x1:0,y1:template.Height - radius,x2:0,y2:radius / 2});// 把路径描边成图章的外框 template.Graphics.DrawPath({pen:pen,path:path});// 在外框内写上两行文字 template.Graphics.DrawString({s:'已核准',font:font,brush:brush,point:new pdfModule.PointF(40,10)});template.Graphics.DrawString({s:'市场营销部',font:font,brush:brush,point:new pdfModule.PointF(30,32)});// 在页面右下角框出图章的位置,矩形与模板等大 let rect = new pdfModule.RectangleF({location:new pdfModule.PointF(page.ActualSize.Width - template.Width - 40,page.ActualSize.Height - template.Height - 40),size:template.Size});// 创建图章注释,并把模板设成它的外观 let stamp = new pdfModule.PdfRubberStampAnnotation({rectangle:rect});let appearance = new pdfModule.PdfAppearance(stamp);appearance.Normal = template;stamp.Appearance = appearance;// 将图章注释添加到页面的注释集合中 page.AnnotationsWidget.Add(stamp);// 保存文档 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={addTextStamp}>开始添加 </button></div>);}export default App;带圆角边框与「已核准」文字的文字图章:

Spire.PDF for JavaScript 还提供 PdfTemplate,用于把任意内容渲染成图章外观。下面的代码演示了在模板上画一行当前时间:字符串用浏览器原生的 Date生成,DrawString接受的就是一段普通字符串,图章内容便会随生成时机变化。
function App(){const addDateTimeStamp = async () =>{// 获取 Spire.PDF WASM 模块 const pdfModule = window.wasmModule?.spirepdf;// 检查模块是否就绪 if (!pdfModule){alert('Spire.PDF is not ready yet');return;}// 将含中文字形的字体载入 VFS 的字体目录 await window.spire.FetchFileToVFS('SIMSUN.TTF','/Library/Fonts/',`${process.env.PUBLIC_URL}/font/`);// 将待处理的 PDF 载入 VFS const inputFileName = '示例文档.pdf';await window.spire.FetchFileToVFS(inputFileName,"",`${process.env.PUBLIC_URL}/data/`);// 创建 PdfDocument 对象并加载文档 let doc = new pdfModule.PdfDocument();doc.LoadFromFile(inputFileName);// 获取第一页 let page = doc.Pages.get_Item(0);// 获取当前日期时间 const now = new Date();const pad = (value) =>String(value).padStart(2,'0');let timeString = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;// 创建模板,宽高按文字长度预留 let template = new pdfModule.PdfTemplate({width:150,height:16});// 准备字体与画笔 let font = new pdfModule.PdfTrueTypeFont({fontFile:'/Library/Fonts/SIMSUN.TTF',size:12});let brush = new pdfModule.PdfSolidBrush({pdfRGBColor:new pdfModule.PdfRGBColor({color:pdfModule.Color.get_Blue()})});// 把时间文字画到模板上 template.Graphics.DrawString({s:timeString,font:font,brush:brush,point:new pdfModule.PointF(0,0)});// 在页面右下角框出图章的位置,矩形与模板等大 let rect = new pdfModule.RectangleF({location:new pdfModule.PointF(page.ActualSize.Width - template.Width - 20,page.ActualSize.Height - template.Height - 20),size:template.Size});// 创建图章注释,并把模板设成它的外观 let stamp = new pdfModule.PdfRubberStampAnnotation({rectangle:rect});let appearance = new pdfModule.PdfAppearance(stamp);appearance.Normal = template;stamp.Appearance = appearance;// 将图章注释添加到页面的注释集合中 page.AnnotationsWidget.Add(stamp);// 保存文档 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={addDateTimeStamp}>开始添加 </button></div>);}export default App;页角记录了当前日期时间的图章:

原因:图章在页面上的位置和大小由 PdfRubberStampAnnotation的矩形决定,而外观又铺满这个矩形。模板尺寸与矩形尺寸不一致时,外观会被拉伸;矩形坐标若直接套用图片的像素宽高而不缩放,图章还会盖过正文甚至越出页面。
解决:先算好目标显示尺寸,让模板与矩形共用同一组宽高,再按页面尺寸定位:
// 模板与矩形用同一组宽高 let template = new pdfModule.PdfTemplate({width:stampWidth,height:stampHeight});let rect = new pdfModule.RectangleF({location:new pdfModule.PointF(page.ActualSize.Width - stampWidth - 40,page.ActualSize.Height - stampHeight - 40),size:template.Size});原因:PdfTrueTypeFont通过 fontFile取字体文件,若该文件不含中文字形(如 ARIAL.TTF这类西文字体),中文取不到字形,就渲染成一串方框。
解决:换用含中文字形的字体,并先把文件载入虚拟文件系统的 /Library/Fonts/:
await window.spire.FetchFileToVFS('SIMSUN.TTF','/Library/Fonts/',`${process.env.PUBLIC_URL}/font/`);const font = new pdfModule.PdfTrueTypeFont({fontFile:'/Library/Fonts/SIMSUN.TTF',size:12});原因:DrawString只画传入的字符串,格式完全由生成字符串的代码决定;toLocaleString之类的写法又依赖浏览器的区域设置,换一个环境输出可能就不一样。
解决:需要固定格式时,用 Date的取值方法自己拼:
const now = new Date();const pad = (value) =>String(value).padStart(2,'0');const timeString = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}${pad(now.getHours())}:${pad(now.getMinutes())}`;如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。