同步发布: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);

  问题:不支持 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/

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

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

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

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

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

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

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

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

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

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

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

  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. 龙哥量化:通达信板块概念FAQ,*期强势、*期弱势是怎么划分的?等问题是官网的解释,股友可以根据文章的提示迸发策略灵感

    如果您需要代写公式, 请联系我. 龙哥QQ:591438821 龙哥微信:Long622889 比如第9条,*期强势:20日涨幅>=30%,     3日涨幅>0,非停牌.非ST.非未开板 ...

  2. 记一次语音合成遇到的坑:PCM音频流转WAV

    需求内容: 预合成音:支持将固定音合成并完成上传操作 解决思路: 调用公有云识别引擎,获取识别引擎合成的音频流, 然后将音频流转成wav文件, 最后将文件上传到oss服务器上. 遇到的问题 问题主要在 ...

  3. elementPlus 问题总结

    第一次搞,遇上很多弱智问题,记录一下 安装elementPlus $ npm install element-plus --save 全局引入 import ElementPlus from 'ele ...

  4. C#中如何将图片添加为程序的资源

    C#中将图片添加为程序的资源的步骤: 1.在C#程序的"Properties"文件夹中双击Resources.resx文件,以便打开资源文件,使其处于可编辑状态: 2.在打开后的R ...

  5. 给 Python 添加进度条 | 给小白的 tqdm 精炼实例!

    给 Python 添加进度条 | 给小白的 tqdm 精炼实例! 假设我们有一个循环: for i in range(100): do_something() # 这里做某些事 假设 do_somet ...

  6. 使用Docker部署的基于binlog实现Mysql8

    概念 MySQL 基于 Binlog 的主从复制(Master-Slave Replication)是 MySQL 数据库中实现数据复制的一种机制.在这种复制模式下,主库(Master)记录所有对数据 ...

  7. 快速修改MySQL数据库名称

    原理:先创建新的数据库,然后利用information_schema数据库表结构信息,用 RENAME命令 将旧的表迁移到新数据库里面,最后删除旧的数据库名称即可. 步骤如下: #查询目标数据库下面的 ...

  8. 所生成项目的处理器架构“MSIL”与 “x86”不匹配

    在 .net 生成时如果修改过某个类库的平台目标,那么通常会出现下面的警告: 之所以产生这个问题是因为类库的"平台目标"不统一,如果选择了 x86, 那么解决方案中所有的项目都应设 ...

  9. oracle 根据节点id递归查询全部的父节点(转载)

    本文转载自   https://blog.csdn.net/BondChenJ/article/details/78581625 1.适用状况:blog 适用树状结构数据,例如包含id,parent_ ...

  10. nacos(一): 下载、运行与鉴权配置

    1.下载 nacao的官网地址是https://nacos.io/ 当前稳定版本是2.5.0,可以在官网下载,也可以在github上下载.其中,官网提供的下载地址是: https://download ...