has_image 0.3.0 → 0.4.0
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.md +100 -0
- data/MIT-LICENSE +15 -17
- data/README.md +159 -0
- data/Rakefile +6 -38
- data/lib/has_image.rb +31 -12
- data/lib/has_image/processor.rb +17 -18
- data/lib/has_image/railtie.rb +7 -0
- data/lib/has_image/storage.rb +31 -31
- data/lib/has_image/version.rb +3 -0
- data/lib/has_image/view_helpers.rb +3 -6
- data/{test_rails → test}/complex_pic_test.rb +10 -5
- data/{test_rails → test}/fixtures/bad_image.jpg +0 -0
- data/{test_rails → test}/fixtures/image.jpg +0 -0
- data/{test_rails → test}/fixtures/image.png +0 -0
- data/{test_rails → test}/pic_test.rb +4 -3
- data/test/processor_test.rb +9 -10
- data/test/storage_test.rb +28 -29
- data/test/test_helper.rb +47 -0
- metadata +92 -35
- data/CHANGELOG +0 -72
- data/FAQ +0 -25
- data/README.textile +0 -193
- data/init.rb +0 -1
- data/test_rails/database.yml +0 -3
- data/test_rails/schema.rb +0 -15
- data/test_rails/test_helper.rb +0 -52
data/lib/has_image/processor.rb
CHANGED
@@ -1,21 +1,21 @@
|
|
1
1
|
require 'mini_magick'
|
2
2
|
|
3
3
|
module HasImage
|
4
|
-
|
4
|
+
|
5
5
|
# Image processing functionality for the HasImage gem.
|
6
6
|
class Processor
|
7
|
-
|
7
|
+
|
8
8
|
attr_accessor :options
|
9
|
-
|
9
|
+
|
10
10
|
class << self
|
11
|
-
|
11
|
+
|
12
12
|
# The form of an {extended geometry string}[http://www.imagemagick.org/script/command-line-options.php?#resize] is:
|
13
13
|
#
|
14
14
|
# <width>x<height>{+-}<xoffset>{+-}<yoffset>{%}{!}{<}{>}
|
15
15
|
def geometry_string_valid?(string)
|
16
|
-
string =~ /\A[\d]*x[\d]*([+-][0-9][+-][0-9])?[%@!<>^]?\
|
16
|
+
string.nil? || string =~ /\A[\d]*x[\d]*([+-][0-9][+-][0-9])?[%@!<>^]?\z/
|
17
17
|
end
|
18
|
-
|
18
|
+
|
19
19
|
# Arg should be either a file or a path. This runs ImageMagick's
|
20
20
|
# "identify" command and looks for an exit status indicating an error.
|
21
21
|
# If there is no error, then ImageMagick has identified the file as
|
@@ -34,22 +34,22 @@ module HasImage
|
|
34
34
|
$? == 0
|
35
35
|
end
|
36
36
|
end
|
37
|
-
|
37
|
+
|
38
38
|
end
|
39
39
|
|
40
40
|
# The constuctor should be invoked with the options set by has_image.
|
41
41
|
def initialize(options) # :nodoc:
|
42
42
|
@options = options
|
43
43
|
end
|
44
|
-
|
44
|
+
|
45
45
|
# Creates the resized image, and transforms it to the desired output
|
46
|
-
# format if necessary.
|
47
|
-
#
|
46
|
+
# format if necessary.
|
47
|
+
#
|
48
48
|
# +size+ should be a valid ImageMagick {geometry string}[http://www.imagemagick.org/script/command-line-options.php#resize].
|
49
49
|
# +format+ should be an image format supported by ImageMagick, e.g. "PNG", "JPEG"
|
50
50
|
# If a block is given, it yields the processed image file as a file-like object using IO#read.
|
51
51
|
def process(file, size = options[:resize_to], format = options[:convert_to])
|
52
|
-
unless
|
52
|
+
unless Processor.geometry_string_valid?(size)
|
53
53
|
raise InvalidGeometryError.new('"%s" is not a valid ImageMagick geometry string' % size)
|
54
54
|
end
|
55
55
|
with_image(file) do |image|
|
@@ -60,12 +60,12 @@ module HasImage
|
|
60
60
|
end
|
61
61
|
end
|
62
62
|
alias_method :resize, :process #Backwards-compat
|
63
|
-
|
63
|
+
|
64
64
|
# Gets the given +dimension+ (width/height) from the image file at +path+.
|
65
65
|
def measure(path, dimension)
|
66
66
|
MiniMagick::Image.from_file(path)[dimension.to_sym]
|
67
67
|
end
|
68
|
-
|
68
|
+
|
69
69
|
private
|
70
70
|
# Operates on the image with MiniMagick. Yields a MiniMagick::Image object.
|
71
71
|
def with_image(file)
|
@@ -75,14 +75,14 @@ module HasImage
|
|
75
75
|
begin
|
76
76
|
image = MiniMagick::Image.from_file(path)
|
77
77
|
yield image
|
78
|
-
rescue MiniMagick::
|
78
|
+
rescue MiniMagick::Invalid
|
79
79
|
raise ProcessorError.new("#{path} doesn't look like an image file.")
|
80
80
|
ensure
|
81
81
|
image.tempfile.close! if defined?(image) && image
|
82
82
|
end
|
83
83
|
end
|
84
84
|
end
|
85
|
-
|
85
|
+
|
86
86
|
# Image resizing is placed in a separate method for easy monkey-patching.
|
87
87
|
# This is intended to be invoked from resize, rather than directly.
|
88
88
|
# By default, the following ImageMagick functionality is invoked:
|
@@ -114,7 +114,6 @@ module HasImage
|
|
114
114
|
def convert_image(image, format=options[:convert_to])
|
115
115
|
image.format(format) unless image[:format] == format
|
116
116
|
end
|
117
|
-
|
118
|
-
end
|
119
117
|
|
120
|
-
end
|
118
|
+
end
|
119
|
+
end
|
data/lib/has_image/storage.rb
CHANGED
@@ -1,23 +1,23 @@
|
|
1
|
-
require '
|
1
|
+
require 'cgi'
|
2
2
|
require 'stringio'
|
3
3
|
require 'fileutils'
|
4
4
|
require 'zlib'
|
5
5
|
|
6
|
-
module HasImage
|
7
|
-
|
6
|
+
module HasImage
|
7
|
+
|
8
8
|
# Filesystem storage for the HasImage gem. The methods that HasImage inserts
|
9
9
|
# into ActiveRecord models only depend on the public methods in this class,
|
10
10
|
# so it should be reasonably straightforward to implement a different
|
11
11
|
# storage mechanism for Amazon AWS, Photobucket, DBFile, SFTP, or whatever
|
12
|
-
# you want.
|
12
|
+
# you want.
|
13
13
|
class Storage
|
14
14
|
class_inheritable_accessor :thumbnail_separator
|
15
15
|
write_inheritable_attribute :thumbnail_separator, '_'
|
16
|
-
|
16
|
+
|
17
17
|
attr_accessor :image_data, :options, :temp_file
|
18
18
|
|
19
19
|
class << self
|
20
|
-
|
20
|
+
|
21
21
|
# {Jamis Buck's well known
|
22
22
|
# solution}[http://www.37signals.com/svn/archives2/id_partitioning.php]
|
23
23
|
# to this problem fails with high ids, such as those created by
|
@@ -29,17 +29,17 @@ module HasImage
|
|
29
29
|
def partitioned_path(id, *args)
|
30
30
|
["%04d" % ((id.to_i / 1e4) % 1e4), "%04d" % (id.to_i % 1e4)].concat(args)
|
31
31
|
end
|
32
|
-
|
32
|
+
|
33
33
|
def id_from_partitioned_path(partitioned_path)
|
34
34
|
partitioned_path.join.to_i
|
35
35
|
end
|
36
|
-
|
36
|
+
|
37
37
|
def id_from_path(path)
|
38
38
|
path = path.split('/') if path.is_a?(String)
|
39
39
|
path_partitions = 2
|
40
40
|
id_from_partitioned_path(path.first(path_partitions))
|
41
41
|
end
|
42
|
-
|
42
|
+
|
43
43
|
# By default, simply accepts and returns the id of the object. This is
|
44
44
|
# here to allow you to monkey patch this method, for example, if you
|
45
45
|
# wish instead to generate and return a UUID.
|
@@ -72,13 +72,13 @@ module HasImage
|
|
72
72
|
@temp_file.open if @temp_file.closed?
|
73
73
|
@temp_file.size < options[:min_size]
|
74
74
|
end
|
75
|
-
|
75
|
+
|
76
76
|
# Is uploaded file larger than the allowed maximum?
|
77
77
|
def image_too_big?
|
78
78
|
@temp_file.open if @temp_file.closed?
|
79
79
|
@temp_file.size > options[:max_size]
|
80
80
|
end
|
81
|
-
|
81
|
+
|
82
82
|
# Invokes the processor to resize the image(s) and the installs them to
|
83
83
|
# the appropriate directory.
|
84
84
|
def install_images(object)
|
@@ -86,16 +86,16 @@ module HasImage
|
|
86
86
|
install_main_image(object.has_image_id, generated_name)
|
87
87
|
generate_thumbnails(object.has_image_id, generated_name) if thumbnails_needed?
|
88
88
|
return generated_name
|
89
|
-
ensure
|
89
|
+
ensure
|
90
90
|
@temp_file.close! if !@temp_file.closed?
|
91
91
|
@temp_file = nil
|
92
92
|
end
|
93
|
-
|
93
|
+
|
94
94
|
# Measures the given dimension using the processor
|
95
95
|
def measure(path, dimension)
|
96
96
|
processor.measure(path, dimension)
|
97
97
|
end
|
98
|
-
|
98
|
+
|
99
99
|
# Gets the "web" path for an image. For example:
|
100
100
|
#
|
101
101
|
# /photos/0000/0001/3er0zs.jpg
|
@@ -103,24 +103,24 @@ module HasImage
|
|
103
103
|
webpath = filesystem_path_for(object, thumbnail).gsub(/\A.*public/, '')
|
104
104
|
escape_file_name_for_http(webpath)
|
105
105
|
end
|
106
|
-
|
106
|
+
|
107
107
|
def escape_file_name_for_http(webpath)
|
108
108
|
dir, file = File.split(webpath)
|
109
109
|
File.join(dir, CGI.escape(file))
|
110
110
|
end
|
111
|
-
|
111
|
+
|
112
112
|
# Deletes the images and directory that contains them.
|
113
113
|
def remove_images(object, name)
|
114
114
|
FileUtils.rm Dir.glob(File.join(path_for(object.has_image_id), name + '*'))
|
115
115
|
Dir.rmdir path_for(object.has_image_id)
|
116
|
-
rescue SystemCallError
|
116
|
+
rescue SystemCallError
|
117
117
|
end
|
118
118
|
|
119
119
|
# Is the uploaded file within the min and max allowed sizes?
|
120
120
|
def valid?
|
121
121
|
!(image_too_small? || image_too_big?)
|
122
122
|
end
|
123
|
-
|
123
|
+
|
124
124
|
# Write the thumbnails to the install directory - probably somewhere under
|
125
125
|
# RAILS_ROOT/public.
|
126
126
|
def generate_thumbnails(id, name)
|
@@ -128,7 +128,7 @@ module HasImage
|
|
128
128
|
options[:thumbnails].keys.each { |thumb_name| generate_thumbnail(id, name, thumb_name) }
|
129
129
|
end
|
130
130
|
alias_method :regenerate_thumbnails, :generate_thumbnails #Backwards-compat
|
131
|
-
|
131
|
+
|
132
132
|
def generate_thumbnail(id, name, thumb_name)
|
133
133
|
size_spec = options[:thumbnails][thumb_name.to_sym]
|
134
134
|
raise StorageError unless size_spec
|
@@ -139,23 +139,23 @@ module HasImage
|
|
139
139
|
end
|
140
140
|
end
|
141
141
|
end
|
142
|
-
|
142
|
+
|
143
143
|
# Gets the full local filesystem path for an image. For example:
|
144
144
|
#
|
145
145
|
# /var/sites/example.com/production/public/photos/0000/0001/3er0zs.jpg
|
146
146
|
def filesystem_path_for(object, thumbnail = nil)
|
147
147
|
File.join(path_for(object.has_image_id), file_name_for(object.send(options[:column]), thumbnail))
|
148
148
|
end
|
149
|
-
|
149
|
+
|
150
150
|
protected
|
151
151
|
|
152
152
|
# Gets the extension to append to the image. Transforms "jpeg" to "jpg."
|
153
153
|
def extension
|
154
154
|
options[:convert_to].to_s.downcase.gsub("jpeg", "jpg")
|
155
155
|
end
|
156
|
-
|
156
|
+
|
157
157
|
private
|
158
|
-
|
158
|
+
|
159
159
|
# File name, plus thumbnail suffix, plus extension. For example:
|
160
160
|
#
|
161
161
|
# file_name_for("abc123", :thumb)
|
@@ -163,7 +163,7 @@ module HasImage
|
|
163
163
|
# gives you:
|
164
164
|
#
|
165
165
|
# "abc123_thumb.jpg"
|
166
|
-
#
|
166
|
+
#
|
167
167
|
# It uses an underscore to separatore parts by default, but that is configurable
|
168
168
|
# by setting HasImage::Storage.thumbnail_separator
|
169
169
|
def file_name_for(*args)
|
@@ -177,15 +177,15 @@ module HasImage
|
|
177
177
|
debugger if $debug
|
178
178
|
File.join(options[:base_path], options[:path_prefix], Storage.partitioned_path(id))
|
179
179
|
end
|
180
|
-
|
180
|
+
|
181
181
|
def absolute_path(id, *args)
|
182
182
|
File.join(path_for(id), file_name_for(*args))
|
183
183
|
end
|
184
|
-
|
184
|
+
|
185
185
|
def ensure_directory_exists!(id)
|
186
186
|
FileUtils.mkdir_p path_for(id)
|
187
187
|
end
|
188
|
-
|
188
|
+
|
189
189
|
# Write the main image to the install directory - probably somewhere under
|
190
190
|
# RAILS_ROOT/public.
|
191
191
|
def install_main_image(id, name)
|
@@ -196,12 +196,12 @@ module HasImage
|
|
196
196
|
end
|
197
197
|
end
|
198
198
|
end
|
199
|
-
|
199
|
+
|
200
200
|
# used in #install_images
|
201
201
|
def thumbnails_needed?
|
202
202
|
!options[:thumbnails].empty? && options[:auto_generate_thumbnails]
|
203
203
|
end
|
204
|
-
|
204
|
+
|
205
205
|
# Instantiates the processor using the options set in my contructor (if
|
206
206
|
# not already instantiated), stores it in an instance variable, and
|
207
207
|
# returns it.
|
@@ -209,5 +209,5 @@ module HasImage
|
|
209
209
|
@processor ||= Processor.new(options)
|
210
210
|
end
|
211
211
|
end
|
212
|
-
|
213
|
-
end
|
212
|
+
|
213
|
+
end
|
@@ -1,9 +1,8 @@
|
|
1
1
|
module HasImage
|
2
|
-
|
3
2
|
# Some helpers to make working with HasImage models in views a little
|
4
3
|
# easier.
|
5
4
|
module ViewHelpers
|
6
|
-
|
5
|
+
|
7
6
|
# Wraps the image_tag helper from Rails. Instead of passing the path to
|
8
7
|
# an image, you can pass any object that uses HasImage. The options can
|
9
8
|
# include the name of one of your thumbnails, for example:
|
@@ -23,7 +22,7 @@ module HasImage
|
|
23
22
|
def image_tag_for(object, options = {})
|
24
23
|
thumb = options.delete(:thumb)
|
25
24
|
if !options[:size]
|
26
|
-
if thumb
|
25
|
+
if thumb
|
27
26
|
size = object.class.thumbnails[thumb.to_sym]
|
28
27
|
options[:size] = size if size =~ /\A[\d]*x[\d]*\Z/
|
29
28
|
else
|
@@ -31,9 +30,7 @@ module HasImage
|
|
31
30
|
options[:size] = size if size =~ /\A[\d]*x[\d]*\Z/
|
32
31
|
end
|
33
32
|
end
|
34
|
-
|
33
|
+
image_tag(object.public_path(thumb), options)
|
35
34
|
end
|
36
|
-
|
37
35
|
end
|
38
|
-
|
39
36
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require 'test_helper'
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
2
|
|
3
3
|
class ComplexPic < ActiveRecord::Base
|
4
4
|
has_image
|
@@ -12,11 +12,11 @@ class ComplexPicTest < Test::Unit::TestCase
|
|
12
12
|
ComplexPic.has_image_options[:base_path] = File.join(RAILS_ROOT, 'tmp')
|
13
13
|
ComplexPic.has_image_options[:resize_to] = nil
|
14
14
|
end
|
15
|
-
|
15
|
+
|
16
16
|
def teardown
|
17
17
|
FileUtils.rm_rf(File.join(RAILS_ROOT, 'tmp', 'complex_pics'))
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
def test_should_save_width_to_db_on_create
|
21
21
|
@pic = ComplexPic.create!(:image_data => fixture_file_upload("/image.jpg", "image/jpeg"))
|
22
22
|
assert_equal 1916, @pic[:width]
|
@@ -26,7 +26,12 @@ class ComplexPicTest < Test::Unit::TestCase
|
|
26
26
|
@pic = ComplexPic.create!(:image_data => fixture_file_upload("/image.jpg", "image/jpeg"))
|
27
27
|
assert_equal 1990, @pic[:height]
|
28
28
|
end
|
29
|
-
|
29
|
+
|
30
|
+
def test_should_save_image_size_to_db_on_create
|
31
|
+
@pic = ComplexPic.create!(:image_data => fixture_file_upload("/image.jpg", "image/jpeg"))
|
32
|
+
assert_equal '1916x1990', @pic[:image_size]
|
33
|
+
end
|
34
|
+
|
30
35
|
def test_should_use_value_from_db_in_height_reader
|
31
36
|
@pic = ComplexPic.create!(:image_data => fixture_file_upload("/image.jpg", "image/jpeg"))
|
32
37
|
@pic[:height] = 60_000
|
@@ -38,5 +43,5 @@ class ComplexPicTest < Test::Unit::TestCase
|
|
38
43
|
@pic[:width] = 60_000
|
39
44
|
assert_equal 60_000, @pic.width
|
40
45
|
end
|
41
|
-
|
46
|
+
|
42
47
|
end
|
File without changes
|
File without changes
|
File without changes
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require 'test_helper'
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
2
|
|
3
3
|
class Pic < ActiveRecord::Base
|
4
4
|
has_image
|
@@ -9,6 +9,7 @@ class PicWithDifferentTableName < ActiveRecord::Base
|
|
9
9
|
end
|
10
10
|
|
11
11
|
class PicTest < Test::Unit::TestCase
|
12
|
+
|
12
13
|
def setup
|
13
14
|
# Note: Be sure to not set the whole options hash in your tests below
|
14
15
|
Pic.has_image_options = HasImage.default_options_for(Pic)
|
@@ -81,7 +82,7 @@ class PicTest < Test::Unit::TestCase
|
|
81
82
|
|
82
83
|
def test_regenerate_thumbnails_succeeds
|
83
84
|
Pic.has_image_options[:thumbnails] = {:small => "100x100", :tiny => "16x16"}
|
84
|
-
|
85
|
+
|
85
86
|
@pic = Pic.new(:image_data => fixture_file_upload("/image.jpg", "image/jpeg"))
|
86
87
|
@pic.save!
|
87
88
|
assert @pic.regenerate_thumbnails
|
@@ -150,4 +151,4 @@ class PicTest < Test::Unit::TestCase
|
|
150
151
|
assert_equal 1990, pic.height
|
151
152
|
end
|
152
153
|
|
153
|
-
end
|
154
|
+
end
|
data/test/processor_test.rb
CHANGED
@@ -1,20 +1,20 @@
|
|
1
|
-
require File.
|
1
|
+
require File.expand_path('../test_helper.rb', __FILE__)
|
2
2
|
|
3
3
|
class StorageTest < Test::Unit::TestCase
|
4
|
-
|
4
|
+
|
5
5
|
def teardown
|
6
6
|
@temp_file.close if @temp_file
|
7
7
|
FileUtils.rm_rf(File.dirname(__FILE__) + '/../tmp')
|
8
8
|
end
|
9
|
-
|
9
|
+
|
10
10
|
def temp_file(fixture)
|
11
11
|
@temp_file = Tempfile.new('test')
|
12
|
-
@temp_file.write(File.new(File.
|
12
|
+
@temp_file.write(File.new(File.expand_path("../fixtures/#{fixture}", __FILE__), "r").read)
|
13
13
|
return @temp_file
|
14
14
|
end
|
15
|
-
|
15
|
+
|
16
16
|
def test_detect_valid_image
|
17
|
-
assert HasImage::Processor.valid?(File.
|
17
|
+
assert HasImage::Processor.valid?(File.expand_path("../fixtures/image.jpg", __FILE__))
|
18
18
|
end
|
19
19
|
|
20
20
|
def test_detect_valid_image_from_tmp_file
|
@@ -22,7 +22,7 @@ class StorageTest < Test::Unit::TestCase
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def test_detect_invalid_image
|
25
|
-
assert !HasImage::Processor.valid?(File.
|
25
|
+
assert !HasImage::Processor.valid?(File.expand_path("../fixtures/bad_image.jpg", __FILE__))
|
26
26
|
end
|
27
27
|
|
28
28
|
def test_detect_invalid_image_from_tmp_file
|
@@ -35,7 +35,7 @@ class StorageTest < Test::Unit::TestCase
|
|
35
35
|
@processor.resize(temp_file("image.jpg"), "bad_geometry")
|
36
36
|
end
|
37
37
|
end
|
38
|
-
|
38
|
+
|
39
39
|
def test_resize_fixed
|
40
40
|
@processor = HasImage::Processor.new({:convert_to => "JPEG", :output_quality => "85"})
|
41
41
|
assert @processor.resize(temp_file("image.jpg"), "100x100")
|
@@ -57,5 +57,4 @@ class StorageTest < Test::Unit::TestCase
|
|
57
57
|
@processor.resize(temp_file("bad_image.jpg"), "100x100")
|
58
58
|
end
|
59
59
|
end
|
60
|
-
|
61
|
-
end
|
60
|
+
end
|