为PDF.js增加注释的保存与恢复功能

February 16, 20252 minutes

引言

PDF.js是Mozilla开发的一个强大的JavaScript库,它允许在Web浏览器中直接渲染和操作PDF文档,无需任何插件。尽管PDF.js提供了基本的注释功能,但默认情况下这些注释是临时的,一旦刷新页面或关闭浏览器,所有的注释数据就会丢失。本文将详细介绍如何为PDF.js扩展注释的保存与恢复功能,使用户可以持久化保存他们的注释,并在任何时候恢复这些注释。

技术背景

在深入讨论实现细节前,先了解一些基本概念:

  1. PDF.js: 一个纯JavaScript编写的PDF渲染引擎,可以直接在浏览器中解析和显示PDF文件。
  2. 注释(Annotations): PDF中的注释包括高亮、下划线、文本注释、绘图等多种形式。
  3. 持久化存储: 我们需要一种机制来存储注释数据,可以是浏览器的localStorage/IndexedDB,或者是服务器端的数据库。

实现思路

我们的实现将分为三个主要部分:

  1. 捕获注释数据
  2. 保存注释数据
  3. 恢复注释数据

1. 捕获注释数据

我目前使用的是PDF.js-4.10.38版本,由于并未在EventBus中发现能够监听AnnotationLayer操作注释的事件,所以获取注释数据只能够一次性获取之前所有的注释数据 具体代码如下:

const { map } = PDFJSApplication.pdfDocument.annotationStorage.serializable;
const vals = Array.from(map.values());

2.保存注释数据

为了方便实现,我将数据直接使用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);

3.恢复注释数据

在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;
};