has_image 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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