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版本就可以了

相关推荐
To_OC1 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic2 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy2 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志2 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble3 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯4 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜5 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司5 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
江华森6 小时前
Web 开发基础:HTTP 与 REST
前端·网络协议·http
IT_陈寒6 小时前
Redis的KEYS命令把我搞崩溃了,改用SCAN才活过来
前端·人工智能·后端