H5 PDF在线预览与手写签名:pdf.js与canvas实战避坑指南

发布时间:2026/10/18 8:57:29

H5 PDF在线预览与手写签名:pdf.js与canvas实战避坑指南 简介这份资源面向需要在网页端实现PDF在线预览与电子签名的前端开发者尤其适合合同签署、表单填写等业务场景。它整合了HTML5 canvas、PDF.js渲染、Web Worker异步处理等关键技术帮助读者理解从加载PDF到捕获手写笔迹、再将签名图像合并回文档的完整链路并涉及HTTPS传输与身份验证等安全考量。压缩包共15个文件约711KB以10个JavaScript脚本为核心配合2个CSS样式、1个HTML入口页面和1份示例PDF文档另含少量辅助文件结构紧凑便于快速运行与二次开发。目前已有419人学习下载。通过这份资源读者可获得可运行的在线预览与签名示例掌握PDF.js与canvas协同的实践思路并了解前端渲染与后端处理两种签名合并方案的取舍为搭建轻量级电子签署功能提供参考。1. 从一次合同回传的翻车说起h5 pdf在线预览 并支持签名到底解决什么问题去年帮一个做房屋租赁的朋友看他们的签约流程用户要在手机上打开合同 PDF看完直接手写签名回传。他们最初的做法是让用户下载 PDF用系统自带的看图工具打开签完再截图上传。结果就是各种玄学问题安卓机下载完找不到文件、iOS 截图签名糊成一团、后台收到的图片方向还是歪的。后来换成在 H5 页面里直接内嵌预览加签名整个链路才顺下来。这个项目要解决的就是这件事在浏览器里把 PDF 渲染出来翻页、缩放然后在上面叠加一层画布让用户手写签名最后把签名合成回 PDF 或者导出成图片回传。它适合做电子合同、审批单、报名表这类需要「看文档 签字确认」场景的前端和全栈开发者。核心关键词就是 pdf签名在线预览难点不在预览本身而在预览和签名两层怎么对齐坐标、怎么导出。2. 技术选型拆解pdf.js 渲染层与 canvas 签名层怎么配合2.1 为什么是 pdf.js 而不是直接 iframe 塞 PDF很多人第一反应是iframe srcxxx.pdf或者embed这在桌面浏览器上确实能显示但一到移动端就原形毕露iOS Safari 对 iframe 内 PDF 的支持时好时坏安卓各家 WebView 行为不一致更关键的是你拿不到渲染后的 DOM没法在上面精确定位签名区域。所以这个项目的渲染层用的是 pdf.js它把 PDF 每一页解析成 canvas页面结构完全由我们控制。pdf.js 有两种引入方式一种是官方提供的 viewer.html 整套界面另一种是只用核心库自己画。这个项目走的是后者因为要自定义签名层viewer 那套 UI 反而碍事。常见做法是通过 CDN 或者 npm 引入pdfjs-dist然后拿到getDocument方法。// 引入 pdf.js 核心库注意 worker 路径要配对否则会报 fake worker 警告 import * as pdfjsLib from pdfjs-dist; pdfjsLib.GlobalWorkerOptions.workerSrc /static/pdf.worker.min.js; // 加载 PDF 文档data 可以是 URL、ArrayBuffer 或 Uint8Array const loadingTask pdfjsLib.getDocument({ url: /api/contract/123.pdf, // 如果后端返回的是二进制流用 data 字段传 ArrayBuffer cMapUrl: /static/cmaps/, // 中文 PDF 必须配否则中文变方块 cMapPacked: true }); const pdfDoc await loadingTask.promise; console.log(总页数, pdfDoc.numPages);这里有几个参数值得说清楚。cMapUrl是中文 PDF 的血泪经验不配的话合同里的中文会渲染成空白或者乱码方块很多人第一次跑起来以为是字体问题其实是缺 cmap 映射表。workerSrc必须指向和主库版本一致的 worker 文件版本不匹配会出现渲染到一半卡死的情况。getDocument返回的是个 Promise拿到pdfDoc之后才能取单页。2.2 单页渲染与高清屏适配拿到pdfDoc之后逐页渲染的逻辑是核心。这里最容易翻车的是清晰度问题直接按 PDF 原始尺寸画到 canvas 上在 Retina 屏上会糊。解决办法是按devicePixelRatio放大 canvas 的实际像素再用 CSS 把显示尺寸压回去。async function renderPage(pageNum, container) { const page await pdfDoc.getPage(pageNum); // scale 是缩放系数1.5 是兼顾清晰度和性能的常用值 const viewport page.getViewport({ scale: 1.5 }); const dpr window.devicePixelRatio || 1; const canvas document.createElement(canvas); const ctx canvas.getContext(2d); // 实际像素放大 dpr 倍保证高清屏不糊 canvas.width viewport.width * dpr; canvas.height viewport.height * dpr; // CSS 尺寸保持逻辑尺寸 canvas.style.width viewport.width px; canvas.style.height viewport.height px; ctx.scale(dpr, dpr); await page.render({ canvasContext: ctx, viewport: viewport }).promise; container.appendChild(canvas); return { canvas, viewport }; }scale控制渲染精度调太大会让大文档内存飙升1.5 到 2 之间比较稳。dpr那两行是高清屏适配的关键少了它安卓高端机和 iPhone 上字都是虚的。page.render返回的 Promise 必须 await否则连续渲染多页时会出现 canvas 内容错乱这是异步竞争导致的很多人踩过。2.3 签名层用 canvas 叠加而不是改 PDF 源文件签名层单独用一个透明 canvas 盖在 PDF canvas 上面而不是去修改 PDF 本身。这样做的好处是签名可以随时撤销重画不用反复重新解析 PDF。两层用绝对定位叠在一起父容器设position: relative。div classpdf-wrapper styleposition: relative; !-- PDF 渲染出来的 canvas 放这里 -- div idpdf-container/div !-- 签名层透明覆盖在上面 -- canvas idsign-canvas styleposition: absolute; top: 0; left: 0;/canvas /div签名 canvas 的宽高要和 PDF canvas 的逻辑尺寸完全一致否则坐标对不上。手写签名的绘制逻辑就是监听touchstart、touchmove、touchend把触点坐标转成 canvas 坐标画线。移动端要记得preventDefault阻止页面滚动不然签名的时候页面跟着乱跑。3. 核心模块实战从手写签名到导出回传的完整链路3.1 手写签名的绘制与坐标换算签名绘制的坑集中在坐标换算上。触摸事件的坐标是相对于视口的而 canvas 有自己的坐标系中间还隔着滚动和缩放。正确做法是用getBoundingClientRect拿到 canvas 在视口中的位置再减掉。const signCanvas document.getElementById(sign-canvas); const sctx signCanvas.getContext(2d); let drawing false; // 把触摸/鼠标事件坐标转成 canvas 内部坐标 function getPos(e) { const rect signCanvas.getBoundingClientRect(); const point e.touches ? e.touches[0] : e; return { x: (point.clientX - rect.left) * (signCanvas.width / rect.width), y: (point.clientY - rect.top) * (signCanvas.height / rect.height) }; } signCanvas.addEventListener(touchstart, (e) { e.preventDefault(); // 阻止页面滚动否则签名时页面乱动 drawing true; const pos getPos(e); sctx.beginPath(); sctx.moveTo(pos.x, pos.y); }, { passive: false }); signCanvas.addEventListener(touchmove, (e) { if (!drawing) return; e.preventDefault(); const pos getPos(e); sctx.lineTo(pos.x, pos.y); sctx.strokeStyle #000; sctx.lineWidth 2; sctx.lineCap round; // 圆头笔触签名更自然 sctx.stroke(); }, { passive: false }); signCanvas.addEventListener(touchend, () { drawing false; });getPos里那个signCanvas.width / rect.width的比值不能省因为 canvas 实际像素和 CSS 显示尺寸可能不一样直接拿 clientX 画会导致笔迹偏移。lineCap: round让笔画连接处圆润不然签名看起来像折线。passive: false配合preventDefault才能真的阻止滚动只写 preventDefault 在部分浏览器里会被忽略。3.2 签名合成回 PDF 的两种方案签名画完之后要回传有两种主流方案。第一种是把签名 canvas 转成图片和 PDF 一起传给后端由后端用 PDF 库合成。第二种是纯前端用 pdf-lib 把签名图片嵌入 PDF 再导出。这个项目两种都支持前端导出适合即时预览后端合成适合需要存档的场景。前端合成用 pdf-lib思路是加载原始 PDF把签名图片画到指定页的指定位置。import { PDFDocument } from pdf-lib; async function mergeSignature(pdfBytes, signDataUrl, pageIndex, position) { const pdfDoc await PDFDocument.load(pdfBytes); const pages pdfDoc.getPages(); const targetPage pages[pageIndex]; const { width, height } targetPage.getSize(); // 把 canvas 的 dataURL 转成 pdf-lib 能识别的图片 const signImage await pdfDoc.embedPng(signDataUrl); // position 用相对比例传避免不同页面尺寸对不上 targetPage.drawImage(signImage, { x: width * position.xRatio, y: height * position.yRatio, width: signImage.width * 0.5, height: signImage.height * 0.5 }); const mergedBytes await pdfDoc.save(); return mergedBytes; // Uint8Array可直接下载或上传 }embedPng要求传入的是 PNG 格式的 dataURL如果签名 canvas 导出的是 JPEG要用embedJpg。坐标这里用相对比例xRatio、yRatio而不是绝对像素是因为 PDF 页面尺寸各不相同写死像素换个文档就错位。pdfDoc.save()返回的是 Uint8Array可以直接new Blob下载也可以塞进 FormData 上传。3.3 导出图片回传的降级方案有些后端接口只收图片不收 PDF这时候就得把「PDF 页面 签名」整体导出成一张图。做法是把 PDF canvas 和签名 canvas 画到同一个离屏 canvas 上。function exportAsImage(pdfCanvas, signCanvas) { const out document.createElement(canvas); out.width pdfCanvas.width; out.height pdfCanvas.height; const octx out.getContext(2d); octx.drawImage(pdfCanvas, 0, 0); // 先画 PDF 内容 octx.drawImage(signCanvas, 0, 0); // 再叠签名 // toBlob 比 toDataURL 省内存适合大图 return new Promise((resolve) { out.toBlob((blob) resolve(blob), image/png, 0.92); }); }顺序不能反先 PDF 后签名否则签名会被 PDF 盖住。toBlob的第三个参数是质量系数PNG 下其实无效但换成 JPEG 时 0.92 是个清晰度和体积的平衡点。大文档导出整页图内存占用高常见做法是只导出签名所在的那一页而不是全部页面。4. 避坑与常见问题排查那些让你怀疑人生的细节4.1 中文显示成方块或空白现象是 PDF 能渲染但中文全是方块。原因是 pdf.js 默认不带中文字体映射表。解决办法是在getDocument里配cMapUrl指向 cmaps 目录并且cMapPacked: true。cmaps 文件要从 pdf.js 发行包里拷到静态资源目录路径写错一样不生效打开控制台能看到 404。4.2 签名笔迹和手指位置偏移现象是手指画在 A 点笔迹出现在 B 点越往右下偏得越多。原因是 canvas 实际像素尺寸和 CSS 显示尺寸不一致代码里直接用了 clientX。解决就是前面getPos里那个缩放比值把 clientX 减掉 rect.left 之后再乘以canvas.width / rect.width。缩放页面或者横竖屏切换后要重新计算因为 rect 变了。4.3 移动端签名时页面跟着滚动现象是手指在签名区画整个页面上下滚。原因是 touchmove 默认行为没被阻止。解决是监听时加{ passive: false }并在回调里e.preventDefault()。只加 preventDefault 不加 passive: false在 Chrome 里会被当成 passive 监听器忽略掉这个坑很隐蔽。4.4 多页 PDF 渲染内存暴涨甚至崩溃现象是几十页的 PDF 一打开浏览器就卡死。原因是所有页的 canvas 同时存在每个都是高清大图。解决办法是只渲染当前页和相邻页翻页时销毁远离的 canvas把canvas.width 0释放内存。或者用虚拟滚动只保留视口内的页面。4.5 导出的 PDF 签名位置对不上现象是预览时签名位置正确导出后签名跑到别处。原因是预览用的坐标是屏幕坐标导出用的应该是 PDF 页面坐标两者比例不同。解决是签名位置统一用相对比例存储导出时按目标页面的实际尺寸换算不要存绝对像素。5. 二次开发改造把签名位置做成可配置的模板真正落地到业务里签名位置不能写死不同合同签字栏位置不一样。我一般会把签名区域抽象成一个配置数组每个元素描述在哪一页、什么相对位置、多大尺寸。这样换一份合同模板只需要改配置不用动渲染代码。// 签名区域配置页码从 0 开始坐标用相对比例 const signSlots [ { page: 2, xRatio: 0.62, yRatio: 0.18, widthRatio: 0.22, heightRatio: 0.08, label: 甲方签字 }, { page: 2, xRatio: 0.62, yRatio: 0.30, widthRatio: 0.22, heightRatio: 0.08, label: 乙方签字 } ]; // 根据配置在签名层上画出提示框引导用户签在正确位置 function drawSlotHints(signCtx, viewport, slots, currentPage) { slots.filter(s s.page currentPage).forEach(slot { signCtx.save(); signCtx.strokeStyle rgba(0, 120, 255, 0.6); signCtx.setLineDash([6, 4]); // 虚线框不遮挡内容 signCtx.strokeRect( viewport.width * slot.xRatio, viewport.height * slot.yRatio, viewport.width * slot.widthRatio, viewport.height * slot.heightRatio ); signCtx.restore(); }); }配置里用相对比例而不是像素是为了适配不同尺寸的 PDF 页面。drawSlotHints在签名层上画虚线框用户一眼就知道该签哪里比纯文字提示友好得多。setLineDash画虚线save和restore保证样式不污染后续绘制。验证改造是否成功我习惯用三份不同尺寸的合同各跑一遍A4 纵向、A4 横向、以及一份扫描件。重点看签名框是否都落在签字栏上、导出后位置是否一致。扫描件因为本身是图片型 PDF还要确认 pdf.js 能正常渲染有些扫描件没嵌字体渲染出来是空白这种就得走后端转图片的方案。还有一个实用技巧是把签名数据存成矢量路径而不是位图。位图放大后会糊矢量路径导出时按目标尺寸重新描边清晰度无损。做法是记录每次 touchmove 的点序列导出时用这些点重新画。代价是数据量大一些但合同这种场景清晰度优先。从那以后我每次接这类需求都强制先把「预览清晰度、签名坐标、导出位置」这三件事在真机上各验一遍再往下写业务逻辑因为这三处一旦返工牵连的是整个链路。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/18 8:52:29

AI编程工具范式收敛:TRAE双端合并背后的协议对齐

1. 这不是功能叠加,而是AI编程工具的范式收敛“TRAE 合并双端之后,AI 编程工具正在走向同一个方向”——这句话初看像一句行业观察,细想却是一把解剖刀,精准切开了过去三年AI原生开发工具演进的底层逻辑。我从2021年就开始跟踪这类…

2026/10/18 8:52:29

YOLO边缘端部署实战:C#上位机与工业缺陷检测系统架构解析

1. 项目整体思路:为什么把YOLO放到边缘端跑1.1 场景痛点:传统上位机方案卡在哪去年年中接到一条产线的视觉检测改造需求,我当时最开始的想法是沿用过去的套路:工业相机拍照,图像传到工控机的上位机软件里,上…

2026/10/18 8:52:29

HTML5响应式简历模板:单文件交付与绿色语义设计

简介:这是一款专为求职者打造的绿色主题HTML5响应式个人简历模板,面向前端初学者与正在准备求职作品集的开发者,解决在线简历缺乏专业性、适配性差及技术展示力不足的问题。资源包共50个文件,包含20张高清图片(用于头像…

2026/10/18 13:52:50

UE4物理时间调度器:解决暂停时Timer失效问题

1. 项目概述:为什么“暂停时还能跑的Timer”是UE4里一个高频痛点在UE4项目开发中,我几乎每次带新人做第一个完整功能模块时,都会遇到同一个问题:明明逻辑写对了,但一按P键暂停游戏,那个本该3秒后弹出的提示…

2026/10/18 13:52:50

HOOPS AI:AI落地CAD的最务实路径,藏在数据管道里

最近总做CAD/PLM的朋友问我:AI到底该怎么进三维设计这条赛道。ChatGPT能写文档,扩散模型能生成图片,可放到CAD这种要精确到小数点后四位、还要对制造负责的领域,AI好像一下子就没那么神了。其实答案不在最上层,而在中间…

2026/10/18 13:52:50

Python+Vue学院兼职平台开发实战:Flask/Django后端与部署全解析

1. 项目概述与核心需求拆解站在一个做过类似项目的开发者角度,第一次看到“基于Python的某学院兼职平台”这个题目,我第一反应是:这是一个非常典型的Web全栈练手项目,也是很多计算机专业同学毕设或者课设最爱选的选题方向。它既有…

2026/10/18 13:52:50

Excel存货健康度看板:12指标+3类归因的业财监控方案

简介:本资源是一份面向企业财务、供应链及数据分析岗位从业者的「财经存货看板」专业文档,聚焦存货可视化管理与财务协同分析场景,解决实时库存监控、呆滞风险识别、周转率追踪及合规数据支撑等核心痛点。文档为单文件Word格式(.d…

2026/10/18 13:47:50

Windows下Chromium编译指南:GN/Ninja参数配置与避坑实践

在Windows上编译Chromium这件事,说难不难,说简单也真不简单。经常有同行跑来问我:为什么gn gen一跑就报错?为什么同一份源码,别人半天就能跑起来,我这边编了一整夜还在链接?最后排查下来&#x…

2026/10/18 0:01:50

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/18 0:01:50

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑