#!/bin/bash
#
# Developed by Fred Weinhaus 8/4/2011 .......... 5/28/2015
#
# ------------------------------------------------------------------------------
# 
# Licensing:
# 
# Copyright © Fred Weinhaus
# 
# My scripts are available free of charge for non-commercial use, ONLY.
# 
# For use of my scripts in commercial (for-profit) environments or 
# non-free applications, please contact me (Fred Weinhaus) for 
# licensing arrangements. My email address is fmw at alink dot net.
# 
# If you: 1) redistribute, 2) incorporate any of these scripts into other 
# free applications or 3) reprogram them in another scripting language, 
# then you must contact me for permission, especially if the result might 
# be used in a commercial or for-profit environment.
# 
# My scripts are also subject, in a subordinate manner, to the ImageMagick 
# license, which can be found at: http://www.imagemagick.org/script/license.php
# 
# ------------------------------------------------------------------------------
# 
####
#
# USAGE: spots [-s size] [-t type] [-p pad] [-b bgcolor] [-e edge] [-E ecolor] [-B brightness] [-C Contrast] infile [spotfile] outfile 
# USAGE: spots [-help]
#
# OPTIONS:
#
# -s      size              spot size expressed as WxH in pixels; default=5x5
# -t      type              type of spot shape; choices are: circle (or c), 
#                           square (or s), or diamond (or d); default=circle
# -p      pad               padding or border around spot cell in pixels on 
#                           each side; integer>=0; default=1
# -b      bgcolor           background color to apply to image where spot
#                           does not cover; any valid IM color; default=black
# -e      edge              edge thickness accent around spot shape; 
#                           integer>=0; default=0
# -E      ecolor            color of accent edge around spot shape; 
#                           any valid IM color; default=gray
# -B      brightness        percent change in brightness of spots; 
#                           integer; default=0
# -C      contrast          percent change in contrast of spots; 
#                           integer; default=0
# 
# 
###
# 
# NAME: SPOTS 
# 
# PURPOSE: Converts the image into a series of uniform-colored spots.
# 
# DESCRIPTION: SPOTS converts the image into a series of uniform-colored 
# spots. The shape of spots allowed are: circle (ellipse), square (rectangle) 
# or diamond. The spot size may be adjusted. A colored edge may also be 
# placed around each spot. An optional spotfile may be provided to define 
# some other shape than those built-in.
# 
# 
# OPTIONS: 
# 
# -s size ... SIZE is the WxH dimensions of the spot cell. Values are a 
# pair of positive integers separated by an x. The default=5x5. If only 
# one value is provided, it will be used for both. This parameter is 
# ignored if a spotfile is provided.
#
# -t type ... TYPE is the type of spot shape. The choices are: circle (or c),
# square (or s), diamond (or d). These may be asymmetric as ellipses or 
# rectangles as controlled by the size parameters. This parameter is 
# ignored if a spotfile is provided.
# 
# -p pad ... PAD is the border around the spot cell. Values are integers>=0. 
# The default=0.
# 
# -b bgcolor ... BGCOLOR is the color to apply to the image between the spots. 
# Any valid IM color is allowed. The default=black.
# 
# -e edge ... EDGE is the thickness of the optional edge hightlight around 
# the spot shape. Values are integers>=0. The default=0.
# 
# -E ecolor ... ECOLOR is the color of the edge highlight. Any valid IM color 
# is allowed. The default=gray.
# 
# -B brightness ... BRIGHTNESS is the percent change in brightness of the 
# spot colors. Values are integers. The default=0
# 
# -C contrast ... CONTRAST is the percent change in contrast of the 
# spot colors. Values are integers. The default=0
# 
# The spotfile must be a binary mask with white for the shape and black 
# for the background and no alpha channel.
# 
# REQUIREMENTS: IM 6.3.6-1 or higher due to the use of
# -define distort:viewport=WxH+X+Y with -distort SRT.
# 
# CAVEAT: No guarantee that this script will work on all platforms, 
# nor that trapping of inconsistent parameters is complete and 
# foolproof. Use At Your Own Risk. 
# 
######
#

# set default values
size="5x5"			# spot WxH
type="circle"		# circle, square, diamond
pad=1				# border padding
edge=0				# edge width
ecolor="gray"		# edge color
bgcolor="black"		# background color
bri=0				# brightness
con=0				# contrast

# set directory for temporary files
dir="."    # suggestions are dir="." or dir="/tmp"

# set up functions to report Usage and Usage with Description
PROGNAME=`type $0 | awk '{print $3}'`  # search for executable on path
PROGDIR=`dirname $PROGNAME`            # extract directory of program
PROGNAME=`basename $PROGNAME`          # base name of program
usage1() 
	{
	echo >&2 ""
	echo >&2 "$PROGNAME:" "$@"
	sed >&2 -e '1,/^####/d;  /^###/g;  /^#/!q;  s/^#//;  s/^ //;  4,$p' "$PROGDIR/$PROGNAME"
	}
usage2() 
	{
	echo >&2 ""
	echo >&2 "$PROGNAME:" "$@"
	sed >&2 -e '1,/^####/d;  /^######/g;  /^#/!q;  s/^#*//;  s/^ //;  4,$p' "$PROGDIR/$PROGNAME"
	}


# function to report error messages
errMsg()
	{
	echo ""
	echo $1
	echo ""
	usage1
	exit 1
	}


# function to test for minus at start of value of second part of option 1 or 2
checkMinus()
	{
	test=`echo "$1" | grep -c '^-.*$'`   # returns 1 if match; 0 otherwise
    [ $test -eq 1 ] && errMsg "$errorMsg"
	}

# test for correct number of arguments and get values
if [ $# -eq 0 ]
	then
	# help information
   echo ""
   usage2
   exit 0
elif [ $# -gt 19 ]
	then
	errMsg "--- TOO MANY ARGUMENTS WERE PROVIDED ---"
else
	while [ $# -gt 0 ]
		do
			# get parameter values
			case "$1" in
		     -help)    # help information
					   echo ""
					   usage2
					   exit 0
					   ;;
				-s)    # get size
					   shift  # to get the next parameter
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID SIZE SPECIFICATION ---"
					   checkMinus "$1"
					   size=`expr "$1" : '\([0-9]*[x]*[0-9]*\)'`
					   [ "$size" = "" ] && errMsg "--- SIZE=$size MUST BE A PAIR OF POSITIVE INTEGERS (with no sign) SEPARATED BY AN x ---"
					   ;;
		 		-t)    # type
					   shift  # to get the next parameter
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID TYPE SPECIFICATION ---"
					   checkMinus "$1"
					   # test mode values
					   type="$1"
					   type=`echo "$type" | tr "[:upper:]" "[:lower:]"`
					   case "$type" in 
					   		circle|c) type="circle" ;;
					   		square|s) type="square" ;;
					   		diamond|d) type="diamond" ;;
					   		*) errMsg "--- TYPE=$type IS AN INVALID VALUE ---" 
					   	esac
					   ;;
				-p)    # get pad
					   shift  # to get the next parameter
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID PAD SPECIFICATION ---"
					   checkMinus "$1"
					   pad=`expr "$1" : '\([0-9]*\)'`
					   [ "$pad" = "" ] && errMsg "--- PAD=$pad MUST BE A NON-NEGATIVE INTEGER (with no sign) ---"
					   ;;
				-b)    # get bgcolor
					   shift  # to get the next parameter
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID BGCOLOR SPECIFICATION ---"
					   checkMinus "$1"
					   bgcolor="$1"
					   ;;
				-e)    # get edge
					   shift  # to get the next parameter
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID EDGE SPECIFICATION ---"
					   checkMinus "$1"
					   edge=`expr "$1" : '\([0-9]*\)'`
					   [ "$edge" = "" ] && errMsg "--- EDGE=$edge MUST BE A NON-NEGATIVE INTEGER (with no sign) ---"
					   ;;
				-E)    # get ecolor
					   shift  # to get the next parameter
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID ECOLOR SPECIFICATION ---"
					   checkMinus "$1"
					   ecolor="$1"
					   ;;
				-B)    # get brightness
					   shift  # to get the next parameter
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID BRIGHTNESS SPECIFICATION ---"
					   #checkMinus "$1"
					   bri=`expr "$1" : '\([-0-9]*\)'`
					   [ "$bri" = "" ] && errMsg "--- BRIGHTNESS=$bri MUST BE AN INTEGER (with no sign) ---"
					   ;;
				-C)    # get contrast
					   shift  # to get the next parameter
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID CONTRAST SPECIFICATION ---"
					   #checkMinus "$1"
					   con=`expr "$1" : '\([-0-9]*\)'`
					   [ "$con" = "" ] && errMsg "--- CONTRAST=$con MUST BE AN INTEGER (with no sign) ---"
					   ;;
			 	-)    # STDIN and end of arguments
					   break
					   ;;
				-*)    # any other - argument
					   errMsg "--- UNKNOWN OPTION ---"
					   ;;
		     	 *)    # end of arguments
					   break
					   ;;
			esac
			shift   # next option
	done
	#
	# get infile and outfile and spotfile
	if [ $# -eq 3 ]; then
		infile="$1"
		spotfile="$2"
		outfile="$3"
	elif [ $# -eq 2 ]; then
		infile="$1"
		outfile="$2"
	else
		errMsg "--- INCONSISTENT NUMBER OF IMAGES PROVIDED ---"
	fi
fi

# test that infile provided
[ "$infile" = "" ] && errMsg "NO INPUT FILE SPECIFIED"

# test that outfile provided
[ "$outfile" = "" ] && errMsg "NO OUTPUT FILE SPECIFIED"

# create temp files
tmpA1="$dir/spots_1_$$.mpc"
tmpB1="$dir/spots_1_$$.cache"
tmpA2="$dir/spots_2_$$.mpc"
tmpB2="$dir/spots_2_$$.cache"
trap "rm -f $tmpA1 $tmpB1 $tmpA2 $tmpB2;" 0
trap "rm -f $tmpA1 $tmpB1 $tmpA2 $tmpB2; exit 1" 1 2 3 15
trap "rm -f $tmpA1 $tmpB1 $tmpA2 $tmpB2; exit 1" ERR

# setup brightness contrast
if [ "$bri" = "0" -a "$con" = "0" ]; then
	bricon=""
else
	bricon="-brightness-contrast $bri,$con"
fi

# test input image
magick -quiet "$infile" $bricon -clamp +repage "$tmpA1" ||
	errMsg "--- FILE $infile DOES NOT EXIST OR IS NOT AN ORDINARY FILE, NOT READABLE OR HAS ZERO SIZE ---"

# test spot file if exists
if [ "$spotfile" != "" ]; then
	magick -quiet "$spotfile" -alpha off +repage "$tmpA2" ||
		errMsg "--- FILE $spotfile DOES NOT EXIST OR IS NOT AN ORDINARY FILE, NOT READABLE OR HAS ZERO SIZE ---"
fi

# get image width, height and aspect
ww=`magick $tmpA1 -ping -format "%w" info:`
hh=`magick $tmpA1 -ping -format "%h" info:`

# get size of spot and center of spot and last pixel of spot and pad size
if [ "$spotfile" = "" ]; then
	sw=`echo $size | cut -dx -f1`
	sh=`echo $size | cut -dx -f2`
	scx=`magick xc: -format "%[fx:($sw-1)/2]" info:`
	scy=`magick xc: -format "%[fx:($sh-1)/2]" info:`
	lx=$((sw-1))
	ly=$((sh-1))
	
	# get pad size
	pw=$((sw+2*pad))
	ph=$((sh+2*pad))

else
	pw=`magick $tmpA2 -ping -format "%w" info:`
	ph=`magick $tmpA2 -ping -format "%h" info:`
fi


if [ "$spotfile" = "" ]; then
	if [ "$pad" = "0" ]; then
		padding=""
	else 
		padding="-bordercolor black -border $pad"
	fi
	
	# create spot template
	if [ "$type" = "circle" ]; then
		magick -size ${sw}x${sh} xc:black \
			+antialias -fill white -draw "ellipse $scx,$scy $scx,$scy 0,360" -alpha off \
			$padding \
			$tmpA2
	
	elif [ "$type" = "square" ]; then
		magick -size ${sw}x${sh} xc:black \
			+antialias -fill white -draw "rectangle 0,0 $lx,$ly" -alpha off \
			$padding \
			$tmpA2
	
	elif [ "$type" = "diamond" ]; then
		magick -size ${sw}x${sh} xc:black \
			+antialias -fill white -draw "polygon $scx,0 $lx,$scy $scx,$ly 0,$scy" -alpha off \
			$padding \
			$tmpA2
	fi
fi

# compute xmin and ymin and virtual canvas size
xmin=`magick xc: -format "%[fx:ceil($ww/$pw)]" info:`
ymin=`magick xc: -format "%[fx:ceil($hh/$ph)]" info:`
www=`magick xc: -format "%[fx:$xmin*$pw]" info:`
hhh=`magick xc: -format "%[fx:$ymin*$ph]" info:`


# process image
if [ "$edge" = "0" ]; then
	magick \( $tmpA1 -define distort:viewport=${www}x${hhh}+0+0 -virtual-pixel mirror -distort SRT 0 \
		-scale ${xmin}x${ymin}! -scale ${www}x${hhh}! -crop ${ww}x${hh}+0+0 +repage \) \
		\( $tmpA2 -write mpr:tile +delete -size ${ww}x${hh}! tile:mpr:tile \) \
		-alpha off -compose copy_opacity -composite -compose over \
		-background $bgcolor -flatten  \
		"$outfile"
else
	magick \( $tmpA1 -define distort:viewport=${www}x${hhh}+0 -virtual-pixel mirror -distort SRT 0 \
		-scale ${xmin}x${ymin}! -scale ${www}x${hhh}! -crop ${ww}x${hh}+0+0 +repage \) \
		\( $tmpA2 -write mpr:tile +delete -size ${ww}x${hh}! tile:mpr:tile \) \
		\( -clone 1 -threshold 0 -edge $edge -clamp -fill $ecolor -opaque white -transparent black \) \
		\( -clone 0 -clone 1 -alpha off -compose copy_opacity -composite -compose over \
		-background $bgcolor -flatten \) \
		-delete 0,1 +swap -compose over -composite \
		"$outfile"
fi

exit 0