Use cached properties to simplify the access to exif metadata.
This commit is contained in:
@@ -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
|
||||
return self.exif and self.exif.get('dateobj', None) or None
|
||||
|
||||
@date.setter
|
||||
def date(self, value):
|
||||
self._date = value
|
||||
|
||||
@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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user