使用fabric.js+pdf.js实现简易盖章

10,866 阅读3分钟

最近公司的一个项目打算实现在线预览文档并给这个文档改一个公章,然后要求前端可以让用户实现盖章拖动,传给后端的数据只需要知道盖章的位置和盖在文档得的哪一页,于是让先弄一个 demo 试试方案。 调查了一番之后,觉得可以使用pdf.js实现预览文档,公章的操作则可以用farbic.js实现。最终呈现的效果类似这样:

pdf.js 实现预览

首先在第一步实现 PDF 的预览这里就碰到了问题,官方有一个pdfjs-dist的库,网上也有相关的封装方法,但是经过多次试验之后始终提示有问题,所以放弃 import 的导入方式采用最古老最直接的 script 导入。这个前提是你需要到官网下载相关包。因为项目是通过 vue-cli3 搭建,需要将资源包放到 static 文件夹下即可。

<script src="static/pdfjs/build/pdf.js"></script>

接下来就是实现预览,这里直接参考官方样例只是我把它改造成了 vue 的版本对照一下一看便知

<div class="center">
  <div>
    <el-button size="small" @click="prevPage">Previous</el-button>
    <el-button size="small" @click="nextPage">Next</el-button>
    &nbsp; &nbsp;
    <span>Page: {{ pageNum }} / <span id="page_count"></span></span>
  </div>
  <canvas id="the-canvas" />
</div>
// import pdfjsLib from "pdfjs-dist";
// pdfjsLib.GlobalWorkerOptions.workerSrc = "pdfjs-dist/build/pdf.worker.js";

let pdfjsLib = window["pdfjs-dist/build/pdf"];
pdfjsLib.GlobalWorkerOptions.workerSrc = `http://${window.location.host}/static/pdfjs/build/pdf.worker.js`;
// pdfjsLib.disableWorker = true;

export default {
  name: "pdfPreview",
  props: {
    pdfUrl: {
      type: String,
    },
    scale: {
      type: Number,
      default: 1.5,
    },
  },
  data() {
    return {
      pdfDoc: null,
      pageNum: 1,
      pageRendering: false,
      pageNumPending: null,
      canvas: null,
      ctx: null,
    };
  },
  methods: {
    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: _this.scale });
        _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);
    },
    nextPage() {
      if (this.pageNum >= this.pdfDoc.numPages) {
        return;
      }
      this.pageNum++;
      this.queueRenderPage(this.pageNum);
    },
  },
  mounted() {
    this.canvas = document.getElementById("the-canvas");
    this.ctx = this.canvas.getContext("2d");

    pdfjsLib.getDocument(this.pdfUrl).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,
        });
      });
    });
  },
};

我将这一部分命名为一个 PdfPreview 的组件,在父组件中调用

<div class="elesign">
  <!-- pdf的预览 -->
  <pdf-preview ref="preview" :pdfUrl="url" @renderPdf="renderPdf"></pdf-preview>

  <!-- 盖章部分 -->
  <canvas id="ele-canvas"></canvas>
  <section class="ele-control">
    <el-button
      size="small"
      icon="el-icon-plus"
      @click="addSignature"
      :disabled="hasSigna"
      >添加签章</el-button
    >
    <el-button
      type="danger"
      size="small"
      icon="el-icon-delete"
      @click="removeSignature"
      :disabled="!hasSigna"
      >删除签章</el-button
    >
    <el-button size="small" @click="getSignature" :disabled="!hasSigna"
      >获取数据</el-button
    >
  </section>
  <!-- <img src="../assets/logo.png" id="img" style="display:none" /> -->
</div>

这样便实现了简易的预览 pdf

fabric.js 实现盖章

翻看 farbic 的文档需要一个 canvas 作为载体,而我们最终想实现把章盖在问文档上这样的效果,那么其实只是个展示问题。于是最简单的实现方案来了,我只需要在预览的 pdf 元素上叠加一个长宽一样的 canavs 就能实现效果

.elesign {
  position: relative;
  margin: 0 auto;
  .canvas-container {
    position: absolute;
    top: 42px;
    left: 0;
  }
}
#ele-canvas {
  border: 1px solid #5ea6ef;
}

然后我需要做的就是在 PDF 预览生成以后将 PDF 的长宽传给父层,父层获取数据之后修改 fabric 的 canvas 就可以了,具体代码传值参考上面得代码片段中的mounted。然后父层获取:

renderPdf(data) {
 this.whDatas = data;
 document.querySelector(".elesign").style.width = data.width + "px";
},

剩下的问题就是添加盖章的图片并可以在绘图区内随意拖动缩放,这里全是官方文档的东西我直接展示加了注释的代码吧,很好理解:

<template>
  <div class="elesign">
    <!-- pdf的预览 -->
    <pdf-preview
      ref="preview"
      :pdfUrl="url"
      @renderPdf="renderPdf"
    ></pdf-preview>

    <!-- 盖章部分 -->
    <canvas id="ele-canvas"></canvas>
    <section class="ele-control">
      <el-button
        size="small"
        icon="el-icon-plus"
        @click="addSignature"
        :disabled="hasSigna"
        >添加签章</el-button
      >
      <el-button
        type="danger"
        size="small"
        icon="el-icon-delete"
        @click="removeSignature"
        :disabled="!hasSigna"
        >删除签章</el-button
      >
      <el-button size="small" @click="getSignature" :disabled="!hasSigna"
        >获取数据</el-button
      >
    </section>
    <!-- <img src="../assets/logo.png" id="img" style="display:none" /> -->
  </div>
</template>
import { fabric } from "fabric";
import pdfPreview from "./pdfPreview";
let goodtop, goodleft, boundingObject;

export default {
  name: "PdfDemo",
  components: {
    pdfPreview,
  },
  data() {
    return {
      url: `http://${window.location.host}/static/doc/hello.pdf`,
      canvas: null,
      whDatas: null,
      signaData: {},
    };
  },
  computed: {
    hasSigna() {
      return !!this.canvas && !!this.canvas.getObjects()[0] ? true : false;
    },
  },
  watch: {
    whDatas: {
      handler() {
        if (!!this.whDatas) {
          this.renderFabric();
          this.canvasEvents();
        }
      },
    },
  },
  methods: {
    // 设置绘图区域宽高
    renderPdf(data) {
      this.whDatas = data;
      document.querySelector(".elesign").style.width = data.width + "px";
    },

    // 生成绘图区域
    renderFabric() {
      let canvaEle = document.querySelector("#ele-canvas");
      canvaEle.width = this.whDatas.width;
      canvaEle.height = this.whDatas.height;

      this.canvas = new fabric.Canvas(canvaEle);
      let container = document.querySelector(".canvas-container");
      container.style.position = "absolute";
      container.style.top = "42px";
      // console.log(this.canvas);
    },

    // 相关事件操作哟
    canvasEvents() {
      // 拖拽边界 不能将图片拖拽到绘图区域外
      this.canvas.observe("object:moving", function (e) {
        var obj = e.target;

        if (obj.height > obj.canvas.height || obj.width > obj.canvas.width) {
          obj.setScaleY(obj.originalState.scaleY);
          obj.setScaleX(obj.originalState.scaleX);
        }
        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
          );
        }
      });
    },

    // 添加签章
    addSignature() {
      fabric.Image.fromURL(
        `http://${window.location.host}/static/imgs/dog3.png`,
        (oImg) => {
          oImg.set({
            left: this.whDatas.width / 2 - 50,
            top: this.whDatas.height / 2 - 100,
            width: 400,
            height: 400,
            angle: 10,
            scaleX: 0.6,
            scaleY: 0.6,
          });
          oImg.scale(0.5); //图片缩小一半
          this.canvas.add(oImg);
        }
      );

      // let imgElement = document.getElementById("img"); //声明我们的图片
      // let imgInstance = new fabric.Image(imgElement, {
      //   //设置图片位置和样子
      //   left: 100,
      //   top: 100,
      //   width: 200,
      //   height: 200,
      //   angle: 30, //设置图形顺时针旋转角度
      // });
      // this.canvas.add(imgInstance);
    },

    // 删除签章
    removeSignature() {
      this.canvas.remove(this.canvas.getObjects()[0]);
    },

    // 生成传给后端的数据
    getSignature() {
      let data = this.canvas.getObjects()[0];
      // console.log(data);
      this.signaData = {
        width: data.width,
        height: data.height,
        top: data.top,
        left: data.left,
        angle: data.angle,
        translateX: data.translateX,
        translateY: data.translateY,
        scaleX: data.scaleX,
        scaleY: data.scaleY,
        pageNum: this.$refs.preview.pageNum,
      };
      console.log(this.signaData);
    },
  },
};

这样,简单的预览盖章就实现了