一、前情提要
1. 需求
仿照e签宝,实现pdf电子签章 => 拿到pdf链接,移动章的位置,获取章的坐标
技术 : 使用fabric + pdfjs-dist + vuedraggable
2. 借鉴
一位大佬的代码仓亏 : 地址
一位大佬写的文章 :地址
3. 优化
在大佬的代码基础上,进行了些许优化,变的更像e签宝
二、下载
ps : 怕版本不同,导致无法运行,请下载指定版本
1.fabric
fabric :是一个功能强大且操作简单的 Javascript HTML5 canvas 工具库
npm install fabric@5.3.0
2.pdfjs-dist
npm install pdfjs-dist@2.5.207
问题一
注意 : 最好配置一下babel,因为打包的时候可能会报错
因为babel默认不会转化node_modules中的包,但是pdfjs-dist用了es6的东东
// 安装包npm install babel-loader @babel/core @babel/preset-env -D
在webpack.config.js中配置
{test: /\.js$/,loader: 'babel-loader',include: [resolve('src'),// 转化pdfjs-dist,之所以分开写,是因为pdfjs-dist里面有很多es6的语法,但是我们只需要转化pdfjs-dist里面的web文件夹下的js文件resolve('node_modules/pdfjs-dist/web/pdf_viewer.js'),resolve('node_modules/pdfjs-dist/build/pdf.js'),resolve('node_modules/pdfjs-dist/build/pdf.worker.js'),resolve('node_modules/pdfjs-dist/build/pdf.worker.entry.js')]},
问题二
pdf.js文件过大,可以给 .babelrc 加上属性,“compact”: false
3.vuedraggable
npm install vuedraggable@2.24.3
三、代码
1. 准备pdf文件
text.pdf 可放置在 src/static 文件夹中
ps : 线上最好让后端返回pdf链接,因为存在pdf跨域问题
2. 大佬的代码
{ (percentage * 100).toFixed(0) + '%' }}+ -->上一页下一页{{ pageNum }}/{{ numPages }}页跳转删除签章清除所有签章提交所有签章信息任务信息文件主题{{ taskInfo.title }}发起方{{ taskInfo.uname }}截止时间{{ taskInfo.endtime }}import draggable from 'vuedraggable';import { fabric } from 'fabric';import workerSrc from 'pdfjs-dist/es5/build/pdf.worker.entry';import * as pdfjsViewer from 'pdfjs-dist/web/pdf_viewer';const pdfjsLib = require('pdfjs-dist/es5/build/pdf.js');pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc;export default {components: { draggable },data() {return {// pdf预览pdfUrl: '',pdfDoc: null,numPages: 1,pageNum: 1,scale: 2.2,pageRendering: false,pageNumPending: null,sealUrl: '',signUrl: '',canvas: null,ctx: null,canvasEle: null,whDatas: null,mainImagelist: [],taskInfo: {}// percentage: 1};},computed: {hasSigna() {if (this.canvasEle && this.canvasEle.getObjects()[0]) {return true;} else {return false;}}},created() {var that = this;that.mainImagelist = [require('@/assets/img/projectCenter/sign.png'), require('@/assets/img/projectCenter/seal.png')];that.taskInfo = { title: '测试盖章', uname: '张三', endtime: '2021-09-01 17:59:59' };this.setPdfArea();},mounted() {// this.showpdf(this.pdfUrl);if (!pdfjsLib.getDocument || !pdfjsViewer.PDFViewer) {// eslint-disable-next-line no-alertalert('Please build the pdfjs-dist library using\n`gulp dist-install`');}},methods: {// pdf预览// zoomIn() {// console.log('缩小');// if (this.scale = 2.2) {// this.$message.error('已经显示最大比例');// } else {// this.scale += 0.1;// this.percentage += 0.1;// this.renderPage(this.pageNum);// this.renderFabric();// }// },renderPage(num) {let _this = this;this.pageRendering = true;return this.pdfDoc.getPage(num).then((page) => {let viewport = page.getViewport({ scale: _this.scale }); // 设置视口大小_this.canvas.height = viewport.height;_this.canvas.width = viewport.width;// Render PDF page into canvas contextlet renderContext = {canvasContext: _this.ctx,viewport: viewport};let renderTask = page.render(renderContext);// Wait for rendering to finishrenderTask.promise.then(() => {_this.pageRendering = false;if (_this.pageNumPending !== null) {// New page rendering is pendingthis.renderPage(_this.pageNumPending);_this.pageNumPending = null;}});});},queueRenderPage(num) {if (this.pageRendering) {this.pageNumPending = num;} else {this.renderPage(num);}},prevPage() {this.confirmSignature();if (this.pageNum = this.numPages) {return;}this.pageNum++;},cutover() {this.confirmSignature();},// 渲染pdf,到时还会盖章信息,在渲染时,同时显示出来,不应该在切换页码时才显示印章信息showpdf(pdfUrl) {let caches = JSON.parse(localStorage.getItem('signs')); // 获取缓存字符串后转换为对象// console.log(caches);if (caches != null) {let datas = caches[this.pageNum];if (datas != null && datas != undefined) {for (let index in datas) {this.addSeal(datas[index].sealUrl,datas[index].left,datas[index].top,datas[index].index);}}}this.canvas = document.getElementById('the-canvas');this.ctx = this.canvas.getContext('2d');pdfjsLib.getDocument({ url: pdfUrl, rangeChunkSize: 65536, disableAutoFetch: false }).promise.then((pdfDoc_) => {this.pdfDoc = pdfDoc_;this.numPages = this.pdfDoc.numPages;this.renderPage(this.pageNum).then(() => {this.renderPdf({width: this.canvas.width,height: this.canvas.height});});this.commonSign(this.pageNum, true);});},/** *盖章部分开始 */// 设置绘图区域宽高renderPdf(data) {this.whDatas = data;// document.querySelector("#elesign").style.width = data.width + "px";},// 生成绘图区域renderFabric() {let canvaEle = document.querySelector('#ele-canvas');let pCenter = document.querySelector('.pCenter');canvaEle.width = pCenter.clientWidth;// canvaEle.height = (this.whDatas.height)*(this.scale);canvaEle.height = this.whDatas.height;this.canvasEle = new fabric.Canvas(canvaEle);let container = document.querySelector('.canvas-container');container.style.position = 'absolute';container.style.top = '50px';// container.style.left = "30%";},// 相关事件操作哟canvasEvents() {// 拖拽边界 不能将图片拖拽到绘图区域外this.canvasEle.on('object:moving', function (e) {var obj = e.target;// if object is too big ignoreif (obj.currentHeight > obj.canvas.height || obj.currentWidth > obj.canvas.width) {return;}obj.setCoords();// top-leftcornerif (obj.getBoundingRect().top < 0 || obj.getBoundingRect().left obj.canvas.height ||obj.getBoundingRect().left + obj.getBoundingRect().width > obj.canvas.width) {obj.top = Math.min(obj.top,obj.canvas.height - obj.getBoundingRect().height + obj.top - obj.getBoundingRect().top);obj.left = Math.min(obj.left,obj.canvas.width - obj.getBoundingRect().width + obj.left - obj.getBoundingRect().left);}});},// 添加公章addSeal(sealUrl, left, top, index) {fabric.Image.fromURL(sealUrl, (oImg) => {oImg.set({left: left,top: top,// angle: 10,scaleX: 0.8,scaleY: 0.8,index: index});// oImg.scale(0.5); //图片缩小一this.canvasEle.add(oImg);});},// 删除签章removeSignature() {this.canvasEle.remove(this.canvasEle.getActiveObject());},// 翻页展示盖章信息commonSign(pageNum, isFirst = false) {if (isFirst == false) this.canvasEle.remove(this.canvasEle.clear()); // 清空页面所有签章let caches = JSON.parse(localStorage.getItem('signs')); // 获取缓存字符串后转换为对象// console.log(caches);if (caches == null) return false;let datas = caches[this.pageNum];if (datas != null && datas != undefined) {for (let index in datas) {this.addSeal(datas[index].sealUrl,datas[index].left,datas[index].top,datas[index].index);}}},// 确认签章位置并保存到缓存confirmSignature() {let data = this.canvasEle.getObjects(); // 获取当前页面内的所有签章信息let caches = JSON.parse(localStorage.getItem('signs')); // 获取缓存字符串后转换为对象let signDatas = {}; // 存储当前页的所有签章信息let i = 0;// let sealUrl = '';for (var val of data) {signDatas[i] = {width: val.width,height: val.height,top: val.top,left: val.left,angle: val.angle,translateX: val.translateX,translateY: val.translateY,scaleX: val.scaleX,scaleY: val.scaleY,pageNum: this.pageNum,sealUrl: this.mainImagelist[val.index],index: val.index};i++;}if (caches == null) {caches = {};caches[this.pageNum] = signDatas;} else {caches[this.pageNum] = signDatas;}localStorage.setItem('signs', JSON.stringify(caches)); // 对象转字符串后存储到缓存},// 提交数据submitSignature() {this.confirmSignature();// let caches = localStorage.getItem('signs');// console.log(JSON.parse(caches));return false;},// 清空数据clearSignature() {this.canvasEle.remove(this.canvasEle.clear()); // 清空页面所有签章localStorage.removeItem('signs'); // 清除缓存},end(e) {this.addSeal(this.mainImagelist[e.newDraggableIndex],e.originalEvent.layerX,e.originalEvent.layerY,e.newDraggableIndex);},// 设置PDF预览区域高度setPdfArea() {this.pdfUrl = './static/text.pdf';// this.pdfurl = res.data.data.pdfurl;this.$nextTick(() => {this.showpdf(this.pdfUrl); // 接口返回的应该还有盖章信息,不只是pdf});}},watch: {whDatas: {handler() {const loading = this.$loading({lock: true,text: 'Loading',spinner: 'el-icon-loading',background: 'rgba(0, 0, 0, 0.7)'});if (this.whDatas) {// console.log(this.whDatas);loading.close();this.renderFabric();this.canvasEvents();let eleCanvas = document.querySelector('#ele-canvas');eleCanvas.style = 'border:1px solid #5ea6ef;margin-top: 10px;';}}},pageNum: function () {this.commonSign(this.pageNum);this.queueRenderPage(this.pageNum);}}};/*pdf部分*/#the-canvas {margin-top: 10px;}html:fullscreen {background: white;}.elesign {display: flex;flex: 1;flex-direction: column;position: relative;/* padding-left: 180px; */margin: auto;/* width:600px; */}.page {text-align: center;margin: 0 auto;margin-top: 1%;}#ele-canvas {/* border: 1px solid #5ea6ef; */overflow: hidden;}.ele-control {text-align: center;margin-top: 3%;}#page-input {width: 7%;}@keyframes ani-demo-spin {from {transform: rotate(0deg);}50% {transform: rotate(180deg);}to {transform: rotate(360deg);}}/* .loadingclass{position: absolute;top:30%;left:49%;z-index: 99;} */.left {position: absolute;top: 42px;left: -5px;padding: 5px 5px;/*border: 1px solid #eee;*//*border-radius: 4px;*/}.left-title {text-align: center;padding-bottom: 10px;border-bottom: 1px solid #eee;}li {list-style-type: none;padding: 10px;}.imgstyle {vertical-align: middle;width: 130px;border: solid 1px #e8eef2;background-image: url('~@/assets/img/projectCenter/tuo.png');background-repeat: no-repeat;}.right {position: absolute;top: 7px;right: -177px;margin-top: 34px;padding-top: 10px;padding-bottom: 20px;width: 152px;/*border: 1px solid #eee;*//*border-radius: 4px;*/}.right-item {margin-bottom: 15px;margin-left: 10px;}.right-item-title {color: #777;height: 20px;line-height: 20px;font-size: 12px;font-weight: 400;text-align: left !important;}.detail-item-desc {color: #333;line-height: 20px;width: 100%;font-size: 12px;display: inline-block;text-align: left;}.btn-outline-dark {color: #0f1531;background-color: transparent;background-image: none;border: 1px solid #3e4b5b;}.btn-outline-dark:hover {color: #fff;background-color: #3e4b5b;border-color: #3e4b5b;}
3. 优化后的代码
印章
将示例印章标识拖到文件相应区域即可获取签章位置
{{ (scale * 100).toFixed(0) + '%' }}/{{ defaultNumPages }} 位置信息
// 拖拽插件import draggable from 'vuedraggable';// pdf插件import { fabric } from 'fabric';import workerSrc from 'pdfjs-dist/es5/build/pdf.worker.entry';const pdfjsLib = require('pdfjs-dist/es5/build/pdf.js');pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc;export default {components: { draggable },data() {return {// pdf地址pdfUrl: '',// 左侧签章列表mainImagelist: [],// 右侧坐标数据coordinateList: [{ name: '名称', page: '所在页面', left: 'x坐标', top: 'Y坐标' }],// 总页数numPages: 1,defaultNumPages: 1,// 当前页pageNum: 1,// 缩放比例scale: 1,// pdf是否显示isFirst: true,isShowPdf: false,// pdf最外层的out-viewoutViewDom: null,// 各页pdf的canvas-layoutcanvasLayoutTopList: [],// 用来签章的canvas数组canvasEle: [],// 绘图区域的宽高whDatas: null,// pdf渲染的canvas数组canvas: [],// pdf渲染的canvas的ctx数组ctx: [],// pdf渲染的canvas的宽高pdfDoc: null,// 隐藏的input,用来提交数据shadowInputValue: ''};},created() {this.mainImagelist = [{ name: '印章', img: require('@/assets/img/projectCenter/contract-sign-img.png') }// { name: '印章', img: require('./sign.png') },// { name: '红章', img: require('@/assets/img/projectCenter/seal.png') }];this.setPdfArea();},mounted() {},methods: {/** * pdf相关部分 */// 设置PDF地址setPdfArea() {// // 1. 获取地址栏// const urlString = window.location.href;// // 2. 截取地址栏// const pdfStr = urlString.split('" />.contract-signature-view {/*pdf部分*/.ele-canvas {overflow: hidden;}.title-operation {height: 80px;padding: 20px 40px;display: flex;align-items: center;justify-content: space-between;.title {font-size: 20px;font-weight: 600;}border-bottom: 1px solid #e4e4e4;}.section-box {position: relative;display: flex;height: calc(100vh - 60px);.signature-img {width: 240px;min-width: 240px;background-color: #fff;padding: 40px 15px;border-right: 1px solid #e4e4e4;.info {margin-bottom: 38px;.name {font-size: 18px;font-weight: 600;color: #000000;line-height: 25px;margin-bottom: 20px;}.text {font-size: 14px;color: #000000;line-height: 20px;}}.item {padding: 10px;border: 1px dashed rgba(0, 0, 0, 0.3);&:not(:last-child) {margin-bottom: 10px;}.img {vertical-align: middle;width: 120px;background-repeat: no-repeat;}}}.main-layout {flex: 1;background-color: #f7f8fa;position: relative;&.is-first {.operate-box {opacity: 0;}}.operate-box {opacity: 1;position: absolute;top: 0;left: 0;width: 100%;height: 40px;background-color: #fff;border-bottom: 1px solid #e4e4e4;display: flex;justify-content: center;align-items: center;.slider-box {width: 230px;display: flex;justify-content: center;align-items: center;border-left: 1px solid #e4e4e4;border-right: 1px solid #e4e4e4;.slider {width: 120px;}.scale-value {margin-left: 24px;font-size: 16px;color: #000000;line-height: 22px;}}.page-change {display: flex;align-items: center;margin-left: 30px;.icon {cursor: pointer;padding: 0 5px;color: #c1c1c1;}.input-box {border: none;/deep/ .el-input__inner {width: 34px;height: 20px;border: none;padding: 0;text-align: center;border-bottom: 1px solid #e4e4e4;}}.default-text {display: flex;line-height: 22px;margin-right: 5px;}}}.out-view {height: calc(100vh - 100px);margin: 40px auto;overflow-x: auto;overflow-y: auto;padding-top: 20px;text-align: center;opacity: 0;transition: all 0.5s;&.is-show {opacity: 1;}.canvas-layout {position: relative;text-align: center;margin: 0 auto 18px;}}.loading {width: 20px;height: 20px;position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);z-index: 999;/deep/ .el-loading-mask {background-color: transparent;}}}.position-info {width: 355px;min-width: 355px;border-left: 1px solid #e4e4e4;background-color: #fff;padding: 14px 15px;.title {font-size: 14px;font-weight: 400;color: #000000;line-height: 20px;padding-bottom: 18px;}.nav {display: flex;flex-direction: column;.item {display: flex;justify-content: space-between;padding: 10px 0;border-bottom: 1px solid #eee;&:first-child {background-color: #f7f8fa;}span {flex: 1;text-align: center;font-size: 12px;color: #000000;line-height: 20px;}}}}}}