用js生成PDF的方案

weixin_33958585發表於2018-05-29

在java裡,我們常用Itext來生成pdf,在pdf檔案裡組合圖片,文字,畫表格,畫線等操作,還會遇到中文支援的問題。

那好,現在想直接在web前端就生成pdf怎麼辦,目前有以下幾個解決方案

1:JSPDF.js

這個庫支援不同型別的PDF檔案格式,包括:文字,數字,圖形,圖片,同時你可以自由的編輯標題或者其它型別元素。

還支援互動的內容製作,例如,你可以輸入文字或者數字,然後jsPDF幫助生成最後的PDF內容。

缺點,不支援中文,不支援svg匯入(解決方案:svg+html轉換成canvas)

http://jspdf.com/

示例程式碼如下:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"> 
<head> 
<title>Downloadify</title> 
<meta http-equiv="content-type" content="text/html;charset=utf-8"> 
<style type="text/css" media="screen"> 
body {background: #fff; width: 500px; margin: 20px auto;} 
input, textarea, p { font-family: 宋體, 黑體; font-size: 12pt;} 
input, textarea { border: solid 1px #aaa; padding: 4px; width: 98%;} 
</style> 
<script type="text/javascript" src="js/swfobject.js"></script> 
<script type="text/javascript" src="js/downloadify.js"></script> 
<script type="text/javascript" src="js/jspdf.js"></script> 
<!-- <script type="text/javascript" src="js/downloadify.min.js"></script> --> 
<script type="text/javascript"> 
window.load=function(){ 
Downloadify.create('downloadify',{ 
filename: function(){ 
return document.getElementById('filename').value; 
}, 
data: function(){ 
var doc = new jsPDF(); 
doc.text(20, 20, document.getElementById('data').value); 
doc.addPage(); 
doc.text(20, 20, document.getElementById('data').value); 
return doc.output(); 
}, 
onComplete: function(){ alert('成功儲存檔案!'); }, 
onCancel: function(){ alert('您已經取消儲存檔案'); }, 
onError: function(){ alert('出現錯誤了'); }, 
swf: 'js/downloadify.swf', 
downloadImage: 'js/download.png', 
width: 100, 
height: 30, 
transparent: true, 
append: false 
}); 

</script> 
</head> 
<body onload="load();"> 
<input type="text" name="filename" value="檔名.pdf" id="filename" /><br /> 
<textarea cols="60" rows="10" name="data" id="data">it seem do not support to Chinese</textarea> 
<p id="downloadify">You must have Flash 10 installed to download this file.</p> 
</body> 
</html>

2:pdfmake

pdfmake的基本使用方法

1.包含以下兩個檔案

1
2
<script src="build/pdfmake.min.js"></script>
 <script src="build/vfs_fonts.js"></script>

2.在JS程式碼中宣告一個Document-definition物件,這個是pdfmake自己的術語。簡單點說,就是建立一個至少包含content屬性的物件。然後就可以呼叫pdfMake的方法匯出PDF,具體見如下程式碼:

1
2
3
4
5
6
7
8
9
10
11
<script type="text/javascript">
 //建立Document-definition物件 
 var dd = {
      content: [
       'One paragraph',
       'Another paragraph, this time a little bit longer to make sure, this line will be divided into at least two lines'
       ]
    };
 //匯出PDF
 pdfMake.createPdf(dd).download();
 </script>

pdfmake支援中文

三個步驟:

1.到pdfmake專案網站,把工程都下載下來,然後進入工程目錄將字型檔案(比如微軟雅黑.ttf)放到examples/fonts目錄下,然後使用grunt dump_dir生成新的vfs_fonts.js檔案;

從上面描述可知該工程是通過grunt管理的,如果無相關知識,請上網先補習下。

grunt dump_dir命令會將fonts目錄下所有檔案都打包,因此無用檔案請別放進去。

微軟雅黑.ttf網上一搜一大把,WINDOWS電腦系統盤下存放字型的目錄也找得到。

2.回到自己的例子程式碼中,JS程式碼中修改pdfMake的fonts物件,宣告當前要用到字型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pdfMake.fonts = {
     Roboto: {
       normal: 'Roboto-Regular.ttf',
       bold: 'Roboto-Medium.ttf',
       italics: 'Roboto-Italic.ttf',
       bolditalics: 'Roboto-Italic.ttf'
     },
     微軟雅黑: {
       normal: '微軟雅黑.ttf',
       bold: '微軟雅黑.ttf',
       italics: '微軟雅黑.ttf',
       bolditalics: '微軟雅黑.ttf',
     }
   };

3.Document-definition物件中宣告預設要使用的字型,具體來說:就是宣告一個物件,除了content屬性,還要有一個defaultStyle屬性,該defaultStyle下面還有再有一個font屬性:

1
2
3
4
5
6
7
8
9
var dd = {
      content: [
       '中英文測試',
       'Another paragraph, this time a little bit longer to make sure, this line will be divided into at least two lines'
       ],
       defaultStyle: {
         font: '微軟雅黑'
       }
    };

以下為根據如上步驟做的一個完整例子原始碼:

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
  <meta charset="utf-8">
  <title>my first export PDF</title>
  <script src="build/pdfmake.min.js"></script>
  <script src="build/vfs_fonts.js"></script>
  <script>
  function down() {
    var dd = {
      content: [
       '中英文測試',
       'Another paragraph, this time a little bit longer to make sure, this line will be divided into at least two lines'
       ],
       defaultStyle: {
         font: '微軟雅黑'
       }
    };
    pdfMake.fonts = {
      Roboto: {
        normal: 'Roboto-Regular.ttf',
        bold: 'Roboto-Medium.ttf',
        italics: 'Roboto-Italic.ttf',
        bolditalics: 'Roboto-Italic.ttf'
      },
      微軟雅黑: {
        normal: '微軟雅黑.ttf',
        bold: '微軟雅黑.ttf',
        italics: '微軟雅黑.ttf',
        bolditalics: '微軟雅黑.ttf',
      }
    };
    pdfMake.createPdf(dd).download();
  }
  </script>
  </head>
  <body>
  <button onclick="down()">下載</button>
  </body>
</html>

插入圖片

在插入圖片方面,jsPDF要求先將圖片轉換成Data URL才行,而pdfmake允許直接指定路徑,看起來是很方便,但這是有條件的,必須是以node.js作為伺服器,或者將圖片放到vfs_fonts.js中,所以總的來說,用處不大,還是一樣得將圖片轉換成Data URL形式才行。

為解決此問題,我寫了一個ImageDataURL的函式物件,可同時傳入多個圖片地址。在圖片都載入完成後,ImageDataURL.oncomplete將被觸發,在回撥中通過this.imgdata取出各個圖片的Data URL,根據pdfmake的要求組織下,就可正確生成pdf了。

ImageDataURL的原理是通過H5的canvas標籤,將圖片繪製在canvas上,然後通過canvas的toDataURL得到影像的Data URL。使用時請注意瀏覽器相容性問題。

以下為將sampleImage.jpg, sampleage.jpg, sampleImage.jpg依次寫入PDF的例子,測試時sampleage.jpg不存在,PDF直接忽略。

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
  <meta charset="utf-8">
  <title>my second export PDF</title>
  <script src="build/pdfmake.min.js"></script>
  <script src="build/vfs_fonts.js"></script>
  <script>
    
  function down() {
    var x = new ImageDataURL(["sampleImage.jpg""samplage.jpg""sampleImage.jpg"]);
    x.oncomplete = function() {
      var imgs = new Array();
      console.log("complete");
      for (key in this.imgdata) {
        if (this.imgdata[key] == this.emptyobj)//不存在的圖片直接忽略
          continue;
        imgs.push({image:this.imgdata[key]});//pdfmake的圖片格式{image:image dataurl}
      }
      var dd = {
        content: [
         'Title',
         imgs,
         ],
      };
      pdfMake.createPdf(dd).download();
    }
  }
  </script>
  </head>
  <body>
  <button onclick="down()">下載</button>
  <script>
  function ImageDataURL(urls) {//urls必須是字串或字串陣列
    this.completenum = 0;
    this.totalnum = 0;
    this.imgdata = new Array();
    this.emptyobj = new Object();
    this.oncomplete = function(){};
    this.getDataURL = function(url, index) {
      var c = document.createElement("canvas");
      var cxt = c.getContext("2d");
      var img = new Image();
      var dataurl;
      var p;
      p = this;
      img.src = url;
      img.onload = function() {
        var i;
        var maxwidth = 500;
        var scale = 1.0;
        if (img.width > maxwidth) {
          scale = maxwidth / img.width;
          c.width = maxwidth;
          c.height = Math.floor(img.height * scale);
        else {
          c.width= img.width;
          c.height= img.height;
        }
        cxt.drawImage(img, 0, 0, c.width, c.height);
  
        p.imgdata[index] = c.toDataURL('image/jpeg');
        for (i = 0; i < p.totalnum; ++i) {
          if (p.imgdata[i] == null)
            break;
        }
        if (i == p.totalnum) {
          p.oncomplete();
        }
      };
      img.onerror = function() {
        p.imgdata[index] = p.emptyobj;
        for (i = 0; i < p.totalnum; ++i) {
          if (p.imgdata[i] == null)
            break;
        }
        if (i == p.totalnum) {
          p.oncomplete();
        }
      };
    }
    if (urls instanceof Array) {
      this.totalnum = urls.length; 
      this.imgdata = new Array(this.totalnum);
      for (key in urls) {
        this.getDataURL(urls[key], key);
      }
    else {
      this.imgdata = new Array(1);
      this.totalnum = 1;
      this.getDataURL(urls, 0);
    }
  }
  
  </script>
  </body>
</html>
 
 
 
 
 

引入依賴指令碼

原理上需要先把需要儲存的節點渲染到一個canvas,然後利用這個canvas製作pdf,所以先引入以下兩個依賴程式碼檔案:

<script src="https://cdn.bootcss.com/html2canvas/0.5.0-beta4/html2canvas.js"></script>
<script src="https://cdn.bootcss.com/jspdf/1.3.4/jspdf.debug.js"></script>
  • 1
  • 2

編寫所需程式碼

假設所需要列印的dom的父容器的class名是right-aside,就可以寫這樣一個函式:

function () {
  var target = document.getElementsByClassName("right-aside")[0];
  target.style.background = "#FFFFFF";

  html2canvas(target, {
    onrendered:function(canvas) {
        var contentWidth = canvas.width;
        var contentHeight = canvas.height;

        //一頁pdf顯示html頁面生成的canvas高度;
        var pageHeight = contentWidth / 592.28 * 841.89;
        //未生成pdf的html頁面高度
        var leftHeight = contentHeight;
        //頁面偏移
        var position = 0;
        //a4紙的尺寸[595.28,841.89],html頁面生成的canvas在pdf中圖片的寬高
        var imgWidth = 595.28;
        var imgHeight = 592.28/contentWidth * contentHeight;

        var pageData = canvas.toDataURL('image/jpeg', 1.0);

        var pdf = new jsPDF('', 'pt', 'a4');

        //有兩個高度需要區分,一個是html頁面的實際高度,和生成pdf的頁面高度(841.89)
        //當內容未超過pdf一頁顯示的範圍,無需分頁
        if (leftHeight < pageHeight) {
        pdf.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight );
        } else {
            while(leftHeight > 0) {
                pdf.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight)
                leftHeight -= pageHeight;
                position -= 841.89;
                //避免新增空白頁
                if(leftHeight > 0) {
                  pdf.addPage();
                }
            }
        }

        pdf.save("content.pdf");
    }
  })
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

以上程式碼將會以a4紙的尺寸來生成pdf檔案,可以分頁,程式碼來自於網路!

坑點提示

jsPDF列印成pdf檔案時,注意頁面要回到列印區域的頂部,我在該demo設定了先回到頁面頂部,再列印,這樣就不會出現黑塊了。

 

jsPDF列印成pdf檔案時,注意設定列印區域dom的背景色為白色,即#FFFFFF

相關文章