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.

Lewis Black on the Root of All Evil: Bloggers

“Blogging is like masturbating in front of a mirror and videotaping yourself so you can watch it later, while masturbating.”
–Lewis Black

Yeah, Lewis, that pretty much sums it up. Of course, as soon as I heard him say this, the first thing that came to mind was, “I’ve got to blog about this!”

A Grotesque Over-Indulgence in Flatulent Analog Synthesizer Wankery

When it comes to vintage (pre-digital) keyboards, there are a few legends: the Hammond organ, the Rhodes electric piano, the Mellotron, and of course, the Minimoog.

I don’t own any of those actual instruments, of course (although an old friend of mine has over the years possessed a large number of them, and used them on some recordings we did back in college), but I have been extremely impressed with the generic replications of these timeless (OK, hopelessly dated, but eternally retro-cool) trademark sounds that Apple has provided with GarageBand. (OK, there’s no Mellotron in GarageBand, but as I’ve learned and demonstrated, a dead-accurate add-on is available.)

Most of the recordings I’ve made since I started using GarageBand (in early 2007) have been brimming with these sounds, most notably the electric piano, and only slightly less notably the organ. Although I’ve been obsessing over the Mellotron lately, my favorite keyboard sound to employ for solo parts is the “Analog Mono” synth, which is fairly similar in tone and timbre to the venerable Minimoog.

I’m currently at work on a “top secret” collaborative music project. (OK, I don’t know for sure how “top secret” it is, but since I’m not in charge of it, I’m keeping mum until I’m told it’s OK to blab.) So far I haven’t nailed what I’m looking for for that project, but along the way I did produce this… interesting track. Recorded entirely today (July 29, 2008, which of course is yesterday now), it’s a fairly static, spacey/electronic jam, 7 1/2 minutes long, with some nice big reverb-y drums and of course an interminable, excessively excessive (yes, that’s possible, and if you doubt it, just give this track a listen) quasi-Minimoog solo. Probably not most people’s cup of tea, but I actually find it kind of cool to have on as background noise.

This track is unlikely to ever be released, at least in anything near its current form, so I provide it here as a curiosity. It was mainly an experiment: an effort to try out various “distressed” electronic sounds, to play around with recording a drum track straight through (instead of perfecting a few 4-measure loops), and of course, going totally batshit crazy with the synth solo, in a number of ways: experimenting with the octave and pitch transposition buttons on my 2-octave portable MIDI keyboard controller, trying out some adjustments to the synth tone, and finally (after about 15 years of improvising solos) dabbling in the realm of scale substitution. Music theory FTW! (I was going to say “Yay, music theory!” but I already used “yay” in a post within the past few hours and I just can’t bring myself to do it again. Plus, this was an opportunity to once again demonstrate my 1337 skillz.)

Now that I’ve proven why this blog is called “Blather,” here’s the freakin’ song.

[audio:http://blog.room34.com/wp-content/uploads/underdog/analogwankery.mp3]

÷0

Yes, it’s true, I’ve already begun another new CD, and this one even has vocals! (Hence the smiley-esque title.) The album is called ÷0 (Division by Zero) and I’ve posted one of the three so-far-complete tracks today, called “Saturday Night Pizza.” Check it out!