use strict;
use warnings;
use Image::Magick;
use Term::ANSIScreen qw/:color :cursor :screen/;
use Win32::Console::ANSI qw/ Cursor /;
use XML::Simple;
	$XML::Simple::PREFERRED_PARSER  = 'XML::Parser';
use constant FATAL => colored ['white on red'], "\aFATAL ERROR: ";
#===== Copyright 2009, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================
# anim_book.pl: Creates an animated GIF that turns "3D" pages of a book.
#===============================================================================================================================
#           Usage : perl anim_book.pl XmlFile
#       Arguments : XmlFile = path of XML file for the animation specifications.
#     Input Files : See arguments.
#    Output Files : The animated GIF specified in the XML data file.
# Temporary Files : None.
#         Remarks : See http://www.webpraxis.ab.ca/flips/anim_book.shtml for details.
#         History : v1.0.0 - October 6, 2009 - Original release.
#===============================================================================================================================
# 0) INITIALIZE:
$| = 1; 																				#set STDOUT buffer to auto-flush
cls();																					#clear screen
print colored ['black on white'], "$0\n\n\n",											#display program name
	  colored ['reset'], 'Initializing... ';											#report start of initialization

my $XmlFile			= shift || die FATAL, 'No XML file specified';						#get path of XML data file
my $Anim			= XMLin( $XmlFile, ForceArray => [qw(page)] );						#read the XML data file
my $ImgWidth		= $$Anim{pageDims}{width};											#parameterize the width of the images
my $ImgHeight		= $$Anim{pageDims}{height};											#parameterize the height of the images
my $ImgStretch		= $$Anim{pageDims}{stretch};										#parameterize the image distortion
my $NoPages			= @{ $$Anim{page} };												#get the number of specified images
my @Spinners		= ( '-', '\\', '|', '/' );											#define symbols for spinner

my $CanvasWidth		= 2 * $ImgWidth;													#set canvas width
my $CanvasHeight	= $ImgHeight + 2 * $ImgStretch;										#set canvas height
print colored ['bold green'], "XML data read\n\n";										#report end of initialization
#-------------------------------------------------------------------------------------------------------------------------------
# 1) LOAD AND SCALE THE IMAGES:
print 'Reading & scaling images... ';													#report start of image processing
my $Images = Image::Magick->new( magick => 'JPG' );										#instantiate an object for the images

die FATAL, "Even number of pages required\n" if $NoPages % 2;							#check for even number of images
die FATAL, "At least 4 pages required\n" if $NoPages < 4;								#check for minimum of 4 images
for( 0..$NoPages-1 ) {																	#repeat for each page image
	my $file = $$Anim{page}[$_];														# parameterize the file name
	die FATAL, "Cannot locate page file '$file'\n" unless -e $file;						# check page image existence
	$Images->Read( $file );																# read page image file
}																						#until all images read
$Images->Quantize( colors => 256, colorspace => 'RGB' );								#ensure uniform color space
$Images->Scale( geometry => "${ImgWidth}x${ImgHeight}!" );								#scale images to stated dimensions
print colored ['bold green'], "Done\n\n";												#report end of image processing
#-------------------------------------------------------------------------------------------------------------------------------
# 2) CREATE ANIMATION FRAMES:
print 'Creating animation frames... ';													#report start of frame processing
my($CursorX,$CursorY)	= Cursor();														#record cursor position

my $Canvas					= Image::Magick->new( magick => 'GIF' );					#instantiate an image object for the canvas
my $CanvasBkgrnd			= Image::Magick->new( magick => 'GIF' );					#instantiate an image object for the canvas background
my $Page					= Image::Magick->new( magick => 'GIF' );					#instantiate an image object for the distorted page
my $Frames					= Image::Magick->new( magick => 'GIF' );					#instantiate an image object for animation frames
my $FrameNo					= 0;														#init no of animation frames
my $Interpolate				= sub	{	my ( $lambda, $start, $end ) = @_;				#anonymous sub for interpolating x,y-coordinates
										int( $lambda * $end + ( 1. - $lambda ) * $start );
									};
my $DeltaLambda				= 1. / ( $$Anim{frames} + 1. );								#set "rotation" step-size

my $ImgX_VERSOtopLeft		= 0;														#top-left corner coords for 2D verso page
my $ImgY_VERSOtopLeft		= $ImgStretch;
my $ImgX_VERSObottomRight	= $ImgWidth - 1;											#bottom-right corner coords for 2D verso page
my $ImgY_VERSObottomRight	= $CanvasHeight - $ImgStretch - 1;

my $ImgX_RECTOtopLeft		= $ImgX_VERSObottomRight + 1;								#top-left corner coords for 2D recto page
my $ImgY_RECTOtopLeft		= $ImgY_VERSOtopLeft;
my $ImgX_RECTObottomRight	= $CanvasWidth - 1;											#bottom-right corner coords for 2D recto page
my $ImgY_RECTObottomRight	= $ImgY_VERSObottomRight;

my $x_left;																				#apex coords for distorted page
my $x_right;
my $y_top;
my $y_bottom;

for( my $imgIdx = 0; $imgIdx <= $NoPages - 2; $imgIdx += 2 ) {							#repeat for each pairing of pages
	&drawBackground( $imgIdx );															# draw corresponding verso and recto pages
	#QUARTER LEFT-HAND-RULE ROTATION OF RECTO IMAGE:
	for( my $lambda = 0.; $lambda < 1. - $DeltaLambda/2.; $lambda += $DeltaLambda ) {	# repeat for each rotation step
		$x_right		= &$Interpolate( $lambda,	$ImgX_RECTObottomRight,	$ImgX_RECTOtopLeft	);
		$y_top			= &$Interpolate( $lambda,	$ImgY_RECTOtopLeft,		0					);
		$y_bottom		= &$Interpolate( $lambda,	$ImgY_RECTObottomRight,	$CanvasHeight - 1	);
		&turnRectoImage( $imgIdx );							 							#  draw the distorted page
		&drawPage();							 										#  draw the animation frame
	}																					# until all rotation steps processed

	#QUARTER LEFT-HAND-RULE ROTATION OF VERSO IMAGE:
	for(my $lambda=$DeltaLambda; $lambda < 1.+$DeltaLambda/2.; $lambda+=$DeltaLambda) {	# repeat for each rotation step
		$x_left			= &$Interpolate( $lambda,	$ImgX_VERSObottomRight,	$ImgX_VERSOtopLeft		);
		$y_top			= &$Interpolate( $lambda,	0,						$ImgY_VERSOtopLeft		);
		$y_bottom		= &$Interpolate( $lambda,	$CanvasHeight - 1,		$ImgY_VERSObottomRight	);
		&turnVersoImage( $imgIdx + 1 );							 						#  draw the distorted page
		&drawPage();							 										#  draw the animation frame
	}																					# until all rotation steps processed
}																						#until all image pairs processed

undef $CanvasBkgrnd;																	#destroy image object for background
#QUARTER RIGHT-HAND-RULE ROTATION OF "BACK COVER":
for( my $lambda = 0.; $lambda < 1. - $DeltaLambda/2.; $lambda += $DeltaLambda ) {		#repeat for each rotation step
	$x_left			= &$Interpolate( $lambda,	$ImgX_VERSOtopLeft,		$ImgX_VERSObottomRight	);
	$y_top			= &$Interpolate( $lambda,	$ImgY_VERSOtopLeft,		0						);
	$y_bottom		= &$Interpolate( $lambda,	$ImgY_VERSObottomRight,	$CanvasHeight - 1		);
	&turnVersoImage( $NoPages - 1 );							 						# draw the distorted back cover
	&drawPage();							 											# draw the animation frame
}																						#until all rotation steps processed

#QUARTER RIGHT-HAND-RULE ROTATION OF "FRONT COVER":
for( my $lambda=$DeltaLambda; $lambda < 1.+$DeltaLambda/2.; $lambda+=$DeltaLambda ) {	# repeat for each rotation step
	$x_right		= &$Interpolate( $lambda,	$ImgX_RECTOtopLeft,		$ImgX_RECTObottomRight	);
	$y_top			= &$Interpolate( $lambda,	0,						$ImgY_RECTOtopLeft		);
	$y_bottom		= &$Interpolate( $lambda,	$CanvasHeight - 1,		$ImgY_RECTObottomRight	);
	&turnRectoImage( 0 );							 									# draw the distorted front cover
	&drawPage();							 											# draw the animation frame
}																						#until all rotation steps processed

print	locate( $CursorY, $CursorX ), clline,											#report end of frame processing
		colored ['bold green'], $FrameNo, " frames\n\n";
undef $Canvas;																			#destroy image objects no longer needed
undef $Images;
undef $Page;
#-------------------------------------------------------------------------------------------------------------------------------
# 3) CREATE ANIMATED GIF IMAGE:
print 'Creating animated GIF image... ';												#report start of animation processing
$Frames->Write																			#output the animation
			(	delay		=> $$Anim{delay},
				loop		=> $$Anim{loops},
				dispose		=> 'background',
				filename	=> $$Anim{output}
			);
print colored ['bold green'], $$Anim{output}, "\n";										#report end of animation processing
exit;
#===== SUBROUTINES =============================================================================================================
#     Usage : &drawBackground( $IMGINDEX );
#   Purpose : Draws the verso and recto background images corresponding the specified image index.
# Arguments : $IMGINDEX = index of the ImageMagick object $Images.
#      Subs : None.
#   Remarks : Background is common to the recto and verso sides of the current page pairs.
#   History : v1.0.0 - October 5, 2009 - Original release.

sub drawBackground {																	#begin sub
	my $imgIdx = shift;																	# parameterize the argument

	@$CanvasBkgrnd = ();																# clear the background canvas
	$CanvasBkgrnd->Set( size => "${CanvasWidth}x${CanvasHeight}" );						# set canvas size
	$CanvasBkgrnd->ReadImage( 'xc:transparent' );										# set canvas background to transparent
	$CanvasBkgrnd->Composite															# draw verso image of previous sheet
				(	image			=> $Images->[$imgIdx - 1],
					compose			=> 'Over',
					geometry		=> "${ImgWidth}x${ImgHeight}+$ImgX_VERSOtopLeft+$ImgY_VERSOtopLeft"
				)
	 if $imgIdx;																		#  only if it exists
	$CanvasBkgrnd->Composite															# draw recto image of next sheet
				(	image			=> $Images->[$imgIdx + 2],
					compose			=> 'Over',
					geometry		=> "${ImgWidth}x${ImgHeight}+$ImgX_RECTOtopLeft+$ImgY_RECTOtopLeft"
				)
	 if $imgIdx < $NoPages-2;															#  only if it exists
}																						#end sub drawBackground
#-------------------------------------------------------------------------------------------------------------------------------
#     Usage : &turnRectoImage( $IMGINDEX );
#   Purpose : Distorts the specified recto image into the required parallelogram.
# Arguments : $IMGINDEX = index of the ImageMagick object $Images.
#      Subs : None.
#   Remarks : None.
#   History : v1.0.0 - October 5, 2009 - Original release.

sub turnRectoImage {																	#begin sub
	my $imgIdx = shift;																	# parameterize the argument

	@$Page = ();																		# clear the distorted-image canvas
	$Page->Set( size => "${CanvasWidth}x${CanvasHeight}" );								# set canvas size
	$Page->ReadImage( 'xc:transparent' );												# set canvas background to transparent
	$Page->Composite																	# init canvas with designated image
				(	image			=> $Images->[$imgIdx],
					compose			=> 'Over',
					geometry		=> "${ImgWidth}x${ImgHeight}+$ImgX_RECTOtopLeft+$ImgY_RECTOtopLeft"
				);
	$Page->Distort																		# match image corners to perspective apexes:
				( 	points			=>	[												#  top left
											$ImgX_RECTOtopLeft,		$ImgY_RECTOtopLeft,		$ImgX_RECTOtopLeft,		$ImgY_RECTOtopLeft,
																						#  bottom left
											$ImgX_RECTOtopLeft,		$ImgY_RECTObottomRight,	$ImgX_RECTOtopLeft,		$ImgY_RECTObottomRight,
																						#  top right
											$ImgX_RECTObottomRight,	$ImgY_RECTOtopLeft,		$x_right,				$y_top,
																						#  bottom right
											$ImgX_RECTObottomRight,	$ImgY_RECTObottomRight,	$x_right,				$y_bottom
										],
					type			=> 'Bilinear',
					'virtual-pixel'	=> 'transparent',
					'best-fit'		=> 1,
				);
}																						#end sub turnRectoImage
#-------------------------------------------------------------------------------------------------------------------------------
#     Usage : &turnVersoImage( $IMGINDEX );
#   Purpose : Distorts the specified verso image into the required parallelogram.
# Arguments : $IMGINDEX = index of the ImageMagick object $Images.
#      Subs : None.
#   Remarks : None.
#   History : v1.0.0 - October 5, 2009 - Original release.

sub turnVersoImage {																	#begin sub
	my $imgIdx = shift;																	# parameterize the argument

	@$Page = ();																		# clear the distorted-image canvas
	$Page->Set( size => "${CanvasWidth}x${CanvasHeight}" );								# set canvas size
	$Page->ReadImage( 'xc:transparent' );												# set canvas background to transparent
	$Page->Composite																	# init canvas with designated image
				(	image			=> $Images->[$imgIdx],
					compose			=> 'Over',
					geometry		=> "${ImgWidth}x${ImgHeight}+$ImgX_VERSOtopLeft+$ImgY_VERSOtopLeft"
				);
	$Page->Distort																		# match image corners to perspective apexes:
				( 	points			=>	[												#  top left
											$ImgX_VERSOtopLeft,		$ImgY_VERSOtopLeft,		$x_left,				$y_top,
																						#  bottom left
											$ImgX_VERSOtopLeft,		$ImgY_VERSObottomRight,	$x_left,				$y_bottom,
																						#  top right
											$ImgX_VERSObottomRight,	$ImgY_VERSOtopLeft,		$ImgX_VERSObottomRight,	$ImgY_VERSOtopLeft,
																						#  bottom right
											$ImgX_VERSObottomRight,	$ImgY_VERSObottomRight,	$ImgX_VERSObottomRight,	$ImgY_VERSObottomRight
										],
					type			=> 'Bilinear',
					'virtual-pixel'	=> 'transparent',
					'best-fit'		=> 1,
				);
}																						#end sub turnVersoImage
#-------------------------------------------------------------------------------------------------------------------------------
#     Usage : &drawPage();
#   Purpose : Draws the current page atop any canvas background and adds the result to the animation frame sequence.
# Arguments : None.
#      Subs : None.
#   Remarks : None.
#   History : v1.0.0 - October 6, 2009 - Original release.

sub drawPage {																			#begin sub

	@$Canvas = ();																		# clear the frame canvas
	$Canvas->Set( size => "${CanvasWidth}x${CanvasHeight}" );							# set canvas size
	$Canvas->ReadImage( 'xc:transparent' );												# set canvas background to transparent
	$Canvas->Composite																	# add any background images to canvas
				(	image			=> $CanvasBkgrnd,
					compose			=> 'Over',
					geometry		=> "${CanvasWidth}x${CanvasHeight}+0+0"
				)
	 if defined $CanvasBkgrnd;
	$Canvas->Composite																	# add foreground page to canvas
				(	image			=> $Page,
					compose			=> 'Over',
					geometry		=> "${CanvasWidth}x${CanvasHeight}+0+0"
				);
	push @$Frames, @$Canvas;															# add frame canvas to animation sequence
	print	locate( $CursorY, $CursorX ), clline,										# report frame processing
			colored ['bold yellow'], '(', $Spinners[++$FrameNo % 4], ')';
}																						#end sub drawPage
#===== Copyright 2009, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================
# end of anim_book.pl
