文章同步发布: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/

hexo 图片添加水印(png, jpeg, jpg, gif)的更多相关文章

  1. Hexo 文章图片添加水印,不用云处理

    由于网上找到的都是借用第三方云处理添加水印,但是我不太想用,所以自己开发了一个插件 Hexo 图片添加水印Github地址 目前插件可以直接在 hexo 官网上搜索到 下面内容都是在 Github 上 ...

  2. java实现给图片添加水印

    package michael.io.image; import java.awt.AlphaComposite; import java.awt.Graphics2D; import java.aw ...

  3. java.imageIo给图片添加水印

    最近项目在做一个商城项目, 项目上的图片要添加水印①,添加图片水印;②:添加文字水印; 一下提供下个方法,希望大家可以用得着: package com.blogs.image; import java ...

  4. 【Python】给图片添加水印的Python及Golang实现

    前言 不知道大家有没有这样的习惯,一篇比较得意的博客在发表一段时间之后会特别关注,前段时间一篇写到凌晨的博客被 码迷 这个网关爬取之后发表了,因为搜索引擎先爬取码迷的,所以我的博客无法被搜索到,即使直 ...

  5. .net为图片添加水印(转) jpg png和gif格式

    .net为图片添加水印(转) jpg png和gif格式 .net为图片添加水印(转) jpg png和gif格式,转自csdn的hyde82,现在跟大家一起来分享下: 利 用.net中System. ...

  6. 完美png图片添加水印类

    完美png图片添加水印类 被添加水印图片和水印图片都可以是png,保证透明无色背景,可调节透明度 <?phpclass Imgshuiyin{ /* 缩略图相关常量定义 */ const THU ...

  7. Python Windows 快捷键自动给剪贴板(复制)图片添加水印

    编写一个能在windows上使用的按下快捷键自动给剪贴板(复制)的图片添加水印的小工具.plyer.PIL.pyinstaller.pynput.win32clipboard库.记录自己踩过的坑,部分 ...

  8. Android 图片添加水印图片或者文字

    给图片添加水印的基本思路都是载入原图,添加文字或者载入水印图片,保存图片这三个部分 添加水印图片: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ...

  9. ASP.NET -- WebForm -- 给图片添加水印标记

    ASP.NET -- WebForm: 给图片添加水印标记 ASP.NET:使用 WebForm(C#) 制作一个简单的为图片添加水印的页面. 1. Test2.aspx文件 <%@ Page ...

  10. JAVA给图片添加水印

    package com.test; import org.junit.Test; import javax.imageio.ImageIO; import java.awt.*; import jav ...

随机推荐

  1. Thrift中enum的一些探究

    http://anruence.com/2018/06/27/enum-thrift/ 问题 在用注解定义的Thrift enum 中,如果客户端端和服务端的enum定义不同,比如调换了enum中的枚 ...

  2. Sqlsugar 跨库查询小心得(同服务器不同数据库)

    同一个服务器下的不同数据库,目前还没有进行跨服务器的查询,以后有待研究-- 1.使用的是Left Join左查询,因此连接字符串应该是写的第一个表所在的数据库的连接字符串 假设数据库A,B,连接字符串 ...

  3. 不为人知的网络编程(十八):UDP比TCP高效?还真不一定!

    本文由LearnLHC分享,原始出处:blog.csdn.net/LearnLHC/article/details/115268028,本文进行了排版和内容优化. 1.引言 熟悉网络编程的(尤其搞实时 ...

  4. Python 代码实现生命之轮Wheel of life

    最近看一个生命之轮的视频,让我们珍惜时间,因为一生是有限的.使用Python创建生命倒计时图表,珍惜时间,活在当下. 生命之轮(Wheel of life),这一概念最初由 Success Motiv ...

  5. Linux安装配置Go语言

    Linux安装配置Go语言 官网:https://go.dev/dl/ 从官网下载,选择linux下载压缩包. sudo cp -r go/ /usr/local sudo gedit /etc/pr ...

  6. 浅说c/c++ coroutine

    浅说c/c++ coroutine 从上面我们可以得到关于协程的几个关键信息, 1.打破传统(regular)函数调用的限制. 2.stackful协程实现方式,基于独立栈,上下文切换. 3.stac ...

  7. 获取不同型号手机小程序导航栏的高度(uniapp)

    uni.getSystemInfo({ success: function(e) { Vue.prototype.StatusBar = e.statusBarHeight; let custom = ...

  8. VueH5页面中input控件placeholder提示字默认颜色修改与禁用时默认字体颜色修改

    一.默认提示字颜色修改 不同浏览器的设置略有区别 以下是只选择name为color的input进行修改 //chrome谷歌浏览器,Safari苹果浏览器 input[name="color ...

  9. uwp IProgress<T>进度通知。

    主要是利用 Pp_ProgressChanged 报告进度: private void BtnDownload_Click(object sender, RoutedEventArgs e) { va ...

  10. Codeforces Round 957 (Div. 3)

    题目链接:Codeforces Round 957 (Div. 3) 总结:E不懂,F差一个set去重 A. Only Pluses fag:枚举 B. Angry Monk fag:模拟 Solut ...