更新记录

更新记录

2021-01-27

图片水印嵌入(待调整)

hexo-butterfly-图片水印

引入方式

<1>引入现有插件生成图片水印

​ 引入hexo-images-watermark插件

<2>基于jimp进行构建

hexo-images-watermark

引入 hexo-images-watermark依赖

1
npm install hexo-images-watermark

主配置文件配置水印内容

1
2
3
4
5
watermark:
enable: true # 图片水印设置(暂不支持文本和图片同时添加水印)
textEnable: true # 文字水印设置(暂不支持文本和图片同时添加水印)
rotate: -45
gravity: centre

基于jimp进行构建

引入jimp依赖

1
npm install jimp gifwrap --save

新建文件构建水印:

themes/butterfly/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);

在项目根目录下创建component构建水印相关组件

index.js

watermark.js

trueTo256.js

  • 新建文件 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);
});
}
  • 新建文件 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
};
  • 新建文件 component/watermark/trueTo256.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
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;
};

在主配置文件中添加水印配置_config.yml

1
2
3
4
# 水印配置
watermark:
# 水印指向logo图片地址
logo: ./component/watermark/logo.png