Passed
Push — master ( 50c6e3...87e042 )
by Jaisen
01:57
created

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

Complexity

Conditions 6

Size

Total Lines 41
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 14
nop 1
dl 0
loc 41
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
            # 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