基于 pdf.js 的 pdf 预览插件
# pdfjs-dist使用
# 一、文件预览
# 1、安装 pdfjs-dist
,此处指定版本为 2.16.105
yarn add pdfjs-dist@2.16.105
注:3.x版本部分功能的实现方法与旧版本存在差异。
# 2、html
结构内容
<template>
<div id="pdf-view">
<canvas v-for="page in state.pdfPages" :key="page" id="pdfCanvas" />
<div id="text-view"></div>
</div>
</template>
1
2
3
4
5
6
2
3
4
5
6
# 3、js
功能实现:
<script setup>
import * as pdfjsViewer from 'pdfjs-dist/web/pdf_viewer.js'
import 'pdfjs-dist/web/pdf_viewer.css'
import * as PDF from 'pdfjs-dist'
// 文件路径
import pdf from './2020试卷.pdf';
import { ref, reactive, onMounted, nextTick } from 'vue';
const state = reactive({
// 文件路径
pdfPath: pdf,
// 总页数
pdfPages: 1,
// 页面缩放
pdfScale: 2,
})
onMounted(() => {
loadFile(state.pdfPath)
});
let pdfDoc = null;
function loadFile(url) {
PDF.getDocument({
url,
cMapUrl: 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.16.105/cmaps/',
cMapPacked: true,
}).promise.then((pdf) => {
pdfDoc = pdf
// 获取pdf文件总页数
state.pdfPages = pdf.numPages
nextTick(() => {
renderPage(1) // 从第一页开始渲染
})
})
}
function renderPage(num) {
pdfDoc.getPage(num).then((page) => {
const canvas = document.getElementById('pdfCanvas')
const ctx = canvas.getContext('2d')
const viewport = page.getViewport({ scale: state.pdfScale })
canvas.width = viewport.width
canvas.height = viewport.height
const renderContext = {
canvasContext: ctx,
viewport
}
page.render(renderContext)
})
}
</script>
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
55
56
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
55
56
# 4、可能出现的问题
# (1) 部分字体出现乱码或浏览器控制台出现警告
浏览器警告:
解决方案:
在 getDocument
方法中追加 cMapUrl
和 cMapPacked
参数 (opens new window):
PDF.getDocument({
url,
cMapUrl: 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.16.105/cmaps/',
cMapPacked: true,
})
1
2
3
4
5
6
2
3
4
5
6
注:cMapUrl
参数可指定为本地文件路径,可在路径 node_modules/pdfjs-dist/cmaps
中获取。通过测试发现,该警告即便不处理依然不影响页面展示,但是在后续的文本选中功能上可能会受影响。
# 二、文本选中
# 1、功能实现
在文件预览的基础上添加以下代码:
import { TextLayerBuilder } from 'pdfjs-dist/web/pdf_viewer.js';
const pdfjsWorker = import('pdfjs-dist/build/pdf.worker.entry')
PDF.GlobalWorkerOptions.workerSrc = pdfjsWorker
const eventBus = new pdfjsViewer.EventBus();
function renderPage(num) {
pdfDoc.getPage(num).then((page) => {
...
const renderContext = {
...
}
// page.render(renderContext)
// 获取文本内容和渲染页面的 Promise
const getTextContentPromise = page.getTextContent();
const renderPagePromise = page.render(renderContext);
Promise.all([getTextContentPromise, renderPagePromise])
.then(([textContent]) => {
const textLayerDiv = document.createElement('div');
// 注意:此处不要修改该元素的class名称,该元素的样式通过外部导入,名称是固定的
textLayerDiv.setAttribute('class', 'textLayer');
// 设置容器样式
textLayerDiv.setAttribute('style', `
z-index: 1;
opacity: 1;
background-color:#fff;
transform: scale(1.1);
width: 100%,
height: 100%,
`);
// 设置容器的位置和宽高
textLayerDiv.style.left = canvas.offsetLeft + 'px';
textLayerDiv.style.top = canvas.offsetTop + 'px';
textLayerDiv.style.height = canvas.offsetHeight + 'px';
textLayerDiv.style.width = canvas.offsetWidth + 'px';
const textView = document.querySelector('#text-view');
textView.appendChild(textLayerDiv);
const textLayer = new TextLayerBuilder({
// container: ,
textLayerDiv: textLayerDiv,
pageIndex: page.pageIndex,
viewport: viewport,
eventBus,
// textDivs: []
});
textLayer.setTextContent(textContent);
textLayer.render();
})
.catch((error) => {
console.error('Error rendering page:', error);
})
})
}
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
55
56
57
58
59
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
55
56
57
58
59
# 2、可能出现的问题:
# (1) 页面文字可选中,但文本不可见
通过测试发现,将 pdfjs-dist/web/pdf_viewer.css
路径下的 color
属性注释后可显示文本。
.textLayer span,
.textLayer br {
/* color: transparent; */
position: absolute;
white-space: pre;
cursor: text;
transform-origin: 0% 0%;
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# (2) 浏览器控制台报错 Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'dispatch')
浏览器报错:
解决方案:
通过上网查找资料得知,需在 TextLayerBuilder
中追加参数 eventBus
:
const eventBus = new pdfjsViewer.EventBus();
function renderPage(num) {
pdfDoc.getPage(num).then((page) => {
...
Promise.all([getTextContentPromise, renderPagePromise])
.then(([textContent]) => {
...
const textLayer = new TextLayerBuilder({
...
eventBus,
});
...
}).catch ((error) => {...})})}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# vue使用pdf-dist实现pdf预览以及水印
# 一.使用pdf-dist插件将PDF文件转换为一张张canvas图片
npm install pdf-dist
1
# 二.页面引入插件
const pdfJS = require("pdfjs-dist");
pdfJS.GlobalWorkerOptions.workerSrc = require("pdfjs-dist/build/pdf.worker.entry");
1
2
2
# 三.渲染PDF
// 根据页码渲染相应的PDF
renderPage(num) {
this.renderingPage = true;
this.pdfData.promise.then((pdf) => {
this.pdfPageNumber = pdf.numPages;
pdf.getPage(num).then((page) => {
// 获取DOM中为预览PDF准备好的canvasDOM对象
let canvas = this.$refs.myCanvas;
let ctx = canvas.getContext("2d");
// 获取页面比率
let ratio = this._getRatio(ctx);
// 根据页面宽度和视口宽度的比率就是内容区的放大比率
let dialogWidth = this.$refs["canvasCont"].offsetWidth;
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);
let renderContext = {
canvasContext: ctx,
viewport: viewport,
};
page.render(renderContext).promise.then(() => {
this.renderingPage = false;
this.pageNo = num;
// 添加水印
this._renderWatermark();
});
});
});
},
// 计算角度
_getRatio(ctx) {
let dpr = window.devicePixelRatio || 1;
let bsr =
ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio ||
1;
return dpr / bsr;
},
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
55
56
57
58
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
55
56
57
58
# 四.添加水印
// 生成水印图片
_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 = "10px Vedana";
ctx.fillStyle = "rgba(0, 0, 0, 0.8)";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillText(this.watermark, 10, 100);
return canvas;
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
# 五.完整代码(带翻页)
<template>
<div class="main-container">
<input type="file" ref="fielinput" @change="uploadFile" />
<div ref="canvasCont" class="canvas-container">
<canvas ref="myCanvas" class="pdf-container"></canvas>
</div>
<div class="pagination-wrapper">
<button @click="clickPre">上一页</button>
<span>第{{ pageNo }} / {{ pdfPageNumber }}页</span>
<button @click="clickNext">下一页</button>
</div>
</div>
</template>
<script>
const pdfJS = require("pdfjs-dist");
pdfJS.GlobalWorkerOptions.workerSrc = require("pdfjs-dist/build/pdf.worker.entry");
export default {
props: {
watermark: {
type: String,
default: "水印文字水印文字水印文字",
},
},
mounted() {},
data() {
return {
pageNo: null,
pdfPageNumber: null,
renderingPage: false,
pdfData: null, // PDF的base64
scale: 1, // 缩放值
width: "",
height: "",
};
},
methods: {
uploadFile() {
let inputDom = this.$refs.fielinput;
let file = inputDom.files[0];
let reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
let data = atob(
reader.result.substring(reader.result.indexOf(",") + 1)
);
this.loadPdfData(data);
};
},
loadPdfData(data) {
// 引入pdf.js的字体
let CMAP_URL = "https://unpkg.com/pdfjs-dist@2.0.943/cmaps/";
//读取base64的pdf流文件
this.pdfData = pdfJS.getDocument({
data: data, // PDF base64编码
cMapUrl: CMAP_URL,
cMapPacked: true,
});
this.renderPage(1);
},
// 根据页码渲染相应的PDF
renderPage(num) {
this.renderingPage = true;
this.pdfData.promise.then((pdf) => {
this.pdfPageNumber = pdf.numPages;
pdf.getPage(num).then((page) => {
// 获取DOM中为预览PDF准备好的canvasDOM对象
let canvas = this.$refs.myCanvas;
let ctx = canvas.getContext("2d");
// 获取页面比率
let ratio = this._getRatio(ctx);
// 根据页面宽度和视口宽度的比率就是内容区的放大比率
let dialogWidth = this.$refs["canvasCont"].offsetWidth;
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);
let renderContext = {
canvasContext: ctx,
viewport: viewport,
};
page.render(renderContext).promise.then(() => {
this.renderingPage = false;
this.pageNo = num;
// 添加水印
this._renderWatermark();
});
});
});
},
// 计算角度
_getRatio(ctx) {
let dpr = window.devicePixelRatio || 1;
let bsr =
ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio ||
1;
return dpr / bsr;
},
// 在画布上渲染水印
_renderWatermark() {
let canvas = this.$refs.myCanvas;
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();
},
// 生成水印图片
_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 = "10px Vedana";
ctx.fillStyle = "rgba(0, 0, 0, 0.8)";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillText(this.watermark, 10, 100);
return canvas;
},
clickPre() {
if (!this.renderingPage && this.pageNo && this.pageNo > 1) {
this.renderPage(this.pageNo - 1);
}
},
clickNext() {
if (
!this.renderingPage &&
this.pdfPageNumber &&
this.pageNo &&
this.pageNo < this.pdfPageNumber
) {
this.renderPage(this.pageNo + 1);
}
},
},
};
</script>
<style scoped>
.main-container {
display: flex;
flex-direction: column;
align-items: center;
}
.canvas-container {
width: 100%;
height: 100%;
border: 1px dashed black;
position: relative;
display: flex;
justify-content: center;
}
.pdf-container {
width: 100%;
height: 100%;
}
.pagination-wrapper {
display: flex;
justify-content: center;
align-items: center;
}
</style>
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# 六.完整代码(滑动)
<template>
<div class="main-container">
<input type="file" ref="fielinput" @change="uploadFile" />
<div ref="canvasCont" class="canvas-container">
<canvas v-for="pageIndex in pdfPageNumber"
:ref="`myCanvas${pageIndex}`" :key="pageIndex" class="pdf-container"></canvas>
</div>
</div>
</template>
<script>
const pdfJS = require("pdfjs-dist");
pdfJS.GlobalWorkerOptions.workerSrc = require("pdfjs-dist/build/pdf.worker.entry");
export default {
props: {
watermark: {
type: String,
default: "水印文字水印文字水印文字",
},
},
mounted() {},
data() {
return {
pageNo: null,
pdfPageNumber: null,
renderingPage: false,
pdfData: null, // PDF的base64
scale: 1, // 缩放值
width: "",
height: "",
};
},
methods: {
uploadFile() {
let inputDom = this.$refs.fielinput;
let file = inputDom.files[0];
let reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
let data = atob(
reader.result.substring(reader.result.indexOf(",") + 1)
);
this.loadPdfData(data);
};
},
loadPdfData(data) {
// 引入pdf.js的字体
let CMAP_URL = "https://unpkg.com/pdfjs-dist@2.0.943/cmaps/";
//读取base64的pdf流文件
this.pdfData = pdfJS.getDocument({
data: data, // PDF base64编码
cMapUrl: CMAP_URL,
cMapPacked: true,
});
this.renderPage(1);
},
// 根据页码渲染相应的PDF
renderPage(num) {
this.renderingPage = true;
this.pdfData.promise.then((pdf) => {
this.pdfPageNumber = pdf.numPages;
pdf.getPage(num).then((page) => {
// 获取DOM中为预览PDF准备好的canvasDOM对象
let canvas = this.$refs[`myCanvas${num}`][0];
let ctx = canvas.getContext("2d");
// 获取页面比率
let ratio = this._getRatio(ctx);
// 根据页面宽度和视口宽度的比率就是内容区的放大比率
let dialogWidth = this.$refs["canvasCont"].offsetWidth;
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);
let renderContext = {
canvasContext: ctx,
viewport: viewport,
};
page.render(renderContext).promise.then(() => {
this.renderingPage = false;
this.pageNo = num;
// 添加水印
this._renderWatermark(num);
if(num < this.pdfPageNumber){
this.renderPage(num+1)
}
});
});
});
},
// 计算角度
_getRatio(ctx) {
let dpr = window.devicePixelRatio || 1;
let bsr =
ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio ||
1;
return dpr / bsr;
},
// 在画布上渲染水印
_renderWatermark(num) {
let canvas = this.$refs[`myCanvas${num}`][0];
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();
},
// 生成水印图片
_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 = "10px Vedana";
ctx.fillStyle = "rgba(0, 0, 0, 0.8)";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillText(this.watermark, 10, 100);
return canvas;
},
},
};
</script>
<style scoped>
.main-container {
display: flex;
flex-direction: column;
align-items: center;
}
.canvas-container {
width: 100%;
height: 100%;
border: 1px dashed black;
position: relative;
/* display: flex; */
/* justify-content: center; */
}
.pdf-container {
width: 100%;
height: 100%;
}
</style>
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168