Module ImageBundleHelper
In: lib/image_bundle_helper.rb

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 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.

Methods

Constants

SPRITE_BASE_DIR = ENV['IMAGE_BUNDLE_SPRITE_BASE_DIR'] || 'sprites' if !defined?(SPRITE_BASE_DIR)

Public Instance methods

image_bundle takes 4 optional parameters:

css_class:When provided css_class restricts the bundling of images to <img> tags of class css_class.
sprite_type: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 can be generated. All images being bundled will be converted to sprite_type.
content_target:By default image_bundle produces content for :head using content_for. Provide a different target if you prefer a different name. Can be anything that can be converted to a symbol.
replacement_image:By default image_bundle replaces the src of bundled images with /images/clear.gif. A 1x1 transparant image is included with the image_bundle plugin. You‘ll find it in the images directory of the plugin. Provide replacement_image 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.
  2. It rewrites the <img> tags of all images included in the bundle to use replacement_image instead.
  3. Each included <img> gets a new class added to the image‘s class attribute. The new class name is unique to the image‘s size and content.
  4. image_bundle creates matching CSS rules to display the portion of the master image equivalent to the <img> tags’ original image. The CSS rules are collected in content_target and can be used later with yield.

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 ENV[‘IMAGE_BUNDLE_SPRITE_BASE_DIR’] 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 :bundle. 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 %>

[Source]

     # File lib/image_bundle_helper.rb, line 123
123:   def image_bundle(css_class = nil, sprite_type = :png, content_target = :head, replacement_image = '/images/clear.gif', *args, &block)
124:     # Bind buffer to the ERB output buffer of the templates.
125:     buffer = eval("_erbout", block.binding)
126: 
127:     # Mark the current position in the buffer
128:     pos = buffer.length
129: 
130:     # Render the block contained within the image_bundle tag. The
131:     # rendered output is appended to buffer.
132:     block.call(*args)
133: 
134:     # Extract the output produced by the block.
135:     block_output = buffer[pos..-1]
136:     buffer[pos..-1] = ''
137: 
138:     # Replace the img tags in the output with links to clear.gif and
139:     # styling to use the master image created from the individual
140:     # images.
141:     images = Hash.new
142:     re = (css_class == nil) ? /(<img\s*)([^>]*?)(\s*\/?>)/im : /(<img\s*)([^>]*?class\s*=\s*["']?[^"']*?#{css_class}[^"']*?["']?[^>]*?)(\s*\/?>)/im
143:     block_rewrite = ''
144:     while pos = (block_output =~ re) do
145: 
146:       # Store match data for later reference.
147:       img_match = $~.to_s
148:       img_tag = $1
149:       attributes = $2
150:       img_closing_tag = $3
151: 
152:       # Remember where to continue searching from in the next
153:       # iteration.
154:       continue_pos = pos+img_match.length
155: 
156:       # Write out the content before the start of the tag
157:       block_rewrite << block_output[0..pos-1]
158:       if img_match =~ /src\=["']?https?:\/\//i then
159:         block_rewrite << img_match
160:       else
161: 
162:         # Write out the opening portion of the image tag (<img).
163:         block_rewrite << img_tag
164: 
165:         # Process all attributes of the img tag.
166:         height_given = width_given = nil
167:         classes = ''
168:         ping = ::ImageBundleHelper::Image.new
169:         while pos = (attributes =~ /([^ =]+?)\s*=\s*(("?([^"=]*?)")|('?([^'=]*?)'))/im) do
170:           attribute = $1
171:           value = $4 || $6
172:           attr_continue_pos = pos+$~.to_s.length
173:           case attribute
174:           when 'src'
175:             ping.path = value
176:             # Read only the image's meta data not its image content.
177:             ping.file = "#{RAILS_ROOT}/public#{ping.path}"
178:             image = ::Magick::Image.ping(ping.file)[0]
179:             ping.height = image.rows
180:             ping.width = image.columns
181:             block_rewrite << "#{attribute}=\"#{replacement_image}\" "
182:           when 'height'
183:             height_given = value.to_i
184:           when 'width'
185:             width_given = value.to_i
186:           when 'class'
187: 
188:             # Prepend a space for later concatenation with bndl class.
189:             classes = " #{value}"
190:           else
191: 
192:             # Pass through all other attributes
193:             block_rewrite << "#{attribute}=\"#{value}\" "
194:           end
195:           attributes = attributes[attr_continue_pos..-1]
196:         end
197: 
198:         # Calculate the height and width of the image based on the
199:         # specified height/width and the source file's height and
200:         # width. Scaling needs to happen when the sprite is created
201:         if height_given == nil then
202:           if width_given != nil then
203:             ping.height = (ping.height * (width_given.to_f / ping.width.to_f)).to_i
204:             ping.width = width_given
205:           end
206:         else
207:           if width_given == nil then
208:             ping.width = (ping.width * (height_given.to_f / ping.height.to_f)).to_i
209:             ping.height = height_given
210:           else
211:             ping.width = width_given
212:             ping.height = height_given
213:           end
214:         end
215: 
216:         # Only add unique images and height/width combinations to the hash.
217:         key = "bndl#{::Digest::MD5.hexdigest("#{ping.path}:#{ping.height}:#{ping.width}").hash}"
218:         images[key] ||= ping
219:         block_rewrite << "class =\"#{key}#{classes}\" "
220:         block_rewrite << "height=\"#{ping.height}\" "
221:         block_rewrite << "width=\"#{ping.width}\" "
222:         block_rewrite << img_closing_tag
223:       end
224:       block_output = block_output[continue_pos..-1]
225:     end
226: 
227:     # Create a sprite when there are source files and if it doesn't
228:     # already exists.
229:     if images.length > 0 then
230:       sprite_path = '/' + SPRITE_BASE_DIR + '/' + ::Digest::MD5.hexdigest(images.keys.inject do |concat_names, key| concat_names + '|' + key end) + ".#{sprite_type}"
231:       sprite_file = "#{RAILS_ROOT}/public/#{sprite_path}"
232:       if !File.exists?(sprite_file) then
233: 
234:         # Stack scaled source images left to right.
235:         sprite = images.values.inject(::Magick::ImageList.new) do |image_list, ping|
236:           image_list << ::Magick::ImageList.new(ping.file)[0].scale(ping.width, ping.height)
237:         end.append(false)
238:         sprite.write(sprite_file)
239:       end
240: 
241:       # Construct style tag to be included in the header.
242:       current_y = 0
243:       bundle_styles = "\n<style type=\"text/css\">\n"
244:       bundle_styles << images.keys.inject('') do |styles, key|
245:         images[key].x_pos = current_y
246:         current_y += images[key].width
247:         styles + ".#{key} {\n   background-image:url(#{sprite_path});\n background-position: -#{images[key].x_pos}px 0px;\n}\n"
248:       end
249:       bundle_styles << "</style>\n"
250:     end
251: 
252:     # Write the remaining block output that follows the last img tag.
253:     block_rewrite << block_output
254:     buffer << block_rewrite if block_rewrite
255:     content_for content_target.to_sym, bundle_styles ||= ''
256:   end

[Validate]