February 16, 20252 minutes
PDF.js是Mozilla开发的一个强大的JavaScript库,它允许在Web浏览器中直接渲染和操作PDF文档,无需任何插件。尽管PDF.js提供了基本的注释功能,但默认情况下这些注释是临时的,一旦刷新页面或关闭浏览器,所有的注释数据就会丢失。本文将详细介绍如何为PDF.js扩展注释的保存与恢复功能,使用户可以持久化保存他们的注释,并在任何时候恢复这些注释。
在深入讨论实现细节前,先了解一些基本概念:
我们的实现将分为三个主要部分:
我目前使用的是PDF.js-4.10.38版本,由于并未在EventBus中发现能够监听AnnotationLayer操作注释的事件,所以获取注释数据只能够一次性获取之前所有的注释数据 具体代码如下:
const { map } = PDFJSApplication.pdfDocument.annotationStorage.serializable;
const vals = Array.from(map.values());为了方便实现,我将数据直接使用localStorage保存到浏览器缓存中,具体可以根据自己的业务需要将注释信息以JSON格式传输到后端进行持久化存储。由于数据的特殊性,所以在进行JSON操作之前进行了相关数据的特殊处理。
const serializedVals = vals.map(val => {
const serializedVal = { ...val }
// quadPoints
if (serializedVal.quadPoints instanceof Float32Array) {
serializedVal._quadPointsType = 'Float32Array';
serializedVal.quadPoints = Array.from(serializedVal.quadPoints);
}
// path
if (serializedVal.paths) {
serializedVal.paths = { ...serializedVal.path}
if (serializedVal.paths.lines[0] instanceof Float32Array) {
serializedVal.paths._linesType = 'Float32Array';
serializedVal.paths.lines[0] = Array.from(serializedVal.paths.lines[0]);
}
if (serializedVal.paths.points[0] instanceof Float32Array) {
serializedVal.paths._pointsType = 'Float32Array';
serializedVal.paths.points[0] = Array.from(serializedVal.paths.points[0]);
}
}
return serializedVal;
});
const serializedData = JSON.stringify(serializedVals);localStorage.setItem("PDFJSAnnotations", serializedData);在PDFJS实例中获取每页pdfViewer的annotationEditorLayer,调用annotationEditorLayer的反序列化方法将注释内容恢复。
const layer = PDFJSApplication.pdfViewer.getPageView(0).annotationEditorLayer.annotationEditorLayer;
const editor = await layer.deserialize(localAnnotationData[0]);
layer.add(editor);在使用中发现这种方法拥有诸多bug,例如freeText类型的注释恢复后无法自动显示,需要出发注释模式变更后才能显示,以及ink类型的注释在绘制以后,如果没有进行触发注释模型变更,则无法通过序列化获取到注释数据。目前针对这些情况下方又补充了修复这些问题的完整代码
const serializeAnnotation = (annotationStorage) => {
const { serializable } = annotationStorage;
return [...serializable.map.entries()].map(([key, value]) => {
if (!value) return;
const serializedVal = { ...value };
// inklists
if (serializedVal.annotationType === 9 && !serializedVal.quadPoints) {
serializedVal.inkLists = serializedVal.outlines.points;
}
// quadPoints
if (serializedVal.quadPoints) {
serializedVal._quadPointsType = 'Float32Array';
serializedVal.quadPoints = [...serializedVal.quadPoints];
}
// path
if (serializedVal.paths) {
serializedVal.paths = { ...serializedVal.paths };
if (serializedVal.paths.lines?.[0]) {
serializedVal.paths._linesType = 'Float32Array';
serializedVal.paths.lines[0] = [...serializedVal.paths.lines[0]];
}
if (serializedVal.paths.points?.[0]) {
serializedVal.paths._pointsType = 'Float32Array';
serializedVal.paths.points[0] = [...serializedVal.paths.points[0]];
}
}
return serializedVal;
});
};
const deserializeAnnotation = (data) => {
const deserializeVals = [];
data.forEach(val => {
if (val.AnnotationContent === "") {
return;
}
const serializedVal = JSON.parse(val.AnnotationContent);
serializedVal._userKey = val.CreatedBy.userKey;
if (serializedVal._quadPointsType === 'Float32Array' && Array.isArray(serializedVal.quadPoints)) {
serializedVal.quadPoints = new Float32Array(serializedVal.quadPoints);
delete serializedVal._quadPointsType;
}
if (serializedVal.paths) {
if (serializedVal.paths.lines?.[0] && Array.isArray(serializedVal.paths.lines[0])) {
if (serializedVal.paths._linesType === 'Float32Array') {
serializedVal.paths.lines[0] = new Float32Array(serializedVal.paths.lines[0]);
delete serializedVal.paths._linesType;
}
}
if (serializedVal.paths.points?.[0] && Array.isArray(serializedVal.paths.points[0])) {
if (serializedVal.paths._pointsType === 'Float32Array') {
serializedVal.paths.points[0] = new Float32Array(serializedVal.paths.points[0]);
delete serializedVal.paths._pointsType;
}
}
}
deserializeVals.push(serializedVal);
});
return deserializeVals;
};