ExifTags Module

Note

The source images loaded from the demos in this page is in the real-world, but I can’t redistribute those. I downloaded those to local by google searching with words “Images for jpeg contained gpsinfo sample”, so most of all of those are not free liscenced.

Keeping exif data when saving

doc:https://pillow.readthedocs.io/en/4.1.x/handbook/image-file-formats.html#jpeg, http://effbot.org/imagingbook/format-jpeg.htm

Before you use ExifTags, note that save method doesn’t preserve the exif in case of default.

>>> from io import BytesIO
>>> from PIL import Image
>>>
>>> src = Image.open("01.jpg")
>>> src.info.get('exif', b'')[:20]
'Exif\x00\x00II*\x00\x08\x00\x00\x00\x0c\x00\x0f\x01\x02\x00'
>>>
>>> #
>>> tmp = BytesIO()
>>> src.save(tmp, "JPEG")  # drop exif data
>>> dst = Image.open(tmp)
>>> dst.info.get('exif', b'')[:20]
''
>>>
>>> #
>>> tmp = BytesIO()
>>> src.save(tmp, "JPEG", exif=b"")  # override exif data
>>> dst = Image.open(tmp)
>>> dst.info.get('exif', b'')[:20]
''
>>>
>>> #
>>> tmp = BytesIO()
>>> src.save(tmp, "JPEG", exif=src.info["exif"])  # keep exif data
>>> dst = Image.open(tmp)
>>> dst.info.get('exif', b'')[:20]
'Exif\x00\x00II*\x00\x08\x00\x00\x00\x0c\x00\x0f\x01\x02\x00'
>>>

Exploring Exif Items As Human Readable

Python 3.5.1 (v3.5.1:37a07cee5969, Dec  6 2015, 01:54:25) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import PIL
>>> PIL.VERSION, PIL.PILLOW_VERSION
('1.1.7', '4.1.1')
>>>
>>> from PIL import Image
>>> from PIL.ExifTags import TAGS, GPSTAGS
>>>
>>> # build reverse dicts
>>> _TAGS_r = dict(((v, k) for k, v in TAGS.items()))
>>> _GPSTAGS_r = dict(((v, k) for k, v in GPSTAGS.items()))
>>>
>>> #
>>> img = Image.open("01.jpg")
>>> img.info.keys()
dict_keys(['dpi', 'exif'])
>>> len(img.info['exif'])
13798
>>> img.info['exif'][:30]  # raw exif data
b'Exif\x00\x00II*\x00\x08\x00\x00\x00\x0c\x00\x0f\x01\x02\x00\x06\x00\x00\x00\x9e\x00\x00\x00\x10\x01'
>>> exifd = img._getexif()  # as dict
>>> type(exifd)
<class 'dict'>
>>> exifd.keys()  # numeric keys
dict_keys([36864, 37377, 37378, 36867, 36868, 40965, 37510, 37383, 37385, 37386, 40962, 41486, 271, 272, 37521, 274, 531, 33432, 37380, 282, 283, 33434, 42032, 34850, 40961, 34853, 34855, 296, 41987, 37121, 34866, 33437, 34864, 42033, 306, 42036, 42037, 42034, 315, 41985, 40960, 41990, 41487, 40963, 37520, 41986, 34665, 37522, 41488, 37500])
>>> #
>>> # 1. ignore "MakerNote" and "UserComment" because these can be too long
>>> # 2. ignore unknwon tag
>>> keys = list(exifd.keys())
>>> keys.remove(_TAGS_r["MakerNote"])
>>> keys.remove(_TAGS_r["UserComment"])
>>> keys = [k for k in keys if k in TAGS]
>>> # symbolic name of keys
>>> print("\n".join([TAGS[k] for k in keys]))
ExifVersion
ShutterSpeedValue
ApertureValue
DateTimeOriginal
DateTimeDigitized
ExifInteroperabilityOffset
MeteringMode
Flash
FocalLength
ExifImageWidth
FocalPlaneXResolution
Make
Model
SubsecTimeOriginal
Orientation
YCbCrPositioning
Copyright
ExposureBiasValue
XResolution
YResolution
ExposureTime
CameraOwnerName
ExposureProgram
ColorSpace
GPSInfo
ISOSpeedRatings
ResolutionUnit
WhiteBalance
ComponentsConfiguration
FNumber
BodySerialNumber
DateTime
LensModel
LensSerialNumber
LensSpecification
Artist
CustomRendered
FlashPixVersion
SceneCaptureType
FocalPlaneYResolution
ExifImageHeight
SubsecTime
ExposureMode
ExifOffset
SubsecTimeDigitized
FocalPlaneResolutionUnit
>>> # each values
>>> print("\n".join([str((TAGS[k], exifd[k])) for k in keys]))
('ExifVersion', b'0230')
('ShutterSpeedValue', (417792, 65536))
--- snip ---
('ColorSpace', 1)
('GPSInfo', {0: b'\x02\x03\x00\x00', 1: 'S', 2: ((20, 1), (150146, 10000), (0, 1)), 3: 'E', 4: ((44, 1), (251558, 10000), (0, 1)), 5: b'\x00', 6: (235, 10), 7: ((14, 1), (31, 1), (59000, 1000)), 8: '12', 9: 'A', 10: '3', 11: (14, 10), 29: '2012:08:13'})
('ISOSpeedRatings', 100)
--- snip ---
>>> gpsinfo = exifd[_TAGS_r["GPSInfo"]]
>>> gpsinfo
{0: b'\x02\x03\x00\x00', 1: 'S', 2: ((20, 1), (150146, 10000), (0, 1)), 3: 'E', 4: ((44, 1), (251558, 10000), (0, 1)), 5: b'\x00', 6: (235, 10), 7: ((14, 1), (31, 1), (59000, 1000)), 8: '12', 9: 'A', 10: '3', 11: (14, 10), 29: '2012:08:13'}
>>> print("\n".join([GPSTAGS[k] for k in gpsinfo.keys()]))
GPSVersionID
GPSLatitudeRef
GPSLatitude
GPSLongitudeRef
GPSLongitude
GPSAltitudeRef
GPSAltitude
GPSTimeStamp
GPSSatellites
GPSStatus
GPSMeasureMode
GPSDOP
GPSDateStamp
>>> print("\n".join([str((GPSTAGS[k], gpsinfo[k])) for k in gpsinfo.keys()]))
('GPSVersionID', b'\x02\x03\x00\x00')
('GPSLatitudeRef', 'S')
('GPSLatitude', ((20, 1), (150146, 10000), (0, 1)))
('GPSLongitudeRef', 'E')
('GPSLongitude', ((44, 1), (251558, 10000), (0, 1)))
('GPSAltitudeRef', b'\x00')
('GPSAltitude', (235, 10))
('GPSTimeStamp', ((14, 1), (31, 1), (59000, 1000)))
('GPSSatellites', '12')
('GPSStatus', 'A')
('GPSMeasureMode', '3')
('GPSDOP', (14, 10))
('GPSDateStamp', '2012:08:13')

Building the exif tags by hand, and saving it

It is too heavy to build exif data by hand with only pillow, because exif data is a binary (of course!), its specification might be larger than you think, and pillow’s supports for it are not rich enough.

Actually, the Exif tag structure is borrowed from TIFF files, so you might think TiffImagePlugin.ImageFileDirectory* are useful, but those are insufficient for Exif (in JPEG). Several reasons:

  1. TiffImagePlugin.ImageFileDirectory* doesn’t know about value type information for non-TIFF tags.
  2. TiffImagePlugin.ImageFileDirectory* doesn’t know about a whole Exif structure in JPEG.
  3. TiffImagePlugin.ImageFileDirectory* doesn’t know about substructure needed by GPSInfo.

If your needs are very simple, indeed you can:

>>> from io import BytesIO
>>> from PIL import Image, TiffImagePlugin, TiffTags
>>> from PIL.ExifTags import TAGS
>>> from PIL.TiffImagePlugin import ImageFileDirectory_v2
>>>
>>> _TAGS_r = dict(((v, k) for k, v in TAGS.items()))
>>>
>>> #
>>> jpgimg1 = Image.new("RGB", (64, 64))
>>>
>>> # Image File Directory
>>> ifd = ImageFileDirectory_v2()
>>>
>>> # TiffTags knows "Artist" (0x013b)
>>> TiffTags.lookup(_TAGS_r["Artist"])
TagInfo(value=315, name='Artist', type=2, length=1, enum={})
>>> ifd[_TAGS_r["Artist"]] = u'somebody'
>>> #ifd.tagtype[_TAGS_r['Artist']] = 2  # string, but you don't have to set explicitly.
>>>
>>> # TiffTags doesn't know "LightSource" (0x9208)
>>> TiffTags.lookup(_TAGS_r["LightSource"])
TagInfo(value=37384, name='LightSource', type=None, length=0, enum={})
>>> ifd[_TAGS_r['LightSource']] = 1  # DayLight
>>> ifd.tagtype[_TAGS_r['LightSource']] = 3  # short, you must set.
>>>
>>> ##
>>> out = BytesIO()
>>> ifd.save(out)
48
>>> ## you must add magic number of exif structure
>>> exif = b"Exif\x00\x00" + out.getvalue()
>>>
>>> jpgimg1.save("out.jpg", exif=exif)
>>> jpgimg2 = Image.open("out.jpg")
>>> jpgimg2._getexif()
{37384: 1, 315: 'somebody'}

But your needs should be more complex, and your code will lose maintainability soon.

You can use other third party library, for example piexif:

>>> from PIL import Image
>>> import piexif
>>> zeroth_ifd = {
...     piexif.ImageIFD.Artist: u"someone",
...     piexif.ImageIFD.XResolution: (96, 1),
...     piexif.ImageIFD.YResolution: (96, 1),
...     piexif.ImageIFD.Software: u"piexif"
...     }
>>> exif_ifd = {
...     piexif.ExifIFD.DateTimeOriginal: u"2099:09:29 10:10:10",
...     piexif.ExifIFD.LensMake: u"LensMake",
...     piexif.ExifIFD.Sharpness: 65535,
...     piexif.ExifIFD.LensSpecification: ((1, 1), (1, 1), (1, 1), (1, 1)),
...     }
>>> exif_dict = {"0th": zeroth_ifd, "Exif": exif_ifd}
>>> exif_bytes = piexif.dump(exif_dict)
>>> jpgimg1 = Image.new("RGB", (64, 64))
>>> jpgimg1.save("out.jpg", exif=exif_bytes)
>>> jpgimg2 = Image.open("out.jpg")
>>> jpgimg2._getexif()
{36867: '2099:09:29 10:10:10', 315: 'someone', 34665: 105, 41994: 65535, 305: 'piexif', 42034: ((1, 1), (1, 1), (1, 1), (1, 1)), 42035: 'LensMake', 282: (96, 1), 283: (96, 1)}