start writing new structure, with config, render, filesystem modules
authorTero Marttila <terom@fixme.fi>
Thu, 04 Jun 2009 11:22:52 +0300
changeset 51 0f39cb5e4b11
parent 50 724121253d34
child 52 3071d0709c4a
start writing new structure, with config, render, filesystem modules
degal/config.py
degal/filesystem.py
degal/render.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/degal/config.py	Thu Jun 04 11:22:52 2009 +0300
@@ -0,0 +1,23 @@
+"""
+    Configuration
+"""
+
+class Configuration (object) :
+    """
+        Various configuration settings
+    """
+
+    # the name of the gallery
+    gallery_title       = "Image Gallery"
+
+    # recognized image extensions
+    image_exts          = ('jpg', 'jpeg', 'png', 'gif', 'bmp')
+    
+    # subdirectory used for generated thumbnails/previews
+    thumb_dir           = 'thumbs'
+    preview_dir         = 'previews'
+
+    # size of generated thumbnails/previews
+    thumb_size          = (160, 120)
+    preview_size        = (640, 480)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/degal/filesystem.py	Thu Jun 04 11:22:52 2009 +0300
@@ -0,0 +1,322 @@
+"""
+    Filesystem path handling
+"""
+
+import os, os.path
+
+class Node (object) :
+    """
+        A filesystem object is basically just complicated representation of a path.
+        
+        On the plus side, it has a parent node and can handle unicode/binary paths.
+    """
+
+    # the binary name
+    fsname = None
+
+    # the unicode name
+    name = None
+    
+    def decode_fsname (self, fsname) :
+        """
+            Decode the given raw byte string representing a filesystem name into an user-readable unicode name.
+
+            XXX: currently just hardcoded as utf-8
+        """
+
+        return fsname.decode('utf-8', 'replace')
+    
+    def encode_name (self, name) :
+        """
+            Returns a suitable fsname for the given unicode name or strict ASCII str
+
+            XXX: currently just hardcoded as utf-8
+        """
+        
+        # this should fail for non-ASCII str
+        return name.encode('utf-8')
+
+    @classmethod
+    def from_node (cls, node) :
+        """
+            Construct from a Node object
+        """
+
+        return cls(node.parent, node.fsname, node.name, node.config)
+
+    def __init__ (self, parent, fsname=None, name=None, config=None) :
+        """
+            Initialize the node with a parent and both name/fsname.
+
+            If not given, fsname is encoded from name, or name decoded from fsname, using encode/decode_name.
+
+            If parent is given, but both fsname and name are None, then this node will be equal to the parent.
+        """
+
+        assert not fsname or isinstance(fsname, str)
+
+        if parent and not fsname and not name :
+            # no name given -> we're the same as parent
+            self.parent, self.config, self.fsname, self.name = parent.parent, parent.config, parent.fsname, parent.name
+
+        else :
+            # store
+            self.parent = parent
+            
+            # config, either as given, or copy from parent
+            if config :
+                self.config = config
+
+            else :
+                self.config = parent.config
+     
+            # fsname
+            if fsname :
+                self.fsname = fsname
+
+            else :
+                self.fsname = self.encode_name(name)
+           
+            # name
+            if name :
+                self.name = name
+
+            else :
+                self.name = self.decode_fsname(fsname)
+        
+    def subnode (self, name) :
+        """
+            Returns a Node object representing the given name behind this node.
+
+            The name should either be a plain ASCII string or unicode object.
+        """
+        
+        return Node(self, name=name)
+ 
+    @property
+    def path (self) :
+        """
+            Build and return the real filesystem path for this node
+        """
+        
+        # build using parent path and our fsname
+        return os.path.join(self.parent.path, self.fsname)
+    
+    @property
+    def unicodepath (self) :
+        """
+            Build and return the fake unicode filesystem path for this node
+        """
+        
+        # build using parent unicodepath and our name
+        return os.path.join(self.parent.path, self.name)
+   
+    def exists (self) :
+        """
+            Tests if this node exists on the physical filesystem
+        """
+
+        return os.path.exists(self.path)
+
+    def is_dir (self) :
+        """
+            Tests if this node represents a directory on the filesystem
+        """
+
+        return os.path.isdir(self.path)
+
+    def is_file (self) :
+        """
+            Tests if this node represents a normal file on the filesystem
+        """
+
+        return os.path.isfile(self.path)
+    
+    def __str__ (self) :
+        """
+            Returns the raw filesystem path
+        """
+
+        return self.path
+
+    def __unicode__ (self) :
+        """
+            Returns the human-readable path
+        """
+
+        return self.unicodepath
+    
+    def __repr__ (self) :
+        """
+            Returns a str representing this dir
+        """
+
+        return "Node(%r, %r)" % (self.parent.path, self.fsname)
+    
+    def __cmp__ (self) :
+        """
+            Comparisons between Nodes
+        """
+
+        return cmp((self.parent, self.name), (other.parent, other.name))
+
+class File (Node) :
+    """
+        A file. Simple, eh?
+    """
+
+    @property
+    def basename (self) :
+        """
+            Returns the "base" part of this file's name, i.e. the filename without the extension
+        """
+
+        basename, _ = os.path.splitext(self.name)
+
+        return basename
+    
+    @property
+    def fileext (self) :
+        """
+            Returns the file extension part of the file's name, without any leading dot
+        """
+
+        _, fileext = os.path.splitext(self.name)
+
+        return fileext.rstrip('.')
+
+    def matchext (self, ext_list) :
+        """
+            Tests if this file's extension is part of the recognized list of extensions
+        """
+        
+        return (self.fileext.lower() in ext_list)
+
+class Directory (Node) :
+    """
+        A directory is a node that contains other nodes.
+    """
+
+    # a list of (test_func, node_type) tuples for use by children() to build subnodes with
+    NODE_TYPES = None
+   
+    def subdir (self, name) :
+        """
+            Returns a Directory object representing the name underneath this dir
+        """
+
+        return Directory(self, name=name)
+    
+    def subfile (self, name) :
+        """
+            Returns a File object representing the name underneath this dir
+        """
+
+        return Directory(self, name=name)
+
+    def listdir (self, skip_dotfiles=True) :
+        """
+            Yield a series of raw fsnames for nodes in this dir
+        """
+        
+        # expressed 
+        return (fsname for fsname in os.listdir(self.path) if not (skip_dotfiles and fsname.startswith('.')))
+
+    def child_nodes (self, skip_dotfiles=True) :
+        """
+            Yield a series of nodes contained in this dir
+        """
+
+        return (Node(self, fsname) for fsname in self.listdir(skip_dotfiles))
+
+    def __iter__ (self) :
+        """
+            Iterating over a Directory yields sub-Nodes.
+
+            Dotfiles are skipped.
+        """
+        
+        return self.childnodes()
+
+    @property
+    def root_path (self) :
+        """
+            Build and return a relative path to the root of this dir tree
+        """
+        
+        # build using parent root_path
+        return os.path.join('..', self.parent.root_path)
+ 
+    def children (self) :
+        """
+            Yield a series of Node subclasses representing the items in this dir.
+            
+            This uses self.NODE_TYPES to figure out what kind of sub-node object to build. This should be a list of
+                (test_func, node_type)
+
+            tuples, of which the first is a function that takes a Node as it's sole argument, and returns a boolean.
+            For the first test_func which returns True, a Node-subclass object is constructed using node_type.from_node.
+        """
+
+        for node in self :
+            # figure out what type to use
+            for test_func, node_type in self.NODE_TYPES :
+                if test_func(node) :
+                    # matches, build
+                    yield node_type.from_node(node)
+
+            else :
+                # unknown file type!
+                raise Exception("unrecongized type of file: %s" % node);
+
+# assign default Directory.NODE_TYPES
+Directory.NODE_TYPES = [
+    (Node.is_dir,   Directory),
+    (Node.is_file,  File),
+]
+
+
+class Root (Directory) :
+    """
+        A special Directory that overrides the Node methods to anchor the recursion/etc at some 'real' filesystem path.
+    """
+
+    def __init__ (self, fspath, config) :
+        """
+            Construct the directory tree root at the given 'real' path, which must be a raw str
+        """
+
+        # abuse Node's concept of a "name" a bit
+        super(Root, self).__init__(None, fspath, config=config)
+
+    @property
+    def path (self) :
+        """
+            Returns the raw path
+        """
+
+        return self.fsname
+
+    @property
+    def unicodepath (self) :
+        """
+            Returns the raw decoded path
+        """
+
+        return self.name
+
+    @property
+    def root_path (self) :
+        """
+            Returns an empty string representing this dir
+        """
+
+        return ''
+
+    def __repr__ (self) :
+        """
+            Override Node.__repr__ to not use self.parent.path
+        """
+
+        return "Root(%r)" % self.fsname
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/degal/render.py	Thu Jun 04 11:22:52 2009 +0300
@@ -0,0 +1,51 @@
+"""
+    Rendering images as thumbnails/previews.
+"""
+
+import PIL.Image
+
+import os.path
+
+class RenderMachine (object) :
+    """
+        RAWR! I'm the render machine!
+
+        TODO: use multithreaded rendering (PIL supports it)
+    """
+
+    def __init__ (self, config) :
+        """
+            Use the given Configuration object's settings for rendering
+        """
+
+        self.config = config
+
+    def render_out (self, img, size, out_path) :
+        """
+            Creates a thumbnail from the given PIL.Image of the given size, and saves it at out_path.
+        """
+
+        # we need to create a copy, as .thumbnail mutates the Image
+        img_out = img.copy()
+
+        # then resample to given size
+        img_out.thumbnail(size, resample=True)
+
+        # and write out
+        img_out.save(out_path)
+
+    def render_img (self, img, dirname, filename) :
+        """
+            Renders new thumbnails/previews from the given loaded image and filename based on the current configuration.
+            
+            Note: this does not check for the existance of the thumbs/previews subdirs!
+        """
+
+        # once for each size
+        for subdir, size in (
+            (self.config.thumb_dir, self.config.thumb_size),
+            (self.config.preview_dir, self.config.preview_size)
+        ) :
+            # resize and write out
+            self.render_out(img, size, os.path.join(dirname, subdir, filename)
+