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

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

方案一

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

该插件依赖 sharp :https://github.com/lovell/sharp 对于我这种安装困难户来说,难搞哦。

安装问题可以参考作者写的安装教程:https://github.com/SpiritLing/hexo-images-watermark/issues/2

方案二

使用 jimp 造个轮子

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

步骤

  1. 首先安装依赖
1
npm install jimp gifwrap --save
  1. 新建文件 themes/landscape/scripts/image_watermark.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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);
  1. 新建文件 component/watermark/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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);
});
}
  1. 新建文件 component/watermark/watermark.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
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
};
  1. 新建文件 component/watermark/trueTo256.js

这部分算法借鉴了大佬写的 Java 算法: https://www.jianshu.com/p/9188b4639a83
详细说明参考 nodejs 图片添加水印(png, jpeg, jpg, gif)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
/**
* 真彩色转 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;
};
  1. 添加配置 _config.yml
1
2
3
4
# 水印
watermark:
# 此处需要改成你的 logo 文件地址
logo: ./component/watermark/logo.png
  1. 重新运行项目即可。

效果参考本站任意有图文章即可。

文字水印

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

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

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

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

  1. 其他文字转图片的方案也有各自安装问题,比如使用 node-canvas 转换文字,安装 node-pre-gyp 困难。
本文由 linx(544819896@qq.com) 创作,采用 CC BY 4.0 CN协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处。本文链接为: https://blog.jijian.link/2020-04-21/hexo-watermark/

如果您觉得文章不错,可以点击文章中的广告支持一下!