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. SP10707 COT2 - Count on a tree II 莫队上树

    题意:求一条链 \((u,v)\) 上不同的颜色数. 我们可以求出树的出栈入栈序(or 括号序?我也不确定). 图(from attack) 然后有一个很优美的性质: 设点 \(u\) 的入栈时间为 ...

  2. PHP流程控制之do...while循环的区别

    do...while与while的语法结构基本一样,也是一个布尔型循环,功能也基本一样.大理石平台价格 基本语法规定如下: do {    //代码块 } while (判断); do...while ...

  3. local

    用于在脚本中声明局部变量, 典型用法是在函数体内,其作用域也在该变量的函数体内,如果试图在函数外使用local声明变量,将提示错误

  4. 洛谷 P4058 [Code+#1]木材 题解

    P4058 [Code+#1]木材 题目描述 有 \(n\) 棵树,初始时每棵树的高度为 \(H_i\),第 \(i\) 棵树每月都会长高 \(A_i\)​.现在有个木料长度总量为 $ S$ 的订单, ...

  5. 洛谷 P1190 接水问题 题解

    P1190 接水问题 题目描述 学校里有一个水房,水房里一共装有 \(m\) 个龙头可供同学们打开水,每个龙头每秒钟的供水量相等,均为1. 现在有 \(n\) 名同学准备接水,他们的初始接水顺序已经确 ...

  6. Solution

    小五的游戏 小碎骨的子集 芙兰朵露的框框 ⑨要求和

  7. (22)打鸡儿教你Vue.js

    vue.js 单页面,多页面 Vue cli工具 复杂单页面应用Vue cli工具 交互设计,逻辑设计,接口设计 代码实现,线上测试 git clone,git int 创建分支,推送分支,合并分支 ...

  8. FCS省选模拟赛 Day7

    Description  Solution T1 island 考虑把问题成两部分计算 纵坐标的距离和很好计算,在输入的同时一次计算了就完事 横坐标又分成两部分 分别在\(y\)轴不同侧的矩形的距离和 ...

  9. 1.xml解析

    public static void main(String[] args) throws DocumentException {         SAXReader saxReader = new ...

  10. Tkinter 之磁盘搜索工具实战

    一.效果图 二.代码 miniSearch.py from tkinter import * from tkinter import ttk, messagebox, filedialog from ...