图片格式:选择困难症的终结
一个摄影师能用到多少种格式?
小李给了我一个”惊喜”。
他上传了 5 张作品,我打开 OSS 一看:
夜景01.JPG - 8.7 MB (索尼 A7R5 直出)
夜景02.ARW - 52 MB (RAW 格式,索尼原厂)
证件照.PNG - 3.2 MB (透明背景,带 Alpha 通道)
Logo.SVG - 12 KB (矢量图标)
表情包.GIF - 2.1 MB (256 色,帧动画)五种格式,每种都不同。我的缩略图脚本处理到 .ARW 文件时直接崩溃了——PIL 不认识这个格式。
我意识到:在做技术决策之前,我需要先理解图片格式本身。
像素与色彩:图片的本质
在谈格式之前,先搞清楚图片到底是什么。
# 一张 6000×4000 的照片意味着什么?
width = 6000 # 水平方向 6000 个像素
height = 4000 # 垂直方向 4000 个像素
total_pixels = width * height # = 2400 万像素
# 每个像素用什么表示?
# RGB 模型:红(R)、绿(G)、蓝(B) 三个通道,每个通道 8 位(0~255)
bytes_per_pixel = 3 # RGB,每通道 1 字节
# 未压缩的原始大小
raw_size_bytes = total_pixels * bytes_per_pixel
raw_size_mb = raw_size_bytes / (1024 * 1024)
# = 24,000,000 × 3 / 1,048,576 ≈ 68.7 MB
print(f"一张 6000×4000 的未压缩图片:{raw_size_mb:.1f} MB")一张 6000×4000 的照片,未压缩是 68.7 MB。 但实际文件只有 8.7 MB——JPEG 压缩掉了 87% 的数据,你却几乎看不出区别。
这就是压缩的力量。
格式大比拼
我把常见的图片格式整理成了一张对比表:
| 格式 | 压缩类型 | 支持透明 | 支持动画 | 典型压缩率 | 浏览器支持 |
|---|---|---|---|---|---|
| JPEG | 有损 | ❌ | ❌ | 10:1~20:1 | 100% |
| PNG | 无损 | ✅ | ❌ | 2:1~5:1 | 100% |
| GIF | 无损 | ✅ (1位) | ✅ | 2:1~5:1 | 100% |
| WebP | 有损/无损 | ✅ | ✅ | 比 JPEG 小 30% | 97% |
| AVIF | 有损/无损 | ✅ | ✅ | 比 JPEG 小 50% | 92% |
| SVG | 无(矢量) | ✅ | ✅ (CSS/SMIL) | 与复杂度相关 | 99% |
JPEG:老当益壮
JPEG 是我遇到最多的格式。摄影师直出的照片几乎都是 JPEG。
# JPEG 压缩的核心思想:
# "人眼对亮度变化敏感,对色彩细节不敏感"
#
# 压缩步骤:
# 1. 色彩空间转换:RGB → YCbCr(亮度 + 色差)
# 2. 色度下采样:色差通道分辨率减半(人眼不敏感)
# 3. 分块 DCT 变换:8×8 块做频域变换
# 4. 量化:丢弃高频信息(质量控制的核心)
# 5. 熵编码:压缩冗余数据
from PIL import Image
def jpeg_quality_comparison(input_path):
"""对比不同 JPEG 质量的效果"""
img = Image.open(input_path)
results = []
for quality in [100, 90, 80, 70, 60, 50]:
output = f'/tmp/test_q{quality}.jpg'
img.save(output, 'JPEG', quality=quality)
size_kb = os.path.getsize(output) / 1024
results.append({
'quality': quality,
'size_kb': round(size_kb),
})
return results
# 典型结果(6000×4000 风景照片):
# quality=100: 12,400 KB (几乎无损,但文件巨大)
# quality=90: 3,200 KB (肉眼几乎无区别)
# quality=80: 1,800 KB (仔细看有微小瑕疵) ← 推荐起点
# quality=70: 1,100 KB (细节开始丢失)
# quality=60: 750 KB (明显的压缩伪影)
# quality=50: 520 KB (色块明显,不建议)JPEG 的最佳实践:质量设 80~85,大多数场景肉眼和原图无区别。
PNG:透明的代价
PNG 是无损压缩,适合线条图、Logo、需要透明通道的场景。
# PNG 和 JPEG 的选择标准
def choose_format(image_type):
if image_type in ['screenshot', 'logo', 'icon', 'diagram']:
return 'PNG' # 线条、文字、锐利边缘
elif image_type in ['photo', 'wallpaper', 'thumbnail']:
return 'JPEG' # 照片、渐变、丰富色彩
else:
return 'WebP' # 默认选 WebP
# PNG 的致命问题:照片类 PNG 体积巨大
# 同一张 800×600 的照片:
# PNG: 1,200 KB
# JPEG: 80 KB (quality=80)
# WebP: 52 KB (quality=80)
# PNG 是 JPEG 的 15 倍!关键原则:照片绝不存 PNG。 但我的用户并不知道这个——他们截图保存的图片往往是 PNG,直接上传会浪费大量存储空间。
WebP:新一代标准
WebP 是 Google 推出的格式,同等质量下比 JPEG 小 30%~50%,还支持透明和动画。
# WebP 的优势
def webp_vs_jpeg(image_path):
img = Image.open(image_path)
# JPEG quality=80
img.save('/tmp/test.jpg', 'JPEG', quality=80)
jpeg_size = os.path.getsize('/tmp/test.jpg')
# WebP quality=80
img.save('/tmp/test.webp', 'WebP', quality=80)
webp_size = os.path.getsize('/tmp/test.webp')
reduction = (1 - webp_size / jpeg_size) * 100
return {
'jpeg_kb': round(jpeg_size / 1024),
'webp_kb': round(webp_size / 1024),
'reduction': f'{reduction:.1f}%',
}
# 典型结果:
# jpeg: 85 KB → webp: 52 KB → 减少 38.8%
# jpeg: 320 KB → webp: 190 KB → 减少 40.6%
# jpeg: 1200 KB → webp: 680 KB → 减少 43.3%WebP 的浏览器兼容性:截至 2024 年,全球支持率 97%。Safari 从 16.0 开始全面支持。剩下的 3% 可以用 <picture> 标签做降级:
<picture>
<source srcset="photo.webp" type="image/webp">
<source srcset="photo.avif" type="image/avif">
<img src="photo.jpg" alt="自动降级到 JPEG">
</picture>AVIF:终极压缩
AVIF 基于 AV1 视频编码技术,压缩率比 WebP 还高 20%~30%。
# AVIF vs WebP vs JPEG(同等主观质量下)
#
# 一张 1200×800 的照片:
# JPEG: 120 KB
# WebP: 72 KB (比 JPEG 小 40%)
# AVIF: 48 KB (比 JPEG 小 60%,比 WebP 小 33%)但 AVIF 的编码速度很慢——比 WebP 慢 5~10 倍。如果在上传时同步生成,用户会等很久。需要用异步处理。
# 编码速度对比(1200×800 图片)
import time
def benchmark_encoding(image_path):
img = Image.open(image_path)
# JPEG
start = time.time()
img.save('/tmp/bench.jpg', 'JPEG', quality=80)
jpeg_time = time.time() - start
# WebP
start = time.time()
img.save('/tmp/bench.webp', 'WebP', quality=80)
webp_time = time.time() - start
# AVIF (需要 pillow-avif-plugin)
start = time.time()
img.save('/tmp/bench.avif', 'AVIF', quality=80)
avif_time = time.time() - start
return {
'JPEG': f'{jpeg_time*1000:.0f}ms',
'WebP': f'{webp_time*1000:.0f}ms',
'AVIF': f'{avif_time*1000:.0f}ms',
}
# 典型结果:
# JPEG: 45ms
# WebP: 120ms
# AVIF: 850ms ← 慢 19 倍!我的决策
基于以上分析,我为”光影”平台制定了格式策略:
# 图片格式策略
FORMAT_STRATEGY = {
# 用户上传的原图:原样保存,不做转换
'original': {
'action': 'keep_as_is',
'reason': '保留原始数据,支持后期重新处理',
},
# 列表缩略图(200~400px)
'thumbnail': {
'format': 'WebP',
'quality': 75,
'reason': '高兼容性 + 较小体积',
},
# 详情页展示图(800~1200px)
'detail': {
'format': 'WebP',
'quality': 80,
'reason': '平衡质量和体积',
},
# 大图预览(1200~1920px)
'preview': {
'format': 'AVIF', # 优先 AVIF,降级 WebP
'quality': 80,
'reason': '最高压缩率,减少带宽成本',
},
# Logo / 图标
'icon': {
'format': 'SVG',
'reason': '矢量格式,任意缩放不失真',
},
}核心原则:
- 原图永远保留——格式可以转换,但原始数据不可恢复
- 展示用图统一 WebP——兼容性和体积的最佳平衡
- AVIF 作为增强选项——支持的浏览器享受更小的体积
我的思考
思考 1
为什么不把所有图片都转成 AVIF,获得最大压缩率?
三个原因:
1. 编码速度太慢
上传一张 8.7 MB 的 JPEG 原图:
- WebP 编码:~200ms
- AVIF 编码:~2000ms
如果用户上传后要等 2 秒才能看到处理结果,体验很差。
特别是批量上传时,20 张图要等 40 秒。2. 解码速度也慢
浏览器解码一张图片:
- JPEG:~5ms
- WebP:~8ms
- AVIF:~25ms
在低端手机上,AVIF 解码可能导致页面渲染变慢。3. 兼容性还不够
全球浏览器支持率(2024年):
- JPEG:100%
- WebP:97%
- AVIF:92%(Safari 16.4+、Chrome 121+、Firefox 113+)
92% 意味着每 100 个用户有 8 个看不到图片。
虽然可以用 <picture> 降级,但需要同时存储 WebP 和 AVIF 两个版本,增加了存储成本。最佳实践:用 WebP 作为默认格式,AVIF 作为渐进增强。
<picture>
<source srcset="photo.avif" type="image/avif">
<source srcset="photo.webp" type="image/webp">
<img src="photo.jpg" alt="降级链">
</picture>思考 2
一张 PNG 截图 1.2 MB,转成 WebP 后只有 80 KB。如果我的平台每天有 1000 张 PNG 上传,不做转换一个月会浪费多少存储空间?
# 计算存储浪费
daily_uploads = 1000
png_avg_size_kb = 1200
webp_avg_size_kb = 80
days_per_month = 30
# 每月 PNG 存储
png_monthly_gb = (daily_uploads * png_avg_size_kb * days_per_month) / (1024 * 1024)
# = 1000 × 1200 × 30 / 1,048,576 ≈ 34.3 GB
# 每月 WebP 存储
webp_monthly_gb = (daily_uploads * webp_avg_size_kb * days_per_month) / (1024 * 1024)
# = 1000 × 80 × 30 / 1,048,576 ≈ 2.3 GB
# 浪费的存储
wasted_gb = png_monthly_gb - webp_monthly_gb # ≈ 32 GB
# 对应的成本(阿里云 OSS 标准存储)
oss_price_per_gb_month = 0.12 # 元/GB/月
wasted_cost = wasted_gb * oss_price_per_gb_month # ≈ 3.84 元/月
# 看起来不多?但别忘了 CDN 流量成本:
cdn_price_per_gb = 0.24 # 元/GB
# 假设每张图平均被访问 100 次
monthly_views = daily_uploads * days_per_month * 100
# = 3,000,000 次
# CDN 流量
png_cdn_traffic_gb = (monthly_views * png_avg_size_kb) / (1024 * 1024)
# = 3,000,000 × 1200 / 1,048,576 ≈ 3,433 GB
webp_cdn_traffic_gb = (monthly_views * webp_avg_size_kb) / (1024 * 1024)
# = 3,000,000 × 80 / 1,048,576 ≈ 229 GB
# CDN 流量成本差异
png_cdn_cost = png_cdn_traffic_gb * cdn_price_per_gb # ≈ 823.9 元
webp_cdn_cost = webp_cdn_traffic_gb * cdn_price_per_gb # ≈ 54.9 元
cdn_savings = png_cdn_cost - webp_cdn_cost # ≈ 769 元/月
print(f"存储节省:{wasted_gb:.1f} GB/月 ({wasted_cost:.2f} 元)")
print(f"CDN 节省:{cdn_savings:.0f} 元/月")
print(f"总计节省:{wasted_cost + cdn_savings:.0f} 元/月")结论:存储本身不贵,但 CDN 流量才是大头。不做格式转换,每月浪费约 773 元——一年就是 9,276 元。
这个数字让格式转换从一个”优化选项”变成了”必须做”。
