#!/usr/local/bin/perl

# THIS IS OUTDATED AND WILL NOT RUN WITH CURRENT GIMP VERSIONS!

######################################################################
#  This program is an automatic way of scaling and tiling lots of
#  images in order to create an index image.
#
#  Todo:
#    - Add more layout algorithms. (fixed grid, dynamic grid, better
#      orphan handling)
#    - Add more decoration algorithms (e.g. negative strip, slides,
#      sunken windows).
#    - Create a way of supplying a template HTML index file into which
#      the image map will be added.
#    - Create a way of supplying a template HTML file in which
#      full size images will be displayed. This file may include
#      image annotations that come from the input file.
#    - Define format of input file. This file should include
#      annotations on/below images, comments in the full size html
#      file, perhaps baloon text on top of images.
#    - Make all parameters of tiling algorithms into options.
#    - Change background of simple-shadow algorithm.
#    - Option for either keeping image in gimp, or flattening and
#      saving to disk.
#    - Add a modular way of adding new layout and decoration algorithms.
#
#  Availability:
#    - You can always find the latest version of this script at
#      http://imagic.weizmann.ac.il/~dov/gimp/perl
#
#  Author:
#    Dov Grobgeld <dov@imagic.weizmann.ac.il>
#
#  Bugs:
#    -  -padx px means at least px and not exactly px. This should
#       be fixed.
#
#  Version: 0.11
######################################################################

use Gimp;

# Defaults
$max_height = 64;
$layout_width = 600;
$bgcolor = [194,194,194];
$decoration = 'drop-shadow';
$gutter_x = 15;
$gutter_y = 15;
$pad_x = 20;
$pad_y = 20;
chop($PWD = `pwd`);

# Routine for parsing the input format
sub get_next_record {
    return (undef,undef) if eof(IN);
    local $_ = <IN>;
    /^(\S+)\s+(.*)/;
    return ($1,$2);
}

# Load an image and return its id
sub load_img {
    my($fn) = shift;
    my($max_height) = shift;
    $fn = "$PWD/$fn" unless $fn=~ m:^/:;
    my ($img);
    $img = gimp_file_load(RUN_NONINTERACTIVE,$fn,$fn);
    my ($w, $h) = ($img->width, $img->height);

    # Resize the img
    if ($h > $max_height) {
	my $scale = $max_height/$h;
	my $new_w = int($w * $scale+0.5);
	my $new_h = int($h * $scale+0.5);
#	print "New w,h = ($new_w, $new_h)\n";
	gimp_image_scale($img, $new_w, $new_h);
    }

    return $img;
}

######################################################################
#  hbox/vbox algorithm. This algorithm is much like a text flowing
#  algorithm with centered paragraphs.
######################################################################
sub hbox_vbox_add_row {
    my($imgs, $layout_width, $row_start_idx, $row_end_idx, $ypos, $gutter_x) = @_;
    my(@row_layout);

    # Calculate teh row width
    my $row_width = 0;
    my $row_height = 0;
    for $i ($row_start_idx..$row_end_idx) {
	my($w,$h) = ($imgs->[$i]->width, $imgs->[$i]->height);
	$row_width += $gutter_x + $w;
	$row_height = $h if $h > $row_height;
    }
    $row_width -= $gutter_x;

    # Do the layout
    my $xpos = ($layout_width-$row_width)/2;
    for $i ($row_start_idx..$row_end_idx) {
	my $y = $ypos + ($row_height-$imgs->[$i]->height)/2;
	push(@row_layout, [$imgs->[$i], $xpos, $y]);
	$xpos+= $gutter_x + $imgs->[$i]->width;
    }
    return(@row_layout);
}

sub hbox_vbox_create_layout {
    my($imgs) = @_;
    my(@layout);        # The positions of all the images

    # A simple maximal row algorithm
    my ($row_start_idx, $row_end_idx);
    my $ypos = $pad_y;
    my $xpos = 0;
    print "imsg->[0] = $imgs->[0]\n";
    my $row_height = $imgs->[0]->height();
    my $i;
    foreach $img_idx (0..@$imgs-1) {
	print "Layouting image #$img_idx\n";
	my $w = $imgs->[$img_idx]->width();
	my $h = $imgs->[$img_idx]->height();

#	print "row_width = $row_width\n";
	# Check for the creation of a new row
	if ($row_width + $pad_x * 2 + $w > $layout_width) {

	    push(@layout,
		 hbox_vbox_add_row($imgs, $layout_width,
				   $row_start_idx, $row_end_idx,
				   $ypos, $gutter_x));
	    $total_width = $row_width if $row_width > $total_width;

	    # Move to next row
	    $ypos+= $gutter_y + $row_height;

	    # Zero out various things
	    $row_start_idx = $row_end_idx+1;
	    $row_end_idx = $row_start_idx;
	    $row_width = 0;
	    if ($row_start_idx < @imgs) {
		$row_width = $imgs->[$row_start_idx]->width;
		$row_height = $imgs->[$row_start_idx]->height;
		$xpos = 0;
	    }
	}
	else {
	    $row_width += $gutter_x + $w;
	    $row_end_idx = $img_idx;
	    $row_height = $h if $h > $row_height;
	}
    }

    if ($row_start_idx < @imgs) {
	push(@layout,
	     hbox_vbox_add_row($imgs, $layout_width,
			       $row_start_idx, $row_end_idx,
			       $ypos, $gutter_x));
	$total_width = $row_width if $row_width > $total_width;

	$ypos+= $row_height;
    }

    $total_width = $layout_width;
    $total_height = $ypos + $pad_y;
    return ($total_width, $total_height, \@layout);
}

######################################################################
#  The decoration_drop_shadow creates one layer with the images
#  and puts a drop shadow behind them.
######################################################################
sub decoration_drop_shadow {
    my($layout) = shift;
    $shadow_xoffs = 7;
    $shadow_yoffs = 7;

    # Put them on a row
    $tiled_img = gimp_image_new($total_width, $total_height, RGB);
    $tiled_drw = gimp_layer_new($tiled_img, $total_width, $total_height,
				RGB_IMAGE, "Tiled", 100, NORMAL_MODE);
    $tiled_shadow = gimp_layer_new($tiled_img, $total_width, $total_height,
				   RGB_IMAGE, "Shadow", 50, NORMAL_MODE);
    $tiled_background = gimp_layer_new($tiled_img, $total_width, $total_height,
				       RGB_IMAGE, "Background", 100, NORMAL_MODE);

    # Create masks
    $tiled_drw_msk = $tiled_drw->create_mask(1);
    $tiled_shadow_msk = $tiled_shadow->create_mask(1);

    # Make sure respective images have alpha channels
    $tiled_drw->layer_add_alpha();
    $tiled_shadow->layer_add_alpha();

    # Connect masks to respective layers
    $tiled_img->add_layer_mask($tiled_drw, $tiled_drw_msk);
    $tiled_img->add_layer_mask($tiled_shadow, $tiled_shadow_msk);

    # Fill all the layers with some contents
    gimp_palette_set_background([128,128,128]);
    $tiled_drw->fill(BG_IMAGE_FILL);

    gimp_palette_set_background($bgcolor);
    $tiled_background->fill(BG_IMAGE_FILL);
    if ($bgpattern) {
	print "Setting pattern\n";
	gimp_patterns_set_pattern($bgpattern);
	$tiled_img->bucket_fill($tiled_background, PATTERN_BUCKET_FILL,
				NORMAL, 100, 0, FALSE, 0,0);
    }
    gimp_palette_set_background([0, 0, 0]);  # Shadow color
    $tiled_shadow->fill(BG_IMAGE_FILL);

    # Add all the layers to the image
    $tiled_img->add_layer($tiled_background,-1);
    $tiled_img->add_layer($tiled_shadow,-1);
    $tiled_img->add_layer($tiled_drw,-1);
    gimp_display_new($tiled_img);

    my $xpos = 0;

    # Set color for drawing in mask
    gimp_palette_set_background([255, 255, 255]);
    for $ly_idx (0..@$layout-1) {
	my ($img, $xpos, $ypos) = @{$layout->[$ly_idx]};
	($layer) = @{gimp_image_get_layers($img)};
	my($w,$h) = ($img->width, $img->height);
	$img->selection_all();
	$img->edit_copy($layer);
	$tiled_img->rect_select($xpos, $ypos, $w, $h, 0, 0, 0);
	$tiled_img->edit_paste($tiled_drw, 0)
	    ->floating_sel_anchor;

	# why is the selection cleared?
	$tiled_img->rect_select($xpos, $ypos, $w, $h, 0, 0, 0);
	$tiled_img->edit_fill($tiled_drw_msk, BG_IMAGE_FILL);

	# why is the selection cleared?
	$tiled_img->rect_select($xpos+$shadow_xoffs,
				$ypos+$shadow_yoffs, $w, $h, 0, 0, 0);
	$tiled_img->edit_fill($tiled_shadow_msk, BG_IMAGE_FILL);

	$tiled_img->selection_none();

    }

    # Blur the shadow
    plug_in_gauss_rle(1, $tiled_img, $tiled_shadow_msk, 7, 1, 1);

    # Apply the shadow mask
    $tiled_img->remove_layer_mask($tiled_shadow, APPLY);
}

sub decoration_sunken_windows {
    my($layout) = shift;
    $shadow_xoffs = 7;
    $shadow_yoffs = 7;

    # Create needed image and layers
    $tiled_img = gimp_image_new($total_width, $total_height, RGB);
    $tiled_drw = gimp_layer_new($tiled_img, $total_width, $total_height,
				RGB_IMAGE, "Tiled", 100, NORMAL_MODE);
    $tiled_punch_layer = gimp_layer_new($tiled_img, $total_width, $total_height,
				RGB_IMAGE, "Punched", 100, NORMAL_MODE);
    $tiled_punch_stencil = gimp_layer_new($tiled_img, $total_width, $total_height,
				RGB_IMAGE, "Punch mask", 100, NORMAL_MODE);
    # Create masks
    $tiled_punch_mask = $tiled_punch_layer->create_mask(0);

    # Make sure respective images have alpha channels
    $tiled_punch_layer->layer_add_alpha();

    # Connect masks to respective layers
    $tiled_img->add_layer_mask($tiled_punch_layer, $tiled_punch_mask);

    # Fill all the layers with some contents
    gimp_palette_set_background([128,128,128]);
    $tiled_drw->fill(BG_IMAGE_FILL);

    gimp_palette_set_background($bgcolor);
    $tiled_punch_layer->fill(BG_IMAGE_FILL);
    if ($bgpattern) {
	print "Setting pattern\n";
	gimp_patterns_set_pattern($bgpattern);
	$tiled_img->bucket_fill($tiled_punch_layer, PATTERN_BUCKET_FILL,
				NORMAL, 100, 0, FALSE, 0,0);
    }
    gimp_palette_set_background([255, 255, 255]);  # Punch stencil
    $tiled_punch_stencil->fill(BG_IMAGE_FILL);

    # Add all the layers to the image
    $tiled_img->add_layer($tiled_punch_stencil,-1);
    $tiled_img->add_layer($tiled_drw,-1);
    $tiled_img->add_layer($tiled_punch_layer,-1);
    gimp_display_new($tiled_img);

    my $xpos = 0;

    # Set color for drawing in mask
    gimp_palette_set_background([0, 0, 0]);
    for $ly_idx (0..@$layout-1) {
	my ($img, $xpos, $ypos) = @{$layout->[$ly_idx]};
	($layer) = @{gimp_image_get_layers($img)};
	my($w,$h) = ($img->width, $img->height);
	$img->selection_all();
	$img->edit_copy($layer);
	$tiled_img->rect_select($xpos, $ypos, $w, $h, 0, 0, 0);
	$tiled_img->edit_paste($tiled_drw, 0)
	    ->floating_sel_anchor;

	# why is the selection cleared?
	$bw = 3;
	$tiled_img->rect_select($xpos-$bw,
				$ypos-$bw, $w+2*$bw, $h+2*$bw, 0, 0, 0);
	$tiled_img->edit_fill($tiled_punch_stencil, BG_IMAGE_FILL);

	# why is the selection cleared?
	$tiled_img->selection_none();
	$tiled_img->rect_select($xpos, $ypos, $w, $h, 0, 0, 0);
	$tiled_img->edit_fill($tiled_punch_mask, BG_IMAGE_FILL);

	$tiled_img->selection_none();
    }

    # Blur the punch stencil
    plug_in_gauss_rle(1, $tiled_img, $tiled_punch_stencil, 7, 1, 1);

    # Bump map
    plug_in_bump_map(1, $tiled_img, $tiled_punch_layer, $tiled_punch_stencil,
		     135, 45, 4,0,0,0,0,1,0, SPHERICAL);

    # Apply the shadow mask
    $tiled_img->remove_layer_mask($tiled_punch_layer, APPLY);
}

sub delete_images {
    my $imgs = shift;
    foreach $img (@$imgs) {
	$img->delete();
    }
}

######################################################################
#  Net is where main continues after it has connected to
#  gimp.
######################################################################
sub net {
    open(IN, shift(@ARGV));

    # Read the file list
    while(($fn,$descr) = get_next_record()) {
	last unless $fn;
	next unless -e $fn;
	print "fn = $fn\n";
	push(@imgs, load_img($fn, $max_height));
	push(@filenames, $fn);
    }

    print "Done reading ", scalar(@imgs), " images\n";

    # Now create a layout of the images. The layout algorithm
    # should really be parameterized.
    my ($total_width, $total_height, $layout) = hbox_vbox_create_layout(\@imgs);

    print "total_size = ($total_width $total_height)\n";

    # This is an example decoration. Others will be created in the future
    if ($decoration eq "drop-shadow") {
	decoration_drop_shadow($layout);
    } elsif ($decoration eq "sunken-windows") {
	decoration_sunken_windows($layout);
    } else {
	delete_images(\@imgs);
	die "Unknown decoration $decoration!\n";
    }

    $tiled_img->flatten() if $do_flatten;
    gimp_displays_flush();

    # Now create the index file
    if ($index_file) {
	open(INDEX, ">$index_file");
	for $idx (0..@filenames-1) {
	    my ($img, $xpos, $ypos) = @{$layout->[$idx]};
	    my($w,$h) = ($img->width, $img->height);

	    printf INDEX "%s %.0f %.0f %.0f %.0f %s\n",
	        $filenames[$idx], $xpos, $ypos, $xpos+$w, $ypos+$h, $descr;
	}
	close(INDEX);
    }

    # Clean up
    delete_images(\@imgs);
}

# Parse command line arguments
while($_ = $ARGV[0], /^-/) {
    shift;
    /^-help/ and do { print <<__; exit; };
make-img-map - Make an image map from a list of images

Syntax:
    gimp-make-img-map [-max_height mh] [-htmlindex hi] [-layoutwidth lw]
	         [-flatten] [-bgcolor clr] [-bgpattern ptn] list

Description:
    gimp-make-img-map communicates with Gimp through the Perl Net-Server
    and automizes the process of combining a list of images into an
    image map for use e.g. within a HTML page.

Options:
    -max_height mh   Set max height of images. (Default $max_height)
    -index if        Create an index file mapping filename to bounding box
	             coordinates in output image, where if is the name of
		     the index file. The index file may e.g. be translated by
		     a subsequent program into a html index file.
    -layoutwidth lw  Set total width of layout. (Default $layout_width)
    -flatten         Flatten the final image.
    -bgcolor         Set bg color.
    -bgpattern       Set bg pattern. Overrides the bgcolor.
    -padx px         Extra space around all images in x-direction. (Default $pad_x)
    -pady py         Extra space around all images in y-direction. (Default $pad_y)
    -gutterx gx      Space between images in x-direction. (Default $gutter_x)
    -gutterx gy      Space between images in y-direction. (Default $gutter_y)
    -decoration alg  Choose algorithm for drawing the decoration in the layout.
		     Known algorithms are:
			   drop-shadow
			   sunken-windows
		     Default is 'drop_shadow'.
__

    /^-max_height/  and do { $max_height = shift; next; };
    /^-index/       and do { $index_file = shift; next; };
    /^-layoutwidth/ and do { $layout_width = shift; next; };
    /^-flatten/     and do { $do_flatten++; next; };
    /^-bgcolor/     and do { $background = shift; next; };
    /^-bgpattern/   and do { $bgpattern = shift; next; };
    /^-decoration/  and do { $decoration = shift; next; };
    /^-gutterx/	    and do { $gutter_x = shift; next; };
    /^-guttery/	    and do { $gutter_y = shift; next; };
    /^-padx/	    and do { $pad_x = shift; next; };
    /^-pady/	    and do { $pad_x = shift; next; };
    die "Unknown option $_!\n";
}

# Translate background into a color according to the X11 color dbase.
exit main;
