????????pdfjs是一個(gè)用JavaScript實(shí)現(xiàn)的PDF文檔查看器,它可以將PDF文檔轉(zhuǎn)換為可操作的HTML5元素翰舌,由此可以:在不需要安裝Adobe Reader的前提下在瀏覽器上預(yù)覽PDF文件恋博,解析出PDF文件中的文字淋纲、圖像等要素進(jìn)行諸如自動(dòng)審核匈挖、文檔修訂等諸多深度應(yīng)用顷级。
????pdfjs官網(wǎng)地址:https://mozilla.github.io/pdf.js/
一胸囱、初識(shí)pdfjs
1、pdfjs的優(yōu)點(diǎn):
? ? ? ?應(yīng)用靈活隔披,pdfjs可以安裝在前端枢冤,也可以安裝在后端鸠姨,這都取決于你的設(shè)計(jì)方案,當(dāng)然要特別注意的是如果是安裝在前端則getDocument(參數(shù))中的“參數(shù)”必須是URL或流數(shù)據(jù)掏导,不能為物理地址(這是由于瀏覽器的安全保護(hù)機(jī)制決定的)享怀;如果是安裝在后端,則“參數(shù)”必須是物理地址或流數(shù)據(jù)趟咆,不能為URL(否則會(huì)報(bào)xmlhttp不支持之類的錯(cuò)誤)添瓷。
? ? ? ?編碼簡(jiǎn)單,在熟悉了pdfjs應(yīng)用的情況下值纱,其代碼實(shí)現(xiàn)還是非常簡(jiǎn)單的鳞贷,而且可以很容易嵌入到各類應(yīng)用中,其代碼量也非常少虐唠。
2搀愧、pdfjs的缺點(diǎn):
? ? ? ?缺文檔,缺乏有效文檔資料疆偿,導(dǎo)致應(yīng)用規(guī)劃咱筛、編碼實(shí)現(xiàn)都比較困難,所以只能找成熟源代碼參考杆故,這也正是本文的價(jià)值所在迅箩。
? ? ? ?難下載,版本兼容性問(wèn)題導(dǎo)致下載安裝困難处铛,不同版本的pdfjs對(duì)nodejs版本以及nodejs內(nèi)各個(gè)組件的版本要求都有莫名的差異饲趋,導(dǎo)致npm install pdfjs-dist時(shí)失敗,解決辦法有兩個(gè)撤蟆,一是都用最新版本奕塑,這適用于全新搭建的開發(fā)環(huán)境;二是反復(fù)試驗(yàn)不同版本的安裝家肯,直到找到一個(gè)可以成功下載安裝的版本(筆者就是用的這個(gè)辦法龄砰,哭…)。
3息楔、應(yīng)用場(chǎng)景
? ? ? ?在線展示PDF文件寝贡,不需要安裝pdfreader的前提下就可以在網(wǎng)頁(yè)上瀏覽PDF文件扒披。這是這個(gè)組件的最主要應(yīng)用值依。
? ? ? ?自動(dòng)審核PDF文件圃泡,可以解析出PDF文件中的文字、圖片等要素信息愿险,然后通過(guò)預(yù)定義的規(guī)則進(jìn)行自動(dòng)化審核颇蜡,如商業(yè)合同的合規(guī)性審核等。這是能體現(xiàn)這個(gè)組件最大價(jià)值的應(yīng)用辆亏。
? ? ? ?其他风秤,如在線修訂、批注PDF文件……扮叨,這些應(yīng)用由于還需要其他組件的支持缤弦,本文中未做具體介紹。
二彻磁、應(yīng)用設(shè)計(jì)思路
?? pdfjs依據(jù)不同的應(yīng)用場(chǎng)景碍沐,有如下三種設(shè)計(jì)思路:
?? 1、展示類應(yīng)用衷蜓,pdfjs只能安裝在前端累提,無(wú)需pdfreader即可通過(guò)瀏覽器在網(wǎng)頁(yè)上瀏覽PDF文件。
?? 2磁浇、簡(jiǎn)單處理類應(yīng)用斋陪,主要是解析PDF中的文本、圖片等要素進(jìn)行快速加工處理置吓,pdfjs一般安裝在前端无虚,當(dāng)然也可以安裝后端,具體取決于和其他業(yè)務(wù)功能的銜接需求衍锚。
?? 3友题、復(fù)雜處理類應(yīng)用,和“簡(jiǎn)單處理類應(yīng)用”的區(qū)別就是加工處理過(guò)程比較復(fù)雜构拳、時(shí)間比較長(zhǎng)咆爽,此類應(yīng)用的pdfjs安裝在后端,可以確保具有良好的界面友好性置森。
三斗埂、pdfjs的安裝
? ? ?在項(xiàng)目的當(dāng)前目錄下執(zhí)行如下命令即可完成下載和安裝:
npm install pdfjs-dist?????????? --save-dev???
? ? ?當(dāng)然也可以下載安裝指定版本的pdfjs:
npm install pdfjs-dist@2.2.228 --save-dev??
? ? ?補(bǔ)充說(shuō)明:下載安裝命令很簡(jiǎn)單,但這個(gè)過(guò)程折騰了筆者好幾天凫海,原因是筆者是在現(xiàn)有應(yīng)用上新增安裝pdfjs呛凶,所以總是出現(xiàn)nodejs版本不匹配、webpack版本不匹配等等之類問(wèn)題導(dǎo)致安裝失敗行贪,經(jīng)過(guò)試驗(yàn)安裝不同的版本終于找到適合的版本才安裝成功(筆者的nodejs是10.2.0,成功安裝的版本是pdfjs-dist2.2.228)漾稀。
?? 以下簡(jiǎn)單列舉幾個(gè)安裝失敗的錯(cuò)誤提示:
? ■ Unsupported platform for fsevents模闲,需要升級(jí)nodejs版本。
? ■?npm ERR! errno CERT_HAS_EXPIRED崭捍,證書過(guò)期問(wèn)題尸折,需要升級(jí)npm
? ■?npm ERR! Cannot read properties of null,需要升級(jí)npm和nodejs的版本
? ■?ReferenceError:primordials is not defined殷蛇,和nodejs版本不兼容
四实夹、編碼準(zhǔn)備
?? 在編寫實(shí)現(xiàn)具體業(yè)務(wù)邏輯的代碼之前,需要做好如下三項(xiàng)準(zhǔn)備工作:
?? 1粒梦、引入組件
?? 在以上成功下載安裝的前提下亮航,通過(guò)以下命令在vue中首先需要引入pdfjs-dist組件:
import * as pdfjsLib from "pdfjs-dist";?? //引入pdfjs-dist組件
?? 2、拷貝文件
?? 為了提供渲染效率匀们,需要設(shè)置workerSrc參數(shù)缴淋,雖然如果不設(shè)置該參數(shù)應(yīng)用程序也能正常運(yùn)行(不過(guò)筆者經(jīng)過(guò)測(cè)試發(fā)現(xiàn),如果不設(shè)置workserSrc泄朴,有時(shí)候程序會(huì)給出出錯(cuò)信息)重抖,但渲染的效率會(huì)比較差。
?? 因此建議大家都需要配置workerSrc參數(shù)叼旋,為了配置該參數(shù)需要首先將一個(gè)文件從pdfjs-dist的安裝目錄(node_modules\pdfjs-dist\build\ pdf.worker.min.js)拷貝到應(yīng)用項(xiàng)目的目錄中(src\statics\pdfjs_dist\):
?? 1)仇哆、pdfjs-dist在下載安裝成功后,一般安裝在當(dāng)前項(xiàng)目的node_modules目錄下夫植,目錄名稱就是pdfjs-dist讹剔。
?? 2)、需要拷貝的文件名稱一般為pdf.worker.min.js详民,但依賴于不同的pdfjs版本延欠,也可能為類似的其他名稱,如pdf.worker.js沈跨、pdf.worker.mjs等由捎;該文件存在于node_modules\pdfjs-dist\build目錄下。
3)饿凛、文件的目錄地址可以自行指定狞玛,只要確保可以訪問(wèn)即可涧窒,例如該目錄放置在src\statics下心肪,專門建立一個(gè)名為pdfjs_dist的目錄放置拷貝過(guò)來(lái)的文件,因此拷貝完成后就得到這個(gè)文件src\statics\pdfjs_dist\pdf.worker.min.js纠吴。
?? 3硬鞍、配置路徑
?? 在文件拷貝成功后,需要通過(guò)如下命令配置workserSrc參數(shù):
pdfjsLib.GlobalWorkerOptions.workerSrc ='statics/pdfjs_dist/pdf.worker.min.js';?//加速渲染配置
?? 1)、依據(jù)實(shí)際的拷貝目標(biāo)地址配置以上參數(shù)目錄
?? 2)固该、以上配置命令一般放置vue文件的mounted()中锅减,當(dāng)然也可以放在其他地方,只需要確保在創(chuàng)建pdf文件實(shí)例前能夠執(zhí)行該命令即可伐坏。
五怔匣、分頁(yè)預(yù)覽的實(shí)現(xiàn)
? ? ? 分頁(yè)預(yù)覽pdf是pdfjs組件最基礎(chǔ)應(yīng)用功能,要實(shí)現(xiàn)這個(gè)應(yīng)用功能著淆,需要如下幾個(gè)步驟:
? ? ?1)劫狠、準(zhǔn)備畫布拴疤,在<template>區(qū)創(chuàng)建pdf展示的容器—畫布
<canvas? ref="the_canvas" style="border: 1px solid black; direction: ltr;"></canvas>
? ? ?2)永部、定義畫布
? ? ?通過(guò)以下代碼得到準(zhǔn)備的畫布:
let canvas = that.$refs.the_canvas;?
let ctx = canvas.getContext('2d');
? ? ?3)、獲取pdf中指定頁(yè)面對(duì)象
? ? ?如果當(dāng)前pdfjs部署在前端呐矾,則以下“參數(shù)”是pdf文件的url苔埋,或是pdf文件的數(shù)據(jù)流;如果部署在后端蜒犯,則“參數(shù)”是pdf文件的物理地址组橄,或是pdf文件的數(shù)據(jù)流。
let loadingPdfDocument = pdfjsLib.getDocument(參數(shù));? // 創(chuàng)建pdf文件實(shí)例
let pdfDoc = await loadingPdfDocument.promise;? ? ? // pdf文件對(duì)象
let pageCount = this.pdfDoc.numPages;? ? ? ? //pdf文件總頁(yè)數(shù)
let page = await pdfDoc.getPage(頁(yè)號(hào));? ? ? ? ? //pdf文件中指定頁(yè)的數(shù)據(jù)對(duì)象
以上“頁(yè)號(hào)”為從1到pageCount之間的整數(shù)罚随。
? ? ?4)玉工、畫布初始化
? ? ?依據(jù)當(dāng)前頁(yè)面的大小初始化畫布,其中“縮放比例”為數(shù)字淘菩,如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)遵班、頁(yè)面渲染展示
? ? ?通過(guò)以下代碼將指定頁(yè)面渲染到畫布上:
page.render(renderContext).promise.then(function() {
? ? //渲染完成后的處理
});
? ? ?渲染完成后,可以隱藏加載進(jìn)度提示條等各類后續(xù)事項(xiàng)潮改,這些都可以在以上命令按鈕的程序體中實(shí)現(xiàn)狭郑。
? ? ?6)、補(bǔ)充說(shuō)明
? ? ?以上只是實(shí)現(xiàn)業(yè)務(wù)功能的核心代碼汇在,一般工程實(shí)踐中還需要加上分頁(yè)邏輯(前翻翰萨、后翻、首頁(yè)糕殉、尾頁(yè))亩鬼、縮放邏輯、顯示渲染進(jìn)度等阿蝶,這些請(qǐng)各位自行按需擴(kuò)展雳锋。
六、文字赡磅、圖片解析的實(shí)現(xiàn)
? ? ?解析pdf文件中的文字魄缚、圖片等要素以便進(jìn)行深入處理(如自動(dòng)審核),是最能體現(xiàn)pdfjs強(qiáng)大功能的應(yīng)用。
? ? ?1)冶匹、準(zhǔn)備容器
? ? ?在<template>區(qū)創(chuàng)建一個(gè)畫布和一個(gè)div元素习劫,其中畫布隱藏,作為指定一頁(yè)pdf對(duì)象的緩存嚼隘;div元素中存放解析出來(lái)的圖片诽里;解析出來(lái)的文字本代碼中未定義容器,讀者在實(shí)際應(yīng)用自行添加即可飞蛹。
<canvas ref="the_canvas" style="display:none;"></canvas>
<div ref="imghome"></div>
? ? ?2)谤狡、定義畫布
? ? ?和分頁(yè)預(yù)覽類似,通過(guò)以下代碼得到準(zhǔn)備的畫布:
let canvas = this.$refs.the_canvas;
let ctx = canvas.getContext('2d');
? ? ?3)卧檐、獲取pdf中指定頁(yè)面對(duì)象
? ? ?如果當(dāng)前pdfjs部署在前端墓懂,則以下“參數(shù)”是pdf文件的url,或是pdf文件的數(shù)據(jù)流霉囚;如果部署在后端捕仔,則“參數(shù)”是pdf文件的物理地址,或是pdf文件的數(shù)據(jù)流盈罐。
let loadingPdfDocument = pdfjsLib.getDocument(參數(shù));? // 創(chuàng)建pdf文件實(shí)例
let pdfDoc = await loadingPdfDocument.promise;? ? ? // pdf文件對(duì)象
let pageCount = this.pdfDoc.numPages;? ? ? ? //pdf文件總頁(yè)數(shù)
let page = await pdfDoc.getPage(頁(yè)號(hào));? ? ? ? ? //pdf文件中指定頁(yè)的數(shù)據(jù)對(duì)象
以上“頁(yè)號(hào)”為從1到pageCount之間的整數(shù)榜跌。
? ? ?4)、解析文本
? ? ?以下命令就可以獲得到指定“頁(yè)號(hào)”PDF頁(yè)面上的所有文本盅粪,并放在變量pagetext中:
let textContent = await page.getTextContent();
let pagetext = textContent.items.map(item => item.str).join(' ');
? ? ?5)钓葫、解析圖片
? ? ?以下代碼可以獲取到指定“頁(yè)號(hào)”PDF頁(yè)面上的所有圖片,并著一顯示到第一步準(zhǔn)備好的DIV元素中:
? ?? 獲取本頁(yè)的所有圖片名稱清單
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) {
? ??將解析出來(lái)的圖片渲染到畫布上
? ? 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);
? ??將畫布上的圖片轉(zhuǎn)放到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)票顾、圖片解析函數(shù)
? ?圖片解析需要的一個(gè)函數(shù):
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;
? ? },
七础浮、總結(jié)
? ? Nodejs可以實(shí)現(xiàn)所有的應(yīng)用,如果有實(shí)現(xiàn)不了的應(yīng)用库物,說(shuō)明你還沒有找到對(duì)應(yīng)的組件……