module FileColumn # :nodoc: class BaseUploadedFile # :nodoc: def transform_with_magick if needs_transform? begin img = ::Magick::Image::read(absolute_path).first rescue ::Magick::ImageMagickError if options[:magick][:image_required] @magick_errors ||= [] @magick_errors << "invalid image" end return end if options[:magick][:versions] options[:magick][:versions].each_pair do |version, version_options| next if version_options[:lazy] dirname = version_options[:name] FileUtils.mkdir_p File.join(@dir, dirname) transform_image(img, version_options, absolute_path(dirname)) end end if options[:magick][:size] or options[:magick][:crop] or options[:magick][:transformation] or options[:magick][:attributes] transform_image(img, options[:magick], absolute_path) end GC.start end end def create_magick_version_if_needed(version) # RMagick might not have been loaded so far. # We do not want to require it on every call of this method # as this might be fairly expensive, so we just try if ::Magick # exists and require it if not. begin ::Magick rescue NameError require 'RMagick' end if version.is_a?(Symbol) version_options = options[:magick][:versions][version] else version_options = MagickExtension::process_options(version) end unless File.exists?(absolute_path(version_options[:name])) begin img = ::Magick::Image::read(absolute_path).first rescue ::Magick::ImageMagickError # we might be called directly from the view here # so we just return nil if we cannot load the image return nil end dirname = version_options[:name] FileUtils.mkdir_p File.join(@dir, dirname) transform_image(img, version_options, absolute_path(dirname)) end version_options[:name] end attr_reader :magick_errors def has_magick_errors? @magick_errors and !@magick_errors.empty? end private def needs_transform? options[:magick] and just_uploaded? and (options[:magick][:size] or options[:magick][:versions] or options[:magick][:transformation] or options[:magick][:attributes]) end def transform_image(img, img_options, dest_path) begin if img_options[:transformation] if img_options[:transformation].is_a?(Symbol) img = @instance.send(img_options[:transformation], img) else img = img_options[:transformation].call(img) end end if img_options[:crop] dx, dy = img_options[:crop].split(':').map { |x| x.to_f } w, h = (img.rows * dx / dy), (img.columns * dy / dx) img = img.crop(::Magick::CenterGravity, [img.columns, w].min, [img.rows, h].min, true) end if img_options[:size] img = img.change_geometry(img_options[:size]) do |c, r, i| i.resize(c, r) end end if img_options[:letterbox] w, h = img_options[:letterbox].split('x').map { |x| x.to_f } img = img.crop((img.columns/2 - w/2), (img.rows/2 - h/2), w, h) end ensure img.write(dest_path) do if img_options[:attributes] img_options[:attributes].each_pair do |property, value| self.send "#{property}=", value end end end File.chmod options[:permissions], dest_path end end end # If you are using file_column to upload images, you can # directly process the images with RMagick, # a ruby extension # for accessing the popular imagemagick libraries. You can find # more information about RMagick at http://rmagick.rubyforge.org. # # You can control what to do by adding a :magick option # to your options hash. All operations are performed immediately # after a new file is assigned to the file_column attribute (i.e., # when a new file has been uploaded). # # == Resizing images # # To resize the uploaded image according to an imagemagick geometry # string, just use the :size option: # # file_column :image, :magick => {:size => "800x600>"} # # If the uploaded file cannot be loaded by RMagick, file_column will # signal a validation error for the corresponding attribute. If you # want to allow non-image files to be uploaded in a column that uses # the :magick option, you can set the :image_required # attribute to +false+: # # file_column :image, :magick => {:size => "800x600>", # :image_required => false } # # == Multiple versions # # You can also create additional versions of your image, for example # thumb-nails, like this: # file_column :image, :magick => {:versions => { # :thumb => {:size => "50x50"}, # :medium => {:size => "640x480>"} # } # # These versions will be stored in separate sub-directories, named like the # symbol you used to identify the version. So in the previous example, the # image versions will be stored in "thumb", "screen" and "widescreen" # directories, resp. # A name different from the symbol can be set via the :name option. # # These versions can be accessed via FileColumnHelper's +url_for_image_column+ # method like this: # # <%= url_for_image_column "entry", "image", :thumb %> # # == Cropping images # # If you wish to crop your images with a size ratio before scaling # them according to your version geometry, you can use the :crop directive. # file_column :image, :magick => {:versions => { # :square => {:crop => "1:1", :size => "50x50", :name => "thumb"}, # :screen => {:crop => "4:3", :size => "640x480>"}, # :widescreen => {:crop => "16:9", :size => "640x360!"}, # } # } # # == Custom attributes # # To change some of the image properties like compression level before they # are saved you can set the :attributes option. # For a list of available attributes go to http://www.simplesystems.org/RMagick/doc/info.html # # file_column :image, :magick => { :attributes => { :quality => 30 } } # # == Custom transformations # # To perform custom transformations on uploaded images, you can pass a # callback to file_column: # file_column :image, :magick => # Proc.new { |image| image.quantize(256, Magick::GRAYColorspace) } # # The callback you give, receives one argument, which is an instance # of Magick::Image, the RMagick image class. It should return a transformed # image. Instead of passing a Proc object, you can also give a # Symbol, the name of an instance method of your model. # # Custom transformations can be combined via the standard :size and :crop # features, by using the :transformation option: # file_column :image, :magick => { # :transformation => Proc.new { |image| ... }, # :size => "640x480" # } # # In this case, the standard resizing operations will be performed after the # custom transformation. # # Of course, custom transformations can be used in versions, as well. # # Note: You'll need the # RMagick extension being installed in order to use file_column's # imagemagick integration. module MagickExtension def self.file_column(klass, attr, options) # :nodoc: require 'RMagick' options[:magick] = process_options(options[:magick],false) if options[:magick] if options[:magick][:versions] options[:magick][:versions].each_pair do |name, value| options[:magick][:versions][name] = process_options(value, name.to_s) end end state_method = "#{attr}_state".to_sym after_assign_method = "#{attr}_magick_after_assign".to_sym klass.send(:define_method, after_assign_method) do self.send(state_method).transform_with_magick end options[:after_upload] ||= [] options[:after_upload] << after_assign_method klass.validate do |record| state = record.send(state_method) if state.has_magick_errors? state.magick_errors.each do |error| record.errors.add attr, error end end end end def self.process_options(options,create_name=true) case options when String then options = {:size => options} when Proc, Symbol then options = {:transformation => options } end if options[:geometry] options[:size] = options.delete(:geometry) end options[:image_required] = true unless options.key?(:image_required) if options[:name].nil? and create_name if create_name == true hash = 0 for key in [:size, :crop] hash = hash ^ options[key].hash if options[key] end options[:name] = hash.abs.to_s(36) else options[:name] = create_name end end options end end end