#!/usr/bin/python # -*- coding: UTF8 -*- #******************************************************************************\ #* $Source$ #* $Id$ #* #* Copyright (C) 2001, Martin Blais #* Copyright (C) 2004-2011, Jerome Kieffer #* #* This program is free software; you can redistribute it and/or modify #* it under the terms of the GNU General Public License as published by #* the Free Software Foundation; either version 2 of the License, or #* (at your option) any later version. #* #* This program is distributed in the hope that it will be useful, #* but WITHOUT ANY WARRANTY; without even the implied warranty of #* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #* GNU General Public License for more details. #* #* You should have received a copy of the GNU General Public License #* along with this program; if not, write to the Free Software #* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. #* #*****************************************************************************/ """Generate HTML image gallery pages. generator is a powerful script that allows one to generate Web page image galleries with the intent of displaying photographic images on the Web, or for a CD-ROM presentation and archiving. It generates static Web pages only - no special configuration or running scripts are required on the server. The script supports many file formats, hierarchical directories, thumbnail generation and update, per-image description file with any attributes, and 'tracks' of images spanning multiple directories. The templates consist of HTML with embedded Python. Running this script only requires a recent Python interpreter (version 2 or more) and the ImageMagick tools. All links it generates are relative links, so that the pages can be moved or copied to different media. Each image page and directory can be associated any set of attributes which become available from the template (this way you can implement descriptions, conversion code, camera settings, and more). You can find the latest version at http://curator.sourceforge.net Input ------------------------------ The image files need to be organized in a directory structure. For each image, the following is required: - .: the main image file to be displayed in the html page, where is an image extension (e.g. jpg, gif, etc.) The following is optional, and will be used if present: - .desc: a per-image description file containing user-provided attributes about the photograph. The format is, e.g.: : : ... Each attribute text is ended with a blank line. You can inclue all the attribute fields you want, it is up to the template file to access them or not. There are, however, some special predefined attributes: - title: A descriptive title for the image (a short one liner). - tracks: ... specifies the tracks that the image is part of - ignore: yes specifies that the image should be ignored - --.: alternative representations of the image. Could be the original scan plate, or alternative resolutions, or anything else related to this image. The image html page can add links to these alternative representations. We assume that we only need to generate an HTML page for the main resolution (i.e. smaller resolutions won't have associated web pages) To configure the generated HTML files, use --save-templates and modify the code that will appear in the output directory. The following files can be put in the root: - template-image.html: template for image HTML file - template-allindex.html: template for global index HTML file - template-dirindex.html: template for directory index HTML file - template-trackindex.html: template for track index HTML file - template-sortindex.html: template for sorted index HTML file The template is a normal HTML file, the way you like it, except that it contains certain special tags that get evaluated by the script in a special environment nwhich contains useful variables and functions. You can use the following two tags: The second tag is implemented as 'print ,'. You can put definitions, function calls, whatever you like. Variable bindings and definitions will remain between tags. The templates are looked up in the following order: - user-specified path (-templates option) - the root of the hierarchy - the dir specified in the env var CURATOR_TEMPLATE If not found, simple fallback templates are used. Remember that under unix, processing python code with carriage returns will fail the python interpreter with a Syntax Error. For a complete description of the environment, look at the code. Output ------------------------------ Note by default nothing that already exists is overwritten. Use the --no* or --force* options to disable or force thumbnails, indexes and image pages. Directories which do not contain images (and whose subdirectories do not contain images) will be ignored. For each image: - --thumb.: associated image thumbnail. - .html (for each image, an associated web page which features it) Thus you will end up with the following files for each image: - . - .desc - --. - --. - ... - --thumb. - .html In each subdirectory of the root: - dirindex.html: an HTML index of the directory, with thumbnails and titles. In the root: - trackindex-.html: for each track, an HTML index of the track, with thumbnails and titles. - allindex.cidx: a text index of all the pictures, with image filenames and titles, each on a single line. - allindex.html: an HTML index of all the pictures, with thumbnails and titles, and list of tracks. - sortindex.html: an HTML index of all the pictures, with some form of sorting This output can be used as a global index for sorting images by name/date/whatever. The images in the author's photo gallery are named by date so sorting by name is sorting by date, which the default template implements. Usage: ------------------------------ generator [] If is not specified, we assume cwd is the root. """ # Developer's manual (ahahah): # # Rules for filenames: # # All filenames are relative to the root. It is up to the template implementor # to call the rel method to generate relative links. This simplifies # handling of paths a lot. __version__ = "$Revision$" __version_pr__ = '2.0' __author__ = "Martin Blais " #=============================================================================== # EXTERNAL DECLARATIONS #=============================================================================== import sys, logging import imagizer logger = logging.getLogger("imagizer.generator") from imagizer.encoding import unicode2html if int(sys.version[0]) != 2: logger.error("You need Python version 2 to run generator.") sys.exit(1) import os, dircache import os.path as op from os.path import join, dirname, basename, normpath, splitext from os.path import isfile, islink, isdir, exists import re, string, time, random, locale import StringIO from pprint import pprint, pformat import urllib import imghdr import stat import distutils.sysconfig #here we detect the OS runnng the program so that we can call exftran in the right way installdir = op.join(distutils.sysconfig.get_python_lib(), "imagizer") if os.name == 'nt': #sys.platform == 'win32': ConfFile = [op.join(os.getenv("ALLUSERSPROFILE"), "imagizer.conf"), op.join(os.getenv("USERPROFILE"), "imagizer.conf")] elif os.name == 'posix': ConfFile = ["/etc/imagizer.conf", op.join(os.getenv("HOME"), ".imagizer")] else: raise "Your platform does not seem to be an Unix nor a M$ Windows.\nI am sorry but the exiftran binary is necessary to run selector, and exiftran is probably not available for you plateform. If you have exiftran installed, please contact the developper to correct that bug, kieffer at terre-adelie dot org" sys.exit(1) from imagizer.config import Config from imagizer.exif import Exif from imagizer.parser import AttrFile config = Config() config.load(ConfFile) if os.getenv("LANGUAGE"): try: locale.setlocale(locale.LC_ALL, os.getenv("LANGUAGE")) except: locale.setlocale(locale.LC_ALL, config.Locale) else: locale.setlocale(locale.LC_ALL, config.Locale) from traceback import print_exception #=============================================================================== # PUBLIC DECLARATIONS #=============================================================================== #=============================================================================== # fcache module #=============================================================================== #=============================================================================== # PUBLIC DECLARATIONS #=============================================================================== def urlquote(text): """same as urllib.quote but windows compliant""" if op.sep == "\\": return urllib.quote(text.replace("\\", "/")) else: return urllib.quote(text) class Entry: def __init__(self, fn, mtime, size, info): self.fn = fn self.mtime = mtime self.size = size self.info = info class FCache: mre = re.compile('^"([^"]*)" (\d+) (\d+): (.*)$') def __init__(self, filename): "Initialize the fcache, reading the given file." self.cachefn = filename self.entries = {} if exists(self.cachefn): f = open(self.cachefn, 'r') lines = f.readlines() for line in lines: mo = FCache.mre.match(line) if mo: (fn, mtime, size, info) = mo.groups() self.entries[fn] = Entry(fn, int(mtime), int(size), info) f.close() def store(self, fn, info): s = os.stat(fn) self.entries[fn] = Entry(fn, s[stat.ST_MTIME], s[stat.ST_SIZE], info) # write out cache try: f = open(self.cachefn, 'w') for e in self.entries.values(): print >> f, '"%s" %d %d: %s' % (e.fn, e.mtime, e.size, e.info) f.close() except IOError: print >> sys.stderr, "Error writing out cache" def lookup(self, fn): try: e = self.entries[fn] except KeyError: return None s = os.stat(fn) if e.mtime != s[stat.ST_MTIME] or e.size != s[stat.ST_SIZE]: return None return e.info def dump(self): if not opts.quiet: return None print "Cache filename:", self.cachefn print for e in self.entries.values(): print "filename:", e.fn print "mtime:", e.mtime print "size:", e.size print "info:", e.info print #=============================================================================== # DIRECTORY NAMES #=============================================================================== #------------------------------------------------------------------------------- # def splitpath(path): """Splits a path into a list of components. This function works around a quirk in string.split().""" # FIXME perhaps call op.split repetitively would be better. #s = string.split( path, '/' ) # we work with fwd slash only inside. #We have decided to use all kind of separator s = [] while True: first, second = op.split(path) s.append(second) if first == "": break else: path = first s.reverse() if len(s) == 1 and s[0] == "": s = [] return s #------------------------------------------------------------------------------- # def rel(dest, curdir): """Returns dest filename relative to curdir.""" sc = splitpath(curdir) sd = splitpath(dest) while len(sc) > 0 and len(sd) > 0: if sc[0] != sd[0]: break sc = sc[1:] sd = sd[1:] if len(sc) == 0 and len(sd) == 0: out = "" elif len(sc) == 0: out = apply(join, sd) elif len(sd) == 0: out = apply(join, map(lambda x: os.pardir, sc)) else: out = apply(join, map(lambda x: os.pardir, sc) + list(sd)) # make sure the path is suitable for html consumption return out # #----------------------------------------------------------------------------- # # # def html_join(a, *p): # # """Version of op.join() that uses strickly '/', for legal HTML # output.""" # # # Hopefully, all paths will at some time have gone through this, it should # # work under Windows. # print 'a', a # print 'p', p # # p = apply( join, ( a, ) + p ) # p = p.replace( os.sep, '/' ) # return p # # if os.sep != '/': # join = html_join #------------------------------------------------------------------------------- # def splitFn(fn): """Returns a tuplet ( dir, base, repn, ext ). Repn is '' if not present.""" (dir, bn) = op.split(fn) fidx = bn.find(opts.separator) if fidx != -1: # found separator, add as an alt repn base = bn[ :fidx ] (repn, ext) = splitext(bn[ fidx + len(opts.separator): ]) else: # didn't find separator, split using extension (base, ext) = splitext(bn) repn = '' return (dir, base, repn, ext) #=============================================================================== # UTILITIES #=============================================================================== #------------------------------------------------------------------------------- # def test_jpeg_exif(h, f): """imghdr test for JPEG data in EXIF format""" if h[6:10].lower() == 'exif': return 'jpeg' imghdr.tests.append(test_jpeg_exif) #------------------------------------------------------------------------------- # fast_imgexts = [ 'jpeg', 'jpg', 'gif', 'png', 'rgb', 'pbm', 'pgm', 'ppm', \ 'tiff', 'tif', 'rast', 'xbm', 'bmp' ] def imgwhat(fn, fast=None): """Faster, sloppier, imgwhat, that doesn't require opening the file if we specified that it should be fast.""" if fast == 1: (base, ext) = splitext(fn) if ext[1:].lower() in fast_imgexts: return ext.lower() else: return None else: # slow method, requires opening the file try: return imghdr.what(fn) except IOError: return None #------------------------------------------------------------------------------- # magick_path_cache = {} def getMagickProg(progname): """Returns the program name to execute for an ImageMagick command.""" if magick_path_cache.has_key(progname): p = magick_path_cache[ progname ] else: if opts.magick_path: prg = join(opts.magick_path, progname) else: prg = progname magick_path_cache[ progname ] = prg prg = normpath(prg) # Issue a warning if we can't find the program where specified. if opts.magick_path: if not exists(prg) and not exists(prg + '.exe'): print >> sys.stderr, \ "Warning: can't stat ImageMagick program %s" % prg print >> sys.stderr, \ "Perhaps try specifying it using --magick-path." p = prg o = p return o #=============================================================================== # BASIC CLASSES (for internal representation) #=============================================================================== #=============================================================================== # CLASS Dir #=============================================================================== class Dir: """Directory class.""" #--------------------------------------------------------------------------- # def __init__(self, path, parent, root): """This ctor is called recursively to implement the recursive find. This returns a directory object. It expects the absolute path to the root and a relative directory name.""" self._path = path self._basename = basename(path) self._parent = parent self._subdirs = [] self._images = [] self._tracks = [] self._root = root self._attrfile = None self._curdir = join(root, path) self._pagefn = None # to be computed outside according to policies files = dircache.listdir(join(root, path)) pattr = join(root, self._path, dirattr_fn) longtitle = op.split(self._basename)[1] if not longtitle in [config.ScaledImages["Suffix"], config.Thumbnails["Suffix"]]: self._attrfile = AttrFile(pattr) if exists(pattr) and isfile(pattr): self._attrfile.read() if self._attrfile.keys(): if opts.quiet: print " read attributes", self._attrfile.keys() if not self._attrfile.has_key("comment"): self._attrfile["comment"] = "" longtitle = op.split(self._basename)[1] if not self._attrfile.has_key("date"): try: self._attrfile["date"] = time.strftime("%A, %d %B %Y", time.strptime(longtitle[:10], "%Y-%m-%d")).capitalize().decode(config.Coding) except: self._attrfile["date"] = "" if not self._attrfile.has_key("title"): if len(longtitle) > 11 and len(self._attrfile["date"]) > 1: self._attrfile["title"] = longtitle[11:] else: self._attrfile["title"] = longtitle if not self._attrfile.has_key("image"): self._attrfile["image"] = "" ####reste un probleme avec les images noms d'image.... il sera resolu 100 lignes plus loin imgmap = {} for f in files: af = join(path, f) paf = join(root, af) if opts.ignore_links and islink(paf): continue # add subdir if isdir(paf): subdir = Dir(af, self, root) # ignore directories which do not have images under them. if subdir.hasImages(): self._subdirs.append(subdir) # check other files in map else: # perhaps ignore file if opts.ignore_pattern and opts.ignore_re.search(af): if opts.quiet: print "ignoring file", af continue # check for separator (dir, base, repn, ext) = splitFn(f) # dir should be nothing here. # ignore html file if not repn and ext == opts.htmlext: continue # don't put index bases if base in [ 'dirindex', 'allindex', 'sortindex' ]: continue if base.startswith('trackindex-'): continue try: img = imgmap[ base ] except KeyError: img = Image(self, base) imgmap[ base ] = img if repn: img._calts.append(repn + ext) else: img._salts.append(ext) # Detect thumbnails, imagepages, attributes files. for i in imgmap.keys(): e = imgmap[i] if opts.quiet: print "looking for images with base '%s'" % \ join(e._dir._path, e._base) e.cleanAlts() if not e.selectName(join(root, path)): del imgmap[i] print for f in imgmap.keys(): img = imgmap[f] img.init(self._attrfile) if img._ignore: if opts.quiet: print "ignoring", img._base, "from desc file ignore tag." continue self._images.append(img) if not longtitle in [config.ScaledImages["Suffix"], config.Thumbnails["Suffix"]]: # fix the problem if there are many pages ... if self._attrfile["image"] != "": if not op.isfile(join(self._curdir, self._attrfile["image"])): found = False source = op.splitext(self._attrfile["image"])[0] # sys.stderr.write("source= %s\n"%source) for i in self._subdirs: # sys.stderr.write("possibilite= %s\n"%[j._base for j in i._images]) if source in [j._base for j in i._images]: self._attrfile["image"] = op.join(op.split(i._path)[1], self._attrfile["image"]) found = True break if not found: self._attrfile["image"] = "" #here we try to find an random image that will represente the whole folder if self._attrfile["image"] == "": if len(self._images) > 0: self._attrfile["image"] = self._images[random.randrange(len(self._images))]._base # sys.stderr.write("image au hasard = %s \n"%self._attrfile["image"]) else: # sys.stderr.write("pas d'images, uniquement les chox suivants : %s\n"%[i._path for i in self._subdirs]) for i in self._subdirs: if len(i._attrfile["image"]) > 0: self._attrfile["image"] = op.join(op.split(i._path)[1], i._attrfile["image"]) break if self._attrfile: if len(self._attrfile["title"]) > 0 and len(self._attrfile["date"]) > 0: self._attrfile.write() def cmp_img(a, b): return cmp(a._base, b._base) self._images.sort(cmp_img) # compute directory files' trackmap (incomplete tracks, just to get # the list of keys) mmap = computeTrackmap(self._images) self._tracks = mmap.keys().sort() #--------------------------------------------------------------------------- # def visit(self, functor): for sd in self._subdirs: sd.visit(functor) functor(self) #--------------------------------------------------------------------------- # def getAllImages(self): """Gathers and returns all images in this directory and in its subdirectories.""" images = list(self._images) for s in self._subdirs: images += s.getAllImages() return images #--------------------------------------------------------------------------- # def getAllDirs(self): """Gathers and returns all dirnames and subdirnames from this directory.""" dirs = [ self ] for d in self._subdirs: if d.hasImages(): dirs += d.getAllDirs() return dirs #--------------------------------------------------------------------------- # def hasImages(self): """Returns true if this directory has images, or if any of it subdirectories has images.""" if len(self._images) > 0: return 1 for s in self._subdirs: if s.hasImages(): return 1 return 0 #------------------------------------------------------------------------------- # def Dir_cmpdates(dir1, dir2): """Compare two subdirectories by date.""" t1, t2 = map(lambda x: os.stat(x._path).st_ctime, [dir1, dir2]) c = cmp(t1, t2) if c != 0: return c return cmp(dir1, dir2) #=============================================================================== # CLASS Image #=============================================================================== class Image: """Image specific processing and storage.""" #--------------------------------------------------------------------------- # def __init__(self, dir, base): """Constructor. Initialize to accumulation information.""" self._dir = dir # directory object self._base = base # base (e.g. dscn0111) self._repn = None # suffix, if present (e.g. --800x800) self._ext = None # file extension (e.g. .jpg) self._salts = [] # simple alt.repns. (i.e. base.ext) self._calts = [] # complex alt.repns (i.e. base--repn.ext) self._size = None self._thumbfn = None # thumbnail filename (i.e. base--thumb.gif) self._thumbsize = None self._scaledfn = None self._scaledsize = None self._attr = None # attributes filename boolean self._title = '' # to be computed upon init() self._pagefn = None # to be computed outside according to policies self._comment = None # Description in Jpeg comment self._exif = None # Exif tags #--------------------------------------------------------------------------- # def init(self, dirattrfile=None): """Performs proper initialization.""" self._filename = join(self._dir._path, self._base) if self._repn: self._filename += opts.separator + self._repn if self._ext: self._filename += self._ext # Create attrfile object. if self._attr: pattr = join(opts.root, self._dir._path, \ self._base + self._attr) if exists(pattr) and isfile(pattr): attr = AttrFile(pattr) attr.read() if attr.keys(): if opts.quiet: print " read attributes", attr.keys() self._attr = attr # Special pre-defined attributes. # FIXME these up in ctor? self._tracks = [] self._ignore = 0 self._dirattr = dirattrfile for a in [ self._dirattr, self._attr ]: if a: try: self.handleDescription(a) except: print >> sys.stderr, \ "Error: in attributes file %s" % a._path self._exif, self._comment = exif(join(opts.root, self._filename)) # no title, so use something unique to the image, it's path if not self._title: self._title = join(self._dir._path, self._base) # make up map of altrepns self._altrepns = {} for k in self._salts: self._altrepns[ k[1:] ] = self._base + k for k in self._calts: self._altrepns[ k ] = self._base + opts.separator + k #--------------------------------------------------------------------------- # def __repr__(self): t = "Image( %s, %s, %s, %s,\n" % \ (self._dir, self._base, self._repn, self._ext) t += " %s, %s, %s,\n" % \ (self._salts, self._calts, self._thumbfn) t += " %s )\n" % self._attr return t #--------------------------------------------------------------------------- # def cleanAlts(self): """Clean up found alt.repns, thumbnails, attributes files, etc.""" # Detect html files, remove them in the altrepn try: idx = self._salts.index(opts.htmlext) if opts.quiet: print " detected existing imagepage '%s'" % opts.htmlext del self._salts[idx] except ValueError: pass # Detect attributes files, remove them in the altrepn try: idx = self._salts.index(opts.attrext) if opts.quiet: print " detected attributes file '%s'" % opts.attrext self._attr = self._salts[idx] del self._salts[idx] except ValueError: pass # Detect thumb file, separate them from altrepns for k in self._calts: if k.startswith(opts.thumb_sfx): if opts.quiet: print " detected thumb file '%s'" % k self._thumbfn = k self._calts.remove(k) break #--------------------------------------------------------------------------- # def selectName(self, absdirname): """Select base file (image file) for image page generation. Returns true if it could find a suitable one.""" if opts.quiet: print " ", self._salts + self._calts # # choose one representation for the imagepage # # 1) the first of the affinity repn which is an image file found = 0 for ref in opts.repn_affinity: for f in self._salts + self._calts: if ref.search(f): ff = self._base + opts.separator + f paf = join(absdirname, ff) if imgwhat(paf, opts.fast): if f in self._salts: self._salts.remove(f) self._ext = f else: self._calts.remove(f) (self._repn, self._ext) = splitext(f) if opts.quiet: print " choosing affinity '%s'" % f return 1 else: if opts.quiet: print " ignoring non-image '%s'" % f # 2) the first of the base files which is an image file if opts.use_repn: # 3) with the first of the alternate representations which # is an image file. eee = self._salts + self._calts else: eee = self._salts for f in eee: if not f.startswith('.'): f = opts.separator + f ff = self._base + f paf = join(absdirname, ff) if imgwhat(paf, opts.fast): if f in self._salts: self._salts.remove(f) self._ext = f else: self._calts.remove(f) (self._repn, self._ext) = splitext(f) # if opts.quiet: print " choosing imagefile '%s'" % f return 1 else: if opts.quiet: print " ignoring non-image '%s'" % f # if opts.quiet: print " no imagepage for base '%s'" % self._base return 0 #--------------------------------------------------------------------------- # def handleDescription(self, attrfile): """Two description files, with precedence for the first.""" tracks = attrfile.get_def('tracks') if tracks: intracks = string.split(tracks) self._tracks = [] for i in intracks: if i not in self._tracks: self._tracks.append(i) ignore = attrfile.get_def('ignore') if ignore: self._ignore = 1 title = attrfile.get_def('title') if title: self._title = title def generateThumbnail(img): """Generates a thumbnail for an image. Make it so that the longest dimension is the specified dimension.""" if not img._thumbfn: return aimgfn = join(opts.root, img._filename) if not opts.fast: img._size = imageSize(aimgfn) athumbfn = join(opts.root, img._thumbfn) if opts.thumb_force: if opts.quiet: print "forced regeneration of '%s'" % img._thumbfn elif not exists(athumbfn): if opts.quiet: print "thumbnail absent '%s'" % img._thumbfn else: # Check if thumbsize has changed if not opts.fast: img._thumbsize = imageSize(athumbfn) if not checkThumbSize(img._size, \ img._thumbsize, \ opts.thumb_size): if opts.quiet: print "thumbnail '%s size has changed" % img._thumbfn try: # Clear cache for thumbnail size. del imageSizeCache[ athumbfn ] except: pass else: # pass # if opts.quiet: print "thumbnail '%s' already generated (size ok)" \ # % img._thumbfn return else: if opts.quiet: print "thumbnail '%s' already generated" % img._thumbfn return if opts.no_magick: if opts.quiet: print "ImageMagick tools disabled, can't create thumbnail" return # create necessary directories d = dirname(athumbfn) if not exists(d): os.makedirs(d) if opts.pil: try: im = PilImage.open(aimgfn) im.thumbnail((opts.thumb_size, opts.thumb_size), config.Thumbnails["Interpolation"]) im.save(athumbfn) img._thumbsize = im.size except IOError, e: raise SystemExit(\ "Error: identifying file '%s'" % aimgfn + str(e)) else: cmd = getMagickProg('convert') + ' -border 2x2 ' # FIXME check if this is a problem if not specified #cmd += '-interlace NONE ' cmd += '-geometry %dx%d ' % (opts.thumb_size, opts.thumb_size) if opts.thumb_quality: cmd += '-quality %d ' % opts.thumb_quality # This doesn't add text into the picture itself, just the comment in # the header. if opts.copyright: cmd += '-comment \"%s\" ' % opts.copyright # We use [1] to extract the thumbnail when there is one. # It is harmless otherwise. subimg = "" if img._ext.lower() in [ ".jpg", ".tif", ".tiff" ]: subimg = "[1]" cmd += '"%s%s" "%s"' % (aimgfn, subimg, athumbfn) if opts.quiet: print "generating thumbnail '%s'" % img._thumbfn (chin, chout, cherr) = os.popen3(cmd) errs = cherr.readlines() chout.close() cherr.close() if errs: print >> sys.stderr, \ "Error: running convert program on %s:" % aimgfn errs = string.join(errs, '\n') print errs if subimg and \ re.compile('Unable to read subimage').search(errs): if opts.quiet: print "retrying without subimage" cmd = string.replace(cmd, subimg, "") (chin, chout, cherr) = os.popen3(cmd) errs = cherr.readlines() chout.close() cherr.close() if errs: print >> sys.stderr, \ "Error: running convert program on %s:" % aimgfn print string.join(errs, '\n') else: img._thumbsize = imageSize(athumbfn) #--------------------------------------------------------------------------- # def generateScaled(img): """Generates a scaled image for screen size. Make it so that the longest dimension is the specified dimension.""" if not img._scaledfn: return aimgfn = join(opts.root, img._filename) if not opts.fast: img._size = imageSize(aimgfn) ascaledfn = join(opts.root, img._scaledfn) if opts.scaled_force: if opts.quiet: print "forced regeneration of '%s'" % img._scaledfn elif not exists(ascaledfn): if opts.quiet: print "thumbnail absent '%s'" % img._scaledfn else: # Check if scaledsize has changed if not opts.fast: img._scaledsize = imageSize(ascaledfn) if not checkThumbSize(img._size, \ img._scaledsize, \ opts.scaled_size): if opts.quiet: print "thumbnail '%s size has changed" % img._scaledfn try: # Clear cache for thumbnail size. del imageSizeCache[ ascaledfn ] except: pass else: if opts.quiet: print "thumbnail '%s' already generated (size ok)" \ % img._scaledfn return else: if opts.quiet: print "thumbnail '%s' already generated" % img._scaledfn return if opts.no_magick: if opts.quiet: print "ImageMagick tools disabled, can't create thumbnail" return # create necessary directories d = dirname(ascaledfn) if not exists(d): os.makedirs(d) if opts.pil: try: im = PilImage.open(aimgfn) im.thumbnail((opts.scaled_size, opts.scaled_size), config.ScaledImages["Interpolation"]) im.save(ascaledfn) img._scaledsize = im.size except IOError, e: raise SystemExit(\ "Error: identifying file '%s'" % aimgfn + str(e)) else: cmd = getMagickProg('convert') + ' -border 2x2 ' # FIXME check if this is a problem if not specified #cmd += '-interlace NONE ' cmd += '-geometry %dx%d ' % (opts.scaled_size, opts.scaled_size) if opts.scaled_quality: cmd += '-quality %d ' % opts.scaled_quality # This doesn't add text into the picture itself, just the comment in # the header. if opts.copyright: cmd += '-comment \"%s\" ' % opts.copyright # We use [1] to extract the thumbnail when there is one. # It is harmless otherwise. subimg = "" if img._ext.lower() in [ ".jpg", ".tif", ".tiff" ]: subimg = "[1]" cmd += '"%s%s" "%s"' % (aimgfn, subimg, ascaledfn) if opts.quiet: print "generating thumbnail '%s'" % img._scaledfn (chin, chout, cherr) = os.popen3(cmd) errs = cherr.readlines() chout.close() cherr.close() if errs: print >> sys.stderr, \ "Error: running convert program on %s:" % aimgfn errs = string.join(errs, '\n') print errs if subimg and \ re.compile('Unable to read subimage').search(errs): if opts.quiet: print "retrying without subimage" cmd = string.replace(cmd, subimg, "") (chin, chout, cherr) = os.popen3(cmd) errs = cherr.readlines() chout.close() cherr.close() if errs: print >> sys.stderr, \ "Error: running convert program on %s:" % aimgfn print string.join(errs, '\n') else: img._scaledsize = imageSize(ascaledfn) #=============================================================================== # IMAGE SIZE CACHE #=============================================================================== #------------------------------------------------------------------------------- # def checkThumbSize(isz, tsz, desired): """Returns true if the sizepair fits the size.""" # tolerate 2% error try: if abs(float(isz[0]) / isz[1] - float(tsz[0]) / tsz[1]) > 0.02: return 0 # aspect has changed, or isz rotated except: return 0 return abs(desired - tsz[0]) <= 1 or abs(desired - tsz[1]) <= 1 #------------------------------------------------------------------------------- # def imageSizeNoCache(filename): """Non-caching finding out the size of an image file.""" if opts.no_magick: return (0, 0) fn = filename if opts.pil: try: im = PilImage.open(fn) s = im.size except IOError, e: raise SystemExit("Error: identifying file '%s'" % fn + str(e)) return s elif not opts.old_magick: cmd = getMagickProg('identify') + ' -format "%w %h" ' + \ '"%s"' % fn po = os.popen(cmd) output = po.read() try: (width, height) = map(lambda x: int(x), string.split(output)) except ValueError: print >> sys.stderr, \ "Error: parsing identify output on %s" % fn return (0, 0) err = po.close() if err: print >> sys.stderr, \ "Error: running identify program on %s" % fn return (0, 0) return (width, height) else: # Old imagemagick doesn't have format tags cmd = getMagickProg('identify') + ' "%s"' % fn po = os.popen(cmd) output = po.read() err = po.close() if err: print >> sys.stderr, \ "Error: running identify program on %s" % fn return (0, 0) mre = re.compile("([0-9]+)x([0-9]+)") mo = mre.match(string.split(output)[1]) if not mo: mo = mre.match(string.split(output)[2]) if mo: (width, height) = map(lambda x: int(x), mo.groups()) return (width, height) print >> sys.stderr, \ "Warning: could not identify size for image '%s'" % filename return (0, 0) #------------------------------------------------------------------------------- # fcachesizes = None szre = re.compile('(\d+)x(\d+)') imageSizeCache = {} def imageSize(path): """Returns the ( width, height ) image size pair. Filename must be absolute. This method uses a cache to avoid having to reopen an image file multiple times.""" global fcachesizes if not fcachesizes: fcachesizes = FCache(join(opts.root, '.fcache')) sizestr = fcachesizes.lookup(path) if sizestr != None: mo = szre.match(sizestr) if mo: return (int(mo.group(1)), int(mo.group(2))) # else: go on... try: size = imageSizeCache[path] fcachesizes.store(path, "%dx%d" % size) return size except KeyError: size = imageSizeNoCache(path) imageSizeCache[path] = size fcachesizes.store(path, "%dx%d" % size) return size # We assume that the images don't change for the # duration that we build this cache. #=============================================================================== # PAGE GENERATION #=============================================================================== #------------------------------------------------------------------------------- # def generatePage(fn, ttype, envir): """Generates an index page, replacing the tags as needed.""" # create necessary directories d = dirname(join(opts.root, fn)) if not exists(d): os.makedirs(d) envir['cd'] = dirname(fn) # Write out modified file. try: afn = join(opts.root, fn) tfile = open(afn, "w") execTemplate(tfile, templates[ttype], envir) tfile.close() except IOError, e: print >> sys.stderr, "Error: can't open file: %s" % fn #------------------------------------------------------------------------------- # def generateSummary(fn, allimages): """Generates the text index that could be used by other processing tools.""" # create necessary directories d = dirname(join(opts.root, fn)) if not exists(d): os.makedirs(d) otext = u"" for i in allimages: l = i._filename l += ',' if i._title: l += i._title # Make sure it's on a single line # print l otext += l.replace('\n', ' ') + '\n' # Write out file. try: afn = join(opts.root, fn) tfile = open(afn, "w") tfile.write(otext.encode(config.Coding)) tfile.close() except IOError, e: print >> sys.stderr, "Error: can't open file: %s" % fn #=============================================================================== # TEMPLATE PARSING AND EXECUTION #=============================================================================== #=============================================================================== # CLASS StringStream #=============================================================================== class StringStream: """Simple string stream with a write() method, for replacing stdout when running in script environment. We can't use StringIO because we need the pop() hack.""" #--------------------------------------------------------------------------- # def __init__(self): self._string = "" self._ignoreNext = 0 #--------------------------------------------------------------------------- # def write(self, s): if self._ignoreNext: s = s[1:] self._ignoreNext = 0 self._string += (s) #--------------------------------------------------------------------------- # def ignoreNextChar(self): self._ignoreNext = 1 #--------------------------------------------------------------------------- # def getvalue(self): return self._string #------------------------------------------------------------------------------- # def readTemplates(): """Reads the template files.""" # Compile HTML templates. templates = {} for tt in [ 'image', 'dirindex', 'allindex', 'trackindex', 'sortindex' ]: fn = 'template-%s' % tt + opts.htmlext ttext = readTemplate(fn) templates[ tt ] = compileTemplate(ttext, fn) fn = 'template-css.css' ttext = readTemplate(fn) templates[ 'css' ] = compileTemplate(ttext, fn) # Compile user-specified rc file. rcsfx = 'rc' templates[ rcsfx ] = [] if opts.rc: try: tfile = open(opts.rc, "r") orc = tfile.read() tfile.close() except IOError, e: print >> sys.stderr, "Error: can't open user rc file:", opts.rc sys.exit(1) o = compileCode('', orc, opts.rc) templates[ rcsfx ] += [ o ] # Compile user-specified code. if opts.rccode: o = compileCode('', opts.rccode, "rccode option") templates[ rcsfx ] += [ o ] # Compile global rc file without HTML tags, just python code. code = readTemplate('template-%s' % rcsfx + '.py') o = compileCode('', code, tt) templates[ rcsfx ] += [ o ] return templates #------------------------------------------------------------------------------- # def readTemplate(tfn): """Reads a template file. This method expects an simple filename.""" if opts.verbose: print "fetching template", tfn found = 0 foundInRoot = 0 # check in user-specified template root. if opts.templates: fn = join(opts.templates, tfn) if opts.verbose: print " looking in %s" % fn if exists(fn): found = 1 # check in hierarchy root if not found: fn = join(opts.root, tfn) if opts.verbose: print " looking in %s" % fn if exists(fn): foundInRoot = 1 found = 1 # look for it in the environment var path if not found: try: curatorPath = os.environ[ 'CURATOR_TEMPLATE' ] pathlist = string.split(curatorPath, os.pathsep) for p in pathlist: fn = join(p, tfn) if opts.verbose: print " looking in %s" % fn if exists(fn): found = 1 break except KeyError: pass if found == 1: # read the file try: tfile = open(fn, "r") t = tfile.read() tfile.close() except IOError, e: print >> sys.stderr, "Error: can't open image template file:", fn sys.exit(1) if opts.verbose: print " succesfully loaded template", tfn else: # bah... can't load it, use fallback templates if opts.verbose: print " falling back on simplistic default templates." global fallbackTemplates try: t = fallbackTemplates[ splitext(tfn)[0] ] except KeyError: t = '' # Save templates in root, if it was requested. if opts.save_templates and foundInRoot == 0: rootfn = join(opts.root, tfn) if opts.verbose: print " saving template in %s" % rootfn # saving the file template if exists(rootfn): bakfn = join(opts.root, tfn + '.bak') if opts.verbose: print " making backup in %s" % bakfn import shutil try: shutil.copy(rootfn, bakfn) except: print >> sys.stderr, \ "Error: can't copy backup template %s", bakfn try: ofile = open(rootfn, "w") ofile.write(t) ofile.close() except IOError, e: print >> sys.stderr, "Error: can't save template file to", rootfn return t #------------------------------------------------------------------------------- # def compileCode(pretext, codetext, filename): """Compile a chunk of code.""" try: if codetext: co = compile(codetext, filename, "exec") o = [ pretext, co, codetext ] else: o = [ pretext, None, codetext ] except: o = [ pretext, None, codetext ] print >> sys.stderr, \ "Error compiling template in the following code:" print >> sys.stderr, codetext try: etype, value, tb = sys.exc_info() print_exception(etype, value, tb, None, sys.stderr) finally: etype = value = tb = None if not opts.ignore_errors: errors = 1 print >> sys.stderr return o #------------------------------------------------------------------------------- # def compileTemplate(ttext, filename): """Compiles template text.""" mre1 = re.compile("") pos = 0 olist = [] errors = 0 while pos < len(ttext): mo1 = mre1.search(ttext, pos) if not mo1: break mo2 = mre2.search(ttext, mo1.end()) if not mo2: print >> sys.stderr, "Error: unfinished tag." sys.exit(1) pretext = ttext[ pos : mo1.start() ] code = ttext[ mo1.end() : mo2.start() ] if not mo1.group('code'): code = "print " + code + "," o = compileCode(pretext, code, filename) olist.append(o) pos = mo2.end() if pos < len(ttext): olist.append([ ttext[ pos: ], None ]) if errors == 1 and not opts.ignore_errors: sys.exit(1) return olist #------------------------------------------------------------------------------- # def execTemplate(outfile, olist, envir): """Executes template text. Output is written to outfile.""" ss = outfile saved_stdout = sys.stdout sys.stdout = ss errors = 0 for o in olist: ss.write(o[0]) if o[1]: try: eval(o[1], envir) # Note: this is a TERRIBLE hack to flush the comma cache of the # python interpreter's print statement between tags when outfile # is a string stream. # # Note: we don't need this anymore, since we're outputting to a # real file object. However, keep this around in case we change # it back to output to a string. #if hack: # ss.ignoreNextChar() except: print >> sys.stderr, \ "Error executing template in the following code:" print >> sys.stderr, o[2] try: etype, value, tb = sys.exc_info() print_exception(etype, value, tb, None, sys.stderr) finally: etype = value = tb = None if not opts.ignore_errors: errors = 1 print >> sys.stderr if errors == 1 and not opts.ignore_errors: sys.exit(1) sys.stdout = saved_stdout #=============================================================================== # MISC #=============================================================================== #------------------------------------------------------------------------------- # def computeTrackmap(imagelist): """Computes a map of files in each track. The key is the track name.""" tracks = {} for i in imagelist: for t in i._tracks: if not tracks.has_key(t): tracks[ t ] = [] tracks[ t ].append(i) return tracks #------------------------------------------------------------------------------- # def clean(allimages, alldirs): """Removes the files generated by generator in the current directory and below.""" for img in allimages: # Delete HTML files htmlfn = join(opts.root, img._dir._path, img._pagefn) if exists(htmlfn): if opts.verbose: print "Deleting", htmlfn try: os.unlink(htmlfn) except: print >> sys.stderr, "Error: deleting", htmlfn # Delete thumbnails if img._thumbfn: thumbfn = join(opts.root, img._thumbfn) if exists(thumbfn): if opts.verbose: print "Deleting", thumbfn try: os.unlink(thumbfn) img._thumbfn = None except: print >> sys.stderr, "Error: deleting", thumbfn for d in alldirs: files = dircache.listdir(join(opts.root, d._path)) # Delete HTML files in directories for f in files: fn = join(opts.root, d._path, f) if f in [ dirindex_fn, allindex_fn, allcidx_fn, sortindex_fn, css_fn ] or \ f.startswith('trackindex-'): if opts.verbose: print "Deleting", fn try: os.unlink(fn) pass except: print >> sys.stderr, "Error: deleting", fn if f == index_fn and islink(fn): os.unlink(fn) #=============================================================================== # DEFAULT TEMPLATES #=============================================================================== # These are the very bare minimum and are provided here so that we just NEVER # abort because we can't find the templates. # Note: we really want this at the end of the file, because it screws up emacs # python-mode highlighting. #--------------------------------------------------------------------------- # def imageSrc(cd, image, xtra=''): assert(image._filename) if image._size: (w, h) = image._size iss = '%s' % \ (urlquote(rel(image._filename, cd)), w, h, image._base, xtra) else: iss = '%s' % \ (urlquote(rel(image._filename, cd)), image._base, xtra) return iss #--------------------------------------------------------------------------- # def thumbImage(cd, image, xtra=''): assert(image._thumbfn) if image._thumbsize: (w, h) = image._thumbsize iss = '%s' % \ (urlquote(rel(image._thumbfn, cd)), w, h, image._base, xtra) else: iss = '%s' % \ (urlquote(rel(image._thumbfn, cd)), image._base, xtra) return '\n%s' % (urlquote(rel(image._pagefn, cd)), iss) #--------------------------------------------------------------------------- # def scaledImage(cd, image, xtra=''): assert(image._scaledfn) if image._scaledsize: (w, h) = image._scaledsize iss = '%s' % \ (urlquote(rel(image._scaledfn, cd)), w, h, image._base, xtra) else: iss = '%s' % \ (urlquote(rel(image._scaledfn, cd)), image._base, xtra) return '\n%s' % (urlquote(rel(image._filename, cd)), iss) #--------------------------------------------------------------------------- # def table(dstdir, images, textfun=None, cols=4): """(images: seq of Image, textfun: function, cols: int) Utility function that generates a table with thumbnails for the given images. Specify textfun a callback if you want to include some text under each image. cols is the number of columns. Of course, you're free to define your own table making function within the template itself if you don't like this one.""" if len(images) == 0: return "" os = StringIO.StringIO() idx = 0 print >> os, '
' while idx < len(images): if len(images) - idx >= cols: isubset = images[idx:idx + cols] idx += cols else: isubset = images[idx:] idx += len(isubset) # Separate tables provide for tighter fitting of thumbnails. # print >> os, '
' print >> os, '' for i in isubset: print >> os, '" for i in range(0, cols - len(isubset)): print >> os, "" print >> os, "" print >> os, "
' altname = 'alt="%s"' % i._title print >> os, thumbImage(dstdir, i) if textfun: print >> os, textfun(i) print >> os, "
" return os.getvalue() #------------------------------------------------------------------------------- # def twoColumns(dstdir, images): os = StringIO.StringIO() print >> os, '
' print >> os, '
    ' for i in images[ :len(images) / 2 ]: print >> os, '
  • %s
  • ' % \ (rel(i._pagefn, dstdir), i._title) print >> os, '
' print >> os, '
' print >> os, '
    ' for i in images[ len(images) / 2: ]: print >> os, '
  • %s
  • ' % \ (rel(i._pagefn, dstdir), i._title) print >> os, '
' print >> os, '
' return os.getvalue() #--------------------------------------------------------------------------- # def imagePile(dstdir, images): if len(images) == 0: return "" os = StringIO.StringIO() print >> os, '
' for i in images: print >> os, thumbImage(dstdir, i) print >> os, "
" return os.getvalue() #--------------------------------------------------------------------------- # dirnavsep = ' / ' def dirnav(cd, dir, rootname="(root)", dirsep=dirnavsep): """(rootname: string, dirsep: string, dir: Dir, ignoreCurrent: bool) Utility that generates an anchored HTML representation for a directory within the image hierarchy. You can click on the directory names.""" d = dir dirs = [] while d: dirs.append(d) d = d._parent dirs.reverse() comps = [] for d in dirs: f = urlquote(join(rel(d._pagefn, cd))) if d._parent == None: name = rootname elif d._attrfile and d._attrfile.get_def("title"): name = unicode2html(d._attrfile.get_def("title")) else: name = d._basename comps.append('%s' % (f, name)) return string.join(comps, '\n%s\n' % dirsep) #--------------------------------------------------------------------------- # def next(image, imglist): idx = imglist.index(image) if idx + 1 < len(imglist): return imglist[idx + 1] return None #--------------------------------------------------------------------------- # def prev(image, imglist): idx = imglist.index(image) if idx - 1 >= 0: return imglist[idx - 1] return None #--------------------------------------------------------------------------- # def cycnext(image, imglist): idxnext = (imglist.index(image) + 1) % len(imglist) return imglist[idxnext] #--------------------------------------------------------------------------- # def cycprev(image, imglist): idxprev = (imglist.index(image) - 1) % len(imglist) return imglist[idxprev] #--------------------------------------------------------------------------- # def textnav(dstdir, image, imglist, midtext="", middest=None, pcycling=0): """(imglist: seq of Image, midtext: string, middest: string, pcycling: bool) Returns an HTML snippet for a text track navigation widget. Set pcycling to 1 if you want it cycling.""" if pcycling == 1: prevfunc = cycprev nextfunc = cycnext else: prevfunc = prev nextfunc = next os = StringIO.StringIO() pi = prevfunc(image, imglist) if pi: print >> os, "" % rel(pi._pagefn, dstdir), print >> os, "prev" if midtext != "": print >> os, "[", if middest: print >> os, "%s" % \ (urlquote(middest), midtext), else: print >> os, "" + midtext + "", print >> os, "]" else: if pi: print >> os, " " ni = nextfunc(image, imglist) if ni: print >> os, "" % rel(ni._pagefn, dstdir), print >> os, "next" return os.getvalue() #----------------------------------------------------------------------- def desc_index(dir): file_ind = join(dir._path, config.CommentFile) attr = None if op.exists(file_ind): attr = AttrFile(file_ind) attr.read() return attr #------------------------------------------------------------------------ def sous_titre(i): s_titre = '
\n ' + i._base + '' description = None if i._attr: description = i._attr.get_def('description') elif i._comment: description = unicode2html(i._comment.replace("
", "\n")).replace("\n", "
") if description: s_titre += '
\n' + description return s_titre def exif(filename): """return exif data + title from the photo""" clef = ['Exif.Image.Make', 'Exif.Image.Model', 'Exif.Image.DateTime', 'Exif.Photo.ExposureTime', 'Exif.Photo.FNumber', 'Exif.Photo.DateTimeOriginal', 'Exif.Photo.DateTimeDigitized', 'Exif.Photo.ShutterSpeedValue', 'Exif.Photo.ApertureValue', 'Exif.Photo.ExposureBiasValue', 'Exif.Photo.Flash', 'Exif.Photo.FocalLength', 'Exif.Photo.ISOSpeedRatings' ] data = {} image_exif = Exif(filename) image_exif.read() comment = image_exif.comment for i in clef: try: data[i] = image_exif.interpretedExifValue(i) except: data[i] = "" return data, comment #=============================================================================== # DEFAULT TEMPLATES #=============================================================================== # Important Note: you can always save these with --save-templates and then edit # them, and they will be used, without modifying this script. You can also put # new common code in template-rc.py and use it from your templates. html_preamble = \ """ \"> %s """ html_postamble = """ """ fallbackTemplates = {} fallbackTemplates[ 'template-css' ] = """ BODY { background-color: #444444; font-family: Arial,Geneva,sans-serif; color: lightgray; } A:link { text-decoration: none; color: #FFFFFF; } A:visited { text-decoration: none; color: #FFFFFF; } .arrownav { width: 100%; border: none; margin: 0; padding: 0; } .arrow { font-weight: bold; text-decoration: none; } .image { border: solid 7px black; } .thumb { border-width: 3px; border-style: ridge; border-color: #AAAAAA; } .desctable { width: 100%; border: none; margin: 0; padding: 0; } .titlecell { vertical-align: top; } .title { font-size: 24px; } .location { font-weight: bold; } .description { font-family: Arial,Geneva,sans-serif; text-align: center; } HR.sephr { background-color: black; height: 1px; width: 90%; } .navtable { width: 100%; border: none } .settingscell { width: 1%; vertical-align: top; text-align: right; white-space: nowrap; } .settingstitle { font-size: x-small; font-weight: bold; } .settings { font-size: x-small; } .tracksind { font-weight: bold; } .toptable { width: 100%; background-color: black; } .dirtop { background-color: black; } .globaltop { background-color: black; } .tracktop { background-color: black; } .sortedtop { background-color: black; } .toptitle { font-size: x-large; } .topsubtitle { font-weight: bold; } .mininav { text-align: right; font-size: smaller; } """ fallbackTemplates[ 'template-image' ] = \ (html_preamble % 'Image: ') + \ """




""" + html_postamble fallbackTemplates[ 'template-dirindex' ] = \ (html_preamble % 'Directory Index: ') + \ """

""" + html_postamble fallbackTemplates[ 'template-trackindex' ] = \ (html_preamble % 'Track Index: ') + \ """

Track index:

""" + html_postamble fallbackTemplates[ 'template-allindex' ] = \ (html_preamble % 'Global Index') + \ """

Global Index

""" + html_postamble fallbackTemplates[ 'template-sortindex' ] = \ (html_preamble % '\"Sorted index\"') + \ """

Sorted index

""" + html_postamble #=============================================================================== # MAIN #=============================================================================== def main(): import optparse parser = optparse.OptionParser(__doc__.strip(), version=__version_pr__) parser.add_option('--help-script', action='store_true', help="show scripting environment documentation.") parser.add_option('-v', '--verbose', action='store_true', dest="verbose", help="run verbosely (default)") parser.add_option('-q', '--quiet', action='store_false', dest="quiet", default=True, help="run quietly (turns verbosity off)") parser.add_option('--no-links', action='store_true', help="don't make links to HTML directory indexes") parser.add_option('--use-repn', action='store', help="""Don't generate an image page if there is no base file (i.e. a file without an alternate repn suffix. The default selection algorithm is to choose 1) the first of the affinity repn which is an image file (see repn-affinity option), 2) the first of the base files which is an image file, 3) (optional) the first of the alternate representations which is an image file. This option adds step (3).""") parser.add_option('--repn-affinity', action='store', help="""Specifies a comma separated list of regular expressions to match for alt.repn files and file extensions to prefer when searching for a main image file to generate a page for (e.g. \"\.jpg,--768\..*,\.gif\".""") parser.add_option('-t', '--templates', action='store', help="""specifies the directory where to take templates from (default: root). This takes precedence over the CURATOR_TEMPLATE environment variable AND over the root""") parser.add_option('--rc', action='store', help="""specifies an additional global file to include and run in the page environment before expanding the templates. This can be used to perform global initialization and customization of template variables. The file template-rc.py is searched in the same order as for templates and is executed in that order as well. Note that output when executing this goes to stdout.""") parser.add_option('--rccode', action='store', help="""specifies additional global init code to run in the global environment. This can be used to parameterize the templates from the command line (see option --rc).""") parser.add_option('--save-templates', action='store', help="""saves the template files in the root of the hierarchy. Previous copies, if existing and different from the current template, are moved into backup files. This can be useful for keeping template files around for future regeneration, or for saving a copy before editing.""") parser.add_option('-k', '--ignore-errors', action='store_true', help="ignore errors in templates") parser.add_option('-I', '--ignore-pattern', action='store', help="regexp to specify image files to ignore") parser.add_option('--htmlext', action='store', help="specifies html files extension (default: '.html')") parser.add_option('--attrext', action='store', help="""specifies attributes files extension (default: '.desc')""") parser.add_option('--thumb-sfx', action='store', help="""specifies the thumbnail alt.repn. suffix (default: 'thumb.jpg')""") parser.add_option('--scaled-sfx', action='store', help="""specifies the thumbnail alt.repn. suffix (default: 'scaled.jpg')""") parser.add_option('-p', '--separator', action='store', help="""specify the image basename separator from the suffix and extension (default: --)""") parser.add_option('-C', '--copyright', action='store', help="""specifies a copyright notice to include in image conversions""") group = optparse.OptionGroup(\ parser, "Conversion tools", "Selects options related to conversion tools.") group.add_option('--magick-path', action='store', metavar="PATH", help="specify imagemagick path to use") group.add_option('--old-magick', action='store_true', help="use old imagemagick features") group.add_option('--new-magick', action='store_false', dest="old_magick", help="use new imagemagick features") group.add_option('--no-magick', action='store_true', help="disable using imagemagick (disable conversions)") # FIXME remove this option at some point group.add_option('--pil', action='store_true', help="""use the Python Imaging Library (PIL) instead of the Imagemagick tools (default).""") parser.add_option_group(group) parser.add_option('-s', '--thumb-size', action='store', help="specifies size in pixels of thumbnail largest side") parser.add_option('-F', '--thumb-force', action='store_true', help="regenerate and xoverwrite existing thumbnails") parser.add_option('-Q', '--thumb-quality', action='store', help="""specify quality for thumbnail conversion (see convert(1))""") parser.add_option('--scaled-size', action='store', help="specifies size in pixels of scaled image largest side") parser.add_option('--scaled-force', action='store_true', help="regenerate and xoverwrite existing scaled image") parser.add_option('--scaled-quality', action='store', help="""specify quality for scaled image conversion (see convert(1))""") parser.add_option('-X', '--fast', action='store_true', help="""disables some miscalleneous slow checks, even if the consistency can be shaken up. Don't use this, this is a debugging tool""") parser.add_option('-l', '--ignore-links', action='store_true', help="Ignore symbolic links to image files.") parser.add_option('--clean', action='store_true', help="""remove all files generated by generator. generator exits after clean up.""") group = optparse.OptionGroup(\ parser, "Output method", "Selects where the output files will be located .") group.add_option('-D', '--out-along', action='store_true', help="Put outputs along with files.") group.add_option('-S', '--out-subdirs', action='store_true', help="Put outputs in subdirectories of the directories (default).") group.add_option('-O', '--out-onedir', action='store_true', help="Put all outputs in a single output subdirectory.") parser.add_option_group(group) # FIXME eventually make it use the full power of optparse # type conversions # aliases global opts opts, args = parser.parse_args() if len(args) == 0: if isdir(config.WebRepository): opts.root = config.WebRepository else: opts.root = os.getcwd() elif len(args) == 1: opts.root = args[0] elif len(args) > 1: print >> sys.stderr, "Error: can only specify one root." sys.exit(1) # # end options parsing. # # Debugging this script opts.debug = 0 # # set defaults # if not opts.thumb_size: opts.thumb_size = config.Thumbnails["Size"] # this is the best default IMHO, that doesn't detract the attention too # much from 1024x768 images (makes the clicker curious) yet gives enough # detail you can find the image in the index. if not opts.scaled_size: opts.scaled_size = config.ScaledImages["Size"] if not opts.separator: opts.separator = "--" if not opts.htmlext: opts.htmlext = ".html" if not opts.attrext: opts.attrext = ".desc" if not opts.thumb_sfx: opts.thumb_sfx = config.Thumbnails["Suffix"] + config.Extensions[0]#"thumb.jpg" will create jpg thumbnails by default if not opts.scaled_sfx: opts.scaled_sfx = config.ScaledImages["Suffix"] + config.Extensions[0] #"scaled.jpg" will create jpg scaled image by default # # Validate options. # if opts.quiet: print "====> validating options" if opts.help_script: print generateScriptHelp() sys.exit(1) if opts.pil == None and opts.old_magick == None : opts.pil = 1 if opts.pil: global PilImage try: import Image as PilImage except ImportError, e: try: import PIL.Image as PilImage except ImportError, e: raise SystemExit(\ "Error: you said to use PIL but PIL seems not to be installed.") if opts.templates: opts.templates = op.expanduser(opts.templates) if (opts.magick_path or opts.old_magick != None) and opts.no_magick: print >> sys.stderr, "Error: Ambiguous options, use Magick or not?" sys.exit(1) # if opts.old_magick == None: # opts.old_magick = 1 try: opts.thumb_size = int(opts.thumb_size) if opts.thumb_size <= 0: raise ValueError() except ValueError: print >> sys.stderr, "Error: Illegal thumbnail size." sys.exit(1) try: opts.scaled_size = int(opts.scaled_size) if opts.scaled_size <= 0: raise ValueError() except ValueError: print >> sys.stderr, "Error: Illegal scaled image size." sys.exit(1) if opts.thumb_quality: try: opts.thumb_quality = int(opts.thumb_quality) except ValueError: print >> sys.stderr, "Error: Illegal thumbnail quality." sys.exit(1) if opts.scaled_quality: try: opts.scaled_quality = int(opts.scaled_quality) except ValueError: print >> sys.stderr, "Error: Illegal scaled Image quality." sys.exit(1) if opts.ignore_pattern: # pre-compile ignore re for efficiency opts.ignore_re = re.compile(opts.ignore_pattern) if opts.repn_affinity: res = [] for r in string.split(opts.repn_affinity, ','): res.append(re.compile(r)) opts.repn_affinity = res else: opts.repn_affinity = [] if len(filter(None, [opts.out_along, opts.out_subdirs, opts.out_onedir])) > 1: print >> sys.stderr, "Error: Make up your mind, which outputs?" sys.exit(1) elif len(filter(None, [opts.out_along, opts.out_subdirs, opts.out_onedir])) == 0: opts.out_subdirs = 1 # Default # # initialize global constants # cidxext = ".cidx" global index_fn, dirindex_fn, dirattr_fn global allindex_fn, allcidx_fn, trackindex_fn, sortindex_fn, css_fn index_fn = "index" + opts.htmlext dirindex_fn = "dirindex" + opts.htmlext dirattr_fn = config.CommentFile #"dir" + opts.attrext allindex_fn = "allindex" + opts.htmlext allcidx_fn = "allindex" + cidxext trackindex_fn = "trackindex-%s" + opts.htmlext sortindex_fn = "sortindex" + opts.htmlext css_fn = 'style.css' global hor_sep, ver_sep hor_sep = "  \n" ver_sep = "
\n" opts.root = normpath(opts.root) if not exists(opts.root) or not isdir(opts.root): print >> sys.stderr, "Error: root %s doesn't exist." % opts.root sys.exit(1) if opts.magick_path: opts.magick_path = normpath(opts.magick_path) if not exists(opts.magick_path) or \ not isdir(opts.magick_path): print >> sys.stderr, \ "Error: magick-path %s is invalid." % opts.magick_path sys.exit(1) # # Find and process list of images, reading necessary information for each # image. # if opts.quiet: print "====> gathering image list and attributes files" rootdir = Dir("", None, opts.root) # # compute pathnames and some global variables # def prepend(path, pfx): (d, f) = op.split(path) return join(d, pfx, f) allimages = rootdir.getAllImages() alldirs = rootdir.getAllDirs() trackmap = computeTrackmap(allimages) tracks = trackmap.keys() tracks.sort() trackindex_fns = {} for t in tracks: trackindex_fns[t] = trackindex_fn % t def compDirName(dir): dir._pagefn = join(dir._path, dirindex_fn) rootdir.visit(compDirName) for i in allimages: i._pagefn = join(i._dir._path, i._base + opts.htmlext) i._thumbfn = join(i._dir._path, i._base + opts.separator + opts.thumb_sfx) i._scaledfn = join(i._dir._path, i._base + opts.separator + opts.scaled_sfx) if opts.out_subdirs: htmldir = 'html' thumbdir = 'thumb' scaleddir = 'scaled' allindex_fn = prepend(allindex_fn, htmldir) allcidx_fn = prepend(allcidx_fn, htmldir) sortindex_fn = prepend(sortindex_fn, htmldir) css_fn = prepend(css_fn, htmldir) for t in tracks: trackindex_fns[t] = prepend(trackindex_fns[t], htmldir) def prependDirVisitor(dir): dir._pagefn = prepend(dir._pagefn, htmldir) rootdir.visit(prependDirVisitor) for i in allimages: i._pagefn = prepend(i._pagefn, htmldir) i._thumbfn = prepend(i._thumbfn, thumbdir) i._scaledfn = prepend(i._scaledfn, scaleddir) elif opts.out_onedir: od = 'html' allindex_fn = join(od, allindex_fn) allcidx_fn = join(od, allcidx_fn) sortindex_fn = join(od, sortindex_fn) css_fn = join(od, css_fn) for t in tracks: trackindex_fns[t] = join(od, trackindex_fns[t]) def prependDirVisitor(dir): dir._pagefn = join(od, dir._pagefn) rootdir.visit(prependDirVisitor) for i in allimages: i._pagefn = join(od, i._pagefn) i._thumbfn = join(od, i._thumbfn) i._scaledfn = join(od, i._scaledfn) # # If asked to clean, clean and exit. # if opts.clean: if opts.quiet: print "====> cleaning up generated files" clean(allimages, alldirs) if opts.quiet: print "====> done." sys.exit(0) # # Read template files. # if opts.quiet: print "====> templates input and compilation" global templates templates = readTemplates() # # Thumbnail generation. # if opts.quiet: print "====> thumbnail generation and sizes computation" for img in allimages: img.generateThumbnail() # # Scaled Image generation. # if opts.quiet: print "====> thumbnail generation and sizes computation" for img in allimages: img.generateScaled() # # Execute global rc file. # if opts.quiet: print "====> executing global rc file" envir = {} envir.update(globals()) envir.update(locals()) execTemplate(sys.stdout, templates['rc'], envir) # # Output directory indexes # if opts.quiet: print "==> directory index generation" def walkDirsForIndex(dir): # this does not do make the link for existing dirindex if dir.hasImages() == 1: envir['dir'] = dir if opts.quiet: print "generating dirindex", dir._pagefn generatePage(dir._pagefn, 'dirindex', envir) lnfn = join(opts.root, dir._path, index_fn) if islink(lnfn): os.remove(lnfn) if not (opts.no_links or opts.out_onedir) and \ not exists(lnfn) and not islink(lnfn): f = open(lnfn, "w") f.write(''' Redirection

You should be reloacted to the correct URL in a second :

html/dirindex.html

''') f.close() # if hasattr(os, 'symlink'): # os.symlink(rel(dir._pagefn, dir._path), lnfn) rootdir.visit(walkDirsForIndex) # # Output track indexes HTML # if opts.quiet: print "==> track index generation" for track in trackmap.keys(): fn = trackindex_fns[track] envir['dir'] = rootdir envir['track'] = track if opts.quiet: print "generating trackindex", fn generatePage(fn, 'trackindex', envir) # # Output global index HTML file and summary text file. # if opts.quiet: print "==> global index generation" envir['dir'] = rootdir # if opts.quiet: print "generating allindex", allindex_fn # generatePage( allindex_fn, 'allindex', envir ) # if opts.quiet: print "generating sortindex", sortindex_fn # generatePage( sortindex_fn, 'sortindex', envir ) if opts.quiet: print "generating summary", allcidx_fn generateSummary(allcidx_fn, allimages) if opts.quiet: print "generating css file", css_fn generatePage(css_fn, 'css', envir) # # Output image HTML files # if opts.quiet: print "====> image page generation" global walkDirsForImages def walkDirsForImages(dir): for d in dir._subdirs: walkDirsForImages(d) for img in dir._images: envir['dir'] = dir envir['image'] = img if opts.quiet: print "generating image page", img._pagefn generatePage(img._pagefn, 'image', envir) walkDirsForImages(rootdir) # signal the end if opts.quiet: print "====> done." # Run main if loaded as a script if __name__ == "__main__": main() #=============================================================================== # END #=============================================================================== # change 'name' to 'file-or-title' # split stuff that could totally be in the includes.py file # remove table b.s. when it all works # make it work with CSS