#!/usr/pkg/bin/perl -w # BINS Photo Album version 1.1.25 # Copyright (C) 2001-2003 Jérôme Sautret (Jerome@Sautret.org) # # Original SWIGS code : # Copyright (C) 2000 Brendan McMahan (mcmahahb@whitman.edu) # Initial code based on IDS 0.21 : # Copyright (C) John Moose (moosejc@muohio.edu) # # $Id: bins,v 1.157 2004/02/22 19:01:21 jerome Exp $ # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; see the file COPYING. If not, write to # the Free Software Foundation, Inc., 59 Temple Place - Suite 330, # Boston, MA 02111-1307, USA. # Type "bins -h" on command line for usage information # and read bins(1) man page. my $verbose = 1; # verbosity level (from 0 to 4) use strict; # Convention: When we pass around paths and directories (aka albums), # we always include the trailing backslash, like "albums/", so we can # easily combine the directories # General Perl modules use POSIX qw(strftime); use Storable qw(dclone); use File::Glob ':glob'; use File::Basename; use File::Spec; use IO::File; use UNIVERSAL; # qw(isa); use Getopt::Long; # Image manipulation use Image::Size; use Image::Info qw(image_info); use Image::Magick; # HTML manipulation use URI::Escape; use HTML::Entities; use HTML::Template; use HTML::Clean; #use HTML::Template::JIT; # XML parsing & writing use XML::Grove; use XML::Grove::Builder; use XML::Grove::Path; use XML::Grove::AsCanonXML; use XML::Grove::PerlSAX; use XML::Parser::PerlSAX; use XML::Handler::YAWriter; use Text::Iconv; #use XML::Handler::XMLWriter; #use XML::Grove::AsString; #use XML::Handler::Composer; #use XML::Filter::Reindent; #use XML::Handler::Sample; # Debugging use Data::Dumper; #use XML::SAX::Expat; sub have_package; sub _; # alias for Getext, if present ############################################################################ # Configuration Section # ############################################################################ # I18N my $localePath = "/usr/pkg/lib/locale"; # Base locale path (for I18N) my $I18N = have_package("Locale::gettext"); if ($I18N) { require Locale::gettext; require POSIX; POSIX::setlocale(&POSIX::LC_ALL, ""); Locale::gettext::bindtextdomain("bins", $localePath); Locale::gettext::textdomain("bins"); } # You can change the following parameters in the /usr/pkg/etc/bins/binsrc or # ~/.bins/binrc configuration files or in the section of the # album or image description files (i.e. album.xml or # .jpg.xml) when it makes sense. my %defaultConfig = ( homeURL => "/", # Set this to your home page. This is # used for the leave button in some # templates. feedbackMail => "", # Put here the mailaddress of the # album-maintainer. If this is set, you # will get a mail-icon in your views that # links to this address. treePreview => 1, # If set to 1, preview-thumbnails will be # showed in the album-tree-page. backgroundImage => "", # Set this to the image that should be displayed # as the background of the album-pages. # The Image will be copied to the static-files # directory. # The name should be unique for the entire album. customStyleSheet => "", # Set this to the name that should be used # for the current album and its subalbums # The Stylesheet will be copied to the # static-files directory. # The name should be unique for the entire album. excludeBackgroundImage => 1, # If set to 1, the image with the name given # in backgroundImage will be excluded from # the current directory. addExifToDescFile => 1, # If set to 1, write exif data found in # the image file to the image desc file. deExifyImages => 1, # If set to 1, do NOT copy exif data found # in the source images to any of the generated # resized images. Setting this option can yield # significant space savings, especially for # thumbnail and imagelist pages. createEmptyDescFields => 1,# If set to 1, add empty description # fields in the section # when the image description file is # created to ease later edition with an # text editor jpegQuality => 75, # Quality of scaled jpegs (lower number = more # compression, lower quality) in 1-100 range. jpegProgressify => "smaller", # values: never, always, smaller. whether # to make jpegs progressive using jpegtran # (if available). smaller means only if # the progressive file is smaller than the # original titleOnThumbnail => 1, # Should the title be displayed on top on the # thumbnail in the thumbnails page ? emptyAlbumDesc => 0, # If set to 1, and album desciption is # not set, no message will be displayed # (instead of the "No long/short # description available" one). reverseOrder => 0, # are we reversing sorting order (see -r) # (0=none,1=dirs,2=pix,3=both) defaultSize => 1, # Size to use when user clicks directly # on the thumbnail in the thumbnails # page instead of one of the size # name. 0 is the first size (Small in # the default config), 1 the second # (Medium), and so on. Set this variable # to -1 if you don't want the thumbnail # to be clickable. thumbnailPageCycling => 1, # If set to 0 next/prev-Links will be hidden if # the actual page is the last/first Thumbnailpage imagePageCycling => 1, # If set to 0 next/prev-Links will be hidden if # the actual page is the last/first Imagepage pathImgNum => 1, # If set to 1 the path in the imageview contains # the number of the current image pathShowIcon => 1, # If set to 1 the path contains icons thumbnailInImageList => 1, # Display thumbnails on the Image List page ? albumThumbInSubAlbumPage =>1, # If set to 1, display the current # album thumbnail in sub-albums page # if it has pictures, with links to # the thumbnails page. allThumbnailsPage => 0, # If set to 1, generate a page with all # thumbnails in the album and # sub-albums. This is deactivated # because it is an alpha feature which # seems to not work properly. thumbnailBackground => 0, # If 1, add a background colour to the # thumbnail's cell in the thumbnails # page so that if the top and bottom # borders are wider than the image (for # example, if it is in portrait mode), # instead of spilling over, there is a # border around the whole picture. thumbsPerRow => 4, # Number of thumbnails displayed in each # row in an album. numThumbsPerPage => 16, # Number of thumbnails displayed # in each page in an album. previewMaxHeight => 150, # Max Thumbnail Height. previewMaxWidth => 150, # Max Thumbnail Width. thumbPrevNext => 1, # If set to 1, display thumbnails close # to the previous and next link at the # bottom of the image page. rotateImages => "destination",# Do we rotate images if the Orientation # Exif tag is found ? If set to # 'original', the original image is # rotated the first time, and then it is # left untouched. If set to # 'destination', this is all the scaled # images and thumbnails that are # rotated. This is less efficient, but # the original images are preserved. If # set to 'none', no rotation is # performed. rotateWithJpegtran => 0, # If set to 1, bins try to use the # jpegtran program to rotate JPEG images # if it is available. jpegtran is faster # and lossless, but some versions fail # to perform rotation correctly, so it # is deactivated in default config. If # set to 0 or if jpegtran is not found, # mogrify (from ImageMagick) is used. scaleIfSameSize => 0, # If set to 1, we scale the picture even # if destination size is the same as the # original picture, if set to 0, the # original image is just copied if the # size is correct. scaleMethod => "scale", # What method should be used to create # scaled pictures and thumbnails ? Can # be either scale or sample. sample is # faster, scale is better. linkInsteadOfCopy => 0, # If set to 1, we link the picture instead # of copying it if possible # (i.e. scaleIfSameSize is set to 0 and # destination image doesn't have to be # rotated : rotateImages is set to # original or none, or orientation is # already correct). updateOriginalPerms => 1, # attempt to update source image permissions enlarge => 0, # If set to 1, small images are enlarged # in the med and large series. maxAlbumsForLongSubAlbum => 20, # If the number of sub albums is greater, # generate a short sub album page # instead of the long one. stripDirPrefix => 0, # If set to 1, Numbers preceding # the album title, followed by an # underscore, are stripped. If this # parameter is set, then prefix # ordering numbers on directories # are removed. For example, if one # has directories may, june, and # august, they can be renamed # 0_may, 1_june, and 2_august and # they will appear in the album in # the correct order. This can be # overridden by the-p command line # option. compactHTML => 1, # If set to 1, generated # HTML code is cleaned up to reduce # the size of pages and thus, speed # up browsing This reduces the size # of HTML BINS files by about # 30%. See HTML::Clean(3) to know # how optimizations are performed. javaScriptPreloadImage => 1, # If set to 1, add some javascript # code in image pages to preload # the next image of the same size # when current one is loaded, to # speed up the album browsing. javaScriptPreloadThumbs => 1, # If set to 1, add some javascript # code in thumbnails pages to # preload thumbnails of the next # page when current one is loaded, # to speed up the album browsing. createHtaccess => 1, # If 1, create an Apache .htaccess file # in the root dir of the album with the # encoding charset bound to html and # htm files noRotation => '_Orig$' , # Don't perform rotation on files # matching this regexp excludeFiles => "" , # exclude image files that match # this regexp (if set). excludeDirs => '^CVS$' , #' exclude directories that match # this regexp (if set). ignore => "", # Put here a comma separated list of keyword. If # one on this keyword is found in the "ignore" # field in the 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 section of an # sub-album.xml, then this sub-album will be # hidden, i.e. it will be generated but not linked # anywhere. You can also use the -n command line # option. colorStyle => "blue", # name of the color style to use templateStyle => "default", # name of the template style to use # note that all of these options are now documented in bins.sgml; # any new options need corresponding new documentation. # The following parameters cannot be set in config files for now : globalConfigDir => "/usr/pkg/etc/bins", # System wide configuration directory globalDataDir => "/usr/local/share/bins", # System wide data directory userConfigDir => "~/.bins", # User configuration directory configFileName => "binsrc", # Configuration file. htmlEncoding => "UTF-8", # HTML pages charset encoding xmlEncoding => "UTF-8", # XML files charset encoding defaultEncoding => "ISO-8859-1", # Charset encoding of your environment. # This value is overridden by # your real local encoding as # reported by the 'locale # charmap' unix command. # This is used to display # strings on console and to # convert strings from .po # files. ); my $localEncoding = `locale charmap`; # ANSI is unspeakably primitive, keep LATIN1 instead if ($? == 0 && $localEncoding && ($localEncoding ne "ANSI_X3.4-1968")) { chop($localEncoding); $defaultConfig{defaultEncoding} = $localEncoding; beVerboseN("Forcing encoding to $localEncoding", 2); } my $local2htmlConverter; $local2htmlConverter = Text::Iconv->new($defaultConfig{defaultEncoding}, $defaultConfig{htmlEncoding}); # Here are set number, name and size of scaled images. # This can be changed in the binsrc or album.xml files. # By default, there is three sizes, but you can remove or add some new # by editing the @scaledWidths, @scaledHeights, @sizeNames and # @longSizeNames lists. # This is this size of each scaled picture : my @scaledWidths = ("40%", "64%", "100%"); # Can be either a resolution or my @scaledHeights = ("40%", "64%", "100%"); # a % of the original picture #my @scaledWidths = (200, 400, 600, 3000); #my @scaledHeights = (200, 400, 600, 3000); #This is the name of each scaled picture. Remember that the _("") #function is used for I18N only and is not mandatory. my @sizeNames = (_("Sm"), _("Med"), _("Lg")); my @longSizeNames = (_("Small"), _("Medium"), _("Large")); my @fileSizeNames = ("small.png", "medium.png", "large.png", "huge.png"); my @fileActiveSizeNames = ("smallActive.png", "mediumActive.png", "largeActive.png", "hugeActive.png"); #my @sizeNames = (_("Sm"), _("Med"), _("Lg"), _("Hg")); #my @longSizeNames = (_("Small"), _("Medium"), _("Large"), _("Huge")); $defaultConfig{scaledWidths} = \@scaledWidths; $defaultConfig{scaledHeights} = \@scaledHeights; $defaultConfig{sizeNames} = \@sizeNames; $defaultConfig{longSizeNames} = \@longSizeNames; $defaultConfig{fileSizeNames} = \@fileSizeNames; $defaultConfig{fileActiveSizeNames} = \@fileActiveSizeNames; # Fields to display (in the list order) under the picture. These # fields are defined in the %fields hash below. my @mainFields = ("description", "people", "location", "date", "event"); # Fields to display (in the list order) in the details page. These # fields are defined in the %fields hash below. my @secondaryFields = ( # DigiCam _("BINS-SECTION DigiCam Info"), "model", "owner", "firmware", # DigiCam settings for the image _("BINS-SECTION DigiCam settings for the image"), "canon_quality", "canon_image_size", "CanonContrast", "CanonSaturation", "CanonSharpness", # DigiCam settings for the photo _("BINS-SECTION DigiCam settings for the photo"), "canon_easy_shooting_mode", "canon_macro", "flash", "canon_flash_mode", "CanonISO", "iso", "exposure_time", "exposure_prog", "canon_digital_zoom", "canon_focus_mode", "CanonFocusType", "subject_distance", "metering_mode", "focal_length", "shutter_speed_value", "aperture_value", "max_aperture_value", "fnumber", "canon_timer_length", "canon_continuous_drive_mode", "focal_plane_x_resolution", "focal_plane_y_resolution", "orientation", # Image characteristics _("BINS-SECTION Image Characteristics"), "date", "file_media_type", "jpeg_type", "interlace", "color_type", "samples_per_pixel", "bits_per_sample", "resolution", "compression", "usercomment", "exif_version", "image_width", "image_length", "compressed_bits", "BINS-SECTION end", # close the last section ); my %fields = # The key is the string used as the name in the picture # description file. # Name corresponds to the string displayed under the picture. # EXIF is the name of the field in the EXIF structure # found in some JPEG images. # The value of the EXIF structure is only used if no value # is present in the picture description file. # Transform is a Perl operator used to convert an exif value # to the desired format to display. ( "title" => { Name => _("Title"), }, "description" => { Name => _("Description"), }, "people" => { Name => _("People"), }, "location" => { Name => _("Location"), }, "date" => { Name => _("Date"), EXIF => "DateTimeOriginal", Transform => 's%^(\d+):(\d+):(\d+) (.*)$%$3/$2/$1 $4%', # french date }, # English version is yyyy:mm:dd hh:mm:ss to yyyy/mm/dd hh:mm:ss : # "s%^(\\d+):(\\d+):(\\d+) (.*)$%\$1/\$2/\$3 \$4%", # French version is yyyy:mm:dd hh:mm:ss to dd/mm/yyyy hh:mm:ss : # 's%^(\d+):(\d+):(\d+) (.*)$%$3/$2/$1 $4%', "event" => { Name => _("Event"), }, "model" => { Name => _("Camera Model"), EXIF => "Model", }, "firmware" => { Name => _("Software"), # EXIF => "Canon-Tag-0x0007", EXIF => "Software", Tip => _("Firmware (internal software of digicam) version number."), }, "owner" => { Name => _("Owner name"), # EXIF => "Canon-Tag-0x0009", EXIF => "Owner", Tip => _("Name of the owner of the digicam."), }, "flash" => { Name => _("Flash"), EXIF => "Flash", }, "usercomment" => { Name => _("User comment"), EXIF => "UserComment", }, "file_media_type" => { Name => _("File Media Type"), EXIF => "file_media_type", Tip =>_("This is the MIME type that is appropriate for the given file format."), }, "color_type" => { Name => _("Color Type"), EXIF => "color_type", }, "jpeg_type" => { Name => _("JPEG Type"), EXIF => "JPEG_Type", }, "interlace" => { Name => _("Interlace method"), EXIF => "Interlace", Tip => _("Interlace method used."), }, "metering_mode" => { Name => _("Metering Mode"), EXIF => "MeteringMode", Tip => _("Exposure metering method."), }, "samples_per_pixel" => { Name => _("Samples Per Pixel"), EXIF => "SamplesPerPixel", Tip => _("This says how many channels there are in the image. For some image formats this number might be higher than the number implied from the \"Color Type\""), }, "resolution" => { Name => _("Physical Resolution"), EXIF => "resolution", Tip => _("The value of this field normally gives the physical size of the original image on screen or paper. When there is no unit then this field denotes the squareness of pixels in the image."), }, "compression" => { Name => _("Compression Algorithm"), EXIF => "Compression", }, "exif_version" => { Name => _("Exif Version"), EXIF => "ExifVersion", }, "subject_distance" => { Name => _("Subject Distance"), EXIF => "SubjectDistance", Tip => _("Distance to focus point."), }, "bits_per_sample" => { Name => _("Bits Per Sample"), EXIF => "BitsPerSample", Tip => _("This says how many bits are used to encode each of samples."), }, "exposure_time" => { Name => _("Exposure Time"), EXIF => "ExposureTime", Tip => _("Exposure time (reciprocal of shutter speed)."), }, "shutter_speed_value" => { Name => _("Shutter Speed Value"), EXIF => "ShutterSpeedValue", Tip => _("Shutter speed by APEX value."), }, "focal_length" => { Name => _("Focal Length"), EXIF => "FocalLength", Tip => _("Focal length of lens used to take image."), }, "aperture_value" => { Name => _("Aperture Value"), EXIF => "ApertureValue", Tip => _("The actual aperture value of lens when the image was taken."), }, "max_aperture_value" => { Name => _("Maximum Aperture Value"), EXIF => "MaxApertureValue", Tip => _("Maximum aperture value of lens."), }, "fnumber" => { Name => _("F-Number"), EXIF => "FNumber", Tip => _("The actual F-number (F-stop) of lens when the image was taken."), }, "focal_plane_y_resolution" => { Name => _("Focal Plane Y Resolution"), EXIF => "FocalPlaneYResolution", Tip => _("Pixel density at CCD's position. If you have MegaPixel digicam and take a picture by lower resolution (e.g.VGA mode), this value is re-sampled by picture resolution. In such case, Focal Plane Y Resolution is not same as CCD's actual resolution."), }, "focal_plane_x_resolution" => { Name => _("Focal Plane X Resolution"), EXIF => "FocalPlaneXResolution", Tip => _("Pixel density at CCD's position. If you have MegaPixel digicam and take a picture by lower resolution (e.g.VGA mode), this value is re-sampled by picture resolution. In such case, Focal Plane X Resolution is not same as CCD's actual resolution."), }, "canon_macro" => { Name => _("Macro"), EXIF => "CanonMacro", #Tip => _(""), }, "canon_timer_length" => { Name => _("Timer Length"), EXIF => "CanonTimerLength", #Tip => _(""), }, "canon_quality" => { Name => _("Quality"), EXIF => "CanonQuality", Tip => _(""), }, "canon_continuous_drive_mode" => { Name => _("Continuous Drive Mode"), EXIF => "CanonContinuousDriveMode", #Tip => _(""), }, "canon_flash_mode" => { Name => _("Flash Mode"), EXIF => "CanonFlashMode", #Tip => _(""), }, "canon_focus_mode" => { Name => _("Focus Mode"), EXIF => "CanonFocusMode", #Tip => _(""), }, "canon_image_size" => { Name => _("Image Size"), EXIF => "CanonImageSize", #Tip => _(""), }, "canon_digital_zoom" => { Name => _("Digital Zoom"), EXIF => "CanonDigitalZoom", #Tip => _(""), }, "canon_easy_shooting_mode" => { Name => _("Easy Shooting Mode"), EXIF => "CanonEasyShootingMode", #Tip => _(""), }, "CanonContrast" => { Name => _("Contrast"), EXIF => "CanonContrast", #Tip => _(""), }, "CanonSaturation" => { Name => _("Saturation"), EXIF => "CanonSaturation", #Tip => _(""), }, "CanonSharpness" => { Name => _("Sharpness"), EXIF => "CanonSharpness", #Tip => _(""), }, "CanonISO" => { Name => _("ISO"), EXIF => "CanonISO", #Tip => _(""), }, "iso" => { Name => _("ISO"), EXIF => "ISOSpeedRatings", #Tip => _(""), }, "CanonFocusType" => { Name => _("Focus Type"), EXIF => "CanonFocusType", #Tip => _(""), }, "exposure_prog" => { Name => _("Exposure Program"), EXIF => "ExposureProgram", #Tip => _(""), }, "image_width" => { Name => _("Original Image Width"), EXIF => "ExifImageWidth", #Tip => _(""), }, "image_length" => { Name => _("Original Image Length"), EXIF => "ExifImageLength", #Tip => _(""), }, "compressed_bits" => { Name => _("Compression Quality"), EXIF => "CompressedBitsPerPixel", #Tip => _(""), }, "Orientation" => { Name => _("Orientation"), EXIF => "Orientation", #Tip => _(""), }, "" => { Name => _(""), EXIF => "", #Tip => _(""), }, ); my @priorityExifTags = (); # the field in this list are taken from # the desc file, even if they are present # in image file (normally, image field # takes precedence on desc file # these substitutions are made in all templates, useful for doing easy # color assignments, etc. The colors can be set in the bins/colors # section of config files or album/pictures desc files. my %colorsSubs = (blue => { PAGE_BACKCOLOR => "#FFFFFF", PAGE_TITLECOLOR => "#000000", MAINBAR_BACKCOLOR => "#000077", MAINBAR_TITLECOLOR => "#FFFFFF", MAINBAR_LINKCOLOR => "#eedd82", MAINBAR_CURRENTPAGECOLOR => "#d2d2d2", SUBBAR_BACKCOLOR => "#6060af", SUBBAR_LINKCOLOR => "#eedd82", SUBBAR_CURRENTPAGECOLOR => "#000000", SUBBAR_TITLECOLOR => "#FFFFFF", }); $defaultConfig{colorsSubs} = \%colorsSubs; # Strings to translate in the HTML template pages (if I18N is used) my %intlSubs = ( STRING_THUMBNAILS => _("thumbnails"), STRING_IMAGELIST => _("Image List"), STRING_HOME => _("Home"), STRING_UP => _("Up one album"), STRING_PREV => _("previous"), STRING_NEXT => _("next"), STRING_FIRST => _("first"), STRING_LAST => _("last"), STRING_SUBALBUMS => _("Sub Albums"), STRING_INTHISALBUM => _("In This Album"), STRING_BACKTOTHEIMAGE => _("Back to the image"), STRING_IMAGE => _("Image"), STRING_ALBUMTREE => _("Album Tree"), STRING_ALBUMGENERATEDBY => _("Album generated by"), STRING_FEEDBACK => _("Send Feedback"), STRING_YOURIMAGE => _("Your Image"), STRING_YOURALBUM => _("Your Album"), BINS_VERSION => "1.1.25", ENCODING => $defaultConfig{htmlEncoding}, GENERATED_DATE => _("on "). local2html(strftime("%c", localtime)), BINS_ID => '', ); # @knownImageExtentions defines file extensions that BINS can handle as # input image. BINS _should_ handle all input format of ImageMagick # (see ImageMagick(1) man page), but there is some formats that cause # problems. If you have tested successfully a format that is not # listed here, or if you have a problem with one of the following # format, please let me know. my @knownImageExtentions = ("jpg", "jpeg", "gif", "png", "tiff", "bmp", "tga", "ps", "eps", "fit", "pcx", "miff", "pix", "pnm", "rgb", "im1", "xcf", "xwd", "xpm", "avs", "dcm", "dcx", "dib", "dps", "dpx", "epdf", "epi", "ept", "fpx", "icb", "mat", "mtv", "pbm", "pcd", "pct", "pdb", "ppm", "ptif", "pwp", "ras", "thm", ); my @webFormats = ("JPEG", "GIF", "PNG"); # Image formats that can go # into the web album (other # formats will be converted # to JPEG). my @filesToLinkExtensions = ( "avi", "mpg", "mpeg", "mov" ); ############################################################################ # End of Configuration Section # ############################################################################ #subroutine declarations sub usage; sub beVerbose; sub beVerboseN; sub min; sub readConfigFile; sub fileSize; sub generateAlbumPages; sub filenameToPreviewName; sub getSizeLinks; sub openTemplate; sub doSubstitutions; sub renderPage; sub generateTreeLoop; sub generateImage; sub getRootDir; #sub generateScaledImage; sub getDesc; sub getExif; sub readField; sub trimWhiteSpace; sub stringToBool; sub ignoreSet; sub generateThumbnailPages; sub generateThumbEntry; sub generateThumbPage; sub generateImagesInAlbum; sub write_htaccess; print "\nBINS Photo Album 1.1.25 (http://bins.sautret.org/)\n"; print "Copyright © 2001,2002 Jérôme Sautret (Jerome\@Sautret.org)\n"; print "Some parts of code:\n"; print "Copyright © 2000 Brendan McMahan (mcmahahb\@whitman.edu)\n"; print "Copyright © John Moose (moosejc\@muohio.edu)\n\n"; print "This is free software with ABSOLUTELY NO WARRANTY.\n"; print "See COPYING file for details.\n\n"; # EVG (Evil Global Variables) # Some on them should be moved to the config hash so they can be # managed by config files my $ignoreOpts=""; # to ignore some albums (see -i) my $hiddenOpts=""; # to hide some albums my $genEditableAlbum; # are we creating a editable album (see -e) my ($imageSource, $oneCopy); # How to handle scaled images my $appendToDescFile; # write to desc file ? (see -d) my $templateDir; my ($picdir, $albumdir); # source and destination directories # charset converters my ($xml2htmlConverter, $html2xmlConverter, $xml2localConverter); my $optimizeConversion = 0; # this cause problem if set to 1, # but may help on Sun Solaris. main(); # process command line arguments before reading config file sub preProcessArgs { my $configHash = shift; # process args my %option; Getopt::Long::Configure("bundling"); die "Invalid options\n" if (!GetOptions(\%option, "h", "p", "r:s", "e", "o:s", "t=s", "d=s", "s=s", "c=s", "v:i", "i=s", "n=s", "f=s")); if (defined($option{v})) { $verbose = $option{v}; } beVerboseN("Verbosity level is set to $verbose.", 2); #get the config file from the command line if ( $option{'f'} ) { (my $junk ,$defaultConfig{'userConfigDir'},$defaultConfig{'configFileName'}) = File::Spec->splitpath($option{'f'}); $defaultConfig{'userConfigDir'} =~ s|/*$||; } return \%option; } # process command line arguments after reading config file sub postProcessArgs { my $option = shift; my $configHash = shift; my %option = %{$option}; if (defined($option{i})) { $ignoreOpts="$option{i}"; beVerboseN("Ignore is set to $ignoreOpts.", 2); } if (defined($option{n})) { $hiddenOpts="$option{n}"; beVerboseN("Hidden is set to $hiddenOpts.", 2); } $genEditableAlbum = ($option{e} ? 1 : 0); if (defined($option{s})) { $configHash->{templateStyle} = $option{s}; } $imageSource = "scaled"; $oneCopy = 0; if (defined($option{o})) { $oneCopy = 1; $imageSource = ($option{o} ? $option{o} : "scaled"); } if (defined($option{d})) { $appendToDescFile = $option{d}; } else { $appendToDescFile = "always"; } die 'invalid option for switch -d. Must be one of "always",'. '"never" or "exist"' if ( $appendToDescFile ne "always" && $appendToDescFile ne "never" && $appendToDescFile ne "exist"); if ($option{t}) { $templateDir = $option{t}; die "template location $templateDir doesn't exist" if (! -d $templateDir); #if ( substr($templateDir,-1,1) eq "/" ) { # $templateDir .= "/"; } if ($option{c}) { $configHash->{colorStyle} = $option{c}; beVerboseN("Color style $option{c} selected.", 2); } $configHash->{reverseOrder} ||= 0; if (defined($option{r})) { if ($option{r} =~ "dirs") { $configHash->{reverseOrder} = 1; } if ($option{r} =~ "pictures") { $configHash->{reverseOrder} += 2; } } #$stripOrderNum = ($option{p} ? 1 : 0); if ($option{p}) { $configHash->{stripDirPrefix} = 1; } my $printHelp = ($option{h} ? 1 : 0); if ($printHelp) { usage(); exit 1; } if ( $oneCopy && !($imageSource =~ /^(scaled|copied|custom)$/) ) { print "\nInvalid image source for -o. If you are leaving the src\n"; print "argument off to get the default, put the -o switch at the\n"; print "end of the line, or use \"-o -\" to get the default.\n\n"; usage(); exit 1; } # directories if ($#ARGV < 1) { print "source_dir and target_dir are required.\n"; print "Type bins -h for more help.\n"; exit 1; } $picdir = $ARGV[0]; $albumdir = $ARGV[1]; $picdir = File::Spec->rel2abs(File::Spec->canonpath($picdir)); $albumdir = File::Spec->rel2abs(File::Spec->canonpath($albumdir)); $picdir =~ s|/*$|/|; $albumdir =~ s|/*$|/|; die "You must specify a source (picture) directory.\n" if (!$picdir); die "You must specify a target (web) directory.\n" if (!$albumdir); #print "\$oneCopy = $oneCopy\n"; #print "\$imageSource = $imageSource\n" if $oneCopy; #print "\$templateDir = $templateDir\n"; #print "\$picdir = $picdir\n"; #print "\$albumdir = $albumdir\n"; } sub initConverters { if (! $optimizeConversion || $defaultConfig{xmlEncoding} ne $defaultConfig{htmlEncoding}) { $xml2htmlConverter = Text::Iconv->new($defaultConfig{xmlEncoding}, $defaultConfig{htmlEncoding}); $html2xmlConverter = Text::Iconv->new($defaultConfig{htmlEncoding}, $defaultConfig{xmlEncoding}); } if ($verbose >= 1 && (!$optimizeConversion || $defaultConfig{xmlEncoding} ne $defaultConfig{defaultEncoding})) { $xml2localConverter = Text::Iconv->new($defaultConfig{xmlEncoding}, $defaultConfig{defaultEncoding}); } beVerboseN("Your system charset encoding is ". $defaultConfig{defaultEncoding}, 2); } # Convert from XML encoding to HTML encoding sub xml2html{ if ($optimizeConversion && $defaultConfig{xmlEncoding} eq $defaultConfig{htmlEncoding}){ return shift; } return $xml2htmlConverter->convert(shift); } # Convert from HTML encoding to XML encoding sub html2xml{ if ($optimizeConversion && $defaultConfig{xmlEncoding} eq $defaultConfig{htmlEncoding}){ return shift; } return $html2xmlConverter->convert(shift); } # Convert from XML encoding to local encoding sub xml2local{ if ($optimizeConversion && $defaultConfig{xmlEncoding} eq $defaultConfig{defaultEncoding}){ return shift; } return $xml2localConverter->convert(shift); } # Convert from local encoding to XML encoding sub local2html{ if ($optimizeConversion && $defaultConfig{htmlEncoding} eq $defaultConfig{defaultEncoding}){ return shift; } return $local2htmlConverter->convert(shift); } ##### main ##### sub main{ my @recursiveImageData; # create charset converters initConverters(); # pre process command line args before reading config files my $options = preProcessArgs(\%defaultConfig); # read configurations files my $defaultConfig = readConfigFile(\%defaultConfig); # post process command line args after reading config files postProcessArgs($options, $defaultConfig); # Create the Apache .htaccess for charset encoding write_htaccess($albumdir, $defaultConfig); # generate the root directory, do recursive traversal of all subalbums my %rootAlbumHash = generateAlbumPages("", \@recursiveImageData, $defaultConfig); # and finally create the tree page. generateTree($rootAlbumHash{config}, %rootAlbumHash); } # Test if a package is installed on the system at run time. # We use it to test LOCALE::Gettext is here (or else, we don't do any I18N). sub have_package { my $name = shift(@_); $name =~ s%::%/%g; foreach my $prefix (@INC) { if (-f "$prefix/$name.pm") { return 1; } } return 0; } # return translated string with HTML encoding sub _ { if ($I18N) { return local2html(Locale::gettext::gettext(shift)); } return local2html(shift); } # return translated string without changing encoding sub translate { if ($I18N) { return Locale::gettext::gettext(shift); } return shift; } BEGIN{ my @done; # list of template styles which have their static dir already copied sub write_static_dir{ my $destDir = shift; my $configHash = shift; if (grep(/^$configHash->{templateStyle}$/, @done)) { return; } push @done, $configHash->{templateStyle}; my $staticDir = templateStaticDir($configHash); $destDir =~ s%/$%%; $destDir .= "/static.".$configHash->{templateStyle}; if ($staticDir) { if (! -d "$destDir") { mkdir $destDir, 0755 or die("\nCannot create $destDir: $?"); } system("cp", "-R", bsd_glob("$staticDir/*", GLOB_TILDE), "$destDir") == 0 or die("\nCannot copy $staticDir directory content to $destDir: $?"); } else { beVerboseN(" Cannot find any static template directory.", 4); } } } sub write_bg_image { my $album = shift; my $configHash = shift; my $staticDir = templateStaticDir($configHash); my $destDir = $albumdir; $destDir =~ s%/$%%; $destDir .= "/static.".$configHash->{templateStyle}; if (! -d "$destDir") { mkdir $destDir, 0755 or die("\nCannot create $destDir: $?"); } system("cp", "-p", "$picdir$album$configHash->{backgroundImage}", "$destDir") == 0 or die("\nCannot copy file $configHash->{backgroundImage} to $destDir: $?"); } sub write_custom_css { my $album = shift; my $configHash = shift; my $staticDir = templateStaticDir($configHash); my $destDir = $albumdir; $destDir =~ s%/$%%; $destDir .= "/static.".$configHash->{templateStyle}; if (! -d "$destDir") { mkdir $destDir, 0755 or die("\nCannot create $destDir: $?"); } system("cp", "-p", "$picdir$album$configHash->{customStyleSheet}", "$destDir") == 0 or die("\nCannot copy file $configHash->{customStyleSheet} to $destDir: $?"); } sub write_htaccess{ my $dir = shift; my $configHash = shift; if (! $configHash->{createHtaccess}) { return } my $file = $dir."/.htaccess"; mkdir("$dir", 0755) if (! -d "$dir"); # 20030422 Hack by Yves Mettier # don't overwrite .htaccess if it is already OK if(open(FILE, $file)) { my $encoding = $configHash->{htmlEncoding}; while() { if(/AddDefaultCharset $encoding/) { close (FILE) || die ("can't close $file ($!)"); return; } if(/AddType text\/html;charset=$encoding html htm/) { close (FILE) || die ("can't close $file ($!)"); return; } } close (FILE) || die ("can't close $file ($!)"); } # End of hack beVerboseN("Writing .htaccess file for album with ". "charset encoding $configHash->{htmlEncoding}.", 2); open(FILE, ">>",$file) or die("Cannot write to file $file ($!)"); printf(FILE "AddDefaultCharset %s\n", ($configHash->{htmlEncoding})); close (FILE) || die ("can't close $file ($!)"); } sub generateTree{ # album hash -- other entries as defined in getAlbumInfo # hash{subalbums} -- returns a ref to a list of subalbums to the album # each entry in the list is a ref to an albumhash of this format my ($configHash, %albumHash) = @_; my $tableHTML = "\n".&generateTreeUL(%albumHash)."\n"; my %subsHash; $subsHash{TREE_TABLE} = $tableHTML; $subsHash{TREE_LOOP} = generateTreeLoop(%albumHash); #beVerboseN("Generate tree Table html:\n $tableHTML ", 3); $subsHash{STATIC_PATH} = "static.".$configHash->{templateStyle}; if ($configHash->{backgroundImage}) { # Do not set this if not configured, so that template # can check for whether defined. $subsHash{BG_IMAGE} = $subsHash{STATIC_PATH}."/".$configHash->{backgroundImage}; } $subsHash{CUSTOM_CSS} = $configHash->{customStyleSheet}; $subsHash{HOME_LINK} = $configHash->{homeURL}; $subsHash{ALBUM_THUMB} = $configHash->{treePreview}; $subsHash{PATH_SHOW_ICON} = $configHash->{pathShowIcon}; renderTemplate("tree", $albumdir."tree.html", \%subsHash, $configHash); } sub generateTreeUL{ my %albumHash = @_; my $link = "". $albumHash{title}.""; my $UL = "
    \n
  • $link\n"; my @subAlbumHashRefList = @{$albumHash{subalbums}}; # may be empty my $subAlbumHashRef; foreach $subAlbumHashRef (@subAlbumHashRefList) { $UL .= &generateTreeUL(%{$subAlbumHashRef})."\n"; } $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} "._("images"), TREE_SAMPLE => $prepath . $preimage, TREE_ALT => _(""), TREE_SAMPLEID => $preid, TREE_HASCHILD => @subAlbumHashRefList > 0, }); if (@subAlbumHashRefList) { foreach $subAlbumHashRef (@subAlbumHashRefList) { my $array = generateTreeLoop(%{$subAlbumHashRef}); push(@result, @$array); } push(@result, {TREE_OUT => "1"}); } return \@result; } sub usage { my $commandname = $0; $commandname =~ s/^.*\///; print < fields in 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 fields in 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() { chomp; s/^\s+//; s/\s+$//; next if /^#/ || /^$/; if(/^\./) { s/^\.\s*//; } else { push(@filesInAlbum, $_) if $isfile{$_}; } $isfile{$_} = 0; } close INLIST; $#tmpFiles = -1; foreach (keys %isfile) { push(@tmpFiles, $_) if $isfile{$_} == 1; } } if ($configHash->{reverseOrder} & 2) { push(@filesInAlbum, sort {$b cmp $a} @tmpFiles); } else { push(@filesInAlbum, sort @tmpFiles); } my($fileInAlbum, @urlimageList, @imageList, @xlinkList, $numAlbums); $numAlbums = 0; foreach $fileInAlbum (@filesInAlbum) { if (-d "$picdir$album$fileInAlbum") { # Is this a subdirectory? my %localAlbumHash = generateAlbumPages($album.$fileInAlbum."/", $recursiveImageData, $configHash, @parentDirNames); # If the "ignore" keyword matches one of the ones passed in the # command line, don't push or count this album. if (! ignoreAndHiddenSet($localAlbumHash{ignore}, $fileInAlbum, $configHash) ) { push(@subalbumHashList, \%localAlbumHash); $numAlbums++; } } else { my $known = 0; foreach my $ext (@knownImageExtentions){ if ($fileInAlbum =~ /\.$ext\Z/i) { $known = 1; last; } } if ($known) { # this is a known image format--remember its name push @imageList, $fileInAlbum; } else { foreach my $ext (@filesToLinkExtensions){ if ($fileInAlbum =~ /\.$ext\Z/i) { $known = 1; last; } } if ($known) { push @xlinkList, $fileInAlbum; my $from="$picdir$album$fileInAlbum"; my $to="$albumdir$album$fileInAlbum"; if ( ! -f $to ) { `cp -p "$from" "$to"`; } } } } } #get virtual images to include my @virtualInclude = getVirtualInclude($album); #print "$_, " foreach (@virtualInclude); push(@imageList, @virtualInclude); $albumHash{subalbums} = \@subalbumHashList; $albumHash{numImages} = $#imageList+1; # note that if (! @imageList), # $#imageList = -1 $albumHash{numSubAlbums} = $numAlbums; $albumHash{numXLinks} = $#xlinkList + 1; # decide whether index page is first thumbnail page (or subalbum page) my $firstIsIndex; if ($numAlbums == 0 ) { $firstIsIndex = 1; } else { $firstIsIndex = 0; } $albumHash{thumbIsIndex} = $firstIsIndex; # generate image pages and get image data my @imageData = generateImagesInAlbum($album, \%albumHash, $firstIsIndex, $configHash, @imageList); $albumHash{sampleimage} = chooseSampleImage(\%albumHash, \@imageData) if (! $albumHash{sampleimage}); # generate thumbnail pages generateThumbnailPages($album, \%albumHash, $firstIsIndex, $configHash, \@xlinkList, @imageData); # generate image list page if ($albumHash{numImages} > 0) { generateImageListPage($album, \%albumHash, \@imageData, \@xlinkList, $configHash); } beVerboseN(" sample image for $album is $albumHash{sampleimage}", 3); # generate subalbum page if ($numAlbums > 0) { generateSubAlbumPage($album, \%albumHash, $recursiveImageData, $configHash); } if ($configHash->{allThumbnailsPage}) { # Munge the @imageData so that it includes path information, and dump this # in @recursiveImageData. for my $thisImageData ( @imageData ) { $thisImageData->{'thumblink'}=$album.$thisImageData->{'thumblink'}; for my $width ( 0..$#{$configHash->{scaledWidths}} ) { $thisImageData->{$width}{'htmlFile'}=$album. $thisImageData->{$width}{'htmlFile'}; } push(@{$recursiveImageData}, $thisImageData); } } return %albumHash; } # if we don't have a sample image from the initial getAlbumInfo # then we pick the first one. If we don't have images in this # album, choose a sampleimage from a subalbum. If that fails, # then no sampleimage. # returns name of sampleimage (a file with path info) sub chooseSampleImage{ my ( $albumHashRef, $imageDataRef) = @_; my @imageData = @{ $imageDataRef }; if (@imageData) { my $th = $imageData[0]->{thumblink}; $th =~ s/_pre\.jpg$/.jpg/; # print "Returning \$imageData[0] = $th\n"; return uri_escape($albumHashRef->{dirname})."/".$th; }else{ my @subAlbumHashList = @{ $albumHashRef->{subalbums} }; my $subAlbumHashRef; foreach $subAlbumHashRef (@subAlbumHashList) { if ($subAlbumHashRef->{sampleimage}) { #print "Returning \$subAlbumHashRef->{sampleimage} = ". # "$subAlbumHashRef->{sampleimage}\n"; return uri_escape($albumHashRef->{dirname})."/". $subAlbumHashRef->{sampleimage}; } } } return ""; } # takes in an image name (with some or no path info), and returns # preview name with equivalent path info sub filenameToPreviewName { my $imageName = shift(@_); my($base, $path, $type); ($base,$path,$type) = fileparse($imageName, '\.[^.]+\z'); # what the thumbnail will be named: my($newPreviewName) = $path.$base . '_pre.jpg'; beVerboseN("Preview name for $imageName is $newPreviewName", 3); return $newPreviewName; } sub getVirtualInclude{ my $album = shift(@_); my $virtualImageFile = $picdir.$album."include_images.txt"; return () if (! -e $virtualImageFile); open (INCLUDE, $virtualImageFile) || die ("cannot open $virtualImageFile for reading: ($!)"); my @include; LINE: while () { 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}; # 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") . " ". ($i+1), NAV_ICON => "thumbnails.png"}; }else{ push @result, {NAV_NAME => _("Thumbnail Page") . " ". ($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 "._("image"); } elsif ($numImages > 1) { $numInfo = "$numImages "._("images"); } $numInfo .= ", " if (length $numInfo > 0 && $numXLinks > 0); if ($numXLinks == 1) { $numInfo .= "1 "._("media file"); } elsif ($numXLinks > 1) { $numInfo .= "$numXLinks "._("media files"); } $numInfo .= ", " if (length $numInfo > 0 && $numAlbums > 0); if ($numAlbums == 1) { $numInfo .= "1 "._("subalbum"); } elsif ($numAlbums > 1) { $numInfo .= "$numAlbums "._("subalbums"); } return $numInfo; } sub generateImageListPage{ my ($album, $albumHashRef, $imageDataRef, $xlinksRef, $configHash) = @_; my %albumHash = %{$albumHashRef}; my @imageData = @{$imageDataRef}; my $pwd; # hash for final substitutions my %finalsubs; $finalsubs{NUM_INFO} = getAlbumNumInfo(%albumHash); $finalsubs{ALBUM_TITLE} = $albumHash{title}; $finalsubs{NAV_BAR_TABLE} = navBarLinks('imagelist', $album, $configHash, %albumHash); $finalsubs{ALBUM_PATH_LINKS} = pathLinks($album, 0, @{$albumHash{parentDirNames}}); $finalsubs{TREE_NAME} = _("tree"); $finalsubs{TREE_TITLE} = _("Tree of all albums and sub-albums"); $finalsubs{TREE_LINK} = getRootDir($album)."tree.html"; if ($#{$albumHash{parentDirNames}} > 0) { $finalsubs{UP_NAME} = _("up"); $finalsubs{UP_TITLE} = _("Up one subalbum"); $finalsubs{UP_LINK} = "../index.html"; } if ($genEditableAlbum) { $pwd = `pwd`; chop($pwd); $pwd = uri_escape($pwd, '^-A-Za-z0-9/_\.'); $finalsubs{ALBUM_DESC} = "". $albumHash{longdesc}.""; } 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} = "". $imageInfo{title}.""; } else { $tablesubs{TITLE} = $imageInfo{title}; } $tablesubs{SIZE_LINKS} = \@sizes; my $atLeastOneField = 0; my $tagValue; $tablesubs{DESC_TABLE} = getDescTable(%imageInfo); if ($configHash->{defaultSize} >= 0){ $tablesubs{THUMB_DEFAULT_SIZE} = $imageInfo{$configHash->{defaultSize}}{'htmlFile'}; } if ($configHash->{thumbnailInImageList}) { %tablesubs = ((THUMB_LINK => $imageInfo{'thumblink'}, THUMB_WIDTH => $imageInfo{'twidth'}, THUMB_HEIGHT => $imageInfo{'theight'}, THUMB_ALT => _("Click on one of the size names above to enlarge this image"), THUMB_LINK_TITLE => _("Click on one of the size names above to enlarge this image"), ), %tablesubs); } push @tables, \%tablesubs; } $finalsubs{XLINK} = getXLinks($xlinksRef); $finalsubs{STATIC_PATH} = getRootDir($album)."static.".$configHash->{templateStyle}; if ($configHash->{backgroundImage}) { # Do not set this if not configured, so that template # can check for whether defined. $finalsubs{BG_IMAGE} = $finalsubs{STATIC_PATH}."/".$configHash->{backgroundImage}; } $finalsubs{CUSTOM_CSS} = $configHash->{customStyleSheet}; $finalsubs{ROOT_PATH} = getRootDir($album); $finalsubs{IMAGE_LIST_TABLE} = \@tables; $finalsubs{HOME_LINK} = $configHash->{homeURL}; $finalsubs{FEEDBACK_LINK} = $configHash->{feedbackMail}; $finalsubs{PATH_SHOW_ICON} = $configHash->{pathShowIcon}; renderTemplate("imagelist", $albumdir.$album."imagelist.html", \%finalsubs, $configHash); } sub generateSubAlbumPage{ my ($album, $albumHashRef, $recursiveImageData, $configHash) = @_; my $numAlbums = $albumHashRef->{numSubAlbums}; if (1 || $numAlbums <= $configHash->{maxAlbumsForLongSubAlbum}) { generateLongSubAlbumPage($album, $albumHashRef, $configHash); }else{ # Short album page is not is not supported for the moment generateShortSubAlbumPage($album, $albumHashRef, $configHash); } if ($configHash->{allThumbnailsPage}){ # Create a thumbnail page of all images generateThumbPage($album, $albumHashRef, "-2", "1", "allthumbs.html", "", "", "", $configHash, undef, undef, $recursiveImageData->[0..$#{$recursiveImageData}]); } } # Short album page is not supported for the moment sub generateShortSubAlbumPage{ my ($album, $albumHashRef, $configHash) = @_; my %albumHash = %{$albumHashRef}; my $pwd = `pwd`; chop($pwd); $pwd = uri_escape($pwd, '^-A-Za-z0-9/_\.'); # hash for final subsitutions my %finalsubs; $finalsubs{''} = getAlbumNumInfo(%albumHash); $finalsubs{''} = $albumHash{title}; $finalsubs{''} = getNavBarLinks('subalbum', $album, $configHash, %albumHash); $finalsubs{''} = getPathLinks($album, 0, @{$albumHash{parentDirNames}}); if ($genEditableAlbum) { $finalsubs{''} = "'} .= $pwd."/"; } $finalsubs{''} .= $albumHash{descFileName}."\">". $albumHash{longdesc}.""; } else { $finalsubs{''} = $albumHash{longdesc}; } my $titleRowText = '   '; my $descRowText = '     '; my $subalbumHashRef; my $tableData = ''; my @subalbumHashList = @{$albumHash{subalbums}}; for $subalbumHashRef (@subalbumHashList) { my %subshash; my %albuminfo = %{$subalbumHashRef}; $subshash{''} = &getAlbumNumInfo(%albuminfo); $subshash{''} = $albuminfo{title}; $subshash{''} = uri_escape($albuminfo{dirname}). "/index.html"; if ($genEditableAlbum) { $subshash{''} = "$albuminfo{shortdesc}"; } else { $subshash{''} = $albuminfo{shortdesc}; } $tableData .= doSubstitutions($titleRowText, $configHash, %subshash); $tableData .= doSubstitutions($descRowText, $configHash, %subshash) if (! $subshash{''} =~ /No short description available/); #print "table after SUBS\n: $tableData\n";w } $tableData .= '
'; $finalsubs{''} = $tableData; my $pageContent = openTemplate("subalbum"); $pageContent = doSubstitutions($pageContent, $configHash, %finalsubs); renderPage($albumdir.$album."index.html", $pageContent); } sub generateLongSubAlbumPage{ my ($album, $albumHashRef, $configHash) = @_; my %albumHash = %{$albumHashRef}; # hash for final subsitutions my %templateParameters = ( NUM_INFO => getAlbumNumInfo(%albumHash), ALBUM_TITLE => $albumHash{title}, NAV_BAR_TABLE => navBarLinks('subalbum', $album, $configHash, %albumHash), ALBUM_PATH_LINKS => pathLinks($album, 0, @{$albumHash{parentDirNames}}), TREE_NAME => _("tree"), TREE_TITLE => _("Tree of all albums and sub-albums"), TREE_LINK => getRootDir($album)."tree.html", ); if ($configHash->{albumThumbInSubAlbumPage} && $albumHash{numImages} > 0) { my $sampleImage = "./".filenameToPreviewName($albumHash{sampleimage}); if ($albumHash{dirname} ne "") { $sampleImage = "../$sampleImage"; } %templateParameters = (%templateParameters, ( ALBUM_THUMB => 1, DESC => $albumHash{shortdesc}, TITLE => $albumHash{title}, CURRENT_NUM_INFO => ($albumHash{numImages} > 1) ? $albumHash{numImages}." "._("images") : "1 "._("image"), LINK => "thumb0.html", THUMB_ALT => _("Click to view thumbnails of the current album"), THUMB_LINK_TITLE => _("Click to view thumbnails of the current album"), THUMB_LINK => $sampleImage, ) ); } my $pwd; if ($genEditableAlbum) { $pwd = `pwd`; chop($pwd); $pwd = uri_escape($pwd, '^-A-Za-z0-9/_\.'); $templateParameters{ALBUM_DESC} = "". $albumHash{longdesc}.""; } else { $templateParameters{ALBUM_DESC} = $albumHash{longdesc}; } my @subAlbunTable; my $cnt = 0; for my $subalbumHashRef (@{$albumHash{subalbums}}) { my %albuminfo = %{$subalbumHashRef}; $cnt += 1; my %subshash = ( NUM_INFO => getAlbumNumInfo(%albuminfo), TITLE => $albuminfo{title}, LINK => uri_escape($albuminfo{dirname}, '^-A-Za-z0-9/_\.')."/index.html", THUMB_ALT => _("Click to view this album"), THUMB_LINK_TITLE => _("Click to view this album"), THUMB_LINK => filenameToPreviewName($albuminfo{sampleimage}), THUMB_NUM => $cnt ); if ($genEditableAlbum) { $subshash{'DESC'} = "". $albuminfo{shortdesc}.""; } else { $subshash{'DESC'} = $albuminfo{shortdesc}; } push @subAlbunTable, \%subshash; } $templateParameters{SUBALBUMS_TABLE} = \@subAlbunTable; if ($#{$albumHash{parentDirNames}} > 0) { $templateParameters{UP_NAME} = _("up"); $templateParameters{UP_TITLE} = _("Up one subalbum"); $templateParameters{UP_LINK} = "../index.html"; } $templateParameters{STATIC_PATH} = getRootDir($album)."static.".$configHash->{templateStyle}; if ($configHash->{backgroundImage}) { # Do not set this if not configured, so that template # can check for whether defined. $templateParameters{BG_IMAGE} = $templateParameters{STATIC_PATH}."/".$configHash->{backgroundImage}; } $templateParameters{CUSTOM_CSS} = $configHash->{customStyleSheet}; $templateParameters{HOME_LINK} = $configHash->{homeURL}; $templateParameters{FEEDBACK_LINK} = $configHash->{feedbackMail}; $templateParameters{PATH_SHOW_ICON} = $configHash->{pathShowIcon}; renderTemplate("subalbum", $albumdir.$album."index.html", \%templateParameters, $configHash); } sub getAlbumInfo{ my $album = shift(@_); my $configHash = shift(@_); my %albuminfo; # what we want to generate $albuminfo{link} = uri_escape($album, '^-A-Za-z0-9/_\.')."index.html"; #link is from $albumdir -- otherwise need to add getRootDir to make work #$album = $picdir.$album; # we need this for the root dir to be right -- # correct behaviour if passed "" my $albumdescfile = "$picdir$album"."album.xml"; if (-e $albumdescfile) { $albuminfo{descFileName} = uri_escape($albumdescfile, '^-A-Za-z0-9/_\.'); $configHash = getDescAlbum($albumdescfile, \%albuminfo, $configHash); # Don't calculate this album's info if it is flagged "ignore". if ( ignoreSet($albuminfo{ignore}, $album, $configHash) ) { return(\%albuminfo); } if (exists $albuminfo{sampleimage} && $albuminfo{sampleimage}) { #$albuminfo{sampleimage} = uri_escape($albuminfo{sampleimage}, # '^-A-Za-z0-9/_\.'); #add directory name of album to sample image my $dirname = $album; $dirname =~ s%^.*/([^/]+)/$%$1/%; $albuminfo{sampleimage} = $dirname.$albuminfo{sampleimage}; $albuminfo{sampleimage} = uri_escape($albuminfo{sampleimage}, '^-A-Za-z0-9/_\.'); } } else { $albuminfo{descFileName} = ""; } if (! $albuminfo{shortdesc} && ! $albuminfo{longdesc}) { if ($configHash->{emptyAlbumDesc}) { $albuminfo{shortdesc} = ""; $albuminfo{longdesc} = ""; } else { $albuminfo{shortdesc} = _("No short description available"); $albuminfo{longdesc} = _("No long description available"); } } elsif (! $albuminfo{shortdesc}) { $albuminfo{shortdesc} = $albuminfo{longdesc}; } elsif (! $albuminfo{longdesc}) { $albuminfo{longdesc} = $albuminfo{shortdesc}; } #get album title my $albumtitle = $picdir.$album; # strip ending / if ($album ne "") { my $endChar = chop($albumtitle); $albumtitle = $album if ($endChar ne "/") ; $albumtitle =~ s/^.*\///g; #remove path } else { $albumtitle = "" } $albuminfo{dirname} = $albumtitle; #single dir name, without path info if (!$albuminfo{title}){ if ($configHash->{stripDirPrefix}) { $albumtitle =~ s/^\d+_//g; } $albumtitle =~ s/_/ /g; # replace underscores with spaces $albuminfo{title} = local2html($albumtitle) ; } beVerboseN("title for album $album is $albuminfo{title}.", 3); return (\%albuminfo, $configHash); } sub calcNumThumbPages{ my $numImages = shift (@_); my $configHash = shift (@_); my $numPages=0; if ($numImages % $configHash->{numThumbsPerPage} == 0) { $numPages = $numImages/$configHash->{numThumbsPerPage}; }else{ $numPages = int($numImages/$configHash->{numThumbsPerPage}) + 1; } return $numPages; } sub getXLinks{ my $xLinksRef = shift; my @xLinks=(); foreach my $xLink ( @$xLinksRef ) { my %row = ( link => $xLink ); push(@xLinks, \%row); } return \@xLinks; } sub generateThumbnailPages{ my ($album, $albumHashRef, $firstIsIndex, $configHash, $xlinkListRef, @imageData) = @_; my @xlinkList=@$xlinkListRef; my %albumHash = %{$albumHashRef}; my $numImages = scalar(@imageData); #element count my $numPages = calcNumThumbPages($numImages, $configHash); my $crntPage; # generate number link for each page my @numLink; if ($firstIsIndex){ push @numLink, {NUMBER => "1", NUM_LINK => "index.html" }; }else{ push @numLink, {NUMBER => "1", NUM_LINK => "thumb0.html" }; } for($crntPage=1; $crntPage < $numPages; $crntPage++) { push @numLink, {NUMBER => ($crntPage+1), NUM_LINK => "thumb".$crntPage.".html" }; } for($crntPage=0; $crntPage < $numPages; $crntPage++) { #calculate prev, next thumb page # we set prev and next equal to the empty string # if we have only 1 page to # to indicate prev and next buttons should not be displayed. my ($prev, $next); if ($numPages == 1) { $prev = ""; $next = ""; }else{ $prev = ($crntPage -1 + $numPages)%$numPages; $prev = "thumb".$prev.".html"; $next = ($crntPage + 1)%$numPages; $next = "thumb".$next.".html"; # special cases if ($configHash->{thumbnailPageCycling} == 1) { $next = "index.html" if ($firstIsIndex && ($crntPage == $numPages-1)); } else { $prev = "" if ($crntPage == 0); $next = "" if ($crntPage == $numPages-1); } $prev = "index.html" if ($firstIsIndex && ($crntPage == 1)); } #first and last images on page my $first = $crntPage*$configHash->{numThumbsPerPage}; my $last = $first + $configHash->{numThumbsPerPage} - 1; $last = $#imageData if ($#imageData <= $last); my @preloadImages; if ($configHash->{javaScriptPreloadThumbs}) { # get the names of images on the next page to preload them using # javascript my $lastNext = $last + $configHash->{numThumbsPerPage}; $lastNext = $#imageData if ($#imageData <= $lastNext); for ( my $i = $last+1 ; $i <= $lastNext ; $i++){ my %image; $image{PRELOAD_IMAGE_NB} = $i-$last; $image{PRELOAD_IMAGE_NAME} = $imageData[$i]->{'thumblink'}; $image{PRELOAD_IMAGE_WIDTH} = $imageData[$i]->{'twidth'}; $image{PRELOAD_IMAGE_HEIGHT} = $imageData[$i]->{'theight'}; push @preloadImages, \%image; } } #fileName my $filename = "thumb".$crntPage.".html"; $filename = "index.html" if ($firstIsIndex && ($crntPage == 0)); # generate sequence of number links my $numLinkSeq = dclone(\@numLink); $numLinkSeq->[$crntPage]{NUM_LINK} = ""; generateThumbPage($album, $albumHashRef, $crntPage, $numPages, $filename, $prev, $next, $numLinkSeq, $configHash, \@preloadImages, \@xlinkList, @imageData[$first..$last]); } # Generate an "all images" thumbnail page if more than one page was # generated. if ( $configHash->{allThumbnailsPage} && $numPages > 1 ) { generateThumbPage($album, $albumHashRef, "-1", "1", "allthumbs.html", "", "", "", $configHash, undef, undef, @imageData[0..$#imageData]); } } sub generateThumbPage{ my ($album, $albumHashRef, $pageNumber, $numPages, $filename, $prevPage, $nextPage, $numberLinks, $configHash, $preloadImages, $xlinkListRef, @imageData) = @_; my @xlinkList; if (!defined($xlinkListRef)) { @xlinkList=(); } else { @xlinkList=@$xlinkListRef; } my %albumHash = %{$albumHashRef}; my $thumbsOnPage = scalar(@imageData); # generate the table containing the thumbnails my $crntImage; my @thumbTable; for ($crntImage = 0; $crntImage<$thumbsOnPage; $crntImage++) { #create the row my $row = thumbEntry($imageData[$crntImage], $imageData[$crntImage]{configuration}); $row->{THUMB_ID} = $crntImage; # begin new row if needed if ( (($crntImage+1) % $configHash->{thumbsPerRow} == 0) && ($crntImage+1 < $thumbsOnPage)) { $row->{THUMB_NEW_LINE} = 1; } push @thumbTable, $row; } # set up all the substitutions, first with subs for header my %subsHash; $subsHash{THUMBS_TABLE} = \@thumbTable; $subsHash{PRELOAD_IMAGES} = $preloadImages; $subsHash{THUMB_NUMBER_LINKS} = $numberLinks; $subsHash{NUM_INFO} = getAlbumNumInfo(%albumHash); $subsHash{ALBUM_TITLE} = $albumHash{title}; $subsHash{NAV_BAR_TABLE} = navBarLinks("thumb$pageNumber", $album, $configHash, %albumHash); $subsHash{ALBUM_PATH_LINKS} = pathLinks($album, 0, @{$albumHash{parentDirNames}}); $subsHash{TREE_NAME} = _("tree"); $subsHash{TREE_TITLE} = _("Tree of all albums and sub-albums"); $subsHash{TREE_LINK} = getRootDir($album)."tree.html"; if ($nextPage eq "" and $prevPage eq "") { $subsHash{MULTIPLE_PAGES} = 0; } else { $subsHash{MULTIPLE_PAGES} = 1; } if ($#{$albumHash{parentDirNames}} > 0) { $subsHash{UP_NAME} = _("up"); $subsHash{UP_TITLE} = _("Up one subalbum"); $subsHash{UP_LINK} = "../index.html"; } if ($genEditableAlbum) { $subsHash{ALBUM_DESC} = "". $albumHash{longdesc}.""; } else { $subsHash{ALBUM_DESC} = $albumHash{longdesc}; } if ($nextPage) { $subsHash{NEXT_THUMB_PAGE} = $nextPage; } if ($prevPage) { $subsHash{PREV_THUMB_PAGE} = $prevPage; } # Correct the special-casing of "-1" and "-2" to mean this is an # "allthumbs" page. if ( $pageNumber=="-1" || $pageNumber=="-2" ) { $pageNumber="0"; } my $pagenumber_string = $pageNumber+1; $pagenumber_string .= "/$numPages"; $subsHash{THUMB_PAGE_NUMBER} = $pagenumber_string; $subsHash{XLINK} = getXLinks(\@xlinkList); $subsHash{STATIC_PATH} = getRootDir($album)."static.".$configHash->{templateStyle}; if ($configHash->{backgroundImage}) { # Do not set this if not configured, so that template # can check for whether defined. $subsHash{BG_IMAGE} = $subsHash{STATIC_PATH}."/".$configHash->{backgroundImage}; } $subsHash{CUSTOM_CSS} = $configHash->{customStyleSheet}; $subsHash{HOME_LINK} = $configHash->{homeURL}; $subsHash{FEEDBACK_LINK} = $configHash->{feedbackMail}; $subsHash{PATH_SHOW_ICON} = $configHash->{pathShowIcon}; if ($albumHashRef->{numSubAlbums} == 0){ $subsHash{FIRST_PAGE} = "index.html"; }else{ $subsHash{FIRST_PAGE} = "thumb0.html"; } $subsHash{LAST_PAGE} = "thumb".($numPages-1).".html"; renderTemplate("thumbnail", $albumdir.$album.$filename, \%subsHash, $configHash); } #creates a single entry in the table of thumbnails for the given image sub thumbEntry{ my $imageHashRef = shift(@_); my $configHash = shift(@_); my ($links, $i); my %thumb = (THUMB_LINK => $imageHashRef->{'thumblink'}, THUMB_WIDTH => $imageHashRef->{'twidth'}, THUMB_HEIGHT => $imageHashRef->{'theight'}, THUMB_ALT => _("Click on one of the size names below to enlarge this image"), THUMB_LINK_TITLE => _("Click on one of the size names below to enlarge this image"), ); if ($configHash->{defaultSize} >= 0){ $thumb{THUMB_DEFAULT_SIZE} = $$imageHashRef{$configHash->{defaultSize}}{'htmlFile'}; } if ($configHash->{titleOnThumbnail}) { $thumb{THUMB_TITLE} = $$imageHashRef{title}; } if (! $configHash->{thumbnailBackground}){ $thumb{THUMB_BACKGROUND} = $configHash->{colorsSubs}{$configHash->{colorStyle}}{PAGE_BACKCOLOR}; }else{ $thumb{THUMB_BACKGROUND} = $configHash->{colorsSubs}{$configHash->{colorStyle}}{SUBBAR_BACKCOLOR}; } #print 'for image'.$imageHashRef->{'filename'}.'we have #$imageHashRef->{\'maxSize\'} = '.$imageHashRef->{'maxSize'}."\n"; my @sizes; for $i (0..($imageHashRef->{'maxSize'})) { if ($i == $configHash->{defaultSize}) { push @sizes , {SIZE_LINK => $$imageHashRef{$i}{'htmlFile'}, SIZE_NAME => $configHash->{sizeNames}[$i], SIZE_TITLE => $configHash->{longSizeNames}[$i]. " ($imageHashRef->{$i}{'width'}x". "$imageHashRef->{$i}{'height'})", SIZE_FILE => $configHash->{fileSizeNames}[$i], SIZE_DFLT => 1, }; } else { push @sizes , {SIZE_LINK => $$imageHashRef{$i}{'htmlFile'}, SIZE_NAME => $configHash->{sizeNames}[$i], SIZE_TITLE => $configHash->{longSizeNames}[$i]. " ($imageHashRef->{$i}{'width'}x". "$imageHashRef->{$i}{'height'})", SIZE_FILE => $configHash->{fileSizeNames}[$i], }; } } $thumb{THUMB_SIZES} = \@sizes; return \%thumb; } sub generateSecondaryFieldsPage{ my $imageInfo = shift; my $album = shift; my $albumInfo = shift; my $imageName = shift; my $configHash = shift; my $table=""; my $fileName=""; my $tagValue; my $sectionTitle=""; my $tableSection=""; my @sections; my @fields; my @tmpFields; foreach my $tagName (@secondaryFields) { if ($tagName =~ m/^BINS-SECTION /) { if ($sectionTitle && @tmpFields){ push (@fields, {SECTION_TITLE => $sectionTitle}); @fields = (@fields, @tmpFields); @tmpFields = (); push (@sections, {SECTION_TITLE => $sectionTitle}); } $sectionTitle = $tagName; $sectionTitle =~ s/^BINS-SECTION //; }else{ $tagValue = $fields{$tagName}; if ($imageInfo->{$tagName}) { my %row = (FIELD_NAME => $tagValue->{'Name'}, FIELD_VALUE => local2html($imageInfo->{$tagName}) ); $row{FIELD_TIP} = $tagValue->{'Tip'} if $tagValue->{'Tip'}; push (@tmpFields, \%row); } } } if (! @fields) { return "" } # on which size of image we go when "back to the image" is clicked and # JavaScript is deactivated: my $size = $configHash->{defaultSize}; if ($size == -1){ $size = 0; } my %subs_hash; $subs_hash{ALBUM_TITLE} = $album; $subs_hash{IMAGE_PAGE_LINK} = $imageInfo->{$size}{'htmlFile'}; $subs_hash{IMAGE_SIZE_NAME} = _("size "). $configHash->{longSizeNames}[$size]; $subs_hash{FILE_NAME} = $imageName; $subs_hash{THUMB_PAGE} = $imageInfo->{'thumbpage'}; $subs_hash{ALBUM_PATH_LINKS} = pathLinks($album, $imageInfo->{'title'}, @{$albumInfo->{parentDirNames}}); $subs_hash{TITLE} = $imageInfo->{'title'}; $subs_hash{DESC_TABLE} = \@fields; $subs_hash{LINKS_TABLE} = \@sections; $subs_hash{STATIC_PATH} = getRootDir($album)."static.".$configHash->{templateStyle}; $subs_hash{HOME_LINK} = $configHash->{homeURL}; $subs_hash{FEEDBACK_LINK} = $configHash->{feedbackMail}; $subs_hash{PATH_SHOW_ICON} = $configHash->{pathShowIcon}; $subs_hash{CUSTOM_CSS} = $configHash->{customStyleSheet}; my @array; push @array, {NAV_NAME => $intlSubs{STRING_BACKTOTHEIMAGE}, NAV_LINK => "javascript:history.back();", NAV_ICON => "back.png", NAV_ID => "back"}; # $subs_hash{NAV_BAR_TABLE} = navBarLinks('secondaryFields', $album, $configHash, %albumHash); $subs_hash{NAV_BAR_TABLE} = \@array; #### $fileName = $imageName.".details.html"; my $outputPage = $albumdir.$album.$fileName; renderTemplate("details", $outputPage, \%subs_hash, $configHash); #$fileName=''. # _("Additional information on the picture").''; $fileName=uri_escape($fileName); return $fileName; } # copy the source image in the dest album, and eventually, perform a # rotation sub copyImage { my ($source, $dest, $imageData, $configHash) = @_; if ( (! -e "$dest") || (-M "$source" < -M "$dest") ) { `cp -p "$source" "$dest"`; system("chmod", "a+r", "$dest") == 0 or die("\nCannot set write permission on $dest: $?"); if ($configHash->{rotateImages} eq 'destination') { # Perform a rotation of the picture if needed if (rotateImage($dest, $imageData->{'Orientation'}, "", $configHash) == 1) { progressifyImage($dest, "", $configHash); # swap width & height my $numsizes = $#{$configHash->{scaledWidths}}; for(my $j=0; $j<=$numsizes; $j++) { #my $tmp = $imageData->{$j}{'width'}; #$imageData->{$j}{'width'} = $imageData->{$j}{'height'}; #$imageData->{$j}{'height'} = $tmp; ($imageData->{$j}{'width'}, $imageData->{$j}{'height'}) = ($imageData->{$j}{'height'}, $imageData->{$j}{'width'}); } } } } } # copy the source file to the given destination sub copyFile { } sub getHTMLImagePageLink{ my ($imageData, $size, $num) = @_; my $sizeLink = $imageData->{'maxSize'}; if ($sizeLink >= $size) { $sizeLink = $size; } return uri_escape($imageData->{'basename'})."_". $imageData->{configuration}{sizeNames}[$sizeLink]. ".jpg.".$num.".html"; } # $album is the album we are generating. # imagepath is path to actual image location. sub generateImagesInAlbum{ my ($album, $albumHashRef, $firstIsIndex, $configHash, @imagesToDisplay) = @_; # an array of references to hashes storing information about each image my @imageData; # generate thumbnails and scaled images for each image # we will display, and generate HTML my $numImages = $#imagesToDisplay+1; my $i; #get description information for($i=0; $i<$numImages; $i++) { my $crntImage = $album.$imagesToDisplay[$i]; $imageData[$i] = getDesc($crntImage, $configHash); #print "-------------------".Dumper($imageData{'configuration')); } #calculate the thumbnail page each image will be on for($i=0; $i<$numImages; $i++) { #my $crntImage = $imagesToDisplay[$i]; $imageData[$i]{'thumbpage'} = getThumbPage($i, $firstIsIndex, $configHash); } #determine scaled sizes, create scaled copies my(@largestSize); # array indexed by image number, holds index of largest # available size for each image. for ($i=0; $i<$numImages; $i++) { my $imageConfigHash = $imageData[$i]{'configuration'}; $largestSize[$i] = 0; my $crntImage = $imagesToDisplay[$i]; beVerbose("\n", 2); beVerboseN(" Image $crntImage", 1); my ($crntImageBase,$imagePath,$crntImageType) = &fileparse($crntImage, '\.[^.]+\z'); $imageData[$i]{'filename'} = $crntImage; $imageData[$i]{'basename'} = $crntImageBase; $imageData[$i]{'type'} = $crntImageType; # don't escape the / character #$imageData[$i]{'imagepath'} = $album; #print "imagepath : «$imagePath»\n"; #$imageData[$i]{'imageurl'} = $url; # to handle virtual images, make sure the images album exists # first, make sure web directory exists if (! -e "$albumdir$album") { beVerboseN(" Creating dir $albumdir$album", 1); `mkdir -p "$albumdir$album"` } # load the image to check if it is bigger than current max dims # to see if need new scaled image # first try with Image::Size, which is quicker than ImageMagick my($width, $height) = imgsize("$picdir$album$crntImage"); if (! defined $width) { # if Image::Size fails (format not recognized), load with # ImageMagick and get the size my($preview) = Image::Magick->new; # read in the picture my($x) = $preview->Read("$picdir$album$crntImage"); warn "$x" if "$x"; ($width, $height) = $preview->Get('width', 'height'); } $imageData[$i]{'width'} = $width; $imageData[$i]{'height'} = $height; #generate thumbnail (with dir if needed) my $thumbName = "$crntImageBase"."_pre.jpg"; #$imageData[$i]{'thumblink'} = &getWebBase($album).$thumbName; $imageData[$i]{'thumblink'} = uri_escape($thumbName); my ($twidth, $theight); #if (! -e $albumdir.$album.$thumbName) { beVerboseN(" Generating thumbnail $album$thumbName from image ". "$crntImageBase.", 3); ($twidth, $theight)= generateThumbnail($album.$crntImage, $album.$thumbName, $width, $height, $imageData[$i], $imageData[$i]{'configuration'}); $imageData[$i]{twidth} = $twidth; $imageData[$i]{theight} = $theight; my $j; # generate scaled sizes, largestSize, write scaled # version if not oneCopy my $filling_in_sizes = 0; my $scaledImage; my $maxWidth; my $maxHeight; my ($scaledWidth, $scaledHeight); for ($j=0; $j <= $#{$imageConfigHash->{scaledWidths}}; $j++) { if (! $filling_in_sizes ) { my $size = $imageConfigHash->{sizeNames}[$j]; $size =~ s/\.//g ; $scaledImage = $crntImageBase."_". $size.".jpg"; $maxWidth = $imageConfigHash->{scaledWidths}[$j]; $maxHeight = $imageConfigHash->{scaledHeights}[$j]; ($scaledWidth, $scaledHeight) = getScaledSize($width, $height, $maxWidth, $maxHeight, $imageConfigHash); beVerboseN(" $imageConfigHash->{sizeNames}[$j] size is ". "$scaledWidth x $scaledHeight.", 3); } # generate scaled version if needed $imageData[$i]{$j}{'width'} = $scaledWidth; $imageData[$i]{$j}{'height'} = $scaledHeight; if ( 1 ) { if ($width<$scaledWidth || $height<$scaledHeight) { print "Skipping $width $height $scaledWidth $scaledHeight\n"; next; } } if (! $filling_in_sizes ) { $largestSize[$i] = $j; if (! $oneCopy) { ($imageData[$i]{$j}{'width'}, $imageData[$i]{$j}{'height'}) = writeRotateScaledVersion($album.$crntImage, $album.$scaledImage, $scaledWidth, $scaledHeight, $imageData[$i], $imageData[$i]{'configuration'}); } #if ( ($width <= $scaledWidth) && ($height <= $scaledHeight) ) { # $filling_in_sizes = 1; #} } } # if oneCopy, create a single canonical image copy, # with the same name as the original if ($oneCopy) { if ($imageSource eq "scaled") { ($imageData[$i]{$j}{'width'}, $imageData[$i]{$j}{'height'}) = writeRotateScaledVersion($album.$crntImage, $album.$crntImage, $imageData[$i]{$largestSize[$i]}{'width'}, $imageData[$i]{$largestSize[$i]}{'height'}, $imageData[$i], $imageData[$i]{'configuration'}); } elsif ($imageSource eq "copied") { copyImage($picdir.$album.$crntImage, $albumdir.$album.$crntImage, $imageData[$i], $imageConfigHash); } elsif ($imageSource eq "orig") { #not yet implemented } elsif ($imageSource eq "custom") { # gets size in KB my $fileSize = ((-s "$picdir$crntImage") / 1024); beVerboseN(" Size is $fileSize KB.", 3); if ($fileSize > 900) { beVerbose("Image $crntImage is BIG, resizing...", 3); my( $newW, $newH) = getScaledSize($imageData[$i]{'width'}, $imageData[$i]{'height'}, 1600, 1600, $imageConfigHash); ($imageData[$i]{$j}{'width'}, $imageData[$i]{$j}{'height'}) = writeRotateScaledVersion($album.$crntImage, $album.$crntImage, $newW, $newH, $imageData[$i], $imageData[$i]{'configuration'}); beVerboseN("done.", 3); } else { beVerbose("Image $crntImage is SMALL, copying...", 3); copyImage("$picdir$album$crntImage", "$albumdir$album$crntImage", $imageData[$i], $imageConfigHash); beVerboseN("done.", 3); } } } beVerboseN(" setting maxSize to $i ($imageData[$i]->{filename})", 3); $imageData[$i]->{'maxSize'}= $largestSize[$i]; } # now calculate everything needed to generate html next, prev html # names, etc #print "crntImage is $crntImage\n"; for ($i=0; $i < $numImages; $i++) { my $imageConfigHash = $imageData[$i]{'configuration'}; #print "-------------------".Dumper($imageConfigHash); my $crntImage = $imagesToDisplay[$i]; my ($crntImageBase,$imagePath,$crntImageType) = &fileparse($crntImage, '\.[^.]+\z'); #now generate scaled versions and HTML for (my $j=0; $j <= $imageData[$i]->{maxSize}; $j++) { #print "\$j = $j\n"; my $maxWidth = $imageConfigHash->{scaledWidths}[$j]; my $maxHeight = $imageConfigHash->{scaledHeights}[$j]; # suffix of filename of this size (add .html for html, # needs prefix) #my $crntSizeSuffix = ; my $size = $imageConfigHash->{sizeNames}[$j]; $size =~ s/\.//g ; my $scaledImage = $crntImageBase."_".$size.".jpg"; #determine next, prev image my $nextImageNum = ($i+1) % ($numImages); my $prevImageNum = ($i-1+$numImages)% ($numImages) ; my $npdiff = $nextImageNum - $prevImageNum; #my ($xbase, $xpath, $xtype) = #fileparse($imagesToDisplay[$nextImageNum], '\.[^.]+\z'); my $sizeLink = $imageData[$nextImageNum]->{'maxSize'}; $imageData[$i]{$j}{'nextHTML'} = getHTMLImagePageLink($imageData[$nextImageNum], $j, $nextImageNum); $imageData[$i]{$j}{'preloadIMG'} = uri_escape($imageData[$nextImageNum]{'basename'})."_". $imageData[$nextImageNum]{configuration}{sizeNames}[$sizeLink]. ".jpg"; $imageData[$i]{$j}{'imgNum'} = $i + 1; $imageData[$i]{$j}{'imgCount'} = $numImages; $imageData[$i]{$j}{'nextIsFirst'} = ($nextImageNum == 0); $imageData[$i]{$j}{'prevIsLast'} = (($prevImageNum + 1) == $numImages); $imageData[$i]{$j}{'prevHTML'} = getHTMLImagePageLink($imageData[$prevImageNum], $j, $prevImageNum); $imageData[$i]{$j}{'nextTitle'}= $imageData[$nextImageNum]{'title'}; $imageData[$i]{$j}{'prevTitle'}= $imageData[$prevImageNum]{'title'}; if ($configHash->{thumbPrevNext}) { $imageData[$i]{$j}{'nextThumb'} = uri_escape($imageData[$nextImageNum]{'basename'})."_pre.jpg"; $imageData[$i]{$j}{'prevThumb'} = uri_escape($imageData[$prevImageNum]{'basename'})."_pre.jpg"; } # used in URL $imageData[$i]{$j}{'htmlFile'}= uri_escape($scaledImage). ".$i.html"; # used to access the file on disk $imageData[$i]{$j}{'htmlFileName'}= $scaledImage.".$i.html"; # sizedFile is the text to go into the link for this image size if ($oneCopy) { #$imageData[$i]{$j}{'sizedFile'} = # &getWebBase($album).$imageData[$i]{'filename'}; $imageData[$i]{$j}{'sizedFile'} = $crntImageBase.$crntImageType; } else { #$imageData[$i]{$j}{'sizedFile'} = # &getWebBase($album).$imageData[$i]{'imagepath'}. # $scaledImage; $imageData[$i]{$j}{'sizedFile'} = $scaledImage; } } $imageData[$i]{'detailsLink'} = generateSecondaryFieldsPage($imageData[$i], $album, $albumHashRef, $crntImageBase.$crntImageType, $configHash); } # now generate html for ($i=0; $i<$numImages; $i++) { for (my $j=0; $j <= $imageData[$i]->{maxSize}; $j++) { my $lastImageURL = getHTMLImagePageLink($imageData[$numImages-1], $j, $numImages-1); my $firstImageURL = getHTMLImagePageLink($imageData[0], $j, 0); generateImage($imageData[$i], $j, $album, $albumHashRef, $imageData[$i]{'configuration'}, $firstImageURL, $lastImageURL); } } #printImageData(@imageData); beVerboseN(" We have ". scalar(@imageData). " images in the album.", 2); return @imageData; } sub getThumbPage{ my ($crntNumber, $firstIsIndex, $configHash) = @_; my $thumbPageNumber = int($crntNumber/$configHash->{numThumbsPerPage}); return "index.html" if ($thumbPageNumber == 0 && $firstIsIndex); return "thumb$thumbPageNumber.html"; } sub generateThumbnail{ my ($crntImage, $thumbName, $width, $height, $imageData, $configHash) = @_; my ($newWidth, $newHeight) = getScaledSize($width, $height, $configHash->{previewMaxWidth}, $configHash->{previewMaxHeight}, $configHash); return writeRotateScaledVersion($crntImage, $thumbName, $newWidth, $newHeight, $imageData, $configHash); } sub getScaledSize{ my ($width, $height, $maxWidth, $maxHeight, $configHash) = @_; my $xfactor; my $yfactor; #if width or height are zero, that is a problem if (not ( $width and $height ) ) { return ($maxWidth,$maxHeight); } if (substr($maxWidth, -1) eq "%") { chop($maxWidth); $xfactor = $maxWidth / 100; $maxWidth = $width * $xfactor; } else { $xfactor = $maxWidth/$width; } if (substr($maxHeight, -1) eq "%") { chop($maxHeight); $yfactor = $maxHeight / 100; $maxHeight = $height * $yfactor; } else { $yfactor = $maxHeight/$height; } if ((! $configHash->{enlarge}) && ($width < $maxWidth) && ($height < $maxHeight)) { return ($width, $height); } my ($newHeight, $newWidth); if ($xfactor <= $yfactor) { $newWidth = $maxWidth; $newHeight = $xfactor*$height; }else{ $newHeight = $maxHeight; $newWidth = $yfactor*$width; } return (int($newWidth), int($newHeight)); } # takes origName and newName with path info (but not pic_dir, of course) sub writeScaledVersion{ my ($origName, $newName, $newWidth, $newHeight, $imageRef, $configHash) = @_; beVerbose(" Generating scaled version of $picdir$origName\n". " to be written to $newName... ", 2); if (-e "$albumdir$newName"){ if (((lstat("$albumdir$newName"))[9]) >= ((stat("$picdir$origName"))[9])){ beVerboseN("\n image already exists and is newer, skipping.", 2); return 0; } } my($preview) = Image::Magick->new; my($x) = $preview->Read("$picdir$origName"); # read in the picture warn "$x" if "$x"; my $format = $preview->Get("magick"); if (!$configHash->{scaleIfSameSize} and grep (/^$format$/, @webFormats ) ) { my ($width, $height) = $preview->Get("width", "height"); if ($width == $newWidth && $height == $newHeight) { if ($configHash->{linkInsteadOfCopy} && (! ($configHash->{rotateImages} eq 'destination') || (! defined $imageRef->{'Orientation'} || $imageRef->{'Orientation'} eq "top_left"))) { beVerbose("\n Image has the right size, just linking... ", 2); system("ln", "-sf", "$picdir$origName", "$albumdir$newName") == 0 or die("\nCannot link $albumdir$newName to $picdir$origName: $?"); # the original file may be r/o but we don't have to modify it # but it must be readable by the http deamon if ($configHash->{updateOriginalPerms}) { system("chmod", "a+r", "$picdir$origName") == 0 or die("\nCannot set read permission on $albumdir$newName: $?"); } beVerboseN("done.", 2); return 0; # Image is processed, no need to try to process it # again. } else { beVerbose("\n Image has the right size, just copying... ", 2); system("cp", "-p", "$picdir$origName", "$albumdir$newName") == 0 or die("\nCannot copy $picdir$origName to $albumdir$newName: $?"); # make it writable in case $origName was r/o system("chmod", "u+w,a+r", "$albumdir$newName") == 0 or die("\nCannot set write permission on $albumdir$newName: $?"); beVerboseN("done.", 2); return 1; } } } $x = $preview->Set(quality => $configHash->{jpegQuality}); warn "$x" if "$x"; if ($configHash->{deExifyImages}) { $x = $preview->Profile(name => "*"); warn "$x" if "$x"; } if ($configHash->{scaleMethod} eq "sample") { $x = $preview->Sample(width=>$newWidth, height=>$newHeight); } else { if ($configHash->{scaleMethod} ne "scale") { warn "Unknown scaleMethod $configHash->{scaleMethod}, using scale" } $x = $preview->Scale(width=>$newWidth, height=>$newHeight); } warn "$x" if "$x"; $x = $preview->Border(color=>'black',width=>1, height=>1); warn "$x" if "$x"; beVerbose("\n Writing scaled image $albumdir$newName... ", 3); $x = $preview->Write("$albumdir$newName"); warn "$x" if "$x"; beVerboseN("done.", 2); return 1; } sub writeRotateScaledVersion{ my ($origName, $newName, $newWidth, $newHeight, $imageRef, $configHash) = @_; if (writeScaledVersion($origName, $newName, $newWidth, $newHeight, $imageRef, $configHash)){ if ($configHash->{rotateImages} eq 'destination'){ # Perform a rotation of the picture if needed if (rotateImage("$albumdir$newName", $imageRef->{'Orientation'}, "", $configHash) == 1){ progressifyImage("$albumdir$newName", "", $configHash); return ($newHeight, $newWidth); } } progressifyImage("$albumdir$newName", "", $configHash); return ($newWidth, $newHeight); } ($newWidth, $newHeight) = imgsize($albumdir.$newName); return ($newWidth, $newHeight); } # $album is album we are generating, _not_ path to image. # For the actual image path, use $imageHashRef sub generateImage { my ($imageHashRef, $size, $album, $albumHashRef, $configHash, $firstImage, $lastImage) = @_; my $crntImage = $imageHashRef->{'filename'}; my $imagetodisplay = $imageHashRef->{$size}{'sizedFile'}; my $filesize = &fileSize("$albumdir$album$imagetodisplay"); $imagetodisplay = uri_escape($imagetodisplay); my $imagetitle = $crntImage; $imagetitle =~ s/\.(\S+)\Z//; $imagetitle =~ s/_/ /g; my($fileExtension) = $imageHashRef->{'type'}; if ($fileExtension =~ /jpg|jpeg/i) { $fileExtension = "JPEG"; } elsif ($fileExtension =~ /gif/i) { $fileExtension = "GIF"; } elsif ($fileExtension =~ /png/i) { $fileExtension = "PNG"; } else { # if the type is unwkown, the image has been converted to JPEG $fileExtension = "JPEG"; } my $width = $imageHashRef->{$size}{width}; my $height = $imageHashRef->{$size}{height}; # this line, with the _double_ quotes, is here for xgettext: _("$filesize $fileExtension image, $width x $height pixels") if (0); my $pictureinfo = _('$filesize $fileExtension image, $width x $height pixels'); $pictureinfo = eval "\"$pictureinfo\""; #add strings to substitute hash my %subs_hash; #$subs_hash{ALBUM_TITLE} = $album; $subs_hash{WIDTH} = $width+2; $subs_hash{HEIGHT} = $height+2; $subs_hash{IMAGE_TO_DISPLAY} = $imagetodisplay; $subs_hash{PICTURE_INFO} = $pictureinfo; #$subs_hash{''} = getRootDir($album); $subs_hash{NEXT_IMAGE} = $imageHashRef->{$size}{nextHTML}; if ($configHash->{imagePageCycling}) { $subs_hash{NEXT_IMAGE_PAGE} = 1; $subs_hash{PREV_IMAGE_PAGE} = 1; } else { $subs_hash{NEXT_IMAGE_PAGE} = ! $imageHashRef->{$size}{nextIsFirst}; $subs_hash{PREV_IMAGE_PAGE} = ! $imageHashRef->{$size}{prevIsLast}; } $subs_hash{IMG_NUM} = $imageHashRef->{$size}{imgNum}; $subs_hash{IMG_COUNT} = $imageHashRef->{$size}{imgCount}; if ($configHash->{javaScriptPreloadImage}) { $subs_hash{IMG_PRELOAD} = $imageHashRef->{$size}{preloadIMG}; } $subs_hash{PREV_IMAGE} = $imageHashRef->{$size}{prevHTML}; $subs_hash{NEXT_TITLE} = $imageHashRef->{$size}{nextTitle}; $subs_hash{PREV_TITLE} = $imageHashRef->{$size}{prevTitle}; if ($configHash->{thumbPrevNext}) { $subs_hash{NEXT_THUMB} = $imageHashRef->{$size}{nextThumb}; $subs_hash{PREV_THUMB} = $imageHashRef->{$size}{prevThumb}; } $subs_hash{FILE_NAME} = $imageHashRef->{basename}. $imageHashRef->{'type'}; $subs_hash{THUMB_PAGE} = $imageHashRef->{thumbpage}; $subs_hash{SIZE_LINKS} = getSizeLinks($size, $imageHashRef, $configHash); my $title = $imageHashRef->{title}; $subs_hash{ALBUM_PATH_LINKS} = pathLinks($album, $title, @{$albumHashRef->{parentDirNames}}); $subs_hash{TITLE} = $title; $subs_hash{DESC_TABLE} = getDescTable(%{$imageHashRef}); $subs_hash{DETAILS_LINK} = $imageHashRef->{detailsLink}; $subs_hash{STRING_DETAILS} = _("Additional information on the picture"); $subs_hash{NAV_BAR_TABLE} = navBarLinks('image', $album, $configHash, %$albumHashRef); $subs_hash{TREE_NAME} = _("tree"); $subs_hash{TREE_TITLE} = _("Tree of all albums and sub-albums"); $subs_hash{TREE_LINK} = getRootDir($album)."tree.html"; $subs_hash{STATIC_PATH} = getRootDir($album)."static.".$configHash->{templateStyle}; $subs_hash{HOME_LINK} = $configHash->{homeURL}; $subs_hash{FEEDBACK_LINK} = $configHash->{feedbackMail}; if ($configHash->{backgroundImage}) { # Do not set this if not configured, so that template # can check for whether defined. $subs_hash{BG_IMAGE} = $subs_hash{STATIC_PATH}."/".$configHash->{backgroundImage}; } $subs_hash{CUSTOM_CSS} = $configHash->{customStyleSheet}; $subs_hash{PATH_IMG_NUM} = $configHash->{pathImgNum}; $subs_hash{PATH_SHOW_ICON} = $configHash->{pathShowIcon}; $subs_hash{FIRST_IMAGE} = $firstImage; $subs_hash{LAST_IMAGE} = $lastImage; renderTemplate("image", $albumdir.$album.$imageHashRef->{$size}{htmlFileName}, \%subs_hash, $configHash); } sub getSizeLinks{ my ($size, $imageHashRef, $configHash) = @_; my @sizes; if ( $size eq 'full' ) { $size = -1; } for my $i (0..$imageHashRef->{'maxSize'}) { if ($i != $size) { # output link push @sizes, {SIZE_NAME => $configHash->{longSizeNames}[$i], SIZE_LINK => $imageHashRef->{$i}{'htmlFile'}, SIZE_TITLE => $imageHashRef->{$i}{'width'}."x". $imageHashRef->{$i}{'height'}, SIZE_FILE => $configHash->{fileSizeNames}[$i], }; } else { #mark as current page push @sizes, {SIZE_NAME => $configHash->{longSizeNames}[$i], SIZE_FILE => $configHash->{fileActiveSizeNames}[$i]}; } } return \@sizes; } sub getDescTable{ my (%hash) = @_; my @descTable; foreach my $tagName (@mainFields) { if ($hash{$tagName}) { my $value=$hash{$tagName}; $value =~ s/'/'/g ; # in case it's used in javascript code push @descTable, {DESC_FIELD_NAME => $fields{$tagName}->{'Name'}, DESC_FIELD_VALUE => $value, }; } } return \@descTable; } # Given a path, not including $picdir, gives the relative path back # to the root of the hierarchy. Note that the trailing slash is critical for the # current implementation. For example, if given as input sample_dir/, which # corresponds to web/sample_dir/ and pic_dir/sample_dir/, the function # returns "../". # changed on 11/15 so it only points into base dir of web hierarchy sub getRootDir{ my $path = $_[0]; my $slashcount = 0; $slashcount++ while($path =~ m'/'g); my $relPath =""; my $i; for($i=$slashcount; $i>0; $i--) { $relPath = $relPath."../"; } return $relPath; } # given an album (path), not including $albumdir, # provides the relative path back to $albumdir. # closely related to getRootDir sub getWebBase{ my $path = $_[0]; my $slashcount = 0; $slashcount++ while($path =~ m'/'g); my $relPath =""; my $i; for($i=$slashcount; $i>0; $i--) { $relPath = $relPath."../"; } return $relPath; } # given the name of a directory in $picdir, not including the leading # $picdir, generates the entry for the table on the home page sub generateAlbumEntry { my $album = shift(@_); my $prettyalbum = $album; $prettyalbum =~ s/_/ /g; # replaces underscores with spaces my $result = ''.$prettyalbum.'
'; return $result; } # return list of directories where templates can be found sub templateDirs { my $configHash = shift; my @dirs; if ($templateDir) { push(@dirs, bsd_glob($templateDir."/templates.". $configHash->{templateStyle}, GLOB_TILDE)); } push(@dirs, bsd_glob($configHash->{userConfigDir}."/templates.". $configHash->{templateStyle}, GLOB_TILDE)); push(@dirs, bsd_glob($configHash->{globalDataDir}."/templates.". $configHash->{templateStyle}, GLOB_TILDE)); return @dirs; } # return static directory path (with dir) if it exists. sub templateStaticDir { my $configHash = shift; my $staticDir; my @dirs = templateDirs($configHash); foreach my $dir (@dirs) { beVerboseN(" Looking for static template directory in $dir...", 4); if (-d $dir."/static") { $staticDir = $dir."/static"; beVerboseN(" Found static template directory $staticDir.", 4); last; } } return $staticDir; } # return template file (with dir) from template name sub templateFileName { my $templateName = shift; my $configHash = shift; $templateName .= ".html"; my $templateFile; my @dirs = templateDirs($configHash); if ($templateDir) { push(@dirs, bsd_glob($templateDir."/templates.". $configHash->{templateStyle}, GLOB_TILDE)); } push(@dirs, bsd_glob($configHash->{userConfigDir}."/templates.". $configHash->{templateStyle}, GLOB_TILDE)); push(@dirs, bsd_glob($configHash->{globalDataDir}."/templates.". $configHash->{templateStyle}, GLOB_TILDE)); foreach my $dir (@dirs) { beVerboseN(" Looking for HTML template $templateName in $dir...", 4); if (-f $dir."/".$templateName) { $templateFile = $dir."/".$templateName; beVerboseN(" Found HTML template $templateFile...", 4); last; } } if (!$templateFile) { print("Error: cannot find HTML template $templateName\n"); exit 2; } return $templateFile; } sub renderTemplate{ my $templateName = shift; my $outputFileName = shift; my $templateParameters = shift; my $configHash = shift; %{$templateParameters} = (%{$templateParameters}, %{$configHash->{colorsSubs}{$configHash->{colorStyle}}}, %intlSubs, ); # open the html template #my $template = # HTML::Template::JIT->new(jit_debug => 1, # jit_path => '/tmp', my $template = HTML::Template->new( filename => templateFileName($templateName, $configHash), blind_cache => 1, loop_context_vars => 1, die_on_bad_params => 0, global_vars => 1, ); # fill in the parameters $template->param($templateParameters); open(OUTFILE, ">$outputFileName") or die "Couldn't open $outputFileName"; if ($configHash->{compactHTML}){ my $page = $template->output(); my $h = new HTML::Clean(\$page); $h->strip({whitespace => 1, shortertags => 1, blink => 0, contenttype => 0, comments => 1, entities => 0, dequote => 1, defcolor => 1, javascript => 1, htmldefaults => 0, # when set to 1, htmldefaults cause problems # with UTF-8 encodings lowercasetags => 0, meta => "", }); my $page2 = $h->data(); print OUTFILE $$page2; } else { $template->output(print_to => *OUTFILE); } close(OUTFILE); } #input path of page to render, not including $root sub renderPage { # Spits out a page of HTML. # to the correct file; in $albumdir/$crnt_dir my($file, $pageContent) = @_; #print "Writing to file: $file\n"; open(OUTFILE, ">$file") or die "Couldn't open $file"; print OUTFILE $pageContent; close(OUTFILE); #print "Content-type: text/html\n\n"; } # Given an image file (no album_dir, with path, as usual), returns # name of the .xml file associated with this image sub getDescFile{ my $crntImage = shift(@_); my ($base,$dir,$type) = &fileparse($crntImage, '\.[^.]+\z'); my $descFile = $picdir.$dir.$base.$type.".xml"; if( ! -e $descFile){ $descFile = $picdir.$dir.$base.".xml"; } if( ! -e $descFile){ $descFile = $picdir.$dir.$base.$type.".xml"; } return $descFile; } sub getXMLAsGrove{ my $file=shift(@_); #my $sample = XML::Handler::Sample->new(); # Get XML document as a Grove my $grove_builder = XML::Grove::Builder->new; my $parser = XML::Parser::PerlSAX->new ( Handler => $grove_builder); #my $parser = XML::SAX::Expat->new ( Handler => $grove_builder); return $parser->parse ( Source => { SystemId => $file } ); } sub getDesc{ my $crntImage = shift(@_); my $configHash= shift(@_); my ($base,$dir,$type) = &fileparse($crntImage, '\.[^.]+\z'); my $descFile = getDescFile($crntImage); my %descHash; my %exifHash; my $document; my @priorityList; if (-e $descFile) { beVerboseN(" Reading desc file $descFile.", 3); $document = getXMLAsGrove($descFile); %descHash = getDescXML($document); #$descHash{descFileName} = uri_escape($descFile, '^-A-Za-z0-9/_\.'); $configHash = getConfigXML($document, '/image/bins', $configHash); %exifHash = getExifXML($document, \@priorityList); } else { $descHash{descFileName} = ""; } processExif($crntImage, \%descHash, \%exifHash, $document, \@priorityList, $configHash); # If no title is set, use the picture file name. if (!trimWhiteSpace($descHash{'title'})) { # If trimming whitespace gets rid of everything (returns empty string) my $imagetitle = $base; $imagetitle =~ s/_/ /g; # replace underscores with spaces $descHash{'title'} = local2html($imagetitle); } $descHash{'configuration'} = $configHash; return \%descHash; } # Given an XML doc as a Grove, returns # fields from that images description file. sub getDescXML{ my $document = shift(@_); my $fieldName; my $fieldValue; my %descHash; beVerboseN(" Reading user description fields...", 3); foreach my $element (@{$document->at_path('/image/description')->{Contents}}) { if (UNIVERSAL::isa($element, 'XML::Grove::Element') && $element->{Name} eq "field") { $fieldName = $element->{Attributes}{'name'}; $fieldValue = ""; if (grep (/^$fieldName$/, keys(%fields))) { beVerbose(" Reading field '$fieldName':", 3); foreach my $characters (@{$element->{Contents}}) { #if (UNIVERSAL::isa($characters, 'XML::Grove::Characters')) { $fieldValue .= $characters->as_canon_xml(); } $fieldValue = trimWhiteSpace(decode_entities(xml2html($fieldValue))); beVerbose("'".$fieldValue."'\n", 3); $descHash{$fieldName} = $fieldValue; } else { beVerbose(" Ignoring unknown field '$fieldName'.", 3); } } } return %descHash; } # Given an XML doc as a Grove, returns # hash from the Exif fields of the description file. sub getExifXML{ my $document = shift; my $priorityList = shift; #my $fieldNb; my $fieldName; my $fieldValue; my %descHash; beVerboseN(" Reading Exif data from description file...", 3); @{$priorityList} = @priorityExifTags; foreach my $element (@{$document->at_path('/image/exif')->{Contents}}) { if (UNIVERSAL::isa($element, 'XML::Grove::Element') && $element->{Name} eq "tag") { #$fieldNb = $element->{Attributes}{'no'}; $fieldName = $element->{Attributes}{'name'}; $fieldValue = ""; beVerbose(" Reading Exif field '$fieldName':", 3); foreach my $characters (@{$element->{Contents}}) { #if (UNIVERSAL::isa($characters, 'XML::Grove::Characters')) { $fieldValue .= $characters->as_canon_xml(); } if (defined $fieldValue){ $fieldValue = trimWhiteSpace(decode_entities(xml2html($fieldValue))); } else { $fieldValue = ""; } beVerbose("'".$fieldValue."'\n", 3); $descHash{$fieldName} = $fieldValue; if ($element->{Attributes}{'priority'}){ push @{$priorityList}, $fieldName; } } } return %descHash; } sub addExif{ my $tagName = shift; my $tagValue = shift; my $exifHash = shift; my $priorityList = shift; $tagValue = trimWhiteSpace($tagValue); if (grep (/^$tagName$/, @{$priorityList})){ beVerboseN(" Keeping value '$exifHash->{$tagName}' for $tagName", 3); beVerboseN(" $tagName has priority over exif value '$tagValue'.", 3); return 0; } if ( ! (exists $exifHash->{$tagName} && $exifHash->{$tagName} eq $tagValue) ) { beVerboseN(" Found new value '".$tagValue."' for $tagName", 3); beVerboseN(" old value was '".$exifHash->{$tagName}."'", 3) if ($exifHash->{$tagName}); beVerboseN(" bla '".$exifHash->{$tagName}."'", 3) if ($exifHash->{$tagName}); $exifHash->{$tagName} = $tagValue; return 1; } return 0; } sub addSpecialExif{ my $tagName = shift; my $tagValue = shift; my $exifHash = shift; my $priorityList = shift; if (UNIVERSAL::isa(\$tagValue, 'SCALAR')){ # Canon special tags if ($tagName eq "Canon-Tag-0x0007"){ return addExif("Software", $tagValue, $exifHash, $priorityList); } if ($tagName eq "Canon-Tag-0x0009"){ return addExif("Owner", $tagValue, $exifHash, $priorityList); } } elsif (UNIVERSAL::isa($tagValue, 'ARRAY')) { if ($tagValue->[1] eq 0) { return 0; } if ($tagName eq "SubjectDistance") { $tagValue = $tagValue->[0]/$tagValue->[1]; $tagValue .= translate(" meters"); return addExif($tagName, $tagValue, $exifHash, $priorityList); } if ($tagName eq "ExposureTime") { $tagValue = "1/".sprintf("%.0f", 1/($tagValue->[0]/$tagValue->[1])); $tagValue .= translate(" second"); return addExif($tagName, $tagValue, $exifHash, $priorityList); } if ($tagName eq "ShutterSpeedValue") { # this is the actual APEX Value my $APEXval = $tagValue->[0]/$tagValue->[1]; # ...but we want to see time in seconds instead if ($APEXval > 0) { $tagValue = sprintf("1/%d", 2**((1)*$APEXval)); } else { $tagValue = sprintf("%d", 2**((-1)*$APEXval)); } $tagValue .= translate(" sec"); return addExif($tagName, $tagValue, $exifHash, $priorityList); } if ($tagName eq "FNumber" || $tagName eq "FocalPlaneXResolution" || $tagName eq "FocalPlaneYResolution") { $tagValue = sprintf("%.2f", $tagValue->[0]/$tagValue->[1]); return addExif($tagName, $tagValue, $exifHash, $priorityList); } if ($tagName eq "FocalLength") { $tagValue = sprintf("%.2f", $tagValue->[0]/$tagValue->[1]). translate(" millimeters"); return addExif($tagName, $tagValue, $exifHash, $priorityList); } if ($tagName eq "ApertureValue" || $tagName eq "MaxApertureValue") { $tagValue = "F".sprintf("%.2f", sqrt(2)**($tagValue->[0]/$tagValue->[1])). " (".sprintf("%.2f", $tagValue->[0]/$tagValue->[1])." APEX)"; return addExif($tagName, $tagValue, $exifHash, $priorityList); } if ($tagName eq "BitsPerSample") { my $result=""; $result .= $_." " foreach(@{$tagValue}); return addExif($tagName, $result, $exifHash, $priorityList); } if ($tagName eq "CompressedBitsPerPixel") { my $result; SWITCH: { $tagValue->[0]==1 && do { $result="Basic"; last; }; $tagValue->[0]==2 && do { $result="Normal"; last; }; $tagValue->[0]==3 && do { $result="Normal"; last; }; $tagValue->[0]==4 && do { $result="Fine"; last; }; $tagValue->[0]==5 && do { $result="Very fine"; last; }; } $result .= " ($tagValue->[0]:$tagValue->[1])"; return addExif($tagName, $result, $exifHash, $priorityList); } if ($tagName eq "Canon-Tag-0x0001") { # CanonMacro my $value = $tagValue->[1]; if ($value == 1) { $value = translate("On"); } elsif ($value == 2) { $value = translate("Off"); } else { $value=""; } my $modified = addExif("CanonMacro", $value, $exifHash, $priorityList) if($value); # CanonTimerLength $value = $tagValue->[2]; if ($value) { $value = $value."/10".translate(" second"); $modified |= addExif("CanonTimerLength", $value, $exifHash, $priorityList); } # CanonQuality $value = $tagValue->[3]; if ($value == 2) { $value = translate("Normal"); } elsif ($value == 3) { $value = translate("Fine"); } elsif ($value == 5) { $value = translate("Superfine"); } else { $value=""; } $modified |= addExif("CanonQuality", $value, $exifHash, $priorityList) if ($value); # CanonFlashMode $value = $tagValue->[4]; if ($value == 1) { $value = translate("Auto"); } elsif ($value == 2) { $value = translate("On"); } elsif ($value == 3) { $value = translate("Red-eye reduction"); } elsif ($value == 4) { $value = translate("Slow synchro"); } elsif ($value == 5) { $value = translate("Auto + red-eye reduction"); } elsif ($value == 6) { $value = translate("On + red-eye reduction"); } elsif ($value == 16) { $value = translate("External flash"); } else { $value=""; } $modified |= addExif("CanonFlashMode", $value, $exifHash, $priorityList) if ($value); # CanonContinuousDriveMode $value = $tagValue->[5]; if ($value) { $value = translate("On"); $modified |= addExif("CanonContinuousDriveMode", $value, $exifHash, $priorityList); } # CanonFocusMode $value = $tagValue->[7]; if ($value == 1) { $value = translate("AI Servo"); } elsif ($value == 2) { $value = translate("AI Focus"); } elsif ($value == 3) { $value = translate("MF"); } elsif ($value == 4) { $value = translate("Single"); } elsif ($value == 5) { $value = translate("Continuous"); } elsif ($value == 6) { $value = translate("MF"); } elsif ($value == 0) { $value = translate("One-Shot"); } else { $value=""; } $modified |= addExif("CanonFocusMode", $value, $exifHash, $priorityList) if ($value); # CanonImageSize $value = $tagValue->[10]; if ($value == 0) { $value = translate("Large"); } elsif ($value == 1) { $value = translate("Medium"); } elsif ($value == 2) { $value = translate("Small"); } else { $value=""; } $modified |= addExif("CanonImageSize", $value, $exifHash, $priorityList) if ($value); # CanonEasyShootingMode $value = $tagValue->[11]; if ($value == 1) { $value = translate("Manual"); } elsif ($value == 2) { $value = translate("Landscape"); } elsif ($value == 3) { $value = translate("Fast Shutter"); } elsif ($value == 4) { $value = translate("Slow Shutter"); } elsif ($value == 5) { $value = translate("Night"); } elsif ($value == 6) { $value = translate("B&W"); } elsif ($value == 7) { $value = translate("Sepia"); } elsif ($value == 8) { $value = translate("Portrait"); } elsif ($value == 9) { $value = translate("Sports"); } elsif ($value == 10) { $value = translate("Macro / Close-Up"); } elsif ($value == 11) { $value = translate("Pan Focus"); } elsif ($value == 0) { $value = translate("Full Auto"); } else { $value=""; } $modified |= addExif("CanonEasyShootingMode", $value, $exifHash, $priorityList) if ($value); # CanonDigitalZoom $value = $tagValue->[12]; if ($value == 1) { $value = translate("2x"); } elsif ($value == 2) { $value = translate("4x"); } elsif ($value == 0) { $value = translate("None"); } else { $value=""; } $modified |= addExif("CanonDigitalZoom", $value, $exifHash, $priorityList) if ($value); # CanonContrast $value = $tagValue->[13]; if ($value == 0xFFFF) { $value = translate("Low"); } elsif ($value == 0x0000) { $value = translate("Normal"); } elsif ($value == 0x0001) { $value = translate("High"); } else { $value=""; } $modified |= addExif("CanonContrast", $value, $exifHash, $priorityList) if ($value); # CanonSaturation $value = $tagValue->[14]; if ($value == 0xFFFF) { $value = translate("Low"); } elsif ($value == 0x0000) { $value = translate("Normal"); } elsif ($value == 0x0001) { $value = translate("High"); } else { $value=""; } $modified |= addExif("CanonSaturation", $value, $exifHash, $priorityList) if ($value); # CanonSharpness $value = $tagValue->[15]; if ($value == 0xFFFF) { $value = translate("Low"); } elsif ($value == 0x0000) { $value = translate("Normal"); } elsif ($value == 0x0001) { $value = translate("High"); } else { $value=""; } $modified |= addExif("CanonSharpness", $value, $exifHash, $priorityList) if ($value); # CanonISO $value = $tagValue->[16]; if ($value == 15) { $value = translate("Auto"); } elsif ($value == 16) { $value = "50"; } elsif ($value == 17) { $value = "100"; } elsif ($value == 18) { $value = "200"; } elsif ($value == 19) { $value = "400"; } else { $value=""; } $modified |= addExif("CanonISO", $value, $exifHash, $priorityList) if ($value); # CanonFocusType if ($tagValue->[18]) { $value = $tagValue->[18]; if ($value == 1) { $value = translate("Auto"); } elsif ($value == 3) { $value = translate("Close-up (macro)"); } elsif ($value == 8) { $value = translate("locked (pan mode)"); } elsif ($value == 0) { $value = translate("Manual"); } else { $value=""; } $modified |= addExif("CanonFocusType", $value, $exifHash, $priorityList) if ($value); } return $modified; } } return 0; } # Merge picture Exif data with Exif data from desc file, then update # descHash if tag is in @fields and if description text file field is void. # Finally, write Exif tags to the desc file if it was updated. sub processExif{ my $crntImage = shift(@_); my $descHash = shift(@_); my $exifHash = shift(@_); my $document = shift(@_); my $priorityList = shift(@_); my $configHash = shift(@_); my ($base,$dir,$type) = &fileparse($crntImage, '\.[^.]+\z'); beVerboseN(" Reading Exif info from picture file $crntImage...", 2); my $pictureFile = $picdir.$dir.$base.$type; my($camerainfo) = image_info($pictureFile); if (exists $camerainfo->{"error"}) { beVerboseN(" Can't read info from picture file $crntImage: ". $camerainfo->{"error"}."\n", 2); return } # Add new Exif tags from picture file to Exif Hash from desc file my $tagName; my $tagValue; my $modified = 0; while ( ($tagName, $tagValue) = each(%$camerainfo) ) { $tagName =~ s/[\x00-\x1F]//g; if (UNIVERSAL::isa(\$tagValue, 'SCALAR')){ # strip characters after the first control code (ascii code <32 ) #$tagValue =~ s/^([\x20-\xFF]*)[^\x20-\xFF].*$/$1/; $tagValue =~ s/[\x00-\x1F].*$//s; $modified |= addExif($tagName, $tagValue, $exifHash, $priorityList); } #print "«$tagName» : "; #print $_, ', ' foreach(@{$tagValue}); #print "\n"; $modified |= addSpecialExif($tagName, $tagValue, $exifHash, $priorityList); } # add value to desc Hash if field is void foreach my $field (keys(%fields)) { my $fieldExif = $fields{$field}->{'EXIF'}; if ((! $descHash->{$field}) && $fieldExif && $exifHash->{$fieldExif}) { beVerboseN(" Using '$field' from EXIF data: ". $exifHash->{$fieldExif}, 3); $descHash->{$field} = $exifHash->{$fieldExif}; if ($fields{$field}->{'Transform'}) { beVerbose(" Evaluating ". "$descHash->{$field} =~ ".$fields{$field}->{'Transform'}. " to ", 4); eval '$descHash->{$field} =~ '.$fields{$field}->{'Transform'}; beVerboseN("'$descHash->{$field}'.", 4); } } } if ($configHash->{rotateImages} eq 'original'){ # Perform a rotation of the picture if needed # we're doing it here because we must change and save the # orientation tag if the image was rotated if(rotateImage($pictureFile, $exifHash->{Orientation}, $exifHash->{file_ext}, $configHash)){ progressifyImage("$pictureFile", $exifHash->{file_ext}, $configHash); push @{$priorityList}, "Orientation"; $exifHash->{Orientation} = "top_left"; } } # Write Exif tags to desc file if ($modified && $configHash->{addExifToDescFile}) { writeExif($crntImage, $exifHash, $document, $priorityList, $configHash); } } sub charac_indent{ my $n = shift(@_); my $s="\n"; for (1..$n){ $s .= " "; } return XML::Grove::Characters->new ( Data => $s ); } sub writeExif{ my $crntImage = shift; my $descHash = shift; my $document = shift; my $priorityList = shift; my $configHash = shift; my $description; my $exif; my $element; my $characters; my $file = getDescFile($crntImage); # create Grove document if file didn't exist if (!$document) { beVerbose(" Creating new XML description file $file... ", 3); $description = XML::Grove::Element->new ( Name => 'description', Contents => [charac_indent(1)]); my $bins = XML::Grove::Element->new ( Name => 'bins', Contents => [charac_indent(1)]); $exif = XML::Grove::Element->new ( Name => 'exif', Contents => [charac_indent(2)]); $element = XML::Grove::Element->new ( Name => 'image', Contents => [charac_indent(1), $description, charac_indent(1), $bins, charac_indent(1), $exif ]); $document = XML::Grove::Document->new ( Contents => [ $element ] ); if ($configHash->{createEmptyDescFields}){ # Add standard fields if a new file is generated (so you can # edit it better) my @fields = @mainFields; push @fields, 'title'; foreach my $field (@fields) { $element = XML::Grove::Element->new ( Name => 'field', Contents => [charac_indent(3), charac_indent(2)], Attributes => {"name" => $field}); push @{$description->{Contents}}, (charac_indent(2), $element); } push @{$description->{Contents}}, charac_indent(1); } beVerboseN("OK.", 3); } # Add exif tags to the Grove if (!$exif) { $exif = $document->at_path('/image/exif'); @{$exif->{Contents}}=(); } my $tagName; my $tagValue; while ( ($tagName, $tagValue) = each(%$descHash) ) { beVerboseN(" Adding Exif tag '$tagName'='$tagValue' in XML desc file.", 3); #$tagValue = html2xml($tagValue); $characters = XML::Grove::Characters->new ( Data => $tagValue ); $element = XML::Grove::Element->new ( Name => 'tag', Contents => [charac_indent(3), $characters, charac_indent(2)], Attributes => {"name" => $tagName}); if (grep (/^$tagName$/, @{$priorityList})){ $element->{Attributes}{"priority"} = "1"; beVerboseN(" with 'priority' attribute.\n", 3); } push @{$exif->{Contents}}, (charac_indent(2), $element); } push @{$exif->{Contents}}, charac_indent(1); # Write the Grove to the desc file beVerbose(" Saving XML description file $file... ", 3); my $fileHandler = new IO::File; open($fileHandler, '>', $file) or die("Cannot open file $file to write Exif tag ($!)"); binmode($fileHandler, ":utf8") if $^V ge v5.8.0; # my $composer = new XML::Handler::Composer (); # my $my_handler = new XML::Filter::Reindent (Handler => $composer); #print Dumper($document); my $my_handler = new XML::Handler::YAWriter( 'Output' => $fileHandler, 'Encoding' => $configHash->{xmlEncoding}, # 'Pretty' => { # 'NoProlog' =>0, # 'NoDTD' =>0, # 'NoPI' =>0, # 'PrettyWhiteIndent'=>1, # 'PrettyWhiteNewline'=>1, # 'NoWhiteSpace'=>0, # 'NoComments'=>0, # 'AddHiddenNewline'=>0, # 'AddHiddenAttrTab'=>1, # } ); # my $my_handler = XML::Handler::XMLWriter->new( Output => $fileHandler, # Newlines => 0); $document->parse(DocumentHandler => $my_handler); close ($fileHandler) || die ("can't close $file ($!)"); beVerboseN("OK.", 3); } sub getDescAlbum { my $descFile = shift(@_); # file name of the album desc file my $hash = shift(@_); # ref to the album description hash my $configHash = shift(@_);# ref to the current config hash my $fieldName; my $fieldValue; my @AlbumFieldNames = ( "title", "sampleimage", "shortdesc", "longdesc", "ignore"); beVerboseN("Reading album description file '$descFile'...", 3); my $document = getXMLAsGrove($descFile); # I have to do that, don't ask me why... #$XML::UM::ENCDIR="/usr/lib/perl5/XML/Parser/"; #my $encode = XML::UM::get_encode ( # Encoding => 'ISO-8859-9', # EncodeUnmapped => \&XML::UM::encode_unmapped_dec); $configHash = getConfigXML($document, "/album/bins", $configHash); foreach my $element (@{$document->at_path('/album/description')->{Contents}}) { if (UNIVERSAL::isa($element, 'XML::Grove::Element') && $element->{Name} eq "field") { $fieldName = $element->{Attributes}{'name'}; $fieldValue = ""; if (grep (/^$fieldName$/, @AlbumFieldNames)) { beVerbose(" Reading field '$fieldName':", 3); foreach my $characters (@{$element->{Contents}}) { $fieldValue .= $characters->as_canon_xml(); } #if ($fieldName ne "shortdesc" && $fieldName ne "longdesc"){ # $fieldValue = decode_entities($fieldValue); #} if ($fieldName eq "sampleimage"){ $fieldValue = trimWhiteSpace(decode_entities($fieldValue)); beVerbose("'".$fieldValue."'\n", 3); }else{ $fieldValue = trimWhiteSpace(decode_entities(xml2html($fieldValue))); beVerbose("'".$fieldValue."'\n", 3); } # $fieldValue = $encode->(trimWhiteSpace($fieldValue)); $hash->{$fieldName} = $fieldValue; } else { beVerboseN(" Ignoring unknown field '$fieldName'.", 3); } } } return $configHash; } sub getConfigColors{ my $colors = shift; my $style = shift; my $configHash = shift; my $modified = 0; foreach my $color (@{$colors->{Contents}}) { if (UNIVERSAL::isa($color, 'XML::Grove::Element')){ if ($color->{Name} eq "color") { if (defined $color->{Attributes}{'name'}){ my $fieldName = $color->{Attributes}{'name'}; my $fieldValue; foreach my $characters (@{$color->{Contents}}) { $fieldValue .= $characters->as_canon_xml(); } if (defined $fieldValue){ $fieldValue = trimWhiteSpace(decode_entities(xml2html($fieldValue))); } if ($fieldValue){ $configHash->{colorsSubs}{$style}{$fieldName.'COLOR'} = $fieldValue; $modified = 1; } else { beVerboseN("Warning, no value for '$fieldName' color, ignoring.", 1); } } else { beVerboseN("Warning, missing 'name' attribute for tag, ". "ignoring.", 1); } } else { beVerboseN("Warning, unknown tag <".$color->{Name}. "> in 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 , ". "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 , ". "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 , ". "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 ". "in section, ignoring all sizes.", 1); return 0; } } else { beVerboseN("Warning, unknown tag <".$size->{Name}."> ". "in section, ignoring.", 1); } } } if (@names) { $configHash->{longSizeNames} = \@names; $configHash->{sizeNames} = \@shortNames; $configHash->{scaledWidths} = \@widths; $configHash->{scaledHeights} = \@heights; return 1; } else { print "Warning, section is empty, ignoring.\n" } return 0; } # Given an XML doc as a Grove, the path to tag and the current # configuration hash, returns hash from the config section of files # ( tag) merged with current hash. sub getConfigXML{ my $document = shift(@_); # Grove document my $path = shift(@_); # path of the 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 ". "in section, ignoring.", 1); } } else { beVerboseN(" Warning, unknown tag <$element->{Name}> ". "in section, ignoring.", 1); } } } if (!$modified){ return $currentConfigHash; # return the original to not keep the # new copy in memory } return \%configHash; } sub commandAvailable{ my $command = shift; my $exists = 0; my (@PATH) = split (/:/, $ENV{"PATH"}); foreach (@PATH) { beVerboseN(" looking for $_/$command...", 4); $exists++ if (-f "$_/$command" && -x "$_/$command"); last if $exists; } return $exists; } BEGIN { my $rotateGeneric="none"; sub rotateGenericCommand{ my $file = shift; my $degrees = shift; my $verbose = shift; if ($rotateGeneric eq "none") { beVerbose(" Looking for a generic rotation utility (mogrify)... ", 3); if (commandAvailable("mogrify")) { $rotateGeneric = 'mogrify -rotate %s "%s"'; beVerboseN(" found mogrify.", 3); } else { beVerboseN(" not found, cannot rotate.", 3); $rotateGeneric = ""; } } if ($rotateGeneric) { $file =~ s|\\|\\\\|g; $file =~ s|\$|\\\$|g; $file =~ s|"|\\"|g; $file =~ s|`|\\`|g; return sprintf($rotateGeneric, $degrees, $file); } return ""; } } BEGIN { my $rotateJPEG="none"; sub rotateJPEGCommand{ my $file = shift; my $degrees = shift; my $verbose = shift; if ($rotateJPEG eq "none") { beVerbose("\n Looking for a JPEG rotation utility (jpegtran)... ", 3); if (commandAvailable("jpegtran")) { $rotateJPEG = 'jpegtran -copy all -rotate %s -outfile "%s.tmp" "%s" && mv "%s.tmp" "%s"'; beVerboseN(" found jpegtran.", 3); } else { $rotateJPEG = ""; beVerboseN(" not found, trying a generic one.", 3); } } if ($rotateJPEG) { $file =~ s|\\|\\\\|g; $file =~ s|\$|\\\$|g; $file =~ s|"|\\"|g; $file =~ s|`|\\`|g; return sprintf($rotateJPEG, $degrees, $file, $file, $file, $file); } return rotateGenericCommand($file, $degrees); } } # return the best command available for rotating a given file type sub rotateCommand{ my $type = shift; my $file = shift; my $degrees = shift; my $configHash = shift; if ($type eq "jpg" && $configHash->{rotateWithJpegtran}){ return rotateJPEGCommand($file, $degrees, $verbose); }else{ return rotateGenericCommand($file, $degrees, $verbose); } } # Rotate an image. Return 0 if no rotation was performed, 1 if a # rotation was performed and it changed width and heigth, and 2 if it # was a 180 degree rotation sub rotateImage{ my $imageName = shift; my $orientation = shift; my $ext = shift; my $configHash = shift; #if (!$imageInfo->{'BinsRotated'} && $imageInfo->{'Orientation'}){ if (!$orientation){ return 0; } beVerboseN(" Orientation for picture is '$orientation'", 3); if ($orientation eq "top_left"){ return 0; } if ($imageName =~ m/$configHash->{noRotation}/) { return 0; } my $degrees; if ($orientation eq "right_top"){ $degrees = 90; }elsif ($orientation eq "left_bot"){ $degrees = 270; }elsif ($orientation eq "bot_right"){ $degrees = 180; } else { print "Warning, Orientation field has an unknown value '$orientation'.\n"; return 0; } beVerbose(" Performing $degrees degrees rotation clockwise on $imageName... ", 2); my $type = ""; if ($ext) { $type = $ext; }else{ $type="jpg"; } my $command = rotateCommand($type, $imageName, $degrees, $configHash); if ($command){ beVerbose("\n Running '$command'... ", 3); if(!system($command)){ #$imageInfo->{'BinsRotated'}="yes"; beVerboseN("OK", 2); if ($degrees == 180){ return 2; } return 1; } }else{ beVerboseN("impossible.\n Cannot perform rotation, no utility installed.", 2); } return 0; } BEGIN { my $progressifyJPEG="none"; sub progressifyJPEGCommand{ my $filein = shift; my $fileout = shift; my $verbose = shift; if ($progressifyJPEG eq "none") { beVerbose("\n Looking for a progressive JPEG utility (jpegtran)... ", 3); if (commandAvailable("jpegtran")) { $progressifyJPEG = 'jpegtran -copy all -progressive -outfile "%s" "%s"'; beVerboseN(" found jpegtran.", 3); } else { $progressifyJPEG = ""; beVerboseN(" not found, cannot make JPEGs progressive", 3); } } if ($progressifyJPEG) { $filein =~ s|\\|\\\\|g; $filein =~ s|\$|\\\$|g; $filein =~ s|"|\\"|g; $filein =~ s|`|\\`|g; $fileout =~ s|\\|\\\\|g; $fileout =~ s|\$|\\\$|g; $fileout =~ s|"|\\"|g; $fileout =~ s|`|\\`|g; return sprintf($progressifyJPEG, $fileout, $filein); } return ""; } } # fileSizeCmp(file1, file2): return file1.file-size - file2.file-size sub fileSizeCmp { my $file1 = shift; my $file2 = shift; return ((stat($file1))[7]) - ((stat($file2))[7]); } # progressifyJPEGImage(imageName, configHash) # make a JPEG image progressive. Return 0 if no conversion was performed, and 1 if a # conversion was performed sub progressifyJPEGImage{ my $imageName = shift; my $configHash = shift; my $tempFile = "$imageName.tmp"; beVerbose(" Making $imageName progressive JPEG... ", 2); my $command = progressifyJPEGCommand($imageName, $tempFile, $verbose); if ($command) { beVerbose("\n Running '$command'... ", 3); if(!system($command)){ if (($configHash->{jpegProgressify} eq "always")) { rename($tempFile, $imageName) or die("\nfailed to rename $tempFile to $imageName: $?"); beVerboseN("OK", 2); return 1; } my $diff = fileSizeCmp($tempFile, $imageName); if ($diff < 0) { rename($tempFile, $imageName) or die("\nfailed to rename $tempFile to $imageName: $?"); $diff = -$diff; beVerbose("$diff bytes smaller; ", 3); beVerboseN("OK", 2); return 1; } else { beVerbose("$diff bytes larger; ", 3); beVerboseN("NO", 2); unlink($tempFile) or warn("\nfailed to unlink $tempFile: $?"); } } }else{ beVerboseN("impossible.\n Cannot make progressive JPEG, no utility installed.", 2); } return 0; } # progressifyImage(imageName, ext, configHash) # make a JPEG image progressive. Return 0 if no conversion was performed, and 1 if a # conversion was performed. Skips non JPEG images or when jpegProgressify # is false (returns 0). sub progressifyImage{ my $imageName = shift; my $ext = shift; my $configHash = shift; my $type = $ext ? $ext : "jpg"; if (($type eq "jpg") && ($configHash->{jpegProgressify} ne "never")) { return progressifyJPEGImage($imageName, $configHash); } return 0; } sub readConfigFile{ my $configHash = shift(@_); my $document; my $globalConfigFile = bsd_glob($configHash->{'globalConfigDir'}."/". $configHash->{'configFileName'}, GLOB_TILDE); if ($globalConfigFile && -e $globalConfigFile) { beVerboseN("Reading global configuration file $globalConfigFile.", 3); $document = getXMLAsGrove($globalConfigFile); $configHash = getConfigXML($document, "/bins", $configHash); }else{ beVerboseN("No global configuration file ". $configHash->{'globalConfigDir'}."/". $configHash->{'configFileName'}." found.", 3); } my $userConfigFile = bsd_glob($configHash->{'userConfigDir'}."/". $configHash->{'configFileName'}, GLOB_TILDE); if ($userConfigFile && -e $userConfigFile) { beVerboseN("Reading user configuration file $userConfigFile.", 3); $document = getXMLAsGrove($userConfigFile); $configHash = getConfigXML($document, "/bins", $configHash); }else{ beVerboseN("No user configuration file ".$configHash->{'userConfigDir'}."/". $configHash->{'configFileName'}." found.", 3); } return $configHash; } sub stringToBool{ my $string = shift(@_); if (! $string) {return 0;} if (trimWhiteSpace($string) eq "true") { return 1;} return 0; } sub trimWhiteSpace{ my $string = shift(@_); return "" if (! defined $string); for ($string) { s/^\s+//; s/\s+$//; } return $string; } sub BEGIN { my %ignoredSets; my %hiddenSets; # this function is only used by ignoreSet, hiddenSet and # ignoreAndHiddenSet functions below sub ignoreOrHiddenSet{ my $ignoreLine = shift; my $album = shift; my $type = shift; my $configHash = shift; my ($set, $userSet); if ($type eq "ignore") { $set = \%ignoredSets; $userSet = $ignoreOpts; if ($ignoreOpts && $configHash->{ignore}){ $userSet .= "," } $userSet .= $configHash->{ignore}; } elsif ($type eq "hidden") { $set = \%hiddenSets; $userSet = $hiddenOpts; if ($hiddenOpts && $configHash->{hidden}){ $userSet .= "," } $userSet .= $configHash->{hidden}; } else { return 0; } # Remove the trailing slash if there is one (or more!) in $album, so # that the hash works for $fileInAlbum invocations as well. if ( defined($album) ) { $album =~ s/\/$//sog; } # Use the hash to optimize the checking of ignoreSets(), since this is # called three times for each album, and it could be expensive if there # are many ignore directives. If we have already checked this album, # just report what we concluded last time. if ( defined($set->{$album}) && $album ne "" ) { return( $set->{$album} ); } if ( defined($ignoreLine) ) { for my $thisIgnoreOpts ( split(',', $userSet ) ) { for my $thisIgnoreLine ( split('\s*,\s*',$ignoreLine) ) { if ( $thisIgnoreOpts eq $thisIgnoreLine ) { $set->{$album}=1; beVerboseN("Skipping \"$album\" because of \"$thisIgnoreOpts\"". " IGNORE directive.", 2); return(1); } } } } $set->{$album}=0; return(0); } } # return 1 if one of $ignoreLine is a ignored album sub ignoreSet{ my $ignoreLine = shift; my $album = shift; my $configHash = shift; return ignoreOrHiddenSet($ignoreLine, $album, "ignore", $configHash); } # return 1 if one of $ignoreLine is a hidden album sub hiddenSet{ my $ignoreLine = shift; my $album = shift; my $configHash = shift; return ignoreOrHiddenSet($ignoreLine, $album, "hidden", $configHash); } # return 1 if one of $ignoreLine is a ignored or hidden album sub ignoreAndHiddenSet{ my $ignoreLine = shift; my $album = shift; my $configHash = shift; if (ignoreOrHiddenSet($ignoreLine, $album, "ignore", $configHash)) { return 1 } return ignoreOrHiddenSet($ignoreLine, $album, "hidden", $configHash); } sub beVerbose { my $output = shift; my $level = shift; print "$output" if ($verbose >= $level); } sub beVerboseN { my $output = shift; my $level = shift; if ($verbose >= $level){ beVerbose($output, $level); print "\n"; } } # for .po generation, until xgettext handles Perl correctly sub dummy_I18N{ _("Thumbnail Page"); _("Thumbnail Page 1"); _("tree"); _("subalbum"); _("subalbums"); _("Hg"); _("Huge"); _("image"); _("images"); _("Click on one of the size names above to enlarge this image"); _("Click to view thumbnails of the current album"); _("Click to view this album"); }