Mirror images with Paperclip
While implementing an awesome design, I needed to dynamically create a mirror effect on some thumbnails. Since I’m more or less dedicated to Paperclip, I decided to see if it could be done, without any monkeypatching. Here’s what I came up with.
First, I should explain my setup of Paperclip attachments in this project. Because I’m a bit lazy, I didn’t want to make too many migrations for every different type of attachment. Instead I created a STI class called PaperclipAttachment that is the base class of all the different types of attachments in this project
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class CreatePaperclipAttachments < ActiveRecord::Migration def self.up create_table(:paperclip_attachments) do |t| t.string :data_file_name t.string :data_content_type t.integer :data_file_size t.datetime :data_updated_at t.string :type t.references :container, :polymorphic => true end end def self.down drop_table :paperclip_attachments end end |
This class is defined as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
class PaperclipAttachment < ActiveRecord::Base # Relationships belongs_to :container, :polymorphic => true # Attributes attr_protected :data_file_name, :data_content_type, :data_file_size # Delegations delegate :path,:url, :to => :data # Extensions has_attached_file :data, { :styles => lambda{|data| data.instance.class.attachment_styles}, :processors => lambda{|instance| instance.class.processors}, :path => ":rails_root/public/system/:class/:id_partition/:style/:basename.:extension", :url => "/system/:class/:id_partition/:style/:basename.:extension", :default_url => "/system/:class/defaults/missing_:style.png" } def self.attachment_styles {} end def self.processors [:thumbnail] end end |
The reason I’m proc’ing :styles and :processors, is that I want to be able to treat the different attachment styles differently. The file structure still stays logical, and the only “downside” I can see with this approach is that all attachments reside in the same table in the database.
So with this as the base for my project, I created a PDF class that inherits from this PaperclipAttachment class. I defined the different JPG snapshots of the PDF that I needed like this:
1 2 3 4 5 6 7 8 9 |
class Pdf < PaperclipAttachment def self.attachment_styles { :small_preview => { :geometry => '106x72>', :format => :jpg }, :preview => { :geometry => '297x210>', :format => :jpg }, :big_preview => { :geometry => '460x300>', :format => :jpg } } end end |
So, it was time to implement the mirror effect. After considering making a processor for a bit, I decided to go with a less time consuming solution. Based on this shellscript from roundcube.net I started hacking on my Pdf class.
First, I added a couple of methods for getting the path and url of this mirror image. I wanted it to be connected to each Paperclip style, so I decided to put it in the same directory:
1 2 3 4 5 6 7 |
def mirror_path(style=:original) path(style).gsub(/.\w+$/,'') + '_mirror.png' end def mirror_url(style=:original) url(style).gsub(/.\w+(|\?\d+)$/,'') + '_mirror.png' end |
Then, the method that does the dirty work turned out like this. You can probably strip out half of it, but I wanted to have the height of the mirror effect snap to my current line-height, and some other semi-bloat stuff:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
def mirror(args = {}) style = args[:style] || :small_preview infile = args[:in] || path(style) outfile = args[:out] || mirror_path(style) alpha = args[:alpha] || 30 line_height = args[:line_height] || 18 height_f = 0.2 width,height = Paperclip::Geometry.from_file(infile).to_s.split('x').collect(&:to_i) height = ( (height * height_f) * ( 1.0 / line_height ) ).round / ( 1.0/line_height ) geometry = [width, height].collect(&:to_i).join 'x' command = "convert -size #{geometry} xc:none" command << " \\( \\( -flip #{infile} -crop #{geometry}+0+0 \\)" command << " -size #{geometry} gradient: -evaluate Pow 1.4" command << " -compose Copy_Opacity -composite \\) " command << " -compose blend -set \"option:compose:args\" #{alpha} -composite #{outfile}" warn "Mirroring: " + command if RAILS_ENV == 'development' system command end |
For configuration, I wanted to use the :styles hash provided by Paperclip. So I added a key called mirror on the styles I wanted to mirror:
1 2 3 4 5 |
{
:small_preview => { :geometry => '106x72>', :format => :jpg, :mirror => {:alpha => 30} },
:preview => { :geometry => '297x210>', :format => :jpg, :mirror => {:alpha => 30} },
:big_preview => { :geometry => '460x300>', :format => :jpg }
} |
And then made a method to check which styles I should mirror, and call the mirror method:
1 2 3 4 5 6 |
def make_mirrors data.styles.each_pair do |style,style_hash| style_hash.select{|k,v| k == :mirror}.each{|k,v| self.mirror({:style => style}.merge(v))} end true end |
Voila! That’s the proof of concept that I’ve come up with so far, but I will probably refactor it a bit before putting it into production. Hopefully it’s of some use for someone else too :)
Jonas Nicklas commented 4 days later (February 11, 2010 00:47)
I wrote up how to do approcimately the same thing with CarrierWave, check it out: http://elabs.se/blog/10-mirror-images-with-carrierwave
Write a comment