#!/usr/bin/perl -w # ## this versions hasa some minor changes by age@systemausfall.org # # Edwin Huffstutler # John Reynolds # # perl script for: web page index/thumbnails of photos. # orginally started life as a menu selector for fvwm2 backgrounds... # # USAGE: # # imageindex [options] # # is assumed to be "." if not given # # Options: (can be abbreviated if unique) # # -title title for this page (saved for susbsequent runs) # -destdir export image/html tree to dir (will be created if needed) # -[no]recurse enable/disable recursion into subdirectories # -[no]medium enable/disable medium size images/links # -[no]slide enable/disable slideshow files # -[no]detail enable/disable detail file # -[no]dirs enable/disable directory entries # -[no]montage enable/disable directory montages # -forceregen force regeneration of thumbnails # -columns number of columns in html table (saved for susbsequent runs) # -exclude Exclude from processing. Can be used multiple times # -includeall Nullifies excluded file list (saved from previous run) # -skipmont Exclude from being included in a directory montage. # -reverse Order timestamps with newest first # -x override thumbnail x size # -y override thumbnail y size # -help show this text # -version show the current version # -d 'var=val' force override of global variable # # See also the configuration section at the top of the program itself, # or in ~/.imageindexrc # # (non-html-generating, utility options) # # -lowercase Lowercase all image files in a directory # -caption Store comment in image # -rotate [cw|ccw] Rotate an image clockwise or counterclockwise # -showexcluded Show which files were excluded in a prior run # ###################################################################### # # Configuration options # # sizes / dirs $thumbnail_dir = 'thumbnail'; $default_thumbnail_x = 200; $default_thumbnail_y = 200; # If both dimensions of the original are within this much of the thumb # dimensions we will skip the thumbnail and just use the original $thumbnail_threshold = 1.0; $med_x = 512; $med_y = 384; $med_dir = 'medium'; # If both dimensions of the original are within this much of the "medium" # dimensions we will skip creating the medium-size format and just use the # original $med_threshold = 1.6; # Enable/disable features, set default for various flags $do_recurse = 0; # Recurse into subdirs? $do_medium = 1; # Generate medium-format? $do_slide = 1; # Generate slides/frame view? $do_detail = 1; # Generate details page? $do_dirs = 1; # Create directory entries? $do_montage = 1; # Create directory montages? $do_emoticons = 1; # Replace ASCII smiley's with images? $do_reverse = 0; # Sort timestamps in reverse order? # What the various image links point to - can be 'index', 'fullsize', # 'medium', 'thumbnail', 'slide', or 'details' $index_linkto = 'slide'; $details_linkto = 'index'; $slide_linkto = 'fullsize'; # Default number of columns to use $default_columns = 3; # Orientation of slide frame - 'horizontal' or 'vertical' $frame_orient = 'vertical'; # Location of items in slide pages; 'top', 'bottom', or 'none' $slide_caption = 'top'; $slide_date = 'bottom'; # Details index uses thumbs reduced by this amount $detailshrink = 2; # Quality for generated images $thumb_quality = 50; $med_quality = 80; # Minimum and maximum number of tiles in directory montage images $montage_min = 4; $montage_max = 36; # Space between montage images $montage_whitespace = 2; # What to do with leftover montage tiles; can be # 'blank' or 'repeat' $montage_fill = 'blank'; # Stylesheet specs # Set element font, etc. properties here $stylesheet = ' body { color: black; background: white; } /* Fonts in the title */ h1.title { font-family: "Comic Sans MS",Helvetica,sans-serif; font-size: 200%; font-weight: bold; text-align: center; } h2.daterange { font-family: Arial,Helvetica,sans-serif; font-size: 125%; text-align: center; } h3 { font-family: Arial,Helvetica,sans-serif; font-size: 90%; text-align: center; } /* Photo captions & Directory titles */ div.caption { font-family: Arial,Helvetica,sans-serif; font-size: 100%; font-weight: bold; margin: 1em; } /* Overall fonts on the index and details page */ div.index { font-family: Arial,Helvetica,sans-serif; font-size: 80%; } div.detail { font-family: Arial,Helvetica,sans-serif; font-size: 80%; } div.credits { font-family: Arial,Helvetica,sans-serif; font-size: 80%; text-align: right; margin: 10px } /* Table attributes */ table.index { background: #ffffff; border: none; border-spacing: 8px; } td.index { border: none; padding: 3px } table.frame { background: #ffffff; border: none } td.frame { border: none; padding: 0px } /* Image attributes */ img.index { border: none; } img.slide { border: none; } img.frame { border: none; } /* Link attributes */ a:link { color: blue; } a:visited { color: green; } a:hover { color: red; } a:active { color: red; } '; # Text $emptycell = "empty"; $updirtext = "up one directory"; $framelinktext = "slideshow view (frames)"; $detaillinktext = "details index"; $indexlinktext = "main index"; $default_titletext = "Image directory"; # These five variables control the TITLE attribute on anchor constructs in the # index and frame views. When TITLE attributes are given they are usually # rendered as "tooltip" bubbles that show text when a cursor hovers and stops # over the active link. We use them here to give a visual cue about the image. # These variables work much like printf(1) strings. # # %f => replaced with the filename of the image # %d => replaced with the date/time of the image (or mtime of the file) # %s => replaced with the size of the file (in Kb) # %r => replaced with the resolution (XxY) of the original image # %c => replaced with the image's caption (if stored with one) # %% => replaced with a literal '%' character # # The following are used when directories are processed and a montage of # that directory is used as the thumbnail of the dir. # # %n => replaced with number of images in a directory # %b => replaced with the "begin" date from a directory of images # %e => replaced with the "end" date from a directory of images # %t => replaced with the "title" from a directory of images # # Other characters (including spaces) are literal. "undef" these in # your ~/.imageindexrc file if you don't want them to show up. The "date/time" # related constructs are interpolated using the date/time format variables # defined below. # $framethumbtitle = "%f - %d"; $indexthumbtitle = "%f (%s)"; $slidethumbtitle = "%f (%s)"; $detailthumbtitle = "%c"; $montagetitle = "%n images %b through %e"; # Date/Time format strings. These strings are formatted much like the above # variables and the definitions of the escape sequences come from the POSIX # strftime(3) definitions. NOT ALL of strftime(3) are supported for obvious # reasons. # # %S is replaced by the second as a decimal number (00-60). # %M is replaced by the minute as a decimal number (00-59). # %I is replaced by the hour (12-hour clock) as a decimal number (01-12). # %H is replaced by the hour (24-hour clock) as a decimal number (00-23). # %p is replaced by national representation of either "ante meridiem" or # "post meridiem" as appropriate (currently only U.S. "am" or "pm") # %R is equivalent to "%H:%M" (in *timeformat variables only). # %r is equivalent to "%I:%M:%S %p" (in *timeformat variables only). # # %Y is replaced by the year with century as a decimal number. # %y is replaced by the year without century as a decimal number (00-99). # %m is replaced by the month as a decimal number (01-12). # %d is replaced by the day of the month as a decimal number (01-31). # %F is equivalent to "%Y-%m-%d" (in *dateformat variables only). # %D is equivalent to "%m/%d/%y" (in *dateformat variables only). # %% is replaced by a literal "%". $framedateformat = "%m/%d/%Y"; $frametimeformat = "%r"; $indexdateformat = "%m/%d/%Y"; $indextimeformat = "%r"; $slidedateformat = "%m/%d/%Y"; $slidetimeformat = "%r"; $detaildateformat = "%m/%d/%Y"; $detailtimeformat = "%I:%M %p"; # Pathnames $indexfile = 'index.html'; $detailfile = 'details.html'; $framefile = 'frame.html'; $slidefile = 'slides.html'; $slide_dir = 'slides'; $stylefile = 'style.css'; $montagefile = 'montage.jpg'; $emoticonprefix = 'ii_'; $emoticonsmile = $emoticonprefix . 'smile.png'; $emoticonwink = $emoticonprefix . 'wink.png'; $emoticonfrown = $emoticonprefix . 'frown.png'; # File exclusion customization (regex) # (Anything non-image and non-dir will be skipped automatically, this just # makes it silent) @exclude = qw( ^CVS$ ^.nautilus-metafile.xml$ ^.thumbnails$ ^.xvpics$ ^.thumbcache$ ^ALBUM.OFA$ ^desktop.ini$ ); # Metatags $columnsmetatag = 'Columns'; $titlemetatag = 'Title'; $begindatemetatag = 'DateBegin'; $enddatemetatag = 'DateEnd'; $excludemetatag = 'ExcludedFiles'; $skipmetatag = 'SkipMontageFiles'; $numimagesmetatag = 'NumImages'; $reversemetatag = 'Reverse'; $thumbxmetatag = 'ThumbnailX'; $thumbymetatag = 'ThumbnailY'; # Any of the above can be overridden in an rc file in the user's home dir $rcfile = "$ENV{'HOME'}/.imageindexrc"; ###################################################################### # # $Id: imageindex,v 1.164 2003/12/30 23:12:00 jjreynold Exp $ # # imageindex 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, or (at your option) # any later version. # # imageindex 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 imageindex; see the file COPYING. # ###################################################################### use Image::Magick; # comes with ImageMagick # from CPAN - optional eval('use Image::Info qw(image_info)'); # Shipped with perl use POSIX; use Getopt::Long; use FileHandle; use File::Basename; use File::Copy; use English; use Carp; require 'flush.pl'; # to shut up -w use vars qw($opt_recurse); use vars qw($opt_slide); use vars qw($opt_dirs); use vars qw($opt_detail); use vars qw($opt_lowercase); use vars qw($opt_help); use vars qw($opt_debug); use vars qw($opt_showexcluded); use vars qw($opt_version); &GetOptions( 'title=s', 'columns=i', 'x=i', 'y=i', 'forceregen', 'medium!', 'slide!', 'detail!', 'dirs!', 'montage!', 'recurse!', 'destdir=s', 'lowercase', 'caption=s', 'rotate=s', 'exclude=s@', 'skipmont=s@', 'showexcluded', 'includeall', 'version', 'help', 'debug', 'reverse!', 'd=s%' ) or die ("Invalid flag\n"); # Find out which platform we're on so we don't give incorrect options to needed # commands # $uname = `uname -s`; chomp ($uname); # Override config variables foreach my $var (keys %opt_d) { $value = $opt_d{$var}; print "(override) $var = $value\n"; eval("\$$var=\"$value\""); } &init_png_array(); # Read RC file if (-e $rcfile) { print "Using settings in $rcfile...\n" if ! defined ($opt_version); require $rcfile; } # Rotate or caption image (then exit) if (defined ($opt_rotate)) { &rotate_image($opt_rotate,\@ARGV); exit (0); } elsif (defined ($opt_caption)) { &caption_image($opt_caption,\@ARGV); exit (0); } elsif (defined ($opt_showexcluded)) { &showexcluded($ARGV[0]); exit (0); } elsif (defined ($opt_version)) { printf ("imageindex version: %s\n", &versionstring); exit (0); } # The directory to search is the first argument if (defined($ARGV[0])) { $srcdir = $ARGV[0]; $srcdir =~ s:/$::; } else { $srcdir = "."; } # Give usage message if (defined($opt_help)) { &usage(); exit(0); } # Show backtrace if debug given if (defined($opt_debug)) { $SIG{__WARN__} = \&Carp::cluck; } # Where to generate files $destdir = $srcdir; if (defined($opt_destdir)) { $destdir = $opt_destdir; $destdir =~ s:/$::; print "Exporting to $destdir\n"; unless (-d $destdir) { printf ("Creating destination directory '$destdir'.\n"); mkdir ($destdir, 0755); } } unless (-w $destdir) { printf ("No write permission for $destdir\n"); exit (1); } if (defined($opt_medium)) { $do_medium = $opt_medium } if (defined($opt_slide)) { $do_slide = $opt_slide; } if (defined($opt_detail)) { $do_detail = $opt_detail; } if (defined($opt_dirs)) { $do_dirs = $opt_dirs; } if (defined($opt_montage)) { $do_montage = $opt_montage; } if (defined($opt_recurse)) { $do_recurse = $opt_recurse; } # no montages if we aren't doing dirs anyway if ($do_dirs == 0) { $do_montage = 0; } &initialize_current_vars(); &read_stored_meta_data(); &override_by_commandline(); if (!defined(&image_info)) { print "Image::Info not found, not extracting EXIF data\n"; } opendir(DIR, "$srcdir") || die "Can't open dir $srcdir: ($!)\n"; @files = readdir DIR; closedir(DIR); @files = grep (!/^\.?\.$/, @files); # Skip the files/dirs we use or generate. Any other patterns go in the # config section (@exclude) or in exclude file my @generated_files = ($thumbnail_dir, $med_dir, $slide_dir, $indexfile, $detailfile, $stylefile, ); foreach my $pattern (@generated_files, @exclude) { @files = grep (!/$pattern/, @files); } @files = &exclude_files(@files); # Change all the names of image files to lowercase. if (defined ($opt_lowercase)) { &lower_case_files(@files); exit (0); } # Keep track of which column to be in my $col_counter = 1; # Count how many files we create my $object_counter = 0; my $dir_counter = 0; my $image_counter = 0; my $thumbnail_counter = 0; my $med_counter = 0; my $slide_counter = 0; my $modified_thumb = 0; # Keep track of max thumb sizes to use for slide frame width my $max_thumb_x = 0; my $max_thumb_y = 0; # Keep track of max thumb sizes to use for montage creation my $max_mont_thumb_x = 0; my $max_mont_thumb_y = 0; # Extract info print "Extracting image info"; flush (STDOUT); foreach my $file (@files) { # If directory, grab the timestamp if (-d "$srcdir/$file") { my $ts; # Grab timestamp from meta tag if (-e "$srcdir/$file/$indexfile") { my $begin = &extract_meta_tag($begindatemetatag,"$srcdir/$file/$indexfile"); if (defined($begin)) { if (!defined($firstdate) or ($begin < $firstdate)) { $firstdate = $begin; } $ts = $begin; } my $end = &extract_meta_tag($enddatemetatag,"$srcdir/$file/$indexfile"); if (defined($end)) { if (!defined($lastdate) or ($end > $lastdate)) { $lastdate = $end; } $ts = $end if (!defined($ts)); } } # Fallback on dir mtime if (!defined($ts)) { my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size, $atime,$mtime,$ctime,$blksize,$blocks) = stat("$srcdir/$file"); $ts = POSIX::strftime ("%Y%m%d%H%M.%S",localtime($mtime)); } push(@{$dir_timestamp{$ts}}, $file); } else { # Collect info from the image &fill_image_info($file); } } print "\n"; # Do dirs first if ($do_dirs) { foreach my $ts (sort bynumber keys %dir_timestamp) { foreach my $dir (sort @{$dir_timestamp{$ts}}) { &dir_entry($dir); } # foreach dir that has this timestamp } # foreach timestamp } # Bail if nothing here if ($object_counter == 0) { print "Nothing to do!\n"; unlink("$destdir/$indexfile") if (-e "$destdir/$indexfile"); unlink("$destdir/$detailfile") if (-e "$destdir/$detailfile"); unlink("$destdir/$stylefile") if (-e "$destdir/$stylefile"); exit(0); } # Make thumb dirs if needed foreach my $checkdir ($thumbnail_dir, $med_dir, $slide_dir) { unless (-d "$destdir/$checkdir") { mkdir("$destdir/$checkdir",0777); } } # Nuke old thumbnails if original image gone &nuke_out_of_date(); # Iterate over the files based on timestamp # This is just to get back/forward links undef $prev; foreach (sort bynumber keys %timestamp) { foreach my $pathname (sort @{$timestamp{$_}}) { if (defined($prev)) { my ($name,$path,$suffix); ($name,$path,$suffix) = fileparse($prev,'\.\S+'); $back{$pathname} = "$name.html"; ($name,$path,$suffix) = fileparse($pathname,'\.\S+'); $forward{$prev} = "$name.html"; } $prev = $pathname; } # foreach image that has this timestamp } # foreach timestamp # Iterate over the files based on timestamp # This will do the real work foreach (sort bynumber keys %timestamp) { foreach my $pathname (sort @{$timestamp{$_}}) { my $filename = $info{$pathname}{'file'}; my $thumbnail = $info{$pathname}{'thumb'}; my $medium = $info{$pathname}{'medium'}; my $slide = $info{$pathname}{'slide'}; if (!defined($firstdate) or ($info{$pathname}{'date'} < $firstdate)) { $firstdate = $info{$pathname}{'date'}; } if (!defined($lastdate) or ($info{$pathname}{'date'} > $lastdate)) { $lastdate = $info{$pathname}{'date'}; } # # First, deal with medium format of the image since we can save time shrinking # the medium down to the thumbnail rather than fullsize->thumbnail # # Skip if we want no medium images at all # if ($do_medium == 0) { $skipmedium{$pathname} = 1; unlink("$destdir/$medium") if (-e "$destdir/$medium"); } elsif (($info{$pathname}{'x'} <= ($med_x * $med_threshold)) and ($info{$pathname}{'y'} <= ($med_y * $med_threshold))) { # Skip if we are below the threshold size $skipmedium{$pathname} = 1; unlink("$destdir/$medium") if (-e "$destdir/$medium"); } else { my $image = new Image::Magick; my $retval; # Create medium sized pic if it is not there, # or is out of date with respect to original image if ((! -e "$destdir/$medium") or ( -M $pathname < -M "$destdir/$medium") or defined($opt_forceregen)) { my $newgeom = $med_x . "x" . $med_y; print "Creating $destdir/$medium\n"; $retval = $image->Read(filename=>$pathname); warn "$retval" if "$retval"; $retval = $image->Resize(geometry=>$newgeom); warn "$retval" if "$retval"; $retval = $image->Set(interlace=>Line); warn "$retval" if "$retval"; $retval = $image->Set(quality=>$med_quality); warn "$retval" if "$retval"; $retval = $image->Write(filename=>"$destdir/$medium"); warn "$retval" if "$retval"; $image_cache{$pathname} = $image; } else { # Up to date, existing medium, grab dimensions # Get the right hsize/vsize tags for the medium slides. Simply do a "Read" of # the file here and the code below will set the med_x/y properties. # $retval = $image->Read("$destdir/$medium"); warn "$retval" if "$retval"; } $info{$pathname}{'med_size'} = &convert_to_kb($image->Get('filesize')); $info{$pathname}{'med_x'} = $image->Get('width'); $info{$pathname}{'med_y'} = $image->Get('height'); $med_counter++; } # # Next, deal with the thumbnail for this image. If we have just created a medium # version of the image, then an open image "handle" will exist for it. We simply # shrink that down to thumbnail size (if appropriate) rather than reading in the # original file again just to shrink it (saves processing time). # # Skip thumb if we are below the threshold size if (($info{$pathname}{'x'} <= ($current_thumbnail_x * $thumbnail_threshold)) and ($info{$pathname}{'y'} <= ($current_thumbnail_y * $thumbnail_threshold))) { $info{$pathname}{'thumb_x'} = $info{$pathname}{'x'}; $info{$pathname}{'thumb_y'} = $info{$pathname}{'y'}; $skipthumb{$pathname} = 1; if (-e "$destdir/$thumbnail") { unlink("$destdir/$thumbnail"); $modified_thumb++; } push(@montagefiles,"$destdir/$filename"); } else { my $image = new Image::Magick; my $retval; # Create thumbnail if it is not there, # or is out of date with respect to original image if ((! -e "$destdir/$thumbnail") or ( -M $pathname < -M "$destdir/$thumbnail") or defined($opt_forceregen)) { my $newgeom = $current_thumbnail_x . "x" . $current_thumbnail_y; print "Creating $destdir/$thumbnail\n"; if (defined ($image_cache{$pathname})) { $image = $image_cache{$pathname}; $retval = $image->Resize(geometry=>$newgeom); warn "$retval" if "$retval"; $retval = $image->Set(quality=>$thumb_quality); warn "$retval" if "$retval"; $retval = $image->Write(filename=>"$destdir/$thumbnail"); warn "$retval" if "$retval"; } else { $retval = $image->Read(filename=>$pathname); warn "$retval" if "$retval"; $retval = $image->Resize(geometry=>$newgeom); warn "$retval" if "$retval"; $retval = $image->Set(interlace=>Line); warn "$retval" if "$retval"; $retval = $image->Set(quality=>$thumb_quality); warn "$retval" if "$retval"; $retval = $image->Write(filename=>"$destdir/$thumbnail"); warn "$retval" if "$retval"; } push(@montagefiles,"$destdir/$thumbnail"); $modified_thumb++; } else { # Up to date, existing thumb # Get the right hsize/vsize tags for the inline thumbs. Simply do a "Read" of # the file here and the code below will set the thumb_x/y properties. # $retval = $image->Read("$destdir/$thumbnail"); warn "$retval" if "$retval"; push(@montagefiles,"$destdir/$thumbnail"); } $info{$pathname}{'thumb_size'} = &convert_to_kb($image->Get('filesize')); $info{$pathname}{'thumb_x'} = $image->Get('width'); $info{$pathname}{'thumb_y'} = $image->Get('height'); $thumbnail_counter++; } # Set the max thumb sizes, to be used for slide frame width if ($info{$pathname}{'thumb_x'} > $max_thumb_x) { $max_thumb_x = $info{$pathname}{'thumb_x'}; } if ($info{$pathname}{'thumb_y'} > $max_thumb_y) { $max_thumb_y = $info{$pathname}{'thumb_y'}; } # Set the max montage thumb sizes, to be used when creating montage images # $bn = basename ($thumbnail); unless (defined ($skipmont{$bn})) { if ($info{$pathname}{'thumb_x'} > $max_mont_thumb_x) { $max_mont_thumb_x = $info{$pathname}{'thumb_x'}; } if ($info{$pathname}{'thumb_y'} > $max_mont_thumb_y) { $max_mont_thumb_y = $info{$pathname}{'thumb_y'}; } } # # Finally, create html for this image # &image_entry($pathname); } # foreach image that has this timestamp } # foreach timestamp # Finish up the columns if needed if (($col_counter != 1) and ($col_counter <= $current_columns) and ($object_counter > $current_columns)) { foreach ($col_counter..$current_columns) { push(@index, " $emptycell\n"); push(@details, " $emptycell\n"); } push(@index, " \n"); push(@details, " \n"); } # Nuke generated dirs if no contents system("rm -rf $destdir/$thumbnail_dir") if ($thumbnail_counter == 0); system("rm -rf $destdir/$slide_dir") if ($slide_counter == 0); system("rm -rf $destdir/$med_dir") if ($med_counter == 0); # Create montage if we had more than just dir entries here if (($dir_counter != $object_counter)) { &create_montage(@montagefiles); } # Create stylesheet &write_css(); # Write index web page open(INDEX,">$destdir/$indexfile") or die ("Can't open $destdir/$indexfile: $!\n"); &page_header('index', $index_linkto); foreach (@index) { print INDEX; } &page_footer('index'); close(INDEX); # Write photo details file if ($do_detail == 1) { open(INDEX,">$destdir/$detailfile") or die ("Can't open $destdir/$indexfile: $!\n"); &page_header('detail', $details_linkto); foreach (@details) { print INDEX; } &page_footer('detail'); close(INDEX); } else { unlink("$destdir/$detailfile") if (-e "$destdir/$detailfile"); } # Write slide/frame files if (($do_slide == 1) and ($slide_counter > 1)) { &write_frameset(); } else { system("rm -rf $destdir/$slide_dir") if (-d "$destdir/$slide_dir"); } # Optionally export images somewhere else if ($opt_destdir) { printf ("Copying image files from '$srcdir' to '$destdir'.\n"); foreach my $image (keys %info) { # BSD's default 'cp' cannot preserve links like GNU fileutils cp can # if ($uname =~ /BSD/) { system("cp -pv $image $destdir"); } else { system("cp -dpuv $image $destdir"); } } } if (defined ($do_emoticons) && $do_emoticons) { foreach $icon ('wink', 'smile', 'frown') { if ($emoticon{$icon}) { &write_emoticon_png ($icon); } else { unlink ($destdir . '/' . $thumbnail_dir . "/$emoticonprefix${icon}.png"); } } } ###################################################################### # # Write the various HTML parts for this image # ###################################################################### sub image_entry { my $pathname = shift(@_); my $filename = $info{$pathname}{'file'}; my $link; &index_html($pathname); if ($do_detail == 1) { &details_html($pathname); } if (($do_slide == 1) and ($image_counter > 1)) { &slide_html($pathname); } else { my $file = $info{$pathname}{slide}; unlink($file) if (-e $file); } # Increment for next time $col_counter++; $col_counter = 1 if ($col_counter > $current_columns); } ############################################################################### # # Generate HTML for index page entry # ############################################################################### sub index_html { my $pathname = shift(@_); my $filename = $info{$pathname}{'file'}; my $link; my $anchortext; # At beginning of row? if ($col_counter == 1) { push(@index, " \n"); } # Image push(@index, " \n"); push(@index, "
"); push(@index, &format_date($info{$pathname}{'date'}, 'index')); push(@index,"
\n"); if (($index_linkto eq 'details') and ($do_detail == 1)) { $link = "$detailfile#$filename"; } elsif (($index_linkto eq 'medium') and !defined($skipmedium{$pathname})) { $link = $info{$pathname}{'medium'}; } elsif (($index_linkto eq 'thumbnail') and !defined($skipthumb{$pathname})) { $link = $info{$pathname}{'thumb'}; } elsif (($index_linkto eq 'slide') and ($do_slide == 1) and ($image_counter > 1)) { $link = $info{$pathname}{'slide'}; } else { $link = $filename; } $anchortext = " "; push(@index, $anchortext); if (defined($skipthumb{$pathname})) { push(@index,"\"\n"); push(@index, "
"); # Full size link push(@index,"full size"); # Medium size link if within the threshold unless (defined($skipmedium{$pathname})) { push(@index," | medium"); } # Detail list link if ($do_detail == 1) { push(@index," | details"); } push(@index,"
\n"); # Caption if any (jpeg comment field) if (defined($info{$pathname}{'comment'})) { my ($tmp); push(@index, "
"); # Hack: if a comment has an ellipsis at the very end, make the HTML use a # non-breakable space before it so that the ellipsis doesn't "wrap" inside # the table field. It just looks better for those cases where the comment # is just long enough to wrap when rendered in the space given # $tmp = $info{$pathname}{'comment'}; $tmp = &htmlize_caption ($tmp); if ($tmp =~ /(\s+)\.\.\.\s*$/) { $tmp =~ s/(\s+)\.\.\.\s*$/ .../; } push(@index, $tmp); push(@index,"
\n"); } push(@index, " \n\n"); # At end of row? if ($col_counter == $current_columns) { push(@index, " \n"); } } ############################################################################### # # Generate HTML for slide/frame pages # ############################################################################### sub slide_html { my $pathname = shift(@_); my $filename = $info{$pathname}{'file'}; my $link; my $anchortext; # # First the index frame info # if ($frame_orient eq 'horizontal') { push(@frame," \n"); } else { push(@frame," \n \n"); } $anchortext = " "; push(@frame, $anchortext); if (defined($skipthumb{$pathname})) { push(@frame,"\"\n"); if ($frame_orient eq 'horizontal') { push(@frame," "); } else { push(@frame," \n "); } push(@frame,"\n"); # # Then the individual slides # my $slide = new FileHandle "> $destdir/$info{$pathname}{slide}"; if (!defined($slide)) { die("$destdir/$info{$pathname}{slide}: $!"); } select($slide); print "\n"; print "\n"; print "\n"; $verstring = &versionstring(); printf ("\n", $verstring); printf ("\n"); print "$current_titletext - $filename\n"; print "\n"; print "\n\n"; &next_prev_links($pathname); # Caption if any if (($slide_caption eq 'top') and defined($info{$pathname}{'comment'})) { print "
"; my $tmp = &htmlize_caption ($info{$pathname}{'comment'}, 'slide'); print $tmp; print "
\n"; } # Date, filename if ($slide_date eq 'top') { print "
"; print &format_date($info{$pathname}{'date'}, 'slide'); print " $filename"; print "
\n"; } if ($slide_linkto eq 'index') { $link = "../$indexfile#$filename"; } elsif (($slide_linkto eq 'details') and ($do_detail == 1)) { $link = "../$detailfile#$filename"; } elsif (($slide_linkto eq 'medium') and !defined($skipmedium{$pathname})) { $link = "../$info{$pathname}{medium}"; } elsif (($slide_linkto eq 'thumbnail') and !defined($skipthumb{$pathname})) { $link = "../$info{$pathname}{thumb}"; } else { $link = "../$filename"; } print "\n

\n"; $anchortext = ""; print "\n"; print "

\n"; # Caption if any if (($slide_caption eq 'bottom') and defined($info{$pathname}{'comment'})) { print "
"; my $tmp = &htmlize_caption ($info{$pathname}{'comment'}, 'slide'); print $tmp; print "
\n"; } # Date, filename if ($slide_date eq 'bottom') { print "
"; print &format_date($info{$pathname}{'date'}, 'slide'); print " $filename"; print "
\n"; } &next_prev_links($pathname); print "\n\n"; select(STDOUT); $slide->close(); $slide_counter++; unless(defined($first_slide)) { $first_slide = $info{$pathname}{'slide'}; } } ############################################################################### # # Generate HTML for details page # ############################################################################### sub details_html { my $pathname = shift(@_); my $filename = $info{$pathname}{'file'}; my ($link, $anchortext); # At beginning of row? if ($col_counter == 1) { push(@details, " \n"); } if ($details_linkto eq 'index') { $link = "$indexfile#$filename"; } elsif (($details_linkto eq 'medium') and !defined($skipmedium{$pathname})) { $link = "$info{$pathname}{medium}"; } elsif (($details_linkto eq 'thumbnail') and !defined($skipthumb{$pathname})) { $link = "$info{$pathname}{thumb}"; } elsif (($details_linkto eq 'slide') and ($do_slide == 1) and ($image_counter > 1)) { $link = $info{$pathname}{'slide'}; } else { $link = $filename; } push(@details," \n"); push(@details," \n"); push(@details," \n"); push(@details," \n\n"); push(@details," \n"); push(@details," \n"); push(@details,"
\n"); push(@details,"
\n"); push(@details," "); push(@details, &format_date($info{$pathname}{'date'}, 'detail')); push(@details,"
\n"); $anchortext = "
"); push(@details,"$filename
"); push(@details,"
\n"); push(@details,"
\n"); push(@details,"
"); push(@details,"Original: $info{$pathname}{geometry}"); push(@details," ($info{$pathname}{size})
"); unless (defined($skipmedium{$pathname})) { push(@details,"Medium: "); push(@details,$info{$pathname}{'med_x'} . 'x' . $info{$pathname}{'med_y'} . ""); push(@details," ($info{$pathname}{med_size})
"); } unless (defined($skipthumb{$pathname})) { push(@details,"Thumbnail: "); push(@details,$info{$pathname}{'thumb_x'} . 'x' . $info{$pathname}{'thumb_y'} . ""); #push(@details," ($info{$pathname}{thumb_size})
"); push(@details,"
"); } # # EXIF data # if (defined($info{$pathname}{'flash'})) { push(@details,"Flash: $info{$pathname}{flash}
"); } if (defined($info{$pathname}{'exposure_time'})) { push(@details,"Exposure time: $info{$pathname}{exposure_time}
"); } if (defined($info{$pathname}{'focus_dist'})) { push(@details,"Focus distance: $info{$pathname}{focus_dist}
"); } if (defined($info{$pathname}{'focal_length'})) { push(@details,"Focal length: $info{$pathname}{focal_length}
"); } if (defined($info{$pathname}{'aperture'})) { push(@details,"Aperture: $info{$pathname}{aperture}
"); } push(@details,"\n"); push(@details,"
\n"); push(@details,"
\n"); push(@details," \n"); # At end of row? if ($col_counter == $current_columns) { push(@details, " \n"); } } ###################################################################### # # Extract info from image # ###################################################################### sub fill_image_info { my $filename = shift (@_); my $pathname = "$srcdir/$filename"; my $image = new Image::Magick; my $retval; print "."; flush (STDOUT); $retval = $image->Read($pathname); if ($retval ne "") { print "\nSkipping $pathname"; flush (STDOUT); return; } else { $object_counter++; $image_counter++; } $info{$pathname}{'file'} = $filename; # Use mtime as a fallback date in case we don't have exif data my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size, $atime,$mtime,$ctime,$blksize,$blocks) = stat($pathname); $info{$pathname}{'date'} = POSIX::strftime ("%Y%m%d%H%M.%S",localtime($mtime)); $info{$pathname}{'x'} = $image->Get('width'); $info{$pathname}{'y'} = $image->Get('height'); $info{$pathname}{'geometry'} = $info{$pathname}{'x'} . "x" . $info{$pathname}{'y'}; $info{$pathname}{'size'} = &convert_to_kb($image->Get('filesize')); $info{$pathname}{'format'} = $image->Get('format'); $info{$pathname}{'comment'} = $image->Get('comment'); my ($name,$path,$suffix) = fileparse($filename,'\.\S+'); if ($info{$pathname}{'format'} =~ /JFIF/i) { $info{$pathname}{'thumb'} = "$thumbnail_dir/$filename"; $thumb_backref{"$thumbnail_dir/$filename"} = $pathname; $info{$pathname}{'medium'} = "$med_dir/$filename"; $med_backref{"$med_dir/$filename"} = $pathname; if (defined(&image_info)) { my $exif = image_info("$pathname"); if (my $error = $exif->{error}) { warn "Can't parse image info: $error\n"; } if (defined($opt_debug)) { print "EXIF data for $pathname:\n"; foreach (keys %$exif) { print " $_ = $exif->{$_}\n"; } print "\n"; } if (defined($exif->{DateTimeOriginal})) { $exif->{DateTimeOriginal} =~ /\s*([\d:]+)\s+([\d:]+)/; my $dt = $1; my $tm = $2; $tm =~ s/://; $tm =~ s/:/\./; $dt =~ s/://g; $info{$pathname}{'date'} = $dt . $tm; } if (defined($exif->{Flash})) { $info{$pathname}{'flash'} = $exif->{'Flash'}; $info{$pathname}{'flash'} =~ s/0/no/; $info{$pathname}{'flash'} =~ s/1/yes/; } if (defined($exif->{FocalLength})) { $info{$pathname}{'focal_length'} = sprintf("%4.1fmm", eval("$exif->{FocalLength}")); } if (defined($exif->{SubjectDistance})) { $info{$pathname}{'focus_dist'} = sprintf("%4.1fm", eval("$exif->{SubjectDistance}")); } if (defined($exif->{ExposureTime})) { $info{$pathname}{'exposure_time'} = $exif->{ExposureTime} . 's'; } if (defined($exif->{FNumber})) { $info{$pathname}{'aperture'} = "f/" . eval ("$exif->{FNumber}"); } } } else { $info{$pathname}{'thumb'} = "$thumbnail_dir/$name.jpg"; $thumb_backref{"$thumbnail_dir/$name.jpg"} = $pathname; $info{$pathname}{'medium'} = "$med_dir/$name.jpg"; $med_backref{"$med_dir/$name.jpg"} = $pathname; } $info{$pathname}{'slide'} = "$slide_dir/$name.html"; $slide_backref{"$slide_dir/$name.html"} = $pathname; push(@{$timestamp{"$info{$pathname}{date}"}}, $pathname); } ###################################################################### # # Write HTML for directory entries # ###################################################################### sub dir_entry { my $dir = shift(@_); my $destdirname = "$destdir/$dir"; my $srcdirname = "$srcdir/$dir"; my $anchortext; print "Processing directory $srcdirname\n"; # Recurse first if ($do_recurse == 1) { my $flags = ""; $flags .= "-medium " if ($do_medium == 1); $flags .= "-nomedium " if ($do_medium == 0); $flags .= "-slide " if ($do_slide == 1); $flags .= "-noslide " if ($do_slide == 0); $flags .= "-dirs " if ($do_dirs == 1); $flags .= "-nodirs " if ($do_dirs == 0); $flags .= "-montage " if ($do_montage == 1); $flags .= "-nomontage " if ($do_montage == 0); $flags .= "-detail " if ($do_detail == 1); $flags .= "-nodetail " if ($do_detail == 0); $flags .= "-reverse " if ($do_reverse == 1); $flags .= "-noreverse " if ($do_reverse == 0); $flags .= "-forceregen " if (defined($opt_forceregen)); $flags .= "-includeall " if (defined($opt_includeall)); $flags .= "-columns $current_columns " if (defined($opt_columns)); $flags .= "-x $opt_x " if (defined($opt_x)); $flags .= "-y $opt_y " if (defined($opt_y)); $flags .= "-destdir $destdirname " if ($destdir ne $srcdir); foreach my $var (keys %opt_d) { $flags .= " -d $var=$opt_d{$var}"; } system("cd $srcdirname ;$0 $flags -recurse"); } my $dirtitle = ""; my $first; my $last; my $montage; my $montage_x; my $montage_y; # Only add entry if this dir has an index file if (-r "$destdirname/$indexfile") { # Go fetch the title and dates from the HTML my $tmp1 = &extract_meta_tag ($titlemetatag,"$destdirname/$indexfile"); my $tmp2 = &extract_meta_tag ($begindatemetatag,"$destdirname/$indexfile"); my $tmp3 = &extract_meta_tag ($enddatemetatag,"$destdirname/$indexfile"); if (defined($tmp1)) { $dirtitle = $tmp1; } if (defined($tmp2)) { $first = $tmp2; } if (defined($tmp3)) { $last = $tmp3; } # If we found generated files in this dir, flag that we found something # valid to index $object_counter++; $dir_counter++; # Set montage file if we found it if (($do_montage == 1) and ( -r "$destdirname/$thumbnail_dir/$montagefile")) { print "Found montage in $destdirname\n" if defined($opt_debug); $montage = "$destdirname/$thumbnail_dir/$montagefile"; my $image = new Image::Magick; my $retval; $retval = $image->Read(filename=>$montage); warn "$retval" if "$retval"; $montage_x = $image->Get('width'); $montage_y = $image->Get('height'); } # At beginning of row? if ($col_counter == 1) { push(@index, "\n"); push(@details, "\n"); } # Entry for this directory in main & details file push(@index, "\n"); push(@details, "\n"); push(@details, "\n"); if (defined($montage)) { push(@details, "
\n"); } else { push(@details, "
\n"); } if (defined($first)) { my ($tmp_first, $tmp_last); push(@index, "
"); push(@details, "
"); $tmp_first = &format_date ($first, 'index', 'dayonly'); $tmp_last = &format_date ($last, 'index', 'dayonly'); if ($first ne $last) { push(@index, "$tmp_first - $tmp_last"); } else { push(@index, "$tmp_first"); } $tmp_first = &format_date ($first, 'detail', 'dayonly'); $tmp_last = &format_date ($last, 'detail', 'dayonly'); if ($first ne $last) { push(@details, "$tmp_first - $tmp_last"); } else { push(@details, "$tmp_first"); } push(@index, "
\n"); push(@details, "
\n"); } if (defined($montage)) { $anchortext = ""); push(@index, "\n"); push(@index,"
"); push(@index, "$dir"); push(@index,"
\n"); $anchortext = ""); push(@details, ""); push(@details, "
\n"); } else { push(@index,"
"); push(@index, "$dir"); push(@index,"
\n"); } push(@index, "
"); push(@details, "
"); if ($dirtitle ne "") { push(@index, "$dirtitle"); push(@details, "$dirtitle"); } push(@details, "
$dir"); push(@index, "
\n"); push(@details, "
\n"); push(@details,"
\n"); push(@index, "\n"); push(@details, "\n"); # At end of row? if ($col_counter == $current_columns) { push(@index, "\n"); push(@details, "\n"); } # Increment for next item $col_counter++; $col_counter = 1 if ($col_counter > $current_columns); } # if dir had index file } ###################################################################### # # Top of HTML index/detail files # ###################################################################### sub page_header { my $this = shift(@_); my $linkto = shift(@_); my $numlink = 0; my $verstring; select(INDEX); print "\n"; print "\n"; print "\n"; $verstring = &versionstring(); printf ("\n", $verstring); printf ("\n"); if (defined ($write_meta_tag{$titlemetatag})) { print "\n"; } if (defined ($write_meta_tag{$columnsmetatag})) { print "\n"; } if (defined ($write_meta_tag{$thumbxmetatag})) { print "\n"; } if (defined ($write_meta_tag{$thumbymetatag})) { print "\n"; } if (defined ($write_meta_tag{$reversemetatag})) { print "\n"; } if (defined($firstdate)) { print "\n"; } if (defined($lastdate)) { print "\n"; } if (!defined ($opt_includeall) && defined (@opt_exclude) && scalar (@opt_exclude)) { my $tmp = join (',', @opt_exclude); my $etmp; # We need to "encode" this string in the HTML so that raw filenames # (that people should not try to access) are not exposed to the # outside world. # $etmp = &encodestring ($tmp); printf ("\n", $etmp); } printf ("\n", $image_counter); if (defined (@opt_skipmont) && scalar (@opt_skipmont)) { my $tmp = join (',', @opt_skipmont); printf ("\n", $tmp); } print "$current_titletext\n"; print "\n"; print "\n"; print "\n"; # Break out of frames print "\n"; print "

$current_titletext

\n"; print "

"; # On all these links, check to see if the variable is also defined. If # not (done in a .imageindexrc file perhaps) then skip the link if ((-e "$destdir/../$indexfile") and ($do_dirs == 1) and defined($updirtext)) { print "$updirtext"; $numlink++; } if (($do_detail == 1) and ($this eq 'index') and defined($detaillinktext)) { print "  |  " if ($numlink != 0); print "$detaillinktext"; $numlink++; } if (($this eq 'detail') and defined($indexlinktext)) { print "  |  " if ($numlink != 0); print "$indexlinktext"; $numlink++; } if (($do_slide == 1) and ($slide_counter > 1) and defined($framelinktext)) { print "  |  " if ($numlink != 0); print "$framelinktext"; $numlink++; } #age test: print "  |  " if ($numlink != 0); print "$updirtext"; #age ende print "\n
\n" if ($numlink != 0); print "

\n"; if (defined($firstdate) and defined($lastdate)) { my $tmp1 = &format_date($firstdate, $this, 'dayonly'); my $tmp2 = &format_date($lastdate, $this, 'dayonly'); if ($tmp1 ne $tmp2) { if ($current_reverse == 0) { print "

$tmp1 - $tmp2

\n"; } else { print "

$tmp2 - $tmp1

\n"; } } else { print "

$tmp1

\n"; } } print "\n"; select(STDOUT); } ###################################################################### # # Bottom of HTML file # ###################################################################### sub page_footer { my $time = localtime(time); my $progurl = 'http://www.edwinh.org/imageindex/'; select(INDEX); print "
\n"; print "
"; print "
\n"; print "
\n"; print "..erstellt mit imageindex.."; print "
\n"; print "\n\n"; select(STDOUT); } ###################################################################### # # A "quickie" routine to show which files were excluded in a prior run # ###################################################################### sub showexcluded { my ($file) = @_; my ($rfile, $tmp, $utmp, @files, $str); if (! defined ($file)) { if (-r $indexfile) { $rfile = $indexfile; } } else { $rfile = $file; } $tmp = &extract_meta_tag ($excludemetatag, $rfile); if (defined($tmp)) { # We need to "decode" this string as it has been encoded for storage # in the HTML so that raw filenames (that people should not try to # access) are not exposed to the outside world. # $utmp = &decodestring ($tmp); (@files) = split (/,/, $utmp); $str = join (',', @files); printf ("File '$rfile' shows the following record of excluded files:\n"); printf ("%s\n", $str); } else { printf ("File '$rfile' shows no record of excluded files.\n"); } return; } ###################################################################### # # Ignore certain files via META data stored in the index.html file # # Exports global variable %skipmont used later during montage # generation. # ###################################################################### sub exclude_files { my @files = @_; my (@filelist, $f, %exclude, $token, @tokens); undef %exclude; # -skipmont flags override any META data found. Else, look for the META tag # then process. Check to see if any of the -skipmont options were given as # strings of filenames concatenated with ',' characters. If so, support it. # if (defined (@opt_skipmont)) { foreach (@opt_skipmont) { (@tokens) = split (/,/, $_); foreach $token (@tokens) { $skipmont{$token}++; } } } elsif (-r "$destdir/$indexfile") { my $tmp = &extract_meta_tag ($skipmetatag, "$destdir/$indexfile"); if (defined($tmp)) { (@opt_skipmont) = split (/,/, $tmp); my $str = join (',', @opt_skipmont); printf ("Using saved skip-montage files: %s\n", $str); foreach (@opt_skipmont) { $skipmont{$_}++; } } } # -exclude flags override any META data found. Else, look for the META tag # then process. Check to see if any of the -exclude options were given as # strings of filenames concatenated with ',' characters. If so, support it. # if (defined (@opt_exclude)) { # -includeall takes priority over -exclude on the commandline if they are # used together (wierd, but ...) # unless (defined ($opt_includeall)) { foreach (@opt_exclude) { (@tokens) = split (/,/, $_); foreach $token (@tokens) { $exclude{$token}++; } } } } elsif (-r "$destdir/$indexfile") { my $tmp = &extract_meta_tag ($excludemetatag, "$destdir/$indexfile"); my $utmp; if (defined($tmp) && !defined ($opt_includeall)) { # We need to "decode" this string as it has been encoded for storage # in the HTML so that raw filenames (that people should not try to # access) are not exposed to the outside world. # $utmp = &decodestring ($tmp); (@opt_exclude) = split (/,/, $utmp); my $str = join (',', @opt_exclude); printf ("Using saved excluded files: %s\n", $str); foreach (@opt_exclude) { $exclude{$_}++; } } } foreach $f (@files) { if (! $exclude{$f}) { push (@filelist, $f); } else { print "Excluding '$f'\n"; if (-d $f) { chmod (0700, $f); } else { chmod (0600, $f); } } } return (@filelist); } ###################################################################### # # Nuke generated files if original image gone # ###################################################################### sub nuke_out_of_date { foreach my $checkdir ($thumbnail_dir, $med_dir, $slide_dir) { opendir(THUMBS,"$destdir/$checkdir") || die "Can't open dir $checkdir: ($!)\n"; foreach (readdir(THUMBS)) { next if (m/^\.?\.$/); next if (m/$framefile/); next if (m/$slidefile/); next if (m/$montagefile/); next if (m/$emoticonsmile/); next if (m/$emoticonwink/); next if (m/$emoticonfrown/); if (!defined($thumb_backref{"$checkdir/$_"}) and !defined($slide_backref{"$checkdir/$_"}) and !defined($med_backref{"$checkdir/$_"})) { print "Removing stale $destdir/$checkdir/$_\n"; unlink("$destdir/$checkdir/$_") || warn "Can't unlink $destdir/$checkdir/$_: ($!)\n"; $modified_thumb++; } } closedir(THUMBS); } } ###################################################################### # # Convert bytes to kb string # ###################################################################### sub convert_to_kb { my $bytes = shift(@_); $bytes = sprintf("%dk", $bytes / 1024); return($bytes); } ###################################################################### # # Sortq by integer date stamp # ###################################################################### sub bynumber { if ($current_reverse == 0) { $a <=> $b; } else { $b <=> $a; } } ###################################################################### # # Write frameset file for slideshows # ###################################################################### sub write_frameset { # This is impossible to get rid of my $framefudge = 35; my $verstring; open(FRAME,">$destdir/$slide_dir/$framefile") or die ("Can't open $destdir/$slide_dir/$framefile: $!\n"); select(FRAME); print "\n"; print "\n"; print "\n"; $verstring = &versionstring(); printf ("\n", $verstring); printf ("\n"); print ""; print "$current_titletext\n"; print "\n"; print "\n"; if ($frame_orient eq 'horizontal') { printf("\n", $max_thumb_y + $framefudge); } else { printf("\n", $max_thumb_x + $framefudge); } print "\n"; print "\n"; print "No frames in this browser...go back\n"; print "\n"; print "\n"; select(STDOUT); close (FRAME); open(FRAME,">$destdir/$slide_dir/$slidefile") or die ("Can't open $destdir/$slide_dir/$slidefile: $!\n"); select(FRAME); print "\n"; print "\n"; print "\n"; $verstring = &versionstring(); printf ("\n", $verstring); printf ("\n"); print "\n"; print "$current_titletext\n"; print "\n\n"; print "\n" if ($frame_orient eq 'horizontal'); foreach (@frame) { print; } print " \n" if ($frame_orient eq 'horizontal'); print "
\n"; print "\n\n"; select(STDOUT); close(FRAME); } ###################################################################### # # Do next/index/prev links on slide pages # ###################################################################### sub next_prev_links { my $pathname = shift(@_); print "
"; if (defined($back{$pathname})) { print "< previous | "; } else { print "< previous | "; } print "index"; if (defined($forward{$pathname})) { print " | next >"; } else { print " | next >"; } print "
\n"; } ###################################################################### # # Lower-case all the filenames. I hate the uppercase filenames that come # from my camera's default software (and Windud software). Plus I didn't # want this "utility" in another script, so just place it here. # ###################################################################### sub lower_case_files { my (@files) = @_; my ($newfile, $lowername); foreach $name (@files) { ($lowername = $name) =~ tr/A-Z/a-z/; if ($name =~ /[A-Z]/) { print "Moving '$name' to '$lowername'\n"; move("$name","$lowername"); } } } ###################################################################### # # extract the NAME tag from an HTML file # ###################################################################### sub extract_meta_tag { my ($tag, $filename) = @_; my ($name, $content, $retval); if (! (open (FILE, $filename))) { print STDERR "Cannot open '$filename' for reading - $!\n"; return (0); } # # while () { if (//) { $name = $1; $content = $2; if ($name eq $tag) { $retval = $content; last; } } } close (FILE); return ($retval); } ############################################################################### # # Rotate given image 90 degrees # ############################################################################### sub rotate_image { my $file = shift(@_); my $argv = shift(@_); if ($file =~ m/^(cw|ccw)$/) { # If file is cw or ccw, # assume the args were given backwards my $tmp = $file; $file = $$argv[0]; $$argv[0] = $tmp; } -r "$file" || die("$file: ", $!); -w "$file" || die("$file: ", $!); # grab the mtime of the file so we can reset it after we update it my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime, $ctime,$blksize,$blocks) = stat($file); my $posix_mtime = POSIX::strftime ("%Y%m%d%H%M.%S",localtime($mtime)); my ($name,$path,$suffix) = fileparse($file,'\.\S+'); my $thumb; my $medium; my $image = new Image::Magick; my $retval = $image->Read("$file"); warn "$retval" if "$retval"; if (!defined($$argv[0]) or ($$argv[0] !~ m/^cc?w$/i)) { print "Need 'cw' or 'ccw' argument to rotate image clockwise/counterclockwise\n"; exit(1); } if ($$argv[0] =~ /^ccw$/i) { $deg = -90; } else { $deg = 90; } print "Rotating $file $deg degrees\n"; $retval = $image->Rotate($deg); warn "$retval" if "$retval"; $retval = $image->Write(filename=>"$file"); warn "$retval" if "$retval"; system ("touch -t $posix_mtime $file"); # Nuke the generated images if they exist # (touching the timestamp above breaks automatic regeneration logic) if ($image->Get('format') =~ /JFIF/i) { $thumb = $path . "$thumbnail_dir/$name" . $suffix; $medium = $path . "$med_dir/$name" . $suffix; } else { $thumb = $path . "$thumbnail_dir/$name.jpg"; $medium = $path . "$med_dir/$name.jpg"; } unlink($thumb) if (-e "$thumb"); unlink($medium) if (-e "$medium"); } ############################################################################### # # Set or display caption for a particular image # ############################################################################### sub caption_image { my $file = shift(@_); my $argv = shift(@_); my ($esc_comment, $tmpfile); -r "$file" || die("$file: ", $!); # grab the mtime of the file so we can reset it after we update it my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime, $ctime,$blksize,$blocks) = stat($file); my $posix_mtime = POSIX::strftime ("%Y%m%d%H%M.%S",localtime($mtime)); my $image = new Image::Magick; my $retval = $image->Read("$file"); warn "$retval" if "$retval"; my $format = $image->Get('format'); warn "$retval" if "$retval"; # Set caption if another arg is present, or just display it if (defined($$argv[0])) { -w "$file" || die("$file: ", $!); # Try to find wrjpgcom so we can use it for adding captions to JPG images my $wrjpgcom_prog = &find_in_path ('wrjpgcom'); my $quote_file = quotemeta ($file); # If a jpeg file and we found a wrjpgcom program in our path, use # it! It simply puts the comment in the JPEG header without reading # (uncompressing) and writing (re-compressing) the file out so # there is no chance for data loss. # if (($format =~ /JFIF/i) and defined($wrjpgcom_prog)) { $tmpfile = "$file.$$"; my $tmpfile_quote = "$quote_file.$$"; $esc_comment = quotemeta ($$argv[0]); # FIXME # check to see how '?' and other punctuation is escaped and fix # it seems things are not correct. system ("$wrjpgcom_prog -replace -comment $esc_comment $quote_file > $tmpfile_quote"); if (($? >> 8) != 0) { printf(STDERR "Error in creating JPEG comment with 'wrjpgcom'. Leaving existing file intact.\n"); } else { move($tmpfile, $file); } } else { # Fall back to PerlMagick's routines. # $retval = $image->Comment("$$argv[0]"); warn "$retval" if "$retval"; $retval = $image->Write(filename=>"$file", quality=>"95", sampling_factor=>"1x1"); warn "$retval" if "$retval"; } system ("touch -t $posix_mtime $quote_file"); } else { my $text = $image->Get('comment'); if (defined($text)) { print "$file: \"$text\"\n"; } else { print "$file: (no caption)\n"; } } } ############################################################################### # # Print usage info from top of file # ############################################################################### sub usage { open(FILE,"$0") or die "Can't open $0: $OS_ERROR"; while() { last if (m/^\#\s+USAGE:/); } while() { last if (m/^\#\#\#\#\#\#\#/); s/^\# ?//; print; } close(FILE); } ###################################################################### # # Format timestamp for HTML pages. This routine assumes that the date # given to it is in YYYYMMDDHHMM.SS format that we've created from the # EXIF/mtime date using strftime(). # ###################################################################### sub format_date { my ($date, $context, $dayonly) = @_; my ($timeformat, $dateformat); if ($context eq 'frame') { $timeformat = $frametimeformat; $dateformat = $framedateformat; } elsif ($context eq 'index') { $timeformat = $indextimeformat; $dateformat = $indexdateformat; } elsif ($context eq 'slide') { $timeformat = $slidetimeformat; $dateformat = $slidedateformat; } else { $timeformat = $detailtimeformat; $dateformat = $detaildateformat; } # Replace "macro" patterns in the format string first # $timeformat =~ s/\%R/\%H:\%M/g; $timeformat =~ s/\%r/\%I:\%M:\%S \%p/g; $dateformat =~ s/\%F/\%Y-\%m-\%d/g; $dateformat =~ s/\%D/\%m\/\%d\/\%y/g; $date =~ /(\d\d\d\d)(\d\d)(\d\d)(\d\d)?(\d\d)?\.?(\d\d)?/; my $year = $1; my $month = $2; my $day = $3; my $hour = $4; my $min = $5; my $sec = $6; my ($ampm, $two_digit_year, $twelve_hour); if ($year =~ /^\d\d(\d\d)$/) { $two_digit_year = $1; } else { $two_digit_year = '??'; # shouldn't ever been seen } # If we're told to, only format a date with no time # if (defined ($dayonly)) { $dateformat =~ s/\%Y/$year/g; $dateformat =~ s/\%y/$two_digit_year/g; $dateformat =~ s/\%m/$month/g; $dateformat =~ s/\%d/$day/g; $dateformat =~ s/\%\%/\%/g; return ($dateformat); } else { if (defined($hour)) { $twelve_hour = $hour; $ampm = 'AM'; if ($hour > 12) { $twelve_hour -= 12; $ampm = 'PM'; } } else { $hour = '??'; $twelve_hour = '??'; $ampm = '??'; #again, should never be seen } if (! defined ($min)) { $min = '??'; } if (! defined ($sec)) { $sec = '??'; } $dateformat =~ s/\%Y/$year/g; $dateformat =~ s/\%y/$two_digit_year/g; $dateformat =~ s/\%m/$month/g; $dateformat =~ s/\%d/$day/g; $dateformat =~ s/\%\%/\%/g; $timeformat =~ s/\%S/$sec/g; $timeformat =~ s/\%M/$min/g; $timeformat =~ s/\%I/$twelve_hour/g; $timeformat =~ s/\%H/$hour/g; $timeformat =~ s/\%p/$ampm/g; $timeformat =~ s/\%\%/\%/g; return("$dateformat $timeformat"); } } ###################################################################### # # Return version string from CVS tag # ###################################################################### sub versionstring { my $ver = ' $Name: v1_0_7 $ '; $ver =~ s/Name//g; $ver =~ s/[:\$]//g; $ver =~ s/\s+//g; $ver =~ s/^v//g; $ver =~ s/_/\./g; if ($ver eq '') { $ver = "cvs devel - " . '$Revision: 1.164 $ '; # Nuke the $ signs -- what if somebody is keeping pages under RCS # or CVS control? $ver =~ s/\$//g; $ver =~ s/\s*$//; } return($ver); } ############################################################################### # # Create CSS file that is shared among the HTML pages # ############################################################################### sub write_css { open(CSS,">$destdir/$stylefile") or die ("Can't open $destdir/$stylefile: $!\n"); select(CSS); print $stylesheet; select(STDOUT); close(CSS); } ############################################################################### # # "Interpolate" %? escapes found in our printf-like strings defined for the # TITLE attributes. See the beginning of this file for their definition # ############################################################################### sub interpolate_title_string { my ($formatstring, $pathname, $context) = @_; my ($filename, $date, $size, $resolution, $caption); my ($tmp); $filename = $info{$pathname}{'file'}; $date = &format_date ($info{$pathname}{'date'}, $context); $size = $info{$pathname}{'size'}; $resolution = $info{$pathname}{'geometry'}; $caption = $info{$pathname}{'comment'}; if (! defined ($caption)) { $caption = ''; } $tmp = $formatstring; $tmp =~ s/\%f/$filename/g if $filename; $tmp =~ s/\%d/$date/g if $date; $tmp =~ s/\%s/$size/g if $size; $tmp =~ s/\%r/$resolution/g if $resolution; $tmp =~ s/\%c/$caption/g; $tmp =~ s/\%\%/%/g; # In case the format string has " marks in it, change all those to '. # The " marks are needed to mark the argument to the TITLE attribute. # $tmp =~ s/\"/\'/g; return ($tmp); } ############################################################################### # # "Interpolate" %? escapes found in our printf-like strings defined for the # TITLE attributes. However, the %? escapes for this function are based on what # you could conceivably need when processing a directory. # # See the beginning of this file for their definition # ############################################################################### sub interpolate_title_string_dir { my ($formatstring, $dir, $context) = @_; my ($tmp, $num, $date, $metadate, $metatitle); $tmp = $formatstring; $num = &extract_meta_tag($numimagesmetatag, "$srcdir/$dir/$indexfile"); # If we plucked out the number of images from the metadata of a directory's # index.html file, replace it. Else, give a warning if we didn't find it but # somebody still used %n # if (defined ($num)) { $tmp =~ s/\%n/$num/g if $num; } else { if ($tmp =~ /\%n/) { if (!defined ($remember_warning{$dir})) { printf (STDERR "Warning: %%n escape used in format string and %s META tag not found in %s. Re-run imageindex in '$dir'.\n", $numimagesmetatag, "$srcdir/$dir/$indexfile"); $remember_warning{$dir}++; } } } $metadate = &extract_meta_tag($begindatemetatag, "$srcdir/$dir/$indexfile"); $date = &format_date ($metadate, $context, 'dayonly'); $tmp =~ s/\%b/$date/g if $date; $metadate = &extract_meta_tag($enddatemetatag, "$srcdir/$dir/$indexfile"); $date = &format_date ($metadate, $context, 'dayonly'); $tmp =~ s/\%e/$date/g if $date; $metatitle = &extract_meta_tag($titlemetatag, "$srcdir/$dir/$indexfile"); $tmp =~ s/\%t/$metatitle/g if $metatitle; # In case the format string has " marks in it, change all those to '. # The " marks are needed to mark the argument to the TITLE attribute. # $tmp =~ s/\"/\'/g; return ($tmp); } ############################################################################### # # Look for external programs we depend on in the $PATH. It just finds the first # occurence of $prog in $PATH. # ############################################################################### sub find_in_path { my ($prog) = @_; my ($retval); undef $retval; foreach $dir (split (/:/, $ENV{'PATH'})) { if (-r "$dir/$prog" && -x "$dir/$prog") { $retval = "$dir/$prog"; } } return ($retval); } ############################################################################### # # Encode/decode routines for exclude filenames when stuffed in a meta tag # ############################################################################### sub encodestring { my ($tmp) = @_; my $etmp; $etmp = pack ("u*", $tmp); # Hack the string to get rid of \n chars so we can store it on 1 line $etmp =~ s/\n/..1xn!_ltr../g; # Get rid of ampersands $etmp =~ s/\&/..xn!_ltr1../g; # Get rid of double-quotes $etmp =~ s/\"/..sb!_lho1../g; return ($etmp); } sub decodestring { my ($tmp) = @_; my $utmp; # Unhack the string to bring back & characters $tmp =~ s/\.\.sb\!_lho1\.\./\"/g; $tmp =~ s/\.\.xn\!_ltr1\.\./\&/g; # Unhack the string to bring back & characters $tmp =~ s/\.\.xn\!_ltr1\.\./\&/g; # Unhack the string to bring back \n characters $tmp =~ s/\.\.1xn\!_ltr\.\./\n/g; $utmp = unpack ("u*", $tmp); return ($utmp); } ############################################################################# # # This routine samples linearly (as possible) across the available files in # a directory. The first pass at sampling is a simple modulo function based # upon the ratio of files to the number of tiles we can use in the montage. # If that first pass sample did not produce enough files, then we go back # iteratively through the list and as evenly-as-possible select unused # files from those left in the pool. # ############################################################################# sub sample_files_for_montage { my (@files) = @_; my ($numdiv, $numchosen, $chunksize, $numfiles, $numleft); my ($i, $index, $f, @ret); $numfiles = scalar (@files); $numdiv = sprintf ("%d", $numfiles / $montage_max); $numdiv++; for ($i = 0; $i < $numfiles; $i++) { if (($i % $numdiv) == 0) { $chosen{$files[$i]}++; } } $numchosen = scalar (keys %chosen); $numleft = $montage_max - $numchosen; if ($numleft) { $chunksize = sprintf ("%d", $numfiles / $numleft); $index = 0; for ($i = 0; $i < $numleft; $i++) { &mark_next_file_for_montage ($index + 1, $numfiles, @files); $index = $index + $chunksize; } } foreach $f (@files) { if ($chosen{$f}) { push (@ret, $f); } } return (@ret); } ############################################################################# # # cycle through the given list of files. If the list[$index] is already marked # (via the global hash %chosen) then move onto the next one, etc. # ############################################################################# sub mark_next_file_for_montage { my ($index, $numfiles, @files) = @_; my ($i); for ($i = $index; $i < $numfiles; $i++) { if (! $chosen{$files[$i]}) { $chosen{$files[$i]}++; last; } } } ############################################################################### # # Exclude certain filenames from the list of thumbnails to be used in the # montage image. # ############################################################################### sub exclude_montage_files { my (@files) = @_; my (@tmp, $file); foreach (@files) { $file = basename ($_); unless (defined ($skipmont{$file})) { push (@tmp, $_); } } return (@tmp); } ############################################################################### # # "html-ize" a caption found in an image. Just in case there are certain # characters used which we want to "escape." # ############################################################################### sub htmlize_caption { my ($caption, $slide) = @_; $caption =~ s/\&/\&/g; $caption =~ s/\/\>/g; $caption =~ s/\"/\"/g; # Help smiley's render in a "mo-better" way when they are at the end of a # caption and enclosed in parens # if ($caption =~ /(:\-?[\(\)])\s*\)\s*$/) { my $tmp = $1; $caption =~ s/:\-?[\(\)]\s*\)\s*$/$tmp\ \)/; } $caption = &emoticonify ($caption, $slide); return ($caption); } ############################################################################### # # Translate ASCII smiley's embedded into image captions into emoticons # ############################################################################### sub emoticonify { my ($caption, $slide) = @_; my ($thumbdir, $attr); return ($caption) if (! defined ($do_emoticons) || $do_emoticons == 0); # This is a hack, please ignore and move on ... nothing to see here. # $caption =~ s/\ /NoNBrEaKaBleSpacE/g; $thumbdir = $thumbnail_dir; if ($slide) { $thumbdir = '../' . $thumbdir; } $attr = 'STYLE="vertical-align: middle;" WIDTH="19" HEIGHT="19"'; if ($caption =~ s/:\-?\)/\\"/g) { $emoticon{'smile'}++; } if ($caption =~ s/;\-?\)/\\"/g) { $emoticon{'wink'}++; } if ($caption =~ s/:\-?\(/\\"/g) { $emoticon{'frown'}++; } # Undo the hack # $caption =~ s/NoNBrEaKaBleSpacE/\ /g; return ($caption); } ############################################################################### # # Write out PNG files representing the emoticons # ############################################################################### sub write_emoticon_png { my ($type) = @_; my ($img); if (! open (IMG, '>' . $destdir . '/' . $thumbnail_dir . "/$emoticonprefix" . $icon . ".png")) { printf (STDERR "Could not open emoticon file for '$icon' for writine - $!\n"); return; } # UUDecode the small PNG files that represent the emoticons and dump them to # the appropriate files in $thumbnail_dir # $img = unpack ("u*", $png{$type}); print IMG $img; close (IMG); } ############################################################################### # # Create a montage of images in the current directory. This image will be # pointed to by the parent directory's index.html file to show a sort of # "thumbnail preview" of the contents of this directory. # ############################################################################### sub create_montage { my @files = @_; my (@modfiles); @files = &exclude_montage_files (@files); foreach (@files) { push (@modfiles, quotemeta ($_)); } # If we have defined that a lesser number of "tiles" can be used in the # montage vs. the # of files in this directory, then we'll "sample" the # files as evenly as possible to avoid clustering of shots that might be # similar to each other. # if (scalar (@modfiles) > $montage_max) { @modfiles = &sample_files_for_montage (@modfiles); } if ($do_montage == 1) { if (($modified_thumb != 0) or (! -e "$destdir/$thumbnail_dir/$montagefile")) { my $number = $#modfiles + 1; my $tile_x = 1;; my $tile_y = 1; # FIXME these both blindly expand x before expanding y # Should this depend on some aspect ratio? while(($tile_x * $tile_y) < $montage_min) { $tile_x++; $tile_y++ if (($tile_x * $tile_y) < $montage_min); } while(($tile_x * $tile_y) < $number) { $tile_x++; $tile_y++ if (($tile_x * $tile_y) < $number); } my $index = 0; while (($#modfiles + 1) < ($tile_x * $tile_y)) { if ($montage_fill eq 'blank') { push(@modfiles, "NULL:"); } else { push(@modfiles, $modfiles[$index]); $index = ($index+1) % $number; } } my $tile = sprintf("%dx%d", $tile_x, $tile_y); my $geom = sprintf("%dx%d", $max_mont_thumb_x, $max_mont_thumb_y); my $newgeom = sprintf("%dx%d", $current_thumbnail_x, $current_thumbnail_y); print "Picked $tile array of $geom for montage\n" if ($opt_debug); print "Creating $destdir/$thumbnail_dir/$montagefile\n"; system("montage -quality $thumb_quality -bordercolor white -transparent white -borderwidth $montage_whitespace -geometry $geom -tile $tile @modfiles $destdir/$thumbnail_dir/$montagefile"); if (($? >> 8) != 0) { printf(STDERR "Error in creating montage file\n"); return(-1); } # Resize to std. thumbnail my $image = new Image::Magick; my $retval; $retval = $image->Read(filename=>"$destdir/$thumbnail_dir/$montagefile"); warn "$retval" if "$retval"; $retval = $image->Resize(geometry=>$newgeom); warn "$retval" if "$retval"; $retval = $image->Set(interlace=>Line); warn "$retval" if "$retval"; $retval = $image->Write(filename=>"$destdir/$thumbnail_dir/$montagefile"); warn "$retval" if "$retval"; } } else { unlink("$destdir/$thumbnail_dir/$montagefile") if (-e "$destdir/$thumbnail_dir/$montagefile"); } } sub read_stored_meta_data { my ($tmp); if (-r "$destdir/$indexfile") { $tmp = &extract_meta_tag ($columnsmetatag, "$destdir/$indexfile"); # If we found data, check it against program defaults if (defined ($tmp)) { $current_columns = $tmp; print "Using saved number of columns: $current_columns\n" if ! defined ($opt_columns); } $tmp = &extract_meta_tag ($titlemetatag, "$destdir/$indexfile"); # If we found data, check it against program defaults if (defined ($tmp)) { $current_titletext = $tmp; print "Using saved title: $current_titletext\n" if ! defined ($opt_title); } $tmp = &extract_meta_tag ($thumbxmetatag, "$destdir/$indexfile"); # If we found data, check it against program defaults if (defined ($tmp)) { $current_thumbnail_x = $tmp; print "Using saved thumbnail X size: $current_thumbnail_x\n" if ! defined ($opt_x); } $tmp = &extract_meta_tag ($thumbymetatag, "$destdir/$indexfile"); # If we found data, check it against program defaults if (defined ($tmp)) { $current_thumbnail_y = $tmp; print "Using saved thumbnail Y size: $current_thumbnail_y\n" if ! defined ($opt_y); } $tmp = &extract_meta_tag ($reversemetatag, "$destdir/$indexfile"); # If we found data, check it against program defaults if (defined ($tmp)) { $current_reverse = $tmp; print "Using saved reverse: $current_reverse\n" if ! defined ($opt_reverse); } &decide_which_md_to_store(); } } sub override_by_commandline { if (defined($opt_columns)) { $current_columns = $opt_columns; } if (defined($opt_title)) { $current_titletext = $opt_title; } if (defined($opt_reverse)) { $current_reverse = $opt_reverse; } if (defined($opt_x)) { $current_thumbnail_x = $opt_x; if ($current_thumbnail_x != $default_thumbnail_x) { $opt_forceregen = 1; } } if (defined($opt_y)) { $current_thumbnail_y = $opt_y; if ($current_thumbnail_y != $default_thumbnail_y) { $opt_forceregen = 1; } } &decide_which_md_to_store(); } sub decide_which_md_to_store { if ($current_columns != $default_columns) { $write_meta_tag{$columnsmetatag}++; } else { undef $write_meta_tag{$columnsmetatag}; } if ($current_thumbnail_x != $default_thumbnail_x) { $write_meta_tag{$thumbxmetatag}++; } else { undef $write_meta_tag{$thumbxmetatag}; } if ($current_thumbnail_y != $default_thumbnail_y) { $write_meta_tag{$thumbymetatag}++; } else { undef $write_meta_tag{$thumbymetatag}; } if ($current_titletext ne $default_titletext) { $write_meta_tag{$titlemetatag}++; } else { undef $write_meta_tag{$titlemetatag}; } if ($current_reverse ne $do_reverse) { $write_meta_tag{$reversemetatag}++; } else { undef $write_meta_tag{$reversemetatag}; } } sub initialize_current_vars { $current_columns = $default_columns; $current_titletext = $default_titletext; $current_thumbnail_x = $default_thumbnail_x; $current_thumbnail_y = $default_thumbnail_y; $current_reverse = $do_reverse; } ############################################################################## # # Just initialize the 'png' array with UUENCODED PNG files for emoticons. This # was placed down here so as not to clutter up the top of the file where the # other globals are initialized. # ############################################################################## sub init_png_array { $png{'wink'} = <<'EOF'; MB5!.1PT*&@H````-24A$4@```!,````3!`,```"`?BO_````$E!,5$7____, MS``S,P#___\```#__P!/FRMM`````7123E,`0.;89@````%B2T=$!?AOZ<<` M``!F241!5'C:;8_1#8`P"$3O@Q'L!CJ!#M`FQP`FL/\J%FJ-)O+U:R.N?=+?A#36P6)H9!(F)Z CI1BYC1+=-K2?9J^^SQ<7J48K([9O4:P`````245.1*Y"8((` ` EOF $png{'smile'} = <<'EOF'; MB5!.1PT*&@H````-24A$4@```!,````3!`,```"`?BO_````$E!,5$7__P#, MS`!F9@#_,P````#___]]YKD%````!G123E/______P"SOZ2_`````6)+1T0% M^&_IQP```&U)1$%4>-I-C\$-P"`,`UV)`?I@@#ZZ03M`*L&?!]E_E=H$(5`> MEP39#MR]E%+=&T@GD*NPD\A"PWBU@4,VADQ$*J,:OHF'<14(BX]14R#0%J1+ M>!.I(&,I=(!Q3+IT2\\+N2D#A\JP)]ORKBM^`[0;1*VK3]P`````245.1*Y" "8((` ` EOF $png{'frown'} = <<'EOF'; MB5!.1PT*&@H````-24A$4@```!,````3!`,```"`?BO_````'E!,5$7____, MS`"9F0!F9@`S,P#,S#/___\S,S,```#__P`[/ZS;`````7123E,`0.;89@`` M``%B2T=$"?'9I>P```"$241!5'C:78^Q#<,P#`19:@1I`WN!`%G`@`<(8"T0 M>`.GE-R8*EV%OVU(24YA@L7A^>"31$3,G*@6!\!7=D$@\(8%!=K)Q&/#]U-4 M=AC>\;&.0I0A:YQVG$FM)\$;-N<-TJEM;0 @UQOZ@DOVMWO_7_P`Y9]*.M\PG><`````245.1*Y"8((` ` EOF }