Sunday, October 11, 2009

Rack::ThumbNailer

‹prev | My Chain | next›

As of yesterday, I have my Rack middleware thumbnail-ing application images when the request has the thumbnail parameter.

I am using ImageScience (documentation), which does all of its work in the filesystem. Since thumbnails are being stored on the filesystem, I might as well keep them there and serve them up whenever subsequent requests come in for the same resource.

Describing this in RSpec:
      context "and a previously generated thumbnail" do
before(:each) do
File.stub!(:exists?).and_return(true)
end
it "should not make a new thumbnail" do
Rack::ThumbNailer.
should_not_receive(:mk_thumbnail)
get "/foo.jpg", :thumbnail => 1
end
end
This fails because nothing is preventing the call to mk_thumbnailer:
1)
Spec::Mocks::MockExpectationError in 'ThumbNailer Accessing an image with thumbnail param and a previously generated thumbnail should not make a new thumbnail'
expected :mk_thumbnail with ("/var/cache/rack/thumbnails/foo.jpg", "image data") 0 times, but received it once
./thumbnailer.rb:18:in `call'
/home/cstrom/.gem/ruby/1.8/gems/rack-test-0.5.0/lib/rack/mock_session.rb:30:in `request'
/home/cstrom/.gem/ruby/1.8/gems/rack-test-0.5.0/lib/rack/test.rb:207:in `process_request'
/home/cstrom/.gem/ruby/1.8/gems/rack-test-0.5.0/lib/rack/test.rb:57:in `get'
./thumbnailer_spec.rb:67:
To make that example pass, I add an unless File.exists? to the call Rack method:
    def call(env)
req = Rack::Request.new(env)
if !req.params['thumbnail'].blank?
filename = @options[:cache_dir] + req.path_info

unless ::File.exists?(filename)
image = ThumbNailer.rack_image(@app, env)
ThumbNailer.mk_thumbnail(filename, image)
end

thumbnail = ::File.new(filename).read
[200, { }, thumbnail]
else
@app.call(env)
end
end
That should just about do it. I add it to my config.ru Rackup file:
...
###
# Thumbnail

require 'rack/thumbnailer'
use Rack::ThumbNailer,
:cache_dir => '/tmp/rack/thumbnails'

###
# Sinatra App
...
Trying this out for real, I access a real image:



And, when I add the thumbnail query parameter:



Nice!

Now that I have my Rack middleware, I need to use it. I have designed it such that the image needs to be called with a query parameter. Normally, a query parameter on an image request will have no effect. Thus, opting for a query parameter seemed like a nice way of keeping my Sinatra app somewhat de-coupled from the Rack middleware. If the middleware is present a thumbnail will be returned. If the Rack middleware is not present, the full-size image will be returned (possibly with width and height attributes scaling the image in the browser).

The homepage currently has meal thumbnails included like:
...
%a{:href => date.strftime("/meals/%Y/%m/%d")}
= (image_link meal, :width => 200, :height => 150)
%h2
%a{:href => date.strftime("/meals/%Y/%m/%d")}= meal["title"]
...
Currently the image_link helper is returning an image tag that tells the browser to scale the image (something like <img src="/images/foo.jpg" width="200" height="150">). I would like to be able to pass additional query parameters to the image_link helper. I describe this in RSpec as:
describe "image_link" do
it "should include query parameters" do
image_link(@doc, { }, :foo => 1).
should have_selector("img",
:src => "/images/#{@doc['_id']}/sample.jpg?foo=1")
end
end
At first that fails with an incorrect number of arguments message:
1)
ArgumentError in 'image_link a document with an image attachment should include query parameters'
wrong number of arguments (3 for 2)
./spec/eee_helpers_spec.rb:202:in `image_link'
./spec/eee_helpers_spec.rb:202:
I change the message by adding an additional, optional third argument to the helper:
    def image_link(doc, options={ }, query_params={ })
# ...
end
I still get a failure because the expectation is not met:
1)
'image_link a document with an image attachment should include query parameters' FAILED
expected following output to contain a <img src='/images/foo/sample.jpg?foo=1'/> tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><img src="/images/foo/sample.jpg"></body></html>
./spec/eee_helpers_spec.rb:202:
To make it pass, I rely on Rack::Utils.build_query to assemble the query string from a hash:
    def image_link(doc, options={ }, query_params={ })
#...
attrs = options.map{|kv| %Q|#{kv.first}="#{kv.last}"|}.join(" ")
query = query_params.empty? ? "" : "?" + Rack::Utils.build_query(query_params)
%Q|<img #{attrs} src="/images/#{doc['_id']}/#{filename}"/>|
end
With that passing, I update the homepage to trigger a thumbnail, if the Rack middleware is present:
          %a{:href => date.strftime("/meals/%Y/%m/%d")}
= (image_link meal, {:width => 200, :height => 150}, {:thumbnail => 1})
%h2
%a{:href => date.strftime("/meals/%Y/%m/%d")}= meal["title"]
And, checking it out in Firebug, the load time for my images is indeed down from 1-2 seconds all the way to ~100ms:



I will get that deployed to the beta site tomorrow and then... I think my chain may be done!

No comments:

Post a Comment