aboutsummaryrefslogtreecommitdiffstats
path: root/mediagoblin
diff options
context:
space:
mode:
Diffstat (limited to 'mediagoblin')
-rw-r--r--mediagoblin/config_spec.ini3
-rw-r--r--mediagoblin/media_types/image/processing.py66
l---------mediagoblin/static/extlib/leaflet1
-rw-r--r--mediagoblin/static/js/comment_show.js18
l---------mediagoblin/static/js/extlib/leaflet1
-rw-r--r--mediagoblin/static/js/geolocation-map.js46
-rw-r--r--mediagoblin/static/js/show_password.js18
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/media.html16
-rw-r--r--mediagoblin/templates/mediagoblin/utils/exif.html33
-rw-r--r--mediagoblin/templates/mediagoblin/utils/geolocation_map.html42
-rw-r--r--mediagoblin/templates/mediagoblin/utils/license.html14
-rw-r--r--mediagoblin/tests/test_exif.py189
-rw-r--r--mediagoblin/tests/test_exif/bad.jpg18
-rw-r--r--mediagoblin/tests/test_exif/empty.jpgbin0 -> 26636 bytes
-rw-r--r--mediagoblin/tests/test_exif/good.jpgbin0 -> 207590 bytes
-rw-r--r--mediagoblin/tests/test_exif/has-gps.jpgbin0 -> 1933121 bytes
-rw-r--r--mediagoblin/tools/exif.py165
l---------mediagoblin/tools/extlib/EXIF.py1
-rw-r--r--mediagoblin/tools/extlib/__init__.py0
19 files changed, 607 insertions, 24 deletions
diff --git a/mediagoblin/config_spec.ini b/mediagoblin/config_spec.ini
index dc286a27..2d410899 100644
--- a/mediagoblin/config_spec.ini
+++ b/mediagoblin/config_spec.ini
@@ -53,6 +53,9 @@ csrf_cookie_name = string(default='mediagoblin_csrftoken')
# Push stuff
push_urls = string_list(default=list())
+exif_visible = boolean(default=False)
+geolocation_map_visible = boolean(default=False)
+
[storage:publicstore]
storage_class = string(default="mediagoblin.storage.filestorage:BasicFileStorage")
base_dir = string(default="%(here)s/user_dev/media/public")
diff --git a/mediagoblin/media_types/image/processing.py b/mediagoblin/media_types/image/processing.py
index cf90388f..78f64be0 100644
--- a/mediagoblin/media_types/image/processing.py
+++ b/mediagoblin/media_types/image/processing.py
@@ -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.tools.exif import exif_fix_image_orientation, \
+ extract_exif, clean_exif, get_gps_data, get_useful
def process_image(entry):
"""
@@ -46,20 +42,29 @@ 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.
thumb_filename = 'thumbnail' + extension
thumb_filepath = create_pub_filepath(entry, thumb_filename)
+
tmp_thumb_filename = os.path.join(
conversions_subdir, thumb_filename)
+
with file(tmp_thumb_filename, 'w') as thumb_file:
thumb.save(thumb_file)
+
mgg.public_store.copy_local_to_storage(
tmp_thumb_filename, thumb_filepath)
@@ -67,23 +72,24 @@ 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
+
+ # Fix 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)
+ medium_filename = 'medium' + extension
+ medium_filepath = create_pub_filepath(entry, medium_filename)
- with file(tmp_medium_filename, 'w') as medium_file:
- medium.save(medium_file)
+ 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 +103,37 @@ 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': clean_exif(exif_tags)}
+ media_data['exif']['useful'] = get_useful(
+ media_data['exif']['clean'])
+ media_data['gps'] = gps_data
# clean up workbench
workbench.destroy_self()
+
+if __name__ == '__main__':
+ import sys
+ import pprint
+
+ pp = pprint.PrettyPrinter()
+
+ result = extract_exif(sys.argv[1])
+ gps = get_gps_data(result)
+ clean = clean_exif(result)
+ useful = get_useful(clean)
+
+ print pp.pprint(
+ clean)
diff --git a/mediagoblin/static/extlib/leaflet b/mediagoblin/static/extlib/leaflet
new file mode 120000
index 00000000..b47e2b1b
--- /dev/null
+++ b/mediagoblin/static/extlib/leaflet
@@ -0,0 +1 @@
+../../../extlib/leaflet/dist/ \ No newline at end of file
diff --git a/mediagoblin/static/js/comment_show.js b/mediagoblin/static/js/comment_show.js
index 2212b9ad..71466a8d 100644
--- a/mediagoblin/static/js/comment_show.js
+++ b/mediagoblin/static/js/comment_show.js
@@ -1,3 +1,21 @@
+/**
+ * GNU MediaGoblin -- federated, autonomous media hosting
+ * Copyright (C) 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
$(document).ready(function(){
$('#form_comment').hide();
$('#button_addcomment').click(function(){
diff --git a/mediagoblin/static/js/extlib/leaflet b/mediagoblin/static/js/extlib/leaflet
new file mode 120000
index 00000000..2fc302d7
--- /dev/null
+++ b/mediagoblin/static/js/extlib/leaflet
@@ -0,0 +1 @@
+../../../../extlib/leaflet/dist/ \ No newline at end of file
diff --git a/mediagoblin/static/js/geolocation-map.js b/mediagoblin/static/js/geolocation-map.js
new file mode 100644
index 00000000..35083d4f
--- /dev/null
+++ b/mediagoblin/static/js/geolocation-map.js
@@ -0,0 +1,46 @@
+/**
+ * GNU MediaGoblin -- federated, autonomous media hosting
+ * Copyright (C) 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+$(document).ready(function () {
+ var longitude = Number(
+ $('#tile-map #gps-longitude').val());
+ var latitude = Number(
+ $('#tile-map #gps-latitude').val());
+
+ // Get a new map instance attached and element with id="tile-map"
+ var map = new L.Map('tile-map');
+
+ var mqtileUrl = 'http://otile{s}.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.jpg';
+ var mqtileAttrib = 'Map data &copy; '
+ + String(new Date().getFullYear())
+ + ' OpenStreetMap contributors, CC-BY-SA.'
+ + ' Imaging &copy; '
+ + String(new Date().getFullYear())
+ + ' <a target="_blank" href="http://mapquest.com">MapQuest</a>.';
+ var mqtile = new L.TileLayer(
+ mqtileUrl,
+ {maxZoom: 18,
+ attribution: mqtileAttrib,
+ subdomains: '1234'});
+
+ var location = new L.LatLng(latitude, longitude);
+ map.setView(location, 13).addLayer(mqtile);
+
+ var marker = new L.Marker(location);
+ map.addLayer(marker);
+});
diff --git a/mediagoblin/static/js/show_password.js b/mediagoblin/static/js/show_password.js
index 519b29c1..513fe327 100644
--- a/mediagoblin/static/js/show_password.js
+++ b/mediagoblin/static/js/show_password.js
@@ -1,3 +1,21 @@
+/**
+ * GNU MediaGoblin -- federated, autonomous media hosting
+ * Copyright (C) 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
$(document).ready(function(){
$("#password").after('<input type="text" value="" name="password_clear" id="password_clear" /><label><input type="checkbox" id="password_boolean" />Show password</label>');
$('#password_clear').hide();
diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html
index 9df3dfbc..dec443cd 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/media.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/media.html
@@ -23,8 +23,20 @@
{% block title %}{{ media.title }} &mdash; {{ super() }}{% endblock %}
{% block mediagoblin_head %}
+<!--[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>
+
+ {% if app_config['geolocation_map_visible'] %}
+ <link rel="stylesheet"
+ href="{{ request.staticdirect('/extlib/leaflet/leaflet.css') }}" />
+
+ <script type="text/javascript"
+ src="{{ request.staticdirect('/extlib/leaflet/leaflet.js') }}"></script>
+ <script type="text/javascript"
+ src="{{ request.staticdirect('/js/geolocation-map.js') }}"></script>
+ {% endif %}
{% endblock mediagoblin_head %}
{% block mediagoblin_content %}
@@ -172,5 +184,9 @@
{% endif %}
{% include "mediagoblin/utils/license.html" %}
+
+ {% include "mediagoblin/utils/geolocation_map.html" %}
+
+ {% include "mediagoblin/utils/exif.html" %}
</div>
{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/utils/exif.html b/mediagoblin/templates/mediagoblin/utils/exif.html
new file mode 100644
index 00000000..9962dd65
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/utils/exif.html
@@ -0,0 +1,33 @@
+{#
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 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 <http://www.gnu.org/licenses/>.
+#}
+
+{% block exif_content %}
+ {% if media.media_data.has_key('exif')
+ and app_config['exif_visible']
+ and media.media_data.exif.has_key('useful') %}
+ <h4>EXIF</h4>
+ <table>
+ {% for key, tag in media.media_data.exif.useful.items() %}
+ <tr>
+ <td>{{ key }}</td>
+ <td>{{ tag.printable }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% endif %}
+{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/utils/geolocation_map.html b/mediagoblin/templates/mediagoblin/utils/geolocation_map.html
new file mode 100644
index 00000000..ce1edc39
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/utils/geolocation_map.html
@@ -0,0 +1,42 @@
+{#
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 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 <http://www.gnu.org/licenses/>.
+#}
+
+{% block geolocation_map %}
+ {% if media.media_data.has_key('gps')
+ and app_config['geolocation_map_visible']
+ and 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 %}
+{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/utils/license.html b/mediagoblin/templates/mediagoblin/utils/license.html
index 056c356e..5a268e39 100644
--- a/mediagoblin/templates/mediagoblin/utils/license.html
+++ b/mediagoblin/templates/mediagoblin/utils/license.html
@@ -17,10 +17,12 @@
#}
{% block license_content -%}
- {% trans %}License:{% endtrans %}
- {% if media.license %}
- <a href="{{ media.license }}">{{ media.get_license_data().abbreviation }}</a>
- {% else %}
- {% trans %}All rights reserved{% endtrans %}
- {% endif %}
+ <p>
+ {% trans %}License:{% endtrans %}
+ {% if media.license %}
+ <a href="{{ media.license }}">{{ media.get_license_data().abbreviation }}</a>
+ {% else %}
+ {% trans %}All rights reserved{% endtrans %}
+ {% endif %}
+ </p>
{% endblock %}
diff --git a/mediagoblin/tests/test_exif.py b/mediagoblin/tests/test_exif.py
new file mode 100644
index 00000000..9f2219c0
--- /dev/null
+++ b/mediagoblin/tests/test_exif.py
@@ -0,0 +1,189 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 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 <http://www.gnu.org/licenses/>.
+
+import os
+import pkg_resources
+import Image
+
+from mediagoblin.tools.exif import exif_fix_image_orientation, \
+ extract_exif, clean_exif, get_gps_data, get_useful
+
+GOOD_JPG = pkg_resources.resource_filename(
+ 'mediagoblin.tests',
+ os.path.join(
+ 'test_exif',
+ 'good.jpg'))
+EMPTY_JPG = pkg_resources.resource_filename(
+ 'mediagoblin.tests',
+ os.path.join(
+ 'test_exif',
+ 'empty.jpg'))
+BAD_JPG = pkg_resources.resource_filename(
+ 'mediagoblin.tests',
+ os.path.join(
+ 'test_exif',
+ 'bad.jpg'))
+GPS_JPG = pkg_resources.resource_filename(
+ 'mediagoblin.tests',
+ os.path.join(
+ 'test_exif',
+ 'has-gps.jpg'))
+
+def test_exif_extraction():
+ '''
+ Test EXIF extraction from a good image
+ '''
+ result = extract_exif(GOOD_JPG)
+ clean = clean_exif(result)
+ useful = get_useful(clean)
+ gps = get_gps_data(result)
+
+ # Do we have the result?
+ assert len(result) == 108
+
+ # Do we have clean data?
+ assert len(clean) == 105
+
+ # GPS data?
+ assert gps == {}
+
+ # Do we have the "useful" tags?
+ assert useful == {
+ 'EXIF Flash': {
+ 'field_type': 3,
+ 'printable': 'No',
+ 'field_offset': 380,
+ 'tag': 37385,
+ 'values': [0],
+ 'field_length': 2},
+ 'EXIF ExposureTime': {
+ 'field_type': 5,
+ 'printable': '1/125',
+ 'field_offset': 700,
+ 'tag': 33434,
+ 'values': [[1, 125]],
+ 'field_length': 8},
+ 'EXIF FocalLength': {
+ 'field_type': 5,
+ 'printable': '18',
+ 'field_offset': 780,
+ 'tag': 37386,
+ 'values': [[18, 1]],
+ 'field_length': 8},
+ 'Image Model': {
+ 'field_type': 2,
+ 'printable': 'NIKON D80',
+ 'field_offset': 152,
+ 'tag': 272,
+ 'values': 'NIKON D80',
+ 'field_length': 10},
+ 'Image Make': {
+ 'field_type': 2,
+ 'printable': 'NIKON CORPORATION',
+ 'field_offset': 134,
+ 'tag': 271,
+ 'values': 'NIKON CORPORATION',
+ 'field_length': 18},
+ 'EXIF ExposureMode': {
+ 'field_type': 3,
+ 'printable': 'Manual Exposure',
+ 'field_offset': 584,
+ 'tag': 41986,
+ 'values': [1],
+ 'field_length': 2},
+ 'EXIF ISOSpeedRatings': {
+ 'field_type': 3,
+ 'printable': '100',
+ 'field_offset': 260,
+ 'tag': 34855,
+ 'values': [100],
+ 'field_length': 2},
+ 'EXIF FNumber': {
+ 'field_type': 5,
+ 'printable': '10',
+ 'field_offset': 708,
+ 'tag': 33437,
+ 'values': [[10, 1]],
+ 'field_length': 8},
+ 'EXIF UserComment': {
+ 'field_type': 7,
+ 'printable': 'Joar Wandborg ',
+ 'field_offset': 26180,
+ 'tag': 37510,
+ 'values': [
+ 65, 83, 67, 73, 73, 0, 0, 0, 74, 111, 97, 114, 32, 87,
+ 97, 110, 100, 98, 111, 114, 103, 32, 32, 32, 32, 32, 32,
+ 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
+ 32, 32, 32],
+ 'field_length': 44}}
+
+def test_exif_image_orientation():
+ '''
+ Test image reorientation based on EXIF data
+ '''
+ result = extract_exif(GOOD_JPG)
+
+ image = exif_fix_image_orientation(
+ Image.open(GOOD_JPG),
+ result)
+
+ # Are the dimensions correct?
+ assert image.size == (428, 640)
+
+ # If this pixel looks right, the rest of the image probably will too.
+ assert image.getdata()[10000] == (41, 28, 11)
+
+def test_exif_no_exif():
+ '''
+ Test an image without exif
+ '''
+ result = extract_exif(EMPTY_JPG)
+ clean = clean_exif(result)
+ useful = get_useful(clean)
+ gps = get_gps_data(result)
+
+ assert result == {}
+ assert clean == {}
+ assert gps == {}
+ assert useful == {}
+
+def test_exif_bad_image():
+ '''
+ Test EXIF extraction from a faithful, but bad image
+ '''
+ result = extract_exif(BAD_JPG)
+ clean = clean_exif(result)
+ useful = get_useful(clean)
+ gps = get_gps_data(result)
+
+ assert result == {}
+ assert clean == {}
+ assert gps == {}
+ assert useful == {}
+
+def test_exif_gps_data():
+ '''
+ Test extractiion of GPS data
+ '''
+ result = extract_exif(GPS_JPG)
+ gps = get_gps_data(result)
+
+ assert gps == {
+ 'latitude': 59.336666666666666,
+ 'direction': 25.674046740467404,
+ 'altitude': 37.64365671641791,
+ 'longitude': 18.016166666666667}
+
diff --git a/mediagoblin/tests/test_exif/bad.jpg b/mediagoblin/tests/test_exif/bad.jpg
new file mode 100644
index 00000000..4cde23cd
--- /dev/null
+++ b/mediagoblin/tests/test_exif/bad.jpg
@@ -0,0 +1,18 @@
+V2UncmUgbm8gc3RyYW5nZXJzIHRvIGxvdmUKWW91IGtub3cgdGhlIHJ1bGVzIGFuZCBzbyBkbyBJ
+CkEgZnVsbCBjb21taXRtZW50J3Mgd2hhdCBJJ20gdGhpbmtpbicgb2YKWW91IHdvdWxkbid0IGdl
+dCB0aGlzIGZyb20gYW55IG90aGVyIGd1eQpJIGp1c3Qgd2FubmEgdGVsbCB5b3UgaG93IEknbSBm
+ZWVsaW4nCkdvdHRhIG1ha2UgeW91IHVuZGVyc3RhbmQKCihDaG9ydXMpCk5ldmVyIGdvbm5hIGdp
+dmUgeW91IHVwCk5ldmVyIGdvbm5hIGxldCB5b3UgZG93bgpOZXZlciBnb25uYSBydW4gYXJvdW5k
+IGFuZCBkZXNlcnQgeW91Ck5ldmVyIGdvbm5hIG1ha2UgeW91IGNyeQpOZXZlciBnb25uYSBzYXkg
+Z29vZGJ5ZQpOZXZlciBnb25uYSB0ZWxsIGEgbGllIGFuZCBodXJ0IHlvdQoKV2UndmUga25vdyBl
+YWNoIG90aGVyIGZvciBzbyBsb25nCllvdXIgaGVhcnQncyBiZWVuIGFjaGluJyBidXQgeW91J3Jl
+IHRvbyBzaHkgdG8gc2F5IGl0Ckluc2lkZSB3ZSBib3RoIGtub3cgd2hhdCdzIGJlZW4gZ29pbmcg
+b24KV2Uga25vdyB0aGUgZ2FtZSBhbmQgd2UncmUgZ29ubmEgcGxheSBpdApBbmQgaWYgeW91IGFz
+ayBtZSBob3cgSSdtIGZlZWxpbicKRG9uJ3QgdGVsbCBtZSB5b3UncmUgdG9vIGJsaW5kIHRvIHNl
+ZQoKKENob3J1cyB4MikKCihHaXZlIHlvdSB1cCwgZ2l2ZSB5b3UgdXApCk5ldmVyIGdvbm5hIGdp
+dmUsIG5ldmVyIGdvbm5hIGdpdmUKKEdpdmUgeW91IHVwKQpOZXZlciBnb25uYSBnaXZlLCBuZXZl
+ciBnb25uYSBnaXZlCihHaXZlIHlvdSB1cCkKCldlJ3ZlIGtub3cgZWFjaCBvdGhlciBmb3Igc28g
+bG9uZwpZb3VyIGhlYXJ0J3MgYmVlbiBhY2hpbicgYnV0IHlvdSdyZSB0b28gc2h5IHRvIHNheSBp
+dApJbnNpZGUgd2UgYm90aCBrbm93IHdoYXQncyBiZWVuIGdvaW5nIG9uCldlIGtub3cgdGhlIGdh
+bWUgYW5kIHdlJ3JlIGdvbm5hIHBsYXkgaXQKSSBqdXN0IHdhbm5hIHRlbGwgeW91IGhvdyBJJ20g
+ZmVlbGluJwpHb3R0YSBtYWtlIHlvdSB1bmRlcnN0YW5kCgooQ2hvcnVzIHgzKQo=
diff --git a/mediagoblin/tests/test_exif/empty.jpg b/mediagoblin/tests/test_exif/empty.jpg
new file mode 100644
index 00000000..37533af5
--- /dev/null
+++ b/mediagoblin/tests/test_exif/empty.jpg
Binary files differ
diff --git a/mediagoblin/tests/test_exif/good.jpg b/mediagoblin/tests/test_exif/good.jpg
new file mode 100644
index 00000000..0ee956fe
--- /dev/null
+++ b/mediagoblin/tests/test_exif/good.jpg
Binary files differ
diff --git a/mediagoblin/tests/test_exif/has-gps.jpg b/mediagoblin/tests/test_exif/has-gps.jpg
new file mode 100644
index 00000000..c7d2cc93
--- /dev/null
+++ b/mediagoblin/tests/test_exif/has-gps.jpg
Binary files differ
diff --git a/mediagoblin/tools/exif.py b/mediagoblin/tools/exif.py
new file mode 100644
index 00000000..3c1aebe5
--- /dev/null
+++ b/mediagoblin/tools/exif.py
@@ -0,0 +1,165 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 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 <http://www.gnu.org/licenses/>.
+
+from mediagoblin.tools.extlib.EXIF import process_file, Ratio
+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_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:
+ raise 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] = _ifd_tag_to_dict(value)
+
+ return clean_exif
+
+def _ifd_tag_to_dict(tag):
+ 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 type(tag.values) == list:
+ data['values'] = []
+ for val in tag.values:
+ if isinstance(val, Ratio):
+ data['values'].append(
+ _ratio_to_list(val))
+ else:
+ data['values'].append(val)
+ else:
+ data['values'] = tag.values
+
+ return data
+
+def _ratio_to_list(ratio):
+ return [ratio.num, ratio.den]
+
+def get_useful(tags):
+ useful = {}
+ for key, tag in tags.items():
+ if key in USEFUL_TAGS:
+ useful[key] = tag
+
+ return useful
+
+
+def get_gps_data(tags):
+ """
+ Processes EXIF data returned by EXIF.py
+ """
+ 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:
+ 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)
+ )(tags['GPS GPSImgDirection'].values[0])
+ except KeyError:
+ pass
+
+ try:
+ gps_data['altitude'] = (
+ lambda a:
+ float(a.num) / float(a.den)
+ )(tags['GPS GPSAltitude'].values[0])
+ except KeyError:
+ pass
+
+ return gps_data
diff --git a/mediagoblin/tools/extlib/EXIF.py b/mediagoblin/tools/extlib/EXIF.py
new file mode 120000
index 00000000..82a2fb30
--- /dev/null
+++ b/mediagoblin/tools/extlib/EXIF.py
@@ -0,0 +1 @@
+../../../extlib/exif/EXIF.py \ No newline at end of file
diff --git a/mediagoblin/tools/extlib/__init__.py b/mediagoblin/tools/extlib/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/mediagoblin/tools/extlib/__init__.py