PDF 书签(Bookmark)记录了文档的大纲结构,一个书签下面还可以挂子书签,层层嵌套形成一棵树。读取这些信息有不少实际用途——把它导出成目录清单、根据标题生成站内导航,或者定位到某一页继续处理。要做到这些,程序需要能够遍历整棵书签树,逐个取出节点的内容。
Spire.PDF for JavaScript 基于 WebAssembly 在浏览器端直接处理 PDF 文档,通过虚拟文件系统(VFS)管理输入输出文件,无需后端服务支持。提取书签的核心入口是 PdfDocument.Bookmarks 属性,它返回一个 PdfBookmarkCollection 集合,其中的每个 PdfBookmark 对象都可以继续访问其子书签集合,从而构成完整的书签树。每个书签节点提供了 Title、DisplayStyle 等属性用于读取外观信息,还可以通过 Destination.Page 结合 PdfPageCollection.IndexOf 得到该书签指向的页码。
本文介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。
提取 PDF 文档的全部书签
PdfDocument.Bookmarks 返回的是顶层书签集合,而书签本身可以包含下一级子书签。要取出文档中的全部书签,需要编写一个递归函数:逐层遍历 PdfBookmarkCollection,读出每个节点的 Title(书签标题)与 DisplayStyle(文字样式),并按层级缩进记录,最终汇总成一份完整的大纲清单。
function App() {
const extractAllBookmarks = 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);
// 存放提取结果的字符串
let content = 'PDF 文档中的全部书签:\r\n';
// 递归遍历书签集合,按层级缩进记录标题与文字样式
const collectBookmarks = (bookmarks, indent) => {
for (let i = 0; i < bookmarks.Count; i++) {
let bookmark = bookmarks.get_Item(i);
// 记录当前书签的标题与文字样式
content += indent + bookmark.Title + '(' + bookmark.DisplayStyle.toString() + ')\r\n';
// 若存在子书签,则递归处理并增加缩进
if (bookmark.Count > 0) {
collectBookmarks(bookmark, indent + ' ');
}
}
};
// 从顶层书签开始提取
collectBookmarks(doc.Bookmarks, '');
// 将提取结果写入文件并触发下载
const outputFileName = '提取书签结果.txt';
window.dotnetRuntime.Module.FS.writeFile(outputFileName, content);
doc.Close();
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={extractAllBookmarks}>
开始提取
</button>
</div>
);
}
export default App;
递归提取出的全部书签标题与文字样式清单

获取书签对应的页码
书签除了记录标题,还包含一个跳转目标。通过 PdfBookmark.Destination.Page 可以取得该书签指向的 PdfPage 对象,再借助 PdfPageCollection.IndexOf 得到它在文档中的索引。由于索引从 0 开始,加 1 即可得到阅读器中显示的页码。这一用法常用于把书签清单导出为“标题 — 页码”形式的目录。
function App() {
const getBookmarkPageNumber = 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);
// 逐一读取每个顶层书签指向的页码
let content = '书签与对应页码:\r\n';
for (let i = 0; i < doc.Bookmarks.Count; i++) {
let bookmark = doc.Bookmarks.get_Item(i);
// Destination.Page 给出书签指向的页面,IndexOf 得到其从 0 开始的索引
let pageNumber = doc.Pages.IndexOf(bookmark.Destination.Page) + 1;
content += bookmark.Title + ' —— 第 ' + pageNumber + ' 页\r\n';
}
// 将提取结果写入文件并触发下载
const outputFileName = '书签页码.txt';
window.dotnetRuntime.Module.FS.writeFile(outputFileName, content);
doc.Close();
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={getBookmarkPageNumber}>
开始提取
</button>
</div>
);
}
export default App;
每个书签的标题及其指向的页码

常见问题
为什么提取到的书签数量比阅读器大纲中看到的少
原因:doc.Bookmarks 只返回顶层书签集合,其 Count 也只统计当前这一层的节点数。嵌套在章节之下的子书签需要继续通过节点自身的集合访问,否则不会被计入。
解决:使用递归方式遍历整棵书签树,把每一层的节点都累加起来:
function countBookmarks(bookmarks) {
let total = 0;
for (let i = 0; i < bookmarks.Count; i++) {
total += 1;
// 递归累加子书签
total += countBookmarks(bookmarks.get_Item(i));
}
return total;
}
const total = countBookmarks(doc.Bookmarks);
为什么提取到的 DisplayStyle 总是 Regular
原因:PdfBookmark.DisplayStyle 返回的是书签在大纲面板中显示的文字样式,只有当书签自身被显式设置为 Bold、Italic 等样式时,读到的才不是默认的 Regular。它反映的是书签的外观设置,与书签标题在页面正文中用的字体无关。
解决:按枚举值原样记录即可;若只需区分是否加粗或斜体,可与 PdfTextStyle 的取值逐一比较:
let style = 'Regular';
if (bookmark.DisplayStyle === pdfModule.PdfTextStyle.Bold) {
style = 'Bold';
} else if (bookmark.DisplayStyle === pdfModule.PdfTextStyle.Italic) {
style = 'Italic';
}
为什么用 Destination.Page 得到的页码和阅读器显示的不一致
原因:PdfPageCollection.IndexOf 返回的是页面在集合中的索引,从 0 开始计数,而阅读器中显示的页码从 1 开始,因此直接使用索引会相差 1。
解决:在索引基础上加 1 即与阅读器显示一致:
// 索引从 0 开始,加 1 得到阅读器中的页码
let pageNumber = doc.Pages.IndexOf(bookmark.Destination.Page) + 1;
此外,如果书签指向的是文档中不存在的页面(例如目标页已被删除),Destination 可能为空,取值前应先做判空处理:
if (bookmark.Destination && bookmark.Destination.Page) {
let pageNumber = doc.Pages.IndexOf(bookmark.Destination.Page) + 1;
}
获取免费许可证
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。







