目录
前言
电子签名通俗来说就是通过技术手段实现在电子文档上加载电子形式的签名,其作用类似于纸质合同上的手写签名或加盖的公章。虽然电子签名多年来合法性一直遭到质疑,但其在企业工作流审批、请柬、单据保全等场景应用广泛,最近的项目中就有这样一个手写签名并生成PDF文件的需求。
实现思路
- 使用canvas来实现手写签名的功能,然后将canvas转化为图片,贴在签名的位置;
- 将整个需要生成文档的dom区域(canvas对应的dom节点所在的区域)使用html2canvas插件转成一张大图;
- 使用JsPDF插件将上述图片生成PDF文档;
- 对于文件内容较多的情况,需要合理选择分页位置;
生成签名
1. 在组件中定义canvas画布
//以在react中Hook组件为例子
const canvasDom = useRef()
<canvas className={styles['set-canvas']} ref={canvasDom} width="350" height="150"/>
2. 定义签名函数
const writing = (
beginX,
beginY,
stopX,
stopY,
ctx,
) => {
ctx.beginPath() // 开启一条新路径
ctx.globalAlpha = 1 // 设置图片的透明度
ctx.lineWidth = 3 // 设置线宽
ctx.strokeStyle = 'red' // 设置路径颜色
ctx.moveTo(beginX, beginY) // 从(beginX, beginY)这个坐标点开始画图
ctx.lineTo(stopX, stopY) // 定义从(beginX, beginY)到(stopX, stopY)的线条(该方法不会创建线条)
ctx.closePath() // 创建该条路径
ctx.stroke() // 实际地绘制出通过 moveTo() 和 lineTo() 方法定义的路径。默认颜色是黑色。
}
3. 注册监听事件
useEffect(() => {
let beginX; let beginY
const canvas = canvasDom.current
const ctx = canvas.getContext('2d')
ctx.fillStyle = '#fff'
ctx.fillRect(0, 0, canvas.width, canvas.height)
canvas.addEventListener('touchstart', function(event) {
event.preventDefault() // 阻止在canvas画布上签名的时候页面跟着滚动
beginX = event.touches[0].clientX - this.offsetLeft
beginY = event.touches[0].pageY - this.offsetTop
})
canvas.addEventListener('touchmove', (event) => {
event.preventDefault() // 阻止在canvas画布上签名的时候页面跟着滚动
event = event.touches[0]
const stopX = event.clientX - canvas.offsetLeft
const stopY = event.pageY - canvas.offsetTop
writing(beginX, beginY, stopX, stopY, ctx)
beginX = stopX // 这一步很关键,需要不断更新起点,否则画出来的是射线簇
beginY = stopY
})
}, [])
4. 实现效果图
如下图所示,点击确定,就会把canvas所画出来的签名以图片的形式显示在签名后面,点击充值,就会清空canvas所在画布的内容,以及签名后面的图片也会隐藏掉;这块儿的代码,会在文章底部全部分享出来,请向下翻阅查看吧!
5.下载图片
点击下载图片对应的代码如下:
<button style={{ marginRight: '4px' }} onClick={download}>下载图片</button>
// download所在的方法
const download = () => {
let img = new Image()
const url = canvasDom.current.toDataURL("image/jpeg", 1.0)
img.src = url
img.onload = () => {
let href = url
let a = document.createElement('a'); // 创建一个a节点插入的document
a.download = 'test.jpeg' // 设置a节点的download属性值
a.href = href ; // 将图片的src赋值给a节点的href
a.click()
}
}
6.下载PDF
点击下载PDF需要用到两个插件:html2canvas和jspdf
- 安装上述两个插件
npm install --save html2canvas npm install jspdf --save
-
把canvas下载成PDF的逻辑,单独封装出来,放在单独的一个文件夹中,建立htmlToPdf.js这个文件,此文件中对应的代码如下:
import html2canvas from 'html2canvas' import jsPdf from 'jspdf' /** * @param canvasDom 打印的节点,canvas所在的节点 */ async function htmlToPdf(canvasDom) { if (!canvasDom) { // tslint:disable-next-line:no-console console.error('导出节点不存在!') return } // 将html dom节点生成canvas const htmlCanvas = await getCanvasByHtmlId(canvasDom) // 将canvas对象转为pdf const pdf = canvasToPdf(htmlCanvas) // 通过浏览器下载pdf downPdf(pdf, '文件名') } /** * @param canvasDom 打印的节点,canvas所在的节点 */ async function getCanvasByHtmlId(canvasDom) { const canvas = await html2canvas(canvasDom, { scale: 2, useCORS: true, allowTaint: true, // taintTest: false, imageTimeout: 0, }).then((canvas) => { return canvas }) return canvas } /** * @param htmlCanvas canvas对象 */ function canvasToPdf(htmlCanvas) { const canvasWidth = htmlCanvas.width const canvasHeight = htmlCanvas.height const imgBase64 = htmlCanvas.toDataURL('image/jpeg', 1.0) // a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高 const imgWidth = 595.28 // 图片高度需要等比缩放 const imgHeight = 595.28 / canvasWidth * canvasHeight let pageHeight = imgHeight // pdf转化后页面总高度 let position = 0 const pdfInstance = new jsPdf('', 'pt', 'a4') pdfInstance.setFontSize(12) if (imgHeight < 841.89) { pdfInstance.addImage(imgBase64, 'JPEG', 0, 0, imgWidth, imgHeight) } else { while (pageHeight > 0) { pdfInstance.addImage(imgBase64, 'JPEG', 0, position, imgWidth, imgHeight) pageHeight -= 841.89 position -= 841.89 if (pageHeight > 0) { pdfInstance.addPage() } } } return pdfInstance } function downPdf(pdfInstance, title) { // 文件名过长导致下载失败 if (title.length > 50) { title = title.substring(title.length - 50) } pdfInstance.save(title + '.pdf', { returnPromise: true }).then(() => { // 搜狗浏览器下载机制问题暂时不关闭 if (!(navigator.userAgent.toLowerCase().indexOf('se 2.x') > -1)) { setTimeout(window.close, 300) } }) } export default htmlToPdf
- 在方法中调用第2步封装好的方法,具体代码如下:
// 引入封装好的方法 import HtmlToPdf from './htmlToPdf'; // 点击下载PDF调用此方法,传入canvas画布所在的DOM节点 const downPdf = () => { HtmlToPdf(canvasDom.current) } <button style={{ marginRight: '4px' }} onClick={downPdf}>下载PDF</button>
7.完整代码分享
分享到这里,就进入尾声了,奉上全部的代码如下,对你有帮助的话,就点赞或者在下方评论参与讨论吧!
import React, { useEffect, useState, useRef } from 'react'
import styles from './lili.scss';
import HtmlToPdf from './htmlToPdf';
function Others() {
const canvasDom = useRef()
const [imgSrc, setSrc] = useState('')
const writing = (
beginX,
beginY,
stopX,
stopY,
ctx,
) => {
ctx.beginPath() // 开启一条新路径
ctx.globalAlpha = 1 // 设置图片的透明度
ctx.lineWidth = 3 // 设置线宽
ctx.strokeStyle = 'red' // 设置路径颜色
ctx.moveTo(beginX, beginY) // 从(beginX, beginY)这个坐标点开始画图
ctx.lineTo(stopX, stopY) // 定义从(beginX, beginY)到(stopX, stopY)的线条(该方法不会创建线条)
ctx.closePath() // 创建该条路径
ctx.stroke() // 实际地绘制出通过 moveTo() 和 lineTo() 方法定义的路径。默认颜色是黑色。
}
useEffect(() => {
let beginX; let beginY
const canvas = canvasDom.current
const ctx = canvas.getContext('2d')
ctx.fillStyle = '#fff'
ctx.fillRect(0, 0, canvas.width, canvas.height)
canvas.addEventListener('touchstart', function(event) {
event.preventDefault() // 阻止在canvas画布上签名的时候页面跟着滚动
beginX = event.touches[0].clientX - this.offsetLeft
beginY = event.touches[0].pageY - this.offsetTop
})
canvas.addEventListener('touchmove', (event) => {
event.preventDefault() // 阻止在canvas画布上签名的时候页面跟着滚动
event = event.touches[0]
const stopX = event.clientX - canvas.offsetLeft
const stopY = event.pageY - canvas.offsetTop
writing(beginX, beginY, stopX, stopY, ctx)
beginX = stopX // 这一步很关键,需要不断更新起点,否则画出来的是射线簇
beginY = stopY
})
}, [])
const clearCanvas = () => {
const canvas = canvasDom.current
const ctx = canvas.getContext('2d')
setSrc('')
ctx.clearRect(0,0,canvas.width,canvas.height);
}
const sure = () => {
const a = canvasDom.current.toDataURL("image/jpeg", 1.0)
setSrc(a)
}
const download = () => {
let img = new Image()
const url = canvasDom.current.toDataURL("image/jpeg", 1.0)
img.src = url
img.onload = () => {
let href = url
let a = document.createElement('a'); // 创建一个a节点插入的document
a.download = 'test.jpeg' // 设置a节点的download属性值
a.href = href ; // 将图片的src赋值给a节点的href
a.click()
}
}
const downPdf = () => {
// 此方法的封装,见上文的步骤6.下载PDF
HtmlToPdf(canvasDom.current)
}
return (
<>
<div className={styles['set-name']}>
签名:
{ imgSrc && <img src={imgSrc} width="100" height="60"/> }
</div>
<canvas className={styles['set-canvas']} ref={canvasDom} width="350" height="150"/>
<button style={{ marginRight: '4px' }} onClick={sure}>确定</button>
<button style={{ marginRight: '4px' }} onClick={clearCanvas}>重置</button>
<button style={{ marginRight: '4px' }} onClick={downPdf}>下载PDF</button>
<button style={{ marginRight: '4px' }} onClick={download}>下载图片</button>
</>
)
}
export default Others;
用到的css样式:
// lili.scss样式文件
.set-canvas {
border: 1px solid #000;
border-radius: 0.05rem;
}
.set-name {
font-size: 0.32rem;
text-align: center;
margin-bottom: 0.2rem;
}