simplified clean_exif
[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
657a4637
E
17try:
18 from EXIF import process_file, Ratio
19except ImportError:
20 from mediagoblin.tools.extlib.EXIF import process_file, Ratio
21
a180ca26
JW
22from mediagoblin.processing import BadMediaFail
23from mediagoblin.tools.translate import pass_to_ugettext as _
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:
64376dc0 53 - REDUCES IMAGE QUALITY by recompressing it
a180ca26
JW
54
55 Pros:
c72d661b 56 - Prevents neck pain
a180ca26
JW
57 """
58 # Rotate image
59 if 'Image Orientation' in exif_tags:
60 rotation_map = {
61 3: 180,
62 6: 270,
63 8: 90}
64 orientation = exif_tags['Image Orientation'].values[0]
dd51c039 65 if orientation in rotation_map:
a180ca26
JW
66 im = im.rotate(
67 rotation_map[orientation])
68
69 return im
70
c72d661b 71
a180ca26
JW
72def extract_exif(filename):
73 """
74 Returns EXIF tags found in file at ``filename``
75 """
a180ca26 76 try:
9aff782b
AVS
77 with file(filename) as image:
78 return process_file(image, details=False)
a180ca26
JW
79 except IOError:
80 raise BadMediaFail(_('Could not read the image file.'))
81
c72d661b 82
a180ca26
JW
83def clean_exif(exif):
84 '''
63bd7c04 85 Clean the result from anything the database cannot handle
a180ca26
JW
86 '''
87 # Discard any JPEG thumbnail, for database compatibility
88 # and that I cannot see a case when we would use it.
89 # It takes up some space too.
90 disabled_tags = [
91 'Thumbnail JPEGInterchangeFormatLength',
92 'JPEGThumbnail',
93 'Thumbnail JPEGInterchangeFormat']
94
5e746bfd
AVS
95 return dict((key, _ifd_tag_to_dict(value)) for (key, value)
96 in exif.iteritems() if key not in disabled_tags)
a180ca26 97
c72d661b 98
a180ca26 99def _ifd_tag_to_dict(tag):
8588505c
JW
100 '''
101 Takes an IFD tag object from the EXIF library and converts it to a dict
102 that can be stored as JSON in the database.
103 '''
a180ca26
JW
104 data = {
105 'printable': tag.printable,
106 'tag': tag.tag,
107 'field_type': tag.field_type,
108 'field_offset': tag.field_offset,
109 'field_length': tag.field_length,
110 'values': None}
8588505c
JW
111
112 if isinstance(tag.printable, str):
113 # Force it to be decoded as UTF-8 so that it'll fit into the DB
114 data['printable'] = tag.printable.decode('utf8', 'replace')
115
a180ca26
JW
116 if type(tag.values) == list:
117 data['values'] = []
118 for val in tag.values:
119 if isinstance(val, Ratio):
120 data['values'].append(
121 _ratio_to_list(val))
122 else:
123 data['values'].append(val)
124 else:
8588505c
JW
125 if isinstance(tag.values, str):
126 # Force UTF-8, so that it fits into the DB
127 data['values'] = tag.values.decode('utf8', 'replace')
128 else:
129 data['values'] = tag.values
a180ca26
JW
130
131 return data
132
c72d661b 133
a180ca26
JW
134def _ratio_to_list(ratio):
135 return [ratio.num, ratio.den]
136
c72d661b 137
a180ca26
JW
138def get_useful(tags):
139 useful = {}
140 for key, tag in tags.items():
141 if key in USEFUL_TAGS:
142 useful[key] = tag
143
144 return useful
c72d661b 145
a180ca26
JW
146
147def get_gps_data(tags):
148 """
149 Processes EXIF data returned by EXIF.py
150 """
a180ca26
JW
151 gps_data = {}
152
63bd7c04
JW
153 if not 'Image GPSInfo' in tags:
154 return gps_data
155
a180ca26
JW
156 try:
157 dms_data = {
158 'latitude': tags['GPS GPSLatitude'],
159 'longitude': tags['GPS GPSLongitude']}
160
161 for key, dat in dms_data.items():
162 gps_data[key] = (
163 lambda v:
164 float(v[0].num) / float(v[0].den) \
c72d661b 165 + (float(v[1].num) / float(v[1].den) / 60) \
a180ca26
JW
166 + (float(v[2].num) / float(v[2].den) / (60 * 60))
167 )(dat.values)
c72d661b 168
bc875dc7 169 if tags['GPS GPSLatitudeRef'].values == 'S':
170 gps_data['latitude'] /= -1
171
c72d661b
JW
172 if tags['GPS GPSLongitudeRef'].values == 'W':
173 gps_data['longitude'] /= -1
174
a180ca26
JW
175 except KeyError:
176 pass
177
178 try:
179 gps_data['direction'] = (
180 lambda d:
181 float(d.num) / float(d.den)
182 )(tags['GPS GPSImgDirection'].values[0])
183 except KeyError:
184 pass
185
186 try:
187 gps_data['altitude'] = (
188 lambda a:
189 float(a.num) / float(a.den)
190 )(tags['GPS GPSAltitude'].values[0])
191 except KeyError:
192 pass
193
194 return gps_data