#!/usr/local/bin/perl5
# gallery.pl: picture gallery generator
my($version) = "1.1.25";
#
# (c) 2002-2005 by Mihai Budiu, mihaib from cs.cmu.edu
# http://www.cs.cmu.edu/~mihaib/ftp/gallery.pl
# see http://cobra.crcl.cs.cmu.edu:8000/favorites for a sample gallery

# usage: just run in a directory containing pictures, twice
# This version works somewhat on windows as well

# needs: - perl
#        - ImageMagick http://imagemagick.org
#        - jhead       http://www.sentex.net/~mwandel/jhead (optional) 
#          (jhead extracts photo information from jpeg files)

# TODO
#      - take care not to delete any user files
#      - make a button to resize the web page to "ideal" dimensions
#      - option to create a separate directory hierarchy
#      - print on top the position in the hierarchy
#      - handle gracefully missing files
#      - BUG: if you use '*' commands in verbatim the program gets confused
#      - print the resolution next to "hi-res"

use strict;

RESTART:

# feel free to edit the copyright, or to replace the generated file
my($copyright) = 'These pictures are (c) 2002 by Mihai Budiu.
However, you can use them freely if you like them.
';

# configuration variables; these can be changed through command-line options too
my(%image_suffixes) = ("jpg" => 1, "gif" => 1, "jpeg" => 1, "png" => 1, "tif" => 1);
my(%options) = (
		imgsizedescr => 0,              # add image size information to each pic
		copyright => 0,	                # print copyright
		thumbx => 128,
		thumby => 128,
		picx => 800,
		picy => 700,
		recurse => 0,	                # descend directories recursively
		master_title => "Web album",    # title of master page
		useexif => 0,                   # exif is image info embedded jpegs
		havejhead => 1,	                # jhead program is available (to read exif file info)
		style => 'plain',		# default style
		cleanup => 0,	                # delete all byproducts of gallery.pl (but not descriptions.txt)
		sharpen => "-sharpen 60",	# default sharpen arguments (for old ImageMagick)
		verbose => 0,
		sortdate => 0,	                # sort pics on date when generating descriptions.txt
		nooriginal => 0,                # do not create a link to the original large image
		rows => 0,
		sanitize => 0,	                # fix image names containing funny characters
		unused => 0,	                # list image files not mentioned in descriptions.txt
		cleanunused => 0,               # remove unused image files and their derivatives
		addnewpics => 0,                # add new images at the end of descriptions.txt
		sametitle => 0,	                # all pages should have the same title 
		nocaption => 0,	                # don't put captions to figures
		skip => "",	                # don't descend these directories
		onepic => "",                   # only reprocess this picture
		reconstruct => 0,               # rebuild from thumb_ and v_, without originals
		);

my(%page) = (			# table representing a html page
	 columns => 3,		# number of columns
	 crt_col => 0,		# current column
	 started => 0,		# started printing
	 pic_cols => 1,         # columns for next picture
         crt_pic => 1,          # current picture number (do not initialize)
	 skipping => 0,
	 crt_sec => 1,		# current section number
	 printed_title => 0,    # title has already been printed
	 filename => "",		
	 inverbatim => 0	# processing 'verbatim' command
);

my(@styles) = ('simple', 'frame', 'plain');	# legal styles
my($commandline) = "gallery.pl " . join(' ', @ARGV); # save it here
my($sysname) = $^O;
print "Running on $sysname\n";
my($login) = "Windows user";
if (! ($sysname =~ /Win/)) {
    $login = getpwuid($<) || "?";
}
my($today) = &getdate();

# parse command line arguments

sub usage {
    print << "EOO";
gallery.pl version $version

  gallery.pl creates a 'descriptions.txt' file if none exists
  gallery.pl uses the commands in 'descriptions.txt' to create HTML pages with pictures

Usage: gallery.pl [options]

The recognized options are:
           -r          - descend recursively subdirectories
           -v          - verbose
           -suff suffix- this suffix indicates image files too
           -rerun      - use exact same arguments as last time
           -skip dirs  - don't descend these dirs (comma-separated list)
           -onepic pic - recreate thumbnails just for this picture
           -clean      - remove all gallery.pl-generated files (but not descriptions.txt)
           -h, -help   - print this help and exit
     layout options
           -tx pixels  - max x size of thumbnails (default $options{thumbx})
           -ty pixels  - max y size of thumbnails (default $options{thumby})
           -x  pixels  - max x size of pics (default $options{picx})
           -y  pixels  - max y size of pics (default $options{picy})
           -columns n  - columns in each page (default $page{columns})
           -rows n     - up to this many table rows per thumbnail page (default unbounded)
           -info       - read EXIF info from pictures
           -noorig     - do not link to original image
           -style s    - use this style (see below)
           -sanitize   - sanitize url extracted from pic name (remove funny chars)
           -imgsz      - embed images size information in html (looks better during download)
           -copyright  - print copyright notice on generated pages
           -nocaptions - generate empty captions for pictures
     descriptions.txt options (i.e. which don't generate HTML)
           -unused     - list pictures not mentioned in descriptions.txt
           -cleanunused- remove pictures not mentioned and thumbnails
           -addnewpics - append new pictures to descriptions.txt
	   -sortdate   - sort pictures in descriptions.txt on date (default is on name)
Legal descriptions.txt lines (special commands start * in first column):
           * title    <page title>
	   * section  <name of section>
	   * columns  <number of columns for next image>
	   * titlepic <image representing this directory>
	   * file     <image file>
	   * skip
           * pagebreak
           * verbatim
             Everything between these two commands is literally copied in the HTML file
	   * endverbatim
	   Text with no * is considered caption for previous image.
The html table of contents can be in one of these styles (-style argument):
EOO
    foreach (@styles) {
	print STDERR "\t$_\n";
    }
    exit 1;
}

while ($ARGV[0] =~ /^[-]/) {
    $_ = $ARGV[0];
  OPTION:
    {
	/^-tx$/ &&          ($options{thumbx} = $ARGV[1], shift, last OPTION);
	/^-ty$/ &&          ($options{thumby} = $ARGV[1], shift, last OPTION);
	/^-onetitle$/ &&    ($options{samtitle} = 1, last OPTION);
	/^-x$/ &&           ($options{picx} = $ARGV[1], shift, last OPTION);
	/^-y$/ &&           ($options{picy} = $ARGV[1], shift, last OPTION);
	/^-columns$/ &&     ($page{columns} = $ARGV[1], shift, last OPTION);
	/^-r$/ &&           ($options{recurse} = 1, last OPTION);
	/^-suff$/ &&        ($image_suffixes{$ARGV[1]} = 1, shift, last OPTION);
	/^-info$/ &&        ($options{useexif} = 1, last OPTION);
	/^-style$/ &&       ($options{style} = $ARGV[1], shift, last OPTION);
	/^-v$/ &&           ($options{verbose} = 1, last OPTION);
	/^-noorig$/ &&      ($options{nooriginal} = 1, last OPTION);
	/^-sanitize$/&&     ($options{sanitize} = 1, last OPTION);
	/^-clean$/ &&       ($options{cleanup} = 1, last OPTION);
	/^-rows$/ &&        ($options{rows} = $ARGV[1], shift, last OPTION);
	/^-unused$/ &&      ($options{unused} = 1, last OPTION);
	/^-cleanunused$/ && ($options{cleanunused} = 1, last OPTION);
	/^-addnewpics$/ &&  ($options{addnewpics} = 1, last OPTION);
	/^-nocaptions$/ &&  ($options{nocaption} = 1, last OPTION);
	/^-imgsz$/ &&       ($options{imgsizedescr} = 1, last OPTION);
	/^-sortdate$/ &&    ($options{sortdate} = 1, last OPTION);
	/^-copyright$/ &&   ($options{copyright} = 1, last OPTION);
	/^-rerun$/    &&    (@ARGV = &reset_argv(), print join('', @ARGV), goto RESTART);
	/^-skip$/ &&        ($options{skip} = $ARGV[1], shift, last OPTION);
	/^-reconstruct$/ && ($options{reconstruct} = 1, last OPTION);
	/^-onepic$/ &&      ($options{onepic} = $ARGV[1], shift, last OPTION);
	/^-he?l?p?$/ &&     (&usage(), shift, last OPTION);
	 print STDERR << "EOI";
gallery.pl version $version: illegal flag '$_'

  gallery.pl creates a 'descriptions.txt' file if none exists
  gallery.pl uses the commands in 'descriptions.txt' to create HTML pages with pictures

Quick usage: 
  (1) make a backup copy of the directory with images
  (2) run gallery.pl to generate descriptions.txt
  (3) edit descriptions.txt file to your taste
  (4) run gallery.pl again to generate html

  Use -h to get a detailed help
EOI
	exit 1;
    }
    shift(@ARGV);
}

# globals
my(%crtpage) = %page;
my(%masterpage) = %page;	# master index page

my($totpics)=0;			# total pictures
my($depth)=0;			# for recursive directory traversals
my(@crtdir) = ();               # stack of directories in depth-first traversal
my(@pics) = ();			# pictures in current directory
my(@secs) = ();			# sections in current directory
my(@captions) = ();		# image captions
my(@secno) = ();		# line where each section begins
my(@secpage) = ();		# for each section, thumbnail page where it resides
my($crtpage, $totpages) = (0,0);# current thumbnail page, and total #
my($lasttitle) = "";		# last title printed
# global file descriptors: 
#  - I for index.html
#  - MASTER_INDEX for recursive traversals
#  - IMG for the current image

&check_programs(); 		# find out which programs aren't installed

my($header) = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
 <title>$title</title>
 <meta name="generator" content="gallery.pl, http://www.cs.cmu.edu/~mihaib/ftp/gallery.pl">
 <meta http-equiv="Content-Type" content="text/html">
 <link rel="StyleSheet" href="gallery.css" type="text/css">
</head>

<!-- Automatically created on $today by $login with gallery.pl v$version
     http://www.cs.cmu.edu/~mihaib/ftp/gallery.pl,
     written by Mihai Budiu, mihaib+gallery at cs.cmu.edu -->

<!-- command line: $commandline -->

<body>
';

my($title_header) = ' <center>
 <h1>$title</h1>
 </center>

';

my($trailer) = "
</body>
</html>
";

my($css) = "
/* gallery.css -- default gallery.pl style sheet; please edit to your own taste */

BODY, P, TD, TH, UL, OL, H1, H2, H3, H4, PRE, SMALL, BIG, A, TABLE, TD, TR, TH  {
        font-family : Helvetica, sans-serif;
        background-color : black;
        color : white;
}

DD  { margin: 1em; }

A:LINK  {
        text-decoration : underline;
        color : white;
}

A:VISITED  {
        text-decoration : underline;
        color : #eee;
}

A:HOVER  {
        text-decoration : underline;
        color : green;
}
";

##################################

sub reset_argv {
    # look for command line arguments in master_index.html or index.html
    my $searchin;
    if (-f "master_index.html") {
	$searchin = "master_index.html";
    }
    elsif (-f "index.html") {
	$searchin = "index.html";
    }
    else {
	die "Error: no *_index.html file to look in for previous arguments\n";
    }
    
    open(S, "< $searchin") || die "** Cannot read $searchin\n";
    my @match = <S>;
    close(S);
    @match = grep /command line:/, @match;

    if ($#match == -1) {
	die "** Could not find previous arguments in $searchin\n";
    }
    $_ = $match[0];
    s/^.*gallery\.pl//;
    s/\s*-->$//;
    print "gallery.pl $_\n";
    return split;
}

sub getpicdate {
    # using exif information return picture date
    my($pic) = $_[0];

    if (!$options{havejhead}) {
	return 0;
    }
    my @info = `jhead $pic`;
    @info = grep /Date\/Time/, @info;
    my $date =  $info[0];
    my($year, $month, $day, $hour, $min, $sec) = $date =~ /(\d+):(\d+):(\d+)\s+(\d+):(\d+):(\d+)$/;
    my $retval = sprintf("%04d:%02d:%02d:%02d:%02d:%02d", $year, $month, $day, $hour, $min, $sec);
#    print "$pic $retval\n";
    return $retval;
}

sub getdate {
    my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
    my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
    $year += 1900;
    return "$months[$mon] $mday, $year";
}

sub sanitize_url {
    # TODO
    if (! $options{sanitize}) {
	return $_[0];
    }
    $_ = $_[0];
    s/ /\%20/g;
    return $_;
}

sub header {
    # compute the header of an html page
    my($title) = $_[0];
    my $myheader = $header;
    $myheader =~ s/\$title/$title/g;
    $myheader =~ s/\$version/$version/;
    $myheader =~ s/\$today/$today/;
    $myheader =~ s/\$login/$login/;
    $myheader =~ s/\$commandline/$commandline/;
    # relative link to .css
    my($depth) = $#crtdir;
    my($reverse) = "../" x $depth;
    $myheader =~ s/(gallery.css)/$reverse\1/; # replace link to css
    return $myheader;
}

sub cpsystem {
    # print, execute system(), check results
    my($c) = $_[0];
    if ($options{verbose}) {
	print " Executing '$c'\n";
    }
    system($c);
    if ($?) {
	die "** Command $c returned " . $? . "\n";
    }
}

sub cleanup {
    # remove all gallery.pl-generated files, except descriptions.txt
    &cpsystem("rm -f thumb_* v_* *.*.html index.html index_[0-9]*.html gallery.css copyright.html");
}

sub write_html_img {
    # write an <IMG SRC="">
    my($image, $border, $file) = @_;
    my($x, $y) = ("", "");

    if ($options{imgsizedescr}) {
	if (! -f $image) {
#	    print STDERR "## Cannot read image $image\n";
#           Sometimes the image directory is not relative to the current directory,
#           and then the file is not found.  I am too lazy to fix this now.	    
	    goto writeit;
	}

	my @identify = `identify -verbose \"$image\"`;
	@identify = grep /geometry/, @identify;
	$#identify > -1 || die "** Can't read $image geometry using 'identify'\n";
	my($size) = $identify[0];
	if ($?) {
#	    print STDERR "## Cannot read image size information for $image\n";
	    goto writeit;
	}
        ($x, $y) = $size =~ /(\d+)x(\d+)/;
	$x = "WIDTH=$x";
	$y = "HEIGHT=$y";
    }
  writeit:
    print $file "<IMG SRC=\"$image\" $x $y ALT=\"\" BORDER=$border>";
}

sub shrink {
    # shrink a picture using ImageMagick convert
    my($img) = $_[0];
    my($command);
    if (! -f "thumb_$img" || (-M "thumb_$img" > -M $img)) {
	$command = "convert $options{sharpen} -geometry $options{thumbx}" . "x" . "$options{thumby} $options{sharpen}  \"$img\" \"thumb_$img\"";
	&cpsystem($command);
    }
	
    if ($options{style} ne "plain" && (! -f "thumb_small_$img" || (-M "thumb_small_$img" > -M $img))) {
	# plain style doesn't use the small thumbnails
	$command = "convert $options{sharpen} -geometry " . $options{thumbx}/2 . "x" . $options{thumby}/2 . " $options{sharpen} \"$img\" \"thumb_small_$img\"";
	&cpsystem($command);
    }
    if (! -f "v_$img" || (-M "v_$img" > -M $img)) {
	my(@tmp) = `identify -verbose \"$img\"`;
	@tmp = grep /[Gg]eometry/, @tmp;
	my($size) = $tmp[0]; 
	my($x, $y) = $size =~ /(\d+)x(\d+)/;
	# shrink only if too big
	if ($x >= $options{picx} || $y >= $options{picy}) {
	    $command = "convert -geometry $options{picx}" . "x" . "$options{picy} \"$img\" \"v_$img\"";
	    &cpsystem($command);
	}
    }
}

sub do_exif {
    # extract exif image information from an image
    # save it in an info_$image.html file.  Return 1 if success
    my($image) = $_[0];

    if (! $options{havejhead}) {
	return 0;
    }
    if (! $image =~ /jpe?g/) {
	# exif cannot handle non-jpeg images
	return 0;
    }
    
    open(INFO, "> info_$image.html") || die "Can't write exif info file info_$image.html\n";
    print INFO &header("Info");
    open(E, "jhead \"$image\" |");
    my(@exif) = <E>;
    close(E);
    pop(@exif);
    print INFO " <center>\n";
    print INFO "  <h2>Original image info</h2>\n  <table>\n";
    foreach (@exif) {
	if (/omment/) { next; }
	my($l, $r);
	$l = substr($_,0,13);
	$r = substr($_,13,-1);
	print INFO "   <tr><td align=\"right\"><small>$l</small></td>" . 
	               "<td align=\"left\"><small>$r</small></td></tr>\n";
    }
    print INFO "  </table>\n";
    print INFO "  <a href=\"\#\" onclick=\"window.close()\">Close</a>\n";
    print INFO " </center>";
    print INFO $trailer;
    close(INFO);
    return 1;
}

sub plain_part {
    # draw the plain part: common between frame & plain styles
    my($image, $caption, $indent) = @_;
    $indent = ' ' x $indent;
    my($p);
    {
	print IMG "$indent<!-- The menu -->\n";
	print IMG "$indent<table cellspacing=5>\n";
	print IMG "$indent <tr>\n";
	if ($crtpage{crt_pic} > 1) {
	    $p = $pics[$crtpage{crt_pic} - 2]; # indexing starts from 0
	    $p = &sanitize_url($p);
	    print IMG "$indent  <td align=\"center\"><a href=\"$p.html\">Previous</a></td>\n"; 
	}
	else {
	    print IMG "$indent  <td align=\"center\">Previous</td>\n";
	}
	print IMG "$indent  <td align=\"center\"><a href=\"$crtpage{filename}\">Up</a></td>\n";
	if ($crtpage{crt_pic} <= $#pics) {
	    $p = $pics[$crtpage{crt_pic}];
	    $p = &sanitize_url($p);
	    print IMG "$indent  <link rel=\"prefetch\" href=\"$p.html\">\n";
	    print IMG "$indent  <td align=\"center\"><a href=\"$p.html\">Next</a></td>\n";
	}
	print IMG "$indent  <td width=$options{picx}>&nbsp;&nbsp;</td>\n"; 
	# check if original image is different from the current
	my($sane_image) = $image;
	if (-f "v_$image" && ! $options{nooriginal}) {
	    $sane_image = &sanitize_url($image);
	    print IMG "$indent  <td><a href=\"$sane_image\">hi-res</a></td>\n";
	}
	if (&do_exif($image)) {
	    print IMG "$indent  <td><a href=\"info_$image.html\" onclick=\"window.open('info_$sane_image.html', 'info', 'menubar=no,statusbar=no,location=no,width=400,height=400,scrollbars=yes');return false;\">info</a></td>\n";
	}
	
	print IMG "$indent </tr>\n";
	print IMG "$indent</table>\n";
    }
}

sub frame_style {
    # do a image page in a frame-style
    my($image, $caption) = @_;

    print IMG "  <h1>$caption</h1>\n";
    print IMG "  <p><table cellspacing=5 align=left>\n";
    print IMG "   <tr>\n";
    print IMG "    <td align=\"center\">Select</td>\n";
    print IMG "    <td align=\"center\">\n";

    plain_part(@_, 5);
    print IMG "    </td>\n";
    print IMG "   </tr>\n";
    print IMG "   <tr>\n";
    print IMG "    <td>\n";

    {
	# the vertical icons bar
	print IMG "     <!-- vertical icons bar -->\n";
	print IMG "     <table cellspacing=5>\n";
	my($p, $nr, $sec) = ("", 0, 0);
	for $p (@pics) {
	    my($border, $color) = (0,"black");
	    if ($p eq $image) {
		$border=4;
		$color = "white";
	    }
	    if ($secno[$nr + $sec]) {
		my($indexfile) = $secpage[$sec];
		my($indexfilename) = &indexhtmlname($indexfile);
		print IMG "      <tr><td><a href=\"$indexfilename\#section_" . ($sec+1) . "\">" . 
		           @secs[$sec] . "</a></td></tr>\n";
		$sec++;
	    }
#	    print STDERR "$p $sec $crtpage{crt_sec}\n";
	    if ($sec+1 == $crtpage{crt_sec}) {
		$p = &sanitize_url($p);
		print IMG "      <tr><td align=\"center\"><a href=\"$p.html\">";
		&write_html_img("thumb_small_$p", $border, *IMG);
		print IMG "</a></td></tr>\n";
	    }
	    $nr++;
	}
	print IMG "     </table>\n";
    }
    {
	print IMG "     <!-- The image itself -->\n";
	print IMG "    <td valign=\"top\" align=\"center\">";
	my $useimg = "v_$image";
	if (! -f "v_$image") {
	    # no shrunk version: use the image itself
	    $useimg = "$image";
	}
	&write_html_img($useimg, 0, *IMG);
	print IMG "</td>\n";
    }
    print IMG "   </tr>\n  </table>\n";
}


sub plain_style {
    # do an image page in a totally plain style: the picture and some links only
    my($image, $caption) = @_;
   
    print IMG "  <h1>$caption</h1>\n";
    print IMG "  <p><table cellspacing=5 align=\"left\">\n";
    print IMG "   <tr><td align=\"center\">\n";

    plain_part(@_, 4);
    print IMG "   </td></tr>\n";
    print IMG "   <tr>";
    {
	# the image itself
	print IMG "<td valign=\"top\" align=\"center\">";
	my $useimg = "v_$image";
	if (! -f "v_$image") {
	    # no shrunk version: use the image itself
	    $useimg = "$image";
	}
	&write_html_img($useimg, 0, *IMG); 
	print IMG "</td></tr>\n";
    }
    print IMG "  </table>\n";
}


sub simple_style {
    # do an image page in a simple style: prev, next, etc. links, but with some thumbnails
    my($image, $caption) = @_;
    my($p);

    print IMG "  <h1>$caption</h1>\n";
    print IMG "  ";

    my $useimg = "v_$image";
    if (! -f "v_$image") {
	# no shrunk version: use the image itself
	$useimg = "$image";
    }

    &write_html_img($useimg, 0, *IMG);
    print IMG "\n";
    print IMG "  <p><table cellspacing=5>\n   <tr>\n";
    print IMG "    <td><a href=\"javascript:history.back()\">&lt; Back</a></td>\n";
    if ($crtpage{crt_pic} > 1) {
	$p = $pics[$crtpage{crt_pic} - 2]; # indexing starts from 0
	$p = &sanitize_url($p);
	print IMG "     <td align=\"center\"><a href=\"$p.html\">";
	&write_html_img("thumb_small_$p", 0, *IMG);
	print IMG "<br>previous</a></td>\n";
    }
    print IMG "    <td align=\"center\"><a href=\"$crtpage{filename}\">";
    &write_html_img("thumb_small_$image", 0, *IMG);
    print IMG "<br>up</a></td>\n";
    if ($crtpage{crt_pic} <= $#pics) {
	$p = $pics[$crtpage{crt_pic}];
	$p = &sanitize_url($p);
	print IMG "    <td align=\"center\"><a href=\"$p.html\">";
	&write_html_img("thumb_small_$p", 0, *IMG);
	print IMG "<br>next</a></td>\n";
    }

    my($sane_image) = $image;
    if (&do_exif($image)) {
	$sane_image = &sanitize_url($image);
	print IMG "    <td><a href=\"info_$sane_image.html\" onclick=\"window.open('info_$image.html', 'info', 'menubar=no,statusbar=no,location=no,width=400,height=400,scrollbars=yes');return false;\">info</a></td>\n";
    }
    print IMG "    <td><a href=\"$sane_image\">original</a></td>\n";
    print IMG "   </tr>\n  </table>\n";
}

sub print_img_in_page {
    # print an image thumbnail in a table
    my($link) = shift;
    my($pic) = shift;
    my($pg) = shift;

    my($filedesc) = $$pg{file};
    defined($filedesc) || die "** undefined file descriptor\n";
    if (! $$pg{begun_table}) {
	print $filedesc "  <table border=0 align=\"center\" cellpadding=5 cellspacing=5>\n";
	$$pg{begun_table} = 1;
	$$pg{crt_col} = 0;
	print $filedesc "   <tr>";
    }
    elsif (! $$pg{crt_col}) {
	print $filedesc "\n   </tr>\n   <tr>";
    }
    $$pg{crt_col} = ($$pg{crt_col}+$$pg{pic_cols}) % $$pg{columns};

    my($link1, $link2);
    if ($link ne "") {
	$link1 = "<a href=\"$link\">";
	$link2 = "</a>";
    }
    else {
	$link1 = "";
	$link2 = "";
    }
   
    print $filedesc "\n    <td align=\"center\" colspan=$$pg{pic_cols}>${link1}";
    if ($pic ne "") {
	&write_html_img("$pic", 0, $filedesc);
    }
    else {
	print $filedesc "&nbsp;";
    }
    print $filedesc "${link2}\n     <P>";

    $$pg{pic_cols} = 1; # reset number of columns taken by next picture
}

sub set_columns_in_page {
    # next object will use this many columns
    my($cols) = shift;
    my($pg) = shift;
    $$pg{pic_cols} = $cols;

    if ($cols > $$pg{columns}) {
	print STDERR "Cannot use more columns ($cols) than maximum ($$pg{columns})\n";
	$$pg{pic_cols} = $$pg{columns};
    }
}

sub close_table_in_page {
    # finished with a <table> object
    my($pg) = $_[0];
    my($file) = $$pg{file};

    if ($$pg{begun_table} != 0) {
	print $file "\n    </td>\n   </tr>\n  </table>\n\n";
	$$pg{begun_table} = 0;
    }
}


sub is_pic_name {
    my($filename) = $_[0];

    if ($filename =~ /^thumb_/) { return ("", 0); }
    if ($filename =~ /^v_/) { return ("", 0); }
    my($retval) = $filename;
    $retval =~ tr/A-Z/a-z/;
    my ($suff) = $retval =~ /\.(\w+)$/;
    if (! $image_suffixes{$suff}) { return ("", 0); }
    $retval =~ s/\.$suff$//;
    return ($retval, 1);
}


sub do_image {
    # process one image (a * file command)
    my($image) = $_[0];

    # check if image actually exists
    if ((! -f $image) && (! $options{reconstruct})) {
	print STDERR "## warning: Cannot find '$image'; skipping\n";
	# this will signal to skip next caption too
	$crtpage{skipping} = 1;
	return;
    }
    else {
	$crtpage{skipping} = 0;
    }

    chmod 444, $_;
    if (! $options{reconstruct}) {
	&shrink($image);
    }
    $image = &sanitize_url($image);
    my $picname = $pics[$crtpage{crt_pic} - 1];
    if ($picname ne $image) {
	die "Error: inconsistent picture numbering '$image' != '$picname'\n"; 
    }
    ($crtpage{crt_pic} <= $#captions+1) || die "Error: mismatch in caption numbering\n";
    my($caption) = @captions[$crtpage{crt_pic} - 1];

    &print_img_in_page("$image.html", "thumb_$image", \%crtpage);
    # for each image create a html file with prev, up, next links using thumbnails
    open(IMG, "> $image.html") || warn "Error: Can't write in $image.html";
    if ($caption =~ /^\s*$/ && $#secs > 0) {
	$caption = $secs[$crtpage{crt_sec}-2];
	# crt_sec starts at 1; also, crt_sec is incremented when *section is met,
	# so the first section is really #2!
    }
    print IMG &header($options{sametitle} ? $options{master_title} : $caption);
    print IMG " <center>\n";

    if ($options{style} eq 'simple') {
	&simple_style($image, $caption);
    }
    elsif ($options{style} eq 'frame') {
	&frame_style($image, $caption);
    }
    elsif ($options{style} eq 'plain') {
	&plain_style($image, $caption);
    }
    else {
	die "Error: unknown style $options{style}; choose from " . join(',', @styles) . "\n";
    }

    $crtpage{crt_pic}++;	# increment if printing a new picture
    
    print IMG " </center>";
    print IMG $trailer;
    close IMG;
}

sub indexhtmlname {
    # generate name of thumbnail page
    my($nr) = $_[0];
    if ($nr == 0) {
	return "index.html";
    }
    return "index_$nr.html";
}

sub command {
    # process a descriptions.txt command
    $_ = $_[0];
    print $_, "\n";

    s/^\*\s*//;
    if (/^title\s+/) {
	if ($crtpage{printed_title}) {
	    print STDERR "## Title already printed";
	    return;
	}
	s/^title\s+//;
	$lasttitle = $_;
	my($mytitle_header) = $title_header;
	if ($totpages) {
	    $_ .= " [" . ($crtpage+1) . "/" . ($totpages+1) . "]";
	}
	$mytitle_header =~ s/\$title/$_/g;
	print I &header($_), $mytitle_header;

	# print table of contents
	if ($#secs > 0) {
	    # if only one section, there's no point
	    print I " <!-- Table of contents -->\n";
	    print I " <ul>\n";
	    my($n)=0;
	    foreach (@secs) {
		my($file) = &indexhtmlname($secpage[$n]);
		print I " <li><a href=\"$file#section_$n\">$_</a>\n";
		$n++;
	    }
	    print I " </ul>\n";
	    print I " <hr>\n\n";
	}
	    
	if ($options{recurse} && ($#crtdir == 1)) {
	    # shallow hierarchy in master_index
	    my $dir = join("/", @crtdir);
	    $dir = &sanitize_url($dir);
	    if ($crtpage == 0) {
		s/\s*\[1\/\d+\]$//;
		print MASTER_INDEX "<a href=\"$dir/index.html\">$_</a>\n";
	    }
	    print I " <a href=\"" . ("../" x $#crtdir) . "master_index.html\">Home</a>\n";
	}
	if ($totpages) {
	    if ($crtpage > 0) {
		my($indexname) = &indexhtmlname($crtpage-1);
		print I " <a href=\"$indexname\">Previous</a>";
	    }
	    if ($crtpage < $totpages) {
		my($indexname) = &indexhtmlname($crtpage+1);
		print I " <a href=\"$indexname\">Next</a>";
	    }
	    print I "<br>\n";
	}
    }
    elsif (/^section\s+/) {
	if ($#secs > 0) {
	    s/^section\s+//;
	    &close_table_in_page(\%crtpage);
	    print I " <a name=\"section_" . ($crtpage{crt_sec} - 1) . "\"></a>\n <h2>$_</h2>\n";
	}
	++$crtpage{crt_sec};
    }
    elsif (/^file\s+/) {
	s/^file\s+//;
	my $image=$_;
	if ($crtpage{begun_table}) {
	    print I "\n    </td>";
	}
	do_image($image);
	$totpics++;
    }
    elsif (/^columns\s+/) {
	s/^columns\s+//;
	&set_columns_in_page($_, \%crtpage);
    }
    elsif (/^pagebreak/) {
	&close_table_in_page(\%crtpage);
	close(I);
	rename($crtpage{filename}, $crtpage{filename} . ".bak");
	rename($crtpage{filename} . ".new", $crtpage{filename});
	++$crtpage;
	my($newpagename) = &indexhtmlname($crtpage);
	open(I, "> $newpagename.new") || die "Error: can't create new index page\n";
	my($savecrtpic) = $crtpage{crt_pic};
	my($savecrtsec) = $crtpage{crt_sec};
	%crtpage = %page;
	$crtpage{crt_pic} = $savecrtpic;
	$crtpage{crt_sec} = $savecrtsec;
	$crtpage{filename} = $newpagename;
	$crtpage{file} = *I;
	# recurse
	&command("* title $lasttitle");
    }
    elsif (/^verbatim/) {
	&close_table_in_page(\%crtpage);
	print I "<!-- begin verbatim -->\n";
	$crtpage{inverbatim} = 1;
    }
    elsif (/^endverbatim/) {
	warn "endverbatim without verbatim; ignoring\n";
    }
    elsif (/^titlepic\s+/) {
	s/^titlepic\s*//;
	my($pic) = $_;
	if ($options{recurse}) {
	    my($align);
	    my($dir) = join("/", @crtdir);
	    $dir = &sanitize_url($dir);
	    if ($#crtdir == 1) {
		if ($pic =~ /^\s*$/) {
#		print "EMPTY TITLEPIC\n";
		    &print_img_in_page("$dir/$crtpage{filename}", "", \%masterpage);
		}
		else {
		    &print_img_in_page("$dir/$crtpage{filename}", "$dir/thumb_$_", \%masterpage);
		}
	    }
	}
	# else do nothing
    }
    elsif (/^skip\s*/) {
	&print_img_in_page("", "", \%crtpage);	# print just a space; no link
    }
    else {
	print STDERR "## Unrecognized command $_ in 'descriptions.txt'\n";
    }
}


sub basename {
    my $file = $_[0];
    if ($sysname =~ /Win/) {
	$file =~ s/.*\\//;
    }
    else {
	$file =~ s/.*\///;
    }
    $file =~ s/\.*//;
    return $file;
}


sub pwd {
    if ($sysname =~ /Win/) {
	return `cd`;
    }
    return `pwd`;
}


sub create_descriptions {
    # create a descriptions.txt file containing all the images in the directory
    open(DESC, "> descriptions.txt") || die "Error: can't write in 'descriptions.txt'";
    my($crtdir) = &pwd();
    $crtdir = &basename($crtdir);
    chop $crtdir;
    print DESC 
	"# descriptions.txt: this file has been automatically created\n",
	"# by gallery.pl, http://www.cs.cmu.edu/~mihaib/ftp/gallery.pl\n",
	"# on $today by $login\n",
	"#\n",
	"# Comments start with #\n",
	"# Use '* columns n' to change number of columns of a picture\n",
	"# Use '* pagebreak' to start a new thumbnails page\n",
	"# Use '* skip' to insert empty 'pictures'\n",
	"# Use '* verbatim...* endverbatim' to insert arbitrary inter-picture html\n\n",
	"* title $crtdir\n",
	"* section $crtdir\n";
	
    opendir(DIR, ".") || die "Error: can't read current directory";
    my @files = readdir(DIR);
    if ($options{sortdate}) {
	my (%dates);
	print "Extracting dates (may take some time)\n";
	map { $dates{$_} = &getpicdate($_) } @files;
#	print join "\n", (keys(%dates));
	@files = sort { $dates{$a} cmp $dates{$b} } @files;
    }
    else {
	@files = sort @files;
    }
    closedir(DIR);
    my($pics, $f, $suff, $first) = (0, "", "", 1);
    for $f (@files) {
#	if (! -f $f) { next; }
	my($file, $process) = &is_pic_name($f);
	if (! $process) { next; }

	if ($first) {
	    print DESC "* titlepic $f\n# Picture to put in master index when recursing\n";
	    print DESC "# titlepic, if present, MUST be after '* title'\n\n";
	}
	
	print DESC "* file $f\n";
	if ($first) {
	    print DESC "# Add on next line text describing $f\n";
	    $first = 0;
	}
	if (! $options{nocaption}) {
	    print DESC "$file\n\n";
	}
	$pics++;
    }

    if (! $pics) {
	print "Warning: no pictures found!\n";
	close(DESC);
	unlink("descriptions.txt");
	return 0;
    }
    else {
	close(DESC);
	return 1;
    }
}

sub addcol {
    # compute new position in table after skipping $skip columns
    my($col, $row, $maxcol, $maxrow, $skip) = @_;
    my($newtable) = 0;

    $col += $skip;
    if ($col > $page{columns}) {
	$row++;
	$col = 1;
	if (($options{rows} > 0) && ($row > $options{rows})) {
	    $row = 1;
	    $newtable = 1;
	}
    }
#    print "($col $row)\n";
    return ($col, $row, $newtable);
}


sub print_copyright {
    my($file) = $_[0];
    if ($options{copyright}) {
	print $file " <br><a href=\"" . ("../" x $#crtdir) . "copyright.html\"><small>copyright</small></a>\n";
    }
}


sub create_index {
    # create the index.html file from the descriptions.txt file
    open(F, "< descriptions.txt") || die "Error: can't read descriptions.txt\n";
    my(@desc) = <F>;
    close(F);

    # remove comments and empty lines
    grep s/\#.*//, @desc;	# remove comments
    @desc = grep !/^\s*$/, @desc; # empty lines
    grep chop, @desc;
    grep(s/\s+$//, @desc);

    # extract picture names
    @pics = grep(/^\*\s*file/, @desc);
    grep(s/\*\s*file\s*//, @pics);
    $crtpage{crt_pic} = 1;

    # extract section names
    @secs = grep(/^\*\s*section/, @desc);
    grep(s/\*\s*section\s*//, @secs);

    # which lines are section numbers
    @secno = grep(/^\*\s*(section|file)/, @desc);
    @secno = map (/^\*\s*section/ && $crtpage{crt_sec}++, @secno);
    $crtpage{crt_sec} = 1;
    $crtpage = 0;

    # if we indicated we want up to a fixed number of rows, compute now where and insert the page breaks
    if ($options{rows}) {
	my($crtrow, $crtcol,$colsnextpic,$newtable) = (1,1,1,0);
	my(@desccopy) = @desc;
	my($index) = 0;
	foreach (@desccopy) {
	    if (/\*\s*file/ || /\*\s*skip/) {
		($crtcol, $crtrow, $newtable) = 
		     &addcol($crtcol, $crtrow, $options{columns}, $options{rows}, $colsnextpic);
		$colsnextpic = 1;
		if ($newtable) {
		    # search for next command and insert a page break just before it
		    do {
			$index++;
			if ($index > $#desc) {
			    goto finished_page_breaks;
			}
		    } while (! ($desc[$index] =~ /^\*/));
		    splice(@desc, $index, 0, "* pagebreak");
		}
	    }
	    elsif (/\*\s*columns/) {
		my($colsnextpic) = $_ =~ /\*\s*columns (\d+)/;
	    }
	    elsif (/\*\s*pagebreak/) {
		($crtrow, $crtcol) = (1,1);
	    }
	    elsif (/\*\s*section/) {
		if ($crtcol > 1) {
		    $crtrow++;
		    $crtcol = 1;
		    if ($crtrow > $options{rows}) {
			$crtrow = 1;
			splice(@desc, $index, 0, "* pagebreak");
			$index++;
		    }
		}
	    }
	    $index++;
	}
    }

finished_page_breaks:
    # sections are separated by pagebreaks, find how
    @secpage = ();
    my($page) = 0;
    foreach (@desc) {
	if (/^\*\s*pagebreak/) {
	    $page++;
	}
	elsif (/^\*\s*section/) {
	    push(@secpage, $page);
#	    print "$#secpage -> $page\n";
	}
    }
    
    # extract caption content
    # This is harder...
    my($text) = join('#', @desc) . "#";
#    print "$text\n";
    @captions = split(/\*\s*file[^\#]*\#/, $text, -1);
    @captions = @captions[1..$#captions]; # drop up to first figure
    push(@captions, "");	# last one may be missing, add there to be safe
    grep(s/\*.*//g, @captions); # remove other formatting elements
#    print join("\n ", @captions), "\n";
    grep(s/\#//g, @captions);	# remove remaining sharp separators
    if ($options{nocaption}) {
	# ignore everything, but keep right number
	grep($_ = "", @captions);
    }

    $totpages = grep(/\*\s*pagebreak/, @desc);

    # do we have a titlepic: if so, move above title
    # otherwise, generate an empty titlepic
    my(@tmp) = grep(/^\*\s*titlepic/, @desc);
    @desc = grep(! /^\*\s*titlepic/, @desc); # remove them

    my($titlepics) = $#tmp;
    if ($titlepics > 0) {
	print STDERR "## Cannot have more than 1 titlepic per directory\n";
    }
    elsif ($titlepics == -1) {
	# no titlepic: put an empty one
	unshift(@desc, "* titlepic ");
    }
    else {
	unshift(@desc, $tmp[0]);
    }
    
    open(I, "> index.html.new") || die "Error: can't write index.html.new";
    $crtpage{file} = *I;
    $crtpage{filename} = "index.html";

    foreach (@desc) {
	# each line is processed independently
	if ($crtpage{inverbatim}) {
	    # lines between * verbatim ... * endverbatim are special
	    if (/^\*\s*endverbatim/) {
		$crtpage{inverbatim} = 0;
		print $_, "\n";
		print I "<!-- end verbatim -->\n";
	    }
	    else {
		print I $_, "\n";
	    }
	}
	elsif (/^\*/) {
	    # lines beginning with "*" are commands
	    &command($_);
	}
	else {
	    # everything else is copied literally in the result
	    # (you can embed html here is you want)
	    if (! $crtpage{skipping} && ! $options{nocaption}) {
		print I $_;
	    }
	}
    }

    if ($crtpage{inverbatim}) {
	warn "verbatim without endverbatim encountered!";
	$crtpage{inverbatim} = 0;
    }

    &close_table_in_page(\%crtpage);

    &print_copyright(*I);
    print I $trailer;
    close(I);
    rename($crtpage{filename}, $crtpage{filename} . ".bak");
    rename($crtpage{filename} . ".new", $crtpage{filename});
}


sub list_unused {
    # list figures in current directory not mentioned in descriptions.txt
    if (! -f "descriptions.txt") {
	print STDERR "## no descriptions.txt here\n";
	return;
    }
    open(D, "< descriptions.txt") || die "** Can't read 'descriptions.txt'\n";
    my(@mentioned) = <D>;
    close(D);

    @mentioned = grep /^\*\s*file/, @mentioned;
    grep(s/\*\s*file\s+//, @mentioned);
    grep(chop, @mentioned);
    grep(s/\s+$//, @mentioned);
    my(%mentioned);
    map( ($mentioned{$_} = 1), @mentioned);

    # now read picture files
    opendir(DIR, ".") || die "Error: can't read current directory";
    my @files = readdir(DIR);
    closedir(DIR);

    if ($options{addnewpics}) {
	open(D, ">> descriptions.txt") || die "Error: can't append new info to 'descriptions.txt'\n";
	print D "\n#### New pictures, added $today by $login\n\n";
    }

    my(@toremove) = ();
    my($f);
    my($found) = 0;
    for $f (@files) {
	my($file, $process) = &is_pic_name($f);
	if (! $process || $mentioned{$f}) { next; }

	if ($options{unused}) {
	    $found++;
	    print " $f\n";
	}
	push (@toremove, $f);
	if ($options{addnewpics}) {
	    $found++;
	    print D "* file $f\n";
	    if (! $options{nocaption}) {
		print D "$file\n\n";
	    }
	}
    }
    print STDERR "$found pictures found\n"; # print on stderr so we can use the stdout output in scripts
    if ($options{addnewpics}) {
	close D;
    }
    if ($options{cleanunused} && ($#toremove >= 0)) {
	my $command = "";
	print "Removing in 5 seconds the following files (press ^C to cancel)\n";
	foreach (@toremove) {
	    my $s = "$_ v_$_ info_$_.html thumb_$_ $_.html thumb_small_$_";
	    print "\t$s\n";
	    $command .= " $s";
	}
	sleep 5;
	&cpsystem("rm -f $command");
    }
}


sub do_directory {
    # process the indicated directory
    my($dir) = $_[0];

    push(@crtdir, $dir);
    %crtpage = %page;
    my($save) = &pwd();
    chop $save;
    if (! -d $dir) {
	die "Error: can't find $dir directory";
    }
    chdir($dir);

    my (%skipdirs) = ();
    map ($skipdirs{$_} = 1, split(",", $options{skip}));

    if ($skipdirs{$dir}) {
	print STDERR "Skipping $dir\n";
	goto done;
    }
    elsif ($options{cleanup}) {
	&cleanup();
    }
    elsif ($options{unused} || $options{addnewpics} || $options{cleanunused}) {
	&list_unused();
    }
    elsif (-f "descriptions.txt") {
	&create_index();
    }   
    else {
	if (&create_descriptions()) {
	    print "I have created a skeleton 'descriptions.txt' file in $dir\n",
	          "Please edit it by hand and run 'gallery.pl' again\n";
	}
    }

    if ($options{recurse}) {
	opendir(DIR, ".");
	my(@files) = sort(readdir(DIR));
	closedir(DIR);
	@files = grep(!/^\./, @files);
	@files = grep(-d $_, @files);

	if ($#files > -1) {
	    $depth++;
	    for (@files) {
		print STDERR "Doing directory $_\n";
		&do_directory($_);
	    }
	    $depth--;
	}
    }

  done:
    chdir($save);
    pop(@crtdir);
}


sub main {
    # create the style sheet
    if (! -f "gallery.css") {
	open(F, "> gallery.css") || die "Error: can't write style sheet\n";
	print F $css;
	close(F);
    }

    if ($options{copyright} && ! -f "copyright.html") {
	open(F, "> copyright.html") || die "Error: can't write copyright file\n";
	print F &header("Copyright");
	print F $copyright;
	print F $trailer;
	close F;
    }

    if ($options{onepic} ne "") {
	# create a fake command and execute it
	&shrink($options{onepic});
	return;
    }
    
    my $time = time();
    my($create_master_index) = $options{recurse} && 
	!$options{cleanup} && !$options{unused} && !$options{addnewpics};
    if ($create_master_index) {
	open(MASTER_INDEX, "> master_index.html.new") || die "Error: can't write master index\n";
	$masterpage{file} = *MASTER_INDEX;
	print MASTER_INDEX &header($options{master_title});
	print MASTER_INDEX " <h1>$options{master_title}</h1>\n";
    }
    if ($options{cleaup} && $options{recurse}) {
	&cpsystem("rm -f master_index.html");
    }
    &do_directory(".");
    if ($create_master_index) {
	&close_table_in_page(\%masterpage);
	&print_copyright(*MASTER_INDEX);
	print MASTER_INDEX $trailer;
	close(MASTER_INDEX);
	rename("master_index.html", "master_index.html.bak");
	rename("master_index.html.new", "master_index.html");
    }
    $time = time() - $time;
    print "Total pictures processed: $totpics in $time seconds\n";
}


# program
&main();

sub check_programs {
    # check required programs
    if ($sysname =~ /Win/) {
	$options{havejhead} = 0;
    }
    else {
	my @tmp = `jhead -h`;
	if ($?) {
	    if ($options{useexif}) {
		warn "## Cannot find jhead program used to extract EXIF picture information\n";
	    }
	    $options{havejhead} = 0;
	}
    }

    # determine convert options; unfortunately these have changed at some point
    my @tmp = `convert -h`;
    if ($?) {
	die "Error: 'convert' program not found";
    }
    @tmp = grep /sharp/, @tmp;
    if ($tmp[0] =~ /geometry/) {
	# modern convert
	$options{sharpen} = "-sharpen 3x1";
    }

    @tmp = `identify -help`;
    if ($?) {
	die "Error: 'identify' program not found";
    }
}
