在把像素交给磁盘之前,先回答三个问题:要不要丢信息、要不要透明、要存多深——JPEG、PNG、TIFF 就是三种不同的答案
核心方法论:艺术与工程
「一幅画的美,一半靠笔触,一半靠画布与颜料。图像文件格式就是数字世界的画布:选对了,作品历久弥新;选错了,再好的像素也会在存储与传输中褪色。这一章我们既当画家、又当工程师,把 JPEG、PNG、TIFF 三块画布的材质——有损与无损、DCT 与量化、调色板与标签——一块块摸透。」
第 1 章我们说过,一张 4000×3000 的 24 位照片,裸数据就要 36 MB。为什么磁盘上的 .jpg 往往只有几 MB?因为压缩(compression)在做减法。而减法分成两种境界:无损压缩(lossless)是"只删冗余、不伤内容"——解压后和压缩前逐字节相同,一个像素都不差;有损压缩(lossy)是"删掉人眼几乎察觉不到的信息"——解压后是近似,肉眼看着差不多,但数据已经永久丢失,无法还原。
无损压缩删的"冗余"到底是什么?想象一行像素"AAAAABBBBCCCC":与其把 13 个字符一个个写下来,不如记账写成"5 个 A、4 个 B、4 个 C"。这种把重复内容"记账"的思路叫行程编码(RLE,Run-Length Encoding),是最朴素的无损压缩;PNG 用的 DEFLATE(LZ77 + Huffman 编码)原理更精巧,但本质相同——找重复、记差异。有损压缩则更进一步:它发现人眼对"亮度"远比"颜色"敏感、对"大面积平缓变化"远比"高频细节"敏感,于是专挑你看不出来的部分下手。一句话说人话:无损是"抄笔记",有损是"画重点"。
怎么快速看出一个文件有没有被压缩过?打开它的十六进制头部——每种格式的魔数(magic number,文件开头用于标识格式的固定字节序列)是身份证:JPEG 以 FF D8 开头,PNG 有 8 字节固定签名,TIFF 以 II 或 MM 加版本号 42 开头。这些特征后面几节逐个拆解,先混个脸熟:
# xxd 查看三种格式的文件头(前 16 字节)
xxd -l 16 photo.jpg
# 00000000: ffd8 ffe0 0010 4a46 4946 0001 0101 0048 .......JFIF.....H
# ff d8 = SOI 标记(Start Of Image),JPEG 文件的身份证
xxd -l 16 icon.png
# 00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452 .PNG........IHDR
# 89 50 4e 47 = 固定签名,后面 IHDR 是第一个数据块
xxd -l 16 scan.tiff
# 00000000: 4949 2a00 0800 0000 3b01 0400 0100 0000 II*.........;....
# 49 49 = "II"(小端序),2a 00 = 版本号 42,见 2.4 节
把行程编码亲手写一遍,是理解"无损"最快的方式——注意第二个例子的反直觉之处:压缩不是每次都变小,数据没有重复时反而会变大,这正是"选对格式"重要性的第一课:
# run_length.py —— 行程编码:无损压缩的最小演示
def rle(data: str) -> str:
if not data:
return ""
out, run, prev = [], 1, data[0]
for ch in data[1:]:
if ch == prev:
run += 1
else:
out.append("{}{}".format(run, prev))
run, prev = 1, ch
out.append("{}{}".format(run, prev))
return "".join(out)
print(rle("AAAAABBBBCCCC")) # 5A4B4C:13 个字符 -> 6 个字符
print(rle("ABCDEFGH")) # 1A1B1C1D1E1F1G1H:没有重复,反而变长
| 维度 | 无损压缩 | 有损压缩 |
|---|---|---|
| 能否还原 | 逐字节还原,分毫不差 | 只能还原近似图像 |
| 原理 | 消除统计冗余(重复、可预测) | 丢弃人眼不敏感的信息 |
| 典型压缩比 | 2:1 ~ 5:1(视内容而定) | 10:1 以上仍可接受 |
| 典型格式 | PNG、TIFF(LZW 压缩)、GIF | JPEG、WebP(有损模式)、HEIC |
| 适用场景 | 截图、线稿、图标、存档 | 照片、网页、传输 |
判断"该不该用无损",问自己一个问题:这份图像还要不要再编辑?要编辑就存无损(PNG/TIFF),最终发布时再转有损(JPEG)。记住一个铁律:有损压缩的每一次保存,都在永久丢失信息——这就是"数字一代损失"(digital generation loss),2.2 节会讲到它为什么可怕。
JPEG(Joint Photographic Experts Group,联合图像专家组)是 1992 年发布的图像压缩标准,二十多年来一直是数字照片的事实标准——相机拍的、网页传的、手机存的,绝大多数是它。它的核心是有损压缩,典型能力是10:1 压缩率下肉眼几乎看不出区别。但 JPEG 有一个致命弱点:不适合文字、线稿、图标这类边缘锐利的图像——相邻像素的剧烈反差会暴露它的压缩痕迹,出现明显的"块状脏边"。
JPEG 凭什么敢丢信息?它押注在人眼的两个生理特性上:第一,人眼对亮度比对颜色敏感得多;第二,人眼对大面积平缓变化敏感,对高频细节(细碎纹理)迟钝。压缩就顺着这两条线索展开。标准的 baseline JPEG 编码流程分六步,每一步"丢不丢信息"清清楚楚:先把 RGB 转成 YCbCr(亮度 Y + 两个色度 Cb/Cr),接着对色度做下采样(最常见的是 4:2:0,色度在水平和垂直方向各减半——反正你看不出);然后把每个通道切成 8×8 像素块;每个块做 DCT 变换(离散余弦变换,把"空间域的像素值"变成"频率域的系数");最关键的一步是量化(quantization)——把每个系数除以量化表里的数再取整,高频系数往往被除成 0;最后对量化结果做 zigzag 扫描 + 行程编码 + Huffman 编码,这一步是无损的。
| 步骤 | 操作 | 丢信息吗? |
|---|---|---|
| 1. 颜色变换 | RGB → YCbCr,亮度与色度分离 | 否(数学可逆) |
| 2. 色度下采样 | Cb/Cr 按 4:2:0 减半 | 是(人眼难辨) |
| 3. 分块 | 每个通道切成 8×8 像素块 | 否 |
| 4. DCT 变换 | 空间域 → 频率域(能量集中低频) | 否(数学可逆) |
| 5. 量化 | 系数除以量化表数值并取整 | 是(主要损失都在这里) |
| 6. 熵编码 | zigzag 扫描 + RLE + Huffman | 否(无损压缩) |
整个流程里,第 5 步量化是唯一的"信息漏斗",也是 JPEG 质量参数的真正含义。量化表(quantization table)是一张 8×8 的数表,左上角数字小(低频系数保留得精细)、右下角数字大(高频系数被除得很粗)。编码器的 quality 参数(比如独立 JPEG 组 IJG 库常用的 0-100 尺度)就是用来缩放这张表的:质量越低,除数越大,被除成 0 的系数越多,文件越小、画质越差。高频系数大量归零后,图像就显出两种典型伪影:块效应(blockiness,8×8 块的边界清晰可见)和振铃(ringing,锐利边缘旁出现波纹状虚影)。
用代码亲手做一次"质量实验",比读十段理论都直观——同样的原图,质量从 95 降到 20,体积急剧缩小,而人眼在屏幕上往往要到 50 以下才明显察觉:
# quality.py —— 同一张原图按不同质量保存,观察体积变化
from PIL import Image
import os
img = Image.open("origin.png") # 一张无损原图
for q in (95, 80, 50, 20):
img.save("out_{}.jpg".format(q), quality=q) # quality 取值 0-100
size = os.path.getsize("out_{}.jpg".format(q))
print("quality={} size={} bytes".format(q, size))
# 输出示例(具体数字随图像内容变化):
# quality=95 size=2123456 bytes
# quality=80 size=789012 bytes
# quality=50 size=321000 bytes
# quality=20 size=118000 bytes
用 ImageMagick 的 identify 或 FFmpeg 的 ffprobe,可以查看一张现成 JPEG 的编码信息——注意 4:2:0 这个采样字样,它直接决定了一张 JPEG 的"省钱程度":
# ImageMagick:查看 JPEG 的编码参数
identify -verbose photo.jpg | head -25
# ffprobe(FFmpeg 自带):同样能读出编码器、像素格式与质量
ffprobe photo.jpg
# 输出节选:
# Stream #0:0: Video: mjpeg, yuvj420p(pc, bt470bg/unknown/unknown), 4000x3000
# yuvj420p 中的 420 就是 4:2:0 色度下采样
注意:JPEG 文件每被"打开-保存"一次,就会重新走一遍编码流程、再丢一轮信息——这就是数字一代损失。来回保存十几次后,即使每次都是高质量,画面也会明显发糊、出现块状斑。所以工作流必须是:编辑阶段存 PNG/TIFF,只有最终交付才转 JPEG。另外,JPEG 的"无损旋转/裁剪"只在尺寸恰好是 16 像素(4:2:0 下采样时的 MCU 块)整数倍时才成立,其余情况仍会重编码丢画质。
PNG(Portable Network Graphics,便携式网络图形)诞生于 1995 年,起因是一桩著名的专利纠纷:1994 年底 GIF 格式使用的 LZW 压缩算法被宣布需要缴纳专利费,社区决定做一个免费开源、且更强大的替代品。PNG 于 1996 年成为 W3C 推荐标准,1997 年发布为 RFC 2083,2004 年成为 ISO/IEC 15948 国际标准。它的定位非常明确:无损、支持透明、适合网络传输——图标、UI 元素、截图、图表,都是它的主场。PNG 至今不原生支持动画(动画要用 APNG/MNG 扩展),也不支持 CMYK 印刷色。
PNG 的文件结构是"8 字节签名 + 一串数据块(chunk)"。签名固定为 89 50 4E 47 0D 0A 1A 0A(前两个字节特意与 ASCII 和常见传输协议避开,防止文件被误当成文本处理)。每个 chunk 的布局是:4 字节长度 + 4 字节类型 + 数据 + 4 字节 CRC 校验。四个关键 chunk 撑起整个文件:IHDR(图像头,必须是第一个,13 字节固定内容:宽、高、位深、颜色类型等)、PLTE(调色板)、IDAT(图像数据,DEFLATE 压缩流,可拆成多个)、IEND(结束标记)。解码器遇到不认识的 chunk 可以安全跳过——这正是 PNG 能持续扩展的原因。
PNG 的调色板(palette)是它区别于 JPEG 的一大杀器:颜色类型 3(索引色)的图像,每个像素只存一个"色号"(1/2/4/8 位),真正的颜色放在 PLTE 调色板里,256 色以下的图标/UI 图因此能压得极小。透明则是第二大杀器:要么用 tRNS chunk 给调色板逐项指定透明度、或声明某个像素值全透明,要么直接用带 alpha 通道的颜色类型(4=灰度+alpha,6=RGBA),支持 0-255 任意程度的半透明。压缩上 PNG 全部采用 DEFLATE 无损压缩(LZ77 + Huffman),并在压缩前对每行像素做滤波预测(5 种滤波器,用左/上/左上邻像素预测当前值、只存差值),让数据更"好压"。位深最高支持每通道 16 位——远超 JPEG 的 8 位。
| Chunk | 全称 | 作用 |
|---|---|---|
IHDR | Image Header | 宽/高/位深/颜色类型等 13 字节,必须排第一 |
PLTE | Palette | 调色板,索引色(类型 3)必需 |
IDAT | Image Data | 图像数据,DEFLATE 压缩流 |
tRNS | Transparency | 透明信息:调色板逐项 alpha,或声明单色全透明 |
IEND | Image End | 结束标记,数据区为空 |
用十六进制编辑器直接看一个真实 PNG 的 chunk 序列——签名之后紧跟 IHDR,长度 00 00 00 0D(十进制 13)正好对应 IHDR 的 13 字节数据:
# 解析 PNG 文件头:签名 + 第一个 chunk
xxd -l 32 icon.png
# 00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452 .PNG........IHDR
# 89 50 4e 47 0d 0a 1a 0a = 8 字节签名(固定不变)
# 00 00 00 0d = chunk 数据长度:13 字节
# 49 48 44 52 = chunk 类型:"IHDR"
# 再往后 13 字节是 IHDR 数据:宽(4) + 高(4) + 位深(1) + 颜色类型(1) + 压缩(1) + 滤波(1) + 隔行(1)
# 颜色类型字段取值(IHDR 第 9 字节偏移处):
# 0 = 灰度,2 = 真彩色 RGB,3 = 索引色(调色板),4 = 灰度+alpha,6 = RGBA
用 Python Pillow 生成"调色板 PNG"和"RGBA 透明 PNG"两种典型形态——前者是图标优化的关键,后者是 Web 半透明贴图的基础:
# make_png.py —— 生成调色板 PNG 与 RGBA 透明 PNG
from PIL import Image
# 形态一:索引色 PNG(颜色类型 3,调色板)
rgb = Image.new("RGB", (2, 2))
rgb.putdata([(255,0,0), (0,255,0), (0,0,255), (255,255,0)])
pal = rgb.convert("P", palette=Image.ADAPTIVE, colors=4) # 自适应生成 4 色调色板
pal.save("palette.png")
# 形态二:RGBA PNG(颜色类型 6,带 alpha 透明通道)
rgba = Image.new("RGBA", (2, 2))
rgba.putdata([(255,0,0,255), (0,255,0,128), (0,0,255,0), (0,0,0,0)])
# 每像素四通道 (R, G, B, A),A=255 不透明,A=0 全透明
rgba.save("rgba.png")
PNG 的缺点也要认清:照片类图像存 PNG 会非常大——真实世界的照片几乎找不到可预测的重复,DEFLATE 无从发挥,体积常常是 JPEG 的 5-10 倍。所以口诀是:照片找 JPEG,图形找 PNG。另外,PNG 的 Adam7 隔行扫描(interlace)允许边下载边显示低分辨率预览,但会略微增大体积,非必要不开启。
TIFF(Tag Image File Format,标签图像文件格式)诞生于 1986 年,由 Aldus 公司(后并入 Adobe)为桌面出版和扫描仪行业设计。它的理念和 JPEG/PNG 截然不同:JPEG/PNG 是"规定好的盒子",TIFF 是"一块可以无限贴标签的画板"——一切信息都以标签(tag)的形式存在,图像数据本身也靠标签指明位置和格式。这种极度灵活的设计让它成为印刷、扫描、医学影像、科学摄影的事实标准,但也带来一句著名的玩笑:TIFF 是 "Thousands of Incompatible File Formats"(成千上万种互不兼容的文件格式)。为此 1992 年的 TIFF 6.0 规范划分了所有实现都必须支持的 Baseline TIFF(基线)与可选的 TIFF Extensions(扩展)。
一个 TIFF 文件的结构是:8 字节文件头 + 一个或多个 IFD。文件头前两个字节是字节序标记:II(小端,Intel 风格)或 MM(大端,Motorola 风格);接下来两个字节是版本号,永远是 42(十六进制 2A,各版本从未变过);再往后 4 字节指向第一个 IFD 的偏移。IFD(Image File Directory,图像文件目录)是 TIFF 的灵魂:它先写一个 2 字节的"条目数量",然后是若干条固定 12 字节的条目,最后 4 字节指向下一个 IFD(没有就写 0)——一个文件可以含多个 IFD,从而实现多页 TIFF(如传真文档)。每个 12 字节条目由四部分组成:2 字节 Tag 编号 + 2 字节数据类型 + 4 字节数量 + 4 字节"值或偏移"。
标签编号是裸数字(如 256 表示宽度),规范只规定"编号对应的含义"——这正是"标签化"的设计精髓:不认识某个标签的阅读器可以直接跳过它,格式因此可以无限扩展而不破坏兼容性。图像数据按strip(条带)组织:整幅图按行切成若干条,每条独立压缩、独立存储,由 StripOffsets(273)等标签给出位置和长度。压缩方案五花八门:不压缩、无损的 LZW 和 PackBits、有损的 JPEG,还有传真用的 CCITT Group 4——基线 TIFF 只强制支持无压缩、PackBits 和 CCITT 三种。数据类型也有讲究:除了常见的 BYTE(1)/ASCII(2)/SHORT(3)/LONG(4)/RATIONAL(5),扩展还支持浮点数样本——这让 TIFF 能保存科学相机 CCD 采到的 16 位甚至更高精度的数据。
| Tag 编号 | 名称 | 含义 |
|---|---|---|
| 256 | ImageWidth | 图像宽度(像素) |
| 257 | ImageLength | 图像高度(像素) |
| 258 | BitsPerSample | 每通道位数(如 8、16) |
| 259 | Compression | 压缩方案:1=无、5=LZW、7=JPEG、32773=PackBits |
| 262 | PhotometricInterpretation | 颜色模型:0/1=黑白、2=RGB、3=调色板 |
| 273 | StripOffsets | 每个 strip 数据在文件中的偏移 |
| 277 | SamplesPerPixel | 每像素通道数(RGB=3,RGBA=4) |
| 278 | RowsPerStrip | 每个 strip 包含多少行 |
| 279 | StripByteCounts | 每个 strip 占多少字节 |
亲手解码一个 TIFF 文件头,把"标签哲学"落到实处——注意 2a 00 这个版本号,以及第 9 字节起那个指向 IFD 的偏移:
# 解析 TIFF 文件头(小端序示例)
xxd -l 16 scan.tiff
# 00000000: 4949 2a00 0800 0000 3b01 0400 0100 0000 II*.........;....
# 49 49 = "II":小端字节序(Intel 风格;"MM" 则为大端)
# 2a 00 = 42:TIFF 版本号,所有版本都是 42
# 08 00 00 00 = 8:第一个 IFD 在文件中的偏移(本例在偏移 8)
# 一个 IFD 条目固定 12 字节:
# 偏移 0-1: Tag 编号(0x0100 = 256 = ImageWidth)
# 偏移 2-3: 数据类型(1=BYTE 2=ASCII 3=SHORT 4=LONG 5=RATIONAL)
# 偏移 4-7: 数量 Count(该类型元素个数)
# 偏移 8-11: 值;超过 4 字节时这里存"指向真实数据的文件偏移"
libtiff 自带的 tiffinfo 命令可以把一个 TIFF 的全部标签"翻译"成人话——对照上表,你能在输出里认出每一个字段的来历:
# 安装 libtiff 工具后(Ubuntu: sudo apt install libtiff-tools)
tiffinfo scan.tiff
# 输出节选(以一张 1600x1200 无压缩 RGB 扫描件为例):
# TIFF Directory at offset 0x8 (8)
# Image Width: 1600 Image Length: 1200
# Bits/Sample: 8
# Compression Scheme: None
# Photometric Interpretation: RGB
# Samples/Pixel: 3
# Rows/Strip: 1200
# Strip Byte Counts: 5760000
TIFF 的"标签容器"思想影响深远:TIFF/EP(ISO 12234-2,电子摄影标准)在 TIFF 6.0 基础上扩展了传感器相关的私有标签,而相机 RAW 格式与 Adobe 的 DNG(Digital Negative,数字底片)都建立在 TIFF 结构之上——这为下一章埋下了伏笔:为什么相机 RAW 文件长着一副 TIFF 的样子?第 3 章拆开给你看。
学到这儿,三种格式的性格已经清楚了:JPEG 是"照片专家"——有损但体积小,人眼买账;PNG 是"图形专家"——无损、支持透明、适合图标和 UI;TIFF 是"档案管理员"——无损、可多页、可存高精度科学数据,但体积大、结构复杂。选型的本质不是"哪个最好",而是"你的场景最不能容忍失去什么"。第 1 章提到的服务端图像处理场景(转码、预览、缩略图)尤其吃这套决策:上游用户上传的可能是任何格式,下游产品要的是"网页缩略图要 JPEG、头像透明底要 PNG、原始存档要原样保留"。
| 维度 | JPEG | PNG | TIFF |
|---|---|---|---|
| 压缩方式 | 有损(默认) | 无损(DEFLATE) | 均可:无 / LZW / JPEG 等 |
| 透明支持 | 不支持 | 支持(tRNS / alpha 通道) | 支持(额外通道) |
| 位深 | 8 位/通道(baseline) | 最高 16 位/通道 | 最高 32 位浮点(扩展) |
| 多页支持 | 不支持 | 不支持 | 支持(多个 IFD) |
| 典型用途 | 照片、网页、传输 | 图标、UI、截图、透明贴图 | 扫描、印刷、科学数据、存档 |
| 文件体积 | 小 | 中(照片上会很大) | 大(常不压缩) |
工程上的第一步永远是"先搞清楚手上是什么"。file 命令按内容识别格式(不信任扩展名——服务器上最常见的坑就是 .png 的文件其实是 JPEG),ImageMagick 的 identify 给出更多细节。下面是服务端转码管线的两个最小命令:
# 按文件内容识别格式(不信任扩展名)
file photo.jpg icon.png scan.tiff
# photo.jpg: JPEG image data, JFIF standard 1.01, resolution (72x72)
# icon.png: PNG image data, 32 x 32, 8-bit/color RGBA, non-interlaced
# scan.tiff: TIFF image data, little-endian, big-endian, RGB
# 批量把 PNG 转成 JPEG(ImageMagick 6 用 convert,7 用 magick)
for f in *.png; do
identify "$f" # 转码前先看格式与尺寸
convert "$f" -quality 85 "${f%.png}.jpg" # 转成 quality=85 的 JPEG
done
最后给一个"选型决策"的骨架伪代码——它概括了本章全部要点,也是第 5 章"六库选型"方法论的小型预演。注意它把透明需求放在最优先:一旦需要透明,JPEG 直接出局,只能在 PNG 与 TIFF 之间选:
# choose_format —— 格式选型决策(伪代码)
def choose_format(need_alpha: bool, need_lossless: bool, target: str) -> str:
if need_alpha and need_lossless:
return "PNG" # 透明 + 无损:UI 图标、头像、贴图
if need_lossless:
return "TIFF" # 无损存档、科学数据、印刷原稿
if target == "web":
return "JPEG" # 照片类网页图:体积优先
return "TIFF" # 印刷 / 存档兜底
注意:别用 JPEG 做"中间产物"。转码管线的正确姿势是:原始上传文件原样存档(PNG/TIFF/RAW 不动),所有尺寸/质量不同的下游产物从原始文件现算。一旦让"JPEG 转 JPEG"发生,每一层都会叠加一代损失,第 16 章回顾全链路时你会再见到这条铁律。至于 AVIF、WebP、HEIC 这些新格式——它们正在蚕食 JPEG/PNG 的地盘,第 16 章的格式演进会专门讲。
把左边的概念与右边最贴切的解释配对:① 无损压缩;② 有损压缩;③ DCT 变换;④ 量化。候选解释:A. 把空间域像素变成频率域系数;B. 逐字节还原,不丢任何信息;C. 除以量化表数值取整,信息主要丢在这里;D. 丢弃人眼不敏感的信息,只还原近似图像。
回顾 2.1 节的"抄笔记 vs 画重点"比喻,以及 2.2 节六步编码表里"丢信息吗"那一列。
① 无损压缩 → B;② 有损压缩 → D;③ DCT 变换 → A;④ 量化 → C。注意 DCT 本身数学可逆,真正的信息损失发生在量化这一步——这也是 JPEG 质量参数只影响量化表的原因。
某目录下有三个文件,扩展名分别是 a.jpg、b.png、c.tiff,但其中有一个文件的真实格式与扩展名不符。用 xxd -l 8 文件 查看文件头,得到如下十六进制:(a) ffd8 ffe0 0010 4a46;(b) 8950 4e47 0d0a 1a0a;(c) 4949 2a00 0800 0000。请判断哪个文件"名不副实"。
对照 2.1 节三种格式的魔数:JPEG 以 FF D8 开头,PNG 是 89 50 4E 47 0D 0A 1A 0A,TIFF 是 49 49 2A 00(或 4D 4D 00 2A)。
(b) 是 PNG 签名、(c) 是 TIFF 头(II + 版本 42),都名实相符;只有 (a) 的头部 ffd8 ffe0 是 JPEG 的 SOI 标记,但它挂着 .png 的扩展名——实际上 (a) 是扩展名为 .png 的 JPEG 文件。这正是 file 命令按内容而非扩展名识别格式的原因,服务端处理用户上传时尤其要防这一手。
小明把一张照片存成 JPEG 后,每次用画图软件"打开再另存"一次,如此往复二十次。请解释为什么最后画面明显发糊,并给出正确的编辑工作流。
回想 2.2 节"数字一代损失":JPEG 每次保存都要重新走一遍 6 步编码,哪一步在重复丢信息?
每次另存都会重新执行"量化"步骤,把已经量化过的系数再除一遍、再取整一遍,误差逐代累积,二十次后块效应和振铃就非常明显了。正确工作流:编辑阶段用 PNG/TIFF 等无损格式保存中间版本,所有编辑完成后再导出一次 JPEG 用于交付——这样只承受一次有损损失。同理,服务端转码管线绝不能让"JPEG 转 JPEG"发生。
用 Python Pillow 完成三次递进实验:① 任选一张照片,按 quality=95/50/20 各存一份 JPEG,打印文件大小对比;② 生成一张带透明通道的 RGBA PNG,再用 xxd 查看它的签名和前两个 chunk 的类型;③ 用 file 和 identify 检查你生成的所有文件,确认格式与预期一致。
JPEG 质量实验参考 2.2 节的 quality.py;RGBA PNG 参考 2.3 节的 make_png.py;格式识别参考 2.5 节的 file/identify 命令。chunk 类型应该看到 IHDR 和 IDAT。
① 体积应随 quality 下降明显变小(具体数值因图而异),quality=20 时肉眼可见块效应;② 文件头应为 8950 4e47 0d0a 1a0a 加 IHDR chunk,xxd -l 32 能看到 4948 4452(IHDR)字样;③ file 应报告 JPEG image data / PNG image data,identify 能读出尺寸与色深。三关全过,你就掌握了本章的全部技能点。