Outputting resized attachment_fu image, stored in database, with RMagick
I have recently started using attachment_fu plugin to store attachments (including images, of course) in database. attachment_fu provides functionality to create thumbnail images during upload, but you can't really foresee what thumbnail sizes you will need down the road, can't you?
That's why I decided not to make any thumbnails at all during the upload stage (except for basic resizing so I will not end up with 4K-pixels images in database.
I store all the images in database (easier to migrate, easier to backup) , which adds some level of complixity when you try to output stuff to the user.
Having all that, I wanted to be able to easily output page_cachable resized images. Here's how I have implemented it.
First of all, I have GalleryImage class which "has_one :fu_attachment", which in turn "has_attachment" (and that's where attachment_fu starts to do its magic).
Here's the FuAttachment model:
class FuAttachment < ActiveRecord::Base
belongs_to :gallery_image, :foreign_key => "parent_id"
has_attachment :max_size => 30.megabytes,
:resize_to => '800×700>'validates_as_attachment
def full_filename
""
endend
The full_name function definition returning blank string is required because of a bug in attachment_fu (it doesn't return full_filename if you store images in database).
And GallerImage model looks like this:
class GalleryImage < ActiveRecord::Base
belongs_to :user
has_one :fu_attachment, :dependent => :destroy, :as => "parent"def uploaded_data=(data)
oneAttachment = FuAttachment.new(:uploaded_data => data, :parent_id => self.id)
if oneAttachment.save
self.fu_attachment = oneAttachment
else
errors.add(:gallery_image, _("file could not be uploaded"))
end
endend
Having all that, here's how my image-resizing controllers looks like:
class GalleryImageController < ApplicationController
def thumbnail
outputResized("100×82")
enddef outputResized(geom, quality=75)
begin
@picture = GalleryImage.find(params[:id])
thePic = @picture.fu_attachment
unless thePic.nil?
image = Magick::Image.from_blob(thePic.db_file.data).first
image.change_geometry!(geom) { |cols, rows, img|
img.resize!(cols, rows)
}send_data (image.to_blob() {self.quality = quality}, :type => thePic.content_type,
:filename => thePic.filename,
:disposition => 'inline')
end
rescue
# can't display an image for any reason at all.
end
endend
When you need to output resized image, just use /gallery_image/thumbnail/<id> path. If you decide to add a new resize option, just duplicated the thumbnail function, give it a new name and set new parameters for outputResized function.
You definitely will want to cache the resizing results. In order to do that, just add "caches_page :thumbnail" just above "def thumbnail" function.
The approach might be not perfect, but it works, and it good enough to my purposes. Of course any comments are welcomed.
February 28th, 2008 at 9:38 am
Nice piece of code!
Is there a way to let nginx / apache serve the file after it has been created? IIRC there's a way for nginx / lighttpd to set a SEND_FILE variable from your cgi script, and then let them serve the file.
This is also handy for for example files that require a login.
February 28th, 2008 at 9:53 am
Joris,
you can just add cache_page to the controller serving images and everything should be served directly to user by apache bypassing mongrel/rails completely. You just need the correct extension to be added to the cached file.
Sort of:
cache_page :thumbnail
As a tip, if you have lots of functions which resize/modify stuff for you in various ways, it might be tedious to add every single one to the cache_page function. I use code like that in my systems:
after_filter lambda { |c| c.cache_page }, :except => [:post_upload, :post_video, :sanitize_filename, :media_attachment]
which caches everything except for specified (non image-resizing/processing functions)
March 4th, 2008 at 7:56 pm
Ok, but what if another client (that doesn't have the file in cache) requests for it? It will go 100% though mongrel
What I meant was something like:
http://blog.kovyrin.net/2006/11/01/nginx-x-accel-redirect-php-rails/
March 5th, 2008 at 7:56 am
Hi Joris,
sorry I guess I've been somewhere else when replying. Indeed, all the requests will go 100% through Mongrel. There's a possible solution though (haven't tried it myself though) would be to cache image (as usual, but with the correct extension as I have noted above) and then just setup a, say, media.xxxxxxx.com subdomain for your server, then setup apache to handle this subdomain directly (without passing stuff through mongrel cluster), and serve all the static recompressed images from there. This might be your best bet (that is, no need to google for docs as the solution is already here), but only if you can easily add subdomains to the domain on which your application is hosted.