#!/usr/pkg/bin/perl -w

#    BINS Photo Album version 1.1.25
#    Copyright (C) 2001-2003 J�r�me Sautret (Jerome@Sautret.org)
#
#    Original SWIGS code :
#    Copyright (C) 2000 Brendan McMahan (mcmahahb@whitman.edu)
#    Initial code based on IDS 0.21 :
#    Copyright (C) John Moose (moosejc@muohio.edu)
#
#    $Id: bins,v 1.157 2004/02/22 19:01:21 jerome Exp $
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; see the file COPYING.  If not, write to
#    the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
#    Boston, MA 02111-1307, USA.


#    Type "bins -h" on command line for usage information
#    and read bins(1) man page.

my $verbose = 1;  # verbosity level (from 0 to 4)
use strict;

# Convention: When we pass around paths and directories (aka albums),
# we always include the trailing backslash, like "albums/", so we can
# easily combine the directories

# General Perl modules
use POSIX qw(strftime);
use Storable qw(dclone);
use File::Glob ':glob';
use File::Basename;
use File::Spec;
use IO::File;
use UNIVERSAL; # qw(isa);

use Getopt::Long;

# Image manipulation
use Image::Size;
use Image::Info qw(image_info);
use Image::Magick;

# HTML manipulation
use URI::Escape;
use HTML::Entities;
use HTML::Template;
use HTML::Clean;
#use HTML::Template::JIT;

# XML parsing & writing
use XML::Grove;
use XML::Grove::Builder;
use XML::Grove::Path;
use XML::Grove::AsCanonXML;
use XML::Grove::PerlSAX;
use XML::Parser::PerlSAX;
use XML::Handler::YAWriter;
use Text::Iconv;

#use XML::Handler::XMLWriter;
#use XML::Grove::AsString;
#use XML::Handler::Composer;
#use XML::Filter::Reindent;
#use XML::Handler::Sample;

# Debugging
use Data::Dumper;
#use XML::SAX::Expat;

sub have_package;
sub _; # alias for Getext, if present

############################################################################
#                            Configuration Section                         #
############################################################################

# I18N
my $localePath = "/usr/pkg/lib/locale"; # Base locale path  (for I18N)

my $I18N = have_package("Locale::gettext");
if ($I18N) {
  require Locale::gettext;
  require POSIX;
  POSIX::setlocale(&POSIX::LC_ALL, "");
  Locale::gettext::bindtextdomain("bins", $localePath);
  Locale::gettext::textdomain("bins");
}


# You can change the following parameters in the /usr/pkg/etc/bins/binsrc or
# ~/.bins/binrc configuration files or in the <bins> section of the
# album or image description files (i.e. album.xml or
# <image_name>.jpg.xml) when it makes sense.

my %defaultConfig =
  (
   homeURL => "/",            # Set this to your home page. This is
                              # used for the leave button in some
                              # templates.

   feedbackMail => "",    # Put here the mailaddress of the
                          # album-maintainer.  If this is set, you
                          # will get a mail-icon in your views that
                          # links to this address.

   treePreview => 1,          # If set to 1, preview-thumbnails will be
                              # showed in the album-tree-page.

   backgroundImage => "",     # Set this to the image that should be displayed
                              # as the background of the album-pages.
			      # The Image will be copied to the static-files
			      # directory.
			      # The name should be unique for the entire album.

   customStyleSheet => "",    # Set this to the name that should be used
                              # for the current album and its subalbums
			      # The Stylesheet will be copied to the
			      # static-files directory.
			      # The name should be unique for the entire album.

   excludeBackgroundImage => 1, # If set to 1, the image with the name given
                                # in backgroundImage will be excluded from
				# the current directory.

   addExifToDescFile => 1,    # If set to 1, write exif data found in
                              # the image file to the image desc file.

   deExifyImages => 1,        # If set to 1, do NOT copy exif data found
                              # in the source images to any of the generated
                              # resized images. Setting this option can yield
                              # significant space savings, especially for
                              # thumbnail and imagelist pages.

   createEmptyDescFields => 1,# If set to 1, add empty description
                              # fields in the <description> section
                              # when the image description file is
                              # created to ease later edition with an
                              # text editor

   jpegQuality => 75,         # Quality of scaled jpegs (lower number = more
                              # compression, lower quality) in 1-100 range.

   jpegProgressify => "smaller", # values: never, always, smaller.  whether
				 # to make jpegs progressive using jpegtran
				 # (if available).  smaller means only if
				 # the progressive file is smaller than the
				 # original

   titleOnThumbnail => 1,     # Should the title be displayed on top on the
                              # thumbnail in the thumbnails page ?

   emptyAlbumDesc => 0,       # If set to 1, and album desciption is
                              # not set, no message will be displayed
                              # (instead of the "No long/short
                              # description available" one).

   reverseOrder => 0,         # are we reversing sorting order (see -r)
                              # (0=none,1=dirs,2=pix,3=both)

   defaultSize => 1,	      # Size to use when user clicks directly
                              # on the thumbnail in the thumbnails
                              # page instead of one of the size
                              # name. 0 is the first size (Small in
                              # the default config), 1 the second
                              # (Medium), and so on. Set this variable
                              # to -1 if you don't want the thumbnail
                              # to be clickable.

   thumbnailPageCycling => 1, # If set to 0 next/prev-Links will be hidden if
   			      # the actual page is the last/first Thumbnailpage

   imagePageCycling => 1,     # If set to 0 next/prev-Links will be hidden if
   			      # the actual page is the last/first Imagepage

   pathImgNum => 1,           # If set to 1 the path in the imageview contains
   			      # the number of the current image

   pathShowIcon => 1,         # If set to 1 the path contains icons

   thumbnailInImageList => 1, # Display thumbnails on the Image List page ?

   albumThumbInSubAlbumPage =>1, # If set to 1, display the current
                                 # album thumbnail in sub-albums page
                                 # if it has pictures, with links to
                                 # the thumbnails page.

   allThumbnailsPage => 0,    # If set to 1, generate a page with all
                              # thumbnails in the album and
                              # sub-albums. This is deactivated
                              # because it is an alpha feature which
                              # seems to not work properly.

   thumbnailBackground => 0,  # If 1, add a background colour to the
                              # thumbnail's cell in the thumbnails
                              # page so that if the top and bottom
                              # borders are wider than the image (for
                              # example, if it is in portrait mode),
                              # instead of spilling over, there is a
                              # border around the whole picture.

   thumbsPerRow => 4,	      # Number of thumbnails displayed in each
                              # row in an album.
   numThumbsPerPage => 16,    # Number of thumbnails displayed
                              # in each page in an album.
   previewMaxHeight => 150,   # Max Thumbnail Height.
   previewMaxWidth => 150,    # Max Thumbnail Width.

   thumbPrevNext => 1,        #�If set to 1, display thumbnails close
                              # to the previous and next link at the
                              # bottom of the image page.

   rotateImages => "destination",# Do we rotate images if the Orientation
                              # Exif tag is found ? If set to
                              # 'original', the original image is
                              # rotated the first time, and then it is
                              # left untouched. If set to
                              # 'destination', this is all the scaled
                              # images and thumbnails that are
                              # rotated. This is less efficient, but
                              # the original images are preserved. If
                              # set to 'none', no rotation is
                              # performed.

   rotateWithJpegtran => 0,   # If set to 1, bins try to use the
                              # jpegtran program to rotate JPEG images
                              # if it is available. jpegtran is faster
                              # and lossless, but some versions fail
                              # to perform rotation correctly, so it
                              # is deactivated in default config. If
                              # set to 0 or if jpegtran is not found,
                              # mogrify (from ImageMagick) is used.

   scaleIfSameSize => 0,      # If set to 1, we scale the picture even
                              # if destination size is the same as the
                              # original picture, if set to 0, the
                              # original image is just copied if the
                              # size is correct.

   scaleMethod => "scale",    # What method should be used to create
                              # scaled pictures and thumbnails ? Can
                              # be either scale or sample. sample is
                              # faster, scale is better.

   linkInsteadOfCopy => 0,    # If set to 1, we link the picture instead
                              # of copying it if possible
                              # (i.e. scaleIfSameSize is set to 0 and
                              # destination image doesn't have to be
                              # rotated : rotateImages is set to
                              # original or none, or orientation is
                              # already correct).

   updateOriginalPerms => 1,  # attempt to update source image permissions

   enlarge => 0,              # If set to 1, small images are enlarged
                              # in the med and large series.

   maxAlbumsForLongSubAlbum => 20, # If the number of sub albums is greater,
                                   # generate a short sub album page
                                   # instead of the long one.

   stripDirPrefix => 0,            # If set to 1, Numbers preceding
                                   # the album title, followed by an
                                   # underscore, are stripped. If this
                                   # parameter is set, then prefix
                                   # ordering numbers on directories
                                   # are removed.  For example, if one
                                   # has directories may, june, and
                                   # august, they can be renamed
                                   # 0_may, 1_june, and 2_august and
                                   # they will appear in the album in
                                   # the correct order. This can be
                                   # overridden by the-p command line
                                   # option.

   compactHTML => 1,               # If set to 1, generated
                                   # HTML code is cleaned up to reduce
                                   # the size of pages and thus, speed
                                   # up browsing This reduces the size
                                   # of HTML BINS files by about
                                   # 30%. See HTML::Clean(3) to know
                                   # how optimizations are performed.

   javaScriptPreloadImage => 1,    # If set to 1, add some javascript
                                   # code in image pages to preload
                                   # the next image of the same size
                                   # when current one is loaded, to
                                   # speed up the album browsing.

   javaScriptPreloadThumbs => 1,   # If set to 1, add some javascript
                                   # code in thumbnails pages to
                                   # preload thumbnails of the next
                                   # page when current one is loaded,
                                   # to speed up the album browsing.

   createHtaccess => 1,       	   # If 1, create an Apache .htaccess file
                              	   # in the root dir of the album with the
                              	   # encoding charset bound to html and
                              	   # htm files

   noRotation => '_Orig$' ,        # Don't perform rotation on files
                                   # matching this regexp

   excludeFiles => "" ,            # exclude image files that match
                                   # this regexp (if set).

   excludeDirs  => '^CVS$' ,       #' exclude directories that match
                                   #  this regexp (if set).

   ignore => "",    # Put here a comma separated list of keyword. If
                    # one on this keyword is found in the "ignore"
                    # field in the <description> section of an
                    # sub-album.xml, then this sub-album will be
                    # ignored, i.e. it will not be processed. You can
                    # also use the -i command line option.

   hidden => "",    # Put here a comma separated list of keyword. If
                    # one on this keyword is found in the "ignore"
                    # field in the <description> section of an
                    # sub-album.xml, then this sub-album will be
                    # hidden, i.e. it will be generated but not linked
                    # anywhere. You can also use the -n command line
                    # option.


   colorStyle => "blue",           #�name of the color style to use
   templateStyle => "default",     #�name of the template style to use
   # note that all of these options are now documented in bins.sgml;
   # any new options need corresponding new documentation.

   # The following parameters cannot be set in config files for now :
   globalConfigDir => "/usr/pkg/etc/bins",       # System wide configuration directory
   globalDataDir   => "/usr/local/share/bins", # System wide data directory
   userConfigDir   => "~/.bins",         # User configuration directory
   configFileName  => "binsrc",          # Configuration file.

   htmlEncoding    => "UTF-8",           # HTML pages charset encoding
   xmlEncoding     => "UTF-8",           # XML files charset encoding
   defaultEncoding => "ISO-8859-1",      # Charset encoding of your environment.
                                         # This value is overridden by
                                         # your real local encoding as
                                         # reported by the 'locale
                                         # charmap' unix command.
                                         # This is used to display
                                         # strings on console and to
                                         # convert strings from .po
                                         # files.
);

my $localEncoding = `locale charmap`;
# ANSI is unspeakably primitive, keep LATIN1 instead
if ($? == 0 && $localEncoding && ($localEncoding ne "ANSI_X3.4-1968")) {
  chop($localEncoding);
  $defaultConfig{defaultEncoding} = $localEncoding;
  beVerboseN("Forcing encoding to $localEncoding", 2);
}
my $local2htmlConverter;
$local2htmlConverter = Text::Iconv->new($defaultConfig{defaultEncoding},
					$defaultConfig{htmlEncoding});

#�Here are set number, name and size of scaled images.
# This can be changed in the binsrc or album.xml files.
# By default, there is three sizes, but you can remove or add some new
# by editing the @scaledWidths, @scaledHeights, @sizeNames and
# @longSizeNames lists.

# This is this size of each scaled picture :
my @scaledWidths = ("40%", "64%", "100%");  # Can be either a resolution or
my @scaledHeights = ("40%", "64%", "100%"); # a % of the original picture

#my @scaledWidths = (200, 400, 600, 3000);
#my @scaledHeights = (200, 400, 600, 3000);

#This is the name of each scaled picture. Remember that the _("")
#function is used for I18N only and is not mandatory.
my @sizeNames =     (_("Sm"), _("Med"), _("Lg"));
my @longSizeNames = (_("Small"), _("Medium"), _("Large"));
my @fileSizeNames = ("small.png", "medium.png", "large.png", "huge.png");
my @fileActiveSizeNames = ("smallActive.png", "mediumActive.png",
			   "largeActive.png", "hugeActive.png");

#my @sizeNames =     (_("Sm"), _("Med"), _("Lg"), _("Hg"));
#my @longSizeNames = (_("Small"), _("Medium"), _("Large"), _("Huge"));

$defaultConfig{scaledWidths} = \@scaledWidths;
$defaultConfig{scaledHeights} = \@scaledHeights;
$defaultConfig{sizeNames} = \@sizeNames;
$defaultConfig{longSizeNames} = \@longSizeNames;
$defaultConfig{fileSizeNames} = \@fileSizeNames;
$defaultConfig{fileActiveSizeNames} = \@fileActiveSizeNames;

# Fields to display (in the list order) under the picture. These
# fields are defined in the %fields hash below.
my @mainFields = ("description", "people", "location", "date",
		       "event");
# Fields to display (in the list order) in the details page. These
# fields are defined in the %fields hash below.

my @secondaryFields = (
		       # DigiCam
		       _("BINS-SECTION DigiCam Info"),
		       "model", "owner", "firmware",
		       # DigiCam settings for the image
		       _("BINS-SECTION DigiCam settings for the image"),
		       "canon_quality",
		       "canon_image_size",
		       "CanonContrast",
		       "CanonSaturation",
		       "CanonSharpness",
		       # DigiCam settings for the photo
		       _("BINS-SECTION DigiCam settings for the photo"),
		       "canon_easy_shooting_mode",
		       "canon_macro",
		       "flash",
		       "canon_flash_mode",
		       "CanonISO",
		       "iso",
		       "exposure_time",
		       "exposure_prog",
		       "canon_digital_zoom",
		       "canon_focus_mode",
		       "CanonFocusType",
 		       "subject_distance",
		       "metering_mode",
		       "focal_length",
                       "shutter_speed_value",
		       "aperture_value",
		       "max_aperture_value",
		       "fnumber",
		       "canon_timer_length",
		       "canon_continuous_drive_mode",
		       "focal_plane_x_resolution",
		       "focal_plane_y_resolution",
		       "orientation",
		       # Image characteristics
		       _("BINS-SECTION Image Characteristics"),
		       "date",
		       "file_media_type",
		       "jpeg_type",
		       "interlace",
		       "color_type",
		       "samples_per_pixel",
		       "bits_per_sample",
		       "resolution",
		       "compression",
		       "usercomment",
		       "exif_version",
		       "image_width",
		       "image_length",
		       "compressed_bits",
		       "BINS-SECTION end", # close the last section
);

my %fields =
  # The key is the string used as the name in the picture
  #   description file.
  # Name corresponds to the string displayed under the picture.
  # EXIF is the name of the field in the EXIF structure
  #   found in some JPEG images.
  # The value of the EXIF structure is only used if no value
  #   is present in the picture description file.
  # Transform is a Perl operator used to convert an exif value
  #   to the desired format to display.
  (
   "title" =>
   { Name => _("Title"),
   },
   "description" =>
   { Name => _("Description"),
   },
   "people" =>
   { Name => _("People"),
   },
   "location" =>
   { Name => _("Location"),
   },
   "date" =>
   { Name => _("Date"),
     EXIF => "DateTimeOriginal",
     Transform => 's%^(\d+):(\d+):(\d+) (.*)$%$3/$2/$1 $4%', # french date
   },
   # English version is yyyy:mm:dd hh:mm:ss to yyyy/mm/dd hh:mm:ss :
   # "s%^(\\d+):(\\d+):(\\d+) (.*)$%\$1/\$2/\$3 \$4%",
   # French version is yyyy:mm:dd hh:mm:ss to dd/mm/yyyy hh:mm:ss :
   # 's%^(\d+):(\d+):(\d+) (.*)$%$3/$2/$1 $4%',

   "event" =>
   { Name => _("Event"),
   },
   "model" =>
   { Name => _("Camera Model"),
     EXIF => "Model",
   },
   "firmware" =>
   { Name => _("Software"),
#     EXIF => "Canon-Tag-0x0007",
     EXIF => "Software",
     Tip => _("Firmware (internal software of digicam) version number."),
   },
   "owner" =>
   { Name => _("Owner name"),
#     EXIF => "Canon-Tag-0x0009",
     EXIF => "Owner",
     Tip => _("Name of the owner of the digicam."),
   },
   "flash" =>
   { Name => _("Flash"),
     EXIF => "Flash",
   },
   "usercomment" =>
   { Name => _("User comment"),
     EXIF => "UserComment",
   },
   "file_media_type" =>
   { Name => _("File Media Type"),
     EXIF => "file_media_type",
     Tip =>_("This is the MIME type that is appropriate for the given file format."),
   },
   "color_type" =>
   { Name => _("Color Type"),
     EXIF => "color_type",
   },
   "jpeg_type" =>
   { Name => _("JPEG Type"),
     EXIF => "JPEG_Type",
   },
   "interlace" =>
   { Name => _("Interlace method"),
     EXIF => "Interlace",
     Tip => _("Interlace method used."),
   },
   "metering_mode" =>
   { Name => _("Metering Mode"),
     EXIF => "MeteringMode",
     Tip => _("Exposure metering method."),
   },
   "samples_per_pixel" =>
   { Name => _("Samples Per Pixel"),
     EXIF => "SamplesPerPixel",
     Tip => _("This says how many channels there are in the image. For some image formats this number might be higher than the number implied from the \"Color Type\""),
   },
   "resolution" =>
   { Name => _("Physical Resolution"),
     EXIF => "resolution",
     Tip => _("The value of this field normally gives the physical size of the original image on screen or paper. When there is no unit then this field denotes the squareness of pixels in the image."),
   },
   "compression" =>
   { Name => _("Compression Algorithm"),
     EXIF => "Compression",
   },
   "exif_version" =>
   { Name => _("Exif Version"),
     EXIF => "ExifVersion",
   },
   "subject_distance" =>
   { Name => _("Subject Distance"),
     EXIF => "SubjectDistance",
     Tip => _("Distance to focus point."),
   },
   "bits_per_sample" =>
   { Name => _("Bits Per Sample"),
     EXIF => "BitsPerSample",
     Tip => _("This says how many bits are used to encode each of samples."),
   },
   "exposure_time" =>
   { Name => _("Exposure Time"),
     EXIF => "ExposureTime",
     Tip => _("Exposure time (reciprocal of shutter speed)."),
   },
   "shutter_speed_value" =>
   { Name => _("Shutter Speed Value"),
     EXIF => "ShutterSpeedValue",
     Tip => _("Shutter speed by APEX value."),
   },
   "focal_length" =>
   { Name => _("Focal Length"),
     EXIF => "FocalLength",
     Tip => _("Focal length of lens used to take image."),
   },
   "aperture_value" =>
   { Name => _("Aperture Value"),
     EXIF => "ApertureValue",
     Tip => _("The actual aperture value of lens when the image was taken."),
   },
   "max_aperture_value" =>
   { Name => _("Maximum Aperture Value"),
     EXIF => "MaxApertureValue",
     Tip => _("Maximum aperture value of lens."),
   },
   "fnumber" =>
   { Name => _("F-Number"),
     EXIF => "FNumber",
     Tip => _("The actual F-number (F-stop) of lens when the image was taken."),
   },
   "focal_plane_y_resolution" =>
   { Name => _("Focal Plane Y Resolution"),
     EXIF => "FocalPlaneYResolution",
     Tip => _("Pixel density at CCD's position. If you have MegaPixel digicam and take a picture by lower resolution (e.g.VGA mode), this value is re-sampled by picture resolution. In such case, Focal Plane Y Resolution is not same as CCD's actual resolution."),
   },
   "focal_plane_x_resolution" =>
   { Name => _("Focal Plane X Resolution"),
     EXIF => "FocalPlaneXResolution",
     Tip => _("Pixel density at CCD's position. If you have MegaPixel digicam and take a picture by lower resolution (e.g.VGA mode), this value is re-sampled by picture resolution. In such case, Focal Plane X Resolution is not same as CCD's actual resolution."),
   },
   "canon_macro" =>
   { Name => _("Macro"),
     EXIF => "CanonMacro",
     #Tip => _(""),
   },
   "canon_timer_length" =>
   { Name => _("Timer Length"),
     EXIF => "CanonTimerLength",
     #Tip => _(""),
   },
   "canon_quality" =>
   { Name => _("Quality"),
     EXIF => "CanonQuality",
     Tip => _(""),
   },
   "canon_continuous_drive_mode" =>
   { Name => _("Continuous Drive Mode"),
     EXIF => "CanonContinuousDriveMode",
     #Tip => _(""),
   },
   "canon_flash_mode" =>
   { Name => _("Flash Mode"),
     EXIF => "CanonFlashMode",
     #Tip => _(""),
   },
   "canon_focus_mode" =>
   { Name => _("Focus Mode"),
     EXIF => "CanonFocusMode",
     #Tip => _(""),
   },
   "canon_image_size" =>
   { Name => _("Image Size"),
     EXIF => "CanonImageSize",
     #Tip => _(""),
   },
   "canon_digital_zoom" =>
   { Name => _("Digital Zoom"),
     EXIF => "CanonDigitalZoom",
     #Tip => _(""),
   },
   "canon_easy_shooting_mode" =>
   { Name => _("Easy Shooting Mode"),
     EXIF => "CanonEasyShootingMode",
     #Tip => _(""),
   },
   "CanonContrast" =>
   { Name => _("Contrast"),
     EXIF => "CanonContrast",
     #Tip => _(""),
   },
   "CanonSaturation" =>
   { Name => _("Saturation"),
     EXIF => "CanonSaturation",
     #Tip => _(""),
   },
   "CanonSharpness" =>
   { Name => _("Sharpness"),
     EXIF => "CanonSharpness",
     #Tip => _(""),
   },
   "CanonISO" =>
   { Name => _("ISO"),
     EXIF => "CanonISO",
     #Tip => _(""),
   },
   "iso" =>
   { Name => _("ISO"),
     EXIF => "ISOSpeedRatings",
     #Tip => _(""),
   },
   "CanonFocusType" =>
   { Name => _("Focus Type"),
     EXIF => "CanonFocusType",
     #Tip => _(""),
   },
   "exposure_prog" =>
   { Name => _("Exposure Program"),
     EXIF => "ExposureProgram",
     #Tip => _(""),
   },
   "image_width" =>
   { Name => _("Original Image Width"),
     EXIF => "ExifImageWidth",
     #Tip => _(""),
   },
   "image_length" =>
   { Name => _("Original Image Length"),
     EXIF => "ExifImageLength",
     #Tip => _(""),
   },
   "compressed_bits" =>
   { Name => _("Compression Quality"),
     EXIF => "CompressedBitsPerPixel",
     #Tip => _(""),
   },
   "Orientation" =>
   { Name => _("Orientation"),
     EXIF => "Orientation",
     #Tip => _(""),
   },
   "" =>
   { Name => _(""),
     EXIF => "",
     #Tip => _(""),
   },
);

my @priorityExifTags = ();  # the field in this list are taken from
                            # the desc file, even if they are present
                            # in image file (normally, image field
                            # takes precedence on desc file

# these substitutions are made in all templates, useful for doing easy
# color assignments, etc. The colors can be set in the bins/colors
# section of config files or album/pictures desc files.
my %colorsSubs = (blue =>
		  { PAGE_BACKCOLOR           => "#FFFFFF",
		    PAGE_TITLECOLOR          => "#000000",
		    MAINBAR_BACKCOLOR        => "#000077",
		    MAINBAR_TITLECOLOR       => "#FFFFFF",
		    MAINBAR_LINKCOLOR        => "#eedd82",
		    MAINBAR_CURRENTPAGECOLOR => "#d2d2d2",
		    SUBBAR_BACKCOLOR         => "#6060af",
		    SUBBAR_LINKCOLOR         => "#eedd82",
		    SUBBAR_CURRENTPAGECOLOR  => "#000000",
		    SUBBAR_TITLECOLOR        => "#FFFFFF",
		  });
$defaultConfig{colorsSubs} = \%colorsSubs;

# Strings to translate in the HTML template pages (if I18N is used)
my %intlSubs = (  STRING_THUMBNAILS   => _("thumbnails"),
		  STRING_IMAGELIST    => _("Image List"),
		  STRING_HOME => _("Home"),
		  STRING_UP   => _("Up one album"),
		  STRING_PREV => _("previous"),
		  STRING_NEXT => _("next"),
		  STRING_FIRST => _("first"),
		  STRING_LAST => _("last"),
		  STRING_SUBALBUMS => _("Sub Albums"),
		  STRING_INTHISALBUM => _("In This Album"),
		  STRING_BACKTOTHEIMAGE => _("Back to the image"),
		  STRING_IMAGE => _("Image"),
		  STRING_ALBUMTREE => _("Album Tree"),
		  STRING_ALBUMGENERATEDBY => _("Album generated by"),
                  STRING_FEEDBACK => _("Send Feedback"),
                  STRING_YOURIMAGE => _("Your Image"),
                  STRING_YOURALBUM => _("Your Album"),
		  BINS_VERSION             => "1.1.25",
		  ENCODING                 => $defaultConfig{htmlEncoding},
		  GENERATED_DATE       => _("on ").
		                          local2html(strftime("%c", localtime)),
		  BINS_ID =>
		  '<!--$Id: bins,v 1.157 2004/02/22 19:01:21 jerome Exp $-->',
	       );

# @knownImageExtentions defines file extensions that BINS can handle as
# input image. BINS _should_ handle all input format of ImageMagick
# (see ImageMagick(1) man page), but there is some formats that cause
# problems. If you have tested successfully a format that is not
# listed here, or if you have a problem with one of the following
# format, please let me know.
my @knownImageExtentions = ("jpg", "jpeg", "gif", "png", "tiff",
			    "bmp", "tga", "ps", "eps", "fit", "pcx",
			    "miff", "pix", "pnm", "rgb", "im1", "xcf",
			    "xwd", "xpm", "avs", "dcm", "dcx", "dib",
			    "dps", "dpx", "epdf", "epi", "ept", "fpx",
			    "icb", "mat", "mtv", "pbm", "pcd", "pct",
			    "pdb", "ppm", "ptif", "pwp", "ras", "thm",
			    );

my @webFormats = ("JPEG", "GIF", "PNG"); # Image formats that can go
                                         # into the web album (other
                                         # formats will be converted
                                         # to JPEG).

my @filesToLinkExtensions = ( "avi", "mpg", "mpeg", "mov" );

############################################################################
#                        End of Configuration Section                      #
############################################################################


#subroutine declarations

sub usage;

sub beVerbose;
sub beVerboseN;
sub min;
sub readConfigFile;
sub fileSize;
sub generateAlbumPages;
sub filenameToPreviewName;
sub getSizeLinks;

sub openTemplate;
sub doSubstitutions;
sub renderPage;
sub generateTreeLoop;
sub generateImage;
sub getRootDir;
#sub generateScaledImage;
sub getDesc;
sub getExif;
sub readField;
sub trimWhiteSpace;
sub stringToBool;
sub ignoreSet;

sub generateThumbnailPages;
sub generateThumbEntry;
sub generateThumbPage;
sub generateImagesInAlbum;

sub write_htaccess;

print "\nBINS Photo Album 1.1.25 (http://bins.sautret.org/)\n";
print "Copyright � 2001,2002 J�r�me Sautret (Jerome\@Sautret.org)\n";
print "Some parts of code:\n";
print "Copyright � 2000 Brendan McMahan (mcmahahb\@whitman.edu)\n";
print "Copyright �      John Moose (moosejc\@muohio.edu)\n\n";
print "This is free software with ABSOLUTELY NO WARRANTY.\n";
print "See COPYING file for details.\n\n";

# EVG (Evil Global Variables)
# Some on them should be moved to the config hash so they can be
# managed by config files
my $ignoreOpts="";             # to ignore some albums (see -i)
my $hiddenOpts="";             # to hide some albums
my $genEditableAlbum;          # are we creating a editable album (see -e)
my ($imageSource, $oneCopy);   # How to handle scaled images
my $appendToDescFile;          # write to desc file ? (see -d)
my $templateDir;
my ($picdir, $albumdir);       # source and destination directories

# charset converters
my ($xml2htmlConverter, $html2xmlConverter, $xml2localConverter);
my $optimizeConversion = 0;	# this cause problem if set to 1,
                                # but may help on Sun Solaris.


main();

# process command line arguments before reading config file
sub preProcessArgs {
  my $configHash = shift;

  # process args
  my %option;
  Getopt::Long::Configure("bundling");
  die "Invalid options\n"
    if (!GetOptions(\%option, "h", "p", "r:s", "e", "o:s", "t=s", "d=s", "s=s",
		    "c=s", "v:i", "i=s", "n=s", "f=s"));

  if (defined($option{v})) {
    $verbose = $option{v};
  }
  beVerboseN("Verbosity level is set to $verbose.", 2);

  #get the config file from the command line
  if ( $option{'f'} ) {
    (my $junk ,$defaultConfig{'userConfigDir'},$defaultConfig{'configFileName'})
      = File::Spec->splitpath($option{'f'});
    $defaultConfig{'userConfigDir'} =~ s|/*$||;
  }

  return \%option;
}

# process command line arguments after reading config file
sub postProcessArgs {
  my $option = shift;
  my $configHash = shift;

  my %option = %{$option};

  if (defined($option{i})) {
    $ignoreOpts="$option{i}";
    beVerboseN("Ignore is set to $ignoreOpts.", 2);
  }

  if (defined($option{n})) {
    $hiddenOpts="$option{n}";
    beVerboseN("Hidden is set to $hiddenOpts.", 2);
  }

  $genEditableAlbum = ($option{e} ? 1 : 0);

  if (defined($option{s})) {
    $configHash->{templateStyle} = $option{s};
  }
  $imageSource = "scaled";
  $oneCopy = 0;
  if (defined($option{o})) {
    $oneCopy = 1;
    $imageSource = ($option{o} ? $option{o} : "scaled");
  }

  if (defined($option{d})) {
    $appendToDescFile = $option{d};
  } else {
    $appendToDescFile = "always";
  }

  die 'invalid option for switch -d. Must be one of "always",'.
    '"never" or "exist"'
      if ( $appendToDescFile ne "always" && $appendToDescFile ne "never" &&
	   $appendToDescFile ne "exist");

  if ($option{t}) {
    $templateDir = $option{t};
    die "template location $templateDir doesn't exist" if (! -d $templateDir);
    #if ( substr($templateDir,-1,1) eq "/" ) {
    #  $templateDir .= "/";
  }

  if ($option{c}) {
    $configHash->{colorStyle} = $option{c};
    beVerboseN("Color style $option{c} selected.", 2);
  }

  $configHash->{reverseOrder} ||= 0;
  if (defined($option{r})) {
      if ($option{r} =~ "dirs") {
	  $configHash->{reverseOrder} = 1;
      }
      if ($option{r} =~ "pictures") {
	  $configHash->{reverseOrder} += 2;
      }
  }

  #$stripOrderNum = ($option{p} ? 1 : 0);
  if ($option{p}) {
    $configHash->{stripDirPrefix} = 1;
  }
  my $printHelp = ($option{h} ? 1 : 0);
  if ($printHelp) {
    usage();
    exit 1;
  }

  if ( $oneCopy && !($imageSource =~ /^(scaled|copied|custom)$/) ) {
    print "\nInvalid image source for -o. If you are leaving the src\n";
    print "argument off to get the default, put the -o switch at the\n";
    print "end of the line, or use \"-o -\" to get the default.\n\n";
    usage();
    exit 1;
  }

  # directories
  if ($#ARGV < 1) {
    print "source_dir and target_dir are required.\n";
    print "Type bins -h for more help.\n";
    exit 1;
  }

  $picdir = $ARGV[0];
  $albumdir = $ARGV[1];

  $picdir = File::Spec->rel2abs(File::Spec->canonpath($picdir));
  $albumdir = File::Spec->rel2abs(File::Spec->canonpath($albumdir));

  $picdir =~ s|/*$|/|;
  $albumdir =~ s|/*$|/|;

  die "You must specify a source (picture) directory.\n" if (!$picdir);
  die "You must specify a target (web) directory.\n" if (!$albumdir);

  #print "\$oneCopy = $oneCopy\n";
  #print "\$imageSource = $imageSource\n" if $oneCopy;
  #print "\$templateDir = $templateDir\n";
  #print "\$picdir = $picdir\n";
  #print "\$albumdir = $albumdir\n";

}

sub initConverters {
  if (! $optimizeConversion ||
      $defaultConfig{xmlEncoding} ne $defaultConfig{htmlEncoding}) {
    $xml2htmlConverter = Text::Iconv->new($defaultConfig{xmlEncoding},
					  $defaultConfig{htmlEncoding});
    $html2xmlConverter = Text::Iconv->new($defaultConfig{htmlEncoding},
					  $defaultConfig{xmlEncoding});
  }
  if ($verbose >= 1 && (!$optimizeConversion ||
			$defaultConfig{xmlEncoding} ne
			$defaultConfig{defaultEncoding})) {
    $xml2localConverter = Text::Iconv->new($defaultConfig{xmlEncoding},
					   $defaultConfig{defaultEncoding});
  }

  beVerboseN("Your system charset encoding is ".
	     $defaultConfig{defaultEncoding}, 2);
}

# Convert from XML encoding to HTML encoding
sub xml2html{
  if ($optimizeConversion &&
      $defaultConfig{xmlEncoding} eq $defaultConfig{htmlEncoding}){
    return shift;
  }
  return $xml2htmlConverter->convert(shift);
}
# Convert from HTML encoding to XML encoding
sub html2xml{
  if ($optimizeConversion &&
      $defaultConfig{xmlEncoding} eq $defaultConfig{htmlEncoding}){
    return shift;
  }
  return $html2xmlConverter->convert(shift);
}
# Convert from XML encoding to local encoding
sub xml2local{
  if ($optimizeConversion &&
      $defaultConfig{xmlEncoding} eq $defaultConfig{defaultEncoding}){
    return shift;
  }
  return $xml2localConverter->convert(shift);
}
# Convert from local encoding to XML encoding
sub local2html{
  if ($optimizeConversion &&
      $defaultConfig{htmlEncoding} eq $defaultConfig{defaultEncoding}){
    return shift;
  }
  return $local2htmlConverter->convert(shift);
}

##### main #####
sub main{
  my @recursiveImageData;

  # create charset converters
  initConverters();

  # pre process command line args before reading config files
  my $options = preProcessArgs(\%defaultConfig);

  #�read configurations files
  my $defaultConfig = readConfigFile(\%defaultConfig);

  # post process command line args after reading config files
  postProcessArgs($options, $defaultConfig);

  # Create the Apache .htaccess for charset encoding
  write_htaccess($albumdir, $defaultConfig);

  # generate the root directory, do recursive traversal of all subalbums
  my %rootAlbumHash = generateAlbumPages("", \@recursiveImageData,
					 $defaultConfig);

  # and finally create the tree page.
  generateTree($rootAlbumHash{config}, %rootAlbumHash);
}

# Test if a package is installed on the system at run time.
# We use it to test LOCALE::Gettext is here (or else, we don't do any I18N).
sub have_package {
  my $name = shift(@_);
  $name =~ s%::%/%g;
  foreach my $prefix (@INC) {
    if (-f "$prefix/$name.pm") {
      return 1;
    }
  }
  return 0;
}

# return translated string with HTML encoding
sub _ {
  if ($I18N) {
    return local2html(Locale::gettext::gettext(shift));
  }
  return local2html(shift);
}

# return translated string without changing encoding
sub translate {
  if ($I18N) {
    return Locale::gettext::gettext(shift);
  }
  return shift;
}

BEGIN{
  my @done; # list of template styles which have their static dir already copied
  sub write_static_dir{
    my $destDir = shift;
    my $configHash = shift;
    if (grep(/^$configHash->{templateStyle}$/, @done)) {
      return;
    }

    push @done, $configHash->{templateStyle};
    my $staticDir = templateStaticDir($configHash);

    $destDir =~ s%/$%%;
    $destDir .= "/static.".$configHash->{templateStyle};

    if ($staticDir) {
      if (! -d "$destDir") {
	mkdir $destDir, 0755
	  or die("\nCannot create $destDir: $?");
      }
      system("cp", "-R", bsd_glob("$staticDir/*", GLOB_TILDE), "$destDir") == 0
	or die("\nCannot copy $staticDir directory content to $destDir: $?");
    } else {
      beVerboseN("  Cannot find any static template directory.", 4);
    }
  }
}


sub write_bg_image {
  my $album = shift;
  my $configHash = shift;

  my $staticDir = templateStaticDir($configHash);
  my $destDir = $albumdir;

  $destDir =~ s%/$%%;
  $destDir .= "/static.".$configHash->{templateStyle};

  if (! -d "$destDir") {
    mkdir $destDir, 0755
      or die("\nCannot create $destDir: $?");
  }
  system("cp", "-p", "$picdir$album$configHash->{backgroundImage}", "$destDir") == 0
    or die("\nCannot copy file $configHash->{backgroundImage} to $destDir: $?");
}

sub write_custom_css {
  my $album = shift;
  my $configHash = shift;

  my $staticDir = templateStaticDir($configHash);
  my $destDir = $albumdir;

  $destDir =~ s%/$%%;
  $destDir .= "/static.".$configHash->{templateStyle};

  if (! -d "$destDir") {
    mkdir $destDir, 0755
      or die("\nCannot create $destDir: $?");
  }
  system("cp", "-p", "$picdir$album$configHash->{customStyleSheet}",
	 "$destDir") == 0
	   or die("\nCannot copy file $configHash->{customStyleSheet} to $destDir: $?");
}

sub write_htaccess{
  my $dir = shift;
  my $configHash = shift;

  if (! $configHash->{createHtaccess}) {
    return
  }

  my $file = $dir."/.htaccess";

  mkdir("$dir", 0755) if (! -d "$dir");

# 20030422 Hack by Yves Mettier <ymettier@libertysurf.fr>
# don't overwrite .htaccess if it is already OK
  if(open(FILE, $file)) {
    my $encoding = $configHash->{htmlEncoding};
    while(<FILE>) {
      if(/AddDefaultCharset $encoding/) {
        close (FILE) || die ("can't close $file ($!)");
	return;
      }
      if(/AddType text\/html;charset=$encoding html htm/) {
        close (FILE) || die ("can't close $file ($!)");
	return;
      }
    }
    close (FILE) || die ("can't close $file ($!)");
  }
# End of hack

  beVerboseN("Writing .htaccess file for album with ".
	"charset encoding $configHash->{htmlEncoding}.", 2);

  open(FILE, ">>",$file)
    or die("Cannot write to file $file ($!)");
  printf(FILE "AddDefaultCharset %s\n",
	 ($configHash->{htmlEncoding}));
  close (FILE) || die ("can't close $file ($!)");
}

sub generateTree{
    # album hash -- other entries as defined in getAlbumInfo
    # hash{subalbums}  -- returns a ref to a list of subalbums to the album
    # each entry in the list is a ref to an albumhash of this format
    my ($configHash, %albumHash) = @_;
    my $tableHTML = "\n".&generateTreeUL(%albumHash)."\n";
    my %subsHash;
    $subsHash{TREE_TABLE} = $tableHTML;
    $subsHash{TREE_LOOP} = generateTreeLoop(%albumHash);

    #beVerboseN("Generate tree Table html:\n $tableHTML ", 3);
    $subsHash{STATIC_PATH} = "static.".$configHash->{templateStyle};
    if ($configHash->{backgroundImage}) {
	# Do not set this if not configured, so that template
	# can check for whether defined.
	$subsHash{BG_IMAGE} =
	    $subsHash{STATIC_PATH}."/".$configHash->{backgroundImage};
    }
    $subsHash{CUSTOM_CSS} = $configHash->{customStyleSheet};
    $subsHash{HOME_LINK} = $configHash->{homeURL};
    $subsHash{ALBUM_THUMB} = $configHash->{treePreview};
    $subsHash{PATH_SHOW_ICON} = $configHash->{pathShowIcon};

    renderTemplate("tree", $albumdir."tree.html",
		 \%subsHash, $configHash);
}

sub generateTreeUL{
    my %albumHash = @_;
    my $link = "<a href=\"".$albumHash{link}."\">".
      $albumHash{title}."</a>";

    my $UL = "<ul>\n<li>$link\n";
    my @subAlbumHashRefList = @{$albumHash{subalbums}}; # may be empty
    my $subAlbumHashRef;
    foreach $subAlbumHashRef (@subAlbumHashRefList) {
	$UL .=  &generateTreeUL(%{$subAlbumHashRef})."\n";	
    }
    $UL .= "</li></ul>\n";
    return $UL;
}

sub generateTreeLoop {
    my(%albumHash) = @_;
    my @result;
    my @subAlbumHashRefList = @{$albumHash{subalbums}}; # may be empty
    my $subAlbumHashRef;
    my($prepath, $preimage, $preimageTmp, $preimagePath, $preid);
    my $hasChild;

    $prepath = $albumHash{link};
    $preimage = filenameToPreviewName($albumHash{sampleimage});
    $prepath =~ s/index.html//;
    $prepath =~ s/^\///;
    if ($prepath eq "") {
        $preimage =~ s/^[^\/]+//;
    }
    $preimageTmp = $preimage;
    $preimageTmp =~ s/\/([^\/]+)$//;
    $preimagePath = $preimageTmp;
    while (rindex($prepath, $preimageTmp) == -1 && $preimageTmp ne "") {
    	if (index($preimageTmp, "/") > -1) {
            $preimageTmp =~ s/\/[^\/]+$//;
	} else {
            $preimageTmp =~ s/^[^\/]+$//;
	}
    }
    $preimage = substr($preimage, length($preimageTmp) + 1);

    $preid = "ID$prepath";
    $preid =~ s/\//_/g;
    push(@result, {TREE_NAME => $albumHash{title},
                   TREE_LINK => $albumHash{link},
	    	   TREE_IMAGES => "$albumHash{numImages}&nbsp;"._("images"),
		   TREE_SAMPLE => $prepath . $preimage,
	    	   TREE_ALT => _(""),
		   TREE_SAMPLEID => $preid,
		   TREE_HASCHILD => @subAlbumHashRefList > 0,
                  });
    if (@subAlbumHashRefList) {
	foreach $subAlbumHashRef (@subAlbumHashRefList) {
	    my $array = generateTreeLoop(%{$subAlbumHashRef});
	    push(@result, @$array);
	}
	push(@result, {TREE_OUT => "1"});
    }

    return \@result;
}

sub usage {
    my $commandname = $0;
    $commandname =~ s/^.*\///;
    print <<EoFprint
$commandname [options] source_dir target_dir
options are:
  -f config_file    use an alternative configuration file, instead of
                    ~/.bins/binsrc

  -o [STR]          Tells script to use only one copy of image using
                    html size specs (height, width specs in the image
                    tag) for scaled versions (instead of generating
                    several images, one for each size).
                    Default is false.
                    STR is an optional argument to set how the one image
                    is generated.
                    Possible values:
                    "scaled" (make scaled copy of orig in target_dir
                      hierarchy, sized to max size). Default.
                    "copied" (copy orig to web dir)
                    "custom" (use copy if filesize < 1 Mb
                      resize, resave, if bigger than 1 Mb)

  -d STR            Determine if tags found in Exif structure are
                    added in desc files.
		    STR is one of "always", "never" or "exist"
                    ("exist" only adds if the desc file already exist.)
                    Default is always.

  -c color_style    Color style to use. Can be blue (default one), green,
                    ivory and pink or any other one defined in config/desc
                    files.

  -s style          Template style to use (the only styles provided for now
                    are 'default' 'joi' and 'satyap').

  -t template_dir   Specify location of html templates.
                    Default is ~/.bins, falling back
                    to default versions in /usr/pkg/etc/bins/templates."style".

  -p                Numbers preceding the album title, followed by 
                    an underscore are stripped.
                    If this option is given, then prefix ordering
                    numbers on directories are removed.  For example,
                    if one has directories may, june, and august, they
                    can be renamed 0_may, 1_june, and 2_august and
                    they will appear in the album in the correct
                    order.

  -r type           Reverse sorting order. 'type' can be 'dirs', 'pictures'
                    or 'dirs,pictures'

  -e                Tells the script to generate an editable version
                    of the album.  If set, some more links and icons
                    are added to directly access the .xml files for
                    editing. This is for editing purposes, not for a
                    final album.

  -i iKey,iKey,...  Sets "ignore" keywords which will be compared against
                    the contents of the "ignore" field of the album's
                    XML file, in the <ignore> fields in <description>
                    section.  If any of the iKeys match those in the
                    album's "ignore" field, that album will not be
                    processed. See also the ignore parameter.

  -n iKey,iKey,...  Sets "hidden" keywords which will be compared against
                    the contents of the "ignore" field of the album's
                    XML file, in the <ignore> fields in <description>
                    section.  If any of the iKeys match those in the
                    album's "ignore" field, that album will not be
                    linked anywhere. See also the hidden parameter.

  -v X              X is the verbosity level (between 0 and 3)

  -h                print this help message
EoFprint
}

sub min {
    my($a,$b) = @_;
    if ($a < $b) { return($a); }
    else { return($b); }
}

sub fileSize {
    my($item) = shift(@_);
    #print("filesize of $item\n");
    my($filesize) = ((-s "$item") / 1024); #get the file's size in KB.
    if ($filesize > 1024) {	# is it larger than a MB?
	$filesize = ($filesize / 1024);
	$filesize =~ s/(\d+[\.,]\d)\d+/$1/;
	$filesize = $filesize._("MB");
    } else {
	$filesize =~ s/(\d+)[\.,]\d+/$1/;
	$filesize = $filesize._("KB");
    }
    return $filesize;
}

# $album is path of album, minus $picdir.
# @parentDirNames is list of parent dirs, not including this one
#   for example if album = /album/may_13_2000/party_pics/
#   then dirs might be (Album, May 13th, 2000, Party Pictures)
#   @parentDirNames is used for generating the back links in the path
sub generateAlbumPages{
  my ($album, $recursiveImageData, $configHash, @parentDirNames) = @_;

  #print "-------------------\n".Dumper($configHash)."\n";

  my $oldBackground = $configHash->{backgroundImage};
  my $oldCss = $configHash->{customStyleSheet};
  my $bgchange = 0;
  my $albumHashRef;
  ($albumHashRef, $configHash) = getAlbumInfo($album, $configHash);
  my %albumHash = %{$albumHashRef};
  $albumHash{config} = $configHash;

  # Don't generate anything else (recurse any further) if we want to ignore
  # this album
  if ( ignoreSet($albumHash{ignore}, $album, $configHash) ) {
    return(%albumHash);
  }

  # create the directory containing static elements (icons,
  # javascript, css, ...)
  write_static_dir($albumdir, $configHash);

  # Check if a new backgroundimage has to be copied
  if (   $configHash->{backgroundImage} ne ""
      && $oldBackground ne $configHash->{backgroundImage}) {
    write_bg_image($album, $configHash);
    $bgchange = 1;
  }

  # Check whether a new stylesheet has to be copied
  if (   $configHash->{customStyleSheet} ne ""
      && $oldCss ne $configHash->{customStyleSheet}) {
    write_custom_css($album, $configHash);
  }

  push(@parentDirNames, $albumHash{title});
  $albumHash{parentDirNames} = \@parentDirNames;

    # albumHash info is complete, except for list of subalbums
    # which is complete after recursive traversal
  my @subalbumHashList;		# goes into albumHash
  #print "generateAlbumPages($album)\n";
  if ($verbose >=1) {
    print xml2local($_)." > " foreach (@parentDirNames);
    print "\n";
  }

  # first, make sure web directory exists
  # use mkdir -p to make parents as needed
  mkdir("$albumdir$album", 0755) if (! -d "$albumdir$album");

  # returns the names of _all_ files/directories in this album's directory
  opendir(DIR, "$picdir$album") || die "can't open dir $picdir$album: $!";
  my @filesInAlbum = grep { !/^\./ } readdir(DIR);
  closedir DIR;

  my @tmpDirs;
  my @tmpFiles;
  foreach my $file (@filesInAlbum) {
      if (-d "$picdir$album$file") {
	  push(@tmpDirs, $file);
      } else {
	  push(@tmpFiles, $file);
      }
  }

  # Exclude files if needed
  @tmpFiles = grep(!/$configHash->{excludeFiles}/, @tmpFiles)
    if ($configHash->{excludeFiles});

  if (   $bgchange
      && $configHash->{backgroundImage}
      && $configHash->{excludeBackgroundImage}) {
    @tmpFiles = grep(!/$configHash->{backgroundImage}/, @tmpFiles);
  }
  @tmpDirs = grep(!/$configHash->{excludeDirs}/, @tmpDirs)
    if ($configHash->{excludeDirs});

  # Now put them in new sorted order back to @filesInAlbum
  if ($configHash->{reverseOrder} & 1) {
    @filesInAlbum = sort {$b cmp $a} @tmpDirs;
  } else {
    @filesInAlbum = sort @tmpDirs;
  }

  #
  # smr Jan 2004 -- sort according to file album.list (if there)
  #
  # first show all images in album.list in the order they appear in
  # this file any image names preceeded with a . are suppressed for
  # the album generation all images in the directory which are not in
  # album.list are appended in usual (sorted) order
  #
  if (-r "$picdir$album/album.list") {
    my(%isfile);
    foreach(@tmpFiles) { $isfile{$_} = 1; }
    open (INLIST, "$picdir$album/album.list") or die "can't open $picdir$album/album.list, $!";

    while(<INLIST>) {
      chomp; s/^\s+//; s/\s+$//;
      next if /^#/ || /^$/;
      if(/^\./) {
        s/^\.\s*//;
      } else {
        push(@filesInAlbum, $_) if $isfile{$_};
      }
      $isfile{$_} = 0;
    }
    close INLIST;
    $#tmpFiles = -1;
    foreach (keys %isfile) {
      push(@tmpFiles, $_) if $isfile{$_} == 1;
    }
  }

  if ($configHash->{reverseOrder} & 2) {
    push(@filesInAlbum, sort {$b cmp $a} @tmpFiles);
  } else {
    push(@filesInAlbum, sort @tmpFiles);
  }

  my($fileInAlbum, @urlimageList, @imageList, @xlinkList, $numAlbums);
  $numAlbums = 0;
  foreach $fileInAlbum (@filesInAlbum) {
    if (-d "$picdir$album$fileInAlbum") { # Is this a subdirectory?
      my %localAlbumHash =
	generateAlbumPages($album.$fileInAlbum."/", $recursiveImageData,
			   $configHash, @parentDirNames);

      # If the "ignore" keyword matches one of the ones passed in the
      # command line, don't push or count this album.
      if (! ignoreAndHiddenSet($localAlbumHash{ignore}, $fileInAlbum,
			       $configHash) ) {
	push(@subalbumHashList, \%localAlbumHash);
	$numAlbums++;
      }
    } else {
      my $known = 0;
      foreach my $ext (@knownImageExtentions){
	if ($fileInAlbum =~ /\.$ext\Z/i) {
	  $known = 1;
	  last;
	}
      }
      if ($known) {
	# this is a known image format--remember its name	
	push @imageList, $fileInAlbum;
      } else {
        foreach my $ext (@filesToLinkExtensions){
          if ($fileInAlbum =~ /\.$ext\Z/i) {
            $known = 1;
            last;
          }
        }
        if ($known) {
          push @xlinkList, $fileInAlbum;
          my $from="$picdir$album$fileInAlbum";
          my $to="$albumdir$album$fileInAlbum";
          if ( ! -f $to ) {
            `cp -p "$from" "$to"`;
          }
        }
      }
    }
  }

  #get virtual images to include
  my @virtualInclude = getVirtualInclude($album);
  #print "$_, " foreach (@virtualInclude);
  push(@imageList, @virtualInclude);
  $albumHash{subalbums} = \@subalbumHashList;
  $albumHash{numImages} = $#imageList+1;     # note that if (! @imageList),
  # $#imageList = -1
  $albumHash{numSubAlbums} = $numAlbums;
  $albumHash{numXLinks} = $#xlinkList + 1;

  # decide whether index page is first thumbnail page (or subalbum page)
  my $firstIsIndex;
  if ($numAlbums == 0 ) {
    $firstIsIndex = 1;
  } else {
    $firstIsIndex = 0;
  }
  $albumHash{thumbIsIndex} = $firstIsIndex;


  # generate image pages and get image data
  my @imageData = generateImagesInAlbum($album, \%albumHash, $firstIsIndex,
					 $configHash, @imageList);

  $albumHash{sampleimage} = chooseSampleImage(\%albumHash, \@imageData)
    if (! $albumHash{sampleimage});

  # generate thumbnail pages
  generateThumbnailPages($album, \%albumHash,
			 $firstIsIndex, $configHash, \@xlinkList, @imageData);

  # generate image list page
  if ($albumHash{numImages} > 0) {
    generateImageListPage($album, \%albumHash, \@imageData, \@xlinkList,
			  $configHash);
  }
  beVerboseN("  sample image for $album is $albumHash{sampleimage}", 3);

  # generate subalbum page
  if ($numAlbums > 0) {
    generateSubAlbumPage($album, \%albumHash, $recursiveImageData,
			 $configHash);
  }


  if ($configHash->{allThumbnailsPage}) {
    # Munge the @imageData so that it includes path information, and dump this
    # in @recursiveImageData.
    for my $thisImageData ( @imageData ) {
      $thisImageData->{'thumblink'}=$album.$thisImageData->{'thumblink'};
      for my $width ( 0..$#{$configHash->{scaledWidths}} ) {
	$thisImageData->{$width}{'htmlFile'}=$album.
	  $thisImageData->{$width}{'htmlFile'};
      }
      push(@{$recursiveImageData}, $thisImageData);
    }
  }

  return %albumHash;
}

# if we don't have a sample image from the initial getAlbumInfo
# then we pick the first one. If we don't have images in this
# album, choose a sampleimage from a subalbum.  If that fails,
# then no sampleimage.
# returns name of sampleimage (a file with path info)
sub chooseSampleImage{
    my ( $albumHashRef, $imageDataRef) = @_;
    my @imageData = @{ $imageDataRef };

    if (@imageData) {
	my $th = $imageData[0]->{thumblink};
	$th =~ s/_pre\.jpg$/.jpg/;
	# print "Returning \$imageData[0] = $th\n";
        return uri_escape($albumHashRef->{dirname})."/".$th;
    }else{
	my @subAlbumHashList = @{ $albumHashRef->{subalbums} };
	my $subAlbumHashRef;
	foreach $subAlbumHashRef (@subAlbumHashList) {
	    if ($subAlbumHashRef->{sampleimage}) {
		#print "Returning \$subAlbumHashRef->{sampleimage} = ".
                #      "$subAlbumHashRef->{sampleimage}\n";
		return uri_escape($albumHashRef->{dirname})."/".
		  $subAlbumHashRef->{sampleimage};
	    }
	}
    }
    return "";
}

# takes in an image name (with some or no path info), and returns
# preview name with equivalent path info
sub filenameToPreviewName {
    my $imageName = shift(@_);
    my($base, $path, $type);
    ($base,$path,$type) = fileparse($imageName, '\.[^.]+\z');
    # what the thumbnail will be named:
    my($newPreviewName) = $path.$base . '_pre.jpg';
    beVerboseN("Preview name for $imageName is $newPreviewName", 3);
    return $newPreviewName;
}

sub getVirtualInclude{
    my $album = shift(@_);
    my $virtualImageFile = $picdir.$album."include_images.txt";
    return () if (! -e $virtualImageFile);
    open (INCLUDE, $virtualImageFile) ||
	die ("cannot open $virtualImageFile for reading: ($!)");
    my @include;
  LINE: while (<INCLUDE>) {
      chomp;
      next LINE if /^#/; #discard comments
      next LINE if /^\s*$/; #ignore total whitespace
      push(@include, $_);
  }
    close (INCLUDE) || die ("can't close $virtualImageFile  ($!)");
    return @include;
}

# return tree and path links of album as a list of hash refs
sub pathLinks{
  my ($album, $title, @dirs) = @_;

  my @result;

  push @result, {PATH_NAME => _("tree"),
		 PATH_TITLE => _("Tree of all albums and sub-albums"),
		 PATH_LINK => getRootDir($album)."tree.html",
		};

  my ($i, $pathlinks);

  my $count = $#dirs-1;
  $count++ if ($title);
  for my $i (0..$count) {
    my $url="";
    my $dirname = $dirs[$i];
    #$dirname=~ s/_/ /g;
    $url .="../" foreach ($i..($#dirs-1));
    $url .= "index.html";	

    push @result, {PATH_NAME => $dirname,
		   PATH_TITLE => $dirname,
		   PATH_LINK => $url,
		   PATH_ISALBUM => 1,
		   PATH_FIRST => ($i == 0),
		  };
  }
  my $dirname = $dirs[$#dirs];
  if (!$title) {
    push @result, {PATH_NAME => $dirname,
		   PATH_ISALBUM => 1,
		   PATH_FIRST => ($#dirs == 0),
		  };
  }else{ #for image page
    push @result, {PATH_NAME => $title,
		  };
  }
  return \@result;
}

# crntPage is "subalbum", "thumb0", ... etc
# values is thumb0, or subalbum, never index
# return a list or hash refs
sub navBarLinks{
    my ($crntPage, $album, $configHash, %albumHash) = @_;

    my @result;

    my $firstIsIndex = $albumHash{thumbIsIndex};
    my $numImages = $albumHash{numImages};

    # <!--navBarRows-->
    my $navrows;
    my $numThumbPages  = calcNumThumbPages($numImages, $configHash);

    #subalbum link
    if ($crntPage eq "subalbum") { # we have a subalbum page (duh)
      push @result, {NAV_NAME => _("Sub Albums"),
                     NAV_ICON => "subalbum.png"};
    } elsif (! $firstIsIndex) { # we have a subalbum page -- it must be
                                # index.html
      push @result, {NAV_NAME => _("Sub Albums"),
		     NAV_LINK => "index.html",
                     NAV_ICON => "subalbum.png",
		     NAV_ID => "sub"};
    }

    #image list link
    if ($crntPage eq "imagelist") {
      push @result, {NAV_NAME => _("Image List"),
                     NAV_ICON => "imagelist.png"};
    } elsif ($numImages > 0) {
      push @result, {NAV_NAME => _("Image List"),
		     NAV_LINK => "imagelist.html",
                     NAV_ICON => "imagelist.png",
		     NAV_ID => "imgl"};
    }

    #if ($crntPage ne "image") {
      # first thumbnail page
      if ($numThumbPages > 0) { # we have a first thumbnail page
          my($thumbpage);

	  if ($numThumbPages == 1) {
	    $thumbpage = _("Thumbnail Page");
	  } else {
	    $thumbpage = _("Thumbnail Page 1");
	  }
          if ($crntPage eq "thumb0") { # and we are on it
            push @result, {NAV_NAME => $thumbpage,
                           NAV_ICON => "thumbnails.png"};
          } elsif ($firstIsIndex) { # and it is index.html
            push @result, {NAV_NAME => $thumbpage,
          		 NAV_LINK => "index.html",
                           NAV_ICON => "thumbnails.png",
			   NAV_ID => "th0"};
          } elsif ($numThumbPages > 0) { # it is thumb0.html
            push @result, {NAV_NAME => $thumbpage,
          		 NAV_LINK => "thumb0.html",
                           NAV_ICON => "thumbnails.png",
			   NAV_ID => "th0"};
          }
      }

      # remaining thumbnail pages
      my $i;
      for $i (1..($numThumbPages-1)) {
          if ($crntPage eq "thumb$i") {
            push @result, {NAV_NAME => _("Thumbnail Page") . "&nbsp;". ($i+1),
                           NAV_ICON => "thumbnails.png"};
          }else{
            push @result, {NAV_NAME => _("Thumbnail Page") . "&nbsp;". ($i+1),
          		 NAV_LINK => "thumb".$i.".html",
                           NAV_ICON => "thumbnails.png",
			   NAV_ID => "th$i"};
          }
      }
    #}

    # all thumbnail page
    # If we have more than one thumbnail page (this is a thumbnail page), or
    # this is the main page (subalbum page), or this is "thumb-2" (a subalbum
    # thumb page)...
    if ( $configHash->{allThumbnailsPage} &&
	 (($numThumbPages > 1) || ($crntPage eq "subalbum")
	  || ($crntPage eq "thumb-2") )) {
      if ($crntPage eq "thumb-1" || $crntPage eq "thumb-2" ) { # we are on it
	push @result, {NAV_NAME => _("All Thumbnails")};
      } else { # we are not on an allthumbnails page
	push @result, {NAV_NAME => _("All Thumbnails"),
		       NAV_LINK => "allthumbs.html"};
      }
    }
    return \@result;
}



sub getAlbumNumInfo{
    my (%albumHash) = @_;

    my $numInfo="";
    my $numImages = $albumHash{numImages};
    my $numAlbums = $albumHash{numSubAlbums};
    my $numXLinks = $albumHash{numXLinks};

    if ($numImages == 1) {
	$numInfo = "1&nbsp;"._("image");
    } elsif ($numImages  > 1) {
	$numInfo = "$numImages&nbsp;"._("images");
    }
    $numInfo .= ", " if (length $numInfo > 0 && $numXLinks > 0);
    if ($numXLinks == 1) {
	$numInfo .= "1&nbsp;"._("media file");
    } elsif ($numXLinks  > 1) {
	$numInfo .= "$numXLinks&nbsp;"._("media files");
    }
    $numInfo .= ", " if (length $numInfo > 0 && $numAlbums > 0);
    if ($numAlbums == 1) {
	$numInfo .= "1&nbsp;"._("subalbum");
    } elsif ($numAlbums > 1) {
	$numInfo .= "$numAlbums&nbsp;"._("subalbums");
    }
    return $numInfo;
}

sub generateImageListPage{
    my ($album, $albumHashRef, $imageDataRef, $xlinksRef, $configHash) = @_;
    my %albumHash = %{$albumHashRef};
    my @imageData = @{$imageDataRef};
    my $pwd;

    # hash for final substitutions
    my %finalsubs;
    $finalsubs{NUM_INFO} = getAlbumNumInfo(%albumHash);
    $finalsubs{ALBUM_TITLE} = $albumHash{title};
    $finalsubs{NAV_BAR_TABLE} =
      navBarLinks('imagelist', $album, $configHash, %albumHash);
    $finalsubs{ALBUM_PATH_LINKS} =
      pathLinks($album, 0, @{$albumHash{parentDirNames}});

    $finalsubs{TREE_NAME} = _("tree");
    $finalsubs{TREE_TITLE} = _("Tree of all albums and sub-albums");
    $finalsubs{TREE_LINK} = getRootDir($album)."tree.html";

    if ($#{$albumHash{parentDirNames}} > 0) {
      $finalsubs{UP_NAME} = _("up");
      $finalsubs{UP_TITLE} = _("Up one subalbum");
      $finalsubs{UP_LINK} = "../index.html";
    }

    if ($genEditableAlbum) {
      $pwd = `pwd`;
      chop($pwd);
      $pwd = uri_escape($pwd, '^-A-Za-z0-9/_\.');
      $finalsubs{ALBUM_DESC} = "<a href=\"file://";
      if ($albumHash{descFileName} =~ "^[^/]"){
	$finalsubs{ALBUM_DESC} .= $pwd."/";
      }
      $finalsubs{ALBUM_DESC} .= $albumHash{descFileName}."\">".
	$albumHash{longdesc}."</a>";
    } else {
      $finalsubs{ALBUM_DESC} = $albumHash{longdesc};
    }
    my @tables;
    my $thumbid = 0;
    foreach my $imageInfoRef (@imageData) {
	my %imageInfo = %{$imageInfoRef};
	my %tablesubs;
	my @sizes;

	$thumbid += 1;
        $tablesubs{THUMB_ID} = $thumbid;
	for my $i (0..$imageInfo{'maxSize'}) {
	  if ($i == $configHash->{defaultSize}) {
	    push @sizes , {SIZE_LINK => $imageInfo{$i}{'htmlFile'},
			   SIZE_NAME => $imageInfoRef->{configuration}{sizeNames}[$i],
			   SIZE_TITLE => $imageInfoRef->{configuration}{longSizeNames}[$i].
			   " ($imageInfo{$i}{'width'}x$imageInfo{$i}{'height'})",
			   SIZE_FILE => $imageInfoRef->{configuration}{fileSizeNames}[$i],
			   SIZE_DFLT => 1,
			  };
	  } else {
	    push @sizes , {SIZE_LINK => $imageInfo{$i}{'htmlFile'},
			   SIZE_NAME => $imageInfoRef->{configuration}{sizeNames}[$i],
			   SIZE_TITLE => $imageInfoRef->{configuration}{longSizeNames}[$i].
			   " ($imageInfo{$i}{'width'}x$imageInfo{$i}{'height'})",
			   SIZE_FILE => $imageInfoRef->{configuration}{fileSizeNames}[$i],
			  };
	  }
	}
	if ($genEditableAlbum) {
	  $tablesubs{TITLE} = "<a href=\"file://";
	  if ($imageInfo{descFileName} =~ "^[^/]"){
	    $tablesubs{TITLE} .= $pwd."/";
	  }
	  $tablesubs{TITLE} .= $imageInfo{descFileName}."\">".
	    $imageInfo{title}."</a>";
	} else {
	  $tablesubs{TITLE} = $imageInfo{title};
	}
	$tablesubs{SIZE_LINKS} = \@sizes;

	my $atLeastOneField = 0;
	my $tagValue;

	$tablesubs{DESC_TABLE} = getDescTable(%imageInfo);

	if ($configHash->{defaultSize} >= 0){
	  $tablesubs{THUMB_DEFAULT_SIZE} =
	    $imageInfo{$configHash->{defaultSize}}{'htmlFile'};
	}
	if ($configHash->{thumbnailInImageList}) {
	  %tablesubs = ((THUMB_LINK => $imageInfo{'thumblink'},
			 THUMB_WIDTH => $imageInfo{'twidth'},
			 THUMB_HEIGHT => $imageInfo{'theight'},
			 THUMB_ALT => _("Click on one of the size names above to enlarge this image"),
			 THUMB_LINK_TITLE => _("Click on one of the size names above to enlarge this image"),
			), %tablesubs);
	}
	push @tables, \%tablesubs;
      }

    $finalsubs{XLINK} = getXLinks($xlinksRef);
    $finalsubs{STATIC_PATH} = 
      getRootDir($album)."static.".$configHash->{templateStyle};
    if ($configHash->{backgroundImage}) {
	# Do not set this if not configured, so that template
	# can check for whether defined.
	$finalsubs{BG_IMAGE} =
	    $finalsubs{STATIC_PATH}."/".$configHash->{backgroundImage};
    }
    $finalsubs{CUSTOM_CSS} = $configHash->{customStyleSheet};
    $finalsubs{ROOT_PATH} = getRootDir($album);

    $finalsubs{IMAGE_LIST_TABLE} = \@tables;
    $finalsubs{HOME_LINK} = $configHash->{homeURL};
    $finalsubs{FEEDBACK_LINK} = $configHash->{feedbackMail};
    $finalsubs{PATH_SHOW_ICON} = $configHash->{pathShowIcon};

    renderTemplate("imagelist", $albumdir.$album."imagelist.html",
		 \%finalsubs, $configHash);
}

sub generateSubAlbumPage{
    my ($album, $albumHashRef, $recursiveImageData, $configHash) = @_;
    my $numAlbums = $albumHashRef->{numSubAlbums};
    if (1 || $numAlbums <= $configHash->{maxAlbumsForLongSubAlbum}) {
	generateLongSubAlbumPage($album, $albumHashRef, $configHash);
    }else{
      # Short album page is not is not supported for the moment
	generateShortSubAlbumPage($album, $albumHashRef, $configHash);
    }

    if ($configHash->{allThumbnailsPage}){
      # Create a thumbnail page of all images
      generateThumbPage($album, $albumHashRef, "-2", "1",
			"allthumbs.html", "", "", "", $configHash, undef,
                        undef,
			$recursiveImageData->[0..$#{$recursiveImageData}]);
    }
}

# Short album page is not supported for the moment
sub generateShortSubAlbumPage{
    my ($album, $albumHashRef, $configHash) = @_;
    my %albumHash = %{$albumHashRef};

    my $pwd = `pwd`;
    chop($pwd);
    $pwd = uri_escape($pwd, '^-A-Za-z0-9/_\.');

    # hash for final subsitutions
    my %finalsubs;
    $finalsubs{'<!--numInfo-->'} = getAlbumNumInfo(%albumHash);
    $finalsubs{'<!--albumTitle-->'} = $albumHash{title};
    $finalsubs{'<!--navBarRows-->'} = getNavBarLinks('subalbum',
						     $album, $configHash,
						     %albumHash);
    $finalsubs{'<!--albumPathLinks-->'} = 
      getPathLinks($album, 0, @{$albumHash{parentDirNames}});
    if ($genEditableAlbum) {
      $finalsubs{'<!--albumDesc-->'} = "<a href=\"file://";
      if ($albumHash{descFileName} =~ "^[^/]"){
	$finalsubs{'<!--albumDesc-->'} .= $pwd."/";
      }
      $finalsubs{'<!--albumDesc-->'} .= $albumHash{descFileName}."\">".
	$albumHash{longdesc}."</a>";
    } else {
      $finalsubs{'<!--albumDesc-->'} = $albumHash{longdesc};
    }

    my $titleRowText =
	'<tr>
           <td colspan="2"><a href="<!--link-->"><b><!--title--></b></a></td>
           <td align="right"> &nbsp; <!--numinfo--></td>
         </tr>';

    my $descRowText =
	'<tr>
            <td> &nbsp; &nbsp; </td>
	    <td valign="top"><!--desc--></td>
        </tr>';

    my $subalbumHashRef;
    my $tableData = '<table border="0" cellpadding="3" >';
    my @subalbumHashList = @{$albumHash{subalbums}};
    for $subalbumHashRef (@subalbumHashList) {
	my %subshash;
	my %albuminfo = %{$subalbumHashRef};
	$subshash{'<!--numinfo-->'} = &getAlbumNumInfo(%albuminfo);
	$subshash{'<!--title-->'} = $albuminfo{title};
	$subshash{'<!--link-->'} = uri_escape($albuminfo{dirname}).
	  "/index.html";
	if ($genEditableAlbum) {
	  $subshash{'<!--desc-->'} = "<a href=\"$pwd/$albuminfo{descFileName}\">$albuminfo{shortdesc}</a>";
	} else {
	  $subshash{'<!--desc-->'} = $albuminfo{shortdesc};
	}


	$tableData .= doSubstitutions($titleRowText, $configHash, %subshash);
	$tableData .= doSubstitutions($descRowText, $configHash, %subshash)
	    if (! $subshash{'<!--desc-->'} =~
		/No short description available/);
	

	#print "table after SUBS\n: $tableData\n";w
    }
    $tableData .=  '</table>';

    $finalsubs{'<!--subalbumtable-->'} = $tableData;
    my $pageContent = openTemplate("subalbum");
    $pageContent = doSubstitutions($pageContent, $configHash, %finalsubs);
    renderPage($albumdir.$album."index.html", $pageContent);
}


sub generateLongSubAlbumPage{
    my ($album, $albumHashRef, $configHash) = @_;
    my %albumHash = %{$albumHashRef};

    # hash for final subsitutions
    my %templateParameters =
      ( NUM_INFO => getAlbumNumInfo(%albumHash),
	ALBUM_TITLE => $albumHash{title},
	NAV_BAR_TABLE => navBarLinks('subalbum',
				    $album, $configHash, %albumHash),
	ALBUM_PATH_LINKS => pathLinks($album, 0,
				      @{$albumHash{parentDirNames}}),
	TREE_NAME =>  _("tree"),
        TREE_TITLE => _("Tree of all albums and sub-albums"),
        TREE_LINK => getRootDir($album)."tree.html",
      );

    if ($configHash->{albumThumbInSubAlbumPage} && $albumHash{numImages} > 0) {
      my $sampleImage = "./".filenameToPreviewName($albumHash{sampleimage});
      if ($albumHash{dirname} ne "") {
        $sampleImage = "../$sampleImage";
      }
      %templateParameters =
	(%templateParameters,
	 ( ALBUM_THUMB => 1,
	   DESC =>     	  $albumHash{shortdesc},
	   TITLE =>    	  $albumHash{title},
	   CURRENT_NUM_INFO =>    ($albumHash{numImages} > 1) ?
	     $albumHash{numImages}."&nbsp;"._("images") : "1&nbsp;"._("image"),
	   LINK =>     	  "thumb0.html",
	   THUMB_ALT => _("Click to view thumbnails of the current album"),
	   THUMB_LINK_TITLE => _("Click to view thumbnails of the current album"),
	   THUMB_LINK =>  $sampleImage,
	 )
	);
    }

    my $pwd;
    if ($genEditableAlbum) {
      $pwd = `pwd`;
      chop($pwd);
      $pwd = uri_escape($pwd, '^-A-Za-z0-9/_\.');

      $templateParameters{ALBUM_DESC} =
	"<a href=\"file://";
      if ($albumHash{descFileName} =~ "^[^/]"){
	$templateParameters{ALBUM_DESC} .= $pwd."/";
      }
      $templateParameters{ALBUM_DESC} .= $albumHash{descFileName}."\">".
	$albumHash{longdesc}."</a>";
    } else {
      $templateParameters{ALBUM_DESC} = $albumHash{longdesc};
    }

    my @subAlbunTable;
    my $cnt = 0;
    for my $subalbumHashRef (@{$albumHash{subalbums}}) {
	my %albuminfo = %{$subalbumHashRef};

	$cnt += 1;

	my %subshash =
	  ( NUM_INFO => getAlbumNumInfo(%albuminfo),
	    TITLE => $albuminfo{title},
	    LINK => uri_escape($albuminfo{dirname},
			       '^-A-Za-z0-9/_\.')."/index.html",
	    THUMB_ALT => _("Click to view this album"),
	    THUMB_LINK_TITLE => _("Click to view this album"),
	    THUMB_LINK => filenameToPreviewName($albuminfo{sampleimage}),
	    THUMB_NUM => $cnt
	  );

	if ($genEditableAlbum) {
	  $subshash{'DESC'} =
	    "<a href=\"file://";
	  if ($albuminfo{descFileName} =~ "^[^/]"){
	    $subshash{'DESC'} .= $pwd."/";
	  }
	  $subshash{'DESC'} .= $albuminfo{descFileName}."\">".
	    $albuminfo{shortdesc}."</a>";
	} else {
	  $subshash{'DESC'} = $albuminfo{shortdesc};
	}
	push @subAlbunTable, \%subshash;
    }

    $templateParameters{SUBALBUMS_TABLE} = \@subAlbunTable;

    if ($#{$albumHash{parentDirNames}} > 0) {
      $templateParameters{UP_NAME} = _("up");
      $templateParameters{UP_TITLE} = _("Up one subalbum");
      $templateParameters{UP_LINK} = "../index.html";
    }
    $templateParameters{STATIC_PATH} =
      getRootDir($album)."static.".$configHash->{templateStyle};
    if ($configHash->{backgroundImage}) {
	# Do not set this if not configured, so that template
	# can check for whether defined.
	$templateParameters{BG_IMAGE} =
	    $templateParameters{STATIC_PATH}."/".$configHash->{backgroundImage};
    }
    $templateParameters{CUSTOM_CSS} = $configHash->{customStyleSheet};

    $templateParameters{HOME_LINK} = $configHash->{homeURL};
    $templateParameters{FEEDBACK_LINK} = $configHash->{feedbackMail};
    $templateParameters{PATH_SHOW_ICON} = $configHash->{pathShowIcon};

    renderTemplate("subalbum", $albumdir.$album."index.html",
		   \%templateParameters, $configHash);
}


sub getAlbumInfo{
    my $album      = shift(@_);
    my $configHash = shift(@_);

    my %albuminfo; # what we want to generate
    $albuminfo{link} = uri_escape($album, '^-A-Za-z0-9/_\.')."index.html";
    #link is from $albumdir -- otherwise need to add getRootDir to make work

    #$album = $picdir.$album;
    # we need this for the root dir to be right --
    # correct behaviour if passed ""

    my $albumdescfile = "$picdir$album"."album.xml";
    if (-e $albumdescfile) {
      $albuminfo{descFileName} = uri_escape($albumdescfile, '^-A-Za-z0-9/_\.');
      $configHash = getDescAlbum($albumdescfile, \%albuminfo, $configHash);

      # Don't calculate this album's info if it is flagged "ignore".
      if ( ignoreSet($albuminfo{ignore}, $album, $configHash) )
	{ return(\%albuminfo); }

      if (exists $albuminfo{sampleimage} && $albuminfo{sampleimage}) {
	#$albuminfo{sampleimage} = uri_escape($albuminfo{sampleimage},
	#				     '^-A-Za-z0-9/_\.');
	#add directory name of album to sample image
	my $dirname = $album;
	$dirname =~ s%^.*/([^/]+)/$%$1/%;
	$albuminfo{sampleimage} = $dirname.$albuminfo{sampleimage};
	$albuminfo{sampleimage} = uri_escape($albuminfo{sampleimage},
                                             '^-A-Za-z0-9/_\.');
      }
    } else {
      $albuminfo{descFileName} = "";
    }
    if (! $albuminfo{shortdesc} && ! $albuminfo{longdesc}) {
      if ($configHash->{emptyAlbumDesc}) {
	$albuminfo{shortdesc} = "";
	$albuminfo{longdesc} = "";
      } else {
	$albuminfo{shortdesc} = _("No short description available");
	$albuminfo{longdesc} = _("No long description available");
      }
    } elsif (! $albuminfo{shortdesc}) {
	$albuminfo{shortdesc} = $albuminfo{longdesc};
    } elsif (! $albuminfo{longdesc}) {
	$albuminfo{longdesc} = $albuminfo{shortdesc};
    }

    #get album title
    my $albumtitle = $picdir.$album;
    # strip ending /
    if ($album ne "") {
      my $endChar = chop($albumtitle);
      $albumtitle = $album if ($endChar ne "/") ;
      $albumtitle =~ s/^.*\///g; #remove path
    } else {
      $albumtitle = ""
    }
    $albuminfo{dirname} = $albumtitle;  #single dir name, without path info

    if (!$albuminfo{title}){
      if ($configHash->{stripDirPrefix}) {
	$albumtitle =~ s/^\d+_//g;	
      }
      $albumtitle =~ s/_/ /g; # replace underscores with spaces
      $albuminfo{title} = local2html($albumtitle) ;
    }
    beVerboseN("title for album $album is $albuminfo{title}.", 3);
    return (\%albuminfo, $configHash);
}

sub calcNumThumbPages{
    my $numImages  = shift (@_);
    my $configHash = shift (@_);
    my $numPages=0;
    if ($numImages % $configHash->{numThumbsPerPage} == 0) {
	$numPages = $numImages/$configHash->{numThumbsPerPage};
    }else{
	$numPages = int($numImages/$configHash->{numThumbsPerPage}) + 1;
    }
    return $numPages;
}

sub getXLinks{
  my $xLinksRef = shift;
  my @xLinks=();
  foreach my $xLink ( @$xLinksRef ) {
    my %row =  ( link => $xLink );
    push(@xLinks, \%row);
  }
  return \@xLinks;
}

sub generateThumbnailPages{
    my ($album, $albumHashRef, $firstIsIndex, $configHash,
          $xlinkListRef,
          @imageData) = @_;
    my @xlinkList=@$xlinkListRef;
    my %albumHash = %{$albumHashRef};
    my $numImages = scalar(@imageData); #element count
    my $numPages = calcNumThumbPages($numImages, $configHash);
    my $crntPage;

    # generate number link for each page
    my @numLink;
    if ($firstIsIndex){
      push @numLink, {NUMBER => "1",
		      NUM_LINK => "index.html"
		     };
    }else{
      push @numLink, {NUMBER => "1",
		      NUM_LINK => "thumb0.html"
		     };
    }
    for($crntPage=1; $crntPage < $numPages; $crntPage++) {
      push @numLink, {NUMBER => ($crntPage+1),
		      NUM_LINK => "thumb".$crntPage.".html"
		     };
    }

    for($crntPage=0; $crntPage < $numPages; $crntPage++) {
	#calculate prev, next thumb page
	# we set prev and next equal to the empty string
        #�if we have only 1 page to
	# to indicate prev and next buttons should not be displayed.
	my ($prev, $next);
	if ($numPages == 1) {
	    $prev = "";
	    $next = "";
	}else{
	    $prev = ($crntPage -1 + $numPages)%$numPages;
	    $prev = "thumb".$prev.".html";	
	    $next = ($crntPage + 1)%$numPages;
	    $next = "thumb".$next.".html";
	    # special cases
	    if ($configHash->{thumbnailPageCycling} == 1) {
	      $next = "index.html"
	        if ($firstIsIndex && ($crntPage == $numPages-1));
	    } else {
	      $prev = "" if ($crntPage == 0);
	      $next = "" if ($crntPage == $numPages-1);
	    }
	    $prev = "index.html" if ($firstIsIndex && ($crntPage == 1));
	}

	#first and last images on page
	my $first = $crntPage*$configHash->{numThumbsPerPage};
	my $last = $first + $configHash->{numThumbsPerPage} - 1;
	$last = $#imageData if ($#imageData <= $last);

	my @preloadImages;
	if ($configHash->{javaScriptPreloadThumbs}) {
	  # get the names of images on the next page to preload them using
	  # javascript
	  my $lastNext = $last + $configHash->{numThumbsPerPage};
	  $lastNext = $#imageData if ($#imageData <= $lastNext);
	  for ( my $i = $last+1 ; $i <= $lastNext ; $i++){
	    my %image;
	    $image{PRELOAD_IMAGE_NB}     = $i-$last;
	    $image{PRELOAD_IMAGE_NAME}   = $imageData[$i]->{'thumblink'};
	    $image{PRELOAD_IMAGE_WIDTH}  = $imageData[$i]->{'twidth'};
	    $image{PRELOAD_IMAGE_HEIGHT} = $imageData[$i]->{'theight'};
	    push @preloadImages, \%image;
	  }
	}
	
	#fileName
	my $filename = "thumb".$crntPage.".html";
	$filename = "index.html" if ($firstIsIndex && ($crntPage == 0));

	# generate sequence of number links
	my $numLinkSeq = dclone(\@numLink);
	$numLinkSeq->[$crntPage]{NUM_LINK} = "";
		
	generateThumbPage($album, $albumHashRef, $crntPage, $numPages,
			  $filename, $prev, $next, $numLinkSeq, $configHash,
			  \@preloadImages, \@xlinkList, @imageData[$first..$last]);
    }
    # Generate an "all images" thumbnail page if more than one page was
    # generated.
    if ( $configHash->{allThumbnailsPage} && $numPages > 1 ) {
	generateThumbPage($album, $albumHashRef, "-1", "1",
			  "allthumbs.html", "", "", "", $configHash,
			  undef, undef, @imageData[0..$#imageData]);
    }
}

sub generateThumbPage{
  my ($album, $albumHashRef, $pageNumber, $numPages, $filename,
      $prevPage, $nextPage, $numberLinks, $configHash, $preloadImages,
      $xlinkListRef, @imageData) = @_;

  my @xlinkList;
  if (!defined($xlinkListRef)) {
    @xlinkList=();
  } else {
    @xlinkList=@$xlinkListRef;
  }

  my %albumHash = %{$albumHashRef};

  my $thumbsOnPage = scalar(@imageData);

  # generate the table containing the thumbnails
  my $crntImage;
  my @thumbTable;
  for ($crntImage = 0; $crntImage<$thumbsOnPage; $crntImage++) {
    #create the row
    my $row = thumbEntry($imageData[$crntImage],
			 $imageData[$crntImage]{configuration});
    $row->{THUMB_ID} = $crntImage;
    # begin new row if needed
    if ( (($crntImage+1) % $configHash->{thumbsPerRow} == 0) &&
	 ($crntImage+1 < $thumbsOnPage)) {
      $row->{THUMB_NEW_LINE} = 1;
    }
    push @thumbTable, $row;
  }

  # set up all the substitutions, first with subs for header
  my %subsHash;
  $subsHash{THUMBS_TABLE} = \@thumbTable;
  $subsHash{PRELOAD_IMAGES} = $preloadImages;
  $subsHash{THUMB_NUMBER_LINKS} = $numberLinks;
  $subsHash{NUM_INFO} = getAlbumNumInfo(%albumHash);
  $subsHash{ALBUM_TITLE} = $albumHash{title};
  $subsHash{NAV_BAR_TABLE} =
    navBarLinks("thumb$pageNumber", $album, $configHash, %albumHash);
  $subsHash{ALBUM_PATH_LINKS} =
    pathLinks($album, 0, @{$albumHash{parentDirNames}});

  $subsHash{TREE_NAME} = _("tree");
  $subsHash{TREE_TITLE} = _("Tree of all albums and sub-albums");
  $subsHash{TREE_LINK} = getRootDir($album)."tree.html";

  if ($nextPage eq "" and $prevPage eq "") {
    $subsHash{MULTIPLE_PAGES} = 0;
  } else {
    $subsHash{MULTIPLE_PAGES} = 1;
  }

  if ($#{$albumHash{parentDirNames}} > 0) {
    $subsHash{UP_NAME} = _("up");
    $subsHash{UP_TITLE} = _("Up one subalbum");
    $subsHash{UP_LINK} = "../index.html";
  }

  if ($genEditableAlbum) {
    $subsHash{ALBUM_DESC} =
      "<a href=\"file://";
    if ($albumHash{descFileName} =~ "^[^/]") {
      my $pwd = `pwd`;
      chop($pwd);
      $pwd = uri_escape($pwd, '^-A-Za-z0-9/_\.');
      $subsHash{ALBUM_DESC} .= $pwd."/";
    }
    $subsHash{ALBUM_DESC} .= $albumHash{descFileName}."\">".
      $albumHash{longdesc}."</a>";
  } else {
    $subsHash{ALBUM_DESC} = $albumHash{longdesc};
  }

  if ($nextPage) {
    $subsHash{NEXT_THUMB_PAGE} = $nextPage;
  }

  if ($prevPage) {
    $subsHash{PREV_THUMB_PAGE} = $prevPage;
  }

  # Correct the special-casing of "-1" and "-2" to mean this is an
  # "allthumbs" page.
  if ( $pageNumber=="-1" || $pageNumber=="-2" ) {
    $pageNumber="0";
  }

  my $pagenumber_string = $pageNumber+1;
  $pagenumber_string .= "/$numPages";
  $subsHash{THUMB_PAGE_NUMBER} = $pagenumber_string;

  $subsHash{XLINK} = getXLinks(\@xlinkList);
  $subsHash{STATIC_PATH} = 
    getRootDir($album)."static.".$configHash->{templateStyle};
  if ($configHash->{backgroundImage}) {
      # Do not set this if not configured, so that template
      # can check for whether defined.
      $subsHash{BG_IMAGE} =
	  $subsHash{STATIC_PATH}."/".$configHash->{backgroundImage};
  }
  $subsHash{CUSTOM_CSS} = $configHash->{customStyleSheet};
  $subsHash{HOME_LINK} = $configHash->{homeURL};
  $subsHash{FEEDBACK_LINK} = $configHash->{feedbackMail};
  $subsHash{PATH_SHOW_ICON} = $configHash->{pathShowIcon};
  if ($albumHashRef->{numSubAlbums} == 0){
    $subsHash{FIRST_PAGE} = "index.html";
  }else{
    $subsHash{FIRST_PAGE} = "thumb0.html";
  }
  $subsHash{LAST_PAGE} = "thumb".($numPages-1).".html";

  renderTemplate("thumbnail", $albumdir.$album.$filename,
		 \%subsHash, $configHash);
}


#creates a single entry in the table of thumbnails for the given image
sub thumbEntry{
  my $imageHashRef = shift(@_);
  my $configHash = shift(@_);
  my ($links, $i);

  my %thumb = (THUMB_LINK => $imageHashRef->{'thumblink'},
	       THUMB_WIDTH => $imageHashRef->{'twidth'},
	       THUMB_HEIGHT => $imageHashRef->{'theight'},
	       THUMB_ALT => _("Click on one of the size names below to enlarge this image"),
	       THUMB_LINK_TITLE => _("Click on one of the size names below to enlarge this image"),
	      );
  if ($configHash->{defaultSize} >= 0){
    $thumb{THUMB_DEFAULT_SIZE} =
      $$imageHashRef{$configHash->{defaultSize}}{'htmlFile'};
  }
  if ($configHash->{titleOnThumbnail}) {
    $thumb{THUMB_TITLE} = $$imageHashRef{title};
  }

  if (! $configHash->{thumbnailBackground}){
    $thumb{THUMB_BACKGROUND} =
      $configHash->{colorsSubs}{$configHash->{colorStyle}}{PAGE_BACKCOLOR};
  }else{
    $thumb{THUMB_BACKGROUND} =
      $configHash->{colorsSubs}{$configHash->{colorStyle}}{SUBBAR_BACKCOLOR};
  }

  #print 'for image'.$imageHashRef->{'filename'}.'we have
  #$imageHashRef->{\'maxSize\'} = '.$imageHashRef->{'maxSize'}."\n";

  my @sizes;
  for $i (0..($imageHashRef->{'maxSize'})) {
    if ($i == $configHash->{defaultSize}) {
      push @sizes , {SIZE_LINK => $$imageHashRef{$i}{'htmlFile'},
        	     SIZE_NAME => $configHash->{sizeNames}[$i],
        	     SIZE_TITLE => $configHash->{longSizeNames}[$i].
        	     " ($imageHashRef->{$i}{'width'}x".
        	     "$imageHashRef->{$i}{'height'})",
		     SIZE_FILE => $configHash->{fileSizeNames}[$i],
		     SIZE_DFLT => 1,
        	    };
    } else {
      push @sizes , {SIZE_LINK => $$imageHashRef{$i}{'htmlFile'},
        	     SIZE_NAME => $configHash->{sizeNames}[$i],
        	     SIZE_TITLE => $configHash->{longSizeNames}[$i].
        	     " ($imageHashRef->{$i}{'width'}x".
        	     "$imageHashRef->{$i}{'height'})",
		     SIZE_FILE => $configHash->{fileSizeNames}[$i],
        	    };
    }
  }
  $thumb{THUMB_SIZES} = \@sizes;

  return \%thumb;
}

sub generateSecondaryFieldsPage{
  my $imageInfo = shift;
  my $album = shift;
  my $albumInfo = shift;
  my $imageName = shift;
  my $configHash = shift;
  my $table="";
  my $fileName="";

  my $tagValue;
  my $sectionTitle="";
  my $tableSection="";
  my @sections;
  my @fields;
  my @tmpFields;
  foreach my $tagName (@secondaryFields) {
    if ($tagName =~ m/^BINS-SECTION /) {
      if ($sectionTitle && @tmpFields){
	push (@fields, {SECTION_TITLE => $sectionTitle});
	@fields = (@fields, @tmpFields);
	@tmpFields = ();
	push (@sections, {SECTION_TITLE => $sectionTitle});
      }
      $sectionTitle = $tagName;
      $sectionTitle =~ s/^BINS-SECTION //;
    }else{
      $tagValue = $fields{$tagName};
      if ($imageInfo->{$tagName}) {
	my %row = (FIELD_NAME => $tagValue->{'Name'},
		   FIELD_VALUE => local2html($imageInfo->{$tagName})
		  );
	$row{FIELD_TIP} = $tagValue->{'Tip'} if $tagValue->{'Tip'};
	push (@tmpFields, \%row);
      }
    }
  }

  if (! @fields) {
    return ""
  }

  # on which size of image we go when "back to the image" is clicked and
  # JavaScript is deactivated:
  my $size = $configHash->{defaultSize};
  if ($size == -1){
    $size = 0;
  }

  my %subs_hash;
  $subs_hash{ALBUM_TITLE} = $album;
  $subs_hash{IMAGE_PAGE_LINK} = $imageInfo->{$size}{'htmlFile'};
  $subs_hash{IMAGE_SIZE_NAME} = _("size ").
    $configHash->{longSizeNames}[$size];
  $subs_hash{FILE_NAME} =  $imageName;
  $subs_hash{THUMB_PAGE} = $imageInfo->{'thumbpage'};
  $subs_hash{ALBUM_PATH_LINKS} =
    pathLinks($album, $imageInfo->{'title'},
	      @{$albumInfo->{parentDirNames}});
  $subs_hash{TITLE} = $imageInfo->{'title'};
  $subs_hash{DESC_TABLE} = \@fields;
  $subs_hash{LINKS_TABLE} = \@sections;
  $subs_hash{STATIC_PATH} =
    getRootDir($album)."static.".$configHash->{templateStyle};
  $subs_hash{HOME_LINK} = $configHash->{homeURL};
  $subs_hash{FEEDBACK_LINK} = $configHash->{feedbackMail};
  $subs_hash{PATH_SHOW_ICON} = $configHash->{pathShowIcon};
  $subs_hash{CUSTOM_CSS} = $configHash->{customStyleSheet};

  my @array;
  push @array, {NAV_NAME => $intlSubs{STRING_BACKTOTHEIMAGE},
                NAV_LINK => "javascript:history.back();",
                NAV_ICON => "back.png",
		NAV_ID => "back"};
  # $subs_hash{NAV_BAR_TABLE} = navBarLinks('secondaryFields', $album, $configHash, %albumHash);
  $subs_hash{NAV_BAR_TABLE} = \@array;
  ####

  $fileName = $imageName.".details.html";
  my $outputPage = $albumdir.$album.$fileName;
  renderTemplate("details", $outputPage, \%subs_hash, $configHash);
  #$fileName='<a href="'.uri_escape($fileName).'">'.
  #  _("Additional information on the picture").'</a>';
  $fileName=uri_escape($fileName);
  return $fileName;
}


# copy the source image in the dest album, and eventually, perform a
# rotation
sub copyImage {
  my ($source, $dest, $imageData, $configHash) = @_;
  if ( (! -e "$dest") || (-M "$source" < -M "$dest") ) {
    `cp -p "$source" "$dest"`;
    system("chmod", "a+r", "$dest") == 0
      or die("\nCannot set write permission on $dest: $?");
    if ($configHash->{rotateImages} eq 'destination') {
      # Perform a rotation of the picture if needed
      if (rotateImage($dest, $imageData->{'Orientation'}, "", $configHash)
	  == 1) {
	progressifyImage($dest, "", $configHash);
	# swap width & height
	my $numsizes = $#{$configHash->{scaledWidths}};
	for(my $j=0; $j<=$numsizes; $j++) {
	  #my $tmp = $imageData->{$j}{'width'};
	  #$imageData->{$j}{'width'} = $imageData->{$j}{'height'};
	  #$imageData->{$j}{'height'} = $tmp;
	  ($imageData->{$j}{'width'}, $imageData->{$j}{'height'}) =
	    ($imageData->{$j}{'height'}, $imageData->{$j}{'width'});
	}
      }
    }
  }
}

# copy the source file to the given destination
sub copyFile {
}

sub getHTMLImagePageLink{
  my ($imageData, $size, $num) = @_;

  my $sizeLink = $imageData->{'maxSize'};
  if ($sizeLink >= $size) {
	$sizeLink = $size;
      }
  return uri_escape($imageData->{'basename'})."_".
    $imageData->{configuration}{sizeNames}[$sizeLink].
      ".jpg.".$num.".html";
}

# $album is the album we are generating.
# imagepath is path to actual image location.
sub generateImagesInAlbum{
    my ($album, $albumHashRef, $firstIsIndex,
	$configHash, @imagesToDisplay) = @_;

    # an array of references to hashes storing information about each image
    my @imageData;

    # generate thumbnails and scaled images for each image
    # we will display, and generate HTML
    my $numImages = $#imagesToDisplay+1;
    my $i;

    #get description information
    for($i=0; $i<$numImages; $i++) {	
      my $crntImage = $album.$imagesToDisplay[$i];
      $imageData[$i] = getDesc($crntImage, $configHash);
      #print "-------------------".Dumper($imageData{'configuration'));
    }

    #calculate the thumbnail page each image will be on
    for($i=0; $i<$numImages; $i++) {	
	#my $crntImage = $imagesToDisplay[$i];
	$imageData[$i]{'thumbpage'} = getThumbPage($i, $firstIsIndex,
						   $configHash);
    }

    #determine scaled sizes, create scaled copies
    my(@largestSize); # array indexed by image number, holds index of largest
                      # available size for each image.
    for ($i=0; $i<$numImages; $i++) {
      my $imageConfigHash = $imageData[$i]{'configuration'};

      $largestSize[$i] = 0;
      my $crntImage = $imagesToDisplay[$i];
      beVerbose("\n", 2);
      beVerboseN("  Image $crntImage", 1);
	
      my ($crntImageBase,$imagePath,$crntImageType) =
	&fileparse($crntImage, '\.[^.]+\z');	
      $imageData[$i]{'filename'} = $crntImage;
      $imageData[$i]{'basename'} = $crntImageBase;
      $imageData[$i]{'type'} = $crntImageType;
      # don't escape the / character
      #$imageData[$i]{'imagepath'} = $album;
      #print "imagepath : �$imagePath�\n";
      #$imageData[$i]{'imageurl'} = $url;

      # to handle virtual images, make sure the images album exists
      # first, make sure web directory exists
      if (! -e "$albumdir$album") {
	beVerboseN("    Creating dir  $albumdir$album", 1);
	`mkdir -p "$albumdir$album"`
      }
      # load the image to check if it is bigger than current max dims
      # to see if need new scaled image

      # first try with Image::Size, which is quicker than ImageMagick
      my($width, $height) = imgsize("$picdir$album$crntImage");
      if (! defined $width) {
	# if Image::Size fails (format not recognized), load with
	# ImageMagick and get the size
	my($preview) = Image::Magick->new;
	# read in the picture
	my($x) = $preview->Read("$picdir$album$crntImage");
	warn "$x" if "$x";
	($width, $height) = $preview->Get('width', 'height');
      }
      $imageData[$i]{'width'} = $width;
      $imageData[$i]{'height'} = $height;

      #generate thumbnail (with dir if needed)
      my $thumbName = "$crntImageBase"."_pre.jpg";
      #$imageData[$i]{'thumblink'} = &getWebBase($album).$thumbName;
      $imageData[$i]{'thumblink'} = uri_escape($thumbName);

      my ($twidth, $theight);
      #if (! -e $albumdir.$album.$thumbName) {
      beVerboseN("  Generating thumbnail $album$thumbName from image ".
		 "$crntImageBase.", 3);
      ($twidth, $theight)=
	generateThumbnail($album.$crntImage, $album.$thumbName,
			  $width, $height, $imageData[$i],
			  $imageData[$i]{'configuration'});
      $imageData[$i]{twidth} = $twidth;
      $imageData[$i]{theight} = $theight;
      my $j;
      # generate scaled sizes, largestSize, write scaled
      # version if not oneCopy
      my $filling_in_sizes = 0;
      my $scaledImage;
      my $maxWidth;
      my $maxHeight;
      my ($scaledWidth, $scaledHeight);
      for ($j=0; $j <= $#{$imageConfigHash->{scaledWidths}}; $j++) {
	if (! $filling_in_sizes ) {
	  my $size = $imageConfigHash->{sizeNames}[$j];
	  $size =~ s/\.//g ;
	  $scaledImage = $crntImageBase."_".
	    $size.".jpg";
	  $maxWidth = $imageConfigHash->{scaledWidths}[$j];
	  $maxHeight = $imageConfigHash->{scaledHeights}[$j];
	  ($scaledWidth, $scaledHeight) =
	    getScaledSize($width, $height, $maxWidth, $maxHeight,
			  $imageConfigHash);
	  beVerboseN("    $imageConfigHash->{sizeNames}[$j] size is ".
		     "$scaledWidth x $scaledHeight.", 3);
	}
	# generate scaled version if needed
	$imageData[$i]{$j}{'width'} = $scaledWidth;
	$imageData[$i]{$j}{'height'} = $scaledHeight;
        if ( 1 ) {
          if ($width<$scaledWidth || $height<$scaledHeight) {
	    print "Skipping $width $height $scaledWidth $scaledHeight\n";
            next;
          }
        }
	if (! $filling_in_sizes ) {
	  $largestSize[$i] = $j;
	  if (! $oneCopy) {
	    ($imageData[$i]{$j}{'width'},
	     $imageData[$i]{$j}{'height'}) =
	       writeRotateScaledVersion($album.$crntImage,
					$album.$scaledImage,
					$scaledWidth, $scaledHeight,
					$imageData[$i],
					$imageData[$i]{'configuration'});
	  }
	  #if ( ($width <= $scaledWidth) && ($height <= $scaledHeight) ) {
	  #  $filling_in_sizes = 1;
	  #}
	}
      }
      # if oneCopy, create a single canonical image copy,
      # with the same name as the original
      if ($oneCopy) {
	if ($imageSource eq "scaled") {
	  ($imageData[$i]{$j}{'width'},
	   $imageData[$i]{$j}{'height'}) =
	     writeRotateScaledVersion($album.$crntImage, $album.$crntImage,
				      $imageData[$i]{$largestSize[$i]}{'width'},
				      $imageData[$i]{$largestSize[$i]}{'height'},
				      $imageData[$i],
				      $imageData[$i]{'configuration'});
	} elsif ($imageSource eq "copied") {
	  copyImage($picdir.$album.$crntImage,
		    $albumdir.$album.$crntImage,
		    $imageData[$i], $imageConfigHash);
	} elsif ($imageSource eq "orig") { #not yet implemented
	} elsif ($imageSource eq "custom") {
	  # gets size in KB
	  my $fileSize = ((-s "$picdir$crntImage") / 1024);
	  beVerboseN("  Size is $fileSize KB.", 3);
	  if ($fileSize > 900) {
	    beVerbose("Image $crntImage is BIG, resizing...", 3);
	    my( $newW, $newH) =
	      getScaledSize($imageData[$i]{'width'},
			    $imageData[$i]{'height'}, 1600, 1600,
			    $imageConfigHash);
	    ($imageData[$i]{$j}{'width'},
	     $imageData[$i]{$j}{'height'}) =
	       writeRotateScaledVersion($album.$crntImage, $album.$crntImage,
					$newW, $newH, $imageData[$i],
					$imageData[$i]{'configuration'});
	    beVerboseN("done.", 3);
	  } else {
	    beVerbose("Image $crntImage is SMALL, copying...", 3);
	    copyImage("$picdir$album$crntImage",
		      "$albumdir$album$crntImage",
		      $imageData[$i], $imageConfigHash);
	    beVerboseN("done.", 3);
	  }
	}
      }
      beVerboseN("    setting maxSize to $i ($imageData[$i]->{filename})",
		 3);
      $imageData[$i]->{'maxSize'}= $largestSize[$i];
    }

    # now calculate everything needed to generate html next, prev html
    # names, etc

    #print "crntImage is $crntImage\n";

    for ($i=0; $i < $numImages; $i++) {
      my $imageConfigHash = $imageData[$i]{'configuration'};

      #print "-------------------".Dumper($imageConfigHash);
      my $crntImage = $imagesToDisplay[$i];
      my ($crntImageBase,$imagePath,$crntImageType) =
	&fileparse($crntImage, '\.[^.]+\z');

      #now generate scaled versions and HTML
      for (my $j=0; $j <= $imageData[$i]->{maxSize}; $j++) {
	#print "\$j = $j\n";
	my $maxWidth = $imageConfigHash->{scaledWidths}[$j];
	my $maxHeight = $imageConfigHash->{scaledHeights}[$j];
	# suffix of filename of this size (add .html for html,
	# needs prefix)
	#my $crntSizeSuffix = ;
	my $size = $imageConfigHash->{sizeNames}[$j];
	$size =~ s/\.//g ;
	my $scaledImage = $crntImageBase."_".$size.".jpg";
	#determine next, prev image
	my $nextImageNum = ($i+1) % ($numImages);
	my $prevImageNum = ($i-1+$numImages)% ($numImages) ;
	my $npdiff = $nextImageNum - $prevImageNum;
	#my ($xbase, $xpath, $xtype) =
	#fileparse($imagesToDisplay[$nextImageNum], '\.[^.]+\z');

	my $sizeLink = $imageData[$nextImageNum]->{'maxSize'};
	
	$imageData[$i]{$j}{'nextHTML'} =
	  getHTMLImagePageLink($imageData[$nextImageNum], $j,
			       $nextImageNum);

	$imageData[$i]{$j}{'preloadIMG'} =
	  uri_escape($imageData[$nextImageNum]{'basename'})."_".
	    $imageData[$nextImageNum]{configuration}{sizeNames}[$sizeLink].
	      ".jpg";
	$imageData[$i]{$j}{'imgNum'} = $i + 1;
	$imageData[$i]{$j}{'imgCount'} = $numImages;
	$imageData[$i]{$j}{'nextIsFirst'} = ($nextImageNum == 0);
	$imageData[$i]{$j}{'prevIsLast'} = (($prevImageNum + 1) == $numImages);
	
	$imageData[$i]{$j}{'prevHTML'} =
	  getHTMLImagePageLink($imageData[$prevImageNum], $j,
			       $prevImageNum);

	$imageData[$i]{$j}{'nextTitle'}= $imageData[$nextImageNum]{'title'};
	$imageData[$i]{$j}{'prevTitle'}= $imageData[$prevImageNum]{'title'};


	if ($configHash->{thumbPrevNext}) {
	  $imageData[$i]{$j}{'nextThumb'} =
	    uri_escape($imageData[$nextImageNum]{'basename'})."_pre.jpg";
	  $imageData[$i]{$j}{'prevThumb'} =
	    uri_escape($imageData[$prevImageNum]{'basename'})."_pre.jpg";
	}

	# used in URL
	$imageData[$i]{$j}{'htmlFile'}= uri_escape($scaledImage).
	  ".$i.html";
	# used to access the file on disk
	$imageData[$i]{$j}{'htmlFileName'}= $scaledImage.".$i.html";

	# sizedFile is the text to go into the link for this image size
	if ($oneCopy) {
	  #$imageData[$i]{$j}{'sizedFile'} =
	  #  &getWebBase($album).$imageData[$i]{'filename'};
	  $imageData[$i]{$j}{'sizedFile'} = $crntImageBase.$crntImageType;
	} else {
	  #$imageData[$i]{$j}{'sizedFile'} =
	  #  &getWebBase($album).$imageData[$i]{'imagepath'}.
	  #    $scaledImage;		
	  $imageData[$i]{$j}{'sizedFile'} = $scaledImage;
	}
      }

      $imageData[$i]{'detailsLink'} =
	generateSecondaryFieldsPage($imageData[$i], $album, $albumHashRef,
				    $crntImageBase.$crntImageType,
				    $configHash);
    }



    # now generate html
    for ($i=0; $i<$numImages; $i++) {
      for (my $j=0; $j <= $imageData[$i]->{maxSize}; $j++) {
	my $lastImageURL = getHTMLImagePageLink($imageData[$numImages-1],
						 $j, $numImages-1);
	my $firstImageURL = getHTMLImagePageLink($imageData[0], $j, 0);
	generateImage($imageData[$i], $j, $album,
		      $albumHashRef, $imageData[$i]{'configuration'},
		     $firstImageURL, $lastImageURL);
      }
    }

    #printImageData(@imageData);
    beVerboseN("  We have ". scalar(@imageData). " images in the album.", 2);
    return @imageData;
  }

sub getThumbPage{
    my ($crntNumber, $firstIsIndex, $configHash) = @_;
    my $thumbPageNumber = int($crntNumber/$configHash->{numThumbsPerPage});
    return "index.html" if ($thumbPageNumber == 0 && $firstIsIndex);
    return "thumb$thumbPageNumber.html";
}

sub generateThumbnail{
    my ($crntImage, $thumbName, $width, $height, $imageData, $configHash) = @_;
    my ($newWidth, $newHeight) = getScaledSize($width, $height,
					       $configHash->{previewMaxWidth},
					       $configHash->{previewMaxHeight},
					       $configHash);
    return writeRotateScaledVersion($crntImage, $thumbName, $newWidth,
				    $newHeight, $imageData, $configHash);
}

sub getScaledSize{
    my ($width, $height, $maxWidth, $maxHeight, $configHash) = @_;
    my $xfactor;
    my $yfactor;

    #if width or height are zero, that is a problem
    if (not ( $width and $height ) ) {
           return ($maxWidth,$maxHeight);
    }
    if (substr($maxWidth, -1) eq "%") {
      chop($maxWidth);
      $xfactor = $maxWidth / 100;
      $maxWidth = $width * $xfactor;
    } else {
      $xfactor = $maxWidth/$width;
    }

    if (substr($maxHeight, -1) eq "%") {
      chop($maxHeight);
      $yfactor = $maxHeight / 100;
      $maxHeight = $height * $yfactor;
    } else {
      $yfactor = $maxHeight/$height;
    }

    if ((! $configHash->{enlarge}) &&
	($width < $maxWidth) && ($height < $maxHeight)) {
	return ($width, $height);
    }

    my ($newHeight, $newWidth);
    if ($xfactor <= $yfactor) {
	$newWidth = $maxWidth;
	$newHeight = $xfactor*$height;
    }else{
	$newHeight = $maxHeight;
	$newWidth = $yfactor*$width;
    }

    return (int($newWidth), int($newHeight));
}

# takes origName and newName with path info (but not pic_dir, of course)
sub writeScaledVersion{
    my ($origName, $newName, $newWidth, $newHeight,
	$imageRef, $configHash) = @_;
    beVerbose("    Generating scaled version of $picdir$origName\n".
	      "      to be written to $newName... ", 2);
    if (-e "$albumdir$newName"){
      if (((lstat("$albumdir$newName"))[9]) >= ((stat("$picdir$origName"))[9])){
	beVerboseN("\n    image already exists and is newer, skipping.", 2);
	return 0;
      }
    }

    my($preview) = Image::Magick->new;
    my($x) = $preview->Read("$picdir$origName"); # read in the picture
    warn "$x" if "$x";

    my $format = $preview->Get("magick");
    if (!$configHash->{scaleIfSameSize}
	and grep (/^$format$/, @webFormats ) ) {
      my ($width, $height) = $preview->Get("width", "height");
      if ($width == $newWidth && $height == $newHeight) {
	if ($configHash->{linkInsteadOfCopy} &&
	    (! ($configHash->{rotateImages} eq 'destination') ||
	     (! defined $imageRef->{'Orientation'} ||
	      $imageRef->{'Orientation'} eq "top_left"))) {
	  beVerbose("\n    Image has the right size, just linking... ", 2);
	  system("ln", "-sf", "$picdir$origName", "$albumdir$newName") == 0
	    or die("\nCannot link $albumdir$newName to $picdir$origName: $?");
	  # the original file may be r/o but we don't have to modify it
	  # but it must be readable by the http deamon
	  if ($configHash->{updateOriginalPerms})
	  {
	  system("chmod", "a+r", "$picdir$origName") == 0
	    or die("\nCannot set read permission on $albumdir$newName: $?");
	  }
         beVerboseN("done.", 2);
         return 0; # Image is processed, no need to try to process it
                   # again.
	} else {
	  beVerbose("\n    Image has the right size, just copying... ", 2);
	  system("cp", "-p", "$picdir$origName", "$albumdir$newName") == 0
	    or die("\nCannot copy $picdir$origName to $albumdir$newName: $?");
	  # make it writable in case $origName was r/o
	  system("chmod", "u+w,a+r", "$albumdir$newName") == 0
	    or die("\nCannot set write permission on $albumdir$newName: $?");
	  beVerboseN("done.", 2);
	  return 1;
	}
      }
    }
    $x = $preview->Set(quality => $configHash->{jpegQuality});
    warn "$x" if "$x";
    if ($configHash->{deExifyImages}) {
	$x = $preview->Profile(name => "*");
	warn "$x" if "$x";
    }
    if ($configHash->{scaleMethod} eq "sample") {
      $x = $preview->Sample(width=>$newWidth, height=>$newHeight);
    } else {
      if ($configHash->{scaleMethod} ne "scale") {
	warn "Unknown scaleMethod $configHash->{scaleMethod}, using scale"
      }
      $x = $preview->Scale(width=>$newWidth, height=>$newHeight);
    }
    warn "$x" if "$x";
    $x = $preview->Border(color=>'black',width=>1, height=>1);
    warn "$x" if "$x";
    beVerbose("\n    Writing scaled image $albumdir$newName... ", 3);
    $x = $preview->Write("$albumdir$newName");
    warn "$x" if "$x";
    beVerboseN("done.", 2);
    return 1;
}

sub writeRotateScaledVersion{
  my ($origName, $newName, $newWidth, $newHeight, $imageRef,
      $configHash) = @_;

  if (writeScaledVersion($origName, $newName, $newWidth, $newHeight,
			 $imageRef, $configHash)){
    if ($configHash->{rotateImages} eq 'destination'){
      # Perform a rotation of the picture if needed
      if (rotateImage("$albumdir$newName", $imageRef->{'Orientation'}, "",
		      $configHash) == 1){
	progressifyImage("$albumdir$newName", "", $configHash);
	return ($newHeight, $newWidth);
      }
    }
    progressifyImage("$albumdir$newName", "", $configHash);
    return ($newWidth, $newHeight);
  }
  ($newWidth, $newHeight) = imgsize($albumdir.$newName);
  return ($newWidth, $newHeight);
}

# $album is album we are generating, _not_ path to image.
# For the actual image path, use $imageHashRef
sub generateImage {
  my ($imageHashRef, $size, $album, $albumHashRef, $configHash,
      $firstImage, $lastImage) = @_;
  my $crntImage = $imageHashRef->{'filename'};
  my $imagetodisplay = $imageHashRef->{$size}{'sizedFile'};
  my $filesize = &fileSize("$albumdir$album$imagetodisplay");
  $imagetodisplay = uri_escape($imagetodisplay);

  my $imagetitle = $crntImage;
  $imagetitle =~ s/\.(\S+)\Z//;
  $imagetitle =~ s/_/ /g;

  my($fileExtension) = $imageHashRef->{'type'};
  if ($fileExtension =~ /jpg|jpeg/i) {
    $fileExtension = "JPEG";
  } elsif ($fileExtension =~ /gif/i) {
    $fileExtension = "GIF";
  } elsif ($fileExtension =~ /png/i) {
    $fileExtension = "PNG";
  } else {
    # if the type is unwkown, the image has been converted to JPEG
    $fileExtension = "JPEG";
  }

  my $width = $imageHashRef->{$size}{width};
  my $height = $imageHashRef->{$size}{height};

  # this line, with the _double_ quotes, is here for xgettext:
  _("$filesize $fileExtension image, $width x $height pixels")
    if (0);
  my $pictureinfo =
    _('$filesize $fileExtension image, $width x $height pixels');
  $pictureinfo = eval "\"$pictureinfo\"";

  #add strings to substitute hash
  my %subs_hash;
  #$subs_hash{ALBUM_TITLE} = $album;
  $subs_hash{WIDTH} = $width+2;
  $subs_hash{HEIGHT} = $height+2;
  $subs_hash{IMAGE_TO_DISPLAY} = $imagetodisplay;
  $subs_hash{PICTURE_INFO} = $pictureinfo;
  #$subs_hash{'<!--rootdir-->'} = getRootDir($album);
  $subs_hash{NEXT_IMAGE} = $imageHashRef->{$size}{nextHTML};
  if ($configHash->{imagePageCycling}) {
    $subs_hash{NEXT_IMAGE_PAGE} = 1;
    $subs_hash{PREV_IMAGE_PAGE} = 1;
  } else {
    $subs_hash{NEXT_IMAGE_PAGE} = ! $imageHashRef->{$size}{nextIsFirst};
    $subs_hash{PREV_IMAGE_PAGE} = ! $imageHashRef->{$size}{prevIsLast};
  }
  $subs_hash{IMG_NUM} = $imageHashRef->{$size}{imgNum};
  $subs_hash{IMG_COUNT} = $imageHashRef->{$size}{imgCount};
  if ($configHash->{javaScriptPreloadImage}) {
  	$subs_hash{IMG_PRELOAD} = $imageHashRef->{$size}{preloadIMG};
  }
  $subs_hash{PREV_IMAGE} = $imageHashRef->{$size}{prevHTML};
  $subs_hash{NEXT_TITLE} = $imageHashRef->{$size}{nextTitle};
  $subs_hash{PREV_TITLE} = $imageHashRef->{$size}{prevTitle};
  if ($configHash->{thumbPrevNext}) {
    $subs_hash{NEXT_THUMB} = $imageHashRef->{$size}{nextThumb};
    $subs_hash{PREV_THUMB} = $imageHashRef->{$size}{prevThumb};
  }
  $subs_hash{FILE_NAME} =  $imageHashRef->{basename}.
    $imageHashRef->{'type'};
  $subs_hash{THUMB_PAGE} = $imageHashRef->{thumbpage};
  $subs_hash{SIZE_LINKS} = getSizeLinks($size, $imageHashRef, $configHash);
  my $title = $imageHashRef->{title};
  $subs_hash{ALBUM_PATH_LINKS} =
    pathLinks($album, $title, @{$albumHashRef->{parentDirNames}});
  $subs_hash{TITLE} = $title;
  $subs_hash{DESC_TABLE} = getDescTable(%{$imageHashRef});
  $subs_hash{DETAILS_LINK} = $imageHashRef->{detailsLink};
  $subs_hash{STRING_DETAILS} = _("Additional information on the picture");

  $subs_hash{NAV_BAR_TABLE} =
    navBarLinks('image', $album, $configHash, %$albumHashRef);

  $subs_hash{TREE_NAME} = _("tree");
  $subs_hash{TREE_TITLE} = _("Tree of all albums and sub-albums");
  $subs_hash{TREE_LINK} = getRootDir($album)."tree.html";
  $subs_hash{STATIC_PATH} =
    getRootDir($album)."static.".$configHash->{templateStyle};
  $subs_hash{HOME_LINK} = $configHash->{homeURL};
  $subs_hash{FEEDBACK_LINK} = $configHash->{feedbackMail};
  if ($configHash->{backgroundImage}) {
      # Do not set this if not configured, so that template
      # can check for whether defined.
      $subs_hash{BG_IMAGE} =
	  $subs_hash{STATIC_PATH}."/".$configHash->{backgroundImage};
  }
  $subs_hash{CUSTOM_CSS} = $configHash->{customStyleSheet};
  $subs_hash{PATH_IMG_NUM} = $configHash->{pathImgNum};
  $subs_hash{PATH_SHOW_ICON} = $configHash->{pathShowIcon};
  $subs_hash{FIRST_IMAGE} = $firstImage;
  $subs_hash{LAST_IMAGE} = $lastImage;

  renderTemplate("image",
		 $albumdir.$album.$imageHashRef->{$size}{htmlFileName},
		 \%subs_hash, $configHash);
}

sub getSizeLinks{
  my ($size, $imageHashRef, $configHash) = @_;
  my @sizes;

  if ( $size eq 'full' ) {
    $size = -1;
  }
  for my $i (0..$imageHashRef->{'maxSize'}) {
    if ($i != $size) {		# output link
      push @sizes, {SIZE_NAME  => $configHash->{longSizeNames}[$i],
		    SIZE_LINK  => $imageHashRef->{$i}{'htmlFile'},
		    SIZE_TITLE => $imageHashRef->{$i}{'width'}."x".
		    $imageHashRef->{$i}{'height'},
		    SIZE_FILE => $configHash->{fileSizeNames}[$i],
		   };
    } else {			#mark as current page
      push @sizes, {SIZE_NAME => $configHash->{longSizeNames}[$i],
		    SIZE_FILE => $configHash->{fileActiveSizeNames}[$i]};
    }
  }
  return \@sizes;
}

sub getDescTable{
  my (%hash) = @_;

  my @descTable;
  foreach my $tagName (@mainFields) {
    if ($hash{$tagName}) {
      my $value=$hash{$tagName};
      $value =~ s/'/&#39;/g  ; # in case it's used in javascript code
      push @descTable, {DESC_FIELD_NAME => $fields{$tagName}->{'Name'},
			DESC_FIELD_VALUE => $value,
		       };
    }
  }
  return \@descTable;
}

# Given a path, not including $picdir, gives the relative path back
# to the root of the hierarchy. Note that the trailing slash is critical for the
# current implementation.  For example, if given as input sample_dir/, which
# corresponds to web/sample_dir/ and pic_dir/sample_dir/, the function
# returns "../".
# changed on 11/15 so it only points into base dir of web hierarchy
sub getRootDir{
    my $path = $_[0];
    my $slashcount = 0;
    $slashcount++ while($path =~ m'/'g);
    my $relPath ="";
    my $i;
    for($i=$slashcount; $i>0; $i--) {
	$relPath = $relPath."../";
    }
    return $relPath;
}

# given an album (path), not including $albumdir,
# provides the relative path back to $albumdir.
# closely related to getRootDir
sub getWebBase{
    my $path = $_[0];
    my $slashcount = 0;
    $slashcount++ while($path =~ m'/'g);
    my $relPath ="";
    my $i;
    for($i=$slashcount; $i>0; $i--) {
	$relPath = $relPath."../";
    }
    return $relPath;
}



# given the name of a directory in $picdir, not including the leading
# $picdir, generates the entry for the table on the home page
sub generateAlbumEntry {
  my $album = shift(@_);
  my $prettyalbum = $album;
  $prettyalbum =~ s/_/ /g;	# replaces underscores with spaces
  my $result = '<span class="albumlist"><a href="'.$album.'album.html">'.$prettyalbum.'</a></span><br>';
  return $result;
}

# return list of directories where templates can be found
sub templateDirs {
  my $configHash = shift;
  my @dirs;

  if ($templateDir) {
    push(@dirs, bsd_glob($templateDir."/templates.".
			 $configHash->{templateStyle}, GLOB_TILDE));
  }
  push(@dirs, bsd_glob($configHash->{userConfigDir}."/templates.".
		       $configHash->{templateStyle}, GLOB_TILDE));
  push(@dirs, bsd_glob($configHash->{globalDataDir}."/templates.".
		       $configHash->{templateStyle}, GLOB_TILDE));

  return @dirs;
}

# return static directory path (with dir) if it exists.
sub templateStaticDir {
  my $configHash = shift;

  my $staticDir;
  my @dirs = templateDirs($configHash);

  foreach my $dir (@dirs) {
    beVerboseN("  Looking for static template directory in $dir...", 4);
    if (-d $dir."/static") {
      $staticDir = $dir."/static";
      beVerboseN("  Found static template directory $staticDir.", 4);
      last;
    }
  }
  return $staticDir;
}


# return template file (with dir) from template name
sub templateFileName {
  my $templateName = shift;
  my $configHash = shift;
  $templateName .= ".html";

  my $templateFile;
  my @dirs = templateDirs($configHash);

  if ($templateDir) {
    push(@dirs, bsd_glob($templateDir."/templates.".
			 $configHash->{templateStyle}, GLOB_TILDE));
  }
  push(@dirs, bsd_glob($configHash->{userConfigDir}."/templates.".
		       $configHash->{templateStyle}, GLOB_TILDE));
  push(@dirs, bsd_glob($configHash->{globalDataDir}."/templates.".
		       $configHash->{templateStyle}, GLOB_TILDE));

  foreach my $dir (@dirs) {
    beVerboseN("  Looking for HTML template $templateName in $dir...", 4);
    if (-f $dir."/".$templateName) {
      $templateFile = $dir."/".$templateName;
      beVerboseN("  Found HTML template $templateFile...", 4);
      last;
    }
  }
  if (!$templateFile) {
    print("Error: cannot find HTML template $templateName\n");
    exit 2;
  }
  return $templateFile;
}

sub renderTemplate{
  my $templateName = shift;
  my $outputFileName = shift;
  my $templateParameters = shift;
  my $configHash =  shift;

  %{$templateParameters} =
    (%{$templateParameters},
     %{$configHash->{colorsSubs}{$configHash->{colorStyle}}},
     %intlSubs,
    );

  # open the html template
  #my $template =
  #  HTML::Template::JIT->new(jit_debug => 1,
  #      		     jit_path => '/tmp',

  my $template =
    HTML::Template->new(
			filename => templateFileName($templateName,
						     $configHash),
			blind_cache => 1,
			loop_context_vars => 1,
			die_on_bad_params => 0,
			global_vars => 1,
		       );

  # fill in the parameters
  $template->param($templateParameters);

  open(OUTFILE, ">$outputFileName") or die "Couldn't open $outputFileName";

  if ($configHash->{compactHTML}){
    my $page = $template->output();
    my $h = new HTML::Clean(\$page);
    $h->strip({whitespace    => 1,
               shortertags   => 1,
               blink         => 0,
               contenttype   => 0,
               comments      => 1,
               entities      => 0,
               dequote       => 1,
               defcolor      => 1,
               javascript    => 1,
               htmldefaults  => 0, # when set to 1, htmldefaults cause problems
                                   # with UTF-8 encodings
               lowercasetags => 0,
               meta          => "",
              });
    my $page2 = $h->data();
    print OUTFILE $$page2;
  } else {
    $template->output(print_to => *OUTFILE);
  }
  close(OUTFILE);
}

#input path of page to render, not including $root
sub renderPage {
    # Spits out a page of HTML.
    # to the correct file; in $albumdir/$crnt_dir

    my($file, $pageContent) = @_;

    #print "Writing to file: $file\n";
    open(OUTFILE, ">$file") or die "Couldn't open $file";
    print OUTFILE $pageContent;
    close(OUTFILE);

    #print "Content-type: text/html\n\n";
}


# Given an image file (no album_dir, with path, as usual), returns
# name of the .xml file associated with this image
sub getDescFile{
  my $crntImage = shift(@_);
  my ($base,$dir,$type) = &fileparse($crntImage, '\.[^.]+\z');
  my $descFile = $picdir.$dir.$base.$type.".xml";
  if( ! -e $descFile){
    $descFile = $picdir.$dir.$base.".xml";
  }
  if( ! -e $descFile){
    $descFile = $picdir.$dir.$base.$type.".xml";
  }
  return $descFile;
}

sub getXMLAsGrove{
  my $file=shift(@_);

  #my $sample = XML::Handler::Sample->new();


  # Get XML document as a Grove
  my $grove_builder = XML::Grove::Builder->new;
  my $parser = XML::Parser::PerlSAX->new ( Handler => $grove_builder);
  #my $parser = XML::SAX::Expat->new ( Handler => $grove_builder);
  return $parser->parse ( Source => { SystemId => $file } );
}

sub getDesc{
  my $crntImage = shift(@_);
  my $configHash= shift(@_);

  my ($base,$dir,$type) = &fileparse($crntImage, '\.[^.]+\z');
  my $descFile = getDescFile($crntImage);

  my %descHash;
  my %exifHash;
  my $document;
  my @priorityList;

  if (-e $descFile) {
    beVerboseN("  Reading desc file $descFile.", 3);
    $document = getXMLAsGrove($descFile);
    %descHash   = getDescXML($document);
    #$descHash{descFileName} =  uri_escape($descFile, '^-A-Za-z0-9/_\.');
    $configHash = getConfigXML($document, '/image/bins', $configHash);
    %exifHash   = getExifXML($document, \@priorityList);
  } else {
    $descHash{descFileName} =  "";
  }
  processExif($crntImage, \%descHash, \%exifHash, $document, \@priorityList,
	      $configHash);

  # If no title is set, use the picture file name.
  if (!trimWhiteSpace($descHash{'title'})) {
    # If trimming whitespace gets rid of everything (returns empty string)
    my $imagetitle = $base;
    $imagetitle =~ s/_/ /g; # replace underscores with spaces
    $descHash{'title'} = local2html($imagetitle);
  }

  $descHash{'configuration'} = $configHash;
  return \%descHash;
}

# Given an XML doc as a Grove, returns
# fields from that images description file.
sub getDescXML{
  my $document = shift(@_);
  my $fieldName;
  my $fieldValue;
  my %descHash;

  beVerboseN("   Reading user description fields...", 3);

  foreach my $element
    (@{$document->at_path('/image/description')->{Contents}}) {
      if (UNIVERSAL::isa($element, 'XML::Grove::Element')
	  && $element->{Name} eq "field") {
	$fieldName = $element->{Attributes}{'name'};
	$fieldValue = "";
	if (grep (/^$fieldName$/, keys(%fields))) {
	  beVerbose("    Reading field '$fieldName':", 3);
	  foreach my $characters (@{$element->{Contents}}) {
	    #if (UNIVERSAL::isa($characters, 'XML::Grove::Characters')) {
	    $fieldValue .= $characters->as_canon_xml();
	  }
	  $fieldValue = trimWhiteSpace(decode_entities(xml2html($fieldValue)));
	  beVerbose("'".$fieldValue."'\n", 3);
	  $descHash{$fieldName} = $fieldValue;
	} else {
	  beVerbose("    Ignoring unknown field '$fieldName'.", 3);
	}
      }
    }
  return %descHash;
}

# Given an XML doc as a Grove, returns
# hash from the Exif fields of the description file.
sub getExifXML{
  my $document = shift;
  my $priorityList = shift;
  #my $fieldNb;
  my $fieldName;
  my $fieldValue;
  my %descHash;

  beVerboseN("   Reading Exif data from description file...", 3);

  @{$priorityList} = @priorityExifTags;

  foreach my $element
    (@{$document->at_path('/image/exif')->{Contents}}) {
      if (UNIVERSAL::isa($element, 'XML::Grove::Element')
	  && $element->{Name} eq "tag") {
	#$fieldNb = $element->{Attributes}{'no'};
	$fieldName = $element->{Attributes}{'name'};
	$fieldValue = "";
	beVerbose("    Reading Exif field '$fieldName':", 3);
	foreach my $characters (@{$element->{Contents}}) {
	  #if (UNIVERSAL::isa($characters, 'XML::Grove::Characters')) {
	  $fieldValue .= $characters->as_canon_xml();
	}
	if (defined $fieldValue){
	  $fieldValue = trimWhiteSpace(decode_entities(xml2html($fieldValue)));
	} else {
	  $fieldValue = "";
	}
	beVerbose("'".$fieldValue."'\n", 3);

	$descHash{$fieldName} = $fieldValue;

	if ($element->{Attributes}{'priority'}){
	  push @{$priorityList}, $fieldName;
	}
      }
    }
  return %descHash;
}

sub addExif{
  my $tagName  = shift;
  my $tagValue = shift;
  my $exifHash = shift;
  my $priorityList = shift;

  $tagValue = trimWhiteSpace($tagValue);

  if (grep (/^$tagName$/, @{$priorityList})){
    beVerboseN("  Keeping value '$exifHash->{$tagName}' for $tagName", 3);
    beVerboseN("   $tagName has priority over exif value '$tagValue'.", 3);
    return 0;
  }

  if ( ! (exists $exifHash->{$tagName} &&
		$exifHash->{$tagName} eq $tagValue) ) {
    beVerboseN("  Found new value '".$tagValue."' for $tagName", 3);
    beVerboseN("   old value was '".$exifHash->{$tagName}."'", 3)
      if ($exifHash->{$tagName});
    beVerboseN("   bla '".$exifHash->{$tagName}."'", 3)
      if ($exifHash->{$tagName});

    $exifHash->{$tagName} = $tagValue;
    return 1;
  }
  return 0;
}

sub addSpecialExif{
  my $tagName  = shift;
  my $tagValue = shift;
  my $exifHash = shift;
  my $priorityList = shift;

  if (UNIVERSAL::isa(\$tagValue, 'SCALAR')){
    # Canon special tags
    if ($tagName eq "Canon-Tag-0x0007"){
      return addExif("Software", $tagValue, $exifHash, $priorityList);
    }
    if ($tagName eq "Canon-Tag-0x0009"){
      return addExif("Owner", $tagValue, $exifHash, $priorityList);
    }
  } elsif (UNIVERSAL::isa($tagValue, 'ARRAY')) {
    if ($tagValue->[1] eq 0) {
      return 0;
    }
    if ($tagName eq "SubjectDistance") {
      $tagValue = $tagValue->[0]/$tagValue->[1];
      $tagValue .= translate(" meters");
      return addExif($tagName, $tagValue, $exifHash, $priorityList);
    }
    if ($tagName eq "ExposureTime") {
      $tagValue = "1/".sprintf("%.0f", 1/($tagValue->[0]/$tagValue->[1]));
      $tagValue .= translate(" second");
      return addExif($tagName, $tagValue, $exifHash, $priorityList);
    }
    if ($tagName eq "ShutterSpeedValue") {
      # this is the actual APEX Value
      my $APEXval = $tagValue->[0]/$tagValue->[1];

      # ...but we want to see time in seconds instead
      if ($APEXval > 0) {
	$tagValue = sprintf("1/%d", 2**((1)*$APEXval));
      } else {
	$tagValue = sprintf("%d", 2**((-1)*$APEXval));
      }
      $tagValue .= translate(" sec");
      return addExif($tagName, $tagValue, $exifHash, $priorityList);
    }
    if ($tagName eq "FNumber"
	|| $tagName eq "FocalPlaneXResolution"
	|| $tagName eq "FocalPlaneYResolution") {
      $tagValue = sprintf("%.2f", $tagValue->[0]/$tagValue->[1]);
      return addExif($tagName, $tagValue, $exifHash, $priorityList);
    }
    if ($tagName eq "FocalLength") {
      $tagValue = sprintf("%.2f", $tagValue->[0]/$tagValue->[1]).
	translate(" millimeters");
      return addExif($tagName, $tagValue, $exifHash, $priorityList);
    }
    if ($tagName eq "ApertureValue" || $tagName eq "MaxApertureValue") {
      $tagValue = "F".sprintf("%.2f", sqrt(2)**($tagValue->[0]/$tagValue->[1])).
	" (".sprintf("%.2f", $tagValue->[0]/$tagValue->[1])." APEX)";
      return addExif($tagName, $tagValue, $exifHash, $priorityList);
    }
    if ($tagName eq "BitsPerSample") {
      my $result="";
      $result .= $_." " foreach(@{$tagValue});
      return addExif($tagName, $result, $exifHash, $priorityList);
    }
    if ($tagName eq "CompressedBitsPerPixel") {
      my $result;
    SWITCH: {
	$tagValue->[0]==1   && do { $result="Basic"; last; };
	$tagValue->[0]==2   && do { $result="Normal"; last; };
	$tagValue->[0]==3   && do { $result="Normal"; last; };
	$tagValue->[0]==4   && do { $result="Fine"; last; };
	$tagValue->[0]==5   && do { $result="Very fine"; last; };
      }
      $result .= " ($tagValue->[0]:$tagValue->[1])";
      return addExif($tagName, $result, $exifHash, $priorityList);
    }
    if ($tagName eq "Canon-Tag-0x0001") {
      # CanonMacro
      my $value = $tagValue->[1];
      if ($value == 1) {
	$value = translate("On");
      } elsif ($value == 2) {
	$value = translate("Off");
      } else {
	$value="";
      }
      my $modified = addExif("CanonMacro", $value, $exifHash, $priorityList)
	if($value);
      # CanonTimerLength
      $value = $tagValue->[2];
      if ($value) {
	$value = $value."/10".translate(" second");
	$modified |= addExif("CanonTimerLength", $value, $exifHash,
			     $priorityList);
      }
      # CanonQuality
      $value = $tagValue->[3];
      if ($value == 2) {
	$value = translate("Normal");
      } elsif ($value == 3) {
	$value = translate("Fine");
      } elsif ($value == 5) {
	$value = translate("Superfine");
      } else {
	$value="";
      }
      $modified |= addExif("CanonQuality", $value, $exifHash, $priorityList)
	if ($value);
      # CanonFlashMode
      $value = $tagValue->[4];
      if ($value == 1) {
	$value = translate("Auto");
      } elsif ($value == 2) {
	$value = translate("On");
      } elsif ($value == 3) {
	$value = translate("Red-eye reduction");
      } elsif ($value == 4) {
	$value = translate("Slow synchro");
      } elsif ($value == 5) {
	$value = translate("Auto + red-eye reduction");
      } elsif ($value == 6) {
	$value = translate("On + red-eye reduction");
      } elsif ($value == 16) {
	$value = translate("External flash");
      } else {
	$value="";
      }
      $modified |= addExif("CanonFlashMode", $value, $exifHash, $priorityList)
	if ($value);
      # CanonContinuousDriveMode
      $value = $tagValue->[5];
      if ($value) {
	$value = translate("On");
	$modified |= addExif("CanonContinuousDriveMode", $value, $exifHash,
			     $priorityList);
      }
      # CanonFocusMode
      $value = $tagValue->[7];
      if ($value == 1) {
	$value = translate("AI Servo");
      } elsif ($value == 2) {
	$value = translate("AI Focus");
      } elsif ($value == 3) {
	$value = translate("MF");
      } elsif ($value == 4) {
	$value = translate("Single");
      } elsif ($value == 5) {
	$value = translate("Continuous");
      } elsif ($value == 6) {
	$value = translate("MF");
      } elsif ($value == 0) {
	$value = translate("One-Shot");
      } else {
	$value="";
      }
      $modified |= addExif("CanonFocusMode", $value, $exifHash, $priorityList)
	if ($value);
      # CanonImageSize
      $value = $tagValue->[10];
      if ($value == 0) {
	$value = translate("Large");
      } elsif ($value == 1) {
	$value = translate("Medium");
      } elsif ($value == 2) {
	$value = translate("Small");
      } else {
	$value="";
      }
      $modified |= addExif("CanonImageSize", $value, $exifHash, $priorityList)
	if ($value);
      # CanonEasyShootingMode
      $value = $tagValue->[11];
      if ($value == 1) {
	$value = translate("Manual");
      } elsif ($value == 2) {
	$value = translate("Landscape");
      } elsif ($value == 3) {
	$value = translate("Fast Shutter");
      } elsif ($value == 4) {
	$value = translate("Slow Shutter");
      } elsif ($value == 5) {
	$value = translate("Night");
      } elsif ($value == 6) {
	$value = translate("B&W");
      } elsif ($value == 7) {
	$value = translate("Sepia");
      } elsif ($value == 8) {
	$value = translate("Portrait");
      } elsif ($value == 9) {
	$value = translate("Sports");
      } elsif ($value == 10) {
	$value = translate("Macro / Close-Up");
      } elsif ($value == 11) {
	$value = translate("Pan Focus");
      } elsif ($value == 0) {
	$value = translate("Full Auto");
      } else {
	$value="";
      }
      $modified |= addExif("CanonEasyShootingMode", $value, $exifHash,
			   $priorityList)
	if ($value);
      # CanonDigitalZoom
      $value = $tagValue->[12];
      if ($value == 1) {
	$value = translate("2x");
      } elsif ($value == 2) {
	$value = translate("4x");
      } elsif ($value == 0) {
	$value = translate("None");
      } else {
	$value="";
      }
      $modified |= addExif("CanonDigitalZoom", $value, $exifHash, $priorityList)
	if ($value);
      # CanonContrast
      $value = $tagValue->[13];
      if ($value == 0xFFFF) {
	$value = translate("Low");
      } elsif ($value == 0x0000) {
	$value = translate("Normal");
      } elsif ($value == 0x0001) {
	$value = translate("High");
      } else {
	$value="";
      }
      $modified |= addExif("CanonContrast", $value, $exifHash, $priorityList)
	if ($value);
      # CanonSaturation
      $value = $tagValue->[14];
      if ($value == 0xFFFF) {
	$value = translate("Low");
      } elsif ($value == 0x0000) {
	$value = translate("Normal");
      } elsif ($value == 0x0001) {
	$value = translate("High");
      } else {
	$value="";
      }
      $modified |= addExif("CanonSaturation", $value, $exifHash, $priorityList)
	if ($value);
      # CanonSharpness
      $value = $tagValue->[15];
      if ($value == 0xFFFF) {
	$value = translate("Low");
      } elsif ($value == 0x0000) {
	$value = translate("Normal");
      } elsif ($value == 0x0001) {
	$value = translate("High");
      } else {
	$value="";
      }
      $modified |= addExif("CanonSharpness", $value, $exifHash, $priorityList)
	if ($value);
      # CanonISO
      $value = $tagValue->[16];
      if ($value == 15) {
	$value = translate("Auto");
      } elsif ($value == 16) {
	$value = "50";
      } elsif ($value == 17) {
	$value = "100";
      } elsif ($value == 18) {
	$value = "200";
      } elsif ($value == 19) {
	$value = "400";
      } else {
	$value="";
      }
      $modified |= addExif("CanonISO", $value, $exifHash, $priorityList)
	if ($value);
      # CanonFocusType
      if ($tagValue->[18]) {
	$value = $tagValue->[18];
	if ($value == 1) {
	  $value = translate("Auto");
	} elsif ($value == 3) {
	  $value = translate("Close-up (macro)");
	} elsif ($value == 8) {
	  $value = translate("locked (pan mode)");
	} elsif ($value == 0) {
	  $value = translate("Manual");
	} else {
	  $value="";
	}
	$modified |= addExif("CanonFocusType", $value, $exifHash, $priorityList)
	  if ($value);
      }
      return $modified;
    }
  }
  return 0;
}

# Merge picture Exif data with Exif data from desc file, then update
# descHash if tag is in @fields and if description text file field is void.
# Finally, write Exif tags to the desc file if it was updated.
sub processExif{
    my $crntImage   = shift(@_);
    my $descHash    = shift(@_);
    my $exifHash    = shift(@_);
    my $document    = shift(@_);
    my $priorityList = shift(@_);
    my $configHash  = shift(@_);
    my ($base,$dir,$type) = &fileparse($crntImage, '\.[^.]+\z');

    beVerboseN(" Reading Exif info from picture file $crntImage...", 2);
    my $pictureFile = $picdir.$dir.$base.$type;
    my($camerainfo) = image_info($pictureFile);
    if (exists $camerainfo->{"error"}) {
      beVerboseN(" Can't read info from picture file $crntImage: ".
	$camerainfo->{"error"}."\n", 2);
      return
    }

    # Add new Exif tags from picture file to Exif Hash from desc file
    my $tagName;
    my $tagValue;
    my $modified = 0;
    while ( ($tagName, $tagValue) = each(%$camerainfo) ) {
      $tagName =~ s/[\x00-\x1F]//g;
      if (UNIVERSAL::isa(\$tagValue, 'SCALAR')){
	# strip characters after the first control code (ascii code <32 )
	#$tagValue =~ s/^([\x20-\xFF]*)[^\x20-\xFF].*$/$1/;
	$tagValue =~ s/[\x00-\x1F].*$//s;
	$modified |= addExif($tagName, $tagValue, $exifHash,
			     $priorityList);
      }
      #print "�$tagName� : ";
      #print $_, ', ' foreach(@{$tagValue});
      #print "\n";
      $modified |= addSpecialExif($tagName, $tagValue, $exifHash,
				  $priorityList);
    }

    # add value to desc Hash if field is void
    foreach my $field (keys(%fields)) {
      my $fieldExif = $fields{$field}->{'EXIF'};
      if ((! $descHash->{$field}) && $fieldExif && $exifHash->{$fieldExif}) {
	beVerboseN("  Using '$field' from EXIF data: ".
 		   $exifHash->{$fieldExif}, 3);

	$descHash->{$field} = $exifHash->{$fieldExif};
	if ($fields{$field}->{'Transform'}) {
	  beVerbose("  Evaluating ".
		    "$descHash->{$field} =~ ".$fields{$field}->{'Transform'}.
		    " to ", 4);
	  eval '$descHash->{$field} =~ '.$fields{$field}->{'Transform'};
	  beVerboseN("'$descHash->{$field}'.", 4);
	}
      }
    }

    if ($configHash->{rotateImages} eq 'original'){
      # Perform a rotation of the picture if needed
      # we're doing it here because we must change and save the
      # orientation tag if the image was rotated
      if(rotateImage($pictureFile, $exifHash->{Orientation},
		     $exifHash->{file_ext}, $configHash)){
	progressifyImage("$pictureFile", $exifHash->{file_ext}, $configHash);
	push @{$priorityList}, "Orientation";
	$exifHash->{Orientation} = "top_left";
      }
    }

    # Write Exif tags to desc file
    if ($modified && $configHash->{addExifToDescFile}) {
      writeExif($crntImage, $exifHash, $document,
		$priorityList, $configHash);
    }
}

sub charac_indent{
  my $n = shift(@_);
  my $s="\n";
  for (1..$n){
    $s .= "   ";
  }
  return XML::Grove::Characters->new ( Data => $s );
}

sub writeExif{
  my $crntImage    = shift;
  my $descHash     = shift;
  my $document     = shift;
  my $priorityList = shift;
  my $configHash   = shift;
  my $description;
  my $exif;
  my $element;
  my $characters;
  my $file = getDescFile($crntImage);

  # create Grove document if file didn't exist
  if (!$document) {
    beVerbose("   Creating new XML description file $file... ", 3);
    $description =
      XML::Grove::Element->new ( Name => 'description',
				 Contents => [charac_indent(1)]);
    my $bins =
      XML::Grove::Element->new ( Name => 'bins',
				 Contents => [charac_indent(1)]);
    $exif =
      XML::Grove::Element->new ( Name => 'exif',
				 Contents => [charac_indent(2)]);
    $element =
      XML::Grove::Element->new ( Name => 'image',
				 Contents =>
				 [charac_indent(1),
				  $description,
				  charac_indent(1),
				  $bins,
				  charac_indent(1),
				  $exif ]);
    $document = XML::Grove::Document->new ( Contents => [ $element ] );

    if ($configHash->{createEmptyDescFields}){
      # Add standard fields if a new file is generated (so you can
      # edit it better)
      my @fields = @mainFields;
      push @fields, 'title';

      foreach my $field (@fields) {
	$element = XML::Grove::Element->new ( Name => 'field',
					      Contents =>  [charac_indent(3),
							    charac_indent(2)],
					      Attributes => {"name" => $field});
	push @{$description->{Contents}}, (charac_indent(2), $element);
      }
      push @{$description->{Contents}}, charac_indent(1);
    }
    beVerboseN("OK.", 3);
  }

  # Add exif tags to the Grove
  if (!$exif) {
    $exif = $document->at_path('/image/exif');
    @{$exif->{Contents}}=();
  }
  my $tagName;
  my $tagValue;
  while ( ($tagName, $tagValue) = each(%$descHash) ) {
    beVerboseN("  Adding Exif tag '$tagName'='$tagValue' in XML desc file.", 3);
    #$tagValue = html2xml($tagValue);
    $characters = XML::Grove::Characters->new ( Data => $tagValue );
    $element = XML::Grove::Element->new ( Name => 'tag',
					  Contents => [charac_indent(3),
						       $characters,
						       charac_indent(2)],
					  Attributes => {"name" => $tagName});

    if (grep (/^$tagName$/, @{$priorityList})){
       $element->{Attributes}{"priority"} = "1";
       beVerboseN("    with 'priority' attribute.\n", 3);
    }

    push @{$exif->{Contents}}, (charac_indent(2), $element);
  }
  push @{$exif->{Contents}}, charac_indent(1);
  # Write the Grove to the desc file
  beVerbose("  Saving XML description file $file... ", 3);
  my $fileHandler = new IO::File;
  open($fileHandler, '>', $file)
    or die("Cannot open file $file to write Exif tag ($!)");
  binmode($fileHandler, ":utf8") if $^V ge v5.8.0;

#  my $composer = new XML::Handler::Composer ();
#  my $my_handler = new XML::Filter::Reindent (Handler => $composer);

  #print Dumper($document);
  my $my_handler =
    new XML::Handler::YAWriter(
			       'Output' => $fileHandler,
			       'Encoding' => $configHash->{xmlEncoding},
#			       'Pretty' => {
#					    'NoProlog' =>0,
#					    'NoDTD' =>0,
#					    'NoPI' =>0,
#					    'PrettyWhiteIndent'=>1,
#					    'PrettyWhiteNewline'=>1,
#					    'NoWhiteSpace'=>0,
#					    'NoComments'=>0,
#					    'AddHiddenNewline'=>0,
#					    'AddHiddenAttrTab'=>1,
#					   }
 					    );

#  my $my_handler = XML::Handler::XMLWriter->new( Output => $fileHandler,
#  						 Newlines => 0);
  $document->parse(DocumentHandler => $my_handler);
  close ($fileHandler) || die ("can't close $file ($!)");
  beVerboseN("OK.", 3);
}


sub getDescAlbum {
  my $descFile = shift(@_);  # file name of the album desc file
  my $hash = shift(@_);      # ref to the album description hash
  my $configHash = shift(@_);# ref to the current config hash
  my $fieldName;
  my $fieldValue;
  my @AlbumFieldNames = ( "title", "sampleimage", "shortdesc", "longdesc",
			  "ignore");

  beVerboseN("Reading album description file '$descFile'...", 3);

  my $document = getXMLAsGrove($descFile);
  # I have to do that, don't ask me why...

  #$XML::UM::ENCDIR="/usr/lib/perl5/XML/Parser/";
  #my $encode = XML::UM::get_encode (
  #             Encoding => 'ISO-8859-9',
  #             EncodeUnmapped => \&XML::UM::encode_unmapped_dec);

  $configHash = getConfigXML($document, "/album/bins", $configHash);

  foreach my $element
    (@{$document->at_path('/album/description')->{Contents}}) {
      if (UNIVERSAL::isa($element, 'XML::Grove::Element') && $element->{Name} eq "field") {
	$fieldName = $element->{Attributes}{'name'};
	$fieldValue = "";
	if (grep (/^$fieldName$/, @AlbumFieldNames)) {
	    beVerbose("    Reading field '$fieldName':", 3);
	    foreach my $characters (@{$element->{Contents}}) {
	      $fieldValue .= $characters->as_canon_xml();
	    }
	    #if ($fieldName ne "shortdesc" && $fieldName ne "longdesc"){
	    #  $fieldValue = decode_entities($fieldValue);
	    #}
	    if ($fieldName eq "sampleimage"){
	      $fieldValue =
		trimWhiteSpace(decode_entities($fieldValue));
	      beVerbose("'".$fieldValue."'\n", 3);
	    }else{
	      $fieldValue =
		trimWhiteSpace(decode_entities(xml2html($fieldValue)));
	      beVerbose("'".$fieldValue."'\n", 3);
	    }
	    #	    $fieldValue = $encode->(trimWhiteSpace($fieldValue));
	    $hash->{$fieldName} = $fieldValue;
	  } else {
	    beVerboseN("    Ignoring unknown field '$fieldName'.", 3);
	  }
      }
    }
  return $configHash;
}


sub getConfigColors{
  my $colors = shift;
  my $style = shift;
  my $configHash = shift;
  my $modified = 0;

  foreach my $color
    (@{$colors->{Contents}}) {
      if (UNIVERSAL::isa($color, 'XML::Grove::Element')){
	if ($color->{Name} eq "color") {
	  if (defined $color->{Attributes}{'name'}){
	    my $fieldName = $color->{Attributes}{'name'};
	    my $fieldValue;
	    foreach my $characters (@{$color->{Contents}}) {
	      $fieldValue .= $characters->as_canon_xml();
	    }
	    if (defined $fieldValue){
	      $fieldValue =
		trimWhiteSpace(decode_entities(xml2html($fieldValue)));
	    }
	    if ($fieldValue){
	      $configHash->{colorsSubs}{$style}{$fieldName.'COLOR'} =
		$fieldValue;
	      $modified = 1;
	    } else {
	      beVerboseN("Warning, no value for '$fieldName' color, ignoring.",
			 1);
	    }
	  } else {
	    beVerboseN("Warning, missing 'name' attribute for <color> tag, ".
		       "ignoring.", 1);
	  }
	} else {
	  beVerboseN("Warning, unknown tag <".$color->{Name}.
		     "> in <colors> section, ignoring.", 1);
	}
      }
    }
  return $modified;
}

sub getConfigSizes{
  my $sizes = shift;
  my $configHash = shift;
  my (@names, @shortNames, @heights, @widths);

  foreach my $size
    (@{$sizes->{Contents}}) {
      if (UNIVERSAL::isa($size, 'XML::Grove::Element')){
	if ($size->{Name} eq "size") {
	  if (defined $size->{Attributes}{'name'}){
	    push @names, _(trimWhiteSpace($size->{Attributes}{'name'}));
	  } else {
	    beVerboseN("Warning, missing parameter 'name' in tag <size>, ".
		       "ignoring all sizes.", 1);
	    return 0;
	  }
	  if (defined $size->{Attributes}{'shortname'}){
	    push @shortNames,
	      _(trimWhiteSpace($size->{Attributes}{'shortname'}));
	  } else {
	    beVerboseN("Warning, missing parameter 'shortname' in tag <size>, ".
		       "ignoring all sizes.", 1);
	    return 0;
	  }
	  if (defined $size->{Attributes}{'height'}){
	    push @heights, _(trimWhiteSpace($size->{Attributes}{'height'}));
	  } else {
	    beVerboseN("Warning, missing parameter 'height' in tag <size>, ".
		       "ignoring all sizes.", 1);
	    return 0;
	  }
	  if (defined $size->{Attributes}{'width'}){
	    push @widths, _(trimWhiteSpace($size->{Attributes}{'width'}));
	  } else {
	    beVerboseN("Warning, missing parameter 'width' in tag <size> ".
		       "in <sizes> section, ignoring all sizes.", 1);
	    return 0;
	  }
	} else {
	  beVerboseN("Warning, unknown tag <".$size->{Name}."> ".
		     "in <sizes> section, ignoring.", 1);
	}
      }
    }

  if (@names) {
    $configHash->{longSizeNames} = \@names;
    $configHash->{sizeNames} = \@shortNames;
    $configHash->{scaledWidths} = \@widths;
    $configHash->{scaledHeights} = \@heights;
    return 1;
  } else {
    print "Warning, <sizes> section is empty, ignoring.\n"
  }
  return 0;
}

# Given an XML doc as a Grove, the path to <bins> tag and the current
# configuration hash, returns hash from the config section of files
# (<bins> tag) merged with current hash.
sub getConfigXML{
  my $document = shift(@_);  # Grove document
  my $path = shift(@_);      #�path of the <bins> tag in the document
  my $currentConfigHash = shift(@_);  #�hash ref to the current configuration
  #my $fieldNb;
  my $fieldName;
  my $fieldValue;
  my %configHash = %{ dclone($currentConfigHash) };

  beVerboseN("   Reading configuration data from file...", 3);

  my $modified = 0;
  foreach my $element
    (@{$document->at_path($path)->{Contents}}) {
      if (UNIVERSAL::isa($element, 'XML::Grove::Element')){
	if ($element->{Name} eq "parameter") {
	  $fieldName = $element->{Attributes}{'name'};
	  $fieldValue = "";
	  foreach my $characters (@{$element->{Contents}}) {
	    #if (UNIVERSAL::isa($characters, 'XML::Grove::Characters')) {
	    $fieldValue .= $characters->as_canon_xml();
	  }
	  if (defined $fieldValue){
	    $fieldValue =
	      trimWhiteSpace(decode_entities(xml2html($fieldValue)));
	  } else {
	    $fieldValue = "";
	  }
	  beVerbose("    Reading configuration parameter '$fieldName':", 3);
	  $configHash{$fieldName} = $fieldValue;
	  beVerbose("'".$fieldValue."'\n", 3);
	  $modified = 1;
	} elsif ($element->{Name} eq "sizes") {
	  beVerbose("    Reading sizes parameters...", 3);
	  $modified |= getConfigSizes($element, \%configHash);
	} elsif ($element->{Name} eq "colors") {
	  my $style = $element->{Attributes}{'style'};
	  if ($style){
	    beVerboseN("    Reading color style parameter '$style'...", 3);
	    $modified |= getConfigColors($element, $style, \%configHash);
	  } else {
	    beVerboseN("     Warning, no 'style' attribute to tag <colors> ".
		       "in <bins> section, ignoring.", 1);
	  }
	} else {
	  beVerboseN("     Warning, unknown tag <$element->{Name}> ".
		     "in <bins> section, ignoring.", 1);
	}
      }
    }
  if (!$modified){
    return $currentConfigHash;  # return the original to not keep the
                                # new copy in memory
  }
  return \%configHash;
}


sub commandAvailable{
  my $command = shift;
  my $exists = 0;
  my (@PATH) = split (/:/, $ENV{"PATH"});
  foreach (@PATH) {
    beVerboseN("  looking for $_/$command...", 4);
    $exists++ if (-f "$_/$command" && -x "$_/$command");
    last if $exists;
  }
  return $exists;
}


BEGIN {
  my $rotateGeneric="none";
  sub rotateGenericCommand{
    my $file = shift;
    my $degrees = shift;
    my $verbose = shift;

    if ($rotateGeneric eq "none") {
      beVerbose("  Looking for a generic rotation utility (mogrify)... ", 3);
      if (commandAvailable("mogrify")) {
	$rotateGeneric = 'mogrify -rotate %s "%s"';
	beVerboseN(" found mogrify.", 3);
      } else {
	beVerboseN(" not found, cannot rotate.", 3);
	$rotateGeneric = "";
      }
    }

    if ($rotateGeneric) {
      $file =~ s|\\|\\\\|g;
      $file =~ s|\$|\\\$|g;
      $file =~ s|"|\\"|g;
      $file =~ s|`|\\`|g;
      return sprintf($rotateGeneric, $degrees, $file);
    }
    return "";
  }
}

BEGIN {
  my $rotateJPEG="none";
  sub rotateJPEGCommand{
    my $file = shift;
    my $degrees = shift;
    my $verbose = shift;

    if ($rotateJPEG eq "none") {
      beVerbose("\n  Looking for a JPEG rotation utility (jpegtran)... ", 3);
      if (commandAvailable("jpegtran")) {
	$rotateJPEG = 'jpegtran -copy all -rotate %s -outfile "%s.tmp" "%s" && mv "%s.tmp" "%s"';
	beVerboseN(" found jpegtran.", 3);
      } else {
	$rotateJPEG = "";
	beVerboseN(" not found, trying a generic one.", 3);
      }
    }

    if ($rotateJPEG) {
      $file =~ s|\\|\\\\|g;
      $file =~ s|\$|\\\$|g;
      $file =~ s|"|\\"|g;
      $file =~ s|`|\\`|g;
      return sprintf($rotateJPEG, $degrees, $file, $file, $file, $file);
    }
    return rotateGenericCommand($file, $degrees);
  }
}

# return the best command available for rotating a given file type
sub rotateCommand{
  my $type = shift;
  my $file = shift;
  my $degrees = shift;
  my $configHash = shift;

  if ($type eq "jpg" && $configHash->{rotateWithJpegtran}){
    return rotateJPEGCommand($file, $degrees, $verbose);
  }else{
    return rotateGenericCommand($file, $degrees, $verbose);
  }
}

# Rotate an image. Return 0 if no rotation was performed, 1 if a
# rotation was performed and it changed width and heigth, and 2 if it
# was a 180 degree rotation
sub rotateImage{
  my $imageName = shift;
  my $orientation = shift;
  my $ext = shift;
  my $configHash = shift;

  #if (!$imageInfo->{'BinsRotated'} && $imageInfo->{'Orientation'}){
  if (!$orientation){
    return 0;
  }

  beVerboseN(" Orientation for picture is '$orientation'", 3);

  if ($orientation eq "top_left"){
    return 0;
  }
  if ($imageName =~ m/$configHash->{noRotation}/) {
    return 0;
  }
  my $degrees;
  if     ($orientation eq "right_top"){
    $degrees = 90;
  }elsif ($orientation eq "left_bot"){
    $degrees = 270;
  }elsif ($orientation eq "bot_right"){
    $degrees = 180;
  } else {
    print "Warning, Orientation field has an unknown value '$orientation'.\n";
    return 0;
  }

  beVerbose(" Performing $degrees degrees rotation clockwise on $imageName... ",
	    2);
  my $type = "";
  if ($ext) {
    $type = $ext;
  }else{
    $type="jpg";
  }
  my $command = rotateCommand($type, $imageName, $degrees, $configHash);
  if ($command){
    beVerbose("\n   Running '$command'... ", 3);
    if(!system($command)){
      #$imageInfo->{'BinsRotated'}="yes";
      beVerboseN("OK", 2);
      if ($degrees == 180){
	return 2;
      }
      return 1;
    }
  }else{
    beVerboseN("impossible.\n  Cannot perform rotation, no utility installed.",
	       2);
  }
  return 0;
}


BEGIN {
  my $progressifyJPEG="none";
  sub progressifyJPEGCommand{
    my $filein = shift;
    my $fileout = shift;
    my $verbose = shift;

    if ($progressifyJPEG eq "none") {
      beVerbose("\n  Looking for a progressive JPEG utility (jpegtran)... ", 3);
      if (commandAvailable("jpegtran")) {
	$progressifyJPEG = 'jpegtran -copy all -progressive -outfile "%s" "%s"';
	beVerboseN(" found jpegtran.", 3);
      } else {
	$progressifyJPEG = "";
	beVerboseN(" not found, cannot make JPEGs progressive", 3);
      }
    }

    if ($progressifyJPEG) {
      $filein =~ s|\\|\\\\|g;
      $filein =~ s|\$|\\\$|g;
      $filein =~ s|"|\\"|g;
      $filein =~ s|`|\\`|g;
      $fileout =~ s|\\|\\\\|g;
      $fileout =~ s|\$|\\\$|g;
      $fileout =~ s|"|\\"|g;
      $fileout =~ s|`|\\`|g;
      return sprintf($progressifyJPEG, $fileout, $filein);
    }
    return "";
  }
}

# fileSizeCmp(file1, file2): return file1.file-size - file2.file-size
sub fileSizeCmp {
  my $file1 = shift;
  my $file2 = shift;
  return ((stat($file1))[7]) - ((stat($file2))[7]);
}

# progressifyJPEGImage(imageName, configHash)
# make a JPEG image progressive. Return 0 if no conversion was performed, and 1 if a
# conversion was performed
sub progressifyJPEGImage{
  my $imageName = shift;
  my $configHash = shift;
  my $tempFile = "$imageName.tmp";

  beVerbose("     Making $imageName progressive JPEG... ", 2);

  my $command = progressifyJPEGCommand($imageName, $tempFile, $verbose);

  if ($command) {
    beVerbose("\n      Running '$command'... ", 3);
    if(!system($command)){
      if (($configHash->{jpegProgressify} eq "always")) {
	rename($tempFile, $imageName)
	  or die("\nfailed to rename $tempFile to $imageName: $?");
	beVerboseN("OK", 2);
	return 1;
      }
      my $diff = fileSizeCmp($tempFile, $imageName);
      if ($diff < 0) {
	rename($tempFile, $imageName)
	  or die("\nfailed to rename $tempFile to $imageName: $?");
	$diff = -$diff;
	beVerbose("$diff bytes smaller; ", 3);
	beVerboseN("OK", 2);
	return 1;
      } else {
	beVerbose("$diff bytes larger; ", 3);
	beVerboseN("NO", 2);
	unlink($tempFile) or warn("\nfailed to unlink $tempFile: $?");
      }
    }
  }else{
    beVerboseN("impossible.\n  Cannot make progressive JPEG, no utility installed.",
	       2);
  }
  return 0;
}

# progressifyImage(imageName, ext, configHash)
# make a JPEG image progressive. Return 0 if no conversion was performed, and 1 if a
# conversion was performed.  Skips non JPEG images or when jpegProgressify
# is false (returns 0).
sub progressifyImage{
  my $imageName = shift;
  my $ext = shift;
  my $configHash = shift;

  my $type = $ext ? $ext : "jpg";
  if (($type eq "jpg") && ($configHash->{jpegProgressify} ne "never")) {
    return progressifyJPEGImage($imageName, $configHash);
  }
  return 0;
}



sub readConfigFile{
  my $configHash = shift(@_);
  my $document;

  my $globalConfigFile = bsd_glob($configHash->{'globalConfigDir'}."/".
				  $configHash->{'configFileName'}, GLOB_TILDE);
  if ($globalConfigFile && -e $globalConfigFile) {
    beVerboseN("Reading global configuration file $globalConfigFile.", 3);
    $document = getXMLAsGrove($globalConfigFile);
    $configHash = getConfigXML($document, "/bins", $configHash);
  }else{
    beVerboseN("No global configuration file ".
	       $configHash->{'globalConfigDir'}."/".
	       $configHash->{'configFileName'}." found.", 3);
  }
  my $userConfigFile = bsd_glob($configHash->{'userConfigDir'}."/".
				$configHash->{'configFileName'}, GLOB_TILDE);
  if ($userConfigFile && -e $userConfigFile) {
    beVerboseN("Reading user configuration file $userConfigFile.", 3);
    $document = getXMLAsGrove($userConfigFile);
    $configHash = getConfigXML($document, "/bins", $configHash);
  }else{
    beVerboseN("No user configuration file ".$configHash->{'userConfigDir'}."/".
	       $configHash->{'configFileName'}." found.", 3);
  }
  return $configHash;
}

sub stringToBool{
    my $string = shift(@_);
    if (! $string) {return 0;}
    if (trimWhiteSpace($string) eq "true") { return 1;}
    return 0;
}

sub trimWhiteSpace{
    my $string = shift(@_);
    return "" if (! defined $string);
    for ($string) {
	s/^\s+//;
	s/\s+$//;
    }
    return $string;
}



sub BEGIN {
  my %ignoredSets;
  my %hiddenSets;
  # this function is only used by ignoreSet, hiddenSet and
  # ignoreAndHiddenSet functions below
  sub ignoreOrHiddenSet{
    my $ignoreLine = shift;
    my $album = shift;
    my $type = shift;
    my $configHash = shift;

    my ($set, $userSet);
    if ($type eq "ignore") {
      $set = \%ignoredSets;
      $userSet = $ignoreOpts;
      if ($ignoreOpts && $configHash->{ignore}){
	$userSet .= ","
      }
      $userSet .= $configHash->{ignore};
    } elsif ($type eq "hidden") {
      $set = \%hiddenSets;
      $userSet = $hiddenOpts;
      if ($hiddenOpts && $configHash->{hidden}){
	$userSet .= ","
      }
      $userSet .= $configHash->{hidden};
    } else {
      return 0;
    }

    # Remove the trailing slash if there is one (or more!) in $album, so
    # that the hash works for $fileInAlbum invocations as well.
    if ( defined($album) ) {
      $album =~ s/\/$//sog;
    }

    # Use the hash to optimize the checking of ignoreSets(), since this is
    # called three times for each album, and it could be expensive if there
    # are many ignore directives.  If we have already checked this album,
    # just report what we concluded last time.
    if ( defined($set->{$album}) && $album ne "" ) {
      return( $set->{$album} );
    }

    if ( defined($ignoreLine) ) {
      for my $thisIgnoreOpts ( split(',', $userSet ) ) {
        for my $thisIgnoreLine ( split('\s*,\s*',$ignoreLine) ) {
          if ( $thisIgnoreOpts eq $thisIgnoreLine ) {
            $set->{$album}=1;
	    beVerboseN("Skipping \"$album\" because of \"$thisIgnoreOpts\"".
		       " IGNORE directive.", 2);
            return(1);
          }
        }
      }
    }
    $set->{$album}=0;
    return(0);
  }
}

# return 1 if one of $ignoreLine is a ignored album
sub ignoreSet{
  my $ignoreLine = shift;
  my $album = shift;
  my $configHash = shift;

  return ignoreOrHiddenSet($ignoreLine, $album, "ignore", $configHash);
}

# return 1 if one of $ignoreLine is a hidden album
sub hiddenSet{
  my $ignoreLine = shift;
  my $album = shift;
  my $configHash = shift;

  return ignoreOrHiddenSet($ignoreLine, $album, "hidden", $configHash);
}

# return 1 if one of $ignoreLine is a ignored or hidden album
sub ignoreAndHiddenSet{
  my $ignoreLine = shift;
  my $album = shift;
  my $configHash = shift;

  if (ignoreOrHiddenSet($ignoreLine, $album, "ignore", $configHash)) {
    return 1
  }
  return ignoreOrHiddenSet($ignoreLine, $album, "hidden", $configHash);
}

sub beVerbose {
    my $output = shift;
    my $level  = shift;
    print "$output" if ($verbose >= $level);
}

sub beVerboseN {
    my $output = shift;
    my $level  = shift;
    if ($verbose >= $level){
      beVerbose($output, $level);
      print "\n";
    }
}

# for .po generation, until xgettext handles Perl correctly
sub dummy_I18N{
  _("Thumbnail Page");
  _("Thumbnail Page 1");
  _("tree");
  _("subalbum");
  _("subalbums");
  _("Hg");
  _("Huge");
  _("image");
  _("images");
  _("Click on one of the size names above to enlarge this image");
  _("Click to view thumbnails of the current album");
  _("Click to view this album");
}