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 | 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: |
9aff782b AVS |
80 | with file(filename) as image: |
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 AVS |
98 | return dict((key, _ifd_tag_to_dict(value)) for (key, value) |
99 | in exif.iteritems() 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 JW |
114 | |
115 | if isinstance(tag.printable, str): | |
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: |
8588505c JW |
123 | if isinstance(tag.values, str): |
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): |
b3566e1d | 137 | return dict((key, tag) for (key, tag) in tags.iteritems()) |
c72d661b | 138 | |
a180ca26 JW |
139 | |
140 | def get_gps_data(tags): | |
141 | """ | |
142 | Processes EXIF data returned by EXIF.py | |
143 | """ | |
a180ca26 JW |
144 | gps_data = {} |
145 | ||
63bd7c04 JW |
146 | if not 'Image GPSInfo' in tags: |
147 | return gps_data | |
148 | ||
a180ca26 JW |
149 | try: |
150 | dms_data = { | |
151 | 'latitude': tags['GPS GPSLatitude'], | |
152 | 'longitude': tags['GPS GPSLongitude']} | |
153 | ||
f2da5bef | 154 | for key, dat in dms_data.iteritems(): |
a180ca26 JW |
155 | gps_data[key] = ( |
156 | lambda v: | |
157 | float(v[0].num) / float(v[0].den) \ | |
c72d661b | 158 | + (float(v[1].num) / float(v[1].den) / 60) \ |
a180ca26 JW |
159 | + (float(v[2].num) / float(v[2].den) / (60 * 60)) |
160 | )(dat.values) | |
c72d661b | 161 | |
bc875dc7 | 162 | if tags['GPS GPSLatitudeRef'].values == 'S': |
163 | gps_data['latitude'] /= -1 | |
164 | ||
c72d661b JW |
165 | if tags['GPS GPSLongitudeRef'].values == 'W': |
166 | gps_data['longitude'] /= -1 | |
167 | ||
a180ca26 JW |
168 | except KeyError: |
169 | pass | |
170 | ||
171 | try: | |
172 | gps_data['direction'] = ( | |
173 | lambda d: | |
174 | float(d.num) / float(d.den) | |
175 | )(tags['GPS GPSImgDirection'].values[0]) | |
176 | except KeyError: | |
177 | pass | |
178 | ||
179 | try: | |
180 | gps_data['altitude'] = ( | |
181 | lambda a: | |
182 | float(a.num) / float(a.den) | |
183 | )(tags['GPS GPSAltitude'].values[0]) | |
184 | except KeyError: | |
185 | pass | |
186 | ||
187 | return gps_data |