Template
<% content_for :header, 'Rails ImageBundle demo' %>
<p>
ImageBundle, a Rails plugin to automatically bundle local images into CSS sprites.
</p>
<p style="padding: 1em 0 2em;">
<%= link_to 'View source', :action => 'src' %> |
<a href="/rdoc/image_bundle">ImgBundle documentation</a> |
<a href="http://thecodemill.biz/repository/plugins/image_bundle/">Get Rails ImgBundle plugin</a>
</p>
<% image_bundle(@class, @type) do %>
<table cellpadding="0" width="100%" cellspacing="4" border="0">
<tr>
<td valign="top" nowrap="nowrap">
<strong>Answers International:</strong>
</td>
<td>
<div id="intl_flag_container">
<img alt="flag" class="bundle flag-ar" title="View Argentina's Answers!" src="/images/arflag.gif"/>
<nobr>Argentina</nobr>
<img alt="flag" class="bundle flag-au" title="View Australia's Answers!" src='/images/auflag.gif'/>
<nobr>Australia</nobr>
<img alt="flag" class="bundle flag-br" title="View Brazil's Answers!" src="/images/brflag.gif"/>
<nobr>Brazil</nobr>
<img alt="flag" class="bundle flag-ca" title="View Canada's Answers!" src="/images/caflag.gif"/>
<nobr>Canada</nobr>
<img alt="flag" class="bundle flag-cn" title="View China's Answers!" src="/images/cnflag.gif"/>
<nobr>China</nobr>
<img alt="flag" class="bundle flag-fr" title="View France's Answers!" src="/images/frflag.gif"/>
<nobr>France</nobr>
<img alt="flag" class="bundle flag-de" title="View Germany's Answers!" src="/images/deflag.gif"/>
<nobr>Germany</nobr>
<img alt="flag" class="bundle flag-hk" title="View Hong Kong's Answers!" src="/images/hkflag.gif"/>
<nobr>Hong Kong</nobr>
<img alt="flag" class="bundle flag-in" title="View India's Answers!" src="/images/inflag.gif"/>
<nobr>India</nobr>
<img alt="flag" class="bundle flag-id" title="View Indonesia's Answers!" src="/images/id_flag.gif"/>
<nobr>Indonesia</nobr>
<img alt="flag" class="bundle flag-it" title="View Italy's Answers!" src="/images/itflag.gif"/>
<nobr>Italy</nobr>
<img alt="flag" class="bundle flag-jp" title="View Japan's Answers!" src="/images/jpflag.gif"/>
<nobr>Japan</nobr>
<img alt="flag" class="bundle flag-my" title="View Malaysia's Answers!" src="/images/myflag.gif"/>
<nobr>Malaysia</nobr>
<img alt="flag" class="bundle flag-mx" title="View Mexico's Answers!" src="/images/mxflag.gif"/>
<nobr>Mexico</nobr>
<img alt="flag" class="bundle flag-nz" title="View New Zealand's Answers!" src="/images/nz_flag.png"/>
<nobr>New Zealand</nobr>
<img alt="flag" class="bundle flag-ph" title="View Philippines's Answers!" src="/images/phflag.gif"/>
<nobr>Philippines</nobr>
<img alt="flag" class="bundle flag-cf" title="View Quebec's Answers!" src="/images/quebec_flag.png"/>
<nobr>Quebec</nobr>
<img alt="flag" class="bundle flag-sg" title="View Singapore's Answers!" src="/images/sgflag.gif"/>
<nobr>Singapore</nobr>
<img alt="flag" class="bundle flag-kr" title="View South Korea's Answers!" src="/images/skrflag.gif"/>
<nobr>South Korea</nobr>
<img alt="flag" class="bundle flag-es" title="View Spain's Answers!" src="/images/esflag.gif"/>
<nobr>Spain</nobr>
<img alt="flag" class="bundle flag-tw" title="View Taiwan's Answers!" src="/images/twflag.gif"/>
<nobr>Taiwan</nobr>
<img alt="flag" class="bundle flag-th" title="View Thailand's Answers!" src="/images/th_flag.gif"/>
<nobr>Thailand</nobr>
<img alt="flag" class="bundle flag-uk" title="View United Kingdom's Answers!" src="/images/ukflag.gif"/>
<nobr>United Kingdom</nobr>
<img alt="flag" class="bundle flag-us" title="View United States's Answers!" src="/images/usflag.gif"/>
<nobr>United States</nobr>
<img alt="flag" class="bundle flag-vn" title="View Vietnam's Answers!" src="/images/vn_flag.gif"/>
<nobr>Vietnam</nobr>
<img alt="flag" class="bundle flag-us" title="View en Español's Answers!" src="/images/usflag.gif"/>
<nobr>en Español</nobr>
</td>
</tr>
</table>
<% end %>
<% image_bundle(@class, @type) do %>
<p>
Some static text with <strong>HTML</strong> <em>markup</em>.<br/>
Plus a dynamic date: <%= Time.now %><br/>
And an 16x16 <img alt="favicon" class="bundle" src="/favicon.ico" height="16" width="16"/><br/>
A 6x? <img alt="favicon" class="bundle" src="/favicon.ico" height="6"/><br/>
A ?x6 <img alt="favicon" class="bundle" src="/favicon.ico" width="6"/><br/>
An ?x? <img alt="favicon" class="bundle" src="/images/rails.png"/><br/>
A 6x16 <img alt="favicon" src="/favicon.ico" height="6" width="16"/> not of class bundle<br/>
My 160x16 multi line example <img alt="favicon" class="bundle" src="/favicon.ico"
height="160"
width="16"/><br/>
Single quote ?x? <img alt="favicon" class='bundle' src="/favicon.ico"/><br/>
Class 'some bundle' ?x? <img alt="favicon" class='some bundle' src="/favicon.ico"/><br/>
Src before class ?x160 <img alt="favicon" src="/favicon.ico" class='bundle' width="160"/><br/>
Src before class 'some bundle' ?x? <img alt="favicon" src="/favicon.ico" class='some bundle'/><br/>
Src = and id ?x? <img alt="favicon" src="/favicon.ico" id="bla" class = 'some bundle'/><br/>
Casper ?x? <img alt="favicon" class="bundle" src="/images/casper-1st-birthday.jpg"/><br/>
<img class="bundle" alt="rails.png" src="/images/rails.png" onmouseover="alert('Mouse Over');" />
</p>
<p>
Some additional text.
</p>
<% end %>
Layout
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.2.2/build/reset-fonts-grids/reset-fonts-grids.css"/>
<link rel="stylesheet" type="text/css" href="/l10n/l10n.css"></link>
<link rel="shortcut icon" href="/favicon.ico"/>
<%= yield :head %>
<title><%= yield(:title) || yield(:header) %></title>
</head>
<body style="margin: 0pt; padding: 0pt;">
<div id="doc" class="yui-t1" style="width: 100%; padding: 0;">
<div id="bd">
<div class="author">
<div style="background-color: #333333; height: 48px; color: #FFFFFF;">
<img src="http://farm1.static.flickr.com/10/buddyicons/44124365893@N01.jpg" width="48" height="48" alt="Bart Teeuwisse portrait" style="float: left; padding-right: 10px; padding-bottom: 10px;"/>
<h2><a href="/=bartt/" style="color: #FFFFFF">Bart Teeuwisse</a></h2>
</div>
</div>
<h1><%= yield :header %></h1>
<div style="padding: 0 10px;">
<%= yield %>
</div>
</div>
</div>
</body>
</html>
Image_bundle Source
# ImageBundleHelper adds view helper +image_bundle+. A helper which
# bundles individual <strong>local images</strong> into a single CSS
# sprite thereby reducing the number of HTTP requests needed to render
# the page.
#
# {Yahoo's Exceptional Performance
# team}[http://developer.yahoo.com/performance/] found that the number
# of HTTP requests has the biggest impact on page rendering speed. You
# can inspect your site's performance with the excellent Firefox
# add-on {YSlow}[http://developer.yahoo.com/yslow/].
module ImageBundleHelper
require 'rubygems'
require 'RMagick'
require 'digest/md5'
SPRITE_BASE_DIR = ENV['IMAGE_BUNDLE_SPRITE_BASE_DIR'] || 'sprites' if !defined?(SPRITE_BASE_DIR)
class Image #:nodoc:
attr_accessor :path, :file, :height, :width, :x_pos
end
# === +image_bundle+ takes 4 optional parameters:
#
# <tt>css_class</tt>::
# When provided <tt>css_class</tt> restricts the bundling of
# images to <tt><img></tt> tags of class <tt>css_class</tt>.
#
# <tt>sprite_type</tt>::
# By default +image_bundle+ generates a PNG master image. Set
# sprite_type to the image type you'd like to use instead. E.g. GIF
# or JPEG. Any type supported by
# {ImageMagick}[http://www.imagemagick.org/] can be generated. All
# images being bundled will be converted to <tt>sprite_type</tt>.
#
# <tt>content_target</tt>::
# By default +image_bundle+ produces content for <tt>:head</tt>
# using <tt>content_for</tt>. Provide a different target if you
# prefer a different name. Can be anything that can be converted
# to a symbol.
#
# <tt>replacement_image</tt>::
# By default +image_bundle+ replaces the +src+ of bundled images
# with <tt>/images/clear.gif</tt>. A 1x1 transparant image is
# included with the +image_bundle+ plugin. You'll find it in the
# +images+ directory of the plugin. Provide
# <tt>replacement_image</tt> if you prefer to use an image of
# different name.
#
# === +image_bundle+ does 4 things:
# 1. It creates a master image of all bundled images, if it doesn't already exist.
# 1. It rewrites the <tt><img></tt> tags of all images included
# in the bundle to use <tt>replacement_image</tt> instead.
# 1. Each included <tt><img></tt> gets a new class <em>added</em> to the
# image's +class+ attribute. The new class name is unique to the
# image's size and content.
# 1. +image_bundle+ creates matching CSS rules to display the portion
# of the master image equivalent to the <tt><img></tt> tags'
# original image. The CSS rules are collected in
# <tt>content_target</tt> and can be used later with
# <tt>yield</tt>.
#
# === +image_bundle+ uses 1 environment variable:
#
# By default +image_bundle+ creates sprites in a directory called
# +sprites+. +image_bundle+ doesn't use +images+ in order to
# eliminate the potential of overwriting your images. Create
# +sprites+ in your +public+ directory before using
# +image_bundle+.
#
# If you prefer to use a different directory set
# <tt>ENV['IMAGE_BUNDLE_SPRITE_BASE_DIR']</tt> in your Rails
# environment. IMAGE_BUNDLE_SPRITE_BASE_DIR is relative to your
# =public= directory.
#
# === Example usages
#
# Instruct your controller to user +image_bundle+:
#
# helper: image_bundle
#
# Bundle all images included within +image_bundle+'s block.
#
# <% image_bundle do %>
# <p>+image_bundle+ can wrap any kind of content: HTML, JS, etc.</p>
# <img src="/images/auflag.gif"/></br>
# <p>Bundled images don't need to be adjacent to one another either.</p>
# <img src="/images/nlflag.gif"/></br>
# <img src="/images/frflag.gif"/></br>
# <% end %>
#
# Bundle only images of class <tt>:bundle</tt>. +image_bundle+ scales resized
# images accordingly. It calculates the 2nd dimension if only one
# dimension is given. Bundled images don't have to be of the same size
# either.
#
# <% image_bundle(:bundle) do %>
# <p>
# Some static text with <strong>HTML</strong> <em>markup</em>.<br/>
# Plus a dynamic date: <%= Time.now %><br/>
# And an 16x16 <img alt="favicon" class="bundle" src="/favicon.ico" height="16" width="16"/><br/>
# A 6x? <img alt="favicon" class="bundle" src="/favicon.ico" height="6"/><br/>
# A ?x6 <img alt="favicon" class="bundle" src="/favicon.ico" width="6"/><br/>
# An ?x? <img alt="favicon" class="bundle" src="/images/rails.png"/><br/>
# A 6x16 <img alt="favicon" src="/favicon.ico" height="6" width="16"/> not of class bundle<br/>
# My 160x16 multi line example <img alt="favicon" class="bundle" src="/favicon.ico"
# height="160"
# width="16"/><br/>
# Single quote ?x? <img alt="favicon" class='bundle' src="/favicon.ico"/><br/>
# Class 'some bundle' ?x? <img alt="favicon" class='some bundle' src="/favicon.ico"/><br/>
# Src before class ?x160 <img alt="favicon" src="/favicon.ico" class='bundle' width="160"/><br/>
# Src before class 'some bundle' ?x? <img alt="favicon" src="/favicon.ico" class='some bundle'/><br/>
# Src = and id ?x? <img alt="favicon" src="/favicon.ico" id="bla" class = 'some bundle'/><br/>
# Casper ?x? <img alt="favicon" class="bundle" src="/images/casper-1st-birthday.jpg"/><br/>
# </p>
# <p>
# Some additional text.
# </p>
# <% end %>
#
# <%= @sprite_css %>
def image_bundle(css_class = nil, sprite_type = :png, content_target = :head, replacement_image = '/images/clear.gif', *args, &block)
# Bind buffer to the ERB output buffer of the templates.
buffer = eval("_erbout", block.binding)
# Mark the current position in the buffer
pos = buffer.length
# Render the block contained within the image_bundle tag. The
# rendered output is appended to buffer.
block.call(*args)
# Extract the output produced by the block.
block_output = buffer[pos..-1]
buffer[pos..-1] = ''
# Replace the img tags in the output with links to clear.gif and
# styling to use the master image created from the individual
# images.
images = Hash.new
re = (css_class == nil) ? /(<img\s*)([^>]*?)(\s*\/?>)/im : /(<img\s*)([^>]*?class\s*=\s*["']?[^"']*?#{css_class}[^"']*?["']?[^>]*?)(\s*\/?>)/im
block_rewrite = ''
while pos = (block_output =~ re) do
# Store match data for later reference.
img_match = $~.to_s
img_tag = $1
attributes = $2
img_closing_tag = $3
# Remember where to continue searching from in the next
# iteration.
continue_pos = pos+img_match.length
# Write out the content before the start of the tag
block_rewrite << block_output[0..pos-1]
if img_match =~ /src\=["']?https?:\/\//i then
block_rewrite << img_match
else
# Write out the opening portion of the image tag (<img).
block_rewrite << img_tag
# Process all attributes of the img tag.
height_given = width_given = nil
classes = ''
ping = ::ImageBundleHelper::Image.new
while pos = (attributes =~ /([^ =]+?)\s*=\s*(("?([^"=]*?)")|('?([^'=]*?)'))/im) do
attribute = $1
value = $4 || $6
attr_continue_pos = pos+$~.to_s.length
case attribute
when 'src'
ping.path = value
# Read only the image's meta data not its image content.
ping.file = "#{RAILS_ROOT}/public#{ping.path}"
image = ::Magick::Image.ping(ping.file)[0]
ping.height = image.rows
ping.width = image.columns
block_rewrite << "#{attribute}=\"#{replacement_image}\" "
when 'height'
height_given = value.to_i
when 'width'
width_given = value.to_i
when 'class'
# Prepend a space for later concatenation with bndl class.
classes = " #{value}"
else
# Pass through all other attributes
block_rewrite << "#{attribute}=\"#{value}\" "
end
attributes = attributes[attr_continue_pos..-1]
end
# Calculate the height and width of the image based on the
# specified height/width and the source file's height and
# width. Scaling needs to happen when the sprite is created
if height_given == nil then
if width_given != nil then
ping.height = (ping.height * (width_given.to_f / ping.width.to_f)).to_i
ping.width = width_given
end
else
if width_given == nil then
ping.width = (ping.width * (height_given.to_f / ping.height.to_f)).to_i
ping.height = height_given
else
ping.width = width_given
ping.height = height_given
end
end
# Only add unique images and height/width combinations to the hash.
key = "bndl#{::Digest::MD5.hexdigest("#{ping.path}:#{ping.height}:#{ping.width}").hash}"
images[key] ||= ping
block_rewrite << "class =\"#{key}#{classes}\" "
block_rewrite << "height=\"#{ping.height}\" "
block_rewrite << "width=\"#{ping.width}\" "
block_rewrite << img_closing_tag
end
block_output = block_output[continue_pos..-1]
end
# Create a sprite when there are source files and if it doesn't
# already exists.
if images.length > 0 then
sprite_path = '/' + SPRITE_BASE_DIR + '/' + ::Digest::MD5.hexdigest(images.keys.inject do |concat_names, key| concat_names + '|' + key end) + ".#{sprite_type}"
sprite_file = "#{RAILS_ROOT}/public/#{sprite_path}"
if !File.exists?(sprite_file) then
# Stack scaled source images left to right.
sprite = images.values.inject(::Magick::ImageList.new) do |image_list, ping|
image_list << ::Magick::ImageList.new(ping.file)[0].scale(ping.width, ping.height)
end.append(false)
sprite.write(sprite_file)
end
# Construct style tag to be included in the header.
current_y = 0
bundle_styles = "\n<style type=\"text/css\">\n"
bundle_styles << images.keys.inject('') do |styles, key|
images[key].x_pos = current_y
current_y += images[key].width
styles + ".#{key} {\n background-image:url(#{sprite_path});\n background-position: -#{images[key].x_pos}px 0px;\n}\n"
end
bundle_styles << "</style>\n"
end
# Write the remaining block output that follows the last img tag.
block_rewrite << block_output
buffer << block_rewrite if block_rewrite
content_for content_target.to_sym, bundle_styles ||= ''
end
end