Commit | Line | Data |
---|---|---|
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 |
17 | import six |
18 | ||
bfb99d65 | 19 | from exifread import process_file |
5e262d8b | 20 | from exifread.utils import Ratio |
657a4637 | 21 | |
a180ca26 | 22 | from mediagoblin.processing import BadMediaFail |
a789b713 | 23 | from mediagoblin.tools.translate import pass_to_ugettext as _ |
a180ca26 | 24 | |
a180ca26 JW |
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', | |
37 | ] | |
38 | ||
c72d661b | 39 | |
0f8221dc B |
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 | |
46 | ||
c72d661b | 47 | |
a180ca26 JW |
48 | def 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 |
75 | def 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 |
86 | def 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 | 102 | def _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 |
132 | def _ratio_to_list(ratio): |
133 | return [ratio.num, ratio.den] | |
134 | ||
c72d661b | 135 | |
a180ca26 | 136 | def 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 | |
141 | def 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 |