dynamic_image 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2006-2010 Inge Jørgensen
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.rdoc ADDED
@@ -0,0 +1,59 @@
1
+ = DynamicImage
2
+
3
+ DynamicImage is a Rails plugin providing transparent uploading
4
+ and processing of image files.
5
+
6
+ Images are processed and cached on demand without need for any
7
+ configuration.
8
+
9
+
10
+ == Installation:
11
+
12
+ Install the gem:
13
+
14
+ gem install dynamic_image
15
+
16
+ Add the gem to your Gemfile:
17
+
18
+ gem 'dynamic_image'
19
+
20
+ Do the migrations:
21
+
22
+ rails generate dynamic_image migrations
23
+ rake db:migrate
24
+
25
+
26
+ == Getting started:
27
+
28
+ Use belongs_to_image in your models:
29
+
30
+ class User
31
+ belongs_to_image :profile_picture
32
+ end
33
+
34
+ Create a form:
35
+
36
+ <%= form_for @user, :html => {:multipart => true} do |f| %>
37
+ Name: <%= f.text_field :name %>
38
+ Profile picture: <%= f.file_field :profile_picture %>
39
+ <%= submit_tag "Save" %>
40
+ <% end %>
41
+
42
+ Use the dynamic_image_tag helper to show images:
43
+
44
+ <%= dynamic_image_tag @user.profile_picture, :size => '64x64' %>
45
+ <%= dynamic_image_tag @user.profile_picture, :size => '150x' %>
46
+
47
+
48
+ == Caching
49
+
50
+ Processing images on the fly is expensive. Therefore, page caching is enabled
51
+ by default, even in development mode. To disable page caching, add the following
52
+ line in your initializers or environment.rb:
53
+
54
+ DynamicImage.page_caching = false
55
+
56
+ == Copyright
57
+
58
+ Copyright © 2006-2010 Inge Jørgensen. See LICENSE for details.
59
+
data/Rakefile ADDED
@@ -0,0 +1,42 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ require "rake"
6
+
7
+ begin
8
+ require "jeweler"
9
+ Jeweler::Tasks.new do |gem|
10
+ gem.name = "dynamic_image"
11
+ gem.summary = "DynamicImage is a rails plugin providing transparent uploading and processing of image files."
12
+ gem.email = "inge@elektronaut.no"
13
+ gem.homepage = "http://github.com/elektronaut/dynamic_image"
14
+ gem.authors = ["Inge Jørgensen"]
15
+ gem.files = Dir["*", "{lib}/**/*", "{app}/**/*", "{config}/**/*"]
16
+ gem.add_dependency("rmagick", "~> 2.12.2")
17
+ gem.add_dependency("vector2d", "~> 1.0.0")
18
+ end
19
+ Jeweler::GemcutterTasks.new
20
+ rescue LoadError
21
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
22
+ end
23
+
24
+ desc 'Default: run unit tests.'
25
+ task :default => :test
26
+
27
+ desc 'Test the dynamic_image plugin.'
28
+ Rake::TestTask.new(:test) do |t|
29
+ t.libs << 'lib'
30
+ t.libs << 'test'
31
+ t.pattern = 'test/**/*_test.rb'
32
+ t.verbose = true
33
+ end
34
+
35
+ desc 'Generate documentation for the dynamic_image plugin.'
36
+ Rake::RDocTask.new(:rdoc) do |rdoc|
37
+ rdoc.rdoc_dir = 'rdoc'
38
+ rdoc.title = 'DynamicImage'
39
+ rdoc.options << '--line-numbers' << '--inline-source'
40
+ rdoc.rdoc_files.include('README')
41
+ rdoc.rdoc_files.include('lib/**/*.rb')
42
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.9.0
@@ -0,0 +1,79 @@
1
+ class ImagesController < ActionController::Base
2
+
3
+ after_filter :cache_dynamic_image
4
+ after_filter :run_garbage_collection_for_dynamic_image_controller
5
+
6
+ unloadable
7
+
8
+ public
9
+
10
+ # Return the requested image. Rescale, filter and cache it where appropriate.
11
+ def render_dynamic_image
12
+
13
+ render_missing_image and return unless Image.exists?(params[:id])
14
+ image = Image.find(params[:id])
15
+
16
+ minTime = Time.rfc2822(request.env["HTTP_IF_MODIFIED_SINCE"]) rescue nil
17
+ if minTime && image.created_at? && image.created_at <= minTime
18
+ render :text => '304 Not Modified', :status => 304
19
+ return
20
+ end
21
+
22
+ unless image.data?
23
+ logger.warn "Image #{image.id} exists, but has no data"
24
+ render_missing_image and return
25
+ end
26
+
27
+ if size = params[:size]
28
+ if size =~ /^x[\d]+$/ || size =~ /^[\d]+x$/
29
+ if params[:original]
30
+ image.cropped = false
31
+ end
32
+ size = Vector2d.new(size)
33
+ image_size = Vector2d.new(image.size)
34
+ size = image_size.constrain_both(size).round.to_s
35
+ end
36
+ imagedata = image.get_processed(size, params[:filterset])
37
+ else
38
+ imagedata = image
39
+ end
40
+
41
+ DynamicImage.dirty_memory = true # Flag memory for GC
42
+
43
+ if image
44
+ response.headers['Cache-Control'] = nil
45
+ response.headers['Last-Modified'] = imagedata.created_at.httpdate if imagedata.created_at?
46
+ send_data(
47
+ imagedata.data,
48
+ :filename => image.filename,
49
+ :type => image.content_type,
50
+ :disposition => 'inline'
51
+ )
52
+ end
53
+
54
+ end
55
+
56
+ protected
57
+
58
+ def render_missing_image
59
+ if self.respond_to?(:render_error)
60
+ render_error 404
61
+ else
62
+ render :status => 404, :text => "404: Image not found"
63
+ end
64
+ end
65
+
66
+ # Enforce caching of dynamic images, even if caching is turned off
67
+ def cache_dynamic_image
68
+ cache_setting = ActionController::Base.perform_caching
69
+ ActionController::Base.perform_caching = true
70
+ cache_page
71
+ ActionController::Base.perform_caching = cache_setting
72
+ end
73
+
74
+ # Perform garbage collection if necessary
75
+ def run_garbage_collection_for_dynamic_image_controller
76
+ DynamicImage.clean_dirty_memory
77
+ end
78
+
79
+ end
@@ -0,0 +1,183 @@
1
+ class Image < ActiveRecord::Base
2
+ unloadable
3
+
4
+ binary_storage :data, :sha1_hash
5
+
6
+ validates_format_of :content_type,
7
+ :with => /^image/,
8
+ :message => "you can only upload pictures"
9
+
10
+ attr_accessor :filterset, :data_checked, :skip_maxsize
11
+
12
+ # Sanitize the filename and set the name to the filename if omitted
13
+ validate do |image|
14
+ image.name = File.basename(image.filename, ".*") if !image.name || image.name.strip == ""
15
+ image.filename = image.friendly_file_name(image.filename)
16
+ if image.cropped?
17
+ image.errors.add(:crop_start, "must be a vector") unless image.crop_start =~ /^[\d]+x[\d]+$/
18
+ image.errors.add(:crop_size, "must be a vector") unless image.crop_size =~ /^[\d]+x[\d]+$/
19
+ else
20
+ image.crop_size = image.original_size
21
+ image.crop_start = "0x0"
22
+ end
23
+ end
24
+
25
+ # Create the binary from an image file.
26
+ def imagefile=(image_file)
27
+ self.filename = image_file.original_filename rescue File.basename( image_file.path )
28
+ self.content_type = image_file.content_type.chomp rescue "image/"+image_file.path.split(/\./).last.downcase.gsub(/jpg/,"jpeg") # ugly hack
29
+ set_image_data(image_file.read)
30
+ end
31
+
32
+ # Return the image hotspot
33
+ def hotspot
34
+ (self.hotspot?) ? self.hotspot : (Vector2d.new(self.size) * 0.5).round.to_s
35
+ end
36
+
37
+ # Check the image data
38
+ def set_image_data(data)
39
+ self.data = data
40
+ if self.data?
41
+ image = Magick::ImageList.new.from_blob(self.data)
42
+ size = Vector2d.new(image.columns, image.rows)
43
+ if DynamicImage.crash_size
44
+ crashsize = Vector2d.new(DynamicImage.crash_size)
45
+ if (size.x > crashsize.x || size.y > crashsize.y)
46
+ raise "Image too large!"
47
+ end
48
+ end
49
+ if DynamicImage.max_size && !self.skip_maxsize
50
+ maxsize = Vector2d.new(DynamicImage.max_size)
51
+ if (size.x > maxsize.x || size.y > maxsize.y)
52
+ size = size.constrain_both(maxsize).round
53
+ image.resize!(size.x, size.y)
54
+ self.data = image.to_blob
55
+ end
56
+ end
57
+ # Convert image to a proper format
58
+ unless image.format =~ /(JPEG|PNG|GIF)/
59
+ self.data = image.to_blob{self.format = 'JPEG'; self.quality = 90}
60
+ self.filename += ".jpg"
61
+ self.content_type = "image/jpeg"
62
+ end
63
+ self.original_size = size.round.to_s
64
+ end
65
+ end
66
+
67
+ # Returns the image width
68
+ def original_width
69
+ Vector2d.new(self.original_size).x.to_i
70
+ end
71
+
72
+ # Returns the image height
73
+ def original_height
74
+ Vector2d.new(self.original_size).y.to_i
75
+ end
76
+
77
+ def crop_start_x
78
+ Vector2d.new(self.crop_start).x.to_i
79
+ end
80
+ def crop_start_y
81
+ Vector2d.new(self.crop_start).y.to_i
82
+ end
83
+ def crop_width
84
+ Vector2d.new(self.crop_size).x.to_i
85
+ end
86
+ def crop_height
87
+ Vector2d.new(self.crop_size).y.to_i
88
+ end
89
+
90
+ # Returns original or cropped size
91
+ def size
92
+ (self.cropped?) ? self.crop_size : self.original_size
93
+ end
94
+
95
+ def size=(new_size)
96
+ self.original_size = new_size
97
+ end
98
+
99
+ # Convert file name to a more file system friendly one.
100
+ # TODO: international chars
101
+ def friendly_file_name( file_name )
102
+ [["æ","ae"], ["ø","oe"], ["å","aa"]].each do |int|
103
+ file_name = file_name.gsub(int[0], int[1])
104
+ end
105
+ File.basename(file_name).gsub(/[^\w\d\.-]/, "_")
106
+ end
107
+
108
+ # Get the base part of a filename
109
+ def base_part_of(file_name)
110
+ name = File.basename(file_name)
111
+ name.gsub(/[ˆ\w._-]/, '')
112
+ end
113
+
114
+ # Rescale and crop the image, and return it as a blob.
115
+ def rescaled_and_cropped_data(*args)
116
+ DynamicImage.dirty_memory = true # Flag to perform GC
117
+ image_data = Magick::ImageList.new.from_blob(self.data)
118
+
119
+ if self.cropped?
120
+ cropped_start = Vector2d.new(self.crop_start).round
121
+ cropped_size = Vector2d.new(self.crop_size).round
122
+ image_data = image_data.crop(cropped_start.x, cropped_start.y, cropped_size.x, cropped_size.y, true)
123
+ end
124
+
125
+ size = Vector2d.new(self.size)
126
+ rescale_size = size.dup.constrain_one(args).round # Rescale dimensions
127
+ crop_to_size = Vector2d.new(args).round # Crop size
128
+ new_hotspot = Vector2d.new(hotspot) * (rescale_size / size) # Recalculated hotspot
129
+ rect = [(new_hotspot-(crop_to_size/2)).round, (new_hotspot+(crop_to_size/2)).round] # Array containing crop coords
130
+
131
+ # Adjustments
132
+ x = rect[0].x; rect.each{|r| r.x += (x.abs)} if x < 0
133
+ y = rect[0].y; rect.each{|r| r.y += (y.abs)} if y < 0
134
+ x = rect[1].x; rect.each{|r| r.x -= (x-rescale_size.x)} if x > rescale_size.x
135
+ y = rect[1].y; rect.each{|r| r.y -= (y-rescale_size.y)} if y > rescale_size.y
136
+
137
+ rect[0].round!
138
+ rect[1].round!
139
+
140
+ image_data = image_data.resize(rescale_size.x, rescale_size.y)
141
+ image_data = image_data.crop(rect[0].x, rect[0].y, crop_to_size.x, crop_to_size.y)
142
+ image_data.to_blob{self.quality = 90}
143
+ end
144
+
145
+ def constrain_size(*max_size)
146
+ Vector2d.new(self.size).constrain_both(max_size.flatten).round.to_s
147
+ end
148
+
149
+ # Get a duplicate image with resizing and filters applied.
150
+ def get_processed(size, filterset=nil)
151
+ size = Vector2d.new(size).round.to_s
152
+ processed_image = Image.new
153
+ processed_image.filterset = filterset || 'default'
154
+ processed_image.data = self.rescaled_and_cropped_data(size)
155
+ processed_image.size = size
156
+ processed_image.apply_filters
157
+ processed_image
158
+ end
159
+
160
+ # Apply filters to image data
161
+ def apply_filters
162
+ filterset_name = self.filterset || 'default'
163
+ filterset = DynamicImage::Filterset[filterset_name]
164
+ if filterset
165
+ DynamicImage.dirty_memory = true # Flag for GC
166
+ data = Magick::ImageList.new.from_blob(self.data)
167
+ data = filterset.process(data)
168
+ self.data = data.to_blob
169
+ end
170
+ end
171
+
172
+ # Decorates to_json with additional attributes
173
+ def to_json
174
+ attributes.merge({
175
+ :original_width => self.original_width,
176
+ :original_height => self.original_height,
177
+ :crop_width => self.crop_width,
178
+ :crop_height => self.crop_height,
179
+ :crop_start_x => self.crop_start_x,
180
+ :crop_start_y => self.crop_start_y
181
+ }).to_json
182
+ end
183
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,16 @@
1
+ # Rails 3 routes
2
+ Rails.application.routes.draw do |map|
3
+ match "dynamic_images/:id/:original(/:size(/:filterset))/*filename" => "images#render_dynamic_image", :size => /\d*x\d*/, :original => /original/
4
+ match "dynamic_images/:id(/:size(/:filterset))/*filename" => "images#render_dynamic_image", :size => /\d*x\d*/
5
+ # Legacy
6
+ match "dynamic_image/:id/:original(/:size(/:filterset))/*filename" => "images#render_dynamic_image", :size => /\d*x\d*/, :original => /original/
7
+ match "dynamic_image/:id(/:size(/:filterset))/*filename" => "images#render_dynamic_image", :size => /\d*x\d*/
8
+ end
9
+
10
+ # Rails 2 routes
11
+ #@set.add_route('dynamic_image/:id/:original/:size/:filterset/*filename', {:controller => 'images', :action => 'render_dynamic_image', :requirements => { :size => /[\d]*x[\d]*/, :original => /original/ }})
12
+ #@set.add_route('dynamic_image/:id/:original/:size/*filename', {:controller => 'images', :action => 'render_dynamic_image', :requirements => { :size => /[\d]*x[\d]*/, :original => /original/ }})
13
+ #@set.add_route('dynamic_image/:id/:original/*filename', {:controller => 'images', :action => 'render_dynamic_image', :requirements => { :original => /original/ }})
14
+ #@set.add_route('dynamic_image/:id/:size/:filterset/*filename', {:controller => 'images', :action => 'render_dynamic_image', :requirements => { :size => /[\d]*x[\d]*/ }})
15
+ #@set.add_route('dynamic_image/:id/:size/*filename', {:controller => 'images', :action => 'render_dynamic_image', :requirements => { :size => /[\d]*x[\d]*/ }})
16
+ #@set.add_route('dynamic_image/:id/*filename', {:controller => 'images', :action => 'render_dynamic_image'})
@@ -0,0 +1,68 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{dynamic_image}
8
+ s.version = "0.9.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Inge J\303\270rgensen"]
12
+ s.date = %q{2010-05-19}
13
+ s.email = %q{inge@elektronaut.no}
14
+ s.extra_rdoc_files = [
15
+ "LICENSE",
16
+ "README.rdoc"
17
+ ]
18
+ s.files = [
19
+ "LICENSE",
20
+ "README.rdoc",
21
+ "Rakefile",
22
+ "VERSION",
23
+ "app/controllers/images_controller.rb",
24
+ "app/models/image.rb",
25
+ "config/routes.rb",
26
+ "dynamic_image.gemspec",
27
+ "init.rb",
28
+ "install.rb",
29
+ "lib/binary_storage.rb",
30
+ "lib/binary_storage/active_record_extensions.rb",
31
+ "lib/binary_storage/blob.rb",
32
+ "lib/dynamic_image.rb",
33
+ "lib/dynamic_image/active_record_extensions.rb",
34
+ "lib/dynamic_image/engine.rb",
35
+ "lib/dynamic_image/filterset.rb",
36
+ "lib/dynamic_image/helper.rb",
37
+ "lib/generators/dynamic_image/USAGE",
38
+ "lib/generators/dynamic_image/dynamic_image_generator.rb",
39
+ "lib/generators/dynamic_image/templates/migrations/create_images.rb",
40
+ "uninstall.rb"
41
+ ]
42
+ s.homepage = %q{http://github.com/elektronaut/dynamic_image}
43
+ s.rdoc_options = ["--charset=UTF-8"]
44
+ s.require_paths = ["lib"]
45
+ s.rubygems_version = %q{1.3.6}
46
+ s.summary = %q{DynamicImage is a rails plugin providing transparent uploading and processing of image files.}
47
+ s.test_files = [
48
+ "test/dynamic_image_test.rb",
49
+ "test/test_helper.rb"
50
+ ]
51
+
52
+ if s.respond_to? :specification_version then
53
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
54
+ s.specification_version = 3
55
+
56
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
57
+ s.add_runtime_dependency(%q<rmagick>, ["~> 2.12.2"])
58
+ s.add_runtime_dependency(%q<vector2d>, ["~> 1.0.0"])
59
+ else
60
+ s.add_dependency(%q<rmagick>, ["~> 2.12.2"])
61
+ s.add_dependency(%q<vector2d>, ["~> 1.0.0"])
62
+ end
63
+ else
64
+ s.add_dependency(%q<rmagick>, ["~> 2.12.2"])
65
+ s.add_dependency(%q<vector2d>, ["~> 1.0.0"])
66
+ end
67
+ end
68
+
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'dynamic_image'
data/install.rb ADDED
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -0,0 +1,144 @@
1
+ require 'binary_storage'
2
+
3
+ module BinaryStorage
4
+ module ActiveRecordExtensions
5
+
6
+ def self.included(base)
7
+ base.send(:extend, BinaryStorage::ActiveRecordExtensions::ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+
12
+ def register_binary(klass, binary_name, binary_column)
13
+ @@binary_columns ||= {}
14
+ @@binary_columns[klass] ||= {}
15
+ @@binary_columns[klass][binary_name] = binary_column
16
+ end
17
+
18
+ def binary_column(klass, binary_name)
19
+ if @@binary_columns && @@binary_columns[klass] && @@binary_columns[klass][binary_name]
20
+ @@binary_columns[klass][binary_name]
21
+ else
22
+ nil
23
+ end
24
+ end
25
+
26
+ # Count existing references to a binary
27
+ def binary_reference_count(hash_string)
28
+ references = 0
29
+ if @@binary_columns
30
+ @@binary_columns.each do |klass, binaries|
31
+ binaries.each do |binary_name, binary_column|
32
+ references += klass.count(:all, :conditions => ["`#{binary_column} = ?`", hash_string])
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ def binary_storage(binary_name, binary_column)
39
+ binary_name = binary_name.to_s
40
+ binary_column = binary_column.to_s
41
+
42
+ register_binary(self, binary_name, binary_column)
43
+
44
+ class_eval <<-end_eval
45
+ before_save do |binary_model|
46
+ binary_model.save_binary("#{binary_name}")
47
+ end
48
+
49
+ after_destroy do |model|
50
+ binary_model.destroy_binary("#{binary_name}")
51
+ end
52
+
53
+ def #{binary_name}
54
+ self.get_binary_data("#{binary_name}")
55
+ end
56
+
57
+ def #{binary_name}=(binary_data)
58
+ self.set_binary_data("#{binary_name}", binary_data)
59
+ end
60
+
61
+ def #{binary_name}?
62
+ self.has_binary_data?("#{binary_name}")
63
+ end
64
+ end_eval
65
+
66
+ send(:include, BinaryStorage::ActiveRecordExtensions::InstanceMethods)
67
+ end
68
+ end
69
+
70
+ module InstanceMethods
71
+
72
+ def binaries
73
+ @binaries ||= {}
74
+ end
75
+
76
+ def binary_column(binary_name)
77
+ if column_name = self.class.binary_column(self.class, binary_name)
78
+ column_name
79
+ else
80
+ raise "Binary column #{binary_name} not defined!"
81
+ end
82
+ end
83
+
84
+ def binary_hash_string(binary_name)
85
+ self.attributes[binary_column(binary_name)]
86
+ end
87
+
88
+ def save_binary(binary_name)
89
+ if binaries.has_key?(binary_name)
90
+ if binary = binaries[binary_name]
91
+ binary.save
92
+ self.attributes = self.attributes.merge({binary_column(binary_name).to_sym => binary.hash_string})
93
+ else
94
+ self.attributes = self.attributes.merge({binary_column(binary_name).to_sym => nil})
95
+ end
96
+ end
97
+ end
98
+
99
+ def destroy_binary(binary_name)
100
+ if binary = binaries[binary_name]
101
+ if hash_string = binary.hash_string
102
+ references = self.class.binary_reference_count
103
+ if references < 1
104
+ binary.delete!
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ def get_binary_data(binary_name)
111
+ # Set directly?
112
+ if binary = binaries[binary_name]
113
+ binary.data
114
+
115
+ # Try loading
116
+ elsif hash_string = binary_hash_string(binary_name)
117
+ if binary = BinaryStorage::Blob.find(hash_string)
118
+ binaries[binary_name] = binary # Cache it
119
+ binary.data
120
+ else
121
+ nil
122
+ end
123
+ end
124
+ end
125
+
126
+ def set_binary_data(binary_name, binary_data)
127
+ binary = (binary_data) ? BinaryStorage::Blob.new(binary_data) : nil
128
+ binaries[binary_name] = binary
129
+ end
130
+
131
+ def has_binary_data?(binary_name)
132
+ if binaries[binary_name]
133
+ true
134
+ else
135
+ hash_string = binary_hash_string(binary_name)
136
+ (hash_string && BinaryStorage::Blob.exists?(hash_string)) ? true : false
137
+ end
138
+ end
139
+ end
140
+
141
+ end
142
+ end
143
+
144
+ ActiveRecord::Base.send(:include, BinaryStorage::ActiveRecordExtensions)
@@ -0,0 +1,105 @@
1
+ module BinaryStorage
2
+ class Blob
3
+
4
+ class << self
5
+ def find(hash_string)
6
+ blob = self.new(:hash_string => hash_string)
7
+ return nil unless blob.exists?
8
+ blob.load
9
+ blob
10
+ end
11
+
12
+ def exists?(hash_string)
13
+ self.new(:hash_string => hash_string).exists?
14
+ end
15
+
16
+ def create(data)
17
+ blob = self.new(data)
18
+ blob.save
19
+ blob
20
+ end
21
+
22
+ def storage_dir(hash_string=nil)
23
+ root = BinaryStorage.storage_dir
24
+ (hash_string) ? File.join(root, hash_string.match(/^(..)/)[1]) : root
25
+ end
26
+
27
+ def storage_path(hash_string)
28
+ File.join(storage_dir(hash_string), hash_string.gsub(/^(..)/, ''))
29
+ end
30
+ end
31
+
32
+ def initialize(*args)
33
+ args = *args
34
+ options = {
35
+ :hash_string => nil,
36
+ :data => nil
37
+ }
38
+ if args.kind_of?(Hash)
39
+ options.merge!(args)
40
+ else
41
+ options[:data] = args
42
+ end
43
+ @hash_string = options[:hash_string]
44
+ @data = options[:data]
45
+ end
46
+
47
+ def data
48
+ @data
49
+ end
50
+
51
+ def data=(new_data)
52
+ @hash_string = nil
53
+ @data = new_data
54
+ end
55
+
56
+ def hash_string
57
+ unless @hash_string
58
+ if @data
59
+ @hash_string = BinaryStorage.hexdigest(data)
60
+ else
61
+ raise "Binary has no data!"
62
+ end
63
+ end
64
+ @hash_string
65
+ end
66
+
67
+ def storage_dir
68
+ BinaryStorage::Blob.storage_dir(hash_string)
69
+ end
70
+
71
+ def storage_path
72
+ BinaryStorage::Blob.storage_path(hash_string)
73
+ end
74
+
75
+ def exists?
76
+ File.exists?(storage_path)
77
+ end
78
+
79
+ def empty?
80
+ (hash_string && !exists?) || !data || data.empty?
81
+ end
82
+
83
+ def load
84
+ raise "File not found" unless exists?
85
+ @data = File.open(storage_path, "rb") {|io| io.read }
86
+ end
87
+
88
+ def delete
89
+ if exists?
90
+ FileUtils.rm(storage_path)
91
+ end
92
+ end
93
+
94
+ def save
95
+ unless exists?
96
+ FileUtils.mkdir_p(storage_dir)
97
+ file = File.new(storage_path, 'wb')
98
+ file.write(@data)
99
+ file.close
100
+ end
101
+ return true
102
+ end
103
+
104
+ end
105
+ end
@@ -0,0 +1,28 @@
1
+ require 'tempfile'
2
+ require 'digest/sha1'
3
+
4
+ require 'rails'
5
+ require 'active_record'
6
+
7
+ require File.join(File.dirname(__FILE__), 'binary_storage/active_record_extensions')
8
+ require File.join(File.dirname(__FILE__), 'binary_storage/blob')
9
+
10
+ module BinaryStorage
11
+ class << self
12
+ def storage_dir
13
+ @@storage_dir ||= Rails.root.join('db/binary_storage', Rails.env)
14
+ end
15
+
16
+ def storage_dir=(new_storage_dir)
17
+ @@storage_dir = new_storage_dir
18
+ end
19
+
20
+ def hexdigest_file(path)
21
+ Digest::SHA1.file(path).hexdigest
22
+ end
23
+
24
+ def hexdigest(string)
25
+ Digest::SHA1.hexdigest(string)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,57 @@
1
+ require 'dynamic_image'
2
+
3
+ module DynamicImage
4
+ module ActiveRecordExtensions
5
+
6
+ def self.included(base)
7
+ base.send :extend, ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ # By using <tt>belongs_to_image</tt> over <tt>belongs_to</tt>, you gain the ability to
12
+ # set the image directly from an uploaded file. This works exactly like <tt>belongs_to</tt>,
13
+ # except the class name will default to 'Image' - not the name of the association.
14
+ #
15
+ # Example:
16
+ #
17
+ # # Model code
18
+ # class Person < ActiveRecord::Base
19
+ # belongs_to_image :mugshot
20
+ # end
21
+ #
22
+ # # View code
23
+ # <% form_for 'person', @person, :html => {:multipart => true} do |f| %>
24
+ # <%= f.file_field :mugshot %>
25
+ # <% end %>
26
+ #
27
+ def belongs_to_image(association_id, options={})
28
+ options[:class_name] ||= 'Image'
29
+ options[:foreign_key] ||= options[:class_name].downcase+'_id'
30
+ belongs_to association_id, options
31
+
32
+ # Overwrite the setter method
33
+ class_eval <<-end_eval
34
+ alias_method :associated_#{association_id}=, :#{association_id}=
35
+ def #{association_id}=(img_obj)
36
+ # Convert a Tempfile to a proper Image
37
+ unless img_obj.kind_of?(ActiveRecord::Base)
38
+ DynamicImage.dirty_memory = true # Flag for GC
39
+ img_obj = Image.create(:imagefile => img_obj)
40
+ end
41
+ # Quietly skip blank strings
42
+ unless img_obj.kind_of?(String) && img_obj.blank?
43
+ self.associated_#{association_id} = img_obj
44
+ end
45
+ end
46
+ end_eval
47
+
48
+ send :include, DynamicImage::ActiveRecordExtensions::InstanceMethods
49
+ end
50
+ end
51
+
52
+ module InstanceMethods
53
+ end
54
+ end
55
+ end
56
+
57
+ ActiveRecord::Base.send(:include, DynamicImage::ActiveRecordExtensions)
@@ -0,0 +1,6 @@
1
+ require 'dynamic_image'
2
+
3
+ module DynamicImage
4
+ class Engine < Rails::Engine
5
+ end
6
+ end
@@ -0,0 +1,79 @@
1
+ require 'dynamic_image'
2
+
3
+ module DynamicImage
4
+
5
+ @@filtersets = Hash.new
6
+
7
+ # Singleton methods for the filtersets hash.
8
+ class << @@filtersets
9
+ def names; keys; end
10
+ end
11
+
12
+ # Accessor for the filtersets hash. Installed filter names are available through the <tt>names</tt> method. Example:
13
+ # @filter_names = DynamicImage.filtersets.names
14
+ def self.filtersets
15
+ @@filtersets
16
+ end
17
+
18
+ # Base class for filter sets. Extending this with your own subclasses will automatically enable them for use.
19
+ # You'll need to overwrite <tt>Filterset.process</tt> in order to make your filter useful. Note that it's a class
20
+ # method.
21
+ #
22
+ # === Example
23
+ #
24
+ # class BlogThumbnailsFilterset < DynamicImage::Filterset
25
+ # def self.process(image)
26
+ # image = image.sepiatone # convert the image to sepia tones
27
+ # end
28
+ # end
29
+ #
30
+ # The filter set is now available for use in your application:
31
+ #
32
+ # <%= dynamic_image_tag( @blog_post.image, :size => "120x100", :filterset => 'blog_thumbnails' ) %>
33
+ #
34
+ # === Applying effects by default
35
+ #
36
+ # If <tt>Image.get_oricessed</tt> is called without filters, it will look for a set named 'default'.
37
+ # This means that you can automatically apply effects on resized images by defining a class called <tt>DefaultFilterset</tt>:
38
+ #
39
+ # class DefaultFilterset < DynamicImage::Filterset
40
+ # def self.process(image)
41
+ # image = image.unsharp_mask # apply unsharp mask on images by default.
42
+ # end
43
+ # end
44
+ #
45
+ # === Chaining filters
46
+ #
47
+ # You can only apply one filterset on an image, but compound filters can easily be created:
48
+ #
49
+ # class CompoundFilterset < DynamicImage::Filterset
50
+ # def self.process(image)
51
+ # image = MyFirstFilterset.process(image)
52
+ # image = SomeOtherFilterset.process(image)
53
+ # image = DefaultFilterset.process(image)
54
+ # end
55
+ # end
56
+ #
57
+ class Filterset
58
+ include ::Magick
59
+
60
+ # Detect inheritance and store the new filterset in the lookup table.
61
+ def self.inherited(sub)
62
+ filter_name = sub.name.gsub!( /Filterset$/, '' ).underscore
63
+ DynamicImage.filtersets[filter_name] = sub
64
+ end
65
+
66
+ # Get a Filterset class by name. Accepts a symbol or string, CamelCase and under_scores both work.
67
+ def self.[](filter_name)
68
+ filter_name = filter_name.to_s if filter_name.kind_of? Symbol
69
+ filter_name = filter_name.underscore
70
+ DynamicImage.filtersets[filter_name] || nil
71
+ end
72
+
73
+ # Process the image. This is a dummy method, you should overwrite it in your subclass.
74
+ def self.process(image)
75
+ # This is a stub
76
+ end
77
+
78
+ end
79
+ end
@@ -0,0 +1,121 @@
1
+ require 'dynamic_image'
2
+
3
+ module DynamicImage
4
+ module Helper
5
+
6
+ # Returns an hash consisting of the URL to the dynamic image and parsed options. This is mostly for internal use by
7
+ # dynamic_image_tag and dynamic_image_url.
8
+ def dynamic_image_options(image, options = {})
9
+ options.symbolize_keys!
10
+
11
+ options = {:crop => false}.merge(options)
12
+ url_options = {:controller => "/images", :action => :render_dynamic_image, :id => image}
13
+
14
+ if options[:original]
15
+ url_options[:original] = 'original'
16
+ options.delete(:original)
17
+ end
18
+
19
+ # Image sizing
20
+ if options[:size]
21
+ new_size = Vector2d.new(options[:size])
22
+ image_size = Vector2d.new(image.size)
23
+
24
+ unless options[:upscale]
25
+ new_size.x = image_size.x if new_size.x > 0 && new_size.x > image_size.x
26
+ new_size.y = image_size.y if new_size.y > 0 && new_size.y > image_size.y
27
+ end
28
+
29
+ unless options[:crop]
30
+ new_size = image_size.constrain_both(new_size)
31
+ end
32
+
33
+ options[:size] = new_size.round.to_s
34
+ url_options[:size] = options[:size]
35
+ end
36
+ options.delete :crop
37
+
38
+ if options[:no_size_attr]
39
+ options.delete :no_size_attr
40
+ options.delete :size
41
+ end
42
+
43
+ # Filterset
44
+ if options[:filterset]
45
+ url_options[:filterset] = options[:filterset]
46
+ options.delete :filterset
47
+ end
48
+
49
+ # Filename
50
+ if options[:filename]
51
+ filename = options[:filename]
52
+ unless filename =~ /\.[\w]{1,4}$/
53
+ filename += "." + image.filename.split(".").last
54
+ end
55
+ url_options[:filename] = filename
56
+ else
57
+ url_options[:filename] = image.filename
58
+ end
59
+
60
+ # Alt attribute
61
+ options[:alt] ||= image.name if image.name?
62
+ options[:alt] ||= image.filename.split('.').first.capitalize
63
+
64
+ if options.has_key?(:only_path)
65
+ url_options[:only_path] = options[:only_path]
66
+ options[:only_path] = nil
67
+ end
68
+ if options.has_key?(:host)
69
+ url_options[:host] = options[:host]
70
+ options[:host] = nil
71
+ end
72
+
73
+ {:url => url_for(url_options), :options => options}
74
+ end
75
+
76
+ # Returns an image tag for the provided image model, works similar to the rails <tt>image_tag</tt> helper.
77
+ # The <tt>alt</tt> tag is set to the image title unless explicitly provided.
78
+ #
79
+ # The following options are supported (the rest will be forwarded to <tt>image_tag</tt>):
80
+ #
81
+ # :size - Resize the image to fit these proportions. Size is given as a string with the format
82
+ # '100x100'. Either dimension can be omitted, for example: '100x'
83
+ # :crop - Boolean, default: false. Crop the image to the size given.
84
+ # :no_size_attr - Boolean, default: false. Do not include width and height attributes in the image tag.
85
+ # :filterset - Apply the given filterset to the image
86
+ #
87
+ # == Examples
88
+ #
89
+ # Tag for original image, without rescaling:
90
+ # <%= dynamic_image_tag(@image) %>
91
+ #
92
+ # Tag for image, rescaled to fit within 100x100 (size will be 100x100 or smaller):
93
+ # <%= dynamic_image_tag(@image, :size => "100x100") %>
94
+ #
95
+ # Tag for image, cropped and rescaled to 100x100 (size will be 100x100 in all cases):
96
+ # <%= dynamic_image_tag(@image, :size => "100x100", :crop => true) %>
97
+ #
98
+ # Tag for image with a filter set applied:
99
+ # <%= dynamic_image_tag(@image, :size => "100x100", :filterset => @filterset) %>
100
+ #
101
+ # Tag for image with a named filter set applied:
102
+ # <%= dynamic_image_tag(@image, :size => "100x100", :filterset => "thumbnails") %>
103
+ #
104
+ # Tag for image without the width/height attributes, and with a custom alt attribute
105
+ # <%= dynamic_image_tag(@image, :size => "100x100", :no_size_attr => true, :alt => "Thumbnail for post" %>
106
+
107
+ def dynamic_image_tag(image, options = {})
108
+ parsed_options = dynamic_image_options(image, options)
109
+ image_tag(parsed_options[:url], parsed_options[:options] ).gsub(/\?[\d]+/,'')
110
+ end
111
+
112
+ # Returns an url corresponding to the provided image model.
113
+ # Special options are documented in ApplicationHelper.dynamic_image_tag, only <tt>:size</tt>, <tt>:filterset</tt> and <tt>:crop</tt> apply.
114
+ def dynamic_image_url(image, options = {})
115
+ parsed_options = dynamic_image_options(image, options)
116
+ parsed_options[:url]
117
+ end
118
+ end
119
+ end
120
+
121
+ ActionView::Base.send(:include, DynamicImage::Helper)
@@ -0,0 +1,77 @@
1
+ require 'tempfile'
2
+ require 'digest/sha1'
3
+
4
+ # Gem dependencies
5
+ require 'rmagick'
6
+ require 'vector2d'
7
+ require 'rails'
8
+ require 'action_controller'
9
+ require 'active_support'
10
+ require 'active_record'
11
+
12
+ require 'binary_storage'
13
+
14
+ if Rails::VERSION::MAJOR == 3
15
+ # Load the engine
16
+ require 'dynamic_image/engine' if defined?(Rails)
17
+ end
18
+
19
+ require 'dynamic_image/active_record_extensions'
20
+ require 'dynamic_image/filterset'
21
+ require 'dynamic_image/helper'
22
+
23
+ module DynamicImage
24
+ @@dirty_memory = false
25
+ @@page_caching = true
26
+
27
+ class << self
28
+
29
+ def dirty_memory=(flag)
30
+ @@dirty_memory = flag
31
+ end
32
+
33
+ def dirty_memory
34
+ @@dirty_memory
35
+ end
36
+
37
+ def page_caching=(flag)
38
+ @@page_caching = flag
39
+ end
40
+
41
+ def page_caching
42
+ @@page_caching
43
+ end
44
+
45
+ def max_size
46
+ @@max_size ||= "2000x2000"
47
+ end
48
+
49
+ def max_size=(new_max_size)
50
+ @@max_size = new_max_size
51
+ end
52
+
53
+ def crash_size
54
+ @@crash_size ||= "10000x10000"
55
+ end
56
+
57
+ def crash_size=(new_crash_size)
58
+ @@crash_size = new_crash_size
59
+ end
60
+
61
+ # RMagick stores image data internally, Ruby doesn't see the used memory.
62
+ # This method performs garbage collection if @@dirty_memory has been flagged.
63
+ # More details here: http://rubyforge.org/forum/message.php?msg_id=1995
64
+ def clean_dirty_memory(options={})
65
+ options.symbolize_keys!
66
+ if @@dirty_memory || options[:force]
67
+ gc_disabled = GC.enable
68
+ GC.start
69
+ GC.disable if gc_disabled
70
+ @@dirty_memory = false
71
+ true
72
+ else
73
+ false
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,6 @@
1
+ Description:
2
+ Creates the migrations
3
+
4
+ Usage:
5
+ rails generate dynamic_image
6
+
@@ -0,0 +1,38 @@
1
+ # Rails 2: class DynamicImageGenerator < Rails::Generator::NamedBase
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/migration'
5
+
6
+ class DynamicImageGenerator < Rails::Generators::Base
7
+
8
+ include Rails::Generators::Migration
9
+
10
+ class << self
11
+ def source_root
12
+ @source_root ||= File.join(File.dirname(__FILE__), 'templates')
13
+ end
14
+
15
+ def next_migration_number(dirname)
16
+ if ActiveRecord::Base.timestamped_migrations
17
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
18
+ else
19
+ "%.3d" % (current_migration_number(dirname) + 1)
20
+ end
21
+ end
22
+
23
+ end
24
+
25
+ def migrations
26
+ migration_template 'migrations/create_images.rb', 'db/migrate/create_images.rb'
27
+ end
28
+
29
+ # def manifest
30
+ # record do |m|
31
+ # #m.file 'controllers/images_controller.rb', 'app/controllers/images_controller.rb'
32
+ # #m.file 'models/image.rb', 'app/models/image.rb'
33
+ # #m.file 'models/binary.rb', 'app/models/binary.rb'
34
+ # m.file 'migrations/20090909231629_create_binaries.rb', 'db/migrate/20090909231629_create_binaries.rb'
35
+ # m.file 'migrations/20090909231630_create_images.rb', 'db/migrate/20090909231630_create_images.rb'
36
+ # end
37
+ # end
38
+ end
@@ -0,0 +1,22 @@
1
+ class CreateImages < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :images do |t|
4
+ t.column :name, :string
5
+ t.column :filename, :string
6
+ t.column :content_type, :string
7
+ t.column :original_size, :string
8
+ t.column :hotspot, :string
9
+ t.column :sha1_hash, :string
10
+ t.column :cropped, :boolean, :null => false, :default => false
11
+ t.column :crop_start, :string
12
+ t.column :crop_size, :string
13
+ t.column :created_at, :datetime
14
+ t.column :updated_at, :datetime
15
+ t.column :filters, :text
16
+ end
17
+ end
18
+
19
+ def self.down
20
+ drop_table :images
21
+ end
22
+ end
@@ -0,0 +1,8 @@
1
+ require 'test_helper'
2
+
3
+ class DynamicImageTest < ActiveSupport::TestCase
4
+ # Replace this with your real tests.
5
+ test "the truth" do
6
+ assert true
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ require 'rubygems'
2
+ require 'active_support'
3
+ require 'active_support/test_case'
data/uninstall.rb ADDED
@@ -0,0 +1 @@
1
+ # Uninstall hook code here
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dynamic_image
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 9
8
+ - 0
9
+ version: 0.9.0
10
+ platform: ruby
11
+ authors:
12
+ - "Inge J\xC3\xB8rgensen"
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-05-19 00:00:00 +02:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rmagick
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ~>
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 2
29
+ - 12
30
+ - 2
31
+ version: 2.12.2
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: vector2d
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ~>
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 1
43
+ - 0
44
+ - 0
45
+ version: 1.0.0
46
+ type: :runtime
47
+ version_requirements: *id002
48
+ description:
49
+ email: inge@elektronaut.no
50
+ executables: []
51
+
52
+ extensions: []
53
+
54
+ extra_rdoc_files:
55
+ - LICENSE
56
+ - README.rdoc
57
+ files:
58
+ - LICENSE
59
+ - README.rdoc
60
+ - Rakefile
61
+ - VERSION
62
+ - app/controllers/images_controller.rb
63
+ - app/models/image.rb
64
+ - config/routes.rb
65
+ - dynamic_image.gemspec
66
+ - init.rb
67
+ - install.rb
68
+ - lib/binary_storage.rb
69
+ - lib/binary_storage/active_record_extensions.rb
70
+ - lib/binary_storage/blob.rb
71
+ - lib/dynamic_image.rb
72
+ - lib/dynamic_image/active_record_extensions.rb
73
+ - lib/dynamic_image/engine.rb
74
+ - lib/dynamic_image/filterset.rb
75
+ - lib/dynamic_image/helper.rb
76
+ - lib/generators/dynamic_image/USAGE
77
+ - lib/generators/dynamic_image/dynamic_image_generator.rb
78
+ - lib/generators/dynamic_image/templates/migrations/create_images.rb
79
+ - uninstall.rb
80
+ has_rdoc: true
81
+ homepage: http://github.com/elektronaut/dynamic_image
82
+ licenses: []
83
+
84
+ post_install_message:
85
+ rdoc_options:
86
+ - --charset=UTF-8
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ segments:
94
+ - 0
95
+ version: "0"
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ segments:
101
+ - 0
102
+ version: "0"
103
+ requirements: []
104
+
105
+ rubyforge_project:
106
+ rubygems_version: 1.3.6
107
+ signing_key:
108
+ specification_version: 3
109
+ summary: DynamicImage is a rails plugin providing transparent uploading and processing of image files.
110
+ test_files:
111
+ - test/dynamic_image_test.rb
112
+ - test/test_helper.rb