前言
业务场景
首先,是因为这样一个需求,我开始尝试使用fabric.js
公司有个项目,是可信电子凭证可视化签章。
支持在打开的PDF文件上能随意拖动一个图片到PDF文档的任意位置。能获取当前拖动的图片在PDF文档的x,y坐标。
后来,又一次用到了fabric.js是档案管理平台的项目。
文件在线预览的页面,需要增加一个遮罩打印的功能,就是在pdf文件上打上马赛克,遮罩一些内容。不通过安装插件,纯前端技术来解决。在PDF文件的预览区域上,按住鼠标左键然后拖着,可生成马赛克的遮罩打印区域。
了解一门新技术,直接看官网和相关文档。
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地址:
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使用手册
Vue PDF文件预览vue-pdf