Merge remote-tracking branch 'joar/exif-rebase'
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
1
mediagoblin/static/extlib/leaflet
Symbolic link
1
mediagoblin/static/extlib/leaflet
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../extlib/leaflet/dist/
|
||||
@@ -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(){
|
||||
|
||||
1
mediagoblin/static/js/extlib/leaflet
Symbolic link
1
mediagoblin/static/js/extlib/leaflet
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../extlib/leaflet/dist/
|
||||
46
mediagoblin/static/js/geolocation-map.js
Normal file
46
mediagoblin/static/js/geolocation-map.js
Normal file
@@ -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 © '
|
||||
+ String(new Date().getFullYear())
|
||||
+ ' OpenStreetMap contributors, CC-BY-SA.'
|
||||
+ ' Imaging © '
|
||||
+ 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);
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
@@ -23,8 +23,20 @@
|
||||
{% block title %}{{ media.title }} — {{ 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 %}
|
||||
|
||||
33
mediagoblin/templates/mediagoblin/utils/exif.html
Normal file
33
mediagoblin/templates/mediagoblin/utils/exif.html
Normal file
@@ -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 %}
|
||||
42
mediagoblin/templates/mediagoblin/utils/geolocation_map.html
Normal file
42
mediagoblin/templates/mediagoblin/utils/geolocation_map.html
Normal file
@@ -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 %}
|
||||
@@ -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 %}
|
||||
|
||||
189
mediagoblin/tests/test_exif.py
Normal file
189
mediagoblin/tests/test_exif.py
Normal file
@@ -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}
|
||||
|
||||
18
mediagoblin/tests/test_exif/bad.jpg
Normal file
18
mediagoblin/tests/test_exif/bad.jpg
Normal file
@@ -0,0 +1,18 @@
|
||||
V2UncmUgbm8gc3RyYW5nZXJzIHRvIGxvdmUKWW91IGtub3cgdGhlIHJ1bGVzIGFuZCBzbyBkbyBJ
|
||||
CkEgZnVsbCBjb21taXRtZW50J3Mgd2hhdCBJJ20gdGhpbmtpbicgb2YKWW91IHdvdWxkbid0IGdl
|
||||
dCB0aGlzIGZyb20gYW55IG90aGVyIGd1eQpJIGp1c3Qgd2FubmEgdGVsbCB5b3UgaG93IEknbSBm
|
||||
ZWVsaW4nCkdvdHRhIG1ha2UgeW91IHVuZGVyc3RhbmQKCihDaG9ydXMpCk5ldmVyIGdvbm5hIGdp
|
||||
dmUgeW91IHVwCk5ldmVyIGdvbm5hIGxldCB5b3UgZG93bgpOZXZlciBnb25uYSBydW4gYXJvdW5k
|
||||
IGFuZCBkZXNlcnQgeW91Ck5ldmVyIGdvbm5hIG1ha2UgeW91IGNyeQpOZXZlciBnb25uYSBzYXkg
|
||||
Z29vZGJ5ZQpOZXZlciBnb25uYSB0ZWxsIGEgbGllIGFuZCBodXJ0IHlvdQoKV2UndmUga25vdyBl
|
||||
YWNoIG90aGVyIGZvciBzbyBsb25nCllvdXIgaGVhcnQncyBiZWVuIGFjaGluJyBidXQgeW91J3Jl
|
||||
IHRvbyBzaHkgdG8gc2F5IGl0Ckluc2lkZSB3ZSBib3RoIGtub3cgd2hhdCdzIGJlZW4gZ29pbmcg
|
||||
b24KV2Uga25vdyB0aGUgZ2FtZSBhbmQgd2UncmUgZ29ubmEgcGxheSBpdApBbmQgaWYgeW91IGFz
|
||||
ayBtZSBob3cgSSdtIGZlZWxpbicKRG9uJ3QgdGVsbCBtZSB5b3UncmUgdG9vIGJsaW5kIHRvIHNl
|
||||
ZQoKKENob3J1cyB4MikKCihHaXZlIHlvdSB1cCwgZ2l2ZSB5b3UgdXApCk5ldmVyIGdvbm5hIGdp
|
||||
dmUsIG5ldmVyIGdvbm5hIGdpdmUKKEdpdmUgeW91IHVwKQpOZXZlciBnb25uYSBnaXZlLCBuZXZl
|
||||
ciBnb25uYSBnaXZlCihHaXZlIHlvdSB1cCkKCldlJ3ZlIGtub3cgZWFjaCBvdGhlciBmb3Igc28g
|
||||
bG9uZwpZb3VyIGhlYXJ0J3MgYmVlbiBhY2hpbicgYnV0IHlvdSdyZSB0b28gc2h5IHRvIHNheSBp
|
||||
dApJbnNpZGUgd2UgYm90aCBrbm93IHdoYXQncyBiZWVuIGdvaW5nIG9uCldlIGtub3cgdGhlIGdh
|
||||
bWUgYW5kIHdlJ3JlIGdvbm5hIHBsYXkgaXQKSSBqdXN0IHdhbm5hIHRlbGwgeW91IGhvdyBJJ20g
|
||||
ZmVlbGluJwpHb3R0YSBtYWtlIHlvdSB1bmRlcnN0YW5kCgooQ2hvcnVzIHgzKQo=
|
||||
BIN
mediagoblin/tests/test_exif/empty.jpg
Normal file
BIN
mediagoblin/tests/test_exif/empty.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
mediagoblin/tests/test_exif/good.jpg
Normal file
BIN
mediagoblin/tests/test_exif/good.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 203 KiB |
BIN
mediagoblin/tests/test_exif/has-gps.jpg
Normal file
BIN
mediagoblin/tests/test_exif/has-gps.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
165
mediagoblin/tools/exif.py
Normal file
165
mediagoblin/tools/exif.py
Normal file
@@ -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
|
||||
1
mediagoblin/tools/extlib/EXIF.py
Symbolic link
1
mediagoblin/tools/extlib/EXIF.py
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../extlib/exif/EXIF.py
|
||||
0
mediagoblin/tools/extlib/__init__.py
Normal file
0
mediagoblin/tools/extlib/__init__.py
Normal file
Reference in New Issue
Block a user