当前位置: 首页 > news >正文

Vue3/React 结合 pdfjs 实现拖拽盖章签名等操作,支持 PDF多页展示,导出图片与 PDF

PDF 拖拽盖章平台

在 AI 能基本实现百分之九十以上的前端代码时,不知道写这种前端工具还有没有人看?

我用相对详细的方式,完整拆解一个「PDF 拖拽盖章平台」的实现过程,覆盖多页渲染、拖拽盖章、撤销/还原、导出图片与 PDF、性能优化(懒渲染)等关键环节。示例包含 React 与 Vue3 两套实现,逻辑一致、写法不同。

目标与约束

目标

  • 支持上传多页 PDF。
  • 在预览区域拖拽印章,支持骑缝章。
  • 支持撤销 / 还原。
  • 支持导出图片和 PDF。
  • 大文件也能流畅渲染,不“卡成 PPT”。

主要约束

  • 浏览器对 canvas 尺寸有上限(不同浏览器略有差异)。
  • 长图导出容易失败,需要降级方案。
  • 大 PDF 一次性渲染会阻塞主线程。

核心思路:统一坐标系 + 多页 canvas

这里的关键是:把整份 PDF 当成一张“虚拟长画布”

  • 每一页各有一个canvas,显示真实页面内容。
  • 所有盖章坐标都以“整份文档坐标系”为准。
  • 每页只要知道自己在整份文档中的位置(pagePositions),就能把盖章正确映射回去。

这样做有两个好处:

  1. 骑缝章天然支持:印章跨页,坐标也能跨页。
  2. 导出更稳定:导出时可自由选择“整图”或“逐页”。
核心依赖
  • pdfjs-dist:解析与渲染 PDF
  • pdf-lib:导出带印章的 PDF(图片型 PDF)

安装示例:

pnpm add pdfjs-dist pdf-lib

PDF 解析与页面尺寸获取

先读取文档并计算每页尺寸。这里只取尺寸,不渲染,避免一开始就卡死。

const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer }); const pdf = await loadingTask.promise; const pages = []; for (let pageIndex = 1; pageIndex <= pdf.numPages; pageIndex += 1) { const page = await pdf.getPage(pageIndex); const viewport = page.getViewport({ scale: PAGE_SCALE }); pages.push({ width: viewport.width, height: viewport.height }); }

拿到pages后,就能计算整份文档尺寸和每页偏移量。

const docSize = useMemo(() => { const width = Math.max(...pdfPages.map((page) => page.width)); const height = pdfPages.reduce( (sum, page, index) => sum + page.height + (index < pdfPages.length - 1 ? PAGE_GAP : 0), 0 ); return { width, height }; }, [pdfPages]); const pagePositions = useMemo(() => { let offsetY = 0; return pdfPages.map((page, index) => { const pos = { x: (docSize.width - page.width) / 2, y: offsetY }; offsetY += page.height + (index < pdfPages.length - 1 ? PAGE_GAP : 0); return pos; }); }, [pdfPages, docSize.width]);

解释:

  • docSize是整个虚拟画布大小。
  • pagePositions是每页在虚拟画布中的左上角坐标。

预览区滚动与布局

多页 PDF 不可能全部撑开,所以预览区必须做“内部滚动”。

.pdf-stage { max-height: clamp(520px, 70vh, 820px); overflow: auto; }

这样页面滚动只发生在 PDF 区域内,用户体验会舒服很多。

拖拽盖章实现

坐标换算

拖拽时需要把屏幕坐标转换成“文档坐标”。关键点就是overlay的矩形位置。

const rect = overlayRef.current.getBoundingClientRect(); const x = event.clientX - rect.left - template.width / 2; const y = event.clientY - rect.top - template.height / 2; const nextStamp = { instanceId: buildInstanceId(template.id), src: template.src, width: template.width, height: template.height, x: clamp(x, 0, docSize.width - template.width), y: clamp(y, 0, docSize.height - template.height), };
实时拖动 + 撤销栈

拖动过程中只更新“临时状态”,拖动结束再写入历史栈,保证撤销栈干净。

// 实时更新 updateLiveStamps((prev) => prev.map(...)); // 拖动结束写入历史 if (drag.moved) commitStamps(liveStampsRef.current);

好处:撤销时不是“细碎步进”,而是一次拖动一个记录。

性能优化:懒渲染 + 队列

渲染 PDF 是最容易卡顿的地方。解决方案是:

  • IntersectionObserver:只有当页面进入视口时才渲染。
  • 渲染队列:保证渲染顺序,不并发拖慢主线程。
  • 预渲染前两页:首屏更快。
const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; const index = Number(entry.target.dataset.index); queueRender(index); }); }, { root: stageElement, rootMargin: '240px 0px', threshold: 0.1 } );

渲染队列逻辑:

const renderPage = async (index) => { const page = await pdfDoc.getPage(index + 1); const viewport = page.getViewport({ scale: PAGE_SCALE }); const canvas = canvasRefs.current[index]; const ctx = canvas.getContext('2d'); canvas.width = viewport.width; canvas.height = viewport.height; await page.render({ canvasContext: ctx, viewport }).promise; };

这样渲染压力被“分散到用户滚动过程”,不会一次性卡死。

导出图片(长图 + 逐页降级)

导出长图时,浏览器对 canvas 尺寸限制很严格。如果文档太长,直接导出会失败,因此需要检测并降级。

const isTooLarge = docSize.width > MAX_EXPORT_DIMENSION || docSize.height > MAX_EXPORT_DIMENSION || docSize.width * docSize.height > MAX_EXPORT_PIXELS; if (isTooLarge) { // 改为逐页导出 }

逐页导出时,要把全局印章坐标换算到当前页坐标,这样骑缝章也不会丢。

导出 PDF(完整文件)

导出 PDF 用pdf-lib做合成:

  1. 每一页画布(含印章)转为 PNG。
  2. 插入到新 PDF 页。
  3. 生成 PDF 并下载。
const pdfDocument = await PDFDocument.create(); const pngImage = await pdfDocument.embedPng(pngBytes); const pdfPage = pdfDocument.addPage([page.width, page.height]); pdfPage.drawImage(pngImage, { x: 0, y: 0, width: page.width, height: page.height });

下载逻辑:

const pdfBytes = await pdfDocument.save(); const blob = new Blob([pdfBytes], { type: 'application/pdf' }); const url = URL.createObjectURL(blob); link.download = `盖章结果-${new Date().toISOString().slice(0, 10)}.pdf`; link.href = url; link.click();

导出的 PDF 为“图片型 PDF”,兼容性高,但文字不可搜索。如果要保留矢量文字,需要更复杂的“原 PDF 叠加”方案。

扩展方向

  1. 矢量 PDF 导出:直接在原 PDF 叠加印章(更复杂,但可保留文字可搜索)。
  2. 通用库封装:提炼核心逻辑为core,React/Vue 只是适配层。
  3. 企业场景扩展:模板库、权限管理、批量盖章。

如果你准备上线到业务系统,建议在此基础上增加:

  • 盖章操作日志
  • 导出前的预检查(页数、尺寸)
  • 失败重试和导出进度提示

这样体验会更接近商业级工具。

http://www.jsqmd.com/news/339099/

相关文章:

  • Matlab裂缝识别检测系统(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_文章底部可以扫码
  • 手写系列:面试官问我 new 的原理,我直接甩出三个版本
  • 宏智树 AI——ChatGPT 学术版驱动的一站式论文写作学术智能平台
  • 机器学习python房屋数据分析可视化预测系统(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_文章底部可以扫码
  • 【开题答辩全过程】以 基于javaweb的音乐节管理系统为例,包含答辩的问题和答案
  • 千万不能忽视!选择洁净厂房设计施工的3大关键点
  • 协同过滤算法+SpringBoot Vue校园二手物品置换系统+万字文档(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_文章底部可以扫码
  • 底盘电控悬架cdc控制系统文档(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_文章底部可以扫码
  • 洁净厂房设计施工,专业企业必选
  • 多智能体协同编队控制(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_文章底部可以扫码
  • 写论文软件哪个好?宏智树 AI 实测:全流程学术适配,毕业生告别写作内耗
  • 风机和水轮机互补发电(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_文章底部可以扫码
  • Redis的Key和Value的设计原则有哪些?
  • Angular 2 JavaScript:深度解析与最佳实践
  • 特斯拉第三代Optimus亮相在即:从“演示”到“年产百万台”的产能挑战
  • 网络安全从入门到入狱,2026黑客技术路线图
  • nethogs 与 iftop 网络监控工具实战指南
  • R 数组:深入探索与高效使用
  • AngularJS 依赖注入
  • 告别利润回吐:5个你必须知道的股票顶部信号
  • 我拥有了超能力!Meta最牛文科生让三个AI互掐,竟造出一人技术部
  • Bootstrap 插件概览
  • GP8630N I2C和PWM转±10V/0-5V/0-10V/4-20mA DAC模块原理图设计,已量产
  • 永别了,互联网!Anthropic联创爆料:人类彻底出局
  • 实用指南:数字货币与区块链:金融革命中的新动力
  • 【开题答辩全过程】以 基于JAVAweb的影视创作论坛系统为例,包含答辩的问题和答案
  • 互联网大厂Java面试实战:以Spring Boot与微服务为核心的高效的技术场景剖析
  • Moltbook带来震撼!AI们自建“黑客空间”互怼哲学,人类只能围观
  • 量子化学中如何消除结构优化/过渡态计算产生的多余虚频?
  • 矢量网络分析仪(VNA)毫米波光