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 +1 -1
- data/Rakefile +22 -22
- data/VERSION +1 -1
- data/app/controllers/images_controller.rb +64 -64
- data/app/models/image.rb +187 -187
- data/config/routes.rb +5 -5
- data/dynamic_image.gemspec +30 -36
- data/init.rb +1 -1
- data/lib/binary_storage.rb +16 -16
- data/lib/binary_storage/active_record_extensions.rb +127 -127
- data/lib/binary_storage/blob.rb +100 -100
- data/lib/dynamic_image.rb +54 -54
- data/lib/dynamic_image/active_record_extensions.rb +49 -49
- data/lib/dynamic_image/engine.rb +3 -3
- data/lib/dynamic_image/filterset.rb +72 -72
- data/lib/dynamic_image/helper.rb +102 -102
- data/lib/generators/dynamic_image/USAGE +0 -1
- data/lib/generators/dynamic_image/dynamic_image_generator.rb +27 -27
- metadata +10 -15
- data/test/dynamic_image_test.rb +0 -8
- data/test/test_helper.rb +0 -3
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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.
|
1
|
+
0.9.6
|
@@ -1,79 +1,79 @@
|
|
1
1
|
class ImagesController < ActionController::Base
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
after_filter :cache_dynamic_image
|
4
|
+
after_filter :run_garbage_collection_for_dynamic_image_controller
|
5
5
|
|
6
|
-
|
6
|
+
unloadable
|
7
7
|
|
8
|
-
|
8
|
+
public
|
9
9
|
|
10
|
-
|
11
|
-
|
10
|
+
# Return the requested image. Rescale, filter and cache it where appropriate.
|
11
|
+
def render_dynamic_image
|
12
12
|
|
13
|
-
|
14
|
-
|
13
|
+
render_missing_image and return unless Image.exists?(params[:id])
|
14
|
+
image = Image.find(params[:id])
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
41
|
+
DynamicImage.dirty_memory = true # Flag memory for GC
|
42
42
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
57
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
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
|