exif.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. # GNU MediaGoblin -- federated, autonomous media hosting
  2. # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
  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. import six
  17. from exifread import process_file
  18. from exifread.utils import Ratio
  19. from mediagoblin.processing import BadMediaFail
  20. from mediagoblin.tools.translate import pass_to_ugettext as _
  21. # A list of tags that should be stored for faster access
  22. USEFUL_TAGS = [
  23. 'Image Make',
  24. 'Image Model',
  25. 'EXIF FNumber',
  26. 'EXIF Flash',
  27. 'EXIF FocalLength',
  28. 'EXIF ExposureTime',
  29. 'EXIF ApertureValue',
  30. 'EXIF ExposureMode',
  31. 'EXIF ISOSpeedRatings',
  32. 'EXIF UserComment',
  33. ]
  34. def exif_image_needs_rotation(exif_tags):
  35. """
  36. Returns True if EXIF orientation requires rotation
  37. """
  38. return 'Image Orientation' in exif_tags \
  39. and exif_tags['Image Orientation'].values[0] != 1
  40. def exif_fix_image_orientation(im, exif_tags):
  41. """
  42. Translate any EXIF orientation to raw orientation
  43. Cons:
  44. - Well, it changes the image, which means we'll recompress
  45. it... not a problem if scaling it down already anyway. We might
  46. lose some quality in recompressing if it's at the same-size
  47. though
  48. Pros:
  49. - Prevents neck pain
  50. """
  51. # Rotate image
  52. if 'Image Orientation' in exif_tags:
  53. rotation_map = {
  54. 3: 180,
  55. 6: 270,
  56. 8: 90}
  57. orientation = exif_tags['Image Orientation'].values[0]
  58. if orientation in rotation_map:
  59. im = im.rotate(
  60. rotation_map[orientation])
  61. return im
  62. def extract_exif(filename):
  63. """
  64. Returns EXIF tags found in file at ``filename``
  65. """
  66. try:
  67. with open(filename, 'rb') as image:
  68. return process_file(image, details=False)
  69. except IOError:
  70. raise BadMediaFail(_('Could not read the image file.'))
  71. def clean_exif(exif):
  72. '''
  73. Clean the result from anything the database cannot handle
  74. '''
  75. # Discard any JPEG thumbnail, for database compatibility
  76. # and that I cannot see a case when we would use it.
  77. # It takes up some space too.
  78. disabled_tags = [
  79. 'Thumbnail JPEGInterchangeFormatLength',
  80. 'JPEGThumbnail',
  81. 'Thumbnail JPEGInterchangeFormat']
  82. return dict((key, _ifd_tag_to_dict(value)) for (key, value)
  83. in six.iteritems(exif) if key not in disabled_tags)
  84. def _ifd_tag_to_dict(tag):
  85. '''
  86. Takes an IFD tag object from the EXIF library and converts it to a dict
  87. that can be stored as JSON in the database.
  88. '''
  89. data = {
  90. 'printable': tag.printable,
  91. 'tag': tag.tag,
  92. 'field_type': tag.field_type,
  93. 'field_offset': tag.field_offset,
  94. 'field_length': tag.field_length,
  95. 'values': None}
  96. if isinstance(tag.printable, six.binary_type):
  97. # Force it to be decoded as UTF-8 so that it'll fit into the DB
  98. data['printable'] = tag.printable.decode('utf8', 'replace')
  99. if type(tag.values) == list:
  100. data['values'] = [_ratio_to_list(val) if isinstance(val, Ratio) else val
  101. for val in tag.values]
  102. else:
  103. if isinstance(tag.values, six.binary_type):
  104. # Force UTF-8, so that it fits into the DB
  105. data['values'] = tag.values.decode('utf8', 'replace')
  106. else:
  107. data['values'] = tag.values
  108. return data
  109. def _ratio_to_list(ratio):
  110. return [ratio.num, ratio.den]
  111. def get_useful(tags):
  112. from collections import OrderedDict
  113. return OrderedDict((key, tag) for (key, tag) in six.iteritems(tags))
  114. def get_gps_data(tags):
  115. """
  116. Processes EXIF data returned by EXIF.py
  117. """
  118. def safe_gps_ratio_divide(ratio):
  119. if ratio.den == 0:
  120. return 0.0
  121. return float(ratio.num) / float(ratio.den)
  122. gps_data = {}
  123. if not 'Image GPSInfo' in tags:
  124. return gps_data
  125. try:
  126. dms_data = {
  127. 'latitude': tags['GPS GPSLatitude'],
  128. 'longitude': tags['GPS GPSLongitude']}
  129. for key, dat in six.iteritems(dms_data):
  130. gps_data[key] = (
  131. lambda v:
  132. safe_gps_ratio_divide(v[0]) \
  133. + (safe_gps_ratio_divide(v[1]) / 60) \
  134. + (safe_gps_ratio_divide(v[2]) / (60 * 60))
  135. )(dat.values)
  136. if tags['GPS GPSLatitudeRef'].values == 'S':
  137. gps_data['latitude'] /= -1
  138. if tags['GPS GPSLongitudeRef'].values == 'W':
  139. gps_data['longitude'] /= -1
  140. except KeyError:
  141. pass
  142. try:
  143. gps_data['direction'] = (
  144. lambda d:
  145. float(d.num) / float(d.den)
  146. )(tags['GPS GPSImgDirection'].values[0])
  147. except KeyError:
  148. pass
  149. try:
  150. gps_data['altitude'] = (
  151. lambda a:
  152. float(a.num) / float(a.den)
  153. )(tags['GPS GPSAltitude'].values[0])
  154. except KeyError:
  155. pass
  156. return gps_data