Change complimentary_task to complementary_tas
[mediagoblin.git] / mediagoblin / tools / exif.py
CommitLineData
a180ca26 1# GNU MediaGoblin -- federated, autonomous media hosting
cf29e8a8 2# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
a180ca26
JW
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU Affero General Public License as published by
6# the Free Software Foundation, either version 3 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
386c9c7c
BP
17import six
18
bfb99d65 19from exifread import process_file
5e262d8b 20from exifread.utils import Ratio
657a4637 21
a180ca26 22from mediagoblin.processing import BadMediaFail
a789b713 23from mediagoblin.tools.translate import pass_to_ugettext as _
a180ca26 24
a180ca26
JW
25# A list of tags that should be stored for faster access
26USEFUL_TAGS = [
27 'Image Make',
28 'Image Model',
29 'EXIF FNumber',
30 'EXIF Flash',
31 'EXIF FocalLength',
32 'EXIF ExposureTime',
33 'EXIF ApertureValue',
34 'EXIF ExposureMode',
35 'EXIF ISOSpeedRatings',
36 'EXIF UserComment',
37 ]
38
c72d661b 39
0f8221dc
B
40def exif_image_needs_rotation(exif_tags):
41 """
42 Returns True if EXIF orientation requires rotation
43 """
44 return 'Image Orientation' in exif_tags \
45 and exif_tags['Image Orientation'].values[0] != 1
46
c72d661b 47
a180ca26
JW
48def exif_fix_image_orientation(im, exif_tags):
49 """
50 Translate any EXIF orientation to raw orientation
51
52 Cons:
8dad2978
CAW
53 - Well, it changes the image, which means we'll recompress
54 it... not a problem if scaling it down already anyway. We might
55 lose some quality in recompressing if it's at the same-size
56 though
a180ca26
JW
57
58 Pros:
c72d661b 59 - Prevents neck pain
a180ca26
JW
60 """
61 # Rotate image
62 if 'Image Orientation' in exif_tags:
63 rotation_map = {
64 3: 180,
65 6: 270,
66 8: 90}
67 orientation = exif_tags['Image Orientation'].values[0]
dd51c039 68 if orientation in rotation_map:
a180ca26
JW
69 im = im.rotate(
70 rotation_map[orientation])
71
72 return im
73
c72d661b 74
a180ca26
JW
75def extract_exif(filename):
76 """
77 Returns EXIF tags found in file at ``filename``
78 """
a180ca26 79 try:
d9aced73 80 with open(filename, 'rb') as image:
9aff782b 81 return process_file(image, details=False)
a180ca26
JW
82 except IOError:
83 raise BadMediaFail(_('Could not read the image file.'))
84
c72d661b 85
a180ca26
JW
86def clean_exif(exif):
87 '''
63bd7c04 88 Clean the result from anything the database cannot handle
a180ca26
JW
89 '''
90 # Discard any JPEG thumbnail, for database compatibility
91 # and that I cannot see a case when we would use it.
92 # It takes up some space too.
93 disabled_tags = [
94 'Thumbnail JPEGInterchangeFormatLength',
95 'JPEGThumbnail',
96 'Thumbnail JPEGInterchangeFormat']
97
5e746bfd 98 return dict((key, _ifd_tag_to_dict(value)) for (key, value)
386c9c7c 99 in six.iteritems(exif) if key not in disabled_tags)
a180ca26 100
c72d661b 101
a180ca26 102def _ifd_tag_to_dict(tag):
8588505c
JW
103 '''
104 Takes an IFD tag object from the EXIF library and converts it to a dict
105 that can be stored as JSON in the database.
106 '''
a180ca26
JW
107 data = {
108 'printable': tag.printable,
109 'tag': tag.tag,
110 'field_type': tag.field_type,
111 'field_offset': tag.field_offset,
112 'field_length': tag.field_length,
113 'values': None}
8588505c 114
9459fa3c 115 if isinstance(tag.printable, six.binary_type):
8588505c
JW
116 # Force it to be decoded as UTF-8 so that it'll fit into the DB
117 data['printable'] = tag.printable.decode('utf8', 'replace')
118
a180ca26 119 if type(tag.values) == list:
6dc508d2
AVS
120 data['values'] = [_ratio_to_list(val) if isinstance(val, Ratio) else val
121 for val in tag.values]
a180ca26 122 else:
9459fa3c 123 if isinstance(tag.values, six.binary_type):
8588505c
JW
124 # Force UTF-8, so that it fits into the DB
125 data['values'] = tag.values.decode('utf8', 'replace')
126 else:
127 data['values'] = tag.values
a180ca26
JW
128
129 return data
130
c72d661b 131
a180ca26
JW
132def _ratio_to_list(ratio):
133 return [ratio.num, ratio.den]
134
c72d661b 135
a180ca26 136def get_useful(tags):
9459fa3c
BP
137 from collections import OrderedDict
138 return OrderedDict((key, tag) for (key, tag) in six.iteritems(tags))
c72d661b 139
a180ca26
JW
140
141def get_gps_data(tags):
142 """
143 Processes EXIF data returned by EXIF.py
144 """
fd4ddeb1
CAW
145 def safe_gps_ratio_divide(ratio):
146 if ratio.den == 0:
147 return 0.0
148 return float(ratio.num) / float(ratio.den)
149
a180ca26
JW
150 gps_data = {}
151
63bd7c04
JW
152 if not 'Image GPSInfo' in tags:
153 return gps_data
154
a180ca26
JW
155 try:
156 dms_data = {
157 'latitude': tags['GPS GPSLatitude'],
158 'longitude': tags['GPS GPSLongitude']}
159
386c9c7c 160 for key, dat in six.iteritems(dms_data):
a180ca26
JW
161 gps_data[key] = (
162 lambda v:
fd4ddeb1
CAW
163 safe_gps_ratio_divide(v[0]) \
164 + (safe_gps_ratio_divide(v[1]) / 60) \
165 + (safe_gps_ratio_divide(v[2]) / (60 * 60))
a180ca26 166 )(dat.values)
c72d661b 167
bc875dc7 168 if tags['GPS GPSLatitudeRef'].values == 'S':
169 gps_data['latitude'] /= -1
170
c72d661b
JW
171 if tags['GPS GPSLongitudeRef'].values == 'W':
172 gps_data['longitude'] /= -1
173
a180ca26
JW
174 except KeyError:
175 pass
176
177 try:
178 gps_data['direction'] = (
179 lambda d:
180 float(d.num) / float(d.den)
181 )(tags['GPS GPSImgDirection'].values[0])
182 except KeyError:
183 pass
184
185 try:
186 gps_data['altitude'] = (
187 lambda a:
188 float(a.num) / float(a.den)
189 )(tags['GPS GPSAltitude'].values[0])
190 except KeyError:
191 pass
192
193 return gps_data