fabric.js实现可视化签章以及遮罩打印的功能

3,881

前言

业务场景

首先,是因为这样一个需求,我开始尝试使用fabric.js

公司有个项目,是可信电子凭证可视化签章

支持在打开的PDF文件上能随意拖动一个图片到PDF文档的任意位置。能获取当前拖动的图片在PDF文档的x,y坐标。

后来,又一次用到了fabric.js是档案管理平台的项目

文件在线预览的页面,需要增加一个遮罩打印的功能,就是在pdf文件上打上马赛克,遮罩一些内容。不通过安装插件,纯前端技术来解决。在PDF文件的预览区域上,按住鼠标左键然后拖着,可生成马赛克的遮罩打印区域。

图片1.png

图片2.png

了解一门新技术,直接看官网和相关文档。

fabricjs官网在此fabricjs.com/

官网首页,写在这样一段话:

Fabric.js is a powerful and simple Javascript HTML5 canvas library

Fabric.js是一个强大而简单的Javascript HTML5 Canvas库

然后看了相关文档,了解到我们能通过使用它实现在canvas上创建,填充图形,给图形填充渐变颜色。组合图形(包括组合图形,图形文字,图片等)等一系列功能。

简单来说,我们可以通过使用Fabric从而以较为简单的方式,实现较为复杂的Canvas功能。

知道和做到之间,有一条天然的鸿沟。

有时,人们了解到前人的经验踩过的坑,但是仍然不可避免的掉进这些坑里。自己掉进这些坑里,再爬出来,才最终学习到这些经验,最终避开这些坑。

快速上手

了解到其基础概览,和应用场景之后,准备快速上手。

在vue项目中引入服务

npm install fabric
import { fabric } from 'fabric'

首先做了一个demo用来实现在pdf预览的区域上拖拽图片。

关于pdf文件预览,之前用的pdf.js基于html的pdf阅读器,从官网下载静态资源,放到项目的static静态资源文件夹里面。

使用pdf.js已经写好的viewer.html页面来预览。

static/pdf/web/viewer.html?file=' + encodeURIComponent(pdf)

使用iframe标签去显示。然后,封装成一个公共工作,在需要的地方,直接调用。

组件代码:

<template>
  <div class="pdf">
    <div class="box-card pdf-viewer">
      <iframe
        :src="'static/pdf/web/viewer.html?file=' + encodeURIComponent(pdf)"
        :height="height"
        width="100%"
        frameborder="0"
      ></iframe>
    </div>
  </div>
</template>
<script>
export default {
  name: "PdfDetail",
  components: {},
  props: {
    pdf: {
      type: String,
      default: "",
    },
    height: {
      type: Number,
      default: 560
    }
  },
  data() {
    return {};
  },
  watch: {},
  computed: {},
  methods: {},
  created() {},
  mounted() {},
};
</script>
<style scoped>
.wrapper {
}
</style>

这里只是顺带说了一下pdf.js预览的方法,我并没有采用这种方法去实现pdf预览功能。

因为,不仅要预览,还需要将pdf预览区域转换成canvas画布,然后在画布上实现图片拖拽位置的功能,并获取坐标。

我采用的是vue-pdf组件

GitHub地址:

github.com/FranckFreib…

npm install --save vue-pdf
<template>
  <pdf src="./static/relativity.pdf"></pdf>
</template>
<script>
import pdf from 'vue-pdf'
export default {
  components: {
    pdf
  }
}

核心代码

以下是demo的核心代码

基础方法

// 初始化画布对象
new fabric.Canvas('canvas')
//赋予一个变量,并且添加了双击事件,通过双击事件删除canvas画布上添加的内容
this.canvas = new fabric.Canvas('canvas')
this.canvas.on('mouse:dblclick', (e) => {
    let items = this.canvas.getObjects()
    items = items.filter((item) => item.width > 1 && item.height > 1)
    let itemIdx = items.indexOf(e.target)
    this.canvas.remove(this.canvas.item(itemIdx));
    this.canvas.renderAll();
})

在created钩子函数中,设置fabric的对象拖拽框

fabric.Object.prototype.setControlsVisibility({
      bl: false, // 左下
      br: false, // 右下
      mb: false, // 下中
      ml: false, // 中左
      mr: false, // 中右
      mt: false, // 上中
      tl: false, // 上左
      tr: false, // 上右
      mtr: false, // 旋转控制键
});

通过图片路径,往画布上添加图片的方法

let imgCoord = fabric.Image.fromURL(imgUrl, (img) => {
    img.scale(1).set({
        crossOrigin: 'anonymous',
        left: 0,
        top: 0,
    })
this.canvas.add(img).setActiveObject(img)
})

通过图片对象,往画布上添加图片的方法

let image= new Image()
image.src = imgUrl
image.crossOrigin = 'Anonymous';
image.onload = () => {
      fabric.Image.fromObject(imgl,(img) => {
          img.scale(1).set({
              crossOrigin: 'anonymous',
              left,
              top,
              width,
              height,
              scaleX,
              scaleY,
          })
        this.canvas.add(img).setActiveObject(img)
          this.canvas.renderAll()
      })
  }

除了添加图片,还可以添加文本框,并且设置文字的颜色字体大小等等。

需要注意的是fontSize参数必须为Number类型。

let attributeObject = {
    fill,
    fontFamily,
    fontWeight,
    textAlign,
    lineHeight,
    width,
    splitByGrapheme:true,
    height,
    fontSize:,
    originX: 'center',
    originY: 'center',
}
var obj = new fabric.Textbox(text, attributeObject)
var group = new fabric.Group([obj], {
    left,
    top,
})
this.canvas.add(group)
this.canvas.renderAll()
  • 为了最终拿到画布上所有对象的属性,以及坐标。我将这些属性和坐标,放到了一个json对象数组里面,保存起来。
  • 图片和文字,除了能够拖拽,文字框还要求,能够改变字体颜色大小等等。
  • 我加了字体颜色大小等属性的选择框,做了数据双向绑定。
  • 每当json对象数组改变的时候,我就清空画布上的所有对象,然后从json对象数组里面拿到保存的属性和坐标,在画布上重新渲染。
//清空画布
this.canvas.clear()

获取画布上所有对象的坐标

getImgPosition() {
    if (!this.canvas) return
    this.imgcoordinate = []
    let items = this.canvas.getObjects()
    items = items.filter((item) => item.width > 1 && item.height > 1)
    items.forEach((item, index) => {
        let itemcoord = {
            floorIndex: index,
            tl: {
                x: item.aCoords.tl.x,
                y: item.aCoords.tl.y,
            },
            tr: {
                x: item.aCoords.tr.x,
                y: item.aCoords.tr.y,
            },
            bl: {
                x: item.aCoords.bl.x,
                y: item.aCoords.bl.y,
            },
            br: {
                x: item.aCoords.br.x,
                y: item.aCoords.br.y,
            },
        }
        this.imgcoordinate.push(itemcoord)
    })
    this.xycoordinate = this.imgcoordinate.map((item) => item.tl)
}

添加马赛克

最后说一下,在canvas画布通过fabric.js添加马赛克的方法

首先,初始化canvas画布对象的时候,增加鼠标事件监听的方法

this.canvas = new fabric.Canvas("canvas");
this.canvas.on("mouse:down", function (e) {
  that.mousedown(e);
});
//鼠标抬起事件
this.canvas.on("mouse:up", function (e) {
  that.mouseup(e);
});
// 移动画布事件
this.canvas.on("mouse:move", function (e) {
  that.mousemove(e);
});
  • 鼠标点击mousedown事件时,记录下画布上点的位置,
  • 鼠标移动后抬起mouseup事件时,记录下画布上点的最终位置,
  • 这样,就可以算出鼠标拖拽的矩形的初始位置x,y坐标,以及矩形的宽高。
let mouse = this.canvas.getPointer(e.e);

接下来是最关键的,实现马赛克的方法,这个花了很长时间。

  • 在初始化canvas画布对象的时候,需要通过getContext() 方法返回一个用于在画布上绘图的环境。
  • 然后传一个图片路径imgUrl,通过drawImage画出底图。
  • 具体生成马赛克的方法,在setColor中,是通过context对象的getImageData方法,获取图片数据。根据设置的马赛克方块大小,通过rgb的颜色设置,模糊掉底图上的图片,实现遮罩的效果。
let Img = new Image();
Img.src = imgUrl;
that.bgImage = Img;
Img.onload = () => {
    that.context.drawImage(Img, 0, 0);
    that.context.save();
};

以下,是画矩形方框,并且填充马赛克,最终实现马赛克的方法

drawMake() {
  if(!this.canvas)return;
  this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
  this.context.drawImage(this.bgImage, 0, 0);
  this.context.save();
  // if (this.canvas) this.canvas.clear();
  this.makeList.forEach((item) => {
    let { beginX, beginY, w, h } = item;
    this.makeGrid(beginX, beginY, w, h);
  });
},
makeGrid(beginX, beginY, rectWidth, rectHight) {
  const row = Math.round(rectWidth / this.squareEdgeLength) + 1;
  const column = Math.round(rectHight / this.squareEdgeLength) + 1;
  for (let i = 0; i < row * column; i++) {
    let x = (i % row) * this.squareEdgeLength + beginX;
    let y = parseInt(i / row) * this.squareEdgeLength + beginY;
    this.setColor(x, y);
  }
},
setColor(x, y) {
  const imgData = this.context.getImageData(
    x,
    y,
    this.squareEdgeLength,
    this.squareEdgeLength
  ).data;
  let r = 0,
    g = 0,
    b = 0;
  for (let i = 0; i < imgData.length; i += 4) {
    r += imgData[i];
    g += imgData[i + 1];
    b += imgData[i + 2];
  }
  r = Math.round(r / (imgData.length / 4));
  g = Math.round(g / (imgData.length / 4));
  b = Math.round(b / (imgData.length / 4));
  this.drawRect(
    x,
    y,
    this.squareEdgeLength,
    this.squareEdgeLength,
    `rgb(${r}, ${g}, ${b})`,
    2,
    `rgb(${r}, ${g}, ${b})`
  );
},
drawRect(
  x,
  y,
  width,
  height,
  fillStyle,
  lineWidth,
  strokeStyle,
  globalAlpha
) {
    this.context.beginPath();
    this.context.rect(x, y, width, height);
    this.context.lineWidth = lineWidth;
    this.context.strokeStyle = strokeStyle;
    fillStyle && (this.context.fillStyle = fillStyle);
    globalAlpha && (this.context.globalAlpha = globalAlpha);
    this.context.fill();
    this.context.stroke();
},

除了fabric.js之外,为了实现遮罩打印的功能。还用到了 html2canvas 和 jsPDF的方法,在此不一一赘述,直接放出遮罩打印的组件完整代码。

实现了基本的业务需求之后,我还做了一些优化,譬如撤销和回退的功能。增加了属性设置弹框,通过拖动滑块选择马赛克方块的大小。通过driver.js实现帮助提示,操作指引。

这里,主要是记录了实现业务需求的解决思路,以及踩坑指南。

参考文章

参考了博客园的两篇文章:

Canvas实用库Fabric.js使用手册

www.cnblogs.com/aaron911/p/…

Vue PDF文件预览vue-pdf

www.cnblogs.com/steamed-twi…