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 | ||
bfb99d65 | 17 | from exifread import process_file |
5e262d8b | 18 | from exifread.utils import Ratio |
657a4637 | 19 | |
a180ca26 | 20 | from mediagoblin.processing import BadMediaFail |
a789b713 | 21 | from mediagoblin.tools.translate import pass_to_ugettext as _ |
a180ca26 | 22 | |
a180ca26 JW |
23 | # A list of tags that should be stored for faster access |
24 | USEFUL_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 |
38 | def 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 |
46 | def 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 |
73 | def 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 |
84 | def 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 | 100 | def _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 |
130 | def _ratio_to_list(ratio): |
131 | return [ratio.num, ratio.den] | |
132 | ||
c72d661b | 133 | |
a180ca26 | 134 | def get_useful(tags): |
b3566e1d | 135 | return dict((key, tag) for (key, tag) in tags.iteritems()) |
c72d661b | 136 | |
a180ca26 JW |
137 | |
138 | def 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 |