整体思路:
- 用户通过uni.chooseImage选择图片后,获得图片文件的path和size。
- 通过path调用uni.getImageInfo获取图片信息,也就是图片宽高。
- 图片宽高等比缩放至指定大小,不然手机处理起来非常久,因为手机随便拍拍就很大。
- 界面定义canvas组件,组件的宽高就是图片缩放后的宽高。
- uni.createCanvasContext创建画布上下文,然后画入图片,再画水印。
- 调用uni.canvasToTempFilePath把画布转成图片。
- 读取生成后的图片信息,获取其大小。
- 压缩图片。
水印后的结果,水印方法不是通用的,只是提供一个思路
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)
})
})
}
码字不易,于你有利,勿忘点赞