#!/usr/bin/perl -w =head1 NAME uffizi - Generate photo galleries from directories of image files =head1 SYNOPSIS uffizi [options] directory ... (type 'uffizi --help' for option listing) =head1 DESCRIPTION I've tried lots of 'photo album' web apps. All are either (a) CGI or similar 'fried' server-side code, which I don't want to use for something as simple as a photo album, or (b) are inflexible and ugly in their output. So here's YA album script. Its good points are: =over 4 =item very self-contained, apart from dependencies on C and the ImageMagick C command =item fast, efficient incremental rebuilding =item generates full CSS-styled, templated and valid HTML =item every part of the generated HTML can be modified through the templates =item generates reasonably-sized images as well as thumbnails, with a link to the full-sized image =back Its bad points: =over 4 =item it's written in perl. =back But if you ask me, that's a good point ;) =head1 NOTES Two external helper applications will be used if available; C is used to make progressive jpeg images. C is used to copy over EXIF metadata into scaled image copies. If either or both are not available, the script will skip that functionality and otherwise work perfectly. =head1 CREDITS It owes quite a bit of inspiration regarding the basic HTML layout, output filesystem structure, and so on to C from 'Dave's Marginal Hacks' (C). (thx Dave, nice script!) The name is a reference to Florence's Galleria degli Uffizi (C), because this app generates galleries of images ;) =head1 AUTHOR Justin Mason, uffizi at jmason dot org =head1 VERSION 0.5 Jan 4 2006 jm =cut my $GENERATOR = "uffizi/0.4 (http://jmason.org/software/uffizi/)"; my $KNOWN_IMG_EXTNS = qr{ jpe?g | jpe | gif | png | bmp | icon? | miff | mpeg | p[bgpn]m | tiff? }ix; my $DEFAULTS = { up_name => 'Albums', title_prefix => 'Album: ', index_filename => 'index.html', toplevel_reverse_order => 1, clean => 0, # rebuild from scratch target => '', metadata => 'metadata.txt', jpegtran => 'jpegtran', jhead => 'jhead', thumbnail_format => 'jpg', thumbnail_convert_args => 'convert -scale __WIDTH__x__HEIGHT__ '. '-border 1x1 -bordercolor black', thumbnail_convert_args_jpg => '-quality 70% -interlace Line', thumbnail_convert_args_png => '', thumbnail_max_h => 200, # max height for thumbs thumbnail_max_w => 200, # max width for thumbs scaled_format => 'jpg', scaled_convert_args => 'convert -scale __WIDTH__x__HEIGHT__', scaled_convert_args_jpg => '-quality 95% -interlace Line', scaled_convert_args_png => '', scaled_max_h => 640, # max height for scaled images scaled_max_w => 640, # max width for scaled images contents_columns => 3, # columns in the "contents" table fastnav_columns => 10 # columns in the "fast navigation" table }; ########################################################################### use File::Find; use Image::Size; use strict; my %template = (); my %found_dirs = (); my %found_metadata = (); # my %found_thumbs = (); my %toplevel_images = (); my %cached_imgsizes = (); my @dirs; my %images; my %metadata = (); my @uffizi_metadata_dirs = (); my %uffizi_metadata = (); my $gendate = scalar localtime; my $ctrl = $DEFAULTS; use Getopt::Long; use vars qw( $opt_help $opt_dumptemplates $opt_template ); my %opts = ( 'help|h' => \$opt_help, 'dumptemplates' => \$opt_dumptemplates, 'template=s' => \$opt_template ); foreach my $ctrlitem (sort keys %{$ctrl}) { $opts{"$ctrlitem=s"} = \$ctrl->{$ctrlitem}; } GetOptions (%opts) or usage(); usage() if ($opt_help); dump_templates() if ($opt_dumptemplates); usage() if (scalar @ARGV <= 0); check_helpers(); read_template(); read_target(); search_dirs(); read_metadata(); sort_images(); gen_thumbs(); gen_toplevel(); exit; sub read_target { my $tgt = $ctrl->{target}; return unless $tgt; my $html; if (-f $tgt && open IN, "<$tgt") { $html = join('', ); close IN; } elsif ($tgt =~ /^https?:/i) { print "target HTTP GET: $tgt\n"; eval q{ use LWP::Simple; $html = get $tgt; if (!defined($html)) { getprint $tgt; die "GET failed"; } }; if ($@) { die "GET $tgt failed: $@ $!"; } } return unless $html =~ m{ }isx; foreach my $line (split(/\n/s, $1)) { $line =~ s/^\s+//s; $line =~ s/\s+$//s; my %mdset = (); foreach my $mditem (split(/\|/, $line)) { next unless ($mditem =~ /^(.*?)=(.*)$/); $mdset{$1} = $2; } next unless defined($mdset{dir}); print "remote set: $mdset{dir}\n"; push @uffizi_metadata_dirs, $mdset{dir}; $uffizi_metadata{$mdset{dir}} = \%mdset; } } ########################################################################### sub usage { my $opts =<{$ctrlitem}."\")\n"; } die "usage: uffizi [options] directory ...\n\n".$opts."\n"; } ########################################################################### sub check_helpers { foreach my $helper (qw(jpegtran jhead)) { my $hpath = $ctrl->{$helper}; if ($hpath =~ /\//) { next if (-x $hpath); } else { # in the path system ("$helper < /dev/null > /dev/null 2>&1"); next if ($?>>8 != 127); # 127 == "command not found" } $ctrl->{$helper} = ''; # unset, so it's not used } } ########################################################################### sub read_template { my $tmpltext; if ($opt_template) { open (IN, "<$opt_template") or die "cannot read $opt_template"; $tmpltext = join ('', ); close IN; } else { $tmpltext = join ('', ); } $tmpltext =~ s/^.*?//is; $tmpltext =~ s/<\/uffizitheme>.*?$//is; 1 while ($tmpltext =~ s{ \s* (?:\s*)* \s* (.*?)\s* <\/\s*templatechunk\s*>\s* }{ parse_tmpl_block($1, $2); }exsig); if ($tmpltext =~ /\S/) { die "failed to parse template: unparseable: '$tmpltext'\n"; } } sub dump_templates { while () { print STDOUT; } exit; } sub parse_tmpl_block { my ($name, $chunk) = @_; $template{$name} = $chunk; return ''; } sub search_dirs { foreach my $dir (@ARGV) { File::Find::find ({ wanted => \&wanted, no_chdir => 1 }, $dir); } } sub wanted { return unless (-f $_); return if ($File::Find::name =~ /[\/\\](?:\.xvpics)[\/\\]/); if ($File::Find::name =~ /^(.*)[\/\\]tn[\/\\](.+)\.index\.html/) { # $found_thumbs{$1} ||= { }; # $found_thumbs{$1}->{$2} = 1; } elsif ($File::Find::name =~ /^(.*)[\/\\]tn[\/\\](.+\.${KNOWN_IMG_EXTNS})$/) { # $found_thumbs{$1} ||= { }; # $found_thumbs{$1}->{$2} = 1; } elsif ($File::Find::name =~ /^(.*)[\/\\](.+\.${KNOWN_IMG_EXTNS})$/) { $found_dirs{$1} ||= [ ]; push (@{$found_dirs{$1}}, { name => $2, dir => $1, textname => name_to_text ($2) }); } elsif ($File::Find::name =~ /^(.*)[\/\\]([^\/\\]+)$/) { my $dir = $1; my $fname = lc $2; my $mdata = lc $ctrl->{metadata}; # lowercase both if ($fname eq $mdata) { $found_metadata{$dir} = $fname; } } else { # warn "ignored $File::Find::name"; } } sub read_metadata { foreach my $dir (keys %found_metadata) { my $metafname = $found_metadata{$dir}; my ($k, $v); if (open (IN, "<$dir/$metafname")) { while () { s/\#.*$//; s/^\s+//; s/\s+$//; next if (/^$/); if (/(.+?)\s*[\t\|\,\:\=]\s*(.*)$/) { $k = $1; $v = $2; } elsif (/^(\S+)\s*(.*)$/) { $k = $1; $v = $2; } else { warn "$dir/$metafname: unparsed line: $_\n"; next; } $metadata{"$dir/$k"} = $v; } close IN; } } } sub dir_name_to_text { my $dir = shift; if ($dir eq '.') { return ''; } else { return name_to_text ($dir); } } sub name_to_text { my $fname = shift; $fname =~ s/[- _]+/ /gs; $fname =~ s/\.${KNOWN_IMG_EXTNS}$//g; return $fname; } sub sort_images { @dirs = sort { $a cmp $b } keys %found_dirs; foreach my $dir (@dirs) { sort_images_1_dir($dir); } } sub sort_images_1_dir { my $dir = shift; print "dir: $dir\n"; # my $thumbs = $found_thumbs{$dir}; my @ary = sort { $a->{name} cmp $b->{name} } @{$found_dirs{$dir}}; my $list = [ ]; my $prev; my $next; my $i; my $numitems = scalar @ary; for ($i = 0; $i < $numitems; $i++) { my $image = $ary[$i]; my $nextimg = $ary[($i+1) % $numitems]; my $previmg = $ary[($i-1) % $numitems]; my $obj = { previmg => $previmg, nextimg => $nextimg, thisimg => $image, }; my $mdata = $metadata{$dir."/".$image->{name}}; if ($mdata) { $image->{description} = $mdata; } # my $thumb = $thumbs->{$image}; # if ($thumb && check_thumb_validity($dir, $thumb, $image)) { # $obj->{thumbvalid} = 1; # } push (@{$list}, $obj); } $images{$dir} = $list; } sub check_thumb_validity { my ($dir, $thumb, $image) = @_; if ($ctrl->{clean}) { return 0; } return 1; # TODO } sub gen_thumbs { foreach my $dir (@dirs) { my $img; foreach $img (@{$images{$dir}}) { gen_thumbs_1_image ($dir, $img); gen_scaled_1_image ($dir, $img); } foreach $img (@{$images{$dir}}) { gen_thumb_page ($dir, $img); } gen_dirindex ($dir, 0); } } sub gen_toplevel { @{$images{'.'}} = (); my @tldirs = (sort(@uffizi_metadata_dirs), @dirs); if ($ctrl->{'toplevel_reverse_order'}) { @tldirs = reverse @tldirs; } my %done = (); foreach my $dir (@tldirs) { next if $done{$dir}; $done{$dir}++; my $obj = $toplevel_images{$dir}; if ($uffizi_metadata{$dir}) { $obj = { thisimg => $uffizi_metadata{$dir} }; $obj->{thisimg}->{isremote} = 1; } push (@{$images{'.'}}, $obj); } gen_dirindex ('.', 1); } sub gen_thumbs_1_image { my ($dir, $imgobj) = @_; my $maxh = $ctrl->{thumbnail_max_h}; my $maxw = $ctrl->{thumbnail_max_w}; my $img = $imgobj->{thisimg}->{name}; my $fmt = $ctrl->{thumbnail_format}; my $thumbrelname = $img."_".$maxw."x".$maxh.".".$fmt; my $thumbfullname = "$dir/tn/$thumbrelname"; my $thumbpage = gen_thumbpage_fname ($dir, $img); # set these so that the dir index can use them $imgobj->{thisimg}->{thumbrelname} = $thumbrelname; $imgobj->{thisimg}->{thumbfullname} = $thumbfullname; $imgobj->{thisimg}->{thumbpage} = $thumbpage; if (!$toplevel_images{$dir}) { $toplevel_images{$dir} = $imgobj; } # and generate the dirs and files, if needed (-d $dir) or mkdir ($dir) or die "$dir: cannot mkdir: $!\n"; (-d "$dir/tn") or mkdir ("$dir/tn") or die "$dir/tn: cannot mkdir: $!\n";; if (!gen_thumbnail_image ($dir, $img, $thumbfullname, $maxh, $maxw)) { return 0; } return 1; } sub gen_scaled_1_image { my ($dir, $imgobj) = @_; my ($scaledrelname, $scaledfullname, $needsscaling); my $img = $imgobj->{thisimg}->{name}; my $maxh = $ctrl->{scaled_max_h}; my $maxw = $ctrl->{scaled_max_w}; my ($imgw, $imgh) = myimgsize ("$dir/$img"); my $urlscaledrelname; if ($imgw <= $maxw && $imgh <= $maxw) { $scaledrelname = "../$img"; # relative to "$dir/tn" $urlscaledrelname = "../".urlencode($img); $scaledfullname = "$dir/$img"; $needsscaling = 0; # original will work fine, thx } else { my $fmt = $ctrl->{scaled_format}; $scaledrelname = $img."_".$maxw."x".$maxh.".".$fmt; $urlscaledrelname = urlencode($scaledrelname); $scaledfullname = "$dir/tn/$scaledrelname"; $needsscaling = 1; } # set these so that the dir index can use them $imgobj->{thisimg}->{scaledrelname} = $scaledrelname; $imgobj->{thisimg}->{urlscaledrelname} = $urlscaledrelname; $imgobj->{thisimg}->{scaledfullname} = $scaledfullname; # and generate the dirs and files, if needed if ($needsscaling) { (-d $dir) or mkdir ($dir) or die "$dir: cannot mkdir: $!\n"; (-d "$dir/tn") or mkdir ("$dir/tn") or die "$dir/tn: cannot mkdir: $!\n";; if (!gen_scaled_image ($dir, $img, $scaledfullname, $maxh, $maxw)) { return 0; } } return 1; } sub gen_thumb_page { my ($dir, $imgobj) = @_; my $img = $imgobj->{thisimg}->{name}; my $thumbpage = $imgobj->{thisimg}->{thumbpage}; my $imgfilepath = "$dir/$img"; # check file modtimes if (!$ctrl->{clean} && -f $thumbpage && -f $imgfilepath && -M $thumbpage < -M $imgfilepath) { print "unchanged_mtime: $thumbpage\n"; return 0; } my $thumbrelname = $imgobj->{thisimg}->{thumbrelname}; # no /s my $thumbfullname = $imgobj->{thisimg}->{thumbfullname}; # write the image page my $prevhref = ''; my $nexthref = ''; my $prevtext = ''; my $nexttext = ''; if ($imgobj->{previmg}) { $prevhref = gen_thumbpage_fname ('.', $imgobj->{previmg}->{name}); $prevhref =~ s/^\..tn.//; $prevtext = get_textname_from_image ($imgobj->{previmg}); } if ($imgobj->{nextimg}) { $nexthref = gen_thumbpage_fname ('.', $imgobj->{nextimg}->{name}); $nexthref =~ s/^\..tn.//; $nexttext = get_textname_from_image ($imgobj->{nextimg}); } my $thistext = get_textname_from_image ($imgobj->{thisimg}); my @navtrail = ''; my @path = split (/[\/\\]+/, $ctrl->{up_name}.'/'.$dir); my $levels = scalar @path; my $isroot = 1; foreach my $elem (@path) { my $opts = { HREF => (($levels ? ('../' x $levels) : '').$ctrl->{index_filename}), NAME => dir_name_to_text($elem) }; $levels--; if ($isroot) { push (@navtrail, fill_tmpl ($template{'img_navtrail_root'}, $opts)); $isroot = 0; # not any more you ain't } else { push (@navtrail, fill_tmpl ($template{'img_navtrail_trunk'}, $opts)); } } push (@navtrail, fill_tmpl ($template{'img_navtrail_leaf'}, { NAME => $thistext })); my ($imgw, $imgh) = myimgsize ($imgobj->{thisimg}->{scaledfullname}); my ($origw, $origh) = myimgsize ("$dir/$img"); my $opts = { PAGETITLE => $ctrl->{title_prefix}.$dir.": ".$thistext, GENERATOR => $GENERATOR, IMAGE_WIDTH => $imgw, IMAGE_HEIGHT => $imgh, IMAGE_PAGE_NAV => join ('', @navtrail), IMAGE_PAGE_BACK_LINK => "../".$ctrl->{index_filename}, IMAGE_BACK_HREF => $prevhref, IMAGE_BACK_NAME => $prevtext, IMAGE_FWD_HREF => $nexthref, IMAGE_FWD_NAME => $nexttext, IMAGE_FILE_HREF => "../".urlencode($img), IMAGE_FILE_WIDTH => $origw, IMAGE_FILE_HEIGHT => $origh, IMAGE_FILE_SRC => $imgobj->{thisimg}->{urlscaledrelname}, IMAGE_FILE_ALT => $thistext, FAST_NAV_TABLE => fast_nav_gen($dir, $imgobj), GEN_DATE => $gendate, }; write_html_if_different_from_disk ('image', $thumbpage, fill_tmpl ($template{'image_page'}, $opts)); } sub gen_dirindex { my ($dir, $istoplevel) = @_; my $dirindexfile = $dir."/".$ctrl->{index_filename}; my $maxh = $ctrl->{thumbnail_max_h}; my $maxw = $ctrl->{thumbnail_max_w}; # check modtimes against all source image files my $canskip = (!$ctrl->{clean} && -f $dirindexfile); my $built_modtime = (-M $dirindexfile); if ($canskip) { foreach my $imgobj (@{$images{$dir}}) { my $obj = $imgobj->{thisimg}; if ($obj->{isremote}) { $canskip = 0; # any remotes mean we have to rebuild the idx next; } my $origfile = $obj->{dir}.'/'.$obj->{name}; if (!(-f $origfile && $built_modtime < -M $origfile)) { $canskip = 0; last; } } if ($canskip) { print "unchanged_mtime: $dirindexfile\n"; return 0; } } my @trs = (); my @tds = (); my $onlinesofar = 0; foreach my $imgobj (@{$images{$dir}}) { my $obj = $imgobj->{thisimg}; my $origfile; my $sizekb; my $thumbpage; my $thumbsrc; my $textname; my $numimgs; if ($obj->{isremote}) { $origfile = undef; ($maxw, $maxh) = ($obj->{maxw}, $obj->{maxh}); $sizekb = -23; } else { $origfile = $obj->{dir}.'/'.$obj->{name}; ($maxw, $maxh) = myimgsize ($obj->{thumbfullname}); $sizekb = int (((-s $origfile) + 1023) / 1024); } if ($obj->{isremote}) { $thumbpage = $obj->{thumbpage}; $thumbsrc = $obj->{thumbsrc}; $textname = $obj->{textname}; $numimgs = $obj->{numimgs}; } elsif ($istoplevel) { $thumbpage = $obj->{dir}.'/'.$ctrl->{index_filename}; $thumbsrc = $obj->{dir}.'/tn/'.urlencode($obj->{thumbrelname}); $textname = dir_name_to_text($obj->{dir}); $numimgs = scalar @{$images{$obj->{dir}}}; } else { $thumbpage = $obj->{thumbpage}; my $depth = 0; my $countslashes = $obj->{dir}; while ($countslashes =~ s/^[^\\\/]+(?:[\\\/]+|$)//) { $depth++; } for (1 .. $depth) { $thumbpage =~ s/^[^\\\/]+[\\\/]+//; } $thumbsrc = "tn/".urlencode($obj->{thumbrelname}); $textname = get_textname_from_image ($obj); $numimgs = 1; } my $str = fill_tmpl ($template{'contents_td'}, { ISTOPLEVEL => $istoplevel, CONTENTS_IMG_PAGE_HREF => $thumbpage, CONTENTS_IMG_WIDTH => $maxw, CONTENTS_IMG_HEIGHT => $maxh, CONTENTS_IMG_BORDER => 0, CONTENTS_THUMB_SRC => $thumbsrc, CONTENTS_THUMB_ALT => $textname, CONTENTS_IMG_NAME => $textname, IMG_SIZE_KB => $sizekb, NUM_IMAGES => $numimgs, CONTENTS_IMG_DESCRIPTION => '' }); push (@tds, $str); $onlinesofar++; if ($onlinesofar >= $ctrl->{contents_columns}) { push (@trs, fill_tmpl ($template{'contents_tr'}, { CONTENTS_TDS => join ('', @tds) })); @tds = (); $onlinesofar = 0; } if ($istoplevel) { add_uffizi_metadata( dir => $obj->{dir}, name => $obj->{name}, numimgs => $numimgs, thumbpage => $thumbpage, thumbsrc => $thumbsrc, textname => $textname, maxw => $maxw, maxh => $maxh ); } } push (@trs, fill_tmpl ($template{'contents_tr'}, { CONTENTS_TDS => join ('', @tds) })); # now the general page stuff my @navtrail = ''; my @path = split (/[\/\\]+/, $ctrl->{up_name}.'/'.$dir); my $lastelem = pop @path; # drop the last item, this dir's name my $levels = scalar @path; my $isroot = 1; foreach my $elem (@path) { my $opts = { HREF => (($levels ? ('../' x $levels) : '').$ctrl->{index_filename}), NAME => dir_name_to_text($elem) }; $levels--; if ($isroot) { push (@navtrail, fill_tmpl ($template{'contents_navtrail_root'}, $opts)); $isroot = 0; # not any more you ain't } else { push (@navtrail, fill_tmpl ($template{'contents_navtrail_trunk'}, $opts)); } } my $thisdirastext = dir_name_to_text($lastelem); push (@navtrail, fill_tmpl ($template{'contents_navtrail_leaf'}, { NAME => $thisdirastext })); my $opts = { PAGETITLE => $istoplevel ? "top level" : $ctrl->{title_prefix}.$thisdirastext, GENERATOR => $GENERATOR, CONTENT_NAV => join ('', @navtrail), CONTENT_BACK_LINK => '../'.$ctrl->{index_filename}, CONTENTS_TRS => join ('', @trs), GEN_DATE => $gendate, UFFIZI_METADATA => $istoplevel ? get_uffizi_metadata() : '' }; write_html_if_different_from_disk ('index', $dirindexfile, fill_tmpl ($template{'contents_page'}, $opts)); } sub add_uffizi_metadata { my %opts = @_; push @uffizi_metadata_dirs, $opts{dir}; $uffizi_metadata{$opts{dir}} = \%opts; } sub get_uffizi_metadata { my %done = (); my $str = ''; foreach my $dir (@uffizi_metadata_dirs) { next if $done{$dir}; $done{$dir} = 1; $str .= join ('|', map { $_."=".$uffizi_metadata{$dir}->{$_} } (keys %{$uffizi_metadata{$dir}}))."\n"; } return $str; } sub write_html_if_different_from_disk { my ($type, $file, $contents) = @_; # compare with disk if (open (IN, "<$file")) { my $ondisk = join ('', ); close IN; my $update = $contents; $update =~ s/\s+//gs; $update =~ s/.*?//gs; $ondisk =~ s/\s+//gs; $ondisk =~ s/.*?//gs; if ($ondisk eq $update) { print "unchanged_html: $file\n"; return 0; } } $contents =~ s/^\s+//gm; # whitespace at SOL $contents =~ s/\s+$//gm; # whitespace at EOL $contents =~ s/\n\s+\n/\n/gs; # multiple newlines # OK, rewrite it if (!open (IDXPAGE, ">$file")) { warn "$file: failed to open: $!\n"; return -1; } print IDXPAGE $contents; if (!close IDXPAGE) { warn "$file: failed to write: $!\n"; return -1; } print "$type: $file\n"; return 1; } sub gen_thumbpage_fname { my ($dir, $img) = @_; $img =~ s/[^-_=\.A-Za-z0-9]+/_/gs; "$dir/tn/$img.index.html"; } sub fast_nav_gen { my ($dir, $img) = @_; my @trs = (); my @tds = (); my $onlinesofar = 0; foreach my $oimg (@{$images{$dir}}) { # use Data::Dumper; # print Dumper $oimg; # print Dumper $img; my $obj = $oimg->{thisimg}; my $thumbpage = $obj->{thumbpage}; $thumbpage =~ s/^[^\\\/]+[\\\/]tn[\\\/]//; # thumbpage: restaurant-1.jpg.png.index.html # thumbrelname: restaurant-1.jpg_200x200.png my $opts = { FAST_NAV_NAME => get_textname_from_image ($obj), FAST_NAV_HREF => $thumbpage, FAST_NAV_THUMB_SRC => urlencode($obj->{thumbrelname}), }; if ($img eq $oimg) { push (@tds, fill_tmpl ($template{'fast_nav_td_on'}, $opts)); } else { push (@tds, fill_tmpl ($template{'fast_nav_td_off'}, $opts)); } $onlinesofar++; if ($onlinesofar >= $ctrl->{fastnav_columns}) { push (@trs, fill_tmpl ($template{'fast_nav_trs'}, { FAST_NAV_TDS => join ('', @tds) })); @tds = (); $onlinesofar = 0; } } push (@trs, fill_tmpl ($template{'fast_nav_trs'}, { FAST_NAV_TDS => join ('', @tds) })); return fill_tmpl ($template{'fast_nav_table'}, { FAST_NAV_TRS => join ('', @trs) }); } sub gen_thumbnail_image { my ($dir, $img, $thumbname, $maxh, $maxw) = @_; return if (!$ctrl->{clean} && -f $thumbname && -s $thumbname); my $imgpath = "$dir/$img"; my $fmt = $ctrl->{thumbnail_format}; my $tmpthumbname = "$dir/tn/tmp.$img.$fmt"; my $tmp2thumbname = "$dir/tn/tmp2.$img.$fmt"; my $args = $ctrl->{thumbnail_convert_args}; if ($ctrl->{'thumbnail_convert_args_'.$fmt}) { $args .= ' '.$ctrl->{'thumbnail_convert_args_'.$fmt}; } $args =~ s/__WIDTH__/$maxw/g; $args =~ s/__HEIGHT__/$maxh/g; my @cmd = split (' ', $args); push (@cmd, $imgpath, $tmpthumbname); print "thumb: ".join (' ', @cmd)."\n"; system (@cmd); if ($? >> 8 != 0) { warn "$thumbname: failed to generate due to 'convert' failure\n"; return 0; } if ($fmt eq 'jpg' && $ctrl->{jpegtran}) { @cmd = ($ctrl->{jpegtran}, "-progressive", "-outfile", $tmp2thumbname, $tmpthumbname); print "jpegtran: ".join (' ', @cmd)."\n"; system (@cmd); if ($? >> 8 != 0) { warn "$thumbname: failed to generate due to '$ctrl->{jpegtran}' failure\n"; return 0; } rename ($tmp2thumbname, $tmpthumbname) or die "rename failed"; } if (!rename $tmpthumbname, $thumbname) { warn "$thumbname: rename from $tmpthumbname failed: $!\n"; return 0; } return 1; } sub gen_scaled_image { my ($dir, $img, $scaledname, $maxh, $maxw) = @_; return if (!$ctrl->{clean} && -f $scaledname && -s $scaledname); my $imgpath = "$dir/$img"; my $fmt = $ctrl->{scaled_format}; my $tmpscaledname = "$dir/tn/tmp.$img.$fmt"; my $tmp2scaledname = "$dir/tn/tmp2.$img.$fmt"; my $args = $ctrl->{scaled_convert_args}; if ($ctrl->{'scaled_convert_args_'.$fmt}) { $args .= ' '.$ctrl->{'scaled_convert_args_'.$fmt}; } $args =~ s/__WIDTH__/$maxw/g; $args =~ s/__HEIGHT__/$maxh/g; my @cmd = split (' ', $args); push (@cmd, $imgpath, $tmpscaledname); print "scaled: ".join (' ', @cmd)."\n"; system (@cmd); if ($? >> 8 != 0) { warn "$scaledname: failed to generate due to 'convert' failure\n"; return 0; } # jpegtran -progressive input > output ; mv output input # jhead -cl "comment" -dt -te orig input # - (jpegtran: used to make jpeg images progressive) # - (jhead: used to copy over EXIF metadata) if ($fmt eq 'jpg' && $ctrl->{jpegtran}) { @cmd = ($ctrl->{jpegtran}, "-progressive", "-outfile", $tmp2scaledname, $tmpscaledname); print "jpegtran: ".join (' ', @cmd)."\n"; system (@cmd); if ($? >> 8 != 0) { warn "$scaledname: failed to generate due to '$ctrl->{jpegtran}' failure\n"; return 0; } rename ($tmp2scaledname, $tmpscaledname) or die "rename failed"; } if ($fmt eq 'jpg' && $ctrl->{jhead}) { @cmd = ($ctrl->{jhead}, "-dt", "-te", $imgpath, $tmpscaledname); print "jhead: ".join (' ', @cmd)."\n"; system (@cmd); if ($? >> 8 != 0) { warn "$scaledname: failed to generate due to '$ctrl->{jhead}' failure\n"; return 0; } } if (!rename ($tmpscaledname, $scaledname)) { warn "$scaledname: rename from $tmpscaledname failed: $!\n"; return 0; } return 1; } sub urlencode { my $str = shift; $str =~ s{([\x00-\x2b\x2f\x3a-\x40\x5b-\x60\x7b-\xff])}{ sprintf "%%%02x", ord $1; }ge; $str; } sub get_textname_from_image { my $obj = shift; if ($obj->{description}) { return $obj->{description}; } else { return $obj->{textname}; } } sub myimgsize { my ($file) = @_; if ($cached_imgsizes{$file}) { return @{$cached_imgsizes{$file}}; } else { my @sizes = imgsize($file); $cached_imgsizes{$file} = \@sizes; return @sizes; } } sub fill_tmpl { my ($tmpl, $opts) = @_; my $out = $tmpl; 1 while $out =~ s!__TEMPLATE:([_a-zA-Z0-9]+)__! $template{$1} !xgs; # conditionals if ($out =~ /<__COND_IF_/) { $out =~ s{<__COND_IF_\(([_a-zA-Z0-9]+)\)__> (.*?) <__COND_ELSE__> (.*?) <\/__COND_IF_\(\1\)__>}{ tmpl_cond_if ($opts->{$1}, $2, $3); }xges; $out =~ s{<__COND_IF_\(([_a-zA-Z0-9]+)\)__> (.*?) <\/__COND_IF_\(\1\)__>}{ tmpl_cond_if ($opts->{$1}, $2); }xges; } foreach my $k (keys %$opts) { $out =~ s/__${k}__/ if (defined $opts->{$k}) { $opts->{$k}; } else { warn "undefined template variable: $k\n"; ''; } /ges; } return $out; } sub tmpl_cond_if { my ($cond, $textiftrue, $textiffalse) = @_; if ($cond) { return $textiftrue; } else { return $textiffalse ? $textiffalse : ''; } } __DATA__ __TEMPLATE:htmlhead__ __PAGETITLE__
__CONTENT_NAV__

__CONTENTS_TRS__

__CONTENT_NAV__
__CONTENTS_TDS__ __CONTENTS_THUMB_ALT__
__CONTENTS_IMG_NAME__
<__COND_IF_(ISTOPLEVEL)__> __NUM_IMAGES__ pictures <__COND_ELSE__> __IMG_SIZE_KB__Kb

__CONTENTS_IMG_DESCRIPTION__
__GEN_DATE__ --> __TEMPLATE:htmlhead__ __PAGETITLE__
__IMAGE_PAGE_NAV__

__IMAGE_FILE_ALT__

click on image to view the full-size version (__IMAGE_FILE_WIDTH__ x __IMAGE_FILE_HEIGHT__)

__FAST_NAV_TABLE__
__FAST_NAV_TRS__
__FAST_NAV_TDS__ __NAME__ : __NAME__
__NAME__
__NAME__ : __NAME__
__NAME__
body { background-color: #fff; color: #000; font-family: verdana,lucida,helvetica,sans-serif; font-size: 12px; line-height: 1.5em; margin-left: 8px; margin-right: 8px; } a:link { font-weight: bold; color: #004000; text-decoration: underline; } a:visited { font-weight: bold; color: #008000; text-decoration: underline; } a:active { font-weight: bold; color: #800000; text-decoration: underline; } div.img_block { text-align: center; padding: 10px 10px 10px 10px; margin: 0px 0px 0px 0px; } img.img_block { font-size: 110%; font-style: italic; color: #888; border: thin solid #333; } div.img_nav_href { padding: 5px; white-space: nowrap; } a.img_nav_href { text-decoration: none; font-size: 120%; border-color: #aaa; border-style: solid; border-width: 1px; padding: 3px 30px 5px 30px; } div.img_page_nav { font-size: 140%; } div.contents_nav { font-size: 140%; } div.img_page_back_link { font-size: 150%; } div.contents_back_link { font-size: 150%; } table.img_nav_top { width: 100%; } table.img_nav_bot { width: 100%; } td.img_nav_top_td { padding: 10px; } td.img_nav_bot_td { padding: 10px; } table.img_nav_table { width: 100%; padding: 10px; border-color: #aaa; border-style: solid; border-width: 1px; } table.contents_nav_table { width: 100%; padding: 10px; border-color: #aaa; border-style: solid; border-width: 1px; } div.contents_footer_text { font-size: 50%; font-style: italic; text-align: right; } div.img_footer_text { font-size: 50%; font-style: italic; text-align: right; } table.contents_table { width: 95%; } tr.contents_tr { } td.contents_td { padding: 10px; } div.contents_img_name { font-size: 90%; font-weight: bold; } div.contents_img_size { font-size: 50%; font-style: italic; color: #999; } div.contents_img_desc { font-size: 80%; } table.fast_nav_table { width: 100%; padding: 8px; border-color: #aaa; border-style: solid; border-width: 1px; } tr.fast_nav_tr { } td.fast_nav_td_off { padding: 10px; } td.fast_nav_td_on { padding: 0px; } hr.img_top_hr { border: 0; width: 10%; height: 1px; } hr.img_bot_hr { border: 0; width: 10%; height: 1px; } hr.contents_top_hr { border: 0; width: 10%; height: 1px; } hr.contents_bot_hr { border: 0; width: 10%; height: 1px; } span.img_navtrail_root { font-size: 50%; } a.img_navtrail_root { color: #999; } span.img_navtrail_trunk { font-size: 50%; color: #999; } a.img_navtrail_trunk { color: #999; } span.img_navtrail_leaf { font-size: 100%; font-weight: bold; } span.contents_navtrail_root { font-size: 50%; } a.contents_navtrail_root { color: #999; } span.contents_navtrail_trunk { font-size: 50%; color: #999; } a.contents_navtrail_trunk { color: #999; } span.contents_navtrail_leaf { font-size: 100%; font-weight: bold; } a.thumb_back_href { color: #999; } a.contents_back_href { color: #999; } a.img_original_href { color: #999; font-weight: normal; } p.img_original_p { text-align: right; }