Node.js 服务器端图像处理工具 - Sharp 实现指南
sharp 是 Node.js 平台中最流行的图像处理库。它是基于C语言编写的libvips库进行高度编码的,因此性能成为了它的最大卖点。 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
,它接受图像参数(包括本地路径字符串或图像缓冲区数据)和选项选项
可选的有关水印的其他信息)和将图像粘贴到原始图像上。逻辑也很简单。这是我们的代码:
/**
* 添加水印
* @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 还支持更复杂的压缩参数,例如多图像压缩和仅 Alpha 通道压缩。对于图像水印等,请参阅 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 })
}
复制代码
照片拼接
照片拼接是一项艰巨的任务。这里我们提供了两个自定义元素:布局样式(水平/垂直)和背景颜色。更容易理解如何连接。无非是图片水平或垂直的排列。背景颜色用于填充空白区域。组合图像时,图像以轴为中心。以图像水平排列为例,示意图如下:
夏普没有提供任何前期工作。一切都通过唯一的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)
}
复制代码
然后我们使用将获得的宽度和高度以输入配置的背景颜色(或普通白色)创建一个新的基础图像:
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()
复制代码
然后在基础图像的基础上重复调用该函数 overlayWith
将需要的图像叠加起来与基础图像单独组合。这里需要考虑的是图像的位置。前面提到,我们会根据实轴对图像进行聚焦,所以每次放置图像时,我们都需要计算顶部和左侧(一个是中心计算,另一个是根据实轴计算偏移)顺序放置图片)。当然,拿到规则后,就变成了一道小学数学题。没什么好说的。另外需要注意的是,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法则”:80%的需求通常用20%的工作来完成。
作者:倪奎
链接:https://juejin.im/post/5b0bd60e6fb9a00a1610e4be
来源:作者掘金。如需商业印刷,请联系作者以获得许可。非商业转载请注明来源。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。