最近在处理博客图片的时候,发现图片体积太大,所以想使用 AVIF 格式来优化图片体积。为啥用 AVIF 呢,因为发现压缩率挺夸张的,而且浏览器支持情况也挺好的,然后在本地写博客因为使用的是 astro 框架,所以直接使用 astro 的图片优化功能就可以。 使用 astro 的图片优化功能。然后想写个脚本,批量转换图片为 AVIF 格式,毕竟大部分场景下,图片都是 JPEG 格式,所以需要批量转换,踩了不少坑,记录一下。
主要是介绍一下 pillow 库的安装和使用,以及一些踩坑的点(AVIF 的支持有不少问题)。
Pillow 库简介
什么是 Pillow?
Pillow 是 Python 中最流行的图像处理库,是 PIL (Python Imaging Library) 的现代化分支。它提供了广泛的图像文件格式支持和强大的图像处理功能。
核心特性
- 多格式支持: JPEG, PNG, GIF, BMP, TIFF, WebP, AVIF 等
- 图像操作: 缩放、裁剪、旋转、滤镜、颜色转换
- 绘图功能: 文本渲染、几何图形绘制
- 格式转换: 无缝转换不同图像格式
- 高性能: 底层 C 实现,处理速度快
安装方式
# 基础安装
pip install Pillow
# UV 环境安装
uv pip install Pillow
# 从源码编译(支持更多格式)
pip install --no-binary pillow Pillow
AVIF 格式介绍
什么是 AVIF?
AVIF (AV1 Image File Format) 是基于 AV1 视频编解码器的现代图像格式,由 Alliance for Open Media 开发。
技术特点
- 基于 AV1: 使用先进的 AV1 视频编解码技术
- 开放标准: 完全开源,无专利费用
- 现代设计: 专为现代 Web 应用优化
浏览器支持情况
浏览器 | 支持版本 | 发布时间 |
---|---|---|
Chrome | 85+ | 2020年8月 |
Firefox | 93+ | 2021年10月 |
Safari | 16+ | 2022年9月 |
Edge | 85+ | 2020年8月 |
AVIF 格式的优势
1. 卓越的压缩效率
AVIF 相比其他格式有显著的文件大小优势:
原始文件: 13,502 bytes (PNG)
├── AVIF: 2,408 bytes (压缩率 82.2%) 🏆
├── WebP: 4,078 bytes (压缩率 69.8%)
└── JPEG: 8,685 bytes (压缩率 35.7%)
2. 高质量图像
- 10-bit 色深支持: 比传统 8-bit 更丰富的色彩
- HDR 支持: 高动态范围图像
- 广色域: 支持 P3、Rec. 2020 等色彩空间
3. 先进功能
- 透明通道: 支持 Alpha 透明度
- 动画支持: 可创建动态图像
- 渐进式加载: 支持渐进式显示
- 无损压缩: 提供无损压缩选项
4. Web 优化
- 更快加载: 文件更小,传输更快
- 带宽节省: 减少 50% 以上的流量消耗
- 用户体验: 更快的页面加载速度
环境配置与安装
方案: 从源码编译 Pillow (完整支持) - Mac
# macOS 环境准备
brew install libavif aom dav1d rav1e
# 设置编译环境变量
export LDFLAGS="-L/opt/homebrew/lib"
export CPPFLAGS="-I/opt/homebrew/include"
export PKG_CONFIG_PATH="/opt/homebrew/lib/pkgconfig"
# 从源码编译安装
pip uninstall pillow
pip install --no-binary pillow Pillow
验证安装
from PIL import Image, features
# 检查 AVIF 支持
print(f"AVIF 支持: {'✅' if features.check('avif') else '❌'}")
print(f"Pillow 版本: {Image.__version__}")
# 测试 AVIF 功能
try:
# 创建测试图像
test_img = Image.new('RGB', (100, 100), color='red')
test_img.save('test.avif', format='AVIF')
# 读取测试
avif_img = Image.open('test.avif')
print("✅ AVIF 功能正常")
except Exception as e:
print(f"❌ AVIF 功能异常: {e}")
使用 Pillow 处理 AVIF 图片
基础操作
1. 格式转换
from PIL import Image
import pillow_avif # 如果使用插件方案
def convert_to_avif(input_path, output_path, quality=80):
"""将图片转换为 AVIF 格式"""
try:
with Image.open(input_path) as img:
# 处理透明度(AVIF 支持 Alpha 通道)
if img.mode in ('RGBA', 'LA'):
# 保持透明度
img.save(output_path, format='AVIF', quality=quality)
elif img.mode == 'P':
# 调色板模式转换
img = img.convert('RGB')
img.save(output_path, format='AVIF', quality=quality)
else:
# 直接保存
img.save(output_path, format='AVIF', quality=quality)
print(f"✅ 转换成功: {input_path} → {output_path}")
return True
except Exception as e:
print(f"❌ 转换失败: {e}")
return False
# 使用示例
convert_to_avif('photo.jpg', 'photo.avif', quality=85)
2. 批量转换
import os
from pathlib import Path
def batch_convert_to_avif(input_dir, output_dir, quality=80):
"""批量转换图片为 AVIF 格式"""
input_dir = Path(input_dir)
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
# 支持的输入格式
supported_formats = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.webp'}
converted_count = 0
total_count = 0
for file_path in input_dir.iterdir():
if file_path.suffix.lower() in supported_formats:
total_count += 1
output_path = output_dir / f"{file_path.stem}.avif"
if convert_to_avif(file_path, output_path, quality):
converted_count += 1
print(f"📊 批量转换完成: {converted_count}/{total_count} 成功")
# 使用示例
batch_convert_to_avif('./images', './avif_images', quality=85)
3. 高级 AVIF 选项
def advanced_avif_save(image, output_path, **options):
"""高级 AVIF 保存选项"""
# 默认参数
default_options = {
'format': 'AVIF',
'quality': 80, # 质量 (1-100)
'speed': 6, # 编码速度 (0-10, 越小越慢但质量更好)
'codec': 'auto', # 编解码器选择
'range': 'full', # 色彩范围
'lossless': False, # 是否无损压缩
}
# 合并用户选项
save_options = {**default_options, **options}
# 保存图像
image.save(output_path, **save_options)
# 使用示例
with Image.open('input.jpg') as img:
# 高质量无损压缩
advanced_avif_save(img, 'output_lossless.avif',
lossless=True, speed=2)
# 快速有损压缩
advanced_avif_save(img, 'output_fast.avif',
quality=75, speed=8)
图像处理组合
def process_and_convert_avif(input_path, output_path):
"""处理图像并转换为 AVIF"""
with Image.open(input_path) as img:
# 1. 自动旋转(基于 EXIF)
if hasattr(img, '_getexif') and img._getexif():
from PIL.ExifTags import ORIENTATION
exif = img._getexif()
if ORIENTATION in exif:
img = img.transpose(method=exif[ORIENTATION])
# 2. 尺寸优化(如果图片过大)
max_size = 1920
if max(img.size) > max_size:
img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
# 3. 颜色优化
if img.mode == 'RGBA':
# 检查是否真的需要透明通道
alpha = img.split()[-1]
if alpha.getbbox() is None: # 完全不透明
img = img.convert('RGB')
# 4. 保存为 AVIF
img.save(output_path, format='AVIF', quality=85, optimize=True)
# 使用示例
process_and_convert_avif('large_photo.jpg', 'optimized.avif')
实际案例与代码示例
完整的图像转换器类
import os
import time
from pathlib import Path
from typing import Union, Dict, List
from PIL import Image, features
import pillow_avif # 启用 AVIF 支持
class ImageConverter:
"""通用图片格式转换器,支持 AVIF"""
def __init__(self):
self.supported_formats = self._get_supported_formats()
self.format_extensions = {
"JPEG": [".jpg", ".jpeg"],
"PNG": [".png"],
"WebP": [".webp"],
"AVIF": [".avif"],
"BMP": [".bmp"],
"TIFF": [".tiff", ".tif"],
"GIF": [".gif"],
}
def _get_supported_formats(self) -> Dict[str, bool]:
"""检查支持的格式"""
return {
"JPEG": True,
"PNG": True,
"WebP": features.check("webp"),
"AVIF": features.check("avif"),
"BMP": True,
"TIFF": True,
"GIF": True,
}
def print_support_info(self):
"""显示格式支持信息"""
print("=" * 50)
print("Pillow 图片格式支持情况:")
print("=" * 50)
for format_name, supported in self.supported_formats.items():
status = "✅ 支持" if supported else "❌ 不支持"
print(f"{format_name:8} {status}")
print("=" * 50)
def convert_single(self, input_path: Union[str, Path],
output_path: Union[str, Path],
target_format: str = "AVIF",
quality: int = 85,
**options) -> bool:
"""转换单个文件"""
if not self.supported_formats.get(target_format, False):
print(f"❌ 格式 {target_format} 不支持")
return False
try:
with Image.open(input_path) as img:
print(f"📸 原始图片: {img.format} {img.size} {img.mode}")
# 处理颜色模式
if img.mode == "RGBA" and target_format == "JPEG":
# JPEG 不支持透明度,转换为白底
background = Image.new("RGB", img.size, (255, 255, 255))
background.paste(img, mask=img.split()[-1])
img = background
print("🔄 已将 RGBA 转换为 RGB")
# 保存参数
save_kwargs = {"format": target_format, **options}
if target_format == "AVIF":
save_kwargs.update({
"quality": quality,
"speed": 6,
"optimize": True,
})
elif target_format == "WebP":
save_kwargs.update({
"quality": quality,
"optimize": True,
})
elif target_format == "JPEG":
save_kwargs.update({
"quality": quality,
"optimize": True,
})
img.save(output_path, **save_kwargs)
# 计算压缩效果
original_size = os.path.getsize(input_path)
converted_size = os.path.getsize(output_path)
compression_ratio = ((original_size - converted_size) / original_size) * 100
print(f"✅ 转换成功: {input_path} ({original_size:,} bytes) → "
f"{output_path} ({converted_size:,} bytes) | "
f"压缩率: {compression_ratio:.1f}%")
return True
except Exception as e:
print(f"❌ 转换失败 {input_path}: {e}")
return False
# 使用示例
converter = ImageConverter()
converter.print_support_info()
# 转换为 AVIF
converter.convert_single('photo.jpg', 'photo.avif', quality=85)
# 转换为无损 AVIF
converter.convert_single('graphic.png', 'graphic.avif',
quality=100, lossless=True)
Web 应用集成示例
from flask import Flask, request, send_file
from werkzeug.utils import secure_filename
import tempfile
import os
app = Flask(__name__)
@app.route('/convert-to-avif', methods=['POST'])
def convert_to_avif_api():
"""Web API: 转换图片为 AVIF"""
if 'file' not in request.files:
return {'error': '没有文件上传'}, 400
file = request.files['file']
if file.filename == '':
return {'error': '文件名为空'}, 400
# 获取参数
quality = int(request.form.get('quality', 85))
speed = int(request.form.get('speed', 6))
try:
# 创建临时文件
with tempfile.NamedTemporaryFile(delete=False, suffix='.avif') as tmp_output:
output_path = tmp_output.name
# 保存上传的文件
with tempfile.NamedTemporaryFile(delete=False) as tmp_input:
file.save(tmp_input.name)
# 转换为 AVIF
with Image.open(tmp_input.name) as img:
img.save(output_path, format='AVIF',
quality=quality, speed=speed, optimize=True)
# 清理输入文件
os.unlink(tmp_input.name)
# 返回 AVIF 文件
return send_file(output_path, as_attachment=True,
download_name=f"{file.filename.rsplit('.', 1)[0]}.avif")
except Exception as e:
return {'error': f'转换失败: {str(e)}'}, 500
if __name__ == '__main__':
app.run(debug=True)
性能对比与测试
压缩效果对比
基于实际测试数据(原始 PNG 文件 13,502 bytes):
格式 | 文件大小 | 压缩率 | 质量评分 | 加载速度 |
---|---|---|---|---|
PNG | 13,502 bytes | 0% | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
JPEG | 8,685 bytes | 35.7% | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
WebP | 4,078 bytes | 69.8% | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
AVIF | 2,408 bytes | 82.2% | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
性能测试代码
import time
import os
from PIL import Image
def performance_test(input_image_path):
"""性能测试:对比不同格式的转换速度和压缩效果"""
formats_config = {
'JPEG': {'quality': 85, 'optimize': True},
'WebP': {'quality': 85, 'optimize': True},
'AVIF': {'quality': 85, 'speed': 6, 'optimize': True},
}
results = {}
original_size = os.path.getsize(input_image_path)
with Image.open(input_image_path) as img:
print(f"原始图片: {img.size} {img.mode} ({original_size:,} bytes)")
print("-" * 60)
for format_name, config in formats_config.items():
output_path = f"test_output.{format_name.lower()}"
# 计时转换过程
start_time = time.time()
try:
if img.mode == 'RGBA' and format_name == 'JPEG':
# JPEG 不支持透明度
rgb_img = Image.new('RGB', img.size, (255, 255, 255))
rgb_img.paste(img, mask=img.split()[-1])
rgb_img.save(output_path, format=format_name, **config)
else:
img.save(output_path, format=format_name, **config)
conversion_time = time.time() - start_time
output_size = os.path.getsize(output_path)
compression_ratio = ((original_size - output_size) / original_size) * 100
results[format_name] = {
'size': output_size,
'compression': compression_ratio,
'time': conversion_time,
'speed': original_size / conversion_time / 1024 / 1024 # MB/s
}
print(f"{format_name:5}: {output_size:8,} bytes | "
f"压缩率: {compression_ratio:5.1f}% | "
f"耗时: {conversion_time:5.3f}s | "
f"速度: {results[format_name]['speed']:5.1f} MB/s")
# 清理输出文件
os.remove(output_path)
except Exception as e:
print(f"{format_name:5}: ❌ 转换失败 - {e}")
print("-" * 60)
# 找出最佳压缩比
best_compression = max(results.items(),
key=lambda x: x[1]['compression'])
print(f"🏆 最佳压缩: {best_compression[0]} "
f"({best_compression[1]['compression']:.1f}%)")
return results
# 运行性能测试
# performance_test('test_image.png')
质量评估
from PIL import Image, ImageChops
import math
def calculate_psnr(img1_path, img2_path):
"""计算两张图片的峰值信噪比 (PSNR)"""
img1 = Image.open(img1_path).convert('RGB')
img2 = Image.open(img2_path).convert('RGB')
# 确保尺寸一致
if img1.size != img2.size:
img2 = img2.resize(img1.size, Image.Resampling.LANCZOS)
# 计算均方误差
diff = ImageChops.difference(img1, img2)
h = diff.histogram()
# 计算 MSE
sq = (value * (idx % 256) ** 2 for idx, value in enumerate(h))
sum_of_squares = sum(sq)
rms = math.sqrt(sum_of_squares / float(img1.size[0] * img1.size[1]))
# 计算 PSNR
if rms == 0:
return float('inf') # 图像完全相同
psnr = 20 * math.log10(255.0 / rms)
return psnr
def quality_comparison(original_path):
"""质量对比测试"""
formats = ['JPEG', 'WebP', 'AVIF']
print("🔍 图像质量对比 (PSNR 值越高越好):")
print("-" * 50)
for fmt in formats:
output_path = f"quality_test.{fmt.lower()}"
# 转换图像
with Image.open(original_path) as img:
if fmt == 'AVIF':
img.save(output_path, format=fmt, quality=85, speed=6)
else:
img.save(output_path, format=fmt, quality=85, optimize=True)
# 计算 PSNR
psnr = calculate_psnr(original_path, output_path)
file_size = os.path.getsize(output_path)
print(f"{fmt:5}: PSNR = {psnr:6.2f} dB | 大小: {file_size:,} bytes")
os.remove(output_path)
# 使用示例
# quality_comparison('original.png')
常见问题与解决方案
1. AVIF 支持检查失败
问题: features.check('avif')
返回 False
解决方案:
# 方案A: 使用插件
pip install pillow-avif-plugin
# 代码中必须先导入
import pillow_avif
from PIL import features
print(features.check('avif')) # 应该返回 True
# 方案B: 重新编译 Pillow
pip uninstall pillow
export LDFLAGS="-L/opt/homebrew/lib"
export CPPFLAGS="-I/opt/homebrew/include"
pip install --no-binary pillow Pillow
2. 内存不足错误
问题: 处理大图时出现 OSError: cannot write mode RGBA as AVIF
解决方案:
def safe_avif_conversion(input_path, output_path, max_size=4096):
"""安全的 AVIF 转换,处理大图和内存限制"""
with Image.open(input_path) as img:
# 检查图片尺寸
if max(img.size) > max_size:
print(f"图片过大 {img.size},调整至 {max_size}")
img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
# 处理颜色模式
if img.mode in ('RGBA', 'LA'):
# 检查是否真的需要 Alpha 通道
if img.mode == 'RGBA':
alpha = img.split()[-1]
if alpha.getbbox() is None: # 完全不透明
img = img.convert('RGB')
print("移除无效的 Alpha 通道")
# 分块保存(对于极大的图片)
img.save(output_path, format='AVIF', quality=85,
speed=8, optimize=True) # 使用更快的速度
3. 颜色失真问题
问题: AVIF 转换后颜色不准确
解决方案:
def color_accurate_avif(input_path, output_path):
"""保持颜色准确的 AVIF 转换"""
with Image.open(input_path) as img:
# 保持颜色配置文件
icc_profile = img.info.get('icc_profile')
# 确保使用合适的颜色空间
if img.mode == 'P': # 调色板模式
img = img.convert('RGB')
save_kwargs = {
'format': 'AVIF',
'quality': 95, # 更高质量
'speed': 4, # 更慢但更准确
'range': 'full', # 完整色彩范围
}
# 保持 ICC 配置文件
if icc_profile:
save_kwargs['icc_profile'] = icc_profile
img.save(output_path, **save_kwargs)
4. 兼容性问题
问题: 某些环境下 AVIF 不被支持
解决方案:
def fallback_conversion(input_path, output_path,
primary_format='AVIF', fallback_format='WebP'):
"""带降级的格式转换"""
from PIL import features
# 检查主要格式支持
if features.check(primary_format.lower()):
target_format = primary_format
target_ext = '.avif'
else:
print(f"⚠️ {primary_format} 不支持,使用 {fallback_format}")
target_format = fallback_format
target_ext = '.webp'
# 调整输出路径
output_path = Path(output_path).with_suffix(target_ext)
with Image.open(input_path) as img:
if target_format == 'AVIF':
img.save(output_path, format='AVIF', quality=85, speed=6)
elif target_format == 'WebP':
img.save(output_path, format='WebP', quality=85, optimize=True)
return output_path
# 使用示例
output = fallback_conversion('photo.jpg', 'photo.avif')
print(f"保存为: {output}")
5. 批量处理性能优化
问题: 批量转换速度慢
解决方案:
import concurrent.futures
from multiprocessing import cpu_count
def parallel_avif_conversion(file_list, output_dir, max_workers=None):
"""并行 AVIF 转换"""
if max_workers is None:
max_workers = min(cpu_count(), len(file_list))
def convert_single_file(file_path):
output_path = Path(output_dir) / f"{file_path.stem}.avif"
try:
with Image.open(file_path) as img:
# 快速转换设置
img.save(output_path, format='AVIF',
quality=80, speed=8, optimize=True)
return True, file_path
except Exception as e:
return False, file_path, str(e)
results = {'success': 0, 'failed': 0, 'errors': []}
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_file = {executor.submit(convert_single_file, file_path): file_path
for file_path in file_list}
for future in concurrent.futures.as_completed(future_to_file):
result = future.result()
if result[0]: # 成功
results['success'] += 1
print(f"✅ {result[1].name}")
else: # 失败
results['failed'] += 1
results['errors'].append((result[1], result[2]))
print(f"❌ {result[1].name}: {result[2]}")
print(f"\n📊 并行转换完成: {results['success']} 成功, {results['failed']} 失败")
return results
# 使用示例
image_files = list(Path('./images').glob('*.jpg'))
parallel_avif_conversion(image_files, './avif_output', max_workers=4)
结论
AVIF 格式代表了图像压缩技术的未来,通过 Pillow 库可以轻松地集成到 Python 项目中。虽然配置过程可能需要一些额外步骤,但其卓越的压缩效率和图像质量使得这些努力完全值得。
关键要点
- 优先选择: AVIF 应该是现代 Web 应用的首选图像格式
- 合理降级: 为不支持的环境准备 WebP 或 JPEG 降级方案
- 质量平衡: 根据使用场景调整质量和速度参数
- 性能优化: 对于批量处理,使用并行转换提高效率
最佳实践
- 🔧 开发环境: 使用 pillow-avif-plugin 快速启用支持
- 🚀 生产环境: 从源码编译 Pillow 获得最佳性能
- 📱 响应式设计: 根据设备和网络条件动态选择格式
- ⚡ 性能监控: 定期测试转换效果和加载速度
随着浏览器支持的不断完善,AVIF 将成为 Web 图像的标准格式。现在开始在项目中采用 AVIF,将为用户带来更快的加载速度和更好的视觉体验!
参考资源: