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

package album;	# For plugins

$|++;	# For galbum

##################################################
##################################################
# 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 $CAVA = 0;	 # Cava packager for Windows apps

if ($WINDOWS) {
	eval "use Cava::Pack";
	$CAVA = 1 unless $@;
	$0 = $^X if $CAVA && $^X !~ m%(^|[/\\])perl(\.exe)?$%i;
}

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

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

##################################################
# CONF FILES
##################################################
my $PROGFILE = ($PROGNAME =~ /^([^\.-]{3,})[\.-]/) ? $1 : $PROGNAME;
#$PROGFILE = "album" if $PROGFILE eq "galbum";
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  -usage=...
#   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,'usage',\&usage, args=>'<level>', emptyarg_okay=>1, usage=>"Show usage as deep as you like.");
add_option(1,'lang',\&get_language, args=>'<lang>', emptyarg_okay=>1, usage=>"Specify language(s)");
add_option(2,'list_langs',\&list_languages, usage=>"List out full language information");
add_option(3,'make_lang',\&make_language, args=>'<lang>', usage=>"Print out a new language file");
add_option(10,'list_html_trans',\&list_languages, usage=>["List HTML translations for each language",
	"Useful for creating multi-lingual images for themes",
	"Output is in HTML and utf-8, change charset as needed"]);
add_option(99,'check_langs',\&check_languages, usage=>"Check installed languages");
add_option(99,'make_lang_check',\&make_language, usage=>"Test that output/translations are okay for -make_lang");
add_option(10,'lang_path', OPTION_ARR, default=>['@DATA_PATH/lang'], usage=>"Add a path to search for language files");
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(9,'dtheme',OPTION_BOOL, one_time=>1, usage=>"Theme debug mode");
add_option(9,'Dtheme',OPTION_BOOL, one_time=>1, usage=>"Theme 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");
add_option(3,'license',\&license, args=>'[type]', emptyarg_okay=>1, usage=>"Show license");
add_option(1,'mv',\&get_plugin, usage=>"Move imgs across albums: see 'album -plugin_info utils/mv'");
add_option(3,'create_plugin',\&get_plugin, usage=>"Create plugin: see 'album -plugin_info utils/create_plugin'");

# Album Options:
add_option(1,'Album Options:',OPTION_SEP);
add_option(9,'crf', OPTION_BOOL, one_time=>1, default=>0, usage=>"Album hash output in computer readable format");
add_option(9,'list_options', OPTION_BOOL, one_time=>1, default=>0, usage=>"Show default options and values for a given album");
add_option(3,'image_pages',OPTION_BOOL, default=>1, usage=>"Create a page for each image");
add_option(2,'thumbs',OPTION_BOOL, default=>1, usage=>"Images have thumbnails");
add_option(2,'dir_thumbs',OPTION_BOOL, default=>1, usage=>"Directories have thumbnail (if supported by theme)");
add_option(9,'thumb_post',OPTION_STR, default=>'', usage=>"Additional postfix for thumbnails.");
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,'slideshow',OPTION_BOOL, default=>0, usage=>"Slideshow capabilities (only with some themes)");
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(3,'folder_count',OPTION_BOOL, default=>1, usage=>"Show folder/image counts for each album");
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', \&deprecated_option, usage=>"DEPRECATED: Size of images");
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 [-1])");
add_option(3,'follow_symlinks',OPTION_NUM, default=>1, usage=>"Dereference symbolic links");
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, date or EXIF date ('exif')");
	## Deprecated!
	add_option(99,'date_sort', \&deprecated_option, instead=>'sort', usage=>"DEPRECATED: Sort images/dirs by date instead of name");
	add_option(99,'name_sort', \&deprecated_option, instead=>'sort', usage=>"DEPRECATED: Sort by name, not caption order");
add_option(2,'reverse_sort',OPTION_BOOL, usage=>"Sort in reverse");
add_option(2,'case_sort',OPTION_BOOL, default=>0, usage=>"Use case sensitive sorting when sorting names");
add_option(3,'body',OPTION_STR, default=>'<body>', usage=>"Specify <body> tags for non-theme output");
add_option(3,'charset', OPTION_STR, args=>'<str>', usage=>["Charset for non-theme and some theme output",
           "This is also set by using language files (with -lang)"]);
add_option(10,'force_charset', OPTION_STR, args=>'<str>', usage=>"Force charset (not overridden by languages)");
add_option(99,'default_charset', OPTION_STR, args=>'<str>', default=>'iso-8859-1', usage=>"Default charset");
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 (also see -default_index)");

# 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(9,'force_subalbum',OPTION_BOOL, one_time=>1, usage=>"Ignore 'You have moved a subalbum' error.");
add_option(2,'sample',OPTION_BOOL, usage=>"Use '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 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 hook info for a specific hook (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",""]);
add_option(3,'list_themes', OPTION_BOOL, one_time=>1, default=>0, usage=>"Show available themes");
add_option(3,'theme_full_info', OPTION_BOOL, default=>0, usage=>"Use full info for themes");
add_option(99,'theme_interp_wrap', \&theme_interp_wrap, one_time=>1, usage=>"Internal option for packaged application support - do not use");

# 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=>'avconv', usage=>"Path to avconv/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");
add_option(10,'credit',OPTION_STR, usage=>"Credit line to add to the bottom of every album");
              # We can't set -no_album as a command-line 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/file if dir/file.no_album 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 (make sure to quote it)");
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,'screen_width',OPTION_NUM, default=>78, usage=>"Width of screen (for hashes and word wrapping)");

#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|CR2|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)
# $data->{head}		String to print between the <head></head> of an album
# $data->{header}	String to add to header
# $data->{footer}	String to add to footer
# $data->{body_tag}     String to print inside the <body> tag of an album
#
# $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->{is_image}	Boolean: is it an image?
# $obj->{has_thumb}	Boolean: does it have a thumbnail?
# $obj->{name}		Name (cleaned and optionally from captions)
# $obj->{cap}		Image caption
# $obj->{cap_image}	Caption to only show in image pages.  (For plugins)
# $obj->{cap_album}	Caption to only show in album pages.
# $obj->{exif}		Exif caption (also 'exif_image' and 'exif_album')
# $obj->{_did_exif}	Have we done exif reading yet?
# $obj->{capfile}	Optional caption file
# $obj->{alt}		Alt tag
# $obj->{intag}		Optional:  Add to the <img|embed> tag for this object
# $obj->{num_pics}	[directories only] Num of pics in directory
# $obj->{num_dirs}	[directories only] Num of dirs in directory
# $obj->{num_pics_str}	Translation of num_pics: 1 image, 2 images, 3 images..
# $obj->{num_dirs_str}	Translation of num_dirs: 1 folder, 2 folders, 3 folders..
#
# 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)
#   $obj->{<size>}{img_tag}		Optional:  Complete replacement of the <img> tag
#   $obj->{<size>}{intag}		Optional:  Add to the <img|embed> tag
#
# 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->{_lang}		Language data
#   @{$opt->{_langInfo}}	List of language_info hashes loaded (in order of load)
#   @{$opt->{lang}}	List of languages
#   $opt->{trans}{$str}		Translations for HTML strings for themes.
#   $opt->{_captions}{$dir}	Captions cache for a path
#   $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 (by loaded name)
#   $opt->{_plugins}{off}	Hash of plugins that are turned off
#   $opt->{_plugin}{$plugin}	Plugin info hash
#   @{$opt->{_plugin_hooks}{$name}}	List of hooks for a name [sub,plugin]
#   $opt->{_curr_plugin}	Current plugin/hook data (see curr_plugin())
#
# 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';
	### could replace this with: eval("use $_[0]") so we can 'use' instead of require
	### could also add "; import $_[0]"
  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 = curr_plugin($GLOBAL_OPT);
  $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};
  }
}

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

#########################
# Windows blows
#########################
  # 1) Can't handle "\Qfile\E";
  sub file_quote {
    my ($opt,$file) = @_;
# I think any file quoting might be causing problems under some versions of CygWin
		return $file if $opt->{cygwin};
    $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;

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

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

    # Win98 (use TCAP)
    if ($opt->{use_tcap}) {
      usage($opt,"Couldn't find '[_1]'", '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 = ($opt->{tcap});
			push(@tcap,$opt->{cmdproxy}) if $opt->{cmdproxy};
      system(@tcap,'-c',$cmd,@args);
      (open($fh, "$opt->{tcap_out}"))
        || fatal($opt,"Can't open [_1] output ~[[_2]~]", $opt->{tcap}, $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 $?;	# Pass on exit value
  }
  $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 plugin add_option calls.  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 usage {
	my ($opt, $msg, @args) = @_;		# Called with error
	($opt, my $option,my $val) = @_;	# Called from option
	
	version();
	
	my $plugin = $option =~ /plugin/ ? get_plugin($opt,$option,$val) : undef;
	
	# If it was called from -h, -more or -usage=#
	my $show=1;
	undef $msg if $plugin;
	undef $msg if $option eq 'h';
	undef $msg, $show=2 if $option eq 'more';
	undef $msg, $show=($val||3) if $option eq 'usage';

	# Otherwise we have a usage error
	if ($msg) {
		nlperror($opt, $msg, @args);	#NOTRANS
		prterr($opt, "\nTry '[_1] -h' for usage info.\n", $PROGNAME);
		my $curr_plugin = curr_plugin($GLOBAL_OPT);
		prterr($opt, "or: '[_1] -plugin_usage [_2]' for plugin usage info.\n", $PROGNAME, $curr_plugin)
			if $curr_plugin;
		print STDERR "\n";
		exit -1;
	}

	# Print usage.
 	my $program_usage = "$PROGNAME [-d] [--scale_opts .. --] [options] <dir>";
	prterr($opt, "\nUsage:\t[_1]\n  Makes a photo album\n\n  All boolean options can be turned off with '[_2]'\n  (Some are default on, defaults shown in ~[brackets~])\n\n", $program_usage, '-no_<option>');

	# Calculate width of -arg string
	my $maxw = 0;
	map { $maxw=max($maxw,length($_)); } all_options($opt,$plugin);
	$maxw=min($maxw,17);	# Don't go overboard

	prterr($opt,"Plugin Options:\n  ~[Use -[_1]:<option> instead of -<option>~]\n", $plugin)
		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 ? trans($opt, "\n$option\n") : "\n";	#NOTRANS
			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;
		# NEEDTRANS?  Do we want trans of $usage_str??
		printf STDERR "  -%-${maxw}s $usage_str$def\n", $opt;
	}

	prterr($opt, "\nDashdash options (--opt) can be specified two ways:\n  With one argument:       '[_1]'\n  With mult. arguments:    '[_2]'\n", '-exif hi -exif there', '--exif hi there --')
		if $dashdash;

	copyright($opt);
}

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

# List all option values after reading album conf files
sub list_options {
	my ($opt) = @_;
	foreach my $option ( all_options($opt) ) {
		my $optinfo = get_option($opt,$option);
		my $type = $optinfo->{type};
		# kludge for saving -geometry and -lang.  Ugh.
		$type=OPTION_NUM if $option eq 'geometry';
		$type=OPTION_ARR if $option eq 'lang';
		next if ref $type eq 'CODE';
		my $usage = $optinfo->{usage};
		my $usage_str = ref $usage eq 'ARRAY' ? join("  ",@$usage) : $usage;
		$usage_str =~ s/"/'/g;
		my $val = $opt->{$option};
		$val = [$val] unless ref $val eq 'ARRAY';
		map { print "crf:option:$optinfo->{lvl}:$type:$option:\"$usage_str\":$_\n" } @$val;
	}
}

#########################
# Version/Copyright Info
#########################
sub version {
	my ($opt,$option,$val) = @_;
	
	unless ($opt->{q} || $MAIN::SHOWED_VERSION++) {
		my $operating_system = $^O;
		prterr($opt, "\nThis is [_1] v[_2] on [_3]", $PROGNAME, $VERSION, $operating_system);	#LVL=2
		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($opt);
	exit 0;
}

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

	my $Copyright = trans($opt,"Copyright:");
	my $Docs = trans($opt,"Docs:");
	my $License = trans($opt,"License:");
	my $PleaseSee = trans($opt,"Please see:");
	my $max = max(max(length($Copyright),length($Docs)),max(length($License),length($PleaseSee)));
	$max += 1;
	$Copyright = sprintf("%-${max}s",$Copyright);
	$Docs = sprintf("%-${max}s",$Docs);
	$License = sprintf("%-${max}s",$License);
	$PleaseSee = sprintf("%-${max}s",$PleaseSee);

	print STDERR <<COPYRIGHT;

$Copyright (c) 2000-2008 David Ljung Madison
$Docs $ALBUM_URL
$PleaseSee ${HOME}Pay/
$License ${HOME}License/
% $PROGNAME -license

COPYRIGHT
	exit 0;
}

sub license {
	my ($opt,$option,$type) = @_;

	$type ||= 'album';
	my $noFindFile = trans($opt,"Couldn't find '[_1]'", $type);
	my $License = trans($opt,"License:");
	my @licenses = path_matches($opt,"^(?i)$type\$",$opt->{data_path},'license');
	unless (@licenses) {
		print "$noFindFile\n\n$License ${HOME}License/\n\n";
		exit -1;
	}

	foreach my $license ( @licenses ) {
		my ($path,$dir,$sub,$file) = @$license;
		open(LIC,"<$path") || usage($opt,$noFindFile);	#NOTRANS
		while (<LIC>) { print; }
		close LIC;
	}
	exit 0;
}

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

  # Plugin?
  my $plugin = curr_plugin($opt);
  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}++;

  ppwarn($opt,"Deprecated option ignored: <-[_1]>",$option);
  my $optinfo = get_option($opt,$option);
	ppwarn($opt,"See <-[_1]> option instead\n",$optinfo->{instead}) if $optinfo->{instead};
}

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

  # Could be a current plugin option
	my $curr_plugin = curr_plugin($opt);
  return $OPTIONS{plugin}{$curr_plugin}{$option}
    if $curr_plugin && $OPTIONS{plugin}{$curr_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;
}

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

  usage($opt,"Can't understand geometry '[_1]'", $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)
my $PARSE_ARGV	= 0;
my $PARSE_CONF	= 1;
my $PARSE_META	= 2;
my $PARSE_THEME	= 3;
sub parse_arg {
  my ($opt,$Option,$val, $from, $type_str) = @_;

  # 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/) ? 1 : 0;
  usage($opt,"Unnecessary '--' in options") if $dashdash && !$Option;

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

  # Figure out type of option
  my $type_option = trans($opt, "command-line option");
  $type_option = trans($opt, "conf option") if $from==$PARSE_CONF;
  $type_option = trans($opt, "meta option") if $from==$PARSE_META;
  $type_option = trans($opt, "theme option") if $from==$PARSE_THEME;
  $type_option .= " [$type_str]" if $type_str;

  # Get option info
  my $optinfo = get_option($opt,$Option);
  usage($opt,"Unknown [_1]:\n\t  [_2]", $type_option, "-$Option")	#EXPLANATION: As in "Unknown command-line option"
    unless $optinfo;
  my $option = full_option($opt,$optinfo);
  my $type = $optinfo->{type};

  print STDERR "[CONF] $option ($val, $no) '$type_option'\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==$PARSE_THEME && $opt->{_set}{$option};

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

  if ($type==OPTION_SEP) {
    usage($opt,"Unknown [_1]:\n\t  [_2]",$type_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
      $val = shift @ARGV if !defined $val && !$from;
      usage($opt,"Missing argument for [_1]: [_2]",$type_option,$option)
      	unless defined $val || $optinfo->{emptyarg_okay};
    } else {
      # We only keep $no when args are used, here we just change $val
      $val = $no ? 0 : 1;
      undef $no;
    }

    # Execute code unless $no
    if (($optinfo->{args} && !$no) || $val) {

      # Call the routine
			enter_plugin($opt, $optinfo->{plugin});
      $code->($opt,$option,$val);
			leave_plugin($opt);

      # 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 not $from, then we're parsing @ARGV, collect args to --
    if (!$from && $dashdash) {
      push(@vals, shift(@ARGV)) while (@ARGV && $ARGV[0] ne "--");
      usage($opt,"Missing '--' at end of [_1]",$option) unless shift(@ARGV);
    }

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

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

  } else {
    # Strings or numbers
#usage($opt,"-no_[_1] doesn't make sense",$option) if $no;
    if ($no) {
      delete $opt->{$option};
    } else {
      $val = shift @ARGV unless $from || defined $val;
      usage($opt,"-[_1] needs an argument",$option) unless $from || defined $val;
      usage($opt,"Bad -[_1] number '[_2]'",$option,$val) if $val=~/[^\d\.]/ && $type==OPTION_NUM;	#EXPLANATION: As in: Bad number for a given -option
      $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 $optinfo = get_option($opt,$option);
  unless ($optinfo) {
    my $culprit = curr_plugin($opt) || $PROGNAME;
    hash_warn($opt,"option_changed() called on unknown option '[_1]'\n\tThis is an error in [_2].",$option,$culprit)
      unless $MAIN::WARN_OPTION_CHANGED{$option}++;
    return 0;
  }

  my $full_option = full_option($opt,$optinfo);

  # 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($opt,"Use '/' instead of '\\' in paths ~[[_1]~]",$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_out($opt,"Can't handle option <[_1]> in [_2]",$option,$file);
  next;
}

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

      parse_arg($opt,$option,$val, $PARSE_META, $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) = @_;

#	# Kludge for galbum
#	get_plugin($opt,"plugin","interface/gui") if $PROGNAME eq "galbum";

  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($opt,"Can't find directory [_1]",$arg) unless (-d $arg);

      add_album_dir($opt,$arg);
    }
  }

	# Handle -list_themes now if they haven't specified an album
	list_themes($opt) if $opt->{list_themes} && !$opt->{_albums};

  # 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")) {
	prterr($opt,"Couldn't open conf: ~[[_1]~]\n",$conf) 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};
  prterr($opt,"Read conf: [_1]\n",$conf_str) unless $opt->{q};	#LVL=2
  $opt->{_read_conf}{$conf} = 1;	# We read a conf file

  while (<$fh>) {
    my ($option,$val) = read_conf_line($_);
    if ($option==-1) {
      pperror($opt,"Can't understand conf: ~[[_1], line [_2]~]\n  [_3]",$conf_str, $., $_);
      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($opt,"Unknown album conf option:\n\t  [_1]","-$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, $PARSE_CONF, $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; #NOTRANS
      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;	#NOTRANS
    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 pperror($opt,"Couldn't write conf: ~[[_1]~]",$conf)
    unless (open(CONF,">$conf"));
  print CONF @new;
  close CONF;
  prtout($opt,"Saved command line options in ~[[_1]~]\n",$conf) 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,$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 ($opt,$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($opt,$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($opt,$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..
  nlppwarn($opt,"duplicate found a ref: '[_1]' - don't know what to do!?",$ref)	#NOTRANS
    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?
  ($pushed,my $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) = @_;

  return if do_hook($opt,undef, 'scrub_opts');
  # Description: Called before scrubbing opts, after parsing args.
  # Description: WARNING!  This cannot be called unless the calling plugin
  # Description:   is specified on the command-line.  This should only
  # Description:   be used by one-time plugins!  (i.e.: utils/mv)
  # Description: Use 'do_album' or 'do_album_top' instead.
  # Returns: 1 to skip normal ARGV scrubbing (generally a bad idea!)

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

  # 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($opt,"Can't add root directory ~[[_1]~]",$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|M4V|3GP|WMV|FLV" 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}) {
      nlppwarn($opt,"Ignoring '[_1]' when '[_2]' set\n\tUse '[_3]' if you want tcap", '-use_tcap','-cygwin','-no_cygwin');
      $opt->{use_tcap} = 0;
    }

    if ($opt->{windows}) {
      nlppwarn($opt,"Option [_1] doesn't seem to work well under windows",'-clean')
        if $opt->{clean};

      my @path = get_path($opt);
      $opt->{convert} = search_path_win($opt,$opt->{convert}, @path);
      usage($opt,"Couldn't find path for [_1], try specifying with [_2] option\n", 'convert', '-convert')
        unless $opt->{convert};
      $opt->{identify} = search_path_win($opt,$opt->{identify}, @path);
      usage($opt,"Couldn't find path for [_1], try specifying with [_2] option\n", 'identify', '-identify')
        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($opt,"Need to specify [_1] with [_2] option", '-medium <geom>', '-just_medium')
    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} || $opt->{crf};

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

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

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

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

	if ($opt->{sort} eq 'exif') {
		eval "require Date::Parse";
		usage($opt,"Error: cannot use -sort exif - Date::Parse not installed") unless $@ eq '';
		import Date::Parse;
	}

	# 'force_charset' overrides language file
	# 'charset' is user setting (overridden by languages)
	# 'default_charset' is our base setting
	$opt->{charset} = $opt->{force_charset} || $opt->{charset} || $opt->{default_charset};
}

##################################################
# Parsing Themes
##################################################
sub list_themes {
  my ($opt, $option, $plugin) = @_;

	# Find all themes
	my %saw;
	foreach my $path ( @{$opt->{theme_path}} ) {
 		foreach my $epath ( expand_path($opt, $path) ) {
			next unless opendir(PATH,$epath);
			my @t = grep { !/^\.{1,2}$/ && -d "$epath/$_" } readdir(PATH);
			# Kludge?  Ignore '.old' and '.bak' directories
			@t = grep { !/\.(bak|old)$/ } @t;
			foreach my $t ( sort {uc($a) cmp uc($b)} @t ) {
				next unless -f "$epath/$t/album.th" || -f "$epath/$t/image.th";
				next if $saw{$t}++;
				if ($opt->{crf}) {
					print "crf:theme: $epath/$t\n";
					next;
				}
				# Show theme info
				print "Theme:  $t\n";
				print "Path:   $epath/$t\n";
				next unless open(CREDIT,"<$epath/$t/CREDIT");
				my $desc = scalar <CREDIT>;
				my $author = scalar <CREDIT>;
				$desc =~ s|<a [^>]+=['"]?([^>]+?)['"]?>(.+)</a>|$2 [$1]|g;
				$author =~ s|<a [^>]+=['"]?([^>]+?)['"]?>(.+)</a>|$2 [$1]|g;
				close CREDIT;
				print "Author: $author";
				print "$desc";
				print "\n";
			}
			closedir PATH;
		}
	}
	exit;
}

# Our wrapper to interpret themes for when we package album as an stand-alone executable
sub theme_interp_wrap {
	my @script = <>;
	no strict;
	eval(join("",@script));
	exit 0 unless $@;
	print STDERR $@;
	exit -1;
}

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

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

  # -theme_url needs -theme
  hash_warn($opt,"[_1] still requires [_2] option, it doesn't replace it",'-theme_url','-theme')
    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_out($opt,"Couldn't find [_1] directory ~[[_2]~] (ignoring)\n\t(Try specifying [_3] with [_1])",'-theme',$dir,'-theme_path')
    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($opt,"No themes found in ~[[_1]~]",$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($opt,"Couldn't read theme ~[[_1]~]",$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, $PARSE_THEME, $file);
      $start_line = $.+1;
      next;
    }
    $top = 0;
    push(@{$opt->{_theme}{$which}},$_);
    $in_head=1 if (/<head>/i);
    if (/Meta\(\)/) {
      usage($opt,"You need to call [_1]() inside <head>..</head> of ~[[_2]~]",'Meta', $file)	#EXPLANATION: "Meta()" is a subroutine they must call inside of <head> and </head>
        unless $in_head;
      $saw_meta=1;
    }
    $in_head=0 if (/<\/head>/i);
    $saw_credit=1 if (/Credit\(\)/);
  }
  close(TEMP);

  usage($opt,"You need to call [_1]() inside <head>..</head> of ~[[_2]~]",'Meta', $file)
    unless $saw_meta;
  usage($opt,"You need to call [_1]() inside <head>..</head> of ~[[_2]~]",'Credit', $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};
	
	# Track the configuration file version
	my $conf_vers = $VERSION;
	$conf_vers =~ s/[^\d\.]//g;	# Just the numbers
	virgin_save($opt,'conf_version',$conf_vers);
	
	# First install.  Hopefully they've got a tty
	prterr($opt,"I see this is your first time running [_1] v[_2]\n\n",$PROGNAME, $VERSION)	#LVL=100
		unless $opt->{_read_conf} && keys %{$opt->{_read_conf}};
	prterr($opt,"I'm going to do some simple installations for you.\n\n");	#LVL=100
	
	install_language_attempt($opt);	# Check if they can choose a language for install

	# 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";
	
	prterr($opt,"I'll save your installation in:\n  [_1]\n\n",$conf);	#LVL=100
	prterr($opt,"You can edit this file later to suit your needs.\n\n");	#LVL=100
	prterr($opt,"** Run [_1] as root if you want to do a system-wide configuration. **\n\n",$PROGNAME)	#LVL=100
		if $< && -d '/etc';
	
	prterr($opt,"Are you ready?\n");	#LVL=100
	exit unless install_get_yn($opt,'y');

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

		install_language_attempt($opt);	# Check if they can now choose a language
	}
	
	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) {
				prterr($opt,"If [_1] has problems finding themes, make sure they're in:\n",$PROGNAME);	#LVL=100
				print STDERR "  $themes\n";
				prterr($opt,"\nOr edit [_1] to set their new location\n",$conf);	#LVL=100
			} else {
				prterr($opt,"I see that the -theme_path does not currently exist:\n  [_1]\n\n",$themes);	#LVL=100
				prterr($opt,"Would you like to try to install the themes there?\n");	#LVL=100
				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",trans($opt,"Themes"));
					if ($curr && !install_move($opt,$curr,$themes)) {
						prterr($opt,"\nCouldn't install themes!\n");	#LVL=100
						prterr($opt,"Please login as a user with the appropriate privileges and copy:\n");	#LVL=100
						print STDERR "  $curr -> $themes\n\n";
						install_hit_enter($opt);
					}
				}
			}
		}
		print STDERR "\n--------------------------------------------------\n\n";
	}
	
	# THEME URL
	unless ($opt->{theme_url} || !$themes) {
		my $ignore = $themes ne port_abs_path($opt,$themes) ? "" :
			trans($opt,"\n   (I've checked and #3 is not currently true.)\n");
		wraperr($opt, "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:\n\n1) You have virtual domains (in some cases).\n2) You're using mod_rewrite on the themes path.\n3) You use a symbolic link to point to the actual themes directory.[_1]\n\nIn 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.\n\nIf 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.\n\n", $ignore);
		
		while (!$opt->{theme_url}) {
			prterr($opt,"Do you want to specify a [_1]?\n",'-theme_url');	#LVL=100
			my $default = $ignore ? 'n' : 'y';
			last unless install_get_yn($opt,$default);
			$opt->{theme_url} = install_get_answer($opt,'theme_url','/Themes/',
				trans($opt, "Some reasonable values:  [_1],  [_2]", '/Themes/', 'http://yourdomain.com/Themes/'));
			undef $opt->{theme_url} if $opt->{theme_url} =~ /\S\s+\S/;
			prterr($opt,"\nThe -theme_url value should not contain any spaces\n\n")	#LVL=100
				unless $opt->{theme_url};
		}
		virgin_save($opt,'theme_url');
	}
	
	# CONVERT/IDENTIFY
	print STDERR "\n";
	install_find_exec($opt,'convert');
	install_find_exec($opt,'identify',1);
	install_find_exec($opt,'ffmpeg',1);
	
	# DONE!
	prterr($opt,"\nEnd of configuration\n\n");	#LVL=100
	
	# Save it.
	save_conf($opt,$conf, '_virgin');
	undef $opt->{_virgin};
	
	exit if $opt->{configure};
	# Otherwise we can continue to run album.
	
	prterr($opt,"Continuing with [_1] run:\n",$PROGNAME);	#LVL=100
}

# Set a conf value for the install configuration
sub virgin_save {
	my ($opt, $conf, $val) = @_;
	
	$opt->{_virgin}{vals}{$conf} = defined $val ? $val : $opt->{$conf};
	push(@{$opt->{_virgin}{order}}, $conf);
}


# If we've found languages, then see if they want to specify an install lang
sub install_language_attempt {
	my ($opt) = @_;

	# Only ask this once.
	return if $opt->{_virgin}{set}{lang};

	# Have they already set a language?
	return unless $#{$opt->{lang}} == -1;

	# Can we find any languages yet?
	my @langs = list_languages($opt,'install_language_attempt', undef, 1);
	return unless @langs;

	$opt->{_virgin}{set}{lang} = 1;

	prterr($opt,"\nWould you like to specify a default language to use?\n");	#LVL=100
	return unless install_get_yn($opt,'y');

	my $lang = install_get_menu($opt,
		trans($opt,"language"),
		trans($opt,"Available languages:"),
		,@langs);
	print STDERR "\n";

	return unless $lang;

	get_language($opt, 'install_language_attempt', $lang);
	virgin_save($opt,'lang',$lang) unless $lang eq 'en';
}

sub install_move {
	my ($opt,$from,$to) = @_;
	
	prterr($opt,"\nAttempting to move:\n");	#LVL=100
	print STDERR "  $from -> $to\n\n";
	
	# Attempt to move the directory.
	return prterr($opt,"Move successful.\n") if rename($from,$to);	#LVL=100
	
	# 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 prterr($opt,"Move successful.\n") unless $?;	#LVL=100
	0;
}

sub install_find_exec {
	my ($opt,$exec,$optional) = @_;
	
	my $try = $opt->{$exec};
	
	if ($try =~ m|[/\\]|) {
		prterr($opt,"Found [_1] setting:  [_2]\n",$exec,$try);	#LVL=100
		prterr($opt,"  But it's missing or not executable!\n") unless -x $try;	#LVL=100
		prterr($opt,"\nFix?\n");	#LVL=100
		return unless install_get_yn($opt, -x $try ? 'n' : 'y');
	} else {
		# Search path
		$try = $try || $exec;
		# Actually, if it's windows we should have found it via scrub_opts
		my @path = get_path($opt);
		my $found = search_path_exec($opt,$exec,@path);
		return prterr($opt,"Found [_1] in path:  [_2]\n",$exec,$found) if -x $found;	#LVL=100
	}
	
	while (1) {
		prterr($opt,"\nCan't find [_1] executable.\n",$exec);	#LVL=100
		
		if ($optional) {
			prterr($opt,"This is optional but is useful to [_1].\n",$PROGNAME);	#LVL=100
			prterr($opt,"Do you want to enter a path to [_1]?\n",$exec);	#LVL=100
			return unless install_get_yn($opt, 'n');
		} else {
			prterr($opt,"I need to know where this is for [_1] to run properly.\n",$PROGNAME);	#LVL=100
		}
		
		$try = install_get_path($opt,trans($opt, "path to [_1]", $exec));	#LVL=100
		
		return if $optional && !$try;
		last if -x $try;
		prterr($opt,"\nExecutable not found: [_1]\nTry again?\n",$try);	#LVL=100
		return unless install_get_yn($opt,'y');
	}
	
	# We need to save this
	$opt->{$exec} = $try;
	virgin_save($opt,$exec);
}

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};
	
	prterr($opt,"- I see you specified the -theme_path.  Using ~[[_1]~]\n",$themes)	#LVL=100
		if $themes;
	return $themes if -x $themes;
	prterr($opt,"  ..but I can't find the directory!\n\n") if $themes;	#LVL=100
	
	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;
 	$example = '/var/www/html/Themes' unless $example =~ /themes/i;
	
	prterr($opt,"- You should specify the option: '[_1]'\n",'theme_path');	#LVL=100
	prterr($opt,"- You can override this value on the command-line: [_1]\n\n",'-theme_path');	#LVL=100
	$themes = install_get_path($opt,'theme_path',$themes,
		wrap_trans($opt, "[_1] supports themes for web page layout, they were probably included with the package you installed, or can be downloaded from:\n  [_2]\n\nYou 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.\n\nThis 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:\n  [_3]\n\nJust hit return if you don't want themes.\n",
			$PROGNAME, $ALBUM_URL, $example));
	
	return unless $themes;
	push(@{$opt->{theme_path}}, $themes);
	virgin_save($opt,'theme_path', $themes);
	$themes;
}

# Find where the themes/plugins/lang were downloaded
sub install_find_current_data {
	my ($opt,$what,$What) = @_;
	my ($curr,$where) = ("/usr/share/$PROGFILE/$what",
		trans($opt, "\n  ** Default is the dpkg install of [_1] **",$PROGNAME));
	($curr,$where) = ("./$what",
		trans($opt, "\n  ** Found local [_1] directory **", "./$what")) 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) {
		prterr($opt,"\nI found a gzipped [_1] tar: [_2]\n\nWould you like me to try to unzip it?\n",$What,$targz);	#LVL=100
		if (install_get_yn($opt,'y')) {
			system("tar","xzf",$targz);
			if ($?) {
				# Try gunzip/tar
				system("gunzip",$targz);
				system("tar","xf",$tar) unless $?;
			}
			return $what unless $? || !-d $what;
			$? ? prterr($opt,"\nError untarring: [_1]\n\n",$!) :	#LVL=100
				nlperror($opt,"Untarring didn't create expected directory '[_1]'\n\n",$what);	#LVL=100
		}
	}
	
	my $zip = "${PROGFILE}_$what.zip";
	if (-f $zip && !-d $what) {
		prterr($opt,"\nI found a zipped [_1] archive: [_2]\n\nWould you like me to try to unzip it?\n",$what,$zip);	#LVL=100
		if (install_get_yn($opt,'y')) {
			system("unzip",$zip);
			return $what unless $? || !-d $what;
			$? ? prterr($opt,"\nError unzipping: [_1]\n\n",$!) :	#LVL=100
				nlperror($opt,"Unzipping didn't create expected directory '[_1]'\n\n",$what);	#LVL=100
		}
	}
	
	while (1) {
		my $cancel = trans($opt, "cancel");	#LVL=100
		my $question = trans($opt,"downloaded [_1] location", $what);	#EXPLANATION: $what is a 'tar' or 'zip'	#LVL=100
		my $dir = install_get_path($opt, $question, $curr || $cancel,
			trans($opt, "I need to find where you downloaded the [_1].  [_2]", $What,$where));
		return $dir if -d $dir;
		prterr($opt,"\nDirectory not found: [_1]\nTry again?\n",$dir) unless $dir eq $cancel;	#LVL=100
		prterr($opt,"\nTry to find [_1] to install them?\n",$What) if $dir eq $cancel;	#LVL=100
		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;	# already translated.
			for (my $i=0; $i<$items; $i++) {
				printf STDERR "%2d] $menu[$i]\n",$i+1;
			}
			print STDERR "\n";
		}
		
		# Get answer
		my $range = trans($opt, "[_1]-[_2]", 1, $items);	#EXPLANATION: A range of numbers, i.e. (25-32)	#LVL=100
		my $cancel = trans($opt, "[_1] to cancel", 0);	#EXPLANATION: Menu option to cancel selection	#LVL=100
		my $ans = install_get_answer($opt,$question." [$range, $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_en) = @_;
	
	# Language support
	my $y = trans($opt,"y");	#EXPLANATION: y as in 'yes' - can be more than one character if necessary	#LVL=100
	my $n = trans($opt,"n");	#EXPLANATION: n as in 'no'	#LVL=100
	my $default = trans($opt,$default_en);	#NOTRANS
	
	while (1) {
		my $ans = install_get_answer($opt,"$y/$n",$default);
		return 1 if $ans =~ /^$y/i;
		return 0 if $ans =~ /^$n/i;
		prterr($opt,"You must answer '[_1]' or '[_2]'\n",$y,$n);	#LVL=100
	}
}

sub install_get_answer {
	my ($opt,$question,$default,$desc) = @_;
	
	print STDERR "$desc\n" if $desc;	# already translated.
	my $ret;
	while (!defined $ret) {
		prterr($opt,"Enter [_1]",$question);	#LVL=100
		prterr($opt," ~[default: [_1]~]",$default) if $default;	#LVL=100
		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) = @_;
	prterr($opt,"Press enter/return to continue ");	#LVL=100
	scalar <>;
}

# album doesn't use this, but plugins can.
sub install_get_text {
	my ($opt,$question,$desc, @args) = @_;
	
	prterr($opt,"$desc\n", @args) if $desc;	#NOTRANS
	my $ret;
	prterr($opt,"Finish typing text by entering a line with only '.' on it.\n");	#LVL=100
	my $num = 0;
	while (1) {
		prterr($opt,"Enter [_1]",$question) unless defined $ret;	#LVL=100	#EXPLANATION: As in: "Enter a default path"
		print STDERR "> ";
		my $line = scalar <STDIN>;
		return $ret if ($line eq ".\n");
		$ret .= $line;
		prterr($opt,"Finish typing text by entering a line with only '.' on it.\n")	#LVL=100
			unless ++$num % 10;
	}
}

##################################################
##################################################
# 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 ($opt,$dir) = @_;
  my $save=`pwd`; chomp($save);
  chdir($dir) || usage($opt,"Couldn't find directory ~[[_1]~]", $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($opt,"Couldn't find directory  ~[[_1]~]", $dir);
  return $CWD ? Cwd::abs_path($dir) : int_abs_path($opt,$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;
  }

  $from =~ s|//|/|g;
  $to =~ s|//|/|g;

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

sub join_paths {
	my (@paths) = @_;
	@paths = grep($_ ,@paths);
	join('/',@paths);	# perhaps we should use $opt->{slash} here...
}

# Like search_path below, but return all matches for a given regexp
# Each match is an array ref:  [$fullpath, $dir, $subpath, $file]
sub path_matches($$\@;$) {
  my ($opt,$match,$paths,$sub) = @_;

	my %saw_path;

	my @ret;
	foreach my $path ( @$paths ) {
  	# Expand @DATA_PATH in path
  	foreach my $epath ( expand_path($opt, $path) ) {
			next if $saw_path{$epath}++;
    	my $dir = $sub ? "$epath/$sub" : $epath;

    	opendir(DIR,"$dir") || next;
    	my @dir = readdir(DIR);
    	closedir(DIR);
    	my @p = $match ? grep(/$match/, @dir) : grep(-f "$dir/$_", @dir);
    	push(@ret, map {[join_paths($epath,$sub,$_),$epath,$sub,$_]} @p);
    	my @d = grep(-d "$dir/$_" && !/^\.{1,2}$/, @dir);
    	push(@ret, map( path_matches($opt,$match,[$epath],$sub?"$sub/$_":$_), @d));
  	}
	}
	@ret;
}

# 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) = @_;
#$path =~ s|/|\\|g;
  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 hash_msg_width {
	my ($opt) = @_;
	my $width = $opt->{screen_width} || 78;
	my $w = $width - $opt->{num_hashes} - 3;
	$w>10 ? $w : 10;
}

sub start_hashes {
  my ($opt,$str, @args) = @_;
  return if $opt->{q};

  my $msg = trans($opt,$str,@args);	#NOTRANS

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

	$msg = "crf:start_hashes: $msg" if $opt->{crf};
  return print STDERR "$msg\n" unless $opt->{hashes};
  my $w = hash_msg_width($opt);
  $_hashes_start = $msg ? sprintf("%-${w}s",$msg) : "";
  $_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;

	print STDERR "crf:hashes: $done/$outof\n" if $opt->{crf};

  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;

  show_hashes($opt,1,1);
  return unless $opt->{hashes} && !$opt->{q};
  undef $_hashes_start;
  undef $_hashes_done;
  print STDERR "]\n";
}
# Translation is done by caller
sub hash_msg {
  my ($opt,$str) = @_;
  return unless $opt->{hashes};
  printf STDERR "%".$opt->{num_hashes}."s]\n",$str;
  undef $_hashes_start;
}

sub _hash_out {
  my ($opt,$pre,$str,@args) = @_;

  $pre = trans($opt,$pre) if $pre;	#NOTRANS
  my $msg = trans($opt,$str,@args);	#NOTRANS

  $msg = "$pre$msg" if $pre;

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

  print STDERR "\n" if $_hashes_start;
  print STDERR "[$PROGNAME] $msg\n";
  return unless $opt->{hashes} && ($_hashes_done || $_hashes_start);
  start_hashes($opt,$_hashes_start);	#NOTRANS
  print STDERR "X"x$_hashes_done;
  undef;
}

sub hash_out {
  my ($opt,$str,@args) = @_;
  _hash_out($opt,undef,$str,@args);
}

sub hash_warn {
  my ($opt,$str,@args) = @_;
  _hash_out($opt,"WARNING: ",$str,@args);
}

##################################################
##################################################
# LANGUAGE SUPPORT
##################################################
##################################################

# These concepts are stolen from Locale::Maketext.
# Thanks to Sean Burke for a clean solution!

# LANGUAGE TODO:
# add_option usage? (then gen_pod for multi language man pages?)
# make_language:  Separate finished translations from unfinished?

#########################
# Translation of strings
#########################

# Find a translation string
my $NOTRANS = '[\s\*-]*';	# regex at beg/end of lines that aren't translated
sub lang_trans {
	my ($opt, $str, @args) = @_;

	# Stuff at beginning/end of lines that we don't translate
	my $pre = ($str =~ s/^($NOTRANS)//) ? $1 : "";
	my $post = ($str =~ s/($NOTRANS)$//) ? $1 : "";

	# If we can't find a translation, consider default: "_"
	my $trans = $opt->{_lang}{$str} || $opt->{_lang}{_};

	# Is it a code ref?
	$trans = &$trans($opt,$str, @args) if ref $trans eq 'CODE';	#NOTRANS

	wantarray ? ($pre, $trans || $str, $post) : $trans;
}

# Translate a string of Locale::Maketext format.
sub trans {
	my ($opt, $str, @args) = @_;

	# Only deal with text in between newlines.
	# All our translations are line at a time
	my @lines = split(/(\n+)/, $str);
	if ($#lines>0) {
		for(my $i=0; $i<=$#lines; $i+=2) {
			my $line = $lines[$i];
			$lines[$i] = trans($opt, $lines[$i], @args);	#NOTRANS
		}
		return join('',@lines);
	}

	my ($pre,$trans,$post) = lang_trans($opt,$str, @args);

	# Deal with [code]
	$trans =~ s/(?<!~)\[(.*?[^~])\]/trans_handle($opt,$1,@args)/eg;

	# Unquote ~[, ~]
	#   This is a little broken, if "~[" is some value we get back from
	#   trans_handle, we don't necessarily want to unquote it.
	#   We should probably do a split above to fix this.
	$trans =~ s/~([\[\]])/$1/g;

	$trans = $pre.$trans.$post;

	# Handle '_post' coderefs
	$post = $opt->{_lang}{_post};
	$trans = &$post($opt,$trans, @args) if $post && ref $post eq 'CODE';

	$trans;
}

sub wrap_trans {
	my ($opt, $str, @args) = @_;
	wrap($opt, trans($opt,$str, @args));	#NOTRANS
}

sub _digits {
	my ($str, @args) = @_;

	return $args[$1-1] if $str =~ /^_(\d+)$/;
	return $args[$1] if $str =~ /^_(-\d+)$/;
	$str;
}

# Numify a number
#   numf(-1999) -> "-1,999"
# Weird things happen if you give numf a non-number
sub _numf {
	my ($num, $comma) = @_;

	my $float = ($num =~ s/(\.\d+)$//) ? $1 : "";

	$comma = $comma || ',';

	my $numify = undef;
	while ($num =~ s/(\d)(\d{3})$/$1/) {
		$numify = defined $numify ? $2.$comma.$numify : $2;
	}
	$numify = defined $numify ? $num.$comma.$numify : $num;
	$numify.$float;
}

# Default is to use a comma, but we can easily replace
# this function in the language library.
sub numf {
	my ($num) = @_;
	_numf($num,',');
}

# Quantifying a noun.  $plural and $neg are optional args.
#   quant($num, "file")
#   quant($num, "directory", "directories")
#   quant($num, "directory", "directories", "no directories")
# Will return things like "42,103 directories"
sub quant {
	my ($num,$sing,$plural,$neg) = @_;
	return $neg unless $num || !defined $neg;
	return "1 $sing" if $num==1;

	$num = numf($num);
	$plural ? "$num $plural" : "$num ${sing}s"
}

# Handle [..] code in translations
sub trans_handle {
	my ($opt, $str, @args) = @_;

	# Split up tokens by commas
	my ($cmd,@str) = split(/(?<!~),/, $str);

	# Unquote "~," and "~~"
	@str = map { s/~([~,])/$1/g; $_ } @str;

	# Do _digits conversions for everything except the first token
	@str = map { _digits($_, @args) } @str;

	# Handle: quant/*
	return quant(@str)
		if $cmd eq "quant" || $cmd eq "*";

	# Handle: numf/#
	return numf(@str)
		if $cmd eq "numf" || $cmd eq "#";

	# Handle: sprintf
	return sprintf(shift @str,@str)
		if $cmd eq "sprintf";

# Arguably we should check to make sure $cmd is either an empty
# string or a _<num> to match Maketext, but maybe later...

	# Handle: _digits/null
	$cmd = _digits($cmd, @args);
	return join('',$cmd, @str);
}

sub space_length {
	my ($opt, $space) = @_;
	# Tabs count as 8 - though sometimes they could be less... :(
	# At worst we'll overcount length, which is okay for wrap()
	length($space) + 7*($space =~ tr/\t/\t/);
}

sub word_length {
	my ($opt, $word) = @_;
	# Consider encodings that have single characters like "&#xea;" ??
	# lang/pl (iso-8859-2) has some of these.
	# Worst case scenario is that we overcount, which again is okay for wrap()
	length($word);
}

# Hard word wrap (splitting a word) (hyphenation)
sub word_split {
	my ($opt, $word, $width) = @_;
	# Non-hyphenated:
	#(substr($word,0,$width), substr($word,$width));
	# Hyphenated
	(substr($word,0,$width-1)."-", substr($word,$width-1));
}

# Wrap text to a specific width.
# Handles ASCII and unicode, see "word_length()" for other encodings.
sub wrap {
	my ($opt, $str, $width) = @_;

	# Wrap width
	$width = $width || $opt->{screen_width};
	return $str unless $width;	# -screen_width=0 means no wrap.

	my @ret;

	# Breakup newlines
	my @lines = split(/(\n+)/, $str);
	foreach my $line ( @lines ) {
		if ($line =~ /^\n/) {
			push(@ret, $line);
			next;
		}

		# Even pieces will be (\s+) whitespace, odd pieces are words
		# This works for unicode as well, but probably not for other encodings.
		my @pieces = split(/(\s+)/, $line);
		while (@pieces) {
			my $chop = shift(@pieces);
			my $length = word_length($opt,$chop);
			# If we want a hard fold (hyphenation?) that would go here..
			if ($length > $width) {
				my ($pre,$post) = word_split($opt, $chop, $width);
				unshift(@pieces, $post);
				$chop = $pre;
			} else {
				while (@pieces) {
					my $space = shift(@pieces);
					my $new_length = space_length($opt,$space)+word_length($opt,$pieces[0]);
					last if $length+$new_length > $width;
					$length += $new_length;
					$chop .= $space.shift(@pieces);
				}
			}
			$chop .= "\n" if @pieces;
			push(@ret, $chop);
		}
	}
	join("",@ret);
}

#########################
# Loading languages
#########################

# Unfortunately this is called early, possibly before lang_path or data_path
# is set.  We could try again after changes to lang_path/data_path, but what
# if the user has loaded a language at that point?
# We'll just let it go, if they need a default lang they can do it in the conf.
sub init_language {
	my ($opt) = @_;

# also see Win32::Locale for MSWin
	get_language($opt, 'init_language', $ENV{LANG}, 1) if $ENV{LANG};
	get_language($opt, 'init_language', $ENV{LANGUAGE}, 1) if $ENV{LANGUAGE};
}

sub en_language_info {
	return {
		language_code => 'en',
		language => 'English',
		language_english => 'English',
		album_version => $VERSION,
		language_version => 1,
		charset => "iso-8859-1",
		translators => [
			[ "David Ljung Madison", "http://GetDave.com/" ],
		],
	};
}

# These subs get replaced by proper language modules
sub language { return undef; }
sub language_info { return en_language_info(); }

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

	my @langs = path_matches($opt,'',@{$opt->{lang_path}});
	my %short;
	map { $short{$_->[3]} = 1; } @langs;
	my @short = sort keys %short;
	unshift(@short,'en');	# Add english to the list

	# Show real languages first
	my @real = sort grep(/^..(-|$)/, @short);
	my @fake = sort grep(!/^..(-|$)/, @short);
	@short = (@real,@fake);

	return @short if $just_return;

	if ($opt->{crf}) {
		map { print "crf:lang: $_\n" } @short;
		exit;
	}

	# Handle -list_html_trans
	if ($option eq 'list_html_trans') {
		my @t = theme_trans($opt);
		print <<TRANS_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><meta http-equiv='Content-Type' content='text/html; charset=utf-8' /></head>
	<body>
	<h1>HTML Translations</h1>
	<h2>Shown in 'utf-8' (most iso-8859-1 works)<br>Edit to see other charsets</h2>
TRANS_HEADER
		print "<table border><tr><th>Lang:</th><th>charset</th><th>";
		print join("</th><th>",@t),"</th></tr>\n";

		foreach my $lang ( @short ) {
			next if $lang eq 'banner';	# Because that's just silly.
			clear_langs($opt);
			next unless install_language($opt,$lang);
			@t = theme_trans($opt);
			my $charset = $opt->{_langInfo}[-1]{charset};
			$charset = "<font color=red>$charset</font>"
				if $charset && $charset ne 'iso-8859-1' && $charset ne 'utf-8';
			print "<tr><td>$lang</td><td>$charset</td>";
			print map { "<td>$opt->{trans}{$_}</span></td>" } @t;
			print "</tr>\n";
			next;
		}
		print "</table></body></html>\n";
		exit 0;
	}

	prtout($opt, "Available languages:\n");
	# Just list languages quickly
	unless ($option) {
		print wrap($opt, "  ".join(', ',@short)."\n");
		exit 0;
	}

	# First find out expected language strings
	my (%expect,$expect_tot,$expect_html, $expect_char);
	my $ml = get_language_strings($opt);
	foreach my $lvl ( sort {$a<=>$b} keys %$ml ) {
		foreach my $line ( @{$ml->{$lvl}} ) {
			next if $expect{$line->{line}};
			$expect_tot++;
			$expect{$line->{line}} = $line;
			$expect_html++ if $line->{lvl}<=1;
			$expect_char += length($line->{line});
		}
	}

	# Similar to get_language..
	foreach my $lang ( @short ) {
		my ($info,$language) = load_language($opt,$lang);

		# en is special case
		if ($lang eq 'en' && !$info) {
			$info = en_language_info($opt);
			$language->{_} = '_';
		}

		unless ($info) {
			prterr($opt, "Couldn't find language [_1]\n", $lang);
			next;
		}

		print "  $info->{language_code}: $info->{language} ($info->{language_english})\n";	#NOTRANS

		# How much complete?
		# (assume that all keys are present whether translated or not)
		my ($trans,$html,$char) = (0,0,0,0);
		if ($language->{_} || $language->{_post}) {
			($trans,$html,$char) = ($expect_tot,$expect_html,$expect_char);
		} else {
			foreach my $key ( keys %$language ) {
				next unless $expect{$key};	# Only if it's used.
				next unless $language->{$key};	# Only if there's a translation
				$trans++;
				$char += length($key);
				$html++ if $expect{$key}{lvl}<=1;
			}
		}
		my $percent = $expect_tot ? int(1000*$trans/$expect_tot)/10 : 0;
		my $char_percent = $expect_char ? int(1000*$char/$expect_char)/10 : 0;
		my $html_percent = $expect_html ? int(1000*$html/$expect_html)/10 : 0;
# Do I need all this info??
		#prtout($opt, "    Complete: [_1]%   (character count: [_2]%, HTML translations: [_3]%)\n", $percent, $char_percent, $html_percent);
		# What about just?
		prtout($opt, "    Complete: [_1]%   HTML: [_2]%\n", $char_percent, $html_percent);

		# Credit
		foreach my $trans ( @{$info->{translators}} ) {
			my ($name,$emailOrURL) = (@$trans);
			prtout($opt, "    translator: [_1] <[_2]>\n", $name, $emailOrURL);
		}
		print "\n";
	}
	exit 0;
}

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

	my @langs = list_languages($opt,'check_languages', undef, 1);

	my $brack_re = '(?<!~)\[(.*?[^~])\]';
	my $brack_sub_re = '\[(quant|\*|numf|\#),';

	# First figure out all the language strings we expect
	my $ml = get_language_strings($opt);
	my %expect;
	foreach my $lvl ( sort {$a<=>$b} keys %$ml ) {
		foreach my $line ( @{$ml->{$lvl}} ) {
			$expect{$line->{line}} = $line;
		}
	}

	my $total;
	foreach my $lang ( @langs ) {
		print "Checking language: $lang\n";
		my $errors;
		my ($info,$language) = load_language($opt,$lang);
		foreach my $line ( keys %$language ) {
			my $trans = $language->{$line};
			next unless $trans;
			my @brack = grep(/$brack_re/, split(/($brack_re)/, $line));
			my @trans_brack = grep(/$brack_re/, split(/($brack_re)/, $trans));
			next unless $#brack >= 0;
			my $off = 0;
			foreach my $brack ( @brack ) {
				next if grep($_ eq $brack, @trans_brack);
				next if $brack =~ /$brack_sub_re/ && grep(/$brack_sub_re/, @trans_brack);
				next if $brack =~ /${brack_sub_re}_(\d+)/ && grep(/\[_$2\]/, @trans_brack);	# [*,_1..] = [_1]
				print "[$lang] Missing '$brack' in translation\n";
				$off++;
			}
			foreach my $trans_brack ( @trans_brack ) {
				next if grep($_ eq $trans_brack, @brack);
				next if $trans_brack =~ /$brack_sub_re/ && grep(/$brack_sub_re/, @brack);
				next if $trans_brack =~ /\[_(\d+)\]/ && grep(/${brack_sub_re}_$1/, @brack);	# [_1] = [*,_1..]
				print "[$lang] Translation has '$trans_brack' not found in original\n";
				$off++;
			}
			next unless $off;
			$errors += $off;

			my $short = length($line)>60 ? substr($line,0,58).'..' : $line;
			print "\t$short\n";
			my $short_trans = length($trans)>60 ? substr($trans,0,58).'..' : $trans;
			print "\t$short_trans\n";
		}

		foreach my $line ( keys %$language ) {
			next if $expect{$line};
			next if $line eq '_' || $line eq '_post';
			my $short = length($line)>60 ? substr($line,0,58).'..' : $line;
			print "[$lang] Unused line: $short\n";
		}

		print "-> ", quant($errors,"error",undef,"No errors"), "\n" unless $opt->{q};
		$total += $errors;
	}

	exit $total;
}

# Clear all installed languages
sub clear_langs {
	my ($opt) = @_;
	undef %{$opt->{_lang}};
	undef @{$opt->{_langInfo}};
	undef @{$opt->{lang}};
	return;
}

# Install a language into our current "dictionary"
sub install_language {
	my ($opt,$lang) = @_;

	my ($language_info, $language) = load_language($opt,$lang);

	return 0 unless $language_info && $language;

	$opt->{charset} = $language_info->{charset}
		if !$opt->{force_charset} && $language_info->{charset};

	# Load it into the current language hash
	map { $opt->{_lang}{$_} = $language->{$_} if $language->{$_} } keys %$language;

	push(@{$opt->{_langInfo}}, $language_info);
	push(@{$opt->{lang}}, $lang);
	return 1;
}

# Load a language (and possibly it's superset)
sub get_language {
	my ($opt,$option,$lang, $quiet) = @_;

	# List languages if no language specified
	return list_languages($opt) unless $lang;

	# If loading language XX-xx, first load XX
	my $XX = ($lang =~ /^(..)-..$/) ? $1 : undef;
	my $load_XX = ($XX && install_language($opt,$XX)) ? 1 : 0;
	my $load_XXxx = install_language($opt,$lang);

	return if $quiet;

	# Special case - 'en' is the default, and it just means
	# we should clear all languages (unless lang/en exists)
	# This allows us to have english subalbums in non-english albums.
	return clear_langs($opt) if $lang eq 'en' && !$load_XX;

	prterr($opt, "Couldn't find language [_1], loaded parent language [_2]\n", $lang, $XX)
		if $load_XX && !$load_XXxx;
	prterr($opt, "Couldn't find language [_1]\n", $lang)
		unless $load_XX || $load_XXxx;
}

# Load a language file (info and translations)
sub load_language {
	my ($opt,$lang) = @_;

	my ($language_info, $language);

	my ($path,$found) = search_path($opt,$lang, @{$opt->{lang_path}});
	($path,$found) = ($lang,1) if !$found && -f $lang;	# Allow for relative path
	return 0 unless $found;

	delete $INC{$path};	# In case we specify a language twice, re-include it.
	eval <<END_EVAL;
		require "$path";
		\$language_info = language_info(\$opt, \$lang);
		\$language = language(\$opt, \$lang);
END_EVAL

	if ($@ || ref $language ne 'HASH' || ref $language_info ne 'HASH') {
		ppwarn($opt,"Problem with loading language [_1]\n\t[_2]", $lang, $@);
		return 0;
	}

	pperror($opt, "Language found '[_1]' didn't match language requested '[_2]'\n", $language_info->{language_code}, $lang)
		unless $language_info->{language_code} eq $lang;

	($language_info, $language);
}


sub lang_has_code {
	my ($opt) = @_;
	foreach my $k ( keys %{$opt->{_lang}} ) {
		return 1 if ref $opt->{_lang}{$k} eq 'CODE';
	}
	0;
}

## A joke/test language creator
## Run -make_lang with this below: $curr_trans = trans_swedish_chef($opt,$line) unless $curr_trans;
#sub trans_swedish_chef {
#	my ($opt, $line) = @_;
#	my $u = url_quote($opt,$line); $u =~ s/'//g;
#	my $t = qx{lynx -dump "http://www.cs.utexas.edu/~jbc/bork/bork.cgi?input=$u&type=chef"};
#	$t =~ s/^[\s\n]+//g; $t =~ s/[\s\n]+$//g;
#	$t;
#}

# All functions that prototype as:  somefunc($opt,"some trans string", @args);
my @TRANS_FUNCS = qw(trans fatal
	prtout prterr perror pprog pperror ppwarn nlppwarn wraperr
	start_hashes hash_out hash_warn hash_error usage);

# Print out a language translation line
my %SAW_LANGUAGE_LINE;
sub make_language_line {
	my ($opt, $ml, $str, $post, @args) = @_;

	return if $opt->{make_lang_check};

	# What level of importance?
	my $lvl = ($post =~ s/\s*#LVL=(\d+)\s*//) ? $1 : 10;

	# Kludge: Convert \n, \t, \\ and \"
	if ($str =~ /(?<!\\)\\([^nt\\"])/) {
		prterr($opt, "-make_lang saw unknown \\code [_1]:\n  [_2]\n", $., $str);	#NOTRANS
		exit -1 if $opt->{make_lang_check};
	}
	$str =~ s/\\n/\n/g;	# A bit kludgy..  Quoting is more complicated than this.
	$str =~ s/\\t/\t/g;
	$str =~ s/\\\\/\\/g;
	$str =~ s/\\"/"/g;
	exit if $str =~ /"/;

	# Breakup newlines
	my @lines = split(/\n/, $str);
	foreach my $line ( @lines ) {
		next if $SAW_LANGUAGE_LINE{$line}++;

		# Remove beginning and end whitespace/non-trans stuff
		$line =~ s/^$NOTRANS//;
		$line =~ s/$NOTRANS$//;
		# Ignore lines with just variables (such as "\n$@\n")
		next if $line =~ /^\$[a-z\@]+$/i;
		# And empty lines
		next unless $line =~ /\S/;
		# And lines with only MakeText type vars [_1]
		next if $line =~ /^\s*(\[_\d+\]\s*)+$/i;
		prterr($opt, "-make_lang saw unknown \$var [_1]:\n  [_2]\n", $., $line) #NOTRANS
			if $line =~ /\$/;

		my %line;
		$line{lvl} = $lvl;
		$line{num} = $.;

		$line{exp} = ($post =~ /#EXPLANATION:\s+(.+)/) ? $1 : "";

		# The translation key->val
		# If it's CODE then we can't save it anyways..
		my $curr_trans = lang_trans($opt,$line);	#NOTRANS
		$line{line} = $line;
		$line{curr_trans} = $curr_trans;

		$line{args} = \@args if @args;

		push(@{$ml->{$lvl}}, \%line);
	}
}

sub print_language_lines {
	my ($opt, $ml) = @_;

	my $Explanation = trans($opt,"Explanation:");
	my $Args = trans($opt,"Args:");

	my $last_line = -1;
	my $tab = "\t";

	foreach my $lvl ( sort {$a<=>$b} keys %$ml ) {
		foreach my $line ( @{$ml->{$lvl}} ) {

			print MAKELANG "\n";

			print MAKELANG "$tab# line $line->{num}\n"
				unless !$lvl || $line->{num}==$last_line;
			$line->{num}=$last_line;

			print MAKELANG "$tab# $Explanation  $line->{exp}\n" if $line->{exp};

			my $curr_line = $line->{line};
			my $curr_trans = $line->{curr_trans};

			$curr_line =~ s/\\/\\\\/g;
			$curr_trans =~ s/\\/\\\\/g;
			$curr_line =~ s/'/\\'/g;
			$curr_trans =~ s/'/\\'/g;
			print MAKELANG "$tab'$curr_line'\n";
			print MAKELANG "$tab=> '$curr_trans',\n";

			if ($line->{args}) {
				my $args;
				for (my $i=0; $i<=$#{$line->{args}}; $i++) {
					$args .= "  [_".($i+1)."]=$line->{args}[$i]";
				}
				print MAKELANG "$tab# $Args$args\n";
			}
		}
	}
}

sub get_language_strings {
	my ($opt, $check) = @_;

	$opt->{make_lang_check} = $check;
 
	my $trans_funcs = join("|",@TRANS_FUNCS);

	#########################
	# Find all translation strings in this script
	#########################
	my $ml = {};
	open(A,"<$0") || fatal($opt,"Couldn't read album script?? ~[[_1]~]",$0);
	while (<A>) {
		next if /#NOTRANS/;

		# Catch: All functions in $trans_funcs
		# This will have problems if our trans strings have \" in them.
		if (/^[^#]*($trans_funcs)\(([^,]+),\s*"(([^"]|\\")+)"((,|\)).*)/) {
			my ($str,$post) = ($3,$5);

			my @args = ();
			@args = split(/\s*,\s*/, $1) if $post =~ s/,\s*([^\)]+?)\s*\)//;
			make_language_line($opt, $ml, $str, $post, @args);
			next;
		}

		# Catch: Any strings before #TRANS	(but here it's #NOTRANS)
		if (/"([^"]+)"([^"]*)#TRANS(\((.+)\))?/) {	#NOTRANS
			my ($line,$post,$args) = ($1,$',$4);
			my @args = $args ? split(/\s*,\s*/, $args) : ();
			make_language_line($opt, $ml, $line, $post, @args);
			next;
		}

		# Catch: add_option(1,'Album Options:',OPTION_SEP);
		if (/add_option\(.*'(.+)',\s*OPTION_SEP/) {
			make_language_line($opt, $ml, $1, '#EXPLANATION: For album usage');
		}

		# Did we miss any uncommented translation functions?
		if (/^[^#]*\b($trans_funcs)\((.*)/) {
			prterr($opt, "Internal error!! -make_lang didn't parse call line [_1]:\n  [_2]\n",	#NOTRANS
		   		$., "$1($2" );
			exit -1 if $opt->{make_lang_check};
			next;
		}

		# Did we miss any uncommented occurrences of [_1] and the like?
		if (/^[^#]*\[_\d+\]/) {
			prterr($opt, "Internal error!! -make_lang possibly missing translation line [_1]:\n  [_2]\n",	#NOTRANS
		   		$., $_ );
			exit -1 if $opt->{make_lang_check};
			next;
		}
	}
	close A;

	if ($opt->{make_lang_check}) {
		prterr($opt, "Successfully passed -make_lang_check\n");
		exit 0;
	}

	$ml;
}

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

	my $ml = get_language_strings($opt, $option eq 'make_lang_check');

	#########################
	# Figure out where to save the output
	#########################
	my $lang = $val;

	# Any way to easily check if $lang is a legal language???
	ppwarn($opt, "Language should only be two or three characters: [_1]", $lang)	#NOTRANS
		unless $lang =~ /^[a-z]{2,3}(-[a-z]{2})?$/i;	# Allow for sublanguage codes (xx-xx)

	# Could we/should we have loaded the language first?
	my $info;
	my ($path,$found) = search_path($opt,$lang, @{$opt->{lang_path}});
	if ($found) {
		my @m = grep($_->{language_code} eq $lang, @{$opt->{_langInfo}});
		$info = pop(@m);
		ppwarn($opt, "A language file already exists for [_1].\n\tYou should probably load it first:\n\t[_2]",$lang,"% album -lang $lang -make_lang $lang") unless $info;	#NOTRANS
	}

	ppwarn($opt, "Current language has code references that won't be copied.\n")
		if (lang_has_code($opt));

	fatal($opt, "Language file [_1] already exists.  Refusing to overwrite.", $lang)	#NOTRANS
		if -f $lang;
	open(MAKELANG, ">$lang") || fatal($opt, "Couldn't write language file [_1]", $lang);	#NOTRANS

	#########################
	# Header
	#########################
	my $date = localtime();
	my $this_is = trans($opt, "This is a language translation file");
	$this_is = "" if $this_is eq "This is a language translation file";
	my $Program = sprintf("%-25s",trans($opt,"Program:"));
	my $Created = sprintf("%-25s",trans($opt,"Created:"));
	my $Language_code = sprintf("%-25s",trans($opt,"Language code:"));
	my $Language = trans($opt,"Language:");
	my $language = $info->{language} || '????';
	my $language_english = $info->{language_english} || '????';
	my $language_version = $info->{language_version}+1;
	my $Charset = sprintf("%-25s",trans($opt,"Charset:"));
	my $charset = $opt->{charset};	# Just guess
	#$charset = "utf-8" if $charset eq $opt->{default_charset};	# More guessing..
	my $English = trans($opt,"English");
	$English = sprintf("%-25s","$Language ($English)");
	$Language = sprintf("%-25s",$Language);

	my @translators = $info->{translators} ? @{$info->{translators}} :
		( ["author name", "author email/url"],
			["another author name", "another author email/url"] );

	my $translators;
	foreach my $t ( @translators ) {
		my ($name,$url) = @$t;
		$name =~ s/"/\\"/g;
		$url =~ s/"/\\"/g;
		$translators .= "\t\t\t[ '$name', '$url' ],\n";
	}

	print MAKELANG <<HEADER;
#####################################
# This is a language translation file
# $this_is
# $Program  $PROGNAME
# $Created  $date
# $Language_code  $lang
# $Language  $language
# $Charset  $charset
# $English  $language_english
#####################################

sub language_info {
	return {
		language_code =>     '$lang',
		language =>          '$language',
		language_english =>  '$language_english',
		album_version	=>     "$VERSION",
		language_version =>  $language_version,
		charset =>           "$charset",
		translators => [		# List of:  ['name','email/url'],
$translators		],
	};
}

# Translations go here
sub language {
	return {
HEADER
	#########################
	# END Header
	#########################

	# The language lines
	print_language_lines($opt, $ml);

	#########################
	# Footer
	#########################
	print MAKELANG <<FOOTER;
	};
}

# Language file must return:
1;

FOOTER

	close MAKELANG;

	prterr($opt, "Successfully wrote language file: [_1]\n", $lang);	#NOTRANS
	exit 0;
}

#########################
# Translating output functions
#########################
sub print_trans {
	my ($opt, $msg, $hash) = @_;

	return unless $msg;

	my @args = @{$hash->{args}};

	my $print = trans($opt,$msg,@args);	#NOTRANS
	$print = trans($opt,"WARNING:")." ".$print if $hash->{warning};
	$print = trans($opt,"ERROR:")." ".$print if $hash->{error};
	$print = "[$PROGNAME] $print" if $hash->{progname};
	$print = "$hash->{pre}$print" if $hash->{pre};
	$print = "$print$hash->{post}" if $hash->{post};

	$print = wrap($opt, $print) if $hash->{wrap};

	if ($hash->{stdout}) { print $print; }
	else { print STDERR $print; }
}

sub fatal {
	my ($opt, $msg, @args) = @_;
	print_trans($opt, $msg, {progname=>1, pre=>"\n", post=>"\n\n", args=>\@args});
	exit -1;
}

# STDOUT
sub prtout {
	my ($opt, $msg, @args) = @_;
	print trans($opt, $msg, @args);	#NOTRANS
}

# STDERR
sub prterr {
	my ($opt, $msg, @args) = @_;
	print_trans($opt, $msg, {args=>\@args});
}

# Word Wrap -> STDERR
sub wraperr {
	my ($opt, $msg, @args) = @_;
	print_trans($opt, $msg, {wrap=>1, args=>\@args});
}

# "ERROR: " message
sub perror {
	my ($opt, $msg, @args) = @_;
	print_trans($opt, $msg, {error=>1,post=>"\n", args=>\@args});
}

# "\nERROR: " message
sub nlperror {
	my ($opt, $msg, @args) = @_;
	print_trans($opt, $msg, {error=>1,pre=>"\n",post=>"\n", args=>\@args});
}

# "[album] " message
sub pprog {
	my ($opt, $msg, @args) = @_;
	print_trans($opt, $msg, {progname=>1, args=>\@args});
}

# "[album] ERROR: " message
sub pperror {
	my ($opt, $msg, @args) = @_;
	print_trans($opt, $msg, {progname=>1, error=>1,post=>"\n", args=>\@args});
}

# "[album] WARNING: " message
sub ppwarn {
	my ($opt, $msg, @args) = @_;
	print_trans($opt, $msg, {progname=>1, warning=>1,post=>"\n", args=>\@args});
}

# "\n[album] WARNING: " message
sub nlppwarn {
	my ($opt, $msg, @args) = @_;
	print_trans($opt, $msg, {progname=>1, warning=>1, pre=>"\n",post=>"\n", args=>\@args});
}


#########################
# This is just for me to run tests in...
sub language_test {
	my ($opt) = @_;

	return;
	print "\n";
	foreach ( 1..20 ) {
		my $c = credit($opt);
		$c =~ s/<[^>]+>/**/g;
		print "$c\n";
	}
	print "\n";
	exit(0);

#my $num_pics = 103030;
#$num_pics = $num_pics ? trans($opt,"[*,_1,image]",$num_pics) : 0;
#prterr($opt,"NUM: $num_pics\n");	#NOTRANS

	# Heres a good test for handling bracket code.
	if (0) {
		prterr($opt,	#NOTRANS
			"\n1 is [_1],TWOis [_2]\n".	#NOTRANS
			"\nRev: 2 is [_2] and 1 is [_1] and list: ~[[_2, not ~~comma '~,' or ,_1]]\n".	#NOTRANS
			"Quant [quant,_3,file] [quant,1,file] [quant,0,file] [quant,0,file,files,no files]\n".	#NOTRANS
			"Quant [quant,1,directory,directories] [quant,20000,directory,directories]\n".	#NOTRANS
			"Numify [numf,0] [numf,-1] [numf,3] [numf,999] [numf,-999] [numf,-1999] [numf,1999] [numf,_4]\n".	#NOTRANS
			"Sprintf test: [sprintf,%20s=~[%s~],_1,_2]!\n",	#NOTRANS
				"ONE","TWO",42,423182973.3241)
	}
prtout($opt," - WARNING:\n\n\n- Copyright: ---\nDave\n and: \n **** WARNING: --\n");	#NOTRANS

	ppwarn($opt,"Uh oh:\n\tNothing to do.  Call album with your photo directory as an option.");	#NOTRANS

	exit;
}

##################################################
##################################################
# PLUGIN CODE
##################################################
##################################################

# Some code to help plugins
# (also see 'sub option' of course)

# Add text to the album <head> section
# Usually called from a do_album hook
sub add_head {
  my ($opt,$data,$text) = @_;
  $data->{head} .= $text;
}

# Add text to an image page <head> section (cleared for each new image)
sub add_image_head {
  my ($opt,$data,$pic,$text) = @_;
  $data->{obj}{$pic}{head} .= $text;
}

# Add text to the album 'header'
# Usually called from a do_album hook
sub add_header {
  my ($opt,$data,$text) = @_;
  $data->{header} .= $text;
}

# Add text to the album 'footer'
# Usually called from a do_album hook
sub add_footer {
  my ($opt,$data,$text) = @_;
  $data->{footer} .= $text;
}


#####

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

  $plugin = 'utils/mv' if $option eq 'mv';
  $plugin = 'utils/create_plugin' if $option eq 'create_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});
  ppwarn($opt,"Multiple plugins match '[_1]'",$plugin)
    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 (load_plugin($opt,$path,$plugin)) {
      push(@{$opt->{_plugins}{loaded}}, $plugin);
      return $plugin;
    }
    ppwarn($opt,"Couldn't load plugin '[_1]'",$plugin);
    return 0;
  }

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

  # Okay, now just try to deep search plugins for any matches
  my $re = $plugin;
  $re .= $opt->{plugin_post} unless $re =~ /\Q$opt->{plugin_post}\E$/;
  my @found = path_matches($opt,"^$re\$",@{$opt->{plugin_path}});
  # This is a bit of overkill - they can at least type the complete name:
  #@found = path_matches($opt,"$re",@{$opt->{plugin_path}}) unless @found;
  if ($#found!=-1) {
    ppwarn($opt,"Multiple plugins match '[_1]'",$plugin) if $#found>0;
    my ($path,$found) = ($found[0][0], join_paths($found[0][2],$found[0][3]));
    if (load_plugin($opt,$path,$found)) {
      push(@{$opt->{_plugins}{loaded}}, $found);
      return $found;
    }
    ppwarn($opt,"Couldn't load plugin '[_1]'",$plugin);
    return 0;
  }

  nlppwarn($opt,"Couldn't find plugin '[_1]'",$plugin);
  sleep 3;
  return 0;
}

# Load a plugin.  $path=<full path>, $plugin=<plugin name/path>
sub load_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);

  pprog($opt,"Using plugin: [_1]\n",$plugin) unless $opt->{q} || $quiet;	#LVL=2
  # 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);
  $package = ($package =~ /^([a-z_]+)/i) ? $1 : "Album_Plugin";
  enter_plugin($opt, $plugin);
  eval <<END_EVAL;
    package $package;
    require "$path";
    \$opt->{_plugin}{\$plugin} = start_plugin(\$opt,\$plugin,\$path);
    #\$sym = \\\%${package}::;
END_EVAL
  leave_plugin($opt);

  if ($@) {
    ppwarn($opt,"Problem with plugin '[_1]' - not using:\n$@",$plugin);
    delete $opt->{_plugin}{$plugin}; return 0;
  }

  unless (ref $opt->{_plugin}{$plugin} eq 'HASH' && $opt->{_plugin}{$plugin}{description}) {
    ppwarn($opt,"Plugin did not return correct info hash.\n\tNot using '[_1]'\n$@",$plugin);
    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($opt,"No plugin found: [_1]",$plugin) unless $plugins[0]
  } else {
    my @found = path_matches($opt,"$opt->{plugin_post}\$",@{$opt->{plugin_path}});
    map { load_plugin($opt,$_->[0],join_paths($_->[2],$_->[3]),1) } @found;
    @plugins = keys %{$opt->{_plugin}};

    usage($opt,"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 trans($opt,"Author:")," $info->{author}";
      print " <$info->{href}>" if $info->{href};
      print "\n\n";
    }

    print "$opt->{_plugin}{$plugin}{description}\n";
    print ($option =~ /crf/ ? "END_PLUGIN: $plugin;;\n" : "\n");
  }
  exit;
}

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

# Temp turn a plugin on/off.
### These should be no longer necessary now that cancall_plugin checks the stack
sub uncall_plugin {
	my ($opt,$from) = @_;
	$from ||= curr_plugin($opt);
	push(@{$opt->{_plugins}{off}}, $from);
}
sub recall_plugin {
	my ($opt) = @_;

	pop(@{$opt->{_plugins}{off}});
}
sub cancall_plugin {
	my ($opt,$from,$name) = @_;

	# Avoid recursive plugin calls
	if (@{$opt->{_curr_plugin}}) {
		foreach my $c ( @{$opt->{_curr_plugin}} ) {
			return 0 if $from eq $c->[0] && $name eq $c->[1];
		}
	}

	return 1 unless $opt->{_plugins}{off};

	# Temp(?) hack - don't allow any plugins..  And here's why:
	# Other plugins would get called twice - unless we turned this
	# one off this time and others off next time??  But would that
	# possibly create a weird stack if there were more to turn off?
	# If you turn off the next time, how would you turn back on??
	# maybe stacking plugins is too difficult here..
	return @{$opt->{_plugins}{off}} ? 0 : 1;

	grep($_ eq $from, @{$opt->{_plugins}{off}}) ? 0 : 1;
}

sub enter_plugin {
	my ($opt, $plugin, $hook) = @_;
	unshift(@{$opt->{_curr_plugin}}, [$plugin,$hook]);
}

sub leave_plugin {
	my ($opt) = @_;
	shift(@{$opt->{_curr_plugin}});
}

sub curr_plugin {
	my ($opt) = @_;
	$opt->{_curr_plugin} ? $opt->{_curr_plugin}[0][0] : undef;
}
sub curr_hook {
	my ($opt) = @_;
	$opt->{_curr_plugin} ? $opt->{_curr_plugin}[0][1] : undef;
}

# 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 $ret = 0;
  my @ret;
  my $got_something = undef;
  foreach my $hook ( @{$opt->{_plugin_hooks}{$name}} ) {
    my ($sub,$from) = @$hook;
    next unless cancall_plugin($opt,$from,$name);
    my @call_args = $data ? ($opt,$data,$name,@args) : ($opt,$name,@args);
    enter_plugin($opt, $from, $name);
    my @got = $sub->(@call_args);	# Do the hook
    leave_plugin($opt);
    if ($reinject) {
      $got_something = 1 if defined $got[0];
      $args[$reinject-1] = $got[0] if defined $got[0];
    } elsif ($one) {
      return wantarray ? @got : $got[0]
        if defined $got[0];
    } else {
      $ret |= $got[0];
      push(@ret,@got);	# Might be messy if we get arrays back..
    }
  }
  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?? ~[[_1]~]",$0);
  my %hooks;
  while (<A>) {
    if (/do_hook(|s|sR)\(([^,]+),([^,]+),\s*'([^']+)'\s*(,(.*)\))?/) {
      my $name = $4;
      my @args = ($2,$3,"'$name'",$6);
			my %hook;
      $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(@{$hook{lc($1)}}, $2);
      }
			$hooks{$name} = \%hook;
    }
  }
  close A;
  \%hooks;
}

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

  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($opt,"No hooks found that match '[_1]'",$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 unless $no_die;
}

sub plugin_warn {
  my ($opt,$msg) = @_;
  my $warning = trans($opt,"WARNING:");
  hash_out($opt, "Plugin ~[[_1]~] hook ~[[_2]~]:\n\t[_3] [_4]", curr_plugin($opt),curr_hook($opt),$warning,$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 ($opt, $file) = @_;
	# I probably could have just used (-s $file) here..  Ah well.
  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 trans($opt, "[_1]k",$size) if ($size<1024);	#EXPLANATION: Filesize, as in kilobytes
  $size=int($size/102.4)/10;
  return trans($opt, "[_1]M",$size) if ($size<1024);	#EXPLANATION: Filesize, as in megabytes
  $size=int($size/102.4)/10;
  return trans($opt, "[_1]G",$size) if ($size<1024);	#EXPLANATION: Filesize, as in gigabytes
}

# 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_out($opt,"Skipping unknown HTML:\n  [_1]",$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};
  my $tn = "$dir/$opt->{dir}";
  my @pics = $data->{pics} ? @{$data->{pics}} : ();

  do_hooks($opt,$data, 'do_clean', $dir, $tn, @pics);
  # Description: Do extra cleaning in an album for plugins.
  # Description: Supplies album directory, thumbnail directory and list of pics.
  # Returns: None.

  return unless -d $tn;

  # Read the thumbnail directory
  opendir(DIR,$tn);
  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 "$tn/$file";	# Don't bother with directories.
    my $remove_type;
    my $name = $file;
    my $html = index_page($opt) ? ('.'.index_page($opt)) : $opt->{html};
    my $post = $opt->{thumb_post} ? ".$opt->{thumb_post}" : "";
    if ($name =~ s/\Q$html\E$//) {
      $remove_type = "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_type = "unused medium image"
        unless $opt->{medium} && $typematch && $saw;
    } elsif ($name  =~ /(.+)\.snap\.(.+?)(\.$opt->{type})?$/i) {
      $name = "$1.$2";
      $remove_type = "unused snapshot image" unless contains($name, @pics);
    } elsif ($name  =~ /(.+)$post\.$opt->{type}$/i) {
      # Thumbnail?
      $name = $1;
      $remove_type = "unused thumbnail"
        unless grep($_ eq $name || /^\Q$name\E.(?i)$opt->{type}$/, @pics);
    } elsif ($name  =~ /(.+)\.$opt->{type}\.ppm$/) {
      my $mov = $1;
      $remove_type = "unused ppm thumbnail?" unless (grep(/^\Q$mov.\E/, @pics));
    } else {
      $remove_type = "unknown file";
    }
    if ($remove_type) {
      hash_out($opt,"Remove [_1]: [_2]",$remove_type,"$tn/$file");
# This doesn't seem to work (or show error) under windows.  Bah.
      hash_out($opt,"Couldn't erase: [_1]",$file)
        unless unlink "$tn/$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) = @_;

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

  # Cache the data
  return $opt->{_captions}{$dir} if $opt->{_captions}{$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,trans($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';

  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")) {
    pperror($opt,"Couldn't read captions: [_1]\n","$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'

		# Allow for clip times in caption file
		$caps{$file}{clip_time} = $1
			if $cap =~ s/\s*\[thumb=([\d\.]+)s\]// || $name =~ s/\s*\[thumb=([\d\.]+)s\]//;

    $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 $exif = 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,trans($opt,"Didn't return a hash."))
    if $exif && ref $exif ne 'HASH' && !$MAIN::WARN_PLUGIN_GET_EXIF_INFO++;

	# The local version of get_exif_info
  unless (defined $exif && ref $exif eq 'HASH') {
		$exif = {};
  	my $qpic = file_quote($opt,$pic);
  	my $jhead = open_pipe($opt,$opt->{jhead},$qpic);
  	undef $opt->{jhead} unless $jhead;
  	return undef unless $jhead;
  	while(<$jhead>) {
    	print STDERR "get_exif_info(): $_" if $opt->{D};
    	if (/command not found/) {	# Kind of kludgy
      	hash_out($opt,"[_1] specified but jhead not found ~[[_2]~]\n\tEither specify [_1], or use one of the EXIF replacement plugins.", '-exif',$opt->{jhead});
      	undef $opt->{jhead};
      	$jhead->close;
      	return undef;
    	}
    	$exif->{$1} .= $2 if /(.+?)\s*:\s*(\S.*)/;
  	}
  	$jhead->close;
	}

  my $new = do_hooksR($opt,$data, 'got_exif_info', $exif, $pic);
  # Description: Postprocessing of exif info for a picture
  # Returns: New exif hash, key->val matches the EXIF key/value pairs
  # Returns: Call 'album::new_html($opt,$data,$pic)' if captions change.
	return $new if ref $new eq 'HASH';

  plugin_warn($opt,trans($opt,"Didn't return a hash."))
    if $new && !$MAIN::WARN_PLUGIN_GOT_EXIF_INFO++;

	$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
  return if $data->{obj}{$pic}{_did_exif}++;

  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 $pic_file = $data->{obj}{$pic}{full}{path} || "$dir/$pic";
  my $exif = get_exif_info($opt,$data,$pic_file);
  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
#########################
# Get a caption for a single file when we don't have the object
# (useful for plugins that use hints in captions file)
sub get_caption {
	my ($opt,$path) = @_;
	my ($dir,$file) = split_path($opt,$path);
	# These should be (or will become) cached
	my $cap = read_captions($opt,$dir);
	wantarray ? ($cap->{$file}{name},$cap->{$file}{cap}) : $cap->{$file};
}

# Get all caption info using captions.txt, subdirectories, plugins, etc..
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 $cap = $data->{obj}{$pic}{cap};
    my $new = do_hooksR($opt,$data, 'modify_caption', $cap, $data->{paths}{dir}, $pic);
    # 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..
				# Not when it's in the same directory instead of in the parent.
        # Hopefully this won't screw up any plugins that override read_captions,
        # they should 'find' the caption that's in the parent directory anyways.
    }

    my $cap = $data->{obj}{$dir}{cap};
    my $new = do_hooksR($opt,$data, 'modify_dir_caption', $cap, $data->{paths}{dir}, $dir);
    # 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, file date or EXIF date
sub sort_rank {
  my ($opt,$data,$f) = @_;
  return ($data->{obj}{$f} && $data->{obj}{$f}{order}) unless ($opt->{sort} eq 'date' || $opt->{sort} eq 'exif');
  # Save times in a cache
  return $data->{paths}{_date_sort_cache}{$f}
    if $data->{paths}{_date_sort_cache} && $data->{paths}{_date_sort_cache}{$f};

  if ($opt->{sort} eq 'exif') {
  	# Try to get the EXIF date for the file and format it like the "-M" file 
  	# test does: a floating point number representing days from program start time. 
  	# Falls through to file date if EXIF info is missing or cannot be parsed.
    my ($exifdate) = str2time(get_exif_info($opt,$data,$f)->{'Date/Time'});
    if ($exifdate ne undef) {
      $data->{paths}{_date_sort_cache}{$f} = ($exifdate-$^T)/86400;
      return $data->{paths}{_date_sort_cache}{$f};
    }
  }
  $data->{paths}{_date_sort_cache}{$f} = -(-M "$data->{paths}{dir}/$f");
  return $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};
  $b = $data->{obj}{$b}{name};

	unless ($opt->{case_sort}) {
		$a = lc($a);
		$b = lc($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';
  $slash = $slash || '/';
  return ($slash,'') if $path eq $slash;	# Special case: "/"
	# Some windows paths actually use '/' ...  what a mess.
	$slash = '/' if $path =~ m|/| && $path !~ m|\Q$slash\E|;
  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);
  }

  # 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 unless $data->{obj}{$pic}{full}{file};
    $data->{obj}{$pic}{full}{path} = "$dir/$pic" unless $data->{obj}{$pic}{full}{path};
    # We also store it in just obj->{file} to match directory objects.
    $data->{obj}{$pic}{file} = $pic unless $data->{obj}{$pic}{file};
  }

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

  # Create static <meta> info in {head}
  add_head($opt,$data,<<"HEAD");
		<meta name='Generator' content='$GEN_STRING' />
		<meta name='Album_Path' content='$data->{paths}{album_path}' />
		<script type='text/javascript'>
		<!--
		// If plugins want to use 'onload' they can call 'AddOnload(func)'
		var OnloadArr = new Array();
		function AddOnload(func) {  // Call with function to add to 'onload' handler
			OnloadArr[OnloadArr.length] = func;
			window.onload = ArrayOnload;
		}
		function ArrayOnload() {
			for(var i=0; i<OnloadArr.length; i++) OnloadArr[i]();
		} 
		//-->
		</script>
HEAD

}

##################################################
##################################################
# 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 if $opt->{Dtheme};
  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"';
  }
  
  # We could *maybe* attempt to translate things in quotes inside $str,
  # but that would be unlikely to be useful, and *very* likely to be wrong!
  $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 $_;
    }
  }
  ppwarn($opt,"Never left perl code ~[[_1], [_2]~]",$opt->{'album.th'}, $line)
    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_out($dmp->{opt},"Option or data <[_1]> was referenced multiple times?\n\t(Internal consistency problem - probably with a plugin?)", $name)
    if $ref && $dmp->{saw}{$var}++;

  return hash_out($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);
	$dmp;
}

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 theme_trans {
	my ($opt) = @_;
	# Translations that we pass to themes
	$opt->{trans}{'Album:'} = trans($opt,"Album:");	#LVL=1
	$opt->{trans}{'Image:'} = trans($opt,"Image:");	#LVL=1
	$opt->{trans}{'More albums:'} = trans($opt,"More albums:");	#LVL=1
	$opt->{trans}{'Prev'} = trans($opt,"Prev");	#EXPLANATION: Short for 'previous'	#LVL=1
	$opt->{trans}{'Next'} = trans($opt,"Next");	#LVL=1
	$opt->{trans}{'Back'} = trans($opt,"Back");	#LVL=1
	$opt->{trans}{'Up'} = trans($opt,"Up");	#LVL=1

	my @trans = ('Album:', 'Image:', 'More albums:', 'Prev', 'Next', 'Back', 'Up');

	return @trans;

	# Sadly this doesn't work for -make_lang
	#map { $opt->{trans}{$_} = trans($opt,$_) } @trans;
}

# Put the (possibly enormous) $data->{obj} into eperl
sub images_to_eperl {
	my ($opt,$data, @images) = @_;
	my (%dmp,$tmp);

	# Just the dumper object for all images (for the index page)
	return dumper(\%dmp,'data->{obj}',$data->{obj}) unless @images;

	foreach my $use ( @images ) {
		next unless defined $use;
		my $pic = $data->{pics}[$use];
		next unless $pic;
		$tmp->{$pic} = $data->{obj}{$pic};
	}
	dumper(\%dmp,'data->{obj}',$tmp);
	return ("<:\n", @{$dmp{dump}}, ":>\n");
}

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

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

	theme_trans($opt);

  # 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} = $opt->{theme_full_info} ? '^(_.*|0)$' : '^(_.*|0|obj)$';
  $dmp{ignore_only_under} = 4;	# Only ignore if below this level
  dumper(\%dmp,'data',$data);

  $dmp{ignore} = '^(_.*|enter_eperl|leave_eperl|0)$';
  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");

	# *all* the image info (for the index page)
	unless ($opt->{theme_full_info}) {
		my $tmp = images_to_eperl($opt,$data);
		$data->{eperl_images} = $tmp->{dump};
  	unshift(@{$data->{eperl_images}},"<:\n");
  	push(@{$data->{eperl_images}},":>//\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]}; }

sub Version { $VERSION; }
sub Version_Num {
  my \$ver = $VERSION;
	\$ver =~ s/[^\\d\\.]//g;
	\$ver + 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}" || \$data->{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};
  print \$data->{header};
}
sub isFooter { return (-r "\$data->{paths}{dir}/\$opt->{footer}" || \$data->{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};
  print \$data->{footer};
}

sub Trans {
  my (\$str) = \@_;
  return \$opt->{trans}{\$str} || \$str;
}

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

sub num {
  my (\$type) = \@_;
	return \$IMAGE_CNT+1 if \$type eq "This_Image";
	return \$IMAGE_CNT+1 if ref \$type eq 'HASH' && \$type == This_Image();
  \$type='pics' unless \$type;
  return \$#{Path('parent_albums')}+1 if \$type eq 'parent_albums';
  \$#{\$data->{\$type}}+1;
}

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

  return \$obj->{\$type}{img_tag} if \$obj->{\$type}{img_tag};

  my \$str;
  \$str .= "<\$tag src=\$src style='border:0'";
  \$str .= " \$obj->{intag} " if \$obj->{intag};
  \$str .= " \$obj->{\$type}{intag} " if \$obj->{\$type}{intag};
  \$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
my \$order = -1;
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 \$num = \$obj->{order} ? \$obj->{order}-2 : \$order--;
  my \$name = url_quote("NAME\$num:".\$obj->{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);
		\$rest =~ s/-/ /g;
    \$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->{file})."-->\\n"
    if $opt->{caption_edit};
  \$cap .= \$obj->{cap} if \$norm_caps;
  \$cap .= join('',read_file(\$obj->{capfile})) if \$norm_caps;
  \$cap .= "<!--END_IMAGE_CAPTION-->\\n" if $opt->{caption_edit};
  \$cap .= Image_Page() ? \$obj->{cap_image} :  \$obj->{cap_album};
  \$cap .= \$obj->{exif} if \$norm_caps;
  \$cap .= Image_Page() ? \$obj->{exif_image} :  \$obj->{exif_album};
  \$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;
}

sub Image_Ref {
	my (\$img,\$and,\$alt) = \@_;
	return Image_Array(\@\$img,\$and,\$alt) if ref \$img eq 'ARRAY';
	return '' unless ref \$img eq 'HASH';

	# Go through languages in reverse, '_' is default
	foreach my \$lang ( reverse(\@{\$opt->{lang}}), '_' ) {
		return Image_Array(\@{\$img->{\$lang}},\$and,\$alt) if \$img->{\$lang};
	}
	'';
}


# 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;
	## Not XHTML Transitional valid:
	#"<td width='\$rx' height='\$ry' background='\$src'></td>";
  "<td width='\$rx' height='\$ry' style='background-image:url(\$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);
	## Not XHTML Transitional valid:
	#"<table cellpadding=0 cellspacing=0 width='\$rx'><tr height='\$ry'><td background='\$src'></td></tr></table>";
	"<table cellpadding=0 cellspacing=0 width='\$rx'><tr height='\$ry'><td tyle='background-image:url(\$src)'</td></tr></table>";
}

# Borders can be 0 pieces, 4 pieces, 8 or 12
# Ordered 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
#
# OVERLAY BORDERS can be used by specifying an "padding" as an extra
# integer parameter to the T, L, R and B image arrays.  This is the
# amount of the border that shouldn't overlap the image (the outside part).
# The T and L padding will be used for the TL border, etc..
# You cannot mix overlay borders with non-overlay, and overlay borders
# are always 8 pieces (you can skip any pieces with empty image arrays).
#
# 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_type, 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);
  my \$href;

  if (ref \$img ne 'HASH') {
    # Called with (img_string,x,y, ..)
    (\$x,\$y) = (shift \@_, shift \@_);
  } else {
    # Called with (img_object,type,href_type, ..)
    my (\$type,\$href_type) = (shift \@_, shift \@_);
    # Medium or full if not thumb?
    \$type = Get(\$img,'medium','x') ? 'medium' : 'full' if \$type ne 'thumb';
    # Lookup href
		if (\$href_type) {
			if (lc(\$href_type) eq 'next') {
				my \$next = Next(\$img);
    		\$href = Get(\$next,'href','image_page','image_page') if \$next;
			} else {
    		\$href = Get(\$img,'href',\$href_type);
			}
		}
    \$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,\$overlay)=(1,2,3);	# 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) = \@_;

    # Stretch top,bottom to fit image
    my \@t = \@\$T; my \@b = \@\$B;	# Copy them so we can alter/stretch
    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) = \@_;

    # OVERLAY BORDERS
    if (\$#\$T==3 || \$#\$R==3 || \$#\$B==3 || \$#\$L==3) {
      my \@t = \@\$T; my \@b = \@\$B;	# Copy them so we can alter/stretch
      my \@l = \@\$L; my \@r = \@\$R;
      my \$t_pad = \$t[\$overlay]; my \$b_pad = \$b[\$overlay];
      my \$l_pad = \$l[\$overlay]; my \$r_pad = \$r[\$overlay];
      my \$full_width = \$l_pad + \$x + \$r_pad;
      my \$full_height = \$t_pad + \$y + \$b_pad;
      \$t[\$width] = \$full_width - \$TL->[\$width]-\$TR->[\$width];
      \$b[\$width] = \$full_width - \$TL->[\$width]-\$TR->[\$width];
      \$l[\$height] = \$full_height - \$TL->[\$height]-\$BL->[\$height];
      \$r[\$height] = \$full_height - \$TR->[\$height]-\$BR->[\$height];
      \$t[\$width] = 0 if \$t[\$width]<0;
      \$b[\$width] = 0 if \$b[\$width]<0;
      \$l[\$height] = 0 if \$l[\$width]<0;
      \$r[\$height] = 0 if \$r[\$width]<0;

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

			# Unfortunately we have to put links on each piece or else they can't click..
			my \$end = \$href ? "</a>" : "";

      print <<OVERLAY8;
					<div style="position: relative; width: \${full_width}px; height: \${full_height}px;">
						<div style="position: absolute; z-index: 0; left: \${l_pad}px; top: \${t_pad}px;">
							\$img
						</div>
						<div style="position: absolute; z-index: 2; top: 0px; left: 0px;">
							\$href\$tl\$end
						</div>
						<div style="position: absolute; z-index: 2; top: 0px; right: 0px;">
							\$href\$tr\$end
						</div>
						<div style="position: absolute; z-index: 2; bottom: 0px; left: 0px;">
							\$href\$bl\$end
						</div>
						<div style="position: absolute; z-index: 2; bottom: 0px; right: 0px;">
							\$href\$br\$end
						</div>
						<div style="position: absolute; z-index: 1; top: 0px; left: \$TL->[\$width]px;">
							\$href\$t\$end
						</div>
						<div style="position: absolute; z-index: 1; bottom: 0px; left: \$BL->[\$width]px;">
							\$href\$b\$end
						</div>
						<div style="position: absolute; z-index: 1; top: \$T->[\$height]px; left: 0px;">
							\$href\$l\$end
						</div>
						<div style="position: absolute; z-index: 1; top: \$T->[\$height]px; right: 0px;">
							\$href\$r\$end
						</div>
					</div>
OVERLAY8
      return;
    }

    # REGULAR 8 PIECE BORDERS
    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;

    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 Body_Tag {
  print "\$data->{body_tag}";
}

# Meta tag needed for regenerating portions of the album tree.
sub Meta {
# DEPRECATED: These two lines 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};
#

  print "\$data->{head}";
  if (Image_Page()) {
		my \$head = Get(This_Image, 'head');
    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') || "''";
    # The <metas> are for album dependency checking
    # The <links> are for browser navigation
    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='..' />
		\$head
		<script type='text/javascript'>
		<!--
		function preloadImages() {
  		Image1 = new Image(); Image1.src = \$Prev_src;
  		Image2 = new Image(); Image2.src = \$Next_src;
		}
		AddOnload(preloadImages);
		//-->
		</script>

PREV_NEXT
  } else {
    print "\t\t<link rel='up' title='Up' href='..' />\n";
  }
  \$CALLED_META=1;
}
sub Album_End {
  die("\nERROR: Di"."dn'"."t call M"."eta() in <head>!\\n") unless \$CALLED_META;
  # Please leave this here.  It's the only way I get paid.
  die("\nERROR: 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);
    # If we have perl
    #my $perl = $WINDOWS ? 'C:\WINDOWS\system32\cmd.exe /c '.$^X : "\Q$^X\E";
    my $perl =
      ($CAVA && $^X =~ m|album[^/]*$|) ? "$^X -q -theme_interp_wrap"
			: $WINDOWS ? 'C:\Perl\bin\perl.exe'
      : "\Q$^X\E";
    print STDERR "\nOPEN PIPE: |$perl > $qout\n" if $opt->{dtheme};
    (open(ALBUM,"|$perl > $qout")) ||
      fatal($opt,"Couldn't start perl pipe for theme '[_1]'\n", $out);
  } else {
    # Just write a file
    (open(ALBUM,">$out")) ||
      fatal($opt,"Couldn't write html '[_1]'\n",$out);
  }
}

sub close_output {
  my ($opt,$out,$theme) = @_;
  print STDERR "close_output\n" if $theme && $opt->{dtheme};
  close(ALBUM);
  my $ret = $?;
  return unless $theme;
  return unless $ret;

  unlink $out;
  fatal($opt, "album theme returned error ~[[_1]~]\n", $ret);
}

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

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

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

  my $body = $opt->{body};
  $body =~ s/<body/<body $data->{body_tag}/i if $data->{body_tag};

  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 ?
		trans($opt,"Back") :	#LVL=1
		trans($opt,"Up");	#LVL=1
  my $UpUrl = $top ? $opt->{top} : "../$index";
  $UpUrl = "<h1><a href='$UpUrl'>$Up</a></h1>" if $UpUrl && $UpUrl ne "''";

  my $head = $data->{head};
  $head .= $obj->{head} if $image_page;

  my $tAlbum = trans($opt, "Album:");	#LVL=1
 
  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> 
      $tAlbum $this
    </title>
    <meta http-equiv='Content-Type' content='text/html; charset=$opt->{charset}' />
    $head
  </head>
  $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 $data->{header};
      print ALBUM "<hr />\n";
    }
  }
}

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

	# We need to avoid being repetitive with "tool"
	my $Ttool = trans($opt,"tool");	#EXPLANATION: Context: "a tool written by Dave"	#LVL=0

	# Created by ...
	my @start = (
		"Photo album generated by [_1]",	#TRANS("Album from MarginalHacks.com")	#LVL=0
		"Photo album generated by [_1]",	#NOTRANS
		"Created with the tool [_1]",	#TRANS("Album from MarginalHacks.com")	#LVL=0
		"Album created by [_1]",	#TRANS("Album from MarginalHacks.com")	#LVL=0
		"Album created by [_1]",	#NOTRANS
		"Powered by [_1]",	#TRANS("Album from MarginalHacks.com")	#LVL=0
	);

	# .. album tool ..
	my $Talbum = trans($opt, "album");	#LVL=0
	my $Talbum_generator = trans($opt, "album generator");	#LVL=0
	my $Talbum_tool = trans($opt, "album tool");	#LVL=0
	my $Talbum_script = trans($opt, "album script");	#LVL=0
	my $Tphoto_album_generator = trans($opt, "photo album generator");	#LVL=0
	my @album = (
		$Talbum, $Talbum, $Talbum, $Talbum_generator, $Talbum_generator,
		$Talbum_tool, $Talbum_tool, $Talbum_script, $Tphoto_album_generator,
	);

	# .. from Dave's MarginalHacks.
	my @from;
	push(@from,trans($opt,"from [_1]'s [_2]", "Dave", "MarginalHacks"));	#LVL=0
	push(@from,trans($opt,"from [_1] by [_2]", "MarginalHacks", "Dave"));	#LVL=0
	push(@from,trans($opt,"a [_1] by [_2]", "Tool", "Dave"));	#LVL=0
	push(@from,trans($opt,"a [_1] written by [_2]", "Tool", "Dave"));	#LVL=0

	# Basically: $start $album $from

	# Avoid repetition of "tool"
	my $album = $album[int(rand($#album+1))];
	if ($album =~ /\Q$Ttool\E/) {	# tool tool tool tool..
		my @nonrep = grep(!/\Q$Ttool\E/, @start);
		@start = @nonrep if @nonrep;
	}
	my $start = $start[int(rand($#start+1))];
	my $from = $from[int(rand($#from+1))];

	my $str = trans($opt, $start, "ALBUM $from");	#NOTRANS

	if ($str !~ /Dave/ || $str !~ /ALBUM/ ||
		($str !~ /MarginalHacks/ && $str !~ /Tool/)) {
		$str = $start;
		$from = "from Dave's MarginalHacks";	# should be more random??
		$str =~ s/\[_1\]/$album $from/;	# Just do it untranslated
	}

	# Now do replacements for Dave, ALBUM, Tool and MarginalHacks

	# Dave
	my @me = ("Dave","David Ljung","David Madison","D. Madison","David","Dave Madison");
	my $me = $me[int(rand($#me+1))];
	$me = "<a href='http://GetDave.com/'>$me</a>";
	$str =~ s/Dave/$me/g;

	my $Tmarginalhack = trans($opt,"Marginal Hack");	#EXPLANATION: If you can't translate this: don't worry about it.  Intentionally capitalized (proper noun).	#LVL=0
	my $Tmarginalhacks = trans($opt,"Marginal Hacks");	#EXPLANATION: Plural.  Okay if you can't translate.  Also proper noun.	#LVL=0

	# MarginalHacks
	my @mhs = ("MarginalHacks", $Tmarginalhacks);
	my $mhs = $mhs[int(rand($#mhs+1))];
	$mhs = "<a href='$HOME'>$mhs</a>";
	$str =~ s/MarginalHacks/$mhs/g;

	# Tool
	my @tool;
	push(@tool, "MarginalHack");
	push(@tool, $Tmarginalhack);
	push(@tool, $Ttool);
	push(@tool, trans($opt,"free tool"));	#EXPLANATION: Context: "a free tool written by Dave"	#LVL=0
	push(@tool, trans($opt,"script"));	#EXPLANATION: Context: "a script written by Dave"	#LVL=0
	if ($album =~ /\Q$Ttool\E/) {	# tool tool tool tool..
		my @nonrep = grep(!/\Q$Ttool\E/, @tool);
		@tool = @nonrep if @nonrep;
	}
	my $tool = $tool[int(rand($#tool+1))];
	$tool = "<a href='$HOME'>$tool</a>";
	$str =~ s/Tool/$tool/g;

	# ALBUM
	# Take a gamble and assume spaces are word separators, put
	# the URL around some random number of the last words.  This
	# allows for making links out of "album [tool]" and "[album tool]"..
	my @album_words = split(/(\s+)/, $album);
	splice(@album_words,2*int(rand(($#album_words+1)/2)),0,"<a href='$ALBUM_URL'>");
	$album = join('',@album_words)."</a>";

	$str =~ s/ALBUM/$album/g;
	$str;
}

# Output is eperl
sub eperl_derc {
  my ($opt) = @_;
  my $derc = pack('C*',qw(67 65 76 76 69 68 95 67 82 69 68 73 84));
  my $c = credit($opt);
	$c =~ s/"/\\"/g;
	"<: sub "."Cr".
	"edit { \$${derc}=1; print \"$c\"; } :>";
}

sub plain_derc {
  my ($opt) = @_;
  my $date = localtime;
  my $cred = credit($opt);
  return <<CREDIT;
    <font size='-1'>
$opt->{credit}
$cred $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 $data->{footer};
    print ALBUM "<hr />\n";
  }
  print ALBUM plain_derc($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 $image_page ? $obj->{cap_image} : $obj->{cap_album};
  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
#   Customers:  plugins/captions/format/hide_shorten.alp
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
  my $out = $data->{paths}{album_file};
  setup_output($opt,$out);
  header($opt,$data,0);

  theme_trans($opt);
  my $More_albums = $opt->{trans}{"More albums:"}; #trans($opt,"More albums:");	#LVL=1

  # 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}) {
        if ($obj->{thumb}{img_tag}) {
          print ALBUM "          $obj->{thumb}{img_tag}<br />\n";
        } else {
          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 " $obj->{intag} " if $obj->{intag};
          print ALBUM " $obj->{thumb}{intag} " if $obj->{thumb}{intag};
          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";

      print ALBUM "<font size='-1'><br />$obj->{num_pics_str}</font>\n" if $obj->{num_pics_str};

      print ALBUM "<font size='-1'><br />$obj->{num_dirs_str}</font>\n" if $obj->{num_dirs_str};
    }
    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} || $obj->{img_tag}) {
      print ALBUM "        <a href=$obj->{URL}{album_page}{image}>\n";
      if ($obj->{thumb}{img_tag}) {
        print ALBUM "          $obj->{thumb}{img_tag}<br />\n";
      } else {
        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 " $obj->{intag} " if $obj->{intag};
        print ALBUM " $obj->{thumb}{intag} " if $obj->{thumb}{intag};
        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 : "??";
			my $Type = trans($opt,"[_1] file:", $type);	#LVL=1	#EXPLANATION: As in: "jpg file" "gif file" etc..
      print ALBUM "        <font size='+1'><b>$Type</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,$out,0);
}

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

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

  # A bit kludgy.
  my $full_or_medium = $opt->{medium} ? 'medium' : 'full';

  # Init the previous info
  my $prev = $#{$data->{pics}};
  my $prev_pic = $data->{pics}[$prev];
  my $prev_url = $data->{obj}{$prev_pic}{URL}{image_page}{image_page};
  my $prev_name = $data->{obj}{$prev_pic}{name};
  my $prev_href = "<h3><a href=$prev_url>$prev_name</a></h3>";
  undef $prev_href 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}};
    next unless $obj->{is_image};
    my $name = $obj->{name};

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

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

    next if do_hooks($opt,$data, 'write_image_page', $dir, $file, $pic, $i, $prev, $next);
    # Description: Called before an album image page is written
    # Description: $pic is the current picture (get obj with $data->{obj}{$pic})
    # Description: $i is the image number as in: $data->{pics}[$i]
    # Description: $prev and $next are undefined on boundaries unless  -image_loop is used.
    # Description: This hook is too late for themes to get any $opt/$data changes!
    # Description: You *can* change <head> with album::add_image_head (but not with -theme_full_info)
    # Returns: 1 to skip this normal image page writing code

    header($opt,$data,1,$obj);

    # Image and Previous/next
    my $prev_next = <<PREV_NEXT;
<table cellspacing='10' width='100%'>
  <tr>
    <td align='left'>
      $prev_href
    </td>
    <td align='right'>
      $next_href
    </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"
      if $obj->{URL}{image_page}{image};
    if ($obj->{full}{img_tag}) {
      print ALBUM "$obj->{full}{img_tag}<br />\n";
    } else {
      print ALBUM "<$obj->{full}{tag} style='border:0'";
      print ALBUM " src=$obj->{URL}{image_page}{image_src}";
      print ALBUM " $obj->{intag} " if $obj->{intag};
      print ALBUM " $obj->{$full_or_medium}{intag} " if $obj->{$full_or_medium}{intag};
      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,"$dir/$file",0);

    $prev = $i;
    $prev_href = "<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) = @_;

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

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

  # 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,$out,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};

  # Options that matter to HTML generation
	my $option_changed =
    option_changed($opt,'exif') ||
    option_changed($opt,'exif_album') ||
    option_changed($opt,'exif_image') ||
    option_changed($opt,'just_medium') ||
    option_changed($opt,'medium') ||
    option_changed($opt,'caption_edit') ||
    option_changed($opt,'theme_url') ||
    option_changed($opt,'credit') ||
    option_changed($opt,'slideshow') ||
    option_changed($opt,'sort') ||
    option_changed($opt,'reverse_sort') ||
    option_changed($opt,'case_sort') ||
    option_changed($opt,'burn');

  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}};
    next unless $obj->{is_image};

    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 ||
      $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}};
    next unless $obj->{is_image};

    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 : $opt->{image_loop} ? $#{$data->{pics}} : undef;
    my $next = $i==$#{$data->{pics}} ? ($opt->{image_loop} ? 0 : undef) : $i+1;
    my $depchanged = $changed[$i];
    $depchanged ||= $changed[$prev] if defined $prev;
    $depchanged ||= $changed[$next] if defined $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 '[_1]'\n",$dir);
    setup_output($opt,"$dir/$file",1);

    next if do_hooks($opt,$data, 'write_image_page', $dir, $file, $pic, $i, $prev, $next);
    # Description: Called before an album image page is written
    # Description: $pic is the current picture (get obj with $data->{obj}{$pic})
    # Description: $i is the image number as in: $data->{pics}[$i]
    # Description: $prev and $next are undefined on boundaries unless  -image_loop is used.
    # Description: This hook is too late for themes to get any $opt/$data changes!
    # Description: You *can* change <head> with album::add_image_head (but not with -theme_full_info)
    # Returns: 1 to skip this normal image page writing code

    # 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",
      eperl_derc($opt));

		# If we don't have 'theme_full_info' set then we put in the info
		# for just the prev/next/curr images
		eperl($opt, images_to_eperl($opt,$data, $i,$prev,$next))
  		unless $opt->{theme_full_info};

    # 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,"$dir/$file",1);
    show_hashes($opt, 2+$i+1, 2 + $#{$data->{pics}}+1);
  }
}

##################################################
##################################################
# create an album
##################################################
##################################################
sub is_image {
  my ($opt,$file) = @_;

  my $ret = do_hook($opt,undef,'is_image', $file);
  # description: determine if a file is an image.
  # returns: 1 for image, 0 for not, undef for letting default code decide.
	return $ret if defined $ret;

  return 0 if -f "${file}$opt->{not_img}";
  return 0 if $file =~ /\.html?$/i;
  return 0 if $file !~ /\.($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) = @_;

  my $ret = do_hook($opt,undef, 'is_movie', $pic);
  # Description: Determine if a file is a movie  (must pass "is_image" first!)
  # Returns: 1 for image, 0 for not, undef for letting default code decide.
	return $ret if defined $ret;

  $pic =~ /\.(mpe?g|moo?v|avi|mp4|m4v|3gp|wmv|flv)$/i ? 1 : 0;
}

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

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

  # Figure out type
  $obj->{is_movie} = is_movie($opt,$path);
  $obj->{is_image} = is_image($opt,$path);

  return unless $obj->{is_image} || !$opt->{known_images};

  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;

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

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

	# If we couldn't make a thumbnail, then it's probably not an image
  return if !$obj->{has_thumb} && $opt->{thumbs} && $opt->{known_images};

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

  $obj->{capfile} = $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->{is_image} && $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};

  my $image_dir = "$opt->{dir}/" if $opt->{dir};
  my $image_path = "$dir/$image_dir";
  if ($use_image_pages) {
    my $image_page = "$pic$data->{paths}{page_post_url}";
    $obj->{URL}{album_page}{image} = url_quote($opt, "$image_dir$image_page");
# Kludge - the number of ".." should be equal to the pathsize of $opt->{dir}
    my $image = ($opt->{just_medium} && $obj->{medium})
      ? $obj->{medium}{path} : $obj->{full}{path};
    $image = diff_path($opt, $image_path, $image);
    $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}{path} || $obj->{full}{path};
    if ($obj->{snapshot}{file}) {
      $image_src = $opt->{embed} ? $obj->{full}{path} :
        ($obj->{medium}{path} || $obj->{snapshot}{path});
    }
    $image_src = diff_path($opt, $image_path, $image_src);
    $obj->{URL}{image_page}{image_src} = url_quote($opt, $image_src);

    # Thumbnail
    $obj->{URL}{album_page}{thumb} =
      url_quote($opt, diff_path($opt, $dir, $obj->{thumb}{path}));
    $obj->{URL}{image_page}{thumb} =
      url_quote($opt, diff_path($opt, $image_path, $obj->{thumb}{path}));
  } else {
    $obj->{URL}{album_page}{image} =
      url_quote($opt, diff_path($opt, $dir, $obj->{full}{path}));
    $obj->{URL}{album_page}{thumb} =
      url_quote($opt, diff_path($opt, $dir, $obj->{thumb}{path}));
    # We might normally have image pages, just not for this non-image
    $obj->{URL}{image_page}{image_page} =
      url_quote($opt, diff_path($opt, $image_path, $obj->{full}{path}))
      if $opt->{image_pages};
  }

  # -transform_url
  if ($opt->{transform_url}) {
    $obj->{URL}{album_page}{image} = $opt->{transform_url};
    my $pic_path = diff_path($opt, $dir, $obj->{full}{path});
    $obj->{URL}{album_page}{image} =~ s/%S/$pic_path/g;
    my $s = $pic;  $s =~ s/\.[^\.]+$//;
    $obj->{URL}{album_page}{image} =~ s/%s/$s/g;
		# We could url_quote here, but that would screw up the
		# ability to add info to the href tags (like rel=nofollow).
		# (Which is important, for example, at http://DavePics.com/Theme_Album/)
		# We'll assume if they use transform_url, they will quote.
  }

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


  return if do_hooks($opt,$data, 'end_handle_file', $dir, $pic, $obj, $path);
  # Description:  Add to final handling of a file/image.
  # Description:  Passes: directory, picture/filename, object hash, full path.
  # Returns: 1 if we should skip this file.

  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, obj
# (See hook below for more info)
sub dir_info {
  my ($opt,$data,$child) = @_;

  # First count the number of images and directories
  #########################
  $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);
  my $num_dirs = $child_data->{dirs} ? @{$child_data->{dirs}} : 0;
  $data->{obj}{$child}{num_dirs} = $num_dirs;
  $data->{obj}{$child}{num_dirs_str} = $num_dirs && $opt->{folder_count} ? trans($opt,"[*,_1,folder]",$num_dirs) : "";	#EXPLANATION: 0 folders, 1 folder, 2 folders, 3 folders..	#LVL=1

  my @images = ();
  if ($child_data->{pics}) {
    # BUG: Technically we should read the album.conf to see if
    #      settings like $opt->{captions} have changed..
    get_captions($opt,$child_data);	# For sorting
    sort_info($opt,$child_data);
    @images = grep is_image($opt,$_), @{$child_data->{pics}};
    my $num_pics = scalar @images;
    $data->{obj}{$child}{num_pics} = $num_pics;
    $data->{obj}{$child}{num_pics_str} = $num_pics && $opt->{folder_count} ? trans($opt,"[*,_1,image]",$num_pics) : "";	#EXPLANATION: 0 images, 1 image, 2 images, 3 images..	#LVL=1
  }

  # Now we can actually get the directory thumb
  #########################
  return undef unless $opt->{dir_thumbs};

  my @ret = do_hook($opt,$data, 'dir_thumb', $child, $child_data, @images);
  # Description: Picks the image for a directory thumbnail
  # Description: Passes the child directory, the child_data object, and
  # Description: an array of the images as ordered in the album.
  # 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];

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

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

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

  # Get directory info, possibly pick an image for thumbnail?
  my ($tn_path,$url,$full_path,$file) = dir_info($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->{is_image} = is_image($opt,$file);

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

    # URL to find the thumbnail
    my $thumb_path = diff_path($opt, $data->{paths}{dir}, $obj->{thumb}{path});
    $obj->{URL}{album_page}{thumb} = url_quote($opt, $thumb_path);
  }

# 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};
	$WINDOWS ? ($alb_path =~ s|^[A-Z]:/||) : ($alb_path =~ s|^/||);
  # If we're starting with a subalbum, update @dir_pieces based on parse_index()
  if (!$opt->{force_subalbum} && !$#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,@args) = ("Your album has moved\n");	#TRANS
      if (@old) {
        $fatal .= "\nIt looks like you've moved a subalbum.  To fix this, run:\n";	#TRANS
        $fatal .= "\n% $PROGNAME -add $dir\n";
      } 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);
        $fatal .= "\nIt looks like you've moved your top album.\n\n";	#TRANS
        $fatal .= "To fix this:\n";	#TRANS
        $fatal .= "1) Erase:   [_1]\n";	#TRANS(<index file>)
				push(@args, $index);
        $fatal .= "2) Run:     [_2]\n";	#TRANS("<album command>")
				push(@args, $PROGNAME.' '.$broke);
        $fatal .= "   or else: [_3]\n" unless $broke eq $dir;	#TRANS("<album command>")
				push(@args, $PROGNAME.' '.$dir);
      }
      fatal($opt,$fatal,@args);	#NOTRANS
    }

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

  return if -l $dir && !$opt->{follow_symlinks};

  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;

	# Handle -list_options, -list_themes
	if ($opt->{list_options} || $opt->{list_themes}) {
		list_options($opt) if $opt->{list_options};
		list_themes($opt) if $opt->{list_themes};
  	pop_opts($opt) if $pushed;
		return;
	}

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

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

  # Print some info
  my $w = hash_msg_width($opt) - 8;
  my $album_name = (length($album) >= $w) ?  '..'.substr($album,-$w+3) : $album;
  start_hashes($opt, "Images:  [_1]", $album_name);	#LVL=2
  my $tUnknown = trans($opt,"unknown");	#EXPLANATION: Short for "unknown album"	#LVL=2
  if ($noalb || $data->{unknown}) {
    hash_msg($opt,"<".($noalb ? $opt->{no_album} : $tUnknown).">");
    prterr($opt, "\nNothing to do.  Call album with your photo directory as an option.\n\n")
			if $data->{start};
    return;
  }
  return hash_msg($opt,"<$tUnknown>") 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');

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

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

  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

  data_to_eperl($opt,$data);

  ($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
  # Description: This hook is too late for themes to get any $opt/$data changes!
	# Description: Instead consider putting $opt/$data changes in write_index
  # 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) = @_;

	# Kludge - sometimes $pic contains directory elements
	# (such as if we use a plugin to specify a thumbnail in another directory)
	($dir,$pic) = ($dir.$opt->{slash}.$1,$2)
		if $pic =~ m|(.+\Q$opt->{slash}\E)(.+?)$|;

  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 '[_1]'\n",$dir);

  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$/i) {
    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},$qimg,"-verbose",$opt->{dev_null});
  $size || fatal($opt,"Couldn't run convert!  ~[[_1]~]\n",$opt->{convert});
  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";
  pperror($opt,"Can't get ~[[_1]~] size from 'convert -verbose' output\n",$img);
  prterr($opt,"\tTry option:  [_1] to ignore garbage files\n",'-known_images')
    unless ($img =~ /\.$IMAGE_TYPES$/i);
  # Gentoo has this goofy thing, see:
  prterr($opt,"\n\tGentoo users: make sure to run as root:\n[_1]","% USE=\"avi gif jpeg mpeg png quicktime tiff\" emerge imagemagick\n")
    if -f "/etc/gentoo-release";
  prterr($opt,"\tWindows users may have an easier time with Cygwin installed!\n")
    if $opt->{windows} && !$opt->{cygwin};
	return (0,0);	# Keep going, just ignore the image
	#fatal($opt);	#NOTRANS
}

# Get a size for an image or for an obj given a type (thumb, medium, full)
# Sometimes there is no image for a given type, and that's fine.
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};
	return (0,0) unless $obj->{$use}{path};
  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;
	# The old imagemagick would create file.jpg.gif.0 instead of file.jpg.gif
	# New ImageMagick creates file.gif instead of file.jpg.gif [convert bug!]
	my $newPoss = $new =~ m|(.+)(\.[^\./]+)(\.[^\./]+)$| ? "$1$3" : "${new}-0";
  return unless -f "$new.0" || -f $newPoss;
  # We shouldn't get here if animated_gifs is set
  return if $opt->{animated_gifs};
  $opt->{animated_gifs}=1;
  rename(-f $newPoss ? $newPoss : "$new.0",$new);	# Try to fix
  print STDERR "\n";
  pperror($opt,"Animated gif was found that wasn't scaled properly.\n\tTry using option [_1] in the future.\n",'-animated_gifs');
}

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  :(
# Patch: Matthew Probst: multi-page pdf/ps files also create mutliple images
  $qimg .= "\[0]" if $opt->{animated_gifs} || ($img =~ /\.(pdf|ps)$/i);
  my @args = ($qimg,'-verbose',@scale);

  # Scale options
  push(@args, @{$opt->{scale_opts}}) if $opt->{scale_opts};
  push(@args, @{$opt->{medium_scale_opts}}) if $medium && $opt->{medium_scale_opts};
  push(@args, @{$opt->{thumb_scale_opts}}) if !$medium && $opt->{thumb_scale_opts};

  # Sharpen?
  push(@args, "-sharpen", $opt->{sharpen}) if $opt->{sharpen};

  # Destination image
  push(@args, file_quote($opt,$new));

  my $size = open_pipe($opt,$opt->{convert},@args);
  $size || fatal($opt,"Couldn't run convert!  ~[[_1]~]\n",$opt->{convert});
  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_out($opt,"Error cropping [_1] (image not found)",$img)
    unless -f $img;
  my @cmd = ($opt->{convert}, $img,"-crop","${x}x${y}+${off_x}+${off_y}",$new);
  print STDERR "crop() run: @cmd\n" if $opt->{d};
  system(@cmd);
  return unless ($?);
  pperror($opt,"Cropping [_1]",$img);
}

#########################
# 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 $cmd = "$opt->{ffmpeg} -y -t 00:00:00.01 $format -i $qmovie -f jpeg $qimg";

  ### ffmpeg -f singlejpeg
	#my $cmd = "$opt->{ffmpeg} -y -t 00:00:00.01 $format -i $qmovie -f singlejpeg $qimg";

	### ffmpeg -f mjpeg  ('singlejpeg' replaced by 'mjpeg'  - don't know when)
	my $cmd = "$opt->{ffmpeg} -y -t 00:00:00.01 $format -i $qmovie -f mjpeg $qimg";

	### If we've upgraded to avconv instead of ffmpeg
  $cmd = "$opt->{ffmpeg} -y -t 00:00:00.01 $format -i $qmovie -f image2 -vframes 1 $qimg"
		if $opt->{ffmpeg} =~ /avconv/;

	# Check if they specified a clip time, such as "[thumb=3.1s]"
	my $cap = get_caption($opt,$movie);
	$cmd .= " -ss $cap->{clip_time}" if $cap->{clip_time};

  print STDERR "movie_frame() run: $cmd\n" if $opt->{d};
  system("$cmd > $opt->{dev_null} 2>&1");
  return $img unless $?;

  hash_out($opt,"Error extracting movie frame:\n\t[_1]\n\n\tDo you have ffmpeg installed?  http://ffmpeg.org/\n",$movie)
    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};

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

  my $ret = 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 ($ret) {
    $obj->{medium}{path} = $ret;
    $obj->{medium}{file} = $ret;
    $obj->{medium}{file} =~ s|.*$opt->{slash}||g;
    return get_size($opt,'medium',$obj)
  }

  # 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},1);
  ($obj->{full}{x},$obj->{full}{y}) = ($fx,$fy)
    if $base eq 'full' && $fx;

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

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

  $obj->{has_thumb} = $opt->{thumbs};

  return 1 if do_hook($opt,undef, 'pre_thumbnail', $dir, $obj);
  # Description: Hook before we do any thumbnail() processing
  # Description: so check in your hook for the types you can read.
  # Returns: 1 to skip further thumbnail processing.

  snapshot($opt,$dir,$obj);	# In case we need a snapshot
  return 1 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};

	my $post = $opt->{thumb_post} ? ".$opt->{thumb_post}" : "";
  my ($default_file, $default) = new_image_path($opt,$dir,$file,$opt->{type},"$post.tn",$post);

  $obj->{thumb}{path} = do_hook($opt,undef, 'thumbnail', $dir, $obj, $file, $full_path, $default);
  # 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 (likely to be $default)
  if ($obj->{thumb}{path}) {
    $obj->{thumb}{file} = $obj->{thumb}{path};
    $obj->{thumb}{file} =~ s|.*$opt->{slash}||g;
    $obj->{has_thumb}=0 unless get_size($opt,'thumb',$obj);
		return $obj->{has_thumb};
  }

  ($obj->{thumb}{file},$obj->{thumb}{path}) = ($default_file,$default);

  # 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')
        && !option_changed($opt,'crop')
        && -M $obj->{thumb}{path} < -M $full_path);

  # In case we didn't get the size yet
  my ($x,$y) = get_size($opt,$base,$obj);
  return $obj->{has_thumb}=0 unless $x && $y;

  # 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 $obj->{has_thumb}=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
  init_language($opt);
  read_confs($opt);
  parse_args($opt);
  scrub_opts($opt,1);

  version($opt);

  virgin_check($opt);

  return if do_hooks($opt,undef, 'pre_do_albums');
  # Description: Called before doing any albums.
  # Description: WARNING!  This cannot be called unless the calling plugin
  # Description:   is specified on the command-line.  This should only
  # Description:   be used by one-time plugins!  (i.e.: utils/mv)
  # Description: Use 'do_album' or 'do_album_top' instead.
  # Returns: 1 to stop here

  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, -dtheme, -Dtheme, -virgin_check, -save_conf, -configure, -crf, -list_options, -image_pages, -thumbs, -dir_thumbs, -just_medium, -slideshow, -embed, -clean, -image_headers, -album_captions, -folder_count, -caption_edit, -file_sizes, -fix_urls, -known_images, -all, -hashes, -reverse_sort, -case_sort, -image_loop, -burn, -crop, -force, -force_html, -sample, -animated_gifs, -use_tcap

String/number options:

% album -thumb_post, -medium, -captions, -top, -sort, -body, -charset, -force_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, -credit, -no_album, -hide_album, -not_img

Array options:

% album --lang_path, --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<-usage>I<=I<level>>

Show usage as deep as you like.

=item B<-lang>I<=I<lang>>

Specify language(s)

=item B<-list_langs>I<>

List out full language information

=item B<-make_lang>I<=I<lang>>

Print out a new language file

=item B<-list_html_trans>I<>

List HTML translations for each language
Useful for creating multi-lingual images for themes
Output is in HTML and utf-8, change charset as needed

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

Add a path to search for language files [Default @DATA_PATH/lang]

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

Theme debug mode [Default OFF]

=item B<-Dtheme>I<>

Theme 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

=item B<-mv>I<>

Move imgs across albums: see 'album -plugin_info utils/mv'

=item B<-create_plugin>I<>

Create plugin: see 'album -plugin_info utils/create_plugin'

=back

=head2 Album Options:

=over 4

=item B<-crf>I<>

Album hash output in computer readable format [Default OFF]

=item B<-list_options>I<>

Show default options and values for a given album [Default OFF]

=item B<-image_pages>I<>

Create a page for each image [Default ON]

=item B<-thumbs>I<>

Images have thumbnails [Default ON]

=item B<-dir_thumbs>I<>

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

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

Additional postfix for thumbnails.

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

Slideshow capabilities (only with some themes) [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<-folder_count>I<>

Show folder/image counts for each album [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 [-1]) [Default -1]

=item B<-follow_symlinks>I<>

Dereference symbolic links [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, date, EXIF date ('exif') [Default captions]

=item B<-reverse_sort>I<>

Sort in reverse [Default OFF]

=item B<-case_sort>I<>

Use case sensitive sorting when sorting names [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 and some theme output
This is also set by using language files (with -lang)

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

Force charset (not overridden by languages)

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

Use '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_plugins_crf>I<>

Print info for all plugins in computer readable format

=item B<-list_hooks>I<>

Show all known plugin hooks (for developers)

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

Show hook info for a specific hook (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 /data/proj/album/Themes]

=item B<-list_themes>I<>

Show available 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<-credit>I<=I<string>>

Credit line to add to the bottom of every album

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

Ignore dir/file if dir/file.no_album 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
