#!/usr/bin/perl -w
#
# Edwin Huffstutler <edwinh@computer.org>
# John Reynolds <johnjen@reynoldsnet.org>
#
# perl script for: web page index/thumbnails of photos.
# orginally started life as a menu selector for fvwm2 backgrounds...
#
# USAGE:
#
#  imageindex [options] <directory>
#
#  <directory> is assumed to be "." if not given
#
#  Options: (can be abbreviated if unique)
#
#    -title <string>  title for this page (saved for susbsequent runs)
#    -destdir <dir>   export image/html tree to dir (will be created if needed)
#    -[no]recurse     enable/disable recursion into subdirectories
#    -[no]medium      enable/disable medium size images/links
#    -[no]slide       enable/disable slideshow files
#    -[no]detail      enable/disable detail file
#    -[no]dirs        enable/disable directory entries
#    -[no]montage     enable/disable directory montages
#    -forceregen      force regeneration of thumbnails
#    -columns <num>   number of columns in html table (saved for susbsequent runs)
#    -exclude <file>  Exclude <file> from processing. Can be used multiple times
#    -x <num>         override thumbnail x size
#    -y <num>         override thumbnail y size
#    -help            show this text
#    -version         show the current version
#    -d 'var=val'     force override of global variable
#
#  See also the configuration section at the top of the program itself,
#  or in ~/.imageindexrc
#
# (non-html-generating, utility options)
#
#    -lowercase               Lowercase all image files in a directory
#    -caption <file> <string> Store comment in image
#    -rotate <file> [cw|ccw]  Rotate an image clockwise or counterclockwise
#    -showexcluded            Show which files were excluded in a prior run
#
######################################################################

#
# Configuration options
#

# sizes / dirs
$thumbnail_dir = 'thumbnail';
$default_thumbnail_x = 200;
$default_thumbnail_y = 200;

# If both dimensions of the original are within this much of the thumb
# dimensions we will skip the thumbnail and just use the original
$thumbnail_threshold = 1.0;

$med_x = 800;
$med_y = 600;
$med_dir = 'medium';

# If both dimensions of the original are within this much of the "medium"
# dimensions we will skip creating the medium-size format and just use the
# original
$med_threshold = 1.6;

# Enable/disable features, set default for various flags
$do_recurse = 0;                                  # Recurse into subdirs?
$do_medium = 1;                                   # Generate medium-format?
$do_slide = 1;                                    # Generate slides/frame view?
$do_detail = 1;                                   # Generate details page?
$do_dirs = 1;                                     # Create directory entries?
$do_montage = 1;                                  # Create directory montages?

# What the various image links point to - can be 'index', 'fullsize',
# 'medium', 'thumbnail', 'slide', or 'details'
$index_linkto = 'slide';
$details_linkto = 'index';
$slide_linkto = 'fullsize';

# Default number of columns to use
$default_columns = 3;

# Orientation of slide frame - 'horizontal' or 'vertical'
$frame_orient = 'vertical';

# Location of items in slide pages; 'top', 'bottom', or 'none'
$slide_caption = 'top';
$slide_date = 'bottom';

# Details index uses thumbs reduced by this amount
$detailshrink = 2;

# Quality for generated images
$thumb_quality = 50;
$med_quality = 80;

# Minimum and maximum number of tiles in directory montage images
$montage_min = 4;
$montage_max = 36;

# Space between montage images
$montage_whitespace = 2;

# What to do with leftover montage tiles; can be
# 'blank' or 'repeat'
$montage_fill = 'blank';

# Stylesheet specs
# Set element font, etc. properties here
$stylesheet = '
body { color: black; background: white; }

/* Fonts in the title */
h1.title { font-family: "Comic Sans MS",Helvetica,sans-serif; font-size: 200%; font-weight: bold; text-align: center; }
h2.daterange { font-family: Arial,Helvetica,sans-serif; font-size: 125%; text-align: center; }
h3 { font-family: Arial,Helvetica,sans-serif; font-size: 90%; text-align: center; }

/* Photo captions & Directory titles */
div.caption { font-family: Arial,Helvetica,sans-serif; font-size: 100%; font-weight: bold; margin: 1em; }

/* Overall fonts on the index and details page */
div.index { font-family: Arial,Helvetica,sans-serif; font-size: 80%; }
div.detail { font-family: Arial,Helvetica,sans-serif; font-size: 80%; }
div.credits { font-family: Arial,Helvetica,sans-serif; font-size: 80%; text-align: right; margin: 10px }

/* Table attributes */
table.index { background: #ffffff; border: none; border-spacing: 8px; }
td.index { border: none; padding: 3px }
table.frame { background: #ffffff; border: none }
td.frame { border: none; padding: 0px }

/* Image attributes */
img.index { border: none; }
img.slide { border: none; }
img.frame { border: none; }

/* Link attributes */
a:link { color: blue; }
a:visited { color: green; }
a:hover { color: red; }
a:active { color: red; }

';


# Text
$emptycell          = "<I>empty</I>";
$updirtext          = "up one directory";
$framelinktext      = "slideshow view (frames)";
$detaillinktext     = "details index";
$indexlinktext      = "main index";
$default_titletext  = "Image directory";

# Pathnames
$indexfile = 'index.html';
$detailfile = 'details.html';
$framefile = 'frame.html';
$slidefile =  'slides.html';
$slide_dir = 'slides';
$stylefile = 'style.css';
$montagefile = 'montage.jpg';

# File exclusion customization (regex)
# (Anything non-image and non-dir will be skipped automatically, this just
#  makes it silent)
@exclude = qw(
	      ^CVS$
	      ^.nautilus-metafile.xml$
	      ^.thumbnails$
	      ^.xvpics$
	      ^ALBUM.OFA$
	      ^desktop.ini$
	      );

# Metatags
$columnsmetatag   = 'Columns';
$titlemetatag     = 'Title';
$begindatemetatag = 'DateBegin';
$enddatemetatag   = 'DateEnd';
$excludemetatag   = 'ExcludedFiles';
$thumbxmetatag    = 'ThumbnailX';
$thumbymetatag    = 'ThumbnailY';

# Any of the above can be overridden in an rc file in the user's home dir
$rcfile = "$ENV{'HOME'}/.imageindexrc";

######################################################################
#
# $Id: imageindex,v 1.147 2002/11/21 22:10:11 edwinh Exp $
#
#  imageindex 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, or (at your option)
#  any later version.
#
#  imageindex 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 imageindex; see the file COPYING.
#
######################################################################

use Image::Magick;                                # comes with ImageMagick

# from CPAN - optional
eval('use Image::Info qw(image_info)');

# Shipped with perl
use POSIX;
use Getopt::Long;
use FileHandle;
use File::Basename;
use File::Copy;
use English;
use Carp;
require 'flush.pl';

# to shut up -w
use vars qw($opt_recurse);
use vars qw($opt_slide);
use vars qw($opt_dirs);
use vars qw($opt_detail);
use vars qw($opt_lowercase);
use vars qw($opt_help);
use vars qw($opt_debug);
use vars qw($opt_showexcluded);
use vars qw($opt_version);

&GetOptions(
	    'title=s',
	    'columns=i',
	    'x=i',
	    'y=i',
	    'forceregen',
	    'medium!',
	    'slide!',
	    'detail!',
	    'dirs!',
	    'montage!',
	    'recurse!',
	    'destdir=s',
	    'lowercase',
	    'caption=s',
	    'rotate=s',
	    'exclude=s@',
	    'showexcluded',
	    'version',
	    'help',
	    'debug',
            'd=s%'
	    ) or die ("Invalid flag\n");


# Override config variables
foreach my $var (keys %opt_d) {
    $value = $opt_d{$var};
    print "(override) $var = $value\n";
    eval("\$$var=\"$value\"");
}


# Read RC file
if (-e $rcfile) {
    print "Using settings in $rcfile...\n" if ! defined ($opt_version);
    require $rcfile;
}

# Rotate or caption image (then exit)
if (defined ($opt_rotate)) {
    &rotate_image($opt_rotate,\@ARGV);
    exit (0);
} elsif (defined ($opt_caption)) {
    &caption_image($opt_caption,\@ARGV);
    exit (0);
} elsif (defined ($opt_showexcluded)) {
    &showexcluded($ARGV[0]);
    exit (0);
} elsif (defined ($opt_version)) {
    printf ("imageindex version: %s\n", &versionstring);
    exit (0);
}

# The directory to search is the first argument
if (defined($ARGV[0])) {
    $srcdir = $ARGV[0];
    $srcdir =~ s:/$::;
} else {
    $srcdir = ".";
}

# Give usage message
if (defined($opt_help)) {
    &usage();
    exit(0);
}

# Show backtrace if debug given
if (defined($opt_debug)) {
    $SIG{__WARN__} = \&Carp::cluck;
}

# Where to generate files
$destdir = $srcdir;
if (defined($opt_destdir)) {
    $destdir = $opt_destdir;
    $destdir =~ s:/$::;
    print "Exporting to $destdir\n";
    unless (-d $destdir) {
	printf ("Creating destination directory '$destdir'.\n");
	mkdir ($destdir, 0755);
    }
}

unless (-w $destdir) {
    printf ("No write permission for $destdir\n");
    exit (1);
}

if (defined($opt_medium)) {
    $do_medium = $opt_medium
}

if (defined($opt_slide)) {
    $do_slide = $opt_slide;
}

if (defined($opt_detail)) {
    $do_detail = $opt_detail;
}

if (defined($opt_dirs)) {
    $do_dirs = $opt_dirs;
}

if (defined($opt_montage)) {
    $do_montage = $opt_montage;
}

if (defined($opt_recurse)) {
    $do_recurse = $opt_recurse;
}

# no montages if we aren't doing dirs anyway
if ($do_dirs == 0) {
    $do_montage = 0;
}

&initialize_current_vars();
&read_stored_meta_data();
&override_by_commandline();

if (!defined(&image_info)) {
    print "Image::Info not found, not extracting EXIF data\n";
}

opendir(DIR, "$srcdir") || die "Can't open dir $srcdir: ($!)\n";
@files = readdir DIR;
closedir(DIR);
@files = grep (!/^\.?\.$/, @files);

# Skip the files/dirs we use or generate. Any other patterns go in the
# config section (@exclude) or in exclude file
my @generated_files = ($thumbnail_dir, $med_dir, $slide_dir,
		       $indexfile, $detailfile, $stylefile,
		       );

foreach my $pattern (@generated_files, @exclude) {
    @files = grep (!/$pattern/, @files);
}

@files = &exclude_files(@files);

# Change all the names of image files to lowercase.
if (defined ($opt_lowercase)) {
    &lower_case_files(@files);
    exit (0);
}

# Keep track of which column to be in
my $col_counter = 1;

# Count how many files we create
my $object_counter = 0;
my $dir_counter = 0;
my $image_counter = 0;
my $thumbnail_counter = 0;
my $med_counter = 0;
my $slide_counter = 0;
my $modified_thumb = 0;

# Keep track of max thumb sizes to use for slide frame width
my $max_thumb_x = 0;
my $max_thumb_y = 0;

# Extract info
print "Extracting image info";
flush (STDOUT);

foreach my $file (@files) {

    # If directory, grab the timestamp
    if (-d "$srcdir/$file") {

        my $ts;

        # Grab timestamp from meta tag
        if (-e "$srcdir/$file/$indexfile") {

            my $begin = &extract_meta_tag($begindatemetatag,"$srcdir/$file/$indexfile");
            if (defined($begin)) {
                if (!defined($firstdate) or ($begin < $firstdate)) {
                    $firstdate = $begin;
                }
                $ts = $begin;
            }

            my $end = &extract_meta_tag($enddatemetatag,"$srcdir/$file/$indexfile");
            if (defined($end)) {
                if (!defined($lastdate) or ($end > $lastdate)) {
                    $lastdate = $end;
                }
                $ts = $end if (!defined($ts));
            }

        }

        # Fallback on dir mtime
        if (!defined($ts)) {
            my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
                $atime,$mtime,$ctime,$blksize,$blocks) = stat("$srcdir/$file");
            $ts = POSIX::strftime ("%Y%m%d%H%M.%S",localtime($mtime));
        }

        push(@{$dir_timestamp{$ts}}, $file);

    } else {

        # Collect info from the image
	&fill_image_info($file);

    }
}
print "\n";


# Do dirs first
if ($do_dirs) {
    foreach my $ts (sort bynumber keys %dir_timestamp) {
        foreach my $dir (sort @{$dir_timestamp{$ts}}) {
            &dir_entry($dir);
        } # foreach dir that has this timestamp
    }  # foreach timestamp
}


# Bail if nothing here
if ($object_counter == 0) {
    print "Nothing to do!\n";
    unlink("$destdir/$indexfile") if (-e "$destdir/$indexfile");
    unlink("$destdir/$detailfile") if (-e "$destdir/$detailfile");
    unlink("$destdir/$stylefile") if (-e "$destdir/$stylefile");
    exit(0);
}

# Make thumb dirs if needed
foreach my $checkdir ($thumbnail_dir, $med_dir, $slide_dir) {
    unless (-d "$destdir/$checkdir") {
	mkdir("$destdir/$checkdir",0777);
    }
}

# Nuke old thumbnails if original image gone
&nuke_out_of_date();

# Iterate over the files based on timestamp
# This is just to get back/forward links
undef $prev;
foreach (sort bynumber keys %timestamp) {

    foreach my $pathname (sort @{$timestamp{$_}}) {

	if (defined($prev)) {
	    my ($name,$path,$suffix);

	    ($name,$path,$suffix) = fileparse($prev,'\.\S+');
	    $back{$pathname} = "$name.html";

	    ($name,$path,$suffix) = fileparse($pathname,'\.\S+');
	    $forward{$prev} = "$name.html";
	}
	$prev = $pathname;

    } # foreach image that has this timestamp

} # foreach timestamp

# We need to see if we can get the META data from the HTML right now
# before we open the file for writing! :)
#
# DELETEME
#unless (defined($opt_title)) {
#    $current_titletext = $titletext;
#    if (-r "$destdir/$indexfile") {
#	 my $tmp = &extract_meta_tag ($titlemetatag,"$destdir/$indexfile");
#	 if (defined($tmp)) {
#	     $current_titletext = $tmp;
#	     print "Using saved title: $current_titletext\n";
#	 }
#    }
#}

# Iterate over the files based on timestamp
# This will do the real work
foreach (sort bynumber keys %timestamp) {

    foreach my $pathname (sort @{$timestamp{$_}}) {

	my $filename = $info{$pathname}{'file'};
	my $thumbnail = $info{$pathname}{'thumb'};
	my $medium = $info{$pathname}{'medium'};
	my $slide = $info{$pathname}{'slide'};

        if (!defined($firstdate) or ($info{$pathname}{'date'} < $firstdate)) {
            $firstdate = $info{$pathname}{'date'};
        }

        if (!defined($lastdate) or ($info{$pathname}{'date'} > $lastdate)) {
            $lastdate = $info{$pathname}{'date'};
        }

        #
        # First, deal with the thumbnail for this image
        #

	# Skip thumb if we are below the threshold size
	if (($info{$pathname}{'x'} <= ($current_thumbnail_x * $thumbnail_threshold)) and
	    ($info{$pathname}{'y'} <= ($current_thumbnail_y * $thumbnail_threshold))) {

	    $info{$pathname}{'thumb_x'} = $info{$pathname}{'x'};
	    $info{$pathname}{'thumb_y'} = $info{$pathname}{'y'};

	    $skipthumb{$pathname} = 1;
            if (-e "$destdir/$thumbnail") {
                unlink("$destdir/$thumbnail");
                $modified_thumb++;
            }

            push(@montagefiles,"$destdir/$filename");

	} else {

	    my $image = new Image::Magick;
	    my $retval;

	    # Create thumbnail if it is not there,
	    # or is out of date with respect to original image
	    if ((! -e "$destdir/$thumbnail") or
		( -M $pathname < -M "$destdir/$thumbnail") or
		defined($opt_forceregen)) {

		my $newgeom = $current_thumbnail_x . "x" . $current_thumbnail_y;

		print "Creating $destdir/$thumbnail\n";

		$retval = $image->Read(filename=>$pathname);
		warn "$retval" if "$retval";
		$retval = $image->Resize(geometry=>$newgeom);
		warn "$retval" if "$retval";
		$retval = $image->Set(interlace=>Line);
		warn "$retval" if "$retval";
		$retval = $image->Set(quality=>$thumb_quality);
		warn "$retval" if "$retval";
		$retval = $image->Write(filename=>"$destdir/$thumbnail");
		warn "$retval" if "$retval";

                push(@montagefiles,"$destdir/$thumbnail");

                $modified_thumb++;

	    } else {

		# Up to date, existing thumb
		# Get the right hsize/vsize tags for the inline thumbs

		$retval = $image->Read("$destdir/$thumbnail");
		warn "$retval" if "$retval";

                push(@montagefiles,"$destdir/$thumbnail");

	    }

	    $info{$pathname}{'thumb_size'} = &convert_to_kb($image->Get('filesize'));
	    $info{$pathname}{'thumb_x'} = $image->Get('width');
	    $info{$pathname}{'thumb_y'} = $image->Get('height');

	    $thumbnail_counter++;
	}

        # Set the max thumb sizes, to be used for slide frame width
        if ($info{$pathname}{'thumb_x'} > $max_thumb_x) {
            $max_thumb_x = $info{$pathname}{'thumb_x'};
        }
        if ($info{$pathname}{'thumb_y'} > $max_thumb_y) {
            $max_thumb_y = $info{$pathname}{'thumb_y'};
        }


        #
        # Next, deal with medium format of the image
        #

	# Skip if we want no medium images at all
	if ($do_medium == 0) {

	    $skipmedium{$pathname} = 1;
	    unlink("$destdir/$medium") if (-e "$destdir/$medium");

	} elsif (($info{$pathname}{'x'} <= ($med_x * $med_threshold)) and
		 ($info{$pathname}{'y'} <= ($med_y * $med_threshold))) {

	    # Skip if we are below the threshold size
	    $skipmedium{$pathname} = 1;
	    unlink("$destdir/$medium") if (-e "$destdir/$medium");

	} else {

	    my $image = new Image::Magick;
	    my $retval;

	    # Create medium sized pic if it is not there,
	    # or is out of date with respect to original image
	    if ((! -e "$destdir/$medium") or
		( -M $pathname < -M "$destdir/$medium") or
		defined($opt_forceregen)) {

		my $newgeom = $med_x . "x" . $med_y;

		print "Creating $destdir/$medium\n";

		$retval = $image->Read(filename=>$pathname);
		warn "$retval" if "$retval";
		$retval = $image->Resize(geometry=>$newgeom);
		warn "$retval" if "$retval";
		$retval = $image->Set(interlace=>Line);
		warn "$retval" if "$retval";
		$retval = $image->Set(quality=>$med_quality);
		warn "$retval" if "$retval";
		$retval = $image->Write(filename=>"$destdir/$medium");
		warn "$retval" if "$retval";

	    } else {

		# Up to date, existing medium, grab dimensions
		$retval = $image->Read("$destdir/$medium");
		warn "$retval" if "$retval";
	    }

	    $info{$pathname}{'med_size'} = &convert_to_kb($image->Get('filesize'));
	    $info{$pathname}{'med_x'} = $image->Get('width');
	    $info{$pathname}{'med_y'} = $image->Get('height');

	    $med_counter++;
	}

        #
        # Finally, create html for this image
        #

	&image_entry($pathname);

    } # foreach image that has this timestamp

} # foreach timestamp


# Finish up the columns if needed
if (($col_counter != 1) and
    ($col_counter <= $current_columns) and
    ($object_counter > $current_columns)) {
    foreach ($col_counter..$current_columns) {
	push(@index,"<TD CLASS=\"index\" VALIGN=middle ALIGN=center>$emptycell</TD>\n");
	push(@details,"<TD CLASS=\"index\" VALIGN=middle ALIGN=center>$emptycell</TD>\n");
    }
    push(@index,"</TR>\n");
    push(@details,"</TR>\n");
}

# Nuke generated dirs if no contents
system("rm -rf  $destdir/$thumbnail_dir") if ($thumbnail_counter == 0);
system("rm -rf $destdir/$slide_dir") if ($slide_counter == 0);
system("rm -rf $destdir/$med_dir") if ($med_counter == 0);

# Create montage if we had more than just dir entries here
if (($dir_counter != $object_counter)) {
    &create_montage(@montagefiles);
}

# Create stylesheet
&write_css();

# Write index web page
open(INDEX,">$destdir/$indexfile") or die ("Can't open $destdir/$indexfile: $!\n");
&page_header('index', $index_linkto);
foreach (@index) {
    print INDEX;
}
&page_footer('index');
close(INDEX);

# Write photo details file
if ($do_detail == 1) {
    open(INDEX,">$destdir/$detailfile") or die ("Can't open $destdir/$indexfile: $!\n");
    &page_header('detail', $details_linkto);
    foreach (@details) {
	print INDEX;
    }
    &page_footer('detail');
    close(INDEX);
} else {
    unlink("$destdir/$detailfile") if (-e "$destdir/$detailfile");
}

# Write slide/frame files
if (($do_slide == 1) and ($slide_counter > 1)) {
    &write_frameset();
} else {
    system("rm -rf $destdir/$slide_dir") if (-d "$destdir/$slide_dir");
}

# Optionally export images somewhere else
if ($opt_destdir) {
    printf ("Copying image files from '$srcdir' to '$destdir'.\n");
    foreach my $image (keys %info) {
	system("cp -dpuv $image $destdir");
    }
}


######################################################################
#
# Write the various HTML parts for this image
#
######################################################################
sub image_entry {

    my $pathname = shift(@_);
    my $filename = $info{$pathname}{'file'};
    my $link;

    &index_html($pathname);

    if ($do_detail == 1) {
	&details_html($pathname);
    }

    if (($do_slide == 1) and ($image_counter > 1)) {
	&slide_html($pathname);
    } else {
	my $file = $info{$pathname}{slide};
	unlink($file) if (-e $file);
    }

    # Increment for next time
    $col_counter++;
    $col_counter = 1 if ($col_counter > $current_columns);

}


###############################################################################
#
# Generate HTML for index page entry
#
###############################################################################
sub index_html {

    my $pathname = shift(@_);
    my $filename = $info{$pathname}{'file'};
    my $link;

    # At beginning of row?
    if ($col_counter == 1) {
	push(@index,"<TR>\n");
    }

    # Image
    push(@index,"<TD CLASS=\"index\" VALIGN=middle ALIGN=center>\n");
    push(@index,"<DIV CLASS=\"index\">");
    push(@index, &format_date($info{$pathname}{'date'}));
    push(@index,"</DIV>\n");

    if (($index_linkto eq 'details') and ($do_detail == 1)) {
	$link = "$detailfile#$filename";
    } elsif (($index_linkto eq 'medium') and !defined($skipmedium{$pathname})) {
	$link = $info{$pathname}{'medium'};
    } elsif (($index_linkto eq 'thumbnail') and !defined($skipthumb{$pathname})) {
	$link = $info{$pathname}{'thumb'};
    } elsif (($index_linkto eq 'slide') and ($do_slide == 1) and ($image_counter > 1)) {
	$link = $info{$pathname}{'slide'};
    } else {
	$link = $filename;
    }

    push(@index,"<A HREF=\"$link\"");
    push(@index," NAME=\"$filename\">");

    if (defined($skipthumb{$pathname})) {
	push(@index,"<IMG SRC=\"$filename\"");
    } else {
	push(@index,"<IMG SRC=\"$info{$pathname}{thumb}\"");
    }
    push(@index," WIDTH=\"$info{$pathname}{thumb_x}\" HEIGHT=\"$info{$pathname}{thumb_y}\"");
    push(@index," ALT=\"$filename\"");
    push(@index," CLASS=\"index\"");
    push(@index,"></A>\n");

    push(@index,"<DIV CLASS=\"index\">");

    # Full size link
    push(@index,"<A HREF=\"$filename\">full size</A>");

    # Medium size link if within the threshold
    unless (defined($skipmedium{$pathname})) {
	push(@index,"&nbsp;|&nbsp;<A HREF=\"$info{$pathname}{medium}\">medium</A>");
    }

    # Detail list link
    if ($do_detail == 1) {
	push(@index,"&nbsp;|&nbsp;<A HREF=\"$detailfile#$filename\">details</A>");
    }

    push(@index,"</DIV>\n");

    # Caption if any (jpeg comment field)
    if (defined($info{$pathname}{'comment'})) {
	push(@index,"<DIV CLASS=\"caption\">");
	push(@index,"$info{$pathname}{comment}");
	push(@index,"</DIV>");
    }

    push(@index,"</TD>\n");

    # At end of row?
    if ($col_counter == $current_columns) {
	push(@index,"</TR>\n");
    }

}


###############################################################################
#
# Generate HTML for slide/frame pages
#
###############################################################################
sub slide_html {

    my $pathname = shift(@_);
    my $filename = $info{$pathname}{'file'};
    my $link;

    #
    # First the index frame info
    #
    if ($frame_orient eq 'horizontal') {
	push(@frame,"<TD CLASS=\"frame\" ALIGN=center VALIGN=middle>");
    } else {
	push(@frame,"<TR><TD CLASS=\"frame\" ALIGN=center VALIGN=middle>");
    }
    push(@frame,"<A HREF=\"../$info{$pathname}{slide}\" TARGET=\"view\">");
    if (defined($skipthumb{$pathname})) {
	push(@frame,"<IMG SRC=\"../$filename\"");
    } else {
	push(@frame,"<IMG SRC=\"../$info{$pathname}{thumb}\"");
    }
    push(@frame," WIDTH=\"$info{$pathname}{thumb_x}\" HEIGHT=\"$info{$pathname}{thumb_y}\"");
    push(@frame," ALT=\"$filename\"");
    push(@frame," CLASS=\"frame\"");
    push(@frame,"></A>");
    if ($frame_orient eq 'horizontal') {
	push(@frame,"</TD>");
    } else {
	push(@frame,"</TD></TR>");
    }
    push(@frame,"\n");

    #
    # Then the individual slides
    #
    my $slide = new FileHandle "> $destdir/$info{$pathname}{slide}";
    if (!defined($slide)) {
        die("$destdir/$info{$pathname}{slide}: $!");
    }

    select($slide);
    print "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"\n";
    print "\"http://www.w3.org/TR/html401/strict.dtd\">\n";
    print "<HTML>\n";
    print "<HEAD>\n";
    $verstring = &versionstring();
    printf ("<META NAME=\"GENERATOR\" CONTENT=\"imageindex %s\">\n", $verstring);
    printf ("<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=ISO-8859-1\">\n");
    print "<TITLE>$current_titletext - $filename</TITLE>\n";
    print "<LINK TYPE=\"text/css\" REL=\"stylesheet\" HREF=\"../$stylefile\">\n";
    print "</HEAD><BODY>\n";

    &next_prev_links($pathname);

    # Caption if any
    if (($slide_caption eq 'top') and defined($info{$pathname}{'comment'})) {
	print "<DIV CLASS=\"caption\">";
	print "$info{$pathname}{comment}";
	print "</DIV>";
    }

    # Date, filename
    if ($slide_date eq 'top') {
        print "<DIV CLASS=\"index\">";
        print &format_date($info{$pathname}{'date'});
        print " $filename";
        print "</DIV>\n";
    }

    if ($slide_linkto eq 'index') {
	$link = "../$indexfile#$filename";
    } elsif (($slide_linkto eq 'details') and ($do_detail == 1)) {
	$link = "../$detailfile#$filename";
    } elsif (($slide_linkto eq 'medium') and !defined($skipmedium{$pathname})) {
	$link = "../$info{$pathname}{medium}";
    } elsif (($slide_linkto eq 'thumbnail') and !defined($skipthumb{$pathname})) {
	$link = "../$info{$pathname}{thumb}";
    } else {
	$link = "../$filename";
    }

    print "<A HREF=\"$link\">";
    if (defined($skipmedium{$pathname})) {
	print "<IMG SRC=\"../$filename\"";
	print " WIDTH=\"$info{$pathname}{x}\" HEIGHT=\"$info{$pathname}{y}\"";
    } else {
	print "<IMG SRC=\"../$info{$pathname}{medium}\"";
	print " WIDTH=\"$info{$pathname}{med_x}\" HEIGHT=\"$info{$pathname}{med_y}\"";
    }
    print " ALT=\"$filename\"";
    print " CLASS=\"slide\">";
    print "</A>\n";

    # Caption if any
    if (($slide_caption eq 'bottom') and defined($info{$pathname}{'comment'})) {
	print "<DIV CLASS=\"caption\">";
	print "$info{$pathname}{comment}";
	print "</DIV>";
    }

    # Date, filename
    if ($slide_date eq 'bottom') {
        print "<DIV CLASS=\"index\">";
        print &format_date($info{$pathname}{'date'});
        print " $filename";
        print "</DIV>\n";
    }

    &next_prev_links($pathname);
    print "</BODY></HTML>\n";

    select(STDOUT);
    $slide->close();
    $slide_counter++;

    unless(defined($first_slide)) {
	$first_slide = $info{$pathname}{'slide'};
    }
}


###############################################################################
#
# Generate HTML for details page
#
###############################################################################
sub details_html {

    my $pathname = shift(@_);
    my $filename = $info{$pathname}{'file'};
    my $link;

    # At beginning of row?
    if ($col_counter == 1) {
	push(@details,"<TR>\n");
    }


    if ($details_linkto eq 'index') {
	$link = "$indexfile#$filename";
    } elsif (($details_linkto eq 'medium') and !defined($skipmedium{$pathname})) {
	$link = "$info{$pathname}{medium}";
    } elsif (($details_linkto eq 'thumbnail') and !defined($skipthumb{$pathname})) {
	$link = "$info{$pathname}{thumb}";
    } elsif (($details_linkto eq 'slide') and ($do_slide == 1) and ($image_counter > 1)) {
	$link = $info{$pathname}{'slide'};
    } else {
	$link = $filename;
    }

    push(@details,"<TD CLASS=\"index\" VALIGN=middle ALIGN=center>\n");
    push(@details,"<TABLE BORDER=0 WIDTH=\"100%\">\n");
    push(@details,"<TR><TD VALIGN=middle ALIGN=center>\n");
    push(@details,"<DIV CLASS=\"detail\">\n");
    push(@details,"<A NAME=\"$filename\">");
    push(@details, &format_date($info{$pathname}{'date'}));
    push(@details,"</A><BR>\n");
    push(@details,"<A HREF=\"$link\">");
    if (defined($skipthumb{$pathname})) {
	push(@details,"<IMG SRC=\"$filename\"");
    } else {
	push(@details,"<IMG SRC=\"$info{$pathname}{thumb}\"");
    }
    my $x = $info{$pathname}{'thumb_x'} / $detailshrink ;
    my $y = $info{$pathname}{'thumb_y'} / $detailshrink ;
    push(@details, sprintf(" WIDTH=\"%d\" HEIGHT=\"%d\"", $x, $y));
    push(@details," ALT=\"$filename\"");
    push(@details," CLASS=\"index\"");
    push(@details,"></A><BR>");
    push(@details,"$filename<BR>");
    push(@details,"</DIV>\n");
    push(@details,"</TD>\n");
    push(@details,"<TD VALIGN=middle ALIGN=left>\n");
    push(@details,"<DIV CLASS=\"detail\">\n");
    push(@details,"Original: <A HREF=\"$filename\">$info{$pathname}{geometry}</A>");
    push(@details,"&nbsp;($info{$pathname}{size})<BR>\n");
    unless (defined($skipmedium{$pathname})) {
	push(@details,"Medium: <A HREF=\"$info{$pathname}{medium}\">");
	push(@details,$info{$pathname}{'med_x'} . 'x' . $info{$pathname}{'med_y'} . "</A>");
	push(@details,"&nbsp;($info{$pathname}{med_size})<BR>\n");
    }
    unless (defined($skipthumb{$pathname})) {
	push(@details,"Thumbnail: <A HREF=\"$info{$pathname}{thumb}\">");
	push(@details,$info{$pathname}{'thumb_x'} . 'x' . $info{$pathname}{'thumb_y'} . "</A>");
	push(@details,"&nbsp;($info{$pathname}{thumb_size})<BR>\n");
    }

    #
    # EXIF data
    #
    if (defined($info{$pathname}{'flash'})) {
	push(@details,"Flash: $info{$pathname}{flash}<BR>");
    }
    if (defined($info{$pathname}{'exposure_time'})) {
	push(@details,"Exposure time: $info{$pathname}{exposure_time}<BR>");
    }
    if (defined($info{$pathname}{'focus_dist'})) {
	push(@details,"Focus distance: $info{$pathname}{focus_dist}<BR>");
    }
    if (defined($info{$pathname}{'focal_length'})) {
	push(@details,"Focal length: $info{$pathname}{focal_length}<BR>");
    }
    if (defined($info{$pathname}{'aperture'})) {
	push(@details,"Aperture: $info{$pathname}{aperture}<BR>");
    }

    push(@details,"</DIV>\n");
    push(@details,"</TD></TR></TABLE>\n");
    push(@details,"</TD>\n");

    # At end of row?
    if ($col_counter == $current_columns) {
	push(@details,"</TR>\n");
    }


}


######################################################################
#
# Extract info from image
#
######################################################################
sub fill_image_info {

    my $filename = shift (@_);
    my $pathname = "$srcdir/$filename";
    my $image = new Image::Magick;
    my $retval;

    print ".";
    flush (STDOUT);
    $retval = $image->Read($pathname);
    if ($retval ne "") {
	print "\nSkipping $pathname";
	flush (STDOUT);
	return;
    } else {
	$object_counter++;
	$image_counter++;
    }

    $info{$pathname}{'file'} = $filename;

    # Use mtime as a fallback date in case we don't have exif data
    my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
	$atime,$mtime,$ctime,$blksize,$blocks) = stat($pathname);
    $info{$pathname}{'date'} = POSIX::strftime ("%Y%m%d%H%M.%S",localtime($mtime));

    $info{$pathname}{'x'} = $image->Get('width');
    $info{$pathname}{'y'} = $image->Get('height');
    $info{$pathname}{'geometry'} = $info{$pathname}{'x'} . "x" . $info{$pathname}{'y'};

    $info{$pathname}{'size'} = &convert_to_kb($image->Get('filesize'));

    $info{$pathname}{'format'} = $image->Get('format');

    $info{$pathname}{'comment'} = $image->Get('comment');

    my ($name,$path,$suffix) = fileparse($filename,'\.\S+');

    if ($info{$pathname}{'format'} =~ /JFIF/i) {

	$info{$pathname}{'thumb'} = "$thumbnail_dir/$filename";
	$thumb_backref{"$thumbnail_dir/$filename"} = $pathname;

	$info{$pathname}{'medium'} = "$med_dir/$filename";
	$med_backref{"$med_dir/$filename"} = $pathname;

	if (defined(&image_info)) {

            my $exif = image_info("$pathname");
            if (my $error = $exif->{error}) {
                warn "Can't parse image info: $error\n";
            }

            if (defined($opt_debug)) {
                print "EXIF data for $pathname:\n";
                foreach (keys %$exif) {
                    print "   $_ = $exif->{$_}\n";
                }
                print "\n";
            }

            if (defined($exif->{DateTimeOriginal})) {
                $exif->{DateTimeOriginal} =~ /\s*([\d:]+)\s+([\d:]+)/;
                my $dt = $1;
                my $tm = $2;
                $tm =~ s/://;
                $tm =~ s/:/\./;
                $dt =~ s/://g;
                $info{$pathname}{'date'} = $dt . $tm;
            }

            if (defined($exif->{Flash})) {
                $info{$pathname}{'flash'} = $exif->{'Flash'};
                $info{$pathname}{'flash'} =~ s/0/no/;
                $info{$pathname}{'flash'} =~ s/1/yes/;
            }

            if (defined($exif->{FocalLength})) {
                $info{$pathname}{'focal_length'} = sprintf("%4.1fmm", eval("$exif->{FocalLength}"));
            }

            if (defined($exif->{SubjectDistance})) {
                $info{$pathname}{'focus_dist'} = sprintf("%4.1fm", eval("$exif->{SubjectDistance}"));
            }

            if (defined($exif->{ExposureTime})) {
                $info{$pathname}{'exposure_time'} = $exif->{ExposureTime} . 's';
            }

            if (defined($exif->{FNumber})) {
                $info{$pathname}{'aperture'} =  "f/" . eval ("$exif->{FNumber}");
            }

        }

    } else {

	$info{$pathname}{'thumb'} = "$thumbnail_dir/$name.jpg";
	$thumb_backref{"$thumbnail_dir/$name.jpg"} = $pathname;

	$info{$pathname}{'medium'} = "$med_dir/$name.jpg";
	$med_backref{"$med_dir/$name.jpg"} = $pathname;

    }

    $info{$pathname}{'slide'} = "$slide_dir/$name.html";
    $slide_backref{"$slide_dir/$name.html"} = $pathname;

    push(@{$timestamp{"$info{$pathname}{date}"}}, $pathname);

}


######################################################################
#
# Write HTML for directory entries
#
######################################################################
sub dir_entry {

    my $dir = shift(@_);
    my $destdirname = "$destdir/$dir";
    my $srcdirname = "$srcdir/$dir";

    print "Processing directory $srcdirname\n";

    # Recurse first
    if ($do_recurse == 1) {
	my $flags = "";
	$flags .= "-medium " if ($do_medium == 1);
	$flags .= "-nomedium " if ($do_medium == 0);
	$flags .= "-slide " if ($do_slide == 1);
	$flags .= "-noslide " if ($do_slide == 0);
	$flags .= "-dirs " if ($do_dirs == 1);
	$flags .= "-nodirs " if ($do_dirs == 0);
	$flags .= "-montage " if ($do_montage == 1);
	$flags .= "-nomontage " if ($do_montage == 0);
	$flags .= "-detail " if ($do_detail == 1);
	$flags .= "-nodetail " if ($do_detail == 0);
	$flags .= "-forceregen " if (defined($opt_forceregen));
	$flags .= "-columns $current_columns " if (defined($opt_columns));
	$flags .= "-x $opt_x " if (defined($opt_x));
	$flags .= "-y $opt_y " if (defined($opt_y));
	$flags .= "-destdir $destdirname " if ($destdir ne $srcdir);
        foreach my $var (keys %opt_d) {
            $flags .= " -d $var=$opt_d{$var}";
        }
	system("cd $srcdirname ;$0 $flags -recurse");
    }

    my $dirtitle = "";
    my $first;
    my $last;
    my $montage;
    my $montage_x;
    my $montage_y;

    # Only add entry if this dir has an index file
    if (-r "$destdirname/$indexfile") {

        # Go fetch the title and dates from the HTML
	my $tmp1 = &extract_meta_tag ($titlemetatag,"$destdirname/$indexfile");
	my $tmp2 = &extract_meta_tag ($begindatemetatag,"$destdirname/$indexfile");
	my $tmp3 = &extract_meta_tag ($enddatemetatag,"$destdirname/$indexfile");
	if (defined($tmp1)) {
	    $dirtitle = $tmp1;
	}
	if (defined($tmp2)) {
	    $first = $tmp2;
	}
	if (defined($tmp3)) {
	    $last = $tmp3;
	}

	# If we found generated files in this dir, flag that we found something
	# valid to index
	$object_counter++;
        $dir_counter++;

        # Set montage file if we found it
        if (($do_montage == 1) and ( -r "$destdirname/$thumbnail_dir/$montagefile")) {

            print "Found montage in $destdirname\n" if defined($opt_debug);
            $montage = "$destdirname/$thumbnail_dir/$montagefile";

            my $image = new Image::Magick;
            my $retval;

            $retval = $image->Read(filename=>$montage);
            warn "$retval" if "$retval";

            $montage_x = $image->Get('width');
            $montage_y = $image->Get('height');

        }


        # At beginning of row?
        if ($col_counter == 1) {
            push(@index, "<TR>\n");
            push(@details, "<TR>\n");
        }

        # Entry for this directory in main & details file
        push(@index, "<TD CLASS=\"index\" VALIGN=middle ALIGN=center>\n");
        push(@details, "<TD CLASS=\"index\" VALIGN=middle ALIGN=center>\n");

        push(@details, "<TABLE BORDER=0 WIDTH=\"100%\">\n");

        if (defined($montage)) {
            push(@details, "<TR><TD VALIGN=middle ALIGN=center>\n");
        } else {
            push(@details, "<TR><TD COLSPAN=2 VALIGN=middle ALIGN=center>\n");
        }

        if (defined($first)) {

            $first = &format_date($first,'dayonly');
            $last = &format_date($last,'dayonly');

            push(@index, "<DIV CLASS=\"index\">");
            push(@details, "<DIV CLASS=\"detail\">");
            if ($first ne $last) {
                push(@index,"$first - $last");
                push(@details,"$first - $last");
            } else {
                push(@index,"$first");
                push(@details,"$first");
            }
            push(@index, "</DIV>\n");
            push(@details, "</DIV>\n");
        }


        if (defined($montage)) {

            push(@index, "<A HREF=\"$dir/$indexfile\">");
            push(@index, "<IMG CLASS=\"index\" SRC=\"$dir/$thumbnail_dir/$montagefile\"");
            push(@index, " WIDTH=\"$montage_x\" HEIGHT=\"$montage_y\"");
            push(@index, " ALT=\"\"");
            push(@index, ">");
            push(@index, "</A>\n");

            push(@index,"<DIV CLASS=\"index\">");
            push(@index, "<A HREF=\"$dir/$indexfile\">$dir</A>");
            push(@index,"</DIV>\n");

            push(@details, "<A HREF=\"$dir/$detailfile\">");
            push(@details, "<IMG CLASS=\"index\" SRC=\"$dir/$thumbnail_dir/$montagefile\"");
            my $x = $montage_x / $detailshrink ;
            my $y = $montage_y / $detailshrink ;
            push(@details, sprintf(" WIDTH=\"%d\" HEIGHT=\"%d\"", $x, $y));
            push(@details, " ALT=\"\"");
            push(@details, ">");
            push(@details, "</A>");

            push(@details, "</TD><TD VALIGN=middle ALIGN=left>\n");

        } else {

            push(@index,"<DIV CLASS=\"index\">");
            push(@index, "<A HREF=\"$dir/$indexfile\">$dir</A>");
            push(@index,"</DIV>\n");

        }

        push(@index, "<DIV CLASS=\"caption\">");
        push(@details, "<DIV CLASS=\"detail\">");

        if ($dirtitle ne "") {
            push(@index, "$dirtitle");
            push(@details, "$dirtitle");
        }

        push(@details, "<BR><A HREF=\"$dir/$detailfile\">$dir</A>");

        push(@index, "</DIV>\n");
        push(@details, "</DIV>\n");

        push(@details,"</TD></TR></TABLE>\n");

        push(@index, "</TD>\n");
        push(@details, "</TD>\n");

        # At end of row?
        if ($col_counter == $current_columns) {
            push(@index, "</TR>\n");
            push(@details, "</TR>\n");
        }


        # Increment for next item
        $col_counter++;
        $col_counter = 1 if ($col_counter > $current_columns);

    } # if dir had index file

}

######################################################################
#
# Top of HTML index/detail files
#
######################################################################
sub page_header {

    my $this = shift(@_);
    my $linkto = shift(@_);
    my $numlink = 0;
    my $verstring;

    select(INDEX);
    print "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"\n";
    print "\"http://www.w3.org/TR/html401/strict.dtd\">\n";
    print "<HTML>\n";
    print "<HEAD>\n";
    $verstring = &versionstring();
    printf ("<META NAME=\"GENERATOR\" CONTENT=\"imageindex %s\">\n", $verstring);
    printf ("<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=ISO-8859-1\">\n");
    if (defined ($write_meta_tag{$titlemetatag})) {
	print "<META NAME=\"$titlemetatag\" CONTENT=\"$current_titletext\">\n";
    }
    if (defined ($write_meta_tag{$columnsmetatag})) {
	print "<META NAME=\"$columnsmetatag\" CONTENT=\"$current_columns\">\n";
    }
    if (defined ($write_meta_tag{$thumbxmetatag})) {
	print "<META NAME=\"$thumbxmetatag\" CONTENT=\"$current_thumbnail_x\">\n";
    }
    if (defined ($write_meta_tag{$thumbymetatag})) {
	print "<META NAME=\"$thumbymetatag\" CONTENT=\"$current_thumbnail_y\">\n";
    }
    if (defined($firstdate)) {
	print "<META NAME=\"$begindatemetatag\" CONTENT=\"$firstdate\">\n";
    }
    if (defined($lastdate)) {
	print "<META NAME=\"$enddatemetatag\" CONTENT=\"$lastdate\">\n";
    }
    if (defined (@opt_exclude) && scalar (@opt_exclude)) {
	my $tmp = join (',', @opt_exclude);
	my $etmp;

	# We need to "encode" this string in the HTML so that raw filenames
	# (that people should not try to access) are not exposed to the
	# outside world.
	#
	$etmp = &encodestring ($tmp);
	printf ("<META NAME=\"$excludemetatag\" CONTENT=\"%s\">\n", $etmp);
    }
    print "<TITLE>$current_titletext</TITLE>\n";
    print "<LINK TYPE=\"text/css\" REL=\"stylesheet\" HREF=\"$stylefile\">\n";
    print "</HEAD>\n";
    print "<BODY>\n";
    print "<H1 CLASS=\"title\">$current_titletext</H1>\n";

    print "<H3>";

    # On all these links, check to see if the variable is also defined. If
    # not (done in a .imageindexrc file perhaps) then skip the link
    if ((-e "$destdir/../$indexfile") and
	($do_dirs == 1) and defined($updirtext)) {
	print "<A HREF=\"../$indexfile\">$updirtext</A>";
        $numlink++;
    }

    if (($do_slide == 1) and ($slide_counter > 1) and
	defined($framelinktext)) {
        print "&nbsp;&nbsp;|&nbsp;&nbsp;" if ($numlink != 0);
	print "<A HREF=\"$slide_dir/$framefile\">$framelinktext</A>";
        $numlink++;
    }

    if (($do_detail == 1) and ($this eq 'index') and defined($detaillinktext)) {
        print "&nbsp;&nbsp;|&nbsp;&nbsp;" if ($numlink != 0);
	print "<A HREF=\"$detailfile\">$detaillinktext</A>";
        $numlink++;
    }

    if (($this eq 'detail') and	defined($indexlinktext)) {
        print "&nbsp;&nbsp;|&nbsp;&nbsp;" if ($numlink != 0);
	print "<A HREF=\"$indexfile\">$indexlinktext</A>";
        $numlink++;
    }

    print "\n<BR>\n" if ($numlink != 0);

    print "</H3>\n";

    if (defined($firstdate) and defined($lastdate)) {

        my $tmp1 = &format_date($firstdate,'dayonly');
        my $tmp2 = &format_date($lastdate,'dayonly');

	if ($tmp1 ne $tmp2) {
	    print "<H2 CLASS=\"daterange\">$tmp1 - $tmp2</H2>\n";
	} else {
	    print "<H2 CLASS=\"daterange\">$tmp1</H2>\n";
	}
    }

    print "<TABLE WIDTH=\"100%\"";
    print " CLASS=\"index\"";
    print ">\n";

    select(STDOUT);

}


######################################################################
#
# Bottom of HTML file
#
######################################################################
sub page_footer {

    my $time = localtime(time);

    my $progurl = 'http://www.edwinh.org/imageindex/';

    select(INDEX);

    print "</TABLE>\n";

    print "<DIV CLASS=\"credits\">";
    print "<I>page created on $time</I><BR>\n";
    print "by <A HREF=\"$progurl\">imageindex</A> ";
    print &versionstring();
    print "<BR>\n";
    print "<A HREF=\"http://www.edwinh.org/\">Edwin Huffstutler</A> <I>&lt;edwinh at computer dot org&gt;</I>";
    print "<BR>\n";
    print "<A HREF=\"http://www.reynoldsnet.org/\">John Reynolds</A> <I>&lt;johnjen at reynoldsnet dot org&gt;</I>";
    print "</DIV>\n";

    print "</BODY></HTML>\n";

    select(STDOUT);
}


######################################################################
#
# A "quickie" routine to show which files were excluded in a prior run
#
######################################################################

sub showexcluded {
    my ($file) = @_;
    my ($rfile, $tmp, $utmp, @files, $str);

    if (! defined ($file)) {
	if (-r $indexfile) {
	    $rfile = $indexfile;
	}
    }
    else {
	$rfile = $file;
    }
    $tmp = &extract_meta_tag ($excludemetatag, $rfile);
    if (defined($tmp)) {
	# We need to "decode" this string as it has been encoded for storage
	# in the HTML so that raw filenames (that people should not try to
	# access) are not exposed to the outside world.
	#
	$utmp = &decodestring ($tmp);
	(@files) = split (/,/, $utmp);
	$str = join (', ', @files);
	printf ("File '$rfile' shows the following record of excluded files:\n");
	printf ("%s\n", $str);
    }
    else {
	printf ("File '$rfile' shows no record of excluded files.\n");
    }
    return;
}

######################################################################
#
# Ignore certain files via META data stored in the index.html file
#
######################################################################
sub exclude_files {

    my @files = @_;
    my (@filelist, $f, %exclude);

    undef %exclude;

    # -exclude flags override any META data found. Else, look for the META tag
    # then process.
    #
    if (defined (@opt_exclude)) {
	foreach (@opt_exclude) {
	    $exclude{$_}++;
	}
    }
    elsif (-r "$destdir/$indexfile") {
	my $tmp = &extract_meta_tag ($excludemetatag, "$destdir/$indexfile");
	my $utmp;
	if (defined($tmp)) {
	    # We need to "decode" this string as it has been encoded for storage
	    # in the HTML so that raw filenames (that people should not try to
	    # access) are not exposed to the outside world.
	    #
	    $utmp = &decodestring ($tmp);
	    (@opt_exclude) = split (/,/, $utmp);
	    my $str = join (', ', @opt_exclude);
	    printf ("Using saved excluded files: %s\n", $str);
	    foreach (@opt_exclude) {
		$exclude{$_}++;
	    }
	}
    }

    foreach $f (@files) {
	if (! $exclude{$f}) {
	    push (@filelist, $f);
	} else {
	    print "Excluding '$f'\n";
	    chmod (0600, $f);
	}
    }
    return (@filelist);
}


######################################################################
#
# Nuke generated files if original image gone
#
######################################################################
sub nuke_out_of_date {
    foreach my $checkdir ($thumbnail_dir, $med_dir, $slide_dir) {
	opendir(THUMBS,"$destdir/$checkdir") || die "Can't open dir $checkdir: ($!)\n";
	foreach (readdir(THUMBS)) {
	    next if (m/^\.?\.$/);
	    next if (m/$framefile/);
	    next if (m/$slidefile/);
	    next if (m/$montagefile/);
	    if (!defined($thumb_backref{"$checkdir/$_"}) and
		!defined($slide_backref{"$checkdir/$_"}) and
		!defined($med_backref{"$checkdir/$_"})) {
		print "Removing stale $destdir/$checkdir/$_\n";
		unlink("$destdir/$checkdir/$_") || warn "Can't unlink $destdir/$checkdir/$_: ($!)\n";
                $modified_thumb++;

	    }
	}
	closedir(THUMBS);
    }

}

######################################################################
#
# Convert bytes to kb string
#
######################################################################
sub convert_to_kb {

    my $bytes = shift(@_);
    $bytes = sprintf("%dk", $bytes / 1024);
    return($bytes);
}

######################################################################
#
# Sort by integer date stamp
#
######################################################################
sub bynumber {
    $a <=> $b;
}


######################################################################
#
# Write frameset file for slideshows
#
######################################################################
sub write_frameset {

    # This is impossible to get rid of
    my $framefudge = 35;
    my $verstring;

    open(FRAME,">$destdir/$slide_dir/$framefile") or die ("Can't open $destdir/$slide_dir/$framefile: $!\n");

    select(FRAME);

    print "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Frameset//EN\"\n";
    print "\"http://www.w3.org/TR/html401/frameset.dtd\">\n";
    print "<HTML>\n";
    print "<HEAD>\n";
    $verstring = &versionstring();
    printf ("<META NAME=\"GENERATOR\" CONTENT=\"imageindex %s\">\n", $verstring);
    printf ("<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=ISO-8859-1\">\n");
    print "<LINK TYPE=\"text/css\" REL=\"stylesheet\" HREF=\"../$stylefile\">";
    print "<TITLE>$current_titletext</TITLE>\n";
    print "<LINK TYPE=\"text/css\" REL=\"stylesheet\" HREF=\"../$stylefile\">\n";
    print "</HEAD>\n";
    if ($frame_orient eq 'horizontal') {
	printf("<FRAMESET ROWS=\"%d, *\">\n", $max_thumb_y + $framefudge);
    } else {
	printf("<FRAMESET COLS=\"%d, *\">\n", $max_thumb_x + $framefudge);
    }
    print "<FRAME NAME=\"thumb\" SRC=\"$slidefile\">\n";
    print "<FRAME NAME=\"view\" SRC=\"../$first_slide\">\n";
    print "<NOFRAMES>No frames in this browser...go back</NOFRAMES>\n";
    print "</FRAMESET>\n";
    print "</HTML>\n";

    select(STDOUT);
    close (FRAME);


    open(FRAME,">$destdir/$slide_dir/$slidefile") or die ("Can't open $destdir/$slide_dir/$slidefile: $!\n");
    select(FRAME);


    print "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"\n";
    print "\"http://www.w3.org/TR/html401/strict.dtd\">\n";
    print "<HTML>\n";
    print "<HEAD>\n";
    $verstring = &versionstring();
    printf ("<META NAME=\"GENERATOR\" CONTENT=\"imageindex %s\">\n", $verstring);
    printf ("<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=ISO-8859-1\">\n");
    print "<LINK TYPE=\"text/css\" REL=\"stylesheet\" HREF=\"../$stylefile\">";
    print "<TITLE>$current_titletext</TITLE>";
    print "</HEAD><BODY>\n";
    print "<TABLE CLASS=\"frame\"\n";
    print "<TR>\n" if ($frame_orient eq 'horizontal');
    foreach (@frame) {
	print;
    }
    print "</TR>\n" if ($frame_orient eq 'horizontal');
    print "</TABLE>\n";
    print "</BODY></HTML>\n";

    select(STDOUT);
    close(FRAME);

}


######################################################################
#
# Do next/index/prev links on slide pages
#
######################################################################
sub next_prev_links {

    my $pathname = shift(@_);

    print "<DIV CLASS=\"index\">";

    if (defined($back{$pathname})) {
	print "<A HREF=\"$back{$pathname}\">&lt;&nbsp;previous</A>&nbsp;|&nbsp;";
    } else {
	print "&lt;&nbsp;previous&nbsp;|&nbsp;";
    }
    print "<A HREF=\"../$indexfile\" TARGET=\"_top\">index</A>";
    if (defined($forward{$pathname})) {
	print "&nbsp;|&nbsp;<A HREF=\"$forward{$pathname}\">next&nbsp;&gt;</A>";
    } else {
	print "&nbsp;|&nbsp;next&nbsp;&gt;";
    }

    print "</DIV>\n";

}


######################################################################
#
# Lower-case all the filenames. I hate the uppercase filenames that come
# from my camera's default software (and Windud software). Plus I didn't
# want this "utility" in another script, so just place it here.
#
######################################################################
sub lower_case_files {
    my (@files) = @_;
    my ($newfile, $lowername);

    foreach $name (@files) {
	($lowername = $name) =~ tr/A-Z/a-z/;
	if ($name =~ /[A-Z]/) {
	    print "Moving '$name' to '$lowername'\n";
	    move("$name","$lowername");
	}
    }
}


######################################################################
#
# extract the NAME tag from an HTML file
#
######################################################################
sub extract_meta_tag {
    my ($tag, $filename) = @_;
    my ($name, $content, $retval);

    if (! (open (FILE, $filename))) {
	print STDERR "Cannot open '$filename' for reading - $!\n";
	return (0);
    }
    # <META NAME="Columns" CONTENT="3">
    #
    while (<FILE>) {
	if (/<META\s+NAME=\"(.*?)\"\s+CONTENT=\"(.*)\">/) {
	    $name = $1;
	    $content = $2;
	    if ($name eq $tag) {
		$retval = $content;
		last;
	    }
	}
    }
    close (FILE);
    return ($retval);
}


###############################################################################
#
# Rotate given image 90 degrees
#
###############################################################################
sub rotate_image {

    my $file = shift(@_);
    my $argv = shift(@_);

    -r "$file" || die("$file: ", $!);
    -w "$file" || die("$file: ", $!);

    # grab the mtime of the file so we can reset it after we update it
    my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,
	$ctime,$blksize,$blocks) = stat($file);
    my $posix_mtime = POSIX::strftime ("%Y%m%d%H%M.%S",localtime($mtime));

    my ($name,$path,$suffix) = fileparse($file,'\.\S+');
    my $thumb;
    my $medium;

    my $image = new Image::Magick;

    my $retval = $image->Read("$file");
    warn "$retval" if "$retval";

    if (!defined($$argv[0]) or
	($$argv[0] !~ m/^cc?w$/i)) {
	print "Need 'cw' or 'ccw' argument to rotate image clockwise/counterclockwise\n";
	exit(1);
    }

    if ($$argv[0] =~ /^ccw$/i) {
	$deg = -90;
    } else {
	$deg = 90;
    }

    print "Rotating $file $deg degrees\n";
    $retval = $image->Rotate($deg);
    warn "$retval" if "$retval";

    $retval = $image->Write(filename=>"$file");
    warn "$retval" if "$retval";

    system ("touch -t $posix_mtime $file");

    # Nuke the generated images if they exist
    # (touching the timestamp above breaks automatic regeneration logic)
    if ($image->Get('format') =~ /JFIF/i) {
        $thumb = $path . "$thumbnail_dir/$name" . $suffix;
        $medium = $path . "$med_dir/$name" . $suffix;
    } else {
        $thumb = $path . "$thumbnail_dir/$name.jpg";
        $medium = $path . "$med_dir/$name.jpg";
    }
    unlink($thumb) if (-e "$thumb");
    unlink($medium) if (-e "$medium");



}

###############################################################################
#
# Set or display caption for a particular image
#
###############################################################################
sub caption_image {

    my $file = shift(@_);
    my $argv = shift(@_);
    my ($esc_comment, $tmpfile);

    -r "$file" || die("$file: ", $!);

    # grab the mtime of the file so we can reset it after we update it
    my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,
	$ctime,$blksize,$blocks) = stat($file);
    my $posix_mtime = POSIX::strftime ("%Y%m%d%H%M.%S",localtime($mtime));

    my $image = new Image::Magick;

    my $retval = $image->Read("$file");
    warn "$retval" if "$retval";

    my $format = $image->Get('format');
    warn "$retval" if "$retval";

    # Set caption if another arg is present, or just display it
    if (defined($$argv[0])) {

	-w "$file" || die("$file: ", $!);

        # Try to find wrjpgcom so we can use it for adding captions to JPG images
        my $wrjpgcom_prog = &find_in_path ('wrjpgcom');

	# If a jpeg file and we found a wrjpgcom program in our path, use
	# it! It simply puts the comment in the JPEG header without reading
	# (uncompressing) and writing (re-compressing) the file out so
	# there is no chance for data loss.
	if (($format =~ /JFIF/i) and defined($wrjpgcom_prog)) {

	    $tmpfile = "$file.$$";
	    $esc_comment = quotemeta ($$argv[0]);
	    # FIXME
	    # check to see how '?' and other punctuation is escaped and fix
	    # it seems things are not correct.
	    system ("$wrjpgcom_prog -replace -comment $esc_comment $file > $tmpfile");
	    if (($? >> 8) != 0) {
		printf(STDERR "Error in creating JPEG comment with 'wrjpgcom'. Leaving existing file intact.\n");
	    } else {
		move($tmpfile, $file);
	    }

	} else {

            # Fall back to PerlMagick's routines.
	    $retval = $image->Comment("$$argv[0]");
	    warn "$retval" if "$retval";

	    $retval = $image->Write(filename=>"$file", quality=>"95",
				    sampling_factor=>"1x1");
	    warn "$retval" if "$retval";
	}

	system ("touch -t $posix_mtime $file");

    } else {

	my $text = $image->Get('comment');

	if (defined($text)) {
	    print "$file: \"$text\"\n";
	} else {
	    print "$file: (no caption)\n";
	}

    }

}


###############################################################################
#
# Print usage info from top of file
#
###############################################################################
sub usage {

    open(FILE,"$0") or die "Can't open $0: $OS_ERROR";
    while(<FILE>) {
	last if (m/^\#\s+USAGE:/);
    }
    while(<FILE>) {
	last if (m/^\#\#\#\#\#\#\#/);
	s/^\# ?//;
	print;
    }
    close(FILE);

}


######################################################################
#
# Format timestamp for HTML pages
#
######################################################################
sub format_date {

    my $date =  shift(@_);
    my $dayonly = shift(@_);

    $date =~ /(\d\d\d\d)(\d\d)(\d\d)(\d\d)?(\d\d)?\.?(\d\d)?/;
    my $year = $1;
    my $month = $2;
    my $day = $3;
    my $hour = $4;
    my $ampm;
    if (!defined($dayonly) and defined($hour)) {
        if ($hour > 12) {
            $hour -= 12;
            $ampm = 'pm';
        } else {
            $ampm = 'am';
        }
        my $min = $5;
        my $sec = $6;
        return("$month/$day/$year $hour:$min$ampm");
    } else {
        return("$month/$day/$year");
    }

}

######################################################################
#
# Return version string from CVS tag
#
######################################################################
sub versionstring {

    my $ver = ' $Name: v1_0_2 $ ';
    $ver =~ s/Name//g;
    $ver =~ s/[:\$]//g;
    $ver =~ s/\s+//g;
    $ver =~ s/^v//g;
    $ver =~ s/_/\./g;
    if ($ver eq '') {
	$ver = "cvs devel - " . '$Revision: 1.147 $ ';
	# Nuke the $ signs -- what if somebody is keeping pages under RCS
	# or CVS control?
	$ver =~ s/\$//g;
	$ver =~ s/\s*$//;
    }
    return($ver);

}

###############################################################################
#
#  Create CSS file that is shared among the HTML pages
#
###############################################################################
sub write_css {


    open(CSS,">$destdir/$stylefile") or die ("Can't open $destdir/$stylefile: $!\n");
    select(CSS);

    print $stylesheet;

    select(STDOUT);
    close(CSS);


}

###############################################################################
#
#  Look for external programs we depend on in the $PATH. It just finds the first
#  occurence of $prog in $PATH.
#
###############################################################################
sub find_in_path {
    my ($prog) = @_;
    my ($retval);

    undef $retval;
    foreach $dir (split (/:/, $ENV{'PATH'})) {
	if (-r "$dir/$prog" && -x "$dir/$prog") {
	    $retval = "$dir/$prog";
	}
    }
    return ($retval);
}


###############################################################################
#
# Encode/decode routines for exclude filenames when stuffed in a meta tag
#
###############################################################################
sub encodestring {
    my ($tmp) = @_;
    my $etmp;
    $etmp = pack ("u*", $tmp);
    # Hack the string to get rid of \n chars so we can store it on 1 line
    $etmp =~ s/\n/..1xn!_ltr../g;
    return ($etmp);
}

sub decodestring {
    my ($tmp) = @_;
    my $utmp;
    # Unhack the string to bring back \n characters
    $tmp =~ s/\.\.1xn\!_ltr\.\./\n/g;
    $utmp = unpack ("u*", $tmp);
    return ($utmp);
}

#############################################################################
#
#  This routine samples linearly (as possible) across the available files in
#  a directory. The first pass at sampling is a simple modulo function based
#  upon the ratio of files to the number of tiles we can use in the montage.
#  If that first pass sample did not produce enough files, then we go back
#  iteratively through the list and as evenly-as-possible select unused
#  files from those left in the pool.
#
#############################################################################
sub sample_files_for_montage {
    my (@files) = @_;
    my ($numdiv, $numchosen, $chunksize, $numfiles, $numleft);
    my ($i, $index, $f, @ret);

    $numfiles = scalar (@files);
    $numdiv = sprintf ("%d", $numfiles / $montage_max);
    $numdiv++;

    for ($i = 0; $i < $numfiles; $i++) {
	if (($i % $numdiv) == 0) {
	    $chosen{$files[$i]}++;
	}
    }

    $numchosen = scalar (keys %chosen);

    $numleft = $montage_max - $numchosen;

    if ($numleft) {
	$chunksize = sprintf ("%d", $numfiles / $numleft);
	$index = 0;
	for ($i = 0; $i < $numleft; $i++) {
	    &mark_next_file_for_montage ($index + 1, $numfiles, @files);
	    $index = $index + $chunksize;
	}
    }

    foreach $f (@files) {
	if ($chosen{$f}) {
	    push (@ret, $f);
	}
    }

    return (@ret);
}

#############################################################################
#
# cycle through the given list of files. If the list[$index] is already marked
# (via the global hash %chosen) then move onto the next one, etc.
#
#############################################################################
sub mark_next_file_for_montage {
    my ($index, $numfiles, @files) = @_;
    my ($i);

    for ($i = $index; $i < $numfiles; $i++) {
	if (! $chosen{$files[$i]}) {
	    $chosen{$files[$i]}++;
	    last;
	}
    }
}

###############################################################################
#
# Create a montage of images in the current directory. This image will be
# pointed to by the parent directory's index.html file to show a sort of
# "thumbnail preview" of the contents of this directory.
#
###############################################################################

sub create_montage {

    my @files = @_;
    my (@modfiles);

    foreach (@files) {
	push (@modfiles, quotemeta ($_));
    }

    # If we have defined that a lesser number of "tiles" can be used in the
    # montage vs. the # of files in this directory, then we'll "sample" the
    # files as evenly as possible to avoid clustering of shots that might be
    # similar to each other.
    #
    if (scalar (@modfiles) > $montage_max) {
	@modfiles = &sample_files_for_montage (@modfiles);
    }

    if ($do_montage == 1) {

        if (($modified_thumb != 0) or (! -e "$destdir/$thumbnail_dir/$montagefile")) {

            my $number = $#modfiles + 1;
            my $tile_x = 1;;
            my $tile_y = 1;

            # FIXME these both blindly expand x before expanding y
            # Should this depend on some aspect ratio?
            while(($tile_x * $tile_y) < $montage_min) {
                $tile_x++;
                $tile_y++ if (($tile_x * $tile_y) < $montage_min);
            }
            while(($tile_x * $tile_y) < $number) {
                $tile_x++;
                $tile_y++ if (($tile_x * $tile_y) < $number);
            }

            my $index = 0;
            while (($#modfiles + 1) < ($tile_x * $tile_y)) {
                if ($montage_fill eq 'blank') {
                    push(@modfiles, "NULL:");
                } else {
                    push(@modfiles, $modfiles[$index]);
                    $index = ($index+1) % $number;

                }
            }

            my $tile = sprintf("%dx%d", $tile_x, $tile_y);
            my $geom = sprintf("%dx%d", $max_thumb_x, $max_thumb_y);
            my $newgeom = sprintf("%dx%d", $current_thumbnail_x, $current_thumbnail_y);

            print "Picked $tile array of $geom for montage\n" if ($opt_debug);

            print "Creating $destdir/$thumbnail_dir/$montagefile\n";

            system("montage -quality $thumb_quality -bordercolor white -transparent white -borderwidth $montage_whitespace -geometry $geom -tile $tile @modfiles $destdir/$thumbnail_dir/$montagefile");
	    if (($? >> 8) != 0) {
		printf(STDERR "Error in creating montage file\n");
                return(-1);
            }

            # Resize to std. thumbnail
            my $image = new Image::Magick;
            my $retval;

            $retval = $image->Read(filename=>"$destdir/$thumbnail_dir/$montagefile");
            warn "$retval" if "$retval";
            $retval = $image->Resize(geometry=>$newgeom);
            warn "$retval" if "$retval";
            $retval = $image->Set(interlace=>Line);
            warn "$retval" if "$retval";
            $retval = $image->Write(filename=>"$destdir/$thumbnail_dir/$montagefile");
            warn "$retval" if "$retval";

        }

    } else {

        unlink("$destdir/$thumbnail_dir/$montagefile")
            if (-e "$destdir/$thumbnail_dir/$montagefile");

    }

}

sub read_stored_meta_data {
    my ($tmp);

    if (-r "$destdir/$indexfile") {
	$tmp = &extract_meta_tag ($columnsmetatag, "$destdir/$indexfile");
	# If we found data, check it against program defaults
	if (defined ($tmp)) {
	    $current_columns = $tmp;	    
	    print "Using saved number of columns: $current_columns\n" if ! defined ($opt_columns);
	}

	$tmp = &extract_meta_tag ($titlemetatag, "$destdir/$indexfile");
	# If we found data, check it against program defaults
	if (defined ($tmp)) {
	    $current_titletext = $tmp;	    
	    print "Using saved title: $current_titletext\n" if ! defined ($opt_title);
	}

	$tmp = &extract_meta_tag ($thumbxmetatag, "$destdir/$indexfile");
	# If we found data, check it against program defaults
	if (defined ($tmp)) {
	    $current_thumbnail_x = $tmp;	    
	    print "Using saved thumbnail X size: $current_thumbnail_x\n" if ! defined ($opt_x);
	}

	$tmp = &extract_meta_tag ($thumbymetatag, "$destdir/$indexfile");
	# If we found data, check it against program defaults
	if (defined ($tmp)) {
	    $current_thumbnail_y = $tmp;	    
	    print "Using saved thumbnail Y size: $current_thumbnail_y\n" if ! defined ($opt_y);
	}

	&decide_which_md_to_store();
    }
}

sub override_by_commandline {
    if (defined($opt_columns)) {
	$current_columns = $opt_columns;
    }
    if (defined($opt_title)) {
	$current_titletext = $opt_title;
    }
    if (defined($opt_x)) {
	$current_thumbnail_x = $opt_x;
	if ($current_thumbnail_x != $default_thumbnail_x) {
	    $opt_forceregen = 1;
	}
    }
    if (defined($opt_y)) {
	$current_thumbnail_y = $opt_y;
	if ($current_thumbnail_y != $default_thumbnail_y) {
	    $opt_forceregen = 1;
	}
    }
    &decide_which_md_to_store();
}

sub decide_which_md_to_store {
    if ($current_columns != $default_columns) {
	$write_meta_tag{$columnsmetatag}++;
    }
    else {
	undef $write_meta_tag{$columnsmetatag};
    }

    if ($current_thumbnail_x != $default_thumbnail_x) {
	$write_meta_tag{$thumbxmetatag}++;
    }
    else {
	undef $write_meta_tag{$thumbxmetatag};
    }

    if ($current_thumbnail_y != $default_thumbnail_y) {
	$write_meta_tag{$thumbymetatag}++;
    }
    else {
	undef $write_meta_tag{$thumbymetatag};
    }

    if ($current_titletext ne $default_titletext) {
	$write_meta_tag{$titlemetatag}++;
    }
    else {
	undef $write_meta_tag{$titlemetatag};
    }
}

sub initialize_current_vars {
    $current_columns = $default_columns;
    $current_titletext = $default_titletext;
    $current_thumbnail_x = $default_thumbnail_x;
    $current_thumbnail_y = $default_thumbnail_y;
}
