前言 📝

当你在微信小程序中制作照片时,你可能需要添加水印,以便在分享时保护你的照片,并标识出你的品牌或网站。本文将介绍如何在uniapp中添加照片水印(微信小程序大同小异)。操作 canvas 相关的 api 使用的是微信最新提供的 (一路过来踩了好多坑…)

1. 我的环境

微信开发者工具: 1.06.2402040
调试基础库:3.4.1

我的项目是采用uniapp脚手架搭建的,开发者工具是vscode微信开发者工具是用来展示小程序样式的。

2. 选择图片和上传图片

由于我之前并不知道有添加水印的需求,我就在原来封装好的选择图片和上传图片的基础上更改(如果这两个方法和我的不一样,也不会影响到水印的方法,只是需要略微调整。我将上传的代码例举出来就是为了大家结合上下文,方便理解)。先来看一下之前封装的选择图片和上传图片的代码。

2.1 选择图片或者视频

uniapp的chooseMediaAPI传送门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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)
      }
    })
  })
}
1
2
3
4
5
6
// 方法使用
const handleClick = async () => {
  const res = await useChooseMix()

// TODO: 选择图片之后根据微信返回的结果在进行后面的操作
}

2.2 上传图片或视频

uniapp的uploadFileAPI传送门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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
2
3
4
5
// 方法使用
// 1. 选择图片
const tempRes = await useChooseMix('image')
// 2. 上传图片
const res = await useUploadFile(tempRes)

3. 添加水印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 * 添加水印方法调用
 * @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。所以在父组件中我们就可以得到所有在微信小程序中选择的图片。

1
2
3
4
5
6
7
<!-- fileInfoRef就是通过微信选择图片api返回的内容 -->
<ChooseImage
style="width: 100%"
v-model="fileInfoRef"
ref="chooseImageRef"
upload-type="image"
></ChooseImage>

uniapp的createSelectorQueryAPI传送门
微信小程序的getImageInfoAPI传送门
微信小程序的canvasToTempFilePathAPI传送们

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 方法使用
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: 提交表单

 }