webp-convert.shon commit improve webp-convert.sh (b8ae487)
   1#!/bin/bash
   2#
   3# webp-convert.sh
   4#
   5# Search through HTML and PHP files in a directory, get a list of the image
   6# files used (jpg/png), and convert these to webp in the same directory as
   7# the originals. Alternatively, convert all jpg/png files in a directory to 
   8# webp. Can be used with a rewrite rule on the web server to serve the webp 
   9# file when the original is requested.
  10#
  11# Andrew Lorimer, January 2021
  12#
  13
  14usagelong="\e[1mUSAGE:\e[0m $(basename "$0") [OPTIONS] [DIRECTORY]
  15
  16\e[1mOPTIONS:\e[0m
  17  -a|--all
  18    Convert all image files in DIRECTORY to webp. This is the default mode. 
  19    This is mutually exclusive with -s|--scan.
  20
  21  -s|--scan
  22    Scan all files with .php or .html extension in DIRECTORY for <img> tags 
  23    and convert source images to webp. This is mutually exclusive with 
  24    -a|--all.
  25
  26  -o|--overwrite
  27    Convert all files even if the output filename exists (the default is to 
  28    ignore files for which a webp already exists). When calculating space 
  29    savings of files which overwrite existing ones, the saving is relative to 
  30    the source file, not the file that is overwritten.
  31
  32  -q|--quiet
  33    Do not print any output (apart from errors). Mutually exclusive with 
  34    -v|--verbose.
  35
  36  -v|--verbose
  37    Print extra output for each file. Mutually exclusive with -q|--quiet.
  38
  39  -d|--dry
  40    Dry run - search for files and attempt to convert them but do not make any
  41    permanent changes. Used for checking possible space savings.
  42
  43  -h|--help
  44    Print this help message and exit.
  45
  46\e[1mDIRECTORY:\e[0m
  47    Directory in which to search for PHP or HTML files (in -s|--scan mode) or 
  48    image files (in -a|--all mode).
  49
  50Converts JPG and PNG files to WebP using ImageMagick's convert(1). Optionally 
  51searches for source file in a directory or searches through HTML/PHP files in 
  52a directory to find references to source files.\n"
  53
  54usageshort="\e[1mUSAGE:\e[0m
  55  $(basename "$0") [MODE] [OPTIONS] [DIRECTORY]
  56
  57For more information, see \e[34m$(basename "$0") --help\e[0m\n"
  58
  59modehint="\x1b[31m-s|--scan and -a|--all cannot be specified simultaneously\x1b[0m\n\n$usageshort"
  60verbosityhint="\x1b[31m-q|--quiet and -v|--verbose cannot be specified simultaneously\x1b[0m\n\n$usageshort"
  61dryhint="\x1b[33mDry run mode\x1b[0m\n"
  62overwritehint="\x1b[33mOverwriting existing files\x1b[0m\n"
  63invalidhint="\x1b[31mInvalid argument $1\x1b[0m\n\n$usageshort"
  64
  65if (( $# > 5)); then
  66  printf "$usageshort"
  67  exit 1
  68fi
  69
  70mode=-1       # -1: initial, 0: all (default), 1: scan
  71overwrite=0
  72dry=0
  73verbosity=-1  # -1: initial, 0: quiet, 1: normal, 2: verbose
  74directory=""
  75
  76while [ $# -gt 0 ]; do
  77  case "$1" in
  78    -a|--all)
  79      if [[ $mode == 1 ]]; then
  80        printf "$modehint"
  81        exit 1
  82      fi
  83      mode=0
  84      shift
  85      ;;
  86    -s|--scan)
  87      if [[ $mode == 0 ]]; then
  88        printf "$modehint"
  89        exit 1
  90      fi
  91      mode=1
  92      shift
  93      ;;
  94    -o|--overwrite)
  95      overwrite=1
  96      shift
  97      ;;
  98    -q|--quiet)
  99      if [[ $verbosity == 2 ]]; then
 100        printf "$verbosityhint"
 101        exit 1
 102      fi
 103      verbosity=0
 104      shift
 105      ;;
 106    -v|--verbose)
 107      if [[ $verbosity == 0 ]]; then
 108        printf "$verbosityhint"
 109        exit 1
 110      fi
 111      verbosity=2
 112      shift
 113      ;;
 114    -d|--dry)
 115      dry=1
 116      shift
 117      ;;
 118    -h|--help)
 119      printf "$usagelong"
 120      exit
 121      ;;
 122    *)
 123      if [[ "$directory" = "" && "$1" != -* ]]; then
 124        directory=$1
 125        shift
 126      else
 127        printf "$invalidhint"
 128        exit 1
 129      fi
 130      ;;
 131  esac
 132done
 133
 134# Set mode to 0 (all) if not specified
 135if [[ $mode == -1 ]]; then
 136  mode=0
 137fi
 138
 139# Set directory to . if not specified
 140if [[ "$directory" = "" ]]; then
 141  directory="."
 142fi
 143
 144# Set verbosity to 1 if not specified
 145if [[ $verbosity == -1 ]]; then
 146  verbosity=1
 147fi
 148
 149# Indicate that we are running in dry mode
 150if [[ $verbosity > 0 && $dry == 1 ]]; then
 151  printf "$dryhint"
 152fi
 153
 154# Indicate that we will overwrite
 155if [[ $verbosity > 0 && $overwrite == 1 ]]; then
 156  printf "$overwritehint"
 157fi
 158
 159savings=()
 160savings_total=0
 161found_count=0
 162convert_count=0
 163larger_count=0
 164failed_count=0
 165
 166convert_webp () {
 167  # Perform the conversion. Takes input filename, output filename.
 168
 169  # Set destination (user-supplied is quoted to handle spaces properly)
 170  if [[ $dry == 1 ]]; then
 171    suffix="/tmp/webp-convert.webp"
 172  else
 173    suffix="$2"
 174  fi
 175
 176  if [[ "$1" == *.png ]]; then
 177    # PNG conversion
 178    convert "$1" -quality 75 -define webp:lossless=true $suffix
 179    convert_status=$?
 180  else
 181    # JPG conversion
 182    convert "$1" -quality 75 $suffix
 183    convert_status=$?
 184  fi
 185
 186  # Check status code & print feedback
 187  if [[ $convert_status == 0 ]]; then
 188    # Successful
 189    ((convert_count+=1))
 190    if [[ $verbosity > 1 ]]; then
 191      echo "Converted $1 to $2"
 192    fi
 193  else
 194    # Failed
 195    ((failed_count+=1))
 196    if [[ $verbosity > 0 ]]; then
 197      printf "\x1b[31mFailed to convert $1 to $2\x1b[0m\n"
 198    fi
 199    return 1  # Return because we don't need to check file size
 200  fi
 201
 202  # Check if webp is actually smaller than original and remove if not
 203  orig_size=$(stat -c%s "$1")
 204  if [[ $dry ]]; then
 205    webp_size=$(stat -c%s $suffix)
 206  else
 207    webp_size=$(stat -c%s "$2")
 208  fi
 209  if (( webp_size > orig_size )); then
 210    if [[ ! $dry ]]; then
 211      rm $2
 212    fi
 213    if [[ $verbosity > 0 ]]; then
 214      printf "\x1b[33mRemoved $2 as it was larger than $1 ($webp_size > $orig_size)\x1b[0m\n"
 215    fi
 216    ((larger_count+=1))
 217    return 1
 218  fi
 219
 220  # Calculate file size saving
 221  saving=$((orig_size - webp_size))
 222  savings+=($saving)
 223  ((savings_total+=$saving))
 224  
 225  return 0
 226}
 227
 228if [[ $mode == 0 ]]; then
 229  # Convert all images in directory
 230  if [[ $verbosity > 1 ]]; then
 231    printf "Converting image files in $directory\n"
 232  fi
 233  for file_path in $(find $directory -type f -and \( -iname '*.jpg' -o -iname '*.jpeg' \)); do
 234    ((found_count+=1))
 235    webp_path=$(sed 's/.jpe\?g$/.webp/' <<< "$file_path")
 236    if [[ ! -f "$webp_path"  || $overwrite == 1 ]]; then
 237      convert_webp $file_path $webp_path
 238    fi
 239  done
 240else
 241  # Scan php and html files for images to convert
 242  while read -r srcfile; do
 243      if [[ $verbosity > 1 ]]; then
 244        printf "Scanning for image files referenced in $srcfile\n"
 245      fi
 246      while IFS= read -r file; do
 247        ((found_count+=1))
 248        file_path=$(echo $file | sed "s|^.|$directory/|")
 249        webp_path=$(sed 's/\.[^.]*$/.webp/' <<< "$file_path")
 250        
 251        # Convert if not already converted
 252        if [[ ! -f "$webp_path"  || $overwrite == 1 ]]; then
 253          convert_webp $file_path $webp_path
 254        fi
 255      done < <(sed -n "s:.*<img src=\"\([^\"]*\.\(jpe\?g\|png\)\)\".*:\1:p" $srcfile)
 256  done <<< "$(find $directory -type f -and \( -iname "*.php" -o -iname "*.html" \))"
 257fi
 258
 259# Calculate statistics
 260net=$((convert_count-larger_count))
 261existing=$((found_count-convert_count))
 262if [[ $net > 0 ]]; then
 263  savings_avg=$((savings_total/net))
 264else
 265  savings_avg=0
 266fi
 267
 268# Convert sizes to human-readable units if numfmt available
 269if command -v numfmt &> /dev/null; then
 270  savings_total=$(numfmt --to iec $savings_total)
 271  savings_avg=$(numfmt --to iec $savings_avg)
 272else
 273  savings_total="$savings_total B"
 274  savings_avg="$savings_avg B"
 275fi
 276
 277# Print statistics
 278if [[ $verbosity > 0 ]]; then
 279  printf "\x1b[32m$found_count found\n"
 280  if [[ $failed_count > 0 ]]; then
 281    printf "\x1b[31m$failed_count failed\n"
 282  fi
 283  printf "\x1b[32m$convert_count converted
 284$net smaller than original
 285$savings_total saved
 286$savings_avg saved average per file\n\x1b[0m"
 287fi
 288
 289# Set exit code
 290if [[ $failed_count > 0 ]]; then
 291  exit 1
 292else
 293  exit 0
 294fi