Skip to content

Instantly share code, notes, and snippets.

@ernstki
Last active February 28, 2023 19:48
Show Gist options
  • Save ernstki/83c65441303728d13642259466f4ae57 to your computer and use it in GitHub Desktop.
Save ernstki/83c65441303728d13642259466f4ae57 to your computer and use it in GitHub Desktop.
Create dropshadowed thumbnails with a simple command

thumb - a command-line thumbnailer & drop-shadower

Quickly generate drop-shadowed thumbnails from the command line using ImageMagick's convert, optionally specifying a maximum geometry for either (or both) dimensions.

Sample images created by this script

You can choose whether or not to draw a thin (1-pixel), grey (#777) border around the image, or leave it off with the -nb / --no-border option.

Sample images created by this script (without the borders)

If you want the image at its original size, just with the border and dropshadow added, then use the -nr / --no-resize option.

All supported options are documented in the script's --help output. Long, human-readable versions of the options are also accepted; see the source, below.

Installation

GIST='https://gist.github.com/ernstki/83c65441303728d13642259466f4ae57'
mkdir -p ~/bin
curl -sL -o ~/bin/thumb "$GIST/raw/thumb.sh"
chmod a+x ~/bin/thumb

If you do not already have $HOME/bin in your search path for executables, this will safely add that, assuming you use the Bash shell:

if ! tr : \\n <<<"$PATH" | grep -q "^$HOME/bin$"; then
    if [[ -f ~/.bash_profile ]]; then
        profile=~/.bash_profile
    else
        profile=~/.profile
    fi
    echo '
# add $HOME/bin at the *end* of your PATH
export PATH="$PATH:$HOME/bin"' >> $profile
    source $profile
    unset profile
fi

Usage

$ thumb --help

  thumb - create thumbnails for specified image(s)

  usage:  thumb [XXXx] [xYYY] [-g GEOM] [-s SUFF] [-nb] [-nr] [-k] [-v] img [...]
          thumb [-h|--help]

  where:  XXXx      max horizontal pixel size (default: 400)
          xYYY      max vertical pixel size (default: keep aspect ratio)
          -g GEOM   arbitrary geometry specification understood by ImageMagick
                    (see: https://tinyurl.com/y6z8g45w#geometry)
          -s SUFF   optional suffix for the thumnbail (default: '_thumb')
          -nb       omit border around image (default: 1 px)
          -nr       do not resize the image (long opt: '--no-resize')
          -k        keep intermediate files (scaled version of )
          -v        be verbose; show what is being done
          -h        you're looking at it ;-)

          Options that take an argument may be "cuddled"; e.g., '-g24x24'.
          Long options like '--geometry=GEOM' and '--keep' are also accepted.

  Problems? --> https://gist.github.com/ernstki/83c65441303728d13642259466f4ae57

Long options --geometry, --suffix, --no-border (or --borderless), --no-resize (or --original-size), --keep, and --verbose are also recognized. The ones that take arguments may either have the argument affixed (with an =), or separated by whitespace, like --geometry 200x200 --suffix _200px.

Examples

Thumbnail and drop-shadow all PNG images in the current directory, using the default maximum horizontal (x dimension) size of 400 pixels:

thumb image*.png
# creates 'image1_thumb.png', 'image2_thumb.png', ...

Same as above, but limit the horizontal dimension to 200 px instead, keeping the aspect ratio; also print what is happening with the -v / --verbose option:

thumb -v 200x image.png
# output:
#   [image.png] scaling to geometry 200x
#   [image.png] adding a drop shadow (bg=none, color=#777, width=1)
#   [image.png] removing intermediate file 'screencap_scaled.png'.

Same as above, but specify different suffix from the default of _thumb:

thumb -s @200x image.png 200x
# creates '[email protected]'

Note that arguments and filenames are understood in any order.

Thumbnail some JPEG images, Limiting the vertical (y) dimension to 200 pixels; good for images like panoramas that may have different x dimensions, but you want to be all the same height:

thumb pano*.jpg x200

JPEG images don't support transparency, so the drop-shadow will be drawn over a white background.

Don't draw the light grey border around the image; useful for non-rectangular images that have their own transparency, like macOS window screenshots:

# the options '--no-border' and '--borderless' are also accepted
thumb -nb window-screenshot.png

Here's a demonstration of some of the human-readable "long" options. Make a thumbnail of a photograph at exactly 0.3 megapixels, and keep the intermediate (scaled, but not drop-shadowed) image around, too:

thumb --geometry=300000@ --suffix=_3MP+shadow --keep photo.jpg
# creates 'photo_scaled.jpg' and 'photo_3MP+shadow.jpg'

See the "Image Geometry" section of the ImageMagick manual for other examples of what you can do with the -g / --geometry option.

Limitations

Existing files (either thumbnails or the _scaled intermediate files) are overwritten silently if they exist.

If the image filenames contain any path elements (e.g., /path/to/image.png), the thumbnails are output in the same directory as the source image, rather than the current working directory. This is usually what you want anyway, right?

Sizes specified with the XXXx and YYYx options are the pixel dimensions that the image is scaled to before adding the drop shadow. If you need an image to have exact pixel dimensions with the drop shadow, you'll need to modify the script on your own to do that math. This was not necessary for my simple use case of attaching screenshots to emails.

The script does not attempt to convert filetypes like JPEG that don't support transparency into another format like PNG, and that was beyond the scope of this one-hour hack; what it does instead is to force a white background.

If you're having trouble with the background color, it can be overridden by setting an environment variable, like this:

THUMB_BGCOLOR=white thumb [other options]

You can also change the width and color of the border, if you wish, using the environment variables THUMB_BORDERWIDTH and THUMB_BORDERCOLOR, respectively. Refer to the ImageMagic help for supported color specifications.

Author

Kevin Ernst (ernstki -at- mail.uc.edu)

Credits

License

MIT.

#!/bin/bash
#
# Thumbnail images specified on the command line, adding a drop shadow, with
# optional max horizontal/vertical pixel size
#
# Author: Kevin Ernst <ernstki -at- mail.uc.edu>
# Date: 2019-06-28
# Source: https://gist.github.com/ernstki/83c65441303728d13642259466f4ae57
#
# set TRACE=1 in the environment to inspect 'convert' commands as they run
(( ${TRACE:-} )) && set -x
ME=$( basename "${BASH_SOURCE[0]}" )
REQUIRES=( convert )
GIST='https://gist.github.com/ernstki/83c65441303728d13642259466f4ae57'
# defaults that you can override these in the environment (or set to null to
# omit); some of these don't have their own command-line options (yet)
MAX_X=${THUMB_MAX_X:-400}
MAX_Y=${THUMB_MAX_Y:-}
SUFFIX=${THUMB_SUFFIX:-_thumb}
BGCOLOR=${THUMB_BGCOLOR:-none}
BORDERWIDTH=${THUMB_BORDERWIDTH:-1}
BORDERCOLOR=${THUMB_BORDERCOLOR:-#777}
USAGE="
$ME - create thumbnails for specified image(s)
usage: $ME [XXXx] [xYYY] [-g GEOM] [-s SUFF] [-nb] [-nr] [-k] [-v] img [...]
$ME [-h|--help]
where: XXXx max horizontal pixel size (default: $MAX_X)
xYYY max vertical pixel size (default: keep aspect ratio)
-g GEOM arbitrary geometry specification understood by ImageMagick
(see: https://tinyurl.com/y6z8g45w#geometry)
-s SUFF optional suffix for the thumbail (default: '$SUFFIX')
-nb omit border around image (default: $BORDERWIDTH px)
-nr do not resize the image (long opt: '--no-resize')
-k keep intermediate files (scaled version of <image>)
-v be verbose; show what is being done
-h you're looking at it ;-)
Options that take an argument may be \"cuddled\"; e.g., '-g24x24'.
Long options like '--geometry=GEOM' and '--keep' are also accepted.
Problems? --> $GIST
"
x=$MAX_X
y=
geom=
suffix=$SUFFIX
bgcolor=$BGCOLOR
borderwidth=$BORDERWIDTH
bordercolor=$BORDERCOLOR
scaledname=
thumbname=
images=()
keepscaled=
verbose=
originalsize=
# Check to see if something's an integer
is_int() { [[ $1 =~ ^[1-9][0-9]+ ]]; }
bail() { echo "DOH! $*" >&2; exit 1; }
preflight() {
for req in "${REQUIRES[@]}"; do
if ! type -a $req &>/dev/null; then
bail "Missing required program '$req'."
fi
done
}
# see also:
# https://gist.github.com/ernstki/8db533a825f0d06b8200136832284d22
uncuddle() {
(( ${TRACE:-} )) && set -x
# all but the last argument
local opts=( ${@:1:$(($#-1))} )
# the input parameter, which might look like '--name=value'
local input=${@: -1}
local value=
for opt in "${opts[@]}"; do
[[ $input =~ ^$opt. ]] && value=${input#$opt}
done
echo "$value"
set -
}
# require at least one argument
if (( $# < 1 )); then echo "$USAGE" >&2; exit 1; fi
# check necessary external programs are present
preflight
while (( $# )); do
if [[ -f $1 ]]; then
images+=("$1")
shift; continue
fi
case "$1" in
-h*|--h*)
echo "$USAGE"
exit
;;
-s*|--suf*)
suffix=$( uncuddle -s --suffix= "$1" )
# 'uncuddle' returns empty string if no "cuddled" option found
if [[ -z $suffix ]]; then
shift
suffix=$1
fi
;;
-nb|--no-border|--borderless)
borderwidth=
;;
-nr|--no-resize|--original-size)
originalsize=1
;;
-k*|--keep)
keepscaled=1
;;
-v*|--verbose)
verbose=1
;;
[1-9]*[0-9]x)
x=${1%x}
;;
x[1-9]*[0-9])
y=${1#x}
;;
-g*|--geom*)
# can include ImageMagick geometry specifiers like '^' or '>'
# if properly quoted; see https://tinyurl.com/y6z8g45w#geometry
geom=$( uncuddle -g --geom= --geometry= "$1" )
if [[ -z $geom ]]; then
shift
geom=$1
fi
;;
*)
echo -e "\nERROR: Invalid option '$1' (or file not found)." >&2
echo "$USAGE" >&2
exit 1
;;
esac
shift
done
if [[ -n $x ]] && ! is_int "$x"; then
bail "Expected an integer for the thumbnail's horizontal (x) dimension."
fi
if [[ -n $y ]] && ! is_int "$y"; then
bail "Expected an integer for the thumbnail's vertical (y) dimension."
fi
# the '-g' / '--geometry' option, if given, overrides both of the above, but
# if it's not given, construct the 'convert -geometry' argument with $x and $y
[[ -z $geom ]] && geom="${x}x$y"
# use '-geometry 100%' if '-nr' / '--no-resize' / '--original-size' given
(( originalsize )) && geom='100%'
for image in "${images[@]}"; do
# considers SHORTEST possible extension; e.g. .1.2.png -> .1.2_thumb.png
scaledname="${image%.*}_scaled.${image##*.}"
thumbname="${image%.*}$suffix.${image##*.}"
# FIXME: don't even bother scaling the image if '--no-resize' given
(( verbose )) && echo "[$image] scaling to geometry ${x}x${y}" >&2
convert "$image" -geometry "$geom" "$scaledname"
# force white background for JPEGs (which can't do transparency)
if [[ $image =~ .[Jj][Pp][Ee]?[Gg]$ ]]; then
bgcolor='white'
fi
if (( verbose )); then
echo -n "[$image] adding a drop shadow (bg=$bgcolor, " >&2
echo "color=$bordercolor, width=$borderwidth)" >&2
fi
# source: https://stackoverflow.com/a/7136561/785213
convert "$scaledname" \
${bordercolor:+-bordercolor $bordercolor} \
${borderwidth:+-border $borderwidth} \
\( +clone -background black -shadow 80x3+2+2 \) \
+swap ${bgcolor:+-background $bgcolor} -layers merge +repage \
"$thumbname"
if (( !keepscaled )); then
(( verbose )) && \
echo "[$image] removing intermediate file '$scaledname'." >&2
test -f "$scaledname" && rm "$scaledname"
else
(( verbose )) && \
echo "[$image] preserving intermediate file '$scaledname'." >&2
fi
done
@ernstki
Copy link
Author

ernstki commented Feb 28, 2023

Here's a small helper script, which will find the last screenshot you took and pass all other options through to thumb. Put it in ~/bin/thumblast.

Modify SCREENSHOT_PATH for platforms other than macOS, and update SCREENSHOT_PATTERNS if your screenshot tool has some other file naming convention.

#!/usr/bin/env bash
##
##  Run the 'thumb' script on the last screenshot
##
##  Author:   Kevin Ernst <ernstki -at- cchmc.org>
##  License:  WTFPL
##  Source:   https://gist.github.com/ernstki/83c65441303728d13642259466f4ae57
##

# this is the location on macOS; change for other platforms
SCREENSHOT_PATH=~/Desktop

# add any patterns necessary to match screenshots on your system
# (quoted so they're not expanded prematurely by the shell)
SCREENSHOT_PATTERNS=(
    'Screenshot*.png'
    'Bildschirmfoto*.png'
    '*Shot*.png'
    '2?????-????.png'
)

get_last_screenshot() {
    # typically, you wouldn't do this*, but the lack of '-printf' in macOS's
    # 'find' doesn't leave many good options for "file list, sorted by mtime"
    #
    # *http://mywiki.wooledge.org/ParsingLs
    ls -t ${SCREENSHOT_PATTERNS[*]} 2>/dev/null \
      | grep -vE '_(thumb|scaled).png$' \
      | head -1
}

set -x
cd "$SCREENSHOT_PATH"
thumb "$@" "$(get_last_screenshot)"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment