React中使用@react-pdf-viewer实现PDF添加签名图片并下载的问题咨询
功能实现指导
前置依赖补充
需要额外安装两个库处理自定义渲染和PDF修改:
npm install @react-pdf-viewer/annotation pdf-lib
核心实现逻辑
1. 新增状态管理
需要在原有代码基础上新增以下状态:
- 签名图片的base64值
- 所有签名的配置数组,每个元素包含「页码、x坐标、y坐标、图片宽度、图片高度」
- 加载完成的原始PDF文档实例
2. 签名图片上传
新增文件选择框,仅支持图片格式,读取后转为base64存储,等待放置到PDF上。
3. PDF页面点击监听与签名预览
使用Viewer组件的renderPage属性,给每个PDF页面绑定点击事件:
- 点击时先判断是否已经选中了签名图片
- 把浏览器点击坐标转换为PDF原生坐标(PDF原点为左下角,浏览器点击原点为左上角,需要用页面高度减去点击的y值完成转换)
- 将新的签名配置存入状态数组
- 同时在页面的自定义渲染层,遍历所有签名配置,把图片渲染到对应位置,实现预览效果
4. 带签名PDF导出
使用pdf-lib处理导出逻辑:
- 加载原始PDF文件
- 把签名base64转为图片对象嵌入PDF
- 遍历所有签名配置,把图片绘制到对应页码的对应坐标
- 导出处理后的PDF为blob,触发浏览器下载
完整修改后代码
import React, { useState, useRef } from "react"; import { Viewer } from "@react-pdf-viewer/core"; import { defaultLayoutPlugin } from "@react-pdf-viewer/default-layout"; import { Worker } from "@react-pdf-viewer/core"; import { PDFDocument } from 'pdf-lib' import "@react-pdf-viewer/core/lib/styles/index.css"; import "@react-pdf-viewer/default-layout/lib/styles/index.css"; export const App = () => { const defaultLayoutPluginInstance = defaultLayoutPlugin(); const [pdfFile, setPdfFile] = useState(null); const [pdfFileError, setPdfFileError] = useState(""); const [viewPdf, setViewPdf] = useState(null); // 新增签名相关状态 const [signatureImg, setSignatureImg] = useState(null); const [signatures, setSignatures] = useState([]); const pdfDocRef = useRef(null); const fileType = ["application/pdf"]; const handlePdfFileChange = (e) => { let selectedFile = e.target.files[0]; if (selectedFile) { if (selectedFile && fileType.includes(selectedFile.type)) { let reader = new FileReader(); reader.readAsArrayBuffer(selectedFile); reader.onloadend = async (e) => { const pdfBuffer = e.target.result; // 同时存base64用于预览,存buffer用于pdf-lib修改 const base64 = Buffer.from(pdfBuffer).toString('base64') setPdfFile(`data:application/pdf;base64,${base64}`); // 预加载PDF到pdf-lib pdfDocRef.current = await PDFDocument.load(pdfBuffer) setPdfFileError(""); }; } else { setPdfFile(null); setPdfFileError("Please select valid pdf file"); } } else { console.log("select your file"); } }; const handlePdfFileSubmit = (e) => { e.preventDefault(); if (pdfFile !== null) { setViewPdf(pdfFile); } else { setViewPdf(null); } }; // 签名图片上传处理 const handleSignatureUpload = (e) => { const file = e.target.files[0] if(file && file.type.startsWith('image/')) { const reader = new FileReader() reader.readAsDataURL(file) reader.onloadend = (e) => { setSignatureImg(e.target.result) } } } // 页面点击放置签名 const handlePageClick = (e, pageIndex, pageHeight) => { if(!signatureImg) return alert('请先上传签名图片') const rect = e.currentTarget.getBoundingClientRect() const x = e.clientX - rect.left // 转换为PDF左下角坐标 const y = pageHeight - (e.clientY - rect.top) setSignatures(prev => [...prev, { pageIndex, x, y, width: 100, // 可自定义调整签名大小 height: 50 }]) } // 导出带签名的PDF const exportPdf = async () => { if(!pdfDocRef.current || signatures.length === 0) return const pdfDoc = pdfDocRef.current // 嵌入签名图片 const signatureImage = await pdfDoc.embedPng(signatureImg) // 把所有签名放到对应页面 signatures.forEach(sig => { const page = pdfDoc.getPage(sig.pageIndex) page.drawImage(signatureImage, { x: sig.x, y: sig.y, width: sig.width, height: sig.height }) }) // 导出并下载 const pdfBytes = await pdfDoc.save() const blob = new Blob([pdfBytes], {type: 'application/pdf'}) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = '已签名.pdf' a.click() URL.revokeObjectURL(url) } // 自定义页面渲染 const renderPage = (props) => { const { pageIndex, height } = props return ( <div style={{position: 'relative', width: '100%', height: '100%'}} onClick={(e) => handlePageClick(e, pageIndex, height)} > {props.canvasLayer} {props.textLayer} {props.annotationLayer} {/* 渲染已添加的签名预览 */} {signatures.filter(sig => sig.pageIndex === pageIndex).map((sig, idx) => ( <img key={idx} src={signatureImg} style={{ position: 'absolute', left: sig.x, top: height - sig.y - sig.height, width: sig.width, height: sig.height, pointerEvents: 'none' }} alt="签名" /> ))} </div> ) } return ( <div className="container"> <br></br> <form className="form-group" onSubmit={handlePdfFileSubmit}> <input type="file" className="form-control" required onChange={handlePdfFileChange} /> {pdfFileError && <div className="error-msg">{pdfFileError}</div>} <br></br> <button type="submit" className="btn btn-success btn-lg"> UPLOAD </button> </form> <br></br> {/* 新增签名上传和导出按钮 */} <div> <input type="file" accept="image/*" onChange={handleSignatureUpload} /> <button onClick={exportPdf} style={{marginLeft: 20}}>导出带签名PDF</button> </div> <br></br> <h4>View PDF</h4> <div className="pdf-container" style={{height: '80vh'}}> {viewPdf && ( <> <Worker workerUrl="https://unpkg.com/pdfjs-dist@2.6.347/build/pdf.worker.min.js"> <Viewer fileUrl={viewPdf} plugins={[defaultLayoutPluginInstance]} renderPage={renderPage} /> </Worker> </> )} {!viewPdf && <>No pdf file selected</>} </div> </div> ); }; export default App;
关键注意点
- 坐标转换是核心,PDF原生坐标原点在左下角,浏览器点击坐标原点在左上角,必须做转换否则签名位置会上下颠倒
- 如果需要支持签名拖动调整位置,可以给预览的签名图片绑定draggable事件修改对应配置即可
- 支持多张不同签名的话只需要修改逻辑存储多张签名图片即可,当前代码默认用最新上传的签名图片放置
内容的提问来源于stack exchange,提问作者Piyush Pandey
相关产品推荐
相关产品推荐

