228 lines
7.3 KiB
Python
228 lines
7.3 KiB
Python
|
import datetime
|
||
|
import struct # Only to catch struct.error due to error in PIL / Pillow.
|
||
|
from PIL import Image
|
||
|
from PIL.ExifTags import TAGS, GPSTAGS
|
||
|
|
||
|
# Original: https://gist.github.com/erans/983821
|
||
|
# License: MIT
|
||
|
# Credits: https://gist.github.com/erans
|
||
|
|
||
|
|
||
|
class ExifException(Exception):
|
||
|
def __init__(self, message):
|
||
|
self._message = message
|
||
|
|
||
|
def __str__(self):
|
||
|
return self._message
|
||
|
|
||
|
|
||
|
class PILExifReader:
|
||
|
def __init__(self, filepath):
|
||
|
self._filepath = filepath
|
||
|
image = Image.open(filepath)
|
||
|
self._exif = self.get_exif_data(image)
|
||
|
image.close()
|
||
|
|
||
|
def get_exif_data(self, image):
|
||
|
"""Returns a dictionary from the exif data of an PIL Image
|
||
|
item. Also converts the GPS Tags"""
|
||
|
exif_data = {}
|
||
|
try:
|
||
|
info = image._getexif()
|
||
|
except OverflowError, e:
|
||
|
if e.message == "cannot fit 'long' into an index-sized integer":
|
||
|
# Error in PIL when exif data is corrupt.
|
||
|
return None
|
||
|
else:
|
||
|
raise e
|
||
|
except struct.error as e:
|
||
|
if e.message == "unpack requires a string argument of length 2":
|
||
|
# Error in PIL when exif data is corrupt.
|
||
|
return None
|
||
|
else:
|
||
|
raise e
|
||
|
if info:
|
||
|
for tag, value in info.items():
|
||
|
decoded = TAGS.get(tag, tag)
|
||
|
if decoded == "GPSInfo":
|
||
|
gps_data = {}
|
||
|
for t in value:
|
||
|
sub_decoded = GPSTAGS.get(t, t)
|
||
|
gps_data[sub_decoded] = value[t]
|
||
|
exif_data[decoded] = gps_data
|
||
|
else:
|
||
|
exif_data[decoded] = value
|
||
|
return exif_data
|
||
|
|
||
|
def read_capture_time(self):
|
||
|
time_tag = "DateTimeOriginal"
|
||
|
|
||
|
# read and format capture time
|
||
|
if self._exif == None:
|
||
|
print "Exif is none."
|
||
|
if time_tag in self._exif:
|
||
|
capture_time = self._exif[time_tag]
|
||
|
capture_time = capture_time.replace(" ","_")
|
||
|
capture_time = capture_time.replace(":","_")
|
||
|
else:
|
||
|
print "No time tag in "+self._filepath
|
||
|
capture_time = 0
|
||
|
|
||
|
# return as datetime object
|
||
|
return datetime.datetime.strptime(capture_time, '%Y_%m_%d_%H_%M_%S')
|
||
|
|
||
|
def _get_if_exist(self, data, key):
|
||
|
if key in data:
|
||
|
return data[key]
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
def _convert_to_degress(self, value):
|
||
|
"""Helper function to convert the GPS coordinates stored in
|
||
|
the EXIF to degrees in float format."""
|
||
|
d0 = value[0][0]
|
||
|
d1 = value[0][1]
|
||
|
d = float(d0) / float(d1)
|
||
|
|
||
|
m0 = value[1][0]
|
||
|
m1 = value[1][1]
|
||
|
m = float(m0) / float(m1)
|
||
|
|
||
|
s0 = value[2][0]
|
||
|
s1 = value[2][1]
|
||
|
s = float(s0) / float(s1)
|
||
|
|
||
|
return d + (m / 60.0) + (s / 3600.0)
|
||
|
|
||
|
def get_lat_lon(self):
|
||
|
"""Returns the latitude and longitude, if available, from the
|
||
|
provided exif_data (obtained through get_exif_data above)."""
|
||
|
lat = None
|
||
|
lon = None
|
||
|
|
||
|
gps_info = self.get_gps_info()
|
||
|
if gps_info is None:
|
||
|
return None
|
||
|
|
||
|
gps_latitude = self._get_if_exist(gps_info, "GPSLatitude")
|
||
|
gps_latitude_ref = self._get_if_exist(gps_info, 'GPSLatitudeRef')
|
||
|
gps_longitude = self._get_if_exist(gps_info, 'GPSLongitude')
|
||
|
gps_longitude_ref = self._get_if_exist(gps_info, 'GPSLongitudeRef')
|
||
|
|
||
|
if (gps_latitude and gps_latitude_ref
|
||
|
and gps_longitude and gps_longitude_ref):
|
||
|
lat = self._convert_to_degress(gps_latitude)
|
||
|
if gps_latitude_ref != "N":
|
||
|
lat = 0 - lat
|
||
|
|
||
|
lon = self._convert_to_degress(gps_longitude)
|
||
|
if gps_longitude_ref != "E":
|
||
|
lon = 0 - lon
|
||
|
|
||
|
if isinstance(lat, float) and isinstance(lon, float):
|
||
|
return lat, lon
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
def calc_tuple(self, tup):
|
||
|
if tup is None or len(tup) != 2 or tup[1] == 0:
|
||
|
return None
|
||
|
return int(tup[0]) / int(tup[1])
|
||
|
|
||
|
def get_gps_info(self):
|
||
|
if self._exif is None or not "GPSInfo" in self._exif:
|
||
|
return None
|
||
|
else:
|
||
|
return self._exif["GPSInfo"]
|
||
|
|
||
|
def get_rotation(self):
|
||
|
"""Returns the direction of the GPS receiver in degrees."""
|
||
|
gps_info = self.get_gps_info()
|
||
|
if gps_info is None:
|
||
|
return None
|
||
|
|
||
|
for tag in ('GPSImgDirection', 'GPSTrack'):
|
||
|
gps_direction = self._get_if_exist(gps_info, tag)
|
||
|
direction = self.calc_tuple(gps_direction)
|
||
|
if direction == None:
|
||
|
continue
|
||
|
else:
|
||
|
return direction
|
||
|
return None
|
||
|
|
||
|
def get_speed(self):
|
||
|
"""Returns the GPS speed in km/h or None if it does not exists."""
|
||
|
gps_info = self.get_gps_info()
|
||
|
if gps_info is None:
|
||
|
return None
|
||
|
|
||
|
if not "GPSSpeed" in gps_info or not "GPSSpeedRef" in gps_info:
|
||
|
return None
|
||
|
speed_frac = gps_info["GPSSpeed"]
|
||
|
speed_ref = gps_info["GPSSpeedRef"]
|
||
|
|
||
|
speed = self.calc_tuple(speed_frac)
|
||
|
if speed is None or speed_ref is None:
|
||
|
return None
|
||
|
|
||
|
speed_ref = speed_ref.lower()
|
||
|
if speed_ref == "k":
|
||
|
pass # km/h - we are happy.
|
||
|
elif speed_ref == "m":
|
||
|
#Miles pr. hour => km/h
|
||
|
speed *= 1.609344
|
||
|
elif speed_ref == "n":
|
||
|
# Knots => km/h
|
||
|
speed *= 1.852
|
||
|
else:
|
||
|
print "Warning: Unknown format for GPS speed '%s' in '%s'." % (
|
||
|
speed_ref, self._filepath)
|
||
|
print "Please file a bug and attache the image."
|
||
|
return None
|
||
|
return speed
|
||
|
|
||
|
def is_ok_num(self, val, minVal, maxVal):
|
||
|
try:
|
||
|
num = int(val)
|
||
|
except ValueError:
|
||
|
return False
|
||
|
if num < minVal or num > maxVal:
|
||
|
return False
|
||
|
return True
|
||
|
|
||
|
def get_time(self):
|
||
|
# Example data
|
||
|
# GPSTimeStamp': ((9, 1), (14, 1), (9000, 1000))
|
||
|
# 'GPSDateStamp': u'2015:05:17'
|
||
|
gps_info = self.get_gps_info()
|
||
|
if gps_info is None:
|
||
|
return None
|
||
|
|
||
|
if not 'GPSTimeStamp' in gps_info or not 'GPSDateStamp' in gps_info:
|
||
|
return None
|
||
|
timestamp = gps_info['GPSTimeStamp']
|
||
|
datestamp = gps_info['GPSDateStamp']
|
||
|
|
||
|
if len(timestamp) != 3:
|
||
|
raise ExifException("Timestamp does not have length 3: %s" %
|
||
|
len(timestamp))
|
||
|
(timeH, timeM, timeS) = timestamp
|
||
|
h = self.calc_tuple(timeH)
|
||
|
m = self.calc_tuple(timeM)
|
||
|
s = self.calc_tuple(timeS)
|
||
|
if None in (h, m, s):
|
||
|
raise ExifException(
|
||
|
"Hour, minute or second is not valid: '%s':'%s':'%s'." %
|
||
|
(timeH, timeM, timeS))
|
||
|
|
||
|
if datestamp.count(':') != 2:
|
||
|
raise ExifException("Datestamp does not contain 2 colons: '%s'" %
|
||
|
datestamp)
|
||
|
(y, mon, d) = [int(str) for str in datestamp.split(':')]
|
||
|
if not self.is_ok_num(y, 1970, 2100) or not self.is_ok_num(
|
||
|
mon, 1, 12) or not self.is_ok_num(d, 1, 31):
|
||
|
raise ExifException(
|
||
|
"Date parsed from the following is not OK: '%s'" % datestamp)
|
||
|
|
||
|
return datetime.datetime(y, mon, d, h, m, s)
|