使用pdfjs仅输一次密码,可多次在线预览加密PDF文件

261 阅读3分钟

需求

多次在线预览有可能加密过的PDF,这些PDF加密时使用的密码都是当前用户设置用于查看该PDF的密码,如果使用iframe直接在线加载这些PDF,每次打开都需要输入密码,操作会比较繁琐。所以在使用该功能时先判断用户是否有设置密码加密PDF,然后在使用预览功能前,先校验用户的密码是否正确,然后保存下来,在用户每次点击PDF时,使用刚刚用户输入的正确密码解密该PDF,然后再加载已解密的PDF,这样就无须多次预览都需要输入密码,只需要校验一次密码;同时支持下载PDF功能但下载的PDF还是原来加密的PDF,这样既保证了用户多次在线预览PDF时的便捷性也维持了下载的PDF的加密性。

组件库

"@types/pdfjs-dist": "^2.10.378",
"pdfjs-dist": "2.16.105",

代码实现

index.tsx

/**
 * 预览base64-PDF和blob-pdf,并支持解密
 * @content base64或blob
 * @password 用于解密PDF的密码
 */
import { Modal, Button, Typography } from 'antd';
import { useEffect, useState } from 'react';
// 引入pdfjs-dist
import { getDocument } from 'pdfjs-dist';
// 配置 worker
import 'pdfjs-dist/build/pdf.worker.entry';
import './index.less';

interface IndexProps {
  // base64
  content?: string;
  // 控制弹窗显示
  open: boolean;
  // 关闭弹窗回调
  onCancel?: () => void;
  children?: React.ReactNode;
  // 是否blob
  isBlob?: boolean;
  // blob
  blob?: Blob;
  // 密码,用于解密PDF
  password?: string;
  // pdf名称
  pdfName: string;
}

const index: React.FC<IndexProps> = ({
  open,
  content,
  onCancel,
  children,
  isBlob = false,
  blob,
  password,
  pdfName,
}) => {
  // 存储pdf.js渲染的数据
  const [pdfData, setPdfData] = useState<any>(null);
  // 默认缩放比例
  const [scale, setScale] = useState(1);

  // Base64 转 Blob
  const base64toBlob = (data: string) => {
    const bytes = atob(data);
    let length = bytes.length;
    let out = new Uint8Array(length);
    while (length--) {
      out[length] = bytes.charCodeAt(length);
    }
    return new Blob([out], { type: 'application/pdf' });
  };

  const decryptPdf = async (pdfBlob: Blob) => {
    try {
      const pdfArrayBuffer = await pdfBlob.arrayBuffer();
      const loadingTask = getDocument({
        data: pdfArrayBuffer,
        // 传入密码进行解密
        password,
      });
      const pdf = await loadingTask.promise;
      const numPages = pdf.numPages;
      // 获取PDF的所有页面,并渲染到canvas中
      const pages = [];
      for (let i = 1; i <= numPages; i++) {
        const page = await pdf.getPage(i);
        // scale用于设置pdf的清晰度
        const viewport = page.getViewport({ scale: 3 });
        const canvas = document.createElement('canvas');
        const context = canvas.getContext('2d', { willReadFrequently: true });
        canvas.height = viewport.height - 120;
        canvas.width = viewport.width;
        await page.render({ canvasContext: context!, viewport }).promise;
        // 将每一页转为Data URL存储
        pages.push(canvas.toDataURL());
      }
      // 更新页面数据
      setPdfData(pages);
    } catch (error) {
      console.error('PDF解密失败', error);
    }
  };

  // 处理 Base64 PDF 解密
  const getPdfContent = async (baseContent: string) => {
    const pdfBlob = base64toBlob(baseContent);
    await decryptPdf(pdfBlob);
  };

  // 处理 Blob PDF 解密
  const getPdfBlobContent = async () => {
    await decryptPdf(blob as Blob);
  };

  useEffect(() => {
    if (open) {
      if (!isBlob) {
        getPdfContent(content as string);
      } else {
        getPdfBlobContent();
      }
    }
  }, [open]);

  const [windowHeight, setWindowHeight] = useState(window.innerHeight);

  const handleResize = () => {
    setWindowHeight(window.innerHeight);
  };

  useEffect(() => {
    window.addEventListener('resize', handleResize);
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  // 下载原版PDF
  const downloadEncryptedPdf = () => {
    let localUrl = '';
    if (!isBlob) {
      const pdfBlob = base64toBlob(content as string);
      localUrl = URL.createObjectURL(pdfBlob);
    } else {
      localUrl = URL.createObjectURL(blob as Blob);
    }
    if (localUrl) {
      const downloadLink = document.createElement('a');
      downloadLink.href = localUrl;
      // 设置文件名
      downloadLink.download = `${pdfName}.pdf`;
      document.body.appendChild(downloadLink);
      downloadLink.click();
      document.body.removeChild(downloadLink);
      URL.revokeObjectURL(localUrl);
    }
  };

  const handleWheel = (event: React.WheelEvent<HTMLDivElement>) => {
    if (event.ctrlKey) {
      event.preventDefault();
      setScale((prev) => {
        // 反转滚动方向
        let newScale = event.deltaY < 0 ? prev * 1.1 : prev / 1.1;
        // 限制缩放范围
        return Math.max(0.5, Math.min(3, newScale));
      });
    }
  };

  const handleZoomIn = () => {
    // 放大
    setScale((prev) => Math.min(1.5, prev + 0.1)); // 最大 150%
  };

  const handleZoomOut = () => {
    // 缩小
    setScale((prev) => Math.max(0.5, prev - 0.1)); // 最小 50%
  };

  const resetZoom = () => {
    // 还原 100%
    setScale(1);
  };

  return (
    <Modal
      className="com-pdf"
      width={880}
      open={open}
      onCancel={onCancel}
      centered={true}
      footer={
        pdfData
          ? [
              <Button key="download" type="primary" onClick={downloadEncryptedPdf}>
                下载
              </Button>,
            ]
          : false
      }
    >
      {/* 缩放控制按钮 */}
      <div className={'com-pdf-zoom-controls'}>
        <Button onClick={handleZoomOut} disabled={scale <= 0.5}>
          -
        </Button>
        <Typography.Text>{Math.round(scale * 100)}%</Typography.Text>
        <Button onClick={handleZoomIn} disabled={scale >= 2}>
          +
        </Button>
        <Button onClick={resetZoom}>
          重置
        </Button>
      </div>
      {open && (
        <div
          className="com-pdf-iframe"
          style={{
            display: 'flex',
            flexDirection: 'column',
            // 放大就居左显示,缩小就居中显示
            alignItems: `${scale > 1 ? 'flex-start' : 'center'}`,
            height: windowHeight - 150,
          }}
          onWheel={handleWheel}
        >
          {pdfData &&
            pdfData.map((pageData: any, index: number) => (
              <div
                key={index}
                style={{
                  width: `${794 * scale}px`, // A4 纸宽度
                  height: `${1123 * scale}px`, // A4 纸高度
                }}
              >
                <img
                  src={pageData}
                  alt={`page-${index + 1}`}
                  style={{
                    width: '100%',
                    height: '100%',
                  }}
                />
              </div>
            ))}
        </div>
      )}
      {children}
    </Modal>
  );
};

export default index;

index.less

.com-pdf {
  &-zoom-controls {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    margin-top: -10px;
    margin-right: 20px;
    gap: 10px;
  }
  &-iframe {
    overflow: auto;
    max-width: 100%;
    text-align: center;
  }
  &-footer {
    display: flex;
    justify-content: center;
    margin-top: 10px;
  }
}

@media print {
  body {
    margin: 0;
  }
  .com-pdf-iframe {
    display: block;
    overflow: visible;
  }
  .com-pdf-iframe img {
    page-break-after: always;
    width: 100%;
    height: auto;
  }
}

总结

上述代码实现了通过用户在输入框输入的密码,在线解密base64或blob格式的PDF,以弹窗方式、A4纸大小展示PDF内容,同时实现了可缩小放大PDF和下载PDF的功能。

参考

github.com/mozilla/pdf…