20210204 补充:
后来回显的PDF中加入了文字(pdf中的文本域),因此产生了字体问题,在原有代码的基础上增加了 cMaps 的引入;
let CMAP_URL = this.GLOBAL.dataUrl + '/staticFront/script/pdfjs-2.6.347-dist/web/cmaps/';
let loadingTask = PDFJS.getDocument({
data: url,
cMapUrl: CMAP_URL,
cMapPacked: true
});
但印章花版的问题又出现了,经过了多处的排查,问题定位在文件服务器(推测文件服务器速度不稳定,也没准网络也有关系);最后由后端提供base64 编码,然后在前端进行解析
getPdfUrl() { //获得pdf
// BASE64
let pdfFileBinary = this.convertDataURIToBinary(this.pdfSrc);
this.loadFile(pdfFileBinary);
},
//将encodeBase64解码
convertDataURIToBinary(dataURI) {
let raw = window.atob(dataURI);
let rawLength = raw.length;
//转换成pdf.js能直接解析的Uint8Array类型,见pdf.js-4068
let array = new Uint8Array(new ArrayBuffer(rawLength));
for (let i = 0; i < rawLength; i++) {
array[i] = raw.charCodeAt(i);
}
return array;
},
原 PDFJS.getDocument 中的url 改为 data模式
最近公司做了一个项目,主要功能是把印章在PDF上拖拽定位的。
这个功能主要两个点:
- 块的拖拽即时显示印章内容;
- PDF回显(因涉及多人操作,回显包括之前人所签的最终效果)。
使用的pdf.js@2.2.28 读取PDF文件,通过canvas 以图像的形式回显在页面上(因为不是单纯的显示,还有很多交互,最终选择了这个方案)
开始一切都挺顺利的,拖拽计算坐标,PDF回显封装,印章属性设置。。。
PDF回显电子签章
在开发过程中没成想PDF回显深了一个大坑,开始是已经有印章不能回显,在网上搜索要改pdf.work.js源码,搜索下面这个判断,都注掉就行了
// if (data.fieldType === 'Sig') {
// data.fieldValue = null;
// _this3.setFlags(_util.AnnotationFlag.HIDDEN);
// }
虽然能够显示印章了,但在翻页时会发生只显示一半颜色的情况,研究了好几天,更换pdf.js 版本、减少canvas 配置项都没成功。
突然在一次测试中发现,要是上来就把所有都渲染出来好像就没问题,随即用了一个比较low的方法,就是上来就把所有pdf都渲染DOM,但不显示出来(display:none),只有与当前页码相同的才显示,目前测试还没发现问题。
另,现有几个小问题:
- 布局时尽量用 padding、box-sizing: content-box;,用margin 有时取坐标数据不好控制;
- 因为找的例子是适配移动端的,里面有个参数window.devicePixelRatio,这个在mac 下通常为2,由于移动端用这个计算了宽度,在mac 下使用的时候基础坐标都*2了,PC 无脑改成1;
关于 window.devicePixelRatio 可以详细参考:
张鑫旭-设备像素比devicePixelRatio简单介绍
附上pdf 的例子
核心渲染pdf 的部分也是网上搜集的,根据业务再进行了修改;pdfjs-fix-sig 只是把电子签章那块处理了,随手发布在自己的私服上了,npm 上肯定没有,正常引到的名称是 pdfjs-dist
<template>
<div class="pdf-box" id="pdf-box">
<div class="pdf-page-btn-box">
<y-button @click="handleSub()" :disabled="disabledSub">上一页</y-button>
<div class="input-box">
<input class="input-wrap"
:title="pdfCurrentPage"
v-model="tunePage"
@blur="pageChange"
@keyup.13="pageChange"/>
</div>
<div class="num-page-box"> / {{pdf_pages}}</div>
<y-button @click="handleAdd()" :disabled="disabledAdd">下一页</y-button>
</div>
<div class="pdf-view-wrap">
<div ref="canvasBox"
class="canvas-box" id="canvas-box"
:style="{width:`${pdf_div_width}px`,margin:'0 auto'}">
<slot name="drag-collection-box"></slot>
<!-- <canvas ref="canvasPage" :id="'the-canvas'+pdfCurrentPage"-->
<!-- style="border: 1px #eeeeee solid;"></canvas>-->
<template v-for="index in pdf_pages">
<canvas ref="canvasPage" :id="'the-canvas'+index"
v-show="index === Number(pdfCurrentPage)"
style="border: 1px #eeeeee solid;"></canvas>
</template>
</div>
</div>
<ul v-if="sealPlaceShow" class="seal-place" ref="sealPlace" :style="`height: ${canvasHeight}px`">
<li v-for="index in 5"></li>
</ul>
</div>
</template>
<script>
// 准备PDF文件和签章数量、类型
import Vue from "vue";
let PDFJS = require('pdfjs-fix-sig');
PDFJS.GlobalWorkerOptions.workerSrc = Vue.prototype.GLOBAL.dataUrl + '/staticFront/script/pdfjs-fix-sig/build/pdf.worker.min.js';
export default {
name: "pdf-box",
props: {
pdfSrc: {
type: String
},
sealPlaceShow: {
type: Boolean,
default: false
}
},
data() {
return {
pdf_scale: 1,//pdf放大系数 (1.33 回显的pdf 章是100,但考虑到回显的pdf是按595*841展示【所对应应的章是75*75】),A4: 595 * 841
pdf_pages: null,
pdfDoc: null,
pdf_div_width: '595',
pdf_src: null,
pdfCurrentPage: 1,
tunePage: 1,
disabledAdd: false,
disabledSub: true, // 初始不能减页
canvasWidth: 0,
canvasHeight: 0,
sealPlaceFlag: false,
}
},
watch: {
sealPlaceShow: {
handler(val) {
this.sealPlaceFlag = val;
},
immediate: true
}
},
created() {
if (this.pdfSrc.length > 0) {
this.getPdfUrl();
} else {
this.$Message.error({content: '请使用正确的PDF文件'});
}
},
methods: {
getPdfUrl() { //获得pdf
//例子:加载pdf文件示例;
this.loadFile(this.pdfSrc);
//线上请求 文件流(2020年11月03日 08:55:02 未测试)
// this.$http.get(this.pdfSrc).then((res) => {
// this.pdf_src = res.url;
// this.loadFile(this.pdf_src)
// }, (err) => {
// console.log(err);
// });
},
//初始化pdf
loadFile(url) {
let _this = this;
let loadingTask = PDFJS.getDocument(url);
loadingTask.promise.then((pdf) => {
_this.pdfDoc = pdf;
_this.pdf_pages = _this.pdfDoc.numPages; // 总页数
_this.$nextTick(() => {
_this.setAddSubBtn();
for (let i = 1; i <= _this.pdf_pages; i++) {
_this.renderPage(i)
}
})
})
},
//渲染pdf页
renderPage(num) {
let _this = this;
this.pdfDoc.getPage(num)
.then((page) => {
let canvas = document.getElementById('the-canvas' + num);
let ctx = canvas.getContext('2d');
// let dpr = window.devicePixelRatio || 1; // mac 的 window.devicePixelRatio 为 2
let dpr = 1;
let bsr = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1;
let ratio = dpr / bsr;
let viewport = page.getViewport({scale: this.pdf_scale});
canvas.width = viewport.width * ratio;
canvas.height = viewport.height * ratio;
_this.canvasWidth = canvas.width;
_this.canvasHeight = canvas.height;
_this.$emit('setCanvasAttr', {
canvasWidth: _this.canvasWidth,
canvasHeight: _this.canvasHeight,
pdfCurrentPage: _this.pdfCurrentPage,
numPages: this.pdfDoc.numPages
});
canvas.style.width = viewport.width + 'px';
_this.pdf_div_width = viewport.width; // 定位在右边时需要canvas的宽度进行计算
canvas.style.height = viewport.height + 'px';
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
let renderContext = {
canvasContext: ctx,
viewport: viewport
};
page.render(renderContext).promise.then((res) => {
// console.log(this.getBounding('canvasPage'));
// console.log(this.getEleClass('layout-menu-left').clientWidth);
// console.log(this.getBounding('canvasPage').right - this.getEleClass('layout-menu-left').clientWidth + 'px');
});
});
},
// 减页
handleSub() {
this.pdfCurrentPage--;
this.tunePage = this.pdfCurrentPage;
this.setAddSubBtn();
},
// 加页
handleAdd(_str) {
this.pdfCurrentPage++;
this.tunePage = this.pdfCurrentPage;
this.setAddSubBtn();
},
// 判断翻页按钮是否禁用并渲染canvas
setAddSubBtn() {
this.disabledSub = this.pdfCurrentPage <= 1;
this.disabledAdd = this.pdfCurrentPage >= this.pdf_pages;
// this.renderPage(this.pdfCurrentPage);
this.$emit('setCanvasAttr', {
canvasWidth: this.canvasWidth,
canvasHeight: this.canvasHeight,
pdfCurrentPage: this.pdfCurrentPage,
numPages: this.pdfDoc.numPages
});
},
pdfChangePage() {
this.$emit('pdf-change-page', true);
},
getEleClass(_str) {
return document.getElementsByClassName(_str)[0];
},
getBounding(_str) {
return this.$refs[_str].getBoundingClientRect();
},
pageChange() {
// 输入字符容错
this.pdfCurrentPage = isNaN(Number(this.tunePage)) ? 1 : Number(this.tunePage);
// 输入大数容错
this.pdfCurrentPage = this.tunePage > this.pdf_pages ? this.pdf_pages : this.tunePage;
this.setAddSubBtn();
}
}
}
</script>
父级引用
<pdfBox ref="pdfBox"
:pdfSrc="doSignData.originFile"
:canvasHeight="canvasHeight"
:sealPlaceShow="true">
<div slot="drag-collection-box" v-if="$route.meta.viewType !== 'view'">
<!-- 这个插槽放在pdf 上显示的印章 -->
</div>
</pdfBox>
最后放上样式
@drag-block-size: 75px;
.e-sign-wrap {
.pdf-page-btn-box {
position: absolute;
text-align: right;
font-size: 12px;
right: 0;
z-index: 5;
padding: 7px;
color: #42474f;
.input-box, .num-page-box {
display: inline-block;
height: 22px;
line-height: 22px;
}
.input-box {
width: 30px;
.input-wrap {
width: 100%;
padding: 0 2px;
font-size: 12px;
border: 1px transparent solid;
border-bottom: 1px #dddee1 solid;
border-radius: 2px;
color: #495060;
text-align: right;
&:focus {
outline: none;
}
}
}
.num-page-box {
vertical-align: middle;
}
}
.pdf-box {
float: left;
font-size: 0;
position: relative;
.pdf-view-wrap {
position: relative;
display: inline-block;
.drag-box {
li {
cursor: move;
user-select: none;
position: absolute;
opacity: .7;
z-index: 5;
border: 1px dashed transparent;
.party {
color: #ffffff;
height: 24px;
line-height: 24px;
padding-left: 5px;
width: 100%;
font-size: 12px;
}
.content {
width: 98px;
height: 49px;
line-height: 18px;
padding: 5px;
font-size: 14px;
text-align: center;
display: table-cell;
vertical-align: middle;
word-break: break-all;
.sign-type {
font-size: 12px;
}
}
&.drag-block {
width: @drag-block-size;
height: @drag-block-size;
background-color: #cfddfb;
.party {
background-color: #9bbcff;
}
.content {
color: #78a5ff;
}
}
&.place-block {
width: @drag-block-size;
height: @drag-block-size;
background-color: #ffcccc;
.party {
background-color: #f09494;
}
.content {
color: #f09494;
line-height: 16px;
padding: 0;
font-size: 12px;
}
//.btn-box {
// .btn {
// width: 100%;
// }
//}
}
&.drag-block, &.place-block {
&.checked::after {
content: '';
width: 100%;
height: 100%;
border: 2px #004CC0 solid;
display: block;
position: absolute;
top: 0;
left: 0;
z-index: 0;
}
img {
-webkit-user-drag: none;
}
}
.btn-box {
cursor: pointer;
.btn {
//float: left;
width: 100%;
font-size: 12px;
text-align: center;
height: 24px;
line-height: 24px;
background-color: #004cc0;
color: #ffffff;
}
}
}
}
canvas {
width: 595px;
height: 842px;
//max-width: 595px;
//max-height: 841px;
}
}
.seal-place {
display: inline-block;
position: absolute;
right: -25px;
min-height: 841px;
li {
width: 5px;
height: 100%;
display: inline-block;
border: 1px #eee solid;
border-left: none;
}
}
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。