#! /usr/bin/env bash
#
# jigsaw [options] photo_image  mask_image  destination
#
# This program takes a image like a photo and mask image (same size as photo),
# and cut out that shaped piece from the original image.  The piece can then
# be rotated, thickness added, and shadowed.
#
# The returned image will, by default, be as small as posible, with the input
# mask trimmed down, unless preserved or set to a specific size. In all cases
# the offset of the resulting mask, and cutout pieces will be preserved
# relative to the source photo image.
#
# By using this script it should be posible to generate a complete set of
# jigsaw pieces, with multiple rotations.
#
# Image arguments may be pipes as they will only be used once.
#
# OPTIONS
#    -o offset    Mask offset, overriding any existing offset in mask image
#    -d offset    Displace the jigsaw piece by this offset from cut position
#
#    -m           Return the mask that will be used, ignore photo argument
#    -mp          Preserve the input mask as is, don't trim or adjust its size
#    -ms size     Trim/expand mask to fit the size given, with peice centered
#                 Warning: mask should include enough space for any effects
#
#    -l           Add an outline along egde of piece
#    -r angle     Rotate piece by this angle
#    -h           Add highlight to piece
#    -t pixels    Add thickness to piece
#    -s           Add a soft shadow to piece
#
####
#
# WARNING: Input arguments are NOT tested for correctness.
# This script represents a security risk if used ONLINE.
# I accept no responsiblity for misuse. Use at own risk.
#
# Anthony Thyssen     26 May 2006
#
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
Usage() {                              # output the script comments as docs
  echo >&2 "$PROGNAME:" "$@"
  sed >&2 -n '/^###/q; /^#/!q; s/^#//; s/^ //; 3s/^/Usage: /; 2,$ p' \
          "$PROGDIR/$PROGNAME"
  exit 10;
} 

# Controls for shape enhancements
shade_angle=120        # lighting angle of the highlight is to use
shade_blur=2           # highlight blurring (smoothing)
                       #    0.001 none   2 good   10 very rounded edge
shade_surface=5        # How shiny are bright spots (reduce non-spot area)
                       #    1 matte    5 good    10 shiny spots only
shade_contrast=10      # Overall reduction of highlight intensity
                       #    2 strong highlights   50 near nothing
                       #
t_color=DarkSlateGrey  # color of added thickness
shadow=40x4+4+4        # shadow argument

# ---------------------
# Add the fixed option parts (do not adjust)
shade_angle="${shade_angle}x21.78"       # shade azmuth angle
shade_blur="0x$shade_blur"               # rounding blur radius limit
shade_surface="${shade_surface}x50%"     # sigmodial-cotrast midpoint
shade_contrast="${shade_contrast}%"      # reduce highlight intensity

# Temporary working images (with auto-clean-up on exit)
tmp1="/tmp/jigsaw_1_$$.png"
tmp2="/tmp/jigsaw_2_$$.png"
trap "rm -f $tmp1 $tmp2; exit 0" 0
trap "rm -f $tmp1 $tmp2; exit 1" 1 2 3 15

# read in commandline options
while [ $# -gt 0 ]; do
  case "$1" in
  --help|--doc*) Usage ;;
  -o)  shift; INIT_OFFSET="$1" ;; # an initial offset for the mask
  -d)  shift; DISPLACE="$1" ;;    # offset displacement of piece

  -m)  MASK_ONLY=true ;;         # return trimmed mask (photo ignored)
  -mp) PRESERVE=true ;;          # preserve the image mask and results as cut
  -ms) shift; MASK_SIZE="$1" ;;  # Crop mask to fit the sized image

  -l)  OUTLINE=true ;;           # add an outline along egde of piece
  -r)  shift; ROTATE=$1 ;;       # rotate piece by this angle
  -h)  HIGHLIGHT=true ;;         # add highlight to piece
  -t)  shift; THICKNESS="$1" ;;  # add thickness to piece
  -s)  SHADOW=true ;;            # add soft shadow to piece

  --)  shift; break ;;    # end of user options
  -*)  Usage "Unknown option \"$1\"" ;;
  *)   break ;;           # end of user options

  esac
  shift   # next option
done

[ $# -lt 3 ] && Usage "Too few arguments."
[ $# -gt 3 ] && Usage "Too many arguments."

# Now the image files (may be '-' for pipelined commands)
photo_input="$1"
mask_input="$2"
destination="$3"

# FUTURE: check input arguments are in correct format
# This can not be done securly in shell, a perl version is needed
# before this script can use online (web) supplied user arguments.
#
if [ "X$MASK_SIZE" != 'X' -a "$PRESERVE" ]; then
  echo >&2 \
    "$PROGNAME: Mask size \"-ms\" ignored due to mask preserve \"-mp\" option"
fi
if [ "$MASK_ONLY" ] && \
   [ "$OUTLINE" -o "$HIGHLIGHT" -o "$SHADOW" \
       -o "X$ROTATE" != 'X' -o "X$THICKNESS" != 'X'  ]; then
  echo >&2 \
      "$PROGNAME: Piece enhancment ignored, when mask only \"-m\" otpion given"
fi

# -----------------------------------------------------

# Subroutine to add the given offset to the current known offset
offset_add() {
  set -$- -- `echo $1 | sed 's/[+-]/ &/g;s/+//g'`
  x=`expr $x + $1`
  y=`expr $y + $2`
  offset=`printf '%+d%+d' $x $y`
}

x=0; y=0; offset=+0+0        # current offset of piece over original image

# --------------
# Read in the mask (once only)
convert "$mask_input" $tmp2
if [ ! -s $tmp2 ]; then
  echo >&2 "$PROGNAME: Mask Image "$mask_input" read failed -- aborting"
  exit 1
fi

# if no starting offset given, see if mask has one
if [ "X$INIT_OFFSET" = 'X' ]; then
  # first lets see if mask has a page offset
  OFFSET=`identify -format %O $tmp2`
  # If no page offset, look for a offset comment (of the right format)
  # This is depreciated method of saving the images offset
  if  [ "X$OFFSET" = 'X+0+0' ]; then
    OFFSET=`identify -format %c $tmp2 | grep '^[-+][0-9][0-9]*[-+][0-9][0-9]*$'`
  fi
else
  OFFSET="$INIT_OFFSET"
fi

# Now save any starting offset (either from image, or command line option)
if [ "X$OFFSET" != 'X' ]; then
  offset_add "$OFFSET"
fi

if [ -z "$PRESERVE" ]; then
  # Trim the mask and adjust offset due to trim (assumes mask is greyscale!)
  offset_add `convert $tmp2 +repage \
              -bordercolor black -border 1x1 -repage -1-1 \
              -trim -format %O -identify +repage $tmp2`

  if [ "X$MASK_SIZE" != 'X' ]; then
    # Center the mask on an image of the size given
    convert $tmp2 -bordercolor black -border $MASK_SIZE \
            -gravity center -crop $MASK_SIZE+0+0\! +repage \
            -background black -flatten  $tmp1

    # adjust mask offset after trimming the mask.
    set -$- -- `identify -format "%w %h " $tmp1 $tmp2`
    offset_add "`expr \( $3 - $1 \) / 2` `expr \( $4 - $2 \) / 2`"
    mv $tmp1 $tmp2    # replace original mask input with new one
    PRESERVE=true     # do not resize mask or image again from this point
  fi
fi

if [ "$MASK_ONLY" ]; then
  # Return the trimmed mask, with adjusted offset (for later re-use).
  convert -page $offset $tmp2 "$destination"
  exit 0
fi

# --------------
# Read the Source Photo Image (once only)
convert "$photo_input" +repage $tmp1
if [ ! -s $tmp1 ]; then
  echo >&2 "$PROGNAME: Photo Image "$photo_input" read failed -- aborting"
  exit 1
fi

# Cut out the Source image at the current offset (add outline)
crop=`identify -format %wx%h$offset $tmp2`
if [ "$OUTLINE" ]; then
  convert $tmp1 -crop $crop\! -background none -flatten +repage \
          \( $tmp2 -edge 1 -blur 0x.5 -normalize -negate \) \
          -compose multiply -composite \
          \( $tmp2 +matte \) -compose CopyOpacity -composite \
          $tmp1
else
  convert $tmp1 -crop $crop\! -background none -flatten +repage \
          \( $tmp2 +matte \) -compose CopyOpacity -composite \
          $tmp1
fi

# Rotate the piece just cut out
if [ "$ROTATE" ]; then
  if [ -z "$PRESERVE" ]; then
    # Trimmed rotate, adjust offset as a result of rotate
    convert $tmp1 -matte -background none -rotate "$ROTATE" \
            -bordercolor none -border 1x1 -trim +repage  $tmp1
    set -$- -- `identify -format "%w %h " $tmp1 $tmp2`
    offset_add "`expr \( $3 - $1 \) / 2` `expr \( $4 - $2 \) / 2`"
  else
    # Rotate but preserve original masked size (no offset adjust needed)
    convert $tmp1 -matte \( +clone -background none -rotate "$ROTATE" \) \
            -gravity center  -compose Src -composite   $tmp1
  fi
fi

rm -f $tmp2  # original mask is now no longer needed

# Adjust location of the piece just cut out
if [ "X$DISPLACE" != 'X' ]; then
  offset_add "$DISPLACE"
fi

if [ "$HIGHLIGHT" ]; then
  # Add highlights to the cutout piece
  convert $tmp1 \
          \( +clone -channel A -separate +channel -negate \
             -background black -virtual-pixel background \
             -blur $shade_blur -shade $shade_angle -normalize \
             -contrast-stretch 0% -fill grey50 -colorize $shade_contrast \
             +sigmoidal-contrast $shade_surface \
             +clone -compose Overlay -composite \
          \) -compose In -composite $tmp1
fi

if [ "$THICKNESS" ]; then
  if [ -z "$PRESERVE" ]; then  # first add some extra space
    convert $tmp1 -background none -gravity SouthEast \
            -splice $[(${THICKNESS}+1)/2]x${THICKNESS}  $tmp1
  fi
  # now generate a 'variable' thickness command.
  convert $tmp1 \( +clone -fill DarkSlateGrey -colorize 100% -repage +0+1 \) \
          $( for i in $( seq 2 "$THICKNESS" ); do
               printf '( +clone -repage %+d%+d ) ' $[ ($i+1)/2 ] $i;
             done ) -background none -compose DstOver -flatten $tmp1
fi

if [ "$SHADOW" ]; then
  if [ -z "$PRESERVE" ]; then  # first add some extra space
    convert $tmp1 -background none -gravity SouthEast -splice 8x8 $tmp1
  fi
  # now add shadow without resizing the resulting image
  convert $tmp1 \( +clone  -background Black -shadow 50x3+4+4 \) \
                -background none -compose DstOver -flatten    $tmp1
fi

# Output the result, adding offset to image format (for PNG, GIF, MIFF)
convert -page $offset $tmp1 "$destination"
