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

news/2024/11/16 23:29:35/

前端通过 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 stringimage.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;}
};

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


http://www.ppmy.cn/news/1423052.html

相关文章

[Kubernetes[K8S]集群:Slaver从节点初始化和Join]:添加到主节点集群内

文章目录 操作流程&#xff1a;上篇主节初始化地址&#xff1a;前置&#xff1a;Docker和K8S安装版本匹配查看0.1&#xff1a;安装指定docker版本 **[1 — 8] ** [ 这些步骤主从节点前置操作一样的 ]一&#xff1a;主节点操作 查看主机域名->编辑域名->域名配置二&#x…

【每日刷题】Day17

【每日刷题】Day17 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. 19. 删除链表的倒数第 N 个结点 - 力扣&#xff08;LeetCode&#xff09; 2. 162. 寻找峰值 - 力扣…

开源模型应用落地-chatglm3-6b-模型输出违禁词检测(九)

一、前言 受限于模型本身的一些缺陷&#xff0c;任何模型均可能会生成一些不正确的输出。如何通过技术的手段去规避模型潜在的风险&#xff0c;提升推理质量是需要持续探究的过程。 如何利用第三方内容安全审核服务去检测模型输出内容的合规性&#xff0c;请查看&#xff1a;开…

golang实现windows提权

golang实现windows提权 package mainimport ("fmt""syscall""unsafe""github.com/shirou/gopsutil/process""golang.org/x/sys/windows" )const (TOKEN_ALL_ACCESS 0x000F01FFSE_PRIVILEGE_ENABLED 0x00000002TOKEN_…

数据库配置加密(自定义加密方式)

1. 首先,自己编写加密工具类,我这里使用的是国密(免得其他地方有要求),并使用hutool工具,需要应入pom package com.banyoyo.epdb.utils;import cn.hutool.core.util.CharsetUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.symmetric.SymmetricCrypto;/**…

成为程序员的收获、体会与未来展望

在当今数字化时代&#xff0c;程序员的角色变得越来越重要。成为一名程序员不仅仅是学习编程语言和技术&#xff0c;更是一个充满挑战和机遇的职业选择。在本文中&#xff0c;我们将探讨成为程序员后所带来的收获和体会&#xff0c;并展望未来的发展前景。第一部分&#xff1a;…

python re.split()函数解析

re.split简单的使用方法&#xff1a; resultre.split(表达式,字符串,re.S)根据表达式拆分字符串并返回数组 如果拆分文本&#xff0c;比如拆分一本小说内容如下 ss第一章 第一章标题\n fadfasdfasdfadafd\n 第二章 第二章标题\n adfafdasdfasdfadsfasd\n 第三章 第三章…

4.16作业

1.总结keil5下载代码和编译代码需要注意的事项 一、在编译代码时需要先点击魔术棒点击 修改flash Downlond 和pack 二、可以通过F12转跳到对应的函数中&#xff0c;查看函数的原型 三、注释出现乱码通过 Edit中的中的来修改 四、要先bulid在load 2.总结STM32Cubemx的使用方…