Mirror images with Paperclip
Posted on · · Tags: 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
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:
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:
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:
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:
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:
{
: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:
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 :)