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>


横板竖版拍照都支持