整体思路:
- 用户通过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 = scaleWidththis.watermarkCanvasOption.height = scaleHeightif (!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).widthif (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).widthif (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 * 1024if (image.size > maxFileSizeLimit) {const fileSize = image.size / 1024// 初始压缩率80let quality = 80if (fileSize > 200 && fileSize <= 500) {// 200 以上,500k以内的图片,压缩70quality = 60} else if (fileSize > 500 && fileSize <= 1024) {// 500 以上,1M以内的图片,压缩50quality = 40} else if (fileSize > 1024 && fileSize <= 2048) {// 1M 以上,2M以内的图片,压缩30quality = 30} else if (fileSize > 2048 && fileSize <= 5012) {// 2M 以上,5M以内的图片,压缩20quality = 20} else if (fileSize > 5012) {// 5M以上的图片,压缩10quality = 10}// 开始压缩const option = {src: image.filePath,quality: quality,success: res => {image.compressPath = res.tempFilePathif (compressAfterSizeFlag) {// 获取压缩后的大小uni.getFileSystemManager().readFile({filePath: res.tempFilePath,success: ({ data }) => {image.compressSize = data.byteLengthresolve(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 } = optionsreturn 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 = 0for (let i = 0; i < textList.length; i++) {if (canvasContext.measureText(currText + textList[i]).width + x > maxWidth - 10) {textY += textY === 0 ? y : ySpacingcanvasContext.fillText(currText, x, textY)currText = textList[i]drawNum++} else {currText += textList[i]}}textY = textY === 0 ? y : textY + ySpacingcanvasContext.fillText(currText, x, textY)drawNum++canvasContext.draw(true, _ => {setTimeout(() => {resolve({ y: textY, res: _, drawNum })}, 100)})})
}
码字不易,于你有利,勿忘点赞