Sharp: 压缩图片用啥 TinyPNG, 我也可以, 而且还免费呢?

引言

图片压缩在项目中是一个常见的需求, 通过减小图片文件的大小, 降低存储和传输成本, 提高网页加载速度, 以及减少带宽占用!

个人站点 《仿 Mac 个人网站开发 |项目复盘》目前是使用 TinyPNG 提供的 API 来压缩图片的, 实际上目前系统就我一个人在使用, 所以 TinyPNG 一个月 500次 的免费额度还完全够用!!! 唯一的遗憾就是 TinyPNG 不支持 GIF 图片压缩, 但是考虑到大部分 GIF 录制软件导出的图片都是可以通过设置参数, 达到压缩的效果, 所以也就这样先用着咯....

直到有幸看到 神光大大 的文章 《Nest + Sharp 实现了一个 gif 压缩工具, 帮我省不少钱》, 原来 Node 完全可以使用 Sharp 来完成图片的压缩、包括 GIF, 所以就生出了改造项目的想法, 而本文就是整个改造过程的一个简单记录....

一、现有方案「TinyPNG」介绍

正如引言所述, 目前个人站点压缩图片用的是 TinyPNG 所以在开始前, 容我简单介绍下它!!!

1.1 是什么?

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

同时 TinyPNG 官方客户端代码库支持各种语言, 通过官方提供的 SDK 我们可以轻松调用它们的 API, 实现图片压缩功能!!!

当然天下没有免费的午餐, TinyPNG API 每月是会有 500次 的免费额度, 超过部分则需要收费咯!!! 当然个人使用的话 500次 基本就够用了!!

1.2 怎么用?

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

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

  1. 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 为什么要放弃?

  1. 每个月只有 500次 的免费额度, 虽然目前够用, 但未来就不一定咯(毕竟砸们也是有很大愿景的)
  2. 无法压缩 GIF
  3. 既然 Node 可以自己做, 又何必受限于 TinyPNG

二、新方案「Sharp」

新方案就是使用 Sharp 来实现, 有了它我们就可以在 Node 中实现图片的压缩, 可以将常见格式的大图转换为较小的、网络友好的不同尺寸的 JPEGPNGWebPGIFAVIF 图像, 同时除了压缩图片, 它还提供了很多强大的图片处理能力....

Sharp 使用起来也炒鸡简单, 如下所示, 只需要一行代码即可完成图片的压缩, 其中 quality 是压缩图片常见的一个参数, 它指的是图片压缩后的 质量, 该值可选范围为 1 ~ 100, 数值越高图片质量越好相应的压缩比例也就越低, 该值通常建议为 75-80, 不建议小于 60

js 复制代码
import sharp from 'sharp'
sharp('1.png').png({ quality: 75 }).toFile('2.png')

来测试下效果: 随便找了个图, 测试了下, 直接将从 112KB 压缩到了 31KB, 这压缩率还是很给力的!!!!

特别的是 gifrawtile 是不能调整 quality 参数的, 所以如果想要对 GIF 进行压缩就得另外处理了, 如下代码所示:

  • animated: 设置为 true 可读取动画图像(GIF、WebP、TIFF)的所有帧, 否则默认只会读取第一帧
  • limitInputPixels: 默认情况下 Sharp 能处理的像素数是有限制的(268402689), 所以图片如果太大将会报错, 如果要解除该限制就可以将该参数设置为 false
  • colours: 设置调色板(基础颜色)的最大数量, 它的值介于 2256 之间, 数值越小那么绘制出来的 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

三、项目改造

  1. 先安装 sharp 依赖
sh 复制代码
npm i sharp
  1. 下面我们使用 sharp 来封装一个通用的方法, 由于 gifrawtile 是不能调整 quality 参数, 并且针对 gif 我们还需要特殊处理, 所以就需要对图片进行判断处理, 具体实现如下:
  • 函数的入参和出参都是文件流(stream)数据, 所以这里针对 streambuffer 做了些转换工作
  • switch 根据不同 文件格式 设置不同的 optionsformatOptions
  • await 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);
};

最后补充两个 streambuffer 相互转换的方法

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;
};
  1. 压缩效果: 最后看下 pnggif 的一个压缩效果吧, 压缩率还是很给力, 图片质量还是 ok

  1. 最后卸载 tinify, 清理相关代码
sh 复制代码
npm uninstall tinify

四、参考

相关推荐
世俗ˊ16 分钟前
CSS入门笔记
前端·css·笔记
子非鱼92116 分钟前
【前端】ES6:Set与Map
前端·javascript·es6
6230_21 分钟前
git使用“保姆级”教程1——简介及配置项设置
前端·git·学习·html·web3·学习方法·改行学it
想退休的搬砖人30 分钟前
vue选项式写法项目案例(购物车)
前端·javascript·vue.js
加勒比海涛44 分钟前
HTML 揭秘:HTML 编码快速入门
前端·html
啥子花道1 小时前
Vue3.4 中 v-model 双向数据绑定新玩法详解
前端·javascript·vue.js
麒麟而非淇淋1 小时前
AJAX 入门 day3
前端·javascript·ajax
茶茶只知道学习1 小时前
通过鼠标移动来调整两个盒子的宽度(响应式)
前端·javascript·css
清汤饺子1 小时前
实践指南之网页转PDF
前端·javascript·react.js
蒟蒻的贤1 小时前
Web APIs 第二天
开发语言·前端·javascript