Passed
Pull Request — master (#320)
by Jaisen
02:32
created

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

Complexity

Conditions 6

Size

Total Lines 37
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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