diff -r 27dac27d1a58 -r c2d8e9a754a1 degal.py --- a/degal.py Thu Dec 20 17:42:04 2007 +0000 +++ b/degal.py Fri Dec 21 20:36:03 2007 +0000 @@ -21,647 +21,11 @@ # import os.path, os -import urllib -import logging -import string -from datetime import datetime -import struct -import base64 -import shelve - -import PIL -import PIL.Image - -import utils - -__version__ = '0.2' - -TEMPLATE_DIR='templates' -TEMPLATE_EXT='html' - -logging.basicConfig( - level=logging.INFO, - format="%(message)s", -# format="%(name)8s %(levelname)8s %(lineno)3d %(message)s", - -) - -tpl = logging.getLogger('tpl') -index = logging.getLogger('index') -render = logging.getLogger('render') - -tpl.setLevel(logging.WARNING) - -#for l in (tpl, index, prepare, render) : -# l.setLevel(logging.DEBUG) - -def readFile (path) : - fo = open(path, 'r') - data = fo.read() - fo.close() - - return data - -class Template (object) : - GLOBALS = dict( - VERSION=__version__, - ) - - def __init__ (self, name) : - self.name = name - self.path = os.path.join(TEMPLATE_DIR, "%s.%s" % (name, TEMPLATE_EXT)) - - tpl.debug("Template %s at %s", name, self.path) - - self.content = readFile(self.path) - - def render (self, **vars) : - content = self.content - - vars.update(self.GLOBALS) - - for name, value in vars.iteritems() : - content = content.replace('' % name, str(value)) - - return content - - def renderTo (self, path, **vars) : - tpl.info("Render %s to %s", self.name, path) - - fo = open(path, 'w') - fo.write(self.render(**vars)) - fo.close() - -gallery_tpl = Template('gallery') -image_tpl = Template('image') - -IMAGE_EXTS = ('jpg', 'jpeg', 'png', 'gif', 'bmp') - -THUMB_DIR = 'thumbs' -PREVIEW_DIR = 'previews' -TITLE_FILE = 'title.txt' - -THUMB_GEOM = (160, 120) -PREVIEW_GEOM = (640, 480) - -DEFAULT_TITLE = 'Image gallery' - -# how many image/page -IMAGE_COUNT = 50 - -def isImage (fname) : - """ - Is the given filename likely to be an image file? - """ - - fname = fname.lower() - base, ext = os.path.splitext(fname) - ext = ext.lstrip('.') - - return ext in IMAGE_EXTS - -def link (url, title) : - """ - Returns an link tag with the given values - """ - - return "%s" % (urllib.quote(url), title) - -def dirUp (count=1) : - """ - Returns a relative path to the directly count levels above the current one - """ - - if not count : - return '.' - - return os.path.join(*(['..']*count)) - -def readTitleDescr (path) : - """ - Read a title.txt or .txt file - """ - - if os.path.exists(path) : - content = readFile(path) - - if '---' in content : - title, descr = content.split('---', 1) - else : - title, descr = content, '' - - return title.strip(), descr.strip() - - return None, None - -class Folder (object) : - def __init__ (self, name='.', parent=None) : - # the directory name - self.name = name - - # our parent Folder, or None - self.parent = parent - - # the path to this dir, as a relative path to the root of the image gallery, always starts with . - if parent and name : - self.path = parent.pathFor(name) - else : - self.path = name - - # the url-path to the index.html file - self.html_path = self.path - - # dict of fname -> Folder - self.subdirs = {} - - # dict of fname -> Image - self.images = {} - - # our human-friendly title - self.title = None - - # our long-winded description - self.descr = '' - - # is this folder non-empty? - self.alive = None - - # self.images.values(), but sorted by filename - self.sorted_images = [] - - # the ShortURL key to this dir - self.shorturl_code = None - - # were we filtered out? - self.filtered = False - - def pathFor (self, *fnames) : - """ - Return a root-relative path to the given path inside this dir - """ - return os.path.join(self.path, *fnames) - - def index (self, filters=None) : - """ - Look for other dirs and images inside this dir. Filters must be either None, - whereupon all files will be included, or a dict of {filename -> next_filter}. - If given, only filenames that are present in the dict will be indexed, and in - the case of dirs, the next_filter will be passed on to that Folder's index - method. - """ - - index.info("Indexing %s", self.path) - - if filters : - self.filtered = True - - # iterate through listdir - for fname in os.listdir(self.path) : - # the full filesystem path to it - fpath = self.pathFor(fname) - - # ignore dotfiles - if fname.startswith('.') : - index.debug("Skipping dotfile %s", fname) - continue - - # apply filters - if filters : - if fname in filters : - next_filter = filters[fname] - else : - index.debug("Skip `%s' as we have a filter", fname) - continue - else : - next_filter = None - - # recurse into subdirs, but not thumbs/previews - if os.path.isdir(fpath) and fname not in (THUMB_DIR, PREVIEW_DIR) : - index.debug("Found subdir %s", fpath) - f = self.subdirs[fname] = Folder(fname, self) - if f.index(next_filter) : # recursion - # if a subdir is alive, we are alive as well - self.alive = True - - # handle images - elif os.path.isfile(fpath) and isImage(fname) : - index.debug("Found image %s", fname) - self.images[fname] = Image(self, fname) - - # ignore everything else - else : - index.debug("Ignoring file %s", fname) - - # sort and link the images - if self.images : - self.alive = True - - # sort the images - fnames = self.images.keys() - fnames.sort() - - prev = None - - # link - for fname in fnames : - img = self.images[fname] - - img.prev = prev - - if prev : - prev.next = img - - prev = img - - # add to the sorted images list - self.sorted_images.append(img) - - return self.alive - - def getObjInfo (self) : - """ - Metadata for shorturls2.db - """ - return 'dir', self.path, 'index' - - def linkTag (self) : - """ - A text-link to this dir - """ - - return link(self.path, self.title) - - def breadcrumb (self) : - """ - Returns a [(fname, title)] list of this dir's parent dirs - """ - - f = self - b = [] - d = 0 - - while f : - b.insert(0, (dirUp(d), f.title)) - - d += 1 - f = f.parent - - return b - - def inRoot (self, *fnames) : - """ - Return a relative URL from this dir to the given path in the root dir - """ - - c = len(self.path.split('/')) - 1 - - return os.path.join(*((['..']*c) + list(fnames))) - - def _page_fname (self, page) : - assert page >= 0 - - if page > 0 : - return 'index_%d.html' % page - else : - return 'index.html' - - def render (self) : - """ - Render the index.html, Images, and recurse into subdirs - """ - - # ded folders are skipped - if not self.alive : - render.info("Skipping dir %s", self.path) - return - - # if this dir's contents were filtered out, then we can't render the index.html, as we aren't aware of all the images in here - if self.filtered : - render.warning("Dir `%s' contents were filtered, so we won't render the gallery index again", self.path) +from optparse import OptionParser - else : - # create the thumb/preview dirs if needed - for dir in (THUMB_DIR, PREVIEW_DIR) : - path = self.pathFor(dir) - - if not os.path.isdir(path) : - render.info("Creating dir %s", path) - os.mkdir(path) - - # figure out our title - title_path = self.pathFor(TITLE_FILE) - - title, descr = readTitleDescr(title_path) - - if title : - self.title = title - self.descr = descr - - # default title for the root dir - elif self.name == '.' : - self.title = 'Index' - - else : - self.title = self.name - - # sort the subdirs - subdirs = self.subdirs.items() - subdirs.sort() - - # generate the 's for the subdirs - subdir_linkTags = [link(f.name, f.title) for fname, f in subdirs if f.alive] - - # stick them into a list - if subdir_linkTags : - directories = "" % "\n\t
  • ".join(subdir_linkTags) - else : - directories = '' - - render.info("Rendering %s", self.path) - - # paginate! - images = self.sorted_images - image_count = len(images) - pages = [] - - while images : - pages.append(images[:IMAGE_COUNT]) - images = images[IMAGE_COUNT:] - - pagination_required = len(pages) > 1 - - if pagination_required : - render.info("Index split into %d pages of %d images each", len(pages), IMAGE_COUNT) - - for cur_page, page in enumerate(pages) : - if pagination_required : - pagination = "" - shorturl = "%s/%s" % (self.shorturl_code, cur_page+1) - else : - pagination = '' - shorturl = self.shorturl_code - - # render to index.html - gallery_tpl.renderTo(self.pathFor(self._page_fname(cur_page)), - STYLE_URL=self.inRoot('style.css'), - BREADCRUMB=" » ".join([link(u, t) for (u, t) in self.breadcrumb()]), - TITLE=self.title, - DIRECTORIES=directories, - PAGINATION=pagination, - CONTENT="".join([i.thumbImgTag() for i in page]), - DESCR=self.descr, - SHORTURL=self.inRoot('s', shorturl), - SHORTURL_CODE=shorturl, - ) - - # render images - for img in self.images.itervalues() : - img.render() - - # recurse into subdirs - for dir in self.subdirs.itervalues() : - dir.render() - -class Image (object) : - def __init__ (self, dir, name) : - # the image filename, e.g. DSC3948.JPG - self.name = name - - # the Folder object that we are in - self.dir = dir - - # the relative path from the root to us - self.path = dir.pathFor(name) - - # the basename+ext, e.g. DSCR3948, .JPG - self.base_name, self.ext = os.path.splitext(name) - - # the root-relative paths to the thumb and preview images - self.thumb_path = self.dir.pathFor(THUMB_DIR, self.name) - self.preview_path = self.dir.pathFor(PREVIEW_DIR, self.name) - - # our user-friendly title - self.title = name - - # our long-winded description - self.descr = '' - - # the image before and after us, both may be None - self.prev = self.next = None - - # the name of the .html gallery view thing for this image, *always* self.name + ".html" - self.html_name = self.name + ".html" - - # the root-relative path to the gallery view - self.html_path = self.dir.pathFor(self.html_name) - - # - # Figured out after prepare - # - - # (w, h) tuple - self.img_size = None - - # the ShortURL code for this image - self.shorturl_code = None - - # what to use in the rendered templates, intended to be overridden by subclasses - self.series_act = "add" - self.series_verb = "Add to" - - def getObjInfo (self) : - """ - Metadata for shorturl2.db - """ - return 'img', self.dir.path, self.name - - def thumbImgTag (self) : - """ - a of this image's thumbnail. Path relative to directory we are in - """ - return link(self.html_name, "%s" % (os.path.join(THUMB_DIR, self.name), self.descr, self.title)) +from lib import folder, shorturl - def previewImgTag (self) : - """ - a of this image's preview. Path relative to directory we are in - """ - return link(self.name, "%s" % (os.path.join(PREVIEW_DIR, self.name), self.descr, self.title)) - - def linkTag (self) : - """ - a text-link to this image - """ - return link(self.html_name, self.title) - - def breadcrumb (self) : - """ - Returns a [(fname, title)] list of this image's parents - """ - - f = self.dir - b = [(self.html_name, self.title)] - d = 0 - - while f : - b.insert(0, (dirUp(d), f.title)) - - d += 1 - f = f.parent - - return b - - def render (self) : - """ - Write out the .html file - """ - - # stat the image file to get the filesize and mtime - st = os.stat(self.path) - - self.filesize = st.st_size - self.timestamp = st.st_mtime - - # open the image in PIL to get image attributes + generate thumbnails - img = PIL.Image.open(self.path) - - self.img_size = img.size - - for out_path, geom in ((self.thumb_path, THUMB_GEOM), (self.preview_path, PREVIEW_GEOM)) : - # if it doesn't exist, or it's older than the image itself, generate - if not (os.path.exists(out_path) and os.stat(out_path).st_mtime > self.timestamp) : - render.info("Create thumbnailed image at %s with geom %s", out_path, geom) - - # XXX: is this the most efficient way to do this? - out_img = img.copy() - out_img.thumbnail(geom, resample=True) - out_img.save(out_path) - - # look for the metadata file - title_path = self.dir.pathFor(self.base_name + '.txt') - - title, descr = readTitleDescr(title_path) - - if title : - self.title = title - self.descr = descr - - render.info("Rendering image %s", self.path) - - image_tpl.renderTo(self.html_path, - STYLE_URL=self.dir.inRoot('style.css'), - UP_URL=('.'), - PREV_URL=(self.prev and self.prev.html_name or ''), - NEXT_URL=(self.next and self.next.html_name or ''), - FILE=self.name, - BREADCRUMB=" » ".join([link(u, t) for u, t in self.breadcrumb()]), - TITLE=self.title, - PREVIOUS_THUMB=(self.prev and self.prev.thumbImgTag() or ''), - IMAGE=self.previewImgTag(), - NEXT_THUMB=(self.next and self.next.thumbImgTag() or ''), - DESCRIPTION=self.descr, - IMGSIZE="%dx%d" % self.img_size, - FILESIZE=fmtFilesize(self.filesize), - TIMESTAMP=fmtTimestamp(self.timestamp), - SHORTURL=self.dir.inRoot('s', self.shorturl_code), - SHORTURL_CODE=self.shorturl_code, - SERIES_URL=self.dir.inRoot('series/%s/%s' % (self.series_act, self.shorturl_code)), - SERIES_VERB=self.series_verb, - ) - - def __str__ (self) : - return "Image `%s' in `%s'" % (self.name, self.dir.path) - -def int2key (id) : - """ - Turn an integer into a short-as-possible url-safe string - """ - for type in ('B', 'H', 'I') : - try : - return base64.b64encode(struct.pack(type, id), '-_').rstrip('=') - except struct.error : - continue - - raise Exception("ID overflow: %s" % id) - -def updateShorturlDb (root) : - """ - DeGAL <= 0.2 used a simple key => path mapping, but now we use - something more structured, key => (type, dirpath, fname), where - - type - one of 'img', 'dir' - dirpath - the path to the directory, e.g. '.', './foobar', './foobar/quux' - fname - the filename, one of '', 'DSC9839.JPG', 'this.png', etc. - """ - - db = shelve.open('shorturls2', 'c', writeback=True) - - id = db.get('_id', 1) - - dirqueue = [root] - - # dict of path -> obj - paths = {} - - index.info("Processing ShortURLs...") - - while dirqueue : - dir = dirqueue.pop(0) - - dirqueue.extend(dir.subdirs.itervalues()) - - if dir.alive : - paths[dir.path] = dir - - for img in dir.images.itervalues() : - paths[img.path] = img - - for key in db.keys() : - if key.startswith('_') : - continue - - type, dirpath, fname = db[key] - - path = os.path.join(dirpath, fname).rstrip('/') - - try : - paths.pop(path).shorturl_code = key - index.debug("Code for `%s' is %s", path, key) - - except KeyError : - index.debug("Path `%s' in DB does not exist?", path) - - for obj in paths.itervalues() : - key = int2key(id) - id += 1 - - index.info("Alloc code `%s' for `%s'", key, obj.html_path) - - obj.shorturl_code = key - - db[key] = obj.getObjInfo() - - db['_id'] = id - db.close() - -def main (targets=()) : +def main (dir='.', targets=()) : root_filter = {} for target in targets : @@ -670,24 +34,19 @@ if path_part : if path_part not in f : f[path_part] = {} + f = f[path_part] - - index.debug('Filter: %s', root_filter) - root = Folder() + root = folder.Folder(dir) root.index(root_filter) - updateShorturlDb(root) + shorturl.updateDB(root) root.render() -def fmtFilesize (size) : - return utils.formatbytes(size, forcekb=False, largestonly=True, kiloname='KiB', meganame='MiB', bytename='B', nospace=False) - -def fmtTimestamp (ts) : - return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") - if __name__ == '__main__' : - from sys import argv - argv.pop(0) - - main(argv) - + parser = OptionParser(usage="usage: %prog [options] ... [target ...]") + + parser.add_option("-d", "--dir", dest="dir", help="look for images in DIR and write the HTML there", metavar="DIR", default=".") + + options, filter_targets = parser.parse_args() + + main(options.dir, filter_targets)