sharp 是 Node.js 平台上至关热门的一个图像处理库,其其实是基于 C 语言编写 的 libvips 库封装而来,所以高性能也成了 sharp 的一大卖点。sharp 能够方便地实现常见的图片编辑操做,如裁剪、格式转换、旋转变换、滤镜添加等。固然,网络上相关的文章比较多,sharp 的官方文档也比较详细,因此这不是本文的重点。这里主要是想记录一下我在使用 sharp 过程当中遇到的一些稍复杂的图片处理需求的解决方案,但愿分享出来可以对你们有所帮助。前端
sharp 总体采用流式处理模式,其在读入图像数据后通过一系列的处理加工而后输出结果。咱们看一个简单的示例就能理解:git
const sharp = require('sharp');
sharp('input.jpg')
.rotate()
.resize(200)
.toBuffer()
.then( data => ... )
.catch( err => ... );
复制代码
sharp 几乎全部的函数接口都挂载在 Sharp
实例上,所以图像处理的第一步操做必定是读入图片数据(sharp
函数接受图片本地路径或者图片 Buffer 数据做为参数)并将其转换为 Sharp
实例,而后才是如流水线通常的加工。所以,这里应该提供一个预处理函数,将服务端接收到的图片转换为 Sharp
实例:github
/** * * @param { String | Buffer } inputImg 图片本地路径或图片 Buffer 数据 * @return { Sharp } */
async convert2Sharp(inputImg) {
return sharp(inputImg)
}
复制代码
而后就能够进行具体的图像处理。canvas
添加水印功能应该算是比较常见的图片处理需求了。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
方法,看一下示例:async
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);
复制代码
实际上,整个添加水印的功能(选择原图、选择水印图片、设置水印图片位置、得到添加水印后的图片)是能够彻底由前端完成的。固然,为了追求服务端功能的完整性,仍是建议使用前端展现+后端处理的模式。ide
粘贴文字的需求实际上与添加水印比较相似。惟一不一样的是添加的水印图片换成了文字,以及咱们可能须要对文字的大小、字体等作一些调整。思路也比较容易想到,把文字转换成图片形式便可。这里咱们用到了 text-to-svg
库,做用是将文字转换成 svg。利用 svg 的特色咱们能够很方便地设置文字的字体大小、颜色等。而后调用 Buffer.from
将 svg 转换为 sharp 可使用的 buffer 数据。最后就是和上面的水印添加同样的步骤了。svg
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 更多的用法之后若是还有机会折腾,会继续跟你们分享~
本文首发于个人博客(点此查看),欢迎关注。