在 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 的文档和源码会非常有帮助。

相关推荐
青皮桔31 分钟前
CSS实现百分比水柱图
前端·css
失落的多巴胺31 分钟前
使用deepseek制作“喝什么奶茶”随机抽签小网页
javascript·css·css3·html5
DataGear34 分钟前
如何在DataGear 5.4.1 中快速制作SQL服务端分页的数据表格看板
javascript·数据库·sql·信息可视化·数据分析·echarts·数据可视化
影子信息36 分钟前
vue 前端动态导入文件 import.meta.glob
前端·javascript·vue.js
青阳流月37 分钟前
1.vue权衡的艺术
前端·vue.js·开源
样子201841 分钟前
Vue3 之dialog弹框简单制作
前端·javascript·vue.js·前端框架·ecmascript
kevin_水滴石穿42 分钟前
Vue 中报错 TypeError: crypto$2.getRandomValues is not a function
前端·javascript·vue.js
翻滚吧键盘42 分钟前
vue文本插值
javascript·vue.js·ecmascript
孤水寒月2 小时前
给自己网站增加一个免费的AI助手,纯HTML
前端·人工智能·html
CoderLiu2 小时前
用这个MCP,只给大模型一个figma链接就能直接导出图片,还能自动压缩上传?
前端·llm·mcp