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