pdfjs库使用记录1

时间:2025-04-19 17:59:15

import React, { useEffect, useState, useRef } from 'react';

import * as pdfjsLib from 'pdfjs-dist';

// 设置 worker 路径

pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.js';

const PDFViewer = ({ url }) => {

  const [pdf, setPdf] = useState(null);

  const [currentPage, setCurrentPage] = useState(1);

  const [numPages, setNumPages] = useState(0);

  const [pageRendering, setPageRendering] = useState(false);

  const [loading, setLoading] = useState(true);

  const [error, setError] = useState(null);

  const canvasRef = useRef(null);

  const linkLayerRef = useRef(null);  // 添加链接层的引用

  const pageCache = useRef(new Map());

  const scale = useRef(1);  // 添加scale引用以在不同函数间共享

  // 添加水印函数

  const addWatermark = (canvas, scale) => {

    const ctx = canvas.getContext('2d');

    const devicePixelRatio = window.devicePixelRatio || 1;

    // 保存当前上下文状态

    ctx.save();

    // 设置水印样式

    ctx.globalAlpha = 0.2; // 水印透明度

    ctx.fillStyle = '#000'; // 水印颜色

    // 计算基础字体大小(根据canvas宽度动态调整)

    const baseFontSize = Math.min(canvas.width, canvas.height) * 0.03; // 3% 的画布大小

    const fontSize = baseFontSize * devicePixelRatio;

    ctx.font = `${fontSize}px Arial`;

    // 水印文本

    const text1 = '45380867';

    const text2 = 'Jun Xiao';

    // 计算水印尺寸

    const text1Width = ctx.measureText(text1).width;

    const text2Width = ctx.measureText(text2).width;

    const lineHeight = fontSize * 1.2;

    const watermarkWidth = Math.max(text1Width, text2Width);

    const watermarkHeight = lineHeight * 2;

    // 计算水印网格

    const xGap = watermarkWidth * 2.5; // 水印之间的横向间距

    const yGap = watermarkHeight * 2.5; // 水印之间的纵向间距

    // 旋转角度(25度)

    const angle = -25 * Math.PI / 180;

    // 绘制水印网格

    for (let y = -yGap; y < canvas.height + yGap; y += yGap) {

      for (let x = -xGap; x < canvas.width + xGap; x += xGap) {

        ctx.save();

        // 移动到水印位置并旋转

        ctx.translate(x, y);

        ctx.rotate(angle);

        // 绘制两行文本

        ctx.fillText(text1, -text1Width / 2, 0);

        ctx.fillText(text2, -text2Width / 2, lineHeight);

        ctx.restore();

      }

    }

    // 恢复上下文状态

    ctx.restore();

  };

  // 添加处理链接的函数

  const setupLinkLayer = (page, viewport) => {

    const linkLayer = linkLayerRef.current;

    if (!linkLayer) return;

    // 清除旧的链接

    while (linkLayer.firstChild) {

      linkLayer.removeChild(linkLayer.firstChild);

    }

    // 获取页面的注解(包括链接)

    page.getAnnotations().then(annotations => {

      annotations.forEach(annotation => {

        if (annotation.subtype === 'Link' && annotation.url) {

          // 创建链接元素

          const link = document.createElement('a');

          const bounds = viewport.convertToViewportRectangle(annotation.rect);

          // 设置链接样式

          link.href = annotation.url;

          link.target = '_blank';  // 在新标签页中打开

          link.style.position = 'absolute';

          link.style.left = `${Math.min(bounds[0], bounds[2])}px`;

          link.style.top = `${Math.min(bounds[1], bounds[3])}px`;

          link.style.width = `${Math.abs(bounds[2] - bounds[0])}px`;

          link.style.height = `${Math.abs(bounds[3] - bounds[1])}px`;

          link.style.cursor = 'pointer';

          // 添加到链接层

          linkLayer.appendChild(link);

        }

      });

    });

  };

  // 初始化 PDF

  useEffect(() => {

    const loadPDF = async () => {

      if (!url) return;

      try {

        setLoading(true);

        setError(null);

        // 创建加载任务

        const loadingTask = pdfjsLib.getDocument(url);

        const pdfDoc = await loadingTask.promise;

        setPdf(pdfDoc);

        setNumPages(pdfDoc.numPages);

      } catch (error) {

        console.error('Error loading PDF:', error);

        setError('PDF加载失败,请稍后重试');

      } finally {

        setLoading(false);

      }

    };

    loadPDF();

    return () => {

      // 清理缓存的页面

      pageCache.current.clear();

      if (pdf) {

        pdf.destroy();

      }

    };

  }, [url]);

  // 渲染页面

  const renderPage = async (pageNum) => {

    if (pageRendering || !pdf) return;

    setPageRendering(true);

    try {

      // 检查缓存

      if (!pageCache.current.has(pageNum)) {

        const page = await pdf.getPage(pageNum);

        pageCache.current.set(pageNum, page);

      }

      const page = pageCache.current.get(pageNum);

      const canvas = canvasRef.current;

      const ctx = canvas.getContext('2d');

      // 计算适合屏幕的缩放比例

      const viewport = page.getViewport({ scale: 1 });

      const devicePixelRatio = window.devicePixelRatio || 1;

      const containerWidth = canvas.parentElement.clientWidth;

      const scale = (containerWidth / viewport.width) * devicePixelRatio;

      // 设置canvas尺寸

      const scaledViewport = page.getViewport({ scale });

      canvas.width = scaledViewport.width;

      canvas.height = scaledViewport.height;

      canvas.style.width = `${containerWidth}px`;

      canvas.style.height = `${scaledViewport.height / devicePixelRatio}px`;

      // 设置链接层尺寸和位置

      if (linkLayerRef.current) {

        linkLayerRef.current.style.width = `${containerWidth}px`;

        linkLayerRef.current.style.height = `${scaledViewport.height / devicePixelRatio}px`;

      }

      // 渲染PDF页面

      const renderContext = {

        canvasContext: ctx,

        viewport: scaledViewport,

        enableWebGL: true,

      };

      await page.render(renderContext).promise;

      // 设置链接

      setupLinkLayer(page, scaledViewport);

      // 在PDF页面渲染完成后添加水印

      addWatermark(canvas, scale);

    } catch (error) {

      console.error('Error rendering page:', error);

      setError('页面渲染失败,请刷新重试');

    } finally {

      setPageRendering(false);

    }

  };

  // 页面变化时重新渲染

  useEffect(() => {

    renderPage(currentPage);

  }, [currentPage, pdf]);

  // 内存管理:清理不可见页面的缓存

  useEffect(() => {

    const cleanupCache = () => {

      if (pageCache.current.size > 3) { // 只保留当前页面附近的几页

        const pagesToKeep = new Set([

          currentPage,

          currentPage - 1,

          currentPage + 1

        ]);

        pageCache.current.forEach((page, pageNum) => {

          if (!pagesToKeep.has(pageNum)) {

            // 确保在删除缓存前释放页面资源

            page.cleanup();

            pageCache.current.delete(pageNum);

          }

        });

      }

    };

    cleanupCache();

  }, [currentPage]);

  // 处理窗口大小变化

  useEffect(() => {

    const handleResize = () => {

      renderPage(currentPage);

    };

    window.addEventListener('resize', handleResize);

    return () => {

      window.removeEventListener('resize', handleResize);

    };

  }, [currentPage]);

  if (loading) {

    return (

      <div className="loading">

        <div className="loading-text">PDF文件加载中...</div>

        <div className="loading-spinner"></div>

      </div>

    );

  }

  if (error) {

    return (

      <div className="error">

        <div className="error-message">{error}</div>

        <button onClick={() => window.location.reload()} className="retry-button">

          重试

        </button>

      </div>

    );

  }

  return (

    <div className="pdf-viewer">

      <div className="pdf-controls">

        <button

          onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}

          disabled={currentPage <= 1 || pageRendering}

          className="control-button"

        >

          上一页

        </button>

        <span className="page-info">{`第 ${currentPage} 页,共 ${numPages} 页`}</span>

        <button

          onClick={() => setCurrentPage(prev => Math.min(prev + 1, numPages))}

          disabled={currentPage >= numPages || pageRendering}

          className="control-button"

        >

          下一页

        </button>

      </div>

      <div className="pdf-container">

        <div className="canvas-container" style={{ position: 'relative' }}>

          <canvas ref={canvasRef} className="pdf-canvas" />

          <div

            ref={linkLayerRef}

            style={{

              position: 'absolute',

              top: 0,

              left: 0,

              right: 0,

              bottom: 0,

              pointerEvents: 'none'  // 允许点击穿透到链接

            }}

            className="link-layer"

          />

        </div>

      </div>

    </div>

  );

};

export default PDFViewer;

相关文章