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

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 += heighttotalWidth += widthmaxHeight = 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++].widthoffsetOpt.top = (maxHeight - imgMetadataList[imgIndex].height) / 2} else {offsetOpt.top = imgMetadataList[imgIndex++].heightoffsetOpt.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 = 0let totalHeight = 0let maxWidth = 0let maxHeight = 0const imgMetadataList = []// 获取所有图片的宽和高,计算和及最大值for (let i = 0, j = imgList.length; i < j; i += i) {const { width, height } = await imgList[i].metadata()imgMetadataList.push({ width, height })totalHeight += heighttotalWidth += widthmaxHeight = 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 = 0const result = await imgList.reduce(async (input, overlay) => {const offsetOpt = {}if (mode === 'horizontal') {offsetOpt.left = imgMetadataList[imgIndex++].widthoffsetOpt.top = (maxHeight - imgMetadataList[imgIndex].height) / 2} else {offsetOpt.top = imgMetadataList[imgIndex++].heightoffsetOpt.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 服务端图片处理利器——sharp 进阶操作指南相关推荐

  1. node.js服务端笔记文档学会写接口,学习分类:path、包、模块化、fs、express、中间件、jwt、开发模式、cors。

    node.js 学习笔记 node.js服务端笔记文档学会写接口,path.包.模块化.fs.express.中间件.JWT.开发模式.cors. gitee:代码接口笔记 1什么是node.js n ...

  2. rds基于什么开发_为什么不学基于TypeScript的Node.js服务端开发?

    为什么不学?学不动了吗?!别躺下啊,我扶你起来! 我们早就知道,如今的JavaScript已经不再是当初那个在浏览器网页中写写简单的表单验证.没事弹个alert框吓吓人的龙套角色了.借助基于v8引擎的 ...

  3. 服务器项目混淆,压缩和混淆node.js服务端代码

    压缩和混淆node.js服务端代码 在前端我们有webpack,gulp等构建工具提供了从项目结构搭建到部署打包,基本所有工作流程所需要的都被覆盖到了. 在后台node.js写的服务端却是透明,很多时 ...

  4. 压缩和混淆node.js服务端代码

    压缩和混淆node.js服务端代码 在前端我们有webpack,gulp等构建工具提供了从项目结构搭建到部署打包,基本所有工作流程所需要的都被覆盖到了. 在后台node.js写的服务端却是透明,很多时 ...

  5. 56 Node.js服务端开发入门

    技术交流QQ群:1027579432,欢迎你的加入! 欢迎关注我的微信公众号:CurryCoder的程序人生 1.服务器端基本概念 1.1 网站的组成 网站应用程序主要分为两大部分:客户端和服务器端. ...

  6. node.js服务端搭建电影网站

    movieService[电影网站]服务端 基于node.js的express搭建的电影网站后台服务器,数据库为MongoDB,具有用户登录.注册.权限管理.专栏文章.点赞.电影推荐及增删改查管理的完 ...

  7. Node.js服务端开发总结(一)

    一.Node简介 为什么要学习Node 前端必备技能.可以更好的学习前端框架.能够进一步了解Web,有助于了解后端开发. Node是什么 Node.js是基于Chrome的V8 JavaScript引 ...

  8. bsdiff php,Apk差分升级Android客户端和Node.js服务端实现

    核心的内容是bsdiff和bspatch 源码根目录/bootable/recovery/applypatch下找到,bsdiff官网同样也是可以的,编出来的二进制文件可以在源码根目录out/host ...

  9. node.js服务端代码学习

    node.js不会自动重启,需要用到supervisor,查看代码的更改,随时重启node.js supervisor 安装 $ npm install -g supervisor 运行 app.js ...

  10. 签到APP:android入门级小项目,Node.js 提供服务端接口。

    2019独角兽企业重金招聘Python工程师标准>>> ###一.项目描述   实验室小伙伴们通过APP连接实验室路由器,比对路由器Mac地址进行签到.此外小伙伴们还可通过APP进行 ...

最新文章

  1. Matlab:成功解决Index must be a positive integer or logical
  2. 数据库系统服务器的运维
  3. 半平面交练习(计算几何)
  4. listview属性_属性提取器:获取ListView即时更新其元素的最佳方法
  5. Tempset 暴风射击
  6. ifen.os x pe.dmg天翼云_3.3K屏显纵览天下 11代酷睿横行职场 华硕灵耀X纵横值得选择...
  7. visionpro图片转图片数组_图片怎么转换成PDF格式文件?图片转PDF软件要这样操作...
  8. resnet的演化(res2net,resnext,se-resnet,sk-resnet,resnest)
  9. AVCHD格式转换为MP4格式的办法
  10. 金蝶K3系统物料替代功能的应用与业务逻辑探讨
  11. 电脑怎么分区硬盘分区方法
  12. C++complex复数类
  13. [人工智能-深度学习-29]:卷积神经网络CNN - 全连接网络与卷积网络结构的互为等效与性能比较
  14. 正六面体染色(java)
  15. 对接阿里云短信服务(附视频教程)
  16. atm系统的用例模型_ATM自动取款机用例图.doc-_装配图网
  17. 借助支付宝实现跨行跨省转账无手续费
  18. 阿里云 MNS 切换为 自建 RabbitMQ
  19. java计算机毕业设计ssm智慧小区团购系统4x45g(附源码、数据库)
  20. [译]为什么R语言是当今最值得学习的数据科学语言

热门文章

  1. 电子线路(线性部分)——第一章 晶体二极管
  2. 广告中的CPM、CPC、CPA解释
  3. win10制作dos启动U盘
  4. ssh远程重装Centos系统
  5. 软件工程的完整生命周期
  6. HTML做一个传统节日端午节 带设计报告4500字
  7. 用简单的Python HTTP server来共享文件
  8. Pr 视频速率与关键帧
  9. ZOJ,PKU--训练题分类
  10. ai画面怎么调大小_AI里面怎么改变文件的大小?