dynamic_image 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|