同步发布: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. Qt开源作品26-通用按钮地图效果

    一.前言 在很多项目应用中,需要根据数据动态生成对象显示在地图上,比如地图标注,同时还需要可拖动对象到指定位置显示,能有多种状态指示,安防领域一般用来表示防区或者设备,可以直接显示防区号,有多种状态颜 ...

  2. Error: Assertion failed (nimages > 0) in cv::calibrateCameraRO, file D:\opencv4\opencv\opencv-4.1.0\modules\calib3d\src\calibration.cpp, line 3691

    报错信息: Error: Assertion failed (nimages > 0) in cv::calibrateCameraRO, file D:\opencv4\opencv\open ...

  3. ERROR: SSL peer shut down incorrectly错误解决(Android Studio)

    错误信息:ERROR: SSL peer shut down incorrectly错误解决(Android Studio) 错误原因:android studio在下载"gradle-4. ...

  4. 基于开源IM即时通讯框架MobileIMSDK:RainbowChat v11.6版已发布

    关于RainbowChat RainbowChat是一套基于开源IM聊天框架 MobileIMSDK 的产品级移动端IM系统.RainbowChat源于真实运营的产品,解决了大量的屏幕适配.细节优化. ...

  5. IM跨平台技术学习(二):Electron初体验(快速开始、跨进程通信、打包、踩坑等)

    本文由蘑菇街前端技术团队分享,原题"Electron 从零到一",有修订和改动. 1.引言 在上篇<快速了解新一代跨平台桌面技术--Electron>,我们已经对Ele ...

  6. echo输出

    linux中不免经常使用echo进行输出,或输出到屏幕,或输出到文件.但是使用的时候会发现,在想要输出一些需要转义的字符时,例如\t等,它却原样不动的输出了. 使用man命令查看echo的帮助文档,会 ...

  7. CDS标准视图:维护活动类型描述 I_MaintenanceActivityTypeText

    视图名称:维护活动类型描述 I_MaintenanceActivityTypeText 视图类型:基础 视图代码: 点击查看代码 @AbapCatalog.sqlViewName: 'IMTACTTY ...

  8. Golang-接口7

    http://c.biancheng.net/golang/interface/ Go语言接口声明(定义) Go语言不是一种 "传统" 的面向对象编程语言:它里面没有类和继承的概念 ...

  9. Robot Framework 自动化测试部署常见问题及处理方法(二)

    书接上文 4.使用Open Browser关键字打开浏览器报错"WebDriverException: Message: 'geckodriver' executable needs to ...

  10. python实现网页爬虫示例

    用python里面的 requests 与 BeautifulSoup 结合,实现网页爬虫示例. 示例一:抓取中国省份: import requests from bs4 import Beautif ...