Skip to content

Instantly share code, notes, and snippets.

@dtonhofer
Created July 3, 2019 16:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dtonhofer/6c44186d65d3b15784b64096e60195a0 to your computer and use it in GitHub Desktop.
Save dtonhofer/6c44186d65d3b15784b64096e60195a0 to your computer and use it in GitHub Desktop.
A shell script around the ImageMagick "convert" command. Shrink jpegs in a source directory to 25% of the original width/height.
#!/bin/bash
# This program passes shellcheck (www.shellcheck.net)
# Author: David Tonhofer
# Complexity: Low
# License: Public Domain / "The Unlicense" - https://unlicense.org/
# ===
#
# Synopsis:
#
# Give help message:
#
# ./resize_photos.sh --help
#
# Dry-run, maybe creates $TARGET:
#
# ./resize_photos.sh --srcdir $SOURCE --trgdir $TARGET
#
# Actual run, processes jpgs in $SOURCE and writes them to $TARGET which
# is deleted before it is recreated ("--cleanup")
#
# ./resize_photos.sh --srcdir $SOURCE --trgdir $TARGET --cleanup --reallydo
#
#
# (you can also put '=' between option and argument)
#
# ===
#
# Description:
#
# Convert every (jpg) image found in a source directory given on the commandline
# by resizing it by 25% and putting the result into a target directory, also
# given on the commandline.
#
# The 25% is currently hardcoded
#
# - If the target directory does not exist, it is created.
#
# - If "--cleanup" is given on the commandline, and the target directory exists,
# it is removed before processing starts.
#
# - Nothing happens (excpet target directory creation) unless "--reallydo" has
# been given on the commandlines (i.e. a "dryrun" is the default behaviour).
#
# - Target file names are based on the original file names, with a ".small."
# inserted before the ending, so one gets "foo.small.jpg" for example.
#
# - The conversion will replace any target file in case of a name clash.
#
# - If the target directory is the same as source directory the "small" files
# will be created in the source directory.
#
# ===
set -o nounset
percentage=25
# ===
# Two functions below taken from
# http://blog.publicobject.com/2006/06/canonical-path-of-file-in-bash.html
# ===
# ---
# Get the canonical path of a file or directory.
# When the file or directory itself is a link then this is not resolved.
#
# Note: the '&> /dev/null' is needed because the 'cd' command will also
# print the directory into which it changes when the CDPATH environment
# variable is set.
#
# 1: the file or directory
# ---
function path-canonical-simple() {
local dst="${1}"
cd -P -- "$(dirname -- "${dst}")" &> /dev/null && echo "$(pwd -P)/$(basename -- "${dst}")"
}
# ---
# Get the canonical path of a file or directory.
# When the file or directory itself is a link then this is also resolved.
#
# 1: the file or directory
# ---
function path-canonical() {
local dst="$(path-canonical-simple "${1}")"
while [[ -h "${dst}" ]]; do
local linkDst="$(ls -l "${dst}" | sed -r -e 's/^.*[[:space:]]*->[[:space:]]*(.*)[[:space:]]*$/\1/g')"
if [[ -z "$(echo "${linkDst}" | grep -E '^/')" ]]; then
# relative link destination
linkDst="$(dirname "${dst}")/${linkDst}"
fi
dst="$(path-canonical-simple "${linkDst}")"
done
echo "${dst}"
}
# ===
# Check whether ImageMagick 'convert' is around, using the bash builtin "command" (replaces "which")
# ===
convertcommand=$(command -v convert) || {
echo "The command 'convert' does not exist." >&2
echo "You may have to install package 'ImageMagick' first!" >&2
exit 1
}
# ===
# Stuff set from the command line
# ===
reallydo= # set if this is not a "dryrun"
cleanup= # will remove targetdir if it exist before starting conversion
srcdir= # source directory with files to resize
trgdir= # target directory in which smaller files will be created
processOptions() {
local print_help=
local use_srcdir=
local use_trgdir=
local unknown=
for param in "$@"; do
#
# get value or previously given option
#
if [[ -n $use_srcdir ]]; then
srcdir=$param
use_srcdir=""
continue
fi
if [[ -n $use_trgdir ]]; then
trgdir=$param
use_trgdir=""
continue
fi
#
# Process "--option=arg" elements, but also cater for "--option arg" style
#
if [[ $param =~ ^--srcdir(=.+)? ]]; then
if [[ $param =~ ^--srcdir=(.+)? ]]; then
srcdir=$(echo "$param" | cut --delimiter="=" --fields=2)
else
use_srcdir=1
fi
continue
fi
if [[ $param =~ ^--trgdir(=.+)? ]]; then
if [[ $param =~ ^--trgdir=(.+)? ]]; then
trgdir=$(echo "$param" | cut --delimiter="=" --fields=2)
else
use_trgdir=1
fi
continue
fi
#
# get flag
#
if [[ $param == '--cleanup' ]]; then
cleanup=1
continue
fi
if [[ $param == '--reallydo' ]]; then
reallydo=1
continue
fi
# Special case: "--help" or "-h"
if [[ $param == '--help' || $param == '-h' ]]; then
print_help=1
break
fi
# if we are here, we encountered something unknown in "param";
# if "unknown" is already set, add a comma for separation
if [[ -n $unknown ]]; then
unknown="$unknown,"
fi
unknown="${unknown}${param}"
done
if [[ -n $unknown ]]; then
echo "Unknown parameters: '$unknown' -- exiting" >&2
print_help=1
fi
if [[ -n $print_help ]]; then
cat >&2 <<HERE
Resize photos!
--reallydo Really perform changes. Otherwise this is just a dryrun.
--cleanup Will remove "trgdir" and its contents if it exists, then re-create it.
--srcdir=DIR Directory containing files to process.
--trgdir=DIR Directory in which resized files are created. Created if it does not exist.
HERE
exit 1
fi
}
processOptions "$@"
# echo "Reallydo = $reallydo"
# echo "Cleanup = $cleanup"
# echo "Srcdir = $srcdir"
# echo "Trgdir = $trgdir"
# ---
# Check what has been passed
# ---
if [[ -z "$srcdir" ]]; then
echo "Source directory not given -- exiting!" >&2
exit 1
fi
if [[ -z "$trgdir" ]]; then
echo "Target directory not given -- exiting!" >&2
exit 1
fi
if [[ ! -d "$srcdir" ]]; then
echo "Passed source directory '$srcdir' does not exist -- exiting!" >&2
exit 1
fi
# This even handles '~'
srcdir=$(path-canonical "$srcdir")
trgdir=$(path-canonical "$trgdir")
echo "Passed source directory made canonical: $srcdir" >&2
echo "Passed target directory made canonical: $trgdir" >&2
if [[ "$srcdir" == "$trgdir" ]]; then
echo "Source directory and target directory are the same. No problemo." >&2
# Do nothing further...
else
if [[ -e "$trgdir" && ! -d "$trgdir" ]]; then
echo "Target directory exists but is not actually a directory -- exiting" >&2
exit 1
fi
if [[ -n $cleanup ]]; then
if [[ -z $reallydo ]]; then
echo "Skipping check about whether target directory should be deleted because this is a dryrun." >&2
else
if [[ -d "$trgdir" ]]; then
echo "Target directory '$trgdir' already exists ... removing it!" >&2
# do not wildly delete recursively, but be somewhat cautious
countfiles=$(find "$trgdir" -maxdepth 1 -mindepth 1 -type f | wc -l)
countany=$(find "$trgdir" -mindepth 1 | wc -l)
if [[ $countfiles -eq $countany ]] ; then
find "$trgdir" -maxdepth 1 -type f -delete
rmdir "$trgdir" || {
echo "Could not remove existing target directory '$trgdir' -- exiting" >&2
exit 1
}
else
echo "Target directory '$trgdir' is not a single directory with files inside ... NOT removing this!" >&2
fi
fi
fi
fi
fi
# ---
# Create the target directory if it is missing.
# This is done even in the case of a "dryrun"
# ---
if [[ ! -d "$trgdir" ]]; then
mkdir "$trgdir" || {
echo "'$trgdir' does not exist but could not create it -- exiting" >&2
exit 1
}
fi
# ---
# cd to the srcdir and trgdir to make doubly sure they exist and can be accessed
# ---
cd "$srcdir" || {
echo "Could not cd to '$srcdir' -- exiting" >&2
exit 1
}
cd "$trgdir" || {
echo "Could not cd to '$trgdir' -- exiting" >&2
exit 1
}
# ---
# Collect files before processing starts, and not using "ls"!
# Actually one could use "readarray" here
# ---
unset thefiles
declare -a thefiles
while IFS= read -r -u3 -d $'\0' somefile; do
thefiles+=( "$somefile" )
done 3< <(find "$srcdir" -maxdepth 1 -type f -print0)
# ---
# Process files!
# ---
for somefile in "${thefiles[@]}"; do
# echo "Processing $somefile" >&2
mimetype=$(file --brief --mime-type "$somefile")
if [[ $mimetype != "image/jpeg" ]]; then
echo "Skipping file '$somefile' with mime type '$mimetype'" >&2
continue
fi
base=$(basename "$somefile")
newbase=$(echo "$base" | perl -n -e 'if ( ~/^(.+)\.(\w+)$/ ) { print "$1.small.$2" } else { print "$_.small" }')
# echo "$base --> $newbase" >&2
# echo "$srcdir/$base --> $trgdir/$newbase" >&2
if [[ -n $reallydo ]]; then
# Here it is! Putting the job into the background and waiting for it allows CTRL-C
# to kill the script, not just the command
"$convertcommand" "$srcdir/$base" -verbose -resize "${percentage}%" "$trgdir/$newbase" &
wait
else
echo "Dryun: $convertcommand '$srcdir/$base' -resize ${percentage}% '$trgdir/$newbase'" >&2
fi
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment