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

如何在React中用pdfjs-dist动态缩放PDF并精准绘制多边形

PDFJS渲染适配容器与多边形坐标对齐问题

问题描述

在React组件中使用pdfjs-dist渲染尺寸不固定的PDF(如1800x1200、580x250等),需求:

  1. 画布动态缩放适配容器
  2. 基于0-1范围的相对坐标绘制多边形,确保与PDF指定位置对齐

当前实现中PDF渲染正常,但画布尺寸与多边形位置错位,例如原1800x1200的PDF视口显示为580x250时,多边形无法对齐。

问题分析与解决方案

1. 确保画布尺寸匹配PDF并正确缩放适配容器

当前代码核心问题:

  • 依赖异步更新的viewportCoords获取容器尺寸,初始值为0会导致计算错误
  • 手动设置transform进行缩放,不如直接用PDFJS的视口API更可靠

修改步骤:

  • 直接获取容器DOM的实际尺寸,避免依赖state
  • 基于PDF原始尺寸(scale=1时的视口)计算适配容器的缩放比例
  • 使用计算后的缩放比例创建适配视口,画布宽高直接使用该视口的尺寸

2. 确保多边形与PDF坐标完美对齐

错位原因:

  • PDF坐标系以左下角为原点,Canvas以左上角为原点,Y轴反转逻辑错误
  • 依赖state中的scale值,异步更新可能导致绘制时使用旧值
  • 手动计算坐标易出错,未利用PDFJS提供的坐标转换方法

修改步骤:

  • 使用PDF视口的convertToViewportPoint方法转换相对坐标到PDF视口坐标
  • 直接传递计算好的scale给绘制函数,避免state异步问题
  • 正确反转Y轴:Canvas的Y坐标 = 画布高度 - 视口转换后的Y坐标

修改后的完整代码片段

import { useState, useEffect, useRef } from 'react';
import { Box } from '@mui/material';
import * as pdfjsLib from 'pdfjs-dist';
import { GlobalWorkerOptions } from 'pdfjs-dist';

const PDFViewer = ({ pdfView }: { pdfView: File | null }) => {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const containerRef = useRef<HTMLDivElement | null>(null);

  const polygonDummy = [
    { X: 0.34002524614334106, Y: 0.27618247270584106 },
    { X: 0.8136482834815979, Y: 0.2764469087123871 },
    { X: 0.8136501908302307, Y: 0.2848358154296875 },
    { X: 0.34002646803855896, Y: 0.2845710515975952 },
  ];

  useEffect(() => {
    if (!pdfView || !canvasRef.current || !containerRef.current) return;

    const url = URL.createObjectURL(pdfView);
    const canvas = canvasRef.current;
    const container = containerRef.current;

    GlobalWorkerOptions.workerSrc = '../../node_modules/pdfjs-dist/build/pdf.worker.mjs';

    const loadingTask = pdfjsLib.getDocument(url);
    loadingTask.promise
      .then(async (pdf) => {
        const page = await pdf.getPage(1);

        // 获取PDF原始尺寸(scale=1时的视口)
        const originalViewport = page.getViewport({ scale: 1 });
        // 获取容器实际尺寸
        const containerWidth = container.clientWidth;
        const containerHeight = container.clientHeight;

        // 计算适配容器的缩放比例,保持宽高比
        const scaleX = containerWidth / originalViewport.width;
        const scaleY = containerHeight / originalViewport.height;
        const scale = Math.min(scaleX, scaleY);

        // 创建适配后的视口
        const scaledViewport = page.getViewport({ scale });

        // 设置画布尺寸
        canvas.width = Math.floor(scaledViewport.width);
        canvas.height = Math.floor(scaledViewport.height);
        canvas.style.width = `${scaledViewport.width}px`;
        canvas.style.height = `${scaledViewport.height}px`;

        const context = canvas.getContext('2d');
        if (context) {
          // 使用适配后的视口渲染PDF,无需手动设置transform
          const renderContext = {
            canvasContext: context,
            viewport: scaledViewport,
          };

          await page.render(renderContext).promise;
          // 传递计算好的参数给绘制函数
          drawPolygon(context, scaledViewport, polygonDummy);
        }
      })
      .catch((error) => console.error('加载PDF错误:', error));

    return () => URL.revokeObjectURL(url);
  }, [pdfView]);

  const drawPolygon = (
    context: CanvasRenderingContext2D,
    viewport: pdfjsLib.PageViewport,
    polygon: { X: number; Y: number }[]
  ) => {
    context.beginPath();
    context.strokeStyle = 'red';
    context.lineWidth = 2;

    const canvasHeight = viewport.height;
    // 转换相对坐标到Canvas像素坐标
    const coords = polygon.map((point) => {
      // 相对坐标转PDF视口坐标(0-1范围映射到PDF原始宽高)
      const pdfPoint = viewport.convertToViewportPoint(point.X * viewport.width / viewport.scale, point.Y * viewport.height / viewport.scale);
      // 转换为Canvas坐标:反转Y轴
      return {
        x: pdfPoint.x,
        y: canvasHeight - pdfPoint.y,
      };
    });

    if (coords.length > 0) {
      context.moveTo(coords[0].x, coords[0].y);
      for (let i = 1; i < coords.length; i++) {
        context.lineTo(coords[i].x, coords[i].y);
      }
      context.closePath();
      context.stroke();
    }
  };

  return (
    <Box
      ref={containerRef}
      sx={{
        width: '100%', // 容器占满父容器宽度,或设置固定值
        height: '600px', // 可根据需求调整高度
        overflow: 'auto',
        border: '1px solid black',
      }}
    >
      <canvas ref={canvasRef}></canvas>
    </Box>
  );
};

export default PDFViewer;

关键修改点说明

  • 新增containerRef直接获取容器DOM尺寸,规避state异步更新的问题
  • 使用originalViewport(scale=1)计算缩放比例,确保基于PDF真实尺寸适配容器
  • 用scaledViewport渲染PDF,自动处理缩放逻辑,无需手动设置transform
  • 绘制多边形时,借助PDFJS的坐标转换方法,配合Y轴反转得到正确的Canvas坐标
  • 绘制函数直接使用当前计算的视口参数,不依赖state,避免异步更新导致的错位

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 20:55:57