Attachment_fu is the shit, no doubt. But sometimes you want to do more than upload, resize, and thumbnail. Designers often have a specific vision that dictates a more complex workflow for incoming images. For these tasks, it may be necessary to reprocess the saved images - something you don't necessarily need to hack attachment_fu to accomplish.
For example, my latest Rails project included a gallery page with a pretty standard layout: a series of thumbnails and an area to display the full size version of the selected thumbnail. However, the static mockup delivered by the designer had thumbnails that were black and white with a blueish tint, only turning color when you moused over them. On top of that, the thumbnails were often made from a manually defined cropping of the image. This meant that in addition to an administrative backend to allow uploading and management, I needed to provide a tool for selecting an area within the image for a custom thumbnail, not to mention figuring out where and how to do the tinting.
So here's how I approached it: I created two STI models deriving from a common GalleryImage model, all of which are related to the GalleryItem that encapsulates the item name, description, etc:
class GalleryItem < ActiveRecord::Base
belongs_to :full_image, :class_name => 'GalleryMainImage',
:foreign_key => 'full_image_id',
:dependent => :destroy
belongs_to :custom_thumbnail,
:class_name => 'GalleryThumbnail',
:dependent => :destroy
end
class GalleryImage < ActiveRecord::Base
end
class GalleryMainImage < GalleryImage
has_one :gallery_item, :dependent => :destroy
has_attachment :content_type => :image,
:storage => :file_system,
:max_size => 50.megabytes,
:resize_to => '457>',
:thumbnails => { :default_thumbnail => '68x68!',
:gray_thumbnail => '68x68!' },
:path_prefix => 'public/gallery',
:thumbnail_class => "GalleryThumbnail"
validates_as_attachment
end
class GalleryThumbnail < GalleryImage
has_one :gallery_item, :dependent => :destroy
has_attachment :content_type => :image,
:storage => :file_system,
:max_size => 1.megabyte,
:resize_to => '68x68',
:thumbnails => { :gray_thumbnail => '68x68' },
:path_prefix => 'public/gallery'
validates_as_attachment
end
So, the important takeaways here are the following:
- All thumbnails would be of class "GalleryThumbnail".
- An item has one GalleryMainImage with it's own attachment_fu-generated thumbnails. These thumbnails are the "uncropped" thumbnails.
- An item can have two more custom cropped thumbnails associated with it - a color one and a gray one.
The next step is doing the grayscaling. I determined the path forward on this first by consulting the designer who delivered the mockup. He went back into Photoshop and gave me an idea of what filters and processes were applied to the thumbnails he had created. Basically, he created a grayscaled version of the thumbnail and then applied a gradient map that mapped black to a shade of blue. It gave it a very interesting effect:
It took a lot of digging, but I was able to find two RMagick methods that could reproduce this effect: quantize and level_colors. Now it was just a matter of finding out how to integrate them into the thumbnailing process. I was surprised that simple callbacks basically did the trick. I put a method in the base class for the images:
class PortfolioImage < ActiveRecord::Base
protected
def update_gray_thumbnail!
return unless thumbnail.blank? # only perform this on a custom thumbnail or an original image
# the code below looks dumb, but I think the trick is that RMagick
# can only perform one operation per file load. I don't get it.
thumb = thumbnails.find_by_thumbnail("gray_thumbnail")
Magick::Image.read(thumb.full_filename).first.quantize(256,Magick::GRAYColorspace).write(thumb.full_filename)
Magick::Image.read(thumb.full_filename).first.level_colors("#201000", "#f7f7f7", false).write(thumb.full_filename)
end
end
Remember: in our modeling, everything is a GalleryImage. What makes a given GalleryImage a thumbnail according to attachment_fu is that it has a non-nil response to the "thumbnail" message (if this were our gray_thumbnail, it would return "gray_thumbnail" in response to the "thumbnail" message). So by proceeding only if thumbnail returns a blank response, we guarantee that we work with a main attachment like a MainImage or custom thumbnail, and that we don't work on any of their associated images.
So look at how we generate thumbnails for a GalleryThumbnail and GalleryMainImage: there's a default_thumbnail and a gray_thumbnail. If you're using attachment_fu with ":storage => :file_system", then you have physical files in the project that you can modify to your heart's content. The above method changes the actual file associated with the "gray_thumbnail", which is initially saved as a color thumbnail. So if you put a hook in your GalleryMainImage and GalleryThumbnail models to make this alteration on saving the model, you should be money:
after_save :update_gray_thumbnail!
Attachment_fu regenerates thumbnails on every model save, so it's important we reapply the RMagick processing each time.
So what about the custom cropping? Well, you'll need a controller that can generate a new GalleryThumbnail to be associated with the GalleryItem. All I'll say on that count is that you should look at some javascript cropping utilities; I'm using jquery so I used imgAreaSelect. Following this example I was able to create a tool letting the user drag a box over with previewing of the resulting thumbnail, passing the coordinates for cropping to the controller via a form submission. Then it was just a matter of cropping the image, which once again is merely a matter of manipulating the actual full-size image file saved in the public directory after the fact:
def create
item = GalleryItem.find(params[:portfolio_item_id])
crop = item.full_image.crop(params[:x1], params[:y1], params[:w], params[:h])
thumb = GalleryThumbnail.new
thumb.uploaded_data = crop
thumb.save
item.portfolio_thumbnail = thumb
if item.save
flash[:notice] = "Cropped custom thumbnail saved."
redirect_to admin_gallery_item_path(item)
else
flash[:error] = "Error resizing"
render :action => 'new'
end
I have a "crop" method on GalleryMainImage defined thusly:
def crop(x, y, width,height)
blob = StringIO.new(Magick::Image.read(full_filename).first.crop(x.to_i, y.to_i, width.to_i, height.to_i).to_blob)
{'tempfile' => blob,
'content_type' => "image/#{filename.split('.').last}",
'filename' => "custom_#{filename}"}
end
The use of StringIO and returning a hash is just tricks to get attachment_fu to accept our cropped image as a parameter for "uploaded_data=". And once the cropped image is passed into "uploaded_data=" and object is saved, the thumbnail will be generated using the cropped image - and grayscaled appropriately!
That's pretty much it - I know this is really complicated, but I hope it helps somebody out there. Feel free to ask questions, and be advised that I may revisit this article to word things differently.
Read this article