杂记 › 技术笔记 › PNG 压了三遍只减 426 字节:无损压缩的真实边界

PNG 压了三遍只减 426 字节:无损压缩的真实边界

66 组档位实测,55 组省 0 字节

一张 PNG 压完只掉几百字节,就把产物再喂回去压第二遍、第三遍,体积几乎不动;更怪的是有几张重编码后其实比原图大,界面那一行仍然写着「节省」。我拿 11 张 PNG 跑 6 档共 66 组输入到输出:55 组原字节退还、省 0 字节,真正换了编码的只有 11 组,80 档下变的只有 noise256,从 224476 B 到 224243 B,省 233 B 即 0.1%。

11 张一起压完,统计条写的是「11 张图片 | 原始: 622.6 KB → 压缩后: 622.4 KB | 节省: 233 B (0.0%)」:总和 637552 B 变 637319 B,0.0365 经 toFixed(1) 显示成 0.0%。

IDAT 就是全部,元数据只是零头

拆开块看:meta400 总 1053 B,IHDR:25,gAMA:16,sRGB:13,pHYs:21,tEXt:39,tEXt:44,tEXt:30,IDAT:845,IEND:12,每块固定 12 B 头尾。IDAT 数据体 833 B,gAMA、sRGB、pHYs 加 3 个 tEXt 合计 208 B,全剥掉省 163 B 即 15.5%。11 张里只这 1 张带 ancillary 块,其余 10 张剥离收益 0 字节。

没元数据的那些,IDAT 占 99.97% 到 99.99%。能动的只剩 DEFLATE 那段码流,而它还能压多少早由内容定死:未压缩扫描线除以实际字节,solid800 是 1440600 / 2311 = 623.4:1,noise256 只有 262400 / 224476 = 1.17:1。噪声每行都是新数据,后向引用翻不出可复用片段。

换个 deflate 级别,多数样本反而变大

纯无损重编码只改级别与每行过滤器,像素零改动,挑 4 张:

样本原图级别 1 + 过滤 0级别 9 + 自适应级别 9 + Paeth
grad256681181297681681
solid800_loose2789781523122311
noise256224476224562230212224476
big25603991261595575399126405031

只有编码写松了的 solid800_loose 从 2789 拿到 2311,省 478 B 即 17.1%;其余持平或变大,noise256 的 224476 变 230212,涨 5736 B。过滤策略选错更夸张,grad256 在级别 1 加过滤 0 下是 181297 B,原图 681 B 的 266 倍。

这条链路其实先做了一次 JPEG

那 55 组「省 0 字节」不是编码器偷懒,是中间有损了一趟:滑块 80 除以 100 得 0.8,再按 adjustedQuality = quality * quality 平方,真正交给 JPEG 的是 0.6400000000000001,连 0.64 都不等于;100 档则整条链短路、原文件直接交付。画布第二次 toBlob 只传 'image/png',质量参数从没出现。字节我在PNG 压缩这一页逐档取了一遍。

有损那步确实砍下了体积:alpha240 输入 6429 B,中间 JPEG 只剩 2507 B。可 JPEG 规范不含 Alpha,透明区被整幅合成不透明,封回 PNG 膨胀到 47688 B,是输入的 7.4 倍;不带透明的 big2560 也从 359872 B 涨到 1905316 B,4.8 倍。兜底判据 pngBlob.size >= item.file.size 相等也退还,交付体积恒不大于输入,界面只剩「无变化」:膨胀在中间那步,透明保住是回退的副作用,不是压缩留下的。

样本 输入 中间 JPEG 封回 PNG 交付 alpha240 6429 2507 47688 6429 退还 noise256 224476 32005 224243 224243 替换 big2560 399126 359872 1905316 399126 退还

图 1|80 档四段字节,封回 PNG 那步才是膨胀现场。

压三遍的两种含义都收敛

同一个列表里连点 3 次「全部压缩」,11 张的 out1、out2、out3 逐字节相同:列表项存的一直是那个原始 File,没有任何一处改写它。手动把产物喂回去才是第二层,此时只有 noise256 在动,224476、224243、224050、224050,第一遍 −233 B,第二遍 −193 B,第三遍 0,累计降 426 B 即 0.19%;同批 5 张小图 115、127、681、2311、2789 三遍累计降幅 0.0%。

原始 224476 B 第 1 遍 224243 B,−233 第 2 遍 224050 B,−193 第 3 遍 224050 B,不再变

图 2|链式三遍唯一在动的样本,条长按差额放大。

能变小的杠杆是颜色表示

调色板量化那组才有量级收益:solid800 量化到 1 色,2311 B 变 560 B,省 1751 B 即 75.8%,被改像素数 0、最大欧氏误差 0,对这张图它就是真无损;big2560 量化到 8 色,399126 B 变 84226 B,省 314900 B 即 78.9%,代价是 1165703 个像素被改、最大误差 255。量化也会输:alpha240 的 254 种 RGBA 误差 0,索引图却要 22144 B,比原图 6429 B 大;grad256 与 meta400 各有 65536 种颜色,砍到 256 色误差 285 与 247。顺序该是先数颜色与重复模式,再决定减色、缩尺寸还是换格式,取舍见压缩格式怎么选。

「节省」这两个字有两套算术

单条列表只有 3 个分支:省了写减掉的字节(-233 B,或 -17993 B 显示成 -17.6 KB),相等与变大统一写「无变化」,没压的写「待压缩」,没有百分比。唯一的百分比在批量统计,分母是总和,字节那一栏还取了绝对值:saved = totalOrigin - totalResult 为负时也显示成正数,比例却留着负号,于是没有体积回退的分支压完变大时,同一行能读出「节省: 1.2 KB (-3.4%)」;100 档那条直通短路见质量调到最右那次。

Q:统计条写「节省 233 B (0.0%)」,是算错了吗?

没算错,是口径太粗:分母是 11 张的总和 637552 B,233 B 只占 0.0365%,toFixed(1) 后成 0.0%;列表里 10 张「无变化」、1 张 -233 B。

Q:那 PNG 还能不能再压小?

高熵又已紧编码的只会变大,noise256 的 224476 B 换自适应过滤就成 230212 B;写松了的 solid800_loose 才有 478 B 可省。

Q:结果变大了却显示「节省」,怎么发现?

盯负号。PNG 这一路相等都退还,不会把更大的文件交给你,代价是全显示成「无变化」;没有这层兜底才露出「节省: 1.2 KB (-3.4%)」。

不是工具不给力,是 IDAT 占了 99.97% 以上、DEFLATE 还能找到的重复只剩 1.17:1,能动手的只剩颜色表示。三遍下来 426 B、0.19%,停在 224050 B 不动。

📎 相关文章

WebP 转 PNG 后体积涨了几倍:什么时候必须转,怎么把涨幅压住 2026-10-22 截图缩到四分之一,小字糊成一团的三种原因 2026-10-22 同样裁 300×300,头像为什么总偏右下角半格 2026-10-22 按每月 1.7 倍预估 18 个月,脚本和表格为什么给出两个数 2026-10-22 接口 JSON 贴成 PHP 数组,哪些值会悄悄变形 2026-10-22