前言 📝
当你在微信小程序中制作照片时,你可能需要添加水印,以便在分享时保护你的照片,并标识出你的品牌或网站。本文将介绍如何在uniapp中添加照片水印(微信小程序大同小异)。操作 canvas 相关的 api 使用的是微信最新提供的 (一路过来踩了好多坑...)
1. 我的环境
微信开发者工具: 1.06.2402040 调试基础库:3.4.1
{% note warning %} 我的项目是采用uniapp脚手架搭建的,开发者工具是vscode,微信开发者工具是用来展示小程序样式的。 {% endnote %}
2. 选择图片和上传图片
由于我之前并不知道有添加水印的需求,我就在原来封装好的选择图片和上传图片的基础上更改(如果这两个方法和我的不一样,也不会影响到水印的方法,只是需要略微调整。我将上传的代码例举出来就是为了大家结合上下文,方便理解)。先来看一下之前封装的选择图片和上传图片的代码。
2.1 选择图片或者视频
uniapp的chooseMediaAPI传送门
type mediaType = 'image' | 'video' | 'mix'
export function useChooseMix(
type: mediaType,
options?: UniNamespace.ChooseMediaOption
): Promise<UniApp.MediaFile[]> {
return new Promise((resolve, reject) => {
// const files = ref<UniApp.MediaFile[]>([])
uni.chooseMedia({
count: 9,
mediaType: [type],
maxDuration: 60,
sourceType: ['album', 'camera'],
sizeType: ['original'],
...options,
success: (res) => {
resolve(res.tempFiles)
},
fail: (err) => {
uni.showToast({
title: '取消上传',
icon: 'none'
})
reject(err)
}
})
})
}
// 方法使用
const handleClick = async () => {
const res = await useChooseMix()
// TODO: 选择图片之后根据微信返回的结果在进行后面的操作
}
2.2 上传图片或视频
uniapp的uploadFileAPI传送门
export function useUploadFile(files: any[], fileType: number = 601): Promise<any> {
// 1. 边界校验
// token
const store = useLogin()
// 如果没给我文件,则直接返回空数组
if (files.length === 0) return Promise.resolve([])
// 整理数组里面的每一项,变成全是路径的数组
const filePath: any = []
files.forEach((item) => {
// useChooseFile该api选择的文件
if (item.path) {
return filePath.push(item.path)
}
// useChooseMix该api选择的视频或图片
return filePath.push(item.tempFilePath)
})
files = filePath
return new Promise((resolve, reject) => {
let index = 0 // 指向下一次请求得url对应得下标
const result: any[] = [] // 存储所有文件上传请求得结果
let count = 0 // 当前请求完成的数量
uni.showLoading({
title: '加载中',
mask: true
})
// 上传文件
function _request() {
const i = index // 存储一下当前得索引,为了以后存储到结果数组对应得位置
const url = files[i]
index++
uni.uploadFile({
url: `${BASE_URL}/api/xxx/uploadFile`,
filePath: url,
name: 'file',
header: {
'content-type': 'multipart/form-data',
Authorization: store.token
},
formData: {
fileType: fileType
},
success: (res) => {
if (typeof res.data === 'string') {
result[i] = JSON.parse(res.data)
} else {
result[i] = res.data
}
},
fail: (err) => {
result[i] = err
uni.showToast({
title: '上传失败',
icon: 'none'
})
},
complete: () => {
// 将所有得请求结果抛出
count++
if (count >= files.length) {
resolve(result)
uni.hideLoading()
}
}
})
}
// 循环发送所有请求
for (let i = 0; i < files.length; i++) {
_request()
}
})
}
// 方法使用
// 1. 选择图片
const tempRes = await useChooseMix('image')
// 2. 上传图片
const res = await useUploadFile(tempRes)
3. 添加水印
/**
* 添加水印方法调用
* @param { Array } fileInfoRef 该参数代表的是所有通过useChooseMix函数选择的图片,换句话说就是通过微信的chooseMediaAPI选择的文件列表
* @param { Instance } chooseImageRef 该参数代表的是上传图片并添加水印组件的实列对象
*/
export async function useAddWatermark(fileInfoRef: any[], chooseImageRef: any): Promise<any> {
const result: any[] = []
for (let i = 0; i < fileInfoRef.length; i++) {
if (fileInfoRef[i]?.fileType.includes('video')) {
result.push(fileInfoRef[i])
continue
}
// 给图片添加水印
const res = await chooseImageRef?.getCanvas(
fileInfoRef[i],
parseTime(+new Date(), '{y}-{m}-{d} {h}:{i}')
)
result.push(res)
}
return result
}
下面这个组件是我封装的一个上传图片和视频的组件,当然添加水印所需要的canvas也在这个页面,简单介绍一个这个组件使用,,我们在内部调用了useChooseMix选择图片,选择完图片之后,会通过v-model将选择的数据提供给父组件fileInfoRef。所以在父组件中我们就可以得到所有在微信小程序中选择的图片。
<!-- fileInfoRef就是通过微信选择图片api返回的内容 -->
<ChooseImage
style="width: 100%"
v-model="fileInfoRef"
ref="chooseImageRef"
upload-type="image"
></ChooseImage>
uniapp的createSelectorQueryAPI传送门
微信小程序的getImageInfoAPI传送门
微信小程序的canvasToTempFilePathAPI传送们
<script setup lang="ts">
// 框架
// 组件
// 方法/类型
import { useChooseMix } from '@/utils/useUploadFile'
import { handleImageScale } from '@/pagesGround/fun/useImageScale'
const props = withDefaults(
defineProps<{
modelValue: any[]
// 代表该组件选择的文件类型
uploadType: 'image' | 'video' | 'mix'
chooseOptions: UniNamespace.ChooseMediaOption
disabled?: boolean
}>(),
{
modelValue: () => [],
uploadType: 'image',
chooseOptions: () => ({}),
disabled: false
}
)
const instance = getCurrentInstance()
const picPaths = computed({
get() {
return props.modelValue
},
set(value) {
emits('update:modelValue', value)
}
})
const emits = defineEmits(['update:modelValue'])
const picFlag = ref(false)
// 点击选择图片
const handleClick = async () => {
const res = await useChooseMix(props.uploadType, props.chooseOptions)
picFlag.value = true
let picContent: any[] = []
res.forEach((item) => {
picContent.push(item)
})
// 重新赋值-需要先解构之前得moduleValue,因为可能只删除了一张图片,还有其余得图片,不能直接 picPaths.value = picContent
picPaths.value = [...props.modelValue, ...picContent]
}
// 关闭某一个图片/视频
const handleClose = (path: string) => {
picFlag.value = false
// 重新赋值
picPaths.value = props.modelValue.filter((item) => item.tempFilePath !== path)
}
// 添加水印代码
// 添加水印代码
// 添加水印代码
const canvasId = ref('canvasId')
function getCanvas(fileItem: any, context: string) {
return new Promise((parResolve) => {
var mycenter = 0 //文字左右居中显示
var myheight = 0 //文字高度
const query = uni.createSelectorQuery()
query
.in(instance)
.select('#' + canvasId.value)
.fields({ node: true, size: true }, (res) => {})
.exec((res) => {
const canvas = res[0].node
const ctx = canvas.getContext('2d')
new Promise(function (resolve) {
// 绘制背景图片
wx.getImageInfo({
src: fileItem.tempFilePath,
success(res) {
var width = res.width
var height = res.height
mycenter = width / 2
myheight = height / 2
canvas.width = width
canvas.height = height
const img = canvas.createImage()
img.src = res.path
img.onload = () => {
ctx.drawImage(img, 0, 0, width, height)
resolve(true)
}
}
})
})
.then(() => {
ctx.font = '700 100px Arial'
ctx.textAlign = 'center'
ctx.fillStyle = 'white'
ctx.fillText(context, mycenter, myheight - 50)
})
.then(function () {
transferCanvasToImage(canvas, fileItem, parResolve)
})
.catch((err) => {})
})
})
}
//canvas转为图片
function transferCanvasToImage(canvas: any, fileItem: any, parResolve: any) {
wx.canvasToTempFilePath({
canvas: canvas,
success(res) {
// canvasImg.value = res.tempFilePath
parResolve({
...fileItem,
tempFilePath: res.tempFilePath
})
}
})
}
defineExpose({
getCanvas
})
</script>
<template>
<view class="pic-item__container">
<text
class="iconfont icon-camera camera item"
@click="handleClick"
v-if="!props.disabled"
></text>
<view class="item" v-for="(item, index) in picPaths" :key="index">
<image
@click="handleImageScale(picPaths, index)"
v-if="item.fileType.includes('image')"
:src="item.tempFilePath"
mode="scaleToFill"
/>
<video
v-if="item.fileType.includes('video')"
:src="item.tempFilePath"
style="width: 100%; height: 100%"
:poster="item.tinyImageUrl"
></video>
<view class="visbale" @click="handleClose(item.tempFilePath)" v-if="!props.disabled">
<text class="iconfont icon-x close"></text>
</view>
</view>
<!-- 画水印 -->
<view style="width: 0rpx; height: 0rpx; overflow: hidden">
<canvas id="canvasId" type="2d" style="position: fixed; left: 9999px"></canvas>
</view>
</view>
</template>
<style scoped lang="scss">
.pic-item__container {
min-height: 200rpx;
@include flex(flex-start, center);
flex-wrap: wrap;
.camera {
font-size: 100rpx;
color: $custom-primary;
}
.item {
width: 199rpx;
height: 199rpx;
margin-bottom: $mr10;
margin-right: 17rpx;
@include flex(center, center);
position: relative;
image {
width: 100%;
height: 100%;
}
.visbale {
width: 28rpx;
height: 28rpx;
position: absolute;
top: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.7);
border-radius: 0 0 0 24rpx;
color: $custom-text-color;
.close {
position: absolute;
right: 1rpx;
color: $custom-inverse;
font-size: 24rpx;
}
}
}
}
</style>
// 方法使用
const handleSubmit = async () => {
// 校验
if (fileInfoRef.value.length === 0) {
return uni.showToast({
title: '请选择设备图片',
icon: 'none',
duration: 2000
})
}
// 此时我们已经选择了图片,并且通过chooseImage组件的v-model绑定到了父组件的fileInfoRef变量上,我们直接添加水印就好了
// 添加水印
const result = await useAddWatermark(fileInfoRef.value, chooseImageRef.value)
// 图片上传
const res = await useUploadFile(result)
// TODO: 提交表单
}