|
| 1 | +#!/bin/bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +usage() { |
| 5 | + cat << EOF |
| 6 | +Usage: $0 [OPTIONS] IMAGE|DIRECTORY... |
| 7 | +
|
| 8 | +Optimize images for size while maintaining quality. Supports JPEG, PNG, WebP, and GIF formats. |
| 9 | +
|
| 10 | +OPTIONS: |
| 11 | + -h, --help Show this help message and exit |
| 12 | + -q, --quality NUM Set quality level (1-100, default: 85) |
| 13 | +
|
| 14 | +DESCRIPTION: |
| 15 | + Optimizes one or more image files using ImageMagick. Creates new files with |
| 16 | + the '.optimized' suffix (e.g., photo.jpg becomes photo.optimized.jpg). |
| 17 | +
|
| 18 | + If a directory is provided, recursively processes all supported images within it. |
| 19 | +
|
| 20 | + The script strips metadata and applies lossy compression to reduce file size |
| 21 | + while maintaining good visual quality. |
| 22 | +
|
| 23 | +PREREQUISITES: |
| 24 | + - ImageMagick must be installed ('magick' or 'convert' command) |
| 25 | +
|
| 26 | +EXAMPLES: |
| 27 | + $0 vacation-selfie.jpg |
| 28 | + $0 --quality 90 cat-meme.png |
| 29 | + $0 photos/summer-2024/ |
| 30 | + $0 logo.png banner.jpg downloads/ |
| 31 | +
|
| 32 | +EXIT CODES: |
| 33 | + 0 All images optimized successfully |
| 34 | + 1 Error occurred during processing |
| 35 | +EOF |
| 36 | +} |
| 37 | + |
| 38 | +# Default settings |
| 39 | +QUALITY=85 |
| 40 | + |
| 41 | +# Parse arguments |
| 42 | +POSITIONAL_ARGS=() |
| 43 | + |
| 44 | +while [[ $# -gt 0 ]]; do |
| 45 | + case $1 in |
| 46 | + -h|--help) |
| 47 | + usage |
| 48 | + exit 0 |
| 49 | + ;; |
| 50 | + -q|--quality) |
| 51 | + QUALITY="$2" |
| 52 | + shift 2 |
| 53 | + ;; |
| 54 | + *) |
| 55 | + POSITIONAL_ARGS+=("$1") |
| 56 | + shift |
| 57 | + ;; |
| 58 | + esac |
| 59 | +done |
| 60 | + |
| 61 | +# Reset positional parameters |
| 62 | +if [[ ${#POSITIONAL_ARGS[@]} -gt 0 ]]; then |
| 63 | + set -- "${POSITIONAL_ARGS[@]}" |
| 64 | +else |
| 65 | + set -- |
| 66 | +fi |
| 67 | + |
| 68 | +check_dependencies() { |
| 69 | + # Try both 'magick' (v7) and 'convert' (v6) commands |
| 70 | + if ! command -v magick &> /dev/null && ! command -v convert &> /dev/null; then |
| 71 | + echo "Error: Missing required dependencies: ImageMagick (magick or convert)" |
| 72 | + echo "Install ImageMagick via: brew install imagemagick" |
| 73 | + exit 1 |
| 74 | + fi |
| 75 | +} |
| 76 | + |
| 77 | +get_file_size() { |
| 78 | + local file="$1" |
| 79 | + # Try GNU stat first, then BSD stat |
| 80 | + if stat -c%s "$file" 2>/dev/null; then |
| 81 | + return |
| 82 | + elif stat -f%z "$file" 2>/dev/null; then |
| 83 | + return |
| 84 | + else |
| 85 | + echo "0" |
| 86 | + fi |
| 87 | +} |
| 88 | + |
| 89 | +format_size() { |
| 90 | + local size=$1 |
| 91 | + if [[ $size -lt 1024 ]]; then |
| 92 | + echo "${size}B" |
| 93 | + elif [[ $size -lt 1048576 ]]; then |
| 94 | + echo "$(( size / 1024 ))KB" |
| 95 | + else |
| 96 | + echo "$(( size / 1048576 ))MB" |
| 97 | + fi |
| 98 | +} |
| 99 | + |
| 100 | +optimize_image() { |
| 101 | + local input_file="$1" |
| 102 | + local basename="${input_file%.*}" |
| 103 | + local extension="${input_file##*.}" |
| 104 | + local output_file="${basename}.optimized.${extension}" |
| 105 | + |
| 106 | + # Skip if already optimized |
| 107 | + if [[ "$input_file" == *".optimized."* ]]; then |
| 108 | + return 0 |
| 109 | + fi |
| 110 | + |
| 111 | + # Get original size |
| 112 | + local original_size |
| 113 | + original_size=$(get_file_size "$input_file") |
| 114 | + |
| 115 | + # Optimize using ImageMagick |
| 116 | + local magick_cmd="magick" |
| 117 | + if ! command -v magick &> /dev/null; then |
| 118 | + magick_cmd="convert" |
| 119 | + fi |
| 120 | + |
| 121 | + if ! "$magick_cmd" "$input_file" -strip -quality "$QUALITY" "$output_file" 2>/dev/null; then |
| 122 | + echo "✗ Failed to optimize: $input_file" |
| 123 | + return 1 |
| 124 | + fi |
| 125 | + |
| 126 | + # Get new size and calculate savings |
| 127 | + local new_size |
| 128 | + new_size=$(get_file_size "$output_file") |
| 129 | + local saved=$((original_size - new_size)) |
| 130 | + local percent=0 |
| 131 | + if [[ $original_size -gt 0 ]]; then |
| 132 | + percent=$(( (saved * 100) / original_size )) |
| 133 | + fi |
| 134 | + |
| 135 | + echo "✓ $input_file" |
| 136 | + echo " $(format_size "$original_size") → $(format_size "$new_size") (saved $(format_size "$saved"), ${percent}%)" |
| 137 | + |
| 138 | + # Update global counters |
| 139 | + TOTAL_ORIGINAL=$((TOTAL_ORIGINAL + original_size)) |
| 140 | + TOTAL_NEW=$((TOTAL_NEW + new_size)) |
| 141 | + PROCESSED_COUNT=$((PROCESSED_COUNT + 1)) |
| 142 | +} |
| 143 | + |
| 144 | +process_path() { |
| 145 | + local path="$1" |
| 146 | + |
| 147 | + if [[ -f "$path" ]]; then |
| 148 | + # Process single file if it's a supported image format |
| 149 | + local ext="${path##*.}" |
| 150 | + ext=$(echo "$ext" | tr '[:upper:]' '[:lower:]') |
| 151 | + case "$ext" in |
| 152 | + jpg|jpeg|png|webp|gif) |
| 153 | + optimize_image "$path" |
| 154 | + ;; |
| 155 | + *) |
| 156 | + echo "Skipping unsupported file: $path" |
| 157 | + ;; |
| 158 | + esac |
| 159 | + elif [[ -d "$path" ]]; then |
| 160 | + # Process directory recursively |
| 161 | + echo "Processing directory: $path" |
| 162 | + while IFS= read -r -d '' file; do |
| 163 | + optimize_image "$file" |
| 164 | + done < <(find "$path" -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.webp" -o -iname "*.gif" \) -print0) |
| 165 | + else |
| 166 | + echo "Error: Path not found: $path" |
| 167 | + exit 1 |
| 168 | + fi |
| 169 | +} |
| 170 | + |
| 171 | +main() { |
| 172 | + check_dependencies |
| 173 | + |
| 174 | + # Validate quality parameter |
| 175 | + if [[ ! "$QUALITY" =~ ^[0-9]+$ ]] || [[ "$QUALITY" -lt 1 ]] || [[ "$QUALITY" -gt 100 ]]; then |
| 176 | + echo "Error: Quality must be between 1 and 100" |
| 177 | + exit 1 |
| 178 | + fi |
| 179 | + |
| 180 | + # Check if we have any arguments |
| 181 | + if [[ $# -eq 0 ]]; then |
| 182 | + echo "Error: No input files or directories specified" |
| 183 | + usage |
| 184 | + exit 1 |
| 185 | + fi |
| 186 | + |
| 187 | + echo "Optimizing images with quality: $QUALITY" |
| 188 | + echo "" |
| 189 | + |
| 190 | + # Global counters |
| 191 | + TOTAL_ORIGINAL=0 |
| 192 | + TOTAL_NEW=0 |
| 193 | + PROCESSED_COUNT=0 |
| 194 | + |
| 195 | + # Process each argument |
| 196 | + for arg in "$@"; do |
| 197 | + process_path "$arg" |
| 198 | + done |
| 199 | + |
| 200 | + echo "" |
| 201 | + echo "Summary:" |
| 202 | + echo "Processed: $PROCESSED_COUNT images" |
| 203 | + if [[ $PROCESSED_COUNT -gt 0 ]]; then |
| 204 | + local total_saved=$((TOTAL_ORIGINAL - TOTAL_NEW)) |
| 205 | + local total_percent=0 |
| 206 | + if [[ $TOTAL_ORIGINAL -gt 0 ]]; then |
| 207 | + total_percent=$(( (total_saved * 100) / TOTAL_ORIGINAL )) |
| 208 | + fi |
| 209 | + echo "Total size: $(format_size "$TOTAL_ORIGINAL") → $(format_size "$TOTAL_NEW")" |
| 210 | + echo "Total saved: $(format_size "$total_saved") (${total_percent}%)" |
| 211 | + fi |
| 212 | +} |
| 213 | + |
| 214 | +main "$@" |
0 commit comments