如何优雅的给图片添加水印

2,626 阅读2分钟

我之前写过一篇文章记录了一次上传图片的优化史,计一次vant组件图片上传优化史
还是上次的项目,现在有了一个新需求,就是要给上传的图片添加水印,水印的内容是图片的时间+上传人+系统Logo+系统名称
这个需求还是比较难处理的,在这之前添加水印一直都是后端处理的,现在希望可以尝试一下在前端处理
而且现在项目里使用的是OSS直接上传的方式,为了减轻服务器的压力,上传的操作是不经过自己的服务器中转的
既然没办法甩给后端处理,那就开干吧,想一想尝试做没做过的事还是挺兴奋的

先来第一版,使用canvas填充文字的方式

/**
 * 添加水印
 * @param {file} 上传的图片文件
 */
async function addWaterMarker(file) {
  // 先将文件转成img标签
  let img = await blobToImg(file)
  return new Promise((resolve, reject) => {
    // 创建canvas画布
    let canvas = document.createElement('canvas')
    canvas.width = img.width
    canvas.height = img.height
    let ctx = canvas.getContext('2d')
    ctx.drawImage(img, 0, 0)

    // 设置填充字号和字体,样式,这里设置字体大小根据canvas的宽度等比缩放,防止大图片生成的水印很小的问题
    ctx.font = `${canvas.width * 0.05}px 宋体`
    ctx.fillStyle = "red"
    // 设置右对齐
    ctx.textAlign = 'right'
    // 在指定位置绘制文字
    ctx.fillText('我是水印1', canvas.width - 100, canvas.height - 100)
    ctx.fillText('我是水印2', canvas.width - 100, canvas.height - 50)

    // 将canvas转成blob文件返回
    canvas.toBlob(blob => resolve(blob))
  })
}

/**
* blob转img标签
*/
function blobToImg(blob) {
  return new Promise((resolve, reject) => {
    let reader = new FileReader()
    reader.addEventListener('load', () => {
      let img = new Image()
      img.src = reader.result
      img.addEventListener('load', () => resolve(img))
    })
    reader.readAsDataURL(blob)
  })
}

水印确实加上去了,如果水印只是简单布局的文字,这也可以用
但思前想后总觉得不优雅,加上水印内容比较复杂,还有图片,靠这种方式要实现还是够呛的

经过苦苦折腾,第二版搞出来了,水印内容通过html转成图片,然后把水印图片合成到上传的图片中

/**
 * 添加水印
 * @param {file} 上传的图片文件
 * @param {el} 水印内容html
 */
async function addWaterMarker(file, el = '#markImg') {
  // 先将文件转成img标签
  let img = await blobToImg(file)
  return new Promise(async (resolve, reject) => {
    try {
      // 创建canvas画布
      let canvas = document.createElement('canvas')
      canvas.width = img.width
      canvas.height = img.height
      let ctx = canvas.getContext('2d')
      ctx.drawImage(img, 0, 0)

      // 创建水印图片canvas画布
      let markCanvas = document.createElement('canvas')
      // 创建水印图片canvas
      const markImg = await createMarkImg(document.querySelector(el))
      //让水印根据图片尺寸等比缩放,默认宽度设成1000
      let zoom = canvas.width / 1000
      let markCtx = markCanvas.getContext('2d')
      // 缩放水印图片canvas
      markCtx.scale(zoom, zoom)

      // 将水印画布的宽高设置成缩放后的水印图片canvas的宽高
      markCanvas.width = markImg.width
      markCanvas.height = markImg.height
      // 将水印图片canvas填充到水印画布中
      markCtx.drawImage(markImg, 0, 0)

      // 将水印画布canvas填充到canvas画布
      ctx.drawImage(markCanvas, canvas.width - markCanvas.width, canvas.height - markCanvas.height, markCanvas.width, markCanvas.height)
      canvas.toBlob(blob => resolve(blob))
    } catch (error) {
      reject(error)
    }
  })
}

/**
* blob转img标签
*/
function blobToImg(blob) {
  return new Promise((resolve, reject) => {
    let reader = new FileReader()
    reader.addEventListener('load', () => {
      let img = new Image()
      img.src = reader.result
      img.addEventListener('load', () => resolve(img))
    })
    reader.readAsDataURL(blob)
  })
}

/**
* 创建水印canvas,需要安装html2canvas.js插件
*/
function createMarkImg(el) {
  return new Promise(async (resolve, reject) => {
    try {
      const markImg = await html2canvas(el, {
        allowTaint: false,   //允许污染
        useCORS: true,
        backgroundColor: null//'transparent'  //背景色
      })
      resolve(markImg)
    } catch (error) {
      reject(error)
    }
  })
}

写得虽然复杂了点,但是很好用,水印内容使用html+css先画好,然后直接合成到图片的指定位置就行了,省心很多
但人有时候就是爱折腾,头天写完的代码第二天就是觉得很别扭,还是觉得不够优雅,自我总结至少有以下两个问题

  1. 水印部分为了实现缩放使用了两层canvas,对性能有一定的耗损,不妥
  2. 上传的图片加了水印后体积大了有两三倍,这还是在上传图片之前先进行压缩的前提下,这无疑会大大增加上传图片的速度

再次折腾,暂定的终极版终于出炉,代码如下

/**
 * 添加水印
 */
export async function addWaterMarker(file, el = '#markImg') {
  // 将文件blob转换成图片
  let img = await blobToImg(file)
  return new Promise(async (resolve, reject) => {
    try {
      // 创建canvas画布
      let canvas = document.createElement('canvas')
      //等比例调整canvas宽高,以缩小图片体积
      let imgRatio = img.naturalWidth / img.naturalHeight //图片比例
      canvas.width = 750  //默认设置成750
      canvas.height = canvas.width / imgRatio

      let ctx = canvas.getContext('2d')

      // 填充上传的图片
      ctx.drawImage(img, 0, 0, canvas.width, canvas.height)

      // 生成水印图片
      const markWidth = document.querySelector(el).clientWidth
      let zoom = canvas.width * 0.3 / markWidth
      let markEle = document.querySelector(el)
      // 先缩放水印html再转成图片
      markEle.style.transform = `scale(${zoom})`
      const markImg = await htmlToCanvas(markEle)

      // 填充水印
      ctx.drawImage(markImg, canvas.width - markImg.width - 15 * zoom, canvas.height - markImg.height - 15 * zoom, markImg.width, markImg.height)

      // 将canvas转换成blob
      canvas.toBlob(blob => resolve(blob))
    } catch (error) {
      reject(error)
    }

  })
}

/**
* blob转img标签
*/
function blobToImg(blob) {
  return new Promise((resolve, reject) => {
    let reader = new FileReader()
    reader.addEventListener('load', () => {
      let img = new Image()
      img.src = reader.result
      img.addEventListener('load', () => resolve(img))
    })
    reader.readAsDataURL(blob)
  })
}

/**
* html转成canvas,需要安装html2canvas.js插件
*/
export function htmlToCanvas(el, backgroundColor = 'rgba(0,0,0,.1)') {
  return new Promise(async (resolve, reject) => {
    try {
      const markImg = await html2canvas(el, {
        allowTaint: false,   //允许污染
        useCORS: true,
        backgroundColor //'transparent'  //背景色
      })
      resolve(markImg)
    } catch (error) {
      reject(error)
    }
  })
}

至此,上面两个问题也算是完美解决,如果图片体积还是过大,适当调整canvas的宽度即可,下面看一下图片加上水印后的效果