hexo 图片添加水印(png, jpeg, jpg, gif)

文章同步发布:https://blog.jijian.link/2020-04-21/hexo-watermark/

本文折腾 hexo 图片添加水印功能,大部分代码沿用: nodejs 图片添加水印(png, jpeg, jpg, gif)

方案一

使用现有插件:https://github.com/SpiritLing/hexo-images-watermark

问题:依赖 sharp 安装困难

方案二

使用 jimp 造个轮子

本文仅处理图片水印,文字水印请参考后文介绍

步骤

1. 安装依赖

npm install jimp gifwrap --save

2. 新建文件 themes/landscape/scripts/image_watermark.js

const { deepMerge } = require(‘hexo-util‘);
const watermark = require(‘../../../component/watermark/index‘);

const defaultOptions = {
  // 保存的图片质量
  quality: 80,
  // 图片宽度小于 100 时不加水印
  minWidth: 100,
  // 图片高度小于 100 时不加水印
  minHeight: 100,
  // 旋转
  rotate: 0,
  // 水印 logo 图片
  logo: ‘‘,

  // 需要添加的图片类型
  include: [‘*.jpg‘, ‘*.jpeg‘, ‘*.png‘, ‘*.gif‘],
  // 文件名为  .watermark.png 禁止添加水印图片
  exclude: [‘*.watermark.*‘],
  // 文章链接,非文章链接不加水印
  articlePath: /^\d{4}-\d{2}-\d{2}/,
};

hexo.config.watermark = deepMerge(defaultOptions, hexo.config.watermark);
hexo.extend.filter.register(‘after_generate‘, watermark);

3. 新建文件 component/watermark/index.js

const fs = require(‘fs‘);
const { isMatch } = require(‘micromatch‘);
const { extname } = require(‘path‘);
const Promise = require(‘bluebird‘);
const { img, gif } = require(‘./watermark‘);

const getBuffer = (hexo, path) => {
  return new Promise((resolve) => {
    const stream = hexo.route.get(path);
    const arr = [];
    stream.on(‘data‘, chunk => arr.push(chunk));
    stream.on(‘end‘, () => resolve(Buffer.concat(arr)));
  });
}

const getExtname = str => {
  if (typeof str !== ‘string‘) return ‘‘;

  const ext = extname(str) || str;
  return ext[0] === ‘.‘ ? ext.slice(1) : ext;
};

module.exports = function () {
  const hexo = this;
  const config = hexo.config.watermark;

  if (!fs.existsSync(config.logo)) {
    // 带颜色的输出: https://www.jianshu.com/p/cca3e72c3ba7
    return console.log(‘\033[41;30m ERROR \033[40;31m Add watermark no logo image found \033[0m‘);
  }

  const route = hexo.route;

  const { include, exclude, articlePath } = config;

  // exclude image
  const routes = route.list().filter((path) => {
    // 如果文件没修改,则不再加水印
    if (!route.isModified(path)) {
      return false;
    }
    if (!articlePath.test(path)) {
      return false;
    }
    if (isMatch(path, exclude, { basename: true })) {
      return false;
    }
    return isMatch(path, include, {
      basename: true
    });
  });
  // 用 Promise 延迟执行,否则 build 命令水印在图片生成前执行会被覆盖
  return Promise.map(routes, async (path) => {
    const ext = getExtname(path);
    const buffer = await getBuffer(hexo, path);
    const arg = {
      input: buffer,
      logo: config.logo,
      quality: config.quality,
      rotate: config.rotate,
      minWidth: config.minWidth,
      minHeight: config.minHeight,
    };
    const newBuffer = ext === ‘gif‘ ? await gif(arg) : await img(arg);
    if (!newBuffer) {
      return;
    }
    route.set(path, newBuffer);
  });
}

4. 新建文件 component/watermark/watermark.js

const Jimp = require(‘jimp‘);
const { GifUtil, GifCodec } = require(‘gifwrap‘);
const trueTo256 = require(‘./trueTo256‘);

// 水印距离右下角百分比
const LOGO_MARGIN_PERCENTAGE = 5 / 100;

function getXY (img, logoImage) {
  // 如果logo小于图片 8/10 ,取 img.width * (8 / 10) 与图片宽度的最小值缩放
  logoImage.resize(Math.min(logoImage.bitmap.width, img.width * (8 / 10)), Jimp.AUTO);

  const margin = Math.min(img.width * LOGO_MARGIN_PERCENTAGE, img.height * LOGO_MARGIN_PERCENTAGE, 20);

  const X = img.width - logoImage.bitmap.width - margin;
  const Y = img.height - logoImage.bitmap.height - margin;

  return {
    X,
    Y,
  };
}

async function gif({
  input = ‘‘,
  logo = ‘‘,
  quality = 80,
  rotate = 0,
} = {}) {
  const inputGif = await GifUtil.read(input);
  const logoImage = await Jimp.read(logo);

  logoImage.rotate(rotate);

  const { X, Y } = getXY({
    width: inputGif.width,
    height: inputGif.height,
  }, logoImage);

  // 给每一帧都打上水印
  inputGif.frames.forEach((frame, i) => {
    const jimpCopied = GifUtil.copyAsJimp(Jimp, frame);

    // 计算获得的坐标再减去每一帧偏移位置,为实际添加水印坐标
    jimpCopied.composite(logoImage, X - frame.xOffset, Y - frame.yOffset, [{
      mode: Jimp.BLEND_SOURCE_OVER,
      opacitySource: 0.1,
      opacityDest: 1
    }]);

    // 压缩图片
    jimpCopied.quality(quality);

    frame.bitmap = jimpCopied.bitmap;

    // 真彩色转 256 色
    frame.bitmap = trueTo256(frame.bitmap);
  });

  // 不使用 trueTo256 也可以使用自带的 quantizeWu 进行颜色转换,不过自带的算法运行需要更多的时间,没有 trueTo256 快
  // GifUtil.quantizeWu(inputGif.frames);

  const codec = new GifCodec();
  return (await codec.encodeGif(inputGif.frames)).buffer;
};

async function img({
  input = ‘‘,
  logo = ‘‘,
  quality = 80,
  rotate = 0,
  minWidth = 0,
  minHeight = 0,
} = {}) {
  const image = await Jimp.read(input);

  if (image.getWidth() < minWidth || image.getHeight() < minHeight) {
    return;
  }

  const logoImage = await Jimp.read(logo);

  logoImage.rotate(rotate);

  const { X, Y } = getXY({
    width: image.getWidth(),
    height: image.getHeight(),
  }, logoImage);

  image.composite(logoImage, X, Y, [{
    mode: Jimp.BLEND_SOURCE_OVER,
    opacitySource: 0.1,
    opacityDest: 1
  }]);

  // 压缩图片
  image.quality(quality);

  return await image.getBufferAsync(Jimp.AUTO);
};

module.exports = {
  gif,
  img
};

5. 新建文件 component/watermark/trueTo256.js

/**
 * 真彩色转 256 色
* https://www.jianshu.com/p/9188b4639a83
*/

function colorTransfer(rgb) {
  var r = (rgb & 0x0F00000) >> 12;
  var g = (rgb & 0x000F000) >> 8;
  var b = (rgb & 0x00000F0) >> 4;
  return (r | g | b);
};

function colorRevert(rgb) {
  var r = (rgb & 0x0F00) << 12;
  var g = (rgb & 0x000F0) << 8;
  var b = (rgb & 0x00000F) << 4;
  return (r | g | b);
}

function getDouble(a, b) {
  var red = ((a & 0x0F00) >> 8) - ((b & 0x0F00) >> 8);
  var grn = ((a & 0x00F0) >> 4) - ((b & 0x00F0) >> 4);
  var blu = (a & 0x000F) - (b & 0x000F);
  return red * red + blu * blu + grn * grn;
}

function getSimulatorColor(rgb, rgbs, m) {
  var r = 0;
  var lest = getDouble(rgb, rgbs[r]);
  for (var i = 1; i < m; i++) {
    var d2 = getDouble(rgb, rgbs[i]);
    if (lest > d2) {
      lest = d2;
      r = i;
    }
  }
  return rgbs[r];
}

function transferTo256(rgbs) {
  var n = 4096;
  var m = 256;
  var colorV = new Array(n);
  var colorIndex = new Array(n);

  //初始化
  for (var i = 0; i < n; i++) {
    colorV[i] = 0;
    colorIndex[i] = i;
  }

  //颜色转换
  for (var x = 0; x < rgbs.length; x++) {
    for (var y = 0; y < rgbs[x].length; y++) {
      rgbs[x][y] = colorTransfer(rgbs[x][y]);
      colorV[rgbs[x][y]]++;
    }
  }

  //出现频率排序
  var exchange;
  var r;
  for (var i = 0; i < n; i++) {
    exchange = false;
    for (var j = n - 2; j >= i; j--) {
      if (colorV[colorIndex[j + 1]] > colorV[colorIndex[j]]) {
        r = colorIndex[j];
        colorIndex[j] = colorIndex[j + 1];
        colorIndex[j + 1] = r;
        exchange = true;
      }
    }
    if (!exchange) break;
  }

  //颜色排序位置
  for (var i = 0; i < n; i++) {
    colorV[colorIndex[i]] = i;
  }

  for (var x = 0; x < rgbs.length; x++) {
    for (var y = 0; y < rgbs[x].length; y++) {
      if (colorV[rgbs[x][y]] >= m) {
        rgbs[x][y] = colorRevert(getSimulatorColor(rgbs[x][y], colorIndex, m));
      } else {
        rgbs[x][y] = colorRevert(rgbs[x][y]);
      }
    }
  }
  return rgbs;
}

// 获取 rgba int 值
function getRgbaInt(bitmap, x, y) {
  const bi = (y * bitmap.width + x) * 4;
  return bitmap.data.readUInt32BE(bi, true);
}

// 设置 rgba int 值
function setRgbaInt(bitmap, x, y, rgbaInt) {
  const bi = (y * bitmap.width + x) * 4;
  return bitmap.data.writeUInt32BE(rgbaInt, bi);
}

// int 值转为 rgba
function intToRGBA (i) {
  let rgba = {};

  rgba.r = Math.floor(i / Math.pow(256, 3));
  rgba.g = Math.floor((i - rgba.r * Math.pow(256, 3)) / Math.pow(256, 2));
  rgba.b = Math.floor(
    (i - rgba.r * Math.pow(256, 3) - rgba.g * Math.pow(256, 2)) /
      Math.pow(256, 1)
  );
  rgba.a = Math.floor(
    (i -
      rgba.r * Math.pow(256, 3) -
      rgba.g * Math.pow(256, 2) -
      rgba.b * Math.pow(256, 1)) /
      Math.pow(256, 0)
  );
  return rgba;
};

// rgba int 转为 rgb int
function rgbaIntToRgbInt (i) {
  const r = Math.floor(i / Math.pow(256, 3));
  const g = Math.floor((i - r * Math.pow(256, 3)) / Math.pow(256, 2));
  const b = Math.floor(
    (i - r * Math.pow(256, 3) - g * Math.pow(256, 2)) /
      Math.pow(256, 1)
  );

  return r * Math.pow(256, 2) +
  g * Math.pow(256, 1) +
  b * Math.pow(256, 0);
};

// rgb int 转为 rgba int
function rgbIntToRgbaInt (i, a) {
  const r = Math.floor(i / Math.pow(256, 2));
  const g = Math.floor((i - r * Math.pow(256, 2)) / Math.pow(256, 1));
  const b = Math.floor(
    (i - r * Math.pow(256, 2) - g * Math.pow(256, 1)) /
      Math.pow(256, 0)
  );
  return r * Math.pow(256, 3) +
  g * Math.pow(256, 2) +
  b * Math.pow(256, 1) +
  a * Math.pow(256, 0);
};

/**
* @interface Bitmap { data: Buffer; width: number; height: number;}
* @param {Bitmap} bitmap
*/
module.exports = function (bitmap) {
  const width = bitmap.width;
  const height = bitmap.height;

  let rgbs = new Array();
  let alphas = new Array();

  for (let x = 0; x < width; x++) {
    rgbs[x] = rgbs[x] || [];
    alphas[x] = alphas[x] || [];
    for (let y = 0; y < height; y++) {
      // 由于真彩色转 256色 算法是使用 int rgb 计算,所以需要把获取到的 int rgba 转为 int rgb
      const rgbaInt = getRgbaInt(bitmap, x, y);
      rgbs[x][y] = rgbaIntToRgbInt(rgbaInt);
      alphas[x][y] = intToRGBA(rgbaInt).a;
    }
  }

  // 颜色转换
  const color = transferTo256(rgbs);

  for (let x = 0; x < width; x++) {
    for (let y = 0; y < height; y++) {
      // 写入转换后的颜色
      setRgbaInt(bitmap, x, y, rgbIntToRgbaInt(color[x][y], alphas[x][y]));
    }
  }

  return bitmap;
};

6. 添加配置 _config.yml

# 水印
watermark:
  # 此处需要改成你的 logo 文件地址
  logo: ./component/watermark/logo.png

7. 重新运行项目即可。

文字水印

  1. 使用 jimp.loadFont 绘制文字水印。

    问题:不能设置文字颜色大小等样式。

  2. 参考 hexo-images-watermark 方案,逻辑是先用 text-to-svg 将文本转为 svg ,在用 svg2png 将 svg 转为 png 图片获得 buffer 数据,再拿 buffer 绘制水印。

    问题:安装困难,svg2png 需要用到 PhantomJS

  3. 其他文字转图片的方案也有各自安装问题,比如使用 node-canvas 转换文字,安装 node-pre-gyp 困难。

hexo 改造系列文章推荐阅读 https://blog.jijian.link/categories/hexo/

相关推荐