微信小程序添加水印功能

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;
相关推荐
_AaronWong4 小时前
一键搞定UniApp WiFi连接!这个Vue 3 Hook让你少走弯路
前端·微信小程序·uni-app
赵庆明老师17 小时前
Uniapp微信小程序开发:微信小程序支付功能后台代码
微信小程序·小程序·uni-app
FliPPeDround1 天前
告别 uni-app 启动烦恼:@uni-helper/unh 让开发流程更顺畅
前端·微信小程序·uni-app
@二十六1 天前
微信小程序订阅消息工具封装,兼容一次性订阅和长期订阅
微信小程序·小程序·订阅消息
用力的活着1 天前
uniapp 微信小程序蓝牙接收中文乱码
微信小程序·小程序·uni-app
知识分享小能手1 天前
微信小程序入门学习教程,从入门到精通,电影之家小程序项目知识点详解 (17)
前端·javascript·学习·微信小程序·小程序·前端框架·vue
笨笨狗吞噬者2 天前
【uniapp】体验优化:开源工具集 uni-toolkit 发布
性能优化·微信小程序·uni-app
知识分享小能手2 天前
微信小程序入门学习教程,从入门到精通,自定义组件与第三方 UI 组件库(以 Vant Weapp 为例) (16)
前端·学习·ui·微信小程序·小程序·vue·编程
爱隐身的官人2 天前
微信小程序反编译教程
微信小程序·小程序·小程序反编译