一张数码照片除了像素本身,往往还带有一组描述拍摄条件的附属数据:时间、曝光参数、设备标识,有时还有经纬度。这些数据按 EXIF 规范组织,物理上以 TIFF 结构存放。在图片浏览器这类应用中,「读图」因此常常包含两个彼此独立的任务:解码像素,以及解析这段结构化元数据。
EXIF 的数据模型并不复杂,但解析实现容易在几个细节上出错:字节序、偏移基准、内联值与偏移值的区分。以下按结构顺序梳理这些规则。
EXIF 在文件中的位置
在 JPEG 中,EXIF 存放在 APP1 标记段(0xFFE1)内。该段以 Exif\0\0 六个字节开头,其后紧跟一个完整的 TIFF 结构。JPEG 标记段的长度字段为 2 字节,单段能承载的数据量有限,因此写在其中的缩略图通常被限制在较小尺寸。标记段的组织方式可参见渐进式 JPEG 与交错 PNG 的编码原理。
在 TIFF 文件中,EXIF 不是附加段,而是文件本身的一部分:TIFF 头之后就是 IFD 链,EXIF 专用标签直接写在这些 IFD 中。PNG 与 WebP 没有原生的 EXIF 结构,通常以私有块或 RIFF 块的形式嵌入同一份 TIFF 结构,因此解析逻辑可以复用。
TIFF 头与字节序
TIFF 结构以一个 8 字节文件头开始:2 字节字节序标记、2 字节魔数(值为 42)、4 字节给出起始 IFD 的偏移。字节序标记决定后续所有多字节整数的解释方式:
II(0x4949):小端序,低位字节在前MM(0x4D4D):大端序,高位字节在前
结构内部的所有偏移都相对于 TIFF 头的起点,而不是相对于文件起点。这一点在解析嵌入 APP1 段中的 EXIF 时尤其容易出错,因为 TIFF 头并不在文件开头。
import struct
def read_tiff_header(buf: bytes):
# buf 必须从 TIFF 头开始
endian = "<" if buf[:2] == b"II" else ">"
magic, first_ifd = struct.unpack_from(endian + "HI", buf, 2)
if magic != 42:
raise ValueError("not a TIFF structure")
return endian, first_ifd
IFD 与条目结构
IFD(Image File Directory)由三部分构成:2 字节的条目数、若干条 12 字节的条目、以及 4 字节的下一 IFD 偏移(为 0 表示链结束)。每个条目的字段含义如下:
struct ifd_entry { /* 固定 12 字节 */
uint16_t tag; /* 标签号,如 0x0112 表示方向 */
uint16_t type; /* 类型:1=BYTE 2=ASCII 3=SHORT 4=LONG 5=RATIONAL ... */
uint32_t count; /* 值的个数 */
uint32_t value_or_offset; /* 值本身,或指向值的偏移 */
};
值能否内联存放取决于类型与数量:若「类型字节数 × 数量」不超过 4,则直接写在这 4 个字节内;否则这 4 个字节存放偏移,指向实际数据。读取时必须先按字节序把 value_or_offset 解释为数值或偏移,再走对应分支,否则会读到错误内容。
常用标签
| 标签 | 名称 | 含义 |
|---|---|---|
0x010E | ImageDescription | 图像描述文本 |
0x0112 | Orientation | 显示方向,取值 1–8 |
0x011A | XResolution | 水平分辨率 |
0x011B | YResolution | 垂直分辨率 |
0x0128 | ResolutionUnit | 分辨率单位 |
0x0132 | DateTime | 修改时间 |
0x8769 | ExifIFDPointer | 指向 EXIF 子 IFD |
0x8825 | GPSInfoIFDPointer | 指向 GPS 子 IFD |
子 IFD:EXIF 与 GPS
主 IFD 中出现 0x8769 或 0x8825 时,其值是一个偏移,指向另外两个 IFD。它们的结构与主 IFD 相同,只是标签号属于不同命名空间。GPS 子 IFD 的标签集中在 0x0000–0x001F,记录纬度、经度、海拔、时间戳等。
经纬度以三个 RATIONAL 表示度、分、秒,并配合 GPSLatitudeRef、GPSLongitudeRef(值为 N/S 或 E/W)确定符号。解析时的一个常见问题是精度:秒值可能带有多位小数,若用单精度浮点参与后续计算,位数损失会导致坐标偏移。
Orientation 与显示方向
Orientation 描述存储的像素排列与正确观看方向之间的旋转或镜像关系,取值为 1 到 8:
| 值 | 含义 |
|---|---|
| 1 | 不变换 |
| 2 | 水平镜像 |
| 3 | 旋转 180° |
| 4 | 垂直镜像 |
| 5 | 水平镜像后顺时针旋转 270°(转置) |
| 6 | 顺时针旋转 90° |
| 7 | 水平镜像后顺时针旋转 90° |
| 8 | 顺时针旋转 270° |
多数解码器按存储顺序输出像素,不会自动应用该标签,方向由显示层负责。若显示层忽略它,竖拍照片就会横置。提供自动旋转选项的实现,通常是在解码后按像素重排,或把对应的旋转矩阵并入后续的绘制变换;后者不改动像素数据,代价是若干位置需要感知当前变换。
元数据与隐私
EXIF 可能包含设备序列号、镜头信息与 GPS 坐标。文件被上传或转发时,这些字段会一并发布,而拍摄者往往并不知情。剥离方式主要有两类:删除承载元数据的整个标记段,或逐个改写选定标签。
前者实现简单,但会连带移除 Orientation,因此需要在剥离前把方向应用到像素上,否则照片会变为横置。后者保留原有结构,只清空或替换敏感字段,代价是要完整解析并重写 IFD 链,还要处理偏移变化带来的连锁修改。
小结
解析 EXIF 的核心是把一段 TIFF 结构读对:先确定字节序,再以 TIFF 头为基准解释偏移,然后区分内联值与偏移值,随后处理子 IFD 与标签语义。存放位置随容器而变,数据模型却始终是同一套 IFD 结构,因此一段解析代码可以在 JPEG、TIFF 与 WebP 之间复用。