#!/bin/bash
#
# USAGE: pagecurl [options] infile outfile
#
# pagecurl - Applies a pagecurl effect to the lower right corner of an image.
#
# This program generates a pagecurl effect to the lower right corner of an
# image. The apex of the curl is nominally in the upper right corner of the
# image, but can be adjusted. The curl is always right to left. The curl can
# be shaded and/or colored. The removed area can be colored or transparent.
# Note that this is a 2D simulation and not a true 3D effect.
#
# OPTIONS:
#
#      -a amount       amount of pagecurl expressed as percent of image
#                      width; integer>=5; default=50
#      -m mode         mode of shading curl; plain, grad or doublegrad;
#                      default=grad
#      -c color        color to apply to curl; any valid IM color;
#                      default=white
#      -b bgcolor      background color to apply to image where curled away;
#                      any valid IM color; default=none (transparent)
#      -e ellipticity  curl flattening from circle to ellipse;
#                      0<=float<1; default=0 for circle; recommended value
#                      other 0 is 0.5 for ellipse shape
#      -x xcoord       x coordinate for apex of curl;
#                      default=right image edge
#      -y ycoord       y coordinate for apex of curl;
#                      default=upper image edge
#      -g gcontrast    contrast adjustment for mode=grad; 0<=integer<=100;
#                      increases contrast only; default=15
#      -d dcontrast    contrast adjustment for mode=doublegrad;
#                      -100<=integer<=100; positive increase contrast;
#                      negative decreases contrast; default=0
#      -i prefix       generate and save the overlay and masking images
#                      in PNG format using the filename prefix provided.
#
#      -help           output this documentation
#
###
#
# -a amount ... AMOUNT of pagecurl expressed as percent of image width. Values
# are in range integer>=5. The default=50
#
# -m mode ... MODE shading on the curl. Choices are: plain (or p), grad (or g)
# for gradient, or doublegrad (or d) for double gradient. Default=grad.
#
# -c color ... COLOR is the color to apply to curl. Any valid IM color is
# allowed. The default=white.
#
# -b bgcolor ... BGCOLOR is the color to apply to curled away part of the image.
# Any valid IM color is allowed. The default=none for transparent.
#
# -e ellipticity ... ELLIPTICITY is the amount of curl flattening from a circle
# to an ellipse. Values are in range 0<=float<1. The default=0 for circle.
# Recommended value other 0 is 0.5 for ellipse shape.
#
# -x xcoord ... XCOORD is the X coordinate for the apex of the curl. Values
# are 0<integers<width. The default is the right edge of the image.
#
# -y ycoord ... YCOORD is the Y coordinate for the apex of the curl. Values
# are integers. The default is the upper edge of the image.
#
# -g gcontrast ... GCONTRAST is the contrast adjustment for mode=grad. Values
# are in range 0<=integer<=100. This increases contrast only. The default=15
#
# -d dcontrast ... DCONTRAST is the contrast adjustment for mode=doublegrad.
# Values are in range -100<=integer<=100. Positive values increase contrast.
# Negative values decrease contrast. The default=0
#
# -i prefix ... save the overlay and masking images generated using the
# prefix/path provided. Filename created are "{prefix}_overlay.png" and
# "{prefix}_mask.png".
#
#
# Thanks to Anthony Thyssen for critiqing the original version and for
# several useful suggestions for improvement.
#
# 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.
#
######
#
# Original script by Fred Weinhaus, 23 May 2010
# Re-developed by Anthony Thyssen, 4 December 2010
#

# set default values
amount=50            # approx percentage of curl of image width; 5 to 100
mode="grad"          # plain, grad, doublegrad
color="white"        # color for plain or gradients
bgcolor="none"       # background color
ellipticity=0        # how circular the curl is 0 circle 0.5 is elliptical
xcoord=""            # apex of cone; default=width (UL corner)
ycoord=""            # apex of cone; default=0 (UL corner)
gcontrast=15         # gradient contrast values; non-neg interger percent
dcontrast=0          # doublegrad contrast values; pos or neg integer percent
swidth=2             # strokewidth; thickness of curl

# 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 -n '/^###/q;  /^#/!q;  s/^#//;  s/^ //;  3,$p' \
      "$PROGDIR/$PROGNAME"
  exit 1
}
usage2() {
  echo >&2 ""
  echo >&2 "$PROGNAME:" "$@"
  sed >&2 -n '/^######/q;  /^#/!q;  s/^#*//;  s/^ //;  3,$p' \
      "$PROGDIR/$PROGNAME"
  exit 0
}


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


# 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
  echo ""
  usage2
elif [ $# -gt 20 ]; then
  errMsg "--- TOO MANY ARGUMENTS WERE PROVIDED ---"
fi

while [ $# -gt 0 ]
  do
    # get parameter values
    case "$1" in
    -help)    # help information
        echo ""
        usage2
        exit 0
        ;;
    -a)  # get amount
        shift  # to get the next parameter
        # test if parameter starts with minus sign
        errorMsg="--- INVALID AMOUNT SPECIFICATION ---"
        checkMinus "$1"
        amount=`expr "$1" : '\([0-9]*\)'`
        [ "$amount" = "" ] && errMsg "--- AMOUNT=$amount MUST BE A NON-NEGATIVE INTEGER (with no sign) ---"
        test1=`echo "$amount < 5" | bc`
        [ $test1 -eq 1 ] && errMsg "--- AMOUNT=$amount MUST BE AN GREATER THAN 4 ---"
        ;;
    -m) # get  mode
        shift  # to get the next parameter
        # test if parameter starts with minus sign
        errorMsg="--- INVALID MODE SPECIFICATION ---"
        checkMinus "$1"
        mode=`echo "$1" | tr '[A-Z]' '[a-z]'`
        case "$mode" in
          plain|p) mode=plain ;;
          grad|g) mode=grad ;;
          doublegrad|d) mode=doublegrad ;;
          *) errMsg "--- MODE=$mode IS AN INVALID VALUE ---" ;;
        esac
        ;;
    -c) # get color
        shift  # to get the next parameter
        # test if parameter starts with minus sign
        errorMsg="--- INVALID COLOR SPECIFICATION ---"
        checkMinus "$1"
        color="$1"
        ;;
    -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 ellipticity
        shift  # to get the next parameter
        # test if parameter starts with minus sign
        errorMsg="--- INVALID ELLIPTICITY SPECIFICATION ---"
        checkMinus "$1"
        ellipticity=`expr "$1" : '\([.0-9]*\)'`
        [ "$ellipticity" = "" ] && errMsg "--- ELLIPTICITY=$arc MUST BE A NON-NEGATIVE FLOAT (with no sign) ---"
        test1=`echo "$ellipticity < 0" | bc`
        test2=`echo "$ellipticity >= 1" | bc`
        [ $test1 -eq 1 -o $test2 -eq 1 ] && errMsg "--- ELLIPTICITY=$arc MUST BE A FLOAT GREATER THAN OR EQUAL 0 AND LESS THAN 1 ---"
        ;;
    -x) # get xcoord
        shift  # to get the next parameter
        # test if parameter starts with minus sign
        errorMsg="--- INVALID XCOORD SPECIFICATION ---"
        checkMinus "$1"
        xcoord=`expr "$1" : '\([0-9]*\)'`
        [ "$xcoord" = "" ] && errMsg "--- XCOORD=$xcoord MUST BE A NON-NEGATIVE INTEGER ---"
        ;;
    -y) # get ycoord
        shift  # to get the next parameter
        # test if parameter starts with minus sign
        #errorMsg="--- INVALID YCOORD SPECIFICATION ---"
        #checkMinus "$1"
        ycoord=`expr "$1" : '\([-0-9]*\)'`
        [ "$ycoord" = "" ] && errMsg "--- YCOORD=$ycoord MUST BE AN INTEGER ---"
        ;;
    -g) # get gcontrast
        shift  # to get the next parameter
        # test if parameter starts with minus sign
        errorMsg="--- INVALID GCONTRAST SPECIFICATION ---"
        checkMinus "$1"
        gcontrast=`expr "$1" : '\([0-9]*\)'`
        [ "$gcontrast" = "" ] && errMsg "--- GCONTRAST=$gcontrast MUST BE A NON-NEGATIVE INTEGER (with no sign) ---"
        test1=`echo "$gcontrast < 0" | bc`
        test2=`echo "$gcontrast > 100" | bc`
        [ $test1 -eq 1 -o $test2 -eq 1 ] && errMsg "--- GCONTRAST=$gcontrast MUST BE AN INTEGER BETWEEN 0 AND 100 ---"
        ;;
    -d) # get dcontrast
        shift  # to get the next parameter
        # test if parameter starts with minus sign
        #errorMsg="--- INVALID DCONTRAST SPECIFICATION ---"
        #checkMinus "$1"
        dcontrast=`expr "$1" : '\([-0-9]*\)'`
        [ "$dcontrast" = "" ] && errMsg "--- DCONTRAST=$dcontrast MUST BE AN INTEGER ---"
        test1=`echo "$dcontrast < -100" | bc`
        test2=`echo "$dcontrast > 100" | bc`
        [ $test1 -eq 1 -o $test2 -eq 1 ] && errMsg "--- DCONTRAST=$dcontrast MUST BE AN INTEGER BETWEEN -100 AND 100 ---"
        ;;
    -i) # save overlay and masking images
        shift
        prefix="$1"
        ;;

    --) shift; break ;;  # end of arguments
    -)  break ;;         # STDIN and end of arguments
    -*) errMsg "--- UNKNOWN OPTION ---" ;;
    *)  break ;;         # end of arguments
  esac
  shift   # next option
done
#
# get infile and outfile
infile="$1"
outfile="$2"

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

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

# create directory for temporary files in /tmp or $TMPDIR
tmp=`mktemp -d "${TMPDIR:-/tmp}/$PROGNAME.XXXXXXXXXX"` ||
  { echo >&2 "$PROGNAME: Unable to create temporary file"; exit 10;}
trap 'rm -rf "$tmp"' 0   # remove on any exit
trap 'exit 2' 1 2 3 15   # error exit on interupt

tmp1="$tmp/pagecurl_1.mpc"  # input file
tmp2="$tmp/pagecurl_2.mpc"  # overlay image
tmp3="$tmp/pagecurl_3.mpc"  # ellipse / transparency mask

# test input image
convert -quiet -regard-warnings "$infile" +gravity +repage "$tmp1" ||
        errMsg "--- FILE $infile DOES NOT EXIST OR IS NOT AN ORDINARY FILE, NOT READABLE OR HAS ZERO SIZE ---"

ww=`convert $tmp1 -ping -format "%w" info:`
hh=`convert $tmp1 -ping -format "%h" info:`
wm1=`convert xc: -format "%[fx:$ww-1]" info:`
hm1=`convert xc: -format "%[fx:$hh-1]" info:`
xc=`convert xc: -format "%[fx:$ww/2]" info:`
yc=`convert xc: -format "%[fx:$hh/2]" info:`

if [ "X$xcoord" = "X" -a "X$ycoord" = "X" ]; then
  xcoord=$wm1
  ycoord=0
elif [ "X$xcoord" = "X" -a "X$ycoord" != "X" ]; then
  xcoord=$wm1
elif [ "X$xcoord" != "X" -a "X$ycoord" = "X" ]; then
  ycoord=0
fi

# set vertex nominally at upper left corner
x1=$xcoord
y1=$ycoord

# compute approx right angle
# dx=pixel distance from lower right corner along bottom edge
dx=`convert xc: -format "%[fx:$ww*$amount/100]" info:`
rangle=`convert xc: -format "%[fx:(180/pi)*atan($dx/($hh-$y1))]" info:`

# compute a=rx as semicircle arc length = dx
# compute b from a and ellipticity
a=`convert xc: -format "%[fx:($dx+$ww-$x1)/pi]" info:`
b=`convert xc: -format "%[fx:$a*(1-$ellipticity)]" info:`

# get diameter by sqrt(2) and a bit more to use for initial ellipse image as it will be trimmed
a2=`convert xc: -format "%[fx:3*$a]" info:`
a2h=`convert xc: -format "%[fx:$a2/2]" info:`

# compute approx center angle from p1 to dx and a
prx=`convert xc: -format "%[fx:$ww-$dx]" info:`
pry=$(($hh-1))
lenr=`convert xc: -format "%[fx:hypot(($y1-$pry),($x1-$prx))]" info:`
dangle=`convert xc: -format "%[fx:(180/pi)*asin(($a/2)/$lenr)]" info:`
angle=`convert xc: -format "%[fx:$rangle+$dangle]" info:`


# create ellipse (and mask) for curl
convert -size ${a2}x${a2} xc:none -fill white \
        -draw "translate $a2h,$a2h rotate $angle ellipse 0,0 $a,$b 0,360" \
        -trim +repage $tmp3

# get width, height of ellipse image
eww=`convert $tmp3 -ping -format "%w" info:`
ehh=`convert $tmp3 -ping -format "%h" info:`

# get center and upper left corner of trimmed ellipse image
# for inserting into full size image for mask
y2=`convert xc: -format "%[fx:$hh-$ehh/2]" info:`
x2=`convert xc: -format "%[fx:$x1-($y2-$y1)*tan(pi*$angle/180)]" info:`
x0=`convert xc: -format "%[fx:$x2-$eww/2]" info:`
y0=`convert xc: -format "%[fx:$y2-$ehh/2]" info:`

# insert ellipse image in white background at correct location to form mask
testx=`convert xc: -format "%[fx:sign($x0)]" info:`
testy=`convert xc: -format "%[fx:sign($y0)]" info:`
if [ $testx -eq 1 -a $testy -eq 1 ]; then
        offsets="+${x0}+${y0}"
elif [ $testx -eq -1 -a $testy -eq 1 ]; then
        offsets="${x0}+${y0}"
elif [ $testx -eq 1 -a $testy -eq -1 ]; then
        offsets="+${x0}${y0}"
elif [ $testx -eq -1 -a $testy -eq -1 ]; then
        offsets="${x0}${y0}"
fi

# get length from apex p1 to center of ellipse p2
len=`convert xc: -format "%[fx:hypot(($x2-$x1),($y2-$y1))]" info:`

# get complement rotation angle
angle2=`convert xc: -format "%[fx:-90+$angle]" info:`


# compute tangent points as if ellipse at origin and apex vertically above at len
# ellipse (x/a)^2 + (y/b)^2 = 1, where a and b are semi-major and semi-minor radii
# differentiate: dy/dx = slope at tangent = -(x*b^2)/(y*a^2)
# tangent line to apex (x1,y1): y-y1=-(x-x1)*(x*b^2)/(y*a^2)
# solve for y and substitute into equation of ellipse to find x from quadratic equation
p0x=0
p0y=-$len
A=`convert xc: -format "%[fx:$a*$a*$p0y*$p0y + $b*$b*$p0x*$p0x]" info:`
B=`convert xc: -format "%[fx:-2*$a*$a*$b*$b*$p0x]" info:`
C=`convert xc: -format "%[fx:$a*$a*$a*$a*($b*$b-$p0y*$p0y)]" info:`
p3x=`convert xc: -format "%[fx:(-$B-sqrt($B*$B-4*$A*$C))/(2*$A)]" info:`
p4x=`convert xc: -format "%[fx:(-$B+sqrt($B*$B-4*$A*$C))/(2*$A)]" info:`
p3y=`convert xc: -format "%[fx:($a*$a*$b*$b-$b*$b*$p0x*$p3x)/($a*$a*$p0y)]" info:`
p4y=`convert xc: -format "%[fx:($a*$a*$b*$b-$b*$b*$p0x*$p4x)/($a*$a*$p0y)]" info:`

# rotate p2 and p3 relative to p1 by angle3
x3=`convert xc: -format "%[fx:($p3x)cos(pi*$angle/180)-($p3y+$len)sin(pi*$angle/180)+$x1]" info:`
y3=`convert xc: -format "%[fx:($p3x)sin(pi*$angle/180)+($p3y+$len)cos(pi*$angle/180)+$y1]" info:`
x4=`convert xc: -format "%[fx:($p4x)cos(pi*$angle/180)-($p4y+$len)sin(pi*$angle/180)+$x1]" info:`
y4=`convert xc: -format "%[fx:($p4x)sin(pi*$angle/180)+($p4y+$len)cos(pi*$angle/180)+$y1]" info:`

# create triangle mask (using the color posibly given by user)
convert -size ${ww}x${hh} xc:none \
        -fill $color -stroke $color -strokewidth $swidth \
        -draw "polygon $x1,$y1 $x3,$y3 $x4,$y4" $tmp2

# Cut out an elliptical shape from trianglar overlay
convert $tmp2 $tmp3 -geometry $offsets -compose DstOut -composite $tmp2

# At this point the ellipse mask in no longer needed!

# setup for grad or doublegrad
if [ "$mode" = "grad" ]; then
  lo=$gcontrast
  hi=$((100-$lo))
  process="-level ${lo}x${hi}% +level-colors black,$color"
elif [ "$mode" = "doublegrad" ]; then
  lo=`convert xc: -format "%[fx:abs($dcontrast)]" info:`
  hi=$((100-$lo))
  test=`convert xc: -format "%[fx:sign($dcontrast)]" info:`
  if [ $test -eq 1 ]; then
    process="-level ${lo}x${hi}%"
  else
    process="+level ${lo}x${hi}%"
  fi
  process="$process -solarize 50% -level 0x50% +level-colors black,$color"
fi

# IF not generating a plain paper curl, recolor the overlay,
# using a -sparse-color gradient, with appropiate processing.
if [ "$mode" != "plain" ]; then
  convert -size ${ww}x${hh} xc: -sparse-color Barycentric \
          "$x3,$y3 white $x4,$y4 black" \
          $process \
          $tmp2 +swap -compose In -composite $tmp2
fi

# FUTURE add a offset shadow to to the overlay image.

# create the transparency mask (using an ellipse and tangent line)
convert -size ${ww}x${hh} xc:white \
        -fill red -draw "line $x1,$y1 $x4,$y4" \
        -draw "translate $x2,$y2 rotate $angle ellipse 0,0 $a,$b 0,360" \
        -fill black -draw "color $wm1,$hm1 floodfill" \
        -fill white +opaque black -blur 2x65000 -level 50x100% \
        -alpha shape $tmp3


# Output overlay and transparency masks
if [ "X$prefix" != "X" ]; then
  convert $tmp2 "${prefix}_overlay.png"   # shaped overlay image
  convert $tmp3 "${prefix}_mask.png"      # shaped masking image
fi

# Do a "paint 'n' mask" of the original image, outputing the final image
convert $tmp1 $tmp2 -composite \
        $tmp3 -compose DstIn -composite \
        -compose over -bordercolor $bgcolor -border 0 \
        "$outfile"

exit 0

