add new exif.py to abstract between different exif libraries, and add partially working support for pyexiv2 and EXIFpy
authorTero Marttila <terom@fixme.fi>
Sun, 14 Jun 2009 23:43:40 +0300
changeset 120 55cb7fc9c8fb
parent 119 e7855eefb4c7
child 121 3bed35e79f41
add new exif.py to abstract between different exif libraries, and add partially working support for pyexiv2 and EXIFpy
degal/command.py
degal/config.py
degal/exif.py
degal/image.py
degal/main.py
degal/templates.py
degal/utils.py
--- a/degal/command.py	Sun Jun 14 22:59:29 2009 +0300
+++ b/degal/command.py	Sun Jun 14 23:43:40 2009 +0300
@@ -91,6 +91,7 @@
 
         except :
             # dump traceback
+            # XXX: skip all crap up to the actual function
             self.handle_error()
 
     def log_msg (self, level, msg, *args, **kwargs) :
--- a/degal/config.py	Sun Jun 14 22:59:29 2009 +0300
+++ b/degal/config.py	Sun Jun 14 23:43:40 2009 +0300
@@ -109,6 +109,12 @@
     # load Exif data for images, this may be slow
     with_exif           = False
 
+    # name of Exif handler to use
+    exif_handler_name   = None
+
+    # explicit Exif handler class
+    exif_handler        = None
+
     # exif tags used in output
     # Copyright (C) 2008, Santtu Pajukanta <santtu@pajukanta.fi>
     # XXX: import from dexif?
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/degal/exif.py	Sun Jun 14 23:43:40 2009 +0300
@@ -0,0 +1,137 @@
+"""
+    The various Exif backends that we have
+"""
+
+from __future__ import with_statement
+
+import utils
+
+class ExifHandler (object) :
+    """
+        Our generic interface for Exif data
+    """
+
+    def __init__ (self, file) :
+        """
+            Load Exif data from the given File.
+        """
+
+        self.file = file
+    
+    def image_tag (self, tag, default=None) :
+        """
+            Load and return the human-readable unicode-safe value for the given main image tag.
+        """
+
+        abstract
+
+    def image_tags (self, tags) :
+        """
+            Load and return the given tags for the main image as a sequence of (tag, value) tuples , following the same
+            rules as image_tag, except there should mainly be only one call to this function, encompassing all the
+            relevant tags.
+
+            The returned list should omit those values which were not found.
+        """
+    
+        for tag in tags :
+            # try and fetch value
+            value = self.image_tag(tag)
+
+            # filter out default not-found
+            if value is not None :
+                yield tag, value
+
+try :
+    import pyexiv2
+
+except ImportError :
+    PyExiv2_Handler = None
+
+else :
+    class PyExiv2_Handler (ExifHandler) :
+        NAME = 'pyexiv2'
+
+        def __init__ (self, file) :
+            """
+                Load as pyexiv2.Image
+            """
+            
+            # create
+            self.image = pyexiv2.Image(file.path)
+
+            # load
+            self.image.readMetadata()
+        
+        def image_tag (self, tag, default=None) :
+            try :
+                # XXX: this is wrong
+                return self.image.interpretedExifValue("Exif.Photo.%s" % tag)
+            
+            except (IndexError, KeyError) :
+                return default
+    
+try :
+    from lib import EXIF
+
+except ImportError :
+    EXIF_Handler = None
+
+else :
+    class EXIF_Handler (ExifHandler) :
+        NAME = 'EXIFpy'
+
+        def __init__ (self, file) :
+            """
+                Load using EXIF.process_file
+            """
+            
+            with file.open('rb') as fh :
+                self.tags = EXIF.process_file(fh)
+        
+        def image_tag (self, tag, default=None) :
+            # XXX: this is wrong
+            return self.tags.get('Image %s' % (tag, ), default)
+
+# ExifHandler implementations to use, in preference order
+EXIF_HANDLERS = [
+    # reasonably fast, but requires the native library and extension module
+    PyExiv2_Handler,
+
+    # pure-python, but very, very slow
+    EXIF_Handler,
+]
+
+# default ExifHandler to use
+EXIF_HANDLER = utils.first(EXIF_HANDLERS)
+
+# ExifHandler implementations by name
+EXIF_HANDLERS_BY_NAME = dict((h.NAME, h) for h in EXIF_HANDLERS)
+
+def load (config, file) :
+    """
+        Load Exif data using the configured Exif handler, and return the instance.
+
+        Returns None if there wasn't any Exif handler available for use.
+    """
+
+    if config.exif_handler :
+        # explicit
+        exif_handler = config.exif_handler
+
+    elif config.exif_handler_name :
+        # by name
+        exif_handler = EXIF_HANDLERS_BY_NAME[config.exif_handler_name]
+
+    else :
+        # default
+        exif_handler = EXIF_HANDLER
+    
+    if exif_handler :
+        # found one, use it
+        return exif_handler(file)
+
+    else :
+        # nothing :(
+        return None
+
--- a/degal/image.py	Sun Jun 14 22:59:29 2009 +0300
+++ b/degal/image.py	Sun Jun 14 23:43:40 2009 +0300
@@ -2,13 +2,10 @@
     Per-image gallery state
 """
 
-from __future__ import with_statement
-
-import filesystem, format, thumbnail
+import filesystem, format, thumbnail, exif
 from utils import lazy_load
 
 import PIL.Image
-from lib import EXIF
 
 class Image (filesystem.File) :
     """
@@ -57,22 +54,13 @@
     @lazy_load
     def exif (self) :
         """
-            Loads the EXIF data for the image and returns as a dict of EXIF tag names -> values.
+            Loads the ExifHandler object for this image.
 
             Returns None if no exif data was found
         """
 
-        # load
-        with self.open('rb') as fh :
-            # XXX: details=False?
-            exif = EXIF.process_file(fh)
-            
-        # empty dict -> no exif data
-        if exif :
-            return exif
-
-        else :
-            return None
+        # look up using exif
+        return exif.load(self.config, self)
     
     @lazy_load
     def metadata (self) :
@@ -87,20 +75,21 @@
         size = self.img_size
         
         # build
-        metadata = dict({
-            "File name":        self.name,
-            "Resolution":       "%dx%d" % size,
-            "File size":        format.filesize(stat.st_size),
-            "Last modified":    format.filetime(stat.st_mtime),
-        })
+        metadata = [
+            ("File name",       self.name),
+            ("Resolution",      "%dx%d" % size),
+            ("File size",       format.filesize(stat.st_size)),
+            ("Last modified",   format.filetime(stat.st_mtime)),
+        ]
         
         # optionally load Exif metadata
-        if self.config.with_exif :
-            exif = self.exif
+        if self.config.with_exif and self.exif :
+            exif_tag_name = dict(self.config.exif_tags)
+            exif_tags = (tag for tag, name in self.config.exif_tags)
 
-            # Get the wanted tags
-            metadata.update(dict(
-                (name, exif[tag]) for tag, name in self.config.exif_tags if exif and tag in exif
+            # merge the wanted tags
+            metadata.extend((
+                (exif_tag_name[tag], value) for tag, value in self.exif.image_tags(exif_tags)
             ))
 
         return metadata
--- a/degal/main.py	Sun Jun 14 22:59:29 2009 +0300
+++ b/degal/main.py	Sun Jun 14 23:43:40 2009 +0300
@@ -38,6 +38,9 @@
     
     parser.add_option("--with-exif",            action="store_true",
             help="Include Exif metadata in updated .html files")
+    
+    parser.add_option("--exif-handler",         metavar='NAME', dest="exif_handler_name",
+            help="Use named Exif handler: pyexiv2, EXIFpy")
 
     parser.add_option('-c', "--thread-count",   metavar='COUNT', type="int",
             help="Use COUNT threads for concurrent tasks")
--- a/degal/templates.py	Sun Jun 14 22:59:29 2009 +0300
+++ b/degal/templates.py	Sun Jun 14 23:43:40 2009 +0300
@@ -51,7 +51,7 @@
 
         # extended info, metadata
         tags.div(id_='info')(*(
-            tags.p(("%s: " % name), value) for name, value in image.metadata.iteritems()
+            tags.p(("%s: " % name), value) for name, value in image.metadata
         )),
     ]
 
--- a/degal/utils.py	Sun Jun 14 22:59:29 2009 +0300
+++ b/degal/utils.py	Sun Jun 14 23:43:40 2009 +0300
@@ -77,6 +77,26 @@
 lazy_load = LazyProperty
 lazy_load_iter = LazyIteratorProperty
 
+def first (iterable) :
+    """
+        Returns the first item from the iterable that evaluates to True, otherwise None.
+
+        >>> first((0, 1))
+        1
+        >>> first("abc")
+        'a'
+        >>> first(('', list(), (), False))
+        None
+    """
+
+    for item in iterable :
+        if item :
+            return item
+
+    else :
+        return None
+
+
 # testing
 if __name__ == '__main__' :
     import doctest