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 | ||
657a4637 E |
17 | try: |
18 | from EXIF import process_file, Ratio | |
19 | except ImportError: | |
20 | from mediagoblin.tools.extlib.EXIF import process_file, Ratio | |
21 | ||
a180ca26 JW |
22 | from mediagoblin.processing import BadMediaFail |
23 | from mediagoblin.tools.translate import pass_to_ugettext as _ | |
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: | |
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 |
72 | def 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 |
83 | def 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 | 99 | def _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 |
134 | def _ratio_to_list(ratio): |
135 | return [ratio.num, ratio.den] | |
136 | ||
c72d661b | 137 | |
a180ca26 JW |
138 | def 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 | |
147 | def 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 |