file_column 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,150 @@
1
+ # This module contains helper methods for displaying and uploading files
2
+ # for attributes created by +FileColumn+'s +file_column+ method. It will be
3
+ # automatically included into ActionView::Base, thereby making this module's
4
+ # methods available in all your views.
5
+ module FileColumnHelper
6
+
7
+ # Use this helper to create an upload field for a file_column attribute. This will generate
8
+ # an additional hidden field to keep uploaded files during form-redisplays. For example,
9
+ # when called with
10
+ #
11
+ # <%= file_column_field("entry", "image") %>
12
+ #
13
+ # the following HTML will be generated (assuming the form is redisplayed and something has
14
+ # already been uploaded):
15
+ #
16
+ # <input type="hidden" name="entry[image_temp]" value="..." />
17
+ # <input type="file" name="entry[image]" />
18
+ #
19
+ # You can use the +option+ argument to pass additional options to the file-field tag.
20
+ #
21
+ # Be sure to set the enclosing form's encoding to 'multipart/form-data', by
22
+ # using something like this:
23
+ #
24
+ # <%= form_tag {:action => "create", ...}, :multipart => true %>
25
+ def file_column_field(object, method, options={})
26
+ result = ActionView::Helpers::InstanceTag.new(object.dup, method.to_s+"_temp", self).to_input_field_tag("hidden", {})
27
+ result << ActionView::Helpers::InstanceTag.new(object.dup, method, self).to_input_field_tag("file", options)
28
+ end
29
+
30
+ # Creates an URL where an uploaded file can be accessed. When called for an Entry object with
31
+ # id 42 (stored in <tt>@entry</tt>) like this
32
+ #
33
+ # <%= url_for_file_column(@entry, "image")
34
+ #
35
+ # the following URL will be produced, assuming the file "test.png" has been stored in
36
+ # the "image"-column of an Entry object stored in <tt>@entry</tt>:
37
+ #
38
+ # /entry/image/42/test.png
39
+ #
40
+ # This will produce a valid URL even for temporary uploaded files, e.g. files where the object
41
+ # they are belonging to has not been saved in the database yet.
42
+ #
43
+ # The URL produces, although starting with a slash, will be relative
44
+ # to your app's root. If you pass it to one rails' +image_tag+
45
+ # helper, rails will properly convert it to an absolute
46
+ # URL. However, this will not be the case, if you create a link with
47
+ # the +link_to+ helper. In this case, you can pass <tt>:absolute =>
48
+ # true</tt> to +options+, which will make sure, the generated URL is
49
+ # absolute on your server. Examples:
50
+ #
51
+ # <%= image_tag url_for_file_column(@entry, "image") %>
52
+ # <%= link_to "Download", url_for_file_column(@entry, "image", :absolute => true) %>
53
+ #
54
+ # If there is currently no uploaded file stored in the object's column this method will
55
+ # return +nil+.
56
+ def url_for_file_column(object, method, options=nil)
57
+ case object
58
+ when String, Symbol
59
+ object = instance_variable_get("@#{object.to_s}")
60
+ end
61
+
62
+ # parse options
63
+ subdir = nil
64
+ absolute = false
65
+ if options
66
+ case options
67
+ when Hash
68
+ subdir = options[:subdir]
69
+ absolute = options[:absolute]
70
+ when String, Symbol
71
+ subdir = options
72
+ end
73
+ end
74
+
75
+ relative_path = object.send("#{method}_relative_path", subdir)
76
+ return nil unless relative_path
77
+
78
+ url = ""
79
+ url << ActionController::Base.relative_url_root.to_s if absolute
80
+ url << "/"
81
+ url << object.send("#{method}_options")[:base_url] << "/"
82
+ url << relative_path
83
+ end
84
+
85
+ # Same as +url_for_file_colum+ but allows you to access different versions
86
+ # of the image that have been processed by RMagick.
87
+ #
88
+ # If your +options+ parameter is non-nil this will
89
+ # access a different version of an image that will be produced by
90
+ # RMagick. You can use the following types for +options+:
91
+ #
92
+ # * a <tt>:symbol</tt> will select a version defined in the model
93
+ # via FileColumn::Magick's <tt>:versions</tt> feature.
94
+ # * a <tt>geometry_string</tt> will dynamically create an
95
+ # image resized as specified by <tt>geometry_string</tt>. The image will
96
+ # be stored so that it does not have to be recomputed the next time the
97
+ # same version string is used.
98
+ # * <tt>some_hash</tt> will dynamically create an image
99
+ # that is created according to the options in <tt>some_hash</tt>. This
100
+ # accepts exactly the same options as Magick's version feature.
101
+ #
102
+ # The version produced by RMagick will be stored in a special sub-directory.
103
+ # The directory's name will be derived from the options you specified
104
+ # (via a hash function) but if you want
105
+ # to set it yourself, you can use the <tt>:name => name</tt> option.
106
+ #
107
+ # Examples:
108
+ #
109
+ # <%= url_for_image_column @entry, "image", "640x480" %>
110
+ #
111
+ # will produce an URL like this
112
+ #
113
+ # /entry/image/42/bdn19n/filename.jpg
114
+ # # "640x480".hash.abs.to_s(36) == "bdn19n"
115
+ #
116
+ # and
117
+ #
118
+ # <%= url_for_image_column @entry, "image",
119
+ # :size => "50x50", :crop => "1:1", :name => "thumb" %>
120
+ #
121
+ # will produce something like this:
122
+ #
123
+ # /entry/image/42/thumb/filename.jpg
124
+ #
125
+ # Hint: If you are using the same geometry string / options hash multiple times, you should
126
+ # define it in a helper to stay with DRY. Another option is to define it in the model via
127
+ # FileColumn::Magick's <tt>:versions</tt> feature and then refer to it via a symbol.
128
+ #
129
+ # The URL produced by this method is relative to your application's root URL,
130
+ # although it will start with a slash.
131
+ # If you pass this URL to rails' +image_tag+ helper, it will be converted to an
132
+ # absolute URL automatically.
133
+ # If there is currently no image uploaded, or there is a problem while loading
134
+ # the image this method will return +nil+.
135
+ def url_for_image_column(object, method, options=nil)
136
+ case object
137
+ when String, Symbol
138
+ object = instance_variable_get("@#{object.to_s}")
139
+ end
140
+ subdir = nil
141
+ if options
142
+ subdir = object.send("#{method}_state").create_magick_version_if_needed(options)
143
+ end
144
+ if subdir.nil?
145
+ nil
146
+ else
147
+ url_for_file_column(object, method, subdir)
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,28 @@
1
+ module FileColumn
2
+
3
+ # This bit of code allows you to pass regular old files to
4
+ # file_column. file_column depends on a few extra methods that the
5
+ # CGI uploaded file class adds. We will add the equivalent methods
6
+ # to file objects if necessary by extending them with this module. This
7
+ # avoids opening up the standard File class which might result in
8
+ # naming conflicts.
9
+
10
+ module FileCompat # :nodoc:
11
+ def original_filename
12
+ File.basename(path)
13
+ end
14
+
15
+ def size
16
+ File.size(path)
17
+ end
18
+
19
+ def local_path
20
+ path
21
+ end
22
+
23
+ def content_type
24
+ nil
25
+ end
26
+ end
27
+ end
28
+
@@ -0,0 +1,265 @@
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
+
100
+ if img_options[:letterbox]
101
+ w, h = img_options[:letterbox].split('x').map { |x| x.to_f }
102
+ img = img.crop((img.columns/2 - w/2), (img.rows/2 - h/2), w, h)
103
+ end
104
+ ensure
105
+ img.write(dest_path) do
106
+ if img_options[:attributes]
107
+ img_options[:attributes].each_pair do |property, value|
108
+ self.send "#{property}=", value
109
+ end
110
+ end
111
+ end
112
+ File.chmod options[:permissions], dest_path
113
+ end
114
+ end
115
+ end
116
+
117
+ # If you are using file_column to upload images, you can
118
+ # directly process the images with RMagick,
119
+ # a ruby extension
120
+ # for accessing the popular imagemagick libraries. You can find
121
+ # more information about RMagick at http://rmagick.rubyforge.org.
122
+ #
123
+ # You can control what to do by adding a <tt>:magick</tt> option
124
+ # to your options hash. All operations are performed immediately
125
+ # after a new file is assigned to the file_column attribute (i.e.,
126
+ # when a new file has been uploaded).
127
+ #
128
+ # == Resizing images
129
+ #
130
+ # To resize the uploaded image according to an imagemagick geometry
131
+ # string, just use the <tt>:size</tt> option:
132
+ #
133
+ # file_column :image, :magick => {:size => "800x600>"}
134
+ #
135
+ # If the uploaded file cannot be loaded by RMagick, file_column will
136
+ # signal a validation error for the corresponding attribute. If you
137
+ # want to allow non-image files to be uploaded in a column that uses
138
+ # the <tt>:magick</tt> option, you can set the <tt>:image_required</tt>
139
+ # attribute to +false+:
140
+ #
141
+ # file_column :image, :magick => {:size => "800x600>",
142
+ # :image_required => false }
143
+ #
144
+ # == Multiple versions
145
+ #
146
+ # You can also create additional versions of your image, for example
147
+ # thumb-nails, like this:
148
+ # file_column :image, :magick => {:versions => {
149
+ # :thumb => {:size => "50x50"},
150
+ # :medium => {:size => "640x480>"}
151
+ # }
152
+ #
153
+ # These versions will be stored in separate sub-directories, named like the
154
+ # symbol you used to identify the version. So in the previous example, the
155
+ # image versions will be stored in "thumb", "screen" and "widescreen"
156
+ # directories, resp.
157
+ # A name different from the symbol can be set via the <tt>:name</tt> option.
158
+ #
159
+ # These versions can be accessed via FileColumnHelper's +url_for_image_column+
160
+ # method like this:
161
+ #
162
+ # <%= url_for_image_column "entry", "image", :thumb %>
163
+ #
164
+ # == Cropping images
165
+ #
166
+ # If you wish to crop your images with a size ratio before scaling
167
+ # them according to your version geometry, you can use the :crop directive.
168
+ # file_column :image, :magick => {:versions => {
169
+ # :square => {:crop => "1:1", :size => "50x50", :name => "thumb"},
170
+ # :screen => {:crop => "4:3", :size => "640x480>"},
171
+ # :widescreen => {:crop => "16:9", :size => "640x360!"},
172
+ # }
173
+ # }
174
+ #
175
+ # == Custom attributes
176
+ #
177
+ # To change some of the image properties like compression level before they
178
+ # are saved you can set the <tt>:attributes</tt> option.
179
+ # For a list of available attributes go to http://www.simplesystems.org/RMagick/doc/info.html
180
+ #
181
+ # file_column :image, :magick => { :attributes => { :quality => 30 } }
182
+ #
183
+ # == Custom transformations
184
+ #
185
+ # To perform custom transformations on uploaded images, you can pass a
186
+ # callback to file_column:
187
+ # file_column :image, :magick =>
188
+ # Proc.new { |image| image.quantize(256, Magick::GRAYColorspace) }
189
+ #
190
+ # The callback you give, receives one argument, which is an instance
191
+ # of Magick::Image, the RMagick image class. It should return a transformed
192
+ # image. Instead of passing a <tt>Proc</tt> object, you can also give a
193
+ # <tt>Symbol</tt>, the name of an instance method of your model.
194
+ #
195
+ # Custom transformations can be combined via the standard :size and :crop
196
+ # features, by using the :transformation option:
197
+ # file_column :image, :magick => {
198
+ # :transformation => Proc.new { |image| ... },
199
+ # :size => "640x480"
200
+ # }
201
+ #
202
+ # In this case, the standard resizing operations will be performed after the
203
+ # custom transformation.
204
+ #
205
+ # Of course, custom transformations can be used in versions, as well.
206
+ #
207
+ # <b>Note:</b> You'll need the
208
+ # RMagick extension being installed in order to use file_column's
209
+ # imagemagick integration.
210
+ module MagickExtension
211
+
212
+ def self.file_column(klass, attr, options) # :nodoc:
213
+ require 'RMagick'
214
+ options[:magick] = process_options(options[:magick],false) if options[:magick]
215
+ if options[:magick][:versions]
216
+ options[:magick][:versions].each_pair do |name, value|
217
+ options[:magick][:versions][name] = process_options(value, name.to_s)
218
+ end
219
+ end
220
+ state_method = "#{attr}_state".to_sym
221
+ after_assign_method = "#{attr}_magick_after_assign".to_sym
222
+
223
+ klass.send(:define_method, after_assign_method) do
224
+ self.send(state_method).transform_with_magick
225
+ end
226
+
227
+ options[:after_upload] ||= []
228
+ options[:after_upload] << after_assign_method
229
+
230
+ klass.validate do |record|
231
+ state = record.send(state_method)
232
+ if state.has_magick_errors?
233
+ state.magick_errors.each do |error|
234
+ record.errors.add attr, error
235
+ end
236
+ end
237
+ end
238
+ end
239
+
240
+
241
+ def self.process_options(options,create_name=true)
242
+ case options
243
+ when String then options = {:size => options}
244
+ when Proc, Symbol then options = {:transformation => options }
245
+ end
246
+ if options[:geometry]
247
+ options[:size] = options.delete(:geometry)
248
+ end
249
+ options[:image_required] = true unless options.key?(:image_required)
250
+ if options[:name].nil? and create_name
251
+ if create_name == true
252
+ hash = 0
253
+ for key in [:size, :crop]
254
+ hash = hash ^ options[key].hash if options[key]
255
+ end
256
+ options[:name] = hash.abs.to_s(36)
257
+ else
258
+ options[:name] = create_name
259
+ end
260
+ end
261
+ options
262
+ end
263
+
264
+ end
265
+ end
@@ -0,0 +1,19 @@
1
+ # require this file from your "config/environment.rb" (after rails has been loaded)
2
+ # to integrate the file_column extension into rails.
3
+
4
+ require 'file_column'
5
+ require 'file_column_helper'
6
+
7
+
8
+ module ActiveRecord # :nodoc:
9
+ class Base # :nodoc:
10
+ # make file_column method available in all active record decendants
11
+ include FileColumn
12
+ end
13
+ end
14
+
15
+ module ActionView # :nodoc:
16
+ class Base # :nodoc:
17
+ include FileColumnHelper
18
+ end
19
+ end