Node.js 服务端图片处理利器
sharp 是 Node.js 平台上相当热门的一个图像处理库,其实际上是基于 C 语言编写 的 libvips 库封装而来,因此高性能也成了 sharp 的一大卖点。sharp 可以方便地实现常见的图片编辑操作,如裁剪、格式转换、旋转变换、滤镜添加等。当然,网络上相关的文章比较多,sharp 的官方文档也比较详细,所以这不是本文的重点。这里主要是想记录一下我在使用 sharp 过程中遇到的一些稍复杂的图片处理需求的解决方案,希望分享出来能够对大家有所帮助。
https://blog.csdn.net/weixin_33972649/article/details/88026389
sharp 基础
sharp 整体采用流式处理模式,其在读入图像数据后经过一系列的处理加工然后输出结果。我们看一个简单的示例就能理解:
const sharp = require('sharp');
.resize(200)
.toBuffer()
.then( data => ... )
.catch( err => ... );
sharp 几乎所有的函数接口都挂载在 Sharp
实例上,因此图像处理的第一步操作一定是读入图片数据(sharp
函数接受图片本地路径或者图片 Buffer 数据作为参数)并将其转换为 Sharp
实例,然后才是如流水线一般的加工。因此,这里应该提供一个预处理函数,将服务端接收到的图片转换为 Sharp
实例:
/**
然后就可以进行具体的图像处理。
添加水印
后端实现
添加水印功能应该算是比较常见的图片处理需求了。sharp 在图像合成方面只提供了一个函数:overlayWith
,其接受一个图片参数(同样是图片本地路径字符串或者图片 Buffer 数据)以及一个可选的 options
配置对象(可配置水印图片的位置等信息)然后将该图片覆盖到原图上。逻辑上也比较简单,我们的代码如下所示:
/**
* @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");
实际上,整个添加水印的功能(选择原图、选择水印图片、设置水印图片位置、获得添加水印后的图片)是可以完全由前端完成的。当然,为了追求服务端功能的完整性,还是建议使用前端展示+后端处理的模式。
粘贴文字
粘贴文字的需求实际上与添加水印比较类似。唯一不同的是添加的水印图片换成了文字,以及我们可能需要对文字的大小、字体等做一些调整。思路也比较容易想到,把文字转换成图片形式即可。这里我们用到了 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
然后我们用得到的宽度和高度数据新建一个背景颜色为传入配置(或默认白色)的 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 服务端图片处理利器的更多相关文章
- node.js服务端程序在Linux上持久运行
如果要想在服务端部署node.js程序,让其持久化运行,就不能单单使用npm start命令运行,当然了,这样运行是毫无问题的,但是当关闭xshell窗口或者是关闭进程的时候(其实关闭xshell窗口 ...
- Node.js 服务端处理图片
Node 服务端处理图片 服务端进行图片处理是很常见的需求,但是Node在这一块相对来说比较薄弱.找了几个比较常见的模块来解决问题. gm GraphicsMagick for node 使用Open ...
- [转] Node.js 服务端实践之 GraphQL 初探
https://medium.com/the-graphqlhub/your-first-graphql-server-3c766ab4f0a2#.n88wyan4e 0.问题来了 DT 时代,各种业 ...
- Node.js 本地Xhr取得Node.js服务端数据的例子
本以为用XHR取Nodejs http出的一段文字很简单,因为xhr取值和nodejs http出文字都是好弄的,谁知一试不是这回事,中间有个关键步骤需要实现. nodejs http出文字显示在浏览 ...
- ASP.NET Core 与 Vue.js 服务端渲染
http://mgyongyosi.com/2016/Vuejs-server-side-rendering-with-aspnet-core/ 原作者:Mihály Gyöngyösi 译者:oop ...
- CKEditor 自定义按钮插入服务端图片
CKEditor 富文本编辑器很好用,功能很强大,在加上支持服务端图片上传的CKFinder更是方便, 最近在使用CKFinder的时候发现存在很多问题,比如上传图片的时候,图片不能按时间降序排列,另 ...
- NET Core 与 Vue.js 服务端渲染
NET Core 与 Vue.js 服务端渲染 http://mgyongyosi.com/2016/Vuejs-server-side-rendering-with-aspnet-core/原作者: ...
- 基于 Egg.js 框架的 Node.js 服务构建之用户管理设计
前言 近来公司需要构建一套 EMM(Enterprise Mobility Management)的管理平台,就这种面向企业的应用管理本身需要考虑的需求是十分复杂的,技术层面管理端和服务端构建是架构核 ...
- 实践案例丨教你一键构建部署发布前端和Node.js服务
如何使用华为云服务一键构建部署发布前端和Node.js服务 构建部署,一直是一个很繁琐的过程 作为开发,最害怕遇到版本发布,特别是前.后端一起上线发布,项目又特别多的时候. 例如你有10个项目,前后端 ...
随机推荐
- MySQL kill进程后出现killed死锁问题
公司同事删除一张大表的数据,本想直接drop表,但是使用了delete删除表,发现很慢,就kill了这个操作, 但是,kill后,表锁住了,因为在回滚表数据. 原文链接:https://blog.cs ...
- DVWA-XSS练习
本周学习内容: 1.学习web应用安全权威指南: 2.学习乌云漏洞: 实验内容: DVWA实验XSS跨站脚攻击 实验步骤: Low 1.打开DVWA,进入DVWA security模块,将难度修改为L ...
- 自用ajxa 后台管理请求
/** * 保存或者修改商品信息 * @returns */ function saveOrUpdateBaseGoodInfo(){ var json={}; var goodName=$.trim ...
- LibreOJ #528. 「LibreOJ β Round #4」求和
二次联通门 : LibreOJ #528. 「LibreOJ β Round #4」求和 /* LibreOJ #528. 「LibreOJ β Round #4」求和 题目要求的是有多少对数满足他们 ...
- nginx+uwsgi+python3+pipenv+mysql+redis部署django程序
1.下载项目 git clone https://github.com/wangyitao/MyBlogs.git 2.进入Myblogs目录 cd MyBlogs 3.创建虚拟环境并且安装依赖 pi ...
- PHP-FPM远程代码执行漏洞(CVE-2019-11043)
0x00 简介 在长亭科技举办的 Real World CTF 中,国外安全研究员 Andrew Danau 在解决一道 CTF 题目时发现,向目标服务器 URL 发送 %0a 符号时,服务返回异常, ...
- Easytrader踩坑之旅(二)
快速阅读 用的是THSTrader进行的调试,同花须必须用8.0的. 在新的机子重新安装requirements已经调用同花顺查股票余额. 继续昨天的话费. 昨天到最后,虽然显示了余额,但是和自己帐户 ...
- UDP如何实现可靠传输
概述 UDP不属于连接协议,具有资源消耗少,处理速度快的优点,所以通常音频,视频和普通数据在传送时,使用UDP较多,因为即使丢失少量的包,也不会对接受结果产生较大的影响. 传输层无法保证数据的可靠传输 ...
- 小福bbs-冲刺日志(第二天)
[小福bbs-冲刺日志(第二天)] 这个作业属于哪个课程 班级链接 这个作业要求在哪里 作业要求的链接 团队名称 小福bbs 这个作业的目标 UI重构完成 作业的正文 小福bbs-冲刺日志(第二天) ...
- python中的__init__方法
init()方法意义重大的原因有两个.第一个原因是在对象生命周期中初始化是最重要的一步:每个对象必须正确初始化后才能正常工作.第二个原因是init()参数值可以有多种形式. __init__方法使用 ...