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/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM