引言
图片压缩在项目中是一个常见的需求, 通过减小图片文件的大小, 降低存储和传输成本, 提高网页加载速度, 以及减少带宽占用!
个人站点 《仿 Mac 个人网站开发 |项目复盘》目前是使用 TinyPNG 提供的 API 来压缩图片的, 实际上目前系统就我一个人在使用, 所以 TinyPNG 一个月 500次 的免费额度还完全够用!!! 唯一的遗憾就是 TinyPNG 不支持 GIF 图片压缩, 但是考虑到大部分 GIF 录制软件导出的图片都是可以通过设置参数, 达到压缩的效果, 所以也就这样先用着咯....
直到有幸看到 神光大大 的文章 《Nest + Sharp 实现了一个 gif 压缩工具, 帮我省不少钱》, 原来 Node 完全可以使用 Sharp 来完成图片的压缩、包括 GIF, 所以就生出了改造项目的想法, 而本文就是整个改造过程的一个简单记录....
一、现有方案「TinyPNG」介绍
正如引言所述, 目前个人站点压缩图片用的是 TinyPNG 所以在开始前, 容我简单介绍下它!!!
1.1 是什么?

正如官网所说, TinyPNG 是一个强大的图片压缩工具, 它使用智能有损压缩技术可以将我们的的 WebP、PNG、JPEG 等图片的文件大小进行一个压缩, 它可以通过选择性的减少图片中的颜色, 使得只需要很少的字节数就能保存数据, 在视觉上压缩的图片前后影响几乎不可见, 但是在文件大小上却有着显著效果!!!

同时 TinyPNG 官方客户端代码库支持各种语言, 通过官方提供的 SDK 我们可以轻松调用它们的 API, 实现图片压缩功能!!!
当然天下没有免费的午餐, TinyPNG API 每月是会有 500次 的免费额度, 超过部分则需要收费咯!!! 当然个人使用的话 500次 基本就够用了!!

1.2 怎么用?
WEB版: 直接进入 TinyPNG 首页, 如图, 将图片拖拽到指定区域或者直接点击(会弹出文件选择窗)选择图片, 选择完图片将自动压缩

压缩完成会有下载入口, 直接下载即可

Node环境使用: 首先需要申请一个API密钥, 这里需要先登录(填写邮件, 邮箱会有个登录按钮, 点击就可以登录); 登录后进入「开发者 API」页面, 然后填写信息获取API密钥

这里也会发一个邮箱, 点击邮箱邮件中按钮会来到 API 管理页面, 这里我们就可以看到一个 API 密钥了

有了 API 密钥, 在 Node 中我们就可以使用 tinify 来完成图片的压缩了
js
import tinify from 'tinify'
// 1. 设置 api key
tinify.key = "YOUR_API_KEY";
// 2. 方法一: 输入为 buffer, 输出为 buffer
tinify.fromBuffer(buffer).toBuffer((err, resultData) => {
error = err;
resolve(err ? buffer : resultData);
});
// 3. 方法一: 输入为「图片路径」, 输出为「图片路径」
tinify.fromFile("unoptimized.png").toFile("optimized.png");
1.3 为什么要放弃?
- 每个月只有
500次的免费额度, 虽然目前够用, 但未来就不一定咯(毕竟砸们也是有很大愿景的) - 无法压缩
GIF - 既然
Node可以自己做, 又何必受限于 TinyPNG
二、新方案「Sharp」
新方案就是使用 Sharp 来实现, 有了它我们就可以在 Node 中实现图片的压缩, 可以将常见格式的大图转换为较小的、网络友好的不同尺寸的 JPEG、PNG、WebP、GIF 和 AVIF 图像, 同时除了压缩图片, 它还提供了很多强大的图片处理能力....
Sharp 使用起来也炒鸡简单, 如下所示, 只需要一行代码即可完成图片的压缩, 其中 quality 是压缩图片常见的一个参数, 它指的是图片压缩后的 质量, 该值可选范围为 1 ~ 100, 数值越高图片质量越好相应的压缩比例也就越低, 该值通常建议为 75-80, 不建议小于 60
js
import sharp from 'sharp'
sharp('1.png').png({ quality: 75 }).toFile('2.png')
来测试下效果: 随便找了个图, 测试了下, 直接将从 112KB 压缩到了 31KB, 这压缩率还是很给力的!!!!

特别的是 gif、raw、tile 是不能调整 quality 参数的, 所以如果想要对 GIF 进行压缩就得另外处理了, 如下代码所示:
animated: 设置为true可读取动画图像(GIF、WebP、TIFF)的所有帧, 否则默认只会读取第一帧limitInputPixels: 默认情况下 Sharp 能处理的像素数是有限制的(268402689), 所以图片如果太大将会报错, 如果要解除该限制就可以将该参数设置为falsecolours: 设置调色板(基础颜色)的最大数量, 它的值介于2到256之间, 数值越小那么绘制出来的gif画质越差, 但是体积相对也越小!! 所以这里得自己多做些尝试, 找到一个合适的数值!!
js
sharp('ScreenFlow.gif', {
animated: true,
limitInputPixels: false
}).gif({
colours: 2,
}).toFile('2.image.gif')
如下图, 是在 colours 设置为 2 情况下的对比图, 可以发现画质严重损坏了, 但是文件体积直接从 36.3MB -> 2.1MB

这里我们还是那句话需要耐心得多做些测试, 找到一个合适的数值, 下面是 128 的一个情况, 只要不放大看还算能接受, 体积也是从 36.3MB -> 3.3MB

三、项目改造
- 先安装
sharp依赖
sh
npm i sharp
- 下面我们使用
sharp来封装一个通用的方法, 由于gif、raw、tile是不能调整quality参数, 并且针对gif我们还需要特殊处理, 所以就需要对图片进行判断处理, 具体实现如下:
- 函数的入参和出参都是文件流(
stream)数据, 所以这里针对stream和buffer做了些转换工作 switch根据不同文件格式设置不同的options、formatOptionsawait sharp(imagesBuffer).metadata()使用sharp获取文件的meta数据
js
import sharp from 'sharp';
import { streamToBuffer, bufferToStream } from '#utils/fs';
/**
* 压缩图片
*
* @param {stream} stream 要压缩图片流
* @returns {stream} 返回的是压缩后的流
*/
export default async (stream) => {
const imagesBuffer = await streamToBuffer(stream);
const metadata = await sharp(imagesBuffer).metadata();
let options = {}; // sharp 配置
let formatOptions = {}; // 不同格式方法参数
// 根据文件格式, 设置不同的配置
switch (metadata.format) {
case 'gif':
options = {
animated: true,
limitInputPixels: false,
};
formatOptions = { colours: 128 };
break;
case 'raw':
case 'tile':
break;
default:
formatOptions = { quality: 75 };
}
// 压缩: 调用 sharp
const newBuffer = await sharp(imagesBuffer, options)
?.[metadata.format](formatOptions)
.toBuffer();
// 返回文件流
return bufferToStream(newBuffer);
};
最后补充两个 stream 和 buffer 相互转换的方法
js
// #utils/fs
// Stream 转 Buffer
export const streamToBuffer = (stream) => new Promise((resolve, reject) => {
const buffers = [];
stream.on('error', reject);
stream.on('data', (data) => buffers.push(data));
stream.on('end', () => resolve(Buffer.concat(buffers)));
});
// Buffer 转 Stream
export const bufferToStream = (buffer) => {
const stream = new Duplex();
stream.push(buffer);
stream.push(null);
return stream;
};
- 压缩效果: 最后看下
png和gif的一个压缩效果吧, 压缩率还是很给力, 图片质量还是ok的

- 最后卸载
tinify, 清理相关代码
sh
npm uninstall tinify