PNG Optimizer

Please note that the original script for this tutorial has changed. It did not originally check for symlinks and could end up in infinite recursion on Unix like systems (the fix is in the last code block and in the downloadable file)

Pages: Page 1 Page 2 Clean up

Alright lets take a minute to reflect on what we just did on the first page. First we set a simple goal and went for it, but more importantly we broke the problem down until we had a manageable step. Obviously this was a very tiny step and when actually writing this script I never specifically set out to accomplish this particular goal. I still did the same sort of things that we did here though. Pretty much everything in Engineering that I've ever seen is handled by the simple approach of taking something complex, breaking it down into separate steps, and then in turn doing the same thing to those steps until you come upon a step that is small enough for you to entirely wrap your brain around. Your mileage of course will vary depending on your personal brain capacity.

So Now I've mentioned a few fundamentals:

  1. Keep the goal in mind and design based on it
  2. Test things even if you are sure of them (keeping fundamental 1 in mind and not wasting time)
  3. Take something complex and break it down until you can fully understand a necessary action then implement that action

Moving on... it's time to take another chunk out of our problem. Now that we can optipng a single file lets expand to also pngout the same file. I'm sure you can figure out what to do but I'll throw down the two new lines to make sure everyone is on the same level.


print "pngouting "+"test.png"
 
os.spawnlp(os.P_WAIT,'pngout','pngout','-q','test.png')

Whoa man the progess... Ok now lets do something a little more substantial, like expand the script to apply itself to every png file in a directory. Since that's a little big we first need to get a list of all the png files in a directory. We'll google around a bit (or read parts of the python tutorial or possibly parts of thinking in python) and discover that again the os module (don't you just love it) provides us with a listdir function. You can check out it's doc string on your own (I would hope.) So lets throw down the code


files = os.listdir('./')
for file in files:
	print file

Here we can see that we have a nice little list of all the files in the current working directory (sweet) so we now need to narrow it down to just the files we want and to do that we'll use the ever amazing if statement in conjunction with a little python magic (python has some really sweet built in text manipulation that you can read about on your own)


files = os.listdir('./')
for file in files:
	if(file.split(".")[-1] == 'png'):
		print file

BAM now we're rolling eh... Now let's bolt what we previously made onto our new control structure (for good measure I'm going to throw it all into a function and add a couple of little simple details as well.)


import os,sys
 
def optimizeImages():
 
	files = os.listdir('./')
	for file in files:
		if(file.split(".")[-1] == 'png'):
			print "optipnging " + file
			os.spawnlp(os.P_WAIT, 'optipng','optipng','-o7','-q',file)
			print "pngouting "+file
			os.spawnlp(os.P_WAIT,'pngout','pngout','-q',file)
 
optimizeImages()
sys.exit(0)

Not too complicated eh. Now we actually have a script that is relatively powerful as we no longer have to run optipng -o7 * and wait for it to finish and then run pngout on all the files (pngout * doesn't work as pngout can be given both an input and an output filename.) So now if you want to optimize all of the png files for your web site you just go to your png directory and run this script right? Well what if you didn't conveniently place all of your png files in one directory? wouldn't it be nice if we could just go to a base directory, run this script, and have all the png files in that directory or any of its children optimized? of course it would and that's where we're going next. So can you figure out the steps? First we'll want to recognize all the directories that are in the current directory. This can be accomplished with a little os.path.isdir action. Then we want to optimize all the Images in that directory. Luckily I've already thrown all the single directory code into it's own function (almost like I knew we were gonna do this eh.) so now we want to modify the function so that it takes a directory as an argument and does it's magic on that directory. Here it is with a little make sure code in as well (normalizing the path and what not.)


def optimizeImages(directory):
	path = os.path.normpath(directory)
	if not os.path.isdir(path):
		raise Error, "Directory %s not found" % path
	os.chdir(path)
	files = os.listdir(path)
	for file in files:
		if(file.split(".")[-1] == 'png'):
			print "optipnging "+ path + '/'+ file
			os.spawnlp(os.P_WAIT, 'optipng','optipng','-o7','-q',file)
			print "pngouting" + path + '/'+file
			os.spawnlp(os.P_WAIT,'pngout','pngout','-q',file)
 
optimizeImages(os.getcwd())
sys.exit(0)

Now we have a more robust system that can be reused more easily, but we don't have any more functionality than we used to. Notice though where I check to see if the value passed to the optimizeImages function is a valid directory. We can make use of this to recursively do other directories by duplicating that check in the for loop (this also takes care of a bug that you may have noticed ie what if you have a directory that ends with .png?) Notice that I change the working directory back after I've done a subdirectory. I'm also going to throw down some try except magic to print out a traceback should anything unforeseen occur, a doc string for good measure and finally the top line to indicate to bash that I use python to run this type of script. You can copy and paste this code into a file set it as executable and run it from a bash prompt (assuming you have python installed properly of course.)


#!/usr/bin/env python
 
import os,traceback,sys
 
 
 
def optimizeImages(directory):
	"run optipng pngout on all png files located in a directory recursively"
	path = os.path.normpath(directory)
	if not os.path.isdir(path):
		raise Error, "Directory %s not found" % path
	os.chdir(path)
	files = os.listdir(path)
	for file in files:
		if(os.path.isdir(path+"/"+file) and not os.path.islink(path+"/"+file)):
			try:
				optimizeImages(path+"/"+file)
				os.chdir(path)
			except:
				traceback.print_exc()
		else:
			try:
				if(file.split(".")[-1] == 'png'):
					print "optipnging "+ path + '/'+ file
					os.spawnlp(os.P_WAIT,'optipng','optipng','-o7','-q',file)
					print "pngouting" + path + '/'+file
					os.spawnlp(os.P_WAIT,'pngout','pngout','-q',file)
			except:
				traceback.print_exc()
				
				
		
			
optimizeImages(os.getcwd())
sys.exit(0)

Now we have a pretty sweet script. There has been an amedment to this script since I first wrote this tutorial as it now verifies that a directory is not a symbolic link. You may or may not want to follow symlinks (if you are using a legacy OS like windows you don't have to worry about them at all..) but if you do follow them and they recurse your script will never end (This was built as a throwaway script and when I ran it I didn't recurse any symlinks but noticed this later.) Just run this script with the working directory set to the root of your webpage/application code/file system root and it will run through and optimize all of your png files for size. Keep in mind that this script will of course take quite a while, pngout in particular takes a long time on even reasonably small png files.

Back to the First Page

Finally of course here is the code.

Please go ahead and take a look at the next installment of this tutorial where I'm going to finally parallelize things (make them work on multiple processors at the same time. At the time of this writing it isn't totally finished but the first installment is here.)