fabric+pdfjs 拖动印章

发布时间 2023-06-06 11:01:11作者: 浅悠

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>
      &nbsp; &nbsp;
      <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>