Tuesday, October 6, 2009

Rack Image Science: A Spike

‹prev | My Chain | next›

I started a spike last night to help me understand what was needed to thumbnail images in Rack. The idea is that, given a request for an image ending in _sm.jpg, the handler should request the full-sized image, thumbnail it, and return the smaller image to the requesting client.

I was actually pretty close last night, but sleepiness eventually got the better of me. This is a functioning version of last night's thumbnailer:
class ThumbNail
def initialize(app); @app = app end
def call(env)

if env['REQUEST_URI'] =~ /_sm.jpe?g/
env['REQUEST_URI'].sub!(/_sm\./, '.')
env['REQUEST_PATH'].sub!(/_sm\./, '.')
env['PATH_INFO'].sub!(/_sm\./, '.')
http_code, headers, original_body = @app.call(env)

img_data = ''
body.each do |data|
img_data << data
end


ImageScience.with_image_from_memory(img_data) do |img|
img.resize(100, 150) do |small|
small.save "/tmp/small.jpg"
end
end

f = File.open "/tmp/small.jpg"
[http_code, headers, f.read]
else
@app.call(env)
end
end
end

use ThumbNail
Missing last night was the bold section, where I actually use the body response from a Rack application like it is supposed to be used—with an each iteration.

I could delete the code and start on implementation proper, but first I would like to explore building a cache for the thumbnails. There is no sense in generating the images on each request. In this case, the call method needs to decide if a cache hit occurs and, if not, generate the thumbnail:
  # Main Rack call responder
def call(env)
if env['REQUEST_URI'] =~ /_sm.jpe?g/
# Handle thumbnail requests
unless File.exists?(filename(env))
mk_thumbnail(env)
end

small = File.new(filename(env)).read
[200, { }, small]
else
# Pass-thru non thumbnail requests
@app.call(env)
end
end
The rest of the code remains essentially the same. The complete, seemingly functional Rack handler:
class ThumbNail
# hard-code the store location (could be set via use options)
STORE = '/var/cache/thumbnail'

# Basic Rack initialization
def initialize(app); @app = app end

# Main Rack call responder
def call(env)
if env['REQUEST_URI'] =~ /_sm.jpe?g/
# Handle thumbnail requests
unless File.exists?(filename(env))
mk_thumbnail(env)
end

small = File.new(filename(env)).read
[200, { }, small]
else
# Pass-thru non thumbnail requests
@app.call(env)
end
end

# Make the thumbnail, storing it in the cache for subsequent
# requests
def mk_thumbnail(env)
$stderr.puts "[thumbnail] building thumbnail..."
cache_filename = filename(env)
path = cache_filename.sub(/\/[^\/]+$/, '')
FileUtils.mkdir_p(path)

env['REQUEST_URI'].sub!(/_sm\./, '.')
env['REQUEST_PATH'].sub!(/_sm\./, '.')
env['PATH_INFO'].sub!(/_sm\./, '.')

http_code, headers, body = @app.call(env)

img_data = ''
body.each do |data|
img_data << data
end

ImageScience.with_image_from_memory(img_data) do |img|
img.resize(200, 150) do |small|
small.save cache_filename
end
end
end

# Helper method for calculating the location of the file in the
# cache.
def filename(env)
"#{STORE}#{env['PATH_INFO']}"
end
end
Updating my Sinatra app to pull these images, I find the response time on my homepage much better (going from 1-2 seconds to ~100ms):



Tomorrow I delete all this code and then go back and do it right. Even in this limited spike, there is already code with which I am unhappy. A little BDD should clear that up.

No comments:

Post a Comment