nodejs 图片编辑工具 sharp 使用及踩坑指南
本文介绍一下 node 中的一个常用的图像处理包 sharp 的主要用法,本文作者是在开发 screeps-world-printer 的时候接触到的这个包。在深入过程中也是遇到了不少坑和需要注意的地方,本文会一并指出。
下面先列一下常用的链接:
- sharp - npm
- sharp - 官网
- sharp - GitHub Issue 区:如果你要深入使用的话,这个地方绝对会来不少次
安装
安装没什么好说的,直接执行下面命令就行:
npm install sharp
# 或者
yarn add sharp
需要注意的是,sharp
是基于 libvips
的封装,所以在安装的时候会去下载 libvips
的本体,所以最好提前配置好镜像源。
创建图像
sharp 创建图像常用的有三种方法,直接生成、从文件读取、Buffer。咱们一个一个看:
直接生成:这个就是新建了,给 sharp 传入一个包含 create 对象的对象,create 里的内容就是新建图像的配置。最后的 background
属性可以是个更详细的对象,见 这里。
const sharp = require('sharp');
const image = sharp({
create: {
// 宽度
width: 300,
// 高度
height: 200,
// 通道数
channels: 4,
// 指定背景色
background: 'black' // 或者 '#000000' 或者更详细的 rgba 配置对象
}
});
从文件读取:这个更简单了,提供一个路径,sharp 会去这个路径找图片,找到了就读进来:
const image = sharp('./origin.png');
从 Buffer 读取:传入一个 buffer,sharp 会用这个 buffer 进行实例化,这个方法是最最常用的创建图像的方法,原因下文会提到。
# sharp 可以解析 svg,很实用
const buffer = Buffer.from(
'<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150"></svg>'
)
const image = sharp(buffer)
这种方式也适合拿来读网络请求来的图片:
// 用 axios 请求网络图片
const axios = require('axios');
axios.get('http://xxxxx/xx/origin.png', {
responseType: 'arraybuffer'
}).then(resp => {
const iamge = sharp(resp.data)
})
保存图像
保存图片有两种方法,一种是直接保存成图片:
const buffer = Buffer.from(
'<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150"></svg>'
)
sharp(buffer)
.png()
.toFile('./result.png')
.then(() => console.log('保存完成'))
第二种是转换成 buffer,然后怎么处理就随你了:
sharp('./origin.png')
.png()
.toBuffer()
.then(buffer => {
console.log('buffer', buffer)
})
这里有两点需要注意:
toBuffer
和toFile
都是异步方法。- 如果实例化的图片是无格式的,要先调方法设置其文件类型。
比如如果你保存的时候弹出来了这个错误:
Error: Unsupported output format ./result.png
这说明你要保存的图片是没有设置格式的(硬要说的话此时的格式为 原始格式 raw ),需要使用 .png
、.jpeg
之类的方法设置格式,具体见 这里。
修改图像
sharp 支持的图像修改操作包括 Resize 和 一大堆其他处理方法。这个也没啥好讲的,搞过图像处理的看名字就知道是搞啥的了。注意这里的方法都是 同步方法 并且支持链式调用,所以可以这样写:
const sharp = require('sharp');
const buffer = Buffer.from(`
<svg>
<rect width="100" height="100" fill="slategrey"/>
<circle cx="60" cy="106" r="50" fill="yellow"/>
</svg>
`)
sharp(buffer)
.flip()
.rotate(45)
.resize(500)
.toFile('./result.png')
.then(() => console.log('保存完成'));
拼接图像
拼接使用的是 composite 方法。
这个方法接收一个数组,数组每个元素都代表了一个要拼接到底图上的图片,其中的 input
属性代表图片本体,blend
代表拼接方法,left
代表拼接到的位置距离底图左边的(像素)宽度,top
代表拼接到的位置距离底图顶部的(像素)宽度:
const sharp = require('sharp');
const run = async function () {
// 100 * 300 的黑色长方形底图
const background = await sharp({ create: {
height: 100, width: 300, channels: 4, background: 'black'
}}).png();
// 100 * 100 的蓝色瓦片
const blueTile = await sharp({ create: {
height: 100, width: 100, channels: 4, background: 'blue'
}}).png().toBuffer();
background
.composite([{ input: blueTile, blend: 'atop', left: 0, top: 0 }])
.toFile('./testResult.png');
}
run();
然后就会生成:
这里的 blend
需要说道说道,上面例子里使用的值 atop
应该是最常用的,就是单纯的把一个图叠到另一张图上。其他支持的属性见 这里,详细介绍可以看 这里。
除了这个之外还有个 gravity
,它和 top
、left
一样是贴图的偏移配置,具体内容见 sharp 的 ts 声明,不再赘述。
这里需要注意的是:composite
方法接收的 input
属性不能是一个 sharp 实例!例如上面的例子里,我先用 sharp 创建了要拼接的瓦片,但是需要在最后使用 toBuffer
方法将其转换为 buffer 然后才能传入 composite 进行拼接,下面是文档中的介绍:
可以看到,要么传入 buffer 或者路径字符串,要么就是用 create 创建新的图片,唯独不能使用 sharp 实例。
除此之外 composite
还有个比较大的问题:不支持链式调用!,还是上面这个例子,如果我们链式一下,尝试用三块蓝色铺满背景图:
background
.composite([{ input: blueTile, blend: 'atop', left: 0, top: 0 }])
.composite([{ input: blueTile, blend: 'atop', left: 100, top: 0 }]) // <= 指定了往右偏移
.composite([{ input: blueTile, blend: 'atop', left: 200, top: 0 }])
.toFile('./testResult.png');
在期望里,它的最终结果应该是全部被蓝色填满了。但是最后生成的却是这样的:
只有最后一个 composite 生效了,要想解决这个问题有两种办法:
第一个办法是 不使用链式调用,把所有的瓦片都放在同一个 composite 接收的数组里:
background
.composite([
{ input: blueTile, blend: 'atop', left: 0, top: 0 },
{ input: blueTile, blend: 'atop', left: 100, top: 0 },
{ input: blueTile, blend: 'atop', left: 200, top: 0 }
])
.toFile('./testResult.png');
这样最大的问题是需要同时把要拼接的所有图片载入到内存里,当要拼接亿像素级别的图像时,对于小内存机器可能不太友好。并且 composite 是同步操作,一定要小心不要让他卡死了进程。
第二个办法是 每次 composite 之后 toBuffer 然后重新实例化,你可以在 这里 找到具体的例子。怎么说呢,我感觉写起来还是很变扭,issue 区对于为什么 composite 不支持链式意见也不小,但是目前(2021-09-30)还没改。
如果你需要处理一张超大图片,我的建议是用第一种方法,先少量并发把每行拼接好保存到本地。等所有行都拼接好之后再把保存路径同时传给大底图把所有行拼在一起。
不仅是 composite 链式调用有问题,composite 和其他操作链式调用也有问题,例如下面的例子,先拼接再缩放,期望里蓝色方块也会被同步缩放,但是最后结果却和先缩放再拼接一样:
再比如,我们拼接、缩放完之后再左右颠倒一下:
background
.composite([{ input: blueTile, blend: 'atop', left: 0, top: 0 }])
.resize(500)
.flop(true) // <= 左右颠倒
.toFile('./testResult.png');
然后就会发现,wtf 颠倒的操作为什么没生效:
在这种情况下我就比较推荐第二种解决方法了。如下,每次操作之后先转换成 buffer(按照 dev 的说法是生成个快照 ):
let buffer = await background
.composite([{ input: blueTile, blend: 'atop', left: 0, top: 0 }])
.toBuffer();
buffer = await sharp(buffer)
.composite([{ input: blueTile, blend: 'atop', left: 0, top: 0 }])
.resize(500)
.toBuffer();
sharp(buffer)
.flop(true)
.toFile('./testResult.png');
最后的结果就和我们预期的一致了:
修改透明度
经常会有需求是调整一个图片的整体透明度,但是 sharp 这里就拉了跨,在 api 文档的通道操作里我们可以看到:
remvoeAlpha
是直接砍掉透明通道,ensureAlpha
是如果透明通道不存在的话就补上。没有直接调整透明度的 api。
这里把解决方法贴上,来自官方 issue 区,当时翻了好久才找到,也是非常巧妙了:
/**
* 给图片添加透明度
* 由于 sharp.ensureAlpha 只会为没有透明通道的图片添加透明通道
* 当一个图片已经有透明通道时是无法使用该方法调整透明度的
* 这里用的方法来自 @see https://github.com/lovell/sharp/issues/618#issuecomment-532293211
*
* @param sharp 要添加的图片
* @param opacity 透明度 0 - 255,255 是完全不透明
* @returns 添加透明度之后的图片
*/
export const addOpacity = function (sharp: Sharp, opacity = 128): Sharp {
return sharp.composite([{
input: Buffer.from([255, 255, 255, opacity]),
raw: { width: 1, height: 1, channels: 4 },
tile: true,
blend: 'dest-in'
}]);
};
一个可疑的 npm ERR 问题
在我写码的时候也遇到了一个如下的报错:
npm ERR! code ELIFECYCLE
npm ERR! errno 3221225725
npm ERR! screeps-world-printer@1.0.0 case: `node test.js`
npm ERR! Exit status 3221225725
npm ERR!
npm ERR! Failed at the screeps-world-printer@1.0.0 case script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm ERR! A complete log of this run can be found in:
npm ERR! C:\Users\a\AppData\Roaming\npm-cache_logs\2021-08-30T12_29_35_503Z-debug.log
最后发现是在 windows 的机器下用 npm run
运行代码时就会报这个错,具体的复现代码可以看 这里。
修改像素上限
在创建 sharp 实例或者 composite 的时候如果遇到了下面的问题:
Error: Input image exceeds pixel limit
那么说明你要读取的图像过大超过了默认的像素上限,你可以通过指定 limitInputPixels
的方式调整像素上限,如下:
// 新建时
sharp('./bigImage.png', { limitInputPixels: 1000000000 }).toFile('result.png');
// 拼接时
sharp('./origin.png')
.composite([{ input: 'bigImage.png', limitInputPixels: 1000000000 }])
.toFile('result.png')
写在最后
本文的内容到这里就差不多结束了。虽然遇到了不少问题,但是最终还是完成了目标。我用的功能主要集中在缩放和拼接方面,对于图像和颜色的操作用的比较少,如果上面写的东西有误的话欢迎指正。
转载自:https://juejin.cn/post/7013595520258015262