EXIF extraction, geolocation map, image rotation
authorJoar Wandborg <git@wandborg.com>
Tue, 10 Jan 2012 01:59:07 +0000 (02:59 +0100)
committerJoar Wandborg <git@wandborg.com>
Wed, 25 Jan 2012 22:43:58 +0000 (23:43 +0100)
- Images are now rotated based on EXIF image orientation
  (in case the image isn't flipped on X or Y axis or
  correctly oriented, then we do nothing)
- *Always* create a medium.jpg in image.processing, for
  the sake of rotation of display image
- Extract EXIF and GPS tags from images and insert them
  into media_data
- Geolocation map display added to media.html
- EXIF display added, then removed. It is not in this
  revision, although some of it is (the "EXIF" h4
  header). Need to make it presentable, filtering out
  purely robotical tags, perhaps.

mediagoblin/media_types/image/processing.py
mediagoblin/templates/mediagoblin/user_pages/media.html

index cf90388f3a9371e991cb339fbfd30cc287618ec5..9eb8fa1669cfccca9ab858ce0fa9fc37ca29df22 100644 (file)
@@ -18,14 +18,10 @@ import Image
 import os
 
 from mediagoblin import mg_globals as mgg
-
 from mediagoblin.processing import BadMediaFail, \
     create_pub_filepath, THUMB_SIZE, MEDIUM_SIZE
-
-################################
-# Media processing initial steps
-################################
-
+from mediagoblin.media_types.image.EXIF import process_file
+from mediagoblin.tools.translate import pass_to_ugettext as _
 
 def process_image(entry):
     """
@@ -46,11 +42,17 @@ def process_image(entry):
     basename = os.path.split(filename_bits[0])[1]
     extension = filename_bits[1].lower()
 
+    # EXIF extraction
+    exif_tags = extract_exif(queued_filename)
+    gps_data = get_gps_data(exif_tags)
+
     try:
         thumb = Image.open(queued_filename)
     except IOError:
         raise BadMediaFail()
 
+    thumb = exif_fix_image_orientation(thumb, exif_tags)
+
     thumb.thumbnail(THUMB_SIZE, Image.ANTIALIAS)
 
     # Copy the thumb to the conversion subdir, then remotely.
@@ -67,23 +69,22 @@ def process_image(entry):
     # file, a `medium.jpg` files is created and later associated with the media
     # entry.
     medium = Image.open(queued_filename)
-    medium_processed = False
+    # Fox orientation
+    medium = exif_fix_image_orientation(medium, exif_tags)
 
     if medium.size[0] > MEDIUM_SIZE[0] or medium.size[1] > MEDIUM_SIZE[1]:
         medium.thumbnail(MEDIUM_SIZE, Image.ANTIALIAS)
 
-        medium_filename = 'medium' + extension
-        medium_filepath = create_pub_filepath(entry, medium_filename)
-        tmp_medium_filename = os.path.join(
-            conversions_subdir, medium_filename)
-
-        with file(tmp_medium_filename, 'w') as medium_file:
-            medium.save(medium_file)
+    medium_filename = 'medium' + extension
+    medium_filepath = create_pub_filepath(entry, medium_filename)
+    tmp_medium_filename = os.path.join(
+        conversions_subdir, medium_filename)
 
-        mgg.public_store.copy_local_to_storage(
-            tmp_medium_filename, medium_filepath)
+    with file(tmp_medium_filename, 'w') as medium_file:
+        medium.save(medium_file)
 
-        medium_processed = True
+    mgg.public_store.copy_local_to_storage(
+        tmp_medium_filename, medium_filepath)
 
     # we have to re-read because unlike PIL, not everything reads
     # things in string representation :)
@@ -97,13 +98,135 @@ def process_image(entry):
             as original_file:
             original_file.write(queued_file.read())
 
+    # Remove queued media file from storage and database
     mgg.queue_store.delete_file(queued_filepath)
     entry.queued_media_file = []
+
+    # Insert media file information into database
     media_files_dict = entry.setdefault('media_files', {})
     media_files_dict['thumb'] = thumb_filepath
     media_files_dict['original'] = original_filepath
-    if medium_processed:
-        media_files_dict['medium'] = medium_filepath
+    media_files_dict['medium'] = medium_filepath
+
+    # Insert exif data into database
+    media_data = entry.setdefault('media_data', {})
+    media_data['exif'] = clean_exif(exif_tags)
+    media_data['gps'] = gps_data
 
     # clean up workbench
     workbench.destroy_self()
+
+def exif_fix_image_orientation(im, exif_tags):
+    """
+    Translate any EXIF orientation to raw orientation
+
+    Cons:
+    - REDUCES IMAGE QUALITY by recompressig it
+
+    Pros:
+    - Cures my neck pain
+    """
+    # Rotate image
+    if 'Image Orientation' in exif_tags:
+        rotation_map = {
+            3: 180,
+            6: 270,
+            8: 90}
+        orientation = exif_tags['Image Orientation'].values[0]
+        if orientation in rotation_map.keys():
+            im = im.rotate(
+                rotation_map[orientation])
+
+    return im
+
+def extract_exif(filename):
+    """
+    Returns EXIF tags found in file at ``filename``
+    """
+    exif_tags = {}
+
+    try:
+        image = open(filename)
+        exif_tags = process_file(image)
+    except IOError:
+        BadMediaFail(_('Could not read the image file.'))
+
+    return exif_tags
+
+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']
+
+    clean_exif = {}
+
+    for key, value in exif.items():
+        if not key in disabled_tags:
+            clean_exif[key] = str(value)
+
+    return clean_exif
+    
+
+def get_gps_data(exif):
+    """
+    Processes EXIF data returned by EXIF.py
+    """
+    if not 'Image GPSInfo' in exif:
+        return False
+
+    gps_data = {}
+
+    try:
+        dms_data = {
+            'latitude': exif['GPS GPSLatitude'],
+            'longitude': exif['GPS GPSLongitude']}
+
+        for key, dat in dms_data.items():
+            gps_data[key] = (
+                lambda v:
+                    float(v[0].num) / float(v[0].den) \
+                    + (float(v[1].num) / float(v[1].den) / 60 )\
+                    + (float(v[2].num) / float(v[2].den) / (60 * 60))
+                )(dat.values)
+    except KeyError:
+        pass
+
+    try:
+        gps_data['direction'] = (
+            lambda d:
+                float(d.num) / float(d.den)
+            )(exif['GPS GPSImgDirection'].values[0])
+    except KeyError:
+        pass
+
+    try:
+        gps_data['altitude'] = (
+            lambda a:
+                float(a.num) / float(a.den)
+            )(exif['GPS GPSAltitude'].values[0])
+    except KeyError:
+        pass
+
+    return gps_data
+
+
+if __name__ == '__main__':
+    import sys
+    import pprint
+
+    pp = pprint.PrettyPrinter()
+
+    result = extract_exif(sys.argv[1])
+    gps = get_gps_data(result)
+
+    import pdb
+    pdb.set_trace()
+
+    print pp.pprint(
+        result)
index cbe26cbfb546553034625fe68b8a4676e98aa2ea..944d7f6ed4d9c23d43ec5af6d22a47155c7d5c81 100644 (file)
 {% block title %}{{ media.title }} &mdash; {{ super() }}{% endblock %}
 
 {% block mediagoblin_head %}
+  <link rel="stylesheet" 
+       href="{{ request.staticdirect('/extlib/leaflet/leaflet.css') }}" />
+<!--[if lte IE 8]><link rel="stylesheet"
+    href="{{ request.staticdirect('/extlib/leaflet/leaflet.ie.css') }}" /><![endif]-->
   <script type="text/javascript"
           src="{{ request.staticdirect('/js/comment_show.js') }}"></script>
+  <script type="text/javascript"
+          src="{{ request.staticdirect('/extlib/leaflet/leaflet.js') }}"></script>
+  <script type="text/javascript"
+          src="{{ request.staticdirect('/js/geolocation-map.js') }}"></script>
 {% endblock mediagoblin_head %}
 
 {% block mediagoblin_content %}
                                    media= media._id) %}
         <a class="button_action" href="{{ delete_url }}">{% trans %}Delete{% endtrans %}</a>
       {% endif %}
+      {% if media.media_data.exif %}
+       {#-
+       TODO:
+        - Render GPS data in a human-readable format
+       
+        <h4>EXIF</h4>
+       <table>
+       {% for tag, value in media.media_data.exif.items() %}
+         <tr>
+           <td>{{ tag }}</td>
+           <td>{{ value }}</td>
+         </tr>
+        {% endfor %}
+       </table>#}
+      {% endif %}
     </p>
     {% if comments %}
       <h3>
       {% include "mediagoblin/utils/tags.html" %}
     {% endif %}
 
+    {% if media.media_data.gps %}
+      <h4>Map</h4>
+      <div>
+       {% set gps = media.media_data.gps %}
+       <div id="tile-map" style="width: 100%; height: 196px;">
+         <input type="hidden" id="gps-longitude"
+                value="{{ gps.longitude }}" />
+         <input type="hidden" id="gps-latitude"
+                value="{{ gps.latitude }}" />
+       </div>
+       <p>
+         <small>
+           View on 
+           <a href="http://openstreetmap.org/?mlat={{ gps.latitude }}&mlon={{ gps.longitude }}">
+             OpenStreetMap
+           </a>
+         </small>
+       </p>
+      </div>
+    {% endif %}
     {% include "mediagoblin/utils/license.html" %}
   </div>
 {% endblock %}