Blog Article

Rotating Paperclip Image Attachments in Rails

With users uploading personal photos, especially ones coming their phones that capture landscape photos in portrait mode and vice versa, one of the things I wanted to integrate into Black Book Singles is the ability to rotate photos. Since I’m using the Paperclip gem, this should be relatively easy to achieve through a custom attachment processor.

Doing a precursory search resulted in a bit of helpful code to get me started. Thanks to tekn0t for sharing his example in this gist.

Unfortunately, I came to the conclusion that this example has three main issues:

  1. It doesn’t follow Paperclip convention of passing a set of option key/value pairs into the Processor class to enable the processing.
  2. It assumes any class that implements it also implements both rotating? and rotation methods or attributes, with no flexibility on the naming.
  3. The base class of the implemented Rotator class is Thumbnail. Because Thumbnail is included by default when the geometry option is used, this means the geometry commands will unnecessarily be executed twice.

With these things in mind, I set out to rewrite the example code into a more robust implementation.

First, we’re going to need an attribute on our model that keeps track of the current angle of rotation. This is necessary so that future rotations will be based upon the current angle in degrees. To do this, create a migration that adds an integer column named rotation to whichever Paperclip model you’re working with.

class AddRotationToPhotos < ActiveRecord::Migration
  def self.up
    add_column :photos, :rotation, :integer, :null => false, :default => 0
  end
 
  def self.down
    remove_column :photos, :rotation
  end
end

Next, we’ll want a simple way to update the rotation value on a model. Since Paperclip needs to be explicitly told when to reprocess an image, we’ll need to tell it to do so whenever this value changes. Also, keep in mind that basic math tells us that the only degrees we should worry about are between 0-360, meaning we can perform a simple modulus operation on our rotation to keep it in this range.

class Photo < ActiveRecord::Base
  before_save :adjust_rotation
  before_update :reprocess_image
 
protected
 
  def adjust_rotation
    self.rotation = self.rotation.to_i
    self.rotation = self.rotation % 360 if (self.rotation >= 360 || self.rotation <= -360)
  end
 
  def reprocess_image
    self.image.reprocess! if self.rotation_changed?
  end
end

Now we can set the rotation value when creating a record (defaulting to 0) or when updating an existing record.

# new record example to rotate 90 degrees clockwise
photo = Photo.new(params[:photo])
photo.rotation = 90
photo.save
 
# update record example to rotate 90 degrees counter-clockwise
photo = Photo.find(params[:id])
photo.update_attribute(:rotation, -90)

So far, our code has’t done anything to tell Paperclip what to do. The first step in notifying Paperclip is to update the hash passed into has_attached_file. Notably, we’ll want to include :rotator (which will be the name of our processor class) into the array of processors. Also, we’ll need to pass a :rotation key/value pair in with each attachment style we wish to process as such. Since this call is being made at the model level, we can’t simply use self.rotation to access the value of the record’s rotation angle. Fortunately, Paperclip allows us to use a lambda function to return a styles hash, which includes the Attachment object. Knowing this, we can access this current record’s rotation via attachment.instance.rotation.

class Photo < ActiveRecord::Base
  has_attached_file :image,
    :processors => [:rotator],
    :styles => lambda { |a| {
      :thumb => {
        :geometry => '50x50#',
        :rotation => a.instance.rotation,
      },
      :full => {
        :geometry => '640x640>',
        :rotation => a.instance.rotation,
      },
    } }
end

Finally, now that we’re passing the rotation value to Paperclip, we need to create the processor to handle it. Much of the basis of this code is taken from the Thumbnail processor included with Paperclip, which I won’t be delving into. The main thing to pay attention to is any code pertaining to the rotation attribute being set and used, most notably in the transformation_command method.

module Paperclip
  class Rotator < Processor
    attr_accessor :file, :rotation, :source_file_options, :convert_options, :whiny, :current_format, :basename
 
    def initialize(file, options = {}, attachment = nil)
      super
      @file                = file
      @rotation            = options[:rotation].to_i
      @source_file_options = options[:source_file_options]
      @convert_options     = options[:convert_options]
      @whiny               = options[:whiny].nil? ? true : options[:whiny]
      @current_format      = File.extname(@file.path)
      @basename            = File.basename(@file.path, @current_format)
    end
 
    def make
      if @rotation != 0
        dst = Tempfile.new([@basename, @format ? ".#{@format}" : ''])
        dst.binmode
 
        begin
          parameters = []
          parameters << source_file_options
          parameters << ":source"
          parameters << transformation_command
          parameters << convert_options
          parameters << ":dest"
 
          parameters = parameters.flatten.compact.join(" ").strip.squeeze(" ")
          success = Paperclip.run("convert", parameters, :source => File.expand_path(@file.path), :dest => File.expand_path(dst.path))
        rescue Cocaine::ExitStatusError => e
          raise PaperclipError, "There was an error processing the image rotation for #{@basename}" if @whiny
        rescue Cocaine::CommandNotFoundError => e
          raise Paperclip::CommandNotFoundError.new("Could not run the `convert` command. Please install ImageMagick.")
        end
 
        dst
      else
        @file
      end
    end
 
  private
 
    def transformation_command
      "-rotate #{@rotation}"
    end
  end
end

And there you have it! An easy-to-use rotation processor for use with your Paperclip enabled model in Rails.

Zubin
January 4th, 2012 at 10:24 pm

This works very well, thanks for posting this Matt!

babu
February 1st, 2012 at 7:45 am

do u know how to use paperclip

Chris
March 19th, 2012 at 1:53 pm

I can’t seem to get this working.

When the image is uploaded I get the error :

“unknown file type: /tmp/stream20120319-15494-1lph3tr-0.jpg”

If I remove the “:processors=>[:rotator]” line then it works fine.

I am pulling my hair out with this! I have copied the Rotator class into the lib/papercip_processors folder. I have checked that the processor is being triggered and it is.

/home/chrisr/.rvm/rubies/ruby-1.8.7-head/lib/ruby/1.8/fileutils.rb:1262:in `copy’
/home/chrisr/.rvm/rubies/ruby-1.8.7-head/lib/ruby/1.8/fileutils.rb:452:in `copy_entry’
/home/chrisr/.rvm/rubies/ruby-1.8.7-head/lib/ruby/1.8/fileutils.rb:1331:in `traverse’
/home/chrisr/.rvm/rubies/ruby-1.8.7-head/lib/ruby/1.8/fileutils.rb:449:in `copy_entry’
/home/chrisr/.rvm/rubies/ruby-1.8.7-head/lib/ruby/1.8/fileutils.rb:508:in `mv’
/home/chrisr/.rvm/rubies/ruby-1.8.7-head/lib/ruby/1.8/fileutils.rb:1402:in `fu_each_src_dest’
/home/chrisr/.rvm/rubies/ruby-1.8.7-head/lib/ruby/1.8/fileutils.rb:1418:in `fu_each_src_dest0′
/home/chrisr/.rvm/rubies/ruby-1.8.7-head/lib/ruby/1.8/fileutils.rb:1400:in `fu_each_src_dest’
/home/chrisr/.rvm/rubies/ruby-1.8.7-head/lib/ruby/1.8/fileutils.rb:495:in `mv’
paperclip (2.3.16) lib/paperclip/storage/filesystem.rb:42:in `flush_writes’
paperclip (2.3.16) lib/paperclip/storage/filesystem.rb:37:in `each’
paperclip (2.3.16) lib/paperclip/storage/filesystem.rb:37:in `flush_writes’
paperclip (2.3.16) lib/paperclip/attachment.rb:162:in `save’

If I remove the “:processors=>[:rotator]” line, upload the file, then replace the line, then set the rotation to something > 0, then it works fine. If I then set the rotation back to 0 i get the same error message.

matt
March 20th, 2012 at 6:32 pm

Is your Rails app loading your Rotator class from your lib folder? You may need to modify your config/application.rb file to specify the path to be loaded, e.g.:

config.autoload_paths += %W(#{config.root}/lib)

Alternatively, try placing your class somewhere under app, e.g.: app/paperclip_processors/rotator.rb.

Peter
May 22nd, 2012 at 12:10 pm

Matt

I’ve just tried to implement using Paperclip 3.0.4. The rotation works
but the image is not resized.

What version of the gem were you / are you using and have you ever had any issues with geometry?

If I create the images normally without lambda and rotation then the images are created in the right size.

Resizes but no rotation:

   has_attached_file :avatar, :styles => {:small => "100x100", :thumb => "64x64#" }

Rotates but not resizing:

  has_attached_file :avatar,
    :processors => [:rotator],
    :styles => lambda { |a| {
      :thumb => {
        :geometry => '50x50#',
        :rotation => a.instance.rotation,
      },
      :full => {
        :geometry => '640x640>',
        :rotation => a.instance.rotation,
      },
    } }

Any thoughts??

matt
May 26th, 2012 at 6:16 pm

Hi Peter. The version I was using when I wrote this article was Paperclip 2.4.5. I haven’t followed the changes between 2.4.5 and 3.0.4 to know whether that would have a drastic effect on how processors are implemented, unfortunately.

July 31st, 2012 at 7:57 am

Justa add :thumbnail as the other processor:

:processors => [:thumbnail, :rotator]
matt
August 4th, 2012 at 3:42 pm

Good catch, Bruno. Yes, including the :thumbnail symbol in the :processors list is what tells Paperclip to perform that step when processing your image.

October 3rd, 2012 at 2:54 am

yeah.. this save me!
appreciate article update with this info.
thanks!

June 6th, 2012 at 5:26 pm

Hey Matt, i’m having some problems, when is trying to upload the images with s3 it shows a bad uri error because is trying to get from http, i put in the s3_protocol https so i don’t know what could be happening… can you help me with this??? thanks

i have this in my environment

  PAPERCLIP_STORAGE_OPTIONS = { :storage => :s3, 
                                :s3_credentials =>  YAML.load_file("#{Rails.root}/config/s3.yml")["development"].symbolize_keys,
                                :bucket => "xxxxxxx",
                                :s3_protocol => "https"
                                }

and this in my model

  has_attached_file :attachment, {
      :styles => lambda { |a| {
        :big   => {
                  :geometry => "50>x50>", 
                  :rotation => a.instance.rotation
                }
      }},
      :convert_options => { :all => '-auto-orient' },
      :path => AMAZON_S3_ROOT_PATH+"/public/system/attachments/:id/:style/:filename",
      :url => ':s3_escaped_path_url',
      :processors => [:rotator]
    }.merge(PAPERCLIP_STORAGE_OPTIONS)

also i’m using a method to scape the url to avoid errors when the image name have spaces, but it shows the error only when the rotator procesor is present.

URI::InvalidURIError (bad URI(is not URI?): http://s3.amazonaws.com:80/bucket/public/system/attachments/628/big/Captura de pantalla 2012-05-22 a las 15.41.30.png):

matt
June 8th, 2012 at 8:14 am

Hi Damien. It’s hard to tell without having a sample project and the full code, but a few things I would look at (in order) are:

* Where is :s3_escaped_path_url defined? How is that getting interpreted in the :url option?
* What is AMAZON_S3_ROOT_PATH defined as?
* What version of Paperclip are you using, and when was the :s3_protocol option introduced?

December 12th, 2012 at 9:06 am

Hello, I’m trying to use the sample, I could see the image is rotated, but I receive a error and it block the service of server:

xm” -rotate -90 “C:/Users/Andreza/AppData/Local/Temp/DSC0222920121212-984-1yva7g620121212-984-btooxm20121212-984-ry522d”
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] Command :: file -b –mime “C:/Users/Andreza/AppData/Local/Temp/DSC0222920121212-984-1yva7g620121212-9
84-btooxm20121212-984-ry522d”
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] [paperclip] Error while determining content type: Cocaine::CommandNotFoundError
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] [paperclip] deleting C:/projetos_web/HopeMSF/public/system/events/images/000/012/275/original/DSC0222
9.JPG
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] [paperclip] deleting C:/projetos_web/HopeMSF/public/system/events/images/000/012/275/thumb/DSC02229.J
PG
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] [paperclip] deleting C:/projetos_web/HopeMSF/public/system/events/images/000/012/275/medium/DSC02229.
JPG
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] ←[1m←[35mCACHE (0.0ms)←[0m SELECT 1 FROM `events` WHERE (`events`.`visit_id` = BINARY 16195 AND `e
vents`.`id` != 12275 AND `events`.`time_position` = 0) LIMIT 1
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] Command :: identify -format %wx%h “C:/Users/Andreza/AppData/Local/Temp/DSC0222920121212-984-1awaabk.J
PG[0]”
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] Command :: identify -format %m “C:/Users/Andreza/AppData/Local/Temp/DSC0222920121212-984-1awaabk.JPG[
0]”
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] Command :: identify -format %m “C:/Users/Andreza/AppData/Local/Temp/DSC0222920121212-984-1awaabk.JPG[
0]”
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] Command :: convert “C:/Users/Andreza/AppData/Local/Temp/DSC0222920121212-984-1awaabk.JPG[0]” -auto-or
ient -resize “150×150>” “C:/Users/Andreza/AppData/Local/Temp/DSC0222920121212-984-1awaabk20121212-984-1bl833n”
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] Command :: convert “C:/Users/Andreza/AppData/Local/Temp/DSC0222920121212-984-1awaabk20121212-984-1bl8
33n” -rotate -90 “C:/Users/Andreza/AppData/Local/Temp/DSC0222920121212-984-1awaabk20121212-984-1bl833n20121212-984-1xxunvq”
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] Command :: file -b –mime “C:/Users/Andreza/AppData/Local/Temp/DSC0222920121212-984-1awaabk20121212-9
84-1bl833n20121212-984-1xxunvq”
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] [paperclip] Error while determining content type: Cocaine::CommandNotFoundError
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] Command :: identify -format %wx%h “C:/Users/Andreza/AppData/Local/Temp/DSC0222920121212-984-1awaabk.J
PG[0]”
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] Command :: identify -format %m “C:/Users/Andreza/AppData/Local/Temp/DSC0222920121212-984-1awaabk.JPG[
0]”
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] Command :: identify -format %m “C:/Users/Andreza/AppData/Local/Temp/DSC0222920121212-984-1awaabk.JPG[
0]”
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] Command :: convert “C:/Users/Andreza/AppData/Local/Temp/DSC0222920121212-984-1awaabk.JPG[0]” -auto-or
ient -resize “640×640>” “C:/Users/Andreza/AppData/Local/Temp/DSC0222920121212-984-1awaabk20121212-984-1tyc0gx”
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] [paperclip] An error was received while processing: #
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] [paperclip] deleting C:/projetos_web/HopeMSF/public/system/events/images/000/012/275/original/DSC0222
9.JPG
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] [paperclip] deleting C:/projetos_web/HopeMSF/public/system/events/images/000/012/275/thumb/DSC02229.J
PG
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] [paperclip] deleting C:/projetos_web/HopeMSF/public/system/events/images/000/012/275/medium/DSC02229.
JPG
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] ←[1m←[36mCACHE (0.0ms)←[0m ←[1mSELECT 1 FROM `events` WHERE (`events`.`visit_id` = BINARY 16195 AN
D `events`.`id` != 12275 AND `events`.`time_position` = 0) LIMIT 1←[0m
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] ←[1m←[35m (22.0ms)←[0m ROLLBACK
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] ←[1m←[36mStatus Load (45.0ms)←[0m ←[1mSELECT `status`.* FROM `status` WHERE `status`.`code` = 6 LI
MIT 1←[0m
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] Rendered master/visits/_image_event.html.erb (300.0ms)
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] Rendered master/visits/rotate_image_event.js.erb (407.0ms)
[2c89acb77d62888a3a021078684976c6] [127.0.0.1] [2012-12-12 12:03:41] Completed 200 OK in 127292ms (Views: 615.0ms | ActiveRecord: 988.1ms)

LEAVE A COMMENT

theme by teslathemes