use strict;
use File::Basename;
use Image::Magick;
#===== Copyright 2008, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================
# img2puzzle.pl: Puzzle image dicer with HTML page for playing the puzzle.
#===============================================================================================================================
#           Usage : perl img2puzzle.pl ImageFile TileWidth TileHeight Verbose
#       Arguments : ImageFile  = path of image file to be diced.
#                   TileWidth  = width of a tile in pixels
#                   TileHeight = height of a tile in pixels
#                   Verbose    = boolean flag for verbose output
#     Input Files : See arguments.
#    Output Files : Tile files in the form "basename_tile_nnn.gif" where "basename" denotes the basename of the
#                     source image file and "nnn" denotes the tile index number.
#                   Image GIF file in the form "basename_puzzle.gif" which is a scaled version of the source image.
#                   HTML page for playing the puzzle in the form "basename_puzzle.htm".
# Temporary Files : None.
#         Remarks : Requires PerlMagick. Used module from ImageMagick 6.3.7.
#         History : v1.0.0 - February 4, 2008  - Original release.
#                   v1.0.1 - February 11, 2008 - Scaling of puzzle image now done with force
#                   v1.0.2 - February 17, 2008 - Added call to "fileparse_set_fstype" to force Unix syntax
#===============================================================================================================================
# 0) INITIALIZE:
system( 'cls' ) if $^O =~ /^MSWin/;															#clear screen if running Windows
print "$0\n", '=' x length($0), "\n\n";														#display program name

my $SourceFile		= shift || die "ERROR: No image file specified\n";						#get path of source image file
my $TileWidth		= shift || die "ERROR: No tile width specified\n";						#get pixel width of a tile
my $TileHeight  	= shift || die "ERROR: No tile height specified\n";						#get pixel height of a tile
my $Verbose			= shift;																#get boolean flag for verbose mode
die "ERROR: Cannot locate image file\n" unless -e $SourceFile;

fileparse_set_fstype('');
my ( $basename,																				#extract basename &
	 $path																					#extract path
   )				= fileparse( $SourceFile, '\..*' );										# of source image file
my $TileBaseName	= "${basename}_tile_";													#compose basename of all tile files
my $HtmlFile		= "$path${basename}_puzzle.htm";										#compose filename of HTML page
my $PuzzleFile		= "$path${basename}_puzzle.gif";										#compose filename of puzzle image
my $PuzzleTable;																			#body of the puzzle table
local *HTML;																				#define filehandle for the HTML page
#-------------------------------------------------------------------------------------------------------------------------------
# 1) SETUP PUZZLE IMAGE:
my $source			= Image::Magick->new;													#instantiate an image object for the source
my ( $SourceWidth,																			#get width &
	 $SourceHeight																			#get height
   )				= ( $source->Ping( $SourceFile ) )[0,1];								#of source
undef $source;																				#destroy the image object for the source

my $No_Tile_Cols	= $SourceWidth / $TileWidth;											#compute raw number of tile columns
   $No_Tile_Cols	= int( ++$No_Tile_Cols )												#adjust to an integral number
					   unless $No_Tile_Cols == int( $No_Tile_Cols );						# if necessary
my $No_Tile_Rows	= $SourceHeight / $TileHeight;											#compute raw number of tile rows
   $No_Tile_Rows	= int( ++$No_Tile_Rows )												#adjust to an integral number
					   unless $No_Tile_Rows == int( $No_Tile_Rows );						# if necessary
my $TileCount		= $No_Tile_Cols * $No_Tile_Rows;										#compute total number of tiles

my $PuzzleWidth		= $No_Tile_Cols * $TileWidth;											#compute pixel width of puzzle image
my $PuzzleHeight	= $No_Tile_Rows * $TileHeight;                            				#compute pixel height of puzzle image
my $Puzzle			= Image::Magick->new( magick=>"GIF" );									#instantiate an image object for the puzzle
   $Puzzle->Read( $SourceFile );															#init puzzle with source image file
   $Puzzle->Scale( geometry=>"${PuzzleWidth}x${PuzzleHeight}!" );							#scale puzzle image
   $Puzzle->Write( filename => $PuzzleFile );												#save the puzzle image to file

print "Source Image:   $SourceFile\n",                                                      #echo initialization results
      "  Width:        $SourceWidth px\n",
      "  Height:       $SourceHeight px\n",
      "Puzzle Image:   $PuzzleFile\n",
      "  Width:        $PuzzleWidth px\n",
      "  Height:       $PuzzleHeight px\n",
      "HTML File:      $HtmlFile\n\n",
      "Puzzle Details: $TileCount tiles\n",
      "  No. of rows:  $No_Tile_Rows\n",
      "  No. of cols:  $No_Tile_Cols\n",
      "  Tile width:   $TileWidth px\n",
      "  Tile height:  $TileHeight px\n\n";
print( "Pause. Press the ENTER key to continue..." ), <STDIN> if $Verbose;
#-------------------------------------------------------------------------------------------------------------------------------
# 2) CREATE TILE IMAGES & PUZZLE TABLE:
{																							#start naked block
	my $tile			= Image::Magick->new( magick=>"GIF" );								# instantiate an image object for a tile
	my $tileIdx			= 0;																# tile index
	my $tileFile;																			# tile file
	my $geometry;																			# geometry of a tile: width, height & x,y offsets
	my $x_top_left;																			# x-coordinate of a tile's top-left corner
	my $y_top_left;																			# y-coordinate of a tile's top-left corner
	my $x_bottom_right;																		# x-coordinate of a tile's bottom-right corner
	my $y_bottom_right;																		# y-coordinate of a tile's bottom-right corner

	print "Creating tile images...\n";
	for ( $y_top_left  = 0;																	# for each tile row: start at top and work down
		  $y_top_left <= $PuzzleHeight - $TileHeight;
		  $y_top_left += $TileHeight
		) {
			$PuzzleTable	.= "<tr>\n";													#  init puzzle table row
			$y_bottom_right	= $y_top_left + $TileHeight - 1;								#  compute y-coord. of the tile's bottom-right corner
			for ( $x_top_left = 0;															#  for each tile column: work left to right
				  $x_top_left < $PuzzleWidth - 1;
				  $x_top_left += $TileWidth
				) {
					$tileFile		 =	"${TileBaseName}${tileIdx}.gif";					#   compose path of tile file
					$PuzzleTable	.=  qq|<td>\n| .										#   compose cell content: tile, hilight & error layers
										qq|<div class="puzzle_cell" onClick="puzzle_select($tileIdx);">\n| .
										qq| <img class="puzzle_tile"   name="tile$tileIdx" src="$tileFile" alt="">\n| .
										qq| <div class="puzzle_hilite" id="hilite$tileIdx"></div>\n| .
										qq| <div class="puzzle_error"  id="error$tileIdx">X</div>\n| .
										qq|</div>\n| .
										qq|</td>\n|;
					$x_bottom_right	 = $x_top_left + $TileWidth - 1;						#   compute x-coord. of the tile's bottom-right corner
					print "\tTile# $tileIdx \@ $x_top_left,$y_top_left-$x_bottom_right,$y_bottom_right "
					 if $Verbose;															#   report progress if requested

					$geometry	= "${TileWidth}x${TileHeight}+$x_top_left+$y_top_left";		#   define tile geometry
					$tile 		= $Puzzle->Clone();											#   init tile to full puzzle image
					$tile->Crop( geometry => $geometry );									#   crop tile area
					$tile->Set( page => '0x0+0+0' );										#   shrink canvas
					$tile->Write( filename => $tileFile );									#   save the tile image to file
					
					print "=> $tileFile\n" if $Verbose;										#   report progress if requested
					++$tileIdx;																#   update tile index
			}																				#  until all tile columns processed
			$PuzzleTable .= qq|</tr>\n|;													#  end table row
	}																						# until all tile rows processed
	undef $Puzzle;																			# destroy the puzzle image object
	undef $tile;																			# destroy the tile image object
}																							#end naked block
#-------------------------------------------------------------------------------------------------------------------------------
# 3) OUTPUT THE PUZZLE PAGE:
my $min				= sub { my( $x, $y ) = @_; return ( $x < $y ) ? $x : $y; };				#anonymous sub: returns the minimum of 2 numbers
my $BorderWidth		= 5;																	#border width of puzzle board
my $IE_Width		= $PuzzleWidth  + 2 * $BorderWidth;										#outer board width for IE
my $IE_Height		= $PuzzleHeight + 2 * $BorderWidth;										#outer board height for IE
my $ErrorFontSize	= int( 0.90 * &$min( $TileWidth, $TileHeight ) );						#define font size for indicating incorrect tile

open HTML, ">$HtmlFile" or die "ERROR: Cannot create HTML file: $!\n";						#open HTML file for output
print HTML <<PUZZLE_PAGE;																	#output filled-in template
<html>
<head>
	<title>Photo Puzzle Page</title>
	<style type="text/css">
		/* Puzzle board consists of 3 main layers: instructions, hint & tiles */
		DIV.puzzle_board {
			position:			relative;
			top:				0px;
			left:				0px;
			border:				${BorderWidth}px outset darkgray;
			width:				${PuzzleWidth}px;
			height:				${PuzzleHeight}px;
		}
		/* Puzzle board instructions layer */
		DIV.puzzle_board DIV#puzzle_howto {
			position:			absolute;
			top:				0px;
			left:				0px;
			width:				${PuzzleWidth}px;
			height:				${PuzzleHeight}px;
			background-color:	silver;
			text-align:			left;
			z-index:			1;
		}
		/* Puzzle board hint layer */
		DIV.puzzle_board IMG#puzzle_hint {
			position:			absolute;
			top:				0px;
			left:				0px;
			width:				${PuzzleWidth}px;
			height:				${PuzzleHeight}px;
			z-index:			2;
		}
		/* Puzzle board tiles layer */
		DIV.puzzle_board TABLE#puzzle_table {
			position:			absolute;
			top:				0px;
			left:				0px;
			border:				0;
			border-collapse:	collapse;
			width:				${PuzzleWidth}px;
			height:				${PuzzleHeight}px;
			z-index:			3;
		}
		/* Puzzle board tile cells */
		DIV.puzzle_board TABLE#puzzle_table TD {
			border:				0;
			padding:			0;
			width:				${TileWidth}px;
			height:				${TileHeight}px;
		}

		/* Puzzle tile cells consists of 3 layers: tile, hilight & error marker */
		DIV.puzzle_cell {
			position:			relative;
			top:				0px;
			left:				0px;
			width:				${TileWidth}px;
			height:				${TileHeight}px;
		}
		/* Puzzle tile layer */
		DIV.puzzle_cell IMG.puzzle_tile {
			position:			absolute;
			top:				0px;
			left:				0px;
			width:				${TileWidth}px;
			height:				${TileHeight}px;
			z-index:			1;
		}
		/* Tile-hilight layer */
		DIV.puzzle_cell DIV.puzzle_hilite {
			position:			absolute;
			top:				0px;
			left:				0px;
			background-color:	red;
			opacity:			0.2;
			filter:				alpha(opacity=20);
			width:				${TileWidth}px;
			height:				${TileHeight}px;
			visibility:			hidden;
			z-index:			2;
		}
		/* Incorrect-tile marker layer */
		DIV.puzzle_cell DIV.puzzle_error {
			position:			absolute;
			top:				0px;
			left:				0px;
			color:				red;
			font-family:		arial,helvetica,sans-serif;
			font-size:			${ErrorFontSize}px;
			font-weight:		bold;
			width:				${TileWidth}px;
			height:				${TileHeight}px;
			text-align:			center;
			visibility:			hidden;
			z-index:			3;
		}
	</style>
	<!--[if IE]>
	<style type="text/css">
		/* Override for puzzle board dimensions */
		DIV.puzzle_board {
			width:				${IE_Width}px;
			height:				${IE_Height}px;
		}
	</style>
	<![endif]-->
	<script language="Javascript">
		var ButtonIds				= new Array( 'button_howto', 'button_scramble', 'button_hint', 'button_errors', 'button_solve' );
		var FirstTile				= null;			/* Index of first of two tiles selected */
		var FirstElemHilite;						/* Hilight element corresponding to FirstTile */
		var	Status_ErrorsToggled	= false;		/* Boolean flag indicating if error markers displayed */
		var	Status_Scrambled		= false;		/* Boolean flag indicating if tiles have been scrambled */
		var	Status_Solving			= false;		/* Boolean flag indicating if puzzle solving in progress */
		var TileCount				= $TileCount;	/* Total number of tiles */

		/* Returns Boolean for whether a tile's image file agrees wih the tile's index or not */
		function puzzle_concordance( tileIdx ) {
			var imgSrc	= document['tile' + tileIdx.toString()].src;
			var regex	= new RegExp( '_' + tileIdx.toString() + '\\.gif\$' );
			return regex.test(imgSrc);
		}
		/* Checks if the puzzle has been solved */
		function puzzle_check() {
			for( var tileIdx = 0; tileIdx < TileCount; ++tileIdx ) {
				if( !puzzle_concordance( tileIdx ) ) return;
			}
			alert("\\nBravo! Well done!");
			Status_Scrambled = false;
			puzzle_toggleButtons( new Array(1,2,3,4) );
		}
		/* Controls the selection of tile pairs */
		function puzzle_select( tileIdx ) {
			if( !Status_Scrambled || Status_ErrorsToggled || Status_Solving ) return;
			var selectedTile = 'tile' + tileIdx.toString();
			if( !FirstTile ) {
				FirstTile	 						= selectedTile;
				FirstElemHilite						= document.getElementById( 'hilite' + tileIdx.toString() );
				FirstElemHilite.style.visibility	= 'visible';
			} else {
				puzzle_swap( FirstTile, selectedTile );
				FirstElemHilite.style.visibility	= 'hidden';
				FirstTile							= null;
				puzzle_check();
			}
		}
		/* Randomly scrambles the tiles */
		function puzzle_scramble() {
			var targetName;
			var sourceName;
			for( var tileIdx = TileCount-1; tileIdx > 0; --tileIdx ) {
				targetName = 'tile' + tileIdx.toString();
				sourceName = 'tile' + ( Math.floor( tileIdx * Math.random() ) ).toString();
				puzzle_swap( targetName, sourceName );
			}
			Status_Scrambled	= 1;
			puzzle_toggleButtons( new Array(1,2,3,4) );
		}
		/* Solves the puzzle by tile swapping every 75 milliseconds */
		function puzzle_solve() {
			if( confirm("\\nAre you sure?") ) {
				Status_Solving = true;
				if( FirstTile ) FirstElemHilite.style.visibility = 'hidden';
				puzzle_toggleButtons( new Array(0,2,3,4) );
				for( var imgIdx = 0; imgIdx < TileCount-1; ++imgIdx ) {
					setTimeout( eval( "\\"puzzle_correct( " + imgIdx + " )\\"" ), 75*imgIdx );
				}
			}
		}
		/* Locates and swaps in the correct image for a specified tile */
		function puzzle_correct( imgIdx ) {
			var imgSrc;
			var regex		= new RegExp('_' + imgIdx +'\\.gif\$');
			var sourceName;
			for( var tileIdx = imgIdx; tileIdx < TileCount; ++tileIdx ) {
				sourceName	= 'tile' + tileIdx.toString();
				imgSrc		= document[sourceName].src;
				if( regex.test(imgSrc) ) break;
			}
			puzzle_swap( 'tile' + imgIdx.toString(), sourceName );
			if( imgIdx == TileCount - 2 ) {
				Status_Scrambled	= false;
				Status_Solving		= false;
				FirstTile			= null;
				puzzle_toggleButtons( new Array(0,1) );
			}
		}
		/* Swaps images between the specified tile pair */
		function puzzle_swap( tile1Name, tile2Name ) {
			var temp				= document[tile1Name].src;
			document[tile1Name].src = document[tile2Name].src;
			document[tile2Name].src = temp;
		}
		/* Toggles the disabled state for the specified button indexes */
		function puzzle_toggleButtons( Idxs ) {
			var elem_button;
			for( var i = 0; i < Idxs.length; ++i ) {
				elem_button				= document.getElementById( ButtonIds[Idxs[i]] );
				elem_button.disabled	= !elem_button.disabled;
			}
		}
		/* Toggles the puzzle-image display by changing its layer index */
		function puzzle_toggleHint() {
			puzzle_toggleButtons( new Array(0,3,4) );
			document.puzzle_image.style.zIndex = (document.puzzle_image.style.zIndex == 4) ? 2 : 4;
		}
		/* Toggles the instructions display by changing its layer index */
		function puzzle_toggleHowTo() {
			puzzle_toggleButtons( (Status_Scrambled) ? new Array(2,3,4) : new Array('1') );
			var elem_howto			= document.getElementById( 'puzzle_howto' );
			elem_howto.style.zIndex	= (elem_howto.style.zIndex == 4) ? 1 : 4;
		}
		/* Toggles the display of the markers for misplaced tiles */
		function puzzle_toggleErrors() {
			puzzle_toggleButtons( new Array(0,2,4) );
			var elem_error;
			if( !Status_ErrorsToggled ) {
				for( var tileIdx = 0; tileIdx < TileCount; ++tileIdx ) {
					if( !puzzle_concordance( tileIdx ) ) {
						elem_error					= document.getElementById( 'error' + tileIdx.toString() );
						elem_error.style.visibility	= 'visible';
					}
				}
			} else {
				for( var tileIdx = 0; tileIdx < TileCount; ++tileIdx ) {
					elem_error					= document.getElementById( 'error' + tileIdx.toString() );
					elem_error.style.visibility	= 'hidden';
				}
			}
			Status_ErrorsToggled = !Status_ErrorsToggled;
		}
	</script>
</head>
<body>
<div align="center">
	<form>
		<input type="button" id="button_howto"    value="Show/Hide Instructions" onClick="puzzle_toggleHowTo();">
		<input type="button" id="button_scramble" value="Scramble"               onClick="puzzle_scramble();">
		<input type="button" id="button_hint"     value="Show/Hide Hint"         onClick="puzzle_toggleHint();"   disabled>
		<input type="button" id="button_errors"   value="Show/Hide Errors"       onClick="puzzle_toggleErrors();" disabled>
		<input type="button" id="button_solve"    value="I Give Up!"             onClick="puzzle_solve();"        disabled>
	</form>
	<div class="puzzle_board">
		<div id="puzzle_howto">
			<ol>
				<li>Begin by examining the image below whose reconstruction will be the objective of the puzzle.
				<li>Press the <b><i>Scramble</i></b> button in order to mix up the tiles randomly.
				<li>Re-assemble the tiles in their correct order by relocating the tiles in pairs. This is
                    done by first single-clicking on a tile. Then, after single-clicking on any second tile,
                    the two tiles will swap their positions. If you need some help, you can click on the
                    <b><i>Show/Hide Hint</i></b> button to display the original image.
                    Moreover, the <b><i>Show/Hide Errors</i></b> button will indicate the misplaced tiles.
				<li>When all the tiles are back in their original locations, you will be notified.
				<li>Finally, if you press the <b><i>I Give Up!</i></b> button, the misplaced tiles will be
					relocated to their correct position.
			</ol>
		</div>
		<img id="puzzle_hint" name="puzzle_image" src="$PuzzleFile">
		<table id="puzzle_table">
			$PuzzleTable
		</table>
	</div>
</div>
</body>
</html>
PUZZLE_PAGE
close HTML;																					#close HTML file
exit;
#===== Copyright 2008, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================
# end of img2puzzle.pl