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