【背景】
昨天財務的同學過來核對新的退款流程,順便提了下之前開發的電子發票的項目,說上傳電子發票PDF文件后,打開無法預覽。起初以為是網絡的原因,因為項目在當時開發完測試的時候是出現過這種問題的,由于這個問題是偶發性的,當時公司的網絡經常出現問題,因此測試同學也就當成網絡問題處理了。現在跟財務同學溝通以后,才知道原來已經不能用好幾個月了。說只是公司內部的系統不能查看,在用戶那邊是正常的,所以就沒有通知我們修復了。那怎么行,程序員的眼里怎么能容下bug。其實當時這塊由于時間關系當時開發的并不理想,后面也一直沒有抽出時間來改。
【需求】
先大致講一下當時的需求吧。原需求是,財務人員導入電子發票后,可通過點擊已經導入的電子發票,像圖片一樣展示 pdf 文件。而用戶在B端查看電子發票時是會下載那個PDF文件的,所以也是財務同學說的公司內部系統無法查看,用戶那邊不影響的結果。
【解決方案】
后臺去騰訊云拿到 pdf 文件然后經過 base64 處理后返回給前端,前端通過使用 pdfjs 將 pdf 文件顯示出來。這個方案當時在做這個項目的時候就在網上找到了,后面因為 pdfjs 這個插件用起來有點麻煩,當時找到一個 vue-show-pdf 的插件可以直接用,但其實效果不怎么好,只是時間趕,而且是公司內部系統,只有財務人員能夠使用,所以就粗糙的上了。
說了一大堆廢話,下面說下具體實現的過程以及過程中遇到的一些問題。
首先,pdfjs 在網上找到的其實大多數都是使用 url 去顯示的。通過 url 顯示 pdf 的話這個比較簡單,網上也很多。但是因為我們文件資源是存放在騰訊云的,涉及到前端跨域的問題。因此是由后端直接去騰訊云拿到 pdf 文件通過 base64 處理后返回給前端。前端拿到 base64 字符串后。是不能直接放到 pdfjs 中使用的,但是在pdfjs 的官方文檔中,有提到使用 base64 的方法。
因此,pdfjs,實際上是支持傳入 經過 base64 處理的字符串,重點就是這個 as an array。
/**
* 函數名:getUint8Array
* 簡介:將base64 格式的字符串轉換成 uint8Array (pdf.js 無法直接接受base64 格式的參數)
* 參數:base64_string(pdf格式的電子發票經過base64處理的字符串)
* return:Array
*/
getUint8Array(base64Str){
let data = base64Str.replace(/[\n\r]/g, ''); // 替換多余的空格和換行
var raw = window.atob(data);
var rawLength = raw.length;
var array = new Uint8Array(new ArrayBuffer(rawLength));
for (var i = 0; i < rawLength; i++) {
array[i] = raw.charCodeAt(i)
}
return array
},
這里涉及到使用 Uint8Array 和 ArrayBuffer,Uint8Array 類型數組表示的8位無符號整數數組。內容初始化為0。一旦建立,您可以使用對象的方法或使用標準數組索引語法(即使用括號表示法)引用數組中的元素。詳細的可以參考 官方文檔
ArrayBuffer 類型化數組,類型化數組是JavaScript操作二進制數據的一個接口。最初為了滿足JavaScript與顯卡之間大量的、實時的數據交換,它們之間的數據通信必須是二進制的,而不能是傳統的文本格式的背景下誕生的。
charCodeAt() 方法可返回指定位置的字符的 Unicode 編碼。這個返回值是 0 - 65535 之間的整數。
將返回的base64 字符串,傳入getUint8Array 方法中,返回一個 array ,將這個 array 交由 pdfjs 調用。
/*將解碼后的值傳給PDFJS.getDocument(),交給pdf.js處理*/
showPdfFile(data) {
let pdfView = this.$refs.pdf;
pdfView.innerHTML = "";
const CMAP_URL = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.0.943/cmaps/';
PDFJS.getDocument({data: data ,cMapUrl: CMAP_URL,cMapPacked: true}).then(pdf => {
pdf.getPage(1).then(page => {
let scale = 1.5; // 默認1.5倍縮放
let viewport = page.getViewport(scale);
if(viewport.height > window.screen.height){
scale = (window.screen.height / viewport.height).toFixed(1);
viewport = page.getViewport(scale);
}
let canvas = document.createElement('canvas');
let canvasContext = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.height;
pdfView.appendChild(canvas);
// 將頁面呈現到畫布上
let renderContext = {
canvasContext: canvasContext,
viewport: viewport
}
page.render(renderContext);
this.isShowPDF = true;
},err => {
// PDF loading error
console.error(err);
});
});
},
這里有幾個地方需要注意一下:
- 電子發票上傳后,點擊預覽發現 pdf 中的中文字符不現實了。網上查詢了一下,是因為中文的編碼問題,引入pdfjs 的編碼文件。在使用的時候傳入即可
const CMAP_URL = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.0.943/cmaps/';
PDFJS.getDocument({data: data ,cMapUrl: CMAP_URL,cMapPacked: true}).then()
- 電子發票字體有些模糊不清。嘗試調整了縮放比例,可以滿足需求。網上有些提到使用了canvas 導致的字體很不清晰的問題,在電子發票上能夠清晰顯示,之前在做電子合同項目的時候,嘗試了相同的方法,文字比較多還涉及了表格,不過效果還行。
let scale = 1.5; // 默認1.5倍縮放
let viewport = page.getViewport(scale);
if(viewport.height > window.screen.height) {
scale = (window.screen.height / viewport.height).toFixed(1);
viewport = page.getViewport(scale);
}
在這里做了一些兼容處理,因為之前的測試數據上傳的是一個隨便找的 pdf 文件,是高大于寬的,在1.5倍縮放的情況下,超出了屏幕的顯示范圍,且無法滾動的情況,所以在這稍微做了下簡單的處理,改變了一下縮放的比例,保證不會超出屏幕的顯示,因為這里的功能主要是預覽導入的電子發票,尺寸一般都是差不多的,所以沒有做比較復雜的處理了。
另外,因為跟財務同學確認過,發票都是單張的,所以沒有做分頁的處理了。之前在做電子合同的項目的時候,做過分頁的處理,直接貼個代碼參考下吧
showPdf(){
let pdfView = document.getElementById('pdf-view');
const CMAP_URL = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.0.943/cmaps/';
pdfjsLib.getDocument({data: this.getUint8Array,cMapUrl: CMAP_URL,cMapPacked: true,})
.then(pdf => {
that.pageCount = pdf.numPages;
for(var i=1;i<pdf.numPages;i++){
pdf.getPage(i).then(page => {
let scale = 1.0;
let viewport = page.getViewport(scale);
let canvas = document.createElement('canvas');
let canvasContext = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.width * 841.229/ 592.28;
pdfView.appendChild(canvas);
// 將頁面呈現到畫布上
let renderContext = {
canvasContext: canvasContext,
viewport: viewport
}
page.render(renderContext);
},err => {
// PDF loading error
console.error(err);
});
}
});
},
合同這里因為是按A4紙規格設計的,所以做了尺寸的處理。
canvas.width = viewport.width;
canvas.height = viewport.width * 841.229 / 592.28;
最后因為各種原因,合同這塊涉及打印的一些問題,并沒有采用 pdfjs 的方案。說實話電子合同這塊的坑蠻多的!后面會抽空把合同這塊踩過的一些坑寫一下。