PNG/JPG 如何变成 WebP?真相不是改后缀!

前面我们已经聊过 PNG、JPG、WebP 的压缩原理。

在开始之前,先认识一下本文反复提到的 libvips。它是一个高性能的图片处理库,也可以理解为图片处理引擎,能完成图片的读取、缩放、裁剪、压缩,以及 PNG、JPG、WebP 等格式之间的转换。

简单回顾一下:

  • PNG:无损压缩,比较适合 UI、文字、透明图片
  • JPG:有损压缩,比较适合照片
  • WebP:既支持有损,也支持无损,还支持透明通道

但这里很容易冒出另一个问题:

PNG、JPG、WebP 的压缩方式明明完全不一样,那 PNG 到底是怎么"变成" WebP 的?

难道只是:

md 复制代码
image.png
↓
改个后缀
↓
image.webp

当然不是。

接下来我们就借助 libvips,简单讲清楚:

图片格式转换,背后到底发生了什么?


一、先说结论:格式转换其实就是"拆开,再重新打包"

先记住这一句话:

图片格式转换的本质,就是先把原来的图片解码成像素,再使用目标格式重新编码。

比如:

md 复制代码
PNG
 ↓
解码
 ↓
RGBA 像素
 ↓
WebP 编码
 ↓
WebP

所以:

md 复制代码
PNG → WebP

并不是直接把 PNG 内部的数据"修改一下",让它变成 WebP。

而是先:

md 复制代码
把 PNG 拆开
↓
得到图片像素

然后再:

md 复制代码
拿这些像素
↓
按照 WebP 的规则重新压缩
↓
生成新的 WebP 文件

你可以把它理解成:

内容还是这张图片,只是换了一种保存方式。


核心流程:

md 复制代码
PNG / JPG / WebP
        ↓
      解码
        ↓
   RGB / RGBA 像素
        ↓
   libvips 处理
        ↓
    目标编码器
        ↓
PNG / JPG / WebP

二、为什么不同格式最后都能互相转换?

因为 PNG、JPG、WebP 的区别,主要是在:

这些像素到底怎么存进文件里。

比如前面我们讲过:

md 复制代码
PNG
→ Filter
→ DEFLATE

JPG 大概是:

md 复制代码
JPG
→ DCT
→ 量化
→ 熵编码

WebP 又有自己的一套预测、变换和压缩方式。

所以在文件里,它们长得完全不一样。

但是一旦解码之后,就不一样了。


三、到了 libvips 这一层,可以先把它们理解成"像素"

假设我们现在有三张图片:

md 复制代码
a.png

b.jpg

c.webp

libvips 读取它们时,会根据图片格式调用对应的解码器。

可以简单理解成:

md 复制代码
PNG ──┐
      │
JPG ──┼→ 解码 → 图像像素
      │
WebP ─┘

解码之后,中间操作的已经不是:

md 复制代码
PNG 的 DEFLATE 数据

也不是:

md 复制代码
JPG 的 DCT 系数

而是图像本身。

比如:

md 复制代码
宽:1920
高:1080
通道:RGB

或者:

md 复制代码
宽:1920
高:1080
通道:RGBA

里面可以理解成是一堆像素:

md 复制代码
(255, 120, 80)
(252, 119, 81)
(250, 118, 79)
...

所以对于我们理解格式转换来说,可以先建立这样一个心智模型:

md 复制代码
文件格式
   ↓
 解码器
   ↓
图像像素

然后输出时:

md 复制代码
图像像素
   ↓
目标格式编码器
   ↓
新的图片文件

这就是为什么 PNG、JPG、WebP 可以互相转换。


四、比如 PNG 转 WebP,到底发生了什么?

我们来看最常见的情况。

假设用户上传:

md 复制代码
logo.png

现在我们希望服务器输出:

md 复制代码
logo.webp

整个流程大概就是:

md 复制代码
PNG
 ↓
PNG 解码器
 ↓
RGBA 像素
 ↓
WebP 编码器
 ↓
WebP

但这里又分成两种情况。


五、PNG → WebP Lossless:只是换一种无损保存方式

第一种:

md 复制代码
PNG
→
WebP Lossless

也就是:

PNG 转 WebP 无损。

流程可以理解成:

md 复制代码
PNG
 ↓
无损解码
 ↓
RGBA 像素
 ↓
WebP Lossless 编码
 ↓
WebP

如果中间没有:

md 复制代码
resize
调色
裁剪

这些会改变像素的操作,那么从像素内容来看:

md 复制代码
PNG 解码出来的像素
=
WebP Lossless 再解码出来的像素

也就是说:

这次格式转换本身没有把图片细节丢掉。

只是原来:

md 复制代码
用 PNG 的方式保存

现在变成:

md 复制代码
用 WebP Lossless 的方式保存

所以可以把它理解成:

像素没变,只是换了一种打包方式。


六、PNG → WebP Lossy:这时候才开始真正丢信息

如果我们选择:

md 复制代码
PNG
→
WebP Lossy

情况就不一样了。

流程还是:

md 复制代码
PNG
 ↓
解码
 ↓
RGBA 像素

到这里都没有问题。

但接下来用了:

md 复制代码
WebP Lossy

也就是 WebP 的有损编码。

于是:

md 复制代码
RGBA 像素
 ↓
WebP 有损编码
 ↓
丢掉部分视觉信息
 ↓
更小的 WebP

所以:

PNG 转 WebP 会不会掉画质,并不是"转 WebP"这件事情本身决定的。

真正决定它的是:

你最终使用的是 WebP Lossless,还是 WebP Lossy。


左边:

md 复制代码
PNG
↓
像素
↓
WebP Lossless
↓
像素保持一致

右边:

md 复制代码
PNG
↓
像素
↓
WebP Lossy
↓
丢掉部分细节

这一张图基本就能把 PNG → WebP 讲明白。


七、那 JPG 转 WebP 呢?

逻辑其实完全一样。

md 复制代码
JPG
 ↓
JPEG 解码器
 ↓
RGB 像素
 ↓
WebP 编码器
 ↓
WebP

但是 JPG 有一个很重要的特殊点:

JPG 本身就是有损格式。

比如一张原始照片:

md 复制代码
原始图片
 ↓
JPEG 有损压缩
 ↓
JPG

生成 JPG 的时候,其实就已经丢掉了一部分信息。

所以我们再用 libvips 读取 JPG:

md 复制代码
JPG
 ↓
解码
 ↓
RGB 像素

这里拿到的是:

当前 JPG 所代表的像素。

而不是 JPG 压缩之前那份真正的原始图片。

这个区别非常重要。


八、所以 JPG → 有损 WebP,可能属于"二次有损"

假设:

md 复制代码
原图
 ↓
第一次有损
 ↓
JPG

然后我们又:

md 复制代码
JPG
 ↓
解码
 ↓
WebP Lossy
 ↓
第二次有损
 ↓
WebP

整个过程就变成:

md 复制代码
原图

 ↓ 第一次有损

JPG

 ↓ 解码

当前 JPG 的像素

 ↓ 第二次有损

WebP

所以经常会说:

JPG 再转有损 WebP,可能会产生二次有损。

尤其是你反复:

md 复制代码
JPG
→ WebP
→ JPG
→ WebP

每次都使用有损编码的话,图片质量就可能一点一点下降。

这个其实和:

md 复制代码
视频反复导出

很像。

你每导出一次有损格式,都有可能再损失一点信息。


【插图 3:JPG 转 WebP 二次有损】

重点突出:

md 复制代码
原图
↓
JPEG 第一次有损
↓
JPG
↓
解码
↓
当前 JPG 的像素
↓
WebP 第二次有损
↓
WebP

九、那 JPG 转 WebP Lossless 呢?

这里有个很容易绕进去的地方。

如果:

md 复制代码
JPG
→
WebP Lossless

是不是就变无损图片了?

答案是:

不能这么理解。

因为 JPG 在生成的时候:

md 复制代码
原始图片
↓
JPEG

已经丢过信息了。

这部分信息已经回不来了。

所以:

md 复制代码
JPG
 ↓
解码
 ↓
当前 JPG 的 RGB 像素
 ↓
WebP Lossless

这里所谓的 Lossless,是:

从"当前 JPG 解码后的像素"开始,不再继续丢信息。

并不是:

把 JPG 恢复成它压缩之前的原图。

所以一定要分清楚:

md 复制代码
无损编码
≠
恢复已经丢失的信息

这个概念非常重要。


十、所以 libvips 到底做了什么?

讲到这里,其实整个模型已经非常简单了。

可以把 libvips 的图片处理链路抽象成三个单词:

md 复制代码
decode
   ↓
transform
   ↓
encode

也就是:

md 复制代码
解码
 ↓
处理
 ↓
编码

比如:

md 复制代码
PNG
 ↓
decode
 ↓
RGBA 像素
 ↓
resize
 ↓
crop
 ↓
rotate
 ↓
WebP encode
 ↓
WebP

中间的:

md 复制代码
resize
crop
rotate
颜色转换

甚至都不是必须的。

如果单纯做格式转换:

md 复制代码
PNG
 ↓
decode
 ↓
encode WebP
 ↓
WebP

就够了。


十一、所以"格式转换"和"图片压缩",其实没有想象中那么割裂

这一点我觉得很有意思。

以前我们可能会觉得:

md 复制代码
图片压缩

和:

md 复制代码
格式转换

是两件完全不同的事情。

但站在 libvips 这条处理链路来看,它们其实非常接近。

比如:

md 复制代码
PNG → PNG

可以理解成:

md 复制代码
PNG
↓
decode
↓
PNG encoder
↓
PNG

属于:

PNG 重新编码 / 压缩。

而:

md 复制代码
PNG → WebP

则是:

md 复制代码
PNG
↓
decode
↓
WebP encoder
↓
WebP

属于:

格式转换 + WebP 压缩。

再比如:

md 复制代码
JPG → JPG

是:

md 复制代码
JPG
↓
decode
↓
JPEG encoder
↓
JPG

而:

md 复制代码
JPG → WebP

只是最后:

md 复制代码
JPEG encoder

换成:

md 复制代码
WebP encoder

而已。

所以从代码设计的角度,其实可以把两件事统一成:

md 复制代码
输入图片
   ↓
decode
   ↓
transform
   ↓
选择目标 encoder
   ↓
输出图片

十二、那用 libvips 做格式转换,代码是不是也不用改很多?

如果你的项目本身已经在使用 libvips 做图片压缩,那通常确实不用重新写一整套逻辑。

原来可能是:

md 复制代码
读取图片
 ↓
处理
 ↓
按照原格式编码
 ↓
输出

增加格式转换之后:

md 复制代码
读取图片
 ↓
处理
 ↓
根据用户指定的目标格式选择 encoder
 ↓
输出

比如用户传:

md 复制代码
format = webp

那最后就走:

md 复制代码
WebP encoder

用户传:

md 复制代码
format = jpeg

就走:

md 复制代码
JPEG encoder

用户传:

md 复制代码
format = png

就走:

md 复制代码
PNG encoder

所以从整体架构来看:

格式转换真正需要变化的地方,主要集中在输出编码阶段。


重点展示:

md 复制代码
读取图片
  ↓
libvips 解码
  ↓
resize / crop / rotate
  ↓
选择目标格式 encoder
  ↓
PNG / JPG / WebP

十三、不过还有几个小坑要注意

虽然核心逻辑很简单,但实际做服务时还是有几个细节。

1. 不同格式的压缩参数不一样

不能简单搞一个:

md 复制代码
quality = 80

然后觉得 JPG、PNG、WebP 全部都一样。

比如:

md 复制代码
JPEG
→ quality

WebP
→ quality / lossless / effort

PNG
→ compression level 等

不同编码器有自己的参数。

所以代码里最好还是:

md 复制代码
目标格式
↓
选择对应 encoder
↓
使用对应参数

2. PNG 转 JPG 要注意透明通道

PNG 支持:

md 复制代码
RGBA

也就是有 Alpha 透明通道。

但 JPG 不支持透明。

所以:

md 复制代码
透明 PNG
↓
JPG

不能直接理解成透明还会保留。

通常要先:

md 复制代码
透明区域
 ↓
铺一个背景色
 ↓
RGB
 ↓
JPEG

比如铺白底。

这一点实际做图片服务的时候一定要考虑。


3. 文件后缀和 Content-Type 也要跟着变

比如:

md 复制代码
PNG
↓
WebP

输出文件不能还叫:

md 复制代码
image.png

而应该是:

md 复制代码
image.webp

HTTP 返回时:

md 复制代码
Content-Type

也应该对应:

md 复制代码
image/webp

这些属于工程层面的细节。


十四、libvips 真的是"先把整张图片全部放进内存"吗?

前面为了方便理解,我们一直画:

md 复制代码
图片文件
 ↓
整张像素图
 ↓
处理
 ↓
输出

作为理解模型,这样完全没问题。

但 libvips 实际实现还要更聪明一点。

它采用的是比较偏:

按需计算、流水线处理

的方式。

所以很多时候,并不是:

md 复制代码
一次性把整张超大图片
全部展开到内存

然后再处理。

而更像:

md 复制代码
需要哪一部分
↓
读取 / 解码哪一部分
↓
处理
↓
继续下一部分

这也是 libvips 很适合用在服务端图片处理场景里的一个原因。

不过对于刚开始理解格式转换来说,这部分知道有这么回事就够了。

不用继续往底层钻。


最后总结

这篇其实只需要记住一个核心模型:

md 复制代码
PNG / JPG / WebP
        ↓
      解码
        ↓
   RGB / RGBA 像素
        ↓
   libvips 处理
        ↓
   目标格式编码器
        ↓
PNG / JPG / WebP

所以:

格式转换的本质,就是先解码成图像,再按照目标格式重新编码。

比如:

md 复制代码
PNG → WebP

不是"修改 PNG"。

而是:

md 复制代码
PNG
↓
像素
↓
WebP

如果:

md 复制代码
PNG → WebP Lossless

那么可以理解成:

像素不变,只是换了一种无损的保存方式。

如果:

md 复制代码
PNG → WebP Lossy

则是:

拿 PNG 的像素,再使用 WebP 的有损规则重新压缩。

而:

md 复制代码
JPG → WebP Lossy

因为 JPG 本身已经有损过一次,所以:

还可能产生第二次有损。

理解到这里,其实图片格式转换已经没有那么神秘了。

最后再用一句话概括:

图片还是那张图片,格式决定的只是:这些像素最终怎么被保存下来。

而 libvips 做的,就是把:

读取 → 解码 → 处理 → 重新编码

这一整条链路串起来。

相关推荐
Zane19941 小时前
String::compareTo凭什么能当参数传?方法引用的四种形式讲透
java·后端
杉氧1 小时前
RN 性能调优指南:重渲染(Re-renders)控制与长列表(FlatList)优化
android·前端·react native
一_个前端1 小时前
[JS] 一站式搞定 PDF、图片、Dom弹窗、表格的浏览器打印功能
前端
JavaGuide1 小时前
阿里 Qoder 又开源了一个专门给 Claude Code、Codex 做“体检”的项目
前端·后端
不一样的少年_1 小时前
JPEG 压缩到底在干嘛?小白也能看懂的 8 步拆解
前端·后端·图片资源
the局外人2 小时前
学习 FastAPI 的 Day 4:完成用户系统与接口联调(完结)
后端·python·fastapi
三小河2 小时前
RAG、LLM Wiki、本体(Ontology)到底是个啥?
前端·人工智能·后端
敲代码的嘎仔2 小时前
28届后端开发-百人小厂面试题
java·后端·面试·程序员·秋招·实习·转正
Bs_MoneyMagnet2 小时前
基于springboot+vue的爱心众筹系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring·管理系统