Nodejs+Vue下的pdfjs应用
pdfjs是一个用JavaScript实现的PDF文档查看器,它可以将PDF文档转换为可操作的HTML5元素,由此可以:在不需要安装Adobe Reader的前提下在浏览器上预览PDF文件,解析出PDF文件中的文字、图像等要素进行诸如自动审核、文档修订等诸多深度应用。
![](https://img.haomeiwen.com/i12875195/a0184c17a3da50dc.png)
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可以实现所有的应用,如果有实现不了的应用,说明你还没有找到对应的组件……