服务器之家:专注于服务器技术及软件下载分享
分类导航

node.js|vue.js|jquery|angularjs|React|json|js教程|

服务器之家 - 编程语言 - JavaScript - vue.js - Vue 集成 PDF.js 实现 PDF 预览和添加水印的步骤

Vue 集成 PDF.js 实现 PDF 预览和添加水印的步骤

2022-01-06 15:12asing1elife vue.js

这篇文章主要介绍了如何在 Vue 中集成 Mozilla/PDF.js ,实现自定义的 PDF 预览器,以及给被预览的 PDF 添加水印

实现效果

Vue 集成 PDF.js 实现 PDF 预览和添加水印的步骤

可用插件介绍

Mozilla 提供了 PDF.js pdfjs-dist ,两者的区别如下:

  • PDF.js ,一个完整的 PDF 查看器,可以直接使用其提供的 viewer.html 查看 PDF 内容,包含完整样式和相关功能。优点是快速集成,不需要自己实现查看器的功能和样式。缺点是如果要自定义样式和功能,反而会很麻烦。
  • pdfjs-dist ,PDF.js 的预购建版本,只包含 PDF 内容的渲染功能,需要自己实现查看器的样式和相关功能。

Vue 官方插件库 Awesome Vue.js 推荐的vue-pdf 就是对 pdfjs-dist 进行了封装实现,一般情况下使用 vue-pdf 即可快速实现 PDF 的预览效果。

根据需求进行插件选型

我们的需求是在现有页面中实现 PDF 预览的同时,在 PDF 内容上添加水印

PDF.js 这种完整版的查看器显得过于臃肿,而 vue-pdf 虽然可以快速实现预览效果,但在添加水印时需要对显示 PDF 的 canvas 进行二次渲染,经过尝试后发现会抛出 Failed to execute 'drawImage' on 'CanvasRenderingContext2D': Overload resolution failed. 的错误。

所以最后选择直接集成 pdfjs-dist 来完成全部功能

安装和引入插件

安装

?
1
yarn add pdfjs-dist

引入

必须手动指定 workerSrc ,不然会抛出 Setting up fake worker failed 的错误。

虽然本地目录 node_modules/pdfjs-dist/build/pdf.worker.js 存在该文件,但实际引入时依旧会报错,所以只能使用 CDN 地址下的 pdf.worker.js 。可以通过传入 PDFJS.version 来提高引入的灵活性。

?
1
2
3
import * as PDFJS from 'pdfjs-dist'
 
PDFJS.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${PDFJS.version}/pdf.worker.js`

初始化插件

用于渲染内容的 canvas 节点

?
1
<canvas id="pdfCanvas"></canvas>

用于接收 PDFJS 实例的对象

?
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
props: {
 // PDF 文件的实际链接
 url: {
 type: String
 }
},
data () {
 return {
 totalPage: 1,
 // PDFJS 实例
 pdfDoc: null
 }
},
methods: {
 _initPdf () {
 PDFJS.getDocument(this.url).promise.then(pdf => {
 // 文档对象
 this.pdfDoc = pdf
 // 总页数
 this.totalPage = pdf.numPages
 // 渲染页面
 this.$nextTick(() => {
 this._renderPage()
 })
 })
 }
}

监听链接变化并初始化实例

当外部传入的 url 有效时,就可以触发 PDF 查看器的初始化函数

?
1
2
3
4
5
6
7
8
9
watch: {
 'url' (val) {
 if (!val) {
 return
 }
 
 this._initPdf()
 }
},

渲染 PDF 内容

获取当前页面比率,用于计算内容的实际宽高

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
methods: {
 _getRatio (ctx) {
 let dpr = window.devicePixelRatio || 1
 let bsr =
 ctx.webkitBackingStorePixelRatio ||
 ctx.mozBackingStorePixelRatio ||
 ctx.msBackingStorePixelRatio ||
 ctx.oBackingStorePixelRatio ||
 ctx.backingStorePixelRatio ||
 1
 
 return dpr / bsr
 }
}

渲染当前页面

page.getViewport({ scale }) 中的 scale 非常关键,直接关系到渲染出来的内容能不能撑满整个父容器,所以这里分别获取了父容器和页面本身的宽度,父容器宽度 / 页面宽度 后得出的比率就是实际页面需要放大多少的比率。

page.view 是一个数组,里面有四个值,分别是 x轴偏移量、y轴偏移量、宽度、高度。 要获取真实的宽度,还需要考虑当前页面比率,所以使用 page.view[2] * ratio 计算得出实际宽度。

?
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
40
41
data () {
 return {
 currentPage: 1,
 totalPage: 1,
 width: 0,
 height: 0,
 pdfDoc: null
 }
},
methods: {
 _renderPage () {
 this.pdfDoc.getPage(this.currentPage).then(page => {
 let canvas = document.querySelector('#pdfCanvas')
 let ctx = canvas.getContext('2d')
 // 获取页面比率
 let ratio = this._getRatio(ctx)
 
 // 根据页面宽度和视口宽度的比率就是内容区的放大比率
 let dialogWidth = this.$refs['pdfDialog'].$el.querySelector('.el-dialog').clientWidth - 40
 let pageWidth = page.view[2] * ratio
 let scale = dialogWidth / pageWidth
 
 let viewport = page.getViewport({ scale })
 
 // 记录内容区宽高,后期添加水印时需要
 this.width = viewport.width * ratio
 this.height = viewport.height * ratio
 
 canvas.width = this.width
 canvas.height = this.height
 
 // 缩放比率
 ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
 
 page.render({
 canvasContext: ctx,
 viewport
 }).promise.then(() => {})
 })
 }
}

实现页面跳转

准备渲染队列,防止渲染顺序混乱

当触发页面跳转时,会调用 _renderQueue() 函数,而不是直接调用 _renderPage() 函数,因为是否开始渲染,要取决于当前是否没有正在被渲染的页面。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
data () {
 return {
 // 是否位于队列中
 rendering: false
 }
},
methods: {
 _renderQueue () {
 if (this.rendering) {
 return
 }
 
 this._renderPage()
 }
}

在渲染页面时改变队列状态

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
methods: {
 _renderPage () {
 // 队列开始
 this.rendering = true
 
 this.pdfDoc.getPage(this.currentPage).then(page => {
 // ... 省略实现代码
 
 page.render({
 canvasContext: ctx,
 viewport
 }).promise.then(() => {
 // 队列结束
 this.rendering = false
 })
 })
 }
}

实现翻页函数

?
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
data () {
 return {
 currentPage: 1,
 totalPage: 1
 }
},
computed: {
 // 是否首页
 firstPage () {
 return this.currentPage <= 1
 },
 // 是否尾页
 lastPage () {
 return this.currentPage >= this.totalPage
 },
},
methods: {
 // 跳转到首页
 firstPageHandler () {
 if (this.firstPage) {
 return
 }
 
 this.currentPage = 1
 this._renderQueue()
 },
 // 跳转到尾页
 lastPageHandler () {
 if (this.lastPage) {
 return
 }
 
 this.currentPage = this.totalPage
 this._renderQueue()
 },
 // 上一页
 previousPage () {
 if (this.firstPage) {
 return
 }
 
 this.currentPage--
 this._renderQueue()
 },
 // 下一页
 nextPage () {
 if (this.lastPage) {
 return
 }
 
 this.currentPage++
 this._renderQueue()
 }
}

在页面内容中添加平铺的文字水印

前端添加水印的方式毋庸置疑都是使用 canvas 进行绘制。

最开始找到的方案是准备一个 div 作为透明的遮罩层挡在内容区的上层,然后将 canvas 绘制的水印使用 canvas.toDataURL('image/png') 导出成 Base64 格式,作为遮罩层的背景图片进行平铺。 虽然可以实现效果,但这种方式只要简单的打开浏览器控制台,删除这个遮罩层就可以去除水印。

之后在 Canvas 绘制另一个 Canvas 中找到 canvas 其实是可以将一个 canvas 作为图片绘制到自身上的,于是有了接下来的方案。

绘制作为水印的 canvas

因为是组件,所以水印的文字 watermark 由外部传入。

绘制水印的 canvas 不需要添加到页面中,绘制完成后直接将 DOM 元素返回即可,注意,返回的是 DOM 元素 ,而不是使用 getContext(2d) 获取的画布实例。

ctx.fillStyle 表示文字的透明度。 ctx.fillText(this.watermark, 50, 50) 表示文字在画布中的位置,第一个值是文字内容,第二个值是 x轴偏移量,第三个值是 y轴偏移量。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
props: {
 watermark: {
 type: String,
 default: 'asing1elife'
 }
},
methods: {
 _initWatermark () {
 let canvas = document.createElement('canvas');
 canvas.width = 200
 canvas.height = 200
 
 let ctx = canvas.getContext('2d')
 ctx.rotate(-18 * Math.PI / 180)
 ctx.font = '14px Vedana'
 ctx.fillStyle = 'rgba(200, 200, 200, .3)'
 ctx.textAlign = 'left'
 ctx.textBaseline = 'middle'
 ctx.fillText(this.watermark, 50, 50)
 
 return canvas
 }
}

将水印平铺到渲染内容的 canvas 中

该方法参考自 HTML5 canvas 平铺的几种方法 ,ctx.rect(0, 0, this.width, this.height) 中的 width 和 height 就是在 _renderPage() 函数中记录的页面内容区的实际宽高。只要将实际宽高传入,canvas 就会自动根据水印图片的大小和内容区的大小自动实现 x轴和 y轴的重复次数。

?
1
2
3
4
5
6
7
8
9
10
11
12
methods: {
 _renderWatermark () {
 let canvas = document.querySelector('#pdfCanvas')
 let ctx = canvas.getContext('2d')
 
 // 平铺水印
 let pattern = ctx.createPattern(this._initWatermark(), 'repeat')
 ctx.rect(0, 0, this.width, this.height)
 ctx.fillStyle = pattern
 ctx.fill()
 }
}

页面内容渲染完成后,再次触发水印渲染

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
methods: {
 // 渲染页面
 _renderPage () {
 this.pdfDoc.getPage(this.currentPage).then(page => {
 // ... 省略实现代码
 
 page.render({
 canvasContext: ctx,
 viewport
 }).promise.then(() => {
 // 渲染水印
 this._renderWatermark()
 })
 })
 }
}

以上就是Vue 集成 PDF.js 实现 PDF 预览和添加水印的的详细内容,更多关于vue 实现 PDF 预览和添加水印的资料请关注服务器之家其它相关文章!

原文链接:https://juejin.cn/post/6920148445084778510

延伸 · 阅读

精彩推荐
  • vue.jsVue多选列表组件深入详解

    Vue多选列表组件深入详解

    这篇文章主要介绍了Vue多选列表组件深入详解,这个是vue的基本组件,有需要的同学可以研究下...

    yukiwu6752022-01-25
  • vue.jsVue2.x 项目性能优化之代码优化的实现

    Vue2.x 项目性能优化之代码优化的实现

    这篇文章主要介绍了Vue2.x 项目性能优化之代码优化的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋...

    优小U9632022-02-21
  • vue.js用vite搭建vue3应用的实现方法

    用vite搭建vue3应用的实现方法

    这篇文章主要介绍了用vite搭建vue3应用的实现方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下...

    Asiter7912022-01-22
  • vue.js详解vue 表单绑定与组件

    详解vue 表单绑定与组件

    这篇文章主要介绍了vue 表单绑定与组件的相关资料,帮助大家更好的理解和学习使用vue框架,感兴趣的朋友可以了解下...

    Latteitcjz6432022-02-12
  • vue.jsVue2.x-使用防抖以及节流的示例

    Vue2.x-使用防抖以及节流的示例

    这篇文章主要介绍了Vue2.x-使用防抖以及节流的示例,帮助大家更好的理解和学习使用vue框架,感兴趣的朋友可以了解下...

    Kyara6372022-01-25
  • vue.jsVue中引入svg图标的两种方式

    Vue中引入svg图标的两种方式

    这篇文章主要给大家介绍了关于Vue中引入svg图标的两种方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的...

    十里不故梦10222021-12-31
  • vue.jsVue项目中实现带参跳转功能

    Vue项目中实现带参跳转功能

    最近做了一个手机端系统,其中遇到了父页面需要携带参数跳转至子页面的问题,现已解决,下面分享一下实现过程,感兴趣的朋友一起看看吧...

    YiluRen丶4302022-03-03
  • vue.js梳理一下vue中的生命周期

    梳理一下vue中的生命周期

    看过很多人讲vue的生命周期,但总是被绕的云里雾里,尤其是自学的同学,可能js的基础也不是太牢固,听起来更是吃力,那我就已个人之浅见,以大白话...

    CRMEB技术团队7992021-12-22