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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.05 17:21:01