dynamic_image 0.9.0
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/LICENSE +20 -0
- data/README.rdoc +59 -0
- data/Rakefile +42 -0
- data/VERSION +1 -0
- data/app/controllers/images_controller.rb +79 -0
- data/app/models/image.rb +183 -0
- data/config/routes.rb +16 -0
- data/dynamic_image.gemspec +68 -0
- data/init.rb +1 -0
- data/install.rb +1 -0
- data/lib/binary_storage/active_record_extensions.rb +144 -0
- data/lib/binary_storage/blob.rb +105 -0
- data/lib/binary_storage.rb +28 -0
- data/lib/dynamic_image/active_record_extensions.rb +57 -0
- data/lib/dynamic_image/engine.rb +6 -0
- data/lib/dynamic_image/filterset.rb +79 -0
- data/lib/dynamic_image/helper.rb +121 -0
- data/lib/dynamic_image.rb +77 -0
- data/lib/generators/dynamic_image/USAGE +6 -0
- data/lib/generators/dynamic_image/dynamic_image_generator.rb +38 -0
- data/lib/generators/dynamic_image/templates/migrations/create_images.rb +22 -0
- data/test/dynamic_image_test.rb +8 -0
- data/test/test_helper.rb +3 -0
- data/uninstall.rb +1 -0
- metadata +112 -0
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
|
data/app/models/image.rb
ADDED
@@ -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,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,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
|
data/test/test_helper.rb
ADDED
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
|