🥕 uniapp开发微信小程序添加水印

1,691 阅读2分钟

原文地址: alvis.org.cn/posts/ee12f…

前言 📝

当你在微信小程序中制作照片时,你可能需要添加水印,以便在分享时保护你的照片,并标识出你的品牌或网站。本文将介绍如何在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: 提交表单

 }