package Apache::App::Gallery::Simple; use strict; ## This is a simple mod_perl gallery application inspired by Apache::Album ## by James D. Woodgate; additional inspiration courtesy _Writing Apache ## Modules with Perl and C_ by Lincoln Stein and Doug MacEachern use Apache::Constants qw(DECLINED OK SERVER_ERROR REDIRECT); use Image::Magick; use Template::Trivial; use File::Spec; use File::Path qw(rmtree); use vars qw($VERSION); $VERSION = '1.09'; use vars qw($DEBUG); $DEBUG = 0; use vars qw(%CONFIG); %CONFIG = (); use vars qw(@LANG); @LANG = (); use constant CAP_LINK => 0; use constant CAP_TEXT => 1; use constant CLINK_F => 0; ## link file use constant CLINK_W => 1; ## link height use constant CLINK_H => 2; ## link width sub handler { my $r = shift; %CONFIG = set_defaults($r); ## make sure gallery root exists unless( $CONFIG{'gallery_root'} ) { $r->log_error("No GalleryRoot configured"); return SERVER_ERROR; } $CONFIG{'gallery_path'} = path($r->document_root, $CONFIG{'gallery_root'}); my $loc = $r->location; my $uri = $r->uri; $uri =~ s/^$loc/$CONFIG{'gallery_root'}/; my $subr = $r->lookup_file($uri); ## final URI my %lang = (); @LANG = grep { ! $lang{$_}++ } map { $_ eq $CONFIG{'gallery_lang'} ? '' : $_ } grep { $_ } map { $_, ($CONFIG{'language_strict'} ? () : /^([^-]+)/) } map { /^([^;]+)/ } map { lc($_) } split(',', ( $r->header_in('Accept-Language') ? $r->header_in('Accept-Language') : $CONFIG{'gallery_lang'} )); $DEBUG=0; $r->log_error("Accept-Language: @LANG") if $DEBUG; $DEBUG=0; if( $DEBUG ) { $r->log_error("r->location: " . $r->location()); $r->log_error("r->filename: " . $r->filename()); $r->log_error("r->uri: " . $r->uri()); $r->log_error("r->path_info: " . $r->path_info()); $r->log_error("\$uri: " . $uri); $r->log_error("r->lookup_uri(uri): " . $subr->filename); } if( -d $subr->finfo() ) { $r->log_error( "found a directory at $uri" ) if $DEBUG; ## show this directory return show_gallery($r, $subr); } elsif( -f _ ) { $r->log_error( "found a regular file at $uri" ) if $DEBUG; if( $subr->content_type =~ m!^image/! ) { return show_image($r, $subr); } elsif( $subr->content_type =~ m!^video/quicktime! ) { return show_mov($r, $subr); } elsif( $subr->content_type =~ m!^video/mp(?:e|g|eg)! ) { return show_mpeg($r, $subr); } elsif( $subr->content_type =~ m!^video/x-msvideo! ) { return show_avi($r, $subr); } ## pass everything else through else { $r->log_error("Pass through content ($uri)") if $DEBUG; return $subr->run; ## FIXME: Apache book says we could do ## internal redirect here for efficiency } } ## not a file or directory else { $r->log_error("File or directory not found: " . $r->uri); return DECLINED; } } sub set_defaults { my $r = shift; my %config = (); ## set defaults $config{'gallery_root'} = $r->dir_config('GalleryRoot') || ''; $config{'gallery_name'} = $r->dir_config('GalleryName') || ''; $config{'gallery_lang'} = $r->dir_config('GalleryLang') || 'en'; $config{'language_strict'} = $r->dir_config('LanguageStrict') || 'no'; $config{'other_gallery_links'} = $r->dir_config('OtherGalleryLinks') || 'yes'; $config{'always_link'} = $r->dir_config('AlwaysLink') || 'no'; $config{'thumb_use'} = $r->dir_config('ThumbUse') || 'width'; $config{'thumb_width'} = $r->dir_config('ThumbWidth') || 100; $config{'thumb_height'} = $r->dir_config('ThumbHeight') || 100; $config{'thumb_aspect'} = $r->dir_config('ThumbAspect') || '1/5'; $config{'thumb_dir'} = $r->dir_config('ThumbDir') || '.thumbs'; $config{'thumb_prefix'} = $r->dir_config('ThumbPrefix') || 'tn__'; $config{'thumb_columns'} = $r->dir_config('ThumbColumns') || 0; $config{'browser_width'} = $r->dir_config('BrowserWidth') || 640; $config{'breadcrumb_home'} = $r->dir_config('BreadcrumbHome') || 'no'; $config{'caption_file'} = $r->dir_config('CaptionFile') || 'caption.txt'; $config{'template_dir'} = $r->dir_config('TemplateDir') || '.templates'; # $config{'thumb_caption'} = # $r->dir_config('ThumbCaption') || # 'no'; ## cleanup configuration directives $config{'language_strict'} = $config{'language_strict'} =~ /^(?:yes|on|true|1)$/i; $config{'other_gallery_links'} = $config{'other_gallery_links'} =~ /^(?:yes|on|true|1)$/i; $config{'always_link'} = $config{'always_link'} =~ /^(?:yes|on|true|1)$/i; $config{'thumb_use'} = lc($config{'thumb_use'}); unless( $config{'thumb_aspect'} =~ m!^[\d/\.]+$! ) { $r->log_error("Illegal character in ThumbAspect: only digits, slashes, and decimal points allowed."); return SERVER_ERROR; } $config{'thumb_dir'} =~ s!/+$!!; if( $config{'thumb_dir'} =~ m!/! ) { $r->log_error("No paths allowed in 'ThumbDir': must be a directory name relative to the current gallery."); return SERVER_ERROR; } # $config{'thumb_caption'} = lc($config{'thumb_caption'}); if( $config{'thumb_prefix'} =~ m!/! ) { $r->log_error("No paths allowed in ThumbPrefix: must be a string with no path separators."); return SERVER_ERROR; } if( $config{'caption_file'} =~ m!/! ) { $r->log_error("No paths allowed in 'CaptionFile': must be a file name relative to the current gallery."); return SERVER_ERROR; } $config{'template_dir'} =~ s!/+$!!; if( $config{'template_dir'} =~ m!/! ) { $r->log_error("No paths allowed in 'TemplateDir': must be a directory name relative to the GalleryRoot"); return SERVER_ERROR; } return %config; } sub show_gallery { my $r = shift; my $subr = shift; my ($path, $dir) = $subr->filename =~ m!^(.*)/([^/]+)$!; ## need to redirect? unless( $r->uri =~ m!/$! ) { $r->warn( "redirecting to " . $r->uri . '/' ); $r->header_out( Location => $r->uri . '/' ); return REDIRECT; } $r->content_type('text/html'); $r->send_http_header; ## setup templates my $tmpl = new Template::Trivial; $tmpl->define_from_string(main => <<_EOF_, {GALLERY_NAME} {GALLERY_STYLE} {GALLERY_TITLE} {BREADCRUMBS} {OTHER_GALLERIES}
{DIR_FIRST}{DIR_PREV} {DIR_NEXT}{DIR_LAST}
{GALLERY}
{GALLERY_FOOTER} _EOF_ gallery_title => q!!, ## was '

{GALLERY_NAME}

' gallery_title_empty => '', gallery_style => <<_EOF_, _EOF_ gallery_footer => <<_EOF_,
Apache::App::Gallery::Simple {VERSION}
_EOF_ other_galleries => <<_EOF_,

Other galleries within this gallery:
{DIRECTORIES}

_EOF_ other_empty => <<_EOF_,

(No other galleries within this gallery)

_EOF_ breadcrumbs => q!!, breadcrumb => q! {BREADCRUMB} ->!, deadcrumb => q! {BREADCRUMB}!, homecrumb => q!home!, first => q!{FIRST_DEFAULT}!, first_empty => '', first_default => q!First
Gallery!, first_caption => q!
({CAPTION})!, previous => q!{PREV_DEFAULT}{PREV}!, previous_empty => '', previous_default => q!Previous
Gallery!, previous_caption => q!
({CAPTION})!, next => q!{NEXT_DEFAULT}{NEXT}!, next_empty => '', next_default => q!Next
Gallery!, next_caption => q!
({CAPTION})!, last => q!{LAST_DEFAULT}!, last_empty => '', last_default => q!Last
Gallery!, last_caption => q!
({CAPTION})!, dir_link => qq!{DIRECTORY}
\n!, dir_caption => q! ({CAPTION})!, ## not used currently gallery_table => q!{ROWS}
!, gallery_empty => q!
(No photos in this gallery)
!, table_row_top => q!!, table_row_middle => q!{ROW_END}{ROW_START}!, table_row_bottom => q!!, table_cell => <<_EOF_, _EOF_ ); TEMPLATE_DIR: for my $tmpl_dir ( grep { -d } map { path($CONFIG{'gallery_path'}, ( $_ ? $CONFIG{'template_dir'} . ".$_" : $CONFIG{'template_dir'} ) ) } reverse @LANG ) { warn "Reading gallery templates from '$tmpl_dir'\n" if $DEBUG; ## process template customizations, if any $tmpl->templates($tmpl_dir); $tmpl->define(main => "gallery_main.txt") if -f path($tmpl_dir, "gallery_main.txt"); $tmpl->define(gallery_title => "gallery_title.txt") if -f path($tmpl_dir, "gallery_title.txt"); $tmpl->define(gallery_title_empty => "gallery_title_empty.txt") if -f path($tmpl_dir, "gallery_title_empty.txt"); $tmpl->define(gallery_style => "gallery_style.txt") if -f path($tmpl_dir, "gallery_style.txt"); $tmpl->define(gallery_footer => "gallery_footer.txt") if -f path($tmpl_dir, "gallery_footer.txt"); $tmpl->define(other_galleries => "gallery_other.txt") if -f path($tmpl_dir, "gallery_other.txt"); $tmpl->define(other_empty => "gallery_other_empty.txt") if -f path($tmpl_dir, "gallery_other_empty.txt"); $tmpl->define(breadcrumbs => "gallery_breadcrumbs.txt") if -f path($tmpl_dir, "gallery_breadcrumbs.txt"); $tmpl->define(breadcrumb => "gallery_breadcrumb.txt") if -f path($tmpl_dir, "gallery_breadcrumb.txt"); $tmpl->define(deadcrumb => "gallery_deadcrumb.txt") if -f path($tmpl_dir, "gallery_deadcrumb.txt"); $tmpl->define(homecrumb => "gallery_homecrumb.txt") if -f path($tmpl_dir, "gallery_homecrumb.txt"); ## navigation links $tmpl->define(first => "gallery_first.txt") if -f path($tmpl_dir, "gallery_first.txt"); $tmpl->define(first_empty => "gallery_first_empty.txt") if -f path($tmpl_dir, "gallery_first_empty.txt"); $tmpl->define(first_default => "gallery_first_default.txt") if -f path($tmpl_dir, "gallery_first_default.txt"); $tmpl->define(first_caption => "gallery_first_caption.txt") if -f path($tmpl_dir, "gallery_first_caption.txt"); $tmpl->define(previous => "gallery_previous.txt") if -f path($tmpl_dir, "gallery_previous.txt"); $tmpl->define(previous_empty => "gallery_previous_empty.txt") if -f path($tmpl_dir, "gallery_previous_empty.txt"); $tmpl->define(previous_default => "gallery_previous_default.txt") if -f path($tmpl_dir, "gallery_previous_default.txt"); $tmpl->define(previous_caption => "gallery_previous_caption.txt") if -f path($tmpl_dir, "gallery_previous_caption.txt"); $tmpl->define(next => "gallery_next.txt") if -f path($tmpl_dir, "gallery_next.txt"); $tmpl->define(next_empty => "gallery_next_empty.txt") if -f path($tmpl_dir, "gallery_next_empty.txt"); $tmpl->define(next_default => "gallery_next_default.txt") if -f path($tmpl_dir, "gallery_next_default.txt"); $tmpl->define(next_caption => "gallery_next_caption.txt") if -f path($tmpl_dir, "gallery_next_caption.txt"); $tmpl->define(last => "gallery_last.txt") if -f path($tmpl_dir, "gallery_last.txt"); $tmpl->define(last_empty => "gallery_last_empty.txt") if -f path($tmpl_dir, "gallery_last_empty.txt"); $tmpl->define(last_default => "gallery_last_default.txt") if -f path($tmpl_dir, "gallery_last_default.txt"); $tmpl->define(last_caption => "gallery_last_caption.txt") if -f path($tmpl_dir, "gallery_last_caption.txt"); $tmpl->define(dir_link => "gallery_dir_link.txt") if -f path($tmpl_dir, "gallery_dir_link.txt"); $tmpl->define(dir_caption => "gallery_dir_caption.txt") if -f path($tmpl_dir, "gallery_dir_caption.txt"); $tmpl->define(gallery_table => "gallery_table") if -f path($tmpl_dir, "gallery_table"); $tmpl->define(gallery_empty => "gallery_empty") if -f path($tmpl_dir, "gallery_empty"); $tmpl->define(table_row_top => "gallery_row_top") if -f path($tmpl_dir, "gallery_row_top.txt"); $tmpl->define(table_row_middle => "gallery_row_middle") if -f path($tmpl_dir, "gallery_row_middle.txt"); $tmpl->define(table_row_bottom => "gallery_row_bottom") if -f path($tmpl_dir, "gallery_row_bottom.txt"); $tmpl->define(table_cell => "gallery_cell") if -f path($tmpl_dir, "gallery_cell.txt"); } my $gallery_name = get_captions($path, $dir); $tmpl->assign( GALLERY_NAME => ($gallery_name ? $gallery_name : $r->uri)); $tmpl->parse( GALLERY_TITLE => ( $gallery_name ? 'gallery_title' : 'gallery_title_empty' )); $tmpl->parse( GALLERY_STYLE => 'gallery_style'); $tmpl->assign( VERSION => $VERSION ); $tmpl->parse( GALLERY_FOOTER => 'gallery_footer'); $tmpl->parse( ROW_START => 'table_row_top'); $tmpl->parse( ROW_END => 'table_row_bottom'); $tmpl->parse( ROWS => 'table_row_top'); DO_BREADCRUMBS: { my $location = $r->location(); my($alias) = $r->uri; $alias =~ s/^$location//; my @breadcrumbs = ($location, grep { $_ } split('/', $alias)); if( $DEBUG ) { warn "##############################################################\n"; warn "URI: " . $r->uri . "\n"; warn "LOCATION: " . $location . "\n"; warn "ALIAS: " . $alias . "\n"; warn "################\n"; } my $lastcrumb = pop @breadcrumbs; ## save for after the loop my $breadcrumblink = ''; $DEBUG=0; for my $crumb ( @breadcrumbs ) { warn "CRUMB: $crumb\n" if $DEBUG; $breadcrumblink .= $crumb . ($crumb =~ m!/$! ? '' : '/'); my ($breadpath) = $breadcrumblink =~ m!^$location(.*)!; warn "BREADPATH: $breadpath\n" if $DEBUG; my $fullpath = path($CONFIG{'gallery_path'}, $breadpath); my ($g_path, $g_dir) = $fullpath =~ m!^(.*/)([^/]+)$!; ## split fullpath into components my $g_name = get_captions($g_path, $g_dir); ## get comment for this directory warn "BREADCRUMB_LINK: $breadcrumblink\n" if $DEBUG; warn "FULLPATH: $fullpath\n" if $DEBUG; if( $crumb eq '/' ) { $tmpl->parse( BREADCRUMB => 'homecrumb') } else { $tmpl->assign( BREADCRUMB => ($g_name ? $g_name : $crumb)) } $tmpl->assign(BREADCRUMB_LINK => $breadcrumblink); $tmpl->parse('.BREADCRUMB_PATH' => 'breadcrumb'); } $DEBUG=0; $tmpl->assign(BREADCRUMB => ( $gallery_name ? $gallery_name : $lastcrumb)); $tmpl->parse('.BREADCRUMB_PATH' => 'deadcrumb'); $tmpl->parse(BREADCRUMBS => 'breadcrumbs'); } my $row_width = 0; my $columns = 0; $r->log_error("Opening path '$path/$dir'") if $DEBUG; unless( opendir DIR, path($path, $dir) ) { $r->log_error("Could not open path '" . path($path, $dir) . "': $!"); return SERVER_ERROR; } ## get captions for subdirectories my %captions = get_captions(path($path, $dir)); my $thumbpath = path($path, $dir, $CONFIG{'thumb_dir'}); SANITARY: { my($tmp) = $thumbpath =~ /^(.*)$/; $thumbpath = $tmp; } my $empty_gallery = 1; my $empty_other = 1; for my $file ( sort { lc($a) cmp lc($b) } grep { ! /^(?:\.|$CONFIG{'thumb_prefix'})/ } readdir DIR ) { my $fullpath = path($path, $dir, $file); if( -d $fullpath ) { $tmpl->assign( DIRECTORY => ( $captions{$file}->[CAP_TEXT] ? $captions{$file}->[CAP_TEXT] : $file ) ); $tmpl->assign( URI_DIR => "$file/" ); $tmpl->assign( CAPTION => ( $captions{$file}->[CAP_TEXT] ? $r->uri . "$file/" : '' ) ); $tmpl->parse( DIR_CAPTION => 'dir_caption'); $tmpl->parse(".DIRECTORIES" => 'dir_link' ); undef $empty_other; } ## a file elsif( -f _ ) { ## only images are good enough do display in the thumbnail gallery ## (however, images may represent other media with the 'link' field ## in the caption.txt file). This algorithm must match image nav next unless $r->lookup_file($fullpath)->content_type =~ m!^image/!; my $fullthumb = path($thumbpath, thumb($file)); SANITARY: { my($tmp) = $fullthumb =~ /^(.*)$/; $fullthumb = $tmp; } warn "FULLPATH: $fullpath\n" if $DEBUG; warn "THUMBPATH: $thumbpath\n" if $DEBUG; warn "FULLTHUMB: $fullthumb\n" if $DEBUG; ## make sure thumbnail exists and is newer than the image unless( -e $fullthumb && (stat(_))[9] > (stat($fullpath))[9] ) { if( -e $thumbpath && ! -d $thumbpath ) { $r->log_error("Warning! '$thumbpath' already exists but is not a directory. Refusing to write to possible dangerous location\n"); return; } mkdir $thumbpath, 0777 unless -d $thumbpath; my $magick = new Image::Magick; unless( $magick ) { $r->log_error("No Image::Magick object: $!"); next; } $magick->Read($fullpath); my($o_width, $o_height) = $magick->Get('width', 'height'); my($new_width, $new_height); unless( $o_width && $o_height ) { $r->log_error("Zero width/height image at '$fullpath': $!\n"); next; } if( $CONFIG{'thumb_use'} eq 'aspect' ) { my $ratio = eval($CONFIG{'thumb_aspect'}) || (1/5); $new_width = $o_width * $ratio; $new_height = $o_height * $ratio; } elsif( $CONFIG{'thumb_use'} eq 'height' ) { my $ratio = ( $o_height ? $o_width / $o_height : 1 ); $new_width = $CONFIG{'thumb_height'} * $ratio; $new_height = $CONFIG{'thumb_height'}; } else { ## width my $ratio = ( $o_height ? $o_width / $o_height : 1 ); $new_width = $CONFIG{'thumb_width'}; $new_height = $CONFIG{'thumb_width'}/$ratio; } ## rescale and write the image to file $magick->Scale( width => $new_width, height => $new_height ); unlink $fullthumb if -e $fullthumb; $magick->Write( $fullthumb ); undef $magick; } ## decide whether to make a new row if( $CONFIG{'thumb_columns'} ? $columns >= $CONFIG{'thumb_columns'} : $row_width >= $CONFIG{'browser_width'} ) { $tmpl->parse('.ROWS' => 'table_row_middle'); $row_width = $columns = 0; } $row_width += $CONFIG{'thumb_width'}; $columns++; undef $empty_gallery; ## check for a link if( ! $CONFIG{'always_link'} && $captions{$file}->[CAP_LINK]->[CLINK_F] ) { my $link_path = path($path, $dir, $captions{$file}->[CAP_LINK]->[CLINK_F]); ## a directory or a recognized media type if( -e $link_path && ( -d $link_path || $r->lookup_file($link_path)->content_type =~ m!^(?:image|video)/! ) ) { $tmpl->assign(URI_IMAGE => $captions{$file}->[CAP_LINK]->[CLINK_F]); } ## link to the image itself else { $tmpl->assign(URI_IMAGE => $file); } } ## otherwise, link to the image itself else { $tmpl->assign(URI_IMAGE => $file); } my $uri = $r->uri; my $loc = $r->location; $uri =~ s/^$loc/$CONFIG{'gallery_root'}/; warn "URI: $uri\n" if $DEBUG; if( my($userdir) = $r->uri =~ m!~([^/]+)! ) { warn "USERDIR: $userdir\n" if $DEBUG; $uri = "~$userdir/$uri"; } $tmpl->assign(URI_THUMB => File::Spec->canonpath("/$uri/$CONFIG{'thumb_dir'}/" . thumb($file))); $tmpl->parse('.ROWS' => 'table_cell'); } ## skip non-files/dirs } closedir DIR; $tmpl->parse('.ROWS' => 'table_row_bottom'); ## the last tr tag $tmpl->parse(GALLERY => ( $empty_gallery ? 'gallery_empty' : 'gallery_table') ); ## won't be needing the thumbnail directory... TIDY: { local $ENV{PATH} = '/bin:/sbin:/usr/bin:/usr/sbin'; ## FIXME: any others? rmtree($thumbpath, 0, 1) if $empty_gallery; } GET_NAVIGATION: { my @dirs = (); my $idx = 0; READ_DIRECTORY: { if( path($path, $dir) eq $CONFIG{'gallery_path'} ) { $r->log_error("Current directory is $CONFIG{'gallery_path'}") if $DEBUG; last READ_DIRECTORY; } opendir DIR, $path or do { $r->log_error( "Could not open '$path': $!" ); last GET_NAVIGATION; }; @dirs = grep { -d path($path, $_) } sort { lc($a) cmp lc($b) } grep { ! /^(?:\.|$CONFIG{'thumb_prefix'})/ } readdir DIR; closedir DIR; $idx++ while $idx < $#dirs && $dirs[$idx] ne $dir; } ## no peer directories? Empty navigation templates unless( scalar(@dirs) ) { $tmpl->parse(DIR_FIRST => 'first_empty'); $tmpl->parse(DIR_PREV => 'previous_empty'); $tmpl->parse(DIR_NEXT => 'next_empty'); $tmpl->parse(DIR_LAST => 'last_empty'); last GET_NAVIGATION; } ## parse navigation templates if( $dirs[0] eq $dir ) { $tmpl->parse(DIR_FIRST => 'first_empty') } else { $tmpl->assign(FIRST_LINK => "../$dirs[0]/"); $tmpl->assign(FIRST => '' ); $tmpl->parse(FIRST_DEFAULT => 'first_default'); if( my $caption = get_captions($path, $dirs[0]) ) { $tmpl->assign(CAPTION => $caption); $tmpl->parse(FIRST => 'first_caption'); } $tmpl->parse(DIR_FIRST => 'first'); } if( ($idx-1) < 0 ) { $tmpl->parse(DIR_PREV => 'previous_empty') } else { $tmpl->assign(PREV_LINK => "../$dirs[$idx-1]/"); $tmpl->assign(PREV => ''); $tmpl->parse(PREV_DEFAULT => 'previous_default'); if( my $caption = get_captions($path, $dirs[$idx-1]) ) { $tmpl->assign(CAPTION => $caption); $tmpl->parse(PREV => 'previous_caption'); } $tmpl->parse(DIR_PREV => 'previous'); } if( ($idx+1) > $#dirs ) { $tmpl->parse(DIR_NEXT => 'next_empty') } else { $tmpl->assign(NEXT_LINK => "../$dirs[$idx+1]/"); $tmpl->assign(NEXT => ''); $tmpl->parse(NEXT_DEFAULT => 'next_default'); if( my $caption = get_captions($path, $dirs[$idx+1]) ) { $tmpl->assign(CAPTION => $caption); $tmpl->parse(NEXT => 'next_caption'); } $tmpl->parse(DIR_NEXT => 'next'); } if( $dirs[$#dirs] eq $dir ) { $tmpl->parse(DIR_LAST => 'last_empty') } else { $tmpl->assign(LAST_LINK => "../$dirs[$#dirs]/"); $tmpl->assign(LAST => ''); $tmpl->parse(LAST_DEFAULT => 'last_default'); if( my $caption = get_captions($path, $dirs[$#dirs]) ) { $tmpl->assign(CAPTION => $caption); $tmpl->parse(LAST => 'last_caption'); } $tmpl->parse(DIR_LAST => 'last'); } } if( $CONFIG{'other_gallery_links'} ) { $tmpl->parse(OTHER_GALLERIES => ( $empty_other ? 'other_empty' : 'other_galleries' )); } else { $tmpl->assign(OTHER_GALLERIES => ''); } $tmpl->parse(MAIN => 'main'); $r->print($tmpl->to_string('MAIN')); return OK; } sub show_image { return show_file(@_, 'image', q!!); } sub show_mov { return show_file(@_, 'mov', <<'_MEDIA_'); _MEDIA_ } sub show_mpeg { return show_file(@_, 'mpeg', <<'_MEDIA_'); _MEDIA_ } sub show_avi { return show_file(@_, 'avi', <<'_MEDIA_'); _MEDIA_ } sub show_file { my $r = shift; my $subr = shift; my ($path, $image) = $subr->filename =~ m!^(.*)/([^/]+)$!; my $mime = shift || 'image'; SANITARY: { my($tmp) = $mime =~ /^(.*)$/; $mime = $tmp; } my $media_tmpl = shift || q!!; $r->content_type('text/html'); $r->send_http_header; return OK if $r->header_only; my $tmpl = new Template::Trivial; $tmpl->define_from_string(main => <<_EOF_, {TITLE} {IMAGE_STYLE}
{IMG_FIRST}{IMG_PREV} {DIR_UP} {IMG_NEXT}{IMG_LAST}
{MEDIA}
{LINK}{COMMENT}
_EOF_ image_style => <<_EOF_, _EOF_ media => $media_tmpl, link => q!More
!, link_empty => '', up => q!{UP_DEFAULT}!, up_default => q!Back to
Gallery!, first => q!{FIRST_DEFAULT}!, first_empty => '', first_default => q!First
Image!, first_caption => q!
({CAPTION})!, previous => q!{PREVIOUS_DEFAULT}!, previous_empty => '', previous_default => q!Previous
Image!, previous_caption => q!
({CAPTION})!, next => q!{NEXT_DEFAULT}!, next_empty => '', next_default => q!Next
Image!, next_caption => q!
({CAPTION})!, last => q!{LAST_DEFAULT}!, last_empty => '', last_default => q!Last
Image!, last_caption => q!
({CAPTION})!, ); TEMPLATE_DIR: for my $tmpl_dir ( grep { -d } map { path($CONFIG{'gallery_path'}, ( $_ ? $CONFIG{'template_dir'} . ".$_" : $CONFIG{'template_dir'} ) ) } reverse @LANG ) { warn "Reading image templates from '$tmpl_dir'\n" if $DEBUG; $tmpl->templates($tmpl_dir); $tmpl->define(main => "image_main.txt") if -f path($tmpl_dir, "image_main.txt"); $tmpl->define(image_style => "image_style.txt") if -f path($tmpl_dir, "image_style.txt"); $tmpl->define(media => "image_$mime.txt") if -f path($tmpl_dir, "image_$mime.txt"); $tmpl->define(link => 'image_link.txt') if -f path($tmpl_dir, 'image_link.txt'); $tmpl->define(link_empty => 'image_link_empty.txt') if -f path($tmpl_dir, 'image_link_empty.txt'); $tmpl->define(up => "image_up.txt") if -f path($tmpl_dir, "image_up.txt"); $tmpl->define(up_default => "image_up_default.txt") if -f path($tmpl_dir, "image_up_default.txt"); $tmpl->define(first => "image_first.txt") if -f path($tmpl_dir, "image_first.txt"); $tmpl->define(first_empty => "image_first_empty.txt") if -f path($tmpl_dir, "image_first_empty.txt"); $tmpl->define(first_default => "image_first_default.txt") if -f path($tmpl_dir, "image_first_default.txt"); $tmpl->define(first_caption => "image_first_caption.txt") if -f path($tmpl_dir, "image_first_caption.txt"); $tmpl->define(previous => "image_previous.txt") if -f path($tmpl_dir, "image_previous.txt"); $tmpl->define(previous_empty => "image_previous_empty.txt") if -f path($tmpl_dir, "image_previous_empty.txt"); $tmpl->define(previous_default => "image_previous_default.txt") if -f path($tmpl_dir, "image_previous_default.txt"); $tmpl->define(previous_caption => "image_previous_caption.txt") if -f path($tmpl_dir, "image_previous_caption.txt"); $tmpl->define(next => "image_next.txt") if -f path($tmpl_dir, "image_next.txt"); $tmpl->define(next_empty => "image_next_empty.txt") if -f path($tmpl_dir, "image_next_empty.txt"); $tmpl->define(next_default => "image_next_default.txt") if -f path($tmpl_dir, "image_next_default.txt"); $tmpl->define(next_caption => "image_next_caption.txt") if -f path($tmpl_dir, "image_next_caption.txt"); $tmpl->define(last => "image_last.txt") if -f path($tmpl_dir, "image_last.txt"); $tmpl->define(last_empty => "image_last_empty.txt") if -f path($tmpl_dir, "image_last_empty.txt"); $tmpl->define(last_default => "image_last_default.txt") if -f path($tmpl_dir, "image_last_default.txt"); $tmpl->define(last_caption => "image_last_caption.txt") if -f path($tmpl_dir, "image_last_caption.txt"); } ## this is not an image: look it up in the caption file my $is_image = 1; $tmpl->assign(MEDIA_SIZE => ''); unless( $subr->content_type =~ m!^image/! ) { ## get link dimensions my ($tmp_image, $tmp_link) = get_captions($path, undef, $image); if( $tmp_link->[CLINK_W] && $tmp_link->[CLINK_H] ) { $tmpl->assign(MEDIA_SIZE => q!WIDTH="! . $tmp_link->[CLINK_W] . q!" HEIGHT="! . $tmp_link->[CLINK_H] . q!" !); } $image = $tmp_image || $image; undef $is_image; } ## set template variables my($link, $comment) = get_captions($path, $image); undef $link unless $is_image; if( $link && $link->[CLINK_F] ) { $tmpl->assign(IMG_LINK => $link->[CLINK_F]); $tmpl->parse(LINK => 'link'); } else { $tmpl->parse(LINK => 'link_empty'); } my $uri = $r->uri; my $loc = $r->location; $uri =~ s/^$loc/$CONFIG{'gallery_root'}/; if( my($userdir) = $r->uri =~ m!~([^/]+)! ) { warn "USERDIR: $userdir\n" if $DEBUG; $uri = "~$userdir/$uri"; } $tmpl->assign(TITLE => $comment); $tmpl->assign(COMMENT => $comment); $tmpl->assign(MEDIA_FILE => "/$uri"); ## uri to actual media file $tmpl->parse(MEDIA => 'media'); ## parse navigation links my $up = $r->uri; $up =~ s!^(.*/)[^/]+$!$1!; $tmpl->assign(UP_LINK => $up); $tmpl->parse(UP_DEFAULT => 'up_default'); $tmpl->parse(DIR_UP => 'up'); $tmpl->parse(IMAGE_STYLE => 'image_style'); GET_NAVIGATION: { opendir DIR, $path or do { $r->log_error( "Could not open '$path': $!" ); last GET_NAVIGATION; }; my @files = (); for my $file ( sort { lc($a) cmp lc($b) } grep { ! /^(?:\.|$CONFIG{'thumb_prefix'})/ } readdir DIR ) { ## this algorithm must match thumbnail gallery display for ## gallery and image navigation to stay synchronized my $subreq = $r->lookup_file(path($path, $file)); push @files, $file if -f $subreq->finfo() && $subreq->content_type =~ m!^image/!; } closedir DIR; if( my $first = ( @files && $files[0] eq $image ? '' : $files[0] ) ) { unless( $CONFIG{'always_link'} ) { my $link = (get_captions($path, $files[0]))[CAP_LINK]; $first = ($link && $link->[CLINK_F] ? $link->[CLINK_F] : $first); } $tmpl->assign(FIRST_LINK => $first); $tmpl->assign(FIRST_CAPTION => ''); $tmpl->parse(FIRST_DEFAULT => 'first_default'); if( my $caption = get_captions( $path, $first ) ) { $tmpl->assign(CAPTION => $caption); $tmpl->parse(FIRST_CAPTION => 'first_caption'); } $tmpl->parse(IMG_FIRST => 'first'); } else { $tmpl->parse(IMG_FIRST => 'first_empty'); } if( my $last = (@files && $files[$#files] eq $image ? '' : $files[$#files]) ) { unless( $CONFIG{'always_link'} ) { my $link = (get_captions($path, $files[$#files]))[CAP_LINK]; $last = ($link && $link->[CLINK_F] ? $link->[CLINK_F] : $last); } $tmpl->assign(LAST_LINK => $last); $tmpl->assign(LAST_CAPTION => ''); $tmpl->parse(LAST_DEFAULT => 'last_default'); if( my $caption = get_captions( $path, $last ) ) { $tmpl->assign(CAPTION => $caption); $tmpl->parse(LAST_CAPTION => 'last_caption'); } $tmpl->parse(IMG_LAST => 'last'); } else { $tmpl->parse(IMG_LAST => 'last_empty'); } ## $idx is required for $prev and $next calculations my $idx = 0; $idx++ while $idx < $#files && $files[$idx] ne $image; if( my $prev = ( ($idx-1) < 0 ? '' : $files[$idx-1] ) ) { unless( $CONFIG{'always_link'} ) { my $link = (get_captions($path, $files[$idx-1]))[CAP_LINK]; $prev = ($link && $link->[CLINK_F] ? $link->[CLINK_F] : $prev); } $tmpl->assign(PREVIOUS_LINK => $prev); $tmpl->assign(PREVIOUS_CAPTION => ''); $tmpl->parse(PREVIOUS_DEFAULT => 'previous_default'); if( my $caption = get_captions( $path, $prev ) ) { $tmpl->assign(CAPTION => $caption); $tmpl->parse(PREVIOUS_CAPTION => 'previous_caption'); } $tmpl->parse(IMG_PREV => 'previous'); } else { $tmpl->parse(IMG_PREV => 'previous_empty'); } if( my $next = ( ($idx+1) > $#files ? '' : $files[$idx+1] ) ) { unless( $CONFIG{'always_link'} ) { my $link = (get_captions($path, $files[$idx+1]))[CAP_LINK]; $next = ($link && $link->[CLINK_F] ? $link->[CLINK_F] : $next); } $tmpl->assign(NEXT_LINK => $next); $tmpl->assign(NEXT_CAPTION => ''); $tmpl->parse(NEXT_DEFAULT => 'next_default'); if( my $caption = get_captions( $path, $next ) ) { $tmpl->assign(CAPTION => $caption); $tmpl->parse(NEXT_CAPTION => 'next_caption'); } $tmpl->parse(IMG_NEXT => 'next'); } else { $tmpl->parse(IMG_NEXT => 'next_empty'); } } ## process main $tmpl->parse(MAIN => 'main'); $r->print($tmpl->to_string('MAIN')); return OK; } sub get_captions { my $path = shift; my $lookup = shift; my $lookup_link = shift; my %captions = (); my $lfile = ''; my $llink = ''; my $lcaption = ''; ## special case for gallery root in scalar context (I used to have ## !wantarray in here instead of "defined $lookup") if( defined $lookup && path($path,$lookup) eq $CONFIG{'gallery_path'} ) { return ( $CONFIG{'gallery_name'} ? $CONFIG{'gallery_name'} : $lcaption ); } CAPTIONS: for my $caption_file ( map { path($path, ($_ ? $CONFIG{'caption_file'} . ".$_" : $CONFIG{'caption_file'}) ) } reverse @LANG ) { $DEBUG=0; warn "Checking for $caption_file\n" if $DEBUG; next CAPTIONS unless -f $caption_file; warn "Found $caption_file...\n" if $DEBUG; open FILE, $caption_file or last CAPTIONS; local $_; while( my $line = ) { chomp $line; my($file,$link,$caption) = split(/:/, $line, 3); next unless $file; ## normalize link $link = ( $link ? [split(';',$link)] : [] ); ## looking up $file by $link if( $lookup_link && $link->[CLINK_F] ) { if( $link->[CLINK_F] eq $lookup_link ) { $lfile = $file; $llink = $link; last; } next; } ## looking up $caption or $link by $file if( $lookup ) { next unless $file eq $lookup; last unless $caption || $link; warn "Setting caption to $caption\n" if $DEBUG; $lcaption = $caption; $llink = $link; last; } warn "Setting caption(2) to $caption\n" if $DEBUG; $captions{$file} = [$link, $caption]; } close FILE; $DEBUG=0; } return ($lfile, $llink) if $lookup_link; return ($llink, $lcaption) if $lookup; return %captions; } sub path { return File::Spec->canonpath(File::Spec->catfile(grep $_, @_)) } sub thumb { return $CONFIG{'thumb_prefix'} . (@_ ? shift : '') } 1; __END__ =head1 NAME Apache::App::Gallery::Simple - Elegant and fast filesystem-based image galleries =head1 SYNOPSIS SetHandler perl-script PerlHandler Apache::App::Gallery::Simple; PerlSetVar GalleryRoot /vacation/images PerlSetVar GalleryName "My Vacation Photos" SetHandler perl-script PerlHandler Apache::App::Gallery::Simple PerlSetVar GalleryRoot /images PerlSetVar GalleryName "Hawkeye's Pictures" PerlSetVar ThumbWidth 250 =head1 DESCRIPTION B creates navigable thumbnail galleries from directories with images on your web server. B is completely configurable via a simple template system, allows for image captions as well as multimedia support, and also allows you to specify multiple languages for your templates and captions. The rest of this document is just the details. B creates an image gallery (complete with thumbnails and navigation links) from a directory or directory hierarchy on a web server. Simply upload images to a directory and the work is done. You can add captions to your images or other media (e.g., movies; see L<"ALTERNATIVE MEDIA">) and the entire B application is customizable using a simple template system (see L<"TEMPLATES">). I wanted an image gallery that was as easy to setup and use as Apache::Album but that offered a little more flexibility with captions, style/layout (including CSS or other customizations). Some effort has been made to retain some of the Apache::Album configuration directives, if not in name, at least in spirit. You should also be able to use existing thumbnail directories if you're migrating from Apache::Album. Using Gallery::Simple is, well, simple. There are no configuration files or anything of that sort (other than the Apache directives described in L<"OPTIONS">). All you have to do is install it, add something like the I block shown above in B<"SYNOPSIS"> section, and the path specified in B has suddenly become an image gallery. There are a few features that are I implemented that Apache::Album does implement, namely: - slideshow - browser-based uploads and gallery configuration - image sorting - variable image size viewing Gallery::Simple does do a couple of things well, however. It allows you to put captions to your images. It allows you to link to an alternative media source in the image page, which means you can also upload movies and other such stuff into your gallery. It allows template sets and caption files for multiple languages, meaning web visitors who prefer to see your site in Spanish may do so, while the German readers can see German galleries. Gallery::Simple handles virtual hosted domains and even handles "tilde-dirs" or directories using Apache's B directive in the form of F. Further, Gallery::Simple also allows you to completely customize the HTML returned for a particular gallery and an image page (you could even customize it to return XML if you wanted). You can add style sheets, alter (even replace) the layout so that it integrates cleanly with your existing web site. =head2 A quick example Say you have a directory under your DocumentRoot (e.g., F) called F that contains digital photos (full path: F) and you want people to access this location with the following url: http://www.joesfamily.org/gallery/ Add the following block to your Apache configuration file: ServerName joesfamily.org DocumentRoot /usr/home/joe/www ... ## this is the gallery section you've just added PerlWarn On PerlTaintCheck On SetHandler perl-script PerlHandler Apache::App::Gallery::Simple PerlSetVar GalleryRoot /family/images PerlSetVar GalleryName "Joe's Photo Gallery" Browsers accessing the url F will be looking at the images in F. =head1 OPTIONS The following options are available in the Apache configuration file with the B directive. =over 4 =item B Default: (empty) Values: The physical path to the image gallery, relative to the document root (for Apache B directories, this is often F or something like that). Description: this setting determines where Gallery::Simple looks for your image gallery. All image files and directories rooted at this location will be part of the gallery. Paths should be specified without a leading slash; paths will be relative to the document root (Apache's B) for this virtual host. Special care has been take to make this work for hosts using Apache's B directive (i.e., "tilde-user" URIs such as "~joe") as well. =item B Default: (empty) Values: Any string value Description: this is the name of your "top" gallery; sub-gallery names may be specified in the F file. =item B Default: en Values: Any ISO-639 language code Description: this setting determines what language the default F file services. Browser clients send an ordered list of preferred languages; Gallery::Simple uses this data to select which F file to read. For example, if a browser prefers Spanish content to English, it might send "es, en" in its request. Gallery::Simple detects this and will look for F and then F for caption information. If B is set to 'es', then Gallery::Simple will assume that F is in Spanish and look for F and then F for caption information. Gallery::Simple automatically tries major language preference if a minor language preference fails. For example, if 'en-US' does not exist, 'en' will be tried next (and not tried again, even if, for example, 'en-UK' were preferred second). =item B Default: no Values: [yes|no] Description: when disabled, Gallery::Simple will try (for example) 'en-us' first (assuming 'en-us' were passed in by the browser) and then fall back to 'en' (the major language type). If B is set to 'yes', only languages explicitly passed in by the browser will be attempted (e.g., in our above example, 'en' would not be tried if 'en-us' were not found). =item B Default: on Values: [on|off] Description: when disabled, no "Other galleries" message will display showing sub-galleries within this directory. You may still access sub-galleries using the F file (see L<"Sub-Gallery Thumbnails"> under L<"CAPTION FILES">). =item B Default: no Values: [yes|no] Description: when enabled, non-image media files (e.g., movies, etc.) will simply appear as a link below the image on the representative image page. When disabled, thumbnails and image navigation links will load a page with the multimedia file embedded (as if it were an image). =item B Default: C Values: C, C, C Description: this setting determines how to thumbnail an image; I means that the value in B will be used and the height will scale accordingly; I means that the value in B will be used and the width will scale accordingly; I means that the ratio in B will be used and the height and width will scale by that factor. =item B Default: C<100> Values: any positive integer value Description: this setting is used when B is set to I. This determines how wide the thumbnails will be. The height will be scaled to preserve the original image's width/height ratio. =item B Default: C<100> Values: any positive integer value Description: this setting is used when ThumbUse is set to I. This determines how high the thumbnails will be. The width will be scaled to preserve the original image's width/height ratio. =item B Default: C<1/5> Values: any fractional value; may be expressed as a decimal number Description: this setting will scale all images by this amount. The default (1/5) means that the image will be 1/5 of its original size. =item B Default: C<.thumbs> Values: any valid directory name Description: this is the name of the directory that will be created to store thumbnails. Only filenames (no pathnames containing '/') are allowed. A B will be created in each gallery directory where images are found. =item B Default: C Values: any string of characters (no '/'). Description: this value will be prepended to each thumbnail image to distinguish it from a regular image. =item B Default: C<0> Values: zero (0) or any positive integer value Description: this determines the number of columns in the thumbnail gallery. This setting overrides the calcuated setting determined by B and can be set to any positive integer value. =item B Default: C<640> Values: any positive integer value Description: this setting determines the optimal browser width for your visitors. The number of columns displayed is determined by dividing the B setting by B (even if B is being used). For example, if B is set to 640 and B is set to 100, then there will be 640/100 = 6 columns of thumbnails in each gallery. =item B Default: off Values: [on|off] Description: when enabled, displays a link to the URL of '/' ("home") in the breadcrumb navigation links in a gallery. =item B Default: C Values: any valid filename Description: this is the name of the file where image caption information will be stored. Only filenames (no pathnames containing '/') are allowed. If a B exists, it will be read and captions found will be displayed with the image. More information about caption files may be found in L<"CAPTION FILES">. =item B Default: C<.templates> Values: any valid directory name Description: this is the name of the directory where you will store your templates, if you wish to override the default look-and-feel of Gallery::Simple. This directory is always found in the B directory. Only filenames (no pathnames containing '/') are allowed. More information about templates may be found in L<"TEMPLATES">. =back =head1 ALTERNATIVE MEDIA You can have non-image media (audio clips, movies, etc.) in your gallery, too, as long as you have an image to represent it. Upload your non-image media just as you would an image. Additionally, you'll need to upload an image that represents your media. For example, if your alternative media were a movie, you might take a screen capture of it and use that as your representative image. Once you've uploaded the media file and a representative image, you'll then need to create a file called F in the same directory you uploaded the image to. The file should contain a line like the following: picnic.jpg:picnic.mov;320;255:First Summer Picnic F is the name of your representative image; F is your movie file (with dimensions), followed by the caption. Now when people browse your gallery, they'll see F in the thumbnail gallery. If they click the image, they'll be taken to a page that contains F as an embedded movie. [If B is enabled, a larger version of the representative image is shown, just as a normal image would, but there will also be a link beneath the image which links to the alternative media file.] For more information about the caption file see L<"CAPTION FILES">. For more information about customizing the look of the gallery, see L<"TEMPLATES">. =head1 CAPTION FILES The format of a caption file is this: imagename:linkname:some caption text I is the name of the image (in this directory) that the caption relates to. I is case-sensitive. I is optional. I is a link where another media file (such as a Quicktime movie, mpeg file, or even a sub-directory) may be found (the file or directory must be in the same directory as the caption.txt file it is referenced in). The final field is where you can put a brief (or long--it goes until the next newline character) description of this image. Here is a sample directory listing: January melissa.jpg jared.jpg PB140011.JPG joe_burns.mov joe_tabasco.jpg and the corresponding caption file: January::January 2003 melissa::Melissa shakes hands with Tom jared.jpg::Jared turfs it joe_tabasco.jpg:joe_burns.mov;320;255:Joe eating Tabasco sauce We notice the following things about these images and the corresponding caption file: =over 4 =item January This is a directory within our gallery; this will be treated as a "sub-gallery". The 'linkname' space is empty ("::") and is currently not used with sub-galleries. The directory will appear with the link "January 2003" instead of just "January". =item melissa.jpg This image has no alternative media--the 'linkname' space is empty ("::"). This image does have a comment (caption): "Melissa shakes hands with Tom". =item jared.jpg This image is like the previous one: no alternative media, but it does have a caption. =item PB140011.JPG This image is not mentioned in the caption file--no caption will be printed for this image. =item joe_tabasco.jpg A thumbnail of this image will appear in the gallery (thumbnail) page; when the thumbnail link is followed, a page with a movie file (F) will appear along with the caption "Joe eating Tabasco sauce". It is necessary to supply the width and height of the movie (in that order) with semicolons in the "link" field of the F file: joe_burns.mov;320;255 This is for correct rendering in Internet Explorer. The height should also include (the author has found by experimentation) about a 15 pixel buffer to show the movie player controls. This may not be enough (or too much) for your media player. If you are aware of a better way to embed multimedia content in web pages, please contact the author. =back =head2 Sub-Gallery Thumbnails Another interesting use for the 'linkname' space is that you can create picture representations of sub-galleries. For example, consider this entry: vacation.png:vacation-2001/:2001 Summer Vacation Photos When someone clicks the F thumbnail, they'll be taken not to the F image (if the 'linkname' space did not reference a directory), but to a sub-gallery named F. You don't I to do sub-galleries this way, of course. It is done automatically for you with links on the left side of the screen under "Other galleries", but this provides yet another (graphical) way to do it. =head1 TEMPLATES B image galleries and image pages may be customized on a per-gallery basis (i.e., per-virtualhost or per UserDir, but not per-subdirectory). This means that you can have one image gallery for your brother-in-law and allow him to make his gallery appear as he wants, and another gallery for Mom and allow her to customize her gallery in her way. The B template system is based on B (see L), a fast, minimalistic template system. B has two template sets: =over 4 =item * thumbnail gallery ("gallery") templates (and associated template variables) =item * image page templates (and associated template variables) =back Each of these sets of templates (including template variables) are described below. To customize a whole or part of the look (HTML) of B, all you need to do is create (or edit, if the template already exists) a template file and upload it to a directory called F<.templates> (see L) in the directory you have designated as the gallery root (see L). B will automatically detect the new template and use it for the next browser request. No server restart is necessary. Associated with each template are zero or more template variables; these variables are assigned values during the browser request (and the value assigned depends on the current gallery, the caption file and other environmental factors). You may insert, rearrange, or remove template variables from the templates to achieve a new gallery and image page look. Please refer to L for more details on how to use templates. In the sections below we describe the available templates and variables to customize any single Gallery::Simple gallery. =head2 Gallery Templates Gallery templates affect the HTML layout of the thumbnail gallery, including navigation links, the thumbnail table layout, "sub-galleries" (directories within the current gallery), breadcrumb links (top level navigation), etc. The gallery template set consists of the following templates: =over 4 =item F Default value: {GALLERY_NAME} {GALLERY_STYLE} {GALLERY_TITLE} {BREADCRUMBS} {OTHER_GALLERIES}
{DIR_FIRST}{DIR_PREV} {DIR_NEXT}{DIR_LAST}
{GALLERY}
{GALLERY_FOOTER} Variables: GALLERY_NAME, GALLERY_TITLE, GALLERY_STYLE, BREADCRUMBS, OTHER_GALLERIES, DIR_FIRST, DIR_PREV, DIR_NEXT, DIR_LAST, GALLERY =item F Default value: (empty) Variables: GALLERY_NAME =item F Default value: (empty) =item F Default value: =item F Default value:
Variables: VERSION =item F Default value:

Other galleries within this gallery:
{DIRECTORIES}

Variables: DIRECTORIES =item F Default value:

(No other galleries within this gallery)

=item F Default value: =item F Default value:  {BREADCRUMB} -> Variables: BREADCRUMB_LINK, BREADCRUMB =item F Default value:  {BREADCRUMB} Variables: BREADCRUMB =item F Default value: home =item F Default value: {FIRST_DEFAULT} Variables: FIRST_LINK, FIRST_DEFAULT, FIRST =item F Default value: (empty) =item F Default value: First
Gallery =item F Default value:
({CAPTION}) Variable: CAPTION =item F Default value: {PREV_DEFAULT}{PREV} Variables: PREV_LINK, PREV_DEFAULT, PREV =item F Default value: (empty) =item F Default value: Previous
Gallery =item F Default value:
({CAPTION}) Variables: CAPTION =item F Default value: {NEXT_DEFAULT}{NEXT} Variables: NEXT_LINK, NEXT_DEFAULT, NEXT =item F Default value: (empty) =item F Default value: Next
Gallery =item F Default value:
({CAPTION}) Variables: CAPTION =item F Default value: {LAST_DEFAULT} Variables: LAST_LINK, LAST_DEFAULT, LAST =item F Default value: (empty) =item F Default value: Last
Gallery =item F Default value:
({CAPTION}) Variables: CAPTION =item F Default value: {DIRECTORY}
\n Variables: URI_DIR, DIRECTORY =item F Default value:  ({CAPTION}) Variables: CAPTION =item F Default value: {ROWS}
Variables: ROWS =item F Default value:
(No photos in this gallery)
=item F Default value: =item F Default value: {ROW_END}{ROW_START} Variables: ROW_END, ROW_START =item F Default value: =item F Default value: Variables: URI_IMAGE, URI_THUMB =back =head2 Gallery Template Variables We now describe the available B