将网站展示图片的格式由 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)
}
相关推荐
文艺理科生5 分钟前
Nginx 路径映射深度解析:从本地开发到生产交付的底层哲学
前端·后端·架构
千寻girling6 分钟前
主管:”人家 Node 框架都用 Nest.js 了 , 你怎么还在用 Express ?“
前端·后端·面试
南极企鹅8 分钟前
springBoot项目有几个端口
java·spring boot·后端
Luke君6079710 分钟前
Spring Flux方法总结
后端
define952713 分钟前
高版本 MySQL 驱动的 DNS 陷阱
后端
忧郁的Mr.Li1 小时前
SpringBoot中实现多数据源配置
java·spring boot·后端
暮色妖娆丶2 小时前
SpringBoot 启动流程源码分析 ~ 它其实不复杂
spring boot·后端·spring
Coder_Boy_2 小时前
Deeplearning4j+ Spring Boot 电商用户复购预测案例中相关概念
java·人工智能·spring boot·后端·spring
Java后端的Ai之路2 小时前
【Spring全家桶】-一文弄懂Spring Cloud Gateway
java·后端·spring cloud·gateway
野犬寒鸦2 小时前
从零起步学习并发编程 || 第七章:ThreadLocal深层解析及常见问题解决方案
java·服务器·开发语言·jvm·后端·学习