Browse Source

Use cached properties to simplify the access to exif metadata.

pull/113/merge
Simon Conseil 12 years ago
parent
commit
a0ed5d88ff
  1. 52
      sigal/gallery.py
  2. 32
      sigal/image.py
  3. 36
      sigal/utils.py
  4. 10
      tests/test_image.py

52
sigal/gallery.py

@ -40,10 +40,10 @@ from PIL import Image as PILImage
from . import image, video, signals
from .compat import UnicodeMixin, strxfrm, url_quote
from .image import process_image, get_exif_tags
from .image import process_image, get_exif_tags, get_exif_data
from .settings import get_thumb
from .utils import (Devnull, copy, check_or_create_dir, url_from_path,
read_markdown)
read_markdown, cached_property)
from .video import process_video
from .writer import Writer
@ -70,6 +70,7 @@ class Media(UnicodeMixin):
self.src_filename = self.filename = self.url = filename
self.path = path
self.settings = settings
self.ext = os.path.splitext(filename)[1].lower()
self.src_path = join(settings['source'], path, filename)
self.dst_path = join(settings['destination'], path, filename)
@ -78,9 +79,6 @@ class Media(UnicodeMixin):
self.thumb_path = join(settings['destination'], path, self.thumb_name)
self.logger = logging.getLogger(__name__)
self.raw_exif = None
self.exif = None
self.date = None
self._get_metadata()
signals.media_initialized.send(self)
@ -145,41 +143,24 @@ class Image(Media):
type = 'image'
extensions = ('.jpg', '.jpeg', '.png')
def __init__(self, filename, path, settings):
super(Image, self).__init__(filename, path, settings)
self._raw_exif = None
self._exif = None
@property
@cached_property()
def date(self):
if self.exif is not None and 'dateobj' in self.exif:
return self.exif['dateobj']
else:
return self._date
@date.setter
def date(self, value):
self._date = value
return self.exif and self.exif.get('dateobj', None) or None
@property
@cached_property()
def exif(self):
if not self._exif:
self._raw_exif, self._exif = get_exif_tags(self.src_path)
return self._exif
return (get_exif_tags(self.raw_exif)
if self.raw_exif and self.ext in ('.jpg', '.jpeg') else None)
@exif.setter
def exif(self, value):
self._exif = value
@property
@cached_property()
def raw_exif(self):
if not self._raw_exif:
self._raw_exif, self._exif = get_exif_tags(self.src_path)
return self._raw_exif
@raw_exif.setter
def raw_exif(self, value):
self._raw_exif = value
try:
return (get_exif_data(self.src_path)
if self.ext in ('.jpg', '.jpeg') else None)
except (IOError, IndexError, TypeError, AttributeError):
self.logger.warning(u'Could not read EXIF data from %s',
self.src_path)
return None
class Video(Media):
@ -191,6 +172,7 @@ class Video(Media):
def __init__(self, filename, path, settings):
super(Video, self).__init__(filename, path, settings)
base = splitext(filename)[0]
self.date = None
self.src_filename = filename
self.filename = self.url = base + '.webm'
self.dst_path = join(settings['destination'], path, base + '.webm')

32
sigal/image.py

@ -153,8 +153,8 @@ def process_image(filepath, outpath, settings):
return Status.SUCCESS
def _get_exif_data(filename):
"""Return a dict with EXIF data."""
def get_exif_data(filename):
"""Return a dict with the raw EXIF data."""
img = PILImage.open(filename)
exif = img._getexif() or {}
@ -175,23 +175,10 @@ def dms_to_degrees(v):
return d + (m / 60.0) + (s / 3600.0)
def get_exif_tags(source):
"""Read EXIF tags from file @source and return a tuple of two dictionaries,
the first one containing the raw EXIF data, the second one a simplified
version with common tags.
"""
def get_exif_tags(data):
"""Make a simplified version with common tags from raw EXIF data."""
logger = logging.getLogger(__name__)
if os.path.splitext(source)[1].lower() not in ('.jpg', '.jpeg'):
return (None, None)
try:
data = _get_exif_data(source)
except (IOError, IndexError, TypeError, AttributeError):
logger.warning(u'Could not read EXIF data from %s', source)
return (None, None)
simple = {}
# Provide more accessible tags in the 'simple' key
@ -209,8 +196,8 @@ def get_exif_tags(source):
elif isinstance(data['ExposureTime'], int):
simple['exposure'] = str(data['ExposureTime'])
else:
logger.warning('Unknown format for ExposureTime: %r (%s)',
data['ExposureTime'], source)
logger.warning('Unknown format for ExposureTime: %r',
data['ExposureTime'])
if 'ISOSpeedRatings' in data:
simple['iso'] = data['ISOSpeedRatings']
@ -227,8 +214,7 @@ def get_exif_tags(source):
else:
simple['datetime'] = dt
except (ValueError, TypeError) as e:
logger.warning(u'Could not parse DateTimeOriginal of %s: %s',
source, e)
logger.warning(u'Could not parse DateTimeOriginal: %s', e)
if 'GPSInfo' in data:
info = data['GPSInfo']
@ -242,7 +228,7 @@ def get_exif_tags(source):
lat = dms_to_degrees(lat_info)
lon = dms_to_degrees(lon_info)
except (ZeroDivisionError, ValueError):
logger.warning('Failed to read GPS info for %s', source)
logger.warning('Failed to read GPS info')
lat = lon = None
if lat and lon:
@ -251,4 +237,4 @@ def get_exif_tags(source):
'lon': - lon if lon_ref_info != 'E' else lon,
}
return (data, simple)
return simple

36
sigal/utils.py

@ -87,3 +87,39 @@ def call_subprocess(cmd):
stderr = stderr.decode('utf8')
stdout = stdout.decode('utf8')
return p.returncode, stdout, stderr
class cached_property(object):
'''Decorator for read-only properties evaluated only once.
© 2011 Christopher Arndt, MIT License
https://wiki.python.org/moin/PythonDecoratorLibrary#Cached_Properties
The value is cached in the '_cache' attribute of the object instance that
has the property getter method wrapped by this decorator. The '_cache'
attribute value is a dictionary which has a key for every property of the
object which is wrapped by this decorator. Each entry in the cache is
created only when the property is accessed for the first time and is a
two-element tuple with the last computed property value and the last time
it was updated in seconds since the epoch.
'''
def __call__(self, fget, doc=None):
self.fget = fget
self.__doc__ = doc or fget.__doc__
self.__name__ = fget.__name__
self.__module__ = fget.__module__
return self
def __get__(self, inst, owner):
try:
value = inst._cache[self.__name__]
except (KeyError, AttributeError):
value = self.fget(inst)
try:
cache = inst._cache
except AttributeError:
cache = inst._cache = {}
cache[self.__name__] = value
return value

10
tests/test_image.py

@ -5,7 +5,8 @@ import pytest
from PIL import Image
from sigal import init_logging
from sigal.image import generate_image, generate_thumbnail, get_exif_tags
from sigal.image import (generate_image, generate_thumbnail, get_exif_tags,
get_exif_data)
from sigal.settings import create_settings
CURRENT_DIR = os.path.dirname(__file__)
@ -62,13 +63,12 @@ def test_exif_copy(tmpdir):
settings = create_settings(img_size=(300, 400), copy_exif_data=True)
generate_image(src_file, dst_file, settings)
raw, simple = get_exif_tags(dst_file)
simple = get_exif_tags(get_exif_data(dst_file))
assert simple['iso'] == 50
settings['copy_exif_data'] = False
generate_image(src_file, dst_file, settings)
raw, simple = get_exif_tags(dst_file)
assert not raw
simple = get_exif_tags(get_exif_data(dst_file))
assert not simple
@ -82,7 +82,7 @@ def test_exif_gps(tmpdir):
settings = create_settings(img_size=(400, 300), copy_exif_data=True)
generate_image(src_file, dst_file, settings)
raw, simple = get_exif_tags(dst_file)
simple = get_exif_tags(get_exif_data(dst_file))
assert 'gps' in simple
lat = 34.029167

Loading…
Cancel
Save