#! /bin/bash -e
# Last edited on 2025-07-20 09:52:08 by stolfi

# USAGE: annotate-pic {SOURCE_IMG} {TEXT_PTSIZE} {TEXT_COLOR} {STROKE_COLOR} {COMMAND}...
#
# Commands:
#  
#   -color {COLORSPEC}                            : set stroke and fill color
#   -textcolor {COLORSPEC}                        : set text color
#   -strokewidth {NUM}                            : set relative stroke width 
#   -S {X0} {Y0} [ {Xi} {Yi} ]..                  : open polygonal line
#   -D {X0} {Y0} [ {Xi} {Yi} ]..                  : open polygonal line with corner dots.
#   -L {XD} {YD} {XT} {YT} {TEXT}                 : label with dot at {XD,YD} and text at {XT,YT}
#   -CL {XD} {YD} {RD} {XT} {YT} {TEXT}           : label with circle of center {XD,YD} radius {RD} and text at {XT,YT}
#   -Q {X1} {Y1} ... {X4} {Y4}                    : quadrilateral outline with given corners.
#   -T {XT} {YT} {AX} {AY} {TEXT}                 : text with position {AX,AY} at {XT,YT}
#   -I {XR} {YR} {XA} {YA} {IMG} {MAG}            : image {IMG} scaled by {MAG} with ref {XA} {YA} at {XR} {YR}
#   ( or )                                        : grouping for ImageMagick
#
# Also "+L" and "+CL" are like "-L" and "-CL" except that the position of the
# text is {XD+XT,XD+YT} instead of {XT,YT}.

src="$1"; shift   # Source image
tsz="$1"; shift   # Text pointsize
trgb="$1"; shift  # Text color
crgb="$1"; shift  # Line/disk color

bwid=`echo "scale=3; (${tsz})*0.042" | bc -lq`
echo "basic line width = ${bwid}" 1>&2

tmarg=`echo "scale=3; (${tsz})*0.28 + 0.5" | bc -lq`
echo "label margin = ${tmarg}" 1>&2

declare -gax cmd=()

function setlinewidth() {
  declare -gax cmd

  local mwid="$1"; shift
  
  # Current line width in pixels:
  cwid=`echo "scale=3; (${bwid})*(${mwid})" | bc -lq`
  # echo "line width = ${cwid}" 1>&2
  
  # Dot radius (assuming not stroked):
  rdot=`echo "scale=3; 1.5*(${cwid})" | bc -lq`
  # echo "bare dot radius = ${rdot}" 1>&2
  
  # Connecting dot radius (assuming not stroked):
  # Dots with radius less than 1 are rendered incorrectly, set them to 0:
  rcon=`gawk -v cwid=${cwid} 'BEGIN{ rcon = 0.5*cwid - 0.5; print (rcon < 0 ? "0" : sprintf("%.3f\n",rcon)); }'`
  # echo "connecting dot radius = ${rdot}" 1>&2
  
  cmd+=( "-strokewidth" "${cwid}" )
}

function drawlabel() {
  # Used by "-L", "+L", "-CL", "+CL"
  
  if [[ $# -lt 7 ]]; then
    echo "** bad args '$*'" 1>&2; exit 1
  fi
  local dx="$1"; shift    # Labeled point X.
  local dy="$1"; shift    # Labeled point Y.
  local dr="$1"; shift    # Radius of circle/dot at labeled point.
  local drgb="$1"; shift  # Fill color of dot/circle, or "none".
  local tx="$1"; shift    # Text center X.
  local ty="$1"; shift    # Text center Y.
  local text="$1"; shift  # The text.

  # echo "enter drawlabel ${dx} ${dy} ${dr} ${drgb} ${tx} ${ty} '${text}'" 1>&2
  
  # echo "drawlabel.1: cmd = ${cmd[@]}" 1>&2
  declare -gax cmd
  # echo "drawlabel.2: cmd = ${cmd[@]}" 1>&2
  
  # --- dot and line ---
  
  # We must draw the line from the periphery of the circle:
  ex=`echo "(${tx})-(${dx})" | bc -lq`
  ey=`echo "(${ty})-(${dy})" | bc -lq`
  ed=`echo "d=sqrt((${ex})^2+(${ey})^2); scale=4; d/1.0" | bc -lq`
  pxy=( `echo "r=(${dr}); d=(${ed}); c=(${ex})/d; s=(${ey})/d; x=(${dx})+c*r; y=(${dy})+s*r; scale=2; x/1.0; y/1.0" | bc -lq` )
  px=${pxy[0]}
  py=${pxy[1]}
  # echo "tip of line = ${px},${py}" 1>&2

  # Edge points of label disks and dot:
  local drx=`echo "(${dx}) + (${dr})" | bc -lq`
  # echo "disk edge X = ${drx}" 1>&2

  cmd+=( "-stroke" "${crgb}" "-fill" "${drgb}" )
  cmd+=( "-draw" "line ${px},${py} ${tx},${ty}" )
  cmd+=( "-draw" "circle  ${dx},${dy} ${drx},${dy}" )

  # --- text label ---
  
  # Number of chars in text.
  local nc=${#text}

  # Nominal char width and height:
  # Courier bold with pointsize 100 has total height 120 and width 60,
  # but its effective height seems to be 60:
  local wx=`echo "scale=3; (${tsz})*0.6" | bc -lq`
  local wy=`echo "scale=3; (${tsz})*0.6" | bc -lq`
  # echo "effective character dimensions = ${wx} by ${wy}" 1>&2

  # Half-distance between half-circle centers:
  local hx=`echo "scale=3; 0.5*(${wx})*(${nc}-1)" | bc -lq`
  # echo "half-distance between centers = ${hx}" 1>&2

  # Radius of a single char bbox:
  local cr=`echo "scale=3; 0.5*sqrt((${wx})^2+(${wy})^2)" | bc -lq`
  # echo "single char radius = ${cr}" 1>&2

  # Radius of half-circles:
  local trd=`echo "(${cr}) + (${tmarg})" | bc -lq`
  # echo "label disk radius = ${trd}" 1>&2

  # We must shift the text by {(1/2)*(wx*nc,wy)/100}.
  # Shift for text:
  local ux=`echo "scale=3; (${tx})-0.5*(${wx})*${nc}" | bc -lq`
  local uy=`echo "scale=3; (${ty})+0.5*(${wy})" | bc -lq`
  # echo "shifted text coords = ${ux},${uy}" 1>&2
  
  cmd+=( "-fill" "${crgb}" )
  if [[ ${nc} -le 1 ]]; then
    # Tag is a single disk:
    local trx=`echo "(${tx}) + (${trd})" | bc -lq`
    cmd+=( "-draw" "circle  ${tx},${ty} ${trx},${ty}" )
  else
    # Tag is a rectangle and two disks: 
    local tx0=`echo "(${tx}) - (${hx})" | bc -lq`
    local tx1=`echo "(${tx}) + (${hx})" | bc -lq`
    # echo "tx0 = ${tx0}  tx1 = ${tx1}" 1>&2
    local trx0=`echo "(${tx0}) + (${trd})" | bc -lq`
    local trx1=`echo "(${tx1}) + (${trd})" | bc -lq`
    # echo "rx0 = ${rx0}  rx1 = ${rx1}" 1>&2
    local ty0=`echo "(${ty}) - (${trd})" | bc -lq`
    local ty1=`echo "(${ty}) + (${trd})" | bc -lq`
    # echo "ty0 = ${ty0}  ty1 = ${ty1}" 1>&2
    cmd+=( "-draw" "circle  ${tx0},${ty} ${trx0},${ty}" )
    cmd+=( "-draw" "circle  ${tx1},${ty} ${trx1},${ty}" )
    cmd+=( "-draw" "rectangle  ${tx0},${ty0} ${tx1},${ty1}" )
  fi
  cmd+=( "-stroke" "none" "-fill" "${trgb}" )
  cmd+=( "-draw" "text ${ux},${uy} '${text}'" )
}

setlinewidth 1

tmp="/tmp/$$"

while [[ $# -gt 0 ]]; do
  opt="$1"; shift
  if [[ "/${opt}" == "/-color" ]]; then
    # Change color for subsequent ops 
    if [[ $# -lt 1 ]]; then
      echo "** bad args '$*'" 1>&2; exit 1
    fi
    crgb="$1"; shift
  elif [[ "/${opt}" == "/-textcolor" ]]; then
    # Change color for subsequent ops 
    if [[ $# -lt 1 ]]; then
      echo "** bad args '$*'" 1>&2; exit 1
    fi
    trgb="$1"; shift
  elif [[ "/${opt}" == "/-strokewidth" ]]; then
    # Change stroke width for subsequent ops 
    if [[ $# -lt 1 ]]; then
      echo "** bad args '$*'" 1>&2; exit 1
    fi
    mwid="$1"; shift
    setlinewidth "${mwid}"
  elif [[ ("/${opt}" == "/(" ) || ( "/${opt}" == "/)" ) ]]; then
    # Pass grouping op to ImageMagick:
    cmd+=( "${opt}" )
  elif [[ ( "/${opt}" == "/-S" ) || ( "/${opt}" == "/-D" ) ]]; then
    # Open polygonal line with round joints ("-S") or knobs ("-D")
    if [[ $# -lt 2 ]]; then
      echo "** bad args '$*'" 1>&2; exit 1
    fi
    if [[ "/${opt}" == "/-D" ]]; then
      rpt="${rdot}"
    else
      rpt="${rcon}"
    fi
    ax="$1"; shift
    ay="$1"; shift
    cmd+=( "-fill" "${crgb}" )
    if [[ "/${rpt}" != "/0" ]]; then
      sx=`echo "(${ax}) + (${rpt})" | bc -lq`
      cmd+=( "-stroke" "none" "-draw" "circle  ${ax},${ay} ${sx},${ay}" )
    fi
    # # DEBUG
    # cx=${ax}
    # for rts in 0.000 0.300 0.600 1.000 1.300 1.600 ; do 
    #   cx=`echo "(${cx}) + 10" | bc -lq`
    #   tx=`echo "(${cx}) + (${rts})" | bc -lq`
    #   cmd+=( "-stroke" "none" "-draw" "circle  ${cx},${ay} ${tx},${ay}" )
    # done
    # # GUBED
    while [[ ( $# -ge 2 ) && ( "/$1" =~ ^/[-+]?[0-9] ) ]]; do
      bx="$1"; shift
      by="$1"; shift
      cmd+=( "-stroke" "${crgb}" "-draw" "line ${ax},${ay} ${bx},${by}" )
      if [[ "/${rpt}" != "/0" ]]; then
        sx=`echo "(${bx}) + (${rpt})" | bc -lq`
        cmd+=( "-stroke" "none" "-draw" "circle  ${bx},${by} ${sx},${by}" )
      fi
      ax="${bx}"
      ay="${by}"
    done
  elif [[ "/${opt}" == "/-P" ]]; then
    # Parallelogram defined by point and two vectors
    if [[ $# -lt 6 ]]; then
      echo "** bad args '$*'" 1>&2; exit 1
    fi
    ax="$1"; shift; ay="$1"; shift
    
    ux="$1"; shift; uy="$1"; shift
    
    vx="$1"; shift; vy="$1"; shift
    
    bx=`echo "(${ax}) + (${ux})" | bc -lq`
    by=`echo "(${ay}) + (${uy})" | bc -lq`
    
    cx=`echo "(${bx}) + (${vx})" | bc -lq`
    cy=`echo "(${by}) + (${vy})" | bc -lq`
    
    dx=`echo "(${ax}) + (${vx})" | bc -lq`
    dy=`echo "(${ay}) + (${vy})" | bc -lq`
    
    cmd+=( "-stroke" "${crgb}" "-fill" "none" )
    cmd+=( "-draw" "polygon ${ax},${ay} ${bx},${by} ${cx},${cy} ${dx},${dy}" )

  elif [[ "/${opt}" == "/-Q" ]]; then
    # Filled quadrilateral 
    if [[ $# -lt 8 ]]; then
      echo "** bad args '$*'" 1>&2; exit 1
    fi
    ax="$1"; shift; ay="$1"; shift
    
    bx="$1"; shift; by="$1"; shift
    
    cx="$1"; shift; cy="$1"; shift
    
    dx="$1"; shift; dy="$1"; shift
    
    cmd+=( "-stroke" "${crgb}" "-fill" "none" )
    cmd+=( "-draw" "polygon ${ax},${ay} ${bx},${by} ${cx},${cy} ${dx},${dy}" )

  elif [[ ( "/${opt}" == "/-L" ) || ( "/${opt}" == "/+L" )]]; then
    # Label in oval with line and dot:
    if [[ $# -lt 5 ]]; then
      echo "** bad args '$*'" 1>&2; exit 1
    fi
    dx="$1"; shift
    dy="$1"; shift
    tx="$1"; shift
    ty="$1"; shift
    text="$1"; shift
    
    # echo "ARGS: ${opt} ${dx} ${dy} ${tx} ${ty} '${text}'" 1>&2
    
    dr=${rdot}
    if [[ "/${opt}" == "/+L" ]]; then
      tx=`echo "(${dx})+(${tx})" | bc -lq`
      ty=`echo "(${dy})+(${ty})" | bc -lq`
    fi

    drawlabel "${dx}" "${dy}" "${dr}" "${crgb}" "${tx}" "${ty}" "${text}"
    # echo "cmd = ${cmd[@]}" 1>&2

  elif [[ ( "/${opt}" == "/-CL" ) || ( "/${opt}" == "/+CL" ) ]]; then
    # Label in oval with line and circle:
    if [[ $# -lt 6 ]]; then
      echo "** bad args '$*'" 1>&2; exit 1
    fi
    dx="$1"; shift
    dy="$1"; shift
    dr="$1"; shift
    tx="$1"; shift
    ty="$1"; shift
    text="$1"; shift
    
    # echo "ARGS: ${opt} ${dx} ${dy} ${dr} ${tx} ${ty} '${text}'" 1>&2
    
    if [[ "/${opt}" == "/+CL" ]]; then
      tx=`echo "(${dx})+(${tx})" | bc -lq`
      ty=`echo "(${dy})+(${ty})" | bc -lq`
    fi

    drawlabel "${dx}" "${dy}" "${dr}" "none" "${tx}" "${ty}" "${text}"
    # echo "cmd = ${cmd[@]}" 1>&2

  elif [[ "/${opt}" == "/-T" ]]; then
    # Text annotation:
    if [[ $# -lt 5 ]]; then
      echo "** bad args '$*'" 1>&2; exit 1
    fi
    dx="$1"; shift
    dy="$1"; shift
    ax="$1"; shift
    ay="$1"; shift
    text="$1"; shift

    # Number of chars in text:
    nc=${#text}
    
    # Courier bold with pointsize 100 has height 120 and width 60
    # So we must shift the text by tsz*(-60*ax*nc,+60*ay)/100 for centered.
    # Shift for text:
    tx=`echo "(${dx})-((${tsz})*${nc}*(${ax})*0.60)" | bc -lq`
    ty=`echo "(${dy})+((${tsz})*(${ay})*0.60)" | bc -lq`

    cmd+=( "-stroke" "none" "-fill" "${trgb}" )
    cmd+=( "-draw" "text ${tx},${ty} '${text}'" )
  elif [[ "/${opt}" == "/-I" ]]; then
    # Text annotation: -I {XR} {YR} {XA} {YA} {IMG} {MAG}
    if [[ $# -lt 6 ]]; then
      echo "** bad args '$*'" 1>&2; exit 1
    fi
    rx="$1"; shift
    ry="$1"; shift
    ax="$1"; shift
    ay="$1"; shift
    img="$1"; shift
    mag="$1"; shift

    # Image size:
    isz=( `identify ${img}` ); isz=( `echo "${isz[2]}" | tr 'x' ' '` )
    if [[ ${#isz[@]} -ne 2 ]]; then echo "** error obtaining image size" 1>&2; exit 1; fi
    
    # Compute offset taking into account the specified alignment:
    ofx=`echo "(${rx}) - (${isz[0]} * (${ax}))*(${mag})" | bc -lq`
    ofy=`echo "(${ry}) - (${isz[1]} * (${ay}))*(${mag})" | bc -lq`
    pct=`echo "(${mag})*100" | bc -lq`
    geom=`echo "+${ofx}+${ofy}" | sed -e 's:[+][-]:-:g'`
    
    # echo "pct = '${pct}'  geom = '${geom}'" 1>&2 
    
    cmd+=( "(" "(" "${img}" "-resize" "${pct}%" ")" "-geometry" "${geom}" ")" "-composite" )
  else
    echo "** unknown option '${opt}'" 1>&2; exit 1
  fi
done

convert ${src} \
  -font 'Courier-Bold' -pointsize ${tsz} \
  -strokewidth ${cwid} \
  "${cmd[@]}" \
  ${tmp}.png

# display ${tmp}.png

cat ${tmp}.png

rm ${tmp}.png

  
#   -fill Red -draw "circle 100,100 150,100" \
#   -fill Blue -draw "text 100,100 'FOO'" \
#   -stroke Green -strokewidth 4 -draw 'line 200,100 250,350' \

# Old label enclosure (circle that grows with text length):
#     # Radius of text bbox:
#     tr=`echo "(${tsz})*sqrt(0.3^2+(0.3*${nc})^2)" | bc -lq`
#     # echo "text radius = ${tr}" 1>&2
#     
#     # Radius of label:
#     rd=`echo "(${tr}) + (${tmarg})" | bc -lq`
#     # echo "label disk radius = ${rd}" 1>&2
#     
#     # Edge points of label disk and dot:
#     rx=`echo "(${dx}) + (${rd})" | bc -lq`
#     sx=`echo "(${dx}) + (${rdot})" | bc -lq`
# 
#     cmd+=( "-stroke" "${crgb}" "-fill" "${crgb}" )
#     cmd+=( "-draw" "line ${dx},${dy} ${dx},${dy}" )
#     cmd+=( "-draw" "circle  ${dx},${dy} ${sx},${dy}" )
#     cmd+=( "-draw" "circle  ${dx},${dy} ${rx},${dy}" )
#     cmd+=( "-stroke" "none" "-fill" "${trgb}" )
#     cmd+=( "-draw" "text ${tx},${ty} '${text}'" )
