使用canvas给照片添加富文本水印(文本、图片,自由排版)

394 阅读4分钟

一.需求描述

在h5页面,调用手机摄像头拍照后,给照片添加水印(文本、图片)

水印大概内容是:

第一行是“拍摄人:xxx”、“拍摄时间:2023.11.22”; 第二行是“所在单位:xxxx有限公司”、“所在部门:万xxxx”; 第三行是“logo: 公司的logo图片”,图片尺寸是50px*50px;

样式布局要求: 水印在照片底部; 距离照片边缘留30px的空白;

每行的两组文字两边对齐;

左右侧文字不能重叠,中间最小间隔是30,文字多自动换行;

文字大小是20,行高24,颜色是#333;

image.png

二.需求难点分析

前端添加水印,本质是图片处理,必然是通过canvas来实现。

难点在于样式布局,据我了解,canvas并没有主流的针对于前端的封装库(类似jquery之于js),使用canvas来实现类似css的布局(见.背景-样式布局要求)是很繁琐的过程。

三.核心方案

方案一,先使用html+css渲染水印dom1,然后使用css定位将dom1定位到照片dom2的上层,然后使用html2Canvas将dom1+dom2整体转为图片。

该方案优点是水印部分使用css布局,简单高效;

缺点是水印存在清晰度问题,因为水印是先渲染到页面上的,受屏幕分辨率影响(类似于截屏);同时,html2Canvas插件生成的图片比较大。

方案二,使用canvas将富文本逐条绘制到照片上面,需要较为繁琐的canvas操作(使用canvas代替css布局),需要对canvas的api较熟悉。

四.详细示例(方案二)

需求描述:在照片上添加5组文字,分为3行排列。
第一行是“拍摄人:xxx”、“拍摄时间:2023.11.22”;
第二行是“所在单位:xxxx有限公司”、“所在部门:xxxx”;
第三行是“logo: 公司的logo图片”,图片尺寸是50px*50px

样式要求:
水印在照片底部,
距离照片边缘留20的空白
每行的两组文字两边对齐,
左右侧文字不能重叠,中间最小间隔是30,文字多自动换行
文字大小是20,行高24,颜色是#333,

// js部分
const padding = 30
const logoSize = {w: 50, h:50}
const fontSize = 20;
const lineHeight = fontSize * 1.2;
ctx.fillStyle = '#333';
let maxWidth // 左右文字换行的宽度,
let y // 每次绘制时的纵坐标
document.getElementById('upload').addEventListener('change', async function(e) {
  if (e.target.files && e.target.files[0]) {
    const file = e.target.files[0];
    const reader = new FileReader();

    reader.onload = async function(event) {
      const img = await loadImage(event.target.result);
      const logoImg = await loadImage('/img/logo.png'); // logo图片的路径
      const canvas = document.getElementById('canvas');
      canvas.width = img.width;
      canvas.height = img.height;
      const ctx = canvas.getContext('2d');
      ctx.drawImage(img, 0, 0);
      // 截止到这里,和方案一一致

      // 设置水印样式
      maxWidth = img.width - (padding * 2) - padding; // 考虑两侧30的间距,左右文字中间30
      // 计算初始y坐标
      y = img.height - padding; // 距底部30px

      // 第三行水印 - 文本和图片
      y -= logoSize.h; // 考虑logo高度
      const textWidth = ctx.measureText('店铺logo: ').width;
      ctx.fillText('店铺logo: ', padding, y);
      ctx.drawImage(logoImg, padding + textWidth, y, logoSize.w, logoSize.h);

      // 第二行水印
      y -= lineHeight; // 暂时不考虑第三行文字换行且比logo还高
      let leftLines = getTextLineNum(ctx, '所在单位:xxxx有限公司')
      let rightLines = getTextLineNum(ctx, '所在部门:xxxx')
      // 取leftLines rightLines较大的来计算起始y
      y -= (Math.max(leftLines, rightLines) - 1) * lineHeight
      wrapText(ctx, leftLines, padding, y, maxWidth / 2, lineHeight, 'left');
      wrapText(ctx, rightLines, img.width - padding, y, maxWidth / 2, lineHeight, 'right');

      y -= lineHeight;
      leftLines = getTextLineNum(ctx, '拍摄人:xxx')
      rightLines = getTextLineNum(ctx, '拍摄时间:2023-11-22')
      y -= (Math.max(leftLines, rightLines) - 1) * lineHeight
      wrapText(ctx, leftLines, padding, y, maxWidth / 2, lineHeight, 'left');
      wrapText(ctx, rightLines, img.width - padding, y, maxWidth / 2, lineHeight, 'right');

      // 显示水印图片
      const watermarkedImage = document.getElementById('watermarkedImage');
      watermarkedImage.src = canvas.toDataURL('image/jpeg');
      canvas.style.display = 'none'; // 隐藏canvas元素
    }
    reader.readAsDataURL(file);
  }
});

// 计算当前文字需要几行绘制
function getTextLineNum(ctx, text) {
  let words = text.split(' ');
  let line = '';
  let lines = []; // 每个元素是一行文字

  for (let n = 0; n < words.length; n++) {
    let testLine = line + words[n] + ' ';
    let metrics = ctx.measureText(testLine);
    let testWidth = metrics.width;
    if (testWidth > maxWidth && n > 0) {
      lines.push(line);
      line = words[n] + ' ';
    } else {
      line = testLine;
    }
  }
  lines.push(line);
  return lines.length // 返回需要绘制几行
}

// 绘制文字
function wrapText(ctx, lines, x, y, lineHeight, align = 'left') {
  ctx.textAlign = align;
  // 把lines按行绘制
  y -= (lines.length - 1) * lineHeight // 假如需要绘制3行,那y需要在原基础上再往上移动2个lineHeight
  for (let i = 0; i < lines.length; i++) {
    ctx.fillText(lines[i], x, y);
    y += lineHeight;
  }
}


// html部分
<!DOCTYPE html>
<html>
<head>
    <title>图片添加多行文本水印</title>
</head>
<body>
    <input type="file" accept="image/*" id="upload">
    <canvas id="canvas" style="display: none;"></canvas>
    <img id="watermarkedImage" style="max-width: 100%;">
    <script src="path_to_your_script.js"></script>
</body>
</html>

五.拓展

上述是添加富文本水印,有些情况是添加图片水印。

这个相对于富文本要简单一些,因为不需要考虑自定义排版,本质是将两张图片合并为一张,logo图片显示在上层。

document.getElementById('upload').addEventListener('change', function(e) {
  if (e.target.files && e.target.files[0]) {
    const file = e.target.files[0]; // 拿到上传的图片

    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = function(event) {
      const img = new Image();
      img.src = event.target.result;
      img.onload = function() {
        const canvas = document.getElementById('canvas');
        canvas.width = img.width;
        canvas.height = img.height;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0);
        // 到这里,上传的照片已经绘制到canvas
        
        // 加载水印图片
        const watermark = new Image();
        watermark.src = 'xxx'; // 水印图片的路径
        watermark.onload = function() {
          // 水印尺寸
          const watermarkWidth = 50;
          const watermarkHeight = 30;
          // 水印位置
          const x = 20;
          const y = img.height - watermarkHeight - 20;
          // 绘制水印图片
          ctx.drawImage(watermark, x, y, watermarkWidth, watermarkHeight);
        };
      }
    }
  }
});