本节摘要:Pillow 支持 30+ 种格式,但高级用法藏在细节里:WebP 的有损/无损与质量参数、多帧 TIFF 的逐页处理、EXIF 元数据的读取与保留、以及专业转换的参数控制。本节逐一拆解这些"格式进阶"能力。
阅读完本节,你应当能够:
日常最常用 JPG/PNG,但真实世界还有 WebP(网页主流)、TIFF(专业/印刷)、BMP、ICO、GIF 等。Pillow 的好消息是打开/保存基本自动识别——难点在于各格式的专属参数与"元数据会不会丢"。
from PIL import Image img = Image.open("photo.jpg") img.save("photo.webp") # 转 WebP img.save("photo.tiff") # 转 TIFF print(img.format) # 原格式 JPEG
按扩展名保存即转换,这点和第二章说的一致。但 WebP、TIFF、EXIF 各有坑,逐个看。
# 有损压缩(默认) img.save("out.webp", quality=80) # 无损压缩 img.save("out_lossless.webp", lossless=True) # 带透明 img.convert("RGBA").save("out_trans.webp", lossless=True)
WebP 比 JPG 体积小 25-35%(同质量),且支持透明。Web 场景 WebP 是 JPG+PNG 的替代者。
| 参数 | 作用 | 说明 |
|---|---|---|
| quality | 有损质量 0-100 | 默认 80 平衡体积质量 |
| lossless | 无损模式 | 适合透明/文本截图 |
| method | 压缩力度 0-6 | 越高越慢越小 |
# 打开多页 TIFF tiff = Image.open("multi_page.tiff") print("页数:", tiff.n_frames) # 遍历提取每一页 pages = [] try: while True: pages.append(tiff.copy()) tiff.seek(tiff.tell() + 1) except EOFError: pass print(f"提取 {len(pages)} 页") # 保存多页 TIFF pages[0].save("output.tiff", save_all=True, append_images=pages[1:])
多帧 TIFF 常用于扫描件、传真、医学影像——处理逻辑与 GIF 完全一致(seek/tell/n_frames)。
from PIL import Image from PIL.ExifTags import TAGS img = Image.open("photo.jpg") exif = img.getexif() for tag_id, value in exif.items(): tag_name = TAGS.get(tag_id, tag_id) if tag_name in ("Make", "Model", "DateTime", "FNumber", "ISOSpeedRatings"): print(f"{tag_name}: {value}")
EXIF 里存着相机型号、拍摄时间、光圈、ISO、GPS 等元数据。处理照片后保留 EXIF 是个好习惯(版权、整理、溯源都需要)。
⚠️ 常见坑 1:保存丢 EXIF。和 ICC 一样,save 默认不保留 EXIF。保留方法:
img.save("out.jpg", quality=92, exif=img.getexif())
⚠️ 常见坑 2:WebP 兼容性。老浏览器不支持 WebP——前端要判断环境或提供 fallback。服务端转码时,"原图 JPG + 派生 WebP"的双格式策略最常见。
img.save("out.jpg", quality=92, # 质量 optimize=True, # 优化 Huffman 表,略慢但更小 progressive=True, # 渐进式加载(网页体验好) subsampling=1) # 色度抽样,1=4:2:2 质量更高
progressive=True 很实用:渐进式 JPG 在网页上"先模糊后清晰",比逐行加载体验好。
| 场景 | 推荐格式 | 关键参数 |
|---|---|---|
| 网页照片 | WebP 或 JPG | quality 80-92 |
| 网页透明图 | WebP 或 PNG | lossless=True |
| 打印/专业 | TIFF | 无损 |
| 图标 | ICO/PNG | 多尺寸 |
| 动画 | GIF/WebP 动图 | duration/loop |
from PIL import Image import os img = Image.open("photo.jpg").convert("RGB") formats = { "jpg_q85": lambda: img.save("fmt.jpg", quality=85), "jpg_q95": lambda: img.save("fmt2.jpg", quality=95), "webp_q80": lambda: img.save("fmt.webp", quality=80), "png": lambda: img.save("fmt.png"), } for name, fn in formats.items(): fn() size = os.path.getsize(f"fmt.{'jpg' if 'jpg' in name else ('webp' if 'webp' in name else 'png')}") print(f"{name}: {size//1024} KB")
对比同图不同格式/质量的体积,你会惊讶:WebP 80 质量体积可能是 JPG 85 的一半,肉眼几乎无差别。
EXIF 不只是"看看信息",还能用于自动化处理:
from PIL import Image from PIL.ExifTags import TAGS import os, shutil def get_datetime(path): try: exif = Image.open(path).getexif() for tag_id, value in exif.items(): if TAGS.get(tag_id) == "DateTime": return str(value)[:10] except Exception: pass return "unknown" # 按日期整理 os.makedirs("sorted", exist_ok=True) for fname in os.listdir("photos"): if fname.lower().endswith((".jpg", ".jpeg")): date = get_datetime(os.path.join("photos", fname)) date_dir = os.path.join("sorted", date.replace(":", "-")) os.makedirs(date_dir, exist_ok=True) shutil.copy(os.path.join("photos", fname), os.path.join(date_dir, fname)) print("已按拍摄日期整理")
按 EXIF 日期归档是照片管理的高频需求。
from PIL import Image, ImageOps with Image.open("phone_photo.jpg") as img: # 读取并修正方向 fixed = ImageOps.exif_transpose(img) exif = img.getexif() fixed.save("fixed.jpg", quality=92, exif=exif.tobytes())
⚠️ 常见坑:exif 参数传 bytes 而非 getexif 对象。某些版本 save 的 exif 参数接受
exif.tobytes()更稳妥。
给"图片体积优化"做一个完整的批量工具(Web 上线前压图):
from PIL import Image, ImageOps import os def optimize_image(src, dst, max_width=1920, quality=80, webp=True): """压缩并优化图片:限宽 + 高质量 + 转 WebP""" with Image.open(src) as img: img = ImageOps.exif_transpose(img) if img.width > max_width: ratio = max_width / img.width img = img.resize((max_width, int(img.height * ratio)), Image.Resampling.LANCZOS) img = img.convert("RGB") if webp: img.save(dst, "WEBP", quality=quality, method=6) else: img.save(dst, "JPEG", quality=quality, optimize=True) os.makedirs("optimized", exist_ok=True) for fname in os.listdir("photos"): if fname.lower().endswith((".jpg", ".jpeg", ".png")): out_name = os.path.splitext(fname)[0] + ".webp" optimize_image(os.path.join("photos", fname), os.path.join("optimized", out_name)) print("批量优化完成")
Web 图片优化标准动作:exif 转正 → 限宽(移动端 1920 足够)→ 转 WebP(quality 80 + method 6 最大压缩)。这套流程能让图片体积普遍降 60-80%——前端性能优化的第一杠杆。
问:WebP 和 PNG 哪个适合透明图?
WebP 无损透明图体积通常比 PNG 小 20-30%,但兼容性略差。优先 WebP,fallback PNG。
问:TIFF 和 JPG 怎么选?
TIFF 无损(适合印刷、专业存档)、JPG 有损(适合 Web、存储)。要"可二次编辑"用 TIFF,要"共享展示"用 JPG。
问:为什么我 save 的 GIF 是静态的?
保存动画需要 save_all=True + append_images(见 2.5)。
问:ICO 图标怎么生成多尺寸?img.save("icon.ico", sizes=[(16,16),(32,32),(48,48),(256,256)]) 一次输出多尺寸图标。
把格式知识组装成"图标生成器"——从文字或图形生成多尺寸 ICO:
from PIL import Image, ImageDraw, ImageFont def make_icon(text, size=256, bg="steelblue", fg="white"): """生成一个文字图标(多尺寸 ICO)""" img = Image.new("RGB", (size, size), bg) draw = ImageDraw.Draw(img) font = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", int(size * 0.6)) bbox = draw.textbbox((0, 0), text, font=font) tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1] draw.text(((size - tw)//2, (size - th)//2 - bbox[1]), text, fill=fg, font=font) return img icon = make_icon("PI", 256) icon.save("app.ico", sizes=[(16, 16), (32, 32), (48, 48), (64, 64), (256, 256)]) icon.save("app.png") print("图标已生成(ICO 多尺寸 + PNG)")
ICO 多尺寸的意义:Windows 会在不同场景自动选择合适尺寸的图标——一次输出全部尺寸,系统按需取用。
生产环境选格式,不只是"哪个小",而是综合权衡:
| 决策维度 | 说明 | 决策方法 |
|---|---|---|
| 兼容性 | 老设备/老浏览器 | 用 JPG/PNG 最稳 |
| 体积 | 流量与存储成本 | WebP 最优 |
| 质量 | 压缩可接受度 | 对比肉眼差异 |
| 功能 | 透明/动画/图层 | 按需求选 |
| 处理链 | 后续还要编辑吗 | 无损格式保底 |
def pick_format(needs): """按需求返回推荐格式""" if needs.get("animation"): return "WEBP" if needs.get("modern") else "GIF" if needs.get("transparency"): return "WEBP" if needs.get("modern") else "PNG" if needs.get("editable") or needs.get("print"): return "TIFF" return "WEBP" if needs.get("modern") else "JPEG" print(pick_format({"transparency": True, "modern": True})) # WEBP print(pick_format({"print": True})) # TIFF print(pick_format({"modern": False})) # JPEG
格式决策框架:先答"动画吗?透明吗?要印刷/编辑吗?",再答"目标设备新旧?",最后落到具体格式。
EXIF 不只是"看看",做一个"EXIF 信息管理器":
from PIL import Image from PIL.ExifTags import TAGS def read_exif_summary(path): """读取并总结 EXIF 关键信息""" with Image.open(path) as img: exif = img.getexif() interesting = { "Make": "厂商", "Model": "型号", "DateTime": "拍摄时间", "FNumber": "光圈", "ISOSpeedRatings": "ISO", "ExposureTime": "快门", "FocalLength": "焦距", "Orientation": "方向", } result = {} for tag_id, value in exif.items(): name = TAGS.get(tag_id, tag_id) if name in interesting: result[interesting[name]] = value return result def remove_exif(src, dst): """去除 EXIF(隐私保护)""" with Image.open(src) as img: img.save(dst, icc_profile=img.info.get("icc_profile")) print(f"已去除 EXIF: {src} → {dst}") info = read_exif_summary("photo.jpg") for k, v in info.items(): print(f"{k}: {v}")
EXIF 管理的两个方向:读取(整理、归档)与去除(隐私)。分享照片前去除 EXIF 是隐私保护习惯——EXIF 里可能包含 GPS 定位等敏感信息。
| 误区 | 正确认知 |
|---|---|
| "WebP 是最好的格式" | 兼容性限制,要 fallback |
| "PNG 无损就完美" | 无损=体积大,要权衡 |
| "转换格式不损失" | JPG 压缩有损 |
| "EXIF 会自动保留" | 需显式传参 |
| "一个格式走天下" | 按场景多格式策略 |
误区根源:把"格式"当"绝对优劣"而非"场景权衡"。格式选择 = 兼容性 × 体积 × 质量 × 功能的权衡。
把格式知识做成"自动优化器",用于图片上传/发布的自动处理:
def auto_optimize(src, dst, max_width=1920, webp=True): """自动优化:限宽 + WebP + 保留关键元数据""" with Image.open(src) as img: img = ImageOps.exif_transpose(img) if img.width > max_width: ratio = max_width / img.width img = img.resize((max_width, int(img.height * ratio)), Image.Resampling.LANCZOS) exif = img.getexif() img = img.convert("RGB") if webp: img.save(dst, "WEBP", quality=82, method=6) else: img.save(dst, "JPEG", quality=85, optimize=True, exif=exif.tobytes()) os.makedirs("optimized", exist_ok=True) for f in os.listdir("photos"): if f.lower().endswith((".jpg", ".jpeg", ".png")): out = os.path.splitext(f)[0] + ".webp" auto_optimize(os.path.join("photos", f), os.path.join("optimized", out)) print("批量优化完成")
自动优化器的标准流程:exif 转正 → 限宽 → 转 WebP(质量+method 平衡)→ 输出。
大格式文件(如 TIFF 多页、超大 WebP)的内存处理技巧:
def stream_convert(src, dst, target_fmt="JPEG", max_dim=4000): """流式转换:大图先缩再转,避免内存爆""" with Image.open(src) as img: w, h = img.size print(f"源尺寸: {w}x{h}") if max(w, h) > max_dim: img.thumbnail((max_dim, max_dim)) if target_fmt in ("JPEG", "WEBP"): img = img.convert("RGB") img.save(dst, target_fmt, quality=85) def process_tiff_pages(src, out_dir, fn): """对 TIFF 每页应用 fn,逐页处理防内存爆""" import os os.makedirs(out_dir, exist_ok=True) tiff = Image.open(src) try: i = 0 while True: page = tiff.copy() fn(page).save(os.path.join(out_dir, f"page_{i:03d}.png")) i += 1 tiff.seek(tiff.tell() + 1) except EOFError: pass print(f"处理 {i} 页")
内存控制要点:先探尺寸(open 惰性)、逐页处理(不整卷载入)、先缩再转。"先看、再缩、后转"是大格式处理的黄金顺序。
综合格式、EXIF、批量能力,做一个"相册归档工具":
def archive_photos(src_dir, dst_dir, thumb_size=(800, 800)): """相册归档:按日期分类 + 生成缩略图 + 统一 WebP""" os.makedirs(dst_dir, exist_ok=True) thumb_dir = os.path.join(dst_dir, "thumbs") os.makedirs(thumb_dir, exist_ok=True) stats = {"archive": 0, "thumb": 0, "failed": 0} for f in os.listdir(src_dir): if not f.lower().endswith((".jpg", ".jpeg")): continue src = os.path.join(src_dir, f) try: with Image.open(src) as img: date = "unknown" exif = img.getexif() for tag_id, value in exif.items(): if TAGS.get(tag_id) == "DateTime": date = str(value)[:10].replace(":", "-") break date_dir = os.path.join(dst_dir, date) os.makedirs(date_dir, exist_ok=True) img = ImageOps.exif_transpose(img) img.convert("RGB").save( os.path.join(date_dir, os.path.splitext(f)[0] + ".webp"), "WEBP", quality=88) stats["archive"] += 1 thumb = ImageOps.fit(img, thumb_size) thumb.save(os.path.join(thumb_dir, f), quality=85) stats["thumb"] += 1 except Exception as e: stats["failed"] += 1 print(f"失败 {f}: {e}") print(f"归档 {stats['archive']} 张, 缩略图 {stats['thumb']} 张, 失败 {stats['failed']}") archive_photos("camera", "archive")
相册归档的三合一:按日期分类(EXIF)、压缩存储(WebP)、缩略图生成(ImageOps)——一个工具完成"整理+压缩+预览"三件事。
格式处理还有安全与合规层面的注意事项:
import io # 1. 解压炸弹防护(小文件解出巨图) MAX_PIXELS = 5000 * 5000 def safe_open(data): img = Image.open(io.BytesIO(data)) if img.width * img.height > MAX_PIXELS: raise ValueError("图片过大(疑似解压炸弹)") img.load() return img # 2. 格式伪装检测(扩展名与实际不符) def verify_format(data, expected): img = Image.open(io.BytesIO(data)) actual = img.format print(f"声称: {expected}, 实际: {actual}") return actual == expected # 3. 敏感元数据清除(分享前) def strip_metadata(src, dst): """去除 EXIF/GPS 等敏感信息""" with Image.open(src) as img: img.convert("RGB").save(dst, "JPEG", quality=90) print(f"已清除元数据: {src} → {dst}")
安全合规三件事:防炸弹(像素上限)、防伪装(格式核验)、防泄露(元数据清除)——**"处理图片的同时守护安全与隐私"**是生产环境的基本要求。
把格式知识综合到一个"上传自动处理"流程,验证全链路:
def upload_process(data, max_dim=2000, quality=85): """上传图片自动处理:校验+优化+多格式""" img = Image.open(io.BytesIO(data)) img = ImageOps.exif_transpose(img) if img.width > max_dim: ratio = max_dim / img.width img = img.resize((max_dim, int(img.height * ratio)), Image.Resampling.LANCZOS) results = {} for fmt in ["JPEG", "WEBP"]: buf = io.BytesIO() img.convert("RGB").save(buf, fmt, quality=quality) results[fmt] = buf.getvalue() return img.size, results with open("upload.jpg", "rb") as f: size, results = upload_process(f.read()) print(f"处理后尺寸: {size}") for fmt, data in results.items(): print(f"{fmt}: {len(data)//1024} KB")
上传处理的全链路:安全校验 → 方向 → 限宽 → 多格式输出——这是 4.1 Web 服务的"离线版",把格式/EXIF/安全知识全部串起来了。
本节小结:格式处理的核心不是"记住每种格式的参数",而是"按场景做决策"——网页用 WebP、透明用 PNG/WebP、印刷用 TIFF、动画用 GIF/WebP。同时记住三个"必须显式":保存必须显式传 exif/icc、GIF 必须转 P 模式、元数据必须主动管理。
把格式知识综合到一个"上传自动处理"流程,验证全链路:
import io def upload_process(data, max_dim=2000, quality=85): """上传图片自动处理:校验+优化+多格式""" img = Image.open(io.BytesIO(data)) img = ImageOps.exif_transpose(img) if img.width > max_dim: ratio = max_dim / img.width img = img.resize((max_dim, int(img.height * ratio)), Image.Resampling.LANCZOS) results = {} for fmt in ["JPEG", "WEBP"]: buf = io.BytesIO() img.convert("RGB").save(buf, fmt, quality=quality) results[fmt] = buf.getvalue() return img.size, results with open("upload.jpg", "rb") as f: size, results = upload_process(f.read()) print(f"处理后尺寸: {size}") for fmt, data in results.items(): print(f"{fmt}: {len(data)//1024} KB")
上传处理的全链路:安全校验 → 方向 → 限宽 → 多格式输出——这是 4.1 Web 服务的"离线版",把格式/EXIF/安全知识全部串起来了。**"一个处理链覆盖多个需求"**正是"格式知识综合应用"的价值——从单点知识到完整方案。
问:为什么 WebP 有时比 JPG 还大?
复杂图像(高细节、大色块边缘)WebP 优势不明显;且 WebP 的 method 参数(压缩力度)默认不高。用 method=6 提高压缩率,或对比两种格式选更小的。
问:TIFF 可以保存动画吗?
多帧 TIFF 支持"多页"但不支持"播放"——它是文档/扫描场景的多页容器,不是动画格式。
问:EXIF 里哪些信息最敏感?
GPS 定位(地理位置)、序列号、拍摄者信息。分享照片前用 strip_metadata 清除(见 3.4 安全小节)。
问:图标生成有什么注意事项?
ICO 需多尺寸(16/32/48/64/256);透明背景用 RGBA;Windows 从大尺寸自动缩放小尺寸,所以至少包含 256px。
把格式处理放进"从文件到线上"的完整工作流:
上传(任意格式) → 校验(大小/内容/格式/像素) → 优化(exif转正→限宽→转WebP) → 存储(OSS/S3,多尺寸) → 分发(CDN,WebP+JPG fallback) → 展示(按设备选尺寸)
| 环节 | 格式要点 | 工具 |
|---|---|---|
| 上传 | 格式白名单 | verify_format |
| 优化 | WebP quality/method | auto_optimize |
| 存储 | 原图+多尺寸 | 批量转换 |
| 分发 | WebP+JPG 双格式 | pick_format |
| 展示 | 响应式尺寸 | 4.1 |
**"全链路格式管理"**是专业团队的标准做法——每个环节都明确"用什么格式、什么参数"。哪怕只做到"上传即优化、输出双格式",图片体积与加载体验就能大幅改善。
误区根源:把"格式"当"绝对优劣"而非"场景权衡"。格式选择 = 兼容性 × 体积 × 质量 × 功能的权衡——理解各格式的取舍,才能按场景选对格式。
把格式处理的知识组织成"决策地图":
决策逻辑:先答"要什么"(网页/透明/印刷/图标/动画),再选格式与参数。"需求→格式→参数"的映射让格式选择从"纠结"变"决策"——遇到任何格式需求,都能快速选对。
格式会转了,最后一节讲"大数据量怎么办"——图像金字塔与缩略图优化,让 Pillow 跑得动大图和批量任务。