将网站展示图片的格式由 JPG 切换到了 WebP

欢迎访问我的个人小站 莹的网络日志 ,不定时更新文章和技术博客~

博客小站需要一个提供展示图片的功能,我已经做了第一版,通过后台上传图片,服务器会将图片转为 质量75的 JPG 格式,但是这个状态下的图片非常模糊。就寻思着有没有一种可能,在图片质量和大小之间找一个平衡点,顺便写点东西出来方便之后查阅。

图片格式

首先想到的便是 JPG 格式及其压缩算法本身的缺陷,自然而然想要了解更多的图片格式和对应的特性,于是直接求助ChatGPT,主要关心的还是常用的图片格式,包括 JPG、PNG、WebP、GIF等等,像 SVG 和 ICO 这种图片格式的用途太特殊了就不在今天的讨论范围了。

本来想把 ChatGPT 的回答完整贴上来的,不过在搜寻资料的时候发现了 MDN 的文档,觉得非常之全面,完全不用再看 ChatGPT 给我的表格了。图像文件类型与格式指南,那么这里我就不赘述了。

PIL

为了搞清楚不同格式图片的具体差别,我打算直接写代码转格式,这样比起使用转换工具的暗箱操作能了解到更多内容。图片处理库比较丰富的语言应该就是 C++ 和 Python 了,考虑到我想要快速验证,所以这里选择了 Python。只是上次使用 Python 已经是很久之前了,那时我还是个小白,记得当时用的 Python 最新版本还是 3.10,哎时过境迁。

bash 复制代码
pip3 install Pillow

凭着记忆中的流程,先尝试 pip install,看上去安装成功了但是代码依旧是报错。求助AI,让我用虚拟环境运行,应该是为了版本隔离,有点不太方便。

bash 复制代码
/opt/homebrew/bin/python3.12 -m venv venv
bash 复制代码
source venv/bin/activate && pip install Pillow
bash 复制代码
source venv/bin/activate && python img/img.py

经过一番操作终于可以运行 py 代码了。虽然是 Python,但是我面向过程编程,直接单文件单函数一把嗦。

python 复制代码
import os
from PIL import Image

def convert_image(input_path):
    if not os.path.exists(input_path):
        return 

    # 打开原始图片
    img = Image.open(input_path)

    # PNG
    img.save("./img/png_output.png", format="PNG")

    # JPG 100
    img.convert("RGB").save("./img/jpg_100_output.jpg", format="JPEG", quality=100)

if __name__ == "__main__":
    convert_image("./img/test.jpg")

PIL 的功能十分强大,支持很多种格式,编码时通过 format 参数控制输出图片的格式,此外不同格式的图片还有其他的一些参数可以使用,比如 JPG 格式最常用的图片质量参数 quality,详情可以参考文档:Pillow Image file formats

PNG

平时最常见的就是 PNG 和 JPG 这两种格式。相比于 JPG,大部分场景下都是我优先考虑的是 PNG,首先无损,不会损失图片信息,而我也不需要太担忧存储空间和流量。而且除了 rgb 之外还有 alpha 通道,可以储存透明信息。我常用的截图软件比如飞书、企业微信和 Snipaste 都是默认使用 PNG 格式的图片保存。

python 复制代码
# PNG
img.save("./img/output.png", format="PNG")

JPG

在做我的博客网站之前,我对于 JPG 非常不屑一顾,但是服务器需要考虑流量成本,因此可以减小图片质量和大小的 JPG 格式,一夜从昨日黄花变成了掌上明珠。话虽如此,考虑到博客的类型基本都是技术博客,使用截图比较多。虽然我之前设置的质量参数是 75,看似有 3/4,但是在视觉上真的惨不忍睹,而即便质量设置到 90,在文字和 UI 等地方也会明显模糊。假如说我的博客多为生活照片分享,那 JPG 就可以考虑了,虽然压缩会导致观感略微下降,但是换来的是图片大小大幅减少。

python 复制代码
# JPG 100
img.convert("RGB").save("./img/output.jpg", format="JPEG", quality=90)

WebP

虽然 WebP 也压缩,但是压缩完之后依旧是那么清晰锐利,我第一时间就想选择 WebP 作为博客图片的主要格式,当时的主要顾虑是 Golang 图片处理相关生态并不是很完善,标准库或是 Google 都没有提供 WebP 格式图片的编码,Google 你就是这么对待你的亲儿子吗!

python 复制代码
# WebP 100
img.save("./img/output.webp", format="WEBP", quality=90)

HEIC

在我的认知中,至少很长一段时间,都会觉得苹果的 Live Photo 真的牛,拍出的照片竟然可以动起来,就像 GIF 一样,而且彼时动态照片修图的 APP 也不多,蛮长时间之后才开始涌现。现在倒是认识到所谓的动态图片其实也只是一个文件格式罢了,技术也都是通用滴,本质就是一个很短的视频只是包装成了图片而已。除了苹果 Family,其它的一些平台对这个格式的适配还是差点,我用 GoogleChrome 访问硬盘里的 .heic 文件直接变成了下载,而 safari 虽然是个没什么人用的老旧浏览器,却是可以轻松显示。

甚至强如 PIL 也没有支持这个格式的读写,只能用插件实现,而且出于性能层面的考量,图片转换算法也是用c实现的,比如这个仓库:bigcat88/pillow_heif

python 复制代码
# 注册HEIF格式支持
pillow_heif.register_heif_opener()

# HEIC
img.save("./img/output.heic", format="HEIF")

感兴趣的话可以了解一下 HEIC 和 AVIC,一个是基于 H.265 编码一个是基于 AV1 编码,是非常高效的图片格式,本想拥抱新技术,只是由于不确定浏览器的适配情况我就还没考虑。

图片大小对比

除了上面提到的,我还增加了一些我都不怎么接触的图片格式做对比。我使用 cursor 编辑器的过程中,会看到其中一些格式的支持和适配很差,比如 bmp 和 heic 格式的文件没法识别成图片文件,文件 icon 依旧是普通文件类型,而点击查看 tiff 和 heic 图片则是直接无法显示。

!IMPORTANT\] 不同类型和内容的图片,压缩后的大小差别也会很大,可能有的算法针对某个场景效果很好,这里我只是随意挑选了一张照片作为测试照片,并没有大量处理和对比,所以可能稍有偏差。

bash 复制代码
ls -l ./img
text 复制代码
13952694 Sep 12 02:20 test.jpg

10542358 Sep 12 02:33 avif_100_output.avif
 1869333 Sep 12 02:33 avif_75_output.avif
72000054 Sep 12 02:32 bmp_output.bmp
11719551 Sep 12 02:32 gif_output.gif
10837445 Sep 12 02:33 heic_100_output.heic
 6899014 Sep 12 02:33 heic_75_output.heic
11856746 Sep 12 02:32 jpg_100_output.jpg
 2414068 Sep 12 02:32 jpg_75_output.jpg
25421608 Sep 12 02:32 png_output.png
72008144 Sep 12 02:32 tiff_output.tiff
 6181798 Sep 12 02:32 webp_100_output.webp
 1436218 Sep 12 02:32 webp_75_output.webp
图片不同格式和压缩比例的大小柱状图
  • bmp 和 tiff 存储的信息最多,文件也是大的离谱
  • png 和 gif 虽然都是无损的格式,不过 gif 本身对色彩数量有压缩所以文件小很多
  • 同等质量参数下 webp 文件大小比 jpg 小得多,图片质量高压缩效率也高,除了浏览器适配稍差之外基本完虐
  • heic 和 avif 的压缩效率也都非常不错,而且本身基于视频压缩算法,怪不得我的苹果手机中有那么多照片却不怎么缺存储空间

Golang Webp

权衡了文件大小和图片质量之后,我最终还是选择了 webp 作为第二版的图片保存格式,质量则是选择了 90。

搜索了一圈并没有发现非常权威和通用的 Golang 库,只有一个 600 star 左右的仓库比较优秀,底层用的是 libwebp,地址是:chai2010/webp,作者也有持续更新,具体使用方法可以参考 Github 上的 readme。

bash 复制代码
go get github.com/chai2010/webp
go 复制代码
// Decode webp
m, err := webp.Decode(bytes.NewReader(data))
if err != nil {
    log.Println(err)
}

// Encode lossless webp
if err = webp.Encode(&buf, m, &webp.Options{Lossless: true}); err != nil {
    log.Println(err)
}
相关推荐
一直_在路上2 小时前
高级 Go 并发架构实践:赋能临床医疗数据平台的高效与稳定
后端
召摇2 小时前
Java Web开发从零开始:初学者完整学习指南
java·后端·面试
林太白2 小时前
NestJS-身份验证JWT的使用以及登录注册
前端·后端·前端框架
Cache技术分享2 小时前
200. Java 异常 - Throwing Exceptions: 指定方法抛出的异常
前端·后端
JaguarJack2 小时前
PHP 快速集成 ChatGPT 用 AI 让你的应用更聪明
后端·php
程序猿不脱发23 小时前
Redis 内存淘汰策略 LRU 和传统 LRU 差异
java·后端·spring
CryptoPP3 小时前
Go语言 对接全球股票K线API实战 - 以美股市场为例
开发语言·后端·golang
心月狐的流火号3 小时前
Go方法接收者语义与嵌入类型方法提升
后端·go
似水流年流不尽思念3 小时前
垃圾收集算法了解吗?
后端