PDF.js中注释的持久化机制分析
通过对PDF.js代码库的分析,我可以详细解释PDF.js如何存储和持久化注释。以下是完整的工作流程:
1. 注释存储机制
AnnotationStorage类
AnnotationStorage 类(定义在 src/display/annotation_storage.js 中)是PDF.js中管理注释数据的核心组件:
- 使用 Map ( #storage )存储注释数据
- 提供 getValue 、 setValue 、 remove 、 has 和 size 等方法操作注释
- 通过 #modified 标志和回调函数跟踪修改状态
class AnnotationStorage {
#modified = false;
#modifiedIds = null;
#storage = new Map();
constructor() {
// 回调函数,用于通知修改状态的变化
this.onSetModified = null;
this.onResetModified = null;
this.onAnnotationEditor = null;
}
// 其他方法...
}
序列化机制
serializable getter方法是持久化的关键,它将存储的注释转换为可序列化的格式:
get serializable() {
if (this.#storage.size === 0) {
return SerializableEmpty;
}
const map = new Map(),
hash = new MurmurHash3_64(),
transfer = [];
const context = Object.create(null);
let hasBitmap = false;
for (const [key, val] of this.#storage) {
const serialized =
val instanceof AnnotationEditor
? val.serialize(/* isForCopying =
*/ false, context)
: val;
if (serialized) {
map.set(key, serialized);
hash.update(`${key}:${JSON.stringify
(serialized)}`);
hasBitmap ||= !!serialized.bitmap;
}
}
// 处理位图数据的传输
if (hasBitmap) {
for (const value of map.values()) {
if (value.bitmap) {
transfer.push(value.bitmap);
}
}
}
return map.size > 0
? { map, hash: hash.hexdigest(),
transfer }
: SerializableEmpty;
}
2. 注释修改状态跟踪
修改状态回调
在 app.js 中, _initializeAnnotationStorageCallbacks 方法设置了回调函数,用于在注释被修改时添加 beforeunload 事件监听器,防止用户意外关闭页面:
_initializeAnnotationStorageCallbacks(pdfDocument) {
if (pdfDocument !== this.pdfDocument) {
return;
}
const { annotationStorage } = pdfDocument;
annotationStorage.onSetModified = () => {
window.addEventListener("beforeunload", beforeUnload);
// ...
};
annotationStorage.onResetModified = () => {
window.removeEventListener("beforeunload", beforeUnload);
// ...
};
// ...
}
3. 保存注释到PDF文件
客户端保存流程
- 用户触发保存操作,调用 app.js 中的 save() 方法:
async save() {
if (this._saveInProgress) {
return;
}
this._saveInProgress = true;
await this.pdfScriptingManager.dispatchWillSave();
try {
const data = await this.pdfDocument.saveDocument();
this.downloadManager.download(data, this._downloadUrl, this._docFilename);
} catch (reason) {
console.error(`Error when saving the document:`, reason);
await this.download();
} finally {
await this.pdfScriptingManager.dispatchDidSave();
this._saveInProgress = false;
}
// ...
}
- pdfDocument.saveDocument() (在 api.js 中)获取序列化的注释数据并发送到worker:
saveDocument() {
if (this.annotationStorage.size <= 0) {
warn("saveDocument called while `annotationStorage` is empty");
}
const { map, transfer } = this.annotationStorage.serializable;
return this.messageHandler
.sendWithPromise(
"SaveDocument",
{
isPureXfa: !!this._htmlForXfa,
numPages: this._numPages,
annotationStorage: map,
filename: this._fullReader?.filename ?? null,
},
transfer
)
.finally(() => {
this.annotationStorage.resetModified();
});
}
服务器端处理
在 worker.js 中, SaveDocument 处理程序负责处理保存请求:
- 收集全局Promise,包括加载流、AcroForm、xref等
- 处理新注释:
- 获取新注释映射
- 更新结构树
- 为每个页面保存新注释
handler.on(
"SaveDocument",
async function ({ isPureXfa, numPages, annotationStorage, filename }) {
// 收集全局Promise
const globalPromises = [/* ... */];
const changes = new RefSetCache();
const promises = [];
const newAnnotationsByPage = !isPureXfa
? getNewAnnotationsMap(annotationStorage)
: null;
// ...
// 处理新注释
if (newAnnotationsByPage) {
// 处理结构树
// ...
// 为每个页面保存新注释
for (const [pageIndex, annotations] of newAnnotationsByPage) {
newAnnotationPromises.push(
pdfManager.getPage(pageIndex).then(page => {
// ...
return page.saveNewAnnotations(/* ... */);
})
);
}
// ...
}
// 处理每个页面的注释
for (let pageIndex = 0; pageIndex < numPages; pageIndex++) {
promises.push(
pdfManager.getPage(pageIndex).then(function (page) {
// ...
return page.save(handler, task, annotationStorage, changes);
})
);
}
// ...
// 执行增量更新
return incrementalUpdate({
originalData: stream.bytes,
xrefInfo: newXrefInfo,
changes,
xref,
// ...
});
}
);
- 在 document.js 中, saveNewAnnotations 方法处理新注释的保存:
async saveNewAnnotations(handler, task, annotations, imagePromises, changes) {
// ...
const pageDict = this.pageDict;
const annotationsArray = this.annotations.filter(/* ... */);
const newData = await AnnotationFactory.saveNewAnnotations(
partialEvaluator,
task,
annotations,
imagePromises,
changes
);
// 更新页面字典
for (const { ref } of newData.annotations) {
if (ref instanceof Ref && !existingAnnotations.has(ref)) {
annotationsArray.push(ref);
}
}
const dict = pageDict.clone();
dict.set("Annots", annotationsArray);
changes.put(this.ref, {
data: dict,
});
// ...
}
4. 打印时的注释处理
PrintAnnotationStorage 类是 AnnotationStorage 的特殊子类,用于打印时冻结序列化数据,防止脚本修改其内容:
class PrintAnnotationStorage extends
AnnotationStorage {
#serializable;
constructor(parent) {
super();
const { map, hash, transfer } = parent.
serializable;
// 创建数据的副本
const clone = structuredClone(map,
transfer ? { transfer } : null);
this.#serializable = { map: clone,
hash, transfer };
}
// ...
}
总结
PDF.js中的注释持久化机制是一个完整的流程:
- 使用 AnnotationStorage 类存储注释数据
- 通过 serializable getter将数据转换为可序列化格式
- 在客户端,通过 saveDocument() 方法将序列化数据发送到worker
- 在worker端, SaveDocument 处理程序处理保存请求,包括:
- 处理新注释
- 更新页面字典
- 执行增量更新
- 最终生成包含新注释的PDF数据,并通过 downloadManager 下载 这种设计允许用户在浏览器中编辑PDF注释,并将更改保存回PDF文件,而无需服务器端处理。
还原
如何从外部数据源加载注释到PDF.js
在PDF.js中,AnnotationStorage类是管理注释数据的核心。当你想要将之前保存的注释加载回PDF时,你可以使用以下方法:
- 获取序列化的注释数据:首先,你需要从PDF中获取序列化的注释数据并保存到数据库中。这可以通过annotationStorage.serializable属性实现,它会返回一个包含map、hash和transfer的对象。
- 加载注释数据:当用户再次打开PDF时,你可以从数据库中检索之前保存的注释数据,并将其加载回PDF.js的annotationStorage中。
具体实现方法
javascript
// 1. 保存注释到数据库(当用户编辑PDF时)
async function saveAnnotationsToDatabase
(pdfDocument, documentId) {
// 获取序列化的注释数据
const serialized = pdfDocument.
annotationStorage.serializable;
if (serialized.map) {
// 将Map转换为可存储的对象
const annotationsObj = Object.
fromEntries(serialized.map);
// 保存到数据库(使用你的数据库API)
await yourDatabaseAPI.saveAnnotations
(documentId, {
annotations: annotationsObj,
hash: serialized.hash
});
}
}
// 2. 从数据库加载注释(当用户打开PDF时)
async function loadAnnotationsFromDatabase
(pdfDocument, documentId) {
// 从数据库获取注释数据
const savedData = await yourDatabaseAPI.
getAnnotations(documentId);
if (savedData && savedData.annotations) {
// 遍历所有保存的注释
for (const [key, value] of Object.
entries(savedData.annotations)) {
// 将每个注释加载到annotationStorage中
pdfDocument.annotationStorage.setValue
(key, value);
}
}
}
收起代码
关键点
- setValue方法:这是将单个注释加载到annotationStorage中的核心方法。你需要为每个注释调用此方法,提供注释的ID和值。
- 测试环境中的_setValues方法:在PDF.js的测试代码中,有一个_setValues方法可以一次性加载多个注释,但这个方法只在测试环境中可用。在生产环境中,你需要使用setValue方法逐个加载注释。
- 注释的结构:保存的注释数据结构取决于注释的类型。对于表单字段,通常是{value: ...}形式;对于编辑器注释,结构会更复杂,包含位置、样式等信息。
- 注释的渲染:加载注释后,PDF.js会在渲染页面时自动显示这些注释。
通过这种方式,你可以实现将PDF注释保存到外部数据库,并在用户再次打开PDF时还原这些注释的功能,使得不同用户可以看到之前添加的注释。