Code前端首页关于Code前端联系我们

Node.js 服务器端图像处理工具 - Sharp 实现指南

terry 2年前 (2023-09-25) 阅读数 48 #后端开发

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 图像处理的第一步几乎是通过示例设置的 Sharpsharp 接受图像位置或 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 })
}
复制代码

照片拼接

照片拼接是一项艰巨的任务。这里我们提供了两个自定义元素:布局样式(水平/垂直)和背景颜色。更容易理解如何连接。无非是图片水平或垂直的排列。背景颜色用于填充空白区域。组合图像时,图像以轴为中心。以图像水平排列为例,示意图如下: Node.js 服务端图片处理利器——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)
}
复制代码

然后我们使用将获得的宽度和高度以输入配置的背景颜色(或普通白色)创建一个新的基础图像:

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前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门