uniapp微信小程序图片生成水印

news2025/5/21 5:20:45

整体思路:

  1. 用户通过uni.chooseImage选择图片后,获得图片文件的path和size。
  2. 通过path调用uni.getImageInfo获取图片信息,也就是图片宽高。
  3. 图片宽高等比缩放至指定大小,不然手机处理起来非常久,因为手机随便拍拍就很大。
  4. 界面定义canvas组件,组件的宽高就是图片缩放后的宽高。
  5. uni.createCanvasContext创建画布上下文,然后画入图片,再画水印。
  6. 调用uni.canvasToTempFilePath把画布转成图片。
  7. 读取生成后的图片信息,获取其大小。
  8. 压缩图片。

水印后的结果,水印方法不是通用的,只是提供一个思路


1:定义canvas组件


<canvas :style="{ width: watermarkCanvasOption.width + 'px', height: watermarkCanvasOption.height + 'px' }" 
					canvas-id="watermarkCanvas" id="watermarkCanvas" style="position: absolute; top: -10000000rpx;" />

里面定义有样式,让它飞出外太空。

data里面定义画布配置

data () {
	return {
		watermarkCanvasOption: {
			width: 0,
			height: 0,
			canvasContext: void (0)
		}
	}
}

2:定义添加水印方法


addWatermark (currentTempFile) {
	return new Promise((resolve, reject) => {
		uni.showLoading({
			mask: true,
			title: '图片生成水印中...'
		})
		// 读取选择后的图片信息
		uni.getImageInfo({
			src: currentTempFile.path,
			success: ({ width, height }) => {
				// 宽度缩放至768附近(具体缩放到多少可以自己在getScaleRatio方法第二个参数定义),高度等比缩放
				const scaleRatio = this.getScaleRatio(width)
				const scaleWidth = Math.ceil(width * scaleRatio)
				const scaleHeight = Math.ceil(height * scaleRatio)
				// 定义页面画布组件宽高
				this.watermarkCanvasOption.width = scaleWidth
				this.watermarkCanvasOption.height = scaleHeight
				if (!this.watermarkCanvasOption.canvasContext) {
					// 创建画布上下文
					this.watermarkCanvasOption.canvasContext = uni.createCanvasContext('watermarkCanvas', this)
				}
				const watermarkCanvasContext = this.watermarkCanvasOption.canvasContext
				// 定义水印信息
				const watermarkInfo = {
					mainText: '10:30',
					secondaryText: `2025-04-09 星期三 福园-谭建林`
				}
				// 清空画布
				watermarkCanvasContext.clearRect(0, 0, scaleWidth, scaleHeight)
				watermarkCanvasContext.draw()
				// 画布写入图片
				watermarkCanvasContext.drawImage(currentTempFile.path, 0, 0, scaleWidth, scaleHeight)
				// 图片宽高的一半
				const halfX = Math.ceil(scaleWidth * 0.5)
				const halfY = Math.ceil(scaleHeight * 0.5)
				// 字体颜色,方向配置
				watermarkCanvasContext.setTextAlign('left')
				watermarkCanvasContext.setFillStyle('#FFF')
				/**
				 * 字体大小配置,由于每张图片大小不一,这里定一个初始大小,和每次递增值
				 * 其实上面缩放指定后,这里不太需要了,当初没有做缩放的时候,图片大小不一,写入的水印文字大小就得动态变
				 * 所以指定了缩放后,下面的作用只剩处理文字横向居中
				 */
				const fontSizeOption = { main: 50, mainIncr: 10, secondary: 17, secondaryIncr: 4 }
				// 写入时分水印
				let mainInitSize = fontSizeOption.main
				// 规定时分信息占据图片宽度大概五分之一
				let widthPart = Math.ceil(scaleWidth / 4.7)
				while(true) {
					watermarkCanvasContext.setFontSize(mainInitSize)
					// 获取当前指定的文字大小后,此文本的宽度
					const textWidth = watermarkCanvasContext.measureText(watermarkInfo.mainText).width
					if (textWidth >= widthPart) {
						// 文本宽度超过将近五分之一后,写入水印
						// 第二个参数是写入x轴,要居中的话就是图片宽度的一半,加上文字宽度的一半
						// 第三个参数是写入y轴,这边要求是中间靠下,所以就是图片高度的一半,再加一半的一半多一点
						watermarkCanvasContext.fillText(watermarkInfo.mainText, halfX - Math.ceil(textWidth * 0.5), halfY + Math.ceil(halfY * 0.35))
						break
					}
					mainInitSize += fontSizeOption.mainIncr
				}
				// 写入日期 + 人员信息水印
				let secondaryInitSize = fontSizeOption.secondary
				// 规定文本占据图片宽度的70%
				widthPart = Math.ceil(scaleWidth * 0.7)
				while(true) {
					watermarkCanvasContext.setFontSize(secondaryInitSize)
					const textWidth = watermarkCanvasContext.measureText(watermarkInfo.secondaryText).width
					if (textWidth >= widthPart) {
						// 第三个参数是写入y轴,这边要求是在上一个水印的下面,那就是上一个水印写入y轴位置 + 上一个水印的字体大小,避免靠太近,粘一起了
						watermarkCanvasContext.fillText(watermarkInfo.secondaryText, halfX - Math.ceil(textWidth * 0.5), halfY + Math.ceil(halfY * 0.35) + mainInitSize)
						break
					}
					secondaryInitSize += fontSizeOption.secondaryIncr
				}

				// 绘制步骤
				watermarkCanvasContext.draw(true, () => {
					setTimeout(() => {
						uni.canvasToTempFilePath({
							canvasId: 'watermarkCanvas',
							quality: 0.1,
							destWidth: scaleWidth,
							destHeight: scaleHeight,
							success: ({ tempFilePath }) => {
								// 画布转成图片,读取图片信息
								uni.getFileSystemManager().readFile({
									filePath: tempFilePath,
									success: ({ data }) => {
										const compressImageInfo = { size: data.byteLength, filePath: tempFilePath }
										// 压缩图片 compressAfterSizeFlag 参数意思是返回压缩后的图片大小,看个人需要,我这边需要再次判断压缩后是否还是超过指定大小
										this.compressImage(compressImageInfo, { compressAfterSizeFlag: true }).then(cRes => {
											uni.hideLoading()
											const imageInfo = Object.assign(
												{ ...currentTempFile },
												{ path: cRes.compressPath || cRes.filePath, size: cRes.compressSize || cRes.size }
											)
											resolve(imageInfo)
										})
									},
									fail: _ => {
										// toast('获取图片大小失败')
										uni.hideLoading()
										resolve(Object.assign({ ...currentTempFile }, { path: tempFilePath }))
									}
								})
							},
							fail: _err => {
								// toast('生成水印图片失败')
								uni.hideLoading()
								resolve(currentTempFile)
							}
						}, this)
					}, 500)
				})
			},
			fail: _ => {
				// toast('获取图片信息失败')
				uni.hideLoading()
				resolve(currentTempFile)
			}
		})
	})
}

其他用到的方法

// 获取到达指定宽度的缩放比率
getScaleRatio (width = 0, targetWidth = 768) {
	if (width <= targetWidth) {
		return 1
	}
	return (targetWidth / width).toFixed(2)
},

// 压缩图片
compressImage (image = {}, options = {}) {
	return new Promise((resolve, reject) => {
		const { width = 0 } = image
		// compressAfterSizeFlag 返回压缩后的大小。scaleFlag 是否缩放图片,scaleTargetWidth 缩放后的指定宽度,高度会等比缩放
		const { compressAfterSizeFlag = false, scaleFlag = false, scaleTargetWidth = 768 } = options
		// 超过100k压缩
		const maxFileSizeLimit = 100 * 1024
		if (image.size > maxFileSizeLimit) {
			const fileSize = image.size / 1024
			// 初始压缩率80
			let quality = 80
			if (fileSize > 200 && fileSize <= 500) {
				// 200 以上,500k以内的图片,压缩70
				quality = 60
			} else if (fileSize > 500 && fileSize <= 1024) {
				// 500 以上,1M以内的图片,压缩50
				quality = 40
			} else if (fileSize > 1024 && fileSize <= 2048) {
				// 1M 以上,2M以内的图片,压缩30
				quality = 30
			} else if (fileSize > 2048 && fileSize <= 5012) {
				// 2M 以上,5M以内的图片,压缩20
				quality = 20
			} else if (fileSize > 5012) {
				// 5M以上的图片,压缩10
				quality = 10
			}
			// 开始压缩
			const option = {
				src: image.filePath,
				quality: quality,
				success: res => {
					image.compressPath = res.tempFilePath
					if (compressAfterSizeFlag) {
						// 获取压缩后的大小
						uni.getFileSystemManager().readFile({
							filePath: res.tempFilePath,
							success: ({ data }) => {
								image.compressSize = data.byteLength
								resolve(image)
							},
							fail: _ => resolve(image)
						})
					} else {
						resolve(image)
					}
				},
				fail: _ => {
					resolve(image)
				}
			}
			// 缩放图片
			if (scaleFlag && width > scaleTargetWidth) {
				option.compressedWidth = scaleTargetWidth
			}
			uni.compressImage(option)
		} else {
			resolve(image)
		}
	})
}

3:调用水印方法 


uni.chooseImage({
	count: 3,
	sizeType: ['original', 'compressed'],
	sourceType: ['album', 'camera'],
	success: async ({ tempFiles = [] }) => {
		for (const tempFile of tempFiles) {
			this.addWatermark(tempFile).then(imageInfo => {
				console.log('水印后的图片', imageInfo)
			})
		}
	}
})

 本次没有用到的方法,纯粹做个记录,与上面的画水印无关,画布动态换行写入文本

/**
 * 画布文本换行绘制
 * canvasContext 画布实例
 * text 要写入的文本
 * x 初始x轴位置
 * y 初始y轴位置
 * ySpacing 换行后,每行直接的间隔
 * maxWidth 此文本写入画布的最大宽度,超过此宽度就换行
 * color 文本颜色
 * size 文本字体大小
 * align 文本方向 left rigt center 额一直搞不清楚这个方向是怎么个原理
 * @returns { textY 绘制最后一行文本的Y轴结束位置,drawNum 画布本次绘制了几次 }
 */
canvasTextNewlinedraw (options) {
	const { canvasContext, text = '', x = 0, y = 0, ySpacing = 0, maxWidth = 0, color, size, align } = options
	return new Promise((resolve, reject) => {
		size && canvasContext.setFontSize(size)
		align && canvasContext.setTextAlign(align)
		color && canvasContext.setFillStyle(color)
		const textList = text.split('')
		let currText = '', textY = 0, drawNum = 0
		for (let i = 0; i < textList.length; i++) {
			if (canvasContext.measureText(currText + textList[i]).width + x > maxWidth - 10) {
				textY += textY === 0 ? y : ySpacing
				canvasContext.fillText(currText, x, textY)
				currText = textList[i]
				drawNum++
			} else {
				currText += textList[i]
			}
		}
		textY = textY === 0 ? y : textY + ySpacing
		canvasContext.fillText(currText, x, textY)
		drawNum++
		canvasContext.draw(true, _ => {
			setTimeout(() => {
				resolve({ y: textY, res: _, drawNum })
			}, 100)
		})
	})
}

 码字不易,于你有利,勿忘点赞

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2332358.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

不用额外下载jar包,idea快速查看使用的组件源码

以nacos为例子&#xff0c;在idea中引入了nacos依赖&#xff0c;就可以查看源码了。 2. idea选择open&#xff08;不关闭项目直接选择file-open也可以&#xff09;, 在maven的仓库里找到对应的包&#xff0c;打开 2.idea中选择 jar包&#xff0c;选择 add as library 3.这样j…

网络通讯协议UDP转发TCP工具_UdpToTcpRelay_双向版

UDP/TCP网络转发器程序说明书 1. 程序概述 本程序是一个高性能网络数据转发工具&#xff0c;支持UDP和TCP协议之间的双向数据转发&#xff0c;并具备以下核心功能&#xff1a; 协议转换&#xff1a;实现UDP↔TCP协议转换数据转换&#xff1a;支持十六进制/ASCII格式的数据转…

DIA——边缘检测

1.边缘 边缘是像素的突变位置。 2.常见边缘检测算法 通过找到一阶导数的极值点或者二阶导数的过零点来确定边缘像素的位置。边缘检测通常使用算子&#xff0c;即特定的卷积核。通过差分对离散的像素点求导&#xff0c;然后转化成卷积核进行卷积。使用卷积统一涵盖求导&…

【万象论坛】论坛系统测试报告

一、项目背景 1.1项目起因 在当今数字化浪潮下&#xff0c;互联网技术呈爆发式发展&#xff0c;新技术、新框架、新应用场景不断涌现。从大型企业的数字化转型到初创公司的技术创新&#xff0c;各个层面都离不开互联网技术的支撑。然而&#xff0c;技术人员在学习与工作过程中…

【AI工具】FastGPT:开启高效智能问答新征程

前言 在人工智能飞速发展的当下&#xff0c;各类 AI 工具如雨后春笋般涌现。FastGPT 作为一款基于大语言模型&#xff08;LLM&#xff09;的知识图谱问答系统&#xff0c;凭借其强大的数据处理和模型调校能力&#xff0c;为用户带来了便捷的使用体验。今天&#xff0c;就让我们…

华为数字芯片机考2025合集1已校正

单选 1&#xff0e;以下低功耗措施中&#xff0c;哪种不是降低电路翻转率的方法? A.在不进行算术运算的时候&#xff0c;使这些模块的输入保持不变&#xff0c;不让新的操作数进来 B.采用Gray 码或One‐hot 码作为状态机编码 C.减少电路中的glitch D.重新安排“if‐else”表达…

HackMyVM - todd记录

HackMyVM - toddhttps://mp.weixin.qq.com/s/E_-hepdfY-0veilL1fl2QA

【完整可用】使用openhtmltopdf生成PDF(带SVG)

文章目录 前言OpenHTMLToPDF 简介maven配置依赖字体文件demo代码其他资源放置截图防止maven编译字体文件 前言 AI和网上都是跑不起来或者版本过低的&#xff0c;还有各种BUG的。本文都是查阅官方文档得出的。如果你能跑起来请给个大大的赞&#xff01; OpenHTMLToPDF 简介 Ope…

CTF web入门之爆破

爆破 web21: 打开burp进行抓包 通过对密码进行解析。得知密码是由拼接而来 admin:1 选择要攻击的参数 攻击方式。 选择payload方式 。。添加参数 1&#xff0c;2&#xff0c;3。账号 分隔符 密码 选择加密方式。添加buse64.去掉url字符。不然buse64后&#xff0c;会在u…

7-openwrt-one通过web页面配置访客网络、无线中继等功能

前几个章节一直在介绍编译、分区之类的,都还没正常开始使用这个路由器的wifi。默认wifi是没有启动的,前面还是通过手动修改uci配置启动的,这个章节介绍下官方web页面的使用。特别是访客网络、无线中继 1、开启wifi,配置wifi基本信息 我们使用有线连接路由器,通过192.168.…

Android使用声网SDK实现音视频互动(RTC)功能

一、前期准备 1、注册声网账号 声网官网 2、创建项目 拿到AppID&#xff0c;主要证书 二、代码部分 先上一下官方提供的demo地址&#xff1a; Agora-RTC-QuickStart: 此仓库包含 Agora RTC Native SDK 的QuickStart示例项目。 - Gitee.comhttps://gitee.com/agoraio-comm…

FPGA_modelsim错误总结

1&#xff0c; 使用modelsim仿真DDR3报错Module ‘SIP_PHY_CONTROL‘ is not defined 在配置ddr3的时候vivado 速度太慢了&#xff0c;所以选用modelsim。我的是2018.3vivado&#xff0c;modelsim用了10.4 但是不行报错 然后看了帖子说 questasim可以下载了还是报错。 然后又…

了解 DeFi:去中心化金融的入门指南与未来展望

去中心化金融&#xff0c;或 DeFi&#xff0c;代表着全球金融体系运作方式的革命性转变。它是一个总称&#xff0c;指的是一个不断增长的去中心化应用程序&#xff08;dapp&#xff09;、协议和平台生态系统&#xff0c;这些生态系统构建在公共区块链网络上&#xff0c;无需传统…

Python爬虫第10节-lxml解析库用 XPath 解析网页

目录 引言 一、XPath简介 二、XPath常用规则 三、实例讲解 四、节点的选取 4.1 所有节点的选取 4.2 子节点的选取 4.3 父节点选取 五、属性匹配获取及文本获取 5.1 属性匹配 5.2 文本获取 5.3 属性获取 5.4 属性多值匹配 5.5 多属性匹配 六、按序选择 七、节点…

【C语言】预处理(预编译)(C语言完结篇)

一、预定义符号 前面我们学习了C语言的编译和链接。 在C语言中设置了一些预定义符号&#xff0c;其可以直接使用&#xff0c;预定义符号也是在预处理期间处理的。 如下&#xff1a; 可以看到上面的预定义符号&#xff0c;其都有两个短下划线&#xff0c;要注意的是&#xff…

关于聊天室数据库建表

首先了解一下外键 ​​一、外键的本质​​ ​​定义​​&#xff1a;外键是某个表中的字段&#xff08;或字段组合&#xff09;&#xff0c;其值必须与另一张表的主键值相匹配。 ​​核心作用​​&#xff1a;强制数据一致性&#xff0c;维护表间关系。 二、外键的核心用途…

基于 OpenHarmony 5.0 的星闪轻量型设备应用开发-Ch1 开发环境搭建

写在前面&#xff1a; 文本所写的工程创建均是基于 HH-SPARK-WS63 星闪无线模组。 此篇是系列文章《基于 OpenHarmony5.0 的星闪轻量型设备应用开发》的第 1 章。 1.1 介绍 HH-SPARK-WS63 星闪无线模组&#xff08;以下简称 WS63&#xff09;是由润和软件推出的基于海思 WS63V…

离线安装 nvidia-docker2(nvidia-container-toolkit)

很多时候大家都有用docker使用gpu的需求&#xff0c;但是因为网络等原因不是那么好用&#xff0c;这里留了一个给ubuntu的安装包&#xff0c;网络好的话也提供了在线安装方式 安装 nvidia-docker2 1 离线安装 &#xff08;推荐&#xff09; unzip解压后进入目录 dpkg -i *.d…

第7篇:Linux程序访问控制FPGA端LEDR<五>

Q&#xff1a;如何设计.c程序代码实现FPGA端外设LEDR流水灯&#xff1f; A&#xff1a;在DE1-SoC开发板上实现的流水灯效果&#xff1a;一次只点亮一个红色LED&#xff0c;初始状态为向左移动直至点亮LEDR9&#xff0c;然后改变移动的方向为向右直至点亮LEDR0&#xff0c;以此…

Unity 实现伤害跳字

核心组件&#xff1a; Dotween TextMeshPro 过程轨迹如下图&#xff1a; 代码如下&#xff1a; using System.Collections; using System.Collections.Generic; using DG.Tweening; using TMPro; using UnityEngine; using UnityEngine.Pool;public class …