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 天的临时许可证。









