Node.js 服务端图片处理利器——sharp 进阶操作指南

Node.js 服务端图片处理利器——sharp 进阶操作指南

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 += 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 } 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 更多的用法以后如果还有机会折腾,会继续跟大家分享~

oss图片上传api_怎么上传图片到OSS相关推荐

  1. oss图片上传api_交互式核保系统:api明细:图片上传oss接口

    { "base_num": "data:image/png;base64,iVBORtejk//pII=" } 返回说明:外层的result_code是指htt ...

  2. dcat-admin oss图片上传

    dcat-admin oss图片上传 安装插件 composer require jacobcyl/ali-oss-storage:^2.1 在app.php的provider数组中添加: Jacob ...

  3. 阿里云对象存储OSS图片上传并回显

    阿里云对象存储OSS图片上传并回显 Java代码实现 引入依赖 <!-- 引入阿里云OSS依赖--><dependency><groupId>com.aliyun. ...

  4. 记录一个阿里云OSS图片上传错误

    upload.js?c0e8:599 POST https://gulimall-.oss-cn-shanghai.aliyuncs.com/ 403 (Forbidden) 今天打开项目运行的时候放 ...

  5. 使用阿里云的OSS图片上传,这里是用的上传网络流

    使用阿里云的OSS图片上传,这里是用的上传网络流 思路: /** 文件上传到OSS 上传网络流 以下代码用于上传网络流: 1.通过element的el-upload插件,通过action发送请求到后台 ...

  6. 若依-vue图片上传本地改OSS前台以及后台-附带oss图片上传工具类

    阿丹: 在二次开发若依的过程中发现若依的图片上传的默认的是在本地,在spring-vue版本中,如果要将平台上线那么就需要考虑这个问题,要使用fastdfs或者oss来完成代替本地的图片上传. 本篇文 ...

  7. flutter阿里云OSS图片上传

    一.选择图片: 使用插件 image_picker: "^0.5.0+3" 使用image_picker选择图片,代码如下: // 相机拍照或者从图库选择图片pickImage(c ...

  8. python与html交互实现图片上传_python 实现上传图片并预览的3种方法(推荐)

    在常见的用户注册页面,需要用户在本地选择一张图片作为头像,并同时预览. 常见的思路有两种:一是将图片上传至服务器的临时文件夹中,并返回该图片的url,然后渲染在html页面:另一种思路是,直接在本地内 ...

  9. java 二进制图片上传_Spring MVC上传图片,Java二进制图片写入数据库,生成略缩图...

    背景描述:最近做到一个项目,有个商品登记功能.登记的信息包括:基本信息若干(文字信息):图片信息,要求将图片保存到数据表中的image字段(sql server 数据库) 步骤:1.将图片上传到服务器 ...

最新文章

  1. UA OPTI570 量子力学34 Harmonic Perturbation简介
  2. 图形处理(七)基于热传播的测地距离计算-Siggraph 2013
  3. IDEA+Hadoop运行TriangleCount程序
  4. SAP 月结F.19与GR/IR
  5. vue - blog开发学习4
  6. 世界上最伟大的十个公式,看看你懂得几个?
  7. JS call()和apply()方法和区别
  8. Ranger-AdminServer安装
  9. 乌云挂了,知识库的文章却在流传
  10. 智能会议系统(7)---实时音视频技术难点及解决方案
  11. tp框架使用心得(六)——分页查询
  12. JS中style属性
  13. linux查看端口出现unix,linux查看端口被占用状况
  14. QImage类详解(QImage类型转换、QImage类函数及QImage像素操作)
  15. z变换与s变换之间的转换(一些零碎且不严谨的想法)
  16. noteexpress选择网页作为题录的使用方法
  17. 基于VerilogHDL的VGA驱动设计
  18. 纯html+css制作banner轮播图
  19. 解决Windows x86网易云音乐不能将音乐下载到SD卡的BUG
  20. 腾讯广告终于迎来全面整合

热门文章

  1. 人际交往三个常见问题
  2. 【C++】7-41 互评成绩(PTA)
  3. 微信公众号开发-(.net)
  4. 【软件测试】软件测试基础知识
  5. VUE ELEMENT UI 清空select 下拉选项
  6. t3软件怎么生成报表_t3财务报表
  7. java经典题之冒泡排序
  8. Java学习26--------冒泡排序
  9. 关于文件读写缓存的问题(flush的使用场景)
  10. linux系统查看进程