????????pdfjs是一個用JavaScript實現的PDF文檔查看器,它可以將PDF文檔轉換為可操作的HTML5元素,由此可以:在不需要安裝Adobe Reader的前提下在瀏覽器上預覽PDF文件,解析出PDF文件中的文字、圖像等要素進行諸如自動審核、文檔修訂等諸多深度應用。
????pdfjs官網地址:https://mozilla.github.io/pdf.js/
一、初識pdfjs
1、pdfjs的優點:
? ? ? ?應用靈活,pdfjs可以安裝在前端,也可以安裝在后端,這都取決于你的設計方案,當然要特別注意的是如果是安裝在前端則getDocument(參數)中的“參數”必須是URL或流數據,不能為物理地址(這是由于瀏覽器的安全保護機制決定的);如果是安裝在后端,則“參數”必須是物理地址或流數據,不能為URL(否則會報xmlhttp不支持之類的錯誤)。
? ? ? ?編碼簡單,在熟悉了pdfjs應用的情況下,其代碼實現還是非常簡單的,而且可以很容易嵌入到各類應用中,其代碼量也非常少。
2、pdfjs的缺點:
? ? ? ?缺文檔,缺乏有效文檔資料,導致應用規劃、編碼實現都比較困難,所以只能找成熟源代碼參考,這也正是本文的價值所在。
? ? ? ?難下載,版本兼容性問題導致下載安裝困難,不同版本的pdfjs對nodejs版本以及nodejs內各個組件的版本要求都有莫名的差異,導致npm install pdfjs-dist時失敗,解決辦法有兩個,一是都用最新版本,這適用于全新搭建的開發環境;二是反復試驗不同版本的安裝,直到找到一個可以成功下載安裝的版本(筆者就是用的這個辦法,哭…)。
3、應用場景
? ? ? ?在線展示PDF文件,不需要安裝pdfreader的前提下就可以在網頁上瀏覽PDF文件。這是這個組件的最主要應用。
? ? ? ?自動審核PDF文件,可以解析出PDF文件中的文字、圖片等要素信息,然后通過預定義的規則進行自動化審核,如商業合同的合規性審核等。這是能體現這個組件最大價值的應用。
? ? ? ?其他,如在線修訂、批注PDF文件……,這些應用由于還需要其他組件的支持,本文中未做具體介紹。
二、應用設計思路
?? pdfjs依據不同的應用場景,有如下三種設計思路:
?? 1、展示類應用,pdfjs只能安裝在前端,無需pdfreader即可通過瀏覽器在網頁上瀏覽PDF文件。
?? 2、簡單處理類應用,主要是解析PDF中的文本、圖片等要素進行快速加工處理,pdfjs一般安裝在前端,當然也可以安裝后端,具體取決于和其他業務功能的銜接需求。
?? 3、復雜處理類應用,和“簡單處理類應用”的區別就是加工處理過程比較復雜、時間比較長,此類應用的pdfjs安裝在后端,可以確保具有良好的界面友好性。
三、pdfjs的安裝
? ? ?在項目的當前目錄下執行如下命令即可完成下載和安裝:
npm install pdfjs-dist?????????? --save-dev???
? ? ?當然也可以下載安裝指定版本的pdfjs:
npm install pdfjs-dist@2.2.228 --save-dev??
? ? ?補充說明:下載安裝命令很簡單,但這個過程折騰了筆者好幾天,原因是筆者是在現有應用上新增安裝pdfjs,所以總是出現nodejs版本不匹配、webpack版本不匹配等等之類問題導致安裝失敗,經過試驗安裝不同的版本終于找到適合的版本才安裝成功(筆者的nodejs是10.2.0,成功安裝的版本是pdfjs-dist2.2.228)。
?? 以下簡單列舉幾個安裝失敗的錯誤提示:
? ■ Unsupported platform for fsevents,需要升級nodejs版本。
? ■?npm ERR! errno CERT_HAS_EXPIRED,證書過期問題,需要升級npm
? ■?npm ERR! Cannot read properties of null,需要升級npm和nodejs的版本
? ■?ReferenceError:primordials is not defined,和nodejs版本不兼容
四、編碼準備
?? 在編寫實現具體業務邏輯的代碼之前,需要做好如下三項準備工作:
?? 1、引入組件
?? 在以上成功下載安裝的前提下,通過以下命令在vue中首先需要引入pdfjs-dist組件:
import * as pdfjsLib from "pdfjs-dist";?? //引入pdfjs-dist組件
?? 2、拷貝文件
?? 為了提供渲染效率,需要設置workerSrc參數,雖然如果不設置該參數應用程序也能正常運行(不過筆者經過測試發現,如果不設置workserSrc,有時候程序會給出出錯信息),但渲染的效率會比較差。
?? 因此建議大家都需要配置workerSrc參數,為了配置該參數需要首先將一個文件從pdfjs-dist的安裝目錄(node_modules\pdfjs-dist\build\ pdf.worker.min.js)拷貝到應用項目的目錄中(src\statics\pdfjs_dist\):
?? 1)、pdfjs-dist在下載安裝成功后,一般安裝在當前項目的node_modules目錄下,目錄名稱就是pdfjs-dist。
?? 2)、需要拷貝的文件名稱一般為pdf.worker.min.js,但依賴于不同的pdfjs版本,也可能為類似的其他名稱,如pdf.worker.js、pdf.worker.mjs等;該文件存在于node_modules\pdfjs-dist\build目錄下。
3)、文件的目錄地址可以自行指定,只要確保可以訪問即可,例如該目錄放置在src\statics下,專門建立一個名為pdfjs_dist的目錄放置拷貝過來的文件,因此拷貝完成后就得到這個文件src\statics\pdfjs_dist\pdf.worker.min.js。
?? 3、配置路徑
?? 在文件拷貝成功后,需要通過如下命令配置workserSrc參數:
pdfjsLib.GlobalWorkerOptions.workerSrc ='statics/pdfjs_dist/pdf.worker.min.js';?//加速渲染配置
?? 1)、依據實際的拷貝目標地址配置以上參數目錄
?? 2)、以上配置命令一般放置vue文件的mounted()中,當然也可以放在其他地方,只需要確保在創建pdf文件實例前能夠執行該命令即可。
五、分頁預覽的實現
? ? ? 分頁預覽pdf是pdfjs組件最基礎應用功能,要實現這個應用功能,需要如下幾個步驟:
? ? ?1)、準備畫布,在<template>區創建pdf展示的容器—畫布
<canvas? ref="the_canvas" style="border: 1px solid black; direction: ltr;"></canvas>
? ? ?2)、定義畫布
? ? ?通過以下代碼得到準備的畫布:
let canvas = that.$refs.the_canvas;?
let ctx = canvas.getContext('2d');
? ? ?3)、獲取pdf中指定頁面對象
? ? ?如果當前pdfjs部署在前端,則以下“參數”是pdf文件的url,或是pdf文件的數據流;如果部署在后端,則“參數”是pdf文件的物理地址,或是pdf文件的數據流。
let loadingPdfDocument = pdfjsLib.getDocument(參數);? // 創建pdf文件實例
let pdfDoc = await loadingPdfDocument.promise;? ? ? // pdf文件對象
let pageCount = this.pdfDoc.numPages;? ? ? ? //pdf文件總頁數
let page = await pdfDoc.getPage(頁號);? ? ? ? ? //pdf文件中指定頁的數據對象
以上“頁號”為從1到pageCount之間的整數。
? ? ?4)、畫布初始化
? ? ?依據當前頁面的大小初始化畫布,其中“縮放比例”為數字,如1為原始尺寸:
let viewport = page.getViewport({
? ? scale: 縮放比例,
});
let outputScale = window.devicePixelRatio || 1;
canvas.width = Math.floor(viewport.width * outputScale);
canvas.height = Math.floor(viewport.height * outputScale);
canvas.style.width = Math.floor(viewport.width) + "px";
canvas.style.height = Math.floor(viewport.height) + "px";
let transform = outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : null;
let renderContext = {
? ? ? ? canvasContext: ctx,
? ? ? ? transform: transform,
? ? ? ? viewport: viewport,
};
? ? ?5)、頁面渲染展示
? ? ?通過以下代碼將指定頁面渲染到畫布上:
page.render(renderContext).promise.then(function() {
? ? //渲染完成后的處理
});
? ? ?渲染完成后,可以隱藏加載進度提示條等各類后續事項,這些都可以在以上命令按鈕的程序體中實現。
? ? ?6)、補充說明
? ? ?以上只是實現業務功能的核心代碼,一般工程實踐中還需要加上分頁邏輯(前翻、后翻、首頁、尾頁)、縮放邏輯、顯示渲染進度等,這些請各位自行按需擴展。
六、文字、圖片解析的實現
? ? ?解析pdf文件中的文字、圖片等要素以便進行深入處理(如自動審核),是最能體現pdfjs強大功能的應用。
? ? ?1)、準備容器
? ? ?在<template>區創建一個畫布和一個div元素,其中畫布隱藏,作為指定一頁pdf對象的緩存;div元素中存放解析出來的圖片;解析出來的文字本代碼中未定義容器,讀者在實際應用自行添加即可。
<canvas ref="the_canvas" style="display:none;"></canvas>
<div ref="imghome"></div>
? ? ?2)、定義畫布
? ? ?和分頁預覽類似,通過以下代碼得到準備的畫布:
let canvas = this.$refs.the_canvas;
let ctx = canvas.getContext('2d');
? ? ?3)、獲取pdf中指定頁面對象
? ? ?如果當前pdfjs部署在前端,則以下“參數”是pdf文件的url,或是pdf文件的數據流;如果部署在后端,則“參數”是pdf文件的物理地址,或是pdf文件的數據流。
let loadingPdfDocument = pdfjsLib.getDocument(參數);? // 創建pdf文件實例
let pdfDoc = await loadingPdfDocument.promise;? ? ? // pdf文件對象
let pageCount = this.pdfDoc.numPages;? ? ? ? //pdf文件總頁數
let page = await pdfDoc.getPage(頁號);? ? ? ? ? //pdf文件中指定頁的數據對象
以上“頁號”為從1到pageCount之間的整數。
? ? ?4)、解析文本
? ? ?以下命令就可以獲得到指定“頁號”PDF頁面上的所有文本,并放在變量pagetext中:
let textContent = await page.getTextContent();
let pagetext = textContent.items.map(item => item.str).join(' ');
? ? ?5)、解析圖片
? ? ?以下代碼可以獲取到指定“頁號”PDF頁面上的所有圖片,并著一顯示到第一步準備好的DIV元素中:
? ?? 獲取本頁的所有圖片名稱清單
let opList = await page.getOperatorList();
const imageNames = opList.fnArray.reduce((acc, curr, i) => {
? ? ? ? if ([pdfjsLib.OPS.paintImageXObject, pdfjsLib.OPS.paintJpegXObject, pdfjsLib.OPS.paintImageXObjectRepeat].includes(curr)) {
? ? ? ? ? acc.push(opList.argsArray[i][0]);
? ? ? ? }
? ? ? ? return acc;
? ? ? }, []);
? ?? 遍歷該清單中的每一張圖片
for (const imageName of imageNames) {
? ??將解析出來的圖片渲染到畫布上
? ? let imageUnit8Array = image.data;
? ? let imageWidth = image.width;
? ? let imageHeight = image.height;
? ? let imageUint8ArrayWithAlphaChanel = that.addAlpha(imageUnit8Array, imageWidth, imageHeight);
? ? let imageData = new ImageData(imageUint8ArrayWithAlphaChanel, imageWidth, imageHeight);
? ? canvas.width = imageWidth;
? ? canvas.height = imageHeight;
ctx.putImageData(imageData, 0, 0);
? ??將畫布上的圖片轉放到DIV元素中
? ? let img_obj = this.$refs.imghome.appendChild(new Image());
? ? img_obj.width = imageWidth * that.scale;
? ? img_obj.height = imageHeight * that.scale;
? ? img_obj.style.border = '2px solid black';
? ? img_obj.style.marginRight = "5px";
? ? let aa = canvas.toDataURL('image/jpeg', 1.0);
? ? img_obj.src = aa;
};
6)、圖片解析函數
? ?圖片解析需要的一個函數:
addAlpha(unit8Array, imageWidth, imageHeight) {
? ? ? let newImageData = new Uint8ClampedArray(imageWidth * imageHeight * 4);
? ? ? for (let j = 0, k = 0, jj = imageWidth * imageHeight * 4; j < jj;) {
? ? ? ? newImageData[j++] = unit8Array[k++];
? ? ? ? newImageData[j++] = unit8Array[k++];
? ? ? ? newImageData[j++] = unit8Array[k++];
? ? ? ? newImageData[j++] = 255;
? ? ? };
? ? ? return newImageData;
? ? },
七、總結
? ? Nodejs可以實現所有的應用,如果有實現不了的應用,說明你還沒有找到對應的組件……