使用 sharp 实现图片转换

为什么是 sharp?

最近在升级 coderyuan-image-server 这个图片服务的项目,主要是想增加对 AVIF、HEIC 图片格式的支持,于是就想找找新的方案。

以前的方案是使用 GraphicsMagick 或者 ImageMagick 来进行图片的转换,但是这两个库对于 HEIC 和 AVIF 这样的编码格式支持的并不好,所以必须要放弃它们了。

偶然间发现 sharp 这个库,从它官方的介绍中看,貌似 API 更简单,而且支持的格式也更多,性能更好,所以就选择它作为图片转换的实现方案了。

先介绍一下 sharp 和它背后的 libvips

简单来说 sharp 是基于 libvips 的 Node.js 图片处理实现,根据 libvips 官方的说明,libvips 具有速度快、占用内存少、支持格式多的特点(具体工作原理参见:https://www.libvips.org/API/current/How-it-works.html),非常适合做静态图片的转换工作,能够在各方面超越传统的 GrapicsMagick 和 ImageMagick。

基于 libvips 出色的性能和丰富的格式支持,也诞生了众多语言的封装,主流的 Java、Python、Go 等语言都有对应的实现,也有一系列基于 libvips 能力扩展的图片处理工具库,sharp 就是其中之一,也几乎是 JavaScript 环境下唯一可用的 libvips 实现。

sharp 底层其实也是 Node C++ 实现的,可以通过在 npm install 后编译 Native 代码,实现多个平台的图片处理功能。

存在的问题

此次升级 coderyuan-image-server 的主要需求是支持 AVIF 和 HEIC 格式的图片,而 sharp 的文档中说明了,sharp 默认使用的 libvips 无法支持真正以 HEVC 编码的图片格式,默认只能以 AV1 编码存储。如果在转换时使用如下代码:

1
2
3
sharp().heif({
compression: 'hevc'
})

则会收到如下错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
/home/yuanguozheng/github/image-utils/node_modules/sharp/lib/output.js:1443
const stack = Error();
^

Error: heifsave: Unsupported compression
at Sharp._read (/home/yuanguozheng/github/image-utils/node_modules/sharp/lib/output.js:1443:19)
at Readable.read (node:internal/streams/readable:737:12)
at resume_ (node:internal/streams/readable:1255:12)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21)
Emitted 'error' event on Sharp instance at:
at Object.<anonymous> (/home/yuanguozheng/github/image-utils/node_modules/sharp/lib/output.js:1487:18)

Node.js v20.18.0

如果只使用 AV1 格式编码的 HEIC 图片,虽然体积更小,但在较低版本的 Safari 浏览器上,则无法正常显示,而 Safari 又不支持 WebP 格式,只能以体积较大的 JPG/PNG 格式返回,导致图片加载速度变慢。

由于在 Linux 系统上,libvips 需要使用 libheif 来支持 HEIC 格式的编码工作,所以如需支持 HEVC 编码格式,需要先安装 libheif 库,如果已经安装,需要补充其 Plugin,然后重新编译安装 libvips 库,完成后再触发 sharp 的 Native 层编译安装。

而 macOS 相对简单,可以直接通过 brew 安装 libvips。

解决 libvips 问题

以下实践基于 Ubuntu 24.04 x86-64 系统环境。

如果你懒得折腾,而且系统是 Ubuntu,可以直接使用我的一键安装脚本安装 libvips,但需要确保你配制的 Ubuntu 的软件源和 Github 都可以正常快速的访问:

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/yuanguozheng/coderyuan-image-server/refs/heads/master/tools/install_vips_ubuntu.sh)"

脚本的工作过程和下面的内容是一致的。

1. 卸载已安装的预编译二进制包

首先,如果已经安装了 Ubuntu 系统自带的 libvips 包,需要先卸载它们,否则可能导致冲突,无法使用,命令如下:

1
sudo apt-get purge --auto-remove libvips* 

2. 安装必要的编译工具链

1
sudo apt install build-essential meson cmake pkg-config libglib2.0-dev

build-essential、cmake 是 Linux 常见的编译构建工具链,而安装 meson、pkg-config 是因为 libvips 使用它们作为构建工具。

3. 安装基础图片的底层能力

1
sudo apt install libarchive-dev libfftw3-dev libmagickcore-dev imagemagick libcfitsio-dev libimagequant-dev libjpeg-dev libspng-dev libpng-dev libwebp-dev libtiff-dev librsvg2-dev libcairo2-dev openslide-tools libmatio-dev liblcms2-dev libopenexr-dev libopenjp2-7-dev libheif-dev libjxl-dev libpoppler-glib-dev liborc-0.4-dev libnifti-dev gettext gir1.2-glib-2.0 libexpat1-dev libcgif-dev libgsf-1-dev libglib2.0-dev liborc-dev libopenslide-dev libjpeg-turbo8-dev libexif-dev libtiff5-dev libpango1.0-dev

以上命令包含了基础的图片处理能力底层库,包括 JPG、PNG、TIFF 等常见图片格式的处理以及图片信息获取工具等,由于这些格式也是 libvips 能够处理的,所以建议都安装上。

4. 安装补全 libheif 处理能力

Ubuntu 一般会自带 libheif,如果没有安装,建议使用 apt 安装预编译包,由于依赖的底层编解码库实在太多了,这里就不推荐从源码安装了,如果是在性能不佳的服务器上,可能会卡死。

安装 libheif 可以使用如下命令:

1
sudo apt install libheif-dev

如果已经安装了 libheif,建议使用下面的命令补全其可以支持的格式:

1
sudo apt install libheif-plugin*

https://github.com/libvips/libvips/releases/download/v8.15.3/vips-8.15.3.tar.xz
这样可以补全 libheif 处理我们需要的 AV1、HEVC 编码的能力。

5. 源码编译安装 libvips

完成 libheif 的插件补全工作后,就可以着手 libvips 的源码编译安装了,建议从其 Github 的 Release 页面 中下载最新的正式版本源码包(非 RC、Pre-release 等,以 tar.xz 结尾)。

比如:https://github.com/libvips/libvips/releases/download/v8.15.3/vips-8.15.3.tar.xz

可以使用 wget 进行下载:

1
wget https://github.com/libvips/libvips/releases/download/v8.15.3/vips-8.15.3.tar.xz

下载完成后使用 tar xvf vips-8.15.3.tar.xz 解压得到 vips-8.15.3 目录,即为 libvips 的源码,接着使用 cd vips-8.15.3 进入其目录:

1
tar xvf vips-8.15.3.tar.xz && cd vips-8.15.3

此时就需要使用到 meson 进行构建配置:

1
meson setup build --libdir=lib --buildtype=release

完成后会展示如图所示的可支持编码格式的情况:

libvips-code

可以看到我们需要的 HEIC、AVIF、WebP 等高压缩格式,以及传统的 JPEG、PNG、TIFF 等都得到了支持,此时需要先进入到 build 目录中后,再执行 compile 命令进行构建:

1
cd build && meson compile

等待所有构建任务完成后,需要使用 sudo 进行安装:

1
sudo meson install

等待执行完成后,可以使用 vips --vips-config 命令验证 libvips 的安装是否成功,如下图所示:

after-install

正确引入 sharp 到 Node.js 项目中

首先需要为项目添加 node-gypnode-addon-api 依赖,前者用于为 npm 模块构建 Native 层能力,后者用于在 JavaScript 层调用 Native 层的 API。

node-addon-api 需要添加到 dependencies 中,而 node-gyp 不做强制要求,可以添加到 devDependencies 中,亦或是两者都不添加到 package.json 中,只在 npm 安装依赖前手动执行 install 来安装到 node_modules 目录中。

这里以 coderyuan-image-server 项目为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"name": "coderyuan-image-server",
// ...
"dependencies": {
// ...
"node-addon-api": "^8.2.1",
"sharp": "^0.33.5"
},
"devDependencies": {
// ...
"node-gyp": "^10.2.0"
}
}

也可以直接使用 coderyuan-image-server 的源码:

1
git clone https://github.com/yuanguozheng/coderyuan-image-server.git

完成后进入项目目录,执行 npm install 安装依赖,此时会触发 sharp 的 Native 层编译安装。

如果 sharp 能够正常编译,则在 node_modules/sharp/src/build/Release 目录中会看到 sharp-linux-x64.node 这样的文件,即为 Native 层能力。

sharp-native

此时也可以使用 coderyuan-image-server 中提供的测试代码进行测试:

1
node tools/codec_test.js

如果能够支持 WebP、HEIC、AVIF 三种格式,则可以在命令行中看到如下输出:

test-output

如果出现任何报错,那么证明 sharp 并没有触发 Native 层的编译安装,需要检查 libvips、node-gyp、node-addon-api、libheif 这些底层依赖的任何一项是否正确、完整的安装了。

也可以先进入到 node_modules/sharp 目录中,再使用 node install/check.js 命令检查 sharp 的编译安全条件是否满足,如果满足条件则会自动触发 Native 层的编译,否则将会列出具体原因,或者直接抛出异常。

check-failed

根据提示完成 libvips、node-gyp、node-addon-api 等依赖的安装后,建议将 node_modules/sharp 目录删除,然后重新执行 npm install 安装依赖,或者执行 npm install --build-from-source 强制触发 Native 层的编译安装。

总结

sharp 是一个优秀的图片处理库,在 Node.js 中使用非常方便,但需要以正确的姿势安装相关依赖,这样才能充分利用 libvips 的优势,实现高效的图片处理。