Uniapp实现水印相机(钉钉小程序)

需求背景

公司有个业务需要去现场拍摄实况照片,但是部分员工会事先拍好照片存在相册里,等巡检时间到了直接从本地上传图片,因此需要一个水印相机来避免这种作弊行为!

技术实现

要实现业务需求,第一点要做的就是禁止相册选择,这个只要将uni.chooseImagesourceType 设置为 camera即可,另外要做的是把需要的水印文字添加到图片上面。在Uniapp里通过uni.getImageInfo获取图片,然后用uni.createCanvasContext在canvas里将图片和水印文字画在一起,最后通过uni.canvasToTempFilePath导出成图片即可。

但为了节省开发时间,便去插件市场找了一个叫hpy-watermark的插件,并修改了部分源码来匹配需求,修改点和修改后的源码如下:

  1. 有时候图片像素太高,上传和渲染比较慢,通过设置scale值来等比缩放图片;同时scale也用来设置水印文字大小和间距,以匹配不同像素的图片
  2. 当同一个页面有多个地方用到组件时,canvasToTempFilePath方法寻找canvas会出现重复问题,导致图片绘制不了,故加了个index唯一值
  3. 钉钉不支持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版本就可以了

相关推荐
y先森26 分钟前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy26 分钟前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu108301891129 分钟前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿2 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡3 小时前
commitlint校验git提交信息
前端
虾球xz3 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇3 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒3 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员3 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐3 小时前
前端图像处理(一)
前端