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

同步发布: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-pre-gyp ,依赖系统,各种安装困难,难搞哦。

  问题:很久不更新了。

  问题:不支持 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);
View Code

  问题:不支持 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 };
View Code

效果如下:

原图:

水印图:

代码下载:

完整代码下载请移步:https://blog.jijian.link/2020-04-17/nodejs-watermark/

posted @ 2020-04-21 16:46  极·简  Views(3305)  Comments(0Edit  收藏  举报