1
|
|
|
""" |
2
|
|
|
The photo module contains the :class:`Photo` class, which is used to track |
3
|
|
|
image objects (JPG, DNG, etc.). |
4
|
|
|
|
5
|
|
|
.. moduleauthor:: Jaisen Mathai <[email protected]> |
6
|
|
|
""" |
7
|
|
|
from __future__ import print_function |
8
|
|
|
from __future__ import absolute_import |
9
|
|
|
|
10
|
|
|
import imghdr |
11
|
|
|
import os |
12
|
|
|
import re |
13
|
|
|
import time |
14
|
|
|
from datetime import datetime |
15
|
|
|
from re import compile |
16
|
|
|
|
17
|
|
|
from elodie import log |
18
|
|
|
from .media import Media |
19
|
|
|
|
20
|
|
|
|
21
|
|
|
class Photo(Media): |
22
|
|
|
|
23
|
|
|
"""A photo object. |
24
|
|
|
|
25
|
|
|
:param str source: The fully qualified path to the photo file |
26
|
|
|
""" |
27
|
|
|
|
28
|
|
|
__name__ = 'Photo' |
29
|
|
|
|
30
|
|
|
#: Valid extensions for photo files. |
31
|
|
|
extensions = ('arw', 'cr2', 'dng', 'gif', 'heic', 'jpeg', 'jpg', 'nef', 'rw2') |
32
|
|
|
|
33
|
|
|
def __init__(self, source=None): |
34
|
|
|
super(Photo, self).__init__(source) |
35
|
|
|
|
36
|
|
|
# We only want to parse EXIF once so we store it here |
37
|
|
|
self.exif = None |
38
|
|
|
|
39
|
|
|
# Optionally import Pillow - see gh-325 |
40
|
|
|
# https://github.com/jmathai/elodie/issues/325 |
41
|
|
|
self.pillow = None |
42
|
|
|
try: |
43
|
|
|
from PIL import Image |
44
|
|
|
self.pillow = Image |
45
|
|
|
except ImportError: |
46
|
|
|
pass |
47
|
|
|
|
48
|
|
|
def get_date_taken(self): |
49
|
|
|
"""Get the date which the photo was taken. |
50
|
|
|
|
51
|
|
|
The date value returned is defined by the min() of mtime and ctime. |
52
|
|
|
|
53
|
|
|
:returns: time object or None for non-photo files or 0 timestamp |
54
|
|
|
""" |
55
|
|
|
if(not self.is_valid()): |
56
|
|
|
return None |
57
|
|
|
|
58
|
|
|
source = self.source |
59
|
|
|
seconds_since_epoch = min(os.path.getmtime(source), os.path.getctime(source)) # noqa |
60
|
|
|
|
61
|
|
|
exif = self.get_exiftool_attributes() |
62
|
|
|
if not exif: |
63
|
|
|
return seconds_since_epoch |
64
|
|
|
|
65
|
|
|
# We need to parse a string from EXIF into a timestamp. |
66
|
|
|
# EXIF DateTimeOriginal and EXIF DateTime are both stored |
67
|
|
|
# in %Y:%m:%d %H:%M:%S format |
68
|
|
|
# we split on a space and then r':|-' -> convert to int -> .timetuple() |
69
|
|
|
# the conversion in the local timezone |
70
|
|
|
# EXIF DateTime is already stored as a timestamp |
71
|
|
|
# Sourced from https://github.com/photo/frontend/blob/master/src/libraries/models/Photo.php#L500 # noqa |
72
|
|
|
for key in self.exif_map['date_taken']: |
73
|
|
|
try: |
74
|
|
|
if(key in exif): |
75
|
|
|
if(re.match('\d{4}(-|:)\d{2}(-|:)\d{2}', exif[key]) is not None): # noqa |
76
|
|
|
dt, tm = exif[key].split(' ') |
77
|
|
|
dt_list = compile(r'-|:').split(dt) |
78
|
|
|
dt_list = dt_list + compile(r'-|:').split(tm) |
79
|
|
|
dt_list = map(int, dt_list) |
80
|
|
|
time_tuple = datetime(*dt_list).timetuple() |
81
|
|
|
seconds_since_epoch = time.mktime(time_tuple) |
82
|
|
|
break |
83
|
|
|
except BaseException as e: |
84
|
|
|
log.error(e) |
85
|
|
|
pass |
86
|
|
|
|
87
|
|
|
if(seconds_since_epoch == 0): |
88
|
|
|
return None |
89
|
|
|
|
90
|
|
|
return time.gmtime(seconds_since_epoch) |
91
|
|
|
|
92
|
|
|
def is_valid(self): |
93
|
|
|
"""Check the file extension against valid file extensions. |
94
|
|
|
|
95
|
|
|
The list of valid file extensions come from self.extensions. This |
96
|
|
|
also checks whether the file is an image. |
97
|
|
|
|
98
|
|
|
:returns: bool |
99
|
|
|
""" |
100
|
|
|
source = self.source |
101
|
|
|
|
102
|
|
|
# HEIC is not well supported yet so we special case it. |
103
|
|
|
# https://github.com/python-pillow/Pillow/issues/2806 |
104
|
|
|
extension = os.path.splitext(source)[1][1:].lower() |
105
|
|
|
if(extension != 'heic'): |
106
|
|
|
# gh-4 This checks if the source file is an image. |
107
|
|
|
# It doesn't validate against the list of supported types. |
108
|
|
|
# We check with imghdr and pillow. |
109
|
|
|
if(imghdr.what(source) is None): |
110
|
|
|
# Pillow is used as a fallback and if it's not available we trust |
111
|
|
|
# what imghdr returned. |
112
|
|
|
if(self.pillow is None): |
113
|
|
|
return False |
114
|
|
|
else: |
115
|
|
|
# imghdr won't detect all variants of images (https://bugs.python.org/issue28591) |
116
|
|
|
# see https://github.com/jmathai/elodie/issues/281 |
117
|
|
|
# before giving up, we use `pillow` imaging library to detect file type |
118
|
|
|
# |
119
|
|
|
# It is important to note that the library doesn't decode or load the |
120
|
|
|
# raster data unless it really has to. When you open a file, |
121
|
|
|
# the file header is read to determine the file format and extract |
122
|
|
|
# things like mode, size, and other properties required to decode the file, |
123
|
|
|
# but the rest of the file is not processed until later. |
124
|
|
|
try: |
125
|
|
|
im = self.pillow.open(source) |
126
|
|
|
except IOError: |
127
|
|
|
return False |
128
|
|
|
|
129
|
|
|
if(im.format is None): |
130
|
|
|
return False |
131
|
|
|
|
132
|
|
|
return extension in self.extensions |
133
|
|
|
|