1 module FileColumn # :nodoc:
 
   3   class BaseUploadedFile # :nodoc:
 
   4     def transform_with_magick
 
   7           img = ::Magick::Image::read(absolute_path).first
 
   8         rescue ::Magick::ImageMagickError
 
   9           if options[:magick][:image_required]
 
  11             @magick_errors << "invalid image"
 
  16         if options[:magick][:versions]
 
  17           options[:magick][:versions].each_pair do |version, version_options|
 
  18             next if version_options[:lazy]
 
  19             dirname = version_options[:name]
 
  20             FileUtils.mkdir File.join(@dir, dirname)
 
  21             transform_image(img, version_options, absolute_path(dirname))
 
  24         if options[:magick][:size] or options[:magick][:crop] or options[:magick][:transformation] or options[:magick][:attributes]
 
  25           transform_image(img, options[:magick], absolute_path)
 
  32     def create_magick_version_if_needed(version)
 
  33       # RMagick might not have been loaded so far.
 
  34       # We do not want to require it on every call of this method
 
  35       # as this might be fairly expensive, so we just try if ::Magick
 
  36       # exists and require it if not.
 
  43       if version.is_a?(Symbol)
 
  44         version_options = options[:magick][:versions][version]
 
  46         version_options = MagickExtension::process_options(version)
 
  49       unless File.exists?(absolute_path(version_options[:name]))
 
  51           img = ::Magick::Image::read(absolute_path).first
 
  52         rescue ::Magick::ImageMagickError
 
  53           # we might be called directly from the view here
 
  54           # so we just return nil if we cannot load the image
 
  57         dirname = version_options[:name]
 
  58         FileUtils.mkdir File.join(@dir, dirname)
 
  59         transform_image(img, version_options, absolute_path(dirname))
 
  62       version_options[:name]
 
  65     attr_reader :magick_errors
 
  67     def has_magick_errors?
 
  68       @magick_errors and !@magick_errors.empty?
 
  74       options[:magick] and just_uploaded? and 
 
  75         (options[:magick][:size] or options[:magick][:versions] or options[:magick][:transformation] or options[:magick][:attributes])
 
  78     def transform_image(img, img_options, dest_path)
 
  80         if img_options[:transformation]
 
  81           if img_options[:transformation].is_a?(Symbol)
 
  82             img = @instance.send(img_options[:transformation], img)
 
  84             img = img_options[:transformation].call(img)
 
  88           dx, dy = img_options[:crop].split(':').map { |x| x.to_f }
 
  89           w, h = (img.rows * dx / dy), (img.columns * dy / dx)
 
  90           img = img.crop(::Magick::CenterGravity, [img.columns, w].min, 
 
  91                          [img.rows, h].min, true)
 
  95           img = img.change_geometry(img_options[:size]) do |c, r, i|
 
 100         img.write(dest_path) do
 
 101           if img_options[:attributes]
 
 102             img_options[:attributes].each_pair do |property, value| 
 
 103               self.send "#{property}=", value
 
 107         File.chmod options[:permissions], dest_path
 
 112   # If you are using file_column to upload images, you can
 
 113   # directly process the images with RMagick,
 
 115   # for accessing the popular imagemagick libraries. You can find
 
 116   # more information about RMagick at http://rmagick.rubyforge.org.
 
 118   # You can control what to do by adding a <tt>:magick</tt> option
 
 119   # to your options hash. All operations are performed immediately
 
 120   # after a new file is assigned to the file_column attribute (i.e.,
 
 121   # when a new file has been uploaded).
 
 125   # To resize the uploaded image according to an imagemagick geometry
 
 126   # string, just use the <tt>:size</tt> option:
 
 128   #    file_column :image, :magick => {:size => "800x600>"}
 
 130   # If the uploaded file cannot be loaded by RMagick, file_column will
 
 131   # signal a validation error for the corresponding attribute. If you
 
 132   # want to allow non-image files to be uploaded in a column that uses
 
 133   # the <tt>:magick</tt> option, you can set the <tt>:image_required</tt>
 
 134   # attribute to +false+:
 
 136   #    file_column :image, :magick => {:size => "800x600>",
 
 137   #                                    :image_required => false }
 
 139   # == Multiple versions
 
 141   # You can also create additional versions of your image, for example
 
 142   # thumb-nails, like this:
 
 143   #    file_column :image, :magick => {:versions => {
 
 144   #         :thumb => {:size => "50x50"},
 
 145   #         :medium => {:size => "640x480>"}
 
 148   # These versions will be stored in separate sub-directories, named like the
 
 149   # symbol you used to identify the version. So in the previous example, the
 
 150   # image versions will be stored in "thumb", "screen" and "widescreen"
 
 152   # A name different from the symbol can be set via the <tt>:name</tt> option.
 
 154   # These versions can be accessed via FileColumnHelper's +url_for_image_column+
 
 157   #    <%= url_for_image_column "entry", "image", :thumb %>
 
 161   # If you wish to crop your images with a size ratio before scaling
 
 162   # them according to your version geometry, you can use the :crop directive.
 
 163   #    file_column :image, :magick => {:versions => {
 
 164   #         :square => {:crop => "1:1", :size => "50x50", :name => "thumb"},
 
 165   #         :screen => {:crop => "4:3", :size => "640x480>"},
 
 166   #         :widescreen => {:crop => "16:9", :size => "640x360!"},
 
 170   # == Custom attributes
 
 172   # To change some of the image properties like compression level before they
 
 173   # are saved you can set the <tt>:attributes</tt> option.
 
 174   # For a list of available attributes go to http://www.simplesystems.org/RMagick/doc/info.html
 
 176   #     file_column :image, :magick => { :attributes => { :quality => 30 } }
 
 178   # == Custom transformations
 
 180   # To perform custom transformations on uploaded images, you can pass a
 
 181   # callback to file_column:
 
 182   #    file_column :image, :magick => 
 
 183   #       Proc.new { |image| image.quantize(256, Magick::GRAYColorspace) }
 
 185   # The callback you give, receives one argument, which is an instance
 
 186   # of Magick::Image, the RMagick image class. It should return a transformed
 
 187   # image. Instead of passing a <tt>Proc</tt> object, you can also give a
 
 188   # <tt>Symbol</tt>, the name of an instance method of your model.
 
 190   # Custom transformations can be combined via the standard :size and :crop
 
 191   # features, by using the :transformation option:
 
 192   #   file_column :image, :magick => {
 
 193   #      :transformation => Proc.new { |image| ... },
 
 197   # In this case, the standard resizing operations will be performed after the
 
 198   # custom transformation.
 
 200   # Of course, custom transformations can be used in versions, as well.
 
 202   # <b>Note:</b> You'll need the
 
 203   # RMagick extension being installed  in order to use file_column's
 
 204   # imagemagick integration.
 
 205   module MagickExtension
 
 207     def self.file_column(klass, attr, options) # :nodoc:
 
 209       options[:magick] = process_options(options[:magick],false) if options[:magick]
 
 210       if options[:magick][:versions]
 
 211         options[:magick][:versions].each_pair do |name, value|
 
 212           options[:magick][:versions][name] = process_options(value, name.to_s)
 
 215       state_method = "#{attr}_state".to_sym
 
 216       after_assign_method = "#{attr}_magick_after_assign".to_sym
 
 218       klass.send(:define_method, after_assign_method) do
 
 219         self.send(state_method).transform_with_magick
 
 222       options[:after_upload] ||= []
 
 223       options[:after_upload] << after_assign_method
 
 225       klass.validate do |record|
 
 226         state = record.send(state_method)
 
 227         if state.has_magick_errors?
 
 228           state.magick_errors.each do |error|
 
 229             record.errors.add attr, error
 
 236     def self.process_options(options,create_name=true)
 
 238       when String then options = {:size => options}
 
 239       when Proc, Symbol then options = {:transformation => options }
 
 241       if options[:geometry]
 
 242         options[:size] = options.delete(:geometry)
 
 244       options[:image_required] = true unless options.key?(:image_required)
 
 245       if options[:name].nil? and create_name
 
 246         if create_name == true
 
 248           for key in [:size, :crop]
 
 249             hash = hash ^ options[key].hash if options[key]
 
 251           options[:name] = hash.abs.to_s(36)
 
 253           options[:name] = create_name