sharp 是 Node.js 平台上相当热门的一个图像处理库,其实际上是基于 C 语言编写 的 libvips 库封装而来,因此高性能也成了 sharp 的一大卖点。sharp 可以方便地实现常见的图片编辑操作,如裁剪、格式转换、旋转变换、滤镜添加等。当然,网络上相关的文章比较多,sharp 的官方文档也比较详细,所以这不是本文的重点。这里主要是想记录一下我在使用 sharp 过程中遇到的一些稍复杂的图片处理需求的解决方案,希望分享出来能够对大家有所帮助。

https://blog.csdn.net/weixin_33972649/article/details/88026389

sharp 基础

sharp 整体采用流式处理模式,其在读入图像数据后经过一系列的处理加工然后输出结果。我们看一个简单的示例就能理解:

 

const sharp = require('sharp');

sharp('input.jpg')
.rotate()

.resize(200)

.toBuffer()

.then( data => ... )

.catch( err => ... );

复制代码

sharp 几乎所有的函数接口都挂载在 Sharp 实例上,因此图像处理的第一步操作一定是读入图片数据(sharp 函数接受图片本地路径或者图片 Buffer 数据作为参数)并将其转换为 Sharp 实例,然后才是如流水线一般的加工。因此,这里应该提供一个预处理函数,将服务端接收到的图片转换为 Sharp 实例:

/**

*
* @param { String | Buffer } inputImg 图片本地路径或图片 Buffer 数据
* @return { Sharp }
*/
async convert2Sharp(inputImg) {
return sharp(inputImg)
}
复制代码

然后就可以进行具体的图像处理。

添加水印

后端实现

添加水印功能应该算是比较常见的图片处理需求了。sharp 在图像合成方面只提供了一个函数:overlayWith,其接受一个图片参数(同样是图片本地路径字符串或者图片 Buffer 数据)以及一个可选的 options 配置对象(可配置水印图片的位置等信息)然后将该图片覆盖到原图上。逻辑上也比较简单,我们的代码如下所示:

 

/**

* 添加水印
* @param { Sharp } img 原图

* @param { String } watermarkRaw 水印图片

* @param { top } 水印距图片上边缘距离

* @param { left } 水印距图片左边缘距离

*/

async watermark(img, { watermarkRaw, top, left }) {

const watermarkImg = await watermarkRaw.toBuffer()

return img

.overlayWith(watermarkImg, { top, left })

}

这里简单起见只支持配置水印图片的位置,sharp 还支持更复杂的配置参数比如是否重复粘贴多个水印图片、是否只在 α 信道粘贴水印图片等,具体可参见 overlayWith 的文档。

前端实现

这里还需要顺带提一下前端的实现。当然,如果服务端是按照固定规则给图片添加水印(比如新浪微博里图片水印放置在固定的位置),前端就不必做什么了。但是某些场景下(比如在线图片编辑类工具中)用户添加水印的时候会期望能够在前端获得所见即所得的体验。这个时候如果用户添加完水印并且选好位置后,必须将数据发送至服务端处理再得到处理结果,势必会影响整个服务的流畅性。幸运的是强大的 HTML5 让前端的功能越来越丰富,借助 canvas 我们就能在前端实现添加水印的功能。具体的实现细节并不难,主要就是借助了 canvas 提供的 drawImage 方法,看一下示例:

var canvas = document.getElementById("canvas");

var ctx = canvas.getContext('2d');
// img: 底图
// watermarkImg: 水印图片
// x, y 是画布上放置 img 的坐标
ctx.drawImage(img, x, y);
ctx.drawImage(watermarkImg, x, y);
复制代码

实际上,整个添加水印的功能(选择原图、选择水印图片、设置水印图片位置、获得添加水印后的图片)是可以完全由前端完成的。当然,为了追求服务端功能的完整性,还是建议使用前端展示+后端处理的模式。

粘贴文字

粘贴文字的需求实际上与添加水印比较类似。唯一不同的是添加的水印图片换成了文字,以及我们可能需要对文字的大小、字体等做一些调整。思路也比较容易想到,把文字转换成图片形式即可。这里我们用到了 text-to-svg 库,作用是将文字转换成 svg。利用 svg 的特点我们可以很方便地设置文字的字体大小、颜色等。然后调用 Buffer.from 将 svg 转换为 sharp 可以使用的 buffer 数据。最后就是和上面的水印添加一样的步骤了。

 

const Text2SVG = require('text-to-svg')

/**

* 粘贴文字

* @param { Sharp } img

* @param { String } text 待粘贴文字

* @param { Number } fontSize 文字大小

* @param { String } color 文字颜色

* @param { Number } left 文字距图片左边缘距离

* @param { Number } top 文字距图片上边缘距离

*/

async pasteText(img, {

text, fontSize, color, left, top,

}) {

const text2SVG = Text2SVG.loadSync()

const attributes = { fill: color }

const options = {

fontSize,

anchor: 'top',

attributes,

}

const svg = Buffer.from(text2SVG.getSVG(text, options))

return img

.overlayWith(svg, { left, top })

}

复制代码

拼接图片

拼接图片的操作相对来说最为复杂。这里我们提供了两个配置项:拼接模式(水平/垂直)以及背景颜色。拼接模式比较好理解,无非是水平或是垂直排列图片。背景颜色则用于填充留白处。拼接图片时,图片以根据轴线居中排列。以水平排列图片为例,示意图如下:

这里也没有 sharp 提供的现成函数,一切还是用唯一的 overlayWith 解决。overlayWith 的用法是将一张图粘贴至另一张图上,这与我们拼接图片的需求略有差异。我们需要转换一下思维:可以预先创建一张底图,背景颜色可以根据配置值确定,然后将所有待拼接图片粘贴至其上,即可满足要求。

首先我们需要读取所有待拼接图片的长与宽。假设拼接模式为水平拼接,那么最终生成的图片的宽度为所有图片宽度之和,高度则取所有图片中的最大高度(垂直拼接的话则反过来):

 

let totalWidth = 0

let totalHeight = 0
let maxWidth = 0
let maxHeight = 0
const imgMetadataList = []
// 获取所有图片的宽和高,计算和及最大值
for (let i = 0, j = imgList.length; i < j; i += i) {
const { width, height } = await imgList[i].metadata()
imgMetadataList.push({ width, height })
totalHeight += height
totalWidth += width
maxHeight = Math.max(maxHeight, height)
maxWidth = Math.max(maxWidth, width)
}

然后我们用得到的宽度和高度数据新建一个背景颜色为传入配置(或默认白色)的 base 图片:

const baseOpt = {

width: mode === 'horizontal' ? totalWidth : maxWidth,

height: mode === 'vertical' ? totalHeight : maxHeight,

channels: 4,

background: background || {

r: 255, g: 255, b: 255, alpha: 1,

},

}

const base = sharp({

create: baseOpt,

}).jpeg().toBuffer()

然后在 base 图片的基础上重复调用 overlayWith 函数,将待拼接图片逐个粘贴至 base 图片上。这里需要注意的是图片的摆放位置,前面也提到过,我们会将图片根据主轴线进行居中对齐,所以每次摆放图片时都需要进行 top 和 left 的计算(一个是居中的计算,一个是随着图片摆放顺序进行偏移的计算),当然,弄明白了原理之后就是小学数学题,没有太多可讲的。另一个需要注意的则是 overlayWith 每次只能完成两张图片之间的合成,因此我们用到了 reduce 方法,持续地将图片粘贴至底图上,并将结果作为下一次的输入。

imgMetadataList.unshift({ width: 0, height: 0 })

let imgIndex = 0

const result = await imgList.reduce(async (input, overlay) => {

const offsetOpt = {}

if (mode === 'horizontal') {

offsetOpt.left = imgMetadataList[imgIndex++].width

offsetOpt.top = (maxHeight - imgMetadataList[imgIndex].height) / 2

} else {

offsetOpt.top = imgMetadataList[imgIndex++].height

offsetOpt.left = (maxWidth - imgMetadataList[imgIndex].width) / 2

}

overlay = await overlay.toBuffer()

return input.then(data => sharp(data).overlayWith(overlay, offsetOpt).jpeg().toBuffer())

}, base)

return result

以下是拼接图片函数的完整实现:

/**

* 拼接图片

* @param { Array<Sharp> } imgList

* @param { String } mode 拼接模式:horizontal(水平)/vertical(垂直)

* @param { Object } background 背景颜色 格式为 {r: 0-255, g: 0-255, b: 0-255, alpha: 0-1} 默认 {r: 255, g: 255, b: 255, alpha: 1}

*/

async joinImage(imgList, { mode, background }) {

let totalWidth = 0

let totalHeight = 0

let maxWidth = 0

let maxHeight = 0

const imgMetadataList = []

// 获取所有图片的宽和高,计算和及最大值

for (let i = 0, j = imgList.length; i < j; i += i) {

const { width, height } = await imgList[i].metadata()

imgMetadataList.push({ width, height })

totalHeight += height

totalWidth += width

maxHeight = Math.max(maxHeight, height)

maxWidth = Math.max(maxWidth, width)

}

const baseOpt = {

width: mode === 'horizontal' ? totalWidth : maxWidth,

height: mode === 'vertical' ? totalHeight : maxHeight,

channels: 4,

background: background || {

r: 255, g: 255, b: 255, alpha: 1,

},

}

const base = sharp({

create: baseOpt,

}).jpeg().toBuffer()

// 获取图片的原始尺寸用于偏移

imgMetadataList.unshift({ width: 0, height: 0 })

let imgIndex = 0

const result = await imgList.reduce(async (input, overlay) => {

const offsetOpt = {}

if (mode === 'horizontal') {

offsetOpt.left = imgMetadataList[imgIndex++].width

offsetOpt.top = (maxHeight - imgMetadataList[imgIndex].height) / 2

} else {

offsetOpt.top = imgMetadataList[imgIndex++].height

offsetOpt.left = (maxWidth - imgMetadataList[imgIndex].width) / 2

}

overlay = await overlay.toBuffer()

return input.then(data => sharp(data).overlayWith(overlay, offsetOpt).jpeg().toBuffer())

}, base)

return result

},

以上就是个人在使用 sharp 过程中总结的一些实用操作。实际上 sharp 还有很多高级的功能我并没有用到,正应了“二八定律”:80% 的需求常常是通过 20% 的功能完成的。sharp 更多的用法以后如果还有机会折腾,会继续跟大家分享~

Node.js 服务端图片处理利器的更多相关文章

  1. node.js服务端程序在Linux上持久运行

    如果要想在服务端部署node.js程序,让其持久化运行,就不能单单使用npm start命令运行,当然了,这样运行是毫无问题的,但是当关闭xshell窗口或者是关闭进程的时候(其实关闭xshell窗口 ...

  2. Node.js 服务端处理图片

    Node 服务端处理图片 服务端进行图片处理是很常见的需求,但是Node在这一块相对来说比较薄弱.找了几个比较常见的模块来解决问题. gm GraphicsMagick for node 使用Open ...

  3. [转] Node.js 服务端实践之 GraphQL 初探

    https://medium.com/the-graphqlhub/your-first-graphql-server-3c766ab4f0a2#.n88wyan4e 0.问题来了 DT 时代,各种业 ...

  4. Node.js 本地Xhr取得Node.js服务端数据的例子

    本以为用XHR取Nodejs http出的一段文字很简单,因为xhr取值和nodejs http出文字都是好弄的,谁知一试不是这回事,中间有个关键步骤需要实现. nodejs http出文字显示在浏览 ...

  5. ASP.NET Core 与 Vue.js 服务端渲染

    http://mgyongyosi.com/2016/Vuejs-server-side-rendering-with-aspnet-core/ 原作者:Mihály Gyöngyösi 译者:oop ...

  6. CKEditor 自定义按钮插入服务端图片

    CKEditor 富文本编辑器很好用,功能很强大,在加上支持服务端图片上传的CKFinder更是方便, 最近在使用CKFinder的时候发现存在很多问题,比如上传图片的时候,图片不能按时间降序排列,另 ...

  7. NET Core 与 Vue.js 服务端渲染

    NET Core 与 Vue.js 服务端渲染 http://mgyongyosi.com/2016/Vuejs-server-side-rendering-with-aspnet-core/原作者: ...

  8. 基于 Egg.js 框架的 Node.js 服务构建之用户管理设计

    前言 近来公司需要构建一套 EMM(Enterprise Mobility Management)的管理平台,就这种面向企业的应用管理本身需要考虑的需求是十分复杂的,技术层面管理端和服务端构建是架构核 ...

  9. 实践案例丨教你一键构建部署发布前端和Node.js服务

    如何使用华为云服务一键构建部署发布前端和Node.js服务 构建部署,一直是一个很繁琐的过程 作为开发,最害怕遇到版本发布,特别是前.后端一起上线发布,项目又特别多的时候. 例如你有10个项目,前后端 ...

随机推荐

  1. SSM整合Dubbo案例

    一.Consumer子类 MyController类 @Controller @RequestMapping("/login") public class MyController ...

  2. SIGAI机器学习第二十集 AdaBoost算法1

    讲授Boosting算法的原理,AdaBoost算法的基本概念,训练算法,与随机森林的比较,训练误差分析,广义加法模型,指数损失函数,训练算法的推导,弱分类器的选择,样本权重削减,实际应用 AdaBo ...

  3. django 学习第二天

    今日内容 一.Django MVC和MTV框架 MVC controller:路由分发 用urls里面放置不同路径 执行不同函数 model 数据库信xi view #views 逻辑相关里面,写函数 ...

  4. Error Codes Messages查找工具介绍

    当我们通过各种手段获取到一个Windows错误码后,如何获取对应的错误消息呢?有两种方法:一是用编程的手段(FormatMessage):其二是用现成的工具.第一种方法一般在我们编码的时候有用,而更多 ...

  5. Python逆向(一)—— 前言及Python运行原理

    一.前言 最近在学习Python逆向相关,涉及到python字节码的阅读,编译及反汇编一些问题.经过长时间的学习有了一些眉目,为了方便大家交流,特地将学习过程整理,形成了这篇专题.专题对python逆 ...

  6. 简要描述Python的垃圾回收机制(garbage collection)

    这里能说的很多.你应该提到下面几个主要的点: Python在内存中存储了每个对象的引用计数(reference count).如果计数值变成0,那么相应的对象就会小时,分配给该对象的内存就会释放出来用 ...

  7. mysql group by 报错 ,only_full_group_by 三种解决方案

    报错信息  Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'data ...

  8. Spring boot Security 登陆安全配置

    实现的效果 访问url时,如果未登录时跳转到Login界面,要求用户登陆,如果登陆过返回请求的数据. 效果图 访问数据时,未登录返回login界面 登陆操作 登陆成功进入登出界面 登陆成功后再次访问数 ...

  9. python中普通函数调用协程

    import asyncio def target(loop, timeout=None): future = asyncio.run_coroutine_threadsafe(add(1, b=2) ...

  10. 如何使用REDIS进行微服务间通讯

    如何使用REDIS进行微服务间通讯 尽可能避免service - to - service通信.为此,需要在服务之间推一个消息队列.回顾一下微服务的概念小型的,非常集中的进程彼此独立运行并且易于维护, ...