EXIF, ICC profiles and image info

Pillow · Advanced · 12 min

Metadata carries capture dates, GPS location and colour profiles: you need it for photo apps and must strip it for privacy.

Pillow exposes metadata in two places.

im.info is a dict of format-specific extras found while opening: dpi, icc_profile (bytes), transparency, duration for animations, PNG text chunks, and the raw exif bytes.

im.getexif() returns an `Image.Exif` object, a mapping from numeric tag ids to values. ExifTags.Base gives readable names: exif[ExifTags.Base.Model], ExifTags.Base.Orientation (0x0112), DateTime. Camera details and GPS are in sub-directories (IFDs): exif.get_ifd(ExifTags.IFD.Exif) for exposure settings and exif.get_ifd(ExifTags.IFD.GPSInfo) for location.

To write, modify the Exif object and pass it to save(..., exif=exif). Supported for JPEG, PNG, WebP, TIFF and others.

Pillow mostly drops metadata when saving unless you pass it: a re-saved JPEG loses EXIF and its ICC profile by default. That is good for privacy (removes GPS coordinates) but changes colours for wide-gamut photos if the ICC profile is lost. A common policy for user uploads:

  • apply ImageOps.exif_transpose first,
  • keep icc_profile=im.info.get("icc_profile") so colours stay right,
  • omit exif= to strip location and device details.

PNG text chunks can be written with PngImagePlugin.PngInfo. XMP data appears in im.info or via im.getxmp() when defusedxml is installed.

Example

from PIL import ExifTags, Image

im = Image.new("RGB", (64, 48), "green")
exif = Image.Exif()
exif[ExifTags.Base.Make] = "ACME"
exif[ExifTags.Base.Model] = "Cam 1"
exif[ExifTags.Base.DateTime] = "2026:05:01 12:00:00"
im.save("tagged.jpg", exif=exif, dpi=(300, 300))

src = Image.open("tagged.jpg")
tags = src.getexif()
print(tags[ExifTags.Base.Model], tags.get(ExifTags.Base.Make))   # Cam 1 ACME
names = {ExifTags.TAGS.get(k, k): v for k, v in tags.items()}
print(sorted(names))                       # ['DateTime', 'Make', 'Model']
print(src.info.get("dpi"))                 # (300, 300)

src.save("clean.jpg")                      # no exif= : metadata dropped
print(len(Image.open("clean.jpg").getexif()))   # 0

keep = {"icc_profile": src.info.get("icc_profile")}   # keep colours, drop EXIF
src.save("web.jpg", **{k: v for k, v in keep.items() if v})

Remember

  • im.info holds format extras (dpi, icc_profile, duration...)
  • im.getexif() maps tag ids to values; use ExifTags.Base for names
  • Pass exif= and icc_profile= to save to keep them
  • Re-saving without them strips metadata, useful for privacy

Watch out for

  • Stripping the ICC profile along with EXIF, so wide-gamut photos look washed out.

Check yourself

  1. You re-save a user's JPEG with `im.save('out.jpg')`. What happens to its GPS EXIF data?
  2. Where do you find a camera's exposure time in Pillow?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: pillow.readthedocs.io/en/stable/reference/ExifTags.html