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