同步发布:https://blog.jijian.link/2020-04-17/nodejs-watermark/
nodejs 作为一个脚本语言,图片处理这方面有点弱鸡,无法跟 php 这种本身集成了图片 api 的语言相比。
不过好在有 https://www.npmjs.com/ ,上面有全世界的大佬写的各种高大上的插件使用。
本文踩在巨人的肩上介绍 nodejs 添加图片水印的几种方式。
方案一:使用云处理
如果图片私有性要求不高,也不嫌弃注册各种云麻烦,那么这种方式比较适合。
国内 七牛云:https://developer.qiniu.com/dora/api/1316/image-watermarking-processing-watermark
国外 Cloudinary:https://cloudinary.com/documentation/node_image_manipulation
方案二:使用 nodejs 插件
注意:程序添加水印都有一个通病,添加水印之后的图片体积至少是原图的2倍以上。
- node-canvas:https://github.com/Automattic/node-canvas
问题:需要安装 node-pre-gyp ,依赖系统,各种安装困难,难搞哦。
问题:很久不更新了。
- node-images:https://github.com/zhangyuanwei/node-images
问题:不支持 gif 图片。
优点:轻量级,不依赖系统,国内大佬写的
使用简单:

var images = require("images"); /** * 添加水印 * @param srcImg 源图 * @param watermarkImg 水印图 * @param x 添加水印水平位置x * @param y 添加水印垂直位置y */ var imageAddWatermark = function(srcImg,watermarkImg,x,y){ images(srcImg).draw(images(watermarkImg), x, y).save(output); }; var srcImg = './img.jpg'; var watermarkImg = './logo.png'; var output = './out.jpg'; imageAddWatermark(srcImg, watermarkImg, 10, 10);
问题:不支持 gif 图片。
优点:功能齐全,不依赖系统,国外大佬写的,有可选用的 gif 代替方案,不过不成熟,如果添加水印之后图片颜色超过 256 色,保存会报错,需要添加颜色转换。
gif 方案:https://github.com/jtlapp/gifwrap
多番考虑,最终选用 jimp 做水印效果
jimp 支持的图片类型有: image/jpeg,image/png,image/bmp,image/x-ms-bmp,image/tiff
安装: npm install jimp --save-dev
jpg 与 png 图片水印
代码如下:
1 const Jimp = require('jimp'); 2 3 // 需要添加的水印图片路径 4 // const ORIGINAL_IMAGE = './img/test.png'; 5 const ORIGINAL_IMAGE = './img/test.jpg'; 6 7 // 水印logo路径 8 const LOGO = './img/logo.png'; 9 10 // 水印距离右下角百分比 11 const LOGO_MARGIN_PERCENTAGE = 5 / 100; 12 13 const main = async () => { 14 const [image, logo] = await Promise.all([ 15 Jimp.read(ORIGINAL_IMAGE), 16 Jimp.read(LOGO) 17 ]); 18 19 // 将 logo 等比缩小 10 倍 20 // logo.resize(inputGif.width / 10, Jimp.AUTO); 21 22 const xMargin = image.bitmap.width * LOGO_MARGIN_PERCENTAGE; 23 const yMargin = image.bitmap.width * LOGO_MARGIN_PERCENTAGE; 24 25 const X = image.bitmap.width - logo.bitmap.width - xMargin; 26 const Y = image.bitmap.height - logo.bitmap.height - yMargin; 27 28 return image.composite(logo, X, Y, [ 29 { 30 mode: Jimp.BLEND_SOURCE_OVER, 31 opacitySource: 0.1, 32 opacityDest: 1 33 } 34 ]); 35 }; 36 37 main().then(image => { 38 const FILENAME = 'new_name.' + image.getExtension(); 39 return image.write(FILENAME, (err) => { 40 if (err) { 41 return console.error(err); 42 }; 43 console.log('水印成功:', FILENAME); 44 }); 45 });
gif 图片水印
安装 gifwrap: npm install gifwrap --save-dev
代码如下:
1 const Jimp = require('jimp'); 2 const { GifUtil } = require('gifwrap'); 3 const trueTo256 = require('./trueTo256'); 4 5 // 需要添加的水印图片路径 6 const ORIGINAL_IMAGE = './img/test.gif'; 7 8 // 水印logo路径 9 const LOGO = './img/logo.png'; 10 11 // 水印距离右下角百分比 12 const LOGO_MARGIN_PERCENTAGE = 5 / 100; 13 14 async function main () { 15 const logo = await Jimp.read(LOGO); 16 17 return GifUtil.read(ORIGINAL_IMAGE).then(inputGif => { 18 // 将 logo 等比缩小 10 倍 19 // logo.resize(inputGif.width / 10, Jimp.AUTO); 20 21 const xMargin = inputGif.width * LOGO_MARGIN_PERCENTAGE; 22 const yMargin = inputGif.height * LOGO_MARGIN_PERCENTAGE; 23 24 const X = inputGif.width - logo.bitmap.width - xMargin; 25 const Y = inputGif.height - logo.bitmap.height - yMargin; 26 27 // 给每一帧都打上水印 28 inputGif.frames.forEach((frame, i) => { 29 // 只为第一帧添加水印,可能会出现水印被覆盖问题 30 /* if (i !== 0) { 31 return; 32 } */ 33 const jimpCopied = GifUtil.copyAsJimp(Jimp, frame); 34 35 // 计算获得的坐标再减去每一帧偏移位置,为实际添加水印坐标 36 jimpCopied.composite(logo, X - frame.xOffset, Y - frame.yOffset, [{ 37 mode: Jimp.BLEND_SOURCE_OVER, 38 opacitySource: 0.1, 39 opacityDest: 1 40 }]); 41 42 frame.bitmap = jimpCopied.bitmap; 43 44 // 输出每一帧图片 45 // jimpCopied.write(`${i}.png`); 46 47 // 真彩色转 256 色 48 frame.bitmap = trueTo256(frame.bitmap); 49 }); 50 51 return inputGif; 52 }); 53 } 54 55 main().then(inputGif => { 56 // Pass inputGif to write() to preserve the original GIF's specs. 57 const FILENAME = 'new_name.gif'; 58 return GifUtil.write(FILENAME, inputGif.frames, inputGif).then(outputGif => { 59 console.log('水印成功:', FILENAME); 60 }).catch((err) => { 61 if (err) { 62 return console.error('水印失败:', err); 63 } 64 }); 65 });
真彩色转 256 色 算法
上面代码中的 trueTo256.js 为 真彩色转 256 色 算法,这部分代码参考了大佬写的 Java 算法(用流行色算法实现转换): https://www.jianshu.com/p/9188b4639a83
源码如下:

1 function colorTransfer(rgb) { 2 var r = (rgb & 0x0F00000) >> 12; 3 var g = (rgb & 0x000F000) >> 8; 4 var b = (rgb & 0x00000F0) >> 4; 5 return (r | g | b); 6 }; 7 8 function colorRevert(rgb) { 9 var r = (rgb & 0x0F00) << 12; 10 var g = (rgb & 0x000F0) << 8; 11 var b = (rgb & 0x00000F) << 4; 12 return (r | g | b); 13 } 14 15 function getDouble(a, b) { 16 var red = ((a & 0x0F00) >> 8) - ((b & 0x0F00) >> 8); 17 var grn = ((a & 0x00F0) >> 4) - ((b & 0x00F0) >> 4); 18 var blu = (a & 0x000F) - (b & 0x000F); 19 return red * red + blu * blu + grn * grn; 20 } 21 22 function getSimulatorColor(rgb, rgbs, m) { 23 var r = 0; 24 var lest = getDouble(rgb, rgbs[r]); 25 for (var i = 1; i < m; i++) { 26 var d2 = getDouble(rgb, rgbs[i]); 27 if (lest > d2) { 28 lest = d2; 29 r = i; 30 } 31 } 32 return rgbs[r]; 33 } 34 35 function transferTo256(rgbs) { 36 var n = 4096; 37 var m = 256; 38 var colorV = new Array(n); 39 var colorIndex = new Array(n); 40 41 //初始化 42 for (var i = 0; i < n; i++) { 43 colorV[i] = 0; 44 colorIndex[i] = i; 45 } 46 47 //颜色转换 48 for (var x = 0; x < rgbs.length; x++) { 49 for (var y = 0; y < rgbs[x].length; y++) { 50 rgbs[x][y] = colorTransfer(rgbs[x][y]); 51 colorV[rgbs[x][y]]++; 52 } 53 } 54 55 //出现频率排序 56 var exchange; 57 var r; 58 for (var i = 0; i < n; i++) { 59 exchange = false; 60 for (var j = n - 2; j >= i; j--) { 61 if (colorV[colorIndex[j + 1]] > colorV[colorIndex[j]]) { 62 r = colorIndex[j]; 63 colorIndex[j] = colorIndex[j + 1]; 64 colorIndex[j + 1] = r; 65 exchange = true; 66 } 67 } 68 if (!exchange) break; 69 } 70 71 //颜色排序位置 72 for (var i = 0; i < n; i++) { 73 colorV[colorIndex[i]] = i; 74 } 75 76 for (var x = 0; x < rgbs.length; x++) { 77 for (var y = 0; y < rgbs[x].length; y++) { 78 if (colorV[rgbs[x][y]] >= m) { 79 rgbs[x][y] = colorRevert(getSimulatorColor(rgbs[x][y], colorIndex, m)); 80 } else { 81 rgbs[x][y] = colorRevert(rgbs[x][y]); 82 } 83 } 84 } 85 return rgbs; 86 } 87 88 // 获取 rgba int 值 89 function getRgbaInt(bitmap, x, y) { 90 const bi = (y * bitmap.width + x) * 4; 91 return bitmap.data.readUInt32BE(bi, true); 92 } 93 94 // 设置 rgba int 值 95 function setRgbaInt(bitmap, x, y, rgbaInt) { 96 const bi = (y * bitmap.width + x) * 4; 97 return bitmap.data.writeUInt32BE(rgbaInt, bi); 98 } 99 100 // int 值转为 rgba 101 function intToRGBA (i) { 102 let rgba = {}; 103 104 rgba.r = Math.floor(i / Math.pow(256, 3)); 105 rgba.g = Math.floor((i - rgba.r * Math.pow(256, 3)) / Math.pow(256, 2)); 106 rgba.b = Math.floor( 107 (i - rgba.r * Math.pow(256, 3) - rgba.g * Math.pow(256, 2)) / 108 Math.pow(256, 1) 109 ); 110 rgba.a = Math.floor( 111 (i - 112 rgba.r * Math.pow(256, 3) - 113 rgba.g * Math.pow(256, 2) - 114 rgba.b * Math.pow(256, 1)) / 115 Math.pow(256, 0) 116 ); 117 return rgba; 118 }; 119 120 // rgba int 转为 rgb int 121 function rgbaIntToRgbInt (i) { 122 const r = Math.floor(i / Math.pow(256, 3)); 123 const g = Math.floor((i - r * Math.pow(256, 3)) / Math.pow(256, 2)); 124 const b = Math.floor( 125 (i - r * Math.pow(256, 3) - g * Math.pow(256, 2)) / 126 Math.pow(256, 1) 127 ); 128 129 return r * Math.pow(256, 2) + 130 g * Math.pow(256, 1) + 131 b * Math.pow(256, 0); 132 }; 133 134 // rgb int 转为 rgba int 135 function rgbIntToRgbaInt (i, a) { 136 const r = Math.floor(i / Math.pow(256, 2)); 137 const g = Math.floor((i - r * Math.pow(256, 2)) / Math.pow(256, 1)); 138 const b = Math.floor( 139 (i - r * Math.pow(256, 2) - g * Math.pow(256, 1)) / 140 Math.pow(256, 0) 141 ); 142 return r * Math.pow(256, 3) + 143 g * Math.pow(256, 2) + 144 b * Math.pow(256, 1) + 145 a * Math.pow(256, 0); 146 }; 147 148 /** 149 * @interface Bitmap { data: Buffer; width: number; height: number;} 150 * @param {Bitmap} bitmap 151 */ 152 module.exports = function (bitmap) { 153 const width = bitmap.width; 154 const height = bitmap.height; 155 156 let rgbs = new Array(); 157 let alphas = new Array(); 158 159 for (let x = 0; x < width; x++) { 160 rgbs[x] = rgbs[x] || []; 161 alphas[x] = alphas[x] || []; 162 for (let y = 0; y < height; y++) { 163 // 由于真彩色转 256色 算法是使用 int rgb 计算,所以需要把获取到的 int rgba 转为 int rgb 164 const rgbaInt = getRgbaInt(bitmap, x, y); 165 rgbs[x][y] = rgbaIntToRgbInt(rgbaInt); 166 alphas[x][y] = intToRGBA(rgbaInt).a; 167 } 168 } 169 170 // 颜色转换 171 const color = transferTo256(rgbs); 172 173 for (let x = 0; x < width; x++) { 174 for (let y = 0; y < height; y++) { 175 // 写入转换后的颜色 176 setRgbaInt(bitmap, x, y, rgbIntToRgbaInt(color[x][y], alphas[x][y])); 177 } 178 } 179 180 return bitmap; 181 };
效果如下:
原图:
水印图:
代码下载:
完整代码下载请移步:https://blog.jijian.link/2020-04-17/nodejs-watermark/