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

相关推荐
梦境之冢34 分钟前
axios 常见的content-type、responseType有哪些?
前端·javascript·http
racerun37 分钟前
vue VueResource & axios
前端·javascript·vue.js
m0_548514771 小时前
前端Pako.js 压缩解压库 与 Java 的 zlib 压缩与解压 的互通实现
java·前端·javascript
AndrewPerfect1 小时前
xss csrf怎么预防?
前端·xss·csrf
Calm5501 小时前
Vue3:uv-upload图片上传
前端·vue.js
浮游本尊1 小时前
Nginx配置:如何在一个域名下运行两个网站
前端·javascript
m0_748239831 小时前
前端bug调试
前端·bug
m0_748232921 小时前
[项目][boost搜索引擎#4] cpp-httplib使用 log.hpp 前端 测试及总结
前端·搜索引擎
新中地GIS开发老师1 小时前
《Vue进阶教程》(12)ref的实现详细教程
前端·javascript·vue.js·arcgis·前端框架·地理信息科学·地信
m0_748249541 小时前
前端:base64的作用
前端