前端通过 canvas 实现给图片打水印

4 篇文章 0 订阅
1 篇文章 0 订阅

前端通过 canvas 实现给图片打水印

一、背景

最近做了一个需求,小程序拍照上传图片时需要打水印,内容是:当时的时间、经纬度和地址
个人认为打水印后端去做比较好一点,不过领导决定了前端来做,那我就开始调研实现
实用技术栈:uniapp-小程序,Vue3&vant3-表单页

二、实现-封装一些方法

  • 实现思路

封装upload组件,在自检change方法中做处理,先处理图片->传给服务->回显

  • 因为上传组件的参数不是 base64 类型,所以要转换一下
const blob = new Blob([blobimage], {
  type: blobimage.type || "image/jpg",
}); //类型一定要写!!
const reader = new FileReader();
reader.readAsDataURL(blob);
  • 最后处理完还要在转回去
// 加完水印 转成file
const base64toFile = (data) => {
  const dataArr = data.split(",");
  const byteString = atob(dataArr[1]);

  const u8Arr = new Uint8Array(byteString.length);
  for (let i = 0; i < byteString.length; i++) {
    u8Arr[i] = byteString.charCodeAt(i);
  }
  return new File([u8Arr], "1231231" + ".jpg", {
    type: "image/jpeg",
    endings: "native",
  }); //返回文件流
};

1.创建 image,用来挂在图片

  const loadImageFromBase64 = (blobimage): Promise<HTMLImageElement> => {
    return new Promise((resolve, reject) => {
      const blob = new Blob([blobimage], {
        type: blobimage.type || 'image/jpg',
      }) //类型一定要写!!
      const reader = new FileReader()
      reader.readAsDataURL(blob)
      reader.onload = () => {
        const image = new Image()
        image.src = reader.result as string
        image.onload = () => resolve(image)
        image.onerror = () =>
          reject(new Error('Failed to load image from Base64'))
      }
      reader.onerror = (error) => reject(error)
    })
  }

2.绘制 canvas

async function imgToCanvas(blobimage, content) {
  try {
    const image = await loadImageFromBase64(blobimage);
    // 创建img元素
    // 创建canvas DOM元素,并设置其宽高和图片一样
    const canvas = document.createElement("canvas");

    const ctx = canvas.getContext("2d");

    canvas.width = image.width;
    canvas.height = image.height;

    // 坐标(0,0) 表示从此处开始绘制,相当于偏移。
    ctx.drawImage(image, 0, 0, image.width, image.height);

    // 由于图片像素大小不一致,文字大小通过比例计算
    const fontSize = Math.floor(canvas.width / 30);
    // // 添加水印
    ctx.font = `${fontSize}px Arial`;

    // 计算水印颜色---见下方
    const color = getMainColor(canvas);

    ctx.fillStyle = color;

    // 判断地址超长---见下方
    // 因为要将地址内容设置为水印,考虑长度问题,要计算截取,吧地址内容折行
    content = isLongAddress(ctx, content, canvas, 0);

    const keys = Object.keys(content);
    let num = 0;
    for (let i = keys.length; i > 0; i--) {
      const key = keys[i - 1];

      if (!content[key]) continue;

      // 计算文本宽度
      const textWidth = ctx.measureText(content[key]).width;

      // PhotoWatermarkStyleEnum.LEFT_BOTTOM  这个是我们系统配置项,用来控制水印是左边还是右边
      const x =
        photoWatermarkStyle === PhotoWatermarkStyleEnum.LEFT_BOTTOM
          ? 20
          : canvas.width - textWidth - 20;
      num++;
      const y = canvas.height - fontSize * num * 1.2;
      ctx.fillText(content[key], x, y);
    }

    // 把文件转换为 file 类型
    const file = base64toFile(canvas.toDataURL("image/png"));
    return file;
  } catch (error) {
    console.error("Error adding watermark:", error);
    throw error;
  }
}

3.计算水印颜色

// 计算主色调
function getMainColor(canvas) {
  const ctx = canvas.getContext("2d");
  // 获取像素数据
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const data = imageData.data;
  const colorCount = {};

  // 分析像素数据
  for (let i = 0; i < data.length; i += 4) {
    const r = data[i];
    const g = data[i + 1];
    const b = data[i + 2];
    const key = `${r},${g},${b}`;
    colorCount[key] = (colorCount[key] || 0) + 1;
  }

  // 确定主色调
  let mainColor = null;
  let maxCount = 0;
  for (const key in colorCount) {
    if (colorCount[key] > maxCount) {
      maxCount = colorCount[key];
      mainColor = key;
    }
  }

  if (!mainColor) return `rgb(0,0,0)`;

  // 这里简化处理,直接取反色作为水印颜色
  const result = `${mainColor}`.split(",");

  const color = result.length
    ? {
        r: parseInt(result[0], 10),
        g: parseInt(result[1], 10),
        b: parseInt(result[2], 10),
      }
    : {
        r: 255,
        g: 255,
        b: 255,
      };

  // 计算亮度
  const brightness = color.r * 0.299 + color.g * 0.587 + color.b * 0.114;

  // 根据亮度决定水印颜色
  if (brightness > 128) {
    // 如果主色调较亮,则使用深色水印
    return "rgb(102, 102, 102)"; // 黑色水印
  } else {
    // 如果主色调较暗,则使用浅色水印
    return "rgb(200,200,200)"; // 白色水印
  }
}

4.判断地址是否超长,处理水印内容

// 判断地址超长
function isLongAddress(ctx, content, canvas, num) {
  let obj = cloneDeep(content);
  const key = num ? `loc${num}` : "loc";
  const text = obj[key];
  const textWidth = ctx.measureText(text).width;
  // 截取的文本 - 前段
  let textBefore = "";
  // 截取的文本 - 后段
  let textAfter = "";

  if (textWidth < canvas.width) return obj;

  // 宽度比 --  Math.floor 是为了防止截取的文本过长,留点空余量
  const ratio = Math.floor((canvas.width / textWidth) * 10) / 10;
  // 要截取的文本百分比
  const ratio1 = Number(ratio.toFixed(1));
  // 计算要截取的文本长度
  const len = Math.floor(text.length * ratio1);
  // 截取文本
  textBefore = text.slice(0, len);
  textAfter = text.slice(len, text.length);

  obj[key] = textBefore;
  Object.assign(obj, { [`loc${num + 1}`]: textAfter });

  // 这里做递归,,以免水印内容截取后半段还超长
  obj = isLongAddress(ctx, obj, canvas, num + 1);

  return obj;
}

三、使用

// 计算水印内容
const watermarkContent = async () => {
  const content = {
    time: "",
    lat_lon: "",
    loc: "",
  };

  // 时间
  if (photoWatermarkContent.includes(PhotoWatermarkContentEnum.TIME)) {
    const date = new Date();
    const year = date.getFullYear();
    const month = date.getMonth() + 1;
    const day = date.getDate();
    const hour = date.getHours();
    const min = date.getMinutes();
    const sec = date.getSeconds();

    content.time = `${year}-${month}-${day} ${hour}:${min}:${sec}`;
  }

  // 经纬度---当前逻辑是小程序webview中实现-踩了很多坑啊
  const res = await getLocationInfo();
  content.lat_lon = res.lat_lon || "";
  content.loc = res.loc || "";

  return content;
};
  • 处理文件
// rawFileList 文件列表
const setWatermark = async (rawFileList) => {
  try {
    // 是否仅拍照上传
    const camera =
      Object.keys(uplaodOptions).includes("uploadRequirement") &&
      uplaodOptions["uploadRequirement"] === IUploadRequirement.CAMERA;

    if (camera && rawFileList.length) {
      // 计算要加水印的内容

      const content = await watermarkContent();
      const file = await imgToCanvas(rawFileList[0].file, content);

      if (!file) return rawFileList;

      rawFileList[0].file = file;
      return rawFileList;

      // return rawFileList
    } else {
      return rawFileList;
    }
  } catch (error) {
    return rawFileList;
  }
};

现在就可以愉快享用了,祝君好运

  • 6
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值