PHP script to blast through a directory and shrink all of the images

This scenario needs a bit of setup, because without knowing the context, it would be logical to say, “Wait, you really should do this a different way.” Trust me. I tried all of the other ways.

I have a client who manages a grant program, and the grant recipients go to my client’s WordPress website to submit receipts and photos via Gravity Forms, in order to get their reimbursements.

The thing is, when you have hundreds of people who each need to submit multiple (sometimes many, sometimes “how the hell could you possibly need to submit that many???”) photos that they’ve taken on their phones, you’re going to end up with thousands of unnecessarily large JPEGs filling up your server.

I didn’t know, until the disk was already near capacity, that the client was even doing this, so I just needed to figure out a way to deal with in excess of 100 GB of disk usage. The client and their partner agency need to be able to continue accessing these files through the Gravity Forms admin, so I can’t move them somewhere else or even change the filenames — Gravity Forms’ gatekeeping download script won’t be able to find them.

There are some Gravity Forms add-ons for processing file uploads, but they generally need to be put in place before people start uploading files. So I resorted to a brute-force, command line PHP script to get the job done.

Fortunately the GD library is great for manipulating images. Its PHP functions are perfect for opening JPEGs and PNGs, creating new ones, scaling them down, cranking up the compression, etc. These images don’t have to be hi-res and beautiful; they just need to be legible. So I can really go to town on them, reducing the file size of many of them by 90% or more.

Here’s my script, with some added comments. It’s a bit quick-and-dirty. The shrink.php script file has to be in the same directory as the images, and it doesn’t work recursively. This restriction was mainly just to get something cobbled together, but I actually see it as a benefit because if something goes wrong, you’re limiting your losses to a single directory.

Initially I had a minor error that would cause the script to continue to cut the dimensions of images in half if you ran it multiple times, which I only realized after I accidentally ran it twice on the same directory — oops. (I was thankful at that moment that the single-directory restriction exists!)

Anyway… here’s what the script does. It blasts through all of the files in the same directory as the script itself, looking only for PNG and JPEG images.

For each image, it creates a new version at the same aspect ratio, but with a width of 1600, 800, or 400 pixels — scaling down to the nearest size from whatever the original size was. (It doesn’t do anything with images under 400 pixels wide.)

Then it re-saves them with more extreme compression applied. 70% quality for JPEG, and… well… as much compression as PNG allows. (I don’t really know much about PNG compression.)

If it finds that it didn’t actually reduce the file size, it restores the original. Otherwise, the original is gone, and your disk space is reclaimed.

It provides some verbose output telling you how much it reduced each file, and then gives an overall total reduction in MB when it’s complete.

This script could be polished up more, but even as it is I think it’s a lot better than the confusing and error-riddled suggestion I found on StackOverflow (on which it’s very loosely based, combined with a healthy dose of consulting the official PHP documentation). Enjoy!

// Get the list of files from the current directory
$images = glob('*');

// We'll tally up our savings
$total = 0;

// Loop through all of the files and run the function on them
foreach ($images as $file) {
  $formats = array('png', 'jpg', 'jpeg');
  $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
  
  // We only want to process PNG or JPEG images
  if (in_array($ext, $formats)) {
    $total = $total + resize_image($file);
  }
}

// All done; tell us how we did
echo "=====================\n" .
     "SHRINK COMPLETE\n" .
     "TOTAL REDUCTION: " . round($total / 1024, 2) . " MB" .
     "\n\n";


// The resizing function (obviously)
function resize_image($file) {
  $formats = array('png', 'jpg', 'jpeg');
  $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
  
  // Yes this is a redundant format check, just to be safe
  if (in_array($ext, $formats)) {
  
    // Get the old file size for later reference
    $old_size = round(filesize($file) / 1024, 2);
    
    // Set our maximum allowed width
    $max_w = 1600;
    
    // Get the dimensions and aspect ratio of the original file
    list($old_w, $old_h) = getimagesize($file);
    $ratio = $old_h / $old_w;
    
    // Set the new dimensions
    // Shrink to $max_w, 1/2 of $max_w or 1/4 of $max_w, depending on original size
    // >= is important so we don't keep cutting the dimensions in half if we run it again!
    $new_w = ($old_w >= $max_w) ? $max_w : (($old_w >= $max_w / 2) ? $max_w / 2 : $max_w / 4);
    $new_h = $new_w * $ratio;
    
    // Create our new image canvas
    $new_img = imagecreatetruecolor($new_w, $new_h);
    
    // Get the original image, minding the source format
    $old_img = ($ext == 'png') ? imagecreatefrompng($file) : imagecreatefromjpeg($file);
    
    // Scale down the original image and copy it into the new image
    imagecopyresampled($new_img, $old_img, 0, 0, 0, 0, $new_w, $new_h, $old_w, $old_h);
    
    // Keep a backup of the old file, for the moment
    rename($file, $file . '_BAK');
    
    // Save the newly reduced PNG...
    if ($ext == 'png') {
      imagepng($new_img, $file, 9, PNG_ALL_FILTERS);
    }
    // ...or the newly reduced JPEG
    else {
      imagejpeg($new_img, $file, 70);
    }
    
    // Did it work?
    if (file_exists($file)) {
    
      // Get the size of the new file, and the difference
      $new_size = round(filesize($file) / 1024, 2);
      $diff = $old_size - $new_size;
      
      // Hold up -- the old one is smaller! We'll restore it
      if ($diff < 0) {
        echo "Unable to reduce size of " . $file . "; original file restored.\n";
        unlink($file);
        rename($file . '_BAK', $file);
        
        // We're returning the KB savings, which is 0 in this case
        return 0;
      }
      
      // We reduced the file successfully, let's report success and delete the backup
      else {
        $pct = round(($diff / $old_size) * 100, 2);
        unlink($file . '_BAK');
        echo "Shrunk " . $file . " from " . $old_size . " KB to " . $new_size . " KB (" . $pct . "% reduction)\n";
      }
    }
    
    // It didn't work; report the error and restore the backup
    else {
      echo "Error processing " . $file . "; original file restored.\n";
      rename($file . '_BAK', $file);
      return 0;
    }
    
    // Pass back the KB savings so we can calculate a grand total
    return $diff;
  }
  
  // We didn't do anything so return 0 to keep the running total going
  return 0;
}

P.S. As always, all code samples are provided as-is with no warranty. Don't blame me if you use this and it blows something up!

P.P.S. I finally bothered to install prism.js on here for this. Now I need to back through and edit all of my older posts with code samples in them. Ugh.