dynamic_image 0.9.5 → 0.9.6

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -88,7 +88,7 @@ trouble than migrating the files out of the database.
88
88
  == Copyright
89
89
 
90
90
  Copyright © 2006-2010 Inge Jørgensen.
91
-
91
+
92
92
  Permission is hereby granted, free of charge, to any person obtaining
93
93
  a copy of this software and associated documentation files (the
94
94
  "Software"), to deal in the Software without restriction, including
data/Rakefile CHANGED
@@ -5,20 +5,20 @@ require 'rake/rdoctask'
5
5
  require "rake"
6
6
 
7
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
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
20
  rescue LoadError
21
- puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
21
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
22
22
  end
23
23
 
24
24
  desc 'Default: run unit tests.'
@@ -26,17 +26,17 @@ task :default => :test
26
26
 
27
27
  desc 'Test the dynamic_image plugin.'
28
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
29
+ t.libs << 'lib'
30
+ t.libs << 'test'
31
+ t.pattern = 'test/**/*_test.rb'
32
+ t.verbose = true
33
33
  end
34
34
 
35
35
  desc 'Generate documentation for the dynamic_image plugin.'
36
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')
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
42
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.5
1
+ 0.9.6
@@ -1,79 +1,79 @@
1
1
  class ImagesController < ActionController::Base
2
2
 
3
- after_filter :cache_dynamic_image
4
- after_filter :run_garbage_collection_for_dynamic_image_controller
3
+ after_filter :cache_dynamic_image
4
+ after_filter :run_garbage_collection_for_dynamic_image_controller
5
5
 
6
- unloadable
6
+ unloadable
7
7
 
8
- public
8
+ public
9
9
 
10
- # Return the requested image. Rescale, filter and cache it where appropriate.
11
- def render_dynamic_image
10
+ # Return the requested image. Rescale, filter and cache it where appropriate.
11
+ def render_dynamic_image
12
12
 
13
- render_missing_image and return unless Image.exists?(params[:id])
14
- image = Image.find(params[:id])
13
+ render_missing_image and return unless Image.exists?(params[:id])
14
+ image = Image.find(params[:id])
15
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
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
21
 
22
- unless image.data?
23
- logger.warn "Image #{image.id} exists, but has no data"
24
- render_missing_image and return
25
- end
22
+ unless image.data?
23
+ logger.warn "Image #{image.id} exists, but has no data"
24
+ render_missing_image and return
25
+ end
26
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
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
40
 
41
- DynamicImage.dirty_memory = true # Flag memory for GC
41
+ DynamicImage.dirty_memory = true # Flag memory for GC
42
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
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
53
 
54
- end
55
-
56
- protected
54
+ end
55
+
56
+ protected
57
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
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
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
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
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
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
data/app/models/image.rb CHANGED
@@ -1,188 +1,188 @@
1
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
- if image_file.kind_of?(String) && image_file =~ /^(ht|f)tps?:\/\//
28
- self.filename = File.basename(image_file)
29
- image_file = open(image_file)
30
- else
31
- self.filename = image_file.original_filename rescue File.basename(image_file.path)
32
- end
33
- self.content_type = image_file.content_type.chomp rescue "image/"+image_file.path.split(/\./).last.downcase.gsub(/jpg/,"jpeg") # ugly hack
34
- set_image_data(image_file.read)
35
- end
36
-
37
- # Return the image hotspot
38
- def hotspot
39
- (self.hotspot?) ? self.hotspot : (Vector2d.new(self.size) * 0.5).round.to_s
40
- end
41
-
42
- # Check the image data
43
- def set_image_data(data)
44
- self.data = data
45
- if self.data?
46
- image = Magick::ImageList.new.from_blob(self.data)
47
- size = Vector2d.new(image.columns, image.rows)
48
- if DynamicImage.crash_size
49
- crashsize = Vector2d.new(DynamicImage.crash_size)
50
- if (size.x > crashsize.x || size.y > crashsize.y)
51
- raise "Image too large!"
52
- end
53
- end
54
- if DynamicImage.max_size && !self.skip_maxsize
55
- maxsize = Vector2d.new(DynamicImage.max_size)
56
- if (size.x > maxsize.x || size.y > maxsize.y)
57
- size = size.constrain_both(maxsize).round
58
- image.resize!(size.x, size.y)
59
- self.data = image.to_blob
60
- end
61
- end
62
- # Convert image to a proper format
63
- unless image.format =~ /(JPEG|PNG|GIF)/
64
- self.data = image.to_blob{self.format = 'JPEG'; self.quality = 90}
65
- self.filename += ".jpg"
66
- self.content_type = "image/jpeg"
67
- end
68
- self.original_size = size.round.to_s
69
- end
70
- end
71
-
72
- # Returns the image width
73
- def original_width
74
- Vector2d.new(self.original_size).x.to_i
75
- end
76
-
77
- # Returns the image height
78
- def original_height
79
- Vector2d.new(self.original_size).y.to_i
80
- end
81
-
82
- def crop_start_x
83
- Vector2d.new(self.crop_start).x.to_i
84
- end
85
- def crop_start_y
86
- Vector2d.new(self.crop_start).y.to_i
87
- end
88
- def crop_width
89
- Vector2d.new(self.crop_size).x.to_i
90
- end
91
- def crop_height
92
- Vector2d.new(self.crop_size).y.to_i
93
- end
94
-
95
- # Returns original or cropped size
96
- def size
97
- (self.cropped?) ? self.crop_size : self.original_size
98
- end
99
-
100
- def size=(new_size)
101
- self.original_size = new_size
102
- end
103
-
104
- # Convert file name to a more file system friendly one.
105
- # TODO: international chars
106
- def friendly_file_name( file_name )
107
- [["æ","ae"], ["ø","oe"], ["å","aa"]].each do |int|
108
- file_name = file_name.gsub(int[0], int[1])
109
- end
110
- File.basename(file_name).gsub(/[^\w\d\.-]/, "_")
111
- end
112
-
113
- # Get the base part of a filename
114
- def base_part_of(file_name)
115
- name = File.basename(file_name)
116
- name.gsub(/[ˆ\w._-]/, '')
117
- end
118
-
119
- # Rescale and crop the image, and return it as a blob.
120
- def rescaled_and_cropped_data(*args)
121
- DynamicImage.dirty_memory = true # Flag to perform GC
122
- image_data = Magick::ImageList.new.from_blob(self.data)
123
-
124
- if self.cropped?
125
- cropped_start = Vector2d.new(self.crop_start).round
126
- cropped_size = Vector2d.new(self.crop_size).round
127
- image_data = image_data.crop(cropped_start.x, cropped_start.y, cropped_size.x, cropped_size.y, true)
128
- end
129
-
130
- size = Vector2d.new(self.size)
131
- rescale_size = size.dup.constrain_one(args).round # Rescale dimensions
132
- crop_to_size = Vector2d.new(args).round # Crop size
133
- new_hotspot = Vector2d.new(hotspot) * (rescale_size / size) # Recalculated hotspot
134
- rect = [(new_hotspot-(crop_to_size/2)).round, (new_hotspot+(crop_to_size/2)).round] # Array containing crop coords
135
-
136
- # Adjustments
137
- x = rect[0].x; rect.each{|r| r.x += (x.abs)} if x < 0
138
- y = rect[0].y; rect.each{|r| r.y += (y.abs)} if y < 0
139
- x = rect[1].x; rect.each{|r| r.x -= (x-rescale_size.x)} if x > rescale_size.x
140
- y = rect[1].y; rect.each{|r| r.y -= (y-rescale_size.y)} if y > rescale_size.y
141
-
142
- rect[0].round!
143
- rect[1].round!
144
-
145
- image_data = image_data.resize(rescale_size.x, rescale_size.y)
146
- image_data = image_data.crop(rect[0].x, rect[0].y, crop_to_size.x, crop_to_size.y)
147
- image_data.to_blob{self.quality = 90}
148
- end
149
-
150
- def constrain_size(*max_size)
151
- Vector2d.new(self.size).constrain_both(max_size.flatten).round.to_s
152
- end
153
-
154
- # Get a duplicate image with resizing and filters applied.
155
- def get_processed(size, filterset=nil)
156
- size = Vector2d.new(size).round.to_s
157
- processed_image = Image.new
158
- processed_image.filterset = filterset || 'default'
159
- processed_image.data = self.rescaled_and_cropped_data(size)
160
- processed_image.size = size
161
- processed_image.apply_filters
162
- processed_image
163
- end
164
-
165
- # Apply filters to image data
166
- def apply_filters
167
- filterset_name = self.filterset || 'default'
168
- filterset = DynamicImage::Filterset[filterset_name]
169
- if filterset
170
- DynamicImage.dirty_memory = true # Flag for GC
171
- data = Magick::ImageList.new.from_blob(self.data)
172
- data = filterset.process(data)
173
- self.data = data.to_blob
174
- end
175
- end
176
-
177
- # Decorates to_json with additional attributes
178
- def to_json
179
- attributes.merge({
180
- :original_width => self.original_width,
181
- :original_height => self.original_height,
182
- :crop_width => self.crop_width,
183
- :crop_height => self.crop_height,
184
- :crop_start_x => self.crop_start_x,
185
- :crop_start_y => self.crop_start_y
186
- }).to_json
187
- end
188
- end
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
+ if image_file.kind_of?(String) && image_file =~ /^(ht|f)tps?:\/\//
28
+ self.filename = File.basename(image_file)
29
+ image_file = open(image_file)
30
+ else
31
+ self.filename = image_file.original_filename rescue File.basename(image_file.path)
32
+ end
33
+ self.content_type = image_file.content_type.chomp rescue "image/"+image_file.path.split(/\./).last.downcase.gsub(/jpg/,"jpeg") # ugly hack
34
+ set_image_data(image_file.read)
35
+ end
36
+
37
+ # Return the image hotspot
38
+ def hotspot
39
+ (self.hotspot?) ? self.hotspot : (Vector2d.new(self.size) * 0.5).round.to_s
40
+ end
41
+
42
+ # Check the image data
43
+ def set_image_data(data)
44
+ self.data = data
45
+ if self.data?
46
+ image = Magick::ImageList.new.from_blob(self.data)
47
+ size = Vector2d.new(image.columns, image.rows)
48
+ if DynamicImage.crash_size
49
+ crashsize = Vector2d.new(DynamicImage.crash_size)
50
+ if (size.x > crashsize.x || size.y > crashsize.y)
51
+ raise "Image too large!"
52
+ end
53
+ end
54
+ if DynamicImage.max_size && !self.skip_maxsize
55
+ maxsize = Vector2d.new(DynamicImage.max_size)
56
+ if (size.x > maxsize.x || size.y > maxsize.y)
57
+ size = size.constrain_both(maxsize).round
58
+ image.resize!(size.x, size.y)
59
+ self.data = image.to_blob
60
+ end
61
+ end
62
+ # Convert image to a proper format
63
+ unless image.format =~ /(JPEG|PNG|GIF)/
64
+ self.data = image.to_blob{self.format = 'JPEG'; self.quality = 90}
65
+ self.filename += ".jpg"
66
+ self.content_type = "image/jpeg"
67
+ end
68
+ self.original_size = size.round.to_s
69
+ end
70
+ end
71
+
72
+ # Returns the image width
73
+ def original_width
74
+ Vector2d.new(self.original_size).x.to_i
75
+ end
76
+
77
+ # Returns the image height
78
+ def original_height
79
+ Vector2d.new(self.original_size).y.to_i
80
+ end
81
+
82
+ def crop_start_x
83
+ Vector2d.new(self.crop_start).x.to_i
84
+ end
85
+ def crop_start_y
86
+ Vector2d.new(self.crop_start).y.to_i
87
+ end
88
+ def crop_width
89
+ Vector2d.new(self.crop_size).x.to_i
90
+ end
91
+ def crop_height
92
+ Vector2d.new(self.crop_size).y.to_i
93
+ end
94
+
95
+ # Returns original or cropped size
96
+ def size
97
+ (self.cropped?) ? self.crop_size : self.original_size
98
+ end
99
+
100
+ def size=(new_size)
101
+ self.original_size = new_size
102
+ end
103
+
104
+ # Convert file name to a more file system friendly one.
105
+ # TODO: international chars
106
+ def friendly_file_name( file_name )
107
+ [["æ","ae"], ["ø","oe"], ["å","aa"]].each do |int|
108
+ file_name = file_name.gsub(int[0], int[1])
109
+ end
110
+ File.basename(file_name).gsub(/[^\w\d\.-]/, "_")
111
+ end
112
+
113
+ # Get the base part of a filename
114
+ def base_part_of(file_name)
115
+ name = File.basename(file_name)
116
+ name.gsub(/[ˆ\w._-]/, '')
117
+ end
118
+
119
+ # Rescale and crop the image, and return it as a blob.
120
+ def rescaled_and_cropped_data(*args)
121
+ DynamicImage.dirty_memory = true # Flag to perform GC
122
+ image_data = Magick::ImageList.new.from_blob(self.data)
123
+
124
+ if self.cropped?
125
+ cropped_start = Vector2d.new(self.crop_start).round
126
+ cropped_size = Vector2d.new(self.crop_size).round
127
+ image_data = image_data.crop(cropped_start.x, cropped_start.y, cropped_size.x, cropped_size.y, true)
128
+ end
129
+
130
+ size = Vector2d.new(self.size)
131
+ rescale_size = size.dup.constrain_one(args).round # Rescale dimensions
132
+ crop_to_size = Vector2d.new(args).round # Crop size
133
+ new_hotspot = Vector2d.new(hotspot) * (rescale_size / size) # Recalculated hotspot
134
+ rect = [(new_hotspot-(crop_to_size/2)).round, (new_hotspot+(crop_to_size/2)).round] # Array containing crop coords
135
+
136
+ # Adjustments
137
+ x = rect[0].x; rect.each{|r| r.x += (x.abs)} if x < 0
138
+ y = rect[0].y; rect.each{|r| r.y += (y.abs)} if y < 0
139
+ x = rect[1].x; rect.each{|r| r.x -= (x-rescale_size.x)} if x > rescale_size.x
140
+ y = rect[1].y; rect.each{|r| r.y -= (y-rescale_size.y)} if y > rescale_size.y
141
+
142
+ rect[0].round!
143
+ rect[1].round!
144
+
145
+ image_data = image_data.resize(rescale_size.x, rescale_size.y)
146
+ image_data = image_data.crop(rect[0].x, rect[0].y, crop_to_size.x, crop_to_size.y)
147
+ image_data.to_blob{self.quality = 90}
148
+ end
149
+
150
+ def constrain_size(*max_size)
151
+ Vector2d.new(self.size).constrain_both(max_size.flatten).round.to_s
152
+ end
153
+
154
+ # Get a duplicate image with resizing and filters applied.
155
+ def get_processed(size, filterset=nil)
156
+ size = Vector2d.new(size).round.to_s
157
+ processed_image = Image.new
158
+ processed_image.filterset = filterset || 'default'
159
+ processed_image.data = self.rescaled_and_cropped_data(size)
160
+ processed_image.size = size
161
+ processed_image.apply_filters
162
+ processed_image
163
+ end
164
+
165
+ # Apply filters to image data
166
+ def apply_filters
167
+ filterset_name = self.filterset || 'default'
168
+ filterset = DynamicImage::Filterset[filterset_name]
169
+ if filterset
170
+ DynamicImage.dirty_memory = true # Flag for GC
171
+ data = Magick::ImageList.new.from_blob(self.data)
172
+ data = filterset.process(data)
173
+ self.data = data.to_blob
174
+ end
175
+ end
176
+
177
+ # Decorates to_json with additional attributes
178
+ def to_json
179
+ attributes.merge({
180
+ :original_width => self.original_width,
181
+ :original_height => self.original_height,
182
+ :crop_width => self.crop_width,
183
+ :crop_height => self.crop_height,
184
+ :crop_start_x => self.crop_start_x,
185
+ :crop_start_y => self.crop_start_y
186
+ }).to_json
187
+ end
188
+ end