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 :)

blog comments powered by Disqus