# GNU MediaGoblin -- federated, autonomous media hosting # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import six from exifread import process_file from exifread.utils import Ratio try: from PIL import Image except ImportError: import Image from mediagoblin.processing import BadMediaFail from mediagoblin.tools.translate import pass_to_ugettext as _ # A list of tags that should be stored for faster access USEFUL_TAGS = [ 'Image Make', 'Image Model', 'EXIF FNumber', 'EXIF Flash', 'EXIF FocalLength', 'EXIF ExposureTime', 'EXIF ApertureValue', 'EXIF ExposureMode', 'EXIF ISOSpeedRatings', 'EXIF UserComment', ] def exif_image_needs_rotation(exif_tags): """ Returns True if EXIF orientation requires rotation """ return 'Image Orientation' in exif_tags \ and exif_tags['Image Orientation'].values[0] != 1 def exif_fix_image_orientation(im, exif_tags): """ Translate any EXIF orientation to raw orientation Cons: - Well, it changes the image, which means we'll recompress it... not a problem if scaling it down already anyway. We might lose some quality in recompressing if it's at the same-size though Pros: - Prevents neck pain """ # Rotate image if 'Image Orientation' in exif_tags: rotation_map = { 3: Image.ROTATE_180, 6: Image.ROTATE_270, 8: Image.ROTATE_90} orientation = exif_tags['Image Orientation'].values[0] if orientation in rotation_map: im = im.transpose( rotation_map[orientation]) return im def extract_exif(filename): """ Returns EXIF tags found in file at ``filename`` """ try: with open(filename, 'rb') as image: return process_file(image, details=False) except OSError: raise BadMediaFail(_('Could not read the image file.')) def clean_exif(exif): ''' Clean the result from anything the database cannot handle ''' # Discard any JPEG thumbnail, for database compatibility # and that I cannot see a case when we would use it. # It takes up some space too. disabled_tags = [ 'Thumbnail JPEGInterchangeFormatLength', 'JPEGThumbnail', 'Thumbnail JPEGInterchangeFormat'] return {key: _ifd_tag_to_dict(value) for (key, value) in exif.items() if key not in disabled_tags} def _ifd_tag_to_dict(tag): ''' Takes an IFD tag object from the EXIF library and converts it to a dict that can be stored as JSON in the database. ''' data = { 'printable': tag.printable, 'tag': tag.tag, 'field_type': tag.field_type, 'field_offset': tag.field_offset, 'field_length': tag.field_length, 'values': None} if isinstance(tag.printable, bytes): # Force it to be decoded as UTF-8 so that it'll fit into the DB data['printable'] = tag.printable.decode('utf8', 'replace') if type(tag.values) == list: data['values'] = [_ratio_to_list(val) if isinstance(val, Ratio) else val for val in tag.values] else: if isinstance(tag.values, bytes): # Force UTF-8, so that it fits into the DB data['values'] = tag.values.decode('utf8', 'replace') else: data['values'] = tag.values return data def _ratio_to_list(ratio): return [ratio.num, ratio.den] def get_useful(tags): from collections import OrderedDict return OrderedDict((key, tag) for (key, tag) in tags.items()) def get_gps_data(tags): """ Processes EXIF data returned by EXIF.py """ def safe_gps_ratio_divide(ratio): if ratio.den == 0: return 0.0 return float(ratio.num) / float(ratio.den) gps_data = {} if not 'Image GPSInfo' in tags: return gps_data try: dms_data = { 'latitude': tags['GPS GPSLatitude'], 'longitude': tags['GPS GPSLongitude']} for key, dat in dms_data.items(): gps_data[key] = ( lambda v: safe_gps_ratio_divide(v[0]) \ + (safe_gps_ratio_divide(v[1]) / 60) \ + (safe_gps_ratio_divide(v[2]) / (60 * 60)) )(dat.values) if tags['GPS GPSLatitudeRef'].values == 'S': gps_data['latitude'] /= -1 if tags['GPS GPSLongitudeRef'].values == 'W': gps_data['longitude'] /= -1 except KeyError: pass try: direction = tags['GPS GPSImgDirection'].values[0] gps_data['direction'] = safe_gps_ratio_divide(direction) except KeyError: pass try: altitude = tags['GPS GPSAltitude'].values[0] gps_data['altitude'] = safe_gps_ratio_divide(altitude) except KeyError: pass return gps_data