1、在wxml中添加canvas
<canvas
id="watermarkCanvas"
type="2d"
style="position: absolute; top: -10000rpx; width:300px; height: 300px;"
></canvas>
2、引入水印工具类,添加水印
demo水印以时间和经纬度为例,获取经纬度需要在配置app.json做如下配置:
"requiredPrivateInfos": [
"getLocation"
],
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于小程序拍照水印功能"
}
},
添加水印:
const watermark = require('../../../utils/watermark.js'); // 引入水印工具类
/**
* 添加水印
* @param {*} cameraPath
*/
async addWaterMarker(cameraPath){
let that=this
try {
// 1. 先检查并获取位置授权
let locationData;
try {
locationData = await watermark.getCurrentLocation();
} catch (error) {
if (error.message === 'USER_AUTH_DENIED') {
wx.showToast({
title: '获取位置失败',
icon: 'none',
duration: 2000
});
return; // 关键:直接return,阻止后续所有操作
}
throw error; // 其他错误正常抛出
}
wx.showLoading({
title: '添加水印中...'
});
// 3. 添加水印,watermarkedPath 为添加完水印后的图片
const watermarkedPath = await watermark.addWatermark({
imagePath: cameraPath,
longitude: locationData.longitude,
latitude: locationData.latitude
});
wx.hideLoading()
} catch (error) {
wx.hideLoading()
// 对用户取消授权的情况不做错误提示,避免打扰
if (error.message !== 'USER_CANCELED_AUTH') {
wx.showToast({
title: '处理失败',
icon: 'none'
});
}
}
},
3、水印工具类
// utils/watermark.js - 完整的水印工具类
class Watermark {
/**
* 为图片添加包含经纬度信息的水印
* @param {Object} options 配置参数
* @param {string} options.imagePath 图片临时路径
* @param {string} options.longitude 经度
* @param {string} options.latitude 纬度
* @param {string} options.userName 用户姓名
* @param {string} options.address 地址信息(可选)
* @param {string} options.canvasId 画布ID,默认'canvas'
* @param {number} options.quality 图片质量,0-1,默认0.8
* @returns {Promise<string>} 带水印的图片临时路径
*/
static async addWatermark(options) {
const {
imagePath,
longitude,
latitude,
userName = '',
address = '',
canvasId = 'canvas',
quality = 0.8
} = options;
try {
// 1. 获取图片原始尺寸信息
const imgInfo = await this.getImageInfo(imagePath);
const originalWidth = imgInfo.width;
const originalHeight = imgInfo.height;
// 2. 初始化Canvas上下文(使用新版Canvas 2D API)[7](@ref)
const { ctx, canvas } = await this.initCanvasContext(canvasId, originalWidth, originalHeight);
// 3. 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 4. 绘制原始图片(按原始尺寸1:1绘制)
const image = await this.loadImage(imagePath);
ctx.drawImage(image, 0, 0, originalWidth, originalHeight);
// 5. 添加水印文字
this.drawWatermarkText(ctx, originalWidth, originalHeight, { longitude, latitude, userName, address });
// 6. 导出图片(保持原始尺寸)[3](@ref)
return await this.exportCanvasImage(canvas, originalWidth, originalHeight, quality);
} catch (error) {
console.error('添加水印失败:', error);
throw new Error(`水印处理失败: ${error.message}`);
}
}
/**
* 初始化Canvas上下文(动态设置宽高)
*/
static initCanvasContext(canvasId, imgWidth, imgHeight) {
return new Promise((resolve, reject) => {
wx.createSelectorQuery()
.select(`#${canvasId}`)
.fields({ node: true, size: true })
.exec((res) => {
if (!res[0] || !res[0].node) {
reject(new Error('未找到Canvas节点'));
return;
}
const canvas = res[0].node;
const dpr = wx.getSystemInfoSync().pixelRatio;
// 动态设置Canvas尺寸匹配图片原始尺寸[6](@ref)
canvas.width = imgWidth * dpr;
canvas.height = imgHeight * dpr;
const ctx = canvas.getContext('2d');
// 缩放上下文以匹配设备像素比
ctx.scale(dpr, dpr);
resolve({ ctx, canvas, dpr });
});
});
}
/**
* 加载图片
*/
static loadImage(src) {
return new Promise((resolve, reject) => {
const image = wx.createOffscreenCanvas ? wx.createOffscreenCanvas().createImage() : wx.createImage();
image.onload = () => resolve(image);
image.onerror = (err) => reject(new Error(`图片加载失败: ${err.message || err}`));
image.src = src;
});
}
/**
* 获取图片信息
*/
static getImageInfo(src) {
return new Promise((resolve, reject) => {
wx.getImageInfo({
src,
success: resolve,
fail: reject
});
});
}
/**
* 绘制水印文字
*/
static drawWatermarkText(ctx, width, height, watermarkData) {
const { longitude, latitude, userName, address } = watermarkData;
const locationText = `坐标: ${longitude}, ${latitude}`;
const currentDate = new Date() // 创建一个Date实例,获取当前时间
const year = currentDate.getFullYear()// 获取年
const month = currentDate.getMonth() + 1 // 获取月(注意,返回值是 0 - 11,所以实际使用时通常要加1)
const day = currentDate.getDate() // 获取日
const hours = currentDate.getHours() // 获取小时
const minutes = currentDate.getMinutes() // 获取分钟
const seconds = currentDate.getSeconds() // 获取秒
const currentTime = `当前时间:${year}年${month}月${day}日 ${hours}:${minutes}:${seconds}`
// 设置水印样式
const baseFontSize = Math.max(width / 30, 14);
ctx.font = `normal ${baseFontSize}px sans-serif`;
ctx.fillStyle = 'rgba(102, 102, 102, 1)';
ctx.textBaseline = 'bottom';
// 文字阴影
ctx.shadowOffsetX = 1;
ctx.shadowOffsetY = 1;
ctx.shadowBlur = 4;
ctx.shadowColor = 'rgba(248, 45, 49, 1)';
// 水印位置(右下角)[3](@ref)
const margin = 15;
const lineHeight = baseFontSize * 1.4;
let currentY = height - margin;
// 时间信息
ctx.fillText(currentTime, margin, currentY);
currentY -= lineHeight;
// 经纬度信息
ctx.fillText(locationText, margin, currentY);
currentY -= lineHeight;
// 地址信息(如果有)
if (address) {
currentY -= lineHeight;
const shortAddress = address.length > 20 ? address.substring(0, 20) + '...' : address;
ctx.fillText(shortAddress, margin, currentY);
}
}
/**
* 导出Canvas为图片
*/
static exportCanvasImage(canvas, destWidth, destHeight, quality) {
return new Promise((resolve, reject) => {
// 确保Canvas绘制完成
setTimeout(() => {
wx.canvasToTempFilePath({
canvas,
destWidth: destWidth, // 关键:指定导出宽度
destHeight: destHeight, // 关键:指定导出高度
fileType: 'jpg',
quality,
success: (res) => {
resolve(res.tempFilePath);
},
fail: (err) => {
reject(new Error(`Canvas导出失败: ${err.errMsg}`));
}
});
}, 100);
});
}
/**
* 获取当前位置信息(强制授权版,拒绝则中断流程)
* @returns {Promise<Object>} 包含经纬度等信息的对象,若用户拒绝授权则reject
*/
static getCurrentLocation() {
return new Promise(async (resolve, reject) => {
try {
// 1. 检查当前授权设置
const settingRes = await this.checkAuthorization();
// 2. 根据授权状态采取不同行动
const authStatus = settingRes.authSetting['scope.userLocation'];
if (authStatus === true) {
// 已授权,直接获取位置
const location = await this.executeGetLocation();
resolve(location);
}
else if (authStatus === undefined) {
// 首次使用,发起授权请求
const location = await this.handleFirstTimeAuthorization();
resolve(location);
}
else if (authStatus === false) {
// 用户已拒绝授权,执行强制拦截流程
await this.handleRejectedAuthorization();
// 如果用户通过设置页授权了,resolve结果,否则reject
const finalStatus = await this.checkAuthorizationAfterRejection();
if (finalStatus.authSetting['scope.userLocation'] === true) {
const location = await this.executeGetLocation();
resolve(location);
} else {
reject(new Error('USER_AUTH_DENIED')); // 使用特定错误码便于页面捕获
}
}
} catch (error) {
reject(error);
}
});
}
/**
* 检查授权状态
*/
static checkAuthorization() {
return new Promise((resolve, reject) => {
wx.getSetting({
success: resolve,
fail: reject
});
});
}
/**
* 处理首次授权
*/
static handleFirstTimeAuthorization() {
return new Promise((resolve, reject) => {
wx.authorize({
scope: 'scope.userLocation',
success: async () => {
const location = await this.executeGetLocation();
resolve(location);
},
fail: (err) => {
// 首次请求用户就拒绝
this.showAuthorizationModal(true).then(resolve).catch(reject);
}
});
});
}
/**
* 处理已拒绝的授权
*/
static handleRejectedAuthorization() {
return this.showAuthorizationModal(false);
}
/**
* 显示授权引导弹窗(核心拦截逻辑)
* @param {boolean} isFirstTime 是否为首次拒绝
*/
static showAuthorizationModal(isFirstTime) {
return new Promise((resolve, reject) => {
const content = isFirstTime
? '您已拒绝授予地理位置权限。此功能必须获取您的位置才能添加水印。'
: '您之前已拒绝授予地理位置权限,无法进行图片上传。';
wx.showModal({
title: '权限不足',
content: content + '\n\n请前往设置页面开启权限,否则将无法使用此功能。',
confirmText: '去设置',
cancelText: '放弃使用',
confirmColor: '#FA5151',
success: (res) => {
if (res.confirm) {
// 用户点击"去设置",打开设置页
wx.openSetting({
success: (settingRes) => {
// 返回设置页的结果,但不在此处判断,由调用方处理
resolve(settingRes);
},
fail: reject
});
} else {
// 用户点击"放弃使用",直接拒绝Promise
reject(new Error('USER_CANCELED_AUTH'));
}
},
fail: reject
});
});
}
/**
* 在用户拒绝后再次检查授权状态
*/
static checkAuthorizationAfterRejection() {
return new Promise((resolve) => {
wx.showToast({
title: '请授权地理位置权限',
icon: 'none',
duration: 1500
});
// 给用户一点时间查看提示,然后检查设置
setTimeout(() => {
this.checkAuthorization().then(resolve);
}, 1600);
});
}
/**
* 执行获取地理位置
*/
static executeGetLocation() {
return new Promise((resolve, reject) => {
wx.getLocation({
type: 'gcj02',
success: (res) => {
resolve({
longitude: res.longitude.toFixed(6),
latitude: res.latitude.toFixed(6),
address: '',
accuracy: res.accuracy
});
},
fail: reject
});
});
}
}
module.exports = Watermark;