一文搞定前端html內(nèi)容轉(zhuǎn)圖片、pdf和word等文件

接上篇富文本編輯器 html 內(nèi)容轉(zhuǎn) word:html-docs-js 避坑指南,我們已經(jīng)完成了 html 內(nèi)容轉(zhuǎn) word 文檔的需求,接著咱們看下圖片和 pdf 的處理。

介紹下用到的庫

html2canvas

圖片和 pdf 的轉(zhuǎn)換都會(huì)用到html2canvas來完成,通過官網(wǎng)上的介紹,我們可以總結(jié)一下它的特點(diǎn):

  1. 不需要后臺(tái)支持,通過純?yōu)g覽器端”截圖“;
  2. 可對(duì)部分或整個(gè)網(wǎng)頁進(jìn)行“截圖”;
  3. 基于 DOM(遍歷頁面的 DOM),利用可用的信息構(gòu)建屏幕截圖;
  4. 有些 css 屬性未被支持,可查看支持的 css 屬性列表
  5. 受同源策略影響;
  6. 無法渲染 iframe,flash 等內(nèi)容。

jsPDF

pdf 的轉(zhuǎn)換用到jsPDF,可以看看這個(gè)demo,對(duì) jsPDF 的介紹比較詳細(xì)。

圖片的轉(zhuǎn)換

相比于 html 轉(zhuǎn) word 來說,圖片和 pdf 的轉(zhuǎn)換相對(duì)來說簡單了許多,咱們來看下圖片的轉(zhuǎn)換過程,主要有以下幾個(gè)步驟:

  1. 克隆需要截圖的 DOM 元素

    通過cloneNode將需要克隆的節(jié)點(diǎn)生成一份副本,這一步的目的是:我們不能直接對(duì)原始 DOM 進(jìn)行操作,因?yàn)闀?huì)影響頁面布局。所以可以修改克隆后的 DOM 節(jié)點(diǎn),通過修改節(jié)點(diǎn)的樣式(border、box-shadow 等)或修改節(jié)點(diǎn)寬高,達(dá)到我們想要的截圖效果。

    const cloneEle = ele.cloneNode(true)
    // 對(duì)克隆的節(jié)點(diǎn)進(jìn)行操作
    cloneEle.style.xxx = ''
    
  2. 通過 html2canvas 截圖

    我們將第一步克隆到的 DOM 進(jìn)行一個(gè)清理的動(dòng)作,清理的作用是:移除不需要截圖的 DOM 節(jié)點(diǎn);將克隆的節(jié)點(diǎn)添加到 DOM 上,并返回新節(jié)點(diǎn)和刪除節(jié)點(diǎn)的方法。刪除節(jié)點(diǎn)cleanHtmlRecover方法用于在截圖完成后移除 DOM 元素。

    接著使用 html2canvas 方法將 DOM 繪制為 canvas,通過調(diào)用 canvas 對(duì)象的 toDataURL 方法將 canvas 轉(zhuǎn)換成圖片。

    這里需要為 html2canvas 提供第二個(gè)參數(shù)useCORS: true,開啟使用 CORS 從服務(wù)器加載圖像,不然如果圖片不同源時(shí)就會(huì)導(dǎo)致一片白。更多參數(shù)配置請(qǐng)參考configuration。

    const cleanHtml = (ele: HTMLElement) => {
     // 移除不需要截圖的DOM節(jié)點(diǎn)
     const selectElements = ele.querySelectorAll('select')
     selectElements.forEach((sel) => (sel.style.display = 'none'))
     const title = document.createElement('div')
    
     const warp = document.createElement('div')
     // 圖片、pdf導(dǎo)出背景色不是白色
     warp.style.position = 'absolute'
     warp.style.zIndex = '-1'
     warp.append(ele)
     document.body.append(warp)
     return {
       warp,
       cleanHtmlRecover: () => {
         warp.remove()
       }
     }
    }
    const { warp, cleanHtmlRecover } = cleanHtml(cloneEle)
    return new Promise<void>((resolve) => {
     Html2canvas(warp, { useCORS: true })
       .then((canvas) => {
         // 生成截圖
         const image = canvas.toDataURL('image/jpg')
         // 下載圖片
       })
       .finally(() => {
         cleanHtmlRecover()
         resolve()
       })
    })
    
  3. 下載圖片

    上一步獲取到轉(zhuǎn)換后的圖片后,就可以通過a標(biāo)簽的方式來下載圖片,我們可以通過 dispatchEvent 來模擬點(diǎn)擊事件完成下載。對(duì) dispatchEvent 的其他使用可以看這篇文章。

    // 下載圖片
    const a = document.createElement('a')
    a.download = filename
    a.href = canvas.toDataURL('image/jpg')
    const event = new MouseEvent('click')
    a.dispatchEvent(event)
    

pdf 的轉(zhuǎn)換

圖片的導(dǎo)出已經(jīng)完成,那么 pdf 的導(dǎo)出應(yīng)該如何做呢?

一開始我們是用的html2pdf-jspdf2,它就是使用 html2canvas 和 jsPDF 結(jié)合在一起,通過和 html2canvas 將 html 內(nèi)容轉(zhuǎn)為 canvas,再通過 jsPDF 將 canvas 轉(zhuǎn)為 pdf。說幾個(gè)我遇到的問題(可能是我用的不對(duì)):

  1. 在 JSPDF 中我設(shè)置了format: 'a4',意思是使用 A4 紙的大小來導(dǎo)出,頁面同樣設(shè)置為 A4,但導(dǎo)出的 pdf 文件寬度顯示不全;
  2. 我們頁面可以設(shè)置成 A3、A4、A5 幾種特定紙張,并且支持設(shè)置寬高自定義紙張,但當(dāng)我傳入寬高后,發(fā)現(xiàn)得到的 pdf 文件不是我設(shè)置好的寬高;
  3. 沒有了,直接換庫跑路 ??

遇到問題解決不了怎么辦?找 leader,找 leader,還是找 leader

通過我們一陣商量,最終確定了一個(gè)方案:先用 html2canvas 將 html 轉(zhuǎn)換為圖片,再利用 jsPDF 提供的addImage方法將圖片貼到 pdf 中,因?yàn)閳D片導(dǎo)出目前是沒有什么問題,而且展示效果也挺好,所以導(dǎo)出的 pdf 應(yīng)該也不會(huì)有什么問題。

接下來就是和產(chǎn)品掰頭環(huán)節(jié),巴拉巴拉的...,成功讓他們改了需求。

最后看下實(shí)現(xiàn)過程:

html2canvas 的使用與前面生成圖片一樣,接著通過generatePDF生成 pdf。

...
Html2canvas(warp, { useCORS: true })
  .then((canvas) => generatePDF(canvas, filename))
...

我們看下generatePDF的實(shí)現(xiàn)步驟:

  1. 計(jì)算一頁 A4 紙能顯示當(dāng)前 html 生成的 canvas 高度;
  2. 如果 canvas 的高度未超過一頁 A4 紙的顯示高度,無需分頁,直接貼圖導(dǎo)出;
  3. 否則需要分頁打印,分頁打印思路如下:
    1. 設(shè)置變量leftHeight記錄剩余高度,打印完一頁后 leftHeight 減去已經(jīng)打印的 canvas 的高度 pageHeight,如果剩余高度大于 0,說明沒打印完,通過addPage()增加分頁繼續(xù)打?。?/li>
    2. 設(shè)置變量position記錄打印開始的距離頭部的位置,打印完一頁后 position 增加一頁 A4 紙的高度繼續(xù)打印。

最后貼上完整代碼:

/** a4紙的尺寸 */
enum A4_PAPER_SIZE_ENUM {
  'width' = 595.28,
  'height' = 841.89,
}
const generatePDF = (canvas: HTMLCanvasElement, filename: string) => {
  const contentWidth = canvas.width
  const contentHeight = canvas.height
  // 一頁pdf顯示html頁面生成的canvas高度
  const pageHeight =
    (contentWidth / A4_PAPER_SIZE_ENUM.width) * A4_PAPER_SIZE_ENUM.height
  // 未生成pdf的html頁面高度
  let leftHeight = contentHeight
  // 頁面偏移
  let position = 0
  const imgWidth = A4_PAPER_SIZE_ENUM.width
  const imgHeight = (A4_PAPER_SIZE_ENUM.width / contentWidth) * contentHeight
  const pageData = canvas.toDataURL('image/jpeg', 1.0)
  const PDF = new JsPDF('p', 'pt', 'a4')

  // 當(dāng)內(nèi)容未超過pdf一頁顯示的范圍,無需分頁
  if (leftHeight < pageHeight) {
    // addImage(pageData, 'JPEG', 左,上,寬度,高度)設(shè)置
    PDF.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight)
  } else {
    // 超過一頁時(shí),分頁打印(每頁高度841.89)
    while (leftHeight > 0) {
      PDF.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight)
      leftHeight -= pageHeight
      position -= A4_PAPER_SIZE_ENUM.height
      if (leftHeight > 0) {
        PDF.addPage()
      }
    }
  }
  PDF.save(filename + '.pdf')
}

小瑕疵

這種方法有一點(diǎn)點(diǎn)小問題:分頁的地方處理不太好,不會(huì)自動(dòng)識(shí)別隔頁處理,而只是比較粗暴的從中間被拆分,類似下面這張圖。

jsPdf-bug.png

總結(jié)

不管是工作還是學(xué)習(xí)中,都要有良好的“小記”習(xí)慣,將遇到的問題、解決的過程記錄下來,最后整理成文,積累沉淀,不僅鍛煉自己的文筆,同時(shí)拓寬知識(shí)面、幫助他人,在以后工作中遇到時(shí)也能更快的解決問題,實(shí)現(xiàn)業(yè)務(wù)需求;而不是做完就停滯了,下次遇到同樣的問題還是處理不了。

以上就是本文的全部內(nèi)容,希望這篇文章對(duì)你有所幫助,歡迎點(diǎn)贊和收藏??,如果發(fā)現(xiàn)有什么錯(cuò)誤或者更好的解決方案及建議,歡迎隨時(shí)聯(lián)系。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容