【實戰】通過 JS 將 HTML 匯出為 PDF 文件

破鑼發表於2018-10-29

背景介紹

某老人院資訊管理系統專案,甲方要求將財務模組的各種報表匯出為PDF文件,方便列印。

之前的解決方案,是將報表生成專門的列印 HTML 頁面,然後按 Ctrl+P 呼叫瀏覽器本身列印功能去列印。 這種方式存在的問題是不同解析度的顯示器,頁面效果不一,需要專門設定列印尺寸,使用起來不夠方便,功能上線後,一直遭到甲方吐槽...

輪子工具選擇

目標很明確明確,將 HTML 內容匯出為PDF。 時間有限,先找輪子,一通谷歌後選定了前端工具 jspdf。具體使用方式比較簡單,參考下列兩個連結:

github.com/linwalker/r…
github.com/MrRio/jsPDF

解決方案解析

先上程式碼:

html2canvas(document.body, {
  onrendered:function(canvas) {
    // 要輸出的 PDF 每頁的寬高尺寸,單位是 pt
    let pageWidth = 841.89
    let pageHeight = 592.28

    // 要列印內容,轉換成 canvas 圖片後的寬高尺寸
    let contentWidth =  canvas.width*3/4
    let contentHeight = canvas.height*3/4

    // 將要列印內容的圖片,等比例縮放至寬度等於輸出時 PDF 每頁的寬度,此時的圖片寬
    let imgWidth = pageWidth
    // 將要列印內容的圖片,等比例縮放至寬度等於輸出時 PDF 每頁的寬度,此時的圖片高
    let imgHeight = pageWidth / contentWidth * contentHeight

    // 起始內容擷取位置
    let position = 0
    // 剩餘未列印內容的高度
    let leftHeight = imgHeight

    // 獲取列印內容 canvas 圖片元素
    let pageData = canvas.toDataURL('image/jpeg', 1.0)
    
    // 初始化 pdf 容器,三個引數分別是:紙張方向(填'',則是橫向)、列印單位、紙張尺寸
    let PDF = new JsPDF('landscape', 'pt', 'a4')
    
    // 迴圈擷取列印內容並新增進容器
    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 -= pageHeight
        if (leftHeight > 0) {
          PDF.addPage()
        }
      }
    }
    
    // 將容器中的內容輸出為 PDF 文件
    pdf.save('content.pdf');
  }
})
複製程式碼

匯出 pdf 的函式我參考了這條github連結,做了部分修改。函式邏輯比較簡單,不做過多解釋。主要提兩點:

  1. 修復了一個小bug,原函式忽略了單位轉換問題(px 要轉 pt),存在匯出的 PDF 最後會有空白頁。
  2. 原函式中 leftHeight 用的是 contentHeight,也就是 canvas 圖片的未縮放換算前的高。這就導致 pageHeight 需要再換算才能得到,這增加了函式邏輯複雜度。其實 leftHeight 可以設為 imgHeight,即縮放換算後的高,而pageHeight 就設為 PDF 單頁的高,這樣程式碼邏輯更清晰。

這函式核心邏輯就三步:

  1. 獲取要列印內容區域的寬高,並等比縮放直至其寬度等於輸出 PDF 的頁面的寬度,以此獲得縮放後的列印內容圖片寬高(imgWidth, imageHeight)
  2. 按單頁 PDF 的寬高 (pageWidth, pageHeight),迴圈擷取縮放後的列印內容圖片,並將每次擷取的內容新增至 PDF 物件容器。(每擷取一次,就是一頁 pdf)
  3. 將 PDF 物件容器中的內容,輸出為 PDF 文件。

問題與補救思路

實踐中遇到的問題是豎直方向上圖片被隨機截斷,如下圖示:

【實戰】通過 JS 將 HTML 匯出為 PDF 文件

針對這個專案的業務場景,我採取的補救方案是“設定列印內容高度”。 具體思路如下:

  1. 確定輸出單頁紙張的尺寸比例
    例如: A4紙寬高比 = 841.89 / 592.28 (橫向);
  2. 保持比例不變,通過簡單換算確定單個列印的頁面寬高
    例如:頁面寬 1920px ,高 1360px;
  3. 通過 CSS,精確控制列印頁面中各元素的高度,使得超出單頁高度的內容,合理過度。
    例如:我想每頁列印表格不超過 34 行,那麼單行高度就應該設定為 1360/34 = 40px;
    第一頁由於有標題、表頭等元素,所以只打 30 行,標題和表頭合計高度為 160px (這個可根據實際需求,只需保證標題、表頭的高度都是單行高的整數倍即可)

最終成功解決豎直方向不規則截斷問題。

【實戰】通過 JS 將 HTML 匯出為 PDF 文件

總結

做的好的三個點:

  1. 快速尋找輪子,思路正確
  2. 知其所以然的態度,促使深入思考實現原理,因此才有可能優化解決方案,使得最終交付的結果更優質
  3. 實在想不到...

不足的兩點:

  1. 整個任務完成花費了一個工作日,效率太低。在專案本地部署環節浪費很多時間(主要因為專案本身技術棧選型不好,也沒有相應的部署說明文件)。另外,一開始未看懂函式就開始瞎改,浪費了不少時間。
  2. 功能沒有合理封裝,最終手動複製黏貼到所有頁面,浪費大量時間。

進一步研究和思考:

  1. 研究 jspdf 原始碼,更進一步瞭解匯出 PDF 的實現原理
  2. 研究通用列印頁面方案,看能否將匯出 PDF 文件功能封裝為 vue 元件(先針對列印內容為表格的)

相關文章