#!/usr/bin/env python2.1
#******************************************************************************\
#* $Source: /u/blais/cvsroot/curator/bin/curator,v $
#* $Id: curator,v 1.15 2002/03/19 17:56:43 blais Exp $
#*
#* Copyright (C) 2001, Martin Blais <blais@iro.umontreal.ca>
#*
#* 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.

Curator 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).

Type 'curator --help-script' for help on the scripting environment.

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:

 - <image>.<ext>: the main image file to be displayed in the html page, where
   <ext> is an image extension (e.g. jpg, gif, etc.)


The following is optional, and will be used if present:

 - <image>.desc: a per-image description file containing user-provided
   attributes about the photograph.  The format is, e.g.:

      <attribute-name>: <text>
      <text>
      <text>

      <attribute-name>: <text>

      ...

   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: <trackname1> <trackname2> ...
      specifies the tracks that the image is part of
      
    - ignore: yes
      specifies that the image should be ignored


 - <image>--<string>.<ext>: 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, 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
which contains useful variables and functions.  You can use the following two
tags:

<!--tagcode:
print 'some python code',
for i in images:
    print 'bla'
-->

<!--tag:title-->

The second tag is implemented as 'print <tag contents>,'. 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 test templates
provided with the script.  You can also use the <!--tagcode:printenv()-->
function (but this does not show you the functions).  The ImageEnvironment and
IndexEnvironment classes are the implementation of the script environment.

This should allow for some decent flexibility in the template. If you desire
some more default tags, email the author.



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:

 - <image>--thumb.<ext>: associated image thumbnail.

 - <image>.html
   (for each image, an associated web page which features it)

Thus you will end up with the following files for each image:

 - <image>.<ext>
 - <image>.desc
 - <image>--<string>.<ext>
 - <image>--<string>.<ext>
 - ...
 - <image>--thumb.<ext>
 - <image>.html


In each subdirectory of the root:

 - dirindex.html: an HTML index of the directory, with thumbnails and
   titles.

In the root:

 - trackindex-<track>.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:
------------------------------
  curator <options> [<root>]

If <root> is not specified, we assume cwd is the root.
"""

# Developer's manual (ahahah):
#
# Rules for filenames:
#
#   Within the script, until the very last moment that we need to access a file
#   (where we prepend the root), we keep filenames relative to the
#   root. However, the template programmer sees only filenames relative to the
#   current directory of the generated html file.

__version__ = "$Revision: 1.15 $"

#===============================================================================
# EXTERNAL DECLARATIONS
#===============================================================================

import sys
if int(sys.version[0]) < 2:
    sys.stderr.write( \
        "Error: you need Python version 2 or more to run this friggin' script.")
    sys.exit(1)

from distutils.fancy_getopt \
     import FancyGetopt, OptionDummy, translate_longopt, wrap_text

import os, os.path, dircache
from os.path import join as pjoin

import re, string
from pprint import PrettyPrinter # for nice debug output
import urllib

import imghdr

from traceback import print_exception


#===============================================================================
# PUBLIC DECLARATIONS
#===============================================================================

#===============================================================================
# DIRECTORY NAMES
#===============================================================================

#-------------------------------------------------------------------------------
#
def psplit( path ):

    """Splits a path into a list of components.
    This function works around a quirk in string.split()."""
    
    # FIXME perhaps call os.path.split repetitively would be better.
    s = string.split( path, '/' ) # we work with fwd slash only inside.
    if len( s ) == 1 and s[0] == "":
        s = []
    return s

#-------------------------------------------------------------------------------
#
def html_join(a, *p):
    
    """Version of os.path.join() that uses strickly '/', for legal HTML
    output."""

    # Hopefully, all paths will at some time have gone through this, it should
    # work under Windows.
    p = apply( os.path.join, ( a, ) + p )
    p = p.replace( os.sep, '/' )
    return p

if os.sep != '/':
    pjoin = html_join


#-------------------------------------------------------------------------------
#
def relativize( dest, curdir ):

    """Returns dest filename relative to curdir."""

    sc = psplit( curdir )
    sd = psplit( 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( pjoin, sd )
    elif len( sd ) == 0:
        out = apply( pjoin, map( lambda x: os.pardir, sc ) )
        # FIXME find constant for .. in doc
    else:
        out = apply( pjoin, map( lambda x: os.pardir, sc ) + list( sd ) )
        # FIXME find constant for .. in doc

    # make sure the path is suitable for html consumption
    return out
    
#print relativize( "a", "a/b" )
#print relativize( "a/b", "a" )
#print relativize( "a/b", "c" )
#print relativize( "a", "" )
#print relativize( "", "a" )
#sys.exit(0)

#-------------------------------------------------------------------------------
#
def splitFn( fn ):
    """Returns a triplet of ( base, repn, ext ).  Repn is '' is not present."""
    
    ( dir, bn ) = os.path.split( fn )

    fidx = bn.find( opts.separator )
    if fidx != -1:
        # found separator, add as an alt repn
        base = bn[ :fidx ]
        ( repn, ext ) = os.path.splitext( bn[ fidx + len(opts.separator): ] )
        
    else:
        # didn't find separator, split using extension
        ( base, ext ) = os.path.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 ) = os.path.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 ):
        return magick_path_cache[ progname ]
    else:
        if opts.magick_path:
            prg = pjoin( opts.magick_path, progname )
        else:
            prg = progname
        magick_path_cache[ progname ] = prg

        prg = os.path.normpath( prg )

        # Issue a warning if we can't find the program where specified.
        if opts.magick_path:
            if not os.path.exists( prg ) and not os.path.exists( prg + '.exe' ):
                print >> sys.stderr,\
                      "Warning: can't stat ImageMagick program %s" % prg
                print >> sys.stderr, \
                      "Perhaps try specifying it using --magick-path."

        return prg


#===============================================================================
# ATTRIBUTES FILE PARSING
#===============================================================================


# Note: The AttrFile class is also defined in the
# curator/lib/curator/attrfile.py module.  We cut-and-paste here for simplicity
# of installation (this script is standalone).

#===============================================================================
# CLASS AttrFile
#===============================================================================

class AttrFile:

    """Attributes file representation and trivial parser."""

    #---------------------------------------------------------------------------
    #
    def __init__( self, path ):

        """Constructor."""

        self._path = path
        self._attrmap = {}

    #---------------------------------------------------------------------------
    #
    def read( self ):
        try:
            f = open( self._path, "r" )
            self._lines = f.read()
            f.close()
        except IOError, e:
            print >> sys.stderr, \
                  "Error: cannot open attributes file", self._path
            self._lines = ''

        self.parse( self._lines )

    #---------------------------------------------------------------------------
    #
    def write( self ):
        try:
            # if there are no field, delete the file.
            if len( self._attrmap ) == 0:
                os.unlink( self._path )
                return

            f = open( self._path, "w" )
            for k in self._attrmap.keys():
                f.write( k )
                f.write( ": " )
                f.write( self._attrmap[k] )
                f.write( "\n\n" )
            f.close()
        except IOError, e:
            print >> sys.stderr, "Error: cannot open attributes file", \
                  self._path
            self._lines = ''

    #---------------------------------------------------------------------------
    #
    def parse( self, lines ):

        """Parse attributes file lines into a map."""

        mre1 = re.compile( "^([^:\n]+)\s*:", re.M )
        mre2 = re.compile( "^\s*$", re.M )

        pos = 0
        while 1:
            mo1 = mre1.search( lines, pos )

            if not mo1:
                break
            
            txt = None
            mo2 = mre2.search( lines, mo1.end() )
            if mo2:
                txt = string.strip( lines[ mo1.end() : mo2.start() ] )
            else:
                txt = string.strip( lines[ mo1.end() : ] )
        
            self._attrmap[ mo1.group( 1 ) ] = txt

            if mo2:
                pos = mo2.end()
            else:
                break

    #---------------------------------------------------------------------------
    #
    def get( self, field, default = None ):

        """Returns an attribute field content extracted from this attributes
        file."""

        if self._attrmap.has_key( field ):
            return self._attrmap[ field ]
        else:
            return default

    #---------------------------------------------------------------------------
    #
    def set( self, field, value ):

        """Sets a field of the description file. Returns true if the value has
        changed.  Set a field value to None to remove the field."""

        if value == None:
            if self._attrmap.has_key( field ):
                del self._attrmap[ field ]
                return 1
            else:
                return 0

        # remove stupid dos chars (\r) added by a web browser
        value = value.replace( '\r', '' )
        value = string.strip( value )

        # remove blank lines from the field value
        mre2 = re.compile( "^\s*$", re.M )
        while 1:
            mo = mre2.search( value )
            if mo and mo.end() != len(value):
                outval = value[:mo.start()]
                id = mo.end()
                while value[id] != '\n': id += 1
                outval += value[id+1:]
                value = outval
            else:
                break

        if '\n' in value:
            value = '\n' + value
            
        if self._attrmap.has_key( field ):
            if self._attrmap[ field ] == value:
                return 0
                
        self._attrmap[ field ] = value
        return 1

    #---------------------------------------------------------------------------
    #
    def __getitem__(self, key):
        return self.get[ key ]
    
    #---------------------------------------------------------------------------
    #
    def keys(self):
        return self._attrmap.keys()

    #---------------------------------------------------------------------------
    #
    def has_key(self, key):
        return self._attrmap.has_key(key)

    #---------------------------------------------------------------------------
    #
    def __len__(self):
        return len( self._attrmap )

    #---------------------------------------------------------------------------
    #
    def __repr__( self ):

        """Returns contents to a string for debugging purposes."""

        txt = ""
        for a in self._attrmap.keys():
            txt += a + ":\n" + self._attrmap[a] + "\n\n"
        return txt



 
#===============================================================================
# BASIC CLASSES (for internal representation)
#===============================================================================

#===============================================================================
# CLASS Dir
#===============================================================================

class Dir:

    """Directory class."""

    #---------------------------------------------------------------------------
    #
    def __init__( self, root, dirname ):

        """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._dirname = dirname
        self._subdirs = []
        self._images = []
        self._tracks = []
        self._attrfile = None

        files = dircache.listdir( pjoin( root, dirname ) )

        pattr = pjoin( root, self._dirname, dirattr_fn )
        if os.path.exists( pattr ) and os.path.isfile( pattr ):
            self._attrfile = AttrFile( pattr )
            self._attrfile.read()
            if opts.verbose and self._attrfile.keys():
                print "  read attributes", self._attrfile.keys()

        imgmap = {}
        for f in files:
            af = pjoin( dirname, f )
            paf = pjoin( root, af )

            # add subdir
            if os.path.isdir( paf ):
                subdir = Dir( root, af )
                # 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.verbose: 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( dirname, base )
                    imgmap[ base ] = img

                if repn:
                    img._calts.append( repn + ext )
                else:
                    img._salts.append( ext )

        #PrettyPrinter().pprint( imgmap )

        # Detect thumbnails, imagepages, attributes files.
        for i in imgmap.keys():
            e = imgmap[i]
            print "looking for images with base '%s'" % pjoin( e._dir, e._base )
            e.cleanAlts()
            if not e.selectName( pjoin( root, dirname ) ):
                del imgmap[i]
            print

        #PrettyPrinter().pprint( imgmap )

        for f in imgmap.keys():
            img = imgmap[f]
            img.init( self._attrfile )
            if img._ignore:
                print "ignoring", img._base, "from desc file ignore tag."
                continue
            self._images.append( img )
        
        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 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._dirname ]
        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

        
#===============================================================================
# CLASS Image
#===============================================================================

class Image:

    """Image specific processing and storage."""

    #---------------------------------------------------------------------------
    #
    def __init__( self, reldir, base ):

        """Constructor. Initialize to accumulation information."""

        self._dir = reldir # directory relative to root
        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._thumb = None # thumbnail filename (i.e. base--thumb.gif)
        self._attr = None # attributes filename boolean

    #---------------------------------------------------------------------------
    #
    def init( self, dirattrfile = None ):

        """Performs proper initialization."""

        self._filename = pjoin( self._dir, 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 = pjoin( opts.root, self._dir, self._base + self._attr )
            if os.path.exists( pattr ) and os.path.isfile( pattr ):
                attr = AttrFile( pattr )
                attr.read()
                if opts.verbose and attr.keys():
                    print "  read attributes", attr.keys()

                self._attr = attr


        # Make up thumbnail name.
        if self._thumb:
            self._thumb = pjoin( self._dir, \
                                 self._base + opts.separator + self._thumb )
        else:
            self._thumb = ""

        # Special pre-defined attributes.
        self._title = None
        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

    #---------------------------------------------------------------------------
    #
    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._thumb )
        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.verbose:
                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.verbose:
                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.verbose:
                    print "  detected thumb file '%s'" % k
                self._thumb = 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.verbose:
            print "  ", self._salts + self._calts
            #PrettyPrinter(indent=4,width=79).pprint( ee )

        #
        # 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 = pjoin( 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 ) = os.path.splitext( f )
                        
                        if opts.verbose:
                            print "  choosing affinity '%s'" % f
                        return 1
                    else:
                        if opts.verbose:
                            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:
            ff = self._base + f
            paf = pjoin( 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 ) = os.path.splitext( f )
                if opts.verbose:
                    print "  choosing imagefile '%s'" % f
                return 1
            else:
                if opts.verbose:
                    print "  ignoring non-image '%s'" % f

        if opts.verbose:
            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( '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( 'ignore' )
        if ignore:
            self._ignore = 1

        title = attrfile.get( 'title' )
        if title:
            self._title = title

    #---------------------------------------------------------------------------
    #
    def getprop( self, curdir, ttype = 'html' ):

        """Returns property of image, suitable for scripting consumption."""
        # Remember to update ImageEnvironment.getprop() if you change this.

        if ttype == 'html':
            return relativize( pjoin( self._dir, self._base ), curdir ) + \
                   opts.htmlext
        elif ttype == 'thumb':
            return relativize( self._thumb, curdir )
        elif ttype == 'image':
            return relativize( self._filename, curdir )
        elif ttype == 'name':
            # The name is used to refer to an image in text form (instead of
            # thumbnail), we set it to either the title or the filename.
            if self._title and self._title != "":
                return self._title
            else:
                return relativize( pjoin( self._dir, self._base ), curdir )
        return None

    #---------------------------------------------------------------------------
    #
    def getattr( self, tag ):

        """Returns an attribute from the attribute files for this image.  This
        method looks up the tag from both the image-specific attributes file and
        the directory attributes file."""

        if self._attr:
            return self._attr.get( tag )
        elif not tag and self._dirattr:
            return self._dirattr.get( tag )
        else:
            return None
        

    #---------------------------------------------------------------------------
    #
    def generateThumbnail( img, force = None ):

        """Generates a thumbnail for an image.
    
        Make it so that the longest dimension is the specified dimension."""

        if img._thumb != "":
            thumbfn = img._thumb
        else:
            thumbfn = pjoin( img._dir, \
                             img._base + opts.separator + \
                             opts.thumb_sfx + opts.newthumbext )
        cfn = pjoin( opts.root, img._filename )
        athumbfn = pjoin( opts.root, thumbfn )

        if img._thumb:
            if not force:
                # Check if thumbsize has changed
    
                if opts.fast or opts.check_thumb_size == 0:
                    if opts.verbose:
                        print "thumbnail %s already generated" % thumbfn
                        return
                else:
                    if not checkThumbSize( imageSize(cfn),\
                                           imageSize(athumbfn), \
                                           opts.thumb_size ):
                        if opts.verbose:
                            print "thumbnail %s size has changed" % athumbfn
                        try:
                            # Clear cache for thumbnail size.
                            del imageSizeCache[ athumbfn ]
                        except:
                            raise "Internal error with image cache."
                    else:
                        if opts.verbose:
                            print "thumbnail %s already generated (size ok)" \
                                  % athumbfn
                        return
            else:
                if opts.verbose: print "forced regeneration of", thumbfn
    
        if opts.no_magick:
            if opts.verbose:
                print "ImageMagick tools disabled, can't create thumbnail"
            return
            
        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"' % ( cfn, subimg, athumbfn )
    
        if opts.verbose: print "generating thumbnail", 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:" % cfn
            errs = string.join(errs, '\n')
            print errs
            
            if subimg and re.compile('Unable to read subimage').search(errs):
                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:" % cfn
                    print string.join(errs, '\n')
                
        img._thumb = thumbfn

#===============================================================================
# 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 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 mo:
            ( width, height ) = map( lambda x: int(x), mo.groups() )
            return ( width, height )
        return ( 0, 0 )

    
#-------------------------------------------------------------------------------
#
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."""

    try:
        size = imageSizeCache[path]
        return size
    except KeyError:
        imageSizeCache[path] = imageSizeNoCache( path )
        return imageSizeCache[path]
        
    
    try:
        cached_mtime, size = imageSizeCache[path]
        del imageSizeCache[path]
    except KeyError:
        cached_mtime, size = -1, []

    try:
        mtime = os.stat(path)[8]
    except os.error:
        return (0,0)

    if mtime != cached_mtime:
        try:
            size = imageSizeNoCache( path )
        except os.error:
            return []

    imageSizeCache[path] = mtime, size
    return size


#===============================================================================
# SCRIPT ENVIRONMENT
#===============================================================================

#===============================================================================
# CLASS CommentEnvironment
#===============================================================================
        
class CommonEnvironment:
    
    #---------------------------------------------------------------------------
    #
    def __init__( self, dir ):

        # Remember to update generateScriptHelp() if you change this.

        if not dir: return

        # Hack thus this cannot be multithreaded
        CommonEnvironment.curself = self

        self.curdir = dir._dirname
        self.rootdir =  pjoin( relativize( "", self.curdir ) )

        self.subdirs = []
        for d in dir._subdirs:
            self.subdirs.append( relativize( d._dirname, self.curdir ) )
        self.dirimages = dir._images
        self.allimages = allimages
        # for tracks, call trackimages( 'trackname' )

        # Index filenames.
        self.dirindex = dirindex_fn
        self.allindex = pjoin( relativize( "", self.curdir ), allindex_fn )
        self.sortindex = pjoin( relativize( "", self.curdir ), sortindex_fn )
        self.rootdirindex =  pjoin( relativize( "", self.curdir ), dirindex_fn )

        self.requested_thumb_size = opts.thumb_size

        self.quote = urllib.quote
        self.unquote = urllib.unquote
        
    #---------------------------------------------------------------------------
    #
    def trackimages( trackname ):

        """(trackname: string)

        Returns a list of image objects for a specific track.  You can access
        some properties of the image through get() or image attributes through
        getattr(). """

        try:
            return trackmap[ trackname ]
        except KeyError:
            raise "Inexistent track name: " + trackname

    #---------------------------------------------------------------------------
    #
    def trackindex( trackname ):

        """(trackname: string)

        Returns the index filename for a specific track. This can be used to
        link to it."""

        self = CommonEnvironment.curself
        return pjoin( relativize( "", self.curdir ), trackindex_fn % trackname )

    #---------------------------------------------------------------------------
    #
    def env():

        """()

        Returns a textual representation of the set of variables freely
        available in the environment. This can be quite useful which debugging
        your template. Put this at the end of your template and observe what
        variables the it gets when rendered. It looks ok between <pre> tags."""

        self = CommonEnvironment.curself
        txt = ""
        for v in self.__dict__.keys():
            if v[0] != '-':
                txt += v + ":" + repr( self.__dict__[ v ] ) + "\n"
        return txt

    #---------------------------------------------------------------------------
    #
    def getindex( dirfn ):

        """(dirfn: string)

        Returns the filename of the directory index which contains this
        image."""

        return pjoin( dirfn, dirindex_fn )

    #---------------------------------------------------------------------------
    #
    def getprop( ttype, img ):

        """See getprop() in ImageEnvironment."""

        if not img:
            return None

        self = CommonEnvironment.curself
        return img.getprop( self.curdir, ttype )

    #---------------------------------------------------------------------------
    #
    def getattr( tag, img ):

        """(img: Image, tag: string)

        Returns an attribute from the attribute files for a specific
        image."""

        return img.getattr( tag )


    #
    # Utility functions
    #

    #---------------------------------------------------------------------------
    #
    def dirnav( rootname = "(root)", dirsep = " / ", dir = None, \
                ignoreCurrent = 1 ):

        """(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."""

        self = CommonEnvironment.curself

        if not dir:
            dir = self.curdir

        dcomps = psplit( dir )
        #print "dcomps", dcomps
    
        dirs_text = ""
    
        ddir = relativize( "", self.curdir )
        if ignoreCurrent == 0 or ddir != "":
            dirs_text += "<A HREF=\"" + \
                         urllib.quote( pjoin( ddir, dirindex_fn ) ) + \
                         "\">" + rootname + "</A>"
        else:
            dirs_text += rootname
        dirs_text += dirsep
    
        ccomp = ""
        for c in dcomps:
            ccomp = pjoin( ccomp, c )
            ddir = relativize( ccomp, self.curdir )
            if ignoreCurrent == 0 or ddir != "":
                dirs_text += "<A HREF=\"" + \
                             urllib.quote( pjoin( ddir, dirindex_fn ) ) + \
                             "\">" + c + "</A>"
            else:
                dirs_text += c
            dirs_text += dirsep
    
        # Remove extraneous separator.
        dirs_text = dirs_text[:-len(dirsep)]
        return dirs_text

    #---------------------------------------------------------------------------
    #
    def imageSrc( imgsrc, computeSize = 1, imgxtra = "" ):

        """(imgsrc: string, computeSize: bool, imgxtra: string)

        Utility that generates an image, with width and height if computeSize =
        1 and if it succeeds getting the size, and user-specified extra fields,
        you can put ALT here, or anything else.

        Typical usage is, e.g.
        imageSrc(getprop('image',i), 1)"""
        
        # imgsrc is a relative path

        self = CommonEnvironment.curself
        if not imgsrc:
            return ''

        if computeSize == 1:
            (w,h) = imageSize( pjoin( opts.root, self.curdir, imgsrc ) )
            if w == 0 or h == 0:
                return '<IMG SRC="%s" %s>' % (urllib.quote(imgsrc), imgxtra)
            else:
                return '<IMG SRC="%s" WIDTH=%d HEIGHT=%d' % \
                       (urllib.quote(imgsrc), w, h) + \
                       ' %s>' % (imgxtra)
        else:
            return '<IMG SRC="%s" %s>' % (imgsrc, imgxtra)

    #---------------------------------------------------------------------------
    #
    def anchor( anch, text ):

        """(anchor: string, text:string)

        Utility that links some text to an anchor."""
        
        return '<A HREF="%s">\n%s</A>' % (urllib.quote(anch), text)


    #---------------------------------------------------------------------------
    #
    def linkImage( imgsrc, anch, computeSize = 1, imgxtra = "" ):

        """(imgsrc: string, anchor: string, computeSize: bool, imgxtra: string)

        Utility that generates linked image, with width and height if
        computeSize = 1 and if it succeeds getting the size.

        Typical usage is, e.g.
        linkImage(getprop('thumb',i), getprop('html',i), 1, 'ALIGN=LEFT' )"""
        
        return CommonEnvironment.anchor.im_func( anch, \
            CommonEnvironment.imageSrc.im_func( imgsrc, computeSize, imgxtra ) )


    #---------------------------------------------------------------------------
    #
    def table( 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
        nuder 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."""
    
        self = CommonEnvironment.curself

        dtxt = ""
        if len(images) == 0:
            return ""
    
        idx = 0
        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.
            dtxt += "<CENTER><TABLE WIDTH=\"100%\">\n" 
    
            dtxt += "<TR ALIGN=CENTER>\n"
            for i in isubset:
                dtxt += "<TD>\n"
                # FIXME perhaps wouldn't need to relativize filename here.

                altname = 'ALT="%s"' % i.getprop( self.curdir, 'name' )

                dtxt += CommonEnvironment.linkImage.im_func( \
                    relativize( i._thumb, self.curdir ),\
                    pjoin( relativize( i._dir, self.curdir ), \
                           i._base + opts.htmlext ),
                    imgxtra = altname )

                if textfun:
                    dtxt += textfun( i )

                dtxt += "</TD>\n"
    
            for i in range( 0, cols - len( isubset ) ):
                dtxt += "<TD></TD>\n"
                
            dtxt += "</TR>\n"
    
            dtxt += "</TABLE></CENTER>\n"
        return dtxt
        
    #---------------------------------------------------------------------------
    #
    def tableTextCb( image ):

        """Simple text callback to include some text under images in a table."""
        
        self = CommonEnvironment.curself

        dtxt = ""
        pfn = 1
        ptitle = 1
        if pfn or ptitle:
            dtxt += '<BR>'

        if pfn:
            dtxt += "<FONT SIZE=\"-2\">\n"
            ( dir, base, repn, ext ) = \
              splitFn( image.getprop( self.curdir, 'image' ) )
            dtxt += pjoin( dir, base )
            dtxt += "</FONT>\n"
            
        if ptitle and image._title:
            dtxt += '<I>'
            if pfn:
                dtxt += '<BR>'
            dtxt += "\n<FONT SIZE=\"-2\">"
            dtxt += image._title + "</FONT>\n"
            dtxt += '</I>'

        return dtxt
    

#===============================================================================
# CLASS IndexEnvironment
#===============================================================================
        
class IndexEnvironment(CommonEnvironment):

    """Exported environment for generating index files."""

    curself = None
    
    #---------------------------------------------------------------------------
    #
    def __init__( self, dir ):

        # Remember to update generateScriptHelp() if you change this.

        if not dir: return

        CommonEnvironment.__init__( self, dir )

        # private
        self._dir = dir

        # Expose variables to the script environment here. Costly operations
        # that should not necessarily be performed should be implemented as
        # member functions of this object, which become simple functions to the
        # script user (i.e. the template).
        
        self.track = None # Overriden by track generating func
        self.tracks = []

    #---------------------------------------------------------------------------
    #
    def getattr( tag ):

        """(tag: string)

        Returns a tag from the directory's attributes file."""
        
        self = CommonEnvironment.curself
        if self._dir._attrfile:
            return self._dir._attrfile.get( tag )
        return None



#===============================================================================
# CLASS ImageEnvironment
#===============================================================================
        
class ImageEnvironment(CommonEnvironment):

    """Exported environment for generating image files."""

    curself = None
    
    #---------------------------------------------------------------------------
    #
    def __init__( self, img, dir ):

        # Remember to update generateScriptHelp() if you change this.

        if not dir or not img: return
        CommonEnvironment.__init__( self, dir )

        # private
        self.imageobj = img

        # Expose variables to the script environment here. Costly operations
        # that should not necessarily be performed should be implemented as
        # member functions of this object, which become simple functions to the
        # script user (i.e. the template).
        
        self.basename = img._base

        self.image = os.path.basename( img._filename )

        self.altrepns = {}
        # make up map of altrepns
        for k in img._salts:
            self.altrepns[ k[1:] ] = self.basename + k
        for k in img._calts:
            self.altrepns[ k ] = self.basename + opts.separator + k

        # special pre-defined attributes.
        self.title = img._title
        self.tracks = img._tracks
        self.ignore = img._ignore

        self.thumb = relativize( img._thumb, self.curdir )


    #---------------------------------------------------------------------------
    #
    def thumbSize():

        """()

        Returns (width, height) pair for thumbnail file."""

        self = CommonEnvironment.curself
        return imageSize( pjoin( opts.root, self.imageobj._thumb ) )

    #---------------------------------------------------------------------------
    #
    def next( imglist ):

        """(imglist: seq of Image)

        Returns the image next to this image in the given list, or None if at
        the end."""

        self = CommonEnvironment.curself
        idx = imglist.index( self.imageobj )
        if idx+1 < len( imglist ):
            return imglist[ idx+1 ]
        return None

    #---------------------------------------------------------------------------
    #
    def prev( imglist ):

        """(imglist: seq of Image)

        Returns the image previous to this image in the given list, or None
        if at the end."""

        self = CommonEnvironment.curself
        idx = imglist.index( self.imageobj )
        if idx-1 >= 0:
            return imglist[ idx-1 ]
        return None

    #---------------------------------------------------------------------------
    #
    def cycnext( imglist ):

        """(imglist: seq of Image)

        Returns the image next to this image in the given list, cycling if at
        the end."""

        self = CommonEnvironment.curself
        idxnext = ( imglist.index( self.imageobj ) + 1 ) % len(imglist)
        return imglist[ idxnext ]

    #---------------------------------------------------------------------------
    #
    def cycprev( imglist ):

        """(imglist: seq of Image)

        Returns the image previous to this image in the given list, cycling if
        at the beginning."""

        self = CommonEnvironment.curself
        idxprev = ( imglist.index( self.imageobj ) - 1 ) % len(imglist)
        return imglist[ idxprev ]

    #---------------------------------------------------------------------------
    #
    def imgsize( imgsrc = None ):

        """(imglist: seq of Image)

        Returns the image size of the specified image path, or of this image if
        no path is specified."""

        self = CommonEnvironment.curself
        if imgsrc:
            (w,h) = imageSize( pjoin( opts.root, self.curdir, imgsrc ) )
        else:
            (w,h) = imageSize( pjoin( opts.root, self.imageobj._filename ) )
        return (w,h)

    #---------------------------------------------------------------------------
    #
    def width( imgsrc = None ):

        """(imgsrc: string)

        Returns the width of the specified image path, or of this image if no
        path is specified."""

        fo = ImageEnvironment.imgsize.im_func
        return fo(imgsrc)[0]

    #---------------------------------------------------------------------------
    #
    def height( imgsrc = None ):

        """(imgsrc: string)

        Returns the height of the specified image path, or of this image if no
        path is specified."""

        fo = ImageEnvironment.imgsize.im_func
        return fo(imgsrc)[1]

    #---------------------------------------------------------------------------
    #
    def getprop( ttype, img = None ):
        
        """(ttype: string)

        Returns an image property, which is a value computed by the script
        (i.e. not an attribute. Valid properties are:

        'html':  the HTML image page filename
        'image': the image path
        'thumb': the image thumbnail image path
        'name':  an always valid text representation for the image (title or
                 image file basename)
        """

        self = CommonEnvironment.curself
        if not img:
            img = self.imageobj
        return CommonEnvironment.getprop.im_func( ttype, img )

    #---------------------------------------------------------------------------
    #
    def getattr( tag, img = None ):

        """(tag: string)

        Returns an attribute from the attribute files associated to this
        image or in this directory, if not found in image file."""

        self = CommonEnvironment.curself
        if not img:
            img = self.imageobj
        return CommonEnvironment.getattr.im_func( tag, img )


    #
    # Utils
    #
    
    #---------------------------------------------------------------------------
    #
    def textnav( 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 = ImageEnvironment.cycprev.im_func
            nextfunc = ImageEnvironment.cycnext.im_func
        else:
            prevfunc = ImageEnvironment.prev.im_func
            nextfunc = ImageEnvironment.next.im_func

        t = ""

        pi = CommonEnvironment.getprop.im_func( 'html', prevfunc( imglist ) )
        if pi:
            t += "<A HREF=\"%s\">" % urllib.quote( pi )
            t += "<FONT SIZE=\"-2\">prev</FONT></A>\n"

        if midtext != "":
            t += "["
            if middest:
                t += "<A HREF=\"%s\"><FONT SIZE=\"-2\">%s</FONT></A>" % \
                     ( urllib.quote( middest ), midtext )
            else:
                t += "<FONT SIZE=\"-2\">" + midtext + "</FONT>"
            t += "]\n"
        else:
            if pi:
                t += "&nbsp;\n"
            
        ni = CommonEnvironment.getprop.im_func( 'html', nextfunc( imglist ) )
        if ni:
            t += "<A HREF=\"%s\">" % urllib.quote( ni )
            t += "<FONT SIZE=\"-2\">next</FONT></A>\n"
        return t


    #---------------------------------------------------------------------------
    #
    def thumbnav( imglist, midtext = "", middest = None, pcycling = 0 ):

        """(imglist: seq of Image, midtext: string, middest: string,
        pcycling: bool)

        Returns a neato thumbnail track navigation widgets, similar to textnav,
        except with thumbnails. Set pcycling to 1 if you want it cycling."""

        self = CommonEnvironment.curself

        if pcycling == 1:
            prevfunc = ImageEnvironment.cycprev.im_func
            nextfunc = ImageEnvironment.cycnext.im_func
        else:
            prevfunc = ImageEnvironment.prev.im_func
            nextfunc = ImageEnvironment.next.im_func

        t = ""
        t += "<TABLE WIDTH=\"1%\">\n<TR ALIGN=CENTER>\n<TD>\n"

        pi = prevfunc( imglist )
        if pi:
            altname = 'ALT="%s"' % pi.getprop( self.curdir, 'name' )

            t += CommonEnvironment.linkImage.im_func(\
                CommonEnvironment.getprop.im_func( 'thumb', pi ),\
                CommonEnvironment.getprop.im_func( 'html', pi ),\
                imgxtra = altname )

        t += "</TD>\n<TD>\n"

        if midtext != "":
            t += "["
            if middest:
                t += "<A HREF=\"%s\"><FONT SIZE=\"-2\">%s</FONT></A>" % \
                     ( urllib.quote( middest ), midtext )
            else:
                t += "<FONT SIZE=\"-2\">" + midtext + "</FONT>"
            t += "]\n"
        else:
            if pi:
                t += "&nbsp;\n"
            
        t += "</TD>\n<TD>\n"

        ni = nextfunc( imglist )
        if ni:
            altname = 'ALT="%s"' % ni.getprop( self.curdir, 'name' )

            t += CommonEnvironment.linkImage.im_func(\
                    CommonEnvironment.getprop.im_func( 'thumb', ni ),\
                    CommonEnvironment.getprop.im_func( 'html', ni ),\
                    imgxtra = altname )

        t += "</TD>\n</TR>\n</TABLE>\n"

        return t

#-------------------------------------------------------------------------------
#
def generateScriptHelp():

    """Generates a string with the help for the scripting environment."""

    def generateEnvDoc( envir ):
        cols = 78
        ot = ""
        for e in envir.keys():
            ee = envir[e]
            try:
                tt = e + envir[e].__doc__
                for l in wrap_text( del_ws( tt ), cols ):
                    ot += l + "\n"
                ot += "\n"
            except:
                pass
        return ot
    

    t = \
"""This is the automatically generated reference for the script environment that
is to be used for templates. There are two types of environments:

  An image environment, which is used during the processing of
  template-image.html;

  An index environment, which is used during the processing of
  template-index.html;

"""

    t += "----------------------------------------\n"
    t += """Variables and functions defined in both environments:

  curdir: current directory
  rootdir: root directory filename
  subdirs: list of subdirectory names
  
  dirimages: list of images in this directory
  allimages: list of all images
  trackimages (trackindex only): list of images in this track
  
  dirindex = directory index HTML filename
  allindex = global index HTML filename
  sortindex = global index HTML filename
  rootdirindex = root directory HTML filename

  requested_thumb_size = requested thumbnail size (one side, an 'int')

"""
    envir = {}
    envir = vars( CommonEnvironment )
    t += generateEnvDoc( envir )
    
    t += "----------------------------------------\n"
    t += """Variables and functions defined in the image environment only:

  imageobj: the current image object (to use in getprop() and getattr()) 
  image: filename of the image
  basename: basename of the image file

  altrepns: a list of strings for alternative representations

  title: title of the image, if present
  tracks: a list of track names
  ignore: 1 if this image is ignored (will always be 0)

  thumb: associated thumbnail filename

  Use getprop() and getattr() to access more information about an image object.

"""

    envir = {}
    envir = vars( ImageEnvironment )
    t += generateEnvDoc( envir )
    
    t += "----------------------------------------\n"
    t += """Variables and functions defined in the index environment only:

track: current track (in trackindex only)

"""

    envir = {}
    envir = vars( IndexEnvironment )
    t += generateEnvDoc( envir )

    return t

#===============================================================================
# PAGE GENERATION
#===============================================================================

#-------------------------------------------------------------------------------
#
def generateImagePage( fn, img, dir, rcenv ):

    """Generates an image page, replacing the tags as needed."""
    
    if opts.verbose: print "generating image page for", img._filename

    # compute environment for image
    envobj = ImageEnvironment( img, dir )
    envir = {}
    envir.update( rcenv )
    envir.update( vars( envobj ) )
    envir.update( vars( CommonEnvironment ) )
    envir.update( vars( ImageEnvironment ) )

    # Write out modified file.
    try:
        afn = pjoin( opts.root, fn )
        tfile = open( afn, "w" )
        execTemplate( tfile, templates['image'], envir )
        tfile.close()

    except IOError, e:
        print >> sys.stderr, "Error: can't open file: %s" % fn

#-------------------------------------------------------------------------------
#
def generateIndexPage( fn, dir, ttype, rcenv, track = None ):

    """Generates an index page, replacing the tags as needed."""
    
    if opts.verbose:
        print "generating index page for", fn

    # compute environment for image
    envobj = IndexEnvironment( dir )
    if ttype == 'allindex' or ttype == 'sortindex':
        envobj.images = allimages
        envobj.tracks = trackmap.keys()
    elif ttype == 'dirindex':
        envobj.images = envobj.dirimages
        envobj.tracks = dir._tracks
    elif ttype == 'trackindex':
        envobj.images = trackmap[ track ]
        envobj.track = track

    envir = {}
    envir.update( rcenv )
    envir.update( vars( envobj ) )
    envir.update( vars( CommonEnvironment ) )
    envir.update( vars( IndexEnvironment ) )

    # Write out modified file.
    try:
        afn = pjoin( 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 ):

    """Generates the text index that could be used by other processing tools."""

    otext = ""

    for i in allimages:
        l = i._filename
        l += ','
        if i._title:
            l += i._title
        # Make sure it's on a single line
        l = string.replace( l, '\n', ' ' )
        otext += l + '\n'

    # Write out file.
    try:
        afn = pjoin( opts.root, fn )
        tfile = open( afn, "w" )
        tfile.write( otext )
        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 )
        if not opts.no_meta:
            ttext = addTemplateMeta( ttext )
        templates[ tt ] = 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 = pjoin( opts.templates, tfn )
        if opts.verbose: print "  looking in %s" % fn
        if os.path.exists( fn ):
            found = 1

    # check in hierarchy root
    if not found:
        fn = pjoin( opts.root, tfn )
        if opts.verbose: print "  looking in %s" % fn
        if os.path.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 = pjoin( p, tfn )
                if opts.verbose: print "  looking in %s" % fn
                if os.path.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[ os.path.splitext( tfn )[0] ]
        except KeyError:
            t = ''

    # Save templates in root, if it was requested.
    if opts.save_templates and foundInRoot == 0:
        rootfn = pjoin( opts.root, tfn )
        if opts.verbose: print "  saving template in %s" % rootfn

        # saving the file template
        if os.path.exists( rootfn ):
            bakfn = pjoin( 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 addTemplateMeta( ttext ):

    """Add a generator meta tag for this script. This can be disabled with an
    option."""

    gentag = '\n   <META NAME="GENERATOR" CONTENT="Curator (%s)">' % \
             __version__[1:-2]
    
    mo = re.compile( '<HEAD>', re.IGNORECASE ).search( ttext )
    if mo:
        otext = ttext[:mo.end()] + gentag + ttext[mo.end():] 
        return otext

    # no head tag, fine, let's just add one
    mo = re.compile( '<HTML>', re.IGNORECASE ).search( ttext )
    if mo:
        gentag = '\n<HEAD>' + gentag + '\n</HEAD>'
        otext = ttext[:mo.end()] + gentag + ttext[mo.end():] 
        return otext

    # bah... give up.
    return ttext


#-------------------------------------------------------------------------------
#
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( "<!--tag(?P<code>code)?:\s*" )
    mre2 = 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.
                #
                #ss.ignoreNextChar()
                #print
                #
                # 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.

            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 curator in the current directory and
    below."""

    for img in allimages:
        # Delete HTML files
        htmlfn = pjoin( opts.root, img._dir, img._base + opts.htmlext )
        if os.path.exists( htmlfn ):
            if opts.verbose:
                print "Deleting", htmlfn
            try:
                os.unlink( htmlfn )
            except:
                print >> sys.stderr, "Error: deleting", htmlfn

        # Delete thumbnails
        if img._thumb:
            thumbfn = pjoin(opts.root, img._thumb)
            if os.path.exists( thumbfn ):
                if opts.verbose:
                    print "Deleting", thumbfn
                try:
                    os.unlink( thumbfn )
                    img._thumb = None
                except:
                    print >> sys.stderr, "Error: deleting", thumbfn

    for d in alldirs:
        files = dircache.listdir( pjoin(opts.root, d) )

        # Delete HTML files in directories
        for f in files:
            if f in [ dirindex_fn, allindex_fn, allcidx_fn, sortindex_fn ] or \
               f.startswith( 'trackindex-' ):
                fn = pjoin( opts.root, d, f )
                if opts.verbose:
                    print "Deleting", fn
                try:
                    os.unlink( fn )
                    pass
                except:
                    print >> sys.stderr, "Error: deleting", 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.

fallbackTemplates = {}

fallbackTemplates[ 'template-image' ] = \
"""<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">
<HTML>
<HEAD>
   <META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=iso-8859-1\">
   <TITLE>Image: <!--tag:getprop('name')--></TITLE>
</HEAD>
<BODY BGCOLOR=\"#FFFFFF\">

<TABLE WIDTH=\"100%\" BGCOLOR=\"#EEEEEE\" CELLPADDING="5"><TR><TD>
<B>
<!--tag:dirnav(dir=curdir,ignoreCurrent=0,dirsep=' / ')-->
 / 
<A HREF=\"<!--tag:quote(image)-->\"><!--tag:image--></A>
</B><BR>
<CENTER><FONT SIZE=\"+2\"><B><!--tag:getprop('name')--></B></FONT></CENTER>
</TD></TR></TABLE>
<BR>

<CENTER>
<!--tagcode:
imagefile=getprop('image')
if imagefile:
    print imageSrc(imagefile, 1, 'BORDER=3 ALIGN=\"MIDDLE\" ALT=\"' + imagefile + '\"' )
-->
<P>

<!--tagcode:
d = getattr('description')
if d:
    print d
--><P>


<!--previous and next thumbnails, with text in between-->
<TABLE WIDTH=\"100%\" BORDER=\"0\">
<TR>

<!--tagcode:
pi = prev( dirimages )

pw = requested_thumb_size
if pi:
    thumbfn = getprop('thumb',pi)
    pw = width(thumbfn)

print '<TD WIDTH=\"%d\">' % pw
if pi:
    print linkImage(thumbfn, \
        getprop('html',pi), 1, 'ALIGN=LEFT ALT=\"Previous in dir\"' )
-->

</TD><TD ALIGN=\"CENTER\" BGCOLOR=\"#EAEAEA\">

<!--dirnav, alternative representations and navigation, 
between the floating thumbs-->

<FONT SIZE=\"-2\">
<!--tag:dirnav(dir=curdir,ignoreCurrent=0,dirsep=' / ')-->
 / 
<A HREF=\"<!--tag:quote(image)-->\"><!--tag:image--></A>
</FONT>
<BR>

<!--tagcode:
if len( altrepns ) > 0:
    print '<FONT SIZE=\"-2\">'
    print \"Alternative representations:\"
    for rep in altrepns.keys():
        print '<A HREF=\"%s\">%s</A>&nbsp;' % \
            ( quote(altrepns[ rep ]), rep )
    print '</FONT>'
-->

<BR>
<!--tagcode:
print textnav( dirimages, 'dir', dirindex )

if len(tracks) > 0:
    print '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<B>tracks:</B>'
    for t in tracks:
        print '&nbsp;&nbsp;&nbsp;&nbsp;'
        print textnav( trackimages(t), t, trackindex(t) )

print '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'
print textnav( allimages, 'all', allindex )
--><P>

</TD>

<!--tagcode:
ni = next( dirimages )

nw = requested_thumb_size
if ni:
    thumbfn = getprop('thumb',ni)
    nw = width(thumbfn)
print '<TD WIDTH=\"%d\">' % nw

if ni:
    print linkImage(thumbfn, \
                    getprop('html',ni), 1, 'ALIGN=RIGHT ALT=\"Next in dir\"' )
-->

</TD></TR></TABLE>

</CENTER>

</BODY>
</HTML>
"""

# Image table instead of border
#<TABLE WIDTH=<!--tag:width()--> HEIGHT=<!--tag:height()-->
#CELLPADDING=0 BGCOLOR=\"#000000\"><TR><TD>
#
#</TD></TR></TABLE>


fallbackTemplates[ 'template-dirindex' ] = \
"""<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">
<HTML>
<HEAD>
   <META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=iso-8859-1\">
   <TITLE>Directory index: <!--tag:curdir--></TITLE>
</HEAD>
<BODY BGCOLOR=\"#FFFFFF\">

<TABLE WIDTH=\"100%\" BGCOLOR=\"#EEEEFF\"><TR><TD>
<H3><!--tag:dirnav(dir=curdir)--></H3>
<B>Directory Index</B>
</TD></TR></TABLE>

<DIV ALIGN=RIGHT>
<A HREF=\"<!--tag:allindex-->\">
Global Index</A><P></DIV>

<!--tagcode:
if len( subdirs ) > 0:
    print '<H3>Sub-directories:</H3>'
    print '<UL>'
    for d in subdirs:
        print '<LI><A HREF=\"%s\">%s</A></LI>' % \
            ( getindex(d), d )
    print '</UL><P>'
-->

<!--tagcode:
if len( images ) > 0:
    print '<H3>Images:</H3>'
    print table( images, tableTextCb )
    
    print '<H3>Images by name:</H3>'
    print '<UL>'
    for i in images:
        print '<LI><A HREF=\"%s\">%s</A></LI>' % \
            ( quote(getprop('html',i)), getprop('name',i) )
    print '</UL>'
-->

</BODY>
</HTML>
"""

fallbackTemplates[ 'template-trackindex' ] = \
"""<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">
<HTML>
<HEAD>
   <META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=iso-8859-1\">
   <TITLE>Track index: <!--tag:track--></TITLE>
</HEAD>
<BODY BGCOLOR=\"#FFFFFF\">

<TABLE WIDTH=\"100%\" BGCOLOR=\"#DDFFDD\"><TR><TD>
<H3>Track index: <!--tag:track--></H3>
</TD></TR></TABLE>

<DIV ALIGN=RIGHT><A HREF=\"<!--tag:allindex-->\">
Global Index</A><P></DIV>

<!--tagcode:
if len( images ) > 0:
    print '<H3>Images:</H3>'
    print table( images, tableTextCb )
    
    print '<H3>Images by name:</H3>'
    print '<UL>'
    for i in images:
        print '<LI><A HREF=\"%s\">%s</A></LI>' % \
            ( quote(getprop('html',i)), getprop('name',i) )
    print '</UL>'
-->

</BODY>
</HTML>
"""

fallbackTemplates[ 'template-allindex' ] = \
"""<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">
<HTML>
<HEAD>
   <META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=iso-8859-1\">
   <TITLE>Global index</TITLE>
</HEAD>
<BODY BGCOLOR=\"#FFFFFF\">

<TABLE WIDTH=\"100%\" BGCOLOR=\"#FFEEEE\"><TR><TD>
<H1>Global Index</H1>
</TD></TR></TABLE>

<DIV ALIGN=RIGHT>
<A HREF=\"<!--tag:rootdirindex-->\">Root Directory Index</A>
<A HREF=\"<!--tag:sortindex-->\">Sorted Index</A><P></DIV>

<!--tagcode:
if len(subdirs) > 0:
    print '<H3>Sub-directories:</H3>'
    print '<UL>'
    for d in subdirs:
        print '<LI><A HREF=\"%s\">%s</A></LI>' % \
            ( getindex(d), d )
    print '</UL><P>'
-->

<!--tagcode:
if len( tracks ) > 0:
    print '<H3>Tracks:</H3>'
    print '<UL>'
    for t in tracks:
        print '<LI><A HREF=\"%s\">%s</A></LI>' % ( trackindex(t), t )
    print '</UL>'
-->

<!--tagcode:
if len( images ) > 0:
    print '<H3>Images:</H3>'
    print table( images, tableTextCb )
    
    print '<H3>Images by name:</H3>'
    print '<UL>'
    for i in images:
        print '<LI><A HREF=\"%s\">%s</A></LI>' % \
            ( quote(getprop('html',i)), getprop('name',i) )
    print '</UL>'
-->

</BODY>
</HTML>
"""

fallbackTemplates[ 'template-sortindex' ] = \
"""<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">
<HTML>
<HEAD>
   <META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=iso-8859-1\">
   <TITLE>Sorted index</TITLE>
</HEAD>
<BODY BGCOLOR=\"#FFFFFF\">

<TABLE WIDTH=\"100%\" BGCOLOR=\"#FFEEEE\"><TR><TD>
<H1>Sorted Index</H1>
</TD></TR></TABLE>

<DIV ALIGN=RIGHT>
<A HREF=\"<!--tag:rootdirindex-->\">Root Directory Index</A>
<A HREF=\"<!--tag:allindex-->\">Global Index</A><P></DIV>

<!--tagcode:
if len( images ) > 0:
    import os
    print '<H3>Images sorted by name:</H3>'
    print '<UL>'
    ilist = list(images)
    ilist.sort( lambda x,y: cmp( x._base, y._base ) )
    for i in ilist:
        print '<LI><A HREF=\"%s\">%s</A><I> (%s)</I></LI>' % \
            ( quote(getprop('html',i)), \
              os.path.split( getprop('image',i) )[1], \
              getprop('image',i) )
    print '</UL>'
-->

</BODY>
</HTML>
"""


#===============================================================================
# MAIN
#===============================================================================

#-------------------------------------------------------------------------------
#
def main():
    #
    # options parsing
    #
    
    # Options declaration
    optmap = [
        ( 'help', 'h', "show detailed help message." ),
        ( 'help-script', None, "show scripting environment documentation." ),
    
        ( 'version', 'V',
          "prints version." ),
    
        ( 'verbose', 'v', "run verbosely (default)" ),
        ( 'quiet', 'q', "run quietly (turns verbosity off)" ),
        ( 'silent', None, "alias for --quiet" ),
    
        ( 'no-thumbgen', None, "don't generate thumbnails" ),
        ( 'force-thumbgen', 'h', "overwrite existing thumbnails" ),
    
        ( 'no-index', None, "don't generate HTML indexes" ),
        ( 'force-index', 'i', "overwrite existing HTML indexes" ),
    
        ( 'no-imagepage', None, "don't generate HTML image pages" ),
        ( 'force-imagepage', 'j', "overwrite existing HTML image pages" ),
    
        ( 'force', None, """generate and overwrite everything (i.e. forces
        thumbnails, indexes and imagepages""" ),
          
        ( 'no-html', 'n', "don't generate html (i.e. indexes and imagepages" ),
        ( 'force-html', 'F', """generate and overwrite html (i.e. forces indexes
        and imagepages""" ),
    
        ( 'use-repn', None, """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)."""),

        ( 'repn-affinity=', None, """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\".""" ),

        ( 'templates=', 't', """specifies the directory where to take templates
        from (default: root). This takes precedence over the CURATOR_TEMPLATE
        environment variable AND over the root""" ),

        ( 'rc=', None, """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.""" ),

        ( 'rccode=', None, """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).""" ),

        ( 'save-templates', 'S', """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.""" ),
    
        ( 'ignore-errors', 'k', "ignore errors in templates" ),
    
        ( 'ignore-pattern=', 'I', "regexp to specify image files to ignore" ),
    
        ( 'htmlext=', None,
          "specifies html files extension (default: '.html')" ),
        ( 'attrext=', None,
          "specifies attributes files extension (default: '.desc')" ),
        ( 'newthumbext=', None,
          "specifies new thumbnail extension/type (default: '.jpg')" ),
        ( 'thumb-sfx=', None,
          "specifies the thumbnail alt.repn. suffix (default: 'thumb')" ),
    
        ( 'separator=', 'p', """specify the image basename separator from the
        suffix and extension (default: --)""" ),
        
        ( 'copyright=', 'C',
          "specifies a copyright notice to include in image conversions" ),
    
        ( 'no-meta', 'M', "disables generator meta information addition" ),
    
        ( 'magick-path=', None, "specify imagemagick path to use" ),
        ( 'old-magick', None, "use old imagemagick features (default)" ),
        ( 'new-magick', None, "use new imagemagick features" ),
        ( 'no-magick', None, "disable using imagemagick" ),
    
        ( 'thumb-size=', 's',
          "specifies size in pixels of thumbnail largest side" ),
        ( 'check-thumb-size', None, """check the size of existing thumbnails to
        make sure they're appropriately sized""" ),
    
        ( 'thumb-quality=', 'Q', """specify quality for thumbnail conversion
        (see convert(1))""" ),
    
        ( 'fast', 'X', """disables some miscalleneous slow checks, even if the
        consistency can be shaken up. Don't use this, this is a debugging
        tool""" ),

        ( 'clean', None, """remove all files generated by curator.  Curator
        exits after clean up.""" )

        ]
    
    def opt_translate(opt):
        o = opt[0]
        if o[-1] == '=': # option takes an argument?
            o = o[:-1]
        return translate_longopt( o )
    
    global wsre
    wsre = re.compile( '\s+', re.MULTILINE )
    def del_ws( t ):
        return re.sub( wsre, ' ', t )
    
    optmapb = []
    for o in optmap:
        optmapb.append( ( o[0], o[1], del_ws(o[2]) ) )
    
    optmap = optmapb
    
    global opts
    opts = OptionDummy( map( opt_translate, optmap ) )
    parser = FancyGetopt( optmap )
    parser.set_aliases({'silent': 'quiet'})
    parser.set_negative_aliases({'quiet': 'verbose'})
    parser.set_negative_aliases({'new-magick': 'old-magick'})
    
    try:
        args = parser.getopt( args=sys.argv[1:], object=opts )
    except:
        print >> sys.stderr, "Error: argument error. Use --help for more info."
        sys.exit(1)
        
    if len(args) == 0:
        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.
    #
    
    if opts.verbose == None:
        opts.verbose = 1

    # Debugging this script
    opts.debug = 0
        
    #
    # set defaults
    #

    if not opts.thumb_size:
        opts.thumb_size = "150"
        # 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.check_thumb_size:
        opts.check_thumb_size = 0
    
    if not opts.separator:
        opts.separator = "--"
    
    if not opts.htmlext:
        opts.htmlext = ".html"
    
    if not opts.attrext:
        opts.attrext = ".desc"
    
    if not opts.newthumbext:
        opts.newthumbext = ".jpg"  # will create jpg thumbnails by default
    
    if not opts.thumb_sfx:
        opts.thumb_sfx = "thumb"
    
    
    #
    # Validate options.
    #
    if opts.verbose: print "====> validating options"
    
    if opts.version:
        print "%s  %s" % ( os.path.basename( sys.argv[0] ), __version__[1:-2] )
        sys.exit(1)
    
    if opts.help:
        for i in parser.generate_help( __doc__ ):
            print i
        sys.exit(1)
    
    if opts.help_script:
        print generateScriptHelp()
        sys.exit(1)
        
    if opts.templates:
        opts.templates = os.path.expanduser( opts.templates )
    
    if opts.no_thumbgen and opts.force_thumbgen:
        print >> sys.stderr, \
              "Error: Contradictory thumbnail generation directives."
        sys.exit(1)
    if opts.no_index and opts.force_index:
        print >> sys.stderr, "Error: Contradictory index generation directives."
        sys.exit(1)
    if opts.no_imagepage and opts.force_imagepage:
        print >> sys.stderr,\
              "Error: Contradictory image page generation directives."
        sys.exit(1)
    
    if opts.force:
        opts.force_thumbgen = 1
        opts.force_index = 1
        opts.force_imagepage = 1
    
    if opts.no_html:
        opts.no_index = 1
        opts.no_imagepage = 0
    elif opts.force_html:
        opts.force_index = 1
        opts.force_imagepage = 1

    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)
    
    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.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 = []
        
    #
    # initialize global constants
    #
    cidxext = ".cidx"
    
    global dirindex_fn, dirattr_fn
    global allindex_fn, allcidx_fn, trackindex_fn, sortindex_fn
    dirindex_fn = "dirindex" + opts.htmlext
    dirattr_fn = "dir" + opts.attrext
    allindex_fn = "allindex" + opts.htmlext
    allcidx_fn = "allindex" + cidxext
    trackindex_fn = "trackindex-%s" + opts.htmlext
    sortindex_fn = "sortindex" + opts.htmlext
    

    global hor_sep, ver_sep
    hor_sep = "&nbsp;&nbsp;\n"
    ver_sep = "<BR>\n"
    
    
    opts.root = os.path.normpath( opts.root )
    if not os.path.exists( opts.root ) or not os.path.isdir( opts.root ):
        print >> sys.stderr, "Error: root %s doesn't exist." % opts.root
        sys.exit(1)
    
    if opts.magick_path:
        opts.magick_path = os.path.normpath( opts.magick_path )
        if not os.path.exists( opts.magick_path ) or \
           not os.path.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.verbose: print "====> gathering image list and attributes files"
    
    global rootdir
    rootdir = Dir( opts.root, "" )

    global allimages, alldirs
    allimages = rootdir.getAllImages()
    alldirs = rootdir.getAllDirs()
    
    #
    # If asked to clean, clean and exit.
    #
    if opts.clean:
        if opts.verbose: print "====> cleaning up generated files"

        clean( allimages, alldirs )
        if opts.verbose: print "====> done."
        sys.exit(1)

    #
    # Read template files.
    #
    if opts.verbose: print "====> templates input and compilation"
    
    global templates
    templates = readTemplates()

    #
    # Thumbnail generation.
    #
    if opts.verbose: print "====> thumbnail generation"
    
    if opts.no_thumbgen:
        if opts.verbose: print "not checking nor generating thumbnails"
    else:
        for img in allimages:
            img.generateThumbnail( opts.force_thumbgen )
    
    #
    # Execute global rc file environment.
    #
    if opts.verbose: print "====> executing global rc file"
    global globalenv
    globalenv = {}
    execTemplate( sys.stdout, templates['rc'], globalenv )

    #
    # Output indexes
    #
    if opts.verbose: print "====> index generation"
    
    # define as globals
    global trackmap
    trackmap = {}
    alldirs = None
    
    if opts.no_index:
        if opts.verbose: print "not generating indexes"
    else:
        if opts.verbose: print "==> directory index generation"
    
        #
        # Output directory indexes
        #
    
        global walkDirsForIndex
        def walkDirsForIndex( dir ):
            for d in dir._subdirs:
                walkDirsForIndex( d )
        
            fn = pjoin( dir._dirname, dirindex_fn )
            if os.path.exists( pjoin( opts.root, fn ) ) and \
               not opts.force_index:
                if opts.verbose:
                    print "not regenerating existing index %s" % fn
                return
        
            if dir.hasImages() == 1:
                generateIndexPage( fn, dir, 'dirindex', globalenv )
        
        walkDirsForIndex( rootdir )
        
        
        #
        # Output track indexes HTML
        #
        if opts.verbose: print "==> track index generation"
    
        trackmap = computeTrackmap( allimages )
        
        for track in trackmap.keys():
            fn = trackindex_fn % track
        
            if not os.path.exists( pjoin( opts.root, fn ) ) or \
               opts.force_index:
                generateIndexPage( fn, rootdir, 'trackindex', globalenv, track )
            else:
                if opts.verbose:
                    print "not regenerating existing index %s" % fn

        
        #
        # Output global index HTML file and summary text file.
        #
        if opts.verbose: print "==> global index generation"
        
        if not os.path.exists( pjoin( opts.root, allindex_fn ) ) or \
           opts.force_index:
            generateIndexPage( allindex_fn, rootdir, 'allindex', globalenv )
        else:
            if opts.verbose:
                print "not regenerating existing index %s" % allindex_fn
            
        if not os.path.exists( pjoin( opts.root, allcidx_fn ) ) or \
           opts.force_index:
            generateSummary( allcidx_fn )
        else:
            if opts.verbose:
                print "not regenerating existing index %s" % allcidx_fn
    
        if not os.path.exists( pjoin( opts.root, sortindex_fn ) ) or \
           opts.force_index:
            generateIndexPage( sortindex_fn, rootdir, 'sortindex', globalenv )
        else:
            if opts.verbose:
                print "not regenerating existing index %s" % sortindex_fn

    #
    # Output image HTML files
    #
    if opts.verbose: print "====> image page generation"
    
    if opts.no_imagepage:
        if opts.verbose: print "not generating imagepages"
    else:
        global walkDirsForImages
        def walkDirsForImages( dir ):
            for d in dir._subdirs:
                walkDirsForImages( d )
            for img in dir._images:
                fn = pjoin( img._dir, img._base + opts.htmlext )
        
                if os.path.exists( pjoin( opts.root, fn ) ) and \
                   not opts.force_imagepage:
                    if opts.verbose:
                        print "not regenerating existing imagepage %s" % fn
                    continue
        
                generateImagePage( fn, img, dir, globalenv )
            
        walkDirsForImages( rootdir )
    
    if opts.verbose: print "====> done."



    
# Run main if loaded as a script
if __name__ == "__main__":
    main()

#===============================================================================
# END
#===============================================================================

# try to remove imageenvironment, can you provide Image() class directly?
# wouldn't that be simpler?
# --> can we figure out a way to remove bind the methods, I really want to avoid
# --> having to access "img.attr"


# idea: per-directory subdirectories for thumbnails and html pages makes more
# sense than a global one.
#
# jpg and desc are always together
# 3 possibilities:
#  - html and others along with original in dir
#  - other stuff in subdirs of orig
#  - other stuff in one global dir

# imageSize should check the real size, thumbnails get rotated sometimes!
# ... or perhaps should check if image is more recent... has rotated...

# how do we apply a copyright of a color that will always show through?

# change 'name' to 'file-or-title'

