uniapp微信小程序实现拍照加水印,水印上添加当前时间,当前地点等信息,地点逆解析使用的是高德地图

uniapp微信小程序实现拍照加水印,水印上添加当前时间,当前地点等信息

javascript 复制代码
<template>
	<view class="photo-container">
		<!-- 照片预览 -->
		<view class="preview-wrap" v-if="previewImg">
			<image :src="previewImg" mode="widthFix" class="preview-img"></image>
			<!-- 重置按钮 -->
			<button class="reset-btn" type="default" @click="resetPhoto">
				重新拍照
			</button>
		</view>

		<!-- 自定义水印内容输入框 -->
		<view class="custom-input-wrap" v-if="!previewImg || showInputAfterReset">
			<input v-model="customWatermark" placeholder="输入水印备注内容(选填)" class="custom-input" />
		</view>

		<!-- 操作按钮:仅无预览图时显示拍照按钮 -->
		<button class="photo-btn" type="primary" @click="takePhotoWithWatermark" :loading="isLoading"
			v-if="!previewImg">
			{{ isLoading ? '处理中...' : '拍照添加水印' }}
		</button>

		<!-- 隐藏的Canvas(用于绘制水印) -->
		<canvas canvas-id="watermarkCanvas" class="hidden-canvas"
			:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"></canvas>
	</view>
</template>

<script>
	export default {
		name: 'PhotoWatermark',
		data() {
			return {
				previewImg: '', // 带水印的预览图
				isLoading: false, // 按钮加载状态
				canvasWidth: 0, // Canvas宽度
				canvasHeight: 0, // Canvas高度
				currentAddress: '', // 解析后的地址
				amapWX: null, // 高德SDK实例
				customWatermark: '', // 自定义水印内容
				showInputAfterReset: false, // 重置后是否显示输入框
				// 水印样式配置
				watermarkStyle: {
					fixedWidth: 240, // 水印固定宽度(px)
					fontSize: 16, // 字体大小
					lineHeight: 20, // 行高
					padding: 10, // 背景框内边距
					bgColor: 'rgba(0,26,171,0.6)', // 背景色(纯黑)
					textColor: '#ffffff', // 文字色(纯白)
					borderColor: 'rgba(255,255,255,0.8)', // 背景边框色
					borderWidth: 1, // 背景边框宽度
					marginRight: 15, // 右间距
					marginBottom: 15 // 下间距
				},
				// 压缩配置
				compressConfig: {
					quality: 0.8, // 图片压缩质量(0-1,0.8兼顾质量和大小)
					maxWidth: 1920, // 压缩后最大宽度(适配手机屏幕)
					maxHeight: 1080 // 压缩后最大高度
				}
			};
		},
		onLoad() {

			// 2. 提前检查权限
			this.checkPermissions();
		},
		methods: {
			/**
			 * 重置方法(清空预览、恢复初始状态,支持反复拍照)
			 */
			resetPhoto() {
				// 清空预览图
				this.previewImg = '';
				// 保留自定义输入内容(可选:也可清空 this.customWatermark = '')
				this.showInputAfterReset = true;
				// 重置Canvas尺寸(避免残留)
				this.canvasWidth = 0;
				this.canvasHeight = 0;
				// 隐藏加载状态
				this.isLoading = false;
				// 提示(可选)
				uni.showToast({
					title: '已重置,可重新拍照',
					icon: 'none',
					duration: 1500
				});
			},

			/**
			 * 1. 检查相机/定位权限(uniapp方式)
			 */
			async checkPermissions() {
				try {
					// 检查相机权限
					const cameraAuth = await uni.getSetting();
					if (!cameraAuth.authSetting['scope.camera']) {
						const cameraRes = await uni.authorize({
							scope: 'scope.camera'
						});
						if (!cameraRes) throw new Error('相机权限申请失败');
					}

					// 检查定位权限
					const locationAuth = await uni.getSetting();
					if (!locationAuth.authSetting['scope.userLocation']) {
						const locationRes = await uni.authorize({
							scope: 'scope.userLocation'
						});
						if (!locationRes) throw new Error('定位权限申请失败');
					}
				} catch (err) {
					uni.showToast({
						title: err.message,
						icon: 'none'
					});
				}
			},

			/**
			 * 新增:图片压缩方法(返回压缩后的临时路径)
			 * @param {String} tempPath - 原始图片临时路径
			 * @returns {String} 压缩后的临时路径
			 */
			async compressImage(tempPath) {
				return new Promise((resolve, reject) => {
					uni.compressImage({
						src: tempPath, // 原始图片路径
						quality: this.compressConfig.quality, // 压缩质量
						width: this.compressConfig.maxWidth, // 压缩宽度
						height: this.compressConfig.maxHeight, // 压缩高度
						success: (res) => {
							resolve(res.tempFilePath); // 返回压缩后的路径
						},
						fail: (err) => {
							// 压缩失败则返回原始路径
							console.warn('图片压缩失败,使用原始图片:', err);
							resolve(tempPath);
						}
					});
				});
			},

			/**
			 * 2. 获取定位(uniapp方式)+ 高德逆解析地址
			 */
			async getLocationAndAddress() {
				let than = this
				return new Promise((resolve, reject) => {
					// uniapp获取定位坐标(GCJ02坐标系,适配高德)
					uni.getLocation({
						type: 'gcj02', // 高德兼容的坐标系
						success: (locRes) => {
							// 高德SDK逆解析地址
							let key = '你的高德地图key'
							uni.request({
								header: {
									'Content-Type': 'application/text',
								},
								//注意:这里的key值需要高德地图的 web服务生成的key  只有web服务才有逆地理编码
								url: `https://restapi.amap.com/v3/geocode/regeo?output=json&location=${locRes.longitude},${locRes.latitude}&key=${key}`,
								success(res) {
									than.currentAddress = res.data.regeocode
										.formatted_address || ''
									console.log(res, "获取位置成功信息xxxxxxxxxxxxx",res.data.regeocode
										.formatted_address)
									resolve();
								},
								fail(err) {
									than.currentAddress = `(${locRes.latitude}, ${locRes.longitude})`;
									resolve();
									console.log(err, "获取位置失败信息")
								}
							})
						},
						fail: (err) => {
							reject(`定位获取失败:${err.errMsg}`);
						}
					});
				});
			},

			/**
			 * 3. 文字换行处理(固定宽度内自动换行)
			 */
			textWrapInFixedWidth(ctx, text) {
				const maxTextWidth = this.watermarkStyle.fixedWidth - this.watermarkStyle.padding * 2;
				const textArr = [];
				let tempText = '';

				for (let i = 0; i < text.length; i++) {
					const char = text.charAt(i);
					const tempWidth = ctx.measureText(tempText + char).width;
					if (tempWidth > maxTextWidth) {
						textArr.push(tempText);
						tempText = char;
					} else {
						tempText += char;
					}
				}
				if (tempText) textArr.push(tempText);
				return textArr;
			},

			/**
			 * 4. 适配图片旋转角度,修正Canvas绘制尺寸和坐标
			 * @param {Object} imgInfo - 图片信息(getImageInfo返回)
			 * @returns {Object} 修正后的宽高和绘制参数
			 */
			adaptImageRotation(imgInfo) {
				let drawWidth = imgInfo.width;
				let drawHeight = imgInfo.height;
				let rotate = 0;

				// 处理图片旋转(常见:横拍时rotate为90/270度)
				if (imgInfo.orientation && [1, 3, 6, 8].includes(imgInfo.orientation)) {
					switch (imgInfo.orientation) {
						case 3: // 旋转180度
							rotate = 180;
							break;
						case 6: // 旋转90度(横拍竖显)
							rotate = 90;
							[drawWidth, drawHeight] = [drawHeight, drawWidth]; // 宽高互换
							break;
						case 8: // 旋转270度
							rotate = 270;
							[drawWidth, drawHeight] = [drawHeight, drawWidth]; // 宽高互换
							break;
						default:
							rotate = 0;
					}
				}

				return {
					drawWidth,
					drawHeight,
					rotate
				};
			},

			/**
			 * 5. 核心:拍照 + 压缩 + 加水印 + 加载提示
			 */
			async takePhotoWithWatermark() {
				if (this.isLoading) return;
				this.isLoading = true;
				// 全局加载提示(覆盖整个绘制过程)
				uni.showLoading({
					title: '处理中...',
					mask: true // 遮罩层,防止用户操作
				});

				try {
					// 步骤1:获取定位和地址
					await this.getLocationAndAddress();

					// 步骤2:uniapp拍照(仅相机)
					const photoRes = await uni.chooseImage({
						count: 1,
						sizeType: ['compressed'], // 先取原图,后续手动压缩
						sourceType: ['camera']
					});
					const originalTempPath = photoRes.tempFilePaths[0];

					// 步骤3:压缩图片(核心优化)
					const compressedTempPath = await this.compressImage(originalTempPath);

					// 步骤4:获取压缩后图片信息(包含旋转角度)
					const imgInfo = await uni.getImageInfo({
						src: compressedTempPath
					});
					// 适配图片旋转,修正绘制尺寸
					const {
						drawWidth,
						drawHeight,
						rotate
					} = this.adaptImageRotation(imgInfo);
					this.canvasWidth = drawWidth;
					this.canvasHeight = drawHeight;

					// 步骤5:Canvas绘制(核心:适配旋转,保证水印在右下角)
					const ctx = uni.createCanvasContext('watermarkCanvas', this);

					// ========== 关键:处理图片旋转,保证原图正常显示 ==========
					ctx.save(); // 保存当前画布状态
					// 平移画布原点到中心
					ctx.translate(drawWidth / 2, drawHeight / 2);
					// 旋转画布(适配图片旋转角度)
					if (rotate !== 0) {
						ctx.rotate((rotate * Math.PI) / 180);
					}
					// 绘制压缩后的照片(修正旋转后的位置)
					ctx.drawImage(compressedTempPath, -drawWidth / 2, -drawHeight / 2, drawWidth, drawHeight);
					ctx.restore(); // 恢复画布状态(避免影响水印绘制)

					// ========== 水印绘制(适配旋转后的画布尺寸) ==========
					// 准备水印内容
					const now = new Date();
					const timeStr =
						`${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')} ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`;
					const timeText = `时间:${timeStr}`;
					const addressText = `位置:${this.currentAddress}`;
					const customText = this.customWatermark.trim() ? `备注:${this.customWatermark.trim()}` : '';
					const projectText = `项目:测试项目xxxxxxxxxxxx`;

					// 计算每类内容的换行数组(先设置字体大小)
					ctx.setFontSize(this.watermarkStyle.fontSize);
					const timeLines = this.textWrapInFixedWidth(ctx, timeText);
					const addressLines = this.textWrapInFixedWidth(ctx, addressText);
					const customLines = customText ? this.textWrapInFixedWidth(ctx, customText) : [];
					const projectLines = this.textWrapInFixedWidth(ctx, projectText);
					

					// 计算水印总高度
					const totalLines = timeLines.length + addressLines.length + customLines.length + projectLines.length;
					const totalHeight = totalLines * this.watermarkStyle.lineHeight + this.watermarkStyle.padding * 2;

					// 计算右下角坐标(基于修正后的画布尺寸)
					const bgStartX = drawWidth - this.watermarkStyle.fixedWidth - this.watermarkStyle.marginRight;
					const bgStartY = drawHeight - totalHeight - this.watermarkStyle.marginBottom;

					// 1. 绘制水印背景(中间层)
					ctx.setFillStyle(this.watermarkStyle.bgColor);
					ctx.fillRect(bgStartX, bgStartY, this.watermarkStyle.fixedWidth, totalHeight);

					// 可选:绘制背景边框
					if (this.watermarkStyle.borderWidth > 0) {
						ctx.setStrokeStyle(this.watermarkStyle.borderColor);
						ctx.setLineWidth(this.watermarkStyle.borderWidth);
						ctx.strokeRect(bgStartX, bgStartY, this.watermarkStyle.fixedWidth, totalHeight);
					}

					// 2. 绘制水印文字(最上层)
					ctx.setFillStyle(this.watermarkStyle.textColor);
					ctx.setTextAlign('left');
					ctx.setTextBaseline('top');

					let currentY = bgStartY + this.watermarkStyle.padding;
					const textStartX = bgStartX + this.watermarkStyle.padding;

					// 绘制时间行
					timeLines.forEach(line => {
						ctx.fillText(line, textStartX, currentY);
						currentY += this.watermarkStyle.lineHeight;
					});
					// 绘制地点行
					addressLines.forEach(line => {
						ctx.fillText(line, textStartX, currentY);
						currentY += this.watermarkStyle.lineHeight;
					});
					// 绘制项目行
					projectLines.forEach(line => {
						ctx.fillText(line, textStartX, currentY);
						currentY += this.watermarkStyle.lineHeight;
					});
					// 绘制自定义行
					customLines.forEach(line => {
						ctx.fillText(line, textStartX, currentY);
						currentY += this.watermarkStyle.lineHeight;
					});
					

					// 执行绘制(异步,需等待绘制完成)
					await new Promise((resolve) => {
						ctx.draw(false, resolve); // 绘制完成后resolve
					});

					// 步骤6:导出Canvas为压缩后的临时图片
					const canvasTempRes = await new Promise((resolve, reject) => {
						uni.canvasToTempFilePath({
							canvasId: 'watermarkCanvas',
							width: drawWidth,
							height: drawHeight,
							destWidth: drawWidth,
							destHeight: drawHeight,
							quality: this.compressConfig.quality, // 导出时也压缩
							success: resolve,
							fail: (err) => reject(`Canvas导出失败:${err.errMsg}`)
						}, this);
					});

					// 步骤7:保存压缩后的带水印图片到相册
					await uni.saveImageToPhotosAlbum({
						filePath: canvasTempRes.tempFilePath
					});

					// 步骤8:显示预览
					this.previewImg = canvasTempRes.tempFilePath;
					this.showInputAfterReset = false;
					uni.showToast({
						title: '拍照加水印成功',
						icon: 'success'
					});
				} catch (err) {
					uni.showToast({
						title: err.message || '操作失败',
						icon: 'none'
					});
				} finally {
					this.isLoading = false;
					// 关闭全局加载提示
					uni.hideLoading();
				}
			}
		}
	};
</script>

<style scoped>
	.photo-container {
		padding: 30rpx;
		display: flex;
		flex-direction: column;
		align-items: center;
		background-color: #f5f5f5;
		min-height: 100vh;
	}

	.preview-wrap {
		width: 100%;
		margin-bottom: 20rpx;
		background-color: #fff;
		padding: 10rpx;
		border-radius: 10rpx;
		display: flex;
		flex-direction: column;
		align-items: center;
	}

	.preview-img {
		width: 100%;
		border-radius: 8rpx;
		margin-bottom: 15rpx;
	}

	/* 重置按钮样式 */
	.reset-btn {
		width: 60%;
		height: 70rpx;
		line-height: 70rpx;
		font-size: 28rpx;
		background-color: #f0f0f0;
		color: #333;
		margin-bottom: 10rpx;
	}

	.custom-input-wrap {
		width: 100%;
		margin-bottom: 20rpx;
	}

	.custom-input {
		width: 100%;
		padding: 20rpx;
		background-color: #fff;
		border-radius: 10rpx;
		font-size: 28rpx;
		border: 1px solid #eee;
	}

	.photo-btn {
		width: 100%;
		height: 88rpx;
		line-height: 88rpx;
		font-size: 32rpx;
		border-radius: 44rpx;
	}

	.hidden-canvas {
		position: fixed;
		top: -9999rpx;
		left: -9999rpx;
		z-index: -1;
	}
</style>

横板竖版拍照都支持

相关推荐
天呐草莓2 小时前
企业微信运维手册
java·运维·网络·python·微信小程序·企业微信·微信开放平台
鲁Q同志2 小时前
微信小程序树形选择组件
微信小程序·小程序
前端小雪的博客.3 小时前
uniapp小程序顶部状态栏占位和自定义头部导航栏
小程序·uni-app
kdniao13 小时前
问答FQA|快递鸟对接系统/小程序常见问题解答产品篇(一)
大数据·小程序
2503_928411564 小时前
12.23 page页面的逻辑
前端·小程序
我叫逢13 小时前
一键去水印实战已上线!心得~
微信小程序·php·去水印
qq_124987075316 小时前
基于微信小程序的电子元器件商城(源码+论文+部署+安装)
java·spring boot·spring·微信小程序·小程序·毕业设计
weixin_lynhgworld20 小时前
科技赋能医疗,陪诊小程序开启就医新体验
科技·小程序
2501_9160074721 小时前
iOS 证书如何创建,从能生成到能长期使用
android·macos·ios·小程序·uni-app·cocoa·iphone