PDF.js实现个性化PDF渲染(文本复制)

我肥来啦😁。看到Redux教程突破3w的浏览量,小窃喜,很高兴自己的文章能够帮助到大家。

这次重返,依然带给大家一个小指南,也是最近工作中遇到的一个小case。

前不久,产品经理提出要在界面上优雅地展示PDF文档,当即就有了两种实现方式:

实现方式一
使用embed标记来使用浏览器自带的pdf工具。

这种实现方式优缺点都很明显:
优点:自带“打印”,“搜索”,“翻页”等功能,强大且实现方便。
缺点:不同浏览器的pdf工具样式不一,且无法满足个性化需求,比如:禁止打印,下载等。

我们的产品经理是挑剔的😒,于是…

实现方式二
使用Mozilla的PDF.js,自定义展示PDF。

下面我们就细致讲述一下使用PDF.js过程中遇到的问题。主要包括:

  • 基础功能集成
  • 使用Text-Layers渲染

什么是PDF.JS

PDF.js是基于HTML5技术构建的,用于展示可移植文档格式的文件(PDF),它可以在现代浏览器中使用且无需安装任何第三方插件。

基础功能集成

1️⃣引用

首先,引用PDF.js就遇到了问题,官网中提到通过CDN引用或者下载源码至本地。
而我们并不想污染我们的index.html并且希望可以对每一个引用的框架有统一的版本管理。于是,我们搜寻到一个包:pdfjs-dist

通过npm install pdfjs-dist,我们引入了PDF.js。

基础功能有两个必须引用的文件:

  • pdf.js
  • pdf.worker.js

如果使用CDN的方式,直接引用如下对应文件即可:

如果使用npm的方式,则在需要使用PDF.js的文件中如下引用:

import PDFJS from ‘pdfjs-dist’;

PDFJS.GlobalWorkerOptions.workerSrc = <span class="hljs-string">'pdfjs-dist/build/pdf.worker.js';</span></span></span></span></code>
1
2
3
4
5
6
7
8
9
10
11
12

这两个文件包含了获取、解析和展示PDF文档的方法,但是解析和渲染PDF需要较长的时间,可能会阻塞其它JS代码的运行。

为解决该问题,pdf.js依赖了HTML5引入的[Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers)——通过从主线程中移除大量CPU操作(如解析和渲染)来提升性能。

PDF.js的API都会返回一个Promise,使得我们可以优雅的处理异步操作。

2️⃣**使用**

首先,我们需要在HTML中添加`<canvas>`元素以渲染PDF:

<pre class="xml hljs"><code class="html"><span class="hljs-tag"><<span class="hljs-name">canvas <span class="hljs-attr">id=<span class="hljs-string">"pdf-canvas"><span class="hljs-tag"></<span class="hljs-name">canvas></span></span></span></span></span></span></code>

然后添加渲染PDF的js代码:

<pre class="javascript hljs"><code class="javascript"><span class="hljs-keyword">var url = <span class="hljs-string">'Helloworld.pdf';

PDFJS.getDocument(url).then(<span class="hljs-function">(<span class="hljs-params">pdf) => {
    <span class="hljs-keyword">return pdf.getPage(<span class="hljs-number">1);
}).then(<span class="hljs-function">(<span class="hljs-params">page) => {
    <span class="hljs-comment">// 设置展示比例
    <span class="hljs-keyword">var scale = <span class="hljs-number">1.5;
    <span class="hljs-comment">// 获取pdf尺寸
    <span class="hljs-keyword">var viewport = page.getViewport(scale);
    <span class="hljs-comment">// 获取需要渲染的元素
    <span class="hljs-keyword">var canvas = <span class="hljs-built_in">document.getElementById(<span class="hljs-string">'pdf-canvas');
    <span class="hljs-keyword">var context = canvas.getContext(<span class="hljs-string">'2d');
    canvas.height = viewport.height;
    canvas.width = viewport.width;

    <span class="hljs-keyword">var renderContext = {
        <span class="hljs-attr">canvasContext: context,
        <span class="hljs-attr">viewport: viewport
    };

    page.render(renderContext);
});</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

现在,PDF已经成功渲染在界面上了。我们来分析一下使用到的函数:

`getDocument()`:用于异步获取PDf文档,发送多个Ajax请求以块的形式下载文档。它返回一个Promise,该Promise的成功回调传递一个对象,该对象包含PDF文档的信息,该回调中的代码将在完成PDf文档获取时执行。

`getPage()`:用于获取PDF文档中的各个页面。

`getViewport()`:针对提供的展示比例,返回PDf文档的页面尺寸。

`render()`:渲染PDF。

到这里,基本功能告一段落了。
满心欢喜准备上线的时候,产品经理提出了另一个需求:文本复制。
然鹅。。。翻了好几遍官方文档,也没有找到文本复制的方法,并且stackoverflow上有很多类似的问题。
在不断的尝试下,我们发现了`Text-Layer`。

#### 使用Text-Layers渲染

PDF.js支持在使用Canvas渲染的PDF页面上渲染文本图层。然而,这个功能需要用到额外的两个文件:`text_layer_builder.js`和`text_layer_builder.css`。我们可以在GitHub的repo中获取到。

如果是使用npm,则需要做如下引用:

<pre class="javascript hljs"><code class="javascript"><span class="hljs-keyword">import { TextLayerBuilder } <span class="hljs-keyword">from <span class="hljs-string">'pdfjs-dist/web/pdf_viewer';
<span class="hljs-keyword">import <span class="hljs-string">'pdfjs-dist/web/pdf_viewer.css';</span></span></span></span></span></code>

现在,我们开始实现文本复制功能。

首先,创建渲染需要用到DOM节点:

<pre class="xml hljs"><code class="html"><span class="hljs-tag"><<span class="hljs-name">div <span class="hljs-attr">id=<span class="hljs-string">"container"><span class="hljs-tag"></<span class="hljs-name">div></span></span></span></span></span></span></code>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

`div#container`为最外层节点,在该div中,我们会为PDF的每个页面创建自己的`div`,在每个页面的`div`中,都会有`Canvas`元素。

接着,我们修改JS代码:

<pre class="javascript hljs"><code class="javascript"><span class="hljs-keyword">var container, pageDiv;

<span class="hljs-function"><span class="hljs-keyword">function <span class="hljs-title">getPDF(<span class="hljs-params">url) {
PDFJS.getDocument(url).then(<span class="hljs-function">(<span class="hljs-params">pdf) => {
pdfDoc = pdf;
container = <span class="hljs-built_in">document.getElementById(<span class="hljs-string">'container');
<span class="hljs-keyword">for (<span class="hljs-keyword">var i = <span class="hljs-number">1; i<= pdf.numPages; i++) {
renderPDF(i);
}
})
}

<span class="hljs-function"><span class="hljs-keyword">function <span class="hljs-title">renderPDF(<span class="hljs-params">num) {
pdf.getPage(num).then(<span class="hljs-function">(<span class="hljs-params">page) => {
<span class="hljs-keyword">var scale = <span class="hljs-number">1.5;
<span class="hljs-keyword">var viewport = page.getViewport(scale);
pageDiv = <span class="hljs-built_in">document.createElement(<span class="hljs-string">'div');
pageDiv.setAttribute(<span class="hljs-string">'id', <span class="hljs-string">'page-' + (page.pageIndex + <span class="hljs-number">1));
pageDiv.setAttribute(<span class="hljs-string">'style', <span class="hljs-string">'position: relative');
container.appendChild(pageDiv);
<span class="hljs-keyword">var canvas = <span class="hljs-built_in">document.createElement(<span class="hljs-string">'canvas');
pageDiv.appendChild(canvas);
<span class="hljs-keyword">var context = canvas.getContext(<span class="hljs-string">'2d');
canvas.height = viewport.height;
canvas.width = view.width;

<span class="hljs-keyword">var renderContext = {
<span class="hljs-attr">canvasContext: context,
<span class="hljs-attr">viewport: viewport
};

page.render(renderContext);
});
}</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code>

以上代码只是实现了多页渲染,接下来,开始渲染文本图层。我们需要将page.render(renderContext)修改为以下代码:

<pre class="javascript hljs"><code class="javascript">page.render(renderContext).then(<span class="hljs-function"><span class="hljs-params">() => {
    <span class="hljs-keyword">return page.getTextContent();
}).then(<span class="hljs-function">(<span class="hljs-params">textContent) => {
    <span class="hljs-comment">// 创建文本图层div
    <span class="hljs-keyword">const textLayerDiv = <span class="hljs-built_in">document.createElement(<span class="hljs-string">'div');
    textLayerDiv.setAttribute(<span class="hljs-string">'class', <span class="hljs-string">'textLayer');
    <span class="hljs-comment">// 将文本图层div添加至每页pdf的div中
    pageDiv.appendChild(textLayerDiv);

    <span class="hljs-comment">// 创建新的TextLayerBuilder实例
    <span class="hljs-keyword">var textLayer = <span class="hljs-keyword">new TextLayerBuilder({
        <span class="hljs-attr">textLayerDiv: textLayerDiv,
        <span class="hljs-attr">pageIndex: page.pageIndex,
        <span class="hljs-attr">viewport: viewport
    });

    textLayer.setTextContent(textContent);

    textLayer.render();
});</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code>```

我们依旧来讲解以下用到的几个关键函数:

page.render():该函数返回一个当PDF页面成功渲染到界面上时解析的promise,我们可以使用成功回调来渲染文本图层。

page.getTextContent():该函数的成功回调会返回PDF页面上的文本片段。

TextLayerBuilder:该类的实例有两个重要的方法。setTextContent()用于设置page.getTextContent()函数返回的文本片段;render()用于渲染文本图层。

Bingo😎!通过以上改造,文本复制功能就实现了。官方文档上可没有这个小技巧哦。

PDF.js是一个很棒的工具,但无奈文档写的较为精简,需要开发人员不断探索PDF.js的强大功能。

如果这篇文章有帮助到您,记得点赞咯👍!

感谢支持原创技术分享