#!/usr/bin/perl
# Filename:	album
# Author:	David Ljung Madison <DaveSource.com>
# See License:	http://MarginalHacks.com/License/
  my $VERSION=  '3.04';
# Rewrite branched off from: 2.53
# Description:	Makes a photo album.
use strict;
use IO::File;
umask 022;	# 0755

##################################################
##################################################
# SETTINGS
##################################################
##################################################

# Operating System?  (Get from $^O variable)
#   OSX=Darwin, Win98=MSWin, WinXP=MSWin (damn), Win2k=MSWin32, Cygwin=cygwin
my $OSX		= ($^O =~ /darwin/i) ? 1 : 0;
my $WINDOWS	= (!$OSX && ($^O =~ /Win/i)) ? 1 : 0;
my $WIN2K	= ($^O =~ /MSWin32/i) ? 1 : 0;
my $CYGWIN	= ($^O =~ /cygwin/i) ? 1 : 0;
# tcap isn't needed under cygwin, and I don't think it's
# needed (or works) under Win2k
my $TCAP	= ($WINDOWS && !$CYGWIN && !$WIN2K) ? 1 : 0;

my ($BASENAME,$PROGNAME) = split_path($WINDOWS ? '\\' : '/', $0);

# Avoid "Broken pipe" messages
$SIG{PIPE} = 'IGNORE';

##################################################
# CONF FILES
##################################################
my $PROGFILE = ($PROGNAME =~ /^([^\.-]{3,})[\.-]/) ? $1 : $PROGNAME;
my @CONFS = (
	"/etc/$PROGFILE/$PROGFILE.conf",
	"/etc/$PROGFILE.conf",
	);
push(@CONFS, "$BASENAME/$PROGFILE.conf") if $BASENAME ne '.';
push(@CONFS, "$ENV{HOME}/.${PROGFILE}rc") if $ENV{HOME};
push(@CONFS, "$ENV{HOME}/.$PROGFILE.conf") if $ENV{HOME};
# I like to keep all my dot files in one directory besides my $HOME
map { push(@CONFS, "$_/${PROGFILE}.conf") } split(':',$ENV{CONF}) if $ENV{CONF};

# Windows: "C:\Documents and Settings\TheUser"
push(@CONFS, "$ENV{USERPROFILE}/$PROGFILE.conf") if $ENV{USERPROFILE};

##################################################
# OPTIONS
##################################################
# add_option(lvl, option, type, hash)
#    lvl = usage printing level:  -h=1  -more=2  -More=3  (not shown)=4+
#   option = option name
#   hash{args} = args for usage (i.e., -option <file>)
#   hash{default} = default value
#   hash{usage} = "This is the usage line"
#   hash{usage} = ["Can also be","an array of lines"]
#   hash{one_time} = Set for options that shouldn't be saved in album.conf
#   type is one of:
my $OPTION_SEP = 1;	# Separator
my $OPTION_BOOL = 2;
my $OPTION_NUM = 3;
my $OPTION_STR = 4;
my $OPTION_ARR = 5;	# Array of strings

add_option(1,'h',\&usage, usage=>"Show usage");
add_option(1,'more',\&usage, usage=>"To show more options.");
add_option(2,'More',\&usage, usage=>"To show even more options.");
add_option(2,'q',$OPTION_BOOL, one_time=>1, usage=>"Be quiet");
add_option(2,'d',$OPTION_BOOL, one_time=>1, usage=>"Set debug mode");
add_option(99,'pod',\&gen_pod, usage=>"Generate pod text");
add_option(1,'conf', \&read_conf, one_time=>1, args=>'<file>', usage=>"Read a .conf file");
add_option(3,'save_conf',$OPTION_BOOL, default=>1, usage=>"Save $PROGFILE.conf files in photo album");
add_option(3,'configure',$OPTION_BOOL, usage=>"Setup initial $PROGNAME site configuration");
add_option(1,'version',\&version, usage=>"Display program version info");

# Album Options:
add_option(1,'Album Options:',$OPTION_SEP);
add_option(3,'image_pages',$OPTION_BOOL, default=>1, usage=>"Create a page for each image");
add_option(2,'dir_thumbs',$OPTION_BOOL, default=>1, usage=>"Directories have thumbnail (if supported by theme)");
add_option(1,'medium', $OPTION_STR, args=>'<geom>', usage=>"Generate medium size images");
add_option(2,'just_medium',$OPTION_BOOL, usage=>"Don't link to full-size images");
add_option(1,'embed',$OPTION_BOOL, default=>1, usage=>"Use image pages for non-picture image pages");
add_option(3,'columns',$OPTION_NUM, default=>4, usage=>"Number of image columns");
add_option(1,'clean',$OPTION_BOOL, one_time=>1, usage=>"Remove unused thumbnails");
add_option(3,'captions',$OPTION_STR, default=>'captions.txt', usage=>"Specify captions filename");
add_option(2,'album_captions',$OPTION_BOOL, default=>1, usage=>"Also show captions on album page");
add_option(1,'caption_edit',$OPTION_BOOL, usage=>"Add comment tags so that caption_edit.cgi will work");
add_option(1,'exif', $OPTION_ARR, args=>'<fmt>', usage=>["Append exif info to captions.  Use %key% in fmt string",
           "Example:  -exif \"<br>Camera: %Camera model%\"",
           "If any %keys% are not found by jhead, nothing is appended."]);
add_option(3,'exif_album', $OPTION_ARR, args=>'<fmt>', usage=>"-exif for just album pages");
add_option(3,'exif_image', $OPTION_ARR, args=>'<fmt>', usage=>"-exif for just image pages");
add_option(2,'file_sizes',$OPTION_BOOL, usage=>"Show image file sizes");
	## For backwards compat with themes?
	add_option(999,'image_sizes',$OPTION_BOOL, usage=>"DEPRECATED OPTION");
add_option(2,'fix_urls',$OPTION_BOOL, default=>1, usage=>"Encode unsafe chars as %xx in URLs");
add_option(2,'known_images',$OPTION_BOOL, default=>1, usage=>"Only include known image types");
add_option(2,'top',$OPTION_STR, default=>'../', usage=>"URL for 'Back' link on top page");
add_option(2,'all',$OPTION_BOOL, usage=>"Do not hide files/directories starting with '.'");
add_option(1,'add', $OPTION_ARR, args=>'<dir>', one_time=>1, usage=>"Add a new directory to the album it's been placed in");
add_option(2,'depth',$OPTION_NUM, default=>-1, one_time=>1, usage=>"Depth to descend directories (default infinite)");
add_option(2,'hashes',$OPTION_BOOL, default=>1, one_time=>1, usage=>"Show hash marks while generating thumbnails");
add_option(2,'name_length',$OPTION_NUM, default=>40, usage=>"Limit length of image/dir names");
add_option(2,'date_sort',$OPTION_BOOL, usage=>"Sort images/dirs by date instead of captions/name");
add_option(2,'name_sort',$OPTION_BOOL, usage=>"Sort by name, not caption order");
add_option(2,'reverse_sort',$OPTION_BOOL, usage=>"Sort in reverse");
add_option(3,'body',$OPTION_STR, usage=>"Specify <body> tags for non-theme output");
add_option(3,'charset', $OPTION_STR, args=>'<str>', default=>'iso-8859-1', usage=>"Charset for non-theme output");
add_option(2,'image_loop',$OPTION_BOOL, default=>1, usage=>"Do first and last image pages loop around?");
add_option(1,'index', $OPTION_STR, args=>'<file>', usage=>["Select the default 'index.html' to use.",
           "For file://, try '-index index.html' to add 'index.html' to index links."]);
add_option(2,'default_index', $OPTION_STR, args=>'<file>', default=>'index.html', usage=>"The file the webserver accesses when no file is specified.");
add_option(3,'html', $OPTION_STR, args=>'<post>', default=>'.html', usage=>"Default postfix for HTML files");

# Thumbnail Options:
add_option(1,'Thumbnail Options:',$OPTION_SEP);
add_option(1,'geometry', \&parse_geometry, args=>'<X>x<Y>', default=>'133x133', usage=>"Size of thumbnail");
  add_option(99,'x',$OPTION_NUM, default=>133, usage=>"x Size of thumbnail");
  add_option(99,'y',$OPTION_NUM, default=>133, usage=>"y Size of thumbnail");
add_option(1,'type',$OPTION_STR, default=>'jpg', usage=>"Thumbnail type (gif, jpg, tiff,...)");
add_option(1,'medium_type',$OPTION_STR, usage=>"Medium type (default is same type as full image)");
add_option(1,'crop',$OPTION_BOOL, default=>0, usage=>["Crop the image to fit thumbnail size",
           "otherwise aspect will be maintained"]);
add_option(3,'CROP',$OPTION_STR, usage=>"Force cropping to be top, bottom, left or right");
add_option(1,'dir',$OPTION_STR, default=>'tn', usage=>"Thumbnail directory");
add_option(2,'force',$OPTION_BOOL, one_time=>1, usage=>["Force overwrite of existing thumbnails",
           "otherwise they are only written when changed"]);
add_option(2,'sample',$OPTION_BOOL, usage=>"convert -sample for thumbnails (faster, low quality)");
add_option(2,'sharpen', $OPTION_STR, args=>'<radius>x<sigma>', usage=>"Sharpen after scaling");
add_option(1,'animated_gifs',$OPTION_BOOL, usage=>"Take first frame of animated gifs (only some systems)");
add_option(2,'scale_opts',$OPTION_ARR, usage=>"Options for convert (use '--' for mult)");
add_option(3,'medium_scale_opts',$OPTION_ARR, usage=>"List of medium convert options");
add_option(3,'full_scale_opts',$OPTION_ARR, usage=>"List of full convert options");

# Theme Options:
add_option(1,'Theme Options:',$OPTION_SEP);
add_option(1,'theme', $OPTION_STR, args=>'<dir>', usage=>"Specify a theme directory");
add_option(2,'theme_url', $OPTION_STR, args=>'<url>', usage=>"In case you want to refer to the theme by absolute URL");
add_option(2,'theme_path', $OPTION_ARR, args=>'<dir>', default=>[], usage=>"Directories that contain themes");

# Paths:
add_option($WINDOWS?1:10,'Paths:',$OPTION_SEP);
add_option(10,'convert',$OPTION_STR, default=>'convert', usage=>"Path to convert (ImageMagick)");
add_option(10,'identify',$OPTION_STR, default=>'identify', usage=>"Path to identify (ImageMagick)");
add_option(10,'jhead',$OPTION_STR, default=>'jhead', usage=>"Path to jhead (extracts exif info)");
add_option(10,'ffmpeg',$OPTION_STR, default=>'ffmpeg', usage=>"Path to ffmpeg (extracting movie frames)");
add_option(10,'conf_file',$OPTION_STR, default=>'album.conf', usage=>"Conf filename for album configurations");
add_option(10,'dev_null',$OPTION_STR, default=>default_dev_null(), usage=>"Throwaway temp file");

# Windows crap:
#  "Windows.  It may be slow, but at least it's hard to use"
add_option($WINDOWS?1:10,'windows',$OPTION_STR, default=>$WINDOWS, usage=>"Are we (unfortunately) running windows?");
add_option($WINDOWS?1:10,'cygwin',$OPTION_STR, default=>$CYGWIN, usage=>"Are we using the Cygwin environment?");
# Win98: Needs TCAP:  ftp://ftp.simtel.net/pub/simtelnet/msdos/sysutl/tcap31.zip
add_option($TCAP?1:10,'use_tcap',$OPTION_BOOL, default=>$TCAP, usage=>"Use tcap? (win98)");
add_option($TCAP?1:10,'tcap',$OPTION_STR, default=>'tcap', usage=>"Path to tcap (win98)");
add_option($TCAP?1:10,'tcap_out',$OPTION_STR, default=>'atrash.tmp', usage=>"tcap output file (win98)");
add_option($TCAP?1:10,'cmdproxy',$OPTION_STR, default=>'cmdproxy', usage=>"Path to cmdproxy (tcap helper for long lines)");

# Default directory page
add_option(10,'header',$OPTION_STR, default=>'header.txt', usage=>"Path to header file");
add_option(10,'footer',$OPTION_STR, default=>'footer.txt', usage=>"Path to footer file");
add_option(10,'no_album',$OPTION_STR, default=>'.no_album', usage=>"Ignore dir/files if file with this postfix exists");
add_option(10,'hide_album',$OPTION_STR, default=>'.hide_album', usage=>"Ignore and don't display these files");
add_option(10,'not_img',$OPTION_STR, default=>'.not_img', usage=>"Don't treat these files as images");

# Generally not used as options stuff..
# <meta name='Album_Path' content='...'>
add_option(99,'path',$OPTION_STR, usage=>"Path of album so far");
# Hacky kludge stuff for internal/my purposes
add_option(99,'transform_url',$OPTION_STR, usage=>"Transform image URL");
add_option(99,'enter_eperl',$OPTION_STR, default=>'<:', usage=>"Enter code region in theme");
add_option(99,'leave_eperl',$OPTION_STR, default=>':>', usage=>"Leave code region in theme");
add_option(99,'num_hashes',$OPTION_NUM, default=>20, one_time=>1, usage=>"How many hashes to print");
add_option(99,'hash_width',$OPTION_NUM, default=>78, usage=>"Width of screen (for hashes)");

# As of "ImageMagick 4.2.9 99/09/01"
# May not be the same as your version of convert, but damn it's alot!
my $IMAGE_TYPES	=
	"AVS|BMP|BMP24|CMYK|DCM|DCX|DIB|EPDF|EPI|EPS|EPS2|EPSF|EPSI|EPT|FAX|".
	"FITS|G3|GIF|GIF87|GRADATION|GRANITE|GRAY|HDF|HISTOGRAM|ICB|ICC|ICO|".
	"IPTC|JPG|JPEG|JPEG24|LABEL|LOGO|MAP|MATTE|MIFF|MNG|MONO|MPG|MPEG|MTV|NULL|P7|".
	"PBM|PCD|PCDS|PCL|PCT|PCX|PDF|PIC|PICT|PICT24|PIX|PLASMA|PGM|PM|PNG|".
	"PNM|PPM|PREVIEW|PS|PS2|PS3|PSD|PTIF|PWP|RAS|RGB|RGBA|RLA|RLE|SCT|SFW|".
	"SGI|SHTML|STEGANO|SUN|TEXT|TGA|TIF|TIFF|TIFF24|TILE|TIM|TTF|TXT|UIL|".
	"UYVY|VDA|VICAR|VID|VIFF|VST|X|XBM|XC|XPM|XV|XWD|YUV";

##################################################
# Data structures
# (hashes unless otherwise specified)
##################################################
# $opt
# ----
# Options from command line and config files
# (and also themes and a few from index.html as well)
#
# $opt->{$option}	Array or string or number depending on @OPTIONS above.
#
# Also:
# $opt->{image.th}	The image.th file
# $opt->{album.th}	The album.th file
# $opt->{topdir}		The path to the top album.
#
# $data
# -----
# Each directory in the album has it's own data structure:
#
# $data->{unknown}	Does the directory contain an unknown HTML?
# $data->{start}	We are starting our call to album here
# @{$data->{dir_pieces}} All the pieces of the path to this part of the album
# $data->{depth}	Depth of the album to this part
# @{$data->{pics}}	Array of all the pics in this album
# @{$data->{dirs}}	Array of all the child directories
# $data->{obj}{$obj}	Object (image/directory) info (see $obj structure)
# $data->{paths}	Path info:
#   {dir}		  Current working album directory
#   {album_file}	  Full path to the album index.html
#   {album_path}	  The path of parent directories
#   {theme}		  Full path to the theme directory
#   {img_theme}		  Full path to the theme directory from image pages
#   {page_post_url}	  The ".html" to add onto image pages
#   {parent_albums}	  Array of parent albums (album_path split up)
#
# $obj
# ----
# Objects are images, movies, subdirectories..
# Each object has an $obj structure at $data->{obj}{$pic}
#
# $obj->{type}		Which list?  pics or dirs
# $obj->{is_movie}	Boolean: is this a movie?
# $obj->{name}		Name (cleaned and optionally from captions)
# $obj->{cap}		Image caption
# $obj->{capfile}	Optional caption file
# $obj->{alt}		Alt tag
# $obj->{num_pics}	[directories only, -dir_thumbs] Num of pics in directory
# $obj->{num_dirs}	[directories only, -dir_thumbs] Num of dirs in directory
#
# There are three sizes of images for each pic: full, medium, thumb.
# For each of these sizes, we have:
#
#   $obj->{<size>}{x}		Width
#   $obj->{<size>}{y}		Height
#   $obj->{<size>}{file}		Filename (without path)
#   $obj->{<size>}{path}		Filename (full path)
#   $obj->{<size>}{filesize}	Filesize in bytes
#   $obj->{full}{tag}	Tag - either 'image' or 'embed' (only for full)
#
# Image objs also have URL info:
#
# $obj->{URL}		URL paths: {URL}{from_page}{to}
#   {album_page}{image}		  Image_URL from album_page
#   {album_page}{thumb}		  Thumbnail from album_page
#   {image_page}{image}		  Image_URL from image_page
#   {image_page}{image_page}	  This image page from another image page
#   {image_page}{image_src}	  The <img src> URL for the image page
#   {image_page}{thumb}		  Thumbnail from image_page
#
# And directory objs have:
#
# $obj->{URL}{album_page}{dir}	URL to the directory from it's parent album page
#
# Internal $data/$opt fields
# --------------------------
# There are also some _fields in $data that are internal (not for theme use)
#
# $opt->{_theme}{..}		Contains entire text of the image.th/album.th
# $opt->{_theme_line}{..}	The starting line
# $opt->{_captions}{$dir}	Captions cache for a path
# 				(in opt to survive multiple do_album calls)
# @{$opt->{_albums}}		List of albums to run on
# $opt->{_album}{$alb}		Hash of any album settings (such as 'add')
# $data->{paths}{_date_sort_cache}	Date sort values
# $data->{eperl}		The ePerl support text

##################################################
# Bootstrap/Simple utilities
##################################################
sub attempt_require { eval "require $_[0]"; $@ ? 0 : 1; }

my %OPTIONS;
my @OPTIONS;
my %DEFAULTS;
sub add_option {
  my ($lvl,$option,$type,%hash) = @_;
  $hash{lvl}=$lvl;
  $hash{type}=$type;
  push(@OPTIONS,$option);
  $hash{default}=0 unless $hash{default} || $type!=$OPTION_BOOL;
  $OPTIONS{$option} = \%hash;
  $DEFAULTS{$option} = $hash{default};
}

#########################
# Windows blows
#########################
  # 1) Can't handle "\Qfile\E";
  sub file_quote {
    my ($opt,$file) = @_;
    $opt->{windows} ? "\"$file\"" : "\Q$file\E";
  }

  # 2) Can't create .files
  # 3) .exe extension if we don't have it
  # (fixed in get_defaults)

  # 4) Stupid $0 is probably '/' not '\'
  # (Fixed in PROGNAME split_path above)

  # 5) Can't handle 'open(FOO,"cmd |")' or 2>&1
  #    (Though 2>&1 works in Win2000, ActivePerl and Cygwin)
  my $TMPFILE;
  sub open_pipe {
    my ($opt,$cmd,$args) = @_;
    print STDERR "run: $cmd $args\n" if $opt->{d};
    my $fh = new IO::File;

    $cmd = file_quote($opt,$cmd);

    # Happy Unix
    return (open($fh, "$cmd $args 2>&1 |")) && $fh
      unless $opt->{cygwin} || $opt->{use_tcap};

    # Win98 (use TCAP)
    if ($opt->{use_tcap}) {
      usage("Couldn't find 'tcap'") unless $opt->{tcap};
      # Put tcap args in the tcap env var, so to reduce line length (128 limit)
      $ENV{tcap}="-overwrite *$opt->{tcap_out}";
      $TMPFILE = $opt->{tcap_out};	# Interrupt handlers can remove it..
      my $tcap = file_quote($opt,$opt->{tcap});
      $tcap .= " ".file_quote($opt,$opt->{cmdproxy}) if $opt->{cmdproxy};
      system("$tcap -c $cmd $args");
      (open($fh, "$opt->{tcap_out}")) || fatal($opt,"Can't open $opt->{tcap} output [$opt->{tcap_out}]");
      return $fh;
    }

    # Windows2000,XP:  -| pipe method, doesn't seem to work on Win98
    # (Only works under Cygwin??)
    # Otherwise error: '-' is not recognized as an internal or external
    #   command, operable program or batch file
    my $pid = (open($fh,"-|"));
    return undef unless defined $pid;	# Failed
    return $fh if $pid;			# Parent
    # Child
    (open(STDERR,">&STDOUT")) || fatal($opt,"open_pipe(): Can't dup stdout\n");
    exec("$cmd $args");
  }

  # 5 1/2)  Clean up the tmp file	(for Win98)
  sub all_done {
    print STDERR "@_\n" if @_;
    unlink($TMPFILE) if $TMPFILE;
    exit;
  }
  $SIG{INT} = \&all_done; $SIG{TERM} = \&all_done;
  $SIG{HUP} = \&all_done; $SIG{QUIT} = \&all_done;
  $SIG{EXIT} = \&all_done; $SIG{__DIE__} = \&all_done;

  # 6) Can't handle /dev/null
  sub default_dev_null() {
    return '/dev/null' if !$WINDOWS || $CYGWIN;
    ($ENV{TMP} || $ENV{TEMP} || '/tmp')."/$PROGFILE.null";
  }

#########################
# URLs for these scripts - don't change
#########################
my $HOME	= "http://MarginalHacks.com/";
my $ALBUM_URL	= "${HOME}Hacks/album/";
my $GEN_STRING	= "album $HOME";
my $OLD_GEN_RE	= "Generated by <a href=.+>$PROGNAME</a> and <a href=.+>thumb</a>";

sub get_defaults {
  my $opt = \%DEFAULTS;

  # Windows defaults are slightly different (no .files)
  if ($opt->{windows}) {
    # These aren't that important because of search_path_win()
    $opt->{convert} .= ".exe" unless $opt->{convert} =~ /\.exe$/;
    $opt->{identify} .= ".exe" unless $opt->{identify} =~ /\.exe$/;

    $opt->{no_album} =~ s/^\.//g;
    $opt->{hide_album} =~ s/^\.//g;
  }

  $opt;
}

##################################################
# COMMAND-LINE OPTIONS AND CONFIGURATIONS
##################################################
sub fatal {
  my ($opt,@msg) = @_;
  print STDERR "\n[$PROGNAME] ",join("\n", @msg),"\n\n" if @msg;
  exit -1;
}

sub usage {
  my (@msg) = @_;		# Called with error
  my ($opt,$option,$val) = @_;	# Called from option

  version();

  # If it was called from -h, -more or -More
  my $show=1;
  if (ref $opt eq 'HASH') {
    $show = 2 if $option eq "more";
    $show = 3 if $option eq "More";
    undef @msg;
  }

  # Otherwise we have a usage error
  if (@msg) {
    map { print STDERR "\nERROR:  $_\n"; } @msg;
    print STDERR "\nTry '$PROGNAME -h' for usage info.\n\n";
    exit -1;
  }

  # Print usage.
  print STDERR <<USAGE;

Usage:\t$PROGNAME [-d] [--scale_opts .. --] [options] <dir>
\tMakes a photo album

\tAll boolean options can be turned off with '-no_<option>'
\t(Some are default on, defaults shown in [brackets])

USAGE

  # Calculate width of -arg string
  my $maxw = 0;
  map { $maxw=max($maxw,length($_)); } @OPTIONS;
  $maxw-=1;
  $maxw=min($maxw,17);

  # Show usage for each option
  my $dashdash=0;
  foreach my $option ( @OPTIONS ) {
    my $type = $OPTIONS{$option}{type};
    my $default = $OPTIONS{$option}{default};
    next unless $OPTIONS{$option}{lvl}<=$show;
    if ($type==$OPTION_SEP) {
      print STDERR $option ? "\n$option\n" : "\n";
      next;
    }
    my $def = $type==$OPTION_BOOL ? ($default ? " [ON]" : " [OFF]") :
              !defined($default) ? "" :
              $type==$OPTION_ARR ? " [@$default]" :
              ($type==$OPTION_STR && !$default) ? "" : " [$default]";
    my $opt = $option;
    $opt .= " $OPTIONS{$option}{args}" if $OPTIONS{$option}{args};
    $opt = "-$opt" if $type==$OPTION_ARR;
    $dashdash++ if $type==$OPTION_ARR;
    my $usage = $OPTIONS{$option}{usage};
    my $usage_str = ref $usage eq 'ARRAY' ? join("\n      ",@$usage) : $usage;
    printf STDERR "  -%-${maxw}s $usage_str$def\n", $opt;
  }

  print STDERR <<DASHDASH_USAGE if $dashdash;

Dashdash options (--opt) can be specified two ways:
  With one argument:       "-exif hi -exif there"
  With mult. arguments:    "--exif hi there --"
DASHDASH_USAGE

  copyright();
}

sub gen_pod {
  my ($opt,$option,$val) = @_;

  my $eq = '=';	# Otherwise pod2man thinks the pod is up here
  print <<START_POD;
${eq}head1 OPTIONS

There are three types of options.  Boolean options, string/num options and
array options.  Boolean options can be turned off by prepending -no_:

% album -no_image_pages

String and number values are specified after a string option:

% album -type gif
% album -columns 5

Array options can be specified two ways, with one argument at a time:

% album -exif hi -exif there

Or multiple arguments using the '--' form:

% album --exif hi there --

START_POD

  my @options = grep($OPTIONS{$_}{lvl}<=10, @OPTIONS);
  my @bool_opts = grep($OPTIONS{$_}{type}==$OPTION_BOOL, @options);
  my @str_opts = grep($OPTIONS{$_}{type}==$OPTION_STR, @options);
  my @arr_opts = grep($OPTIONS{$_}{type}==$OPTION_ARR, @options);

  print "Boolean options:\n\n";
  print '% album -'.join(', -',@bool_opts),"\n\n";

  print "String/number options:\n\n";
  print '% album -'.join(', -',@str_opts),"\n\n";

  print "Array options:\n\n";
  print '% album --'.join(', --',@arr_opts),"\n\n";

  print <<START_OPTS;

${eq}head2 OPTION DESCRIPTIONS

${eq}over 4

START_OPTS

  foreach my $option ( @options ) {
    next if $OPTIONS{$option}{lvl} > 10;
    my $type = $OPTIONS{$option}{type};
    my $default = $OPTIONS{$option}{default};
    if ($type==$OPTION_SEP) {
      print <<POD_SEP if $option;
${eq}back

${eq}head2 $option

${eq}over 4

POD_SEP
      next;
    }
    my $def = $type==$OPTION_BOOL ? ($default ? " [Default ON]" : " [Default OFF]") :
              !defined($default) ? "" :
              $type==$OPTION_ARR ? " [Default @$default]" :
              ($type==$OPTION_STR && !$default) ? "" : " [Default $default]";
    my $opt = $option;
    $opt = "-$opt" if $type==$OPTION_ARR;
    my $args = $OPTIONS{$option}{args};
    $args = "string" if $type==$OPTION_STR && !$args;
    $args = "strings" if $type==$OPTION_ARR && !$args;
    unless ($args =~ s/^<([^<>]*)>$/$1/) {
      $args =~ s/</E<lt>/g;
      $args =~ s/(?<!E<lt)>/E<gt>/g;
    }
    $args = $args ? ($args =~ /^<.*>$/ ? "=I$args" : "=I<$args>") : "";
    my $usage = $OPTIONS{$option}{usage};
    my $usage_str = ref $usage eq 'ARRAY' ? join("\n",@$usage) : $usage;
    printf "=item B<-$opt>I<$args>\n\n$usage_str$def\n\n";
  }

  print "\n\n=back\n\n";

  exit 0;
}
#########################
# Version/Copyright Info
#########################
sub version {
  my ($opt,$option,$val) = @_;
  
  unless ($opt->{q} || $MAIN::SHOWED_VERSION++) {
    print STDERR "\nThis is $PROGNAME v$VERSION";
    print STDERR "  ** BETA version.  DO NOT DISTRIBUTE. **"
      if $VERSION =~ /b/;
    print STDERR "\n\n";
  }

  return unless $option;	# Called from -version, do the whole thing

  copyright();
  exit 0;
}

sub copyright {
  print <<COPYRIGHT;

Copyright:   (c) 2000-2004 David Ljung Madison
Docs:        $ALBUM_URL
License:     ${HOME}License/
Please see!  ${HOME}Pay/

COPYRIGHT
  exit 0;
}

#########################
# Parse command line
#########################
sub parse_geometry {
  my ($opt,$option,$val) = @_;

  usage("Can't understand geometry [$val]") unless $val =~ /^(\d+)x(\d+)$/;
  $opt->{geometry} = $val;	# To get saved in conf files
  ($opt->{x},$opt->{y}) = ($1,$2);
}

# Parse a single argument with optional value (and 'no_' flag)
sub parse_arg {
  my ($opt,$option,$val, $from) = @_;

  print STDERR "[ARG] $option $val - $from\n" if $opt->{d};

  # Should 'no_' be undef?
  $val = 0 if ($option =~ s/^no_?//);	# Handle 'no_' options
  my $dashdash = ($option =~ s/^-//) ? 1 : 0;
  usage("Unnecessary '--' in options") if $dashdash && !$option;

  usage("Unknown ".($from||"command-line option").":\n\t  -$option") unless $OPTIONS{$option};
  my $type = $OPTIONS{$option}{type};

  # Theme options don't override any options that are previously set.
  return if $from =~ /theme option/ && $opt->{_set}{$option};

  my $prev_val = $opt->{$option};	#duplicate($opt->{$option});

  if ($type==$OPTION_SEP) {
    usage("Unknown option: $option");

  } elsif ($type==$OPTION_BOOL) {
    # Booleans are either on or off
    my $set = 1;
    $set = ($val ? 1 : 0) if defined $val;
    $set = 0 if $val =~ /^(off|no)$/i;
    #$set = ($set ? 0 : 1) if $no;
    $opt->{$option} = $set;

  } elsif (ref $type eq 'CODE') {
    # Handle actions (code ref type)
    unless (defined $val || !$OPTIONS{$option}{args}) {
      # We need an arg - but $from implies we don't have any args to get..
      usage("Missing argument for $from option: $option") if $from;
      $val = shift @ARGV 
    }
    &$type($opt,$option,$val);

  } elsif ($type==$OPTION_ARR) {
    my @val;
    $val = shift @ARGV unless $dashdash || $from || defined $val;
    push(@val, $val) if defined $val;
    # If $from isn't defined, then we're parsing @ARGV, collect args to --
    if (!$from && $dashdash) {
      push(@val, shift(@ARGV)) while (@ARGV && $ARGV[0] ne "--");
      usage("Missing -- at end of $option") unless shift(@ARGV);
    }
    # Do we care about 'no_'?
    push(@{$opt->{$option}}, @val) if @val;

  } else {
    # Strings or numbers
    $val = shift @ARGV unless $from || defined $val;
    usage("-$option needs an argument") unless defined $val;
    usage("Bad -$option number [$val]") if $val=~/\D/ && $type==$OPTION_NUM;
    $opt->{$option} = $val;
  }

  # Keep track of which args were on the command-line ('SAVE' hash)
  # Always save array options when spec'd in ARGV, too hard to figure
  # out if they've changed.
  $opt->{SAVE}{$option} = 1 unless $from
    || ($type!=$OPTION_ARR && defined($prev_val) && $val eq $prev_val);
  $opt->{_set}{$option} = 1;
}

# Add to the list of albums
sub add_album_dir {
  my ($opt,$dir) = @_;

  my $path = port_abs_path($opt,$dir);
  return if $opt->{_album}{$path}{added}++;
  $opt->{_album}{$path}{arg} = $dir;
  push(@{$opt->{_albums}}, $path);

  # Get previous album options saved in meta tags
# DEPRECATED - this is all saved in album.conf now..
  return if $opt->{notheme};
  return if $opt->{theme};	# Already defined

# We could remove this ugliness, but we'd lose backwards compat.  And long
# before we do that, we should make sure that these get saved by {SAVE}..
  my $file = index_page($opt,1,$dir);
  my $theme;
  (open(FILE,"<$file")) || return;
  while(<FILE>) {
    # Did we find any meta args?
    if (/meta\s+name='Album_(.+)'\s+content='(.+)'/i) {
      my ($option,$val) = ($1,$2);
      $option = lc($option) unless $OPTIONS{$option};
      # This is for backwards compat - future opts get saved in conf files
# ToConsider: Is this dangerous?  If someone can edit the HTML, they
# can edit the album params.  Is this a big issue?  If someone else is
# running album, then by changing paths and whatnot this could be an issue..
unless ($option eq 'path' || $option eq 'theme') {
  hash_warn($opt,"Can't handle option [$option] in $file");
  next;
}

# We might not know the -theme_path yet..
#      if ($option eq 'theme' && ! -d search_path($opt,$val,@{$opt->{theme_path}})) {
#        hash_warn($opt,"Couldn't find theme [$val] - ignoring");
#        next;
#      }

      parse_arg($opt,$option,$val, "meta option [$file]");
    }
    # Stop if we see the end of head or start of body
    last if (/<\/head/i || /<body/i);
  }
  close FILE;
}

# Parse command-line options
sub parse_args {
  my ($opt) = @_;
  while (my $arg = shift @ARGV) {
    if ($arg =~ /^-([^=]+)(=(.+))?/) {
      # -options
      my ($option,$val) = ($1,$3);
      parse_arg($opt,$option,$val);

    } else {
      # Album directories
      $arg =~ s|/$||;	# Little cleanup
      usage("Can't find directory $arg") unless (-d $arg);

      add_album_dir($opt,$arg);
    }
  }

  # Default is current directory
  add_album_dir($opt,'.') unless $opt->{_albums} || $opt->{add};
}

#########################
# Configuration files
#########################
sub read_conf {
  my ($opt,$option,$conf,$check_argv) = @_;

  return if $opt->{_read_conf}{$conf};
  $opt->{_read_conf}{$conf} = 1;	# We read a conf file

  my $fh = new IO::File;
  unless (open($fh,"<$conf")) {
    print STDERR "[$PROGNAME] ERROR: Couldn't open conf: [$conf]\n" if $option;
    return 0;
  }

  my $conf_str = $conf;
  $conf_str =~ s|^\Q$opt->{topdir}\E/?||;
  print STDERR "Read conf: $conf_str\n" unless $opt->{q};

  while (<$fh>) {
    s/#.*//;	# Ignore comments
    next unless /\S/;	# And white-space
    unless (/^(\S+)(\s+(\S.*?))?\s*$/) {
      print STDERR "[$PROGNAME] Can't understand conf: [$conf_str, line $.]\n  $_";
      next;
    }
    my ($option,$val) = ($1,$3);
    $val = $1 if $val =~ /^"(.*)"$/ || $val =~ /^'(.*)'$/;
    parse_arg($opt,$option,$val, "conf option [$conf_str]")
      unless $check_argv && $opt->{SAVE}{$option};
  }
  close $fh;
  1;
}

sub read_confs {
  my ($opt) = @_;
  map { read_conf($opt,undef,$_) } @CONFS;
  $opt;
}

# Save any command-line specified options to the config file
sub save_conf {
  my ($opt,$data,$conf,$save) = @_;

  # If we specify $save then we want to save no matter what.
  my $changed = 0;
  $changed = 1 if $save;

  $save = $save || $opt->{SAVE};

  my @new;
  my %arr;
  if (open(CONF,"<$conf")) {
    while (<CONF>) {
      my ($line,$comment) = /(.*?)(\s*#.*)/ ? ($1,$2) : ($_);

      # Not an option line
      unless ($line =~ /^(\S+)((\s+)(\S.*))?$/) {
        push(@new,$_);
        next;
      }

      # Not a command-line option
      my ($option,$sp,$conf_val) = ($1,$3,$4);
      $conf_val = $1 if $conf_val =~ /^"(.*)"$/ || $conf_val =~ /^'(.*)'$/;
      unless ($save->{$option} && !$OPTIONS{$option}{one_time}) {
        push(@new,$_);
        next;
      }

      # This is an option we specified on the command line

      # Make a copy of array options
      my $array_op = ref $opt->{$option} eq 'ARRAY' ? 1 : 0;
      delete $save->{$option} unless $array_op;
      @{$arr{$option}} = @{$opt->{$option}}
        unless !$array_op || $arr{$option};
      my $array_empty = ($array_op && !@{$arr{$option}}) ? 1 : 0;

      my $opt_val = $array_op ? shift(@{$arr{$option}}) : $opt->{$option};

      $changed++ if $conf_val ne $opt_val;
      $opt_val = "'$opt_val'" if $opt_val =~ /\s/;
      $sp = "\t" unless $sp;
      push(@new,"$option$sp$opt_val$comment\n") unless $array_empty;
    }
    close CONF;
  }

  foreach my $option ( keys %{$save} ) {
    next if $OPTIONS{$option}{one_time};
    my @val = ref $opt->{$option} eq 'ARRAY' ? (@{$opt->{$option}}) : ($opt->{$option});
    @val = @{$arr{$option}} if $arr{$option};
    $changed+= $#val+1;
    map {
      $_ = "'$_'" if /\s/;
      push(@new,"$option\t$_\t# command-line saved option\n");
    } @val;
  }

  return unless $changed;

  return print STDERR "[$PROGNAME] ERROR: Couldn't write conf: [$conf]\n"
    unless (open(CONF,">$conf"));
  print CONF @new;
  close CONF;
  my $conf_str = $data ? "$data->{paths}{album_path}/$opt->{conf_file}" : $conf;
  print "Saved command line options in [$conf]\n" unless $opt->{q};
}

# Push and pop the option stack, for when we enter albums with local confs
sub push_opts {
  my ($opt) = @_;
  $opt->{_saved_opt} = duplicate($opt);
}

sub pop_opts {
  my ($opt) = @_;
  %$opt = %{$opt->{_saved_opt}};
}

# Any references in $opt need to be copied as well when we push_opts.
# We need to make a complete duplicate of the entire data structure.
sub duplicate {
  my ($thing) = @_;

  my $ref = ref $thing;

  if ($ref eq 'HASH') {
    my %copy;
    foreach my $key ( keys %$thing ) {
      # Don't need to make new copies of: SAVE and _captions or _set
      if (grep($key eq $_, qw(SAVE _captions _set))) {
        $copy{$key} = $thing->{$key};	# Just copy the reference
        next;
      }
      $copy{$key} = duplicate($thing->{$key});
    }
    return \%copy;
  }

  if ($ref eq 'ARRAY') {
    my @copy;
    $copy[$#$thing] = 0 if $#$thing>0;	# Performance: Grow to full size
    for(my $i=0; $i<=$#$thing; $i++) {
      $copy[$i] = duplicate($thing->[$i]);
    }
    return \@copy;
  }

  # We're in trouble if it's CODE or something else weird..
  # This will probably only happen if a plugin adds such a thing to $opt
  # and we'll need to figure out what to do with it when that happens..
  print STDERR "\n[$PROGNAME] Warning:  duplicate found a ref: [$ref] - don't know what to do!?\n"
    if $ref && !$MAIN::DUPLICATE_UNKNOWN_REF++;

  $thing;
}

# All the albums can have album.conf files
# (and we automatically save any command-line options here)
sub album_confs {
  my ($opt,$data) = @_;

  my $dir = $data->{paths}{dir};
  my $scrub = 0;

  # If we just got started and this is a subalbum, read all higher confs
  if ($data->{start}) {
    my @parents = @{$data->{dir_pieces}};
    for (my $i=0; $i<$#parents; $i++) {
      my $conf = "$opt->{topdir}/".join('/',@parents[0..$i])."/$opt->{conf_file}";
      next unless -r $conf;
      read_conf($opt,undef,$conf,1);
      $scrub = 1;
    }
  }

  # Do we have a local conf?
  my ($pushed,$conf) = (0,"$dir/$opt->{conf_file}");
  if (-r $conf) {
    push_opts($opt) unless $data->{start};
    $pushed = 1 unless $data->{start};
    read_conf($opt,undef,$conf,$data->{start});
    $scrub = 1;
  }

  # Options sanity checks and the like.
  scrub_opts($opt) if $scrub;

  # Should we save any command line options?
  save_conf($opt,$data,$conf) if $data->{start} && $opt->{save_conf};
  $pushed;
}

#########################
# After handling options, clean them up and sanity check them
#########################
sub scrub_opts {
  my ($opt,$first) = @_;

  # We only scrub these once, they pretty much shouldn't be changed
  # in any per-album configurations, and that would probably screw
  # things up if they were anyways...
  if ($first) {
    # Handle -add
    foreach my $add ( @{$opt->{add}} ) {
      my $path = port_abs_path($opt,$add);
      usage("Can't add root directory [$add]") if $path eq '/';
      my ($parent,$name) = split_path($opt,$path);
      # Add the parent and add to the parent's {add} array
      add_album_dir($opt,$parent);
      push(@{$opt->{_album}{$parent}{add}}, $name);
    }

    # ffmpeg can handle AVI, MOV
    $IMAGE_TYPES.="|AVI|MOV|MOOV" if $opt->{ffmpeg};

    #########################
    # Windows crap
    # "The box said Windows 95 or better.  So I bought a Macintosh."
    #########################
    # use_tcap or cygwin imply windows
    $opt->{windows}=1 if ($opt->{use_tcap} || $opt->{cygwin});

    # Don't do tcap and cygwin
    if ($opt->{use_tcap} && $opt->{cygwin}) {
      print STDERR "\n[$PROGNAME] Warning: Ignoring '-use_tcap' when '-cygwin' set\n  Use '-no_cygwin' if you want tcap\n";
      $opt->{use_tcap} = 0;
    }

    if ($opt->{windows}) {
      print STDERR "\n[$PROGNAME] Warning: Option -clean doesn't seem to work well under windows\n"
        if $opt->{clean};

      my @path = get_path($opt);
      $opt->{convert} = search_path_win($opt,$opt->{convert}, @path);
      usage("Couldn't find path for convert, try specifying with -convert option\n")
        unless $opt->{convert};
      $opt->{identify} = search_path_win($opt,$opt->{identify}, @path);
      usage("Couldn't find path for identify, try specifying with -identify option\n")
        unless $opt->{identify};
      $opt->{tcap} = search_path_win($opt,$opt->{tcap}, @path);
      $opt->{cmdproxy} = search_path_win($opt,$opt->{cmdproxy}, @path);
    }
  }

  # -medium needs image pages
  $opt->{image_pages}=1 if $opt->{medium};
  # -just_medium needs -medium
  usage("Need to specify -medium <geom> with -just_medium option")
    if $opt->{just_medium} && !$opt->{medium};

  # If the medium scaling is <width>x<height>, add ">" on the end so
  # convert will only shrink the images, never grow them
  $opt->{medium}.='\>' if ($opt->{medium} =~ /^\d+x\d+$/);

  # -clean and hashes is ugly
  $opt->{hashes}=0 if $opt->{clean} || $opt->{d} || $opt->{q};

  # -caption_edit needs themes
  usage("Can't use -caption_edit without a theme")
    if $opt->{caption_edit} && !$opt->{theme};

  # -theme_url needs -theme
  # Don't check here, it might be a conf arg or it might be set by the album
#  usage("-theme_url requires -theme option (it does not replace it)")
#    if $opt->{theme_url} && !$opt->{theme};
  # How about this instead?
  hash_warn($opt,"WARNING: -theme_url still requires -theme option, it doesn't replace it")
    if $opt->{theme_url} && !$opt->{theme} && !$MAIN::WARNED_THEME_URL++;

  usage("-CROP must be top, bottom, left or right")
    if ($opt->{CROP} && $opt->{CROP} !~ /^(top|bottom|left|right)$/);
}

##################################################
# Parsing Themes
##################################################

# Theme directories contain album.th and image.th
sub get_themes {
  my ($opt) = @_;

  my $theme = $opt->{theme};

  # -no_theme
  unless ($theme) {
    undef $opt->{'album.th'}; undef $opt->{'image.th'};
    $opt->{notheme} = 1;
    return;
  }

  # Find it
  my ($dir,$found_path) = search_path($opt,$theme,@{$opt->{theme_path}});
  unless (-d $dir) {
    # The old meta tag method ('Album_Theme') would be a relative path.
    # With -theme_path being stressed, we'll try just the theme name
    my (undef,$try) = split_path($opt,$theme);
    my ($newdir,$newfound_path) = search_path($opt,$try,@{$opt->{theme_path}});
    if (-d $newdir) {
      $opt->{theme} = $try;
      ($dir,$found_path) = ($newdir,$newfound_path)
    }
  }
  $dir = port_abs_path($opt,$dir) if -d $dir;

  # If it's a directory, look for "image.th" and "album.th"
  return hash_warn($opt,"Couldn't find -theme directory [$dir] (ignoring)\n\t(Try specifying -theme_path with -theme)")
    unless -d $dir;

  # Already got this theme
  return if $opt->{_theme_full} eq $dir;

  # New theme
  $opt->{_theme_full} = $dir;
  get_theme($opt,'album.th', "$dir/album.th") if -f "$dir/album.th";
  get_theme($opt,'image.th', "$dir/image.th")
    if -f "$dir/image.th" && $opt->{image_pages};
    # -no_image_pages kills image theme

  usage("No themes found in [$theme]")
    unless $opt->{'album.th'} || $opt->{'image.th'};
}

# Read in a whole template/theme file
# Check for Meta() and Credit() and theme options.
# (These tags are actually needed for proper operation,
#  not just my ego gratification!  Please don't override!)
sub get_theme {
  my ($opt,$which,$file) = @_;

  $opt->{$which} = $file;
  undef $opt->{_theme}{$which};	# In case we've specified themes twice..

  my $top = 1;	# Options can only be specified at the top of the file
  my $start_line = 1;

  # Privoxy web proxy software has a bug that converts " open(" to "concat("
  # So I'll use "(open" everywhere.  Dumbass proxy.
  (open(TEMP,"<$file")) || usage("Couldn't read theme [$file]");
  my ($in_head,$saw_meta,$saw_credit);
  while (<TEMP>) {
    if ($top && /^\s*(#c)?\s*(\/\/)?\s*options?:\s*(\S.*)/i) {
      my $str = $3;
      $str =~ s/\s+$//g;
      my ($option,$val) = split(/\s+/,$str,2);
      $option =~ s/^\-//;
      parse_arg($opt,$option,$val, "theme option [$file]");
      $start_line = $.+1;
      next;
    }
    $top = 0;
    push(@{$opt->{_theme}{$which}},$_);
    $in_head=1 if (/<head>/i);
    if (/Meta\(\)/) {
      usage("Meta() must be inside <head>...</head>") if (!$in_head);
      $saw_meta=1;
    }
    $in_head=0 if (/<\/head>/i);
    $saw_credit=1 if (/Credit\(\)/);
  }
  close(TEMP);

  usage("You need to call Meta() inside <head>..</head> of [$file]") unless $saw_meta;
  usage("You need to call Credit() in your theme [$file]") unless $saw_credit;

  $opt->{_theme_line}{$which} = $start_line;
}

##################################################
##################################################
# Is this their first time?
##################################################
##################################################
sub virgin_check {
  my ($opt) = @_;

  # Eventually we'll keep version numbers in the conf files, not today.
  return if $opt->{_read_conf} && keys %{$opt->{_read_conf}} && !$opt->{configure};

  # Options that we want to save
  my %save;
  $opt->{configure_save} = \%save;

  # First install.  Hopefully they've got a tty
  print STDERR "I see this is your first time running $PROGNAME v$VERSION.\n\n"
    unless $opt->{_read_conf} && keys %{$opt->{_read_conf}};
  print STDERR "I'm going to do some simple installations for you.\n\n";

  # Figure out where the conf is going to go.
  my $conf = !$< ? '/etc' : $ENV{HOME};
  $conf = $ENV{USERPROFILE} if ($opt->{windows} && (!$conf || !-d $conf));
  $conf = $BASENAME unless $conf && -d $conf;
  $conf = '.' unless $conf && -d $conf;	# Don't know how that happened..

  $conf .= $conf eq $ENV{HOME} ? "/.$PROGFILE.conf" : "/$PROGFILE.conf";

  print STDERR "I'll save your installation in:\n  $conf\n\n";
  print STDERR "You can edit this file later to suit your needs.\n\n";
  print STDERR "Run $PROGNAME as root if you want to do a system-wide configuration.\n\n"
    if $< && -d '/etc';

  print STDERR "Are you ready?\n";
  exit unless install_get_yn($opt,'y');

  print STDERR "--------------------------------------------------\n\n";

  # THEME PATH
  my $themes;
  unless ($opt->{notheme}) {
    $themes = install_theme_path($opt);

    print STDERR "\n--------------------------------------------------\n\n";

    if ($themes) {
      # INSTALL THEMES?
      if (-d $themes) {
        print STDERR "If $PROGNAME has problems finding themes, make sure they're in:\n";
        print STDERR "  $themes\n";
        print STDERR "\nOr edit $conf to set their new location\n";
      } else {
        print STDERR "I see that the -theme_path does not currently exist:\n  $themes\n\n";
        print STDERR "Would you like to try to install the themes there?\n";
        if (install_get_yn($opt,'y')) {
          print STDERR "\n";
          # Try to find where the themes were installed
          my $curr = install_find_current_themes($opt);
          if ($curr && !install_move($opt,$curr,$themes)) {
            print STDERR "\nCouldn't install themes!\n";
            print STDERR "Please login as a user with the appropriate privileges and copy:\n";
            print STDERR "  $curr -> $themes\n\n";
          }
        }
      }
    }
    print STDERR "\n--------------------------------------------------\n\n";
  }

  # THEME URL
  unless ($opt->{theme_url} || !$themes) {
    my $num3 = $themes ne port_abs_path($opt,$themes) ? "" :
      "\n   (I've checked and #3 is not currently true.)\n";
    print STDERR <<CONFIG_THEME_URL;
You can also specify a default -theme_url path.  This is useful if the
mapping between your file path and URLs are not clear, for example if:

1) You have virtual domains.
2) You're using mod_rewrite on the themes path.
3) You use a symbolic link to point to the actual themes directory.$num3

In general if you don't know what these mean, then you probably don't
need to specify -theme_url.  Usually the filesystem and URL match after
you remove the web root directory.  Otherwise, this should be the URL
that you can put in your browser to see a listing of the theme directories.

CONFIG_THEME_URL

    while (!$opt->{theme_url}) {
      print STDERR "Do you want to specify a -theme_url?\n";
      my $default = $num3 ? 'n' : 'y';
      last unless install_get_yn($opt,$default);
      $opt->{theme_url} = install_get_answer($opt,'theme_url','/Themes/',<<THEME_URL);

Some reasonable values:  /Themes/,  http://yourdomain.com/Themes/
THEME_URL
      undef $opt->{theme_url} if $opt->{theme_url} =~ /\S\s+\S/;
      print STDERR "\nThe -theme_url value should not contain any spaces\n\n"
        unless $opt->{theme_url};
    }
    $opt->{configure_save}{theme_url} = 1 if $opt->{theme_url};
  }

  # CONVERT/IDENTIFY
  print STDERR "\n";
  install_find_exec($opt,'convert');
  install_find_exec($opt,'identify',1);
  install_find_exec($opt,'ffmpeg',1);

  # DONE!
  print STDERR "\nEnd of configuration\n\n";

  # Save it.
  save_conf($opt,undef,$conf,\%save);
  undef $opt->{configure_save};

  exit if $opt->{configure};
  # Otherwise we can continue to run album.

  print STDERR "Continuing with $PROGNAME run:\n";
}

sub install_move {
  my ($opt,$from,$to) = @_;

  print STDERR "\nAttempting to move:\n  $from -> $to\n\n";

  # Attempt to move the directory.
  return print "Move successful.\n" if rename($from,$to);

  # If rename doesn't work, try mv (rename might break across filesystems)
  my $mv = '/bin/mv';
  $mv = '/usr/bin/mv' unless -x $mv;
  $mv = '/sbin/mv' unless -x $mv;
  return 0 unless -x $mv;
  system($mv,$from,$to);
  return print "Move successful.\n" unless $?;
  0;
}

sub install_find_exec {
  my ($opt,$what,$optional) = @_;

  my $try = $opt->{$what};

  if ($try =~ m|[/\\]|) {
    print STDERR "Found $what setting:  $try\n";
    print STDERR "  But it's missing or not executable!\n" unless -x $try;
    print STDERR "\nChange?\n";
    return unless install_get_yn($opt, -x $try ? 'n' : 'y');
  } else {
    # Search path
    $try = $try || $what;
    # Actually, if it's windows we should have found it via scrub_opts
    my @path = get_path($opt);
    my $slash = $opt->{windows} ? '\\' : '/';
    my $found = search_path_exec($opt,$slash,$what,@path);
    return print STDERR "Found $what in path:  $found\n" if -x $found;
  }

  while (1) {
    my $ques = "\nCan't find $what executable.\n";
    $ques .= $optional ?
      "This is optional but is useful to $PROGNAME.\n" :
      "I need to know where this is for $PROGNAME to run properly.\n";
    $try = install_get_answer($opt,"path to $what",undef,$ques);

    last if -x $try;
    print STDERR "\nExecutable not found: [$try]\nTry again?\n";
    return unless install_get_yn($opt,'y');
  }

  # We need to save this
  $opt->{configure_save}{$what} = 1;
  $opt->{$what} = $try;
}

sub install_theme_path {
  my ($opt) = @_;

  my $themes;
  foreach $themes ( reverse @{$opt->{theme_path}} ) {
    last if -x $themes;
  }

  # Assume they meant to use the path they specified
  $themes = $opt->{theme_path}[-1] if !$themes && $opt->{theme_path};

  print STDERR "- I see you specified the -theme_path.  Using [$themes]\n"
    if $themes;
  return $themes if -x $themes;
  print STDERR "  ..but I can't find the directory!\n\n" if $themes;

  unless ($themes) {
    # Try to guess path
    $themes = '/var/www/html' unless -x $themes;
    $themes = '/var/www' unless -x $themes;
    $themes = '/home/httpd' unless -x $themes;
    $themes = '/home/http' unless -x $themes;
    $themes = "$ENV{HOME}/public_html" unless -x $themes;
    $themes = '' unless -x $themes;
    $themes .= "/Themes" if $themes;
  }

  my $example = $themes ? $themes : '/var/www/html/Themes';

  print STDERR "- You need to specify the option: 'theme_path'\n";
  print STDERR "- You can override this value on the command-line: -theme_path\n\n";
  my $themes = install_get_answer($opt,'theme_path',$themes,<<THEME_PATH);
$PROGNAME supports themes for web page layout, they were probably included
with the package you installed, or can be downloaded from:
  $ALBUM_URL

You need to put them in a directory *inside* your web path.
If you can't find the themes from your web browser, then any
images or css you use will not be displayed.

A good location is a directory inside the web root, something like:
  $example
THEME_PATH

  return unless $themes;
  push(@{$opt->{theme_path}}, $themes);
  $opt->{configure_save}{theme_path} = 1;	# Make sure to save it
  $themes;
}

# Find where the themes were downloaded
sub install_find_current_themes {
  my ($opt) = @_;
  my ($curr,$where) = ("/usr/share/$PROGFILE/themes","\n  Default is the dpkg install of $PROGNAME.");
  ($curr,$where) = ('./Themes',"\n  Found local ./Themes directory.") unless -x $curr;
  ($curr,$where) = (undef,undef) unless -x $curr;


  # Is it still zipped up?
  my $targz = "${PROGFILE}_themes.tar.gz";
  if (-f $targz && !-d 'Themes') {
    print STDERR "\nI found a gzipped themes tar: $targz\n\nWould you like me to try to unzip it?\n";
    if (install_get_yn($opt,'y')) {
      system("tar xzf $targz");
      if ($?) {
        # Try gunzip/tar
        system("gunzip $targz");
        system("tar xzf $targz") unless $?;
      }
      return 'Themes' unless $? || !-d 'Themes';
      print STDERR "\nError untarring: $!\n";
    }
  }

  my $zip = "${PROGFILE}_themes.zip";
  if (-f $zip && !-d 'Themes') {
    print STDERR "\nI found a zipped themes archive: $zip\n\nWould you like me to try to unzip it?\n";
    if (install_get_yn($opt,'y')) {
      system("unzip $targz");
      return 'Themes' unless $? || !-d 'Themes';
      print STDERR "\nError untarring: $!\n";
    }
  }

  while (1) {
    my $dir = install_get_answer($opt,'downloaded theme location',$curr,<<CURRENT);
I need to find where you downloaded the Themes.  $where
CURRENT
    return $dir if -d $dir;
    print STDERR "\nDirectory not found: [$dir]\nTry again?\n";
    return undef unless install_get_yn($opt,'y');
    print STDERR "\n";
  }
}

sub install_get_yn {
  my ($opt,$default) = @_;
  while (1) {
    my $ans = install_get_answer($opt,'y/n',$default);
    return 1 if $ans =~ /^y/i;
    return 0 if $ans =~ /^n/i;
    print STDERR "You must answer 'y' or 'n'\n";
  }
}

sub install_get_answer {
  my ($opt,$question,$default,$desc) = @_;

  print STDERR "$desc\n";
  my $ret;
  while (!$ret) {
    print STDERR "Enter $question";
    print STDERR " [default: $default]" if $default;
    print STDERR "> ";
    my $line = scalar <STDIN>;  chomp($line);
    $ret = $line || $default;
  }
  $ret;
}

##################################################
##################################################
# UTILITIES
##################################################
##################################################
sub max { $_[0]>$_[1] ? $_[0] : $_[1]; }
sub min { $_[0]<$_[1] ? $_[0] : $_[1]; }

#########################
# Stupid privoxy bug.
#########################
sub concat {
  my $rep = 'op';  $rep .= 'en(';
  die(<<PRIVOXY_SUCKS);
Your proxy (privoxy) has a bug in it:

  http://www.privoxy.org/faq/misc.html#DOWNLOADS

Privoxy corrupts text scripts by changing o-p-e-n-( to concat(
And they don't seem concerned about it.  So you might want to consider
getting a new proxy.  Until then, replace all 'concat(' in this script
with '$rep'

PRIVOXY_SUCKS
}

#########################
# abs_path
# Use internal version if they don't have the Cwd package
#########################
my $CWD = attempt_require('Cwd');
sub int_abs_path {
  my ($dir) = @_;
  my $save=`pwd`; chomp($save);
  chdir($dir) || usage("Couldn't find [$dir]");
  my $name=`pwd`;  chomp($name);
  chdir($save);
  $name;
}

sub port_abs_path {
  my ($opt,$dir) = @_;
  -d $dir || usage("Couldn't find directory [$dir]");
  return $CWD ? Cwd::abs_path($dir) : int_abs_path($dir);
}


#########################
# Diff two paths
# Find a relative path between the two
#########################
sub diff_path {
  my ($opt,$from,$to) = @_;

  # Remove file component
  $from =~ s|/[^/]+$|| unless -d $from;

  my $back = "";
  while ($to !~ /^\Q$from\E/) {
    $back .= "../";
    $from =~ s|/[^/]+$||;
  } 
  $to =~ s|^\Q$from\E/?||;
  
  $back.$to;
}

#########################
# Search path: If a path isn't absolute, get it from the given dir
#########################
sub get_path {
  my ($opt) = @_;
  return split(':',$ENV{PATH}) unless $opt->{windows};
  my @path = split(';',$ENV{PATH});
  push(@path,'C:\PROGRAM FILES\IMAGEMAGICK');
  # c:\windows\system32 has an NTFS utility called convert which isn't right
  # And besides, the system32 directory isn't going to help us.
  grep(!m|windows[/\\]system32|, @path);
}

# Returns path and result.
#   result=2 if found in @dir
#   result=1 if found elsewhere
#   result=0 if not found
sub search_path_test {
  my ($opt,$test,$slash,$path,@dir) = @_;

  # Absolute path
  return ($path, &$test($path) ? 1 : 0)
    if $path =~ /^\// || ($opt->{windows} && $path =~ m|^([a-z]:)?[/\\]|i);

  # Kludge for '~'
  return search_path_test($opt,$test,$slash,"$ENV{HOME}/$1",@dir)
    if $path =~ m|^~/(.*)|;

  # Check @dir
  foreach my $dir ( @dir ) {
    $dir =~ s/$slash$//;
    return ("$dir$slash$path",2) if &$test("$dir$slash$path");
  }

  ($path, &$test($path) ? 1 : 0);
}

sub search_path {
  my ($opt,$path,@dir) = @_;
  my ($p,$r) = search_path_test($opt, sub {-e $_[0]}, '/', $path, @dir);
  wantarray ? ($p,$r) : $p;
}

sub search_path_exec {
  my ($opt,$slash,$path,@dir) = @_;
  my ($p,$r) = search_path_test($opt, sub {-x $_[0]}, $slash, $path, @dir);
  wantarray ? ($p,$r) : $p;
}

sub search_path_win {
  my ($opt,$path,@dir) = @_;
  my $try = search_path_exec($opt,'\\', $path, @dir);
  return $try if -x $try;
# To be complete we could check everything in $ENV{PATHEXT}..
  $try = search_path_exec($opt,'\\', $path.'.exe', @dir);
  return $try if -x $try;
  $try = search_path_exec($opt,'\\', $path.'.com', @dir);
  return $try if -x $try;
  undef;
}

#########################
# What's the file for an index page?
# $file=1 means return full path (include even default index)
#########################
sub index_page {
  my ($opt, $file, $dir) = @_;
  $dir .= '/' if $dir;
  my $index = $opt->{index};
  return $dir unless $index || $file;
  $index = $index || $opt->{default_index};
  $dir.$index;
}

#########################
# Hash code
#########################
my $_hashes_done = 0;
my $_hashes_start;
sub start_hashes {
  my ($opt,$str) = @_;
  return if $opt->{q};
  return print STDERR "$str\n" unless $opt->{hashes};
  my $w = $opt->{hash_width} - $opt->{num_hashes} - 3;
  $_hashes_start = $str ? sprintf("%-${w}s",$str) : "";
  $_hashes_done = 0;
  print STDERR "$_hashes_start ["," "x$opt->{num_hashes},"]\b","\b"x$opt->{num_hashes};
}
sub show_hashes {
  my ($opt,$done,$outof) = @_;
  return unless $opt->{hashes} && !$opt->{q};
  return unless $outof;
  my $needed = int($opt->{num_hashes}*($done/$outof));
  print STDERR "X"x($needed-$_hashes_done);
  $_hashes_done = $needed;
}
sub stop_hashes {
  my ($opt) = @_;
  return unless $opt->{hashes} && !$opt->{q};
  show_hashes($opt,1,1);
  undef $_hashes_start;
  undef $_hashes_done;
  print STDERR "]\n";
}
sub hash_msg {
  my ($opt,$str) = @_;
  return unless $opt->{hashes};
  printf STDERR "%".$opt->{num_hashes}."s]\n",$str;
  undef $_hashes_start;
}
sub hash_warn {
  my ($opt,@msgs) = @_;
  print STDERR "\n" if $_hashes_start;
  foreach my $msg (@msgs) { print STDERR "[$PROGNAME] $msg\n"; }
  return unless $opt->{hashes} && ($_hashes_done || $_hashes_start);
  start_hashes($opt,$_hashes_start);
  print STDERR "X"x$_hashes_done;
  undef;
}

##################################################
##################################################
# ALBUM UTILITIES
##################################################
##################################################
# Nice name for printing
sub clean_name {
  my ($opt,$name,$capname,$no_html) = @_;

  $name = $capname || $name;

  # No tags in filenames  :)
  $name =~ s/\</&lt;/g unless $capname;

  # Remove postfixes
  $name =~ s/\.($IMAGE_TYPES)$//i;
  $name =~ s/\Q$opt->{html}\E$//i;

  # Remove thumbnail cropping directives
  $name =~ s/CROP(top|bottom|left|right)$//;

  unless ($capname) {
    # Underbar = space
    $name =~ s/_/ /g;
    $name =~ s/\./ /g;

    # No paths
    $name =~ s|^.*/||g;
  }

  # I sort my albums by date:   2001-10-03.some_directory
  $name = "<font size='-1'>$1</font><br>$3"
    if !$no_html && $name =~ /^(\d{4}-\d{1,2}(-\d{1,2})?)( .+)$/;

  $name;
}

# What's the filesize of a file?  (String format)
sub filesize($) {
  my ($file) = @_;
  my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
      $atime,$mtime,$ctime,$blksize,$blocks) = stat($file);
  $size=int($size/102.4)/10;
  $size=int($size) if ($size>10);
  return "${size}k" if ($size<1024);
  $size=int($size/102.4)/10;
  return "${size}M" if ($size<1024);
  $size=int($size/102.4)/10;
  "${size}G";
}

# Is there some unknown HTML (that we didn't create?)
# If we know this HTML, get the full album path,
# in case we are only regenerating a branch of the full tree
sub parse_index {
  my ($opt,$data) = @_;

  my $index = index_page($opt,1,$data->{paths}{dir});
  return 1 unless -f $index;
  return 1 if -z $index;

  $data->{unknown} = 1;
  (open(INDEX,"<$index")) || return;
  while(<INDEX>) {
    $data->{unknown} = 0  if (/$OLD_GEN_RE/);	# Old string, backwards compat
    $data->{unknown} = 0  if (/meta\s+name='Generator'\s+content='$GEN_STRING'/i);
    $data->{paths}{album_path} = $1 if (/meta\s+name='Album_Path'\s+content='(.+)'/i);

    last if defined $data->{paths}{album_path} && !$data->{unknown};
  }
  close(INDEX);
  hash_warn($opt,"Skipping unknown HTML:\n  $index") if $data->{unknown};
}

#########################
# Clean out unused images/files from the thumbnail directory
#########################
sub clean_thumb_dir {
  my ($opt,$data) = @_;

  my $dir = "$data->{paths}{dir}/$opt->{dir}";
  my @pics = $data->{pics} ? @{$data->{pics}} : ();

  return unless -d $dir;

  # Read the thumbnail directory
  opendir(DIR,$dir);
  my @files = grep(!/^\.{1,2}$/, readdir(DIR));
  closedir(DIR);

  # Check each file to make sure it's a currently used thumbnail or image_page
  foreach my $file ( @files ) {
    my $remove;
    my $name = $file;
    my $html = $opt->{index} ? ('.'.$opt->{index}) : $opt->{html};
    if ($name =~ s/\Q$html\E$//) {
      $remove = "unused image page"
        unless ($opt->{image_pages} && grep($_ eq $name, @pics));
    } elsif ($name  =~ /(.+)\.med\.(.+)/) {
      $name = "$1.$2";
      $remove = "unused medium image"
        unless $opt->{medium} && grep($_ eq $name, @pics);
    } elsif ($name  =~ /(.+)\.snap\.(.+?)(\.$opt->{type})?$/) {
      $name = "$1.$2";
      $remove = "unused snapshot image" unless grep($_ eq $name, @pics);
    } elsif ($name  =~ /(.+)\.$opt->{type}$/) {
      # Thumbnail?
      $name = $1;
      $remove = "unused thumbnail"
        unless grep($_ eq $name || $_ eq "$name.$opt->{type}", @pics);
    } elsif ($name  =~ /(.+)\.$opt->{type}\.ppm$/) {
      my $mov = $1;
      $remove = "unused ppm thumbnail?" unless (grep(/^\Q$mov.\E/, @pics));
    } else {
      $remove = "unknown file";
    }
    if ($remove) {
      print "Remove $remove: $dir/$file\n";
      hash_warn($opt,"Remove $remove: $dir/$file");
# This doesn't seem to work (or show error) under windows.  Bah.
      hash_warn($opt,"Couldn't erase [$file]")
        unless unlink "$dir/$file";
    }
  }
}

#########################
# Quote stuff to avoid errors
#########################
sub quote {
  my ($path,$opt,$url) = @_;

  if ($url) {
    # Handle ['"#?*!:%] always, regardless of -fix_urls - see RFC 1630
    # or - RFC1630 legally allows:  [\?\/\+a-zA-Z0-9\$\-_\@\.&!\*"'\(\),]
    $path =~ s/(['"#?*!:%])/"%".sprintf("%2.2x",ord($1))/eg;
  } else {
    $path =~ s/\'/%27/g;	# Just quotes
  }

  $path = "'$path'";	# And quote the rest
  return $path unless $url && $opt && $opt->{fix_urls};

  # Handle all unsafe characters including space.
  # Encode everything below space and above 127
  $path =~ s/([\x00-\x20\x7F-\xFF])/"%".sprintf("%2.2x",ord($1))/eg;

  $path;
}

##################################################
##################################################
# CAPTIONS
##################################################
##################################################

#########################
# Read a captions file.
#########################
sub read_captions {
  my ($opt,$dir) = @_;

  # $dir may actually be $data, get $data->{paths}{$dir}
  $dir = $dir->{paths}{dir} if ref $dir eq "HASH";

  return $opt->{_captions}{$dir} if $opt->{_captions}{$dir};

  my %caps;
  $opt->{_captions}{$dir} = \%caps;

  my $caps = $opt->{captions};
  return \%caps unless $caps;
  return \%caps unless -r "$dir/$caps";
  if (!open(CAPS,"<$dir/$caps")) {
    print STDERR "[$PROGNAME] Couldn't read captions: [$dir/$caps]";
    return \%caps;
  }
  while (<CAPS>) {
    chomp;
    my $split_tabs = /\t/ ? 1 : 0;
    my ($file,$name,$cap,$alt)=
      $split_tabs ? split(/\t+/, $_, 4) : split(/\s*::\s*/, $_, 4);
    $name=$file if (!$name && $cap);
    next unless $file;
    #$file=$1 if ($file eq '.' && $dir =~ m|([^/]+)$|);
    $caps{$file}{name}=$name;
    $caps{$file}{cap}=$cap if $cap;
    $caps{$file}{alt}=quote($alt,$opt);
    $caps{$file}{line}=$.+1;
  }
  close CAPS;

  \%caps;
}

# EXIF information added to captions.
sub get_exif {
  my ($opt,$pic) = @_;

  my %exif;
  my $qpic = file_quote($opt,$pic);
  my $jhead = open_pipe($opt,$opt->{jhead},$qpic);
  undef $opt->{jhead} unless $jhead;
  return "" unless $jhead;
  while(<$jhead>) {
    print STDERR "get_exif(): $_" if $opt->{d};
    if (/command not found/) {	# Kind of kludgy
      hash_warn($opt,"-exif specified but jhead not found [$opt->{jhead}]");
      undef $opt->{jhead};
      $jhead->close;
      return "";
    }
    $exif{$1} = $2 if /(.+?)\s*:\s*(\S.*)/;
  }
  $jhead->close;
  \%exif;
}

# Any EXIF key found in the exif string inside %% is replaced.
sub exif_replace {
  my ($exif,@str) = @_;
  my $ret;
  foreach my $str ( @str ) {
    $str =~ s/%([^%]+)%/$exif->{$1} ? $exif->{$1} : "% %"/eg;
    # Ignore this EXIF caption if we missed any keys.
    $ret .= $str unless $str =~ /% %/;
  }
  $ret;
}

sub get_exif_captions {
  my ($opt,$data,$pic) = @_;

  return "" unless $opt->{exif} || $opt->{exif_image} || $opt->{exif_album};
  return "" if !$opt->{jhead};
  return "" unless $pic =~ /\.jpe?g$/i;

  my $dir = $data->{paths}{dir};

  my $exif = get_exif($opt,"$dir/$pic");

  $data->{obj}{$pic}{exif} = exif_replace($exif, @{$opt->{exif}})
    if $opt->{exif};
  $data->{obj}{$pic}{exif_image} = exif_replace($exif, @{$opt->{exif_image}})
    if $opt->{exif_image};
  $data->{obj}{$pic}{exif_album} = exif_replace($exif, @{$opt->{exif_album}})
    if $opt->{exif_album};
}

#########################
# Get directory caption
#########################
sub get_dir_caption {
  my ($opt,$dir,$name) = @_;

  my $path = $dir ? "$opt->{topdir}/$dir" : $opt->{topdir};

  # First check parent directory for $name
  my $caps = read_captions($opt,$path);
  return $caps->{$name} if $caps->{$name};

  # Check the $name directory for $name or '.'
  $caps = read_captions($opt,"$path/$name");
  return $caps->{$name} if $caps->{$name};

  return $caps->{'.'} if $caps->{'.'};
  undef;
}

#########################
# Get pics/dirs caption info
#########################
sub get_captions {
  my ($opt,$data) = @_;

  # First read the captions file
  my $caps = read_captions($opt,$data);

  # Commented out lines in captions is same as .no_album
  @{$data->{pics}} = grep(!$caps->{"#".$_} || $caps->{$_}, @{$data->{pics}});
  @{$data->{dirs}} = grep(!$caps->{"#".$_} || $caps->{$_}, @{$data->{dirs}});

  # Put caption info into %data
  foreach my $pic ( @{$data->{pics}} ) {
    $data->{obj}{$pic} = $caps->{$pic};	# Put cap info into data
    get_exif_captions($opt,$data,$pic);

    $data->{obj}{$pic}{name} = clean_name($opt,$pic,$data->{obj}{$pic}{name});
  }

  # Directories - the caption can be here or in it's own directory
  foreach my $dir ( @{$data->{dirs}} ) {
    if ($caps->{$dir}) {
      $data->{obj}{$dir} = $caps->{$dir};	# Put cap info into data
    } else {
      my $dircaps = read_captions($opt,"$data->{paths}{dir}/$dir");
      $data->{obj}{$dir} = $dircaps->{'.'} || $dircaps->{$dir};
    }
    $data->{obj}{$dir}{name} = clean_name($opt,$dir,$data->{obj}{$dir}{name});
  }
}

##################################################
##################################################
# SORTING PICS/DIRS
##################################################
##################################################

# The sort rank for a file, according to the captions file or date
sub sort_rank {
  my ($opt,$data,$f) = @_;
  return ($data->{obj}{$f} && $data->{obj}{$f}{line}) unless $opt->{date_sort};
  # Save mod times in a cache
  return $data->{paths}{_date_sort_cache}{$f}
    if $data->{paths}{_date_sort_cache} && $data->{paths}{_date_sort_cache}{$f};
  $data->{paths}{_date_sort_cache}{$f} = -(-M "$data->{paths}{dir}/$f");
  $data->{paths}{_date_sort_cache}{$f};
}

# Compare two names using natural sort (bob_2.jpg < bob_10.jpg)
sub natural_cmp {
  my ($a,$b) = @_;
  my @a = split /(\d+)/, $a;
  my @b = split /(\d+)/, $b;
  my $M = @a > @b ? @a : @b;
  for (my $i = 0; $i < $M; $i++) {
    return -1 if ! defined $a[$i];
    return 1 if  ! defined $b[$i];
    return $a[$i] <=> $b[$i] if $a[$i] =~ /\d/ && $a[$i] <=> $b[$i];
    return $a[$i] cmp $b[$i] if $a[$i] !~ /\d/ && $a[$i] cmp $b[$i];
  }
  0;
}

sub sort_order {
  my ($opt,$data,$a,$b) = @_;

  ($a,$b) = ($b,$a) if $opt->{reverse_sort};

  my ($an,$bn);
  unless ($opt->{name_sort}) {
    $an = sort_rank($opt,$data,$a);
    $bn = sort_rank($opt,$data,$b);
  }

  # Get name
  $a = $data->{obj}{$a}{name};
  $b = $data->{obj}{$b}{name};

# This tries to mingle captioned images with non-captioned.  It won't work.
# Consider images:  a, b, c and captions file only has c then a.  No sort!
#  return $an <=> $bn if ($an && $bn);
#  return ($a cmp $b);

  # This code will put captioned images above non-captioned images
  if ($an) {
    return $bn ? ($an <=> $bn) : -1;
  } else {
    return $bn ? 1 : natural_cmp($a,$b);
  }
}

sub sort_count {
  my ($opt,$data,$what) = @_;
  my $i=0;
  foreach my $bob ( @{$data->{$what}} ) {
    $data->{obj}{$bob}{num} = $i++;
    $data->{obj}{$bob}{type} = $what;
  }
}

# Sort the pictures and directories as required
sub sort_info {
  my ($opt,$data) = @_;
  @{$data->{pics}} = sort { sort_order($opt,$data,$a,$b) } @{$data->{pics}};
  @{$data->{dirs}} = sort { sort_order($opt,$data,$a,$b) } @{$data->{dirs}};
  sort_count($opt,$data,'pics');
  sort_count($opt,$data,'dirs');
}

#########################
# Figure out all the paths
#########################
# /dave/bob/joe -> (/dave/bob,joe)
sub split_path {
  my ($slash,$path) = @_;
  $slash = $slash->{slash} if ref $slash eq 'HASH';
  my $slash = $slash || '/';
  return ($slash,'') if $path eq $slash;
  my $re = "(.*)\Q$slash\E([^$\Q$slash\E}]+)\$";
  $path =~ m|$re| ? ($1 ? $1 : $slash, $2) : ('.',$path);
}

sub calc_paths {
  my ($opt,$data) = @_;

  my $dir = $data->{paths}{dir};

  # Paths to the album HTML
  $data->{paths}{album_file} = index_page($opt,1,$dir);
  $data->{paths}{album_path} = join('/',@{$data->{dir_pieces}});

  # Captions for all the dir_pieces leading up to this path.
  my @path = ();
  foreach my $dir_name ( @{$data->{dir_pieces}} ) {
    my $dir_cap = get_dir_caption($opt,join('/',@path),$dir_name);
    my $cap = $dir_cap->{name} if $dir_cap;
    my $name = clean_name($opt,$dir_name, $cap,1);
    # We only do {name} for parent_albums.  We could do {cap} as well,
    # but I don't imagine many themes would use it, and more importantly
    # we can store it via a hash because it's possible that the names of
    # parent_albums are the same.  (i.e.: /album/bob/holiday/bob/)
    push(@{$data->{paths}{parent_albums}}, $name);
    push(@path,$dir_name);
  }

  # Paths to theme files
  if ($opt->{theme}) {
    $data->{paths}{theme} = diff_path($opt,$dir,$opt->{_theme_full});
  
    # assume $opt{dir} is one level (we make that assumption in many other places)
    $data->{paths}{img_theme} = "../$data->{paths}{theme}";
  }

  # Image page URLs are <img.html> or <img.indexname.html>
  $data->{paths}{page_post_url} = $opt->{index} ? ('.'.$opt->{index}) : $opt->{html};

  # Setup obj info
  # Final obj is a hash of hashes:  full, medium, thumb
  # Each hash contains:  file, x, y and possibly filesize and path
  foreach my $pic ( @{$data->{pics}} ) {
    $data->{obj}{$pic}{full}{file} = $pic;
    $data->{obj}{$pic}{full}{path} = "$dir/$pic";
  }

  # Links to sub-albums
  if (@{$data->{dirs}}) {
    foreach my $child ( @{$data->{dirs}} ) {
      my $obj = $data->{obj}{$child};
      $obj->{dir} = $child;	# The unadulterated name
      $obj->{path} = "$dir/$child";
      $obj->{URL}{album_page}{dir} = quote(index_page($opt,0,$child),$opt,1);
    }
  }
}

##################################################
##################################################
# EPERL CODE (main code lifted out of my ePerl perl script)
##################################################
##################################################
sub eperl_set_file {
  my ($file,$line) = @_;
  #print STDERR "\n# line $line \"$file\"\n";
  print ALBUM "\n# line $line \"$file\"\n";
}

sub send_perl {
  my ($opt,$code) = @_;

  #print STDERR $code;	# For debugging eperl
  print ALBUM $code;

#  my $line_info = "";
#  if ($opt->{line_info}) {
#    my $file = get_filename($opt);
#    my $line = $opt->{lines}[0] + $opt->{offset}[0];
#    $line_info = "\n# line $line \"$file\"\n";
#    $opt->{line_info} = 0;
#  }
#  print ALBUM $line_info.$code;
}

sub send_perl_code {
  my ($opt,$code,$just_entered,$leaving) = @_;

  # Add final ';' unless ending with _
  $code = ($code =~ /_$/) ? $` : "$code;" if ($leaving);

  # <:=$var:>
  $code = "print $'" if ($just_entered && $code =~ /^=/);

  send_perl($opt,$code);
}

sub eperl_quote {
  my ($str) = @_;

  # Fix quoting/slashes
  $str =~ s/\\/\\\\/g;
  $str =~ s/'/\\'/g;

  "'$str'";
}

# Convert plaintext to perl code (print statement)
sub send_perl_text {
  my ($opt,$str,$entering,$just_left) = @_;

  my $nl;  $nl = 1 if (chomp($str));
  my $line_continue = 0;

  # <: perl :>//  Text here is ignored
  return send_perl($opt,"\n") if ($just_left && $str =~ m|^//|);

  # Line continuation with \<CR>
  if ($str =~ /\\$/) {
    $line_continue = 1;
    $str = $`;
  }

  if ($str ne "") {
    $str=eperl_quote($str);
    $str.=',"\n"' if ($nl && !$line_continue);
  } else {
    return unless $nl;
    $str = '"\n"';
  }
  
  $str = "print $str;";
  $str.="\n" if $nl;
  send_perl($opt,$str);
}

sub eperl {
  my $opt = shift @_; my @lines = @_;

  my $in_perl = 0;
  my ($just_entered,$just_left) = (0,0);

  my $line = 0;

  undef $_;
  while ($line <= $#lines+1) {

## This code screwed up line numbers by skipping empty comments.
## And on top of that, it printed out newlines in place of comments.  Bah.
#    while (!defined $_) {
#      $_ = $lines[$line++];
#      last unless defined $_;
#      if (/^#c/) {	# Comments
#        $_ = (m|//$|) ? (undef) : "c\n";
#      }
#    }

    $_ = $lines[$line++] unless defined $_;
    # My cheap eperl album comments '#c...'
    if (!$in_perl && /^#c/) {
      send_perl_code($opt,"\n");
      undef $_;
    } elsif (!$in_perl && /$opt->{enter_eperl}/) {
      $in_perl = 1;
      my ($out,$rest) = ($`,$');
      send_perl_text($opt,$out,1,$just_left);
      $just_entered = 1; $just_left = 0;
      $_ = $rest;
    } elsif ($in_perl && /$opt->{leave_eperl}/) {
      $in_perl = 0;
      my ($in,$rest) = ($`,$');
      send_perl_code($opt,$in,$just_entered,1);
      $just_entered = 0; $just_left = 1;
      $_ = $rest;
    } elsif ($in_perl) {
      send_perl_code($opt,$_,$just_entered,0);
      $just_entered = 0; $just_left = 0;
      undef $_;
    } else {
      send_perl_text($opt,$_,1,$just_left);
      $just_entered = 0; $just_left = 0;
      undef $_;
    }
  }
  print STDERR "[$PROGNAME] Warning: Never left perl code [$opt->{'album.th'}, $line]\n"
    if $in_perl;
}


##################################################
##################################################
# DATA -> EPERL / THEME SUPPORT ROUTINES
##################################################
##################################################

#########################
# Simple Data dumper
# Doesn't handle multiple pointers to objects (such as recursive pointers)
# (But fails at least!)
# If needed we could create a new object for each ref and add it to the top
# and use reference pointers..  i.e. "my $_dumper_HASH::82332 = { ..."
# But that's ugly.
# I suppose I could keep track of the full path and use references, such
# as ($data->{prev}{object}{stored}{here}) but I'm not sure if that works
# in initialization data.
#########################
sub dumper {
  my ($dmp,$name,$var) = @_;

  return if $name =~ /$dmp->{ignore}/;

  my $ref = ref($var);
  return hash_warn($dmp->{opt},"Option or data [$name] was used multiple times?\n\t(Internal consistency problem - probably with a module?)")
    if $ref && $dmp->{saw}{$var}++;

  return hash_warn($dmp->{opt},"Variable with no name?") unless $dmp->{lvl} || defined $name;
  my $s = " "x$dmp->{lvl};
  my $qname = defined $name ? dumper_quote($name)." => " : "";

  my $start = $dmp->{lvl} ? $s.$qname : "\$$name = ";
  push(@{$dmp->{dump}}, $start);

  dumper_array($dmp,$var) if ($ref eq 'ARRAY');
  dumper_hash($dmp,$var) if ($ref eq 'HASH');
  # Assume plain old scalar (not a scalar ref!) otherwise
  dumper_value($dmp,$var) if ($ref ne 'ARRAY' && $ref ne 'HASH');

  my $end = ($dmp->{lvl} ? ",\n" : ";\n");
  push(@{$dmp->{dump}}, $end);
}

sub dumper_value {
  my ($dmp,$val) = @_;
  push(@{$dmp->{dump}}, dumper_quote($val));
}

sub dumper_quote {
  my ($str) = @_;
  defined $str ? eperl_quote($str) : 'undef';
}

sub dumper_array {
  my ($dmp,$array) = @_;

  return push(@{$dmp->{dump}}, "[]") unless @$array;

  push(@{$dmp->{dump}}, "[\n");
  $dmp->{lvl}+=2;
  map(dumper($dmp,undef,$_), @$array);
  $dmp->{lvl}-=2;
  push(@{$dmp->{dump}}, " "x$dmp->{lvl} . "]");
}

sub dumper_hash {
  my ($dmp,$hash) = @_;

  return push(@{$dmp->{dump}}, "{}") unless %$hash;
  push(@{$dmp->{dump}}, "{\n");
  $dmp->{lvl}+=2;
  map(dumper($dmp,$_,$hash->{$_}), keys %$hash);
  $dmp->{lvl}-=2;
  push(@{$dmp->{dump}}, " "x$dmp->{lvl} . "}");
}

#########################
# Convert to eperl
#########################
# Put the 'data' structure into an input string for eperl,
# and throw in some access functions
# (Writing perl with perl is a bitch!  Quoting nightmare!)
sub data_to_eperl {
  my ($opt,$data) = @_;

  # Data dump.  Convert $data to assignment statements
  my %dmp;
  $dmp{opt} = $opt;	# opt handle

  # Ignore the eperl delimiters, 0 vars and '_internal' variables
  $dmp{ignore} = '^(_.*|enter_eperl|leave_eperl|0)$';
  dumper(\%dmp,'data',$data);
  dumper(\%dmp,'opt',$opt);

# For debug - dump data structures
#print; foreach ( @{$dmp{dump}} ) { print; } exit;

  $data->{eperl} = $dmp{dump};
  unshift(@{$data->{eperl}},"<:\n");
  push(@{$data->{eperl}},":>//\n");

##################################################
##################################################
# Album support routines!
##################################################
##################################################

  # Position of this line relative to start of SUPPORT is crucial
  my $start_line = __LINE__ + 4;
  # Think you're clever, eh?  Okay, so you found it.
  # Please leave this requirement in, it's the only way I get paid..
  my $derc = pack('C*',qw(67 65 76 76 69 68 95 67 82 69 68 73 84));
  my $dercerr = pack('C*',qw(100 105 100 110 39 116 32 99 97 108 108 32 67 114 101 100 105 116 40 41 33));

  push(@{$data->{eperl}},<<SUPPORT);
<:
# line $start_line "$0"

# These are changed by write_img_themes
my \$IMAGE_PAGE = 0;
my \$PAGE_TYPE = 'album_page';
my \$THIS_IMAGE = 0;	# For image pages

# Get any of the command line options
sub Option { \$opt->{\$_[0]}; }

#########################
# Paths
#########################
sub Path {
  my (\$path) = \@_;

  return \$data->{paths}{parent_albums}[-1] if \$path eq 'album_name';
  \$data->{paths}{\$path};
}

sub Image_Page() { \$IMAGE_PAGE; }
sub Page_Type() { \$IMAGE_PAGE ? 'image_page' : 'album_page' }
sub Theme_Path() { Image_Page() ? Path('img_theme') : Path('theme'); }
sub Theme_URL() { \$opt->{theme_url} ? "\$opt->{theme_url}/\$opt->{theme}" : Theme_Path(); }

sub read_file {
  my (\$f) = \@_;
  return '' unless \$f;
  return '' unless (-r \$f);
  return '' unless (open(FILE,"<\$f"));
  my \@contents;
  while(<FILE>) { push(\@contents,\$_); }
  close FILE;
  return \@contents;
}

sub quote { my (\$s) = \@_; \$s =~ s/"/&quot;/g;  '"'.\$s.'"'; }

# Header/Footer
sub isHeader { return (-r "\$data->{paths}{dir}/\$opt->{header}") ? 1 : 0; }
sub pHeader {
  print "<!--HEADER name=".quote("HEADER:\$opt->{header}")."-->\\n" if \$opt->{caption_edit};
  pFile("\$data->{paths}{dir}/\$opt->{header}");
  print "<!--END_HEADER-->\\n" if \$opt->{caption_edit};
}
sub isFooter { return (-r "\$data->{paths}{dir}/\$opt->{footer}") ? 1 : 0; }
sub pFooter {
  print "<!--FOOTER name=".quote("FOOTER:\$opt->{footer}")."-->\\n" if \$opt->{caption_edit};
  pFile("\$data->{paths}{dir}/\$opt->{footer}");
  print "<!--END_FOOTER-->\\n" if \$opt->{caption_edit};
}

#########################
# Object Iterators
#########################
sub num {
  my (\$type) = \@_;
  \$type='pics' unless \$type;
  return \$#{Path('parent_albums')}+1 if \$type eq 'parent_albums';
  \$#{\$data->{\$type}}+1;
}

# OBSOLETE:  Global counter variables
my \$IMAGE_CNT;	# DEPRECATED
my \$CHILD_ALBUM_CNT = 0;
my \$PARENT_ALBUM_CNT = 0;
# END OBSOLETE:  Global counter variables

# There are three types of arguments for our image subs:
# 1) No argument:  Use current image (IMAGE_CNT)
# 2) Number arg:   Use image #num
# 2) obj arg:  Use that obj
sub get_obj {
  my (\$pic,\$type,\$loop) = \@_;

  return \$pic if ref \$pic eq 'HASH';

  # DEPRECATED:
  \$pic = defined \$pic ? \$pic :
    (\$type eq 'dirs' ? \$CHILD_ALBUM_CNT : \$IMAGE_CNT);	# DEPRECATED

  return undef if \$pic<0 && !\$loop;
  \$pic=0 if \$loop && \$pic>=num(\$type);
  \$type='pics' unless \$type;
  my \$img = \$data->{\$type}[\$pic];
  return undef unless \$img;
  \$obj = \$data->{obj}{\$img};
}

sub First { get_obj(0,\$_[0]); }

sub Last { get_obj(-1,\$_[0],1); }

sub Next {
  my (\$what,\$loop) = \@_;
  my \$obj = get_obj(\$what);
  get_obj(\$obj->{num}+1, \$obj->{type}, \$loop);
}

sub Prev {
  my (\$what,\$loop) = \@_;
  my \$obj = get_obj(\$what);
  get_obj(\$obj->{num}-1, \$obj->{type}, \$loop);
}

#########################
# Parent albums
#########################
sub Parent_Album {
  my (\$num) = \@_;
  \$num = \$PARENT_ALBUM_CNT unless defined \$num;	# DEPRECATED
  my \$parent_albums = Path('parent_albums');
  return "" if \$num >= num('parent_albums');
# You know..  This should be done in album and saved as "parent_album_urls"..
  my \$dotdots = num('parent_albums') - \$num - 1;
  \$dotdots++ if Image_Page();
  my \$str = "";
  \$str .= "<a href='". ("../"x\$dotdots). Option('index'). "'>" if \$dotdots;
  \$str .= \$parent_albums->[\$num];
  \$str .= "</a>" if \$dotdots;
  \$str;
}

# Return an array or join of all the calls to Parent_Album
sub Parent_Albums {
  my (\$join) = \@_;
  my \@pa = map Parent_Album(\$_), 0..(num('parent_albums')-1);
  \$join ? join(\$join, \@pa) : \@pa;
}

# Go back to a previous index, or just ".." for the top page of the album
sub Back {
  (num('parent_albums')>1 || Image_Page()) ?
    "'../".Option('index')."'" : "'".Option('top')."'";
}

#########################
# Images
#########################
# The image number for an image page
sub This_Image() { get_obj(\$THIS_IMAGE); }

# <img> tags for medium/full/thumb images
# Call as:  Image(<image>,<type>)
# i.e.:  Image(4,'thumb') or Image(\$img,'full'), etc..
sub Image {
  my (\$pic,\$type) = \@_;
  my \$obj = get_obj(\$pic);

  # Medium or full if not thumb?
# TODO: Shouldn't this medium/full nonsense be a new: \$obj->{page}{...}
  \$type = Get(\$obj,'medium','x') ? 'medium' : 'full' if \$type ne 'thumb';
  return '' unless \$obj->{\$type}{x};

  my \$src = \$type eq 'thumb' ?
    \$obj->{URL}{Page_Type()}{thumb} :
    \$obj->{URL}{image_page}{image_src};

  my \$tag = \$type eq 'thumb' ? 'img' : Get(\$obj,'full','tag');
  my \$y = \$obj->{\$type}{y};
  my \$embed_movie = (\$obj->{is_movie} && \$type ne 'thumb') ? 1 : 0;
  \$y += 15 if \$embed_movie && \$y;

  my \$str;
  \$str .= "<\$tag src=\$src border='0'";
  \$str .= " alt=\$obj->{alt}" if \$obj->{alt};
  \$str .= " width='\$obj->{\$type}{x}'" if \$obj->{\$type}{x};
  \$str .= " height='\$y'" if \$y;
  \$str .= " />";
  \$str;
}

# Get a field/subfield property of an object.
# Can also get URLs, with the "from" portion set to Page_Type if not specified
# If field is 'href' - then return the URL wrapped in <a href=..>
sub Get {
  my (\$pic,\$field,\$subfield,\$subsub) = \@_;
  my \$obj = get_obj(\$pic);

  return Name(\$pic) if \$field eq 'Name';
  return Caption(\$pic) if \$field eq 'Caption';
  return '<a href='.Get(\$pic,'URL',\$subfield,\$subsub).'>'
    if \$field eq 'href';

  if (\$field eq 'URL') {
    return \$obj->{URL}{\$subfield}{\$subsub} if \$subsub;
    \$this_page = (Image_Page() ? 'image_page' : 'album_page');
return \$obj->{URL}{Page_Type()}{\$subfield};
}
  \$subfield ? \$obj->{\$field}{\$subfield} : \$obj->{\$field};
}

# We can chop down extra long image names on the album page if needed
sub Name {
  my \$obj = get_obj(\@_);
  my \$n = \$obj->{name};
  return "Back" unless \$n;
  return \$n if Image_Page() && !\$MAIN::FIRST_NAME_CLEAN++;	#Kludge!
  my \$shorten = !Image_Page() && $opt->{name_length} && length(\$n)>($opt->{name_length}+3);
  # Don't shorten if name has HTML (kludge..)
  \$shorten = 0 if \$n =~ /<(a|font|br)\\b.*>/;
  my \$s = !\$shorten ? \$n :
    substr(\$n,0,$opt->{name_length}/2) . "..." . substr(\$n,-$opt->{name_length}/2,$opt->{name_length}/2);
  return \$s if !$opt->{caption_edit};
  \$n = quote(\$n);
  my \$name = quote("NAME\$obj->{num}:".\$obj->{full}{file});
  \$s =~ s/\\n//mg;
  "<!--IMAGE_NAME name=\$name value=\$n-->\$s<!--END_IMAGE_NAME-->";
}
sub Caption {
  my \$obj = get_obj(\@_);
  my \$norm_caps = ($opt->{album_captions} || Image_Page()) ? 1 : 0;
  my \$cap = "";
  \$cap .= "<!--IMAGE_CAPTION name=".quote("CAPTION:".\$obj->{full}{file})."-->\\n"
    if $opt->{caption_edit};
  \$cap .= \$obj->{cap} if \$norm_caps;
  \$cap .= \$obj->{exif} if \$norm_caps;
  \$cap .= Image_Page() ? \$obj->{exif_image} :  \$obj->{exif_album};
  \$cap .= join('',read_file(\$obj->{capfile})) if \$norm_caps;
  \$cap .= "<!--END_IMAGE_CAPTION-->\\n" if $opt->{caption_edit};
  \$cap;
}

##################################################
# OBSOLETE/DEPRECATED SUPPORT ROUTINES!
##################################################
# I've reduced the number of functions that
# need to be called (and remembered).
# These old routines may be removed soon.

# Paths
sub pAlbum_Name { Path('album_name'); }
sub Album_Filename { Path('album_file'); }
sub pFile { print read_file(\@_); }
sub Get_Opt { Option(\@_); }
sub Index { Option('index'); }

# Parents
sub pParent_Album { print Parent_Album(\@_); }
sub Parent_Albums_Left { num('parent_albums') - \$PARENT_ALBUM_CNT; }
sub Parent_Album_Cnt { \$PARENT_ALBUM_CNT+1; }
sub Next_Parent_Album { \$PARENT_ALBUM_CNT++; }
sub pJoin_Parent_Albums { print Parent_Albums(\@_); }

# Images
sub pImage {
  return undef unless Images();
  print "<a href=".Get(undef,'URL','image').">";
  print Image(undef,'thumb') if (Get(undef,'thumb'));
  if (!defined \$_[0] || \$_[0]) {
    print "<br />\\n";
    print Name();
  }
  print "</a>";
}
sub pImage_Src { print Image(\@_,'full'); }
sub Image_Src { Image(\@_,'full'); }
sub pImage_Thumb_Src { print Image(\@_,'thumb'); }
sub Image_Name { Name(\@_); }
sub Image_Caption { Caption(\@_); }
sub Image_Thumb { Get(\$_[0],'URL','thumb'); }
sub Image_Is_Pic { Get(\$_[0],'thumb'); }
sub Image_Alt { Get(\$_[0],'alt'); }
sub Image_Filesize { Get(\$_[0],'full','filesize'); }
sub Image_Path { Get(\$_[0],'full','path'); }
sub Image_Width { Get(\$_[0],'medium','x') || Get(\$_[0],'full','x'); }
sub Image_Height { Get(\$_[0],'medium','y') || Get(\$_[0],'full','y'); }
sub Image_Filename { Get(\$_[0],'full','file'); }
sub Image_Tag { Get(\$_[0],'full','tag'); }
sub Image_URL { Get(\$_[0],'URL','image'); }

# In the image page we use the real URL (back one dir)
# Otherwise we use the url to the image page (or just the image)
sub Image_Page_URL { Get(\$_[0],'URL','image_page','image_page') || Back(); }
sub pImage_Caption { print Get(\$_[0],'Caption'); }

#########################
# Deprecated Child Album routines
# These are ugly.  Please don't use them
#########################
sub Child_Album {
  my (\$num,\$nobr) = \@_;
  my \$obj = get_obj(\$num,'dirs');
  my \$name = Name(\$obj);
  \$name =~ s/<br>//g if \$nobr;
  my \$url = Get(\$obj,'URL','dir');
  "<a href=\$url>\$name</a>";
}
sub pChild_Album { print Child_Album(undef,\@_); }	# Stupid undef kludge here..
sub Child_Album_Caption { Caption(\$_[0],'dirs'); }
sub pChild_Album_Caption { print Child_Album_Caption(\@_); }
sub Child_Album_URL { Get(get_obj(\$_[0],'dirs'),'URL','dir'); }
sub Child_Album_Name { Name(get_obj(\$_[0],'dirs')); }

#########################
# Just don't work..
#########################
# References to unused data structures, such as:
# \@PARENT_ALBUMS, \$#PARENT_ALBUMS, \$PARENT_ALBUMS[...], etc..
# Use:  \@PARENT_ALBUMS = \@{Path('parent_albums')};
# \@CHILD_ALBUMS, \@CHILD_ALBUM_NAMES, \@CHILD_ALBUM_URLS, ...

##################################################
# END OF OBSOLETE/DEPRECATED SUPPORT ROUTINES!
##################################################

##################################################
# DEPRECATED GLOBAL VARIABLE METHODS
##################################################
# These are less likely to be removed anytime soon,
# I haven't decided if these are a good way to access the info.
my \$picdata;
sub Images { (\$IMAGE_CNT < num('pics')) ? 1 : 0; }
sub Image_Cnt { Get(\$_[0],'num')+1; }
sub Images_Left { num('pics') - \$IMAGE_CNT; }
sub Next_Image { Set_Image(\$IMAGE_CNT+1); }
Set_Image(0);

sub Image_Prev { \$THIS_IMAGE ? \$THIS_IMAGE-1 : $opt->{image_loop} ? \$#{\$data->{pics}} : \$#{\$data->{pics}}+1; }
sub Image_Next { \$THIS_IMAGE!=\$#{\$data->{pics}} ? \$THIS_IMAGE+1 : $opt->{image_loop} ? 0 : \$#{\$data->{pics}}+1; }
sub Set_Image {
  my (\$to) = \@_;
  \$IMAGE_CNT = \$to;
  my \$img = \$data->{pics}[\$to];
  \$picdata = \$img ? \$data->{obj}{\$img} : undef;
}
sub Set_Image_Prev { Set_Image(Image_Prev()); }
sub Set_Image_Next { Set_Image(Image_Next()); }
sub Set_Image_This { Set_Image(\$THIS_IMAGE); }

# Child albums
sub Child_Albums { (\$CHILD_ALBUM_CNT < num('dirs')) ? 1 : 0; }
sub Child_Album_Cnt { \$CHILD_ALBUM_CNT+1; }
sub Child_Albums_Left { num('dirs') - \$CHILD_ALBUM_CNT; }
sub Next_Child_Album { \$CHILD_ALBUM_CNT++; }

##################################################
# END OF DEPRECATED GLOBAL VARIABLE METHODS
##################################################

##################################################
# BORDERS (handle corners)
##################################################
# Show an image given an 'image array'
sub Image_Array {
  my (\$src,\$x,\$y,\$and,\$alt) = \@_;
  return '' unless \$src;
  my \$str = "<img src='\$src'";
  \$str .= " width='\$x'" if \$x;
  \$str .= " height='\$y'" if \$y;
  \$str .= " \$and" if \$and;
  \$str .= " alt='\$alt'" if \$alt;
  \$str .= " border='0' />";
  \$str;
}

# Borders can be 0 pieces, 4 pieces, 8 or 12
# Called starting from top (left) going clockwise.
#   12 piece borders     
#      TL  T  TR          8 piece borders       4 piece borders
#      LT     RT            TL  T  TR            TTTTTTT
#      L  IMG  R            L  IMG  R            L IMG R
#      LB     RB            BL  B  BR            BBBBBBB
#      BL  B  BR
sub Border {
  # Either call with:
  #   (img_object, type, href, border_image_arrays)
  #   (img_string, x,y, border_image_arrays)

  my \$img = shift \@_;
  my (\$x,\$y);

  if (ref \$img ne 'HASH') {
    # Called with (img_string,x,y, ..)
    \$x = shift \@_;
    \$y = shift \@_;
  } else {
    # Called with (img_object,type,href, ..)
    my \$type = shift \@_;
    my \$href = shift \@_;
    # Medium or full if not thumb?
    \$type = Get(\$img,'medium','x') ? 'medium' : 'full' if \$type ne 'thumb';
    # Lookup href
    \$href = Get(\$img,'href',\$href) if \$href;
    \$x = Get(\$img,\$type,'x');
    \$y = Get(\$img,\$type,'y');
    my \$str = Image(\$img,\$type);
    # Don't put anchor around movies, some browsers will follow href when
    # they press play on the embedded player controls
    my \$embed_movie = (\$img->{is_movie} && \$type ne 'thumb') ? 1 : 0;
    \$str = \$href.\$str.'</a>' if \$href && !\$embed_movie;
    # And leave space for embedded player controls
    \$y+=15 if \$embed_movie;
    \$img = \$str;
  }

  my \$width=1; my \$height=2;

  if (scalar \@_ == 0) {
    # No corners
    print "\$img<br />\n";
  } elsif (scalar \@_ == 4) {
    my (\$T,\$R,\$B,\$L) = \@_;

    # Stretch top,bottom to fit image
    my \@t = \@\$T; my \@b = \@\$B;
    my \@l = \@\$L; my \@r = \@\$R;
    \$t[\$width] = \$x+\$L->[\$width]+\$R->[\$width];
    \$b[\$width] = \$x+\$L->[\$width]+\$R->[\$width];
    \$l[\$height] = \$y;
    \$r[\$height] = \$y;

    my (\$t,\$r,\$b,\$l) =
      map( Image_Array(\@\$_), (\\\@t,\\\@r,\\\@b,\\\@l));

    print <<BORDER4;
					<span style='white-space: nowrap'>\$t</span><br />
					<span style='white-space: nowrap'>\$l\$img\$r</span><br />
					<span style='white-space: nowrap'>\$b</span><br />
BORDER4
  } elsif (scalar \@_ == 8) {
    my (\$TL,\$T,\$TR,\$R,\$BR,\$B,\$BL,\$L) = \@_;

    # Stretch top,bottom to fit image
    my \@t = \@\$T; my \@b = \@\$B;
    my \@l = \@\$L; my \@r = \@\$R;
    # Just *try* to read this mess...
    \$t[\$width] = \$x+\$L->[\$width]+\$R->[\$width]-\$TL->[\$width]-\$TR->[\$width];
    \$b[\$width] = \$x+\$L->[\$width]+\$R->[\$width]-\$BL->[\$width]-\$BR->[\$width];
    \$l[\$height] = \$y;
    \$r[\$height] = \$y;

    my (\$tl,\$t,\$tr,\$r,\$br,\$b,\$bl,\$l) =
      map( Image_Array(\@\$_), 
        (\$TL,\\\@t,\$TR,\\\@r,\$BR,\\\@b,\$BL,\\\@l));

    print <<BORDER8;
					<span style='white-space: nowrap'>\$tl\$t\$tr</span><br />
					<span style='white-space: nowrap'>\$l\$img\$r</span><br />
					<span style='white-space: nowrap'>\$bl\$b\$br</span><br />
BORDER8
  } elsif (scalar \@_ == 12) {
    my (\$TL,\$T,\$TR,\$RT,\$R,\$RB,\$BR,\$B,\$BL,\$LB,\$L,\$LT) = \@_;

    # We might be calling without the need for 12 border pieces
    my \$border = (\@\$T || \@\$R || \@\$B || \@\$L) ? 1 : 0;
    my \$corners8 = (\@\$TR || \@\$TL || \@\$BL || \@\$BR) ? 1 : 0;
    my \$corners12 = (\@\$RT || \@\$RB || \@\$LB || \@\$LT) ? 1 : 0;
    return Border(\$img,\$x,\$y,\$TL,\$T,\$TR,\$R,\$BR,\$B,\$BL,\$L)
      if \$corners8 && !\$corners12;
    return Border(\$img,\$x,\$y,\$T,\$R,\$B,\$L) if \$border && !\$corners12;
    return Border(\$img,\$x,\$y) if !\$border && !\$corners12;

    # Stretch top,bottom,sides to fit image
    my \@t = \@\$T; my \@b = \@\$B;
    my \@l = \@\$L; my \@r = \@\$R;
    # Just *try* to read this mess...
    \$t[\$width] = \$x+\$L->[\$width]+\$R->[\$width]-\$TL->[\$width]-\$TR->[\$width];
    \$b[\$width] = \$x+\$L->[\$width]+\$R->[\$width]-\$BL->[\$width]-\$BR->[\$width];
    \$l[\$height] = \$y-\$LT->[\$height]-\$LB->[\$height];
    \$r[\$height] = \$y-\$RT->[\$height]-\$RB->[\$height];

    my (\$tl,\$t,\$tr,\$rt,\$r,\$rb,\$br,\$b,\$bl,\$lb,\$l,\$lt) =
      map( Image_Array(\@\$_), 
        (\$TL,\\\@t,\$TR,\$RT,\\\@r,\$RB,\$BR,\\\@b,\$BL,\$LB,\\\@l,\$LT));

    # Embedded stuff is aligned top in case there are controls, otherwise
    # align middle in case the thumbnail is very small and there's blank space.
    my \$align = \$img =~ /<embed/ ? 'top' : 'middle';
    print <<BORDER12;
					<table border='0' cellpadding='0' cellspacing='0'>
						<tr>
							<td colspan='3'>\$tl\$t\$tr</td>
						</tr> <tr>
							<td>\$lt</td>
							<td rowspan='3' valign=\$align>\$img</td>
							<td>\$rt</td>
						</tr> <tr>
							<td>\$l</td>
							<td>\$r</td>
						</tr> <tr>
							<td>\$lb</td>
							<td>\$rb</td>
						</tr> <tr>
							<td colspan='3'>\$bl\$b\$br</td>
						</tr>
					</table>
BORDER12

  } else {
    print STDERR "[",Theme_Path(),"] Error: Border called with wrong number of args\n";
  }
}

##################################################
# END CODE
##################################################
srand(time^\$\$);
sub Credit {
  my \@start = (
  "Photo album generated by",
  "Created with the tool",
  "Album created by",
  );
  my \$start = \$start[int(rand(\$#start+1))];

  my \@album = (
  "<a href='$ALBUM_URL'>album</a>",
  "<a href='$ALBUM_URL'>album</a>",
  "<a href='$ALBUM_URL'>album generator</a>",
  "<a href='$ALBUM_URL'>album generator</a>",
  "<a href='$ALBUM_URL'>album tool</a>",
  "album <a href='$ALBUM_URL'>tool</a>",
  "album <a href='$ALBUM_URL'>script</a>",
  "<a href='$ALBUM_URL'>photo album generator</a>",
  );
  my \$album = \$album[int(rand(\$#album+1))];

  my \@me = ("Dave","David Ljung","David Madison","D. Madison","David","Dave Madison");
  my \$me = "<a href='http://GetDave.com/'>";
  my \@mh = ("MarginalHack","Marginal Hack");
  my \$mh = "<a href='$HOME'>";
  my \@tool = (\@mh,"Marginal Hack","tool","free tool","script");
  \@tool = grep(!/tool/, \@tool) if \$album =~ /tool/;	# tool tool tool tool..
  my \$tool = "<a href='$HOME'>";
  my \@formats = ("from {me}'s</a> {mh}s</a>", "from {mh}s</a> by {me}</a>", "a {tool}</a> by {me}</a>", "a {tool}</a> written by {me}</a>");
  my \$from = \$formats[int(rand(\$#formats+1))];
  \$from =~ s/{me}/\$me.\$me[int(rand(\$#me+1))]/e;
  \$from =~ s/{mh}/\$mh.\$mh[int(rand(\$#mh+1))]/e;
  \$from =~ s/{tool}/\$tool.\$tool[int(rand(\$#tool+1))]/e;

  print "\$start \$album \$from";
  \$${derc}=1;
}

# Meta tag needed for regenerating portions of the album tree.
sub Meta {
  print "<meta name='Generator' content='$GEN_STRING' />\\n";
  print "<meta name='Album_Path' content='\$data->{paths}{album_path}' />\\n";
# DEPRECATED: These two are unnecessary unless you want to switch back to v2.0
# (But see prev_next_theme_path_changed)
  print "<meta name='Album_Theme' content='\$opt->{theme}' />\\n";
  print "<meta name='caption_edit' content='yes' />\\n" if $opt->{caption_edit};
  if (Image_Page()) {
    my \$Prev = Prev(This_Image, \$opt->{image_loop});
    my \$Prev_pic = Get(\$Prev,'full','file');
    my \$Prev_url = Get(\$Prev,'URL','image_page','image_page');
    my \$Prev_src = Get(\$Prev,'URL','image_page','image_src') || "''";
    my \$Next = Next(This_Image, \$opt->{image_loop});
    my \$Next_pic = Get(\$Next,'full','file');
    my \$Next_url = Get(\$Next,'URL','image_page','image_page');
    my \$Next_src = Get(\$Next,'URL','image_page','image_src') || "''";
    print <<PREV_NEXT;
<meta name='Prev_Image' content='\$Prev_pic' />
<meta name='Next_Image' content='\$Next_pic' />
<link rel='prev' href=\$Prev_url />
<link rel='next' href=\$Next_url />
<link rel='up' href='..' />
<script type='text/javascript'>
<!--
if (document.images) {
  Image1 = new Image(); Image1.src = \$Prev_src;
  Image2 = new Image(); Image2.src = \$Next_src;
}
//-->
</script>

PREV_NEXT
  }
  \$CALLED_META=1;
}
sub Album_End {
  die("ERROR: Didn't call Meta() in <head>!\\n") unless \$CALLED_META;
  # Please leave this here.  It's the only way I get paid.
  die("ERROR: Theme $dercerr\\n") if (!\$CALLED_CREDIT && !Image_Page());
}

:>//
SUPPORT
##################################################
##################################################
# End album support routines
##################################################
##################################################

  push(@{$data->{end_eperl}},"<:Album_End():>");
}

##################################################
##################################################
# HTML I/O
##################################################
##################################################
sub setup_output {
  my ($opt,$out,$theme) = @_;

  if ($theme) {
    # We pipe into eperl stdin
    my $qout = file_quote($opt,$out);
    (open(ALBUM,"|$^X > $qout")) ||
      fatal($opt,"Couldn't start perl pipe for theme [$out]\n");
  } else {
    # Just write a file
    (open(ALBUM,">$out")) ||
      fatal($opt,"Couldn't write html [$out]\n");
  }
}

sub close_output {
  my ($opt,$theme) = @_;
  close(ALBUM);
  my $ret = $?;
  return unless $theme;
  return unless $ret;

  print STDERR "[$PROGNAME] album theme returned error [$?]\n" if ($?);
  fatal($opt);
}

##################################################
##################################################
# Default HTML (no ePerl)
##################################################
##################################################
sub header {
  my ($opt,$data,$image_page,$name) = @_;

  my $dir = $data->{paths}{dir};

  my @names = @{$data->{paths}{parent_albums}};
  push(@names,$name) if $name;
  my $top = (@names || $image_page) ? 0 : 1;

  my $this = pop(@names);
  my $header = "";
  my $back = $#names;
  while (my $n = pop(@names)) {
    $header = "<a href='".("../"x($back-$#names))."$opt->{index}'>$n</a> : $header";
  }
  $header.=$this;

  my $Up = $image_page ? "Back" : "Up";
  my $UpUrl = $top ? $opt->{top} : "../$opt->{index}";
  $UpUrl = "<h1><a href='$UpUrl'>$Up</a></h1>" if $UpUrl && $UpUrl ne "''";

  print ALBUM <<END_OF_HEADER;
<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN'
    'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'>
<html xmlns='http://www.w3.org/1999/xhtml'>
  <head>
    <title> 
      Album: $this
    </title>
    <meta http-equiv='Content-Type' content='text/html; charset=$opt->{charset}' />
    <meta name='Generator' content='$GEN_STRING' />
    <meta name='Album_Path' content='$data->{paths}{album_path}' />
  </head>
  $opt->{body}
  <table width='95%'>
    <tr>
      <td align='left'>
        <h2>$header</h2>
      </td>
      <td align='right'>
        $UpUrl
      </td>
    </tr>
  </table>
  <hr />
END_OF_HEADER

  if (-f "$dir/$opt->{header}" && (open(HEADER,"<$dir/$opt->{header}"))) {
    while(<HEADER>) { print ALBUM; }
    print ALBUM "<hr />\n";
  }
}

sub footer {
  my ($opt,$data) = @_;

  my $dir = $data->{paths}{dir};

  if (-f "$dir/$opt->{footer}" && (open(FOOTER,"<$dir/$opt->{footer}"))) {
    while(<FOOTER>) { print ALBUM; }
    print ALBUM "<hr />\n";
  }
  my $date = localtime;
  print ALBUM <<END_OF_FOOTER;
    <font size='-1'>
      Photo album generated by
      <a href='$ALBUM_URL'>$PROGNAME</a>
      from <a href='http://GetDave.com/'>Dave's</a>
      <a href='$HOME'>MarginalHacks</a>
      on $date
    </font>
  </body>
</html>
END_OF_FOOTER

}

#########################
# Table stuff
#########################
my $TABLE_COUNT;
sub start_table {
  $TABLE_COUNT = 0;
  print ALBUM "  <table cellspacing='10' width='95%'>\n";
  print ALBUM "    <tr>\n";
}

sub end_table {
  print ALBUM "       </td>\n";
  print ALBUM "    </tr>\n";
  print ALBUM "  </table>\n";
}

# Return true if we started a new row
sub new_element {
  my ($opt) = @_;
  my $new_row = 0;
  if ($TABLE_COUNT) {
    print ALBUM "      </td>\n";
    unless ($TABLE_COUNT % $opt->{columns}) {
      print ALBUM "    </tr><tr>\n";
      $new_row=1;
    }
  }
  print ALBUM "      <td align='center' ";
  print ALBUM "width='",(100/$opt->{columns}),"%' "
    if ($TABLE_COUNT < $opt->{columns});
  print ALBUM "valign='top'>\n";
  $TABLE_COUNT++;
}

#########################
# Index page
#########################
sub caption {
  my ($obj,$image_page) = @_;
  print ALBUM $obj->{cap};
  print ALBUM $obj->{exif};
  print ALBUM $image_page ? $obj->{exif_image} : $obj->{exif_album};
  if (-f $obj->{capfile} && (open(CAP,"<$obj->{capfile}"))) {
    while(<CAP>) { print ALBUM; }
    close CAP;
  }
}

sub write_index {
  my ($opt,$data) = @_;

  my $dir = $data->{paths}{dir};

  # TOP
  setup_output($opt,$data->{paths}{album_file});
  header($opt,$data,0);

  # DIRECTORIES
  if (@{$data->{dirs}}) {
    start_table();
    new_element($opt);
    print ALBUM "<font size='+2'><i>More albums:</i></font>\n";

    foreach my $child ( @{$data->{dirs}} ) {
      new_element($opt);
      my $obj = $data->{obj}{$child};
      if ($obj->{thumb}) {
        print ALBUM "<a href=$obj->{URL}{album_page}{dir}>\n";
        print ALBUM "          <img";
        print ALBUM " width='$obj->{thumb}{x}'" if $obj->{thumb}{x};
        print ALBUM " height='$obj->{thumb}{y}'" if $obj->{thumb}{y};
        print ALBUM " border='0' src=$obj->{URL}{album_page}{thumb}";
        print ALBUM " alt=$obj->{alt} /><br />\n";
        print ALBUM "<font size='+1'>$obj->{name}</font></a>\n";
        print ALBUM "<font size='-1'><br \>$obj->{num_pics} images</font>\n"
          if defined $obj->{num_pics};
      } else {
        print ALBUM "<font size='+1'><a href=$obj->{URL}{album_page}{dir}>$obj->{name}</a></font>\n";
      }
    }
    end_table();
    print ALBUM "<hr />\n";
  }

  # IMAGES
  start_table();
  foreach my $pic ( @{$data->{pics}} ) {
    new_element($opt);
    my $obj = $data->{obj}{$pic};
    my $name = $obj->{name};
    my $pname = $name;
    $pname = substr($name,0,$opt->{name_length}/2) . "..." . substr($name,-$opt->{name_length}/2,$opt->{name_length}/2)
      if ($opt->{name_length} && length($name)>($opt->{name_length}+3));

    # Picture - thumbnail and all..
    if ($obj->{thumb}) {
      print ALBUM "        <a href=$obj->{URL}{album_page}{image}>\n";
      print ALBUM "          <img";
      print ALBUM " width='$obj->{thumb}{x}'" if $obj->{thumb}{x};
      print ALBUM " height='$obj->{thumb}{y}'" if $obj->{thumb}{y};
      print ALBUM " border='0' src=$obj->{URL}{album_page}{thumb}";
      print ALBUM " alt=$obj->{alt} /><br />\n";
      print ALBUM "          $pname\n";
      print ALBUM "          <font size='-1'><i>[$obj->{full}{filesize}]</i></font>\n"
        if $opt->{file_sizes};
      print ALBUM "        </a><br />\n";

    # Not a picture?
    } else {
      my $type = ($pic =~ /\.([^\.]+)$/) ? $1 : "??";
      print ALBUM "        <font size='+1'><b>$type file:</b></font>\n";
      print ALBUM "        <p>\n";
      print ALBUM "        <a href=$obj->{URL}{album_page}{image}>\n";
      print ALBUM "          $pname\n";
      print ALBUM "          <font size='-1'><i>[$obj->{full}{filesize}]</i></font>\n"
        if $opt->{file_sizes};
      print ALBUM "        </a><br />\n";
    }

    # Caption?
    print ALBUM "          <font size='-2'>\n";
    caption($obj,0);
    print ALBUM "          </font>\n";
  }

  end_table();
  print ALBUM "<hr />\n" if @{$data->{pics}};
  footer($opt,$data);

  close_output($opt,0);
}

#########################
# Image pages
#########################
sub write_img_indexes {
  my ($opt,$data) = @_;

  my $dir = $data->{paths}{dir};

  # Init the previous info
  my $last_pic = $data->{pics}[-1];
  my $prev_url = $data->{obj}{$last_pic}{URL}{image_page}{image_page};
  my $prev_name = $data->{obj}{$last_pic}{name};

  for(my $i=0; $i<=$#{$data->{pics}}; $i++) {
    my $pic = $data->{pics}[$i];
    my $obj = $data->{obj}{$pic};
    next unless $obj->{thumb} && %{$obj->{thumb}};
    my $name = $obj->{name};

    my $next = $i+1 > $#{$data->{pics}} ? 0 : $i+1;
    my $next_pic = $data->{pics}[$next];
    my $next_url = $data->{obj}{$next_pic}{URL}{image_page}{image_page};
    my $next_name = $data->{obj}{$next_pic}{name};

    my $file = "$opt->{dir}/$pic$data->{paths}{page_post_url}";
    setup_output($opt,"$dir/$file",0);
    header($opt,$data,1,$name);

    # Image and Previous/next
    my $prev_next = <<PREV_NEXT;
<table cellspacing='10' width='100%'>
  <tr>
    <td align='left'>
      <h3><a href=$prev_url>$prev_name</a></h3>
    </td>
    <td align='right'>
      <h3><a href=$next_url>$next_name</a></h3>
    </td>
  </tr>
</table>
PREV_NEXT

    print ALBUM $prev_next;

    print ALBUM "<center><i><font size='+1'>\n";
    print ALBUM "<a href=$obj->{URL}{image_page}{image}>\n";
    print ALBUM "<$obj->{full}{tag} border='0'";
    print ALBUM " src=$obj->{URL}{image_page}{image_src}";
    print ALBUM " alt=$obj->{alt}";
    print ALBUM " /></a><br />\n";
    caption($obj,1);
    print ALBUM "</font></i></center>\n";

    print ALBUM $prev_next;

    print ALBUM "<hr />\n";

    footer($opt,$data);

    close_output($opt,0);

    $prev_url = $obj->{URL}{image_page}{image_page};
    $prev_name = $name;
  }
}

##################################################
##################################################
# Themes
##################################################
##################################################
sub write_theme {
  my ($opt,$data) = @_;

  setup_output($opt,$data->{paths}{album_file},1);

  # Write the support data/functions
  eperl_set_file("album theme initialization",1);
  eperl($opt,@{$data->{eperl}});

  # Write the theme
  eperl_set_file($opt->{'album.th'},$opt->{_theme_line}{'album.th'});
  eperl($opt,
    @{$opt->{_theme}{'album.th'}},
    @{$data->{end_eperl}});

  close_output($opt,1);
}

# Is this worth it?
# The default image writing is pretty zippy..
sub dependency_changed {
  my ($file,@dependencies) = @_;
  return 1 unless -f $file;
  my $file_mod = -M $file;
  foreach my $dep ( @dependencies ) {
    next unless -f $dep;
    my $mod = -M $dep;
    return 1 if $mod <= $file_mod;
  }
  return 0;
}

sub prev_next_theme_path_changed {
  my ($file,$prev,$next,$theme,$path) = @_;
  return 1 unless -f $file;
  return 1 unless (open(FILE,"<$file"));
  my ($got_prev,$got_next,$got_theme,$got_path);
  while(<FILE>) {
    $got_next = $1 if (/meta\s+name='Next_Image'\s+content='(.+)'/i);
    $got_prev = $1 if (/meta\s+name='Prev_Image'\s+content='(.+)'/i);
# DEPRECATED: We should be keeping track of this as a changed opt somehow.
    $got_theme = $1 if (/meta\s+name='Album_Theme'\s+content='(.+)'/i);
    $got_path = $1 if (/meta\s+name='Album_Path'\s+content='(.+)'/i);
    if ($got_next && $got_prev && $got_theme && $got_path) {
      close(FILE);
      return 1 unless $next eq $got_next;
      return 1 unless $prev eq $got_prev;
      return 1 unless $theme eq $got_theme;
      return 1 unless $path eq $got_path;
      return 0;
    }
    last if m|</head>|i;
  }
  close(FILE);
  return 1;
}

sub write_img_themes {
  my ($opt,$data) = @_;

  my $dir = $data->{paths}{dir};

  my @changed;
  # Which image pages have had source changes?
  for(my $i=0; $i<=$#{$data->{pics}}; $i++) {
    my $pic = $data->{pics}[$i];
    my $obj = $data->{obj}{$pic};
    next unless $obj->{thumb} && %{$obj->{thumb}};

    my $file = "$opt->{dir}/$pic$data->{paths}{page_post_url}";

    my $theme = search_path($opt,$opt->{theme},@{$opt->{theme_path}});
    $changed[$i] = dependency_changed("$dir/$file",
      "$dir/$pic",		# The image itself
      $obj->{capfile},		# The image.txt file
      "$dir/$opt->{captions}",	# The captions file
      $0,			# Heck, even this program
      "$theme/image.th",	#   or the theme itself
    );
    # Or if -exif has changed, we have to regen all HTML...
    $changed[$i] = 1 if $opt->{SAVE}{exif};
    $changed[$i] = 1 if $opt->{SAVE}{exif_album};
    $changed[$i] = 1 if $opt->{SAVE}{exif_image};
  }

  for(my $i=0; $i<=$#{$data->{pics}}; $i++) {
    my $pic = $data->{pics}[$i];
    my $obj = $data->{obj}{$pic};
    next unless $obj->{thumb} && %{$obj->{thumb}};

    my $file = "$opt->{dir}/$pic$data->{paths}{page_post_url}";

    # Okay - if the source for this image didn't change, and
    # the prev/next images and theme are the same, *and* the
    # the prev/next images didn't have source changes (because
    # they might have a name change or some such..), *THEN* we
    # can skip generating this file, and save some time.
    my $prev = $i ? $i-1 : $#{$data->{pics}};
    my $next = $i==$#{$data->{pics}} ? 0 : $i+1;
    my $depchanged = $changed[$i] || $changed[$prev] || $changed[$next];
    my $pntp_changed = prev_next_theme_path_changed("$dir/$file",
      $data->{pics}[$prev],$data->{pics}[$next],$opt->{theme},$data->{paths}{album_path})
        unless $depchanged;	# we don't need to even check if we know we've changed

    next unless $depchanged || $pntp_changed;

    (-d $dir) || mkdir($dir,0755) || fatal($opt,"Couldn't make directory [$dir]\n");
    setup_output($opt,"$dir/$file",1);

    # Write the support data/functions with IMAGE_NUM/THIS_IMAGE
    eperl_set_file("album theme initialization",1);
    eperl($opt,
      @{$data->{eperl}},
      "<: \$IMAGE_PAGE = 1; \$PAGE_TYPE = 'image_page'; Set_Image($i); \$THIS_IMAGE = $i; :>//\n");

    # Write the theme
    eperl_set_file($opt->{'image.th'},$opt->{_theme_line}{'image.th'});
    eperl($opt,
      @{$opt->{_theme}{"image.th"}},
      @{$data->{end_eperl}});

    close_output($opt,1);
  }
}

##################################################
##################################################
# CREATE AN ALBUM
##################################################
##################################################
sub is_image {
  my ($opt,$pic) = @_;
  return 0 if -f "${pic}$opt->{not_img}";
  return 0 if $pic =~ /\.html?$/i;
  return 0 if $opt->{known_images} && $pic !~ /\.($IMAGE_TYPES)$/i;
  1;
}

sub gather_contents {
  my ($opt,$data) = @_;

  my $dir = $data->{paths}{dir};

  #########################
  # Get images and subdirectories
  #########################
  opendir(DIR,$dir);
  my @contents = grep(!/^\.{1,2}$/, readdir(DIR));
  closedir(DIR);

  # Sort through the contents
  foreach my $bob ( @contents ) {
    my $path = "$dir/$bob";
    next if $bob =~ /^CVS|SCCS|RCS$/;	# Ignore revision control directories
    next if $bob eq ".xvpics";		# Silly xv
    next if $bob eq $opt->{dir};	# Thumbnail/HTML directory
    next if -f "$path$opt->{hide_album}";
    next if -f "$path/$opt->{hide_album}";
    next if $bob =~ /^\./ && !$opt->{all};	# Dot files/directories

    # Ignore the .no_album/.hide_album/.not_img/.txt.. directive files
    next unless -s $path || -d $path;		# Ignore zero byte files
      # (windows can have zero byte directories)
    next if -f "$path$opt->{no_album}";	# Ignore .no_album files/directories
    next if $bob =~ /\Q$opt->{not_img}\E$/;
    next if $bob =~ /\Q$opt->{no_album}\E$/;
    next if $bob =~ /\Q$opt->{hide_album}\E$/;

    # Directories
    push(@{$data->{dirs}}, $bob) if -d $path;
    next if -d $path;

    # Files
    next if $opt->{known_images} && !is_image($opt,$path);
    next if $bob =~ /\.(txt|htaccess|cvsignore)$/;
    next if $bob =~ /~$/;		# Emacs backup files
    next if $bob eq index_page($opt,1);	# Index html
    next if $bob eq $opt->{header};	# Header/footer
    next if $bob eq $opt->{footer};
    next if $bob eq $opt->{captions};	# Captions
    next if $bob eq $opt->{conf_file};
    push(@{$data->{pics}}, $bob);
  }
}

# Deal with each image/picture/file/whatever
sub handle_file {
  my ($opt,$data,$pic) = @_;

  # Paths
  my $dir = $data->{paths}{dir};
  my $path = "$dir/$pic";
  my $obj = $data->{obj}{$pic};

  $obj->{full}{filesize} = filesize $path if $opt->{file_sizes};

  # Figure out type
  $obj->{is_movie} = $pic =~ /\.(mpe?g|mpg|mov|avi)$/i ? 1 : 0;
  my $can_embed = ($pic =~ /\.(mpe?g|mov|avi|pdf|ps)$/i) ? 1 : 0;
  my $tag = 'img';
  $tag = 'embed' if $can_embed && $opt->{embed};
  $obj->{full}{tag} = $tag;

  my $is_image = is_image($opt,$path);

  #########################
  # Handle pictures, generate thumbnails
  #########################
  if ($is_image) {
    snapshot($opt,$dir,$obj);
    medium($opt,$dir,$obj);
    thumbnail($opt,$dir,$obj);
  }

  return if !$obj->{thumb} && $opt->{known_images};

  #########################
  # Get sizes (in case we didn't generate them this time)
  #########################
  if ($is_image) {
    get_size($opt,'full',$obj);
    get_size($opt,'medium',$obj);
    get_size($opt,'thumb',$obj);
  }
  # Get rid of any object size hashes that are empty
  #Bah:#map { undef $obj->{$_} unless keys %{$obj->{$_}} } qw(full medium thumb);

  $obj->{capfile} = $obj->{full}{path};
  $obj->{capfile} =~ s/(\.[^\.]+)?$/.txt/;

  #########################
  # URL paths {URL}{from_page}{to}
  #########################
  # Okay - this gets confusing.  We have a few URLs:
  #
  # {URL}{image_page}{image}
  # {URL}{album_page}{image}
  # - image/image_page from album_page/image_page
  #   (was called "image_urls"/"image_image_urls")
  #   album_page: $pic -or- tn/$pic.html
  #   image_page: ../$pic or just_medium
  #   non-images: $pic or ../$pic
  #
  # {URL}{image_page}{image_page}
  # - This image page from another image page
  #   (was called "image_page_urls")
  #   $pic.html
  #
  # {URL}{image_page}{image_src}
  # - The <img src> URL for the image page
  #   $medium or ../$pic
  #
  # {URL}{album_page}{thumb}
  # {URL}{image_page}{thumb}
  # - Thumbnail from album_page/image_page
  #   (was called "image_page_thumbs"/"image_thumbs")
  #   tn/$
  #
  # If we don't have image pages, we'll only use Image_URL
  #
  my $use_image_pages = $obj->{thumb} && $opt->{image_pages} ? 1 : 0;

  # -no_embed USED to mean that movie pages didn't have image pages..
  #$use_image_pages = 0 if $obj->{is_movie} && !$opt->{embed};

  if ($use_image_pages) {
    my $image_page = "$pic$data->{paths}{page_post_url}";
    $obj->{URL}{album_page}{image} = quote("$opt->{dir}/$image_page",$opt,1);
# Kludge - the number of ".." should be equal to the pathsize of $opt->{dir}
    my $image = ($opt->{just_medium} && $obj->{thumb})
      ? $obj->{medium}{file} : "../$pic";
    $obj->{URL}{image_page}{image} = quote($image,$opt,1);
    $obj->{URL}{image_page}{image_page} = quote($image_page,$opt,1);

    # The image src (only needed for 'image_page' actually).  Possibilites:
    # -embed: pic or medium, but pic if movie (../$pic)
    # -noembed and -medium:	medium
    # -noembed and -nomedium:	snapshot
    my $image_src = $obj->{medium}{file} || "../$pic";
    if ($obj->{snapshot}{file}) {
      $image_src = $opt->{embed} ? "../$pic" :
        ($obj->{medium}{file} || $obj->{snapshot}{file});
    }
    $obj->{URL}{image_page}{image_src} = quote($image_src, $opt, 1);

    # Thumbnail
    $obj->{URL}{album_page}{thumb} = quote("$opt->{dir}/$obj->{thumb}{file}", $opt, 1);
    $obj->{URL}{image_page}{thumb} = quote($obj->{thumb}{file}, $opt, 1);
  } else {
    $obj->{URL}{album_page}{image} = quote($pic,$opt,1);
    $obj->{URL}{album_page}{thumb} = quote("$opt->{dir}/$obj->{thumb}{file}", $opt, 1);
    # We might normally have image pages, just not for this non-image
    $obj->{URL}{image_page}{image_page} = quote("../$pic",$opt,1)
      if $opt->{image_pages};
  }

  # -transform_url
  if ($opt->{transform_url}) {
    $obj->{URL}{album_page}{image} = $opt->{transform_url};
    $obj->{URL}{album_page}{image} =~ s/%S/$pic/g;
    my $s = $pic;  $s =~ s/\.[\.]+$//;
    $obj->{URL}{album_page}{image} =~ s/%s/$s/g;
  }

#  print "\nURL album image: $obj->{URL}{album_page}{image}\n";
#  print "URL image image: $obj->{URL}{image_page}{image}\n";
#  print "URL image imagepage: $obj->{URL}{image_page}{image_page}\n";

  push(@{$data->{pics}}, $pic);
}

# Pick an image to use for the directory thumbnail
# Returns:  thumbnail path, full path to image, local path to image, image filename.
# 
# Thumbnail path:  Where the tn/ directory and the thumbnail gets created
#   (local path from the current working directory)
# Local thumbnail path:  Thumbnail path, but from the current album directory.
# Full path to image:  Path to full size image from current working directory
# Image filename:  Just the image part of the filename.
#
# The thumbnail path and the path used in the full path to the image
# don't have to be the same, but they probably will be.
#
# Example:  (album/dir/, dir/, album/dir/image.gif, image.gif)
sub dir_image {
  my ($opt,$data,$child) = @_;

  return undef unless $opt->{dir_thumbs};

  $data->{obj}{$child}{num_pics} = 0;
  $data->{obj}{$child}{num_dirs} = 0;

  my $dir = $data->{paths}{dir};
  my $child_path = "$dir/$child";

  # Basic method, choose first picture by captions.txt
  #   This is redundant work, we'll probably do this again when
  #   we traverse the child - can we cache this somewhere??
  my $child_data = new_album_data($opt,$child_path);
  gather_contents($opt,$child_data);
  $data->{obj}{$child}{num_dirs} = @{$child_data->{dirs}} if $child_data->{dirs};

  return undef unless $child_data->{pics};
  get_captions($opt,$child_data);	# For sorting
  sort_info($opt,$child_data);
  my @images = grep is_image($opt,$_), @{$child_data->{pics}};

  $data->{obj}{$child}{num_pics} = scalar @images;

  return undef unless @images;
  ($child_path,$child,"$child_path/$images[0]",$images[0]);
}

sub handle_child {
  my ($opt,$data,$child) = @_;

# What if we don't want a thumbnail?

  my $obj = $data->{obj}{$child};

  # Pick an image for thumbnail?
  my ($tn_path,$url,$full_path,$file) = dir_image($opt,$data,$child);
  if ($full_path) {
    ($obj->{full}{path},$obj->{full}{file}) = ($full_path,$file);

    # Simplify for now..
    $obj->{full}{tag} = 'img';

    thumbnail($opt,$tn_path,$obj);
    return unless $obj->{thumb};
    get_size($opt,'thumb',$obj);

    # URL to find the thumbnail
    $obj->{URL}{album_page}{thumb} = quote("$url/$opt->{dir}/$obj->{thumb}{file}",$opt,1);
  }

# Should caption be done here?
}

# Create a new album data structure
sub new_album_data {
  my ($opt,$dir) = @_;
  my %data;
  my $data = \%data;

  # Directory
  $data->{paths}{dir} = $dir || '.';

  $data;
}

# We may have started inside a subalbum.  Figure all this out.
sub figure_depth {
  my ($opt,$data,$dir,$noalb,@dir_pieces) = @_;

  # Our calling depth may be different from album depth.
  $data->{calling_depth} = $#dir_pieces+1 unless $opt->{_depth_diff};

  # The current index
  parse_index($opt,$data) unless $noalb;

  # Is this the first call to do_album?
  $data->{start} = $#dir_pieces<1 ? 1 : 0;

  my $alb_path = $data->{paths}{album_path};
  # If we're starting with a subalbum, update @dir_pieces based on parse_index()
  if (!$#dir_pieces && $alb_path) {
    # Can we find the album_path in this directory?
    if ($dir !~ s|/$alb_path$||) {
      # They've moved something!  Damn!
      # Try to figure out where things changed.
      my @old = split(/\//, $alb_path);
      while(my $old = pop @old) {
        last if $dir !~ s|/$old$||;
      }
      my @fatal = ("ERROR: Your album has moved");
      push(@fatal, "\nIt looks like you've moved your top album.",
        "",
        "To fix this:",
        "1) Erase: $dir/$opt->{default_index}",
        "2) Run:   $PROGNAME $dir") unless @old;
      push(@fatal, "\nIt looks like you've moved a subalbum.  To fix this, run:",
        "",
        "% $PROGNAME -add $dir") if @old;
      fatal($opt,@fatal);
    }

    # Correct $opt->{topdir} and pieces
    $opt->{topdir} = $dir;
    @dir_pieces = split(/\//,$alb_path);
  }
  $data->{dir_pieces} = \@dir_pieces;

  # How deep are we?  Album depth:
  $data->{depth} = $#{$data->{dir_pieces}} + 1;
  # Calling depth:
  if ($opt->{_depth_diff}) {
    $data->{calling_depth} = $data->{depth} - $opt->{_depth_diff};
  } else {
    $opt->{_depth_diff} = $data->{depth} - $data->{calling_depth};
  }
}

#########################
# Do an album!
#########################
sub do_album {
  my ($opt,$dir,@dir_pieces) = @_;

  my $data = new_album_data($opt,$dir);

  # How deep?
  my $noalb = -f "$dir/$opt->{no_album}" ? 1 : 0;
  figure_depth($opt,$data,$dir,$noalb,@dir_pieces);
  return if $opt->{depth}>=0 && $data->{calling_depth} > $opt->{depth};

  # Deal with album.conf files
  my $argv_theme_url = $opt->{SAVE}{theme_url};
  my $pushed = album_confs($opt,$data);

  # Sanity check (no other place to put this!?)
  usage("-theme_url requires -theme option (it does not replace it)")
    if $argv_theme_url && $opt->{theme_url} && !$opt->{theme};

  # Print some info
  my $p = join('/',@{$data->{dir_pieces}});
  my $w = $opt->{hash_width} - $opt->{num_hashes} - 3 - 8;
  $p = '..'.substr($p,-$w+2) if (length($p) > $w);
  start_hashes($opt, "Images: $p");
  if ($noalb || $data->{unknown}) {
    hash_msg($opt,"<".($noalb ? $opt->{no_album} : "unknown").">");
    print STDERR "\nCall album with your photo directory as an option.\n\n"
      if $data->{start};
    return;
  }
  return hash_msg($opt,"<unknown>") if $data->{unknown};

  # Get the list of pics/dirs
  gather_contents($opt,$data);

  # Clean out thumbnail directory of images we don't have anymore
  clean_thumb_dir($opt,$data) if $opt->{clean};

  # Lookup captions and get names of files
  get_captions($opt,$data);

  # Sort pictures/dirs
  sort_info($opt,$data);

  #########################
  # Paths
  #########################
  get_themes($opt);
  calc_paths($opt,$data);


  #########################
  # Handle images (make thumbnails, get info, etc..)
  #########################
  my @pics = @{$data->{pics}};
  $data->{pics} = [];
  my $hashes = $#pics+1 + $#{$data->{dirs}}+1;

  foreach my $pic ( @pics ) {
    handle_file($opt,$data,$pic);
    show_hashes($opt, $data->{obj}{$pic}{num}+1, $hashes);
  }
  foreach my $child ( @{$data->{dirs}} ) {
    handle_child($opt,$data,$child);
    show_hashes($opt, $#pics+1+$data->{obj}{$child}{num}+1, $hashes);
  }

  $hashes ? stop_hashes($opt) : hash_msg($opt,"<no thumbs>");

  #########################
  # Write the HTML
  #########################
  data_to_eperl($opt,$data);

  ($opt->{'album.th'}) ?
    write_theme($opt,$data) : write_index($opt,$data);
  ($opt->{'image.th'} ?
    write_img_themes($opt,$data) : write_img_indexes($opt,$data))
      if $opt->{image_pages};

  #########################
  # Do the children albums
  #########################
  #my $first = "$opt->{topdir}/$data->{dir_pieces}[0]";
  my @add = $opt->{_album}{$dir}{add} ?  @{$opt->{_album}{$dir}{add}} : ();
  foreach my $child ( @{$data->{dirs}} ) {
    do_album($opt,"$dir/$child",@{$data->{dir_pieces}},$child)
      unless $data->{start} && @add && !grep($child eq $_, @add);
  }

  pop_opts($opt) if $pushed;
}

##################################################
# Thumbnail code
##################################################

# Create a new image pathname
# bob.jpg, jpg  ->  tn/bob.jpg
# bob.gif, jpg  ->  tn/bob.gif.jpg
sub new_image_path {
  my ($opt,$dir,$pic,$type,$add_nodir,$add_dir) = @_;

  # Separate/replace postfix
  my $post="";
  ($pic,$post)=($1,$2) if ($pic =~ /(.+)\.([^\.\/]+)$/);
  $post = $post || $type;
  $post .= ".$type" if $type && lc($type) ne lc($post);

  return ("${pic}.${add_nodir}$post","${pic}.${add_nodir}$post")
    unless $opt->{dir};
  my $file = "${pic}.${add_dir}$post";

  $dir = $dir ? "$dir/$opt->{dir}" : $opt->{dir};
  (-d $dir) || mkdir($dir,0755) || fatal($opt,"Couldn't make directory [$dir]\n");

  return ($file,"$dir/$file");
}

sub get_xy {
  my ($opt,$img) = @_;

  return (0,0) unless (-f $img);

  my $qimg = file_quote($opt,$img);

  # if image is a jpeg, try "jhead" first (faster)
  if ($opt->{jhead} && $img=~/.jpe?g$/) {
    return ($1,$2) if (qx/$opt->{jhead} -c $qimg 2>$opt->{dev_null}/=~/\s(\d+)x(\d+)(\s)/);
    undef $opt->{jhead};	# jhead didn't work, don't keep trying...
  }

  my $try_noidentify = 0;	# Did identify fail?

  # Try to use identify if we have it
  if ($opt->{identify}) {
    my $size = open_pipe($opt,$opt->{identify},"-ping $qimg");
    if ($size) {
      while(<$size>) {
        print STDERR "get_xy(): $_" if $opt->{d};
        if (/command not found/) {	# Kind of kludgy
          $opt->{identify} = 0;
          last;
        }
        return (0,0) if (/no delegate for this image format/);
        return (0,0) if (/support .*not yet available/);
        if (/\s(\d+)x(\d+)(\s|\+|\-)/) {
          $size->close;
          return ($1,$2);
        }
      }
    }
    # I wish there was an easy way to tell if they had identify!
    $try_noidentify = 1;
  }

  # Kludgy way to get size, but works with all images that convert reads
  my $size=open_pipe($opt,$opt->{convert},"-verbose $qimg $opt->{dev_null}");
  $size || fatal($opt,"Couldn't run convert!  [$opt->{convert}]\n");
  while(<$size>) {
    print STDERR "get_xy(): $_" if $opt->{d};
    if(/\s(\d+)x(\d+)(\s|\+|\-)/) {
      $size->close;
      $opt->{identify} = 0 if $try_noidentify;	# identify didn't work, convert did
      return ($1,$2);
    }
  }
  print STDERR "\n\n[$PROGNAME] Can't get [$img] size from 'convert -verbose' output\n";
  print STDERR "\tTry option:  -known_images to ignore garbage files\n"
    unless ($img =~ /\.$IMAGE_TYPES$/i);
  # Gentoo has this goofy thing, see:
  print STDERR "\n\tGentoo users: make sure to run as root:\n% USE=\"avi gif jpeg mpeg png quicktime tiff\" emerge imagemagick\n"
    if -f "/etc/gentoo-release";
  print STDERR "\tWindows users may have an easier time with Cygwin installed!\n"
    if $opt->{windows} && !$opt->{cygwin};
  fatal($opt);
}

sub get_size {
  my ($opt,$img,$obj) = @_;
  return get_xy($opt,$img) unless $obj;
  # Use snapshot for full if available
  my $use = $img eq 'full' && $obj->{snapshot} ? 'snapshot' : $img;
  return ($obj->{$img}{x},$obj->{$img}{y}) if $obj->{$img}{x} && $obj->{$img}{y};
  my ($x,$y) = get_xy($opt,$obj->{$use}{path});
  return (0,0) unless $x && $y;
  ($obj->{$img}{x},$obj->{$img}{y}) = ($x,$y);
}

# See if they should have used -animated_gifs
# (If we get new.0 instead of new and don't have animated_gifs turned on)
sub check_anim_gifs {
  my ($opt,$img,$new) = @_;
  return if -f $new;
  return unless -f "$new.0";
  # We shouldn't get here if animated_gifs is set
  return if $opt->{animated_gifs};
  $opt->{animated_gifs}=1;
  rename("$new.0",$new);	# Try to fix
  print STDERR "\n[$PROGNAME] Error: Animated gif was found that wasn't scaled properly.\n\tTry using option -animated_gifs in the future.\n\n";
}

sub scale {
  my ($opt,$img,$scale_arg,$new,$medium) = @_;

  my $scale = $opt->{sample} ? "-sample $scale_arg" : "-geometry $scale_arg";

  # Source image
  my $qimg = file_quote($opt,$img);
# This works only on some systems with some versions of convert  :(
  $qimg .= "\[0]" if $opt->{animated_gifs};
  my $args = "-verbose $qimg $scale ";

  # Scale options
  $args .= "$opt->{scale_opts} " if $opt->{scale_opts};
  $args .= "$opt->{med_scale_opts} " if $medium && $opt->{med_scale_opts};
  $args .= "$opt->{full_scale_opts} " if !$medium && $opt->{full_scale_opts};

  # Sharpen?
  $args .= "-sharpen $opt->{sharpen} " if $opt->{sharpen};

  # Destination image
  my $qnew = file_quote($opt,$new);
  $args .= " $qnew";
#composite -quality 95 -gravity south -compose difference \.album/copyright.jpg $qnew $qnew

  my $size = open_pipe($opt,$opt->{convert},$args);
  $size || fatal($opt,"Couldn't run convert!  [$opt->{convert}]\n");
  my ($ax,$ay,$bx,$by);
  while(<$size>) {
    print STDERR "scale(): $_" if $opt->{d};
    if (/((\d+)x(\d+))?=>(\d+)x(\d+)/) {
      ($ax,$ay,$bx,$by) = ($2,$3,$4,$5);
      last;
    }
  }
  $size->close;
  check_anim_gifs($opt,$img,$new);

  # Sometimes convert doesn't give us the new size information
  ($bx,$by) = get_size($opt,$new) unless $bx;
  ($ax,$ay,$bx,$by);
}

sub crop {
  my ($opt,$img,$off_x,$off_y,$new) = @_;

  my ($x,$y) = ($opt->{x},$opt->{y});

  return hash_warn($opt,"Error cropping $img (image not found)")
    unless -f $img;
  my $qimg = file_quote($opt,$img);
  my $qnew = file_quote($opt,$new);
  my $cmd = file_quote($opt,$opt->{convert});
  $cmd .= " $qimg -crop ${x}x${y}+${off_x}+${off_y} $qnew";
  print STDERR "crop() run: $cmd\n" if $opt->{d};
  system($cmd);
  return unless ($?);
  print STDERR "[$PROGNAME] Error cropping $img\n";
}

#########################
# Generate thumbnail/medium images
#########################
sub movie_frame {
  my ($opt,$movie,$img) = @_;

  my $qmovie = file_quote($opt,$movie);
  my $qimg = file_quote($opt,$img);

#OLD## Has problems with conversion, but when it works, it looks better :(
#OLD## Unfortunately most thumbnails end up clipped/mostly green.
#OLD#my $cmd = "mpeg2decode -f -o3 -1 $qmovie $qimg";

  return $img if -f $img && !$opt->{force} && -M $img < -M $movie;

  # ffmpeg has problems recognizing .mov format
  my $format = ($qmovie =~ /\.mov$/i) ? "-f mov" : "";

  ### ffmpeg -f jpeg
  #my $tmpout = "album.tmp.$$.%d.jpg";	# This is the command output
  #my $tmpret = "album.tmp.$$.1.jpg";	# This is where the file goes
  #my $cmd = "$opt->{ffmpeg} -y -t 00:00:00.01 $format -i $qmovie -f jpeg $tmpout";

  ### ffmpeg -f singlejpeg
  my $cmd = "$opt->{ffmpeg} -y -t 00:00:00.01 $format -i $qmovie -f singlejpeg $qimg";
  print STDERR "movie_frame() run: $cmd\n" if $opt->{d};
  system("$cmd > $opt->{dev_null} 2>&1");
  return $qimg unless $?;

  hash_warn($opt,"Error extracting movie frame:\n\t$movie\n\n\tDo you have ffmpeg installed?  http://ffmpeg.org");
  return $movie;
}

# Make a snapshot/preview image for non-image types (like movies)
sub snapshot {
  my ($opt,$dir,$obj) = @_;

  # Currently only for movies..  Will re-org when I do plugins.
  return unless $obj->{is_movie};

  # Based off full
  my $file = $obj->{full}{file};
  my $full_path = $obj->{full}{path};

  my $type = $opt->{type};
  ($obj->{snapshot}{file},$obj->{snapshot}{path})
    = new_image_path($opt,$dir,$file,$type,'snap.','snap.');

  movie_frame($opt,$full_path,$obj->{snapshot}{path});
}

sub medium {
  my ($opt,$dir,$obj) = @_;

  my $file = $obj->{full}{file};
  # Based off of full, unless we have a snapshot image.
  my $base = $obj->{snapshot} ? 'snapshot' : 'full';
  my $full_path = $obj->{$base}{path};

  return 0 unless $opt->{medium};
  return 0 if $obj->{is_movie} && $opt->{embed};

  my $type = $opt->{medium_type};
  $type = $opt->{type} if $obj->{is_movie} && !$type;
  ($obj->{medium}{file},$obj->{medium}{path})
    = new_image_path($opt,$dir,$file,$type,'med.','med.');

  # Don't regenerate mediums unless the image has changed
  return get_size($opt,'medium',$obj)
    if (-f $obj->{medium}{path} && !$opt->{force} && -M $obj->{medium}{path} < -M $full_path);

# It would be neat if we could look at the scale options and figure
# out if the medium option has changed??  Can we alternatively do
# this by seeing what .conf options have been respec'd?

  my ($fx,$fy,$mx,$my) = scale($opt,$full_path,$opt->{medium},$obj->{medium}{path},1);
  ($obj->{full}{x},$obj->{full}{y}) = ($fx,$fy)
    if $base eq 'full' && $fx;

  return 0 unless $mx;
  ($obj->{medium}{x},$obj->{medium}{y}) = ($mx,$my);
  1;
}

# An image thumbnail
sub thumbnail {
  my ($opt,$dir,$obj) = @_;

  my $file = $obj->{full}{file};
  # Based off of full, unless we have a snapshot image.
  my $base = $obj->{snapshot} ? 'snapshot' : 'full';
  my $full_path = $obj->{$base}{path};

  return unless -r $full_path;

  print STDERR "IMAGE: $full_path\n" if $opt->{d};

  ($obj->{thumb}{file},$obj->{thumb}{path})
    = new_image_path($opt,$dir,$file,$opt->{type},'tn.','');

  # Don't regenerate thumbs if we don't need to.
  return get_size($opt,'thumb',$obj)
    if (-f $obj->{thumb}{path} && !$opt->{force} && -M $obj->{thumb}{path} < -M $full_path);

  # In case we didn't get the size yet
  my ($x,$y) = get_size($opt,$base,$obj);

  # Which way do we need to shrink?  convert will scale down w/ aspect
  # as much as is needed to *fit* inside the geometry we give it
  # Kludge:  Assume the image is larger than a thumbnail
  my ($scale_x,$scale_y) = ($opt->{x},$opt->{y});
  if ($opt->{crop}) {
    if ( $x/$opt->{x} < $y/$opt->{y} ) {
      # Make vertical bigger so that we don't scale horizontal past $opt->{x}
      $scale_y = $y;
    } else {
      $scale_x = $x;
    }
  }
  my ($fx,$fy,$tx,$ty) = scale($opt,$full_path,$scale_x."x".$scale_y,$obj->{thumb}{path},0);

  return 0 unless $tx;

  ($obj->{thumb}{x},$obj->{thumb}{y}) = ($tx,$ty);
  return 1 unless $opt->{crop};

  # Now crop the other dimension
  my ($off_x,$off_y) = (0,0);

  $off_x = int(($tx-$opt->{x})/2) if ($tx > $opt->{x} );
  $off_y = int(($ty-$opt->{y})/2) if ($ty > $opt->{y} );

  # Do they have any cropping directives in the image name?
  if ($file =~ /CROP(top|bottom|left|right)\.[^\.]+$/ ||
      $opt->{CROP} =~ /^(top|bottom|left|right)$/) {
    $off_y = 0 if ($1 eq "top");
    $off_y = $ty-$opt->{y} if ($1 eq "bottom");
    $off_x = 0 if ($1 eq "left");
    $off_x = $tx-$opt->{x} if ($1 eq "right");
  }

  crop($opt,$obj->{thumb}{path},$off_x,$off_y,$obj->{thumb}{path})
    unless ($tx==$opt->{x} && $ty==$opt->{y});

  ($obj->{thumb}{x},$obj->{thumb}{y}) = ($opt->{x},$opt->{y});
  1;
}

##################################################
# Main code
##################################################
sub main {
  my $opt = get_defaults();
  $opt->{d} = 1 if grep($_ eq '-d', @ARGV);	# Hack to see read_confs debugs
  read_confs($opt);
  parse_args($opt);
  scrub_opts($opt,1);

  version($opt);

  virgin_check($opt);

  foreach my $dir ( @{$opt->{_albums}} ) {
    print "\n" unless $opt->{q};
    ($opt->{topdir},my $name) = split_path($opt,$dir);

    do_album($opt,$dir,$name);
  }

}
main();

END { all_done(); }


##################################################
# POD/man
##################################################

# Generate entire "=head1 OPTIONS" section with: album -pod

__END__

=pod
=head1 NAME

album - Make a web photo album

=head1 SYNOPSIS

B<album> [S<I<album options>>]

=head1 DESCRIPTION

album is an HTML photo album generator that supports themes. It takes 
a directory of images and creates all the thumbnails and HTML that 
you need. It's fast, easy to use, and very powerful.

Place your photos in a new directory somewhere inside your web pages.
Then run C<album> from a command-line prompt with the directory path
as an argument, and that's it.

To use themes, make sure the C<Themes> directory is inside your web
path, and then use the -theme option.

=head1 OPTIONS

There are three types of options.  Boolean options, string/num options and
array options.  Boolean options can be turned off by prepending -no_:

% album -no_image_pages

String and number values are specified after a string option:

% album -type gif
% album -columns 5

Array options can be specified two ways, with one argument at a time:

% album -exif hi -exif there

Or multiple arguments using the '--' form:

% album --exif hi there --

Boolean options:

% album -q, -d, -save_conf, -image_pages, -dir_thumbs, -just_medium, -embed, -clean, -album_captions, -caption_edit, -file_sizes, -fix_urls, -known_images, -all, -hashes, -name_length, -date_sort, -name_sort, -reverse_sort, -image_loop, -crop, -force, -sample, -animated_gifs, -use_tcap

String/number options:

% album -medium, -captions, -top, -body, -charset, -index, -default_index, -html, -type, -medium_type, -CROP, -dir, -sharpen, -theme_url, -theme_path, -convert, -identify, -jhead, -ffmpeg, -conf_file, -dev_null, -windows, -cygwin, -tcap, -tcap_out, -cmdproxy, -header, -footer, -no_album, -hide_album, -not_img

Array options:

% album --exif, --exif_album, --exif_image, --add, --scale_opts, --medium_scale_opts, --full_scale_opts


=head2 OPTION DESCRIPTIONS

=over 4

=item B<-h>I<>

Show usage

=item B<-q>I<>

Be quiet [Default OFF]

=item B<-d>I<>

Set debug mode [Default OFF]

=item B<-conf>I<=I<file>>

Read a .conf file

=item B<-save_conf>I<>

Save album.conf files in albums [Default ON]

=back

=head2 Album Options:

=over 4

=item B<-image_pages>I<>

Create a page for each image [Default ON]

=item B<-dir_thumbs>I<>

Directories have thumbnail (if supported by theme) [Default ON]

=item B<-medium>I<=I<geom>>

Generate medium size images

=item B<-just_medium>I<>

Don't link to full-size images [Default OFF]

=item B<-embed>I<>

Use image pages for non-picture image pages [Default ON]

=item B<-columns>I<>

Number of image columns [Default 4]

=item B<-clean>I<>

Remove unused thumbnails [Default OFF]

=item B<-captions>I<=I<string>>

Specify captions filename [Default captions.txt]

=item B<-album_captions>I<>

Also show captions on album page [Default ON]

=item B<-caption_edit>I<>

Add comment tags so that caption_edit.cgi will work [Default OFF]

=item B<--exif>I<=I<fmt>>

Append exif info to captions.  Use %key 0n fmt string
Example:  -exif "<br>Camera: %Camera model%"
If any %keys% are not found by jhead, nothing is appended.

=item B<--exif_album>I<=I<fmt>>

-exif for just album pages

=item B<--exif_image>I<=I<fmt>>

-exif for just image pages

=item B<-file_sizes>I<>

Show image file sizes [Default OFF]

=item B<-fix_urls>I<>

Encode unsafe chars as 0x in URLs [Default ON]

=item B<-known_images>I<>

Only include known image types [Default ON]

=item B<-top>I<=I<string>>

URL for 'Back' link on top page [Default ../]

=item B<-all>I<>

Do not hide files/directories starting with '.' [Default OFF]

=item B<--add>I<=I<dir>>

Add a new directory to the album it's been placed in

=item B<-depth>I<>

Depth to descend directories (default infinite) [Default -1]

=item B<-hashes>I<>

Show hash marks while generating thumbnails [Default ON]

=item B<-name_length>I<>

Limit length of image/dir names [Default ON]

=item B<-date_sort>I<>

Sort images/dirs by date instead of captions/name [Default OFF]

=item B<-name_sort>I<>

Sort by name, not caption order [Default OFF]

=item B<-reverse_sort>I<>

Sort in reverse [Default OFF]

=item B<-body>I<=I<string>>

Specify <body> tags for non-theme output

=item B<-charset>I<=I<str>>

Charset for non-theme output [Default iso-8859-1]

=item B<-image_loop>I<>

Do first and last image pages loop around? [Default ON]

=item B<-index>I<=I<file>>

Select the default 'index.html' to use.
For file://, try '-index index.html' to add 'index.html' to index links.

=item B<-default_index>I<=I<file>>

The file the webserver accesses when no file is specified. [Default index.html]

=item B<-html>I<=I<post>>

Default postfix for HTML files [Default .html]

=back

=head2 Thumbnail Options:

=over 4

=item B<-geometry>I<=I<E<lt>XE<gt>xE<lt>YE<gt>>>

Size of thumbnail [Default 133x133]

=item B<-type>I<=I<string>>

Thumbnail type (gif, jpg, tiff,...) [Default jpg]

=item B<-medium_type>I<=I<string>>

Medium type (default is same type as full image)

=item B<-crop>I<>

Crop the image to fit thumbnail size
otherwise aspect will be maintained [Default OFF]

=item B<-CROP>I<=I<string>>

Force cropping to be top, bottom, left or right

=item B<-dir>I<=I<string>>

Thumbnail directory [Default tn]

=item B<-force>I<>

Force overwrite of existing thumbnails
otherwise they are only written when changed [Default OFF]

=item B<-sample>I<>

convert -sample for thumbnails (faster, low quality) [Default OFF]

=item B<-sharpen>I<=I<E<lt>radiusE<gt>xE<lt>sigmaE<gt>>>

Sharpen after scaling

=item B<-animated_gifs>I<>

Take first frame of animated gifs (only some systems) [Default OFF]

=item B<--scale_opts>I<=I<strings>>

Options for convert (use '--' for mult)

=item B<--medium_scale_opts>I<=I<strings>>

List of medium convert options

=item B<--full_scale_opts>I<=I<strings>>

List of full convert options

=back

=head2 Theme Options:

=over 4

=item B<-theme>I<=I<dir>>

Specify a theme directory

=item B<-theme_url>I<=I<url>>

In case you want to refer to the theme by absolute URL

=item B<-theme_path>I<=I<dir>>

Directory that contains themes

=item B<-more>I<>

To show more options.

=item B<-More>I<>

To show even more options.

=item B<-version>I<>

Display program version info

=back

=head2 Paths:

=over 4

=item B<-convert>I<=I<string>>

Path to convert (ImageMagick) [Default convert]

=item B<-identify>I<=I<string>>

Path to identify (ImageMagick) [Default identify]

=item B<-jhead>I<=I<string>>

Path to jhead (extracts exif info) [Default jhead]

=item B<-ffmpeg>I<=I<string>>

Path to ffmpeg (extracting movie frames) [Default ffmpeg]

=item B<-conf_file>I<=I<string>>

Conf filename for album configurations [Default album.conf]

=item B<-dev_null>I<=I<string>>

Throwaway temp file [Default /dev/null]

=item B<-windows>I<=I<string>>

Are we (unfortunately) running windows?

=item B<-cygwin>I<=I<string>>

Are we using the Cygwin environment?

=item B<-use_tcap>I<>

Use tcap? (win98) [Default OFF]

=item B<-tcap>I<=I<string>>

Path to tcap (win98) [Default tcap]

=item B<-tcap_out>I<=I<string>>

tcap output file (win98) [Default atrash.tmp]

=item B<-cmdproxy>I<=I<string>>

Path to cmdproxy (tcap helper for long lines) [Default cmdproxy]

=item B<-header>I<=I<string>>

Path to header file [Default header.txt]

=item B<-footer>I<=I<string>>

Path to footer file [Default footer.txt]

=item B<-no_album>I<=I<string>>

Ignore dir/files if file with this postfix exists [Default .no_album]

=item B<-hide_album>I<=I<string>>

Ignore and don't display these files [Default .hide_album]

=item B<-not_img>I<=I<string>>

Don't treat these files as images [Default .not_img]



=back

=head1 ENVIRONMENT

=over 6

=item HOME

Home directory for finding user-specific configuration files (.albumrc)

=item DOT

Instead of looking for .albumrc, album also looks for $DOT/album.conf
(I'm not a big fan of .dotfiles cluttering my home directory).

=item tcap

Set/overwritten by the Win98 version of album for tcap arguments.

=back

=head1 FILES

=over 6

=item F</etc/album/album.conf>

=item F</etc/album.conf>

Site-specific configuration

=item F<$HOME/.albumrc>

=item F<$HOME/.album.conf>

=item F<$DOT/album.conf>

User-specific configuration

=item F<E<lt>albumE<gt>/album.conf>

Album-specific configuration.

B<Will be modified with any new command-line options!>

=item F<E<lt>albumE<gt>/header.txt>

=item F<E<lt>albumE<gt>/footer.txt>

=item F<E<lt>albumE<gt>/captions.txt>

=item F<E<lt>albumE<gt>/.no_album>

=item F<E<lt>albumE<gt>/E<lt>imageE<gt>.no_album>

=item F<E<lt>albumE<gt>/.hide_album>

=item F<E<lt>albumE<gt>/E<lt>imageE<gt>.hide_album>

=item F<E<lt>albumE<gt>/E<lt>imageE<gt>.not_img>

Specifies album information

=back

=head1 SEE ALSO

L<ImageMagick(1)>, L<jhead(1)>, L<ffmpeg(1)>

=head1 AUTHOR

David Ljung Madison <http://MarginalHacks.com/>

=cut
