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.
- data/CHANGELOG +69 -0
- data/Gemfile +10 -0
- data/README +54 -0
- data/Rakefile +50 -0
- data/TODO +6 -0
- data/VERSION +1 -0
- data/init.rb +12 -0
- data/lib/file_column.rb +721 -0
- data/lib/file_column_helper.rb +150 -0
- data/lib/file_compat.rb +28 -0
- data/lib/magick_file_column.rb +265 -0
- data/lib/rails_file_column.rb +19 -0
- data/lib/test_case.rb +124 -0
- data/lib/validations.rb +112 -0
- data/test/abstract_unit.rb +63 -0
- data/test/connection.rb +17 -0
- data/test/file_column_helper_test.rb +97 -0
- data/test/file_column_test.rb +650 -0
- data/test/fixtures/entry.rb +32 -0
- data/test/fixtures/invalid-image.jpg +1 -0
- data/test/fixtures/kerb.jpg +0 -0
- data/test/fixtures/mysql.sql +25 -0
- data/test/fixtures/schema.rb +10 -0
- data/test/fixtures/skanthak.png +0 -0
- data/test/magick_test.rb +380 -0
- data/test/magick_view_only_test.rb +21 -0
- metadata +86 -0
@@ -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
|
data/lib/file_compat.rb
ADDED
@@ -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
|