前言
電子簽名通俗來說就是通過技術手段實現在電子文檔上加載電子形式的簽名,其作用類似于紙質合同上的手寫簽名或加蓋的公章。雖然電子簽名多年來合法性一直遭到質疑,但其在企業工作流審批、請柬、單據保全等場景應用廣泛,最近的項目中就有這樣一個手寫簽名并生成PDF文件的需求。
實現思路
- 使用canvas來實現手寫簽名的功能,然后將canvas轉化為圖片,貼在簽名的位置;
- 將整個需要生成文檔的dom區域使用html2canvas插件轉成一張大圖;
- 使用JsPDF插件將上述圖片生成PDF文檔;
- 對于文件內容較多的情況,需要合理選擇分頁位置;
生成簽名
1. 在tsx中定義canvas畫布
<canvas className={styles.canvas} ref={canvasDom} width="350" height="150" />
注意
:Canvas的寬高必須要使用內聯樣式定義,這是因為Canvas標簽有自己的默認寬高300px×150px。它內聯樣式定義的width和height是繪畫區域(畫布)實際寬度和高度,繪制的圖形都是在這個上面。如果在style外鏈文件中定義其width和height,那么這個width和height是Canvas在瀏覽器中被渲染的高度和寬度。如果Canvas中沒有直接定義width和height沒或值不正確,就會被設置成默認值{width:300px,height:150px}。所以,如果你在style中外鏈文件中設置了canvas {width: 200px; height: 200px;},卻沒有直接在canvas上定義畫布寬高,那么此時你輸出canvas.height 值依舊為150,canvas.width值依舊為300。也就是一塊150×300的畫布在200×200的區域渲染,因而圖片會出現拉伸、變形等現象。
2. 定義簽名函數
const writing = (
beginX: number,
beginY: number,
stopX: number,
stopY: number,
ctx: any,
) => {
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. 注冊監聽事件
let beginX: number, beginY: number;
const canvas: HTMLCanvasElement = canvasDom.current;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
canvas.addEventListener('touchstart', function(event: any) {
event.preventDefault(); // 阻止在canvas畫布上簽名的時候頁面跟著滾動
beginX = event.touches[0].clientX - this.offsetLeft;
beginY = event.touches[0].pageY - this.offsetTop;
});
canvas.addEventListener('touchmove', (event: any) => {
event.preventDefault(); // 阻止在canvas畫布上簽名的時候頁面跟著滾動
event = event.touches[0];
let stopX = event.clientX - canvas.offsetLeft;
let stopY = event.pageY - canvas.offsetTop;
writing(beginX, beginY, stopX, stopY, ctx);
beginX = stopX; // 這一步很關鍵,需要不斷更新起點,否則畫出來的是射線簇
beginY = stopY;
});
注意
:
- 在注冊“touchstart”和“touchmove”事件時,需要阻止默認事件,否則頁面會跟著手勢上下滑動。
- 移動端的每個觸摸事件對象中都包括了touches這個屬性,它用于描述位于屏幕上的所有手指的一個列表,獲取當前事件對象我們習慣性的使用event = event.touches[0],而在PC端則不需要這么操作。
- offsetLeft值跟offsetTop值跟父級元素沒關系,而是跟其上一級的定位元素(除position:static外的所有定位如fixed,relative,absolute元素)有關系。若上一級定位元素都沒有除position:staice外的定位,則這個偏移量是相對于body而言的。
需要理清移動端事件對象的幾個屬性,⏬

clientX/clientY: 觸摸位置距離當前body可視區域的x,y坐標;
pageX/pageY: 對于整個頁面來說,觸摸位置距離body左上角的x,y坐標,包括被scrollTop和scrollLeft的值;
screenX/screenY: 觸摸位置距離顯示器左邊和頂部的x,y距離。
所以,在獲取結束點坐標的時候,如果當前頁面沒有出現滾動條,使用clientY和pageY計算差別不大,如果頁面比較長,出現了滾動條,那么就必須要使用pageY來計算。clientX同理,但是移動端通常橫向滾動的場景不多,所以用clientX來計算即可。
在簽名(touchmove)這個動作過程中,我們需要不斷的更新起點位置,否則畫出來是這樣🔽

其實這個原理和微積分很相似,線段本質上就是由無窮多個小線段組成,宏觀一點來看可以把線段當成一個個長度很小的小線段首尾相連構成。所以我一直覺得編程編到最后就是考驗一個人的數學能力,交并集、邏輯思維、算法等都能看到數學的身影。最后生成簽名如下:

生成PDF文檔
html2canvas是一款將HTML代碼轉換成Canvas的插件,因此需要用一個div包裹住需要打印的內容區域,獲得這個dom節點。
html2Canvas(dom, {
allowTaint: true,
width: dom.offsetWidth, //設置獲取到的canvas寬度
height: dom.offsetHeight, //設置獲取到的canvas高度
x: 0, //頁面在水平方向滾動的距離
y: 0, //頁面在垂直方向滾動的距離
})
注意
:此處需要設置width和height及x,y,否則當頁面內容只有一頁的時候沒有問題,但是若頁面內容有很多頁的時候,就會出現生成的圖片只有一小部分有內容的現象。問題就出現在這個配置參數上,若沒有設置寬高,則默認只取當前視口的內容,丟棄掉其他超出當前視口的內容。
設置打印參數:
const print = () => {
let dom: HTMLElement = pdfDom.current;
html2Canvas(dom, {
allowTaint: true,
width: dom.offsetWidth, //設置獲取到的canvas寬度
height: dom.offsetHeight, //設置獲取到的canvas高度
x: 0, //頁面在水平方向滾動的距離
y: 0, //頁面在垂直方向滾動的距離
}).then((canvas: HTMLCanvasElement) => {
let canvasWidth = canvas.width;
let canvasHeight = canvas.height;
let pageHeight = (canvasWidth / 592.28) * 841.89; // 一頁A4 pdf能顯示的canvas高度
let imgWidth = 595.28; // 設置圖片寬度和A4紙寬度相等
let imgHeight = (592.28 / canvasWidth) * canvasHeight;//等比例換算成A4紙的高度
let totalHeight = imgHeight; // 需要打印的圖片總高度,初始狀態和圖片高度相等
let pageData = canvas.toDataURL('image/png', 1.0);
let PDF = new JsPDF('p', 'pt', 'a4', true);
if (totalHeight < pageHeight) { //
PDF.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight); // 從頂部開始打印
} else {
let top = 0; // 打印初始區域
while (totalHeight > 0) {
PDF.addImage(pageData, 'JPEG', 0, top, imgWidth, imgHeight); // 從圖片頂部往下top位置開始打印
totalHeight -= pageHeight;
top -= 841.89;
if (totalHeight > 0) {
PDF.addPage();
}
}
}
PDF.save('test.pdf');
});
};
選擇分頁位置
按照上述步驟生成了一份PDF文檔,但是當PDF頁數有很多的時候,會有這樣的問題⏬
可以看到,分頁的時候從這段文字這里懶腰截斷了。這顯然不是我們想要看到的效果,如何解決這個問題呢?🤔
PDF文檔頁數較少的情況
可以在開發測試的時候預先在將要分頁的地方插入一個padding,就是提前預留分頁位置
PDF文檔頁數較多
對于這種情況,筆者嘗試遍歷要打印的dom節點的子節點,將每一頁所能打印的dom節點高度累加,若超過了頁面所能承載的最大高度,則將最后一個節點增加padding,打印完畢將樣式還原。這種方法因為要計算每個dom節點的高度,非常耗性能,也要求頁面dom元素的顆粒度較細,否則會出現一個頁面有大塊空白,完全無法模擬出word生成pdf的那種效果,所以就不展開討論了。
到此這篇關于Html5基于canvas實現電子簽名并生成PDF文檔的文章就介紹到這了,更多相關canvas電子簽名內容請搜索腳本之家以前的文章或繼續瀏覽下面的相關文章,希望大家以后多多支持腳本之家!