Vue.js中pdf.js文本选择功能实现的技术难题
PDF.js文本选择功能实现问题及代码解析
我在基于pdf.js实现PDF文本选择功能时遇到了困难。以下是一个Vue.js组件代码,该组件通过Canvas元素展示PDF文档,基于pdf.js库渲染,支持根据坐标数据高亮特定区域、为高亮区域添加提示框、处理鼠标事件选中高亮区域,还能监听数据仓库变化以更新高亮区域。
<template> <div> <canvas ref="pdfCanvas"></canvas> </div> </template> <script setup> import { ref, onMounted, watchEffect } from "vue"; import * as pdfjsLib from "pdfjs-dist/build/pdf"; import { usePdfOnlyStore } from "@/store/pdf-only/store"; const pdfOnlyStore = usePdfOnlyStore(); const coordinatesData = JSON.parse( JSON.stringify(pdfOnlyStore.coordinatesData) ); let tooltipDiv = null; const props = defineProps({ pdfUrl: { type: String, required: true }, }); const pdfCanvas = ref(null); // let currentHighlightedArea = null; function removeAllHighlights() { if (props.pdfUrl) { return new Promise((resolve) => { const pdfCanvasElement = pdfCanvas.value; const offscreenCanvas = document.createElement("canvas"); offscreenCanvas.width = pdfCanvasElement?.width; offscreenCanvas.height = pdfCanvasElement?.height; const offscreenContext = offscreenCanvas.getContext("2d"); // Re-render the PDF without highlights to the offscreen canvas pdfjsLib.getDocument(props.pdfUrl).promise.then((pdf) => { pdf.getPage(1).then((page) => { // Get the viewport without explicitly setting the rotation const viewport = page.getViewport({ scale: 1.4 }); offscreenCanvas.width = viewport.width; offscreenCanvas.height = viewport.height; const renderContext = { canvasContext: offscreenContext, viewport }; page.render(renderContext).promise.then(() => { // Swap the offscreen canvas with the visible canvas const canvasContext = pdfCanvasElement?.getContext("2d"); canvasContext?.clearRect( 0, 0, pdfCanvasElement?.width, pdfCanvasElement?.height ); canvasContext?.drawImage(offscreenCanvas, 0, 0); resolve(); // Resolve the promise when done }); }); }); }); } } const renderPdf = () => { const pdfUrl = props.pdfUrl ? props.pdfUrl : ""; const pdfCanvasElement = pdfCanvas.value; // console.log(coordinatesData) pdfjsLib.GlobalWorkerOptions.workerSrc = "https://unpkg.com/pdfjs-dist@2.7.570/build/pdf.worker.min.js"; pdfjsLib .getDocument(pdfUrl) .promise.then((pdf) => pdf.getPage(1)) .then((page) => { const canvasContext = pdfCanvasElement.getContext("2d"); const viewport = page.getViewport({ scale: 1.4 }); pdfCanvasElement.height = viewport.height; pdfCanvasElement.width = viewport.width; const renderContext = { canvasContext, viewport }; return page.render(renderContext).promise; }) .then(() => { const canvasContext = pdfCanvasElement.getContext("2d"); highlightArea(canvasContext, coordinatesData.total); highlightArea(canvasContext, coordinatesData?.invoice_number); highlightArea(canvasContext, coordinatesData.currency); pdfCanvasElement.addEventListener("mousemove", handleMouseMove); pdfCanvas.value.addEventListener("click", handleMouseMove); }); }; onMounted(() => { renderPdf(); }); function highlightArea(ctx, area) { const padding = 5; ctx.fillStyle = area?.color; ctx.fillRect( area?.bbox[0] - padding, area?.bbox[1] - padding, area?.bbox[2] - area?.bbox[0] + 2 * padding, area?.bbox[3] - area?.bbox[1] + 2 * padding ); } function handleMouseMove(event) { const canvasRect = pdfCanvas.value.getBoundingClientRect(); const x = event.clientX - canvasRect.left; const y = event.clientY - canvasRect.top; if (tooltipDiv) tooltipDiv.remove(); const highlightedArea = getHighlightedArea(x, y); if (highlightedArea) { tooltipDiv = createTooltip( event.clientX, event.clientY - 30, highlightedArea ); document.body.appendChild(tooltipDiv); // Change cursor to pointer when hovering over a highlighted area pdfCanvas.value.style.cursor = "pointer"; } else { // Reset cursor to auto when not hovering over a highlighted area pdfCanvas.value.style.cursor = "auto"; } // Check if the event is a click event if (event.type === "click") { // alert(`${highlightedArea} has been selected.`); pdfOnlyStore.selectedHighlight = highlightedArea; } } function getHighlightedArea(x, y) { for (const [area, { bbox }] of Object.entries(coordinatesData)) { if (x >= bbox[0] && x <= bbox[2] && y >= bbox[1] && y <= bbox[3]) { return area; } } return ""; } async function highlightTotal(item) { const canvasContext = pdfCanvas.value?.getContext("2d"); const bbox = coordinatesData[item].bbox; const padding = 3; // Wait for all highlights to be removed await removeAllHighlights(); // Re-apply all highlights for (const area of Object.values(coordinatesData)) { highlightArea(canvasContext, area); } // Apply the new border canvasContext.strokeStyle = "black"; // Set border color to black canvasContext.setLineDash([2, 2]); // Set border to dotted canvasContext.lineWidth = 1; canvasContext.strokeRect( bbox[0] - padding, bbox[1] - padding, bbox[2] - bbox[0] + 2 * padding, bbox[3] - bbox[1] + 2 * padding ); // currentHighlightedArea = item; } function createTooltip(left, top, text) { const tooltip = document.createElement("div"); tooltip.textContent = text; tooltip.style.cssText = `position: absolute; left: ${left}px; top: ${top}px; background: #f1f1f1; padding: 5px; border-radius: 5px; z-index: 9999;`; return tooltip; } watchEffect(() => { if (pdfOnlyStore.highlightedFelid) { highlightTotal(pdfOnlyStore.highlightedFelid); } }); watchEffect(() => { if (pdfOnlyStore.nextPdf) { // removeAllHighlights(); pdfOnlyStore.nextPdf = false; } }); </script>
现有代码的问题与优化建议
- 坐标数据不同步:
coordinatesData仅在组件初始化时复制仓库数据,后续仓库更新不会同步,导致高亮区域无法实时更新。可改为监听pdfOnlyStore.coordinatesData,触发时重新赋值并渲染高亮。 - 重复渲染性能浪费:
removeAllHighlights每次都重新请求并渲染PDF,开销极大。建议缓存初始渲染的Canvas内容,清除高亮时直接恢复缓存即可。 - 事件重复绑定:
renderPdf每次执行都会绑定mousemove和click事件,多次调用会导致事件重复触发。需在组件卸载时解绑事件,或确保只绑定一次。 - 缺少自由文本选择:当前仅支持预定义坐标的区域选择,未实现真正的自由文本选择功能。
自由文本选择实现思路
- 获取文本内容:通过
page.getTextContent()获取PDF页面所有文本片段的位置、内容等信息。 - 监听拖拽事件:记录鼠标按下和抬起的坐标,转换为PDF视图坐标(利用
viewport.convertToPdfPoint方法)。 - 匹配选中文本:遍历文本片段,判断其是否落在鼠标选中的矩形范围内,收集并拼接这些文本。
- 高亮选中区域:在Canvas上绘制高亮矩形,或通过DOM层叠加高亮元素实现。
内容的提问来源于stack exchange,提问作者Adnan Ali
相关产品推荐
相关产品推荐

