H5基于Canvas實現(xiàn)電子簽名并生成PDF文檔
(給加星標,提升前端技能)
轉自:coyota666
https://juejin.cn/post/6901273585428463624
前言
電子簽名通俗來說就是通過技術手段實現(xiàn)在電子文檔上加載電子形式的簽名,其作用類似于紙質(zhì)合同上的手寫簽名或加蓋的公章。雖然電子簽名多年來合法性一直遭到質(zhì)疑,但其在企業(yè)工作流審批、請柬、單據(jù)保全等場景應用廣泛,最近的項目中就有這樣一個手寫簽名并生成PDF文件的需求。
實現(xiàn)思路
使用canvas來實現(xiàn)手寫簽名的功能,然后將canvas轉化為圖片,貼在簽名的位置; 將整個需要生成文檔的dom區(qū)域使用html2canvas插件轉成一張大圖; 使用JsPDF插件將上述圖片生成PDF文檔; 對于文件內(nèi)容較多的情況,需要合理選擇分頁位置;
生成簽名
1. 在tsx中定義canvas畫布
??height="150"?/>注意:Canvas的寬高必須要使用內(nèi)聯(lián)樣式定義,這是因為Canvas標簽有自己的默認寬高300px×150px。它內(nèi)聯(lián)樣式定義的width和height是繪畫區(qū)域(畫布)實際寬度和高度,繪制的圖形都是在這個上面。如果在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的區(qū)域渲染,因而圖片會出現(xiàn)拉伸、變形等現(xiàn)象。
2. 定義簽名函數(shù)
?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)的線條(該方法不會創(chuàng)建線條)
????ctx.closePath();??//?創(chuàng)建該條路徑
??? ctx.stroke();??//?實際地繪制出通過 moveTo()?和 lineTo()?方法定義的路徑。默認顏色是黑色。
??};3. 注冊監(jiān)聽事件
????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可視區(qū)域的x,y坐標;
pageX/pageY: 對于整個頁面來說,觸摸位置距離body左上角的x,y坐標,包括被scrollTop和scrollLeft的值;
screenX/screenY: 觸摸位置距離顯示器左邊和頂部的x,y距離。
所以,在獲取結束點坐標的時候,如果當前頁面沒有出現(xiàn)滾動條,使用clientY和pageY計算差別不大,如果頁面比較長,出現(xiàn)了滾動條,那么就必須要使用pageY來計算。clientX同理,但是移動端通常橫向滾動的場景不多,所以用clientX來計算即可。
在簽名(touchmove)這個動作過程中,我們需要不斷的更新起點位置,否則畫出來是這樣?
其實這個原理和微積分很相似,線段本質(zhì)上就是由無窮多個小線段組成,宏觀一點來看可以把線段當成一個個長度很小的小線段首尾相連構成。所以我一直覺得編程編到最后就是考驗一個人的數(shù)學能力,交并集、邏輯思維、算法等都能看到數(shù)學的身影。最后生成簽名如下:
生成PDF文檔
html2canvas是一款將HTML代碼轉換成Canvas的插件,因此需要用一個div包裹住需要打印的內(nèi)容區(qū)域,獲得這個dom節(jié)點。
html2Canvas(dom,?{
????allowTaint:?true,
????width:?dom.offsetWidth,?//設置獲取到的canvas寬度
????height:?dom.offsetHeight,?//設置獲取到的canvas高度
????x:?0,?//頁面在水平方向滾動的距離
????y:?0,?//頁面在垂直方向滾動的距離
???})注意:此處需要設置width和height及x,y,否則當頁面內(nèi)容只有一頁的時候沒有問題,但是若頁面內(nèi)容有很多頁的時候,就會出現(xiàn)生成的圖片只有一小部分有內(nèi)容的現(xiàn)象。問題就出現(xiàn)在這個配置參數(shù)上,若沒有設置寬高,則默認只取當前視口的內(nèi)容,丟棄掉其他超出當前視口的內(nèi)容。
設置打印參數(shù):
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;?//?需要打印的圖片總高度,初始狀態(tài)和圖片高度相等
??????let?pageData?=?canvas.toDataURL('image/png',?1.0);
??????let?PDF?=?new?JsPDF('p',?'pt',?'a4',?true);
??????if?(totalHeight?????????PDF.addImage(pageData,?'JPEG',?0,?0,?imgWidth,?imgHeight);?//?從頂部開始打印
??????}?else?{
????????let?top?=?0;???//?打印初始區(qū)域
????????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頁數(shù)有很多的時候,會有這樣的問題?
可以看到,分頁的時候從這段文字這里懶腰截斷了。這顯然不是我們想要看到的效果,如何解決這個問題呢??
PDF文檔頁數(shù)較少的情況
可以在開發(fā)測試的時候預先在將要分頁的地方插入一個padding,就是提前預留分頁位置
PDF文檔頁數(shù)較多
對于這種情況,筆者嘗試遍歷要打印的dom節(jié)點的子節(jié)點,將每一頁所能打印的dom節(jié)點高度累加,若超過了頁面所能承載的最大高度,則將最后一個節(jié)點增加padding,打印完畢將樣式還原。這種方法因為要計算每個dom節(jié)點的高度,非常耗性能,也要求頁面dom元素的顆粒度較細,否則會出現(xiàn)一個頁面有大塊空白,完全無法模擬出word生成pdf的那種效果,所以就不展開討論了。如若有讀者有比較好的解放方案,歡迎不吝賜教,感謝~??
關注數(shù):10億+?文章數(shù):10億+
粉絲量:10億+?點擊量:10億+
?
懸賞博主專區(qū)請掃描這里

喜愛數(shù):?1億+?發(fā)帖數(shù):?1億+
回帖數(shù):?1億+?結貼率:?99.9%
—————END—————
喜歡本文的朋友,歡迎關注公眾號?程序員哆啦A夢,收看更多精彩內(nèi)容
點個[在看],是對小達最大的支持!
如果覺得這篇文章還不錯,來個【分享、點贊、在看】三連吧,讓更多的人也看到~


