Easy way to reduce the size of png files

I’ve been doing some work with some hefty png files recently and was pointed to a page on my colleagues website where he explains how to compress pngs without losing any quality. I won’t go through the details of what he’s doing – you can go have a read up of his site for that, but I’ve seen reductions in size from about 400Kb to about 110Kb without any loss of image quality.

Make sure you backup your files before you do any of this – I won’t be held responsible for any issues you have if this messes up for you!

I’ve replicated the steps below but formatted them for use in bash and included a little python script I wrote to sort through and pick out the smallest files as I found that not all the steps reduced the file size of all the png files, depending on the original file.

for i in *.png; do pngquant -ext -convert1.png 256 $i; done
for i in *-convert1.png; do pngout -c3 -d8 -y -force $i ${i%%convert1.png}convert2.png; done
for i in *-convert2.png; do pngcrush -bit_depth 8 -brute -rem alla -reduce $i ${i%%convert2.png}convert3.png; done

pngcrush takes ages on my machine, so make sure you have enough time if you have lots of files to crunch.

After this you should have a list of files:

  • landscape.png <- this being your original file
  • landscape-convert1.png
  • landscape-convert2.png
  • landscape-convert3.png

Now all you need to do is pick out the smallest, which I’ve written the following Python script for. It’s not optimised, but it works. It’ll run through all your files and remove all but the smallest and rename it to the original name of the png:

#!/usr/bin/python
import os, re

class PngFile(object):
    def __init__(self, filename):
        self.name = filename
        self.path = path + filename
        self.size = os.path.getsize(self.path)

path="<INSERT PATH HERE>"  
dirList=os.listdir(path)
out = ''
convertString = '-convert'
pngString = '.png'
reobj = re.compile('([^.]+)\.png$')
reobj2 = re.compile('[^.]+convert.\.png$')
filename = ''
count = 0
sizesaved = 0
for fname in dirList:
   if reobj.match(fname) is not None:
       if reobj2.match(fname) is None:
	   files = [PngFile(fname)]
	   count+=1
           filename = reobj.match(fname).group(1)
           if(os.path.exists(path + filename + convertString + '1' + pngString)):
	       files.append(PngFile(filename + convertString + '1' + pngString))
           if(os.path.exists(path + filename + convertString + '2' + pngString)):
	       files.append(PngFile(filename + convertString + '2' + pngString))
           if(os.path.exists(path + filename + convertString + '3' + pngString)):       
	       files.append(PngFile(filename + convertString + '3' + pngString))
	   smallestFileSize = min(files, key=lambda pngFile: pngFile.size).size
	   biggestFileSize = max(files, key=lambda pngFile: pngFile.size).size
	   smallestFilePath = min(files, key=lambda pngFile: pngFile.size).path
	   saved = biggestFileSize - smallestFileSize
	   sizesaved += saved
	   print "{2} - smallest file found {0} - saving {1} - {3}".format(smallestFilePath, saved, filename, fname)
	   for file in files:
	       if file.path is not smallestFilePath:
		   print "remove {0}".format(file.name)
		   os.remove(file.path)
	   print "RENAME {0} -> {1}".format(smallestFilePath, path + fname)   
	   os.rename(smallestFilePath,path + fname)
print "files found={0} - saving {1}".format(count, sizesaved)