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

相关推荐
糕冷小美n4 小时前
elementuivue2表格不覆盖整个表格添加固定属性
前端·javascript·elementui
小哥不太逍遥4 小时前
Technical Report 2024
java·服务器·前端
沐墨染4 小时前
黑词分析与可疑对话挖掘组件的设计与实现
前端·elementui·数据挖掘·数据分析·vue·visual studio code
anOnion4 小时前
构建无障碍组件之Disclosure Pattern
前端·html·交互设计
threerocks5 小时前
前端将死,Agent 永生
前端·人工智能·ai编程
问道飞鱼5 小时前
【前端知识】Vite用法从入门到实战
前端·vite·项目构建
爱上妖精的尾巴5 小时前
8-10 WPS JSA 正则表达式:贪婪匹配
服务器·前端·javascript·正则表达式·wps·jsa
Aliex_git7 小时前
浏览器 API 兼容性解决方案
前端·笔记·学习
独泪了无痕7 小时前
useStorage:本地数据持久化利器
前端·vue.js
程序员林北北7 小时前
【前端进阶之旅】JavaScript 一些常用的简写技巧
开发语言·前端·javascript