#!/bin/bash # # ========================================================================= # Title: Optimize WordPress images # Description: Optimizes JPG, PNG, GIF and WebP images recursively # Author: David Carrero Fernández-Baillo <dcarrero@stackscale.com> # Website: https://carrero.es # GitHub: https://github.com/dcarrero # Twitter/X: @carrero # Version: 0.1 beta # Created: 2025-04-12 # License: MIT License # ========================================================================= # Default configuration IMAGES_DIR="." LOG_FILE="./image_optimization.log" DATE=$(date '+%Y-%m-%d %H:%M:%S') RECURSIVE=true # Process subdirectories recursively ENABLE_LOG=true # Enable logging JPG_QUALITY=85 # JPG quality (0-100) PNG_LEVEL=3 # PNG optimization level (0-7) WEBP_QUALITY=80 # WebP quality (0-100) DRY_RUN=false # Simulation mode SKIP_JPG=false # Skip JPG optimization SKIP_PNG=false # Skip PNG optimization SKIP_GIF=false # Skip GIF optimization SKIP_WEBP=false # Skip WebP optimization QUIET=false # Quiet mode VERBOSE=false # Verbose mode TOTAL_ORIGINAL=0 # Tracking statistics TOTAL_OPTIMIZED=0 TOTAL_SAVED=0 TOTAL_FILES=0 # Check if webp is available in the system WEBP_AVAILABLE=false if command -v cwebp &> /dev/null; then WEBP_AVAILABLE=true fi # Show help show_help() { cat << EOF Usage: $(basename "$0") [OPTIONS] Script to optimize images (JPG, JPEG, PNG, GIF, WebP) for WordPress and other websites. Options: -h, --help Show this help -d, --dir DIR Images directory (default: current directory) -l, --log FILE Log file (default: ./image_optimization.log) --no-recursive Don't process subdirectories --no-log Don't generate log file --jpg-quality N JPG/JPEG quality (0-100) (default: 85) --png-level N PNG optimization level (0-7) (default: 3) --webp-quality N WebP quality (0-100) (default: 80) --dry-run Run without changing files (simulation) --skip-jpg Skip JPG/JPEG optimization --skip-png Skip PNG optimization --skip-gif Skip GIF optimization --skip-webp Skip WebP optimization --quiet Quiet mode (errors only) --verbose Verbose mode Examples: $(basename "$0") -d /var/www/html/wp-content/uploads $(basename "$0") --dir ./photos --jpg-quality 90 --no-recursive $(basename "$0") --dry-run -d /var/www/uploads -l /tmp/optimization.log EOF exit 0 } # Parse arguments parse_arguments() { while [[ $# -gt 0 ]]; do case "$1" in -h|--help) show_help ;; -d|--dir) IMAGES_DIR="$2" shift 2 ;; -l|--log) LOG_FILE="$2" shift 2 ;; -r|--recursive) RECURSIVE=true shift ;; --no-recursive) RECURSIVE=false shift ;; --no-log) ENABLE_LOG=false shift ;; --jpg-quality) JPG_QUALITY="$2" shift 2 ;; --png-level) PNG_LEVEL="$2" shift 2 ;; --webp-quality) WEBP_QUALITY="$2" shift 2 ;; --dry-run) DRY_RUN=true shift ;; --skip-jpg) SKIP_JPG=true shift ;; --skip-png) SKIP_PNG=true shift ;; --skip-gif) SKIP_GIF=true shift ;; --skip-webp) SKIP_WEBP=true shift ;; --quiet) QUIET=true shift ;; --verbose) VERBOSE=true shift ;; *) echo "Unknown option: $1" echo "Use --help to see available options" exit 1 ;; esac done } # Log message with appropriate levels log_message() { local message="$1" local level="$2" # 0=error, 1=info, 2=verbose # Don't show info messages in quiet mode if [ "$QUIET" = true ] && [ "$level" -eq 1 ]; then return fi # Only show verbose messages in verbose mode if [ "$level" -eq 2 ] && [ "$VERBOSE" = false ]; then return fi # Prefix based on level local prefix="" if [ "$level" -eq 0 ]; then prefix="[ERROR] " elif [ "$level" -eq 2 ]; then prefix="[DEBUG] " fi if [ "$ENABLE_LOG" = true ]; then echo "${prefix}${message}" | tee -a "$LOG_FILE" else echo "${prefix}${message}" fi } # Check for required dependencies check_dependencies() { local missing_deps=() local required_tools=() # Always required tools required_tools=(find bc) # bc for calculations # Add tools based on enabled formats if [ "$SKIP_JPG" = false ]; then required_tools+=(jpegoptim) fi if [ "$SKIP_PNG" = false ]; then required_tools+=(optipng) fi if [ "$SKIP_GIF" = false ]; then required_tools+=(gifsicle) fi if [ "$SKIP_WEBP" = false ] && [ "$WEBP_AVAILABLE" = true ]; then required_tools+=(cwebp) fi # Handle stat command differently between macOS and Linux if [[ "$OSTYPE" == "darwin"* ]]; then if ! command -v gstat &> /dev/null; then missing_deps+=("gstat (GNU stat from coreutils)") fi else required_tools+=(stat) fi for cmd in "${required_tools[@]}"; do if ! command -v "$cmd" &> /dev/null; then missing_deps+=("$cmd") fi done if [ ${#missing_deps[@]} -ne 0 ]; then log_message "Missing dependencies: ${missing_deps[*]}" 0 if [[ "$OSTYPE" == "darwin"* ]]; then log_message "Install dependencies on macOS with:" 1 log_message "brew install jpegoptim optipng gifsicle webp coreutils bc" 1 elif [ -f /etc/debian_version ]; then log_message "Install dependencies on Debian/Ubuntu with:" 1 log_message "sudo apt-get install jpegoptim optipng gifsicle webp bc" 1 elif [ -f /etc/redhat-release ]; then log_message "Install dependencies on CentOS/RHEL/CloudLinux with:" 1 log_message "sudo yum install epel-release" 1 log_message "sudo yum install jpegoptim optipng gifsicle bc" 1 if [ "$SKIP_WEBP" = false ] && [ "$WEBP_AVAILABLE" = false ]; then log_message "Note: WebP tools are not available in standard repositories for your system." 1 log_message "WebP processing will be skipped. If you need WebP support, compile from source or" 1 log_message "find an alternative repository with WebP tools." 1 SKIP_WEBP=true fi else log_message "Install the missing dependencies using your package manager" 1 if [ "$SKIP_WEBP" = false ] && [ "$WEBP_AVAILABLE" = false ]; then log_message "Note: WebP support will be skipped" 1 SKIP_WEBP=true fi fi exit 1 fi } # Check if directory exists and is writable check_directory() { if [ ! -d "$IMAGES_DIR" ]; then log_message "Directory does not exist: $IMAGES_DIR" 0 exit 1 fi if [ ! -w "$IMAGES_DIR" ] && [ "$DRY_RUN" = false ]; then log_message "No write permission on $IMAGES_DIR" 0 log_message "Use --dry-run to test without writing or run with proper permissions" 1 exit 1 fi } # Get file size based on OS get_file_size() { local file="$1" if [[ "$OSTYPE" == "darwin"* ]]; then # macOS requires gstat (GNU stat from coreutils) gstat -c %s "$file" else # Linux stat -c %s "$file" fi } # Optimize a single image optimize_image() { local img="$1" local img_type="$2" local before_size=$(get_file_size "$img") local output="" local status="unchanged" # Show that we're starting to process this file echo -n "Processing: $img ($img_type, $before_size bytes)... " # Don't make actual changes in dry-run mode if [ "$DRY_RUN" = false ]; then case "$img_type" in jpg|jpeg) if [ "$SKIP_JPG" = false ]; then jpegoptim --strip-all --max="$JPG_QUALITY" --quiet "$img" fi ;; png) if [ "$SKIP_PNG" = false ]; then optipng -o"$PNG_LEVEL" -quiet "$img" fi ;; gif) if [ "$SKIP_GIF" = false ]; then gifsicle -b -O3 "$img" 2>/dev/null fi ;; webp) if [ "$SKIP_WEBP" = false ]; then # Reoptimize WebP (create temp file and replace if smaller) local temp_file="$(mktemp).webp" cwebp -quiet -mt -q "$WEBP_QUALITY" "$img" -o "$temp_file" if [ -f "$temp_file" ]; then local temp_size=$(get_file_size "$temp_file") if [ "$temp_size" -lt "$before_size" ]; then mv "$temp_file" "$img" else rm "$temp_file" fi fi fi ;; esac fi local after_size=$(get_file_size "$img") local saved_size=$((before_size - after_size)) local saved_percent=0 if [ "$before_size" -ne 0 ]; then saved_percent=$((saved_size * 100 / before_size)) fi TOTAL_FILES=$((TOTAL_FILES + 1)) if [ "$before_size" -ne "$after_size" ] || [ "$DRY_RUN" = true ]; then if [ "$DRY_RUN" = true ]; then echo "SIMULATED (would save ~15%)" status="simulated" # In simulation, assume 15% savings for approximate statistics local simulated_size=$((before_size * 85 / 100)) TOTAL_ORIGINAL=$((TOTAL_ORIGINAL + before_size)) TOTAL_OPTIMIZED=$((TOTAL_OPTIMIZED + simulated_size)) TOTAL_SAVED=$((TOTAL_SAVED + (before_size - simulated_size))) output="SIMULATED: $img (current size: $before_size bytes, estimated new size: $simulated_size bytes)" else echo "OPTIMIZED! $before_size -> $after_size bytes ($saved_percent% reduced)" status="optimized" TOTAL_ORIGINAL=$((TOTAL_ORIGINAL + before_size)) TOTAL_OPTIMIZED=$((TOTAL_OPTIMIZED + after_size)) TOTAL_SAVED=$((TOTAL_SAVED + saved_size)) output="Optimized: $img ($before_size -> $after_size bytes, $saved_percent% reduced)" fi else echo "Already optimized" status="unchanged" # Still add the file size to totals TOTAL_ORIGINAL=$((TOTAL_ORIGINAL + before_size)) TOTAL_OPTIMIZED=$((TOTAL_OPTIMIZED + after_size)) output="Already optimized: $img" fi if [ "$ENABLE_LOG" = true ]; then echo "$output" >> "$LOG_FILE" fi } # Process images of a specific type process_images() { local type="$1" local pattern="$2" # Skip disabled image types case "$type" in jpg|jpeg) if [ "$SKIP_JPG" = true ]; then log_message "Skipping processing of JPG/JPEG images" 2 return fi ;; png) if [ "$SKIP_PNG" = true ]; then log_message "Skipping processing of PNG images" 2 return fi ;; gif) if [ "$SKIP_GIF" = true ]; then log_message "Skipping processing of GIF images" 2 return fi ;; webp) if [ "$SKIP_WEBP" = true ] || [ "$WEBP_AVAILABLE" = false ]; then log_message "Skipping processing of WebP images" 2 return fi ;; esac log_message "Searching for $type images..." 1 # Use recursive or non-recursive mode local find_cmd="find \"$IMAGES_DIR\"" if [ "$RECURSIVE" = false ]; then find_cmd="$find_cmd -maxdepth 1" fi # First count total files for this type local total_files_for_type=$(eval "$find_cmd -type f -iname \"$pattern\"" | wc -l) if [ "$total_files_for_type" -eq 0 ]; then if [ "$VERBOSE" = true ]; then log_message "No $type images found in $IMAGES_DIR" 2 fi return else log_message "Found $total_files_for_type $type images to process" 1 # Display header for this image type section local separator="--------------------------------------" echo "$separator" echo "PROCESSING $type IMAGES ($total_files_for_type files)" echo "$separator" fi # Process files one by one - simple and reliable local file_count=0 while IFS= read -r img; do # Skip if empty [ -z "$img" ] && continue # Update progress counter file_count=$((file_count + 1)) # Show file number/total at the beginning of each line echo -n "[$file_count/$total_files_for_type] " # Process the image (will show detailed output) optimize_image "$img" "$type" done < <(eval "$find_cmd -type f -iname \"$pattern\"") # Show summary for this image type echo "$separator" echo "Completed processing $file_count $type images" echo "" } # Show statistics show_stats() { if [ "$TOTAL_FILES" -gt 0 ]; then # Convert to MB for better readability local original_mb=$(echo "scale=2; $TOTAL_ORIGINAL/1048576" | bc) local optimized_mb=$(echo "scale=2; $TOTAL_OPTIMIZED/1048576" | bc) local saved_mb=$(echo "scale=2; $TOTAL_SAVED/1048576" | bc) local percent=0 if [ "$TOTAL_ORIGINAL" -ne 0 ]; then percent=$(echo "scale=2; $TOTAL_SAVED*100/$TOTAL_ORIGINAL" | bc) fi local title="OPTIMIZATION SUMMARY ($DATE)" if [ "$DRY_RUN" = true ]; then title="OPTIMIZATION SIMULATION ($DATE)" fi local separator="======================================================" log_message "$separator" 1 log_message "$title" 1 log_message "${separator//?/-}" 1 log_message "Directory: $IMAGES_DIR" 1 log_message "Files processed: $TOTAL_FILES" 1 log_message "Original size: $original_mb MB" 1 log_message "Optimized size: $optimized_mb MB" 1 log_message "Space saved: $saved_mb MB ($percent%)" 1 if [ "$DRY_RUN" = true ]; then log_message "NOTE: This is a simulation (--dry-run), no changes were made" 1 fi if [ "$VERBOSE" = true ]; then log_message "Configuration used:" 2 log_message "- JPG quality: $JPG_QUALITY" 2 log_message "- PNG level: $PNG_LEVEL" 2 log_message "- WebP quality: $WEBP_QUALITY" 2 log_message "- Recursive: $RECURSIVE" 2 fi # Add estimated disk space savings if [ "$saved_mb" != "0.00" ]; then log_message "" 1 if [ "$DRY_RUN" = true ]; then log_message "Potential disk space savings: $saved_mb MB" 1 else log_message "Disk space saved: $saved_mb MB" 1 fi # For non-dry runs, add some human-readable context to the savings if [ "$DRY_RUN" = false ]; then if (( $(echo "$saved_mb > 1000" | bc -l) )); then log_message "That's more than 1 GB of disk space saved!" 1 elif (( $(echo "$saved_mb > 500" | bc -l) )); then log_message "That's half a GB of disk space saved!" 1 elif (( $(echo "$saved_mb > 100" | bc -l) )); then log_message "That's a significant amount of disk space saved!" 1 elif (( $(echo "$saved_mb > 10" | bc -l) )); then log_message "Every byte counts - good optimization!" 1 fi fi fi log_message "$separator" 1 else log_message "No images found to optimize" 1 fi } # Main function main() { # Process command line arguments parse_arguments "$@" # Prepare log file if [ "$ENABLE_LOG" = true ]; then # Create log directory if it doesn't exist LOG_DIR=$(dirname "$LOG_FILE") if [ ! -d "$LOG_DIR" ]; then mkdir -p "$LOG_DIR" 2>/dev/null if [ $? -ne 0 ]; then echo "Could not create log directory: $LOG_DIR" echo "Disabling logs..." ENABLE_LOG=false fi fi # Test writing to log file if [ "$ENABLE_LOG" = true ]; then touch "$LOG_FILE" 2>/dev/null if [ $? -ne 0 ]; then echo "Could not write to log file: $LOG_FILE" echo "Disabling logs..." ENABLE_LOG=false fi fi fi # Show execution mode if [ "$DRY_RUN" = true ]; then log_message "STARTING OPTIMIZATION SIMULATION ($DATE)" 1 log_message "Simulation mode: No changes will be made to files" 1 else log_message "STARTING IMAGE OPTIMIZATION ($DATE)" 1 fi # Initial checks check_dependencies check_directory # Configuration information if [ "$VERBOSE" = true ]; then log_message "Directory: $IMAGES_DIR" 2 log_message "Recursive: $RECURSIVE" 2 log_message "Log file: $LOG_FILE" 2 log_message "JPG quality: $JPG_QUALITY" 2 log_message "PNG level: $PNG_LEVEL" 2 log_message "WebP quality: $WEBP_QUALITY" 2 # Show OS-specific information if [[ "$OSTYPE" == "darwin"* ]]; then log_message "Operating System: macOS" 2 elif [ -f /etc/debian_version ]; then log_message "Operating System: Debian/Ubuntu" 2 elif [ -f /etc/redhat-release ]; then log_message "Operating System: CentOS/RHEL" 2 elif [ -f /etc/arch-release ]; then log_message "Operating System: Arch Linux" 2 else log_message "Operating System: Linux (unknown distro)" 2 fi fi # Reset counters TOTAL_ORIGINAL=0 TOTAL_OPTIMIZED=0 TOTAL_SAVED=0 TOTAL_FILES=0 # Show start time START_TIME=$(date +%s) # Process each image type process_images "jpg" "*.jpg" process_images "jpeg" "*.jpeg" process_images "png" "*.png" process_images "gif" "*.gif" process_images "webp" "*.webp" # Calculate elapsed time END_TIME=$(date +%s) ELAPSED_TIME=$((END_TIME - START_TIME)) # Format time as minutes and seconds MINS=$((ELAPSED_TIME / 60)) SECS=$((ELAPSED_TIME % 60)) # Show statistics show_stats # Show completion message with time if [ "$DRY_RUN" = true ]; then log_message "SIMULATION COMPLETED in ${MINS}m ${SECS}s ($DATE)" 1 else log_message "OPTIMIZATION COMPLETED in ${MINS}m ${SECS}s ($DATE)" 1 fi } # Run the main program with received arguments main "$@"