#!/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&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 magick -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=`magick $tmp1 -ping -format "%w" info:` hh=`magick $tmp1 -ping -format "%h" info:` wm1=`magick xc: -format "%[fx:$ww-1]" info:` hm1=`magick xc: -format "%[fx:$hh-1]" info:` xc=`magick xc: -format "%[fx:$ww/2]" info:` yc=`magick 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=`magick xc: -format "%[fx:$ww*$amount/100]" info:` rangle=`magick 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=`magick xc: -format "%[fx:($dx+$ww-$x1)/pi]" info:` b=`magick 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=`magick xc: -format "%[fx:3*$a]" info:` a2h=`magick xc: -format "%[fx:$a2/2]" info:` # compute approx center angle from p1 to dx and a prx=`magick xc: -format "%[fx:$ww-$dx]" info:` pry=$(($hh-1)) lenr=`magick xc: -format "%[fx:hypot(($y1-$pry),($x1-$prx))]" info:` dangle=`magick xc: -format "%[fx:(180/pi)*asin(($a/2)/$lenr)]" info:` angle=`magick xc: -format "%[fx:$rangle+$dangle]" info:` # create ellipse (and mask) for curl magick -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=`magick $tmp3 -ping -format "%w" info:` ehh=`magick $tmp3 -ping -format "%h" info:` # get center and upper left corner of trimmed ellipse image # for inserting into full size image for mask y2=`magick xc: -format "%[fx:$hh-$ehh/2]" info:` x2=`magick xc: -format "%[fx:$x1-($y2-$y1)*tan(pi*$angle/180)]" info:` x0=`magick xc: -format "%[fx:$x2-$eww/2]" info:` y0=`magick xc: -format "%[fx:$y2-$ehh/2]" info:` # insert ellipse image in white background at correct location to form mask testx=`magick xc: -format "%[fx:sign($x0)]" info:` testy=`magick 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=`magick xc: -format "%[fx:hypot(($x2-$x1),($y2-$y1))]" info:` # get complement rotation angle angle2=`magick 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=`magick xc: -format "%[fx:$a*$a*$p0y*$p0y + $b*$b*$p0x*$p0x]" info:` B=`magick xc: -format "%[fx:-2*$a*$a*$b*$b*$p0x]" info:` C=`magick xc: -format "%[fx:$a*$a*$a*$a*($b*$b-$p0y*$p0y)]" info:` p3x=`magick xc: -format "%[fx:(-$B-sqrt($B*$B-4*$A*$C))/(2*$A)]" info:` p4x=`magick xc: -format "%[fx:(-$B+sqrt($B*$B-4*$A*$C))/(2*$A)]" info:` p3y=`magick xc: -format "%[fx:($a*$a*$b*$b-$b*$b*$p0x*$p3x)/($a*$a*$p0y)]" info:` p4y=`magick 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=`magick xc: -format "%[fx:($p3x)cos(pi*$angle/180)-($p3y+$len)sin(pi*$angle/180)+$x1]" info:` y3=`magick xc: -format "%[fx:($p3x)sin(pi*$angle/180)+($p3y+$len)cos(pi*$angle/180)+$y1]" info:` x4=`magick xc: -format "%[fx:($p4x)cos(pi*$angle/180)-($p4y+$len)sin(pi*$angle/180)+$x1]" info:` y4=`magick 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) magick -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 magick $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=`magick xc: -format "%[fx:abs($dcontrast)]" info:` hi=$((100-$lo)) test=`magick 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 magick -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) magick -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 magick $tmp2 "${prefix}_overlay.png" # shaped overlay image magick $tmp3 "${prefix}_mask.png" # shaped masking image fi # Do a "paint 'n' mask" of the original image, outputing the final image magick $tmp1 $tmp2 -composite \ $tmp3 -compose DstIn -composite \ -compose over -bordercolor $bgcolor -border 0 \ "$outfile" exit 0