微信小程序添加水印功能
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=thistry {// 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结果,否则rejectconst 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 {// 用户点击“放弃使用”,直接拒绝Promisereject(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;