uniapp APP端水印相机实现
使用插件https://ext.dcloud.net.cn/plugin?id=4892
在插件功能上增加定位,定时功能,水印相机页面每十秒重新获取一次地址,时间,增加水印生canvas文本多行换行功能
由于相机组件app不支持,所以插件使用 live-pusher 直播推流 组件实现的自定义相机功能。
拍照页面使用nvue,可以实现应用内拍照以及拍照画面自定义元素等功能。
水印相机页watermarkCamera.nvue
<template> <view class="live-camera" :style="{ width: windowWidth, height: windowHeight }"> <view class="preview" :style="{ width: windowWidth, height: windowHeight}"> <live-pusher id="livePusher" ref="livePusher" class="livePusher" mode="FHD" beauty="0" whiteness="0" :aspect="aspect" min-bitrate="1000" audio-quality="16KHz" device-position="back" auto-focus="false" muted="true" :enable-camera="true" :enable-mic="false" :zoom="false" @statechange="statechange" :style="{ width: windowWidth, height: windowHeight }"></live-pusher> <!--提示语--> <cover-view class="remind"> <text class="remind-text remind-name" style="">{{ username }}</text> <text class="remind-text remind-address" style="">{{ address }}</text> <text class="remind-text remind-time" style="">{{ time }}</text> </cover-view> </view> <view class="menu"> <!--底部菜单区域背景--> <cover-image class="menu-mask" src="@/static/camera/bar.png"></cover-image> <!--返回键--> <cover-image class="menu-back" @tap="back" src="@/static/camera/back.png"></cover-image> <!--快门键--> <cover-image class="menu-snapshot" @tap="snapshot" src="@/static/camera/shutter.png"></cover-image> <!--反转键--> <cover-image class="menu-flip" @tap="flip" src="@/static/camera/flip.png"></cover-image> </view> </view> </template> <script> let _this = null; export default { data() { return { dotype: 'watermark', message: 'live-camer', //水印内容 username: uni.getStorageSync('loginUserName'), address: '无法获取地址', time: '2022-2-14 10:23', poenCarmeInterval: null, //打开相机的轮询 aspect: '2:3', //比例 windowWidth: '', //屏幕可用宽度 windowHeight: '', //屏幕可用高度 camerastate: false, //相机准备好了 livePusher: null, //流视频对象 snapshotsrc: null ,//快照, timer:null,//定时器 }; }, onLoad(e) { _this = this; if (e.dotype != undefined) this.dotype = e.dotype; this.initCamera(); }, onReady() { this.getAddress(); let date = new Date() this.time= this.dateFormat("YYYY-mm-dd HH:MM", date); this.livePusher = uni.createLivePusherContext('livePusher', this); this.startPreview(); //开启预览并设置摄像头 this.poenCarme(); }, onShow() { clearInterval(this.timer) // 每隔10秒刷新地址和时间 this.timer = setInterval(()=>{ this.getAddress(); let date = new Date() this.time= this.dateFormat("YYYY-mm-dd HH:MM", date); },10000); }, onUnload(){ clearInterval(this.timer) }, methods: { getAddress(){ uni.getLocation({ type: 'gcj02', geocode: true, isHighAccuracy:true, success:(res) => { this.address = res.address.province+res.address.city+res.address.district+res.address.street+res.address.streetNum+res.address.poiName; console.log('当前位置:' , this.address); console.log('当前位置的经度:' + res.longitude); console.log('当前位置的纬度:' + res.latitude); } }); }, //轮询打开 poenCarme() { //#ifdef APP-PLUS if (plus.os.name == 'Android') { this.poenCarmeInterval = setInterval(function() { console.log(_this.camerastate); if (!_this.camerastate) _this.startPreview(); }, 2500); } //#endif }, //初始化相机 initCamera() { uni.getSystemInfo({ success: function(res) { _this.windowWidth = res.windowWidth; _this.windowHeight = res.windowHeight; let zcs = _this.aliquot(_this.windowWidth, _this.windowHeight); _this.aspect = _this.windowWidth / zcs + ':' + _this.windowHeight / zcs; console.log('画面比例:' + _this.aspect); } }); }, //整除数计算 aliquot(x, y) { if (x % y == 0) return y; return this.aliquot(y, x % y); }, //开始预览 startPreview() { this.livePusher.startPreview({ success: a => { console.log(a); } }); }, //停止预览 stopPreview() { this.livePusher.stopPreview({ success: a => { _this.camerastate = false; //标记相机未启动 } }); }, //状态 statechange(e) { //状态改变 console.log(e); if (e.detail.code == 1007) { _this.camerastate = true; } else if (e.detail.code == -1301) { _this.camerastate = false; } }, //返回 back() { uni.navigateBack(); }, //抓拍 snapshot() { this.livePusher.snapshot({ success: e => { _this.snapshotsrc = e.message.tempImagePath; _this.stopPreview(); _this.setImage(); uni.navigateBack(); } }); }, //反转 flip() { this.livePusher.switchCamera(); }, //设置 setImage() { let pages = getCurrentPages(); let prevPage = pages[pages.length - 2]; //上一个页面 //直接调用上一个页面的setImage()方法,把数据存到上一个页面中去 prevPage.$vm.watermark({ path: _this.snapshotsrc, info: { username: this.username, address: this.address, time: this.time } }); }, dateFormat(fmt, date) { let ret; const opt = { "Y+": date.getFullYear().toString(), // 年 "m+": (date.getMonth() + 1).toString(), // 月 "d+": date.getDate().toString(), // 日 "H+": date.getHours().toString(), // 时 "M+": date.getMinutes().toString(), // 分 "S+": date.getSeconds().toString() // 秒 // 有其他格式化字符需求可以继续添加,必须转化成字符串 }; for (let k in opt) { ret = new RegExp("(" + k + ")").exec(fmt); if (ret) { fmt = fmt.replace(ret[1], (ret[1].length == 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, "0"))) }; }; return fmt; }, } }; </script> <style lang="less"> .live-camera { justify-content: center; align-items: center; } .preview { justify-content: center; align-items: center; } .remind { position: absolute; top: 80rpx; left: 20rpx; z-index: 100; } .remind-text { color: #dddddd; width: 710rpx; } .remind-name{ font-size: 40rpx; } .remind-address{ font-size: 36rpx; } .remind-time{ font-size: 30rpx; } .menu { position: absolute; left: 0; bottom: 0; width: 750rpx; height: 180rpx; z-index: 98; align-items: center; justify-content: center; } .menu-mask { position: absolute; left: 0; bottom: 0; width: 750rpx; height: 180rpx; z-index: 98; } .menu-back { position: absolute; left: 30rpx; bottom: 50rpx; width: 80rpx; height: 80rpx; z-index: 99; align-items: center; justify-content: center; } .menu-snapshot { width: 130rpx; height: 130rpx; z-index: 99; } .menu-flip { position: absolute; right: 30rpx; bottom: 50rpx; width: 80rpx; height: 80rpx; z-index: 99; align-items: center; justify-content: center; } </style>
相片展示和水印生成页index.vue
<template> <view class="page"> <nav-bar :background="'#0042b8'" :is-back="true" title="测试"></nav-bar> <view style="height: 80rpx;"></view> <navigator class="buttons" url="./watermarkCamera"><button type="primary">打开定制水印相机</button></navigator> <view style="height: 80rpx;"></view> <view>拍摄结果预览图,见下方</view> <view class="img-list"> <view class="img-item" v-for="(item,index) in imgList" :key="index" @click="lookImg(index)"> <image :src="item"></image> </view> </view> <canvas id="canvas-clipper" canvas-id="canvas-clipper" type="2d" :style="{width: canvasSiz.width+'px',height: canvasSiz.height+'px',position: 'absolute',left:'-500000px',top: '-500000px'}" /> </view> </template> <script> var _this; export default { data() { return { windowWidth:'', windowHeight:'', imagesrc: null, imgList:[], canvasSiz:{ width:188, height:273 } }; }, onLoad() { _this= this; this.init(); }, methods: { //添加照片水印 watermark(info){ console.log("获取到的数据为",info) uni.getImageInfo({ src: info.path, success: function(image) { console.log(image); _this.canvasSiz.width =image.width; _this.canvasSiz.height =image.height; let maxWidth = image.width - 60; console.log("获取最大宽度",maxWidth) //担心尺寸重置后还没生效,故做延迟 setTimeout(()=>{ let ctx = uni.createCanvasContext('canvas-clipper', _this); ctx.drawImage( info.path, 0, 0, image.width, image.height ); //具体位置如需和相机页面上一致还需另外做计算,此处仅做大致演示 ctx.setFillStyle('white'); ctx.setFontSize(50); ctx.fillText(info.info.username, 20, 150); ctx.setFontSize(50); let previousRowHeight = _this.textPrewrap(ctx,info.info.address,20,220,70,maxWidth,3); //再来加个时间水印 ctx.setFontSize(40); ctx.fillText(info.info.time, 20, previousRowHeight+70); ctx.draw(false, () => { uni.canvasToTempFilePath( { destWidth: image.width, destHeight: image.height, canvasId: 'canvas-clipper', fileType: 'jpg', success: function(res) { _this.savePhoto(res.tempFilePath); } }, _this ); }); },500) } }); }, /** ctx: 画布的上下文环境 content: 需要绘制的文本内容 drawX: 绘制文本的x坐标 drawY: 绘制文本的y坐标 lineHeight:文本之间的行高 lineMaxWidth:每行文本的最大宽度 lineNum:最多绘制的行数 */ textPrewrap(ctx, content, drawX, drawY, lineHeight, lineMaxWidth, lineNum) { var drawTxt = ''; // 当前绘制的内容 var drawLine = 1; // 第几行开始绘制 var drawIndex = 0; // 当前绘制内容的索引 // 判断内容是否可以一行绘制完毕 if (ctx.measureText(content).width <= lineMaxWidth) { ctx.fillText(content.substring(drawIndex, i), drawX, drawY); } else { for (var i = 0; i < content.length; i++) { drawTxt += content[i]; if (ctx.measureText(drawTxt).width >= lineMaxWidth) { if (drawLine >= lineNum) { ctx.fillText(content.substring(drawIndex, i) + '..', drawX, drawY); break; } else { ctx.fillText(content.substring(drawIndex, i + 1), drawX, drawY); drawIndex = i + 1; drawLine += 1; drawY += lineHeight; drawTxt = ''; } } else { // 内容绘制完毕,但是剩下的内容宽度不到lineMaxWidth if (i === content.length - 1) { ctx.fillText(content.substring(drawIndex), drawX, drawY); return drawY; console.log("最后高度为",drawY); } } } } }, //保存图片到相册,方便核查 savePhoto(path){ this.imgList.push(path) // this.imagesrc = path; //保存到相册 // uni.saveImageToPhotosAlbum({ // filePath: path, // success: () => { // uni.showToast({ // title: '已保存至相册', // duration: 2000 // }); // } // }); }, lookImg(index){ // 预览图片 uni.previewImage({ current:index, urls: this.imgList, }); }, //初始化 init(){ let _this = this; uni.getSystemInfo({ success: function(res) { _this.windowWidth = res.windowWidth; _this.windowHeight = res.windowHeight; } }); } } }; </script> <style lang="less"> .page { width: 750rpx; justify-content: center; align-items: center; flex-direction:column; display: flex; .buttons { width: 600rpx; } } .img-list{ padding: 20rpx; display: flex; align-items: center; justify-content: flex-start; flex-wrap: wrap; } .img-item{ width: 100rpx; height: 100rpx; margin-right: 20rpx; margin-bottom: 20rpx; } .img-item image{ width: 100%; height: 100%; } </style>
canvas文本换行方法:https://segmentfault.com/a/1190000017869922