小程序实现摄像头拍照 + 水印绘制

news2024/11/25 6:47:01

文章标题

  • 01 功能说明
  • 02 使用方式 & 效果图
    • 2.1 基础用法
    • 2.2 拍照 + 底部定点水印 + 预览
    • 2.3 拍照 + 整体背景水印 + 预览
  • 03 全部代码
    • 3.1 页面布局 html
    • 3.2 业务核心 js
    • 3.3 基础样式 css

01 功能说明

需求:小程序端需要调用前置摄像头进行拍照,并且将拍好的照片添加水印后返回。下面的代码支持 底部定点水印整体背景水印

技术栈uniappvue

迭代:后期还可以继续 扩展多方位的定点水印 和 支持绘制多句话的背景水印。

02 使用方式 & 效果图

文件路径:"@/components/CameraSnap.vue"

2.1 基础用法

// (1)仅拍照 + 预览
<CameraSnap />

// (4)仅给图片添加水印 + 预览
<CameraSnap 
	photoSrc="xxx" 
	:mark-list="['今天天气很好','2023-01-01 00:00:00']" 
/>

2.2 拍照 + 底部定点水印 + 预览

使用方式:

<CameraSnap 
	:mark-list="['今天天气很好','2023-01-01 00:00:00']" 
	textSize="24" 
	useTextMask 
/>

效果如下:
拍照_底部定点水印_预览

2.3 拍照 + 整体背景水印 + 预览

使用方式:

// 若不设置 markType,则默认为 底部定点水印
// 目前背景水印只会取 markList 的第一项来绘制背景水印
<CameraSnap 
	markType="background" 
	:mark-list="['今天天气很好']" 
	textColor="rgba(255,255,255,0.5)"  
/>

效果如下:
拍照_整体背景水印_预览

03 全部代码

uni-app camera 的官方文档:https://uniapp.dcloud.net.cn/component/camera.html#camera

3.1 页面布局 html

<template>
	<view class="camera-wrapper">
		<!-- 拍照 -->
		<template v-if="!snapSrc">
			<!-- 相机 -->
			<camera device-position="front" flash="off" @error="handleError" class="image-size">
				<view class="photo-btn" @click="handleTakePhoto">拍照</view>
			</camera>
			<!-- 水印 -->
			<canvas canvas-id="photoMarkCanvas" id="photoMarkCanvas" class="mark-canvas"
				:style="{width: canvasWidth+'px',height: canvasHeight+'px'}" />
		</template>
		<!-- 预览 -->
		<template v-else>
			<view class="re-photo-btn" @click="handleRephotograph">重拍</view>
			<image class="image-size" :src="snapSrc"></image>
		</template>
	</view>
</template>

3.2 业务核心 js

<script>
	export default {
		name: 'CameraSnap',
		props: {
			// 照片地址(若传递了照片地址,则默认为预览该照片或添加水印后预览)
			photoSrc: {
				type: String,
				default: ""
			},
			// 水印类型
			markType: {
				type: String,
				default: "fixed", // 定点水印 fixed,背景水印 background
			},
			// 水印文本列表(支持多行)
			markList: {
				type: Array,
				default: () => []
			},
			textColor: {
				type: String,
				default: "#FFFFFF"
			},
			textSize: {
				type: Number,
				default: 32
			},
			// 定点水印的遮罩(为了让水印更清楚)
			useTextMask: {
				type: Boolean,
				default: true
			}
		},
		data() {
			return {
				snapSrc: "",
				canvasWidth: "",
				canvasHeight: "",
			}
		},
		watch: {
			photoSrc: {
				handler: function(newValue, oldValue) {
					if (newValue) {
						this.getWaterMarkImgPath(newValue)
					}
				},
				immediate: true
			}
		},
		methods: {
			handleTakePhoto() {
				const ctx = uni.createCameraContext();
				ctx.takePhoto({
					quality: 'high',
					success: (res) => {
						const imgPath = res.tempImagePath
						if (this.markList.length) {
							this.getWaterMarkImgPath(imgPath)
						} else {
							this.snapSrc = imgPath;
							console.log("default", this.snapSrc)
							this.$emit('complete', imgPath)
						}
					}
				});
			},
			handleRephotograph() {
				this.snapSrc = ""
			},
			handleError(err) {
				uni.showModal({
					title: '警告',
					content: '若不授权使用摄像头,将无法使用拍照功能!',
					cancelText: '不授权',
					confirmText: '授权',
					success: (res) => {
						if (res.confirm) {
							// 允许打开授权页面,调起客户端小程序设置界面,返回用户设置的操作结果
							uni.openSetting({
								success: (res) => {
									res.authSetting = { "scope.camera": true }
								},
							})
						} else if (res.cancel) {
							// 拒绝打开授权页面
							uni.showToast({ title: '您已拒绝授权,无法进行拍照', icon: 'error', duration: 2500 });
						}
					}
				})
			},
			setWaterMark(context, image) {
				const listLength = this.markList?.length
				switch (this.markType) {
					case 'fixed':
						const spacing = 4 // 行间距
						const paddingTopBottom = 20 // 整体上下间距
						// 默认每行的高度 = 字体高度 + 向下间隔
						const lineHeight = this.textSize + spacing
						const allLineHeight = lineHeight * listLength
						// 矩形遮罩的 Y 坐标
						const maskRectY = image.height - allLineHeight
						// 绘制遮罩层
						if (this.useTextMask) {
							context.setFillStyle('rgba(0,0,0,0.4)');
							context.fillRect(0, maskRectY - paddingTopBottom, image.width, allLineHeight + paddingTopBottom)
						}
						// 文本与 x 轴之间的间隔
						const textX = 10
						// 文本一行的最大宽度(减去 20 是为了一行的左右留间隙)
						const maxWidth = image.width - 20
						context.setFillStyle(this.textColor)
						context.setFontSize(this.textSize)
						this.markList.forEach((item, index) => {
							// 因为文本的 Y 坐标是指文本基线的 Y 轴坐标,所以要获取文本顶部的 Y 坐标
							const textY = maskRectY - paddingTopBottom / 2 + this.textSize + lineHeight * index
							context.fillText(item, textX, textY, maxWidth);
						})
						break;
					case 'background':
						context.translate(0, 0);
						context.rotate(30 * Math.PI / 180);
						context.setFillStyle(this.textColor)
						context.setFontSize(this.textSize)
						const colSize = parseInt(image.height / 6)
						const rowSize = parseInt(image.width / 2)
						let x = -rowSize
						let y = -colSize
						// 循环绘制 5 行 6 列 的文字
						for (let i = 1; i <= 6; i++) {
							for (let j = 1; j <= 5; j++) {
								context.fillText(this.markList[0], x, y, rowSize)
								// 每个水印间隔 20
								x += rowSize + 20
							}
							y += colSize
							x = -rowSize
						}
						break;
				}
				context.save();
			},
			getWaterMarkImgPath(src) {
				const _this = this
				uni.getImageInfo({
					src,
					success: (image) => {
						this.canvasWidth = image.width
						this.canvasHeight = image.height
						const context = uni.createCanvasContext("photoMarkCanvas", this)
						context.drawImage(src, 0, 0, image.width, image.height)
						// 设置水印
						this.setWaterMark(context, image)
						// 若还需其他操作,可在操作之后叠加保存:context.restore()
						// 将画布上的图保存为图片
						context.draw(false, () => {
							setTimeout(() => {
								uni.canvasToTempFilePath({
										destWidth: image.width,
										destHeight: image.height,
										canvasId: 'photoMarkCanvas',
										fileType: 'jpg',
										success: function(res) {
											_this.snapSrc = res.tempFilePath
											console.log("water", _this.snapSrc)
											_this.$emit('complete', _this.snapSrc)
										}
									},
									_this
								);
							}, 200)
						});
					}
				})
			},
		}
	}
</script>

3.3 基础样式 css

<style lang="scss" scoped>
	.camera-wrapper {
		position: relative;
	}

	.mark-canvas {
		position: absolute;
		/* 将画布移出展示区域 */
		top: -200vh;
		left: -200vw;
	}

	.image-size {
		width: 100%;
		height: 100vh;
	}

	.photo-btn {
		position: absolute;
		bottom: 100rpx;
		left: 50%;
		transform: translateX(-50%);
		width: 140rpx;
		height: 140rpx;
		line-height: 140rpx;
		text-align: center;
		background-color: #000000;
		border-radius: 50%;
		border: 10rpx solid #ffffff;
		color: #fff;
	}

	.re-photo-btn {
		position: absolute;
		bottom: 80rpx;
		right: 40rpx;
		padding: 10rpx 20rpx;
		background-color: #000000;
		border-radius: 10%;
		border: 6rpx solid #ffffff;
		color: #fff
	}
</style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/989176.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

当面试官问你离职原因的时候怎么回答比较好?

所有的前提都是建立在有一定的物质基础&#xff0c;当你的一日三餐都成了问题&#xff0c;都需要家庭支持的时候我希望你可以找一份工作&#xff0c;靠自己的本事养活自己从来不丢人&#xff0c;我觉得死要面子活受罪才是真的让你看不起。 所有的建议都是建立在我们是普通打工人…

如何用Jmeter编写脚本压测?

随着商业业务不断扩张&#xff0c;调用adsearch服务频率越来越高&#xff0c;所以这次想做个压测&#xff0c;了解目前多少并发量可以到达adsearch服务的界值。 这次选用的jmeter压测工具&#xff0c;压测思路如图&#xff1a; 一、日志入参 日志选取的adsearch 的 getads部分…

电工-什么是电流

什么是电流&#xff1f;电流计算公式和单位换算及电流方向讲解 前面了解到电路的基本组成是包括&#xff1a;电能、负载、导线构成的&#xff0c;而这电路就是电流流通的路径&#xff0c;那么什么是电流呢&#xff1f;下面就讲讲电流形成的基本概念以及电流计算公式、单位和方…

2023-9-8 求组合数(二)

题目链接&#xff1a;求组合数 II #include <iostream> #include <algorithm>using namespace std;typedef long long LL; const int mod 1e9 7; const int N 100010;// 阶乘&#xff0c;阶乘的逆 int fact[N], infact[N];LL qmi(int a, int k, int p) {int res…

HTTPS 之fiddler抓包--jmeter请求

一、浅谈HTTPS 我们都知道HTTP并非是安全传输&#xff0c;在HTTPS基础上使用SSL协议进行加密构成的HTTPS协议是相对安全的。目前越来越多的企业选择使用HTTPS协议与用户进行通信&#xff0c;如百度、谷歌等。HTTPS在传输数据之前需要客户端&#xff08;浏览器&#xff09;与服…

selenium的Chrome116版驱动下载

这里写自定义目录标题 下载地址https://googlechromelabs.github.io/chrome-for-testing/#stable 选择chromedriver 对应的平台和版本 国内下载地址 https://download.csdn.net/download/dongtest/88314387

分享一个Python Django影片数据爬取与数据分析系统源码

&#x1f495;&#x1f495;作者&#xff1a;计算机源码社 &#x1f495;&#x1f495;个人简介&#xff1a;本人七年开发经验&#xff0c;擅长Java、Python、PHP、.NET、微信小程序、爬虫、大数据等&#xff0c;大家有这一块的问题可以一起交流&#xff01; &#x1f495;&…

数据结构——带头双向循环链表

数据结构——带头双向循环链表 一、带头双向循环链表的定义二、带头双向循环链表的实现2.1初始化创建带头双向循环链表的节点2.2申请新节点2.3节点的初始化2.4带头双向循环链表的尾插2.5带头双向循环链表的头插2.6判空函数2.7带头双向循环链表的打印函数2.8带头双向循环链表的尾…

计算机竞赛 基于深度学习的目标检测算法

文章目录 1 简介2 目标检测概念3 目标分类、定位、检测示例4 传统目标检测5 两类目标检测算法5.1 相关研究5.1.1 选择性搜索5.1.2 OverFeat 5.2 基于区域提名的方法5.2.1 R-CNN5.2.2 SPP-net5.2.3 Fast R-CNN 5.3 端到端的方法YOLOSSD 6 人体检测结果7 最后 1 简介 &#x1f5…

OpenCV 04(通道分离与合并 | 绘制图形)

一、通道的分离与合并 - split(mat)分割图像的通道 - merge((ch1,ch2, ch3)) 融合多个通道 import cv2 import numpy as npimg np.zeros((480, 640, 3), np.uint8)b,g,r cv2.split(img)b[10:100, 10:100] 255 g[10:100, 10:100] 255img2 cv2.merge((b, g, r))cv2.imshow…

《TCP/IP网络编程》阅读笔记--并发多进程服务端的使用

1--并发服务器端 并发服务器端主要有以下三类&#xff1a; ① 多进程服务器&#xff1a;通过创建多个进程提供服务&#xff1b; ② 多路复用服务器&#xff1a;通过捆绑并统一管理I/O对象提供服务&#xff1b; ③ 多线程服务器&#xff1a;通过生成与客户端等量的线程提供服务&…

C/C++ ——内存管理

前言 为什么要研究内存管理&#xff1f; (1)程序员写的程序可以分为动态和静态两种状态。静态&#xff1a;就是程序被存放在ROM中&#xff0c;也就是磁盘、固态硬盘、eMMC等存储介质&#xff1b;动态&#xff1a;程序被执行&#xff0c;此时程序在RAM内存中运行&#xff1b; (…

图床项目数据库表设计

一、表设计 share_picture_list 和 share_file_list 类似&#xff0c;只是 share_picture_list 只存储共享图片相关的信息&#xff0c;及分享给未注册用户看的。share_file_list 是存储共享文件&#xff08;包括图片文件&#xff09;相关的信息&#xff0c;分享给已注册用户的。…

【数据结构】 七大排序详解(贰)——冒泡排序、快速排序、归并排序

文章目录 ⚽冒泡排序⚾算法步骤&#x1f3a8;算法优化&#x1f94e;代码实现&#xff1a;&#x1f3c0;冒泡排序的特性总结 &#x1f9ed;快速排序⚽算法思路&#x1f4cc;思路一&#xff08;Hoare版&#xff09;&#x1f4cc;思路二&#xff08;挖坑法&#xff09;&#x1f4c…

PCL入门(四):kdtree简单介绍和使用

目录 1. kd树的意义2. kd树的使用 参考博客《欧式聚类&#xff08;KD-Tree&#xff09;详解&#xff0c;保姆级教程》和《(三分钟)学会kd-tree 激光SLAM点云搜索常见》 1. kd树的意义 kd树是什么&#xff1f; kd树是一种空间划分的数据结构&#xff0c;对于多个维度的数据&a…

小米汽车,能否在新能源汽车江湖站稳脚跟?

最近&#xff0c;圈内都在传小米汽车亦庄工厂已试生产近一个月&#xff0c;每周可产50辆样车&#xff0c;正在为首款新能源汽车量产做最后的准备。 此前的业绩交流会上&#xff0c;小米集团总裁卢伟冰透露&#xff0c;小米汽车结束了夏测且进展非常顺利&#xff0c;2024年上半…

计算机竞赛 基于深度学习的植物识别算法 - cnn opencv python

文章目录 0 前言1 课题背景2 具体实现3 数据收集和处理3 MobileNetV2网络4 损失函数softmax 交叉熵4.1 softmax函数4.2 交叉熵损失函数 5 优化器SGD6 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; **基于深度学习的植物识别算法 ** …

数仓学习之DWD学习

登录不是原子行为&#xff0c;而登录成功是&#xff0c; 而支付也不是原子&#xff0c;也分成功失败。 什么是原子型? 一条线 注意一下密码, mysql的密码是MD5加密,而MD5的长度为固定的32 1.怎么构建事务表? 1.确定表名 2.确定一行数据所表示的含义 3.确定列定义 4.确定度…

【算法训练笔记】栈的OJ题

&#x1f525;&#x1f525; 欢迎来到小林的博客&#xff01;&#xff01;       &#x1f6f0;️博客主页&#xff1a;✈️林 子       &#x1f6f0;️博客专栏&#xff1a;✈️ 小林的算法训练笔记       &#x1f6f0;️社区 :✈️ 进步学堂       …

rsa加密解密java和C#互通

前言 因为第三方项目是java的案例&#xff0c;但是原来的项目使用的是java&#xff0c;故需要将java代码转化为C#代码&#xff0c;其中核心代码就是RSA加密以及加签和验签&#xff0c;其他的都是api接口请求难度不大。 遇到的问题 java和c#密钥格式不一致&#xff0c;java使…