FORMA

图片上传前的自定义水印实践

背景

项目需要在图片上传前叠加动态水印。水印内容不是固定文案,而是由多个动态元素组成(如时间、地点、业务字段等)。

虽然七牛云支持图片水印能力,但更适合静态水印或固定参数场景。对于“多元素动态组合”场景,如果完全依赖云端参数拼接,通常会遇到:

  • 动态内容表达能力有限;
  • 需要额外生成并维护水印资源;
  • 存储与流量成本增加。

因此,本方案采用“前端本地合成水印后再上传”的方式,降低云端处理复杂度与成本。

水印效果示意:

效果


方案一:传统 canvas 直接绘制

思路是将原图绘制到 canvas,再通过 fillText/drawImage 绘制水印元素。

优点:

  • 依赖少、性能可控;
  • 输出过程清晰。

不足:

  • 复杂水印(多行文本、图标、富样式)需要手动计算大量坐标;
  • 布局变化时维护成本较高。

对于动态字段较多、样式复杂的场景,不是最优方案。


方案二:使用 html2canvas 组合 DOM 后导出

该方案适合“水印样式复杂、元素动态”的场景。

核心流程:

  1. 在页面中准备一个“离屏渲染容器”(可见区域外,但仍可被渲染);
  2. 上传前将原图转为对象 URL,并等待图片加载完成;
  3. 将原图与水印 DOM 组合到离屏容器,并设置为原图尺寸;
  4. 使用 html2canvas 将容器转为 canvas
  5. canvas -> blob -> File,得到可上传的新文件。

示例代码:

javascript
this.getDom("#file-input").addEventListener("change", function (e) {
  // 仅演示单图上传场景
  const file = e.target.files[0];
  if (!file) return;

  const tempImg = new Image();
  tempImg.src = URL.createObjectURL(file);

  tempImg.addEventListener("load", function () {
    const node = _this.getDom("#node");
    const imgDom = _this.getDom("img");

    // 将原图写入离屏容器,并同步尺寸
    imgDom.setAttribute("src", tempImg.src);
    node.style.width = `${tempImg.width}px`;
    node.style.height = `${tempImg.height}px`;

    html2canvas &&
      html2canvas(node).then(function (canvas) {
        canvas.toBlob((blob) => {
          if (!blob) return;

          // 生成可上传文件
          const newFile = new File([blob], file.name, { type: file.type });
          console.log(newFile);

          // 本地预览
          const imgSrc = canvas.toDataURL("image/jpeg", 1);
          _this.getDom(".temp-container").innerHTML = `
            <img
              class="show"
              style="width:${tempImg.width}px;height:${tempImg.height}px"
              src="${imgSrc}"
              alt=""
            />
          `;
        });
      });
  });
});

方案三:SVG 片段 + canvas 合并

如果水印结构相对固定,也可以先将水印模板写成 SVG,再与原图绘制到同一个 canvas 中。

核心步骤:

  1. 生成水印 SVG 片段;
  2. SVG 转为 Image
  3. 先绘制原图,再在指定位置绘制水印图层;
  4. 导出新文件并上传。

示例代码:

javascript
// 水印 SVG 片段
const data = `
  <svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
    <foreignObject width="100%" height="100%">
      <div xmlns="http://www.w3.org/1999/xhtml">
        <div style="color:red">这里是水印元素</div>
      </div>
    </foreignObject>
  </svg>
`;

const imgSvg = new Image();
const blob = new Blob([data], { type: "image/svg+xml;charset=utf-8" });

function img2Canvas(img) {
  const canvas = document.createElement("canvas");
  canvas.width = img.width;
  canvas.height = img.height;
  const ctx = canvas.getContext("2d");

  ctx.drawImage(img, 0, 0);

  const svgUrl = URL.createObjectURL(blob);
  imgSvg.onload = function () {
    ctx.drawImage(imgSvg, 20, canvas.height - 120);
    URL.revokeObjectURL(svgUrl);
  };
  imgSvg.src = svgUrl;

  return canvas;
}

this.getDom("#file-input2").addEventListener("change", function (e) {
  const file = e.target.files[0];
  if (!file) return;

  const tempImg = new Image();
  tempImg.src = URL.createObjectURL(file);

  tempImg.addEventListener("load", () => {
    img2Canvas(tempImg).toBlob((blob) => {
      if (!blob) return;
      const newFile = new File([blob], file.name, { type: file.type });
      console.log(newFile);
    });
  });
});

实施建议

  • 图片处理完成后及时调用 URL.revokeObjectURL 释放对象 URL;
  • 对超大图增加压缩或尺寸限制,避免浏览器内存峰值过高;
  • 优先异步处理并给出上传中状态,避免主线程卡顿;
  • 若涉及跨域图片,请确保资源支持 CORS,否则 canvas 可能被污染。