#!/usr/bin/env bash
#
# capafix.sh
# Author: Robert Miller, St. John's, Newfoundland and Labrador
#
# Purpose:
# Resize images to meet CAPA-style pixel dimensions and save corrected
# copies as new JPEG files while preserving all metadata.
#
# Notes:
# Designed for macOS with ExifTool and ImageMagick installed.
# Compatible with Linux and other UNIX-based systems.
# Update MAGICK and EXIFTOOL paths, if required.
#
# On macOS:
# Install Homebrew, then run:
# brew install exiftool
# brew install imagemagick
#
# Installation:
# Save this script to a directory in your system PATH (e.g., ~/bin or /usr/local/bin).
# Example:
# mkdir -p ~/bin
# mv capafix.sh ~/bin/
# chmod +x ~/bin/capafix.sh
#
# To verify your PATH:
# echo $PATH
#
# Ensure script location is listed. If not, add it:
# export PATH="$HOME/bin:$PATH"
#
# To make this permanent (macOS with bash):
# echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bash_profile
#
# Reload your shell:
# source ~/.bash_profile
#
# Confirm script is accessible:
# which capafix.sh
# capafix.sh --help
#
# Behaviour:
# - does not default to all files
# - accepts one or more filenames or globs
# - resizes to fit within CAPA maximum dimensions
# - writes output as basename-fixed.jpg
# - preserves all metadata from source file
# - appends comment: "Dimensions corrected by capafix.sh"
#
# Usage examples:
# ./capafix.sh "*.jpg"
# ./capafix.sh image1.jpg image2.tif
# ./capafix.sh --quality 92 *.jpg
# ./capafix.sh --filter Lanczos --unsharp 0x0.5 *.jpg
# ./capafix.sh --suffix=-fixed *.png
#
set -u
set -o pipefail
MAGICK="/usr/local/bin/magick"
EXIFTOOL="/usr/local/bin/exiftool"
MAX_WIDTH=1400
MAX_HEIGHT=1050
JPEG_QUALITY=92
FILTER="Lanczos"
UNSHARP="0x0.5"
USE_UNSHARP=1
SUFFIX="-fixed"
OVERWRITE=0
usage() {
cat <<EOF
Usage:
$(basename "$0") [options] [files or globs]
Options:
--max-width N Set max width
--max-height N Set max height
--quality N Set JPEG quality (default: $JPEG_QUALITY)
--filter NAME Set resize filter (default: $FILTER)
--unsharp VALUE Set unsharp mask (default: $UNSHARP)
--no-unsharp Disable unsharp mask
--suffix TEXT Set output suffix (default: $SUFFIX)
--overwrite Overwrite output file if it already exists
--help | -h | -? Show this help
Notes:
- At least one file or glob is required.
- Script does not default to scanning all files.
- Output is always written as JPEG.
- Output name is:
originalname${SUFFIX}.jpg
- All metadata is copied from source file to output file.
- Comment appended:
Dimensions corrected by capafix.sh
Examples:
$(basename "$0") "*.jpg"
$(basename "$0") image1.jpg image2.png
$(basename "$0") --quality 90 *.tif
$(basename "$0") --filter Lanczos --unsharp 0x0.5 *.jpg
$(basename "$0") --suffix=-capa *.png
EOF
}
die() {
echo "Error: $*" >&2
exit 1
}
warn() {
echo "Warning: $*" >&2
}
info() {
echo "$*"
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
lower_ext() {
printf "%s" "${1##*.}" | tr '[:upper:]' '[:lower:]'
}
is_supported_image() {
case "$(lower_ext "$1")" in
jpg|jpeg|png|tif|tiff) return 0 ;;
*) return 1 ;;
esac
}
build_output_name() {
input="$1"
dir=$(dirname "$input")
file=$(basename "$input")
stem="${file%.*}"
printf "%s/%s%s.jpg" "$dir" "$stem" "$SUFFIX"
}
add_file_if_valid() {
candidate="$1"
[ -f "$candidate" ] || return 0
if [ "${#FILES[@]}" -gt 0 ]; then
for existing in "${FILES[@]}"; do
[ "$existing" = "$candidate" ] && return 0
done
fi
FILES+=("$candidate")
}
expand_input_patterns() {
for arg in "$@"; do
if [ -e "$arg" ]; then
add_file_if_valid "$arg"
else
for match in $arg; do
[ -e "$match" ] || continue
add_file_if_valid "$match"
done
fi
done
}
INPUT_ARGS=()
while [ "$#" -gt 0 ]; do
case "$1" in
--max-width)
shift
[ "$#" -gt 0 ] || die "Missing value for --max-width"
MAX_WIDTH="$1"
;;
--max-height)
shift
[ "$#" -gt 0 ] || die "Missing value for --max-height"
MAX_HEIGHT="$1"
;;
--quality)
shift
[ "$#" -gt 0 ] || die "Missing value for --quality"
JPEG_QUALITY="$1"
;;
--filter)
shift
[ "$#" -gt 0 ] || die "Missing value for --filter"
FILTER="$1"
;;
--unsharp)
shift
[ "$#" -gt 0 ] || die "Missing value for --unsharp"
UNSHARP="$1"
USE_UNSHARP=1
;;
--no-unsharp)
USE_UNSHARP=0
;;
--suffix)
shift
[ "$#" -gt 0 ] || die "Missing value for --suffix"
SUFFIX="$1"
;;
--overwrite)
OVERWRITE=1
;;
--help|-h|-?)
usage
exit 0
;;
--*)
die "Unknown option: $1"
;;
*)
INPUT_ARGS+=("$1")
;;
esac
shift
done
[ "${#INPUT_ARGS[@]}" -gt 0 ] || {
usage >&2
exit 1
}
[ -x "$MAGICK" ] || command_exists magick || die "ImageMagick 'magick' not found"
[ -x "$EXIFTOOL" ] || command_exists exiftool || die "ExifTool not found"
if [ ! -x "$MAGICK" ]; then
MAGICK="$(command -v magick)"
fi
if [ ! -x "$EXIFTOOL" ]; then
EXIFTOOL="$(command -v exiftool)"
fi
case "$MAX_WIDTH" in ''|*[!0-9]*) die "--max-width must be an integer" ;; esac
case "$MAX_HEIGHT" in ''|*[!0-9]*) die "--max-height must be an integer" ;; esac
case "$JPEG_QUALITY" in ''|*[!0-9]*) die "--quality must be an integer" ;; esac
FILES=()
expand_input_patterns "${INPUT_ARGS[@]}"
[ "${#FILES[@]}" -gt 0 ] || die "No matching files found"
processed=0
failed=0
skipped=0
for input in "${FILES[@]}"; do
if [ ! -f "$input" ]; then
warn "Skipping non-file: $input"
skipped=$((skipped + 1))
continue
fi
if ! is_supported_image "$input"; then
warn "Skipping unsupported file type: $input"
skipped=$((skipped + 1))
continue
fi
output="$(build_output_name "$input")"
if [ "$input" = "$output" ]; then
warn "Skipping because output would overwrite input: $input"
skipped=$((skipped + 1))
continue
fi
if [ -e "$output" ] && [ "$OVERWRITE" -ne 1 ]; then
warn "Skipping existing output: $output"
skipped=$((skipped + 1))
continue
fi
tmp="${output}.tmp.$$"
info "Processing: $input"
info " Output: $output"
if [ "$USE_UNSHARP" -eq 1 ]; then
"$MAGICK" "$input" \
-auto-orient \
-colorspace sRGB \
-filter "$FILTER" \
-resize "${MAX_WIDTH}x${MAX_HEIGHT}>" \
-unsharp "$UNSHARP" \
-quality "$JPEG_QUALITY" \
-interlace none \
"$tmp"
else
"$MAGICK" "$input" \
-auto-orient \
-colorspace sRGB \
-filter "$FILTER" \
-resize "${MAX_WIDTH}x${MAX_HEIGHT}>" \
-quality "$JPEG_QUALITY" \
-interlace none \
"$tmp"
fi
if [ $? -ne 0 ]; then
warn "Resize failed: $input"
rm -f "$tmp"
failed=$((failed + 1))
continue
fi
"$EXIFTOOL" -overwrite_original \
-TagsFromFile "$input" -all:all \
-Comment+="Dimensions corrected by capafix.sh" \
"$tmp" >/dev/null 2>&1
if [ $? -ne 0 ]; then
warn "Metadata copy failed: $input"
rm -f "$tmp"
failed=$((failed + 1))
continue
fi
mv -f "$tmp" "$output"
if [ $? -ne 0 ]; then
warn "Could not move output into place: $output"
rm -f "$tmp"
failed=$((failed + 1))
continue
fi
processed=$((processed + 1))
done
echo
echo "Summary:"
echo " Processed: $processed"
echo " Failed: $failed"
echo " Skipped: $skipped"
[ "$failed" -eq 0 ] || exit 1
exit 0