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

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

H5 PDF在线预览与手写签名合成:pdf.js与pdf-lib实战避坑指南 简介这是一份面向前端开发者的H5 PDF在线预览与电子签名实现方案适用于合同签署、表单填写等需要网页内直接查看并手写签名的业务场景。资源包共15个文件约711KB以10个JavaScript脚本为核心配合2个CSS样式、1个HTML页面与1个示例PDF文档涵盖PDF渲染、手势触控、二维码生成等模块结构紧凑便于快速集成。技术实现上预览部分基于PDF.js在Canvas中渲染文档签名部分借助HTML5 Canvas捕获鼠标或触笔笔迹并转为图像再结合PDF处理库将签名合并回原文档同时涉及Web Worker避免主线程阻塞及HTTPS传输等安全考量。已有419人学习下载读者可从中获取完整的页面结构、脚本调用方式与签名落位思路适合需要快速搭建在线预览签名功能的中级前端开发者参考。1. 从一次合同回传的翻车说起H5 里预览 PDF 并签名的真实需求去年帮一个做企业服务的团队改后台对方运营提了个需求销售在手机上打开客户发来的合同 PDF看完直接在末尾手写签名点确认回传。听起来简单结果第一版用 iframe 直接嵌 PDF 链接安卓微信里白屏iOS Safari 里能看但手指一划就整页缩放签名板叠上去坐标全乱。这就是典型的「预览和签名各做各的最后对不上」。H5 PDF 在线预览并支持签名本质是两件事拼在一起一是把 PDF 渲染到 canvas 或 DOM 上让用户能翻页、缩放二是拿到用户在某一页上的手写笔迹把它转成坐标数据最终合成到 PDF 文件里。它解决的是「不装 App、不依赖桌面软件在浏览器里完成看和签」的场景适合电子合同、审批单、回执确认这类业务。下面按我实际拆过的路径从渲染选型讲到签名合成把参数和坑都摊开。2. 预览方案选型pdf.js 渲染到 canvas 还是直接塞 iframe2.1 为什么不能只靠浏览器原生 PDF 插件浏览器自带的 PDF 查看器Chrome 的 pdfium、Safari 的 PDFKit确实能显示但它是黑匣子你拿不到当前页码、缩放比例、滚动位置也没法在上面精确叠加一个签名层。移动端更麻烦微信内置浏览器对原生 PDF 的支持时好时坏经常直接触发下载而不是预览。所以只要涉及「在 PDF 上做交互」就必须自己接管渲染。常见做法是用 Mozilla 的 pdf.js它把 PDF 解析成页面对象每页可以渲染到独立的 canvas 上。这样页码、缩放、坐标全在你手里签名层想放哪就放哪。代价是要引入 pdf.js 的库文件并且处理 worker 路径和跨域。2.2 pdf.js 的最小可用渲染流程先看一段能跑起来的核心代码把第一页渲染到 canvas// 引入 pdf.js 后pdfjsLib 是全局对象 const loadingTask pdfjsLib.getDocument({ url: /files/contract.pdf, // 关键参数cMap 用于中文等复杂字体不配会乱码 cMapUrl: /pdfjs/cmaps/, cMapPacked: true, }); loadingTask.promise.then((pdf) { // 取第一页 return pdf.getPage(1); }).then((page) { const scale 1.5; // 缩放比例移动端建议 1.2~2.0 const viewport page.getViewport({ scale }); const canvas document.getElementById(pdf-canvas); const ctx canvas.getContext(2d); // canvas 尺寸必须按 viewport 设置否则模糊或裁切 canvas.width viewport.width; canvas.height viewport.height; const renderTask page.render({ canvasContext: ctx, viewport: viewport, }); return renderTask.promise; }).then(() { console.log(第一页渲染完成); }).catch((err) { console.error(渲染失败, err); });逻辑说明getDocument负责加载和解析getPage拿到单页对象getViewport根据缩放算出画布尺寸render把页面画到 canvas。参数上scale直接决定清晰度和内存占用移动端别超过 2.0否则大文档容易卡死cMapUrl指向 pdf.js 包里的 cmaps 目录中文合同不配这个大概率显示方块。2.3 多页与翻页控制单页渲染只是起点合同通常十几页。我一般维护一个currentPage变量翻页时清空 canvas 再渲染目标页let currentPage 1; let pdfDoc null; function renderPage(num) { pdfDoc.getPage(num).then((page) { const viewport page.getViewport({ scale: 1.5 }); const canvas document.getElementById(pdf-canvas); const ctx canvas.getContext(2d); canvas.width viewport.width; canvas.height viewport.height; page.render({ canvasContext: ctx, viewport }).promise.then(() { currentPage num; document.getElementById(page-info).textContent num / pdfDoc.numPages; }); }); } // 上一页 / 下一页按钮绑定 document.getElementById(prev).onclick () { if (currentPage 1) return; renderPage(currentPage - 1); }; document.getElementById(next).onclick () { if (currentPage pdfDoc.numPages) return; renderPage(currentPage 1); };这里有个容易忽略的点每次渲染前必须重设canvas.width和canvas.height否则 pdf.js 会在旧画布上叠加出现重影。翻页边界要自己判断pdf.js 不会帮你拦。2.4 iframe 方案什么时候还能用如果需求只是「看一眼」不需要签名和交互iframe 嵌 PDF 链接仍然是最省事的代码一行iframe src/files/contract.pdf width100% height600px/iframe但它和签名需求天然冲突你无法在 iframe 内部定位坐标跨域时连内容都读不到。所以只要标题里带了「签名」iframe 就只能当降级方案不能当主方案。3. 签名层实现canvas 手写、坐标换算与笔迹数据3.1 签名 canvas 的初始化与绘制签名层是叠在 PDF canvas 上面的另一个 canvas用绝对定位覆盖。核心是监听 touch 和 mouse 事件把轨迹画出来const signCanvas document.getElementById(sign-canvas); const sctx signCanvas.getContext(2d); let drawing false; let lastPoint null; // 初始化画笔 sctx.lineWidth 3; sctx.lineCap round; sctx.lineJoin round; sctx.strokeStyle #000; function getPos(e) { const rect signCanvas.getBoundingClientRect(); const touch e.touches ? e.touches[0] : e; return { x: touch.clientX - rect.left, y: touch.clientY - rect.top, }; } signCanvas.addEventListener(touchstart, (e) { e.preventDefault(); // 阻止页面滚动否则签名时页面乱跑 drawing true; lastPoint getPos(e); }); signCanvas.addEventListener(touchmove, (e) { e.preventDefault(); if (!drawing) return; const p getPos(e); sctx.beginPath(); sctx.moveTo(lastPoint.x, lastPoint.y); sctx.lineTo(p.x, p.y); sctx.stroke(); lastPoint p; }); signCanvas.addEventListener(touchend, () { drawing false; });逻辑说明getBoundingClientRect把屏幕坐标转成 canvas 内部坐标这是坐标不漂移的关键。touchstart里必须preventDefault不然手指一划页面跟着滚签名直接断线。lineCap和lineJoin设成 round笔迹才圆润默认的 miter 会有尖角。3.2 坐标换算屏幕像素到 PDF 点签名最终要落到 PDF 上而 PDF 的坐标单位是「点」1 点 1/72 英寸原点在左下角和 canvas 的左上角原点相反。所以存笔迹时不能直接存 canvas 像素要按比例换算// 假设 PDF 页面原始宽度是 pdfWidth点canvas 显示宽度是 canvasWidth像素 const ratio pdfWidth / canvasWidth; function toPdfPoint(canvasPoint) { return { x: canvasPoint.x * ratio, // y 轴翻转canvas 从上往下PDF 从下往上 y: pdfHeight - canvasPoint.y * ratio, }; }参数说明pdfWidth和pdfHeight从page.getViewport({ scale: 1 })拿那是 PDF 的原始尺寸。ratio是显示缩放和原始尺寸的桥梁。y 轴翻转是最容易翻车的地方我第一次做时签名整体上下颠倒排查了半天才发现是原点方向问题。3.3 笔迹数据的存储结构不要只存一张图片要存可重绘的轨迹点这样缩放、重签、撤销都好做// 每条笔迹是一个点数组 let strokes []; function startStroke(point) { strokes.push([point]); } function addPoint(point) { strokes[strokes.length - 1].push(point); } // 重绘所有笔迹比如窗口 resize 后 function redraw() { sctx.clearRect(0, 0, signCanvas.width, signCanvas.height); strokes.forEach((stroke) { sctx.beginPath(); stroke.forEach((p, i) { if (i 0) sctx.moveTo(p.x, p.y); else sctx.lineTo(p.x, p.y); }); sctx.stroke(); }); }存点而不是存图好处是撤销只需strokes.pop()重绘也快。点太密时可以按距离抽稀比如两点距离小于 2 像素就丢弃能显著减小数据量。3.4 撤销与清空撤销就是弹出最后一条笔迹再重绘清空就是重置数组document.getElementById(undo).onclick () { strokes.pop(); redraw(); }; document.getElementById(clear).onclick () { strokes []; redraw(); };这两个操作都依赖redraw所以笔迹数据必须完整保留不能画完就丢。我见过有人只往 canvas 上画不存数据结果撤销功能根本没法实现只能整页重来。4. 合成与导出把签名烧进 PDF 的两种路径4.1 前端合成pdf-lib 叠加图片签名画完后最直接的做法是把签名 canvas 导出成 PNG再用 pdf-lib 贴到指定页的指定位置import { PDFDocument } from pdf-lib; async function mergeSignature(pdfBytes, signDataUrl, pageIndex, pos) { const pdfDoc await PDFDocument.load(pdfBytes); const pages pdfDoc.getPages(); const page pages[pageIndex]; // 把 dataURL 转成图片嵌入 const pngImage await pdfDoc.embedPng(signDataUrl); const { width, height } pngImage.scale(0.5); // 缩放签名大小 page.drawImage(pngImage, { x: pos.x, y: pos.y, width: width, height: height, }); const mergedBytes await pdfDoc.save(); return mergedBytes; }逻辑说明PDFDocument.load读入原始 PDF 字节embedPng把签名图嵌进去drawImage定位。参数pos.x、pos.y用的是 PDF 坐标左下角原点所以要用 3.2 节换算后的值。scale(0.5)控制签名显示大小具体倍数看合同留白。4.2 后端合成前端只传坐标和图片前端合成的问题是大 PDF 在手机上加载和保存都吃内存而且原始文件暴露给了客户端。更稳的做法是前端只传「签名图片 页码 坐标」后端用 Python 的 pypdf 或 Java 的 PDFBox 合成# 后端示例Python pypdf reportlab 生成签名图 from pypdf import PdfReader, PdfWriter from reportlab.pdfgen import canvas from reportlab.lib.utils import ImageReader import io def sign_pdf(src_path, sign_img_path, page_num, x, y, out_path): reader PdfReader(src_path) writer PdfWriter() # 生成一个只含签名的透明覆盖层 packet io.BytesIO() can canvas.Canvas(packet) can.drawImage(ImageReader(sign_img_path), x, y, width120, height60, maskauto) can.save() packet.seek(0) overlay PdfReader(packet) for i, page in enumerate(reader.pages): if i page_num: page.merge_page(overlay.pages[0]) writer.add_page(page) with open(out_path, wb) as f: writer.write(f)参数说明x、y是签名左下角在 PDF 页面的坐标width、height控制签名尺寸maskauto让 PNG 透明背景生效。merge_page把覆盖层合到目标页其他页原样保留。这种方案前端压力小也便于留痕审计。4.3 两种路径怎么选维度前端 pdf-lib后端合成实现速度快纯 JS需接口联调大文件表现手机易卡稳定安全性原文件下发原文件不出服务端适用场景小合同、Demo正式业务、批量我一般建议验证阶段用前端 pdf-lib 快速跑通正式上线切后端合成。两者坐标体系一致切换成本主要在接口。5. 避坑与排查签名错位、中文乱码、移动端手势冲突5.1 签名位置整体偏移现象签名贴上去后整体偏左或偏上和预期位置差几十像素。原因canvas 显示尺寸和 PDF 原始尺寸的比例没算对或者忽略了 canvas 的 CSS 尺寸和实际像素尺寸不一致。解决统一用getBoundingClientRect拿显示尺寸用getViewport({ scale: 1 })拿原始尺寸两者相除得到比例所有坐标都过一遍这个比例。5.2 中文合同显示方块现象PDF 里中文全是方块或空白。原因pdf.js 默认不带中文字体映射缺少 cmaps。解决下载 pdf.js 发行包里的cmaps目录配置cMapUrl指向它并设cMapPacked: true。这一步漏了英文文档正常中文必挂。5.3 移动端签名时页面跟着滚现象手指在签名区滑动页面上下滚动笔迹断断续续。原因touchmove 默认行为触发页面滚动。解决在touchstart和touchmove里都调e.preventDefault()同时给签名 canvas 加 CSStouch-action: none双保险。5.4 翻页后签名消失或错页现象签完第一页翻到第二页再翻回来签名没了或者签到了错误的页。原因笔迹数据没和页码绑定或者翻页时清空了 canvas 但没清空 strokes。解决用{ page: 1, strokes: [...] }的结构按页存笔迹翻页时根据当前页取对应数据重绘别用全局单一数组。5.5 导出 PDF 体积暴涨现象合成后文件从几百 KB 变成几 MB。原因签名 PNG 分辨率过高或者 pdf-lib 重复嵌入图片。解决导出签名图时限制尺寸比如最大宽度 400 像素用canvas.toDataURL(image/png)前先缩小同一张签名图只 embed 一次多页复用同一个 image 对象。6. 进阶技巧用坐标归一化让签名适配任意缩放前面所有坐标换算都依赖一个前提你知道当前显示比例。但用户可能双指缩放、旋转屏幕、切换横竖屏比例随时在变。如果每次都重新算代码会散得到处都是。我的习惯是把签名坐标归一化到 0~1 区间存相对位置渲染时再乘当前尺寸。具体做法存笔迹时x 除以 canvas 显示宽度y 除以显示高度得到 0~1 的相对值。重绘或合成时用相对值乘以目标尺寸。这样无论页面怎么缩放签名永远落在合同的同一个相对位置。// 存归一化坐标 function normalize(point, w, h) { return { x: point.x / w, y: point.y / h }; } // 还原到目标尺寸 function denormalize(point, w, h) { return { x: point.x * w, y: point.y * h }; }配合一个 resize 监听屏幕变化时用归一化数据重绘签名不会跑偏window.addEventListener(resize, () { const rect signCanvas.getBoundingClientRect(); signCanvas.width rect.width; signCanvas.height rect.height; redrawWithNormalized(strokes, rect.width, rect.height); });验证方法也简单签完后把手机横过来再竖回去看签名是否还在原位再导出 PDF用桌面阅读器打开对比签名和合同文字的相對位置。如果两边一致说明归一化链路是通的。还有一个细节合成到 PDF 时归一化坐标要乘的是 PDF 原始尺寸点不是屏幕尺寸。所以链路是「屏幕坐标 → 归一化 → PDF 点坐标」中间别混用。我踩过一次坑归一化后直接乘了屏幕宽度去合成结果签名跑到页面外排查时一度怀疑是 pdf-lib 的 bug其实是自己单位搞混了。从那以后我每次做签名合成都强制走一遍「归一化 → 目标尺寸 → 导出验证」三步不管需求多简单都不省。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/18 8:57:29

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

简介:这份资源面向需要在网页端实现PDF在线预览与电子签名的前端开发者,尤其适合合同签署、表单填写等业务场景。它整合了HTML5 canvas、PDF.js渲染、Web Worker异步处理等关键技术,帮助读者理解从加载PDF到捕获手写笔迹、再将签名图像合并回…

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 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
免费获取方案
☎咨询二维码 ☎ ↑