需求
多次在线预览有可能加密过的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的功能。