Writing custom Asset Pipeline processors to compress images

Published on 27 Oct 2013
The Asset Pipeline in Rails minimizes Javascript and CSS, but images are mostly left out in the cold. There are a bunch of lossless and lossy algorithms we can use to optimize and compress images as much as possible, and I’ll show you how to integrate them in the Asset Pipeline.

For lossless compression we can use the sprockets-image_compressor gem, which optimizes all the PNG and JPG files without quality loss in the asset pipeline. The results are OK, but we can do better. There’s a lossy compression app for PNG files named pngquant which basically converts 32bit colors into 8bit colors in a way that’s hardly noticeable to the human eye. The result is often a 50% decrease in filesize or more.

First make sure to install pngquant and that you have at least version 2. It’s best to compile from sources because some of the published binaries are older versions which don’t have the same interface. There’s a gem to use pngquant, but it only works with files, and I don’t feel a gem is necessary here since we’ll only be using one command:

# Important to tell Sprockets this is a binary type, else you'll get UTF-8 byte sequence errors
Rails.application.assets.register_mime_type 'image/png', '.png'
 
Rails.application.assets.register_postprocessor 'image/png', :png_compressor do |context, data|
  IO.popen("pngquant -", "rb+") do |process|
    process.write(data)
    process.close_write
    process.read
  end
end

We’re passing the contents of the PNG file, contained in data, to an instance of pngquant using pipes and reading the resulting compressed PNG file back out, passing it as the result of the block.

Put that file in your config/initializers directory, and run rake assets:precompile to try it out. You should notice that the generated PNG files are all much smaller than their originals. Yay! Give yourself a high-five, ‘cause you just saved your visitors a lot of bandwidth.

However, there’s a tiny problem: sometimes the result is actually a bigger file, and sometimes the extra compression is visually observable. For these reasons, we’d like to conditionally run pngquant< only on certain files, but not on others. We’ll also put everything in a class to show you another way of how to do things — a cleaner, more testable way. Our post-processor class should subclass ::Tilt::Template, and implement the prepare and evaluate method. We can ignore the prepare-method, and focus on the evaluate method. Check out the Tilt gem for more info.

class PngQuantProcessor < ::Tilt::Template
  def prepare
    # noop
  end
 
  def evaluate(scope, locals, &block)
    IO.popen("pngquant -", "rb+") do |process|
      process.write(data)
      process.close_write
      process.read
    end
  end
end

Inside evaluate the ‘data’ method will contain the contents of the PNG file currently being processed. It’s mostly the same as our first code. Next we have to register our little processor with the asset pipeline. Like before, put these lines in initializers/sprockets.rb:

Rails.application.assets.register_mime_type 'image/png', '.quant'
Rails.application.assets.register_engine '.quant', PngQuantProcessor

Notice the subtle changes - we’re registering an engine for all files with extension .quant instead of a post-processor for image/png. This works much like any other template engine, like .coffee, and the asset pipeline will remove the extra extension before serving it up to the client.

And that’s it! Any PNG file you want to be quantized, rename its .png extension to .png.quant, and it’ll automatically be processed.

Happy coding!

David Verhasselt

I’m a consultant & entrepreneur. I build webapps and optimize Minimal Viable Products for clients all over the world. If you'd like to chat, hit me up on david@crowdway.com.

Like this? Sign up to get regular updates
David
Verhasselt