Dynamic Graphics with Rails 1.2 21 comments

posted Monday, December 18, 2006 by topfunky

The upcoming release of Rails 1.2 has some nice features for creating dynamic graphics in your application.

Here we have a simple shopping cart icon (purchased and modified from the Iconfactory).

I want to show the number of items in the cart. I could manually create a series of graphics with each number, but that seems inelegant. Anytime the icon needed tweaking, I would have to regenerate all the icons.

Rails 1.2 has the ability to send different types of content from the same action. Basically, I just want a graphical representation of the shopping cart. I’ll use the Cart#show action to render a graphic if it is accessed with a “png” extension. Otherwise, it will show the shopping cart items, total price, and checkout button in HTML.

Add a Content-Type

In config/environment.rb:

# For the drawing
require "RMagick" 

Mime::Type.register "image/png", :png

The Action

Purchase a TTF font and copy it to your Rails app so it can be deployed to the server. I put it in artwork/fonts.

class CartsController < ApplicationController

  def show
    @order = Order.find(params[:id])
    respond_to do |format|
      format.html do
        # Render the show.rhtml template
      end

      format.png do
        # Show cart icon with number of items in it
        icon = Magick::Image.read("#{RAILS_ROOT}/public/images/cart.png").first

        drawable = Magick::Draw.new
        drawable.pointsize = 18.0
        drawable.font = ("#{RAILS_ROOT}/artwork/fonts/VeraMono.ttf")
        drawable.fill = 'black'
        drawable.gravity = Magick::CenterGravity

        # Tweak the font to draw slightly up and left from the center
        drawable.annotate(icon, 0, 0, -3, -6, @order.quantity.to_s)

        send_data icon.to_blob, :filename => "#{@order.id}.png", 
                                :disposition => 'inline', 
                                :type => "image/png" 
      end

    end
  end

Reference the dynamic icon

Add some logic to your view (or a helper) to draw the icon with the number if the cart has items in it. You can use a regular image tag, but reference the controller instead:

# Generates an image tag to "/carts/1.png" 
image_tag formatted_cart_path(@order, :png)

The result

Caveat

If you can, use caching to speed up the delivery of images that have already been rendered. Rails doesn’t automatically apply the correct extension for non-standard content-types CORRECTION: This has been fixed and now works smoothly with rev 5736 (and maybe earlier revisions).

Resources

21 comments

Leave a response

  • I like this, but Rails caching won’t work with this construct – how does anything know that the number of items in the @order object is important to the cache response?

    Luckily, there is a solution!

    My action_cache plugin allows you to cache these images, and serve them up correctly, using the custom cache key method to make the order ID, response type, and order quantity part of the fragment key used by the action cache code.

    Ben’s plugin site has the lowdown: http://agilewebdevelopment.com/plugins/action_cache

    My blog has info too, but is less structured! I usually answer email too.

  • Gravatar icon Daniel Haran

    I’m assuming formatted_cart_path(@order, :png) is auto-generated, is that right?

  • Gravatar icon topfunky

    @Tom: Nice work…I’ll look at your plugin. Still, what’s wrong with explicitly expiring the cache when a new item is added to the cart? That’s how I’ve done most caching in Rails…as a cache/expire pair.

    @Daniel: Yes, formatted_cart_path is auto-generated by Rails 1.2 when you do map.resources :carts. See also this cheatsheet.

  • Gravatar icon Luke Redpath

    Wow, this is really cool. I never thought about graphical representations of a resource.

    One small change i’d make is to extract all of that image creation code into some kind of factory or an object of its own (a simple facade to the rmagick stuff).

  • Gravatar icon Matthew King

    Great tip.

    Alex Wayne’s FlexImage plugin works about the same way, abstracting out most of the ImageMagick details.

  • Gravatar icon topfunky

    @Luke: Indeed. I put it inline for the purposes of the tutorial, but often make a plugin or a module in “lib” to do the actual rendering of the graphics.

  • Side question: What is the benefit of rendering the count of items into the image of the cart? Wouldn’t we almost always be better off leaving the count as text and using, say, CSS to superimpose the text and image? (It seems that by rendering the text into the art we would lose on both accessibility and intermediary-caching grounds.)

    If I’m missing something, please let me know.

    Cheers. —Tom

  • Gravatar icon topfunky

    @Tom: Using graphics doesn’t mean you have to sacrifice accessibility. You can still do alt text, use CSS hacks to replace the page text with images, etc.

    Having the ability to generate dynamic graphics is just another tool. It makes it much easier to use alternate typefaces or do pixel-perfect placement of elements.

    CSS can add text to a page, but what about taking it further to include shapes and lines? These make it possible to describe a lot of information in a small amount of space.

  • With the default Rails action cache, you’d get an image cached per user. With the right configuration, you could make it cache one image per number of items by dropping the params[:id] from the cache key. This is what I was thinking as I read the post.

    You would also have to keep the per-user cache entry for the HTML format whilst doing the above for the PNG format.

    This is an advanced technique!

  • Gravatar icon topfunky

    @Tom: Good point. I was thinking of using page caching and doing something similar, but it seems to go against the philosophy of REST (i.e. having different keys for the same resource). So action caching and a relevant key might be the better solution.

  • topfunky, I agree with you i definitely want to try this png creation method out !

  • Gravatar icon Martin

    Tom Moertel, of course this example might bebetter to do with plain text, but its an example!! imagine the use of this in your own real situations, for example captcha! this article is about learning, not just doing exact as it say. and im very thankful to the author! <3

  • RE: dropping params[:id]

    You could map this as a singleton resource, since, most likely, each user won’t have multiple carts.

  • Gravatar icon Mark Thomas

    Another idea:

    Make /carts/1.png return a static file, if it exists. If it doesn’t, create it using the above technique. This way you only create it dynamically once, ever.

  • Gravatar icon Joost

    Yet another idea:

    Make /carts/1.png return a db blob, if it exists. If it doesn’t create it using the above technique and save the image as blob in the db.

    :)

  • Gravatar icon Greg

    Would someone be able to clarify the “Image Overlay with CSS” approach regarding the displaying of dynamic text within an image?

    How is this accomplished? Is it based on HTML 4.0 “Button”? Is the idea to slice up the image (say a button) that you want so you can then use DIV’s to have the edges of the cutup image grow/shrink depending on the size of the dynamic text?

    Background – I’m keen to include web 2.0 styles buttons I have from photoshop however don’t really want to lose the flexibility of text (e.g. changing the text, multiple languages supports – don’t need to have multiple images)

    Regards Greg

  • @Mark: This technique is exactly how page caching works. However, it won’t work in this case, as the URL does not contain any information about the number to display in the image. The ID in the url is the order ID, and the image should contain the current item count from the order. This can be worked around with expiring the page cache item on order update, but this adds a whole bunch of extra complexity (get page caching working with PNG files, and expire the cached items correctly)

  • Hiya, seemed a good place to announce Saucy, my new plugin for rails that renders images unobtrusively.

    Check it out here

    Like to hear any feedback

  • Gravatar icon Anton

    It seems very complciated to me.

    I’ve done this in the past by using a graphic as a background for a table (or other cell) . You just write the number if not zero/nil into the cell.

    K.I.S.S.

  • wow, does this mean that anyone has created a SIIR type plugin for rails?

  • While the overlay with CSS stuff is an option in many cases, in some cases, you just need this stuff. Case in point – images for pins on a Google Map. I can definitely use this technique to avoid creating tonnes of new, custom, images for use on our Google Maps.

Your Comment

Nuby on Rails

Geoffrey Grosenbach / Ruby / Code / Graphics / Design / Rails / Merb / Javascript / CSS

Ads by The Lounge

Manufactured with

Subscribe

Subscribe (RSS)