#5314 Prevent ZeroDivisionError in exif.py
[larjonas-mediagoblin.git] / mediagoblin / tools / exif.py
blobfafd987d167680e267b893ab663edd7aa75f23d8
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
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.
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.
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/>.
17 import six
19 from exifread import process_file
20 from exifread.utils import Ratio
22 from mediagoblin.processing import BadMediaFail
23 from mediagoblin.tools.translate import pass_to_ugettext as _
25 # A list of tags that should be stored for faster access
26 USEFUL_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',
40 def 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
48 def exif_fix_image_orientation(im, exif_tags):
49 """
50 Translate any EXIF orientation to raw orientation
52 Cons:
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
58 Pros:
59 - Prevents neck pain
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]
68 if orientation in rotation_map:
69 im = im.rotate(
70 rotation_map[orientation])
72 return im
75 def extract_exif(filename):
76 """
77 Returns EXIF tags found in file at ``filename``
78 """
79 try:
80 with open(filename, 'rb') as image:
81 return process_file(image, details=False)
82 except IOError:
83 raise BadMediaFail(_('Could not read the image file.'))
86 def clean_exif(exif):
87 '''
88 Clean the result from anything the database cannot handle
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']
98 return dict((key, _ifd_tag_to_dict(value)) for (key, value)
99 in six.iteritems(exif) if key not in disabled_tags)
102 def _ifd_tag_to_dict(tag):
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.
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}
115 if isinstance(tag.printable, six.binary_type):
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')
119 if type(tag.values) == list:
120 data['values'] = [_ratio_to_list(val) if isinstance(val, Ratio) else val
121 for val in tag.values]
122 else:
123 if isinstance(tag.values, six.binary_type):
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
129 return data
132 def _ratio_to_list(ratio):
133 return [ratio.num, ratio.den]
136 def get_useful(tags):
137 from collections import OrderedDict
138 return OrderedDict((key, tag) for (key, tag) in six.iteritems(tags))
141 def get_gps_data(tags):
143 Processes EXIF data returned by EXIF.py
145 def safe_gps_ratio_divide(ratio):
146 if ratio.den == 0:
147 return 0.0
148 return float(ratio.num) / float(ratio.den)
150 gps_data = {}
152 if not 'Image GPSInfo' in tags:
153 return gps_data
155 try:
156 dms_data = {
157 'latitude': tags['GPS GPSLatitude'],
158 'longitude': tags['GPS GPSLongitude']}
160 for key, dat in six.iteritems(dms_data):
161 gps_data[key] = (
162 lambda v:
163 safe_gps_ratio_divide(v[0]) \
164 + (safe_gps_ratio_divide(v[1]) / 60) \
165 + (safe_gps_ratio_divide(v[2]) / (60 * 60))
166 )(dat.values)
168 if tags['GPS GPSLatitudeRef'].values == 'S':
169 gps_data['latitude'] /= -1
171 if tags['GPS GPSLongitudeRef'].values == 'W':
172 gps_data['longitude'] /= -1
174 except KeyError:
175 pass
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
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
193 return gps_data