Python Pillow 写入 JPG 的 EXIF 后日期错乱,如何修复?
结论
通过 im.getexif() 获取 EXIF,再用 im.save(target, exif=imexif) 保存,这个用法本身没有问题。Pillow 7.0.0 的 EXIF 序列化默认偏移量存在问题,7.2.0 的变更记录中列出了对应修复 #4594。你遇到的日期截断和相邻字段内容串入,与偏移量错误的表现相符,建议先排查这个已知问题。Pillow 变更记录
没有原始 JPG,无法确认文件中的所有异常都由这处缺陷引起。可以先升级到与运行环境兼容的较新 Pillow,再用同一张原图验证。如果暂时不能升级,可以在旧版中显式指定序列化偏移量。
为什么没有修改标签也会出错?
传入 Exif 对象保存时,标签仍会重新序列化。即使标签值没有变化,生成的 EXIF 字节布局也可能改变。
Pillow 7.0.0 的 Exif.tobytes() 默认使用 offset=0,但它生成的数据包含一个 8 字节 TIFF 头。如果没有正确计入这个头部,标签指向字符串数据的位置就会偏移,读回时可能读到前一个字段的尾部。这个解释来自旧版实现,也与后续的默认偏移量修复相符。Pillow 7.0.0 源码
因此,仅把调用改成 exif=imexif.tobytes(),在这个旧版本中仍会使用有问题的默认值。
暂时保留 Pillow 7.0.0 时怎么绕过?
针对默认偏移量问题,可以把原来的保存语句改为:
im.save(target, exif=imexif.tobytes(offset=8))
完整的最小示例:
from PIL import Image
source = "./orig2.jpg"
target = "./modified.jpg"
with Image.open(source) as im:
exif = im.getexif()
# 需要修改 DateTime 时再启用这一行。
# exif[306] = "2003:01:12 03:04:15"
# 针对 Pillow 7.0.0 的默认偏移量问题。
im.save(target, format="JPEG", exif=exif.tobytes(offset=8))
这里的 8 对应 TIFF 头长度。这个方法用于绕过旧版的默认偏移量问题,不能保证修复源文件中已有的损坏,也不应据此手动修改其他标签的偏移量。
完全不修改 EXIF 时,直接保留原始字节
如果只想把原图的 EXIF 带到新 JPG,可以直接传入原始 EXIF 字节,避免重新序列化标签。JPEG 保存参数 exif 支持接收原始 EXIF 数据。Pillow JPEG 保存参数
from PIL import Image
source = "./orig2.jpg"
target = "./copied_exif.jpg"
with Image.open(source) as im:
raw_exif = im.info.get("exif")
if raw_exif is None:
raise ValueError("源文件没有可复制的 EXIF 数据")
im.save(target, format="JPEG", exif=raw_exif)
with Image.open(target) as im:
if im.info.get("exif") != raw_exif:
raise RuntimeError("输出文件的 EXIF 字节与源文件不同")
这段代码保留的是原始 EXIF。如果修改了 im.getexif() 返回的对象,再传入旧的 im.info["exif"],修改就不会写入文件。
升级后如何修改并检查日期?
升级时还需要调整读取代码:从 Pillow 8.2.0 开始,getexif() 不再把 Exif IFD 中的标签合并到顶层。DateTimeOriginal 和 DateTimeDigitized 通常需要通过 get_ifd(0x8769) 读取,不能只凭顶层标签判断它们是否丢失。Pillow 8.2.0 发布说明
下面的示例适用于采用该层级结构的 Pillow,修改 tag 306,并检查另外两个日期是否保持原值:
from PIL import Image
EXIF_IFD = 0x8769
def read_dates(path):
with Image.open(path) as im:
exif = im.getexif()
details = exif.get_ifd(EXIF_IFD)
return {
306: exif.get(306),
36867: details.get(36867),
36868: details.get(36868),
}
source = "./orig2.jpg"
target = "./modified.jpg"
new_datetime = "2003:01:12 03:04:15"
expected = read_dates(source)
expected[306] = new_datetime
with Image.open(source) as im:
exif = im.getexif()
exif[306] = new_datetime
im.save(target, format="JPEG", exif=exif.tobytes())
actual = read_dates(target)
for tag in (306, 36867, 36868):
print(tag, repr(actual[tag]))
if actual != expected:
raise RuntimeError("保存后读回的日期与预期不一致")
如果要修改拍摄时间,需要修改 Exif IFD 中对应的标签;只修改 tag 306 不会自动修改另外两个日期。get_ifd() 返回用于访问子目录的字典。Pillow Exif API
原代码还有哪些注意事项?
- tag 282、283 的警告涉及分辨率字段,仅凭这些警告不能认定它们导致日期错乱。尤其不要直接使用
imexif[r][0]:如果返回值是分子、分母组成的有理数表示,取第一个元素会改变实际数值。 '2066:55:44 33:22:11'的月份、日期和时间均不合法。验证正式写入行为时,应使用有效的YYYY:MM:DD HH:MM:SS值,避免混入额外变量。- 检查日期时,用
repr()输出完整字符串,不要截断内容。若仍有异常,可以使用独立的 EXIF 读取工具交叉检查,区分写入问题与读取问题。 - Pillow 保存 JPG 时会重新编码图像。复制原始 EXIF 字节不代表整个 JPG 文件保持字节一致。
- 粘贴出的代码缺少 Python 缩进;执行前需要恢复函数、条件分支和循环的缩进。这个语法问题与已经观察到的 EXIF 字符串错乱需要分别处理。
备注:内容仅供参考。
