H5基於canvas實現電子簽名並生成PDF文檔

前言

電子簽名通俗來講就是經過技術手段實如今電子文檔上加載電子形式的簽名,其做用相似於紙質合同上的手寫簽名或加蓋的公章。雖然電子簽名多年來合法性一直遭到質疑,但其在企業工做流審批、請柬、單據保全等場景應用普遍,最近的項目中就有這樣一個手寫簽名並生成PDF文件的需求。html

實現思路
  1. 使用canvas來實現手寫簽名的功能,而後將canvas轉化爲圖片,貼在簽名的位置;
  2. 將整個須要生成文檔的dom區域使用html2canvas插件轉成一張大圖;
  3. 使用JsPDF插件將上述圖片生成PDF文檔;
  4. 對於文件內容較多的狀況,須要合理選擇分頁位置;
生成簽名
  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的區域渲染,於是圖片會出現拉伸、變形等現象。算法

  1. 定義簽名函數

 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() 方法定義的路徑。默認顏色是黑色。
  };
  1. 註冊監聽事件

 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;
    });

注意:編程

  1. 在註冊「touchstart」和「touchmove」事件時,須要阻止默認事件,不然頁面會跟着手勢上下滑動。
  2. 移動端的每一個觸摸事件對象中都包括了touches這個屬性,它用於描述位於屏幕上的全部手指的一個列表,獲取當前事件對象咱們習慣性的使用event = event.touches[0],而在PC端則不須要這麼操做。
  3. offsetLeft值跟offsetTop值跟父級元素不要緊,而是跟其上一級的定位元素(除position:static外的全部定位如fixed,relative,absolute元素)有關係。若上一級定位元素都沒有除position:staice外的定位,則這個偏移量是相對於body而言的。
  4. 須要理清移動端事件對象的幾個屬性,⏬watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

clientX/clientY: 觸摸位置距離當前body可視區域的x,y座標; pageX/pageY: 對於整個頁面來講,觸摸位置距離body左上角的x,y座標,包括被scrollTop和scrollLeft的值;screenX/screenY: 觸摸位置距離顯示器左邊和頂部的x,y距離。因此,在獲取結束點座標的時候,若是當前頁面沒有出現滾動條,使用clientY和pageY計算差異不大,若是頁面比較長,出現了滾動條,那麼就必需要使用pageY來計算。clientX同理,可是移動端一般橫向滾動的場景很少,因此用clientX來計算便可。canvas

  1. 在簽名(touchmove)這個動做過程當中,咱們須要不斷的更新起點位置,不然畫出來是這樣????

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=其實這個原理和微積分很類似,線段本質上就是由無窮多個小線段組成,宏觀一點來看能夠把線段當成一個個長度很小的小線段首尾相連構成。因此我一直以爲編程編到最後就是考驗一我的的數學能力,交併集、邏輯思惟、算法等都能看到數學的身影。最後生成簽名以下:watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=瀏覽器

生成PDF文檔

html2canvas是一款將HTML代碼轉換成Canvas的插件,所以須要用一個div包裹住須要打印的內容區域,得到這個dom節點。dom

html2Canvas(dom, {
    allowTaint: true,
    width: dom.offsetWidth, //設置獲取到的canvas寬度
    height: dom.offsetHeight, //設置獲取到的canvas高度
    x: 0, //頁面在水平方向滾動的距離
    y: 0, //頁面在垂直方向滾動的距離
   })

注意:此處須要設置width和height及x,y,不然當頁面內容只有一頁的時候沒有問題,可是若頁面內容有不少頁的時候,就會出現生成的圖片只有一小部分有內容的現象。問題就出如今這個配置參數上,若沒有設置寬高,則默認只取當前視口的內容,丟棄掉其餘超出當前視口的內容。設置打印參數:ide

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頁數有不少的時候,會有這樣的問題⏬ watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk= 能夠看到,分頁的時候從這段文字這裏懶腰截斷了。這顯然不是咱們想要看到的效果,如何解決這個問題呢?????函數

  • PDF文檔頁數較少的狀況

能夠在開發測試的時候預先在將要分頁的地方插入一個padding,就是提早預留分頁位置性能

  • PDF文檔頁數較多

對於這種狀況,筆者嘗試遍歷要打印的dom節點的子節點,將每一頁所能打印的dom節點高度累加,若超過了頁面所能承載的最大高度,則將最後一個節點增長padding,打印完畢將樣式還原。這種方法由於要計算每一個dom節點的高度,很是耗性能,也要求頁面dom元素的顆粒度較細,不然會出現一個頁面有大塊空白,徹底沒法模擬出word生成pdf的那種效果,因此就不展開討論了。如如有讀者有比較好的解放方案,歡迎不吝賜教,感謝~❤️測試

最後

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=
相關文章
相關標籤/搜索