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

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事件,多次调用会导致事件重复触发。需在组件卸载时解绑事件,或确保只绑定一次。
  • 缺少自由文本选择:当前仅支持预定义坐标的区域选择,未实现真正的自由文本选择功能。

自由文本选择实现思路

  1. 获取文本内容:通过page.getTextContent()获取PDF页面所有文本片段的位置、内容等信息。
  2. 监听拖拽事件:记录鼠标按下和抬起的坐标,转换为PDF视图坐标(利用viewport.convertToPdfPoint方法)。
  3. 匹配选中文本:遍历文本片段,判断其是否落在鼠标选中的矩形范围内,收集并拼接这些文本。
  4. 高亮选中区域:在Canvas上绘制高亮矩形,或通过DOM层叠加高亮元素实现。

内容的提问来源于stack exchange,提问作者Adnan Ali

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 12:54:55