uniapp结合u-upload组件实现拍照加水印图片上传功能(详细)

前言

本文将介绍如何使用uniApp和u-upload组件封装一个自动添加水印并上传图片的功能。整个过程涵盖了图片上传、canvas画布操作、水印绘制等多个技术点。

主要技术点

  • u-upload组件:用于图片的上传与展示。
  • Canvas画布: 用于在图片上绘制水印信息。
  • 异步操作: 用来处理图片信息的获取与水印绘制,确保操作的正确顺序。
  • 图片信息的获取: 通过uni.getImageInfo()获取图片的宽高等信息。
  • 地理位置的获取: 使用uni.getLocation()结合腾讯地图API获取当前位置,并显示在水印中。

实现效果

用户在选择或拍摄图片后,图片不会立即上传,而是通过Canvas在图片上绘制时间、地址、用户名称等水印信息。处理完毕后,带水印的图片将被替换原图并上传。(以下是运行到微信小程序的测试效果)

 

实现步骤

1. 使用u-upload组件

使用u-upload组件来处理图片选择和上传。同时准备了一个隐藏的 canvas 元素,用于绘制图片水印

<template>
  <view style="width: 100%">
    <u-upload
      ref="uUpload"
      :action="action"
      :source-type="sourceType"
      :file-list="defaultValue"
      :auto-upload="!isWatermark"
      @on-choose-complete="onChooseComplete"
    />
    <!--  Canvas画布,用于添加水印 -->
    <canvas
      canvas-id="watermarkCanvas"
      v-if="isCanvasShow"
      :style="{
        position: 'absolute',
        top: '-9999px',
        left: '-9999px',
        width: canvasWidth + 'px',
        height: canvasHeight + 'px',
      }"
    ></canvas>
    <!-- 模拟提示 -->
    <u-toast ref="uToast" />
  </view>
</template>

2. 处理选择图片后的逻辑

当用户选择图片后,触发 onChooseComplete 方法,开始处理图片水印。

由于这里是使用uView1.X版本所以在on-choose-complete事件中处理图片,如果是使用uView2.X版本,则在afterRead事件中处理图片

methods: {
  // 当选择图片后触发的事件
  async onChooseComplete(list, name) {
    if (this.isWatermark) {
      try {
        const index = list.length - 1;
        await this.drawWatermark(list, index); // 添加水印
      } catch (error) {
        console.error("水印处理失败", error);
      }
    }
  },

  // 处理水印并手动上传
  async drawWatermark(list, index) {
    try {
      const url = list[index].url;
      const size = list[index].file.size;

      // 获取当前时间:年-月-日 时:分:秒 星期
      this.currentTime = parseTime(new Date(), "{y}-{m}-{d} {h}:{i}:{s} {a}");
      // 设置mask: true,避免水印加载过程中用户执行其他操作
      uni.showLoading({ title: "加载水印中", mask: true });

      await this.getCurrentAddress(); // 获取当前地址

      const { width, height, path } = await this.getImageInfo(url); // 获取图片信息

      // 设置画布宽高
      this.canvasWidth = width;
      this.canvasHeight = height;
      this.isCanvasShow = true;

      // 等待 canvas 元素创建
      this.$nextTick(() => {
        let ctx = uni.createCanvasContext("watermarkCanvas", this);

        // 清除画布
        ctx.clearRect(0, 0, width, height);
        // 绘制图片
        ctx.drawImage(path, 0, 0, width, height);

        // 绘制地址水印
        this.drawAddressWatermark(ctx, width, height);

        // 绘制竖线水印、时间、创建人等
        this.drawOtherWatermarks(ctx, width, height);

        // 绘制结束后生成临时文件并上传
        ctx.draw(false, () => {
          uni.canvasToTempFilePath({
            canvasId: "watermarkCanvas",
            quality: 0.6, // 图片质量,范围0-1,1为最高质量
            width: width, // 画布宽度
            height: height, // 画布高度
            destWidth: width, // 输出图片宽度
            destHeight: height, // 输出图片高度
            success: (data) => {
              // 替换原始图片为带水印图片
              this.$refs.uUpload.lists[index] = {
                size: size,
                thumb: data.tempFilePath,
                type: "png",
                url: data.tempFilePath,
              };
              this.$refs.uUpload.upload(); // 手动上传图片

              // 隐藏画布
              this.isCanvasShow = false;
              uni.hideLoading();
            },
            fail: (err) => {
              console.error("canvasToTempFilePath failed", err);
              uni.hideLoading();
            },
          },this); // 加上 this 确保 canvas 正确关联当前页面或组件,避免报错 "canvasToTempFilePath: fail canvas is empty"
        });
      });
    } catch (error) {
      console.error("drawWatermark error:", error);
      uni.hideLoading();
    }
  },
}

3. 获取地址信息

使用 uni.getLocation 获取用户当前的地理位置,并通过腾讯地图 API 获取详细的地址信息。

getCurrentAddress() {
  return new Promise((resolve, reject) => {
    uni.authorize({
      scope: "scope.userLocation",
      success: () => {
        uni.getLocation({
          type: "gcj02",
          success: (res) => {
            const lat = res.latitude;
            const lng = res.longitude;
            // 通过腾讯地图 API 获取用户的当前位置(请将以下的key值替换为真实的key值)
            const URL = `https://apis.map.qq.com/ws/geocoder/v1/?location=${lat},${lng}&key=ABCDE-FGHIJ-KLMNO-PQRST-UVWXY`;

            wx.request({
              url: URL,
              success: (result) => {
                const Res_Data = result.data.result;
                this.currentAddress =
                  Res_Data.address +
                  " (" +
                  Res_Data.formatted_addresses.recommend +
                  ")";
                resolve();
              },
              fail: () => {
                this.currentAddress = "";
                this.$refs.uToast.show({
                  title: "加载定位失败!",
                  type: "error",
                  icon: true,
                  duration: 2000,
                });
                reject();
              },
            });
          },
          fail: (err) => {
            reject(err);
          },
        });
      },
      fail: () => {
        this.$refs.uToast.show({
          title: "用户拒绝授权!",
          type: "error",
          icon: true,
          duration: 2000,
        });
        reject();
      },
    });
  });
}

4.绘制地址信息水印

为了确保水印的美观,动态计算了文字在画布上的换行位置,并且通过图片宽高对比区分横屏\竖屏,以此调整文字的高度。

// 绘制地址水印
drawAddressWatermark(ctx, width, height) {
  // 文字大小
  const fontSize = 30;
  ctx.font = 'bold 30px "Microsoft YaHei"';
  // 通过width、height对比判断照片:横屏\竖屏,调整绘制的高度
  const addressHeight = width > height ? 40 : 0;
  const maxWidth = width - 45; // 45是留给定位图标的空间
  const iconX = 10;
  const initialX = 45; // 地址文字起始x位置
  let y = height - 120 + addressHeight;
  let currentLine = "";
  let currentLineWidth = 0; // 当前行的总宽度

  // 绘制定位图标
  this.fillText(ctx, '📍', iconX, y, {
    font: 'bold 30px "Microsoft YaHei"',
    color: '#fff'
  });

  // 循环处理每个字符
  for (let i = 0; i < this.currentAddress.length; i++) {
    const char = this.currentAddress[i];
    const charWidth = ctx.measureText(char).width;

    // 预测添加字符后的总宽度
    if (currentLineWidth + charWidth <= maxWidth) {
      // 如果不会超出行宽,继续添加到当前行
      currentLine += char;
      currentLineWidth += charWidth;
    } else {
      // 当前行满了,绘制并换行
      this.fillText(ctx, currentLine, initialX, y, {
        font: 'bold 30px "Microsoft YaHei"',
        color: "#fff",
      });
      // 重置行参数
      currentLine = char;
      currentLineWidth = charWidth;
      y += fontSize + 10; // 换行
    }
  }

  // 绘制最后一行
  if (currentLine.length > 0) {
    this.fillText(ctx, currentLine, initialX, y, {
      font: 'bold 30px "Microsoft YaHei"',
      color: "#fff",
    });
  }
},

5.绘制时间、姓名等信息

// 绘制其他水印内容(时间、姓名等)
drawOtherWatermarks(ctx, width, height) {
  const currentTime = this.currentTime;
  const time = currentTime.substring(11, 16);
  const day = `${currentTime.substring(0, 4)}年${currentTime.substring(
    5,
    7
  )}月${currentTime.substring(8, 10)}日 星期${currentTime.substring(
    currentTime.length - 1
  )}`;
  // 通过width、height对比判断照片:横屏\竖屏,调整绘制的 👤 创建人名称 水印的高度
  const addressHeight = width > height ? 40 : 0;

  // 竖线水印
  this.fillRectangle(ctx, 40, 60, 8, 120, "#fff"); 
  // 时间水印
  this.fillText(ctx, time, 80, 100, {
    font: 'bold 50px "Microsoft YaHei"',
    color: "#fff",
  });
  this.fillText(ctx, day, 80, 150, {
    font: 'bold 40px "Microsoft YaHei"',
    color: "#fff",
  });
  this.fillText(ctx, "👤", width - 195, height - (160 - addressHeight), {
    font: 'bold 30px "Microsoft YaHei"',
    color: "#fff",
  });
  const createByName = this.$store.state.userInfo.userName; // 获取创建人名称
  this.fillText(
    ctx,
    createByName,
    width - 150,
    height - (160 - addressHeight),
    { font: 'bold 35px "Microsoft YaHei"', color: "#fff" }
  );
},

完整代码

<template>
  <view style="width: 100%">
    <!-- 上传组件 -->
    <u-upload
      ref="uUpload"
      :action="action"
      :source-type="sourceType"
      :file-list="defaultValue"
      :auto-upload="!isWatermark"
      @on-choose-complete="onChooseComplete"
    />
    <!--  Canvas画布,用于添加水印 -->
    <canvas
      canvas-id="watermarkCanvas"
      v-if="isCanvasShow"
      :style="{
        position: 'absolute',
        top: '-9999px',
        left: '-9999px',
        width: canvasWidth + 'px',
        height: canvasHeight + 'px',
      }"
    ></canvas>
    <!-- 模拟提示 -->
    <u-toast ref="uToast" />
  </view>
</template>
  
<script>
import { parseTime } from "@/utils";

export default {
  name: "my-upload",
  props: {
    value: {
      type: Array,
      default: () => [],
    },
    // 选择图片的来源,album-从相册选图,camera-使用相机
    sourceType: {
      type: Array,
      default: () => ["album", "camera"],
    },
    // 是否需要添加水印,控制自动上传
    isWatermark: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    return {
      action: "http://www.example.com/upload", // 上传地址
      currentTime: "", // 当前时间
      currentAddress: "", // 当前地址
      isCanvasShow: false, // 是否显示画布
      canvasWidth: 300, // 画布宽度
      canvasHeight: 225, // 画布高度
    };
  },
  computed: {
    // 显示的图片列表,确保为数组类型
    defaultValue() {
      return Array.isArray(this.value) ? this.value : [];
    },
  },
  methods: {
    // 当选择图片后触发的事件
    async onChooseComplete(list, name) {
      if (this.isWatermark) {
        try {
          const index = list.length - 1;
          await this.drawWatermark(list, index); // 添加水印
        } catch (error) {
          console.error("水印处理失败", error);
        }
      }
    },

    // 处理水印并手动上传
    async drawWatermark(list, index) {
      try {
        const url = list[index].url;
        const size = list[index].file.size;

        // 获取当前时间:年-月-日 时:分:秒 星期
        this.currentTime = parseTime(new Date(), "{y}-{m}-{d} {h}:{i}:{s} {a}");
        // 设置mask: true,避免水印加载过程中用户执行其他操作
        uni.showLoading({ title: "加载水印中", mask: true });

        await this.getCurrentAddress(); // 获取当前地址

        const { width, height, path } = await this.getImageInfo(url); // 获取图片信息

        // 设置画布宽高
        this.canvasWidth = width;
        this.canvasHeight = height;
        this.isCanvasShow = true;

        // 等待 canvas 元素创建
        this.$nextTick(() => {
          let ctx = uni.createCanvasContext("watermarkCanvas", this);

          // 清除画布
          ctx.clearRect(0, 0, width, height);
          // 绘制图片
          ctx.drawImage(path, 0, 0, width, height);

          // 绘制地址水印
          this.drawAddressWatermark(ctx, width, height);

          // 绘制竖线水印、时间、创建人等
          this.drawOtherWatermarks(ctx, width, height);

          // 绘制结束后生成临时文件并上传
          ctx.draw(false, () => {
            uni.canvasToTempFilePath({
              canvasId: "watermarkCanvas",
              quality: 0.6, // 图片质量,范围0-1,1为最高质量
              width: width, // 画布宽度
              height: height, // 画布高度
              destWidth: width, // 输出图片宽度(默认为 width * 屏幕像素密度)
              destHeight: height, // 输出图片高度(默认为 height * 屏幕像素密度)
              success: (data) => {
                // 替换原始图片为带水印图片
                this.$refs.uUpload.lists[index] = {
                  size: size,
                  thumb: data.tempFilePath,
                  type: "png",
                  url: data.tempFilePath,
                };
                this.$refs.uUpload.upload(); // 手动上传图片

                // 隐藏画布
                this.isCanvasShow = false;
                uni.hideLoading();
              },
              fail: (err) => {
                console.error("canvasToTempFilePath failed", err);
                uni.hideLoading();
              },
            },this); // 注意这里要加this(确保 canvas 与当前页面或者组件正确关联)否则报错:"canvasToTempFilePath: fail canvas is empty"
          });
        });
      } catch (error) {
        console.error("drawWatermark error:", error);
        uni.hideLoading();
      }
    },

    // 绘制地址水印
    drawAddressWatermark(ctx, width, height) {
      // 文字大小
			const fontSize = 30;
			ctx.font = 'bold 30px "Microsoft YaHei"';
      // 通过width、height对比判断照片:横屏\竖屏,调整绘制的高度
			const addressHeight = width > height ? 40 : 0;
			const maxWidth = width - 45; // 45是留给定位图标的空间
			const iconX = 10;
			const initialX = 45; // 地址文字起始x位置
			let y = height - 120 + addressHeight;
			let currentLine = "";
			let currentLineWidth = 0; // 当前行的总宽度

			// 绘制定位图标
			this.fillText(ctx, '📍', iconX, y, {
				font: 'bold 30px "Microsoft YaHei"',
				color: '#fff'
			});

			// 循环处理每个字符
			for (let i = 0; i < this.currentAddress.length; i++) {
				const char = this.currentAddress[i];
				const charWidth = ctx.measureText(char).width;

				// 预测添加字符后的总宽度
				if (currentLineWidth + charWidth <= maxWidth) {
					// 如果不会超出行宽,继续添加到当前行
					currentLine += char;
					currentLineWidth += charWidth;
				} else {
					// 当前行满了,绘制并换行
					this.fillText(ctx, currentLine, initialX, y, {
						font: 'bold 30px "Microsoft YaHei"',
						color: "#fff",
					});
					// 重置行参数
					currentLine = char;
					currentLineWidth = charWidth;
					y += fontSize + 10; // 换行
				}
			}

			// 绘制最后一行
			if (currentLine.length > 0) {
				this.fillText(ctx, currentLine, initialX, y, {
					font: 'bold 30px "Microsoft YaHei"',
					color: "#fff",
				});
			}
    },

    // 绘制其他水印(竖线、时间等)
    drawOtherWatermarks(ctx, width, height) {
      const currentTime = this.currentTime;
      const time = currentTime.substring(11, 16);
      const day = `${currentTime.substring(0, 4)}年${currentTime.substring(
        5,
        7
      )}月${currentTime.substring(8, 10)}日 星期${currentTime.substring(
        currentTime.length - 1
      )}`;
      // 通过width、height对比判断照片:横屏\竖屏,调整绘制的 👤 创建人名称 水印的高度
      const addressHeight = width > height ? 40 : 0;

      // 竖线水印
      this.fillRectangle(ctx, 40, 60, 8, 120, "#fff"); 
      // 时间水印
      this.fillText(ctx, time, 80, 100, {
        font: 'bold 50px "Microsoft YaHei"',
        color: "#fff",
      });
      this.fillText(ctx, day, 80, 150, {
        font: 'bold 40px "Microsoft YaHei"',
        color: "#fff",
      });
      this.fillText(ctx, "👤", width - 195, height - (160 - addressHeight), {
        font: 'bold 30px "Microsoft YaHei"',
        color: "#fff",
      });
      const createByName = this.$store.state.userInfo.userName; // 获取创建人名称
      this.fillText(
        ctx,
        createByName,
        width - 150,
        height - (160 - addressHeight),
        { font: 'bold 35px "Microsoft YaHei"', color: "#fff" }
      );
    },

    // 绘制文字
    fillText(cxt, content, x, y, style) {
      cxt.save();
      cxt.font = style.font;
      cxt.fillStyle = style.color;
      cxt.fillText(content, x, y);
      cxt.restore();
    },

    // 绘制矩形竖线水印
    fillRectangle(cxt, x, y, width, height, style) {
      cxt.save();
      cxt.fillStyle = style;
      cxt.fillRect(x, y, width, height);
      cxt.restore();
    },

    // 获取图片信息封装为 Promise,便于异步处理
    getImageInfo(src) {
      return new Promise((resolve, reject) => {
        uni.getImageInfo({
          src: src,
          success: resolve,
          fail: reject,
        });
      });
    },

    // 获取当前地址信息
    getCurrentAddress() {
      return new Promise((resolve, reject) => {
        uni.authorize({
          scope: "scope.userLocation",
          success: () => {
            uni.getLocation({
              type: "gcj02",
              success: (res) => {
                const lat = res.latitude;
                const lng = res.longitude;
                // 通过腾讯地图 API 获取用户的当前位置
                const URL = `https://apis.map.qq.com/ws/geocoder/v1/?location=${lat},${lng}&key=ABCDE-FGHIJ-KLMNO-PQRST-UVWXY`;
                wx.request({
                  url: URL,
                  success: (result) => {
                    const Res_Data = result.data.result;
                    this.currentAddress =
                      Res_Data.address +
                      " (" +
                      Res_Data.formatted_addresses.recommend +
                      ")";
                    resolve();
                  },
                  fail: () => {
                    this.currentAddress = "";
                    this.$refs.uToast.show({
                      title: "加载定位失败!",
                      type: "error",
                      icon: true,
                      duration: 2000,
                    });
                    reject();
                  },
                });
              },
              fail: (err) => {
                reject(err);
              },
            });
          },
          fail: () => {
            this.$refs.uToast.show({
              title: "用户拒绝授权!",
              type: "error",
              icon: true,
              duration: 2000,
            });
            reject();
          },
        });
      });
    },
  },
};

使用方式

在需要用到的页面引用该组件即可,设置sourceType的值为camera(相机拍摄),isWatermark值为true(开启水印加载)

<my-upload
    v-model="imgList"
    :source-type="['camera']"
    :is-watermark="true"
/>

注意点

  • 上述代码中uni.canvasToTempFilePath({},this)不能缺少this,确保 canvas 与当前页面正确关联,否则报错:"canvasToTempFilePath: fail canvas is empty"
  • 代码中是将获取实时定位信息的步骤放在了加载水印的过程中执行,可能会导致加载水印的整体耗时比较长,可以选择在onShow生命周期里调用 getCurrentAddress 方法
  • 在微信小程序里如果设置中禁用了“选中的照片或视频”的权限,会导致onChooseComplete事件中拿不到刚拍摄的图片文件从而导致绘制水印失败。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值