You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

pdf.js同一Canvas渲染多PDF滚动显示旧页面问题求助

问题描述

我使用Flask、HTML和JS构建了一个UI,该UI包含一个元素列表,每个元素都有独立按钮,可在同一个canvas中渲染不同的PDF。问题在于,当我打开第2个或第3个PDF并使用鼠标滚动页面时,会显示第一个PDF的页面。我已经尝试了StackOverflow上两个相关问题的所有建议,但问题仍未解决。

相关代码

HTML代码

<div class="col-lg-6" id="col-preview">
    {% if data_to_render %} <h5>Preview</h5> {% endif %}
    <canvas id="pdfCanvas"></canvas>
</div>

JS脚本

pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';

document.addEventListener('DOMContentLoaded', function () {
    var buttons = document.querySelectorAll('.btn-secondary');
    buttons.forEach(function (button) {
      button.addEventListener('click', function () {
        var pdfUrl = button.getAttribute('data-pdf-url');
        var chunk = button.getAttribute('data-chunk');
        renderPdf(pdfUrl, 'pdfCanvas', chunk);
      });
    });
  });

function renderPdf(url, containerId, chunk) {
    var canvas = document.getElementById(containerId);
    var pdfDoc = null;
    var currentPage = 1;
    var heightRatio = window.innerHeight/953;
    var widthRatio = window.innerWidth/1920;
    var scale = Math.min(heightRatio, widthRatio);

    function getCanvasTopLeftCoordinates(canvas) {                
        if (canvas) {
            const rect = canvas.getBoundingClientRect();
            const left = rect.left;
            const top = rect.top;

            return { left, top };
        } else {
            return null; // 未找到canvas
        }
    }
    
    // 小屏幕下显示有问题(待修复)
    function renderHighlightChunk(page, chunk, maxY) {
        const canvasCoordinates = getCanvasTopLeftCoordinates(canvas);
        var lowerCaseChunk = chunk.toLowerCase();

        page.getTextContent().then(function (textContent) {
            const textItems = textContent.items;

            textItems.forEach(function (textItem) {
                const text = textItem.str;
                var lowerCaseText = text.toLowerCase();
                if (lowerCaseChunk.includes(lowerCaseText)) {

                    posX = (textItem.transform[4] + canvasCoordinates.left) * scale;
                    posY = (maxY - textItem.transform[5] - textItem.transform[0] + canvasCoordinates.top + window.scrollY) * scale;

                    const highlight = document.createElement('div');
                    highlight.className = 'highlight';
                    highlight.style.position = 'absolute';
                    highlight.style.left = posX + 'px';
                    highlight.style.top = posY + 'px';
                    highlight.style.width = textItem.width * scale + 'px';
                    highlight.style.height = textItem.height * scale + 'px';
                    highlight.style.backgroundColor = 'yellow';
                    highlight.style.opacity = '0.5';
                    document.body.appendChild(highlight);
                    }
            });
        });
    };

    function clearHighlights() {
        // 找到所有高亮div并移除
        const highlights = document.querySelectorAll('.highlight');
        highlights.forEach(function (highlight) {
            highlight.parentNode.removeChild(highlight);
        });
    }

    var renderingInProgress = false;

    function renderPage(pageNumber) {
        if (renderingInProgress) {
            return;
        }
        renderingInProgress = true;

        clearHighlights();

        pdfDoc.getPage(pageNumber).then(function (page) {
            var viewport = page.getViewport({ scale: scale });

            canvas.height = viewport.height;
            canvas.width = viewport.width;

            var context = canvas.getContext('2d');            
            var renderContext = {
                canvasContext: context,
                viewport: viewport
            };

            page.render(renderContext).promise.then(function () {
                renderHighlightChunk(page, chunk, canvas.height);
                renderingInProgress = false;
            });

        }).catch(function (reason) {
            console.error('页面渲染错误', reason);
            renderingInProgress = false;
        });
    }

    function clearCanvas() {
        var context = canvas.getContext('2d');
        context.clearRect(0, 0, canvas.width, canvas.height);
    }

    pdfjsLib.getDocument(url).promise.then(function (pdf) {
        pdfDoc = pdf;
        clearCanvas();
        renderPage(currentPage);

        var isMouseOverCanvas = false;

        canvas.addEventListener('mouseenter', function () {
            isMouseOverCanvas = true;
        });
        
        canvas.addEventListener('mouseleave', function () {
            isMouseOverCanvas = false;
        });

        canvas.addEventListener('wheel', function (e) {
            if (!isMouseOverCanvas) {
                return;
            }

            e.preventDefault();
            if (e.deltaY < 0 && currentPage > 1) {
                currentPage--;
                renderPage(currentPage);
            } else if (e.deltaY > 0 && currentPage < pdfDoc.numPages) {
                currentPage++;
                renderPage(currentPage);
            }
        });

    }).catch(function (reason) {
        console.error('PDF加载错误', reason);
    });
}
问题根源与解决建议

问题根源

每次点击按钮调用renderPdf时,都会给canvas绑定新的wheel、mouseenter、mouseleave事件监听,但旧的监听并未移除。同时,之前的PDF加载和渲染任务也没有中断。当滚动鼠标时,多个事件处理函数会同时触发,而这些旧的函数引用的是第一次调用时闭包里的pdfDoc和currentPage变量,导致显示第一个PDF的页面。

修复方案

1. 移除旧的事件监听并中断旧的PDF任务

在renderPdf函数开头,先清理之前的事件监听和未完成的PDF加载任务:

function renderPdf(url, containerId, chunk) {
    var canvas = document.getElementById(containerId);
    
    // 移除之前绑定的事件监听
    if (canvas._wheelHandler) {
        canvas.removeEventListener('wheel', canvas._wheelHandler);
    }
    if (canvas._mouseEnterHandler) {
        canvas.removeEventListener('mouseenter', canvas._mouseEnterHandler);
    }
    if (canvas._mouseLeaveHandler) {
        canvas.removeEventListener('mouseleave', canvas._mouseLeaveHandler);
    }
    
    // 中断之前未完成的PDF加载任务
    if (canvas._pdfLoadingTask) {
        canvas._pdfLoadingTask.cancel();
    }

    // 原有的变量定义...
    var pdfDoc = null;
    var currentPage = 1;
    // ...后续代码
}

2. 保存事件处理函数引用,方便后续移除

将事件处理函数定义为变量,绑定后保存到canvas元素上,以便下次调用时能找到并移除:

pdfjsLib.getDocument(url).promise.then(function (pdf) {
    pdfDoc = pdf;
    clearCanvas();
    renderPage(currentPage);

    var isMouseOverCanvas = false;

    // 定义命名的事件处理函数
    const mouseEnterHandler = function () {
        isMouseOverCanvas = true;
    };
    const mouseLeaveHandler = function () {
        isMouseOverCanvas = false;
    };
    const wheelHandler = function (e) {
        if (!isMouseOverCanvas) {
            return;
        }

        e.preventDefault();
        if (e.deltaY < 0 && currentPage > 1) {
            currentPage--;
            renderPage(currentPage);
        } else if (e.deltaY > 0 && currentPage < pdfDoc.numPages) {
            currentPage++;
            renderPage(currentPage);
        }
    };

    // 绑定事件
    canvas.addEventListener('mouseenter', mouseEnterHandler);
    canvas.addEventListener('mouseleave', mouseLeaveHandler);
    canvas.addEventListener('wheel', wheelHandler);

    // 保存函数引用到canvas
    canvas._mouseEnterHandler = mouseEnterHandler;
    canvas._mouseLeaveHandler = mouseLeaveHandler;
    canvas._wheelHandler = wheelHandler;

}).catch(function (reason) {
    console.error('PDF加载错误', reason);
});

3. 保存PDF加载任务引用

在调用pdfjsLib.getDocument时,将任务对象保存到canvas上,方便后续中断:

// 替换原有的pdfjsLib.getDocument调用
canvas._pdfLoadingTask = pdfjsLib.getDocument(url);
canvas._pdfLoadingTask.promise.then(function (pdf) {
    // 原有的后续逻辑...
}).catch(function (reason) {
    console.error('PDF加载错误', reason);
});

4. 额外优化:确保渲染状态重置

在renderPdf开头重置渲染状态,避免旧状态影响新的PDF渲染:

function renderPdf(url, containerId, chunk) {
    var canvas = document.getElementById(containerId);
    // ...前面的清理代码

    // 重置渲染状态
    var renderingInProgress = false;
    // ...后续代码
}

内容的提问来源于stack exchange,提问作者Marco Abbatangelo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 18:39:52