Merge remote-tracking branch 'gsoc2016/Subtitle-1'
[mediagoblin.git] / mediagoblin / tools / exif.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
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
17 import six
18
19 from exifread import process_file
20 from exifread.utils import Ratio
21
22 try:
23 from PIL import Image
24 except ImportError:
25 import Image
26
27 from mediagoblin.processing import BadMediaFail
28 from mediagoblin.tools.translate import pass_to_ugettext as _
29
30 # A list of tags that should be stored for faster access
31 USEFUL_TAGS = [
32 'Image Make',
33 'Image Model',
34 'EXIF FNumber',
35 'EXIF Flash',
36 'EXIF FocalLength',
37 'EXIF ExposureTime',
38 'EXIF ApertureValue',
39 'EXIF ExposureMode',
40 'EXIF ISOSpeedRatings',
41 'EXIF UserComment',
42 ]
43
44
45 def exif_image_needs_rotation(exif_tags):
46 """
47 Returns True if EXIF orientation requires rotation
48 """
49 return 'Image Orientation' in exif_tags \
50 and exif_tags['Image Orientation'].values[0] != 1
51
52
53 def exif_fix_image_orientation(im, exif_tags):
54 """
55 Translate any EXIF orientation to raw orientation
56
57 Cons:
58 - Well, it changes the image, which means we'll recompress
59 it... not a problem if scaling it down already anyway. We might
60 lose some quality in recompressing if it's at the same-size
61 though
62
63 Pros:
64 - Prevents neck pain
65 """
66 # Rotate image
67 if 'Image Orientation' in exif_tags:
68 rotation_map = {
69 3: Image.ROTATE_180,
70 6: Image.ROTATE_270,
71 8: Image.ROTATE_90}
72 orientation = exif_tags['Image Orientation'].values[0]
73 if orientation in rotation_map:
74 im = im.transpose(
75 rotation_map[orientation])
76
77 return im
78
79
80 def extract_exif(filename):
81 """
82 Returns EXIF tags found in file at ``filename``
83 """
84 try:
85 with open(filename, 'rb') as image:
86 return process_file(image, details=False)
87 except IOError:
88 raise BadMediaFail(_('Could not read the image file.'))
89
90
91 def clean_exif(exif):
92 '''
93 Clean the result from anything the database cannot handle
94 '''
95 # Discard any JPEG thumbnail, for database compatibility
96 # and that I cannot see a case when we would use it.
97 # It takes up some space too.
98 disabled_tags = [
99 'Thumbnail JPEGInterchangeFormatLength',
100 'JPEGThumbnail',
101 'Thumbnail JPEGInterchangeFormat']
102
103 return dict((key, _ifd_tag_to_dict(value)) for (key, value)
104 in six.iteritems(exif) if key not in disabled_tags)
105
106
107 def _ifd_tag_to_dict(tag):
108 '''
109 Takes an IFD tag object from the EXIF library and converts it to a dict
110 that can be stored as JSON in the database.
111 '''
112 data = {
113 'printable': tag.printable,
114 'tag': tag.tag,
115 'field_type': tag.field_type,
116 'field_offset': tag.field_offset,
117 'field_length': tag.field_length,
118 'values': None}
119
120 if isinstance(tag.printable, six.binary_type):
121 # Force it to be decoded as UTF-8 so that it'll fit into the DB
122 data['printable'] = tag.printable.decode('utf8', 'replace')
123
124 if type(tag.values) == list:
125 data['values'] = [_ratio_to_list(val) if isinstance(val, Ratio) else val
126 for val in tag.values]
127 else:
128 if isinstance(tag.values, six.binary_type):
129 # Force UTF-8, so that it fits into the DB
130 data['values'] = tag.values.decode('utf8', 'replace')
131 else:
132 data['values'] = tag.values
133
134 return data
135
136
137 def _ratio_to_list(ratio):
138 return [ratio.num, ratio.den]
139
140
141 def get_useful(tags):
142 from collections import OrderedDict
143 return OrderedDict((key, tag) for (key, tag) in six.iteritems(tags))
144
145
146 def get_gps_data(tags):
147 """
148 Processes EXIF data returned by EXIF.py
149 """
150 def safe_gps_ratio_divide(ratio):
151 if ratio.den == 0:
152 return 0.0
153 return float(ratio.num) / float(ratio.den)
154
155 gps_data = {}
156
157 if not 'Image GPSInfo' in tags:
158 return gps_data
159
160 try:
161 dms_data = {
162 'latitude': tags['GPS GPSLatitude'],
163 'longitude': tags['GPS GPSLongitude']}
164
165 for key, dat in six.iteritems(dms_data):
166 gps_data[key] = (
167 lambda v:
168 safe_gps_ratio_divide(v[0]) \
169 + (safe_gps_ratio_divide(v[1]) / 60) \
170 + (safe_gps_ratio_divide(v[2]) / (60 * 60))
171 )(dat.values)
172
173 if tags['GPS GPSLatitudeRef'].values == 'S':
174 gps_data['latitude'] /= -1
175
176 if tags['GPS GPSLongitudeRef'].values == 'W':
177 gps_data['longitude'] /= -1
178
179 except KeyError:
180 pass
181
182 try:
183 direction = tags['GPS GPSImgDirection'].values[0]
184 gps_data['direction'] = safe_gps_ratio_divide(direction)
185 except KeyError:
186 pass
187
188 try:
189 altitude = tags['GPS GPSAltitude'].values[0]
190 gps_data['altitude'] = safe_gps_ratio_divide(altitude)
191 except KeyError:
192 pass
193
194 return gps_data