|
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
|
|
|
|