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