#!/bin/bash
#
# Copyright 2018,2019 Sébastien Ballet. All rights reserved.
# 
# Use of this source code is governed by the BSD 3-clause license
# that can be found in the LICENSE file. 
#

                    ###
                    #    Variables/Constantes    #
                                               ###

# setxdmbg's version
#
VERSION="2020.0101"

# The X server display name, without field 'screennumber', and to 
# XDM resource name format, that is, with underscores in place of 
# dots and colons.
#
XDISP=$(echo ${DISPLAY} | cut -f1 -d":" | tr "." "_")_$(echo ${DISPLAY} | cut -f2- -d":" | cut -f1 -d".")

# Used to store cache directory full path.
#
CACHEDIR=""

# Used to store full path to the source image used to generate the XDM
# background image.
#
SRCIMG=""

# Used to store the output log file passed through option '-l|--log'.
#
LOGFILE=""

# Used to store the 'screen identifier' passed through option -p|--physical-screen.
#
PHYSICAL_SCREEN_ID=""

# Used to store the height and color of border at the top of XDM background
# image, as an array ( <HEIGHT> <COLOR> )
#
BORDER_TOP=( 0 black )

# Used to store the height and color of border at the bottom of XDM background
# image, as an array ( <HEIGHT> <COLOR> )
#
BORDER_BTM=( 0 black )

# Used to store the extra image operator (passed through the option
# -x|--extra-image-op) to apply to the source image to generate the
# XDM background image.
#
EXTRA_IMG_OPS=()

# Used to store the resolution and delta of the source image pointed by
# SRCIMG, as an array ( <WIDTH> <HEIGHT> <DELTA> )
#
SRCIMG_RES=()

# Used to store the information (see below) on the X abstract screen :
#
#     0  1  2  3  4  5
#  ( mW mH cW cH MW MH)
#
#    mW : minimum width    mH : minimum height
#    cW : current width    cH : current height
#    MW : maximum width    MH : maximum height
#
XSCREEN_NFO=()

# Used to store the information (see below) on the physical screen
# PHYSICAL_SCREEN_ID, if any.
#
#      0    1   2   3    4      5      6      7       8      9
#  ( <OUT> <W> <H> <mW> <mH> <XOFF> <YOFF> <AFLAG> <PFLAG> <RANK> )
#
#  OUT  : The output (ex. VGA1, HDMI1,...)
#  W    : The X resolution (pixels)
#  H    : The Y resolution (pixels)
#  mW   : The X size (mm)
#  mH   : The Y size (mm)
#  XOFF : The X offset
#  YOFF : The Y offset
#  AFLAG: The automatic flag (on or off)
#  PFLAG: The primary flag (on or off)
#  RANK : rank (or # number) of the display (>=0)
#
PHYSICAL_SCREEN_NFO=()

# Used to store the resolution and delta of the XDM background image
# to generate, as an array ( <WIDTH> <HEIGHT> <DELTA> )
#
XDMIMG_RES=()

# Used to store the full path to the generated XDM background image.
#
# This image is stored in directory pointed by CACHEDIR, and his
# name is to the format: xdm.<XDISP>.pixmap.
#
# <XDISP> is the X server display name, without field 'screennumber', 
# and to XDM resource name format, that is, with underscores in place 
# of dots and colons.
#
# For instance the value of <XDISP> in case of X server display name
# :0 is "_0". it is "_1" in case of X server display name :1, it is
# "com_foo_bar_0" in case of X server display name com.foo.bar:0.
#
XDMIMG=""

# Used to store the full path to the information file about the
# last generated XDM background image.
#
# This file :
#   * is stored in directory pointed by CACHEDIR, and his 
#     name is to the format: xdm.<XDISP>.pixmap.inf
#
#     see XDMIMG for more about <XDISP>.
# 
#   * contains the following key-value pairs
#
#         key               value
#      srcimg.md5 : md5sum of source image
#      xdmimg.md5 : md5sum of XDM background image
#
#      srcimg.res : Resolution and delta of source image
#      xdmimg.res : Resolution and delta of XDM background image
#
#      border.top : Configuration of border at top of XDM background image
#      border.btm : Configuration of border at bottom of XDM background image
#
#      image.ops  : image operators used to generate the XDM background image
#
# The informations in this file are used to prevent re-generation of
# the XDM background image when setxdmbg is run repeatedly with the 
# same configuration.
#
XDMIMG_INF=""

                    ###
                    #    Functions    #
                                    ###
    
# Returns 0 when $1 matches (case insensitive) 1|on|yes|true,
# otherwise returns 1.
#
function is_enabled() {
  echo "$1" | grep -qiE "^(1|on|yes|true)$" && return 0
  return 1
}

# Returns 0 when $(is_enabled $1) returns 1, otherwise return 1.
#
function is_disabled() {
  is_enabled $1 && return 1
  return 0
}

# Prints the informations (ie. $@) onto the log file pointed
# by LOGFILE, when defined, otherwise, returns immediately.
#
function log_infos() {
 [ -z "${LOGFILE}" ] && return
 echo -e "$(date +%Y/%m/%d\ %T) setxdmbg [$$]: $@" >> ${LOGFILE}
}

# Prints the error informations (ie. $@) onto the log file pointed by
# LOGFILE, when defined, otherwise on stderr, then terminate with exit-
# code 1.
#
function error() {
 local OUTFILE=/dev/stderr
 
 [ ! -z "${LOGFILE}" ] && OUTFILE=${LOGFILE}
 
 echo -e "$(date +%Y/%m/%d\ %T) setxdmbg [$$]: Error, $@" >> ${OUTFILE}
 exit 1
}

# Returns informations on the X abstract screen (when available) as 
# an array to the format below :
#
#     0  1  2  3  4  5
#  ( mW mH cW cH MW MH)
#
#    mW : minimum width    mH : minimum height
#    cW : current width    cH : current height
#    MW : maximum width    MH : maximum height
#
function get_xscreen_infos() {
  local XSCRSTR=$(xrandr 2>/dev/null |grep -E "^Screen[ ]+[0-9]+[:]")
  
  # XSCRSTR is to the format :
  #  "Screen %d: minimum %d x %d, current %d x %d, maximum %d x %d"

  echo "${XSCRSTR}" \
    | grep --only-matching -E "[0-9]+[ ][x][ ][0-9]+" \
    | tr "x" " "
}

# Returns informations on the display specified by $1 (when available),
# which can be :
#
# * the keyword 'primary' to get informations on the primary display.
#
# * a number (>=0), to get informations on the display #$1
#
# * an output name (ex. VGA1,HDMI1,DVI-D-0,...) to get informations on 
#   the display connected to that output.
#
# Collected informations are returned as an array to the format below  :
#
#      0    1   2   3    4      5      6      7       8      9
#  ( <OUT> <W> <H> <mW> <mH> <XOFF> <YOFF> <AFLAG> <PFLAG> <RANK> )
#
#  OUT  : The output (ex. VGA1, HDMI1,...)
#  W    : The X resolution (pixels)
#  H    : The Y resolution (pixels)
#  mW   : The X size (mm)
#  mH   : The Y size (mm)
#  XOFF : The X offset
#  YOFF : The Y offset
#  AFLAG: The automatic flag (on or off)
#  PFLAG: The primary flag (on or off)
#  RANK : rank (or # number) of the display (>=0)
#
function get_display_infos() {
  local DISP=$1
  local INFOS=()
  local REGEX
  local DATA
  
  # To get information on a display, it is required to parse
  # the output of xrandr --listmonitors which is to the format: 
  #  
  #  " <RANK>: +*<OUT> <W>/mW>x<H>/<mH>+<XOFF>+<YOFF> <OUT>"
  #
  #   RANK : rank (or # number) of the monitor
  #   +    : automatic flag, when set
  #   *    : primary flag, when set
  #   OUT  : output name
  #   W    : X resolution (pixel)
  #   mW   : X resolution (mm)
  #   H    : Y resolution (pixel)
  #   mH   : Y resolution (mm)
  #   XOFF : X offset
  #   YOFF : Y offset
  
  if [ "${DISP}" = "primary" ] ; then
    # Get informations on primary display
    #
    REGEX="^[ ][0-9]+[:][ ][+]{0,1}[*]" 
  elif echo ${DISP} | grep -qE "^[0-9]+$" ; then
    # Get informations on the display #${DISP}
    #
    REGEX="^[ ]${DISP}[:]"
  else 
    # Get informations on the display connected to the output $DISP.
    #
    REGEX="^[ ][0-9]+[:][ ][+]{0,1}[*]{0,1}${DISP}[ ]" 
  fi
  
  DATA=( $(xrandr --listmonitors | grep -E "${REGEX}") )
  
  # On success, there are 4 items in DATA :
  #   DATA[0] = rank (or # number) of display
  #   DATA[1] = automatic/primary flags (if any) and output name
  #   DATA[2] = Width/mWidthxHeight/mHeight+Xoff+Yoff
  #   DATA[3] = output name
  
  if [ ${#DATA[@]} -eq 4 ] ; then
     #        0     1      2       3     4    5
     # TMP: Width mWidth Height mHeight Xoff Yoff
     #
    local TMP=( $(echo "${DATA[2]}" | tr "/x+" " ") )
    local AFLAG=off
    local PFLAG=off
    local RANK=$(echo "${DATA[0]}" | tr --delete ":")

    [ "${DATA[1]:0:1}" = "+" ] && AFLAG=on
    [ "${DATA[1]:0:1}" = "*" ] || [ "${DATA[1]:1:1}" = "*" ] && PFLAG=on

    echo ${DATA[3]} ${TMP[0]} ${TMP[2]} ${TMP[1]} ${TMP[3]} ${TMP[4]} ${TMP[5]} ${AFLAG} ${PFLAG} ${RANK}
  fi
}

# Collects information on the X abstract screen and store them in
# variable XSCREEN_NFO on success.
#
function collect_xscreen_infos() {
  XSCREEN_NFO=( $(get_xscreen_infos) )

  # reminder about XSCREEN_NFO structure :
  #
  #                   0  1  2  3  4  5
  #                ( mW mH cW cH MW MH)
  #
  #    mW : minimum width    mH : minimum height
  #    cW : current width    cH : current height
  #    MW : maximum width    MH : maximum height

  if [ ! -z "${XSCREEN_NFO}" ] ; then
    local MIN="minimum ${XSCREEN_NFO[0]} x ${XSCREEN_NFO[1]}"
    local CUR="current ${XSCREEN_NFO[2]} x ${XSCREEN_NFO[3]}"
    local MAX="maximum ${XSCREEN_NFO[4]} x ${XSCREEN_NFO[5]}"
    log_infos "X abstract screen: ${MIN}, ${CUR}, ${MAX}"
  else    
    log_infos "Warning, failed to get X abstract screen informations."
  fi
}

# Collects information on the physical screen that "matches" the
# identifier $1 and store them in variable PHYSICAL_SCREEN_NFO on
# success.
#
function collect_physical_screen_infos() {
  local SCREEN_ID=$1
  
  PHYSICAL_SCREEN_NFO=( $(get_display_infos ${SCREEN_ID}) )

  # reminder about structure of PHYSICAL_SCREEN_NFO
  #
  #  ( <OUT> <W> <H> <mW> <mH> <XOFF> <YOFF> <AFLAG> <PFLAG> <RANK> )
  #      0    1   2   3    4      5      6      7       8      9
  #
  #  OUT  : Output
  #  W    : X resolution                 H   : Y resolution
  #  mW   : X size (mm)                 mH   : Y size (mm)
  #  XOFF : X offset                    YOFF : Y offset
  #  AFLAG: automatic flag              PFLAG: primary flag
  #  RANK : rank

  if [ ! -z "${PHYSICAL_SCREEN_NFO}" ] ; then
    local TTL
    local RES="${PHYSICAL_SCREEN_NFO[1]}x${PHYSICAL_SCREEN_NFO[2]}"
    local DIM="${PHYSICAL_SCREEN_NFO[3]}x${PHYSICAL_SCREEN_NFO[4]} mm"
    local OFS="+${PHYSICAL_SCREEN_NFO[5]}+${PHYSICAL_SCREEN_NFO[6]}"
    local RNK=${PHYSICAL_SCREEN_NFO[9]}
      
    if is_enabled ${PHYSICAL_SCREEN_NFO[8]} ; then
      TTL="Primary display :"
    else
      TTL="Display :"
    fi
    log_infos "${TTL} [#${RNK}] ${PHYSICAL_SCREEN_NFO[0]} ${RES}${OFS} (${DIM})"
  else
    log_infos "Warning, failed to get informations on physical screen '${PHYSICAL_SCREEN_ID}'."
  fi    
}

# Gets the resolution and delta of the source image pointed by SRCIMG,
# and store these informations in variable SRCIMG_RES.
#
function collect_source_image_res() {
  local RES
  local DLT
  
  if ! which identify > /dev/null 2>&1 ; then
    error "setxdmbg requires 'identify' from imagemagick suite."
  fi
  
  RES=( $(identify -format "%[width] %[height]" ${SRCIMG}) )
  
  if ! echo "${RES[0]}${RES[1]}" | grep -qE "^[0-9]+$" ; then
    error "${SRCIMG} is not a valid image file."
  fi

  # bash does not support floating point operations, therefore, 
  # the delta is x 1000.
  #
  DLT=$(( (${RES[0]} * 1000) / ${RES[1]} ))
  
  SRCIMG_RES=( ${RES[0]} ${RES[1]} ${DLT} )
  
  log_infos "information about image ${SRCIMG} :"
  log_infos "  resolution : ${SRCIMG_RES[0]} x ${SRCIMG_RES[1]}"
  log_infos "  delta      : ${SRCIMG_RES[2]}\n"
}

# Computes the resolution and delta of the XDM background image to 
# generate according to the current configuration, and store these
# informations in variable XDMIMG_RES.
#
function compute_dest_image_res() {
  local WIDTH=0
  local HEIGHT=0
  local DELTA

  # reminder about structure of :
  #  XSCREEN_NFO:
  #                   0  1  2  3  4  5
  #                ( mW mH cW cH MW MH)
  #
  #  PHYSICAL_SCREEN_NFO:
  #    ( <OUT> <W> <H> <mW> <mH> <XOFF> <YOFF> <AFLAG> <PFLAG> <RANK> )
  #        0    1   2   3    4      5      6      7       8      9
  
  if [ ! -z "${XSCREEN_NFO}" ] ; then
    WIDTH=${XSCREEN_NFO[2]}
    HEIGHT=${XSCREEN_NFO[3]}
  fi
  
  if [ ! -z "${PHYSICAL_SCREEN_NFO}" ] ; then
    WIDTH=${PHYSICAL_SCREEN_NFO[1]}
    HEIGHT=${PHYSICAL_SCREEN_NFO[2]}
  fi
  
  if ! echo "${WIDTH}${HEIGHT}" | grep -qE "^[0-9]+$" ; then
    error "invalid screen resolution ${WIDTH} x ${HEIGHT}"
  fi
  
  # bash does not support floating point operations, therefore, 
  # the delta is x 1000.
  #
  DELTA=$(( (${WIDTH} * 1000) / ${HEIGHT} ))
  
  XDMIMG_RES=( ${WIDTH} ${HEIGHT} ${DELTA} )

  log_infos "information about XDM background image :"
  log_infos "  resolution : ${XDMIMG_RES[0]} x ${XDMIMG_RES[1]}"
  log_infos "  delta      : ${XDMIMG_RES[2]}\n"
}

# script's initialization function. Executes the following tasks:
#
#  * initializes variables CACHEDIR, XDMIMG and XDMIMG_INF
#
#  * gets X abstract screen information (stored in XSCREEN_NFO)
#
#  * when PHYSICAL_SCREEN_ID is set, gets information about the
#    physical screen that matches this id (stored in PHYSICAL_SCREEN_NFO)
#
#  * gets source image resolution and delta (stored in SRCIMG_RES)
#
#  * computes destination image resolution and delta (stored in XDMIMG_RES)
#
# Must be called after parse_command_line().
#
function init() {

  log_infos "setxdmbg ver. ${VERSION}"
  
  if [ $(id --user) -eq 0 ] ; then
    CACHEDIR=/var/cache/setxdmbg
  else
    CACHEDIR=${HOME}/.setxdmbg
  fi
  
  if [ ! -e "${CACHEDIR}" ] ; then
    if ! mkdir 2>/dev/null -p ${CACHEDIR} ; then
      error "Failed to create cache directory ${CACHEDIR}"
    fi
  fi
  
  XDMIMG=${CACHEDIR}/xdm.${XDISP}.pixmap
  XDMIMG_INF=${CACHEDIR}/xdm.${XDISP}.pixmap.inf
  
  collect_xscreen_infos
  
  if [ ! -z "${PHYSICAL_SCREEN_ID}" ] ; then
    collect_physical_screen_infos ${PHYSICAL_SCREEN_ID}
  fi 

  collect_source_image_res
  compute_dest_image_res
}

# Prints help page.
#
function show_help() {
  cat << EOF
setxdmbg ver. ${VERSION}

USAGE: setxdmbg --help
       setxdmbg --image <path> [OPTION]...
       
-h|--help 
  Prints this help page
  
-i|--image <path>
  Full path to the source image used to generate XDM background image.
  
-l|--log [<path>]
  Full path to the file in which setxdmbg must log its activity. Default
  to /dev/stdout when <path> is not specified.
  
-p|--physical-screen <screen>
  Displays the XDM background image on a given physical screen only.
  
  <screen> can be any existing output supported by xrandr (ex. VGA-1,
  HDMI-0, DVI-D-0,...), or the keyword "primary" to select the primary
  screen.
  
-T|--top-border <height>[:<color>]
  Defines the height, and optionally the color, of the border at the 
  top of the XDM background image.
  
  <color> can be any color supported by the imagemagick primitive 
  -fill (ex: #ddddff, rgb(1,2,3), none). When <color> is not specified, 
  default to black.
  
-B|--bottom-border <height>[:<color>]
  Defines the height, and optionally the color, of the border at the 
  bottom of the XDM background image.
  
  <color> can be any color supported by the imagemagick primitive 
  -fill (ex: #ddddff, rgb(1,2,3), none). When <color> is not specified, 
  default to black.
  
-x|--extra-image-op <op>
  Adds <op> to the list of extra image operator to apply to the source
  image to generate the XDM background image. The following imagemagick
  operators are supported :

    -adaptive-blur <radius[xSigma]>  -adaptive-sharpen <radius[xSigma]>
    -auto-gamma                      -black-threshold <value[%]>
    -blue-shift <factor>             -blur <radius[xSigma]>
    -brightness-contrast <B[xC][%]>  -charcoal <factor>
    -colorize <percent>              -fill <color>
    -contrast                        -contrast-stretch <BP[xWP][%]>
    -emboss <radius>                 -enhance
    -fft                             -flip
    -flop                            -gaussian-blur <radius[xSigma]>
    -grayscale <method>              -monochrome
    -negate                          +noise <type>
    -ordered-dither <map[,lvl...]>   -paint <radius>
    -posterize <levels>              -radial-blur <angle>
    -raise <thickness>               +raise <thickness>
    -sharpen <radius[xSigma]>        -sketch <radius[xSigma[+angle]]>
    -solarize <percent-threshold>    -tint <value>
  
EOF
}

# Parses setxdmbg's command line.
#
function parse_command_line() {
  local SHIFT=0
  local BHEIGHT
  local BCOLOR
  local OPERATOR
  local OP_ARGS
  local SUPPORTED_OP
  local MISS_ARG
  
  if [ $# -eq 0 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ] ; then
    show_help
    exit 0
  fi
  
  if [ "$1" = "-i" ]  || [ "$1" = "--image" ] ; then
    
    [ -z "$2" ] && error "$1: Argument <path> is missing."
    
    SRCIMG=$(realpath 2>/dev/null -e "$2")
    
    [ -z "${SRCIMG}" ] && error "$1 : File $2 does not exist."
    
    shift 2
  else
    error "Expected argument '-i|--image' is missing. Found '$1'"
  fi

  # Parses optional arguments ...
  #
  while [ ! -z "$1" ] ; do
    SHIFT=1
    
    case "$1" in
      -l|--log) 
        if [ ! -z "$2" ] && [ "${2:0:1}" != "-" ] ; then
	  LOGFILE=$2
	  SHIFT=2
	else
	  # $2 is empty or seems to be an option (ie. starts with a -). In
	  # these case, logs actvity to /dev/stdout
	  #
	  LOGFILE=/dev/stdout
	fi
      ;;
           
      -p|--physical-screen) 
        [ -z "$2" ] && error "$1: Argument <screen> is missing."
	PHYSICAL_SCREEN_ID="$2"
	SHIFT=2
      ;;
      
      -T|--top-border|-B|--bottom-border)
        [ -z "$2" ] && error "$1: Argument <height>[:<color>] is missing."
	
        BHEIGHT="$2" 
	BCOLOR=black
	
	if echo "$2" | grep -qE ":" ; then
          BHEIGHT=$(echo "$2" | cut -f1 -d":")
	  BCOLOR=$(echo "$2" | cut -f2- -d":")
	  
	  [ -z "${BCOLOR}" ] && BCOLOR=black
	fi
	
	if ! echo "${BHEIGHT}" | grep -qE "^[0-9]+$" ; then
	  log_infos "Warning, $1: Unsigned integer value expected but got ${BHEIGHT}. Default to 40."
	  BHEIGHT=40
	fi
	
	if [ "$1" = "-T" ] || [ "$1" = "--top-border" ] ; then
	  BORDER_TOP=( ${BHEIGHT}  ${BCOLOR} )
	else
	  BORDER_BTM=( ${BHEIGHT}  ${BCOLOR} )
	fi
	
	SHIFT=2
      ;;
      
      -x|--extra-image-op)
        [ -z "$2" ] && error "$1: Argument <op> is missing."
	
        OPERATOR="$2"
        OP_ARGS=""
        SUPPORTED_OP=0
        MISS_ARG=""
	
	case "$2" in
	  -adaptive-blur|-adaptive-sharpen)
	    SUPPORTED_OP=1
	    OP_ARGS="$3"
	    [ -z "${OP_ARGS}" ] && MISS_ARG="radius[xSigma]"
	  ;;
	  
	  -auto-gamma|-contrast|-enhance|-fft|-flip|-flop|-monochrome|-negate)
	    SUPPORTED_OP=1
	  ;;
	  
	  -blue-shift|-charcoal)
	    SUPPORTED_OP=1
            OP_ARGS="$3"
            [ -z "${OP_ARGS}" ] && MISS_ARG="factor"
	  ;;
	  
	  -emboss|-paint)
	    SUPPORTED_OP=1
	    OP_ARGS="$3"
	    [ -z "${OP_ARGS}" ] && MISS_ARG="radius"
	  ;;
	  
	  -raise|+raise)
	    SUPPORTED_OP=1
	    OP_ARGS="$3"
	    [ -z "${OP_ARGS}" ] && MISS_ARG="thickness"
	  ;;
	  
	  -black-threshold)
	    SUPPORTED_OP=1
	    OP_ARGS="$3"
	    [ -z "${OP_ARGS}" ] && MISS_ARG="value[%]"
	  ;;
	  
	  -blur)
	    SUPPORTED_OP=1
            OP_ARGS="$3"
            [ -z "${OP_ARGS}" ] && MISS_ARG="radius[xSigma]"
	  ;;
	  
	  -brightness-contrast)
	    SUPPORTED_OP=1
	    OP_ARGS="$3"
	    [ -z "${OP_ARGS}" ] && MISS_ARG="Brightness[xContrast][%]"
	  ;;
	  
	  -colorize)
	    SUPPORTED_OP=1
	    OP_ARGS="$3"
	    [ -z "${OP_ARGS}" ] && MISS_ARG="percent"
	  ;;
	  
	  -fill)
	    SUPPORTED_OP=1
	    OP_ARGS="$3"
	    [ -z "${OP_ARGS}" ] && MISS_ARG="color"
	  ;;
	  
	  -contrast-stretch)
	    SUPPORTED_OP=1
	    OP_ARGS="$3"
	    [ -z "${OP_ARGS}" ] && MISS_ARG="Black-Point[xWhite-Point][%]"
	  ;;
	  
	  -gaussian-blur)
	    SUPPORTED_OP=1
	    OP_ARGS="$3"
	    [ -z "${OP_ARGS}" ] && MISS_ARG="radius[xSigma]"
	  ;;
	  
	  -grayscale)
	    SUPPORTED_OP=1
	    OP_ARGS="$3"
	    [ -z "${OP_ARGS}" ] && MISS_ARG="method"
	  ;;
	  
	  +noise)
	    SUPPORTED_OP=1
	    OP_ARGS="$3"
	    [ -z "${OP_ARGS}" ] && MISS_ARG="type"
	  ;;
	  
	  -ordered-dither)
	    SUPPORTED_OP=1
	    OP_ARGS="$3"
	    [ -z "${OP_ARGS}" ] && MISS_ARG="threshold_map[,level...]"
	  ;;
	  
	  -posterize)
	    SUPPORTED_OP=1
	    OP_ARGS="$3"
	    [ -z "${OP_ARGS}" ] && MISS_ARG="levels"
	  ;;
	  
	  -radial-blur)
	    SUPPORTED_OP=1
	    OP_ARGS="$3"
	    [ -z "${OP_ARGS}" ] && MISS_ARG="angle"
	  ;;
	  
	  -sharpen)
	    SUPPORTED_OP=1
	    OP_ARGS="$3"
	    [ -z "${OP_ARGS}" ] && MISS_ARG="radius[xSigma]"
	  ;;
	  
	  -sketch)
	    SUPPORTED_OP=1
	    OP_ARGS="$3"
	    [ -z "${OP_ARGS}" ] && MISS_ARG="radius[xSigma[+angle]]"
	  ;;
	  
	  -solarize)
	    SUPPORTED_OP=1
	    OP_ARGS="$3"
	    [ -z "${OP_ARGS}" ] && MISS_ARG="percent-threshold"
	  ;;
	  
	  -tint)
	    SUPPORTED_OP=1
	    OP_ARGS="$3"
	    [ -z "${OP_ARGS}" ] && MISS_ARG="value"
	  ;;
	esac
	
	[ ${SUPPORTED_OP} -eq 0 ] && error "$1: Unsupported image operators '$2'."
	[ ! -z "${MISS_ARG}" ] && error "$1 ${OPERATOR}: Argument <${MISS_ARG}> is missing."

	# Attention, any extra operations in EXTRA_IMG_OPS might include
	# space. Therefore, to prevent these values from being splitted 
	# in multiple cells inside EXTRA_IMG_OPS array, it is required 
	# to put ${EXTRA_IMG_OPS[@]} in quotes.
	#
	# source: https://stackoverflow.com/a/9089186
	#
	
	if [ -z "${OP_ARGS}" ] ; then
	  EXTRA_IMG_OPS=( "${EXTRA_IMG_OPS[@]}" "${OPERATOR}" )
	  SHIFT=2
	else
	  EXTRA_IMG_OPS=( "${EXTRA_IMG_OPS[@]}" "${OPERATOR} ${OP_ARGS}" )
	  SHIFT=3
	fi
      ;;
      
      *)
        error "$1: Unrecognized argument."
      ;;
    esac
    
    shift ${SHIFT}
  done
}

# Stores the (current) configuration of the XDM background image in 
# file specified by $1.
#
function store_image_config() {
  local OUTFILE=$1
  
  cat<<EOF > ${OUTFILE}
srcimg.md5=$(md5sum ${SRCIMG} | cut -f1 -d" ")
xdmimg.md5=$(md5sum ${XDMIMG} | cut -f1 -d" ")
srcimg.res=${SRCIMG_RES[*]}
xdmimg.res=${XDMIMG_RES[*]}
border.top=${BORDER_TOP[*]}
border.btm=${BORDER_BTM[*]}
image.ops=${EXTRA_IMG_OPS[*]}
EOF
}

# Generates the XDM background image according to the current 
# configuration, if required.
#
# The generated image is stored in file pointed by XDMIMG.
#
function generate_image() {
  local WIDTH
  local HEIGHT
  local XOFS
  local YOFS
  local ARGS=()

  if ! which convert > /dev/null 2>&1 ; then
    error "setxdmbg requires 'convert' from imagemagick suite."
  fi

  # If the XDM background image already exists, check if
  # it is required to re-generate it.
  #
  if [ -e "${XDMIMG}" ] && [ -e "${XDMIMG_INF}" ] ; then
    store_image_config ${XDMIMG_INF}.chk

    local OLDMD5=$(md5sum ${XDMIMG_INF} | cut -f1 -d" ")
    local NEWMD5=$(md5sum ${XDMIMG_INF}.chk | cut -f1 -d" ")
    
    rm -f ${XDMIMG_INF}.chk
  
    if [ "${OLDMD5}"  = "${NEWMD5}" ] ; then
      log_infos "XDM background image already generated."
      return
    fi
  fi

  # reminder :
  #   The variables SRCIMG_RES and XDMIMG_RES are arrays to the 
  #   format : ( <width> <height> <delta> )
  #                 0        1       2
  #   
  
  if [ ${SRCIMG_RES[2]} -ne ${XDMIMG_RES[2]} ] ; then
    # The source and target images don't have the same delta. To avoid
    # distortion of the source, an area with the same delta as the 
    # destination must be extracted then resized to the destination 
    # resolution.
    #
    # For that, it is needed to compute :
    #
    #  * The width  WIDTH  so that WIDTH/SRCIMG_RES[1]  = XDMIMG_RES[2]
    #  * The heigth HEIGHT so that SRCIMG_RES[0]/HEIGHT = XDMIMG_RES[2]
    #
    # if WIDTH is <= SRCIMG_RES[0], the area to extract is :
    #
    #  { (SRCIMG_RES[0] - WIDTH)/2, 0, WIDTH, SRCIMG_RES[1] }
    #
    # if HEIGHT <= SRCIMG_RES[1], the area to extract is:
    #
    #  { 0, (SRCIMG_RES[1]-HEIGHT)/2, SRCIMG_RES[0], HEIGHT }
    # 
    WIDTH=$(( (${SRCIMG_RES[1]} * ${XDMIMG_RES[0]}) / ${XDMIMG_RES[1]} ))
    HEIGHT=$(( (${SRCIMG_RES[0]} * ${XDMIMG_RES[1]}) / ${XDMIMG_RES[0]} ))
      
    log_infos "The source and target images have different delta :"
    log_infos "  Computed width for delta=${XDMIMG_RES[2]}  : ${WIDTH}"
    log_infos "  Computed height for delta=${XDMIMG_RES[2]} : ${HEIGHT}"

    if [ ${WIDTH} -le ${SRCIMG_RES[0]} ] ; then
      XOFS=$(( (${SRCIMG_RES[0]} - ${WIDTH}) / 2 ))
      log_infos "  Computed XOFF for width=${WIDTH} : ${XOFS}"
	  
      # Argument to pass to convert to extract the computed
      # area, then to resize it to the screen resolution.
      #
      # Notes: 
      #  * -crop is used instead of -extract to prevent the
      #    output image to include an offset.
      #
      #  * the character '!' is used to notify 'convert' it
      #  must ignore the original aspect of the image.
      #
      ARGS=( "-crop" "${WIDTH}x${SRCIMG_RES[1]}+${XOFS}+0!" \
	         "-resize" "${XDMIMG_RES[0]}x${XDMIMG_RES[1]}!" )
		
    elif [ ${HEIGHT} -le ${SRCIMG_RES[1]} ] ; then
      YOFS=$(( (${SRCIMG_RES[1]} - ${HEIGHT}) / 2 ))
      log_infos "  Computed YOFF for height=${HEIGHT} : ${YOFS}"

      # Argument to pass to convert to extract the computed
      # area, then to resize it to the screen resolution.
      #
      # Notes: 
      #  * -crop is used instead of -extract to prevent the
      #    output image to include an offset.
      #
      #  * the character '!' is used to notify 'convert' it
      #  must ignore the original aspect of the image.
      #
      ARGS=( "-crop" "${SRCIMG_RES[0]}x${HEIGHT}+0+${YOFS}!" \
             "-resize" "${XDMIMG_RES[0]}x${XDMIMG_RES[1]}!" )
    fi
  fi
  
  if [ -z "${ARGS}" ] ; then
    # The image and screen share the same delta, or none of the computed
    # width and height are OK.
    #
    # In such case, simply convert the image to the screen resolution.
    #
    # Note: the character '!' is used to notify 'convert' it must ignore
    #       the original aspect of the image.
    #
    ARGS=( "-resize" "${XDMIMG_RES[0]}x${XDMIMG_RES[1]}!" )
  fi

  # Adds the extra operators to the arguments of imagemagick's convert 
  # tool, if any.
  #
  if [ ! -z "${EXTRA_IMG_OPS}" ] ; then
    ARGS+=( ${EXTRA_IMG_OPS[@]} )
  fi
		
  log_infos "  Execing 'convert ${ARGS[*]} ${SRCIMG} ${XDMIMG}'"
  if ! convert "${ARGS[@]}" ${SRCIMG} ${XDMIMG} ; then
    error "Failure while converting image ${SCRIMG}."
  fi

  # reminder :
  #   The variables BORDER_TOP and BORDER_BTM are arrays to the 
  #   format : ( <height> <color>  )
  #                 0        1
  
  if [ ${BORDER_TOP[0]} -gt 0 ] || [ ${BORDER_BTM[0]} -gt 0 ] ; then
    local BKIMG=${XDMIMG}_background.png
    local MGEO
    
    # There must be a border :
    #   * at top of XDMIMG when BORDER_TOP[0] > 0
    #   * at bottom of XDMIMG when BORDER_BTM[0] > 0
    #
    
    # 1st, create an image to the size of XDM background image.
    #
    # When BORDER_TOP[0]>0, the top area of that height is filled with 
    # color BORDER_TOP[1].
    #
    # When BORDER_BTM[0]>0, the bottom area of that height is filled with
    # color BORDER_BTM[1].
    #
    ARGS=( "-size" "${XDMIMG_RES[0]}x${XDMIMG_RES[1]}" "xc:none" )
    
    if [ ${BORDER_TOP[0]} -gt 0 ] ; then
      ARGS+=( "-fill" "${BORDER_TOP[1]}" \
              "-draw" "rectangle 0,0 ${XDMIMG_RES[0]},${BORDER_TOP[0]}" )
    fi
    
    if [ ${BORDER_BTM[0]} -gt 0 ] ; then
      local Y0=$(( ${XDMIMG_RES[1]} - ${BORDER_BTM[0]} ))
      ARGS+=( "-fill" "${BORDER_BTM[1]}" \
              "-draw" "rectangle 0,${Y0} ${XDMIMG_RES[0]},${XDMIMG_RES[1]}" )
    fi
    
    log_infos "  Execing convert ${ARGS[*]} ${BKIMG}"
    if ! convert "${ARGS[@]}" ${BKIMG} ; then
      error "Failure while creating background."
    fi

    # crop the image to the screen height minus the border at top and/or 
    # bottom...
    #
    # MGEO is the position at which the image XDMIMG must be placed in 
    # BKIMG to generate  the final image.
    #
    BTOTAL=$(( ${BORDER_TOP[0]} + ${BORDER_BTM[0]} ))
    HEIGHT=$(( ${XDMIMG_RES[1]} - ${BTOTAL} ))
    
    if [ ${BORDER_TOP[0]} -gt 0 ] ; then
      ARGS=( "-crop" "${XDMIMG_RES[0]}x${HEIGHT}+0+${BORDER_TOP[0]}" )
      MGEO="+0+${BORDER_TOP[0]}"
    else
      ARGS=( "-crop" "${XDMIMG_RES[0]}x${HEIGHT}+0+0" )
      MGEO="+0+0"
    fi

    log_infos "  Execing 'convert ${ARGS[*]} ${XDMIMG} ${XDMIMG}'"
    if ! convert "${ARGS[@]}" ${XDMIMG} ${XDMIMG} ; then
      error "Failure while converting image."
    fi
    
    # Create the final image (XDMIMG) by merging the images XDMIMG and 
    # BKIMG. XDMIMG is placed on BKIMG at the position specified by MGEO. 
    #
    ARGS=( "${BKIMG}" "${XDMIMG}" "-colorspace" "sRGB" "-geometry" "${MGEO}" "-compose" "over" "-composite" )
    log_infos "  Execing 'convert ${ARGS[*]} ${XDMIMG}'"
    
    if ! convert "${ARGS[@]}" ${XDMIMG} ; then
      error "Failure while merging ${XDMIMG} with ${BKIMG}"
    fi
  fi
     
  # saves information about the generated image in file pointed by XDMIMG_INF
  #
  store_image_config ${XDMIMG_INF}
}

# Display the image pointed by XDMIMG with image viewer 'feh' if 
# installed, with image viewer 'display' otherwise.
#
# Since 'display' image viewer is not compatible with compton (*), 
# 'feh' is used instead of 'display' when present.
#
#  (*) when display is used when compton is running, the displayed 
#      image is greyed and compton emits warning messages.
#
function display_image() {
  local ARGS=()
  
  if which feh > /dev/null 2>&1 ; then
    ARGS=( "--no-fehbg" "--bg-center" "--geometry" "+0+0" )
    
    if [ ! -z ${PHYSICAL_SCREEN_NFO} ] ; then
      ARGS+=( "--xinerama-index" "${PHYSICAL_SCREEN_NFO[9]}" )
    else
      ARGS+=( "--no-xinerama" )
    fi

    log_infos "Execing 'feh ${ARGS[*]} ${XDMIMG}'"
    feh ${ARGS[@]} ${XDMIMG}
  elif which display > /dev/null 2>&1 ; then
    
    # The image viewer 'feh' is not available. Display the image to 
    # background of the X root window using image viewer 'display'. 
    #
    local GEOM
      
    if [ ! -z ${PHYSICAL_SCREEN_NFO} ] ; then
      GEOM="+${PHYSICAL_SCREEN_NFO[5]}+${PHYSICAL_SCREEN_NFO[6]}"
    else
      GEOM="+0+0"
    fi

    ARGS=( "-geometry" "${GEOM}" "-window" "root" )

    log_infos "Execing 'display ${ARGS[*]} ${XDMIMG}'"
    display ${ARGS[@]} ${XDMIMG}
    
    # Surprisingly, the command 'display' always returns 1, even 
    # if the image to display exist and has been successfully 
    # displayed. To prevent setxdmbg to return this wrong exit 
    # code, which could be mis-interpreted by the caller (ex. Xsetup), 
    # there is no other choice than to return 0.
    # 
    return 0
  else
    error "Unable to display image, neither \"feh\" nor \"display\" is installed."
  fi
}

# setxdmbg's main function
#
function main() {
  generate_image
  display_image
}

                    ###
                    #    Entry Point    #
                                      ###

parse_command_line "$@"
init
main
