研究一下能不能用pdfjs直接保存批注或者还原

275 阅读5分钟

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文件

客户端保存流程

  1. 用户触发保存操作,调用 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;
  }
  // ...
}
  1. 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 处理程序负责处理保存请求:

  1. 收集全局Promise,包括加载流、AcroForm、xref等
  2. 处理新注释:
    • 获取新注释映射
    • 更新结构树
    • 为每个页面保存新注释
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,
      // ...
    });
  }
);
  1. 在 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中的注释持久化机制是一个完整的流程:

  1. 使用 AnnotationStorage 类存储注释数据
  2. 通过 serializable getter将数据转换为可序列化格式
  3. 在客户端,通过 saveDocument() 方法将序列化数据发送到worker
  4. 在worker端, SaveDocument 处理程序处理保存请求,包括:
    • 处理新注释
    • 更新页面字典
    • 执行增量更新
  5. 最终生成包含新注释的PDF数据,并通过 downloadManager 下载 这种设计允许用户在浏览器中编辑PDF注释,并将更改保存回PDF文件,而无需服务器端处理。

还原

如何从外部数据源加载注释到PDF.js

在PDF.js中,AnnotationStorage类是管理注释数据的核心。当你想要将之前保存的注释加载回PDF时,你可以使用以下方法:

  1. 获取序列化的注释数据:首先,你需要从PDF中获取序列化的注释数据并保存到数据库中。这可以通过annotationStorage.serializable属性实现,它会返回一个包含map、hash和transfer的对象。
  2. 加载注释数据:当用户再次打开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);
    }
  }
}
收起代码

关键点

  1. setValue方法:这是将单个注释加载到annotationStorage中的核心方法。你需要为每个注释调用此方法,提供注释的ID和值。
  2. 测试环境中的_setValues方法:在PDF.js的测试代码中,有一个_setValues方法可以一次性加载多个注释,但这个方法只在测试环境中可用。在生产环境中,你需要使用setValue方法逐个加载注释。
  3. 注释的结构:保存的注释数据结构取决于注释的类型。对于表单字段,通常是{value: ...}形式;对于编辑器注释,结构会更复杂,包含位置、样式等信息。
  4. 注释的渲染:加载注释后,PDF.js会在渲染页面时自动显示这些注释。

通过这种方式,你可以实现将PDF注释保存到外部数据库,并在用户再次打开PDF时还原这些注释的功能,使得不同用户可以看到之前添加的注释。