#!/usr/pkg/bin/perl -w
#    -*- cperl-indent-level: 4 -*-

#    BINS Photo Album version 1.1.29
#    Copyright (C) 2001-2005 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.182 2005/08/22 23:52:33 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;

# Date parsing
use Date::Parse;

# Used by the search engine
use Text::Unaccent;

#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/share/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.

# note that all of these options are now documented in bins.sgml; any
# new options need corresponding new documentation.

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.

     dateString => "%c",        # Specify the format of date strings; this
                                # accepts all formats supported by date(1).

     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.

     borderOnThumbnails =>0,    # Width of the border of the thumbnail's
                                # image in the thumbnails page, in
                                # pixels. 0 means no border.

     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.

     whenSrcSmaller => "enlarge", # What to do if the source image is smaller
                                  # than the size of the generated image.
                                  # If set to "enlarge" the generated image is
                                  # enlarged to the requested size.
                                  # If set to "original" the generate image is
                                  # the same size as the original image.
                                  # If set to "skip" do not generate any image.

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

     linkRelative => 1,         # If set to 1, we use a relative path for the
                                # link

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

     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.

     searchEngine => 1,              # If 1, generate a search page.
                                     # Images can be searched on
                                     # description fields set in the
                                     # searchFields parameter.

     searchFields => "title description people location event comment",
                                     # Space separated list of
                                     # description field names used by
                                     # the search engine, if
                                     # searchEngine is set to 1.

     searchLimit => 50,              # Maximum results returned by the
                                     # search engine. Note that if
                                     # this number is too high, it can
                                     # hang the browser.

     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 => "swigs",     # name of the template style to use

     # 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 $codeset;
eval {
    require I18N::Langinfo;
    I18N::Langinfo->import(qw(langinfo CODESET));
    $codeset = langinfo(CODESET());
};
# ANSI is unspeakably primitive, keep LATIN1 instead
# Solaris refers to ISO 646 as "646" but that is not a valid codeset
if (!$@ && $codeset && $codeset ne "ANSI_X3.4-1968" && $codeset ne "646") {
    $defaultConfig{defaultEncoding} = $codeset;
    beVerboseN("Forcing encoding to $codeset", 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 getFields function below.
my @mainFields = ("description", "people", "location", "date",
                                  "event", "comment");


# Fields to display (in the list order) in the details page. These
# fields are in the getFields function below.
my @secondaryFields = (
                       # DigiCam
                       _("BINS-SECTION DigiCam Info"),
                       "make", "model", "owner", "firmware",
                       # DigiCam settings for the image
                       _("BINS-SECTION DigiCam settings for the image"),
                       "artist",
                       "copyright",
                       "imagedescription",
                       "usercomment",
                       "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",
                       "lightsource",
                       "flash",
                       "canon_flash_mode",
                       "flashenergy",
                       "CanonISO",
                       "iso",
                       "exposure_time",
                       "exposure_prog",
                       "canon_digital_zoom",
                       "canon_focus_mode",
                       "CanonFocusType",
                       "subject_distance",
                       "metering_mode",
                       "focal_length",
                       "shutter_speed_value",
                       "brightness",
                       "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",
                       "comment",
                       "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
                       );

sub getFields {
    # 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.  It is evaluated as
    #   normal Perl code and the result has to be in $_.
    my %fields =
          (
         "title" =>
         { Name => _("Title"),
       },
         "description" =>
         { Name => _("Description"),
       },
         "people" =>
         { Name => _("People"),
       },
         "location" =>
         { Name => _("Location"),
                 },
         "date" =>
         { Name => _("Date"),
           EXIF => "DateTimeOriginal",
           Transform => '$_ = local2html(strftime $configHash->{dateString}, gmtime str2time "$_") if str2time "$_"',
           # Alternatively, you could use regex substitution:
           # English version is yyyy:mm:dd hh:mm:ss to yyyy/mm/dd hh:mm:ss :
           # Transform => 's%^(\\d+):(\\d+):(\\d+) (.*)$%\$1/\$2/\$3 \$4%',
           # French version is yyyy:mm:dd hh:mm:ss to dd/mm/yyyy hh:mm:ss :
           # Transform => 's%^(\d+):(\d+):(\d+) (.*)$%$3/$2/$1 $4%',
                 },

             "event" =>
         { Name => _("Event"),
           },
                 "comment" =>
                 { Name => _("Comment"),
                   EXIF => "Comment",
           },
         "model" =>
         { Name => _("Equipment Model"),
           EXIF => "Model",
       },
         "make" =>
         { Name => _("Equipment Make"),
           EXIF => "Make",
       },
         "firmware" =>
         { Name => _("Software"),
#     EXIF => "Canon-Tag-0x0007",
           EXIF => "Software",
           Tip => _("Firmware (internal software of device) version number."),
       },
         "owner" =>
         { Name => _("Owner name"),
#     EXIF => "Canon-Tag-0x0009",
           EXIF => "Owner",
           Tip => _("Name of the owner of the digicam."),
       },
         "artist" =>
         { Name => _("Artist name"),
           EXIF => "Artist",
           Tip => _("Name of the camera owner, photographer or image creator."),
       },
         "copyright" =>
         { Name => _("Copyright"),
           EXIF => "Copyright",
           Tip => _("Copyright information."),
       },
         "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. The unit is the APEX (Additive System of Photographic Exposure) setting"),
       },
         "brightness" =>
         { Name => _("Brightness"),
           EXIF => "BrightnessValue",
           Tip => _("The value of brightness. The unit is the APEX (Additive System of Photographic Exposure) 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 => _("ISO Speed and ISO Latitude of the camera or input device as specified in ISO 12232."),
       },
         "CanonFocusType" =>
         { Name => _("Focus Type"),
           EXIF => "CanonFocusType",
           #Tip => _(""),
       },
         "exposure_prog" =>
         { Name => _("Exposure Program"),
           EXIF => "ExposureProgram",
           Tip => _("The class of the program used by the camera to set exposure when the picture is taken."),
       },
         "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 => _(""),
       },
         "lightsource" =>
         { Name => _("Light Source"),
           EXIF => "LightSource",
           Tip => _("The kind of light source."),
       },
         "usercomment" =>
         { Name => _("User Comment"),
           EXIF => "UserComment",
           Tip => _("Keywords or comments on the image."),
       },
         "imagedescription" =>
         { Name => _("Image Description"),
           EXIF => "ImageDescription",
           #Tip => _(""),
       },
         "flashenergy" =>
         { Name => _("Flash Energy"),
           EXIF => "FlashEnergy",
           Tip => _("Indicates the strobe energy at the time the image is captured, as measured in Beam Candle Power Seconds (BCPS)."),
       },
         "sensingmethod" =>
         { Name => _("Sensing Method"),
           EXIF => "SensingMethod",
           Tip => _("Indicates the image sensor type on the camera or input device."),
       },
         );
    return \%fields;
}

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;

sub getIntlSubs{
    my $configHash = shift;
    # 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_ALBUM => _("Album"),
                      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_BACK => _("Back"),
                      STRING_BACKTOTHEIMAGE => _("Back to the image"),
                      STRING_IMAGE => _("Image"),
                      STRING_TREE => _("tree"),
                      STRING_ALBUMTREE => _("Album Tree"),
                      STRING_ALBUMGENERATEDBY => _("Album generated by"),
                      STRING_FEEDBACK => _("Send Feedback"),
                      STRING_YOURIMAGE => _("Your Image"),
                      STRING_YOURALBUM => _("Your Album"),
                      STRING_IMAGESEARCH => _("Image Search"),
                      STRING_SEARCHKEYWORDS => _("Search keywords"),
                      STRING_SEARCHONLYWHOLEWORDS => _("Search only whole words"),
                      STRING_SEARCHJSERROR => _("ERROR: your browser must have Javascript enable in order to use the search feature"),
                      STRING_SEARCH => _("search"),
                      STRING_SEARCHTITLE => _("Search a picture in all albums"),
                      STRING_TOOMANYRESULTS => gettext("Too many results, only the first %d are displayed. Try to refine your search."),
                      STRING_NODOM => gettext("Your browser doesn't support the Level 1 DOM"),
                      STRING_SEARCHCOMPLETED => gettext("Search completed: %d found"),
                      BINS_VERSION             => "1.1.29",
                      ENCODING                 => $defaultConfig{htmlEncoding},
                      GENERATED_DATE       => _("on ").
                      local2html(strftime($configHash->{dateString},
                                          localtime)),
                      BINS_ID =>
                      '<!--$Id: bins,v 1.182 2005/08/22 23:52:33 jerome Exp $-->',
                      );
    return \%intlSubs;
}

# @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 generateTreeLoop;
sub generateImage;
sub getRootDir;
#sub generateScaledImage;
sub getDesc;
sub getExif;
sub readField;
sub trimWhiteSpace;
sub stringToBool;
sub ignoreSet;
sub getFields;
sub getIntlSubs;

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

sub write_htaccess;

print "\nBINS Photo Album 1.1.29 (http://bins.sautret.org/)\n";
print "Copyright © 2001-2004 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(File::Spec->rel2abs($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];

    {
        # check that $albumdir is _not_ a subdirectory of $picdir
        # to avoid an infinite recursivity

        # we can't just use $albumdir and $picdir since they may contain
        # "../" that will pose problems:
        # foo/../bar would be considered a subdirectory of foo/

        my $pwd = File::Spec->rel2abs(".");
        #print "pwd: $pwd\n";

        chdir($picdir) or warn "Can't chdir $picdir: $!";
        my $picdir2 = File::Spec->rel2abs(".");
        chdir($pwd);

        if (! -d "$albumdir") {
            mkdir $albumdir, 0755
                or die("\nCannot create $albumdir: $?");
        }
        chdir($albumdir) or warn "Can't chdir $albumdir: $!";
        my $albumdir2 = File::Spec->rel2abs(".");
        chdir($pwd);

        #print "picdir:   $picdir2\n";
        #print "albumdir: $albumdir2\n";

        die "albumdir_dir ($albumdir) can't be a subdirectory of picdir_dir ($picdir)"
            if ($albumdir2 =~ m/^$picdir2\//);
    }

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

}

# figure out a relative path from 1 file to another for relative linking
sub relpath {
    my $p1 = $_[0];
    my $p2 = $_[1];

    my $l1 = length($p1);
    my $l2 = length($p2);

    my $cl = cls($p1, $p2);

    my $p2r = substr( $p2, $cl, $l2);

    my $p1dc = 0;
    my $i = 0;

    #print "relpath($p1, $p2)\n";
    #print "    \$p2r = $p2r\n";
    for ( $i=$l1; $i > $cl; $i--) {
        if ( substr($p1,$i,1) eq "/" ) { $p1dc++; }
    }

    for( $i = 0 ; $i < $p1dc ; $i++ ) {
        $p2r = "../" . $p2r;
    }

    #print "-> $p2r\n";
    return $p2r;
}

# taken directly from
# http://www.people.cornell.edu/pages/tco2/papers/common_leading_substring/2000-06-09-0830
# which is a paper by
# Todd Olson
# Cornell University
# tco2 at cornell.edu
sub cls {
    my $l0 = length($_[0]);
    my $l1 = length($_[1]);

    my $l = ($l0 < $l1) ? $l0 : $l1;

    my $i = 0;

    for ( $i=0; $i < $l; $i++) {
        last if ( substr($_[0],$i,1) ne substr($_[1],$i,1) );
    }
    return $i;
}


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

    # write files used by the search engine
    writeSearchFiles($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 any encoding transformation
sub gettext{
    if ($I18N) {
        return Locale::gettext::gettext(shift);
    }
    return 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 createSearchResultTemplate {
    my $source = shift;
    my $dest = shift;

    open(FILEOUT, ">", $dest)
      or die("\nCannot open file $dest for writing: $?");

    print(FILEOUT "result_html=''+\n");

    open(FILEIN, $source) or die("\nCannot open file $source: $?");
    while(<FILEIN>) {
        chomp;
        print(FILEOUT "'$_\\n'+\n");
    }
    close (FILEIN) || die ("can't close $source ($!)");
    print(FILEOUT "''\n");
    close (FILEOUT) || die ("can't close $dest ($!)");
    unlink($source);
}

# write templates and javascript files used for the search functionality
sub writeSearchFiles {
    my $album = shift;
    my $configHash = shift;

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

    # transform searchFields parameter in a list
    my @l = $configHash->{searchFields} =~ /(\w+)/g;
    $configHash->{searchFields}= \@l;

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

    if (! -d "$destDir") {
        mkdir $destDir, 0755
            or die("\nCannot create $destDir: $?");
    }

    # Copy the javascript
    my $source_file;
    foreach my $file ("search_data.js",) {
        $source_file = templateFileName($file, $configHash);
        system("cp", "-p", $source_file, "$destDir") == 0
          or die("\nCannot copy file $file to $destDir: $?");
    }

    # Render the HTML templates
    my %subsHash;
    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{SEARCH_LIMIT} = $configHash->{searchLimit};

    renderFile(templateFileName("search.js", $configHash), $album."search.js",
               \%subsHash, $configHash);

    $subsHash{HOME_LINK} = $configHash->{homeURL};
    $subsHash{ALBUM_THUMB} = $configHash->{treePreview};
    $subsHash{PATH_SHOW_ICON} = $configHash->{pathShowIcon};
    $subsHash{DATE} = strftime($configHash->{dateString}, localtime);

    renderTemplate("search", $album."search.html",
                   \%subsHash, $configHash);

    my $tmpfile = $album."search_result.html";

    renderTemplate("search_result",
                   $tmpfile, \%subsHash, $configHash);
    createSearchResultTemplate($tmpfile, $album."search_result.js");
}

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{HOME_LINK} = $configHash->{homeURL};
    $subsHash{ALBUM_THUMB} = $configHash->{treePreview};
    $subsHash{PATH_SHOW_ICON} = $configHash->{pathShowIcon};
    $subsHash{DATE} = strftime($configHash->{dateString}, localtime);

    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 => $albumHash{title},
                   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;
}

sub jssafe_uri_escape
{
    return uri_escape(@_, '^-A-Za-z0-9/_\.');
}

# 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/;
        $th = jssafe_uri_escape($albumHashRef->{dirname}) . "/" . $th;
#   print "Returning from ImageData = $th\n";
        return $th;
    }else{
        my @subAlbumHashList = @{ $albumHashRef->{subalbums} };
        my $subAlbumHashRef;
        foreach $subAlbumHashRef (@subAlbumHashList) {
            if ($subAlbumHashRef->{sampleimage}) {
                my $th = jssafe_uri_escape($albumHashRef->{dirname}) . "/" .
                    $subAlbumHashRef->{sampleimage};
#       print "Returning from sub-album \$th = $th\n";
                return($th);
            }
        }
    }
    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 = jssafe_uri_escape($pwd);
        $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 ($albumHash{descFileName} =~ /^[^\/]/){
                $tablesubs{TITLE} .= $pwd."/";
            }
            $tablesubs{TITLE} .= $albumHash{descFileName}."\">".
                $imageInfo{title}."</a>";
        } else {
            $tablesubs{TITLE} = $imageInfo{title};
        }
        $tablesubs{SIZE_LINKS} = \@sizes;

        my $atLeastOneField = 0;
        my $tagValue;

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

        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};
    $finalsubs{DATE} = strftime($configHash->{dateString}, localtime);

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

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

    my $pwd = `pwd`;
    chop($pwd);
    $pwd = jssafe_uri_escape($pwd);

    # 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-->'} = jssafe_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";
    }
    $tableData .=  '</table>';

    $finalsubs{'<!--subalbumtable-->'} = $tableData;
    my $pageContent = openTemplate("subalbum");
    $pageContent = doSubstitutions($pageContent, $configHash, %finalsubs);
    renderTemplate($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 = jssafe_uri_escape($pwd);

        $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 => jssafe_uri_escape($albuminfo{dirname})."/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};
    $templateParameters{DATE} = strftime($configHash->{dateString}, localtime);

    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} = jssafe_uri_escape($album)."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} = jssafe_uri_escape($albumdescfile);
        $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} = jssafe_uri_escape($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;
}

# escape a string so it can used as a javascript string
sub escapeJSString{
    my $string = shift;
    $string =~ s/\"/\\\"/g;
    $string =~ s/\n//g;
    return $string;
}

# normalize a string by translating accents
sub normalizeString{
    my $charset = shift;
    my $string = shift;
    my $result;

    $result = lc($string);
    $result = unac_string($charset, $result);
    if (!defined($result)) {
        warn "Invalid encoding $charset for string '$string'";
        return "";
    }
    return $result;
}

# split a string into keywords for search engine
sub splitInKeywords{
    my $string = shift;

    $string =~ s/[^a-z0-9]/ /gi;
    return $string;
}

# write data for the search engine
sub writeSearchString{
    my $imageHashRef = shift;
    my $configHash = shift;
    my $albumHashRef = shift;
    my $album = jssafe_uri_escape(shift);

    beVerboseN("  Writing information for the search engine.", 4);

    my $charset = $configHash->{htmlEncoding};
    my $search_string = " ";
    foreach my $field (@{$configHash->{searchFields}}){
        if ($$imageHashRef{$field}) {
            $search_string .= escapeJSString(normalizeString($charset,
                                                             $$imageHashRef{$field}))." ";
        }
    }
    $search_string = splitInKeywords($search_string);

    my $size = $configHash->{defaultSize};
    if (! $size >= 0){
        $size = 0;
    }
    my $url = $album.$$imageHashRef{$size}{'htmlFile'};

    my $album_title = $albumHashRef->{'title'};
    my $album_url = $albumHashRef->{'link'};
    my $thumb_url = $album.$imageHashRef->{'thumblink'};
    my $title = escapeJSString($imageHashRef->{'title'});
    my $width = $imageHashRef->{'twidth'};
    my $height = $imageHashRef->{'theight'};

    my $file = $albumdir."search_data.js";
    open(FILE, ">>", $file)
        or die("Cannot write to file $file ($!)");

    print(FILE "sd[sd.length] = new io(\"$search_string\", \"$title\", \"$url\", \"$thumb_url\", \"$width\", \"$height\", \"$album_title\", \"$album_url\");\n");
    close (FILE) || die ("can't close $file ($!)");

}

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 = jssafe_uri_escape($pwd);
            $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};
    $subsHash{DATE} = strftime($configHash->{dateString}, localtime);
    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 = getFields($configHash)->{$tagName};
            if ($imageInfo->{$tagName}) {
                my %row = (FIELD_NAME => $tagValue->{'Name'},
                           FIELD_VALUE => local2html($imageInfo->{$tagName})
                           );
                if ($tagValue->{'Tip'}) {
                    $row{FIELD_TIP} = $tagValue->{'Tip'};
                    # Escape double quote to unconfuse emacs code highlighter.
                    $row{FIELD_TIP} =~ s/\"/&quot;/g;
                }
                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{TREE_LINK} = getRootDir($album)."tree.html";
    $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} =  encode_entities($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};
    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{HOME_LINK} = $configHash->{homeURL};
    $subs_hash{FEEDBACK_LINK} = $configHash->{feedbackMail};
    $subs_hash{PATH_SHOW_ICON} = $configHash->{pathShowIcon};
    $subs_hash{DATE} = strftime($configHash->{dateString}, localtime);
    $subs_hash{CUSTOM_CSS} = $configHash->{customStyleSheet};

    my @array;
    push @array, {NAV_NAME => getIntlSubs($configHash)->{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 = jssafe_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 jssafe_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'} = jssafe_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 (! $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$album$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'};
            if ($sizeLink >= $j ) {
                $sizeLink = $j;
            }

            $imageData[$i]{$j}{'nextHTML'} =
                getHTMLImagePageLink($imageData[$nextImageNum], $j,
                                     $nextImageNum);

            $imageData[$i]{$j}{'preloadIMG'} =
                jssafe_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'} =
                    jssafe_uri_escape($imageData[$nextImageNum]{'basename'})."_pre.jpg";
                $imageData[$i]{$j}{'prevThumb'} =
                    jssafe_uri_escape($imageData[$prevImageNum]{'basename'})."_pre.jpg";
            }

            # used in URL
            $imageData[$i]{$j}{'htmlFile'}= jssafe_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);
        if ( $configHash->{searchEngine}) {
            writeSearchString($imageData[$i], $configHash, $albumHashRef,
                              $album);
        }
    }



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

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

    if ($width<$newWidth || $height<$newHeight) {
        if ($configHash->{whenSrcSmaller} eq 'enlarge') {
            beVerboseN("Enlarging small image from ${width}x${height} to ${newWidth}x${newHeight}", 2);
        } elsif ($configHash->{whenSrcSmaller} eq 'original') {
            beVerboseN("Keeping original size of small image, instead of from ${width}x${height} to ${newWidth}x${newHeight}", 2);
            ($newWidth, $newHeight) = ($width, $height);
        } elsif ($configHash->{whenSrcSmaller} eq 'skip') {
            beVerboseN("Skipping small image instead of scaling from ${width}x${height} to ${newWidth}x${newHeight}", 2);
            next;
        } else {
            warn "Unrecognized configuration setting for \"whenSrcSmaller\":".$configHash->{whenSrcSmaller};
        }
    }

    return ($newWidth, $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);
                my $newpath;
                if( $configHash->{linkRelative} ) {
                    $newpath = relpath("$albumdir$newName", "$picdir$origName");
                } else {
                    $newpath = "$picdir$origName";
                }
                beVerboseN("Linking from $albumdir$newName to $newpath... ", 2);
                system("ln", "-sf", $newpath, "$albumdir$newName") == 0
                    or die("\nCannot link $albumdir$newName to $newpath: $?");
                # 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 => "*", profile => "");
        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";
    my $borderOnThumbnails = $configHash->{borderOnThumbnails};
    if ($borderOnThumbnails) {
        $x = $preview->Border(color=>'black',width=>$borderOnThumbnails,
                              height=>$borderOnThumbnails);
        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 = jssafe_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};

    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} =  encode_entities($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}, $configHash);
    $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};
    $subs_hash{DATE} = strftime($configHash->{dateString}, localtime);
    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;
        $subs_hash{IMAGE_COMMENT} = $imageHashRef->{comment};

    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 ($hashref, $configHash) = @_;

    my @descTable;
    foreach my $tagName (@mainFields) {
        if (${%$hashref}{$tagName}) {
            my $value=${%$hashref}{$tagName};
            $value =~ s/'/&#39;/g  ; # in case it's used in javascript code
            push @descTable, {DESC_FIELD_NAME => getFields($configHash)->{$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;

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

# render an html file from the templates
sub renderTemplate{
    my $templateName = shift;
    my $outputFileName = shift;
    my $templateParameters = shift;
    my $configHash =  shift;

    renderFile(templateFileName($templateName.".html", $configHash),
               $outputFileName,
               $templateParameters,
               $configHash,
              );
}

# render a file (replace template tags with real values)
sub renderFile{
    my $sourceFileName = shift;
    my $outputFileName = shift;
    my $templateParameters = shift;
    my $configHash =  shift;

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

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

    my $template =
        HTML::Template->new(
                            filename => $sourceFileName,
                            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);
}

# 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, $configHash);
        #$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, $configHash) = @_;
    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(%{getFields($configHash)}))) {
                    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") {
            if (($tagValue->[0] == 0) || ($tagValue->[1] == 0)) {
                $tagValue = 0;
            } else {
                $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(%{getFields($configHash)})) {
        my $fieldExif = getFields($configHash)->{$field}->{'EXIF'};
        if ((! $descHash->{$field}) && $fieldExif && $exifHash->{$fieldExif}) {
            beVerboseN("  Using '$field' from EXIF data: ".
                       $exifHash->{$fieldExif}, 3);

            my $func = getFields($configHash)->{$field}->{'Transform'};
            if ($func) {
                $_ = $exifHash->{$fieldExif};
                beVerbose("  Evaluating '$func' from '$_'", 4);
                eval $func;
                if ($@) {
                    beVerboseN("", 4);
                    print("Error: evaluation of transformation for $field failed.\n");
                    exit 2;
                }
                $descHash->{$field} = $_;
                beVerboseN(" to '$descHash->{$field}'.", 4);
            } else {
                $descHash->{$field} = $exifHash->{$fieldExif};
            }
        }
    }

    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);
            } elsif (commandAvailable("jpegtran-mmx")) {
                $rotateJPEG = 'jpegtran-mmx -copy all -rotate %s -outfile "%s.tmp" "%s" && mv "%s.tmp" "%s"';
                beVerboseN(" found jpegtran-mmx.", 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");
}
