导航菜单

图片格式:选择困难症的终结

一个摄影师能用到多少种格式?

小李给了我一个”惊喜”。

他上传了 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:1100%
PNG无损2:1~5:1100%
GIF无损✅ (1位)2:1~5:1100%
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 元

这个数字让格式转换从一个”优化选项”变成了”必须做”。

搜索