dynamic_image 0.9.5 → 0.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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