Pretty image
People often ask how we do what we do. This series explains…
This month's question: How do you generate the different cover images on pragprog.com?

We use book covers all over the Pragmatic online store: on the book pages, in the order summary, on your bookshelf listing, and so on. We display the images in five different sizes, from 40x48 thumbnails up to full-size 72dpi images. And, to make life even more interesting, we overlay those images with variable little icons to show that the book is in beta, that an eBook update is available, and so on.

Being pragmatic (read “lazy”) we didn’t want to manage all those combinations manually, so Mike Clark and John Long cooked up the software and CSS to make it all automatic.

It all starts on my desktop machine, where I use InDesign to create the covers that I’ll send to the printer. As a part of that process, I export full-size 72dpi and 300dpi JPEG versions of the front cover. Our production folks check these images out of our repository and upload them to our admin system.

Perhaps surprisingly for those used to modern Rails, we don’t use a library such as PaperClip to do this upload. Even if it had been around when we wrote the store, we do enough custom processing that I’m not sure it would have added much value. Instead we just use a raw file_field_tag to upload the full-size JPEG.

We save the original covers away—you can view them by clicking on the large cover image on the main page for each title. We then feed the cover through ImageMagick to create the scaled versions. We could have done this using library calls, but we were concerned about reported memory leaks, and so preferred to shell out:

 def create_all_sizes
  base_name = File.basename(path_to_original)
 
  geometries = ['40x48', '75x90', '120x144', '140x168', '190x228']
 
  geometries.each do |geo|
  FileUtils.mkdir_p(File.join(COVERS_DIRECTORY, geo))
  cmd = "convert -resize #{geo} #{path_to_original} " +
  "#{COVERS_DIRECTORY}/#{geo}/#{base_name}"
  system cmd
  end
 end

Next, we create the versions of the covers that have the eBook, beta, and the PDF-out-of-date stamps. There’s a version of each stamp for each different cover size (we tried scaling them down from a large master, but they looked horrible). Here’s the code that generates the two sizes of cover with the eBook stamp:

 def create_ebook_overlays
  # small_ebook
  create_cover_with_overlay(
  :overlay => File.join(IMAGES_DIRECTORY, 'ebook-28.png'),
  :file => path_to_small,
  :output_dir => File.join(COVERS_DIRECTORY, '40x48', 'ebooks'))
 
  # medium book
  create_cover_with_overlay(
  :overlay => File.join(IMAGES_DIRECTORY, 'ebook-46.png'),
  :file => path_to_medium,
  :output_dir => File.join(COVERS_DIRECTORY, '75x90', 'ebooks'))
 end

The code that does the overlaying uses some RMagick magic:

 def create_cover_with_overlay(options)
  FileUtils.mkdir_p(options[:output_dir])
  base_name = File.basename(options[:file])
  images = Magick::ImageList.new(options[:file], options[:overlay])
  x_offset = images[0].columns - images[1].columns
  y_offset = images[0].rows - images[1].rows
  images[1].page =
  Magick::Rectangle.new(images[0].columns, images[0].rows,
  x_offset, y_offset)
  images.
  flatten_images.
  write(File.join(options[:output_dir], base_name))
 end

A little processing up front, and we have 16 versions of a book cover all ready to serve in the store. A wee post-deploy hook symlinks the master covers directory into the application each time we update it.

Dave Thomas is one of the Pragmatic Programmers.