1、思路:pdf或者图片通过canvas预览,作为背景;印章则通过另一个canvas覆盖其上,使用fabric进行拖动。最后获取印章的相对位置、相对尺寸等信息,传递给后端,由后端生成最终文件。
2、npm install fabric
npm install pdfjs-dist
我用的是 fabric 5.3.0,pdfjs-dist 2.5.207 版本
3、代码部分
(1)pdf或者图片canvas预览 pdfOImgPreview.vue
*注意:pdfjs引用
<template>
<div class="center">
<div v-if="isFilePDF">
<el-button size="mini" @click="prevPage">上一页</el-button>
<el-button size="mini" @click="nextPage">下一页</el-button>
<span>页码: {{ pageNum }} / <span id="page_count"></span></span>
</div>
<canvas id="the-canvas" />
</div>
</template>
<script>
import * as PDFJS from "pdfjs-dist/legacy/build/pdf.js";
import pdfjsWorker from "pdfjs-dist/legacy/build/pdf.worker.entry";
PDFJS.GlobalWorkerOptions.workerSrc = pdfjsWorker;
export default {
name: "pdfOImgPreview",
props: {
//预览文件
sealFile: {
type: File | null,
required: true,
default: () => null,
},
},
data() {
return {
pdfDoc: null,
pageNum: 1,
pageRendering: false,
pageNumPending: null,
canvas: null,
ctx: null,
pdfScale: 1,
};
},
computed: {
//文件类型是否是pdf
isFilePDF() {
return this.sealFile.type.includes("pdf");
},
},
async mounted() {
this.canvas = document.getElementById("the-canvas");
this.ctx = this.canvas.getContext("2d");
if (this.isFilePDF) {
await this.printPDF(this.sealFile);
} else {
await this.printImg(this.sealFile);
}
},
methods: {
//预览pdf
async printPDF(pdfData) {
const pdfjsLib = PDFJS;
const Base64Prefix = "data:application/pdf;base64,";
pdfData =
pdfData instanceof Blob ? await this.readBlob(pdfData) : pdfData;
const data = atob(
pdfData.startsWith(Base64Prefix)
? pdfData.substring(Base64Prefix.length)
: pdfData
);
// Using DocumentInitParameters object to load binary data.
const loadingTask = pdfjsLib.getDocument({ data });
return loadingTask.promise.then((pdfDoc_) => {
this.pdfDoc = pdfDoc_;
document.getElementById("page_count").textContent =
this.pdfDoc.numPages;
this.renderPage(this.pageNum).then((res) => {
this.$emit("renderPdf", {
width: this.canvas.width,
height: this.canvas.height,
pages: this.pdfDoc.numPages,
ratio: this.pdfScale,
});
});
});
},
readBlob(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener("load", () => resolve(reader.result));
reader.addEventListener("error", reject);
reader.readAsDataURL(blob);
});
},
renderPage(num) {
let _this = this;
this.pageRendering = true;
// Using promise to fetch the page
return this.pdfDoc.getPage(num).then((page) => {
let viewport = page.getViewport({ scale: 1 });
const { ratio } = this.getWidthHeight4Pdf(viewport);
const newScale = +ratio.toFixed(1) - 0.1;
this.pdfScale = newScale;
console.log("pdfScale", newScale);
viewport = page.getViewport({ scale: newScale });
_this.canvas.height = viewport.height;
_this.canvas.width = viewport.width;
// Render PDF page into canvas context
let renderContext = {
canvasContext: _this.ctx,
viewport: viewport,
};
let renderTask = page.render(renderContext);
// Wait for rendering to finish
renderTask.promise.then(() => {
_this.pageRendering = false;
if (_this.pageNumPending !== null) {
// New page rendering is pending
this.renderPage(_this.pageNumPending);
_this.pageNumPending = null;
}
});
});
},
queueRenderPage(num) {
if (this.pageRendering) {
this.pageNumPending = num;
} else {
this.renderPage(num);
}
},
prevPage() {
if (this.pageNum <= 1) {
return;
}
this.pageNum--;
this.queueRenderPage(this.pageNum);
this.$emit("getPageNum", {
oldNum: this.pageNum + 1,
newNum: this.pageNum,
});
},
nextPage() {
if (this.pageNum >= this.pdfDoc.numPages) {
return;
}
this.pageNum++;
this.queueRenderPage(this.pageNum);
this.$emit("getPageNum", {
oldNum: this.pageNum - 1,
newNum: this.pageNum,
});
},
//获取PDF宽高,策略:按照dialog宽度,同比例缩放
getWidthHeight4Pdf({ width }) {
const newWidth = window.screen.width * 0.9 - 60;
const ratio = newWidth / width;
return { ratio };
},
//获取图片宽高,策略:如果超过宽度超过dialog,则同比例缩小,否则取原图大小
getWidthHeight4Img({ width, height }) {
const maxWidth = window.screen.width * 0.9 - 60;
const isOverMax = width > maxWidth;
const ratio = isOverMax ? maxWidth / width : 1;
const newWidth = isOverMax ? maxWidth : width;
const newHeight = ratio * height;
return { width: newWidth, height: newHeight, ratio };
},
//预览图片
async printImg(imgData) {
const img = new Image();
img.src = URL.createObjectURL(imgData);
const that = this;
img.onload = () => {
const { width, height, ratio } = this.getWidthHeight4Img(img);
that.canvas.width = width;
that.canvas.height = height;
that.ctx.drawImage(img, 0, 0, width, height);
that.$emit("renderPdf", {
width: width,
height: height,
pages: 1,
ratio,
});
};
},
},
};
</script>
<style lang="scss" scoped>
.center {
width: 100%;
height: 100%;
> div {
height: 30px;
}
}
</style>
(2)完成盖章部分,此处我使用弹框的方式展现。 fabricSeal.vue
<template> <el-dialog width="90%" :title="title" :visible.sync="dialogIsVisible" :close-on-click-modal="false" append-to-body destroy-on-close class="fabricSeal" > <div class="elesign" id="elesign"> <!-- pdf或者图片canvas预览 --> <pdfOImgPreview v-if="sealFile_" ref="preview" :sealFile="sealFile_" @renderPdf="renderPdf" @getPageNum="getPageNum" ></pdfOImgPreview> <!-- 盖章部分 --> <canvas id="ele-canvas"></canvas> </div> <div slot="footer" class="dialog-footer"> <el-button size="small" @click="onClose"> 取消 </el-button> <el-button size="small" type="primary" @click="onConfirm"> 确定 </el-button> </div> </el-dialog> </template> <script> import { fabric } from "fabric"; export default { components: { pdfOImgPreview: () => import("./pdfOimgPreview.vue"), }, props: { title: { type: String, default: () => "手动设置电子章", }, // 控制弹窗显隐开关 必传 dialogVisible: { type: Boolean, required: true, default: () => false, }, //预览文件 必传 sealFile: { type: File | null, required: true, default: () => null, }, //印章图片链接 sealUrl: { type: String, default: () => "", }, //印章初始化参数 apiDataInit: { type: Array, default: () => [ { left: undefined, top: undefined, height: undefined, width: undefined, pageNum: 1, }, ], }, //pdf情况,每个页面是否允许独立设置印章 //false: signaData 返回对象,true: signaData 返回数组 isPageSeal: { type: Boolean, default: () => false, }, }, data() { return { canvas: null, whDatas: null, apiData: [], }; }, computed: { //实现.sync双向绑定数据 dialogIsVisible: { get() { return this.dialogVisible; }, set(newValue) { this.$emit("update:dialogVisible", newValue); }, }, sealFile_() { return this.dialogIsVisible ? this.sealFile : null; }, //文件类型是否是pdf isFilePDF() { return this.sealFile.type.includes("pdf"); }, }, watch: { whDatas: { handler() { if (!!this.whDatas) { this.renderFabric(); this.canvasEvents(); } }, }, }, methods: { //取消 onClose() { this.$emit("update:dialogVisible", false); }, //确定 onConfirm() { const pageNum = this.$refs.preview.pageNum; this.setPageData(pageNum); console.log("this.apiData", this.apiData); const apiData = this.isPageSeal ? this.apiData : this.apiData.find((item) => item.pageNum === pageNum); this.$emit("getSealData", apiData); this.onClose(); }, //设置每页参数 setPageData(pageNum) { const data = this.canvas.getObjects()[0]; const apiData = { ...this.signaData2ApiData(data), ...{ pageNum }, }; const index = this.apiData.findIndex((item) => item.pageNum === pageNum); this.apiData[index] = apiData; }, //获取pdf页码 getPageNum({ oldNum, newNum }) { if (!this.isPageSeal) { return; } this.setPageData(oldNum); this.removeSignature(); const tempData = this.apiData.find((item) => item.pageNum === newNum); let apiData = undefined; if (tempData.top) { apiData = tempData; } this.addSignature(apiData); }, // 设置绘图区域宽高 renderPdf(data) { this.whDatas = data; const { width: whWdith, height: whHeight } = this.whDatas; let apiData = []; for (let i = 1; i <= this.whDatas.pages; i++) { const defaultApiData = { width: +(150 / whWdith).toFixed(2), height: +(150 / whHeight).toFixed(2), top: 0.01, left: 0.01, pageNum: i, }; const index = this.apiDataInit.findIndex((item) => item.pageNum === i); if (index < 0) { apiData.push(defaultApiData); } else { const newApiDatum = { ...defaultApiData, ...this.apiDataInit[i] }; apiData.push(newApiDatum); } } this.apiData = apiData; document.querySelector(".elesign").style.width = `${data.width}px`; this.addSignature(); }, // 生成绘图区域 renderFabric() { const { width, height } = this.whDatas; const canvaEle = document.querySelector("#ele-canvas"); canvaEle.width = width; canvaEle.height = height; this.canvas = new fabric.Canvas(canvaEle); const container = document.querySelector(".canvas-container"); container.id = "newContainer"; Object.assign(container.style, { position: "absolute", top: `${this.isFilePDF ? 120 : 90}px`, }); }, // 相关事件操作哟 canvasEvents() { // 拖拽边界 不能将图片拖拽到绘图区域外 this.canvas.on("object:moving", (e) => { let obj = e.target; obj.setCoords(); if ( obj.getBoundingRect().top - obj.cornerSize / 2 < 0 || obj.getBoundingRect().left - obj.cornerSize / 2 < 0 ) { obj.top = Math.max( obj.top, obj.top - obj.getBoundingRect().top + obj.cornerSize / 2 ); obj.left = Math.max( obj.left, obj.left - obj.getBoundingRect().left + obj.cornerSize / 2 ); } if ( obj.getBoundingRect().top + obj.getBoundingRect().height + obj.cornerSize > obj.canvas.height || obj.getBoundingRect().left + obj.getBoundingRect().width + obj.cornerSize > obj.canvas.width ) { obj.top = Math.min( obj.top, obj.canvas.height - obj.getBoundingRect().height + obj.top - obj.getBoundingRect().top - obj.cornerSize / 2 ); obj.left = Math.min( obj.left, obj.canvas.width - obj.getBoundingRect().width + obj.left - obj.getBoundingRect().left - obj.cornerSize / 2 ); } }); },// 删除签章 removeSignature() { this.canvas.remove(this.canvas.getObjects()[0]); }, // 添加签章 async addSignature(apiDataInit = this.apiDataInit[0]) { const that = this; const sealUrl = this.sealUrl; //设置印章初始化参数 const signaData = this.apiData2SignaData(apiDataInit); const { left, top, scaleX, scaleY, angle, height, width, opacity, lockRotation, } = signaData; fabric.Image.fromURL( sealUrl, (oImg) => { oImg.set({ left: left > this.canvas.width - width * scaleX - 10 ? this.canvas.width - width * scaleX - 10 : left, top: top > this.canvas.height - height * scaleY - 10 ? this.canvas.height - height * scaleY - 10 : top, scaleX, scaleY, angle, }); that.canvas.add(oImg); }, { opacity, lockRotation } ); }, // 签章参数===》接口参数 signaData2ApiData(signaData) { const { width: whWdith, height: whHeight } = this.whDatas; const { left = 10, top = 10, scaleX = 0.5, scaleY = 0.5, angle = 0, height = 300, width = 300, opacity = 0.5, lockRotation = true, } = signaData; const ratio = this.whDatas.ratio; return { width: +((width / whWdith) * scaleX).toFixed(2), //印章宽度百分比(对比背景宽度) height: +((height / whHeight) * scaleY).toFixed(2), //印章高度百分比(对比背景高度) left: +(left / whWdith).toFixed(2), //印章距左边百分比 top: +(top / whHeight).toFixed(2), //印章顶边百分比 scaleX: +scaleX.toFixed(2), //印章宽度百分比(对比印章原始宽度) scaleY: +scaleY.toFixed(2), //印章宽度百分比(对比印章原始高度) widthpx: +width.toFixed(0), //印章宽度 px heightpx: +height.toFixed(0), //印章高度 px leftpx: +left.toFixed(0), //印章距左边 px toppx: +top.toFixed(0), //印章距左边 px ratio, //图片预览时缩放的尺寸 }; }, // 接口参数===》签章参数 apiData2SignaData(apiData) { const { width: whWdith, height: whHeight } = this.whDatas; const { left = 0.01, top = 0.01, width = 150 / whWdith, height = 150 / whHeight, } = apiData; return { left: +(left * whWdith).toFixed(0), top: +(top * whHeight).toFixed(0), scaleX: +((width * whWdith) / 300).toFixed(2), scaleY: +((height * whHeight) / 300).toFixed(2), angle: 0, height: 300, width: 300, opacity: 0.5, lockRotation: true, }; }, }, }; </script> <style lang="scss" scoped> .fabricSeal { /deep/.el-dialog { margin: 0 auto !important; .el-dialog__header { line-height: 30px; } // .el-dialog__footer{ // position: absolute; // bottom: 0; // right: 0; // } } } </style>
(3)父组件使用
<template>
<fabricSeal
v-if="showFabricSeal"
:dialogVisible.sync="showFabricSeal"
:sealFile="sealFile"
:sealUrl="sealUrl"
@getSealData="getSealData"
></fabricSeal>
</template>
<script>
export default {
components:{
fabricSeal: () => import("./fabricSeal.vue"),
},
data(){
return{
showFabricSeal:false,
sealFile:"",//预览文件,可以通过el-upload获取
sealUrl:"",//印章路由
}
},
methods:{
//手动拼图获取印章位置信息
getSealData(sealData){
}
}
}
</script>
浙公网安备 33010602011771号