需求背景
公司有个业务需要去现场拍摄实况照片,但是部分员工会事先拍好照片存在相册里,等巡检时间到了直接从本地上传图片,因此需要一个水印相机来避免这种作弊行为!
技术实现
要实现业务需求,第一点要做的就是禁止相册选择,这个只要将uni.chooseImage
的 sourceType
设置为 camera
即可,另外要做的是把需要的水印文字添加到图片上面。在Uniapp里通过uni.getImageInfo
获取图片,然后用uni.createCanvasContext
在canvas里将图片和水印文字画在一起,最后通过uni.canvasToTempFilePath
导出成图片即可。
但为了节省开发时间,便去插件市场找了一个叫hpy-watermark的插件,并修改了部分源码来匹配需求,修改点和修改后的源码如下:
- 有时候图片像素太高,上传和渲染比较慢,通过设置
scale
值来等比缩放图片;同时scale
也用来设置水印文字大小和间距,以匹配不同像素的图片 - 当同一个页面有多个地方用到组件时,
canvasToTempFilePath
方法寻找canvas会出现重复问题,导致图片绘制不了,故加了个index唯一值 - 钉钉不支持cavans的
measureText
方法,自己也尝试通过字体数量和大小来代替计算,但都不是很准确,所以最后只采用了左上角和左下角这两个位置的水印
vue
<template>
<view class="watermark-content">
<canvas :canvas-id="'watermarkCanvas'+index" :id="'watermarkCanvas'+index" :style="{width:canvasWidth + 'px', height:canvasHeight + 'px'}"></canvas>
</view>
</template>
<script>
export default {
name:'hpy-watermark',
props:{
/**
* 组件唯一值标识
*/
index:{
type:[Number, String],
default:0
},
/**
* 文字文字位置(默认:左下角)可选值:左上角:topLeft、右上角:topRight、左下角:bottomLeft、右下角:bottomRight
*/
markAlign:{
type:String,
default:function(){
return 'bottomLeft'
}
},
/**
* 设置文本的水平对齐方式,默认:start,文本在指定的位置开始。
* end 文本在指定的位置结束。
* center 文本的中心被放置在指定的位置。
* left 文本左对齐。
* right 文本右对齐。
*/
textAlign:{
type:String,
default:function(){
return 'start';
}
},
/**
* 设置文本的垂直对齐方式,默认:alphabetic文本基线是普通的字母基线。
* top 文本基线是 em 方框的顶端。
* hanging 文本基线是悬挂基线。
* middle 文本基线是 em 方框的正中。
* ideographic 文本基线是表意基线。
* bottom 文本基线是 em 方框的底端。
*/
textBaseline:{
type:String,
default:function(){
return 'alphabetic';
}
},
/**
* 文字大小
*/
fontSize:{
type:[Number, String],
default:40
},
/**
* 文字颜色
*/
fontColor:{
type:String,
default:function(){
return '#FFFFFF'
}
},
/**
* 阴影颜色
*/
shadowColor:{
type:String,
default:function(){
return 'rgba(0, 0, 0, 1.0)';
}
},
/**
* 阴影边框大小
*/
shadowWidth:{
type:[Number, String],
default:2
},
/**
* 图片的质量,取值范围为 (0, 1],不在范围内时当作1处理
*/
quality:{
type:[Number, String],
default:1
},
/**
* 目标文件的类型,只支持 'jpg' 或 'png'。默认为 'png'
*/
fileType:{
type:String,
default:function(){
return 'png'
}
}
},
data() {
return {
canvasWidth:0,
canvasHeight:0
};
},
methods: {
/**
* 增加水印
* @param {Object} {filePaths:['图片地址1', '图片地址2'], fillTexts:['水印1', '水印2']}
*/
async addWaterMark({ filePaths = [], fillTexts = [] }) {
uni.showLoading({title:'图片处理中···'});
try{
for (const filePath of filePaths) {
await this.drawImage(filePath, fillTexts.reverse());
}
}catch(e){
// TODO handle the exception
}finally{
uni.hideLoading();
}
},
/**
* 绘制单个图片
*/
async drawImage(filePath, fillTexts,index){
const ctx = uni.createCanvasContext('watermarkCanvas'+this.index, this);
return new Promise(resolve => {
uni.getImageInfo({
src: filePath,
success: (image) => {
let scale = 0.8
let customWidth = image.width * scale
let customHeight = image.height *scale
this.canvasWidth = customWidth;
this.canvasHeight = customHeight;
ctx.clearRect(0, 0, customWidth, customHeight);
setTimeout(()=>{
ctx.drawImage(image.path, 0, 0, customWidth, customHeight);
ctx.setFontSize(this.fontSize * (customWidth/960));
ctx.setFillStyle(this.fontColor);
// 设置阴影
let shadowWidth = Number(this.shadowWidth + "");
if(shadowWidth > 0){
ctx.shadowColor = this.shadowColor;
ctx.shadowOffsetX = shadowWidth;
ctx.shadowOffsetY = shadowWidth;
}
// 设置水平对齐方式
ctx.textAlign = this.textAlign;
// 设置垂直对齐方式
ctx.textBaseline = this.textBaseline;
fillTexts.forEach((mark, index) => {
let gap = (index*60+60)* (customWidth/960)
if(this.markAlign == "topLeft"){
ctx.fillText(mark, 20, customHeight - gap);
}else{
ctx.fillText(mark, 20, gap);
}
});
ctx.draw(false, (() => {
setTimeout(()=>{
uni.canvasToTempFilePath({
canvasId: 'watermarkCanvas'+this.index,
fileType:this.fileType,
quality:Number(this.quality + "" || "1"),
success: (res) => {
this.$emit('waterMark', res.tempFilePath);
},
fail:(err) => {
console.log(err)
},
complete: () => {
resolve();
}
}, this);
}, 300);
})());
}, 200);
},
fail: (e) => {
resolve();
}
});
});
}
}
}
</script>
<style scoped>
.watermark-content{width: 0;height: 0;overflow: hidden;}
</style>
业务组件封装
vue
<template>
<view>
<slot></slot>
<hpy-watermark :index="index" :ref="'watermark' + index" @waterMark="waterMark"></hpy-watermark>
</view>
</template>
<script>
export default {
props:{
index:{
type:[Number, String],
default:0
},
},
data() {
return {
imageList:[],
}
},
methods: {
// 选择图片
chooseImage() {
uni.chooseImage({
count: 9, // 限制的图片数量
sizeType: ['compressed'], // original 原图,compressed 压缩图,默认二者都有
sourceType: ['camera'],// album 从相册选图,camera 使用相机,默认二者都有
success: (res) => {
var imgPathList = res.tempFilePaths;
if(imgPathList.length > 0){
this.addImages(imgPathList);
}
},
fail: (err) => {
if("chooseImage:fail cancel" == err.errMsg){
uni.showToast({
icon:'none',
title:'取消了选择'
});
}
}
});
},
// 添加图片
addImages(filePaths){
if(filePaths.length > 0){
let userInfo = uni.getStorageSync('userInfoLogin')
var fillTexts = ["拍摄人:" + userInfo.nickName, "时间:" + this.getNowTime()];
// 添加水印
console.log(filePaths, 'filePaths', this.index)
this.$refs['watermark' + this.index].addWaterMark({
filePaths,
fillTexts
});
}
},
/**
* 水印添加回调,在H5平台下,filePath 为 base64
*/
waterMark(filePath){
this.imageList = []
this.imageList.push(filePath);
console.log(this.imageList, 'this.imageList')
this.$emit('getImageList', this.imageList)
},
/**
* 获取当前时间
*/
getNowTime(){
var date = new Date(),
year = date.getFullYear(),
month = date.getMonth() + 1,
day = date.getDate(),
hour = date.getHours() < 10 ? "0" + date.getHours() : date.getHours(),
minute = date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes(),
second = date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds();
month >= 1 && month <= 9 ? (month = "0" + month) : "";
day >= 0 && day <= 9 ? (day = "0" + day) : "";
return (year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second);
}
}
}
</script>
<style scoped>
.ul{border: red solid 1px; text-align: center; margin-right: 12px; position: relative; min-height: 100px; }
.ul .li .img{display:block; width: 20px; }
</style>
组件引用
sql
<camera-watermark :index="index" :ref="'cameraWatermark' + index" @getImageList="getImageList">
<view>图片展示区</view>
<view>上传按钮</view>
</camera-watermark>
小插曲:同一套Uniapp代码,同事在Hbuliderx上编译出来的钉钉小程序布局是乱的,后来发现是Hbuilder版本太新... 降到3.4.18版本就可以了