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

发布于:2025-04-11 ⋅ 阅读:(61) ⋅ 点赞:(0)

整体思路:

  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)
		})
	})
}

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


网站公告

今日签到

点亮在社区的每一天
去签到