文章同步发布: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. 重新运行项目即可。
文字水印
-
使用 jimp.loadFont 绘制文字水印。
问题:不能设置文字颜色大小等样式。
-
参考 hexo-images-watermark 方案,逻辑是先用 text-to-svg 将文本转为 svg ,在用 svg2png 将 svg 转为 png 图片获得 buffer 数据,再拿 buffer 绘制水印。
问题:安装困难,svg2png 需要用到 PhantomJS。
-
其他文字转图片的方案也有各自安装问题,比如使用 node-canvas 转换文字,安装
node-pre-gyp困难。
hexo 改造系列文章推荐阅读 https://blog.jijian.link/categories/hexo/
