Vue專案pdf(base64)轉圖片

yeyan1996發表於2018-10-19

公司有個業務需求,要求後臺傳pdf的base64編碼給前端,前端顯示到介面上,後來在網上搜尋了很多關於base64轉pdf的文章,都寫的不是非常的詳細,在實現的過程中遇到很多坑,經過一天的研究終於實現了這個功能,分享一下我在這個功能中遇到的問題和解決方法

要註明的是這裡用到的核心外掛是pdf.js,原理是動態生成canvas標籤,然後通過pdf.js生成一個能渲染出pdf的物件,隨後渲染每個canvas,並且生成的pdf是畫面的形式,並沒有pdf之類的控制元件

引入外掛

這裡很多部落格都是使用JavaScript原生的方法引入pdf.js,例如使用script標籤引入外部的js指令碼,或者直接把pdf.js的原始碼複製到專案中,但是我嘗試這些方法的時候都不是特別好用,而且引入後導致專案體積過於龐大,

Vue專案pdf(base64)轉圖片

隨後我去github上尋找通過包管理器引入pdf.js的方法,在pdf.js的github上官方說明的是用gulp如何使用pdf.js,但是對於npm來說並沒有詳細說明,終於我在字裡行間發現了這麼一句話

To use PDF.js in a web application you can choose to use a pre-built version of the library or to build it from source. We supply pre-built versions for usage with NPM and Bower under the pdfjs-dist name. For more information and examples please refer to the wiki page on this subject.

大致的意思就是如果使用npm包管理器或者bower的話,引入的名字為pdfjs-dist,那麼我們使用npm的方法引入這個pdfjs-dist,引入的名字就隨意取名了這裡我叫PDFJS

 import PDFJS from 'pdfjs-dist'
複製程式碼

使用pdfjs-dist

這裡後臺傳給我的是一個由pdf檔名字和pdf的base64編碼組成的物件的陣列,我取名為pdfDataList

Vue專案pdf(base64)轉圖片

可以看到fileName是pdf的名字,fileVale是pdf檔案的base64編碼,thumbnail是pdf縮圖的base64編碼這裡用不到先不管,之前說到需要動態生成canvas節點(這裡不會canvas也不要緊,只需要根據程式碼一步步做就能渲染canvas)

  1. 首先我們建立一個承載所有canvas節點的父節點,取名為pdfList

    Vue專案pdf(base64)轉圖片

  2. 然後建立一個非同步函式showPdf(不懂什麼是非同步函式的可以去查一下async/await,這裡不用非同步函式也可以使用promise.then的方法,但是async/await作為非同步操作的終極方案最好還是學習一下)

async showPdf() {
       
    }
複製程式碼
  1. 使用querySelector選擇類名為pdfList的dom節點,隨後遍歷後臺傳過來的pdfDataList陣列的每一項,這裡用到一個瀏覽器自帶的atob()方法解碼base64,MDN上是這麼解釋的:

你可以使用 window.btoa() 方法來編碼一個可能在傳輸過程中出現問題的資料,並且在接受資料之後,使用 atob() 方法再將資料解碼。

語法: var decodedData = scope.atob(encodedData);

隨後呼叫pdf.js外掛的getDocument方法,getDocument是一個promise,所以使用非同步函式的話前面需要加await關鍵字(不使用非同步函式的話在方法後面加.then((pdf)=>{.......}),這個pdf物件和我這個pdf物件是同一個,同時這裡暫時也沒考慮非同步操作出錯的情況,有要求的話可以在加個catch捕獲錯誤) getDocument方法的引數是一個物件,物件鍵名為data,值為base64解碼後的值,此方法返回一個pdf物件,這個物件有幾個屬性,可以列印出來觀察一下

Vue專案pdf(base64)轉圖片

這裡我們先用到的是numPages屬性,它指的是當前pdf檔案有多少頁

async showPdf() {
        let pdfList = document.querySelector('.pdfList') //通過querySelector選擇DOM節點,使用document.getElementById()也一樣
        for(let value of this.pdfDataList){ //遍歷後臺傳過來的pdfDataList
            let base64 = value.fileValue //獲得bas464編碼
            let decodedBase64 = atob(base64) //使用瀏覽器自帶的方法解碼
            let pdf = await  PDFJS.getDocument({data: decodedBase64}) //返回一個pdf物件
            let pages = pdf.numPages //宣告一個pages變數等於當前pdf檔案的頁數
        }
    }
複製程式碼
  1. 獲取當前pdf檔案的物件和當前pdf檔案的所有頁數後,迴圈遍歷每個頁數,執行如下操作:

1)動態建立canvas節點

2)呼叫pdf物件原型上的getPage()方法和getViewport()方法,依次傳入當前迴圈的頁數和canvas的縮放大小(這裡不懂的可以直接複製黏貼)

3)渲染當前的canvas節點

4)呼叫page物件的render()方法渲染當前頁,此方法也是一個promise,需要使用await關鍵字等到狀態為resolve後再執行之後的程式碼

5)給顯示當前頁面的canvas節點一個className為canvas方便修改樣式,最後把這個canvas節點插入到pdfList節點中

async showPdf() {
        let pdfList = document.querySelector('.pdfList')  //通過querySelector選擇DOM節點,使用document.getElementById()也一樣
        for(let value of this.pdfDataList){ //遍歷後臺傳過來的pdfDataList
            let base64 = value.fileValue //獲得bas464編碼
            let decodedBase64 = atob(base64) //使用瀏覽器自帶的方法解碼
            let pdf = await  PDFJS.getDocument({data: decodedBase64}) //返回一個pdf物件
            let pages = pdf.numPages //宣告一個pages變數等於當前pdf檔案的頁數
            for (let i = 1; i <= pages; i++) { //迴圈頁數
              let canvas = document.createElement('canvas') 
              let page = await pdf.getPage(i) //呼叫getPage方法傳入當前迴圈的頁數,返回一個page物件
              let scale = 1;//縮放倍數,1表示原始大小
              let viewport = page.getViewport(scale); 
              let context = canvas.getContext('2d'); //建立繪製canvas的物件
              canvas.height = viewport.height; //定義canvas高和寬
              canvas.width = viewport.width;
              let renderContext = {
                canvasContext: context,
                viewport: viewport
              };
              await page.render(renderContext)
              canvas.className = 'canvas' //給canvas節點定義一個class名,這裡我取名為canvas
              pdfList.appendChild(canvas) //插入到pdfList節點的最後
            }
            
        }
    }
   
複製程式碼

至此頁面上就會多出一個canvas節點並且顯示當前pdf檔案的第一頁的圖片,如果當前pdf檔案有多頁就會渲染出多個canvas節點,有多個pdf檔案就會先迴圈外層,然後再迴圈內層,把每個pdf檔案的每一頁都生成一個canvas節點

Vue專案pdf(base64)轉圖片

修改樣式

渲染出頁面後還有個要注意的點,Vue框架會給每個元件的DOM節點生成一個自定義屬性,而節點動態生成的canvas節點,並沒有data-v-xxxxx這樣的自定義屬性

Vue專案pdf(base64)轉圖片
而Vue會給每個元件裡面的樣式新增這個自定義屬性,Vue框架這樣做可以防止樣式的相互汙染(也就是style旁邊的scoped屬性)
Vue專案pdf(base64)轉圖片

Vue專案pdf(base64)轉圖片

我們這裡可以在這個style下面再建立一個style寫入樣式來達到修改canvas樣式的效果,但是記得這樣做你整個專案裡面類名叫canvas的都會獲得這個樣式,需要注意

Vue專案pdf(base64)轉圖片

寫在最後

這裡使用的是動態生成canvas節點然後渲染這個節點生成的圖片,然而直接使用createElement生成一個節點並且頻繁操作DOM會對效能有一定的影響,如果有更好的方法歡迎留言交流,感謝觀看

後記

在之前的程式碼中,我們遍歷生成pdf物件的每一頁,之後動態生成canvas節點,而這樣做會讓瀏覽器反覆渲染新資訊,可以使用documentFragment來優化canvas節點的渲染

語法: let fragment = document.createDocumentFragment();

documentFragment會建立一個空的文件片段,它類似一個'倉庫',可以暫時儲存我們生成的節點,然後一次性新增到父節點中,這樣減少了渲染次數可以一定程度上提高效能,我們修改一下之前的程式碼,新增documentFragment

async showPdf() {
        let pdfList = document.querySelector('.pdfList')
        let fragment = document.createDocumentFragment() //生成一個空的documentFragment文件片段 //建立documentFragment儲存canvas節點一次性渲染//通過querySelector選擇DOM節點,使用document.getElementById()也一樣
        for(let value of this.pdfDataList){ //遍歷後臺傳過來的pdfDataList
            let base64 = value.fileValue //獲得bas464編碼
            let decodedBase64 = atob(base64) //使用瀏覽器自帶的方法解碼
            let pdf = await  PDFJS.getDocument({data: decodedBase64}) //返回一個pdf物件
            let pages = pdf.numPages //宣告一個pages變數等於當前pdf檔案的頁數
            for (let i = 1; i <= pages; i++) { //迴圈頁數
              let canvas = document.createElement('canvas') 
              let page = await pdf.getPage(i) //呼叫getPage方法傳入當前迴圈的頁數,返回一個page物件
              let scale = 1;//縮放倍數,1表示原始大小
              let viewport = page.getViewport(scale); 
              let context = canvas.getContext('2d'); //建立繪製canvas的物件
              canvas.height = viewport.height; //定義canvas高和寬
              canvas.width = viewport.width;
              let renderContext = {
                canvasContext: context,
                viewport: viewport
              };
              await page.render(renderContext)
              canvas.className = 'canvas' //給canvas節點定義一個class名,這裡我取名為canvas
              fragment.appendChild(canvas) //新增canvas節點到fragment文件片段中
            }
             pdfList.appendChild(fragment) //將fragment插入到pdfList節點的最後
        }
    }

複製程式碼

相關文章