js-watermark

JavaScript 图片文字水印、图片图片水印生成工具,生成 base64 编码图片。

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greasyfork.org/scripts/452322/1413240/js-watermark.js

/**
 * @js-watermark.js
 * @author WhiteSev
 * @Created: 22-09-26
 * @repository: https://github.com/WhiteSevs/js-watermark
 * @forked by:https://github.com/gisonyeung/js-watermark
 * @description JavaScript 图片文字水印、图片图片水印生成工具,生成 base64 编码图片。
 */

(function (global, factory) {
  /**
   * 不使用define
   * typeof define === "function" && define.amd
   * define(factory)
   */
  if (typeof exports === "object" && typeof module !== "undefined") {
    /* 适用于NodeJs或typeScript */
    module.exports = factory();
  } else {
    global = typeof globalThis !== "undefined" ? globalThis : global || self;
    /* 适用于浏览器中,且this对象是window,如果this是其它,那么会在其它对象下注册对象 */
    global.Watermark = factory(global.Watermark);
  }
})(typeof window !== "undefined" ? window : this, function (AnotherWatermark) {
  "use strict";
  /**
   * @class
   */
  let Watermark = function () {};
  /**
   * @author zhangxinxu(.com)
   * @licence MIT
   * @description http://www.zhangxinxu.com/wordpress/?p=7362
   */
  /* api扩展-设置字符间距 */
  CanvasRenderingContext2D.prototype.letterSpacingText = function (
    text,
    x,
    y,
    letterSpacing
  ) {
    var context = this;
    var canvas = context.canvas;

    if (!letterSpacing && canvas) {
      letterSpacing = parseFloat(window.getComputedStyle(canvas).letterSpacing);
    }
    if (!letterSpacing) {
      return this.fillText(text, x, y);
    }

    var arrText = text.split("");
    var align = context.textAlign || "left";

    /* 这里仅考虑水平排列 */
    var originWidth = context.measureText(text).width;
    /* 应用letterSpacing占据宽度 */
    var actualWidth = originWidth + letterSpacing * (arrText.length - 1);
    /* 根据水平对齐方式确定第一个字符的坐标	 */
    if (align == "center") {
      x = x - actualWidth / 2;
    } else if (align == "right") {
      x = x - actualWidth;
    }

    /* 临时修改为文本左对齐	 */
    context.textAlign = "left";
    /* 开始逐字绘制	 */
    arrText.forEach(function (letter) {
      var letterWidth = context.measureText(letter).width;
      context.fillText(letter, x, y);
      /* 确定下一个字符的横坐标	 */
      x = x + letterWidth + letterSpacing;
    });
    /* 对齐方式还原	 */
    context.textAlign = align;
  };

  /* api扩展-自动换行 */
  CanvasRenderingContext2D.prototype.wrapText = function (
    text,
    x,
    y,
    maxWidth,
    lineHeight,
    stroke
  ) {
    if (
      typeof text != "string" ||
      typeof x != "number" ||
      typeof y != "number"
    ) {
      return;
    }

    var context = this;
    var canvas = context.canvas;

    if (typeof maxWidth == "undefined") {
      maxWidth = (canvas && canvas.width) || 300;
    }
    if (typeof lineHeight == "undefined") {
      lineHeight =
        (canvas && parseInt(window.getComputedStyle(canvas).lineHeight)) ||
        parseInt(window.getComputedStyle(document.body).lineHeight);
    }

    /* 字符分隔为数组	 */
    var arrText = text.split("");
    var line = "";

    for (var n = 0; n < arrText.length; n++) {
      var testLine = line + arrText[n];
      var metrics = context.measureText(testLine);
      var testWidth = metrics.width;
      if (testWidth > maxWidth && n > 0) {
        if (stroke) {
          context.strokeText(line, x, y, canvas.width);
        } else {
          context.fillText(line, x, y);
        }
        line = arrText[n];
        y += lineHeight;
      } else {
        line = testLine;
      }
    }
    if (stroke) {
      context.strokeText(line, x, y, canvas.width);
    } else {
      context.fillText(line, x, y);
    }
  };

  /* api扩展-垂直排列 */
  CanvasRenderingContext2D.prototype.fillTextVertical = function (text, x, y) {
    var context = this;
    var canvas = context.canvas;

    var arrText = text.split("");
    var arrWidth = arrText.map(function (letter) {
      return context.measureText(letter).width;
    });

    var align = context.textAlign;
    var baseline = context.textBaseline;

    if (align == "left") {
      x = x + Math.max.apply(null, arrWidth) / 2;
    } else if (align == "right") {
      x = x - Math.max.apply(null, arrWidth) / 2;
    }
    if (
      baseline == "bottom" ||
      baseline == "alphabetic" ||
      baseline == "ideographic"
    ) {
      y = y - arrWidth[0] / 2;
    } else if (baseline == "top" || baseline == "hanging") {
      y = y + arrWidth[0] / 2;
    }

    context.textAlign = "center";
    context.textBaseline = "middle";

    /* 开始逐字绘制 */
    arrText.forEach(function (letter, index) {
      /* 确定下一个字符的纵坐标位置 */
      var letterWidth = arrWidth[index];
      /* 是否需要旋转判断 */
      var code = letter.charCodeAt(0);
      if (code <= 256) {
        context.translate(x, y);
        /* 英文字符,旋转90° */
        context.rotate((90 * Math.PI) / 180);
        context.translate(-x, -y);
      } else if (index > 0 && text.charCodeAt(index - 1) < 256) {
        /* y修正 */
        y = y + arrWidth[index - 1] / 2;
      }
      context.fillText(letter, x, y);
      /* 旋转坐标系还原成初始态 */
      context.setTransform(1, 0, 0, 1, 0, 0);
      /* 确定下一个字符的纵坐标位置 */
      var letterWidth = arrWidth[index];
      y = y + letterWidth;
    });
    /* 水平垂直对齐方式还原 */
    context.textAlign = align;
    context.textBaseline = baseline;
  };

  /**
   * 加载File对象
   *
   * @param {object} file
   * @async
   */
  function loadFile(file) {
    let fileReader = new FileReader();
    return new Promise((res) => {
      fileReader.onloadend = async function (e) {
        res(e);
      };
      fileReader.readAsDataURL(file);
    });
  }

  /**
   * 加载Image对象
   *
   * @param {?string} src
   * @async
   */
  function loadImage(src) {
    let image = new Image();

    return new Promise((res) => {
      image.onload = () => {
        res(image);
      };
      image.src = src;
    });
  }
  /**
   * 检查坐标是否重复
   * @param {Array} arrayData
   * @param {number} x
   * @param {number} y
   * @returns {boolean}
   */
  function checkInArrayByPos(arrayData, x, y) {
    let flag = false;
    Array.from(arrayData).forEach((item) => {
      if (item["x"] == x && item["y"] == y) {
        flag = true;
        return;
      }
    });
    return flag;
  }
  /**
   * 获取文字占据的宽度,高度
   * @param {string} char
   * @param {object} style
   * @returns {{height:Number,width:Number}}
   */
  function getCharSizeByCanvas(char, style = {}) {
    let textCanvas = document.createElement("canvas");
    textCanvas.style.positon = "ablsolute";
    let textCTX = textCanvas.getContext("2d");
    let { fontSize = 14, fontFamily = "Microsoft Yahei" } = style;
    document.body.appendChild(textCanvas);
    textCTX.font = `${fontSize}px ${fontFamily}`;
    document.body.removeChild(textCanvas);
    let text = textCTX.measureText(char); /* TextMetrics object */
    textCTX.fillText(char, 50, 50);
    let result = {
      height: parseInt(fontSize),
      width: parseInt(text.width),
    };
    return result;
  }
  /**
   * 获取随机值
   * @param {Array} arr
   */
  function getRandValue(arr) {
    if (arr instanceof Array) {
      return arr[Math.floor(Math.random() * arr.length)];
    } else {
      return arr;
    }
  }
  /**
   * 通过 file 对象载入图片文件-异步
   * @async
   * @param {object} file
   */
  Watermark.prototype.setFile = function (file) {
    let that = this;
    return new Promise(async (res) => {
      var fileReader = await loadFile(file);
      await that.setImage(fileReader.target.result);
      res(true);
    });
  };
  /**
   * 通过 base64 载入图片文件-异步
   * @param {string} src
   * @async
   */
  Watermark.prototype.setImage = function (src) {
    this.dataUrl = src;
    let that = this;
    return new Promise(async (res) => {
      var image = await loadImage(src);
      that.sizes = {
        width: image.width,
        height: image.height,
      };

      var canvas = document.createElement("canvas");

      canvas.width = that.sizes.width;
      canvas.height = that.sizes.height;
      var ctx = canvas.getContext("2d");

      ctx.drawImage(image, 0, 0);
      image = null;
      that.canvas = canvas;
      res(true);
    });
  };

  /**
   * 获取是否存在图片对象
   * @returns {boolean}
   */
  Watermark.prototype.hasImage = function () {
    return !!this.dataUrl;
  };

  /**
   * 获取当前图片尺寸
   * @returns {number}
   */
  Watermark.prototype.getSize = function () {
    return this.sizes;
  };

  /**
   * 清空水印
   */
  Watermark.prototype.clearMark = function () {
    let that = this;
    if (typeof that.canvas === "undefined") {
      return;
    }

    function _clearMark_() {
      var ctx = that.canvas.getContext("2d");
      /* 清空画布 */
      ctx.clearRect(0, 0, that.canvas.width, that.canvas.height);
      var w = that.canvas.width;
      var h = that.canvas.height;
      that.canvas.width = w;
      that.canvas.height = h;
      /* 清除path路径 */
      ctx.beginPath();
      /* 重绘 */
      var image = new Image();
      image.src = that.dataUrl;
      ctx.drawImage(image, 0, 0);
      image = null;
    }
    _clearMark_();
  };

  /**
   * 添加文字水印(全屏)
   * @param {object} opts
   */
  Watermark.prototype.addText = function (opts) {
    var options = {
      text: ["Call By waterMark.addText"],
      fontSize: "6vw",
      fontFamily: "Microsoft Yahei",
      color: "#000000",
      textAlign: "center",
      /* 描边 */
      stroke: false,
      globalAlpha: 0.7,
      /* -360 ~ 360 */
      rotateAngle: 50,
      /* 必须大于0 */
      maxWidth: 100,
      /* 必须大于0 */
      xMoveDistance: 30,
      /* 必须大于0 */
      yMoveDistance: 30,
    };
    for (let key in options) {
      if (typeof opts[key] !== "undefined") {
        options[key] = opts[key];
      }
    }
    options.maxWidth = parseInt(options.maxWidth) > 0 ? options.maxWidth : 1;
    options.xMoveDistance =
      parseInt(options.xMoveDistance) > 0 ? options.xMoveDistance : 1;
    options.yMoveDistance =
      parseInt(options.yMoveDistance) > 0 ? options.yMoveDistance : 1;
    var ctx = this.canvas.getContext("2d");

    var fontSize = options.fontSize;
    fontSize = fontSize.toString();
    /* 转换 vw */
    if (~fontSize.indexOf("vw")) {
      fontSize = ((this.sizes.width / 100) * parseInt(fontSize)).toFixed(0);
    }
    fontSize = parseInt(fontSize);

    /* 绘制水印 */
    ctx.font = fontSize + "px " + options.fontFamily;
    ctx.fillStyle = options.color;
    ctx.textAlign = options.textAlign;
    ctx.globalAlpha = options.globalAlpha; /* 透明度 */

    let canvasWidth = this.sizes.width,
      /* 画布宽高 */
      canvasHeight = this.sizes.height;
    let rotateAngle = (options.rotateAngle * Math.PI) / 180;
    let xMoveDistance = options.xMoveDistance; /* 水平移动距离 */
    let yMoveDistance = options.yMoveDistance; /* 垂直移动距离 */
    let maxWidth = options.maxWidth; /* 文字最大宽度 */
    let lineHeight = fontSize; /* 文字占据高度 */
    let pos = [];
    for (var i = canvasWidth / 2; i < canvasWidth; i += xMoveDistance) {
      /* 右侧铺满 */
      for (var j = canvasHeight / 2; j < canvasHeight; j += yMoveDistance) {
        /* 右下 */
        if (!checkInArrayByPos(pos, i, j)) {
          pos = pos.concat({
            x: i,
            y: j,
          });
          ctx.setTransform(1, 0, 0, 1, 0, 0);
          ctx.translate(i, j);
          ctx.rotate(rotateAngle);
          ctx.wrapText(
            getRandValue(options.text),
            0,
            0,
            maxWidth,
            lineHeight,
            options.stroke
          );
        }
      }
      for (var k = canvasHeight / 2; k > 0; k -= yMoveDistance) {
        /* 右上 */
        if (!checkInArrayByPos(pos, i, k)) {
          pos = pos.concat({
            x: i,
            y: k,
          });
          ctx.setTransform(1, 0, 0, 1, 0, 0);
          ctx.translate(i, k);
          ctx.rotate(rotateAngle);
          ctx.wrapText(
            getRandValue(options.text),
            0,
            0,
            maxWidth,
            lineHeight,
            options.stroke
          );
        }
      }
    }

    for (var i = canvasWidth / 2; i > 0; i -= xMoveDistance) {
      /* 左侧铺满 */
      for (var j = canvasHeight / 2; j < canvasHeight; j += yMoveDistance) {
        /* 左下 */
        if (!checkInArrayByPos(pos, i, j)) {
          pos = pos.concat({
            x: i,
            y: j,
          });
          ctx.setTransform(1, 0, 0, 1, 0, 0);
          ctx.translate(i, j);
          ctx.rotate(rotateAngle);
          ctx.wrapText(
            getRandValue(options.text),
            0,
            0,
            maxWidth,
            lineHeight,
            options.stroke
          );
        }
      }
      for (var k = canvasHeight / 2; k > 0; k -= yMoveDistance) {
        /* 左上 */
        if (!checkInArrayByPos(pos, i, k)) {
          pos = pos.concat({
            x: i,
            y: k,
          });
          ctx.setTransform(1, 0, 0, 1, 0, 0);
          ctx.translate(i, k);
          ctx.rotate(rotateAngle);
          ctx.wrapText(
            getRandValue(options.text),
            0,
            0,
            maxWidth,
            lineHeight,
            options.stroke
          );
        }
      }
    }
  };

  /**
   * 添加像素文字水印(单个)
   * @param {object} opts
   */
  Watermark.prototype.addPixelText = function (opts) {
    var options = {
      text: "像素文字水印",
      /* 像素文字 */
      big: {
        fontSize: 150,
        fontFamily: "微软雅黑",
        textAlign: "center",
        rotateAngle: 0,
        /* 描边 */
        stroke: false,
      },
      /* 绘制像素的文字 */
      small: {
        fontSize: 10,
        fontFamily: "微软雅黑",
        color: "#000",
        textAlign: "center",
        globalAlpha: 0.7,
      },
    };
    for (let key in options) {
      if (typeof opts[key] !== "undefined") {
        options[key] = opts[key];
      }
    }
    var ctx = this.canvas.getContext("2d");
    var tmpCanvas = document.createElement("canvas");
    var tmpctx = tmpCanvas.getContext("2d");
    tmpCanvas.width = this.sizes.width;
    tmpCanvas.height = this.sizes.height;
    tmpctx.font = options.big.fontSize + "px " + options.big.fontFamily;
    tmpctx.textAlign = options.big.textAlign;
    tmpctx.textBaseline = "middle";
    tmpctx.translate(tmpCanvas.width / 2, tmpCanvas.height / 2);
    tmpctx.rotate((options.big.rotateAngle * Math.PI) / 180);
    tmpctx.translate(-tmpCanvas.width / 2, -tmpCanvas.height / 2);
    if (options.big.stroke) {
      tmpctx.strokeText(
        options.text,
        tmpCanvas.width / 2,
        tmpCanvas.height / 2,
        tmpCanvas.width
      );
    } else {
      tmpctx.fillText(options.text, tmpCanvas.width / 2, tmpCanvas.height / 2);
    }

    var textArray = options.text.split("");
    var textPixleInfo = tmpctx.getImageData(
      0,
      0,
      tmpCanvas.width,
      tmpCanvas.height
    );
    var pixelArray = [];
    for (var i = 0; i < tmpCanvas.height; i += options.small.fontSize) {
      for (var j = 0; j < tmpCanvas.width; j += options.small.fontSize) {
        var index = j + i * tmpCanvas.width;
        var a = textPixleInfo.data[index * 4 + 3];
        if (a > 128) {
          //存入数组
          pixelArray.push({
            text: getRandValue(textArray),
            x: j,
            y: i,
          });
        }
      }
    }

    ctx.font = options.small.fontSize + "px " + options.small.fontFamily;
    ctx.fillStyle = options.small.color;
    ctx.textAlign = options.small.textAlign;
    ctx.textBaseline = "middle";
    ctx.globalAlpha = options.small.globalAlpha;
    pixelArray.forEach((item) => {
      ctx.fillText(item.text, item.x, item.y);
    });
  };

  /**
   * 添加图片水印(全屏)
   * @param {object} opts
   * @returns
   */
  Watermark.prototype.addImage = function (opts) {
    if (opts.imageArray == null) {
      alert("参数缺少imageArray");
      return false;
    }
    if (opts.imageArray.length === 0) {
      alert("参数imageArray不能为空");
      return false;
    }
    let options = {
      imageArray: [],
      /* 里面为水印Image对象 */
      width: 50,
      /* 必须大于0 */
      height: 50,
      /* 必须大于0 */
      globalAlpha: 0.5,
      rotateAngle: 0,
      xMoveDistance: 70,
      /* 必须大于0 */
      yMoveDistance: 70,
      /* 必须大于0 */
    };
    for (let key in options) {
      if (typeof opts[key] !== "undefined") {
        options[key] = opts[key];
      }
    }
    options.width = parseInt(options.width) > 0 ? options.width : 1;
    options.height = parseInt(options.height) > 0 ? options.height : 1;
    options.xMoveDistance =
      parseInt(options.xMoveDistance) > 0 ? options.xMoveDistance : 1;
    options.yMoveDistance =
      parseInt(options.yMoveDistance) > 0 ? options.yMoveDistance : 1;
    let ctx = this.canvas.getContext("2d");

    let waterImageCanvasArray = [];
    let waterImageCanvasDiagonal = parseInt(
      Math.sqrt(options.width * options.width + options.height * options.height)
    ); /* 水印对角线 */

    let canvasWidth = this.sizes.width,
      /* 画布宽高 */
      canvasHeight = this.sizes.height;
    let rotateAngle = (options.rotateAngle * Math.PI) / 180; /* 旋转角度 */
    let xMoveDistance = options.xMoveDistance; /* 水平移动距离 */
    let yMoveDistance = options.yMoveDistance; /* 垂直移动距离 */

    let centerDrawLeftPosX =
      canvasWidth / 2 -
      waterImageCanvasDiagonal / 2; /* 中心的绘制水印的左上角坐标x */
    let centerDrawLeftPosY =
      canvasHeight / 2 -
      waterImageCanvasDiagonal / 2; /* 绘制水印的左上角坐标y */
    let waterDrawPosX =
      (waterImageCanvasDiagonal - options.width) / 2; /* 水印里图片坐标x */
    let waterDrawPosY =
      (waterImageCanvasDiagonal - options.height) / 2; /* 水印里图片坐标y */

    Array.from(options.imageArray).forEach((item) => {
      /* 先把水印绘制好 */
      var waterImageCanvas = document.createElement("canvas");
      var waterctx = waterImageCanvas.getContext("2d");

      waterImageCanvas.width = waterImageCanvasDiagonal;
      waterImageCanvas.height = waterImageCanvasDiagonal;
      waterctx.globalAlpha = options.globalAlpha; /* 透明度 */
      waterctx.translate(
        waterImageCanvasDiagonal / 2,
        waterImageCanvasDiagonal / 2
      );
      waterctx.rotate(rotateAngle);
      waterctx.translate(
        -waterImageCanvasDiagonal / 2,
        -waterImageCanvasDiagonal / 2
      );
      waterctx.drawImage(
        item,
        waterDrawPosX,
        waterDrawPosY,
        options.width,
        options.height
      );

      waterImageCanvasArray = waterImageCanvasArray.concat(waterImageCanvas);
    });

    function randomArrayData(array_data) {
      /* 随机项 */
      return array_data[Math.floor(Math.random() * array_data.length)];
    }
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    let pos = [];
    for (let i = centerDrawLeftPosX; i < canvasWidth; i += xMoveDistance) {
      /* 右侧铺满 */
      for (let j = centerDrawLeftPosY; j < canvasHeight; j += yMoveDistance) {
        /* 右下 */
        if (!checkInArrayByPos(pos, i, j)) {
          pos = pos.concat({
            x: i,
            y: j,
          });
          ctx.drawImage(
            randomArrayData(waterImageCanvasArray),
            i,
            j
          ); /* 绘制水印 */
        }
      }
      for (
        let k = centerDrawLeftPosY;
        k > -Math.abs(waterImageCanvasDiagonal);
        k -= yMoveDistance
      ) {
        /* 右上 */
        if (!checkInArrayByPos(pos, i, k)) {
          pos = pos.concat({
            x: i,
            y: k,
          });
          ctx.drawImage(randomArrayData(waterImageCanvasArray), i, k);
        }
      }
    }
    for (
      let i = centerDrawLeftPosX;
      i > -Math.abs(waterImageCanvasDiagonal);
      i -= xMoveDistance
    ) {
      /* 左侧铺满 */
      for (let j = centerDrawLeftPosY; j < canvasHeight; j += yMoveDistance) {
        /* 左下 */
        if (!checkInArrayByPos(pos, i, j)) {
          pos = pos.concat({
            x: i,
            y: j,
          });
          ctx.drawImage(randomArrayData(waterImageCanvasArray), i, j);
        }
      }
      for (
        let k = centerDrawLeftPosY;
        k > -Math.abs(waterImageCanvasDiagonal);
        k -= yMoveDistance
      ) {
        /* 左上 */
        if (!checkInArrayByPos(pos, i, k)) {
          pos = pos.concat({
            x: i,
            y: k,
          });
          ctx.drawImage(randomArrayData(waterImageCanvasArray), i, k);
        }
      }
    }
  };

  /**
   * 获得原图
   * @returns {?string}
   */
  Watermark.prototype.getPreview = function () {
    return this.dataUrl;
  };

  /**
   * 绘制图片
   * @param {string} type png|jpeg
   * @returns
   */
  Watermark.prototype.render = function (type) {
    type = type === "png" ? "png" : "jpeg";
    return this.canvas.toDataURL("image/" + type);
  };

  /**
   * 绘制图片Blob Url-异步
   * @async
   */
  Watermark.prototype.renderBlob = function () {
    let that = this;
    return new Promise((res) => {
      that.canvas.toBlob(function (blob) {
        res(window.URL.createObjectURL(blob));
      });
    });
  };

  /**
   * 释放控制权
   * @returns {Watermark}
   */
  Watermark.prototype.noConflict = function () {
    if (window.Watermark) {
      delete window.Watermark;
    }
    if (AnotherWatermark) {
      window.Watermark = AnotherWatermark;
    }
    return Watermark;
  };
  return Watermark;
});