Welcome, guest | Sign In | My Account | Store | Cart

Iterates through a directory, reading the EXIF data from each jpg/jpeg file. Parses the date/time from EXIF data and:

  1. if it differs from file modification date/time then changes file date/time
  2. moves file to YYYY/YYYY_MM_DD directory
Python, 170 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#!/usr/bin/env python

__version__ = '$Id: exif_redater.py 2294 2014-12-11 10:04:54Z mn $'

USAGE = "exif_redater.py\n\tredate .jpg/.jpeg files according to EXIF data"

"""
Iterates through a directory, reading the EXIF data from each jpg/jpeg file.
Parses the date/time from EXIF data and:
1. if it differs from file modification date/time then changes file date/time
2. moves file to YYYY/YYYY_MM_DD directory

author: Michal Niklas
"""

import os
import os.path
import shutil
import sys
import time
import traceback

try:
	from PIL import Image
except:
	pass

try:
	import exifread
except:
	pass


ALL_CNT = 0
CHANGED_CNT = 0
DEBUG = 0

# which file should be checked
EXTENSIONS = ('.jpg', '.jpeg')

# what tags use to redate file (use first found)
DT_TAGS = ["Image DateTime", "EXIF DateTimeOriginal", "DateTime"]


def log_error(s):
	sys.stderr.write('%s\n' % (s))


def show_fdt(fdt):
	"""human readable format of file modification datetime"""
	return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(fdt))


def exif_info2time(ts):
	"""changes EXIF date ('2005:10:20 23:22:28') to number of seconds since 1970-01-01"""
	tpl = time.strptime(ts + 'UTC', '%Y:%m:%d %H:%M:%S%Z')
	return time.mktime(tpl)


MAX_DIFF = 130.0


def get_exif_date_exif(jpegfn):
	"""return EXIF datetime using exifread (formerly EXIF)"""
	dt_value = None
	f = open(jpegfn, 'rb')
	try:
		tags = exifread.process_file(f)
		if DEBUG:
			print('tags cnt: %d' % len(tags))
			print('\n'.join(tags))
		for dt_tag in DT_TAGS:
			try:
				dt_value = '%s' % tags[dt_tag]
				if DEBUG:
					print('%s: %s' % (dt_tag, dt_value))
				break
			except:
				continue
		if dt_value:
			exif_time = exif_info2time(dt_value)
			return exif_time
	finally:
		f.close()
	return None


def get_exif_date_pil(jpegfn):
	"""return EXIF datetime using PIL"""
	im = Image.open(jpegfn)
	if hasattr(im, '_getexif'):
		exifdata = im._getexif()
		dt_value = exifdata[0x9003]
		exif_time = exif_info2time(dt_value)
		return exif_time
	return None


def redate_by_exif(fn):
	"""reads EXIF from jpg/jpeg and if file datetime differs from EXIF changes file date"""
	global ALL_CNT, CHANGED_CNT
	ALL_CNT += 1
	exif_time = None
	s = os.stat(fn)
	file_time = s[8]
	if DEBUG:
		print(fn)
	try:
		exif_time = get_exif_date_pil(fn)
	except:
		s1 = traceback.format_exc()
		try:
			exif_time = get_exif_date_exif(fn)
		except:
			s2 = traceback.format_exc()
			print('Something is terribly wrong! Both PIL and exifread raises exception')
			print('-' * 20)
			print(s1)
			print('-' * 20)
			print(s2)
			print('-' * 20)
			print('-' * 20)
	if exif_time:
		dir_n = time.strftime("%Y/%Y_%m_%d", time.gmtime(exif_time))
		try:
			os.makedirs(dir_n)
		except:
			pass
		secs_diff = file_time - exif_time
		print("%s    %s -> %s (%s)" % (fn, show_fdt(file_time), show_fdt(exif_time), dir_n))
		if secs_diff > MAX_DIFF or secs_diff < -MAX_DIFF:
			os.utime(fn, (exif_time, exif_time))
			CHANGED_CNT += 1
		shutil.move(fn, dir_n)


def process_dir(_, dir_name, files):
	"""looks for .jpg/.jpeg file in dir"""
	sys.stdout.write('%-70s\r' % (dir_name))
	for fname in files:
		can_change = False
		fnl = fname.lower()
		for ext in EXTENSIONS:
			if fnl.endswith(ext):
				can_change = True
				break
		if can_change:
			fname = os.path.join(dir_name, fname)
			if os.path.isfile(fname):
				redate_by_exif(fname)


def main():
	os.path.walk('.', process_dir, None)
	print('\nChecked: %d\nChanged %d\n' % (ALL_CNT, CHANGED_CNT))


def test():
	redate_by_exif('grunwald.jpg')


if __name__ == '__main__':
	if '--version' in sys.argv:
		print(__version__)
	elif '--help' in sys.argv:
		print(USAGE)
	elif '--test' in sys.argv:
		test()
	else:
		main()

Works with PIL or exifread (formerly EXIF).

Chnaged posixpath to os.path. Thanks Frederic Mantegazza :)

I also extended EXIF tags to look for. My Canon writes "Image DateTime" while hp photosmart 120 does not, but hp writes "EXIF DateTimeOriginal". You should check what tags you camera writes.

6 comments

Frederic Mantegazza 16 years, 1 month ago  # | flag

posixpath import. Is there any reason to directly import the posixpath module, instead of os.path? I think this recipe works on all plateformes...

Michal Niklas (author) 16 years, 1 month ago  # | flag

Changed. Changed posixpath to os.path. Thanks :)

Jean Brouwers 16 years, 1 month ago  # | flag

Use os.path.join(). Instead of

fname = dir_name + '/' + fname

you may want to use

fname = os.path.join(dir_name, fname)

for more platform indepedent.

Michal Niklas (author) 16 years, 1 month ago  # | flag

OK. Changed.

Denis Barmenkov 15 years, 2 months ago  # | flag

Python Image Library (PIL) extracts EXIF date in smaller amount of code:

Sample: http://code.activestate.com/recipes/576646/

Michal Niklas (author) 9 years, 4 months ago  # | flag

I changed it to work with both PIL and exifread (formerly EXIF). Unfortunately some photos made with Olympus camera cannot be read with exifread (see: https://github.com/ianare/exif-py/issues/42 )