has_image 0.1.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 ADDED
@@ -0,0 +1,3 @@
1
+ 2008-07-25 Norman Clarke <norman@randomba.org>
2
+
3
+ * 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,139 @@
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 norman-has_image --source http://gems.github.com
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
+
98
+ == Hacking it
99
+
100
+ Don't like the way it makes images? Want to pipe the images through some
101
+ {crazy fast seam carving library written in
102
+ OCaml}[http://eigenclass.org/hiki/seam-carving-in-ocaml], or watermark them
103
+ with your corporate logo? Happiness is just a monkey-patch[http://en.wikipedia.org/wiki/Monkey_patch] away:
104
+
105
+ module HasImage
106
+ class Processor
107
+ def resize_image(size)
108
+ # your new-and-improved thumbnailer code goes here.
109
+ end
110
+ end
111
+ end
112
+
113
+ HasImage[http://github.com/norman/has_image] follows a philosophy of "{skinny
114
+ model, fat plugin}[http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model]."
115
+ This means that it tries to pollute your ActiveRecord model with as little
116
+ functionality as possible, so that in a sense, the model is acts like a
117
+ "controller" and the plugin like a "model" as regards the image handling
118
+ functionality. This makes it easier to test, hack, and reuse, because the
119
+ storage and processing functionality is largely independent of your model, and
120
+ of Rails.
121
+
122
+ My goal for HasImage[http://github.com/norman/has_image] is to keep it very
123
+ small. If you need <strong>a lot</strong> of functionality that's not here, instead of patching
124
+ this code, you will likely be better off using
125
+ attachment_fu[http://github.com/technoweenie/attachment_fu], which is much
126
+ more powerful, but also more complex.
127
+
128
+ == Bugs
129
+
130
+ Please report them on Lighthouse[http://randomba.lighthouseapp.com/projects/14674-has_image].
131
+
132
+ At the time of writing (July 2008),
133
+ HasImage[http://github.com/norman/has_image] is in its infancy. Your patches,
134
+ bug reports and withering criticism are more than welcome.
135
+
136
+
137
+
138
+ Copyright (c) 2008 {Norman Clarke}[mailto:norman@randomba.org], released under
139
+ 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,76 @@
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
+ # Arg should be either a file, or a path. This runs ImageMagick's
12
+ # "identify" command and looks for an exit status indicating an error. If
13
+ # there is no error, then ImageMagick has identified the file as something
14
+ # it can work with and it will be converted to the desired output format.
15
+ def valid?(arg)
16
+ arg.close if arg.respond_to?(:close) && !arg.closed?
17
+ silence_stderr do
18
+ `identify #{arg.respond_to?(:path) ? arg.path : arg.to_s}`
19
+ $? == 0
20
+ end
21
+ end
22
+ end
23
+
24
+ # The constuctor should be invoked with the options set by has_image.
25
+ def initialize(options) # :nodoc:
26
+ @options = options
27
+ end
28
+
29
+ # Create the resized image, and transforms it to the desired output
30
+ # format if necessary. The size should be a valid ImageMagick {geometry
31
+ # string}[http://www.imagemagick.org/script/command-line-options.php#resize].
32
+ def resize(file, size)
33
+ silence_stderr do
34
+ path = file.respond_to?(:path) ? file.path : file
35
+ file.close if file.respond_to?(:close) && !file.closed?
36
+ @image = MiniMagick::Image.from_file(path)
37
+ convert_image
38
+ resize_image(size)
39
+ return @image
40
+ end
41
+ rescue MiniMagick::MiniMagickError
42
+ raise ProcessorError.new("That doesn't look like an image file.")
43
+ end
44
+
45
+ # Image resizing is placed in a separate method for easy monkey-patching.
46
+ # This is intended to be invoked from resize, rather than directly.
47
+ # By default, the following ImageMagick functionality is invoked:
48
+ # * auto-orient[http://www.imagemagick.org/script/command-line-options.php#auto-orient]
49
+ # * strip[http://www.imagemagick.org/script/command-line-options.php#strip]
50
+ # * resize[http://www.imagemagick.org/script/command-line-options.php#resize]
51
+ # * gravity[http://www.imagemagick.org/script/command-line-options.php#gravity]
52
+ # * extent[http://www.imagemagick.org/script/command-line-options.php#extent]
53
+ # * quality[http://www.imagemagick.org/script/command-line-options.php#quality]
54
+ def resize_image(size)
55
+ @image.combine_options do |commands|
56
+ commands.send("auto-orient".to_sym)
57
+ commands.strip
58
+ commands.resize "#{size}^"
59
+ commands.gravity "center"
60
+ commands.extent size
61
+ commands.quality options[:output_quality]
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ # This was placed in a separate method largely to facilitate debugging
68
+ # and profiling.
69
+ def convert_image
70
+ return if @image[:format] == options[:convert_to]
71
+ @image.format(options[:convert_to])
72
+ end
73
+
74
+ end
75
+
76
+ 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,242 @@
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
+
71
+ class << self
72
+
73
+ def included(base) # :nodoc:
74
+ base.extend(ClassMethods)
75
+ end
76
+
77
+ # Enables has_image functionality. You probably don't need to ever invoke
78
+ # this.
79
+ def enable # :nodoc:
80
+ return if ActiveRecord::Base.respond_to? :has_image
81
+ ActiveRecord::Base.send(:include, HasImage)
82
+ return if ActionView::Base.respond_to? :image_tag_for
83
+ ActionView::Base.send(:include, ViewHelpers)
84
+ end
85
+
86
+ # If you're invoking this method, you need to pass in the class for which
87
+ # you want to get default options; this is used to determine the path where
88
+ # the images will be stored in the file system. Take a look at
89
+ # HasImage::ClassMethods#has_image to see examples of how to set the options
90
+ # in your model.
91
+ #
92
+ # This method is called by your model when you call has_image. It's
93
+ # placed here rather than in the model's class methods to make it easier
94
+ # to access for testing. Unless you're working on the code, it's unlikely
95
+ # you'll ever need to invoke this method.
96
+ #
97
+ # * :resize_to => "200x200",
98
+ # * :thumbnails => {},
99
+ # * :max_size => 12.megabytes,
100
+ # * :min_size => 4.kilobytes,
101
+ # * :path_prefix => klass.to_s.tableize,
102
+ # * :base_path => File.join(RAILS_ROOT, 'public'),
103
+ # * :convert_to => "JPEG",
104
+ # * :output_quality => "85",
105
+ # * :invalid_image_message => "Can't process the image.",
106
+ # * :image_too_small_message => "The image is too small.",
107
+ # * :image_too_big_message => "The image is too big.",
108
+ def default_options_for(klass)
109
+ {
110
+ :resize_to => "200x200",
111
+ :thumbnails => {},
112
+ :max_size => 12.megabytes,
113
+ :min_size => 4.kilobytes,
114
+ :path_prefix => klass.to_s.tableize,
115
+ :base_path => File.join(RAILS_ROOT, 'public'),
116
+ :convert_to => "JPEG",
117
+ :output_quality => "85",
118
+ :invalid_image_message => "Can't process the image.",
119
+ :image_too_small_message => "The image is too small.",
120
+ :image_too_big_message => "The image is too big."
121
+ }
122
+ end
123
+
124
+ end
125
+
126
+ module ClassMethods
127
+ # To use HasImage with a Rails model, all you have to do is add a column
128
+ # named "has_image_file." For configuration defaults, you might want to take
129
+ # a look at the default options specified in HasImage#default_options_for.
130
+ # The different setting options are described below.
131
+ #
132
+ # Options:
133
+ # * <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.
134
+ # * <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.
135
+ # * <tt>:min_size</tt> - Minimum file size allowed. It's recommended that you set this size in kilobytes.
136
+ # * <tt>:max_size</tt> - Maximum file size allowed. It's recommended that you set this size in megabytes.
137
+ # * <tt>:base_path</tt> - Where to install the images. You should probably leave this alone, except for tests.
138
+ # * <tt>:path_prefix</tt> - Where to install the images, relative to basepath. You should probably leave this alone.
139
+ # * <tt>:convert_to</tt> - An ImageMagick format to convert images to. Recommended formats: JPEG, PNG, GIF.
140
+ # * <tt>:output_quality</tt> - Image output quality passed to ImageMagick.
141
+ # * <tt>:invalid_image_message</tt> - The message that will be shown when the image data can't be processed.
142
+ # * <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.
143
+ # * <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.
144
+ #
145
+ # Examples:
146
+ # has_image # uses all default options
147
+ # has_image :resize_to "800x800", :thumbnails => {:square => "150x150"}
148
+ # has_image :resize_to "100x150", :max_size => 500.kilobytes
149
+ # has_image :invalid_image_message => "No se puede procesar la imagen."
150
+ def has_image(options = {})
151
+ options.assert_valid_keys(:resize_to, :thumbnails, :max_size, :min_size,
152
+ :path_prefix, :base_path, :convert_to, :output_quality,
153
+ :invalid_image_message, :image_too_big_message, :image_too_small_message)
154
+ options = HasImage.default_options_for(self).merge(options)
155
+ class_inheritable_accessor :has_image_options
156
+ write_inheritable_attribute(:has_image_options, options)
157
+
158
+ after_create :install_images
159
+ after_save :update_images
160
+ after_destroy :remove_images
161
+
162
+ validate_on_create :image_data_valid?
163
+
164
+ include ModelInstanceMethods
165
+ extend ModelClassMethods
166
+
167
+ end
168
+
169
+ end
170
+
171
+ module ModelInstanceMethods
172
+
173
+ # Sets the uploaded image data. Image data can be an instance of Tempfile,
174
+ # or an instance of any class than inherits from IO.
175
+ def image_data=(image_data)
176
+ return if image_data.blank?
177
+ storage.image_data = image_data
178
+ end
179
+
180
+ # Is the image data a file that ImageMagick can process, and is it within
181
+ # the allowed minimum and maximum sizes?
182
+ def image_data_valid?
183
+ return if !storage.temp_file
184
+ if storage.image_too_big?
185
+ errors.add_to_base(self.class.has_image_options[:image_too_big_message])
186
+ elsif storage.image_too_small?
187
+ errors.add_to_base(self.class.has_image_options[:image_too_small_message])
188
+ elsif !HasImage::Processor.valid?(storage.temp_file)
189
+ errors.add_to_base(self.class.has_image_options[:invalid_image_message])
190
+ end
191
+ end
192
+
193
+ # Gets the "web path" for the image, or optionally, its thumbnail.
194
+ def public_path(thumbnail = nil)
195
+ storage.public_path_for(self, thumbnail)
196
+ end
197
+
198
+ # Deletes the image from the storage.
199
+ def remove_images
200
+ return if has_image_file.blank?
201
+ storage.remove_images(self.id)
202
+ rescue Errno::ENOENT
203
+ logger.warn("Could not delete files for #{self.class.to_s} #{to_param}")
204
+ end
205
+
206
+ # Creates new images and removes the old ones when image_data has been
207
+ # set.
208
+ def update_images
209
+ return if storage.temp_file.blank?
210
+ storage.remove_images(self.id)
211
+ update_attribute(:has_image_file, storage.install_images(self.id))
212
+ end
213
+
214
+ # Processes and installs the image and its thumbnails.
215
+ def install_images
216
+ return if !storage.temp_file
217
+ update_attribute(:has_image_file, storage.install_images(self.id))
218
+ end
219
+
220
+ # Gets an instance of the underlying storage functionality. See
221
+ # HasImage::Storage.
222
+ def storage
223
+ @storage ||= HasImage::Storage.new(has_image_options)
224
+ end
225
+
226
+ end
227
+
228
+ module ModelClassMethods
229
+
230
+ # Get the hash of thumbnails set by the options specified when invoking
231
+ # HasImage::ClassMethods#has_image.
232
+ def thumbnails
233
+ has_image_options[:thumbnails]
234
+ end
235
+
236
+ end
237
+
238
+ end
239
+
240
+ if defined?(Rails) and defined?(ActiveRecord) and defined?(ActionController)
241
+ HasImage.enable
242
+ end
@@ -0,0 +1,49 @@
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
33
+ @processor = HasImage::Processor.new({:convert_to => "JPEG", :output_quality => "85"})
34
+ assert @processor.resize(temp_file("image.jpg"), "100x100")
35
+ end
36
+
37
+ def test_resize_and_convert
38
+ @processor = HasImage::Processor.new({:convert_to => "JPEG", :output_quality => "85"})
39
+ assert @processor.resize(temp_file("image.png"), "100x100")
40
+ end
41
+
42
+ def test_resize_should_fail_with_bad_image
43
+ @processor = HasImage::Processor.new({:convert_to => "JPEG", :output_quality => "85"})
44
+ assert_raises HasImage::ProcessorError do
45
+ @processor.resize(temp_file("bad_image.jpg"), "100x100")
46
+ end
47
+ end
48
+
49
+ 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,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: has_image
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Norman Clarke
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-07-23 00:00:00 -03:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: HasImage is a Ruby on Rails gem/plugin that allows you to attach images to ActiveRecord models.
17
+ email: norman@randomba.org
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README
24
+ - CHANGELOG
25
+ - FAQ
26
+ files:
27
+ - CHANGELOG
28
+ - FAQ
29
+ - MIT-LICENSE
30
+ - README
31
+ - init.rb
32
+ - lib/has_image.rb
33
+ - lib/has_image/processor.rb
34
+ - lib/has_image/storage.rb
35
+ - lib/has_image/view_helpers.rb
36
+ - Rakefile
37
+ has_rdoc: true
38
+ homepage: http://randomba.org
39
+ post_install_message:
40
+ rdoc_options:
41
+ - --main
42
+ - README
43
+ - --inline-source
44
+ - --line-numbers
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ version:
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ version:
59
+ requirements: []
60
+
61
+ rubyforge_project:
62
+ rubygems_version: 1.2.0
63
+ signing_key:
64
+ specification_version: 2
65
+ summary: Lets you attach images with thumbnails to active record models.
66
+ test_files:
67
+ - test_rails/database.yml
68
+ - test_rails/fixtures/bad_image.jpg
69
+ - test_rails/fixtures/image.jpg
70
+ - test_rails/fixtures/image.png
71
+ - test_rails/pic.rb
72
+ - test_rails/pic_test.rb
73
+ - test_rails/schema.rb
74
+ - test_rails/test_helper.rb
75
+ - test/processor_test.rb
76
+ - test/storage_test.rb