#!/usr/bin/perl
# Filename:	album
# Author:	David Ljung Madison <DaveSource.com>
# See License:	http://MarginalHacks.com/License/
  my $VERSION=  '3.11.afnog2006';
  # XXX lightly hacked for AfNOG by apb, mostly to remove hardcoded stuff
  # that was not under the control of a theme.
# Description:	Makes a photo album.
use strict;
use IO::File;
umask 022;	# 0755

package album;	# For plugins

##################################################
##################################################
# 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/conf",
	"/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};
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};

my @DATA_PATH = (
	"/etc/$PROGFILE",
	"/usr/share/$PROGFILE",
	);
push(@DATA_PATH, "$ENV{HOME}/.$PROGFILE") if $ENV{HOME};

my @PLUGIN_PATH = ( '@DATA_PATH/plugins' );

##################################################
# 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:
sub OPTION_SEP() { 1; }	# Separator
sub OPTION_BOOL() { 2; }
sub OPTION_NUM() { 3; }
sub OPTION_STR() { 4; }
sub 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(3,'D',OPTION_BOOL, one_time=>1, usage=>"Heavy 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(2,'virgin_check',OPTION_BOOL, one_time=>1, default=>1, usage=>"Do the virgin check to see if you've run album before");
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(3,'image_headers',OPTION_BOOL, default=>0, usage=>"Show header.txt on image pages (default theme only)");
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,'sort',OPTION_STR, default=>'captions', usage=>"Sort type, captions, name or date");
	## Deprecated!
	add_option(99,'date_sort', \&deprecated_option, args=>'sort', usage=>"DEPRECATED: Sort images/dirs by date instead of captions/name");
	add_option(99,'name_sort', \&deprecated_option, args=>'sort', usage=>"DEPRECATED: Sort by name, not caption order");
add_option(2,'reverse_sort',OPTION_BOOL, usage=>"Sort in reverse");
add_option(3,'body',OPTION_STR, default=>'<body>', 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,'burn', OPTION_BOOL, usage=>["Setup an album to burn to CD",
           "Implies '-index index.html' and '-no_theme_url'"]);
add_option(2,'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",
           "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, singleval=>1, 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 and HTML",
           "otherwise they are only written when changed"]);
add_option(2,'force_html',OPTION_BOOL, one_time=>1, usage=>"Force rewrite of HTML");
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,'thumb_scale_opts',OPTION_ARR, usage=>"List of thumbnail convert options");

# Plugin Options:
add_option(1,'Plugin and Theme Options:',OPTION_SEP);
add_option(1,'data_path', OPTION_ARR, default=>\@DATA_PATH, usage=>["Path for themes, plugins, language files, etc...",""]);
add_option(1,'plugin', \&get_plugin, args=>'<plugin>', usage=>"Load a plugin.");
add_option(1,'plugin_usage', \&usage, args=>'<plugin>', usage=>"Show usage for a plugin.");
add_option(3,'plugin_info', \&list_plugins, args=>'<plugin>', one_time=>1, usage=>"Print info for a specific plugins.");
add_option(10,'plugin_path', OPTION_ARR, default=>['@DATA_PATH/plugins'], usage=>"Add a path to search for plugins.\n\t");
add_option(10,'plugin_post', OPTION_STR, default=>'.alp', usage=>"Default postfix for plugins.");
add_option(3,'list_plugins', \&list_plugins, one_time=>1, usage=>"Print info for all known plugins.");
add_option(10,'list_plugins_crf', \&list_plugins, one_time=>1, usage=>"Print info for all known plugins in computer readable format.");
add_option(4,'list_hooks', \&list_hooks, one_time=>1, usage=>"Show all known plugin hooks (for developers).");
add_option(4,'hook_info', \&list_hooks, args=>'<hook>', one_time=>1, usage=>"Show all known plugin hooks (for developers).");

# 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(10,'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,'conf_version',OPTION_NUM, usage=>"Configuration file version");
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?");
add_option($WINDOWS?1:99,'slash',OPTION_STR, default=>$WINDOWS ? '\\' : '/', usage=>"The slash used between path components");
# 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");
              # We can't set -no_album as an option because it gets misread
              # as a -no_<option>, but that's okay with me...
              # This wouldn't be difficult to fix in parse_arg,
              # though it would be less readable.
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=>25, one_time=>1, usage=>"How many hashes to print");
add_option(99,'hash_width',OPTION_NUM, default=>78, usage=>"Width of screen (for hashes)");

#add_option(99,'zod',sub { print "CALLED ZOD!\n" }, usage=>"Tmp arg testing");

# 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->{has_thumb}	Boolean: does it have a thumbnail?
# $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 internal _fields in $opt/$data (not for theme use)
# These are mostly just for album development - my own personal notes.
#
# @{$opt->{_albums}}		List of albums to run on
# $opt->{_album}{$alb}		Hash of any per-album settings (-add,..)
#
# Saved/cached data:
#   $opt->{_theme}{..}		Contains entire text of the image.th/album.th
#   $opt->{_theme_line}{..}	The starting line
#   $opt->{_theme_full}		Full theme path
#   $opt->{_captions}{$dir}	Captions cache for a path
#   $opt->{_done_exif}{$pic}	Have we run exif for a photo?
#   $opt->{_read_conf}{$conf}	Avoid reading conf files multiple times
# 				(in opt to survive multiple do_album calls)
#   $opt->{_depth_diff}		Beginning depth of starting album
#   $data->{paths}{_date_sort_cache}	Date sort values
#   $data->{eperl}		The ePerl support text
#
# Plugins:
#   @{$opt->{_plugins}{loaded}}	List of all the plugins already loaded
#   $opt->{_plugin}{$plugin}	Plugin info hash
#   @{$opt->{_plugin_hooks}{$name}}	List of hooks for a name [sub,plugin]
#   $opt->{_curr_plugin}	For the plugin of the hook we are calling
#   $opt->{_curr_hook}	The hook we are calling
#
# Args/confs:
#   $opt->{_set}{$opt}		Options that were set by any method (so theme doesn't clobber)
#   $opt->{_argv}{vals}{$opt}	Saved vals for argv options
#   $opt->{_argv}{no}{$opt}	Saved 'no' vals for (array) options (-no_plugin bob, ..)
#   $opt->{_argv}{clear}{$opt}	Options that have been cleared
#   @{$opt->{_argv}{order}}	Ordering of argv short options (for saving)
#   $opt->{_argv}{deferred}{..	Like {vals} - track conf options deferred by argv
#
# Option stack:  (we push and pop all the options with each album we enter)
#   $opt->{_saved_opt}		Duplication of $opt for parent album

##################################################
# Bootstrap/Simple utilities
##################################################
sub attempt_require {
  my $save = $SIG{__DIE__};
  $SIG{__DIE__} = 'ignore';
  eval "require $_[0]";
  my $ret = $@ ? 0 : 1;
  $SIG{__DIE__} = $save;
  $ret;
}

my %OPTIONS;
my %DEFAULTS;
my $GLOBAL_OPT;
sub add_option {
  my ($lvl,$option,$type,%hash) = @_;
  my $plugin = $GLOBAL_OPT->{_curr_plugin};
  $hash{lvl}=$lvl;
  $hash{type}=$type;
  $hash{default}=0 unless $hash{default} || $type!=OPTION_BOOL;
  $hash{plugin}=$plugin;
  $hash{option}=$option;
  if ($plugin) {
    $OPTIONS{plugin}{$plugin}{$option} = \%hash;
    push(@{$OPTIONS{plugin_order}{$plugin}}, $option);
    $DEFAULTS{"$plugin:$option"} = $hash{default};
    # We've probably already called get_defaults() already
    $GLOBAL_OPT->{"$plugin:$option"} = $hash{default};
  } else {
    $OPTIONS{album}{$option} = \%hash;
    push(@{$OPTIONS{album_order}}, $option);
    $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;	# Copy defaults
  my $opt = \%opt;
  $GLOBAL_OPT = $opt;	# Global for add_option plugins.  Shh! Don't tell anyone...

	$opt->{PROGNAME} = $PROGNAME;	# For plugins, mostly
	$opt->{BASENAME} = $BASENAME;

  # 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();

  my $plugin = $option =~ /plugin/ ? get_plugin($opt,$option,$val) : undef;

  # 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($_)); } all_options($opt,$plugin);
  $maxw-=1;
  $maxw=min($maxw,17);

  print "Plugin Options [$plugin]:\n" if $plugin;

  # Show usage for each option
  my $dashdash=0;
  foreach my $option ( all_options($opt,$plugin) ) {
    my $optinfo = get_option($opt,$option,$plugin);
    my $type = $optinfo->{type};
    my $default = $optinfo->{default};
    next unless $optinfo->{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 .= " $optinfo->{args}" if $optinfo->{args};
    $opt = "-$opt" if $type==OPTION_ARR;
    $dashdash++ if $type==OPTION_ARR;
    my $usage = $optinfo->{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 --

You can remove specific array options with -no_<option>:

% album -no_exif hi

Or clear all the array options with -clear_<option>:

% album -clear_exif

START_POD

  my (@bool_opts, @str_opts, @arr_opts);
  foreach my $option ( all_options($opt) ) {
    my $optinfo = get_option($opt,$option);
    next unless $optinfo->{lvl}<=10;
    push(@bool_opts, $option) if $optinfo->{type}==OPTION_BOOL;
    push(@str_opts, $option) if $optinfo->{type}==OPTION_STR;
    push(@arr_opts, $option) if $optinfo->{type}==OPTION_ARR;
  }

  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 ( all_options($opt) ) {
    my $optinfo = get_option($opt,$option);
    next unless $optinfo->{lvl}<=10;
    my $type = $optinfo->{type};
    my $default = $optinfo->{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 = $optinfo->{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 = $optinfo->{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 on $^O";
    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 STDERR <<COPYRIGHT;

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

COPYRIGHT
  exit 0;
}

#########################
# Parse command line
#########################
# Lookup an option val (check in current plugin options first)
sub option {
  my ($opt,$option) = @_;

  # Plugin?
  my $plugin = $opt->{_curr_plugin};
  my $plugin_option = "$plugin:$option";
  my $optinfo = get_option($opt,$option,$plugin);
  return $opt->{$plugin_option} if $plugin && $optinfo;

  $opt->{$option};
}

sub deprecated_option {
  my ($opt, $option, $val) = @_;
  return unless !$MAIN::DEPRECATED_OPTION{$option}++;

  my $err = "Deprecated option ignored: [-$option].";

  my $optinfo = get_option($opt,$option);
  print STDERR "WARNING: Deprecated option ignored: [-$option]\n";
  print STDERR "         See [-$optinfo->{args}] option instead\n" if $optinfo->{args};
}

# Get option info hash for an option (for an optional plugin)
sub get_option {
  my ($opt,$option,$plugin) = @_;

	#$option =~ s/(clear|no)[_-]?//;	# Doesn't quite work in save_conf

  if ($plugin) {
    # Make sure we've loaded the plugin and get the full plugin name
    $plugin = get_plugin($opt,$option,$plugin);
    return 0 unless $plugin;
    return $OPTIONS{plugin}{$plugin}{$option};
  }

  return $OPTIONS{album}{$option} if $OPTIONS{album}{$option};

  # Might be a -plugin:option
  return 0 unless $option =~ /([^:]+):(.+)/;
  get_option($opt,$2,$1);
}

# Expand the full option string (expand plugin name if needed)
# Call as:  full_option($opt,$optinfo) or full_option($opt,$option)
sub full_option {
  my ($opt,$option) = @_;
  my $optinfo = (ref $option eq 'HASH') ? $option : get_option($opt,$option);
  my ($option_str,$plugin) = ($optinfo->{option}, $optinfo->{plugin});
  $plugin ? "$plugin:$option_str" : $option_str;
}

# Give me a list of all the options by added order (optionally for a plugin)
sub all_options {
  my ($opt, $plugin) = @_;
  return @{$OPTIONS{album_order}} unless $plugin;
  $OPTIONS{plugin_order}{$plugin} ?  @{$OPTIONS{plugin_order}{$plugin}} : ();
}

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 deal with --option and -no_option and -clear_option)
sub parse_arg {
  my ($opt,$Option,$val, $from) = @_;

  # Is it a -no_option or -clear_option or a --option?
  my $clear = ($Option =~ s/^clear[_-]?//) ? 1 : 0;
  my $dashdash = ($Option =~ s/^-//) ? 1 : 0;
  my $no = ($Option =~ s/^no[_-]?//) ? 1 : 0;
  usage("Unnecessary '--' in options") if $dashdash && !$Option;

  return if $no && $clear;	# That's just silly.

  # Get option info
  my $optinfo = get_option($opt,$Option);
  usage("Unknown ".($from||"command-line option").":\n\t  -$Option")
    unless $optinfo;
  my $option = full_option($opt,$optinfo);
  my $type = $optinfo->{type};

  print STDERR "[CONF] $option ($val, $no) '$from'\n" if $from && $opt->{d};
  print STDERR "[ARGV] $option ($val, $no, $ARGV[0])\n" if !$from && $opt->{d};

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

  my @vals;	# In case it's a multi-val with dashdash

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

  } elsif ($type==OPTION_BOOL) {
    # Booleans are either on or off
    $val = 1 unless defined $val;
    $val = 0 if $val =~ /^(off|no)$/i;
    $val = ($no||$clear) ? ($val ? 0 : 1) : ($val ? 1 : 0);
    undef $no;  # We only keep $no for array options, here we just change $val
    $opt->{$option} = $val;

  } elsif ($clear) {
    # Keep track of which options were cleared
    $opt->{_argv}{clear}{$option} = 1;

    # And clear..
    undef $opt->{$option};
      # Should this return to default??
      # Mostly no (we need to be able to override default!)

    # As well as _argv (if for some reason they've specified this
    # option already on the command-line and changed their mind..)
    undef $opt->{_argv}{vals}{$option};
    @{$opt->{_argv}{order}} = grep($_ ne $Option, @{$opt->{_argv}{order}});

  } elsif (ref $type eq 'CODE') {
    # Handle actions (code ref type)
    my $code = $type;

    if ($optinfo->{args}) {
      # We need an arg
      usage("Missing argument for $from option: $option")
        unless defined $val || !$from;
      $val = shift @ARGV unless defined $val;
    } else {
      # We only keep $no when args are used, here we just change $val
      $val = $no ? 0 : 1;
      undef $no;
    }

    # No?
    if ($val && !$no) {
      my $save_curr_plugin = $opt->{_curr_plugin};
      $opt->{_curr_plugin} = $optinfo->{plugin};

      # Call the routine
      $code->($opt,$option,$val);

      $opt->{_curr_plugin} = $save_curr_plugin;

      # Kind of kludgy, don't save deprecated options
      next if $code eq \&deprecated_option;
    }

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

    usage("Missing value for option $Option") unless @vals;

    # Do we care about 'no_'?
    push(@{$opt->{$option}}, @vals) unless $no;

  } else {
    # Strings or numbers
#usage("-no_$option doesn't make sense") if $no;
    if ($no) {
      delete $opt->{$option};
    } else {
      $val = shift @ARGV unless $from || defined $val;
      usage("-$option needs an argument") unless $from || defined $val;
      usage("Bad -$option number [$val]") if $val=~/[^\d\.]/ && $type==OPTION_NUM;
      $opt->{$option} = $val;
    }
  }

  unless ($from || $clear) {
    # Keep track of which args were on the command-line to override conf opts
    push(@{$opt->{_argv}{order}}, $option) unless $no;

    # And keep track of the values for save_conf
    if (multival_option($opt,$option)) {
      push(@vals, $val) unless @vals;
      my $what = $no ? 'no' : 'vals';
      push(@{$opt->{_argv}{$what}{$option}}, @vals);
    } else {
      # We don't want to do it this way anymore for -no.., because we want
      # to allow for -no_string (such as -no_theme) without forcing
      # the string option to be '0'  Boolean options will set $val to 0.
      #$opt->{_argv}{vals}{$option} = defined $val ? $val : 0;
      $opt->{_argv}{vals}{$option} = $val;
      $opt->{_argv}{no}{$option} = 1 if $no;
      $opt->{_changed}{$option} = 1;
    }
  }

  # Keep track of *all* set options (override theme opts)
  $opt->{_set}{$option} = 1;
}

# Did an option 'change' on the ARGV?  (Used by plugins and others)
sub option_changed {
  my ($opt,$option) = @_;
  my $full_option = full_option($opt,$option);

  # We use {_changed} for non multival,
  return $opt->{_changed}{$full_option}
    unless multival_option($opt,$full_option);

  # but {_argv} for multival (too complex..)
  defined $opt->{_argv}{vals}{$full_option} ? 1 : 0;
}

# Which args can be called multiple times in a meaningful way?
# Either array type, or code types with args.
# (Assume that plain code types don't need to be called mult times)
sub multival_option {
  my ($opt,$option) = @_;
  my $optinfo = get_option($opt,$option);
  return 0 if $optinfo->{singleval};	# Allow for 'singleval' hint.
  my $type = $optinfo->{type};
  return 1 if $type==OPTION_ARR;
  return 1 if ref($type) eq 'CODE' && $optinfo->{args};
  0;
}

# Is this an option/val that needs to be ignored because it will override
# what we saw in argv?  (argv takes precedence over the top album.conf)
sub defer_to_argv {
  my ($opt,$option,$val, $what) = @_;

  $what = $what || '_argv';

  # Any cleared options are always "deferred"
  return 1 if $opt->{$what}{clear}{$option};

  # Was this option even in argv?
  if (defined $opt->{$what}{vals}{$option}) {
    # non-multival options (boolean,num,str,CODE<no argv>) are always deferred
    return 1 unless multival_option($opt,$option);

    # multival options are NOT deferred unless the value is in the argv array
    return 1 if contains($val, @{$opt->{$what}{vals}{$option}});
  }

  # Or was it a -no_option in argv?
  if (defined $opt->{$what}{no}{$option}) {
    # non-multival options (boolean,num,str,CODE<no argv>) are always deferred
    return -1 unless multival_option($opt,$option);

    # multival options are NOT deferred unless the value is in the argv array
    return -1 if contains($val, @{$opt->{$what}{no}{$option}});
  }
  0;
}

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

	# If on windows, C:/Photos is preferred over C:\Photos
  usage("Use '/' instead of '\\' in paths [$dir]")
  	if $opt->{windows} && $dir =~ /^[a-z]:\\/i;

  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..
# We could remove this ugliness, but we'd lose backwards compat.
# Long before we do that, we should make sure that these get saved like {_argv}
# Otherwise, we could still have a theme in an old album without an album.conf

  # But we can skip it if the theme is spec'd
  return if defer_to_argv($opt,'theme','ignored');

  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 get_option($opt,$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
#########################
# Read a config line
# Returns:  (option,val,whitespace,comment)
sub read_conf_line {
  my ($line) = @_;

  chomp($line);

  # Comments after '#' (that aren't quoted '\#') are ignored
  my $comment;
  $comment = $1 if $line =~ s/(\s*(?<!\\)#.*)//;

  return 0 unless $line =~ /\S/;

  # "option  val"
  return -1 unless $line =~ /^(\S+)((\s+)(\S.*?))?\s*$/;
  my ($option,$sp,$val) = ($1,$3,$4);

  # Values can be quoted
  $val = $1 if $val =~ /^"(.*)"$/ || $val =~ /^'(.*)'$/;
  $val =~ s/\\#/#/g;	# Unquote any '#'

  ($option,$val,$sp,$comment);
}

sub save_conf_line {
  my ($option,$val,$whitespace,$comment) = @_;

  $whitespace = $whitespace || "\t";

  # Quote value if it has whitespace
  $val = "'$val'" if $val =~ /\s/;

  # Convert '#' to '\#'
  $val =~ s/#/\\#/g;

  return "$option$whitespace$val\n" unless $comment;
  "$option$whitespace$val\t$comment\n";
}

sub read_conf {
  my ($opt,$option,$conf,$defer_to_argv) = @_;

  return if $opt->{_read_conf}{$conf};

  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/?|| if $opt->{topdir};
  $conf_str =~ s|^$ENV{HOME}|~| if $ENV{HOME};
  print STDERR "Read conf: $conf_str\n" unless $opt->{q};
  $opt->{_read_conf}{$conf} = 1;	# We read a conf file

  while (<$fh>) {
    my ($option,$val) = read_conf_line($_);
    if ($option==-1) {
      print STDERR "[$PROGNAME] Can't understand conf: [$conf_str, line $.]\n  $_";
      next;
    }
    next unless $option;

    # Don't parse if it's an option for a plugin that had -no_plugin
    next if no_plugin_option($opt,$option,$val);

    # Get the dealio on the option (load plugins as needed)
    my $optinfo = get_option($opt,$option);

    usage("Unknown album conf option:\n\t  -$option") unless $optinfo;
    my $full_option = full_option($opt,$optinfo);

    # We don't parse if it's an argv option and $defer_to_argv
    if ($defer_to_argv && defer_to_argv($opt,$full_option,$val)) {
      next if multival_option($opt,$full_option);
      # Did the option change?  Keep track in {_changed}.
      # We only do this for non-multival options (because otherwise
      # we'd need to keep track of the list of multivals, which would suck).
      # This is good enough to avoid the common problem of users re-specifying
      # options that haven't changed, such as -medium, which would have a
      # huge performance penalty.
      $opt->{_changed}{$full_option} =
        ($opt->{_argv}{vals}{$full_option} eq $val) ? 0 : 1;
      next;
    }

    parse_arg($opt,$option,$val, "conf option [$conf_str]");
  }
  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,$conf, $what) = @_;

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

  $what = $what || '_argv';
  my $order = $opt->{$what}{order};	# Either {_argv}{order} or {_virgin}{order}
  my $save = $opt->{$what}{vals};	# Either {_argv}{vals} or {_virgin}{vals}
  my $no = $opt->{$what}{no};		# Either {_argv}{no} or (unused)

  my @new;	# The new conf lines
  my $new_comment = "\t# command-line saved option";
  my %saw;
  if (open(CONF,"<$conf")) {
    while (<CONF>) {
      my ($short_option,$conf_val,$sp,$comment) = read_conf_line($_);

      # Not an option line
      unless ($short_option) {
        push(@new,$_);
        next;
      }

      # We skip any options for plugins that were turned off.
      if (no_plugin_option($opt,$short_option,$conf_val)) {
        $changed++;
        next;
      }

      # Get the dealio on the option (the plugins should be loaded, though)
      my $optinfo = get_option($opt,$short_option);
      fatal($opt,"Internal error!! [$short_option, 1]\nsave_conf() doesn't recognize an option that read_conf did??") unless $optinfo;
      my $option = full_option($opt,$optinfo);

      $saw{$option}++;

      my $defer = defer_to_argv($opt,$option,$conf_val, $what);

      unless (defined $save->{$option} || defined $no->{$option} || $defer) {
        push(@new,$_);
        next;
      }

      # This is an option we specified on the command line

      # Non-array options are simpler, deal with them first.
      unless (multival_option($opt,$option)) {
        # Has the option changed?
        if ($conf_val ne $save->{$option}) {
          push(@new, "$short_option\t$save->{$option}$comment\n");
          $changed++;

        # Was it a -no_option?
        } elsif (defined $no->{$option}) {
          $changed++;

        } else {
          push(@new,$_);
        }
        next;
      }

      if ($saw{$option}==1) {
        # It's the first sighting of an array option.  Do saved options first.
        # (since they got processed before the conf file...
        #  a little counter-intuitive, but this is an ordering problem)
        foreach my $val ( @{$save->{$option}} ) {
          push(@new, save_conf_line($short_option,$val,"\t",$new_comment));
          $changed++;
        }
      }

      # Then the conf value, unless it was deferred.
      $changed++ if $defer;
      push(@new,$_) unless $defer;
    }
    close CONF;
  } else {
    # Creating a new conf file..  (if there are changes)
    push(@new, "#\n# Configuration file automatically created by $PROGNAME\n#\n");
    $new_comment = "";
  }

  # Finally, take care of any options that we didn't already see.
  foreach my $short_option ( @{$order} ) {
    # Get the dealio...  yeah, yeah.
    my $optinfo = get_option($opt,$short_option);
    fatal($opt,"Internal error!! [$short_option, 2]\nsave_conf() doesn't recognize an option that parse_arg did??") unless $optinfo;
    my $option = full_option($opt,$optinfo);

    # Not if we saw it already or it's a one-timer.
    next if $saw{$option};
    next if $optinfo->{one_time};

    # Get the value and print it out.
    my @val = ref $save->{$option} eq 'ARRAY' ? (@{$save->{$option}}) : ($save->{$option});
    map {
      push(@new, save_conf_line($short_option,$_,"\t",$new_comment));
      $changed++;
    } @val;
  }

  return unless $changed;

  return print STDERR "[$PROGNAME] ERROR: Couldn't write conf: [$conf]\n"
    unless (open(CONF,">$conf"));
  print CONF @new;
  close 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);
  1;
}

sub pop_opts {
  my ($opt) = @_;
  # Arguably I should 'pop' the add_option calls from plugins that get popped
  %$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 'CODE') {
    # We see CODE refs for plugin hooks - just return the code ref.
    return $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 (contains($key, qw(_argv _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 $pushed = 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;
      $pushed = push_opts($opt) unless $pushed;
      read_conf($opt,undef,$conf,1);
    }
  }

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

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

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

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

  $opt->{d}=1 if $opt->{D};

  # 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 (see 'is_movie()')
    $IMAGE_TYPES.="|AVI|MOV|MOOV|MP4" 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};

  # Old versions of album saved '\>' in medium options, but that's broke now.
  $opt->{medium} =~ s/\\//g;

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

  # -caption_edit needs themes
# Maybe this should be in get_themes?  Bah..
  usage("Can't use -caption_edit without a theme")
    if $opt->{caption_edit} && !$opt->{theme};

  # -burn doesn't use -theme_url
  usage("Can't specify -theme_url with -burn")
    if $opt->{burn} && option_changed($opt,'theme_url');

  # Check -crop option (though somewhat deprecated)
  usage("-CROP must be top, bottom, left or right")
    if ($opt->{CROP} && $opt->{CROP} !~ /^(top|bottom|left|right)$/);

  # Allowed sort types
  usage("Unknown sort option: $opt->{sort}")
    unless grep($opt->{sort} eq $_, qw(captions name date));
}

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

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

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

  # -theme_url needs -theme
  hash_warn($opt,"WARNING: -theme_url still requires -theme option, it doesn't replace it")
    if option_changed($opt,'theme_url') && $opt->{theme_url} && !$theme && !$MAIN::WARNED_THEME_URL++;

  # -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)
    }
  }

  # 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;

  $dir = port_abs_path($opt,$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) = @_;

  return unless $opt->{virgin_check};

  # 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
  $opt->{_virgin}{vals} = {};
  $opt->{_virgin}{order} = [];

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

  # DATA PATH (install plugins)
  # We don't need to do anything if the plugins are already in data_path
  # (use search_path_exec to handle windows slash and '~')
  my ($path,$found) = search_path_exec($opt,'plugins', @DATA_PATH);
  if ($found) {
    print STDERR "- Plugins already installed correctly\n\n";
  } else {
    my $plugins = install_find_current_data($opt,"plugins");
    if ($plugins) {
      print STDERR "\nI see that you haven't installed plugins yet.  Would you like to do that now?\n";
      if (install_get_yn($opt,'y')) {
        my $data = install_get_menu_path($opt,"data path for installing plugins",
          "I'm going to install your plugins, pick a data path to install into:",@DATA_PATH);
        if ($data) {
          unless (install_move($opt,$plugins,$data)) {
            print STDERR "\nCouldn't install plugins!\n";
            print STDERR "Please login as a user with the appropriate privileges and copy:\n";
            print STDERR "  $plugins -> $data\n\n";
            install_hit_enter($opt);
          }
        } else {
          print STDERR "\nCancelled install.\n";
          print STDERR "Move the plugin directory into your data path to install later\n\n";
          install_hit_enter($opt);
        }
      }
    }
  }

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

  # THEME PATH (not necessarily @DATA_PATH because it needs to be URL accessible)
  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_data($opt,"Themes");
          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";
            install_hit_enter($opt);
          }
        }
      }
    }
    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 (in some cases).
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.

If you run album on a local machine and then upload your album to your
web server, then you will need to upload the themes as well, this is
where you would specify the URL to the directory with the themes in it.

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->{_virgin}{vals}{theme_url} = $opt->{theme_url};
    push(@{$opt->{_virgin}{order}}, '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";

  # Track the configuration file version
  $opt->{_virgin}{vals}{conf_version} = $VERSION;
  $opt->{_virgin}{vals}{conf_version} =~ s/[^\d\.]//g;	# Just the numbers
  push(@{$opt->{_virgin}{order}}, 'conf_version');

  # Save it.
  save_conf($opt,$conf, '_virgin');
  undef $opt->{_virgin};

  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 $found = search_path_exec($opt,$what,@path);
    return print STDERR "Found $what in path:  $found\n" if -x $found;
  }

  while (1) {
    print STDERR "\nCan't find $what executable.\n";

    if ($optional) {
      print STDERR "This is optional but is useful to $PROGNAME.\n";
      print STDERR "Do you want to enter a path to $what?\n";
      return unless install_get_yn($opt, 'n');
    } else {
      print STDERR "I need to know where this is for $PROGNAME to run properly.\n";
    }

    $try = install_get_path($opt,"path to $what");

    return if $optional && !$try;
    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->{$what} = $try;
  $opt->{_virgin}{vals}{$what} = $opt->{$what};
  push(@{$opt->{_virgin}{order}}, $what);
}

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 = '/usr/share/album/themes' unless -x $themes;
    foreach my $dp ( @DATA_PATH ) {
      $themes = "$dp/themes" unless -x $themes;
      $themes = "$dp/Themes" unless -x $themes;
      last if -x $themes;
    }
    $themes = '' unless -x $themes;
    $themes .= "/Themes" if $themes && $themes !~ m|/themes$|i && -x "$themes/Themes";
    $themes .= "/themes" if $themes && $themes !~ m|/themes$|i && -x "$themes/themes";
  }

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

  print STDERR "- You should 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_path($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.

This is the path to the themes on the machine that you run album on, not
a URL.  A good location is a directory inside the web root, something like:
  $example

Just hit return if you don't want themes.
THEME_PATH

  return unless $themes;
  push(@{$opt->{theme_path}}, $themes);
  push(@{$opt->{_virgin}{vals}{theme_path}}, $themes);
  push(@{$opt->{_virgin}{order}}, 'theme_path');
  $themes;
}

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


  # Is it still zipped up?
  my $targz = "${PROGFILE}_$what.tar.gz";
  my $tar   = "${PROGFILE}_$what.tar";
  if (-f $targz && !-d $What) {
    print STDERR "\nI found a gzipped $What 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 xf $tar") unless $?;
        system("tar", "xzf", $targz);
      }
      return $What unless $? || !-d $What;
      print STDERR $? ? "\nError untarring: $!\n\n" :
        "\nError: Untarring didn't create expected directory [$What]\n\n";
    }
  }

  my $zip = "${PROGFILE}_$what.zip";
  if (-f $zip && !-d $What) {
    print STDERR "\nI found a zipped $What archive: $zip\n\nWould you like me to try to unzip it?\n";
    if (install_get_yn($opt,'y')) {
      system("unzip $zip");
      return $What unless $? || !-d $What;
      print STDERR $? ? "\nError unzipping: $!\n\n" :
        "\nError: Unzipping didn't create expected directory [$What]\n\n";
    }
  }

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

sub install_get_menu {
  my ($opt,$question,$desc,@menu) = @_;

  my $items = $#menu+1;

  my $cnt = 0;
  while (1) {
    unless ($cnt++ % 5) {
      # Show menu at beginning and every 5 mistakes
      print STDERR "\n";
      print STDERR "$desc\n" if $desc;
      for (my $i=0; $i<$items; $i++) {
        printf STDERR "%2d] $menu[$i]\n",$i+1;
      }
      print STDERR "\n";
    }

    # Get answer
    my $ans = install_get_answer($opt,$question." [1-$items, 0 to cancel]", "");
    return undef if $ans eq "0";
    return $menu[$ans-1] if $ans>=1 && $ans<=$items;
  }
}

sub install_get_menu_path {
  my ($opt,$question,$desc,@menu) = @_;
  my $path = install_get_menu($opt,$question,$desc,@menu);
  $path =~ s|^~/|$ENV{HOME}/| if $ENV{HOME};
  $path;
}

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" if $desc;
  my $ret;
  while (!defined $ret) {
    print STDERR "Enter $question";
    print STDERR " [default: $default]" if $default;
    print STDERR "> ";
    my $line = scalar <STDIN>;  chomp($line);
    $ret = $line eq "" ? $default : $line;
  }
  $ret;
}

sub install_get_path {
  my ($opt,$question,$default,$desc) = @_;
  my $path = install_get_answer($opt,$question,$default,$desc);
  $path =~ s|^~/|$ENV{HOME}/| if $ENV{HOME};
  $path;
}

sub install_hit_enter {
  my ($opt) = @_;
  print STDERR "Press enter/return to continue ";
  scalar <>;
}

##################################################
##################################################
# UTILITIES
##################################################
##################################################
sub max { $_[0]>$_[1] ? $_[0] : $_[1]; }
sub min { $_[0]<$_[1] ? $_[0] : $_[1]; }
sub contains { my $w = shift(@_); grep($w eq $_, @_) ? 1 : 0; }

#########################
# Stupid privoxy bug.
# See mozilla/bugzilla #98118
#########################
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) = @_;

  my $val = do_hook($opt,undef, 'port_abs_path', $dir);
  # Description: Calculates absolute path for a directory.
  # Description: Useful if you can't use 'Cwd::abs_path' or chdir().
  # Returns: Path.
  return $val if defined $val;

# Do we care about handling '~' here??
  -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;

  if ($WINDOWS && !$CYGWIN) {
    my $fdrive = ($from =~ s|^(\w):||) ? $1 : undef;
    my $tdrive = ($to =~ s|^(\w):||) ? $1 : undef;
    # If one is missing a drive spec, assume it's the same drive..
    # But if they both have drive spec and one's diff, use full path
    return "${tdrive}:$to" if $tdrive && $fdrive && $tdrive ne $fdrive;
  }

  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);
}

# Expand '@DATA_PATH' in a given path
sub expand_path {
  my ($opt, $path) = @_;
  return $path unless $path =~ /(.*)\@DATA_PATH(.*)/;
  my ($pre,$post) = ($1,$2);
  my @dirs;
  foreach my $dp ( @DATA_PATH ) {
    push(@dirs, $pre.$dp.$post);
  }
  @dirs;
}

# Returns path and result.
#   result=2 if found in @dir
#   result=1 if found elsewhere (absolute or relative path)
#   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|^~/(.*)| && $ENV{HOME};

  # Check @dir
  foreach my $dir ( @dir ) {
    $dir =~ s/$slash$//;
    $dir =~ s|^~/|$ENV{HOME}/|;

    # Expand '@DATA_PATH' in path
    foreach my $d ( expand_path($opt, $dir) ) {
      return ("$d$slash$path",2) if $test->("$d$slash$path");
    }
  }

# This is trouble - we might run album from a different path next time
#  # Check relative path
#  return ($path,1) if $test->($path);

  ($path, 0);
}

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

sub search_path_exec {
  my ($opt,$path,@dir) = @_;
  my ($p,$r) = search_path_test($opt, sub {-x $_[0]}, $opt->{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)
# Also handles -burn (use "index_page($opt)" instead of "$opt->{index}")
#########################
sub index_page {
  my ($opt, $file, $dir) = @_;
  $dir .= '/' if $dir;
  my $index = $opt->{index};
  return $dir unless $index || $file || $opt->{burn};
  $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};

  my $val = do_hook($opt,undef, 'start_hashes', $str);
  # Description: Start a progress meter with label 'str'
  # Returns: 1 if you don't want normal hashes printed
  return $val if defined $val;

  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) = @_;

  my $val = do_hook($opt,undef, 'show_hashes', $done, $outof);
  # Description: Update the progress meter
  # Returns: 1 if you don't want normal hashes printed
  return $val if defined $val;

  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) = @_;

  my $val = do_hook($opt,undef, 'stop_hashes');
  # Description: Finish the progress meter
  # Description: (You should hook all or none of the *_hashes hooks.)
  # Returns: 1 if you don't want normal hashes printed
  return $val if defined $val;

  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) = @_;

  my $val = do_hook($opt,undef, 'hash_warn', @msgs);
  # Description: Show a warning/error message over a progress meter
  # Returns: 1 if you don't want the normal hash_warn code to be used.
  return $val if defined $val;

  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;
}

##################################################
##################################################
# PLUGIN CODE
##################################################
##################################################
# Get a plugin.  Full path or short path, with or without extension
# Examples:  caption/paypal/oneprice.alp
#            caption/paypal/oneprice
#            oneprice.alp
#            oneprice
sub find_plugin {
  my ($opt, $plugin, $list) = @_;
  return unless $list;

  # Exact match?
  return $plugin if contains($plugin,@$list);

  # Try to find it by name
  my @matches = grep(/\b$plugin$/, @$list);
  return @matches if @matches;

  # And actually, we'll allow basic regex matching as a last resort
  grep(/\b$plugin/, @$list);
}

# Is this a plugin option that we need to ignore?
sub no_plugin_option {
  my ($opt,$option,$val) = @_;

  # It's either -plugin or a plugin option:
  my $plugin;
  if ($option eq "plugin") {
    $plugin = $val;
  } elsif ($option =~ /([^:]+):(.+)/) {
    $plugin = $1;
  } else {
    return 0;
  }

  return 1 if $opt->{_argv}{clear}{plugin};

  # Does this match a -no_plugin?  If so, don't load.
  # Allow plugin-regex matching
  $plugin =~ s/$opt->{plugin_post}$//;
  return grep($plugin =~ /\b$_/, @{$opt->{_argv}{no}{plugin}}) ? 1 : 0;
}

sub get_plugin {
  my ($opt,$option,$plugin) = @_;

  return 0 unless $plugin;

  # Remove postfix
  $plugin =~ s/$opt->{plugin_post}$//;

  # Do we already know about this plugin?  (Already loaded?)
  my @matches = find_plugin($opt,$plugin,$opt->{_plugins}{loaded});
  print STDERR "[$PROGNAME] WARNING: Multiple plugins match [$plugin]\n"
    if $#matches>1;
  return $matches[0] if $#matches==0;

  # Search the path
  my @path = @{$opt->{plugin_path}};
  my ($path,$r) = search_path_test($opt, sub {-r $_[0] && !-d $_[0]}, '/',"$plugin$opt->{plugin_post}",@path);
     ($path,$r) = search_path_test($opt, sub {-r $_[0] && !-d $_[0]}, '/', $plugin,@path) unless $r;
  if ($r) {
    if (use_plugin($opt,$path,$plugin)) {
      push(@{$opt->{_plugins}{loaded}}, $plugin);
      return $plugin;
    }
    print STDERR "[$PROGNAME] WARNING: Couldn't load plugin [$plugin]\n";
    return 0;
  }

  # Kludgy allowance (mostly for my testing) - remove '^plugins/'
  return get_plugin($opt,$option,$plugin) if $plugin =~ s|^plugins/||;

# Perhaps we should search 'all_plugins' without loading them?
# Otherwise, we currently require the full plugin path the first time.
  # but if we don't require the full path, we can have namespace collisions..
# Maybe a 'find_plugins' that would return an array of plugins without use_plugin?
  print STDERR "[$PROGNAME] WARNING: Couldn't find 'plugin' [$plugin]\n";
  return 0;
}

sub all_plugins {
  my ($opt,$path,$cat) = @_;

  # Expand @DATA_PATH in path
  foreach my $epath ( expand_path($opt, $path) ) {
    my $dir = $cat ? "$epath/$cat" : $epath;

    opendir(DIR,"$dir") || return;
    my @dir = readdir(DIR);
    closedir(DIR);
    my @p = grep(/\Q$opt->{plugin_post}\E$/, @dir);
    my @d = grep(-d "$dir/$_" && !/^\.{1,2}$/, @dir);
    map( use_plugin($opt,"$dir/$_",$cat?"$cat/$_":$_,1), @p);
    map( all_plugins($opt,$epath,$cat?"$cat/$_":$_), @d);
  }
}

sub use_plugin {
  my ($opt,$path,$plugin,$quiet) = @_;

  # Add '.' to @INC if it doesn't have it and plugin path is relative
  push(@INC,'.') unless $path =~ m|^/| || contains('.', @INC);

  print STDERR "[$PROGNAME] Using plugin: $plugin\n" unless $opt->{q} || $quiet;
  # Args to the require (this isn't kludgy, is it??  :)
  @_ = ($opt);		# Actually this is what @_ already was...

## I hate eval.  Here's a non-eval version, but it can't
## properly namespace the plugin or catch errors.  :(
#  {
#    package Album_Plugin;
#    my $req = require "$path";
#  }

  # So here's the eval version.
  my (undef,$package) = split_path($opt,$path);
  my $package = ($package =~ /^([a-z_]+)/i) ? $1 : "Album_Plugin";
  my $save_curr_plugin = $opt->{_curr_plugin};
  $opt->{_curr_plugin} = $plugin;
  eval <<END_EVAL;
    package $package;
    require "$path";
    \$opt->{_plugin}{\$plugin} = start_plugin(\$opt,\$plugin,\$path);
    #\$sym = \\\%${package}::;
END_EVAL
  $opt->{_curr_plugin} = $save_curr_plugin;

  if ($@) {
    print STDERR "[$PROGNAME] Warning: Problem with plugin [$plugin] - not using:\n$@\n";
    delete $opt->{_plugin}{$plugin}; return 0;
  }

  unless (ref $opt->{_plugin}{$plugin} eq 'HASH' && $opt->{_plugin}{$plugin}{description}) {
    print STDERR "[$PROGNAME] Warning: Plugin did not return correct info hash.\n\tNot using: $plugin\n$@\n";
    delete $opt->{_plugin}{$plugin}; return 0;
  }

  1;
}

sub list_plugins {
  my ($opt, $option, $plugin) = @_;

  # One plugin info
  my @plugins;
  if ($option eq 'plugin_info') {
    @plugins = get_plugin($opt,$option,$plugin);
    usage("No plugin found: $plugin") unless $plugins[0]
  } else {
    map(all_plugins($opt, $_), @{$opt->{plugin_path}});
    @plugins = keys %{$opt->{_plugin}};
    usage("No plugins found.  Install plugins in the data_path first")
      unless @plugins;
  }

  # All plugin info
  foreach my $plugin ( @plugins ) {
    my $info = $opt->{_plugin}{$plugin};

    if ($option =~ /crf/) {
      print "PLUGIN: $plugin;;$info->{version};;$info->{author};;$info->{href}\n";
    } else {
      print "-"x length($plugin),"\n";
      print "$plugin";
      print " v$info->{version}" if $info->{version};
      print ":\n";
      print "-"x length($plugin),"\n";

      print "Author:  $info->{author}";
      print " <$info->{href}>" if $info->{href};
      print "\n\n";
    }

    print "$opt->{_plugin}{$plugin}{description}\n";
    print "\n";
  }
  exit;
}

# Register a hook for a given hook name
sub hook {
  my ($opt,$name,$sub) = @_;
  push(@{$opt->{_plugin_hooks}{$name}}, [$sub,$opt->{_curr_plugin}]);
}

# Call some hooks
# $one: Do hooks until a single value found, return that
# $reinject: Do hooks, use the value from previous hook as next arg
#   (Actually, $reinject-1 will choose which arg to reinject)
# Don't use this to define a specific hook, use one of the do_hook* subs below.
sub do_hooks_loop {
  my ($opt,$data,$one,$reinject,$name,@args) = @_;
  my $save_curr_hook = $opt->{_curr_hook};
  my $save_curr_plugin = $opt->{_curr_plugin};
  $opt->{_curr_hook} = $name;
  my $ret = 0;
  my @ret;
  my $got_something = undef;
  foreach my $hook ( @{$opt->{_plugin_hooks}{$name}} ) {
    my ($sub,$from) = @$hook;
    $opt->{_curr_plugin} = $from;
    my @call_args = $data ? ($opt,$data,$name,@args) : ($opt,$name,@args);
    my @got = $sub->(@call_args);	# Do the hook
    if ($reinject) {
      $got_something = 1 if defined $got[0];
      $args[$reinject-1] = $got[0] if defined $got[0];
    } elsif ($one) {
      if (defined $got[0]) {
        $opt->{_curr_plugin} = $save_curr_plugin;
        $opt->{_curr_hook} = $save_curr_hook;
        return wantarray ? @got : $got[0];
      }
    } else {
      $ret |= $got[0];
      push(@ret,@got);	# Might be messy if we get arrays back..
    }
  }
  $opt->{_curr_plugin} = $save_curr_plugin;
  $opt->{_curr_hook} = $save_curr_hook;
  return $got_something ? $args[$reinject-1] : undef if $reinject;
  return undef if $one;
  wantarray ? @ret : $ret;
}

# Calls all hooks and returns either an array of return values or an 'or'
sub do_hooks {
  my ($opt,$data,$name,@args) = @_;
  do_hooks_loop($opt,$data,0,0,$name,@args);
}

# Calls hooks until one of them returns a defined value
sub do_hook {
  my ($opt,$data,$name,@args) = @_;
  do_hooks_loop($opt,$data,1,0,$name,@args);
}

# Calls all hooks and reinjects the returns as the first arg
sub do_hooksR {
  my ($opt,$data,$name,@args) = @_;
  do_hooks_loop($opt,$data,0,1,$name,@args);
}


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

  open(A,"<$0") || fatal($opt,"Couldn't read album script?? [$0]");
  my %hooks;
  while (<A>) {
    if (/do_hook(|s|sR)\(([^,]+),([^,]+),\s*'([^']+)'\s*(,(.*)\))?/) {
      my $hook = $4;
      my @args = ($2,$3,"'$hook'",$6);
      $hooks{$hook}{args} = ($args[1] =~ /^\s*undef\s*$/) ?
        "($args[0], $args[2], $args[3])" :
        "($args[0],$args[1], $args[2], $args[3])";
      while (<A>) {
        chomp;
        last unless /^\s*#\s*(Description|Returns):\s*(\S.*)?/i;
        push(@{$hooks{$hook}{lc($1)}}, $2);
      }
    }
  }
  close A;
  \%hooks;
}

sub list_hooks {
  my ($opt, $option, $just_hook) = @_;

  my $hooks = gather_hook_info($opt);
  my @hooks = sort keys %$hooks;

  if ($option eq "hook_info") {
    @hooks = grep($_ eq $just_hook, @hooks);
    @hooks = grep(/$just_hook/, sort keys %$hooks) unless @hooks;
    usage("No hooks found that match [$just_hook]") unless @hooks;
  }

  # Display hooks info
  foreach my $hook ( @hooks ) {
    print "Hook: $hook\n";
    foreach my $key ( qw(args description returns) ) {
      next unless $hooks->{$hook}{$key};
      print "  ".ucfirst($key).": ";
      print ((ref $hooks->{$hook}{$key} eq 'ARRAY') ?
        join("\n    ",@{$hooks->{$hook}{$key}})."\n" :
        "$hooks->{$hook}{$key}\n");
    }
    print "\n";
  }
  exit;
}

sub plugin_warn {
  my ($opt,$msg) = @_;
  $msg = "Plugin [$opt->{_curr_plugin}] hook [$opt->{_curr_hook}]:\n  WARNING: $msg";
  hash_warn($opt,$msg);
}

# For plugins to call if they don't do album generation (such as plugin utils)
sub done {
  # Currently nothing needed...
	exit(0);
}

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

  $name = $capname || $name;
  my $iscaption = $capname ? 1 : 0;

  my $val = do_hooksR($opt,undef, 'clean_name', $name, $iscaption);
  # Description: Clean a filename for printing.
  # Description: The name is either the filename or comes from the caption file.
  # Returns: Clean name
  return $val if defined $val;

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

  # 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 ($iscaption) {
    # Underbar = space
    $name =~ s/_/ /g;
    $name =~ s/\./ /g;

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

  $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 ) {
    next unless -f "$dir/$file";	# Don't bother with directories.
    my $remove;
    my $name = $file;
    my $html = index_page($opt) ? ('.'.index_page($opt)) : $opt->{html};
    if ($name =~ s/\Q$html\E$//) {
      $remove = "unused image page"
        unless ($opt->{image_pages} && contains($name, @pics));
    } elsif ($name  =~ /(.+)\.med\.(.+)/) {
      $name = "$1.$2";

      # Check for matching -medium_type:
      my $typematch = (!$opt->{medium_type} || $name =~ s/\.$opt->{medium_type}$//i);

      my $saw = contains($name, @pics);
      $saw = grep(/^\Q$name.\E(?i)$opt->{medium_type}$/, @pics)
        unless $saw || !$opt->{medium_type};

      $remove = "unused medium image"
        unless $opt->{medium} && $typematch && $saw;
    } elsif ($name  =~ /(.+)\.snap\.(.+?)(\.$opt->{type})?$/i) {
      $name = "$1.$2";
      $remove = "unused snapshot image" unless contains($name, @pics);
    } elsif ($name  =~ /(.+)\.$opt->{type}$/i) {
      # Thumbnail?
      $name = $1;
      $remove = "unused thumbnail"
        unless grep($_ eq $name || /^\Q$name\E.(?i)$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) {
      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 url_quote {
  my ($opt, $url) = @_;

  my $val = do_hook($opt,undef, 'url_quote', $url);
  # Description: Quote a path string, generally for a URL or CGI form field
  # Description: Generally the result should be in 'quotes' of some sort
  # Returns: Quoted path
  return $val if defined $val;

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

  # originally: $url =~ s/\'/%27/g;	# Just quotes

  # XHTML doesn't like [href='some<f>ile']
  $url =~ s/</%3C/g;  $url =~ s/>/%3E/g;

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

  "'$url'";
}

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

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

  my $val = do_hook($opt,undef, 'read_captions', $dir);
  # Description: Read captions for a given album directory.
  # Returns: hash reference.  Keys are filenames which point to caption hashes:
  # Returns: caption hash keys are name, cap, alt, and order (for sorting)
  plugin_warn($opt,"Didn't return a hash.")
    if $val && ref $val ne 'HASH' && !$MAIN::WARN_PLUGIN_READ_CAPTIONS++;
  return $val if defined $val && ref $val eq 'HASH';

  # $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 =~ s/#\s*/#/g;		# Allow '# commented files'

    $caps{$file}{name}=$name;
    $caps{$file}{cap}=$cap if $cap;
    # {alt} needs to be quoted because it's inside the <img> tag
    # Though we add the actual outer ['] later so that files without
    # captions can get them too (alt='')
    $alt =~ s/\&/&amp;/g;  $alt =~ s/\'/&rsquo;/g;
    $caps{$file}{alt}=$alt;
    $caps{$file}{order}=$.+1;
  }
  close CAPS;

  \%caps;
}

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

  my $val = do_hook($opt,$data, 'get_exif_info', $pic);
  # Description: Gets EXIF info for a picture
  # Returns: hash reference, key->val matches the EXIF key/value pairs
  plugin_warn($opt,"Didn't return a hash.")
    if $val && ref $val ne 'HASH' && !$MAIN::WARN_PLUGIN_GET_EXIF_INFO++;
  return $val if defined $val && ref $val eq 'HASH';

  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_info(): $_" if $opt->{D};
    if (/command not found/) {	# Kind of kludgy
      hash_warn($opt,"-exif specified but jhead not found [$opt->{jhead}]",
        "Either specify -jhead, or use one of the EXIF replacement plugins");
      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_caption {
  my ($opt,$data,$pic) = @_;

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

  # Only do it once per image (it will get saved in $opt->{_captions}{dir})
  return if $opt->{_done_exif}{$path}++;

  do_hooks($opt,$data, 'get_exif_caption', $pic, $dir);
  # Description: Creates EXIF captions for a picture.
  # Returns: None.
  # Returns: Optionally adds to $data->{obj}{$pic}{exif, exif_image, exif_album}
  # Returns: Call 'album::new_html($opt,$data,$pic)' if captions change.

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

  my $exif = get_exif_info($opt,$data,"$dir/$pic");
  return unless $exif;

  $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_caption($opt,$data,$pic);

    my $new = do_hook($opt,$data, 'modify_caption', $data->{paths}{dir}, $pic, $data->{obj}{$pic}{cap});
    # Description: Modify captions for each file
    # Returns: The new caption.
    $data->{obj}{$pic}{cap} = $new if defined $new;

    $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};
      delete $data->{obj}{$dir}{order};	# Line number is *not* used for sorting..
        # Hopefully this won't screw up any plugins that override read_captions,
        # they should 'find' the caption it in the parent directory anyways.
    }

    my $new = do_hook($opt,$data, 'modify_dir_caption', $data->{paths}{dir}, $dir, $data->{obj}{$dir}{cap});
    # Description: Modify captions for directories
    # Returns: The new caption.
    $data->{obj}{$dir}{cap} = $new if defined $new;

    $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}{order}) unless $opt->{sort} eq 'date';
  # 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->{sort} eq 'name') {
    $an = sort_rank($opt,$data,$a);
    $bn = sort_rank($opt,$data,$b);
  }

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

# 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 obj_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) = @_;

  my @pics = @{$data->{pics}};
  my @dirs = @{$data->{dirs}};

  my @np = do_hook($opt,$data, 'sort_pics', @pics);
  # Description: Sort pictures
  # Returns: The array of sorted pictures.

  my @nd = do_hook($opt,$data, 'sort_dirs', @dirs);
  # Description: Sort directories
  # Returns: The array of sorted directories.

  @{$data->{pics}} = defined $np[0] ? @np : sort { sort_order($opt,$data,$a,$b) } @pics;
  @{$data->{dirs}} = defined $nd[0] ? @nd : sort { sort_order($opt,$data,$a,$b) } @dirs;
  obj_count($opt,$data,'pics');
  obj_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);
    # 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);
    # last dircap is for our own dirctory
    $data->{dircap} = ($dir_cap ? $dir_cap->{cap} : undef);
  }

  # 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} = index_page($opt) ? ('.'.index_page($opt)) : $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} = url_quote($opt, index_page($opt,0,$child));
    }
  }
}

##################################################
##################################################
# 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.
#
# Takes dump options in the $dmp hash:
# $dmp->{ignore}		regex for fields to ignore
# $dmp->{ignore_only_under}	minimum level for checking {ignore}
#
# Returns the dump in the $dmp hash.
# $dmp->{lvl}			Keeps track of the internal dump level
# @{$dmp->{dump}}		Array of the dump output
#
# Simple calling example:
#   my $dmp;
#   dumper($dmp,'MyHash',%MyHash);
#   print @{$dmp->{dump}};
#########################
sub dumper {
  my ($dmp,$name,$var) = @_;

  my $ignore = 0;
  $ignore = 1 if $dmp->{ignore} && $name =~ /$dmp->{ignore}/;
  $ignore = 0 if $dmp->{ignore_only_under} && $dmp->{lvl} >= $dmp->{ignore_only_under};
  return if $ignore;

  my $ref = ref($var);
  return hash_warn($dmp->{opt},"Option or data [$name] was referenced 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) = @_;

  return unless $opt->{'album.th'} || $opt->{'image.th'};

  # 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)$';
  $dmp{ignore_only_under} = 4;	# Only ignore if below this level
  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!
##################################################
##################################################

  # 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));

  # Position of this line relative to start of SUPPORT is crucial
  my $start_line = __LINE__ + 4;
  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->{burn}) ? "\$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;
}

# Not quite the same as the url_quote above
sub url_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=".url_quote("HEADER:\$opt->{header}")."-->\\n" if \$opt->{caption_edit};
  print read_file("\$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=".url_quote("FOOTER:\$opt->{footer}")."-->\\n" if \$opt->{caption_edit};
  print read_file("\$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);
}

sub New_Row {
  my (\$obj,\$cols,\$offset) = \@_;
  my \$num = \$obj->{num};
  my \$new_row = !((\$num+1+\$offset) % \$cols) ? 1 : 0;
  return 0 unless \$new_row;
  return 0 unless Next(\$obj);
  1;
}

#########################
# 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 = "";
	my \$index = Option('index');
	\$index = \$index || Option('default_index') if Option('burn');
	\$str .= "<a href='". ("../"x\$dotdots). \$index. "'>" if \$dotdots;
  \$str .= Pretty(\$parent_albums->[\$num],1);
  \$str .= "</a>" if \$dotdots;
  \$str;
}

# Return an array or join of all the calls to Parent_Album
sub Parent_Albums {
  my (\$join) = \@_;
  # Someday just replace 'CNT' with 0
  my \@pa = map Parent_Album(\$_), \$PARENT_ALBUM_CNT..(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 {
	my \$index = Option('index');
	\$index = \$index || Option('default_index') if Option('burn');
  (num('parent_albums')>1 || Image_Page()) ?
    "'../".\$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}{...}
# Also see Overlay below
  \$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 style='border:0'";
  \$str .= " alt='\$obj->{alt}'";
  \$str .= " title='\$obj->{alt}'" if \$obj->{alt};
  \$str .= " width='\$obj->{\$type}{x}'" if \$obj->{\$type}{x};
  \$str .= " height='\$y'" if \$y;
  \$str .= " />";
  \$str .= \$obj->{\$type}{overlay};	# Optional overlay..
  \$str;
}

# Overlay an image on an image tag
# how values:  full, top-left, top-right, ...
# (only how=full is currently supported)
sub Overlay {
  my (\$pic, \$type, \$how, \$overlay) = \@_;
  \$type = Get(\$obj,'medium','x') ? 'medium' : 'full' if \$type ne 'thumb';
  return '' unless \$obj->{\$type}{x};

  my (\$oversrc,\$overx,\$overy) = \@\$overlay;
  my (\$x,\$y) = (\$obj->{\$type}{x}, \$obj->{\$type}{y});

  # Only how=full right now..
  \$obj->{\$type}{overlay} .= Image_Array(\$oversrc,\$x,\$y,"style='margin-left:-\${x}px;'");
}

# 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=..>
# If field is 'link' - then return <a href=url>name</a>
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';
  return Get(\$pic,'href',\$subfield,\$subsub).Name(\$pic).'</a>'
    if \$field eq 'link';

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

# Explanation given in the non-eperl subroutine
sub mysplit {
  my (\$str) = \@_;
  return split(//, \$str) unless \$str =~ /(<.*>|&.*;)/;
  my \@a;
  while (\$str =~ /(.*?)(<[^>]+>|&[^;]+;)(.*)/) {
    push(\@a,split(//,\$1),\$2); 
    \$str = \$3;
  }
  (\@a,split(//,\$str)); 
}

# Explanation given in the non-eperl subroutine
sub shorten {
  my (\$str,\$len) = \@_;

  return \$str if length(\$str) < \$len+3;
  my \@str = mysplit(\$str);
  return \$str if \@str <= \$len;
  my \@a = splice(\@str,0,\$len/2);
  my \@b = splice(\@str,-\$len/2,\$len/2);
  \@str = grep(m|^<.*>|, \@str);
  join('',\@a,\@str,"..",\@b);
}

# We can chop down extra long image names on the album page if needed
sub Name {
  my \$obj = get_obj(\@_);
  my \$n = \$obj->{name};
  ##I don't have a clue why this next line was here:
  #return "Back" unless \$n;
  return \$n if Image_Page() && !\$MAIN::FIRST_NAME_CLEAN++;	#Kludge!
  my \$s = (Image_Page() || !$opt->{name_length} || $opt->{caption_edit}) ? \$n : shorten(\$n,$opt->{name_length});
  return \$s if !$opt->{caption_edit};
  \$n = url_quote(\$n);
  my \$name = url_quote("NAME\$obj->{num}:".\$obj->{full}{file});
  \$s =~ s/\\n//mg;
  "<!--IMAGE_NAME name=\$name value=\$n-->\$s<!--END_IMAGE_NAME-->";
}

# Pretty format (for dates)
sub Pretty {
  my (\$str,\$html,\$lines) = \@_;

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

sub Caption {
  my \$obj = get_obj(\@_);
  my \$norm_caps = ($opt->{album_captions} || Image_Page()) ? 1 : 0;
  my \$cap = "";
  \$cap .= "<!--IMAGE_CAPTION name=".url_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 { print 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(\$_[0],'full'); }
sub Image_Src { Image(\$_[0],'full'); }
sub pImage_Thumb_Src { print Image(\$_[0],'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(\$obj,'medium','x') ?
    Get(\$_[0],'medium','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;	# No longer necessary
  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;
  # Kind of a kludge, avoid multiple declarations of style:
  \$str .= " style='border:0'" unless \$and =~ s/style='/style='border:0;/;
  \$str .= " \$and" if \$and;
  #\$alt = '-' unless \$alt;	# This is better for lynx, but...
  \$str .= " alt='\$alt'";
  \$str .= " title='\$alt'" if \$alt;
  \$str .= " />";
  \$str;
}

# Show an image using a repeating table background
# This is an internal album function - don't use this as it may change
sub Image_Repeat_td {
  my (\$rx, \$ry, \$src, \$x, \$y) = \@_;
	\$rx = \$rx || \$x;
	\$ry = \$ry || \$y;
  "<td width='\$rx' height='\$ry' background='\$src'></td>";
}
sub Image_Repeat {
  my (\$rx, \$ry, \$src, \$x, \$y) = \@_;
  \$rx = \$rx || \$x;
  \$ry = \$ry || \$y;
  #"<div style='float:left;width:\$rx; height:\$ry; background-image:url(\$src);
	"<table cellpadding=0 cellspacing=0 width='\$rx'><tr height='\$ry'><td background='\$src'></td></tr></table>";
}

# 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
#
# Constraints for 12 piece borders:
#   Same width:  LT,L,LB.  T,R,RB
#   Same height:  LT,RT.  LB,RB
#   Should be same height:  TL,T,TR.  BL,B,BR
#   height LT = height RT,  height LB = height RB
sub Border {
  # Either call with:
  #   (img_object, type, href, border_image_arrays)
  #   (img_string, x,y, border_image_arrays)  <-- DEPRECATED!
  #   Can also call (color,padding) instead of border_image_arrays (Minimalist)

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

  if (ref \$img ne 'HASH') {
    # Called with (img_string,x,y, ..)
    (\$x,\$y) = (shift \@_, shift \@_);
  } else {
    # Called with (img_object,type,href, ..)
    my (\$type,\$href) = (shift \@_, 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,\$height)=(1,2);	# Constants for image arrays

  # (color, padding) format (like Minimalist)
  if (ref \$_[0] ne 'ARRAY' && \$_[1] =~ /^\\d+\$/) {
    print <<COLOR_BORDER;
          <table bgcolor='\$_[0]' cellspacing=0 cellpadding='\$_[1]'>
            <tr>
              <td>\$img</td>
            </tr>
          </table>
COLOR_BORDER
    return;
  }

  # No corners
  if (scalar \@_ == 0) {
    print "\$img<br />\n";
    return;
  }

  if (scalar \@_ == 4) {
    my (\$T,\$R,\$B,\$L) = \@_;

    # Old method: 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
    return;
  }

  if (scalar \@_ == 8) {
    my (\$TL,\$T,\$TR,\$R,\$BR,\$B,\$BL,\$L) = \@_;

#    # Old Method: 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

    # New Method: Repeat everything to fit image.
    my \$t = Image_Repeat_td(\$x+\$L->[\$width]+\$R->[\$width]-\$TL->[\$width]-\$TR->[\$width],0, \@\$T);
    my \$b = Image_Repeat_td(\$x+\$L->[\$width]+\$R->[\$width]-\$BL->[\$width]-\$BR->[\$width],0, \@\$B);
    my \$l = Image_Repeat_td(0,\$y, \@\$L);
    my \$r = Image_Repeat_td(0,\$y, \@\$R);
    my (\$tl,\$tr,\$br,\$bl) = map( Image_Array(\@\$_), (\$TL,\$TR,\$BR,\$BL));

    print <<BORDER8;
					<table border='0' cellpadding='0' cellspacing='0'><tr>
							<td>\$tl</td>\$t<td>\$tr</td>
					</tr></table>
					<table border='0' cellpadding='0' cellspacing='0'><tr>
							\$l
							<td>\$img</td>
							\$r
					</tr></table>
					<table border='0' cellpadding='0' cellspacing='0'><tr>
							<td>\$bl</td>\$b<td>\$br</td>
					</tr></table>
BORDER8
    return;
  }

  if (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;

#    # Old Method: 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

    # New Method: Repeat everything to fit image.
    my \$t = Image_Repeat_td(\$x+\$L->[\$width]+\$R->[\$width]-\$TL->[\$width]-\$TR->[\$width],0, \@\$T);
    my \$b = Image_Repeat_td(\$x+\$L->[\$width]+\$R->[\$width]-\$BL->[\$width]-\$BR->[\$width],0, \@\$B);
    my \$l = Image_Repeat_td(0,\$y-\$LT->[\$height]-\$LB->[\$height], \@\$L);
    my \$r = Image_Repeat_td(0,\$y-\$RT->[\$height]-\$RB->[\$height], \@\$R);
    my (\$tl,\$tr,\$rt,\$rb,\$br,\$bl,\$lb,\$lt) =
      map( Image_Array(\@\$_), (\$TL,\$TR,\$RT,\$RB,\$BR,\$BL,\$LB,\$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';
		# This is a freakin' mess.  Ugh.  Maybe it's time to convert to <div>?
    print <<BORDER12;
					<table border='0' cellpadding='0' cellspacing='0'>
						<tr>
							<td colspan='3'><table cellpadding='0' cellspacing='0'><tr><td>\$tl</td>\$t<td>\$tr</td></tr></table></td>
						</tr> <tr>
							<td>\$lt</td>
							<td rowspan='3' valign=\$align>\$img</td>
							<td>\$rt</td>
						</tr> <tr>
							\$l
							\$r
						</tr> <tr>
							<td>\$lb</td>
							<td>\$rb</td>
						</tr> <tr>
							<td colspan='3'><table cellpadding='0' cellspacing='0'><tr><td>\$bl</td>\$b<td>\$br</td></tr></table></td>
						</tr>
					</table>
BORDER12

    return;
  }

  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",
  "Photo album generated by",
  "Created with the tool",
  "Album created by",
  "Album created by",
  "Powered 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";
 if (0) {
  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 \$First_url = Get(First(),'URL','image_page','image_page');
    my \$Last_url = Get(Last(),'URL','image_page','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='first' title='First Image' href=\$First_url>
<link rel='last' title='Last Image' href=\$Last_url>
<link rel='prev' title='Previous Image' href=\$Prev_url>
<link rel='next' title='Next Image' href=\$Next_url>
<link rel='up' title='Album' 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
# Dominik Lamp <d_lamp at informatik uni-kl de> points out that
# "document.images" above will eval true even before page is fully loaded.
# To fix this we could put the above code into a function and then do:
#   <body onloaded='javascript:preloadImages()'>
# Problem is the <body> tag isn't controlled in themes..


  } else {
    print "<link rel='up' title='Up' href='..'>";
  }
 }
  \$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,"|\Q$^X\E > $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>0 || $image_page) ? 0 : 1;

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

  my $Up = $image_page ? "Back" : "Up";
  my $UpUrl = $top ? $opt->{top} : "../$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 (!$image_page || $opt->{image_headers}) {
    if (-f "$dir/$opt->{header}" && (open(HEADER,"<$dir/$opt->{header}"))) {
      while(<HEADER>) { print ALBUM; }
      print ALBUM "<hr />\n";
    }
  }
}

sub credit {
  my ($opt) = @_;
  my $date = localtime;
  return <<CREDIT;
    <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>
CREDIT
}

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";
  }
  print ALBUM credit($opt);
  print ALBUM <<END_OF_FOOTER;
  </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;
  }
}

# Split up all characters, but don't split '<html>' or '&chars;'
# This is for the shortening algorithm
sub mysplit {
  my ($str) = @_;
  return split(//, $str) unless $str =~ /(<.*>|&.*;)/;
  my @a;
  while ($str =~ /(.*?)(<[^>]+>|&[^;]+;)(.*)/) {
    push(@a,split(//,$1),$2);
    $str = $3;
  }
  (@a,split(//,$str));
}

# Shorten a string, but don't break up '&chars;' or '<html>' and
# don't lose any closing html tags in the middle
# (this heuristic may cause unneeded closing tags, but the complete
# solution is far more complex and somewhat unwarranted)
sub shorten {
  my ($str,$len) = @_;

  return $str if length($str) < $len+3;
  my @str = mysplit($str);
  return $str if @str <= $len;
  my @a = splice(@str,0,$len/2);
  my @b = splice(@str,-$len/2,$len/2);
#  # Keep any closing tags that got chopped
#  @str = grep(m|^</.|, @str);
  # Actually, keep *any* tags, otherwise we'll have bad HTML
  @str = grep(m|^<.*>|, @str);
  join('',@a,@str,"..",@b);
}

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};
      print ALBUM "<a href=$obj->{URL}{album_page}{dir}>\n";
      if ($obj->{thumb}) {
        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 " style='border:0' src=$obj->{URL}{album_page}{thumb}";
        print ALBUM " alt='$obj->{alt}'";
        print ALBUM " title='$obj->{alt}'" if $obj->{alt};
        print ALBUM " /><br />\n";
      }
      print ALBUM "<font size='+1'>$obj->{name}</font></a>\n";

      my $num_pics = $obj->{num_pics};
      $num_pics = $num_pics ? $num_pics>1 ? "$num_pics images" : "1 image" : 0;
      print ALBUM "<font size='-1'><br />$num_pics</font>\n" if $num_pics;

      my $num_dirs = $obj->{num_dirs};
      $num_dirs = $num_dirs ? $num_dirs>1 ? "$num_dirs folders" : "1 folder" : 0;
      print ALBUM "<font size='-1'><br />$num_dirs</font>\n" if $num_dirs;
    }
    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 = shorten($name,$opt->{name_length}) if $opt->{name_length};

    # 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 " style='border:0' src=$obj->{URL}{album_page}{thumb}";
      print ALBUM " alt='$obj->{alt}'";
      print ALBUM " title='$obj->{alt}'" if $obj->{alt};
      print ALBUM " /><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?
    if ($opt->{album_captions}) {
      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};
  my $prev = "<h3><a href=$prev_url>$prev_name</a></h3>";
  undef $prev unless $opt->{image_loop};

  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_num = $i+1 > $#{$data->{pics}} ? 0 : $i+1;
    my $next_pic = $data->{pics}[$next_num];
    my $next_url = $data->{obj}{$next_pic}{URL}{image_page}{image_page};
    my $next_name = $data->{obj}{$next_pic}{name};
    my $next = "<h3><a href=$next_url>$next_name</a></h3>";
    undef $next unless $next_num || $opt->{image_loop};

    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'>
      $prev
    </td>
    <td align='right'>
      $next
    </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} style='border:0'";
    print ALBUM " src=$obj->{URL}{image_page}{image_src}";
    print ALBUM " alt='$obj->{alt}'";
    print ALBUM " title='$obj->{alt}'" if $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 = "<h3><a href=$obj->{URL}{image_page}{image_page}>$name</a></h3>";

    show_hashes($opt, 2+$i+1, 2 + $#{$data->{pics}}+1);
  }
}

##################################################
##################################################
# 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;
}

# Allow plugins to specify HTML that needs regen
sub new_html {
  my ($opt,$data, $pic) = @_;
  $data->{_changed}{$pic} = 1;
}

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] = $opt->{force_html} ||
      option_changed($opt,'exif') ||
      option_changed($opt,'exif_album') ||
      option_changed($opt,'exif_image') ||
      option_changed($opt,'just_medium') ||
      option_changed($opt,'caption_edit') ||
      option_changed($opt,'theme_url') ||
      option_changed($opt,'burn') ||
      $data->{_changed}{$pic} ||
      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
      );
  }

  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);
    show_hashes($opt, 2+$i+1, 2 + $#{$data->{pics}}+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 $pic !~ /\.($IMAGE_TYPES)$/i;
  1;
}

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

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

  return if do_hook($opt,$data,'gather_contents', $dir);
  # Description: Gather the list of pictures and subdirectories for a directory
  # Description: Should populate the @{$data->{pics}} and @{$data->{dirs}} arrays
  # Returns: 1 to skip normal gather_contents code

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

  # Sort through the contents
  foreach my $item ( @contents ) {
    next if do_hook($opt,$data,'gather_contents_item', $dir, $item);
    # Description: Handle the gathering of a file item in gather_contents.
    # Description: Generally ignores the file or adds to @{$data->{pics}} and @{$data->{dirs}}
    # Returns: 1 to finish handling this file (don't run gather_contents for it)

    my $path = "$dir/$item";
    next if $item =~ /^CVS|SCCS|RCS$/;	# Ignore revision control directories
    next if $item eq ".xvpics";		# Silly xv
    next if $item eq $opt->{dir};	# Thumbnail/HTML directory
    next if -f "$path$opt->{hide_album}";
    next if -f "$path/$opt->{hide_album}";
    next if $item =~ /^\./ && !$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)
    # Wrong!  no_album doesn't ignore the file/dir, it just doesn't descend!
    #next if -f "$path$opt->{no_album}";	# Ignore .no_album files/directories
    next if $item =~ /\Q$opt->{not_img}\E$/;
    next if $item =~ /\Q$opt->{no_album}\E$/;
    next if $item =~ /\Q$opt->{hide_album}\E$/;

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

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

sub is_movie {
  my ($opt,$pic) = @_;
  $pic =~ /\.(mpe?g|mov|avi|mp4)$/i ? 1 : 0;
}

# 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};

  # Figure out type
  $obj->{is_movie} = is_movie($opt,$pic);
  my $can_embed = $obj->{is_movie};
  $can_embed = 1 if $pic =~ /\.(pdf|ps)$/i;
  my $tag = 'img';
  $tag = 'embed' if $can_embed && $opt->{embed};
  $obj->{full}{tag} = $tag;

  # Being a bit presumptive, here.  We can undo it if we fail to make a thumb.
  # (see snapshot() call in thumbnail())
  $obj->{has_thumb} = is_image($opt,$path);

  #########################
  # Handle pictures, generate thumbnails
  #########################
  thumbnail($opt,$dir,$obj) if $obj->{has_thumb};
  medium($opt,$dir,$obj) if $obj->{has_thumb};

  # File sizes
  $obj->{full}{filesize} = filesize $path if $opt->{file_sizes};
  $obj->{medium}{filesize} = filesize $obj->{medium}{path}
    if $opt->{file_sizes} && $obj->{medium}{path};

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

  #########################
  # Get sizes (in case we didn't generate them this time)
  #########################
  if ($obj->{has_thumb}) {
    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} = url_quote($opt, "$opt->{dir}/$image_page");
# 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} = url_quote($opt, $image);
    $obj->{URL}{image_page}{image_page} = url_quote($opt, $image_page);

    # 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} = url_quote($opt, $image_src);

    # Thumbnail
    $obj->{URL}{album_page}{thumb} = url_quote($opt, "$opt->{dir}/$obj->{thumb}{file}");
    $obj->{URL}{image_page}{thumb} = url_quote($opt, $obj->{thumb}{file});
  } else {
    $obj->{URL}{album_page}{image} = url_quote($opt, $pic);
    $obj->{URL}{album_page}{thumb} = url_quote($opt, "$opt->{dir}/$obj->{thumb}{file}");
    # We might normally have image pages, just not for this non-image
    $obj->{URL}{image_page}{image_page} = url_quote($opt, "../$pic")
      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.
# (See hook below for more info)
sub dir_image {
  my ($opt,$data,$child) = @_;

  return undef unless $opt->{dir_thumbs};

  my @ret = do_hook($opt,$data, 'dir_image', $child);
  # Description: Picks the image for a directory thumbnail
  # Returns: thumb path, full path to image, local path to image, image filename
  # Returns: Thumb path:  Where the tn/ directory and the thumbnail gets created
  # Returns: = (local path from the current working directory)
  # Returns: Full path to image:  Path to full size image from current working directory
  # Returns: Local path:  Thumbnail path, but from the current album directory.
  # Returns: Image filename:  Just the image part of the filename.
  # Returns:
  # Returns: The thumbnail path and the path used in the full path to the image
  # Returns: don't have to be the same, but they probably will be.
  # Returns:
  # Returns: Example:  (album/dir/, dir/, album/dir/image.gif, image.gif)
  return @ret if defined $ret[0];

  $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';
    $obj->{is_movie} = is_movie($opt,$file);

    $obj->{has_thumb} = 1;	# Assume...
    thumbnail($opt,$tn_path,$obj);
    return unless $obj->{thumb};
    get_size($opt,'thumb',$obj);

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

# Should caption be done here?
}

# Create a new album data structure
sub new_album_data {
  my ($opt,$dir) = @_;
  my $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) = @_;

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

  # 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;

  my $alb_path = $data->{paths}{album_path};
	$alb_path =~ s|^/||;
  # 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|/\Q$alb_path\E$||) {
      # 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");
			if (@old) {
        push(@fatal, "\nIt looks like you've moved a subalbum.  To fix this, run:");
        push(@fatal, "\n% $PROGNAME -add $dir");
      } else {
        # Find the broken index.html, either it's the top dir or the full dir
        my $broke = $dir;
        $broke .= "/$dir_pieces[0]" unless -f index_page($opt,1,$broke);
        my $index = index_page($opt,1,$broke);
        push(@fatal, "\nIt looks like you've moved your top album.\n");
        push(@fatal, "To fix this:");
        push(@fatal, "1) Erase:   $index");
        push(@fatal, "2) Run:     $PROGNAME $broke");
        push(@fatal, "   or else: $PROGNAME $dir") unless $broke eq $dir;
      }
      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};
  my $album = join('/',@{$data->{dir_pieces}});

  # Deal with album.conf files
  my $pushed = album_confs($opt,$data) unless $noalb;

  #########################
  # Start hooks
  #########################
  return if $data->{start} && do_hook($opt,$data, 'do_album_top', $dir, $album);
  # Description: Start of an album (top only, no sub-albums)
  # Returns: Return false to skip album

  return if do_hook($opt,$data, 'do_album', $dir, $album);
  # Description: Start of an album or sub-album
  # Returns: Return false to skip album

  # Print some info
  my $w = $opt->{hash_width} - $opt->{num_hashes} - 3 - 8;
  my $p = (length($album) >= $w) ?  '..'.substr($album,-$w+3) : $album;
  start_hashes($opt, "Images:  $p");
  if ($noalb || $data->{unknown}) {
    hash_msg($opt,"<".($noalb ? $opt->{no_album} : "unknown").">");
    print STDERR "\nNothing to do.  Call 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);

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

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

  # 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);
  }
  obj_count($opt,$data,'pics');	# We may have lost some
  foreach my $child ( @{$data->{dirs}} ) {
    handle_child($opt,$data,$child);
    show_hashes($opt, $#pics+1+$data->{obj}{$child}{num}+1, $hashes);
  }
  obj_count($opt,$data,'dirs');

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

  #########################
  # Write the HTML
  #########################
  start_hashes($opt, "Indexes: $p");
  $hashes = 2 + $#pics+1;	# data conversion, album, then each picture index

  data_to_eperl($opt,$data);
  show_hashes($opt, 1, $hashes);

  my $did_index = do_hooks($opt,$data, 'write_index', $dir, $album);
  # Description: Called before the album index page is written for a directory.
  # Returns: 1 to skip the normal index writing code

  ($opt->{'album.th'}) ?
    write_theme($opt,$data) : write_index($opt,$data)
      unless $did_index;
  show_hashes($opt, 2, $hashes);

  my $did_images = do_hooks($opt,$data, 'write_image_pages', $dir, $album);
  # Description: Called before the album image pages are written
  # Returns: 1 to skip the normal image page writing code

  ($opt->{'image.th'} ?
    write_img_themes($opt,$data) : write_img_indexes($opt,$data))
      if $opt->{image_pages} && !$did_images;
  stop_hashes($opt);

  #########################
  # 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}} ) {
    next if -f "$dir/$child/$opt->{no_album}" || -f "$dir/$child$opt->{no_album}";
    do_album($opt,"$dir/$child",@{$data->{dir_pieces}},$child)
      unless $data->{start} && @add && !contains($child, @add);
  }

  #########################
  # End hooks
  #########################
  do_hook($opt,$data, 'end_album_top', $dir, $album) if $data->{start};
  # Description: End of an album (top only, no sub-albums)
  # Returns: No return value needed

  do_hook($opt,$data, 'end_album', $dir, $album);
  # Description: End of an album or sub-album
  # Returns: No return value needed

  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) = @_;

  my $postfix = $add_nodir;
  my @new = do_hook($opt,undef, 'new_image_path', $dir,$pic,$type,$postfix);
  # Description: Create a pathname for a new image (such as a thumbnail)
  # Description: Called with: directory, picture, new picture type and:
  # Description: postfix hint, such as 'snap.' or 'med.'
  # Returns: (filename, full pathname)
  return @new if defined $new[0];

  # 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");
}

my $IMAGESIZE = attempt_require('Image::Size');
sub get_xy {
  my ($opt,$img) = @_;

  my @xy = do_hook($opt,undef, 'get_xy', $img);
  # Description: Figure out resolution of image
  # Returns: (x,y)
  return @xy if defined $xy[0];

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

  # Try to use Image::Size first, evidently (and oddly) it's faster than convert
  # Debian users can just: "apt-get install libimage-size-perl"
  if ($IMAGESIZE) {
    my ($x,$y,$typeErr) = Image::Size::imgsize($img);
    return ($x,$y) if defined $x && defined $y;
  }

  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 $unsupported = 0;

  # 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;
        }
        if (/no delegate for this image format/
            || /support .*not yet available/) {
          $unsupported=1;
          last;
        }
        if (/\s(\d+)x(\d+)(\s|\+|\-)/) {
          $size->close;
          return ($1,$2);
        }
      }
			$size->close;
    }
  }

  # 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;
      return ($1,$2);
    }
  }
	$size->close;
  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 @xyxy = do_hook($opt,undef, 'scale', $img, $scale_arg, $new, $medium);
  # Description: Scale an image by $scale_arg and save in $new.
  # Description: $medium=1 for medium scaling (for -medium_scale_opts)
  # Returns: (img_x, img_y, new_x, new_y)
  # Returns: [since our scale routine should know this anyways]
  return @xyxy if defined $xyxy[0];

  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->{medium_scale_opts}} " if $medium && $opt->{medium_scale_opts};
  $args .= "@{$opt->{thumb_scale_opts}} " if !$medium && $opt->{thumb_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 $ret = do_hook($opt,undef, 'crop', $img, $off_x, $off_y, $new);
  # Description: Crop an image (at $off_x,$off_y) and save in $new.
  # Returns: 1 on success.
  return if $ret;

  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 $ret = do_hook($opt,undef, 'movie_frame', $movie, $img);
  # Description: Create a movie frame from $movie and save in $img
  # Description: This might be called with "non-movie" types,
  # Description: so check in your hook for the types you can read.
  # Returns: 1 on success.
  return if $ret;

  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";
	# 'singlejpeg' has been replaced by 'mjpeg'??  Don't know when..  *grrr*
  my $cmd = "$opt->{ffmpeg} -y -t 00:00:00.01 $format -i $qmovie -f mjpeg $qimg";
  print STDERR "movie_frame() run: $cmd\n" if $opt->{d};
  system("$cmd > $opt->{dev_null} 2>&1");
  return $img unless $?;

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

# 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 $full_file = $obj->{full}{file};
  my $full_path = $obj->{full}{path};

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

  return $obj->{has_thumb}=0 unless movie_frame($opt,$full_path,$path);
  ($obj->{snapshot}{file},$obj->{snapshot}{path}) = ($file,$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};

  $obj->{medium}{path} = do_hook($opt,undef, 'medium', $dir, $obj, $file, $full_path);
  # Description: Create a medium image using an image object or path.
  # Description: This might be called with "non-image" types (such as movies)
  # Description: so check in your hook for the types you can read.
  # Returns: Full path to medium image
  if ($obj->{medium}{path}) {
    $obj->{medium}{file} = $obj->{medium}{path};
    $obj->{medium}{file} =~ s|.*$opt->{slash}||g;
    return get_size($opt,'medium',$obj)
  }

  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}
        && !option_changed($opt,'medium')
        && -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??

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

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

# Hack!  This doesn't work on Windows - at least on ActivePerl!  :(
# I think it's because ActivePerl uses DOS for system calls and DOS doesn't
# let you quote '>' to avoid redirection?  It complains that we need an
# argument for -geometry option otherwise.
  $medium =~ s/\>/\\>/g unless $opt->{windows};


  my ($fx,$fy,$mx,$my) = scale($opt,$full_path,$medium,$obj->{medium}{path});
  ($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) = @_;

  snapshot($opt,$dir,$obj);	# In case we need a snapshot
  return unless $obj->{has_thumb};	# Snapshot might fail our thumbing..

  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}{path} = do_hook($opt,undef, 'thumbnail', $dir, $obj, $file, $full_path);
  # Description: Create a thumbnail using an image object or path
  # Description: This might be called with "non-image" types (such as movies)
  # Description: so check in your hook for the types you can read.
  # Returns: Full path to the thumbnail
  if ($obj->{thumb}{path}) {
    $obj->{thumb}{file} = $obj->{thumb}{path};
    $obj->{thumb}{file} =~ s|.*$opt->{slash}||g;
    return get_size($opt,'thumb',$obj);
  }

  ($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}
        && !option_changed($opt,'geometry')
        && -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 contains('-d', @ARGV);	# Hack to see read_confs debugs
  $opt->{q} = 1 if contains('-q', @ARGV);	# Hack for read_conf quiet
  read_confs($opt);
  parse_args($opt);
  scrub_opts($opt,1);

  version($opt);

  virgin_check($opt);

  do_hook($opt,undef, 'pre_do_albums');
  # Description: Called before doing any albums.
  # Returns: No return value needed

  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 --

You can remove specific array options with -no_<option>:

% album -no_exif hi

Or clear all the array options with -clear_<option>:

% album -clear_exif

Boolean options:

% album -q, -d, -D, -virgin_check, -save_conf, -configure, -image_pages, -dir_thumbs, -just_medium, -embed, -clean, -image_headers, -album_captions, -caption_edit, -file_sizes, -fix_urls, -known_images, -all, -hashes, -reverse_sort, -image_loop, -burn, -crop, -force, -force_html, -sample, -animated_gifs, -use_tcap

String/number options:

% album -medium, -captions, -top, -sort, -body, -charset, -index, -default_index, -html, -type, -medium_type, -CROP, -dir, -sharpen, -plugin_post, -theme, -theme_url, -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, --thumb_scale_opts, --data_path, --plugin_path, --theme_path


=head2 OPTION DESCRIPTIONS

=over 4

=item B<-h>I<>

Show usage

=item B<-more>I<>

To show more options.

=item B<-More>I<>

To show even more options.

=item B<-q>I<>

Be quiet [Default OFF]

=item B<-d>I<>

Set debug mode [Default OFF]

=item B<-D>I<>

Heavy debug mode [Default OFF]

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

Read a .conf file

=item B<-virgin_check>I<>

Do the virgin check to see if you've run album before [Default ON]

=item B<-save_conf>I<>

Save album.conf files in photo album [Default ON]

=item B<-configure>I<>

Setup initial album site configuration [Default OFF]

=item B<-version>I<>

Display program version info

=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<-image_headers>I<>

Show header.txt on image pages (default theme only) [Default OFF]

=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 40]

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

Sort type, captions, name or date [Default captions]

=item B<-reverse_sort>I<>

Sort in reverse [Default OFF]

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

Specify <body> tags for non-theme output [Default <body>]

=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<-burn>I<>

Setup an album to burn to CD
Implies '-index index.html' and '-no_theme_url' [Default OFF]

=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
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 and HTML
otherwise they are only written when changed [Default OFF]

=item B<-force_html>I<>

Force rewrite of HTML [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<--thumb_scale_opts>I<=I<strings>>

List of thumbnail convert options

=back

=head2 Plugin and Theme Options:

=over 4

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

Path for themes, plugins, language files, etc...
 [Default /etc/album /usr/share/album /home/dave/.album]

=item B<-plugin>I<=I<plugin>>

Load a plugin.

=item B<-plugin_usage>I<=I<plugin>>

Show usage for a plugin.

=item B<-plugin_info>I<=I<plugin>>

Print info for a specific plugins.

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

Add a path to search for plugins.
	 [Default @DATA_PATH/plugins]

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

Default postfix for plugins. [Default .alp]

=item B<-list_plugins>I<>

Print info for all known plugins.

=item B<-list_hooks>I<>

Show all known plugin hooks (for developers).

=item B<-hook_info>I<=I<hook>>

Show all known plugin hooks (for developers).

=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>>

Directories that contain themes [Default /data/proj/album/Themes/]

=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<-conf_version>I<>

Configuration file version

=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
