Nodejs+Vue下的pdfjs应用

2024-05-15  本文已影响0人  工程师54

        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可以实现所有的应用,如果有实现不了的应用,说明你还没有找到对应的组件……

上一篇 下一篇

猜你喜欢

热点阅读