#!/usr/bin/perl -w use strict; use XML::Twig; use Image::Magick; use Text::Template; use File::Copy; use File::Path; use Data::Dumper; use Getopt::Std; use DB_File; getopts('hIvdtTbng:f:'); our ($opt_h, $opt_I, $opt_d, $opt_t, $opt_b, $opt_n, $opt_g, $opt_f, $opt_T); &usage if ($opt_h); my $debug = $opt_d; my $force_thumbs = $opt_t; my $thumbs_only = $opt_T; my $rot_backup = $opt_b; my $no_index = $opt_n; my $index_only = $opt_I; my $gallery_file = $opt_g; my $config_file = $opt_f || 'config/xhconf.xml'; die("Can't find configuration file $config_file - check path or use -f\n") unless (-f $config_file); die("Index_only and No_index are mutually exclusive. Pick one!\n") if ($no_index and $index_only); # Read in configuration files and set up some stuff my %config = read_config($config_file); my $base_url = $config{'base_url'}; $base_url =~ s/\/$//; my $root_dir = $config{'root_dir'}; $root_dir =~ s/\/$//; my $conf_dir = "$root_dir/config"; my $cache_file = "$conf_dir/.cache"; my $index_template = "$conf_dir/$config{'index_template'}"; my $gallery_template = "$conf_dir/$config{'gallery_template'}"; my $photo_template = "$conf_dir/$config{'photo_template'}"; for ($index_template, $gallery_template, $photo_template) { die("Can't find template $_!") unless (-f $_); } chdir($root_dir); my $gallery_width = $config{'gallery_width'}; my $thumbnail_size = $config{'thumbnail_size'}; my $thumbs_on_page = $config{'thumbs_on_page'} || 16; my $galleries_on_index = $config{'galleries_on_index'} || 8; my $extension = $config{'page_extension'} || '.html'; my $max_photo_width = $config{'max_image_width'} || 600; my $thumbnail_prefix = $config{'thumbnail_prefix'} ||'_tn_'; my $backup_suffix = '.BAK'; my $image_ext = '(gif|jpg|jpeg)'; my @gallery_files; if ($gallery_file) { die("Can't find $gallery_file!") unless (-f $gallery_file); } @gallery_files = @{$config{'galleries'}}; my %CACHE; dbmopen(%CACHE,$cache_file,0700) || die "can't open $cache_file: $!"; # Start stepping through the galleries my @gallery_info; for my $file (@gallery_files) { my $rewrite_flag; my $full_file = "$conf_dir/$file"; debug("Processing gallery: $full_file..."); if (!-f $full_file) { warn "Can't find gallery file: $full_file!\n"; next; } # Read and parse gallery XML file my $content; open(GXML, $full_file) || die "can't open $full_file: $!"; { local $/ = undef; $content = ; } close(GXML); my $t = XML::Twig->new(PrettyPrint => 'indented'); $t->parse($content); my $r = $t->root; my %gallery_page_info; $gallery_page_info{'name'} = $r->att('name'); $gallery_page_info{'name'} =~ s/\s/_/g; $gallery_page_info{'title'} = $r->first_child('gallery_title')->text; $gallery_page_info{'text'} = $r->first_child('gallery_text')->text; $gallery_page_info{'teaser'} = $r->first_child('gallery_teaser')->text; $gallery_page_info{'date'} = $r->first_child('gallery_date')->text; my $display_gallery = $r->att('display'); if ((defined $display_gallery) && ($display_gallery eq "false") && (! $force_thumbs) ) { debug(" display set to FALSE - skipping"); next; ### TBD: remove or de-permission directory? } push @gallery_info, \%gallery_page_info; # Step through and process images if ($gallery_file) { next unless ($full_file =~ /$gallery_file$/); } my @image_info; for my $image_twig ($t->root->children('image')) { # Skip images that are marked as hidden next if ($image_twig->first_child('hide')); my $image_src = $image_twig->att('src'); my $image_date = $image_twig->first_child('date')->text;; my $image_title = $image_twig->first_child('title')->text; my $image_caption = $image_twig->first_child('caption')->text; die("No source specified for image $image_twig in $full_file!") unless ($image_src); die("Can't find image $image_src from $full_file!") unless (-f $image_src); debug(" processing image $image_src"); # Rotate images as marked while (my $spinner = $image_twig->first_child('rotation')) { rotate_image($image_src, $spinner->text); # delete rotate flags when done, set flag to rewrite file $spinner->delete; $rewrite_flag = 1; # changed tree - write back to file } # Generate thumbnail if needed my $thumb_file = thumb_name($image_src); if ((! -e $thumb_file) || $force_thumbs || $rewrite_flag ) { debug(" generating thumbnail $thumb_file"); make_thumb($image_src, $thumb_file, $thumbnail_size); } # Add info to image info array my ($img_height, $img_width, $t_height, $t_width) = get_image_sizes($image_src, $thumbnail_size); my $info = { src => $image_src, date => $image_date, title => $image_title, caption => $image_caption, height => $img_height, width => $img_width, t_height => $t_height, t_width => $t_width, }; push @image_info, $info; } next if $index_only; # Generate gallery index page and photo pages $gallery_page_info{'thumbs'} = \@image_info; make_gallery_index(%gallery_page_info); make_gallery($gallery_page_info{'name'}, @image_info); # Finish and clean up if ($rewrite_flag) { debug(" Tree changed - writing back to file"); open(OUT,">$full_file") || die "can't open $full_file for writing: $!"; print OUT $t->sprint; close(OUT); } } make_xhibition_index(@gallery_info) unless $no_index; dbmclose(%CACHE); debug("done."); ##### sub make_xhibition_index { return if ($thumbs_only); my @info = @_; return unless @info; my @nav_pages; my $maxpage = int((@info-1)/$galleries_on_index); for my $navnum (0..$maxpage) { my $index_name = "index"; $index_name .= ($navnum+1) if ($navnum); push @nav_pages, "$index_name.$extension"; } for my $pagenum (0..$maxpage) { my $maxgal = ($pagenum * $galleries_on_index + $galleries_on_index - 1); $maxgal = $#info if ($maxgal > $#info); my @info_slice = @info[($pagenum * $galleries_on_index)..$maxgal]; my @gallery_info; for my $gallery (@info_slice) { my %info; my ($height, $width, $t_height, $t_width) = get_image_sizes($gallery->{'teaser'}, $thumbnail_size); # I know, some of this is redundant, but I prefer it this way. $info{'title'} = $gallery->{'title'}; $info{'text'} = $gallery->{'text'}; $info{'date'} = $gallery->{'date'}; $info{'url'} = "$base_url/$gallery->{'name'}"; $info{'teaser'} = thumb_name("$base_url/$gallery->{'teaser'}"); $info{'t_height'} = $t_height; $info{'t_width'} = $t_width; push @gallery_info, \%info; } # Fill template and write index page my $template = Text::Template->new(TYPE => 'FILE', SOURCE => $index_template, UNTAINT => 1 ); my $temp_hash = { base_url => $base_url, galleries => \@gallery_info, nav => \@nav_pages, index_num => $pagenum, }; my $content = $template->fill_in(HASH => $temp_hash); my $index_page = "$root_dir/$nav_pages[$pagenum]"; debug(" Writing xhibition index page to: $index_page"); open(INDEX,">$index_page") || die "can't write to $index_page: $!"; print INDEX $content; close(INDEX); } } sub make_gallery_index { # Takes a hash of config information and generates an index page return if ($thumbs_only); return unless @_; my %info = @_; my $gallery_name = $info{'name'}; my $gallery_title = $info{'title'}; my $gallery_text = $info{'text'}; my $gallery_date = $info{'date'}; my $thumbs_info = $info{'thumbs'}; my @nav_pages; my $maxpage = int((@$thumbs_info-1)/$thumbs_on_page); for my $navnum (0..$maxpage) { my $index_name = "index"; $index_name .= ($navnum+1) if ($navnum); push @nav_pages, "$index_name.$extension"; } for my $pagenum (0..$maxpage) { my $maxthumb = ($pagenum * $thumbs_on_page + $thumbs_on_page - 1); $maxthumb = $#$thumbs_info if ($maxthumb > $#$thumbs_info); my @info_slice = @$thumbs_info[($pagenum * $thumbs_on_page)..$maxthumb]; my @thumbnails; for my $image (@info_slice) { my %thumb; my $image_name = $image->{'src'}; $image_name =~ s/^.*\/([^\/]+)$/$1/; # Again, some redundancy. Deal with it. $thumb{'src'} = "$base_url/".thumb_name($image->{'src'}); $thumb{'url'} = "$base_url/$gallery_name/$image_name.$extension"; $thumb{'height'} = $image->{'t_height'}; $thumb{'width'} = $image->{'t_width'}; $thumb{'title'} = $image->{'title'}; $thumb{'date'} = $image->{'date'}; push @thumbnails,\%thumb; } # Fill template and write gallery index file my $template = Text::Template->new(TYPE => 'FILE', SOURCE => $gallery_template, UNTAINT => 1 ); my $temp_hash = { base_url => $base_url, gallery_title => $gallery_title, gallery_text => $gallery_text, gallery_width => $gallery_width, gallery_date => $gallery_date, gallery_name => $gallery_name, thumbnails => \@thumbnails, nav => \@nav_pages, index_num => $pagenum, }; my $content = $template->fill_in(HASH => $temp_hash); my $gallery_index = "$root_dir/$gallery_name/$nav_pages[$pagenum]"; debug(" Writing gallery index to: $gallery_index"); (my $gallery_path = $gallery_index) =~ s/^(.*)\/[^\/]*$/$1/; mkpath($gallery_path); open(INDEX,">$gallery_index") || die "can't write to $gallery_index: $!"; print INDEX $content; close(INDEX); } } sub make_gallery { # Takes a gallery name and an array of photo information and # generates photo pages for each image return if ($thumbs_only); my $gallery_name = shift || return; my $gallery_url = "$base_url/$gallery_name"; my @info = @_; for (my $i=0; $i<@info; $i++) { my $photo = $info[$i]; my $first_href = $info[0]->{'src'}; $first_href =~ s/^.*\/([^\/]+)$/$1/; $first_href = qq(href="$gallery_url/$first_href.$extension"); my $last_href = $info[-1]->{'src'}; $last_href =~ s/^.*\/([^\/]+)$/$1/; $last_href = qq(href="$gallery_url/$last_href.$extension"); my $previous_href = ''; if ($i > 0) { $previous_href = $info[$i-1]->{'src'}; $previous_href =~ s/^.*\/([^\/]+)$/$1/; $previous_href = qq(href="$gallery_url/$previous_href.$extension"); } my $next_href = ''; if ($i < (@info - 1)) { $next_href = $info[$i+1]->{'src'}; $next_href =~ s/^.*\/([^\/]+)$/$1/; $next_href = qq(href="$gallery_url/$next_href.$extension"); } my $gallery_href = qq(href="$gallery_url/"); # Calculate display height and width my $photo_height = $photo->{'height'}; my $photo_width = $photo->{'width'}; if ($photo_width > $max_photo_width) { my $ratio = $max_photo_width/$photo_width; $photo_width = int($photo_width * $ratio); $photo_height = int($photo_height * $ratio); } # Fill template and write gallery index file my $template = Text::Template->new(TYPE => 'FILE', SOURCE => $photo_template, UNTAINT => 1 ); my $temp_hash = { image_title => $photo->{'title'}, image_src => "$base_url/$photo->{'src'}", image_height => $photo_height, image_width => $photo_width, image_caption => $photo->{'caption'}, first_href => $first_href, previous_href => $previous_href, gallery_href => $gallery_href, next_href => $next_href, last_href => $last_href, base_url => $base_url, }; my $content = $template->fill_in(HASH => $temp_hash); my $image_name = $photo->{'src'}; $image_name =~ s/^.*\/([^\/]+)$/$1/; my $photo_page = "$root_dir/$gallery_name/$image_name.$extension"; debug(" Writing photo page to: $photo_page"); (my $photo_path = $photo_page) =~ s/^(.*)\/[^\/]*$/$1/; mkpath($photo_path); open(INDEX,">$photo_page") || die "can't write to $photo_page: $!"; print INDEX $content; close(INDEX); } } ## Image manipulation subroutines sub rotate_image { # Takes an filename and degrees, and rotates image and writes to disk my $suffix = $backup_suffix; my $filename = shift || return; my $degrees = shift || return; my $err; if ($filename !~ /\.$image_ext$/i) { warn "$filename not an image in thumb_name()"; return; } if ($degrees % 90) { warn "must rotate multiple of 90, not $degrees, in rotate_image()"; return; } debug(" rotating $filename $degrees degrees"); my $image = Image::Magick->new; $err = $image->Read($filename); warn("***Can't read $filename in rotate_image(): $err") if $err; $image->Rotate( degrees => $degrees); copy($filename,"$filename$suffix") if $rot_backup; $err = $image->Write(filename => $filename); warn("***Can't write to $filename in rotate_image(): $err") if $err; } sub thumb_name { # Takes a filename and returns the name of that image's thumbnail my $file = shift || return; if ($file !~ /\.$image_ext$/i) { warn "$file not an image in thumb_name()"; return; } if ($file =~ /\//) { $file =~ s/^(.*\/)([^\/]+)$/$1$thumbnail_prefix$2/; } else { $file = "$thumbnail_prefix$file"; } return $file; } sub make_thumb { # adapted from Bernie Porter's photo_album.cgi at # http://www.tildebernie.com/~bernie/cgi/photo_album.txt my $file = shift || return; my $thumb = shift || return; my $size = shift || return; my $err; my ($height, $width, $new_height, $new_width) = get_image_sizes($file, $size); # scale the image using new dimensions and write my $image = Image::Magick->new; $err = $image->Read($file); warn "**Read error: $err on $image" if $err; $image->Scale( width => $new_width, height => $new_height); $err = $image->Write(filename=>$thumb); warn "**Write error: $err on $thumb" if $err; } sub get_image_sizes { # Takes an image name and a thumbnail size as arguments, and returns a # list with image height and width and thumbnail height and width my $src = shift || return; my $size = shift || return; if ($src !~ /\.$image_ext$/i) { warn "$src not an image in get_image_sizes()"; return; } my $width; my $height; my $new_width; my $new_height; my $src_timestamp = (stat $src)[9]; my ($cache_timestamp, @cache_sizes); if (defined $CACHE{$src}) { ($cache_timestamp, @cache_sizes) = split ":::",$CACHE{$src}; } if ((defined $CACHE{$src}) && ($cache_timestamp >= $src_timestamp)) { ($height, $width, $new_height, $new_width) = @cache_sizes; } else { # Calculate new sizes, cache results my $err; my $image = Image::Magick->new; $err = $image->Read($src); warn "**Read error: $err on $image" if $err; $width = $image->Get('width'); $height = $image->Get('height'); # figure out the new dimensions my $ratio = $height/$width; if ($width < $height) { $new_height = $size; $new_width = int($size/$ratio); } elsif ($height < $width) { $new_height = int($size*$ratio); $new_width = $size; } else { $new_height = $size; $new_width = $size; } $CACHE{$src} = join ":::",($src_timestamp, $height, $width, $new_height, $new_width); } return ($height, $width, $new_height, $new_width); } ### sub read_config { # Reads an XML configuration file, and returns a hash of values my $file = shift || return; my %config; my $content; open(CONF,$file) || die "can't open config file $file: $!"; { local $/ = undef; $content = ; } close(CONF); my $t = XML::Twig->new(PrettyPrint => 'indented'); $t->parse($content); my $r = $t->root; $config{'base_url'} = $r->first_child('base_url')->text; $config{'root_dir'} = $r->first_child('root_directory')->text; $config{'thumbnail_size'} = $r->first_child('thumbnail_size')->text; $config{'gallery_width'} = $r->first_child('gallery_width')->text; $config{'index_template'} = $r->first_child('index_template')->text; $config{'gallery_template'} = $r->first_child('gallery_template')->text; $config{'photo_template'} = $r->first_child('photo_template')->text; $config{'page_extension'} = $r->first_child('page_extension')->text; $config{'max_image_width'} = $r->first_child('max_image_width')->text; $config{'thumbnail_prefix'} = $r->first_child('thumbnail_prefix')->text; $config{'thumbs_on_page'} = $r->first_child('thumbs_on_page')->text; $config{'galleries_on_index'} = $r->first_child('galleries_on_index')->text; my @galleries; for my $gtwig ($r->first_child('galleries')->children('gallery_file')) { push @galleries, $gtwig->text; } $config{'galleries'} = \@galleries; return %config; } sub debug { return unless $debug; my $string = shift; warn("$string\n"); } sub usage { print <