]> git.openstreetmap.org Git - rails.git/blob - vendor/plugins/file_column/lib/magick_file_column.rb
Merge branch 'master' into openstreetbugs
[rails.git] / vendor / plugins / file_column / lib / magick_file_column.rb
1 module FileColumn # :nodoc:
2   
3   class BaseUploadedFile # :nodoc:
4     def transform_with_magick
5       if needs_transform?
6         begin
7           img = ::Magick::Image::read(absolute_path).first
8         rescue ::Magick::ImageMagickError
9           if options[:magick][:image_required]
10             @magick_errors ||= []
11             @magick_errors << "invalid image"
12           end
13           return
14         end
15         
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))
22           end
23         end
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)
26         end
27
28         GC.start
29       end
30     end
31
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.
37       begin 
38         ::Magick 
39       rescue NameError
40         require 'RMagick'
41       end
42
43       if version.is_a?(Symbol)
44         version_options = options[:magick][:versions][version]
45       else
46         version_options = MagickExtension::process_options(version)
47       end
48
49       unless File.exists?(absolute_path(version_options[:name]))
50         begin
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
55           return nil
56         end
57         dirname = version_options[:name]
58         FileUtils.mkdir File.join(@dir, dirname)
59         transform_image(img, version_options, absolute_path(dirname))
60       end
61
62       version_options[:name]
63     end
64
65     attr_reader :magick_errors
66     
67     def has_magick_errors?
68       @magick_errors and !@magick_errors.empty?
69     end
70
71     private
72     
73     def needs_transform?
74       options[:magick] and just_uploaded? and 
75         (options[:magick][:size] or options[:magick][:versions] or options[:magick][:transformation] or options[:magick][:attributes])
76     end
77
78     def transform_image(img, img_options, dest_path)
79       begin
80         if img_options[:transformation]
81           if img_options[:transformation].is_a?(Symbol)
82             img = @instance.send(img_options[:transformation], img)
83           else
84             img = img_options[:transformation].call(img)
85           end
86         end
87         if img_options[:crop]
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)
92         end
93
94         if img_options[:size]
95           img = img.change_geometry(img_options[:size]) do |c, r, i|
96             i.resize(c, r)
97           end
98         end
99       ensure
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
104             end
105           end
106         end
107         File.chmod options[:permissions], dest_path
108       end
109     end
110   end
111
112   # If you are using file_column to upload images, you can
113   # directly process the images with RMagick,
114   # a ruby extension
115   # for accessing the popular imagemagick libraries. You can find
116   # more information about RMagick at http://rmagick.rubyforge.org.
117   #
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).
122   #
123   # == Resizing images
124   #
125   # To resize the uploaded image according to an imagemagick geometry
126   # string, just use the <tt>:size</tt> option:
127   #
128   #    file_column :image, :magick => {:size => "800x600>"}
129   #
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+:
135   #
136   #    file_column :image, :magick => {:size => "800x600>",
137   #                                    :image_required => false }
138   #
139   # == Multiple versions
140   #
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>"}
146   #       }
147   #
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"
151   # directories, resp. 
152   # A name different from the symbol can be set via the <tt>:name</tt> option.
153   #
154   # These versions can be accessed via FileColumnHelper's +url_for_image_column+
155   # method like this:
156   #
157   #    <%= url_for_image_column "entry", "image", :thumb %>
158   #
159   # == Cropping images
160   #
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!"},
167   #       }
168   #    }
169   #
170   # == Custom attributes
171   #
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
175   # 
176   #     file_column :image, :magick => { :attributes => { :quality => 30 } }
177   # 
178   # == Custom transformations
179   #
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) }
184   #
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.
189   #
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| ... },
194   #      :size => "640x480"
195   #    }
196   #
197   # In this case, the standard resizing operations will be performed after the
198   # custom transformation.
199   #
200   # Of course, custom transformations can be used in versions, as well.
201   #
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
206
207     def self.file_column(klass, attr, options) # :nodoc:
208       require 'RMagick'
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)
213         end
214       end
215       state_method = "#{attr}_state".to_sym
216       after_assign_method = "#{attr}_magick_after_assign".to_sym
217       
218       klass.send(:define_method, after_assign_method) do
219         self.send(state_method).transform_with_magick
220       end
221       
222       options[:after_upload] ||= []
223       options[:after_upload] << after_assign_method
224       
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
230           end
231         end
232       end
233     end
234
235     
236     def self.process_options(options,create_name=true)
237       case options
238       when String then options = {:size => options}
239       when Proc, Symbol then options = {:transformation => options }
240       end
241       if options[:geometry]
242         options[:size] = options.delete(:geometry)
243       end
244       options[:image_required] = true unless options.key?(:image_required)
245       if options[:name].nil? and create_name
246         if create_name == true
247           hash = 0
248           for key in [:size, :crop]
249             hash = hash ^ options[key].hash if options[key]
250           end
251           options[:name] = hash.abs.to_s(36)
252         else
253           options[:name] = create_name
254         end
255       end
256       options
257     end
258
259   end
260 end