png压缩初探

  之前写插件,碰到了需要压缩图片的场景,当时设计给到的参考对象是 tinyPng,压缩后的图片能达到 tinyPng 的效果即可(图片质量   压缩率)。

  最初设想当然是直接用 tinyPng,但是现在 tinyPng 有免费数量限制,每月免费 500 张,超出计费。这当然是不能忍的,于是就找了 nodejs 的相关压缩工具,压缩 png 广泛使用的就找到以下两个 sharp.js、pngquant。

  三个压缩工具现在来做个对比,主要是压缩率做个对比,首先用一个复杂的图片,如下

原始图片

pngquant 质量范围设置为 0.7-0.8, sharpjs 质量设置为 80 最终压缩后

图片 大小
pngquant 1.1MB
sharpjs 1.4MB
tinyPng 962KB

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import imagemin from "imagemin";
import imageminPngquant from "imagemin-pngquant";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import sharp from "sharp";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const baseName = "bb";
const imageBuffer = fs.readFileSync(path.join(__dirname, baseName + ".png"));
imagemin
.buffer(imageBuffer, { plugins: [imageminPngquant({ quality: [0.7, 0.8] })] })
.then((result) => {
fs.writeFileSync(path.join(__dirname, baseName + "pngquant.png"), result);
});

sharp(imageBuffer)
.png({
quality: 80,
compressionLevel: 9,
adaptiveFiltering: true,
force: true,
})
.toBuffer()
.then((data) => {
fs.writeFileSync(path.join(__dirname, baseName + "sharp.png"), data);
});

  看上去是 tinyPng > pngquant > sharpjs,但是我们把压缩率调整一下 pngquant 设置为 0.3-0.4 sharpjs 设置为 40,结果如下
| 图片 | 大小 |
| ——– | —– |
| pngquant | 861KB |
| sharpjs | 1.4MB |
| tinyPng | 962KB |

  那么现在对问题就来到了 tinyPng 压缩效果是否比得上 pngquant 质量范围 0.3-0.4 了。这个时候就无法通过肉眼看需要了解一下压缩原理,通过数据说话了。通过网上的文章了解了一下 png 格式组成,参考如下[PNG 文件格式解析][https://www.ihubin.com/blog/audio-video-basic-11-png-file-format-detail/],大佬可以直接看[png spec](https://www.w3.org/TR/2003/REC-PNG-20031110/)这里通过一张简单图片来分析一下 pngquant 和 tinyPng 压缩后的图片差异以及优劣,原图如下

原始图片

查看十六进制数据
原始数据

  所需要注意的数据点都已经标注在上面了,再看看pngquant和tinyPng所压缩后的图像,首先是pngquant的。

pngquant压缩

  然后是tinyPng的

tinyPng压缩

  在这里我们可以看到,tinyPng把制定色域的sRGB和gAMA删掉了,而pngquant删掉了,这里会有一个大小的差距。我们可以试试吧这部分数据删除,可以发现,现在pngquant和tinyPng的差距就只有1byte。

  看上去似乎就差了这么一点内容没有删除,但是pngquant的质量范围调小后会发现,压缩率下不去了,应该是图像过于简单,后续无法优化了。依然使用上述复杂图像进行再一次对比。pngquant质量范围为0.3-0.4

图片 大小
pngquant 868KB
tinyPng 963KB

  再次查看数据,pngquant 数据plte的长度是012f 也就是 303,tinypng的plte长度是 0300 也就是 768 ,说明pngquant 0.3-0.4的质量其实是比不过tinypng的,调整一下质量参数,大致调整到两者的plte数量相等,质量区间参数大约是0.7-0.8。这个时候他们的图片大小如下

图片 大小
pngquant 1.06MB
tinyPng 963KB

  此时他们的数据差距就比较大了,这个时候我们来看看数据差距在哪里。这个时候可以用png-chunks-extract 来获取所有的png的chunk大小。 处理结果如下所示。

图片 组成
pngquant { IHDR: 13, PLTE: 768, IDAT: 1117493, IEND: 0 }
tinyPng { IHDR: 13, PLTE: 768, IDAT: 961906, IEND: 0 }

  很明显了,主要差距在于IDAT,此时我们知道,一张png,大小一致的png图片,最终解析出来的数据大小应当也是一致的。问题就只有一个,那就是IDAT的压缩算法不同,tinyPng的压缩算法更好一点。
  那么压缩算法哪家强呢?我们可以找找wiki。首先,png spec规定了png的IDAT压缩算法

Compression method is a single-byte integer that indicates the method used to compress the image data. Only compression method 0 (deflate/inflate compression with a sliding window of at most 32768 bytes) is defined in this International Standard. All conforming PNG images shall be compressed with this scheme.

  而Wiki上找到了更高压缩率的算法实现是 7-zip的实现 链接如下 https://zh.wikipedia.org/wiki/Deflate

更高压缩率的DEFLATE是7-zip所实现的。AdvanceCOMP也使用这种实现,它可以对gzip、PNG、MNG以及ZIP文件进行压缩从而得到比zlib更小的文件大小。在Ken Silverman的KZIP与PNGOUT中使用了一种更加高效同时要求更多用户输入的DEFLATE程序。

  此时我们只需要找到一个7-zip的实现库,给png的数据做压缩,然后分段(分段需要尽量的少一点,减少crc校验码和段长码的数量)。nodejs中可以使用 https://github.com/quentinrossetti/node-7z#commands这个库来进行7-zip压缩。

  尝试了一下得到压缩后的数据为999 KB (1,023,096 字节)压缩率不及tinyPng但是确实是高于pngquant。当然这里只是跑了一遍压缩,没有做分段处理,但是即使分了段,按照tinyPng那样分个两段三段,数量也比pngquant小,而且一般来说数据量越大(压缩的原图越大),最终的结果大小差距也会越大,大致来讲效果还是高于pngquant的。

  至此,终于算是了解了tinyPng的压缩原理,大家如果不想付费使用tinyPng,也可以使用pngquant和7-zip压缩达到一个效果类似tinypng,压缩率还不错的服务。

  后续 ->
  最近看到了一篇文章 Chrome插件:切图压缩工具,里面提到了一个叫做advpng 的压缩工具,用的就是上述思路。突然发现我思路有一点局限。一开始搜索压缩工具就不应该局限在nodejs,应当直接搜索图片压缩工具。如果是网页版压缩工具,可以使用puppeteer,如果是c++的工具,可以看看是不是能用node扩展。顺便的,如果看到有啥C++工具没有nodejs版本/胶水 ,可以试试搞一个,也算是这方面nodejs先驱(水github)