在 PDF.js 的 viewer.html 基础上进行改造,实现同一个 PDF 文件在网页中上下拆分显示,并且两部分的标注数据能够实时同步

在 PDF.js 的 viewer.html 中实现同一 PDF 文件在网页中上下拆分显示,并且上下两部分添加的标注数据能够实时同步,这是一个比较高级的定制化需求。PDF.js 本身不直接提供开箱即用的"同一文档双视图同步标注"功能,因此需要进行较多的开发和改造。

这里提供一个实现思路和关键步骤:

核心思路:

最直接且可能相对容易管理的方式是创建两个独立的 PDF.js 查看器实例,让它们加载同一个 PDF 文件。然后,关键在于建立一个机制来监听一个实例中的标注变化,并将这些变化实时应用到另一个实例上。

实现步骤:

  1. 修改 viewer.html 结构:

    • 将原有的主内容区域(通常是 #viewerContainer 或类似的容器)拆分成两个独立的容器,例如 #viewerContainerTop#viewerContainerBottom
    • 使用 CSS(例如 Flexbox 或 Grid 布局)来设置这两个容器,使它们在页面上垂直排列,各占一部分高度(比如各 50%),并确保它们都有独立的滚动条 (overflow: auto;)。
    html 复制代码
    <!-- 简化示例 -->
    <body tabindex="1">
      <div id="outerContainer">
        <!-- ... 其他工具栏等 ... -->
        <div id="mainContainer">
          <div id="viewerContainerTop" class="split-viewer">
             <div id="viewerTop" class="pdfViewer"></div>
          </div>
          <div id="viewerContainerBottom" class="split-viewer">
             <div id="viewerBottom" class="pdfViewer"></div>
          </div>
        </div>
        <!-- ... 其他侧边栏等 ... -->
      </div>
      <!-- ... 其他元素和脚本 ... -->
    </body>
    css 复制代码
    /* 简化示例 CSS */
    #mainContainer {
      display: flex;
      flex-direction: column;
      height: calc(100vh - /* header height */); /* 减去头部工具栏高度 */
    }
    .split-viewer {
      flex: 1; /* 均分高度 */
      overflow: auto; /* 独立滚动 */
      position: relative; /* 用于内部绝对定位元素 */
    }
    /* 可能需要调整 .pdfViewer 的样式 */
  2. 初始化两个 PDF.js 实例:

    • viewer.js (或相关的应用初始化脚本) 中,你需要修改 PDFViewerApplication 的初始化逻辑,或者创建两个独立的 PDFViewerApplication 实例(这可能更复杂,因为 PDFViewerApplication 是一个单例设计)。
    • 更可行的方法可能是:保持一个 PDFViewerApplication 来管理核心逻辑(如文档加载、权限等),但创建两个独立的 PDFViewer 实例,并将它们分别关联到 #viewerTop#viewerBottom 这两个 DOM 元素上。
    • 你需要确保两个 PDFViewer 实例都使用同一个加载后的 PDFDocumentProxy 对象。
    javascript 复制代码
    // 在 viewer.js 或类似文件中修改或扩展
    // 这部分代码需要深入理解 PDFViewerApplication 的结构,可能需要重构
    
    // 假设 PDFViewerApplication.pdfDocument 已经加载完成
    const pdfDocument = PDFViewerApplication.pdfDocument;
    const eventBusTop = new pdfjsViewer.EventBus();
    const eventBusBottom = new pdfjsViewer.EventBus();
    
    // 创建链接服务,可能需要为每个视图创建独立的
    const pdfLinkServiceTop = new pdfjsViewer.PDFLinkService({ eventBus: eventBusTop });
    const pdfLinkServiceBottom = new pdfjsViewer.PDFLinkService({ eventBus: eventBusBottom });
    
    // 创建查找控制器,可能需要独立的
    const pdfFindControllerTop = new pdfjsViewer.PDFFindController({ /* ... */ });
    const pdfFindControllerBottom = new pdfjsViewer.PDFFindController({ /* ... */ });
    
    // *** 关键:创建两个 PDFViewer 实例 ***
    const pdfViewerTop = new pdfjsViewer.PDFViewer({
        container: document.getElementById('viewerContainerTop'),
        viewer: document.getElementById('viewerTop'),
        eventBus: eventBusTop,
        linkService: pdfLinkServiceTop,
        findController: pdfFindControllerTop,
        // ... 其他必要的配置,如 textLayerMode, annotationMode 等
        textLayerMode: pdfjsViewer.TextLayerMode.ENABLE,
        annotationMode: pdfjsViewer.AnnotationMode.ENABLE_STORAGE, // 启用存储
    });
    pdfLinkServiceTop.setViewer(pdfViewerTop);
    
    const pdfViewerBottom = new pdfjsViewer.PDFViewer({
        container: document.getElementById('viewerContainerBottom'),
        viewer: document.getElementById('viewerBottom'),
        eventBus: eventBusBottom,
        linkService: pdfLinkServiceBottom,
        findController: pdfFindControllerBottom,
        // ... 其他必要的配置
        textLayerMode: pdfjsViewer.TextLayerMode.ENABLE,
        annotationMode: pdfjsViewer.AnnotationMode.ENABLE_STORAGE, // 启用存储
    });
    pdfLinkServiceBottom.setViewer(pdfViewerBottom);
    
    // *** 将同一个文档设置给两个 Viewer ***
    pdfViewerTop.setDocument(pdfDocument);
    pdfViewerBottom.setDocument(pdfDocument);
    
    // 将 Viewer 设置给 LinkService (如果需要的话)
    // pdfLinkServiceTop.setDocument(pdfDocument, null);
    // pdfLinkServiceBottom.setDocument(pdfDocument, null);
    
    // 你可能需要将这两个 viewer 实例集成到 PDFViewerApplication 的管理中
    // 例如,更新缩放、页面跳转等需要同时作用于两个视图,或者提供独立的控制
    // 这部分需要根据 PDFViewerApplication 的具体实现进行调整
  3. 实现标注同步:

    • 这是最核心和复杂的部分。你需要利用 PDF.js 的 AnnotationStorage 和事件系统 (EventBus)。
    • 监听标注变化:
      • PDF.js 在添加、修改或删除标注时,会通过 AnnotationStorage 或相关的编辑器(如 AnnotationEditorLayer)触发事件。你需要找到合适的事件来监听。可能相关的事件有 annotationeditorstateschanged 或通过 AnnotationStorageonSetModified 等钩子。
      • 为两个 PDFViewer 实例(或它们关联的 AnnotationLayer / AnnotationEditorLayer)添加事件监听器。
    • 获取变化的标注数据:
      • 当监听到一个视图(例如 Top 视图)的标注发生变化时,你需要获取这个变化标注的完整数据(类型、位置、内容、ID 等)。可以使用 AnnotationStorage.getValues() 或其他相关 API。
    • 在另一个视图中应用变化:
      • 获取到 Top 视图变化的标注数据后,你需要调用 Bottom 视图对应的 AnnotationStorage 的 API(例如 setValue 或更底层的添加/更新方法)来应用这个变化。
      • 反之亦然,当 Bottom 视图发生变化时,同步到 Top 视图。
    • 避免无限循环:
      • 一个关键问题是防止同步操作本身触发新的同步事件,导致无限循环(A 更新 -> 触发 A 事件 -> 同步到 B -> B 更新 -> 触发 B 事件 -> 同步到 A ...)。
      • 实现方法:
        • 标记来源: 在触发同步操作时,添加一个标记(例如,一个临时变量或事件对象中的属性),表明这次更新是来自同步操作。在事件监听器中检查这个标记,如果是来自同步的更新,则不再触发对另一个视图的同步。
        • 比较数据: 在尝试同步前,先获取目标视图中对应 ID 的标注数据,如果数据已经一致,则无需更新。
    javascript 复制代码
    // 简化示例:标注同步逻辑 (需要放在合适的初始化位置)
    
    const annotationStorageTop = pdfViewerTop.annotationStorage; // 获取存储对象
    const annotationStorageBottom = pdfViewerBottom.annotationStorage;
    
    let isSyncing = false; // 用于防止无限循环的标志
    
    function syncAnnotations(sourceStorage, targetStorage, sourceViewer) {
        if (isSyncing) return; // 如果正在同步中,则忽略
        isSyncing = true;
    
        sourceStorage.getAll().then(annotations => { // 获取源存储的所有标注
            if (annotations) {
                for (const key in annotations) { // key 通常是 annotation ID
                    const value = annotations[key];
                    // 使用 setValue 更新目标存储,它通常会处理添加和修改
                    // 注意:直接使用 setValue 可能效率不高,更好的方式是只同步变化的标注
                    // PDF.js 内部可能有更精细的事件或API来获取增量变化
                    targetStorage.setValue(key, value).catch(err => {
                        console.error("Sync error:", err);
                    });
                }
                // 可能还需要处理删除的情况:检查目标存储中存在但源存储中不存在的标注
            }
        }).catch(err => {
             console.error("Error getting annotations for sync:", err);
        }).finally(() => {
             isSyncing = false; // 重置标志
             // 可能需要强制目标视图重新渲染标注层
             // targetViewer.eventBus.dispatch('annotationlayerrendered', { source: targetViewer }); // 或其他触发重绘的方法
        });
    }
    
    // 监听标注修改事件 (需要找到正确的事件和方式)
    // 假设 AnnotationStorage 提供了 'setmodified' 或类似事件
    // 或者监听 EventBus 上的特定事件
    // 以下为伪代码,具体事件名和方式需查阅 PDF.js 源码或文档
    
    /*
    annotationStorageTop.on('setmodified', (key, value) => {
        if (!isSyncing) {
           console.log('Top modified, syncing to Bottom');
           syncAnnotations(annotationStorageTop, annotationStorageBottom, pdfViewerBottom);
           // 或者更精细地只同步变化的 key/value
           // targetStorage.setValue(key, value).finally(() => { isSyncing = false; });
        }
    });
    
    annotationStorageBottom.on('setmodified', (key, value) => {
       if (!isSyncing) {
           console.log('Bottom modified, syncing to Top');
           syncAnnotations(annotationStorageBottom, annotationStorageTop, pdfViewerTop);
           // 或者更精细地只同步变化的 key/value
           // targetStorage.setValue(key, value).finally(() => { isSyncing = false; });
        }
    });
    */
    
    // 另一个可能的方式是监听 EventBus 上的 Annotation Editor 相关事件
    // eventBusTop.on('annotationeditorstateschanged', (data) => { /* ...同步逻辑... */ });
    // eventBusBottom.on('annotationeditorstateschanged', (data) => { /* ...同步逻辑... */ });
  4. 处理滚动和缩放(可选同步):

    • 你可能还希望在一个视图中滚动或缩放时,另一个视图也能同步。
    • 监听一个视图的滚动 (scroll) 和缩放 (scalechanged) 事件。
    • 获取当前的滚动位置 (scrollTop, scrollLeft) 或缩放比例。
    • 在另一个视图上设置相应的滚动位置或缩放比例。
    • 同样需要注意避免无限循环。

挑战与注意事项:

  • 复杂性: 这涉及到对 PDF.js 内部工作原理(特别是视图管理、事件系统和标注存储)的深入理解和修改。
  • PDF.js 版本: PDF.js 的 API 和内部结构可能会随版本更新而变化,你需要针对你使用的版本进行开发,并注意后续版本的兼容性问题。
  • 性能: 创建两个完整的 PDFViewer 实例并实时同步标注可能会对性能产生影响,尤其是在标注数量很多或 PDF 页面很复杂时。需要进行测试和优化。
  • 事件监听: 找到精确监听标注"已完成"变化的事件可能比较棘手,需要仔细研究 AnnotationLayer, AnnotationEditorLayer, AnnotationStorage 以及 EventBus 的相关代码和事件。可能需要监听多个事件或组合使用。
  • 标注 ID: 确保两个视图中的标注使用相同的唯一 ID 是同步的基础。通常 PDF.js 会自动生成 ID,只要操作的是同一个 AnnotationStorage 或能正确传递 ID,应该没问题。

总结:

实现这个功能需要较强的 JavaScript 和 PDF.js 开发能力。你需要:

  1. 改造 HTML/CSS 实现上下布局。
  2. 在 JS 中创建和管理两个 PDFViewer 实例,加载同一文档。
  3. 深入研究 PDF.js 的标注存储和事件机制。
  4. 编写 JavaScript 代码来监听标注变化,并在两个实例间进行双向同步,同时处理好循环调用的问题。

建议先从简单的部分开始,比如先实现两个视图加载同一个 PDF,然后再逐步实现标注的单向同步,最后实现双向同步和防循环机制。仔细查阅 PDF.js 的文档和源码会非常有帮助。

相关推荐
天天扭码5 分钟前
一分钟吃透一道面试算法题——字母异位词分组(最优解)
前端·javascript·算法
天天扭码17 分钟前
JavaScript 中字符串转字符数组的两种优雅方式
前端·javascript·代码规范
何遇er19 分钟前
在 AI 编程的热潮下对低代码的思考
前端·低代码·ai编程
何遇er22 分钟前
一句 Prompt 自动生成表单:我在低代码平台里是怎么接入生成式 AI 的
前端·低代码·ai编程
_一条咸鱼_24 分钟前
Vue 指令模块深度剖析:从基础应用到源码级解析(十二)
前端·javascript·面试
薯条不要番茄酱32 分钟前
【JavaEE初阶】多线程重点知识以及常考的面试题-多线程进阶(一)
java·前端·java-ee
只会安静敲代码的 小周1 小时前
uniapp上传图片时(可选微信头像、相册、拍照)
前端·微信·uni-app
kovlistudio1 小时前
红宝书第四十六讲:Node.js基础与API设计解析
前端·javascript·node.js
陈哥聊测试1 小时前
这款自研底层框架,你说不定已经用上了
前端·后端·开源
m0_zj1 小时前
41.[前端开发-JavaScript高级]Day06-原型关系图-ES6类的使用-ES6转ES5
开发语言·javascript·es6