norman-has_image 0.1.5

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 ADDED
@@ -0,0 +1,19 @@
1
+ 2008-07-29 Norman Clarke <norman@randomba.org>
2
+
3
+ * Made image deletion nullify the "has_image_file" field.
4
+ * Added "has_image?" method to model instances.
5
+ * Fixed ENONENT error with record update when there are no images yet.
6
+ * Reverted thumbnail sorting feature - it's fast but makes terrible quality
7
+ thumbnails. It's just not worth it.
8
+
9
+ 2008-07-28 Norman Clarke <norman@randomba.org>
10
+
11
+ * Added sorted thumbnail processing. This improves thumbnail generation
12
+ speed by about 25% for 4.5 meg jpegs with 5 thumbnails.
13
+ * Fixed broken resize for non-fixed-width thumbnails.
14
+ * Added check for bad geometry strings.
15
+ * Added dependencies and Rubyforge project to gemspec, updated docs.
16
+
17
+ 2008-07-25 Norman Clarke <norman@randomba.org>
18
+
19
+ * First public release.
data/FAQ ADDED
@@ -0,0 +1,25 @@
1
+ = Frequently Asked Questions
2
+
3
+ HasImage is too new to have many FAQ items yet. {Ask
4
+ me}[mailto:norman@randomba.org] and they will be included; this is a work in
5
+ progress.
6
+
7
+ = How do I validate the mime type of my uploaded images?
8
+
9
+ You don't. Rather than examine the mime type, HasImage runs the "identify"
10
+ command on the file to determine if it is processable by ImageMagick, and if
11
+ it is, converts it to the format you specify, which defaults to JPEG.
12
+
13
+ This is better than checking for mime types, because your users may upload
14
+ exotic image types that you didn't even realize would work, such as Truevision
15
+ Targa images, or Seattle Film Works files.
16
+
17
+ If you wish to give users a list of file types they can upload, a good start
18
+ would be jpeg, png, bmp, and maybe gif and ttf if your installation of
19
+ ImageMagick understands them. You can find out what image types your
20
+ ImageMagick understands by running:
21
+
22
+ identify -list format
23
+
24
+ Ideally, if your users just upload files that "look like" images on their
25
+ computers, it HasImage should "just work."
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 [name of plugin creator]
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,143 @@
1
+ = HasImage[http://github.com/norman/has_image] -- Image attachment gem/plugin for Ruby on Rails
2
+
3
+ HasImage[http://github.com/norman/has_image] was created as a smaller,
4
+ simpler, lighter alternative to
5
+ attachment_fu[http://github.com/technoweenie/attachment_fu] for applications
6
+ that need to handle uploaded images.
7
+
8
+ It creates only one database record per image, requires only one column in
9
+ your model, and creates great-looking fixed-dimension thumbnails by using
10
+ {ImageMagick's}[http://www.imagemagick.org/]
11
+ resize[http://www.imagemagick.org/script/command-line-options.php#resize],
12
+ crop[http://www.imagemagick.org/script/command-line-options.php#crop] and
13
+ gravity[http://www.imagemagick.org/script/command-line-options.php#gravity]
14
+ functions.
15
+
16
+ Some typical use cases are: websites that want to create photo galleries with
17
+ fixed-dimension thumbnails, or that want to store user profile pictures
18
+ without creating a separate model for the images.
19
+
20
+ It supports only filesystem storage, and uses only MiniMagick[http://github.com/probablycorey/mini_magick] to process
21
+ images. However, the codebase is very small, simple, readable, and hackable.
22
+ So it should be easy to modify or enhance its functionality with different
23
+ storage or processor options.
24
+
25
+ == Another image attachment library? Why?
26
+
27
+ <em>The three chief virtues of a programmer are: Laziness, Impatience and Hubris.</em> - {Larry Wall}[http://en.wikipedia.org/wiki/Larry_Wall]
28
+
29
+ Attachment_fu is too large and general for some of the places I want to use
30
+ images. I sometimes found myself writing more code to hack attachment_fu than
31
+ it took to create this gem. In fact, most of the code here has been plucked
32
+ from my various projects that use attachment_fu.
33
+
34
+ The other image attachment libraries I found fell short of my needs for
35
+ various other reasons, so I decided to roll my own.
36
+
37
+ == Examples
38
+
39
+ Point-and-drool use case. It's probably not what you want, but it may be
40
+ useful for bootstrapping.
41
+
42
+ class Member < ActiveRecord::Base
43
+ has_image
44
+ end
45
+
46
+ Single image, no thumbnails, with some size limits:
47
+
48
+ class Picture < ActiveRecord::Base
49
+ has_image :resize_to => "200x200",
50
+ :max_size => 3.megabytes,
51
+ :min_size => 4.kilobytes
52
+ end
53
+
54
+ Image with some thumbnails:
55
+
56
+ class Photo < ActiveRecord::Base
57
+ has_image :resize_to => "640x480",
58
+ :thumbnails => {
59
+ :square => "200x200",
60
+ :medium => "320x240"
61
+ },
62
+ :max_size => 3.megabytes,
63
+ :min_size => 4.kilobytes
64
+ end
65
+
66
+ It also provides a view helper to make displaying the images extremely simple:
67
+
68
+ <%= image_tag_for(@photo, :thumb => :square) %>
69
+
70
+ == Getting it
71
+
72
+ Has image can be installed as a gem, or as a Rails plugin. Gem installation
73
+ is easiest, and recommended:
74
+
75
+ gem install has_image
76
+
77
+ and add
78
+
79
+ require 'has_image'
80
+
81
+ to your environment.rb file.
82
+
83
+ Alternatively, you can install it as a Rails plugin:
84
+
85
+ ./script plugin install git://github.com/norman/has_image.git
86
+
87
+ Rails versions before 2.1 do not support plugin installation using Git, so if
88
+ you're on 2.0 (or earlier), then please install the gem rather than the
89
+ plugin.
90
+
91
+ Then, make sure the model has a column named "has_image_file."
92
+
93
+ {Git repository}[http://github.com/norman/has_image]:
94
+
95
+ git://github.com/norman/has_image.git
96
+
97
+ == Hacking it
98
+
99
+ Don't like the way it makes images? Want to pipe the images through some
100
+ {crazy fast seam carving library written in
101
+ OCaml}[http://eigenclass.org/hiki/seam-carving-in-ocaml], or watermark them
102
+ with your corporate logo? Happiness is just a monkey-patch[http://en.wikipedia.org/wiki/Monkey_patch] away:
103
+
104
+ module HasImage
105
+ class Processor
106
+ def resize_image(size)
107
+ # your new-and-improved thumbnailer code goes here.
108
+ end
109
+ end
110
+ end
111
+
112
+ HasImage[http://github.com/norman/has_image] follows a philosophy of "{skinny
113
+ model, fat plugin}[http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model]."
114
+ This means that it tries to pollute your ActiveRecord model with as little
115
+ functionality as possible, so that in a sense, the model is acts like a
116
+ "controller" and the plugin like a "model" as regards the image handling
117
+ functionality. This makes it easier to test, hack, and reuse, because the
118
+ storage and processing functionality is largely independent of your model, and
119
+ of Rails.
120
+
121
+ My goal for HasImage[http://github.com/norman/has_image] is to keep it very
122
+ small. If you need *a lot* of functionality that's not here, instead of patching
123
+ this code, you will likely be better off using
124
+ attachment_fu[http://github.com/technoweenie/attachment_fu], which is much
125
+ more powerful, but also more complex.
126
+
127
+ == Bugs
128
+
129
+ Please report them on Lighthouse[http://randomba.lighthouseapp.com/projects/14674-has_image].
130
+
131
+ At the time of writing (July 2008),
132
+ HasImage[http://github.com/norman/has_image] is in its infancy. Your patches,
133
+ bug reports and withering criticism are more than welcome.
134
+
135
+ == Links
136
+
137
+ * {HasImage RDocs}[http://randomba.org/projects/has_image] (regenerated nightly)
138
+ * {HasImage on GitHub}[http://github.com/norman/has_image]
139
+ * {HasImage on Rubyforge}[http://rubyforge.org/projects/has-image/]
140
+ * {HasImage on Lighthouse}[http://randomba.lighthouseapp.com/projects/14674-has_image]
141
+
142
+ Copyright (c) 2008 {Norman Clarke}[mailto:norman@randomba.org], released under
143
+ the MIT license
data/Rakefile ADDED
@@ -0,0 +1,47 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the non-Rails part of has_image.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.libs << 'test'
12
+ t.pattern = 'test/**/*_test.rb'
13
+ t.verbose = true
14
+ end
15
+
16
+ desc 'Test the Rails part of has_image.'
17
+ Rake::TestTask.new(:test_rails) do |t|
18
+ t.libs << 'lib'
19
+ t.libs << 'test_rails'
20
+ t.pattern = 'test_rails/**/*_test.rb'
21
+ t.verbose = true
22
+ end
23
+
24
+ desc "Run rcov"
25
+ task :rcov do
26
+ rm_f "coverage"
27
+ rm_f "coverage.data"
28
+ if PLATFORM =~ /darwin/
29
+ exclude = '--exclude "gems"'
30
+ else
31
+ exclude = '--exclude "rubygems"'
32
+ end
33
+ rcov = "rcov --rails -Ilib:test --sort coverage --text-report #{exclude} --no-validator-links"
34
+ cmd = "#{rcov} #{Dir["test/**/*.rb"].join(" ")}"
35
+ sh cmd
36
+ end
37
+
38
+ desc 'Generate documentation for has_image.'
39
+ Rake::RDocTask.new(:rdoc) do |rdoc|
40
+ rdoc.rdoc_dir = 'rdoc'
41
+ rdoc.title = 'HasImage'
42
+ rdoc.options << '--line-numbers' << '--inline-source' << '-c UTF-8'
43
+ rdoc.rdoc_files.include('README')
44
+ rdoc.rdoc_files.include('FAQ')
45
+ rdoc.rdoc_files.include('CHANGELOG')
46
+ rdoc.rdoc_files.include('lib/**/*.rb')
47
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'has_image'
@@ -0,0 +1,93 @@
1
+ require 'mini_magick'
2
+
3
+ module HasImage
4
+
5
+ # Image processing functionality for the HasImage gem.
6
+ class Processor
7
+
8
+ attr_accessor :options
9
+
10
+ class << self
11
+
12
+ # "The form of an {extended geometry
13
+ # string}[http://www.imagemagick.org/script/command-line-options.php?#resize] is
14
+ # <width>x<height>{+-}<xoffset>{+-}<yoffset>{%}{!}{<}{>}"
15
+ def geometry_string_valid?(string)
16
+ string =~ /\A[\d]*x[\d]*([+-][0-9][+-][0-9])?[%@!<>^]?\Z/
17
+ end
18
+
19
+ # Arg should be either a file, or a path. This runs ImageMagick's
20
+ # "identify" command and looks for an exit status indicating an error. If
21
+ # there is no error, then ImageMagick has identified the file as something
22
+ # it can work with and it will be converted to the desired output format.
23
+ def valid?(arg)
24
+ arg.close if arg.respond_to?(:close) && !arg.closed?
25
+ silence_stderr do
26
+ `identify #{arg.respond_to?(:path) ? arg.path : arg.to_s}`
27
+ $? == 0
28
+ end
29
+ end
30
+ end
31
+
32
+ # The constuctor should be invoked with the options set by has_image.
33
+ def initialize(options) # :nodoc:
34
+ @options = options
35
+ end
36
+
37
+ # Create the resized image, and transforms it to the desired output
38
+ # format if necessary. The size should be a valid ImageMagick {geometry
39
+ # string}[http://www.imagemagick.org/script/command-line-options.php#resize].
40
+ def resize(file, size)
41
+ unless Processor.geometry_string_valid?(size)
42
+ raise InvalidGeometryError.new('"%s" is not a valid ImageMagick geometry string' % size)
43
+ end
44
+ silence_stderr do
45
+ path = file.respond_to?(:path) ? file.path : file
46
+ file.close if file.respond_to?(:close) && !file.closed?
47
+ @image = MiniMagick::Image.from_file(path)
48
+ convert_image
49
+ resize_image(size)
50
+ return @image
51
+ end
52
+ rescue MiniMagick::MiniMagickError
53
+ raise ProcessorError.new("That doesn't look like an image file.")
54
+ end
55
+
56
+ # Image resizing is placed in a separate method for easy monkey-patching.
57
+ # This is intended to be invoked from resize, rather than directly.
58
+ # By default, the following ImageMagick functionality is invoked:
59
+ # * auto-orient[http://www.imagemagick.org/script/command-line-options.php#auto-orient]
60
+ # * strip[http://www.imagemagick.org/script/command-line-options.php#strip]
61
+ # * resize[http://www.imagemagick.org/script/command-line-options.php#resize]
62
+ # * gravity[http://www.imagemagick.org/script/command-line-options.php#gravity]
63
+ # * extent[http://www.imagemagick.org/script/command-line-options.php#extent]
64
+ # * quality[http://www.imagemagick.org/script/command-line-options.php#quality]
65
+ def resize_image(size)
66
+ @image.combine_options do |commands|
67
+ commands.send("auto-orient".to_sym)
68
+ commands.strip
69
+ # Fixed-dimension images
70
+ if size =~ /\A[\d]*x[\d]*!?\Z/
71
+ commands.resize "#{size}^"
72
+ commands.gravity "center"
73
+ commands.extent size
74
+ # Non-fixed-dimension images
75
+ else
76
+ commands.resize "#{size}"
77
+ end
78
+ commands.quality options[:output_quality]
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ # This was placed in a separate method largely to facilitate debugging
85
+ # and profiling.
86
+ def convert_image
87
+ return if @image[:format] == options[:convert_to]
88
+ @image.format(options[:convert_to])
89
+ end
90
+
91
+ end
92
+
93
+ end
@@ -0,0 +1,167 @@
1
+ require 'active_support'
2
+ require 'stringio'
3
+ require 'fileutils'
4
+ require 'zlib'
5
+
6
+ module HasImage
7
+
8
+ # Filesystem storage for the HasImage gem. The methods that HasImage inserts
9
+ # into ActiveRecord models only depend on the public methods in this class, so
10
+ # it should be reasonably straightforward to implement a different storage
11
+ # mechanism for Amazon AWS, Photobucket, DBFile, SFTP, or whatever you want.
12
+ class Storage
13
+
14
+ attr_accessor :image_data, :options, :temp_file
15
+
16
+ class << self
17
+
18
+ # Stolen from {Jamis Buck}[http://www.37signals.com/svn/archives2/id_partitioning.php].
19
+ def partitioned_path(id, *args)
20
+ ("%08d" % id).scan(/..../) + args
21
+ end
22
+
23
+ # Generates a 4-6 character random file name to use for the image and its
24
+ # thumbnails. This is done to avoid having files with unfortunate names.
25
+ # On one of my sites users frequently upload images with Arabic names, and
26
+ # they end up being hard to manipulate on the command line. This also
27
+ # helps prevent a possibly undesirable sitation where the uploaded images
28
+ # have offensive names.
29
+ def random_file_name
30
+ Zlib.crc32(Time.now.to_s + rand(10e10).to_s).to_s(36)
31
+ end
32
+
33
+ end
34
+
35
+ # The constuctor should be invoked with the options set by has_image.
36
+ def initialize(options) # :nodoc:
37
+ @options = options
38
+ end
39
+
40
+ # The image data can be anything that inherits from IO. If you pass in an
41
+ # instance of Tempfile, it will be used directly without being copied to
42
+ # a new temp file.
43
+ def image_data=(image_data)
44
+ raise StorageError.new if image_data.blank?
45
+ if image_data.is_a?(Tempfile)
46
+ @temp_file = image_data
47
+ else
48
+ image_data.rewind
49
+ @temp_file = Tempfile.new 'has_image_data_%s' % Storage.random_file_name
50
+ @temp_file.write(image_data.read)
51
+ end
52
+ end
53
+
54
+ # Is uploaded file smaller than the allowed minimum?
55
+ def image_too_small?
56
+ @temp_file.open if @temp_file.closed?
57
+ @temp_file.size < options[:min_size]
58
+ end
59
+
60
+ # Is uploaded file larger than the allowed maximum?
61
+ def image_too_big?
62
+ @temp_file.open if @temp_file.closed?
63
+ @temp_file.size > options[:max_size]
64
+ end
65
+
66
+ # A tip of the hat to attachment_fu.
67
+ alias uploaded_data= image_data=
68
+
69
+ # A tip of the hat to attachment_fu.
70
+ alias uploaded_data image_data
71
+
72
+ # Invokes the processor to resize the image(s) and the installs them to
73
+ # the appropriate directory.
74
+ def install_images(id)
75
+ random_name = Storage.random_file_name
76
+ install_main_image(id, random_name)
77
+ install_thumbnails(id, random_name) if !options[:thumbnails].empty?
78
+ return random_name
79
+ ensure
80
+ @temp_file.close! if !@temp_file.closed?
81
+ @temp_file = nil
82
+ end
83
+
84
+ # Gets the "web" path for an image. For example:
85
+ #
86
+ # /photos/0000/0001/3er0zs.jpg
87
+ def public_path_for(object, thumbnail = nil)
88
+ filesystem_path_for(object, thumbnail).gsub(options[:base_path], '')
89
+ end
90
+
91
+ # Deletes the images and directory that contains them.
92
+ def remove_images(id)
93
+ FileUtils.rm_r path_for(id)
94
+ end
95
+
96
+ # Is the uploaded file within the min and max allowed sizes?
97
+ def valid?
98
+ !(image_too_small? || image_too_big?)
99
+ end
100
+
101
+ protected
102
+
103
+ # Gets the extension to append to the image. Transforms "jpeg" to "jpg."
104
+ def extension
105
+ options[:convert_to].to_s.downcase.gsub("jpeg", "jpg")
106
+ end
107
+
108
+ private
109
+
110
+ # File name, plus thumbnail suffix, plus extension. For example:
111
+ #
112
+ # file_name_for("abc123", :thumb)
113
+ #
114
+ # gives you:
115
+ #
116
+ # "abc123_thumb.jpg"
117
+ #
118
+ #
119
+ def file_name_for(*args)
120
+ "%s.%s" % [args.compact.join("_"), extension]
121
+ end
122
+
123
+ # Gets the full local filesystem path for an image. For example:
124
+ #
125
+ # /var/sites/example.com/production/public/photos/0000/0001/3er0zs.jpg
126
+ def filesystem_path_for(object, thumbnail = nil)
127
+ File.join(path_for(object.id), file_name_for(object.has_image_file, thumbnail))
128
+ end
129
+
130
+ # Write the main image to the install directory - probably somewhere under
131
+ # RAILS_ROOT/public.
132
+ def install_main_image(id, name)
133
+ FileUtils.mkdir_p path_for(id)
134
+ main = processor.resize(@temp_file, @options[:resize_to])
135
+ main.write(File.join(path_for(id), file_name_for(name)))
136
+ main.tempfile.close!
137
+ end
138
+
139
+ # Write the thumbnails to the install directory - probably somewhere under
140
+ # RAILS_ROOT/public.
141
+ def install_thumbnails(id, name)
142
+ FileUtils.mkdir_p path_for(id)
143
+ path = File.join(path_for(id), file_name_for(name))
144
+ options[:thumbnails].each do |thumb_name, size|
145
+ thumb = processor.resize(path, size)
146
+ thumb.write(File.join(path_for(id), file_name_for(name, thumb_name)))
147
+ thumb.tempfile.close!
148
+ end
149
+ end
150
+
151
+ # Get the full path for the id. For example:
152
+ #
153
+ # /var/sites/example.org/production/public/photos/0000/0001
154
+ def path_for(id)
155
+ File.join(options[:base_path], options[:path_prefix], Storage.partitioned_path(id))
156
+ end
157
+
158
+ # Instantiates the processor using the options set in my contructor (if
159
+ # not already instantiated), stores it in an instance variable, and
160
+ # returns it.
161
+ def processor
162
+ @processor ||= Processor.new(options)
163
+ end
164
+
165
+ end
166
+
167
+ end
@@ -0,0 +1,39 @@
1
+ module HasImage
2
+
3
+ # Some helpers to make working with HasImage models in views a little
4
+ # easier.
5
+ module ViewHelpers
6
+
7
+ # Wraps the image_tag helper from Rails. Instead of passing the path to
8
+ # an image, you can pass any object that uses HasImage. The options can
9
+ # include the name of one of your thumbnails, for example:
10
+ #
11
+ # image_tag_for(@photo)
12
+ # image_tag_for(@photo, :thumb => :square)
13
+ #
14
+ # If your object uses fixed dimensions (i.e., "200x200" as opposed to
15
+ # "200x200>"), then the height and width properties will automatically be
16
+ # added to the resulting img tag unless you explicitly specify the size in
17
+ # the options.
18
+ #
19
+ # All arguments other than :thumb will simply be passed along to the Rails
20
+ # image_tag helper without modification.
21
+ #
22
+ # See also: HasImage::ModelInstanceMethods#public_path
23
+ def image_tag_for(object, options = {})
24
+ thumb = options.delete(:thumb)
25
+ if !options[:size]
26
+ if thumb
27
+ size = object.class.thumbnails[thumb.to_sym]
28
+ options[:size] = size if size =~ /\A[\d]*x[\d]*\Z/
29
+ else
30
+ size = object.class.resize_to
31
+ options[:size] = size if size =~ /\A[\d]*x[\d]*\Z/
32
+ end
33
+ end
34
+ image_tag(object.public_path(thumb), options)
35
+ end
36
+
37
+ end
38
+
39
+ end
data/lib/has_image.rb ADDED
@@ -0,0 +1,249 @@
1
+ require 'has_image/processor'
2
+ require 'has_image/storage'
3
+ require 'has_image/view_helpers'
4
+
5
+ # = HasImage
6
+ #
7
+ # HasImage allows Ruby on Rails applications to have attached images. It is very
8
+ # small and lightweight: it only requires one column ("has_image_file") in your
9
+ # model to store the uploaded image's file name.
10
+ #
11
+ # HasImage is, by design, very simplistic: It only supports using a filesystem
12
+ # for storage, and only supports
13
+ # MiniMagick[http://github.com/probablycorey/mini_magick] as an image processor.
14
+ # However, its code is very small, clean and hackable, so adding support for
15
+ # other backends or processors should be fairly easy.
16
+ #
17
+ # HasImage works best for sites that want to show image galleries with
18
+ # fixed-size thumbnails. It uses ImageMagick's
19
+ # crop[http://www.imagemagick.org/script/command-line-options.php#crop] and
20
+ # {center
21
+ # gravity}[http://www.imagemagick.org/script/command-line-options.php#gravity]
22
+ # functions to produce thumbnails that generally look acceptable, unless the
23
+ # image is a panorama, or the subject matter is close to one of the margins,
24
+ # etc. For most sites where people upload pictures of themselves or their pets
25
+ # the generated thumbnails will look good almost all the time.
26
+ #
27
+ # It's pretty easy to change the image processing / resizing code; you can just
28
+ # override HasImage::Processor#resize_image to do what you wish:
29
+ #
30
+ # module HasImage::
31
+ # class Processor
32
+ # def resize_image(size)
33
+ # @image.combine_options do |commands|
34
+ # commands.my_custom_resizing_goes_here
35
+ # end
36
+ # end
37
+ # end
38
+ # end
39
+ #
40
+ # Compared to attachment_fu, HasImage has advantages and disadvantages.
41
+ #
42
+ # = Advantages:
43
+ #
44
+ # * Simpler, smaller, more easily hackable codebase - and specialized for
45
+ # images only.
46
+ # * Installable via Ruby Gems. This makes version dependencies easy when using
47
+ # Rails 2.1.
48
+ # * Creates only one database record per image.
49
+ # * Has built-in facilities for making distortion-free, fixed-size thumbnails.
50
+ # * Doesn't regenerate the thumbnails every time you save your model. This means
51
+ # you can easily use it, for example, inside a Member model to store member
52
+ # avatars.
53
+ #
54
+ # = Disadvantages:
55
+ #
56
+ # * Doesn't save image dimensions. However, if you're using fixed-sized images,
57
+ # this is not a problem because you can just read the size from MyModel.thumbnails[:my_size]
58
+ # * No support for AWS or DBFile storage, only filesystem.
59
+ # * Only supports MiniMagick[http://github.com/probablycorey/mini_magick/tree] as an image processor, no RMagick, GD, CoreImage,
60
+ # etc.
61
+ # * No support for anything other than image attachments.
62
+ # * Not as popular as attachment_fu, which means fewer bug reports, and
63
+ # probably more bugs. Use at your own risk!
64
+ module HasImage
65
+
66
+ class ProcessorError < StandardError ; end
67
+ class StorageError < StandardError ; end
68
+ class FileTooBigError < StorageError ; end
69
+ class FileTooSmallError < StorageError ; end
70
+ class InvalidGeometryError < ProcessorError ; end
71
+
72
+ class << self
73
+
74
+ def included(base) # :nodoc:
75
+ base.extend(ClassMethods)
76
+ end
77
+
78
+ # Enables has_image functionality. You probably don't need to ever invoke
79
+ # this.
80
+ def enable # :nodoc:
81
+ return if ActiveRecord::Base.respond_to? :has_image
82
+ ActiveRecord::Base.send(:include, HasImage)
83
+ return if ActionView::Base.respond_to? :image_tag_for
84
+ ActionView::Base.send(:include, ViewHelpers)
85
+ end
86
+
87
+ # If you're invoking this method, you need to pass in the class for which
88
+ # you want to get default options; this is used to determine the path where
89
+ # the images will be stored in the file system. Take a look at
90
+ # HasImage::ClassMethods#has_image to see examples of how to set the options
91
+ # in your model.
92
+ #
93
+ # This method is called by your model when you call has_image. It's
94
+ # placed here rather than in the model's class methods to make it easier
95
+ # to access for testing. Unless you're working on the code, it's unlikely
96
+ # you'll ever need to invoke this method.
97
+ #
98
+ # * :resize_to => "200x200",
99
+ # * :thumbnails => {},
100
+ # * :max_size => 12.megabytes,
101
+ # * :min_size => 4.kilobytes,
102
+ # * :path_prefix => klass.to_s.tableize,
103
+ # * :base_path => File.join(RAILS_ROOT, 'public'),
104
+ # * :convert_to => "JPEG",
105
+ # * :output_quality => "85",
106
+ # * :invalid_image_message => "Can't process the image.",
107
+ # * :image_too_small_message => "The image is too small.",
108
+ # * :image_too_big_message => "The image is too big.",
109
+ def default_options_for(klass)
110
+ {
111
+ :resize_to => "200x200",
112
+ :thumbnails => {},
113
+ :max_size => 12.megabytes,
114
+ :min_size => 4.kilobytes,
115
+ :path_prefix => klass.to_s.tableize,
116
+ :base_path => File.join(RAILS_ROOT, 'public'),
117
+ :convert_to => "JPEG",
118
+ :output_quality => "85",
119
+ :invalid_image_message => "Can't process the image.",
120
+ :image_too_small_message => "The image is too small.",
121
+ :image_too_big_message => "The image is too big."
122
+ }
123
+ end
124
+
125
+ end
126
+
127
+ module ClassMethods
128
+ # To use HasImage with a Rails model, all you have to do is add a column
129
+ # named "has_image_file." For configuration defaults, you might want to take
130
+ # a look at the default options specified in HasImage#default_options_for.
131
+ # The different setting options are described below.
132
+ #
133
+ # Options:
134
+ # * <tt>:resize_to</tt> - Dimensions to resize to. This should be an ImageMagick {geometry string}[http://www.imagemagick.org/script/command-line-options.php#resize]. Fixed sizes are recommended.
135
+ # * <tt>:thumbnails</tt> - A hash of thumbnail names and dimensions. The dimensions should be ImageMagick {geometry strings}[http://www.imagemagick.org/script/command-line-options.php#resize]. Fixed sized are recommended.
136
+ # * <tt>:min_size</tt> - Minimum file size allowed. It's recommended that you set this size in kilobytes.
137
+ # * <tt>:max_size</tt> - Maximum file size allowed. It's recommended that you set this size in megabytes.
138
+ # * <tt>:base_path</tt> - Where to install the images. You should probably leave this alone, except for tests.
139
+ # * <tt>:path_prefix</tt> - Where to install the images, relative to basepath. You should probably leave this alone.
140
+ # * <tt>:convert_to</tt> - An ImageMagick format to convert images to. Recommended formats: JPEG, PNG, GIF.
141
+ # * <tt>:output_quality</tt> - Image output quality passed to ImageMagick.
142
+ # * <tt>:invalid_image_message</tt> - The message that will be shown when the image data can't be processed.
143
+ # * <tt>:image_too_small_message</tt> - The message that will be shown when the image file is too small. You should ideally set this to something that tells the user what the minimum is.
144
+ # * <tt>:image_too_big_message</tt> - The message that will be shown when the image file is too big. You should ideally set this to something that tells the user what the maximum is.
145
+ #
146
+ # Examples:
147
+ # has_image # uses all default options
148
+ # has_image :resize_to "800x800", :thumbnails => {:square => "150x150"}
149
+ # has_image :resize_to "100x150", :max_size => 500.kilobytes
150
+ # has_image :invalid_image_message => "No se puede procesar la imagen."
151
+ def has_image(options = {})
152
+ options.assert_valid_keys(:resize_to, :thumbnails, :max_size, :min_size,
153
+ :path_prefix, :base_path, :convert_to, :output_quality,
154
+ :invalid_image_message, :image_too_big_message, :image_too_small_message)
155
+ options = HasImage.default_options_for(self).merge(options)
156
+ class_inheritable_accessor :has_image_options
157
+ write_inheritable_attribute(:has_image_options, options)
158
+
159
+ after_create :install_images
160
+ after_save :update_images
161
+ after_destroy :remove_images
162
+
163
+ validate_on_create :image_data_valid?
164
+
165
+ include ModelInstanceMethods
166
+ extend ModelClassMethods
167
+
168
+ end
169
+
170
+ end
171
+
172
+ module ModelInstanceMethods
173
+
174
+ # Does the object have an image?
175
+ def has_image?
176
+ !has_image_file.blank?
177
+ end
178
+
179
+ # Sets the uploaded image data. Image data can be an instance of Tempfile,
180
+ # or an instance of any class than inherits from IO.
181
+ def image_data=(image_data)
182
+ return if image_data.blank?
183
+ storage.image_data = image_data
184
+ end
185
+
186
+ # Is the image data a file that ImageMagick can process, and is it within
187
+ # the allowed minimum and maximum sizes?
188
+ def image_data_valid?
189
+ return if !storage.temp_file
190
+ if storage.image_too_big?
191
+ errors.add_to_base(self.class.has_image_options[:image_too_big_message])
192
+ elsif storage.image_too_small?
193
+ errors.add_to_base(self.class.has_image_options[:image_too_small_message])
194
+ elsif !HasImage::Processor.valid?(storage.temp_file)
195
+ errors.add_to_base(self.class.has_image_options[:invalid_image_message])
196
+ end
197
+ end
198
+
199
+ # Gets the "web path" for the image, or optionally, its thumbnail.
200
+ def public_path(thumbnail = nil)
201
+ storage.public_path_for(self, thumbnail)
202
+ end
203
+
204
+ # Deletes the image from the storage.
205
+ def remove_images
206
+ return if has_image_file.blank?
207
+ storage.remove_images(self.id)
208
+ update_attribute(:has_image_file, nil)
209
+ rescue Errno::ENOENT
210
+ logger.warn("Could not delete files for #{self.class.to_s} #{to_param}")
211
+ end
212
+
213
+ # Creates new images and removes the old ones when image_data has been
214
+ # set.
215
+ def update_images
216
+ return if storage.temp_file.blank?
217
+ remove_images
218
+ update_attribute(:has_image_file, storage.install_images(self.id))
219
+ end
220
+
221
+ # Processes and installs the image and its thumbnails.
222
+ def install_images
223
+ return if !storage.temp_file
224
+ update_attribute(:has_image_file, storage.install_images(self.id))
225
+ end
226
+
227
+ # Gets an instance of the underlying storage functionality. See
228
+ # HasImage::Storage.
229
+ def storage
230
+ @storage ||= HasImage::Storage.new(has_image_options)
231
+ end
232
+
233
+ end
234
+
235
+ module ModelClassMethods
236
+
237
+ # Get the hash of thumbnails set by the options specified when invoking
238
+ # HasImage::ClassMethods#has_image.
239
+ def thumbnails
240
+ has_image_options[:thumbnails]
241
+ end
242
+
243
+ end
244
+
245
+ end
246
+
247
+ if defined?(Rails) and defined?(ActiveRecord) and defined?(ActionController)
248
+ HasImage.enable
249
+ end
@@ -0,0 +1,61 @@
1
+ require 'test_helper.rb'
2
+
3
+ class StorageTest < Test::Unit::TestCase
4
+
5
+ def teardown
6
+ @temp_file.close if @temp_file
7
+ FileUtils.rm_rf(File.dirname(__FILE__) + '/../tmp')
8
+ end
9
+
10
+ def temp_file(fixture)
11
+ @temp_file = Tempfile.new('test')
12
+ @temp_file.write(File.new(File.dirname(__FILE__) + "/../test_rails/fixtures/#{fixture}", "r").read)
13
+ return @temp_file
14
+ end
15
+
16
+ def test_detect_valid_image
17
+ assert HasImage::Processor.valid?(File.dirname(__FILE__) + "/../test_rails/fixtures/image.jpg")
18
+ end
19
+
20
+ def test_detect_valid_image_from_tmp_file
21
+ assert HasImage::Processor.valid?(temp_file("image.jpg"))
22
+ end
23
+
24
+ def test_detect_invalid_image
25
+ assert !HasImage::Processor.valid?(File.dirname(__FILE__) + "/../test_rails/fixtures/bad_image.jpg")
26
+ end
27
+
28
+ def test_detect_invalid_image_from_tmp_file
29
+ assert !HasImage::Processor.valid?(temp_file("bad_image.jpg"))
30
+ end
31
+
32
+ def test_resize_with_invalid_geometry
33
+ @processor = HasImage::Processor.new({:convert_to => "JPEG", :output_quality => "85"})
34
+ assert_raises HasImage::InvalidGeometryError do
35
+ @processor.resize(temp_file("image.jpg"), "bad_geometry")
36
+ end
37
+ end
38
+
39
+ def test_resize_fixed
40
+ @processor = HasImage::Processor.new({:convert_to => "JPEG", :output_quality => "85"})
41
+ assert @processor.resize(temp_file("image.jpg"), "100x100")
42
+ end
43
+
44
+ def test_resize_unfixed
45
+ @processor = HasImage::Processor.new({:convert_to => "JPEG", :output_quality => "85"})
46
+ assert @processor.resize(temp_file("image.jpg"), "1024x768>")
47
+ end
48
+
49
+ def test_resize_and_convert
50
+ @processor = HasImage::Processor.new({:convert_to => "JPEG", :output_quality => "85"})
51
+ assert @processor.resize(temp_file("image.png"), "100x100")
52
+ end
53
+
54
+ def test_resize_should_fail_with_bad_image
55
+ @processor = HasImage::Processor.new({:convert_to => "JPEG", :output_quality => "85"})
56
+ assert_raises HasImage::ProcessorError do
57
+ @processor.resize(temp_file("bad_image.jpg"), "100x100")
58
+ end
59
+ end
60
+
61
+ end
@@ -0,0 +1,98 @@
1
+ require 'test_helper.rb'
2
+
3
+ class StorageTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ end
7
+
8
+ def teardown
9
+ FileUtils.rm_rf(File.dirname(__FILE__) + '/../tmp')
10
+ @temp_file.close! if @temp_file && !@temp_file.closed?
11
+ end
12
+
13
+ def default_options
14
+ HasImage.default_options_for("tests").merge(
15
+ :base_path => File.join(File.dirname(__FILE__), '..', 'tmp')
16
+ )
17
+ end
18
+
19
+ def test_partitioned_path
20
+ assert_equal(["0001", "2345"], HasImage::Storage.partitioned_path("12345"))
21
+ end
22
+
23
+ def test_random_file_name
24
+ assert_match(/[a-z0-9]{4,6}/i, HasImage::Storage.random_file_name)
25
+ end
26
+
27
+ def test_path_for
28
+ @storage = HasImage::Storage.new(default_options)
29
+ assert_match(/\/tmp\/tests\/0000\/0001/, @storage.send(:path_for, 1))
30
+ end
31
+
32
+ def test_public_path_for
33
+ @storage = HasImage::Storage.new(default_options)
34
+ pic = stub(:has_image_file => "mypic", :id => 1)
35
+ assert_equal "/tests/0000/0001/mypic_square.jpg", @storage.public_path_for(pic, :square)
36
+ end
37
+
38
+ def test_filename_for
39
+ @storage = HasImage::Storage.new(default_options)
40
+ assert_equal "test.jpg", @storage.send(:file_name_for, "test")
41
+ end
42
+
43
+ def test_set_data_from_file
44
+ @storage = HasImage::Storage.new(default_options)
45
+ @file = File.new(File.dirname(__FILE__) + "/../test_rails/fixtures/image.jpg", "r")
46
+ @storage.image_data = @file
47
+ assert @storage.temp_file.size > 0
48
+ assert_equal Zlib.crc32(@file.read), Zlib.crc32(@storage.temp_file.read)
49
+ end
50
+
51
+ def test_set_data_from_tempfile
52
+ @storage = HasImage::Storage.new(default_options)
53
+ @storage.image_data = temp_file("image.jpg")
54
+ assert @storage.temp_file.size > 0
55
+ assert_equal Zlib.crc32(@storage.temp_file.read), Zlib.crc32(@temp_file.read)
56
+ end
57
+
58
+ def test_install_and_remove_images
59
+ @storage = HasImage::Storage.new(default_options)
60
+ @storage.image_data = temp_file("image.jpg")
61
+ assert @storage.install_images(1)
62
+ assert @storage.remove_images(1)
63
+ end
64
+
65
+ def test_image_not_too_small
66
+ @storage = HasImage::Storage.new(default_options.merge(:min_size => 1.kilobyte))
67
+ @storage.image_data = temp_file("image.jpg")
68
+ assert !@storage.image_too_small?
69
+ end
70
+
71
+ def test_image_too_small
72
+ @storage = HasImage::Storage.new(default_options.merge(:min_size => 1.gigabyte))
73
+ @storage.image_data = temp_file("image.jpg")
74
+ assert @storage.image_too_small?
75
+ end
76
+
77
+ def test_image_too_big
78
+ @storage = HasImage::Storage.new(default_options.merge(:max_size => 1.kilobyte))
79
+ @storage.image_data = temp_file("image.jpg")
80
+ assert @storage.image_too_big?
81
+ end
82
+
83
+ def test_image_not_too_big
84
+ @storage = HasImage::Storage.new(default_options.merge(:max_size => 1.gigabyte))
85
+ @storage.image_data = temp_file("image.jpg")
86
+ assert !@storage.image_too_big?
87
+ end
88
+
89
+ private
90
+
91
+ def temp_file(fixture)
92
+ file = File.new(File.dirname(__FILE__) + "/../test_rails/fixtures/#{fixture}", "r")
93
+ @temp_file = Tempfile.new("test")
94
+ @temp_file.write(file.read)
95
+ return @temp_file
96
+ end
97
+
98
+ end
@@ -0,0 +1,3 @@
1
+ sqlite3:
2
+ adapter: sqlite3
3
+ database: ":memory:"
Binary file
Binary file
Binary file
data/test_rails/pic.rb ADDED
@@ -0,0 +1,3 @@
1
+ class Pic < ActiveRecord::Base
2
+ has_image
3
+ end
@@ -0,0 +1,79 @@
1
+ require 'test_helper'
2
+
3
+ class PicTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ Pic.has_image_options = HasImage.default_options_for(Pic)
7
+ Pic.has_image_options[:base_path] = File.join(RAILS_ROOT, '/tmp')
8
+ end
9
+
10
+ def teardown
11
+ FileUtils.rm_rf(File.join(RAILS_ROOT, 'tmp', 'pics'))
12
+ end
13
+
14
+ def test_should_be_valid
15
+ @pic = Pic.new(:image_data => fixture_file_upload("/image.jpg", "image/jpeg"))
16
+ assert @pic.valid? , "#{@pic.errors.full_messages.to_sentence}"
17
+ end
18
+
19
+ def test_should_be_too_big
20
+ Pic.has_image_options[:max_size] = 1.kilobyte
21
+ @pic = Pic.new(:image_data => fixture_file_upload("/image.jpg", "image/jpeg"))
22
+ assert !@pic.valid?
23
+ end
24
+
25
+ def test_should_be_too_small
26
+ Pic.has_image_options[:min_size] = 1.gigabyte
27
+ @pic = Pic.new(:image_data => fixture_file_upload("/image.jpg", "image/jpeg"))
28
+ assert !@pic.valid?
29
+ end
30
+
31
+ def test_invalid_image_detected
32
+ @pic = Pic.new(:image_data => fixture_file_upload("/bad_image.jpg", "image/jpeg"))
33
+ assert !@pic.valid?
34
+ end
35
+
36
+ def test_create
37
+ @pic = Pic.new(:image_data => fixture_file_upload("/image.jpg", "image/jpeg"))
38
+ assert @pic.save!
39
+ end
40
+
41
+ def test_update
42
+ @pic = Pic.new(:image_data => fixture_file_upload("/image.jpg", "image/jpeg"))
43
+ @pic.save!
44
+ @pic.image_data = fixture_file_upload("/image.png", "image/png")
45
+ assert @pic.save!
46
+ end
47
+
48
+ def test_create_model_without_setting_image_data
49
+ assert Pic.new.save!
50
+ end
51
+
52
+ def test_destroy_model_without_no_images
53
+ @pic = Pic.new
54
+ @pic.save!
55
+ assert @pic.destroy
56
+ end
57
+
58
+ def test_destroy_model_with_images_already_deleted_from_filesystem
59
+ @pic = Pic.new
60
+ @pic.save!
61
+ @pic.update_attribute(:has_image_file, "test")
62
+ assert @pic.destroy
63
+ end
64
+
65
+ def test_create_with_png
66
+ Pic.has_image_options[:min_size] = 1
67
+ @pic = Pic.new(:image_data => fixture_file_upload("/image.png", "image/png"))
68
+ assert @pic.save!
69
+ end
70
+
71
+ def test_multiple_calls_to_valid_doesnt_blow_away_temp_image
72
+ Pic.has_image_options[:min_size] = 1
73
+ @pic = Pic.new(:image_data => fixture_file_upload("/image.png", "image/png"))
74
+ @pic.valid?
75
+ assert @pic.valid?
76
+ end
77
+
78
+ end
79
+
@@ -0,0 +1,9 @@
1
+ ActiveRecord::Schema.define(:version => 1) do
2
+
3
+ create_table "pics", :force => true do |t|
4
+ t.string :has_image_file
5
+ t.datetime :created_at
6
+ t.datetime :updated_at
7
+ end
8
+
9
+ end
@@ -0,0 +1,54 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+
3
+ ENV['RAILS_ENV'] = 'test'
4
+
5
+ require 'test/unit'
6
+ require File.expand_path(File.join(File.dirname(__FILE__), '/../../../../config/environment.rb'))
7
+ require 'active_record/fixtures'
8
+ require 'action_controller/test_process'
9
+
10
+ config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
11
+ ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
12
+
13
+ db_adapter = ENV['DB']
14
+
15
+ # no db passed, try one of these fine config-free DBs before bombing.
16
+ db_adapter ||=
17
+ begin
18
+ require 'rubygems'
19
+ require 'sqlite3'
20
+ 'sqlite3'
21
+ rescue MissingSourceFile
22
+ begin
23
+ require 'sqlite'
24
+ 'sqlite'
25
+ rescue MissingSourceFile
26
+ end
27
+ end
28
+
29
+ if db_adapter.nil?
30
+ raise "No DB Adapter selected. Pass the DB= option to pick one, or install Sqlite3 or Sqlite."
31
+ end
32
+
33
+ ActiveRecord::Base.establish_connection(config[db_adapter])
34
+
35
+ load(File.dirname(__FILE__) + "/schema.rb")
36
+
37
+ Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures"
38
+ $LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path)
39
+
40
+ class Test::Unit::TestCase #:nodoc:
41
+ include ActionController::TestProcess
42
+ def create_fixtures(*table_names)
43
+ if block_given?
44
+ Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield }
45
+ else
46
+ Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names)
47
+ end
48
+ end
49
+
50
+ self.use_transactional_fixtures = true
51
+ self.use_instantiated_fixtures = false
52
+
53
+ end
54
+ require 'pic'
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: norman-has_image
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.5
5
+ platform: ruby
6
+ authors:
7
+ - Norman Clarke
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-07-29 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: mini_magick
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.2.3
23
+ version:
24
+ description: HasImage is a Ruby on Rails gem/plugin that allows you to attach images to ActiveRecord models.
25
+ email: norman@randomba.org
26
+ executables: []
27
+
28
+ extensions: []
29
+
30
+ extra_rdoc_files:
31
+ - README
32
+ - CHANGELOG
33
+ - FAQ
34
+ files:
35
+ - CHANGELOG
36
+ - FAQ
37
+ - MIT-LICENSE
38
+ - README
39
+ - init.rb
40
+ - lib/has_image.rb
41
+ - lib/has_image/processor.rb
42
+ - lib/has_image/storage.rb
43
+ - lib/has_image/view_helpers.rb
44
+ - Rakefile
45
+ has_rdoc: true
46
+ homepage: http://randomba.org
47
+ post_install_message:
48
+ rdoc_options:
49
+ - --main
50
+ - README
51
+ - --inline-source
52
+ - --line-numbers
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ version:
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: "0"
66
+ version:
67
+ requirements: []
68
+
69
+ rubyforge_project: has-image
70
+ rubygems_version: 1.2.0
71
+ signing_key:
72
+ specification_version: 2
73
+ summary: Lets you attach images with thumbnails to active record models.
74
+ test_files:
75
+ - test_rails/database.yml
76
+ - test_rails/fixtures/bad_image.jpg
77
+ - test_rails/fixtures/image.jpg
78
+ - test_rails/fixtures/image.png
79
+ - test_rails/pic.rb
80
+ - test_rails/pic_test.rb
81
+ - test_rails/schema.rb
82
+ - test_rails/test_helper.rb
83
+ - test/processor_test.rb
84
+ - test/storage_test.rb