Passed
Pull Request — master (#326)
by Jaisen
01:58
created

elodie.media.photo.Photo.is_valid()   B

Complexity

Conditions 6

Size

Total Lines 39
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 14
nop 1
dl 0
loc 39
rs 8.6666
c 0
b 0
f 0
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
            if(imghdr.what(source) is None):
107
                # gh-4 This checks if the source file is an image.
108
                # It doesn't validate against the list of supported types.
109
                # We check with imghdr and pillow.
110
                if(self.pillow is not None):
111
                    # imghdr won't detect all variants of images (https://bugs.python.org/issue28591)
112
                    # see https://github.com/jmathai/elodie/issues/281
113
                    # before giving up, we use `pillow` imaging library to detect file type
114
                    #
115
                    # It is important to note that the library doesn't decode or load the
116
                    # raster data unless it really has to. When you open a file,
117
                    # the file header is read to determine the file format and extract
118
                    # things like mode, size, and other properties required to decode the file,
119
                    # but the rest of the file is not processed until later.
120
                    try:
121
                        im = self.pillow.open(source)
122
                    except IOError:
123
                        return False
124
125
                    if(im.format is None):
126
                        return False
127
                else:
128
                    return False
129
        
130
        return extension in self.extensions
131