dynamic_image 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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.md CHANGED
@@ -1,19 +1,109 @@
1
1
  # DynamicImage
2
2
 
3
- > We're programmers. Programmers are, in their hearts, architects, and the
4
- > first thing they want to do when they get to a site is to bulldoze
5
- > the place flat and build something grand.
6
- > (...)
7
- > When you throw away code and start from scratch, you are throwing away all
8
- > that knowledge. All those collected bug fixes. Years of programming work.
9
- > _-Joel Spolsky_
3
+ DynamicImage is a Rails plugin that simplifies image uploading and processing.
4
+ No configuration is necessary, as resizing and processing is done on demand and
5
+ cached rather than on upload.
10
6
 
11
- Good riddance.
7
+ Note: This version is currently Rails 3 specific, although the final 1.0
8
+ version will also be compatible with 2.x.
12
9
 
13
- DynamicImage is being rewritten.
14
10
 
15
- ## License
11
+ ## Installation
16
12
 
17
- Copyright 2006-2014 Inge Jørgensen
13
+ Install the gem:
18
14
 
19
- DynamicImage is released under the [MIT License](http://www.opensource.org/licenses/MIT).
15
+ gem install dynamic_image
16
+
17
+ Add the gem to your Gemfile:
18
+
19
+ gem 'dynamic_image'
20
+
21
+ Do the migrations:
22
+
23
+ rails generate dynamic_image migrations
24
+ rake db:migrate
25
+
26
+
27
+ ## Getting started
28
+
29
+ Let's create a model with an image:
30
+
31
+ class User
32
+ belongs_to_image :mugshot
33
+ end
34
+
35
+ Uploading files is pretty straightforward, just add a <tt>file_field</tt>
36
+ to your form and update your record as usual:
37
+
38
+ <%= form_for @user, :html => {:multipart => true} do |f| %>
39
+ Name: <%= f.text_field :name %>
40
+ Mugshot: <%= f.file_field :mugshot %>
41
+ <%= submit_tag "Save" %>
42
+ <% end %>
43
+
44
+ You can now use the <tt>dynamic_image_tag</tt> helper to show off your
45
+ new image:
46
+
47
+ <% if @user.mugshot? %>
48
+ <%= dynamic_image_tag @user.profile_picture, :size => '64x64' %>
49
+ <% end %>
50
+
51
+
52
+ ## Filters
53
+
54
+ I'm cleaning up the filters syntax, watch this space.
55
+
56
+
57
+ ## Technical
58
+
59
+ The original master files are stored in the file system and identified a
60
+ SHA-1 hash of the contents. If you're familiar with the internal workings
61
+ of git, this should seem familiar.
62
+
63
+ Processing images on the fly is expensive. Therefore, page caching is enabled
64
+ by default, even in development mode. To disable page caching, add the following
65
+ line in your initializers:
66
+
67
+ DynamicImage.page_caching = false
68
+
69
+
70
+ ## History
71
+
72
+ DynamicImage was originally created in early 2006 to handle images
73
+ for the Pages CMS. It was later extracted as a Rails Engine for Rails
74
+ 1.2 in 2007, which also marked the first public release as
75
+ dynamic_image_engine.
76
+
77
+ The API has remained more or less unchanged, but the internal workings
78
+ have been refactored a few times over the years, most notably dropping
79
+ the Engines dependency and transitioning from database storage to file
80
+ system.
81
+
82
+ The current version is based on an internal branch targeting Rails
83
+ 2.3, and modified to work as a Rails 3 plugin. It's not directly
84
+ compatible with earlier versions, but upgrading shouldn't be more
85
+ trouble than migrating the files out of the database.
86
+
87
+
88
+ ## Copyright
89
+
90
+ Copyright © 2006 Inge Jørgensen.
91
+
92
+ Permission is hereby granted, free of charge, to any person obtaining
93
+ a copy of this software and associated documentation files (the
94
+ "Software"), to deal in the Software without restriction, including
95
+ without limitation the rights to use, copy, modify, merge, publish,
96
+ distribute, sublicense, and/or sell copies of the Software, and to
97
+ permit persons to whom the Software is furnished to do so, subject to
98
+ the following conditions:
99
+
100
+ The above copyright notice and this permission notice shall be
101
+ included in all copies or substantial portions of the Software.
102
+
103
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
104
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
105
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
106
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
107
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
108
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
109
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,33 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+
4
+ require "rake"
5
+
6
+ begin
7
+ require "jeweler"
8
+ Jeweler::Tasks.new do |gem|
9
+ gem.name = "dynamic_image"
10
+ gem.summary = "DynamicImage is a rails plugin providing transparent uploading and processing of image files."
11
+ gem.email = "inge@elektronaut.no"
12
+ gem.homepage = "http://github.com/elektronaut/dynamic_image"
13
+ gem.authors = ["Inge Jørgensen"]
14
+ gem.files = Dir["*", "{lib}/**/*", "{app}/**/*", "{config}/**/*"]
15
+ gem.add_dependency("rmagick", "~> 2.13.2")
16
+ gem.add_dependency("vector2d", "~> 1.0.0")
17
+ end
18
+ Jeweler::GemcutterTasks.new
19
+ rescue LoadError
20
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
21
+ end
22
+
23
+ desc 'Default: run unit tests.'
24
+ task :default => :test
25
+
26
+ desc 'Test the dynamic_image plugin.'
27
+ Rake::TestTask.new(:test) do |t|
28
+ t.libs << 'lib'
29
+ t.libs << 'test'
30
+ t.pattern = 'test/**/*_test.rb'
31
+ t.verbose = true
32
+ end
33
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.1
@@ -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
@@ -0,0 +1,188 @@
1
+ class Image < ActiveRecord::Base
2
+ unloadable
3
+
4
+ binary_storage :data, :sha1_hash
5
+
6
+ validates_format_of :content_type,
7
+ :with => /^image/,
8
+ :message => "you can only upload pictures"
9
+
10
+ attr_accessor :filterset, :data_checked, :skip_maxsize
11
+
12
+ # Sanitize the filename and set the name to the filename if omitted
13
+ validate do |image|
14
+ image.name = File.basename(image.filename, ".*") if !image.name || image.name.strip == ""
15
+ image.filename = image.friendly_file_name(image.filename)
16
+ if image.cropped?
17
+ image.errors.add(:crop_start, "must be a vector") unless image.crop_start =~ /^[\d]+x[\d]+$/
18
+ image.errors.add(:crop_size, "must be a vector") unless image.crop_size =~ /^[\d]+x[\d]+$/
19
+ else
20
+ image.crop_size = image.original_size
21
+ image.crop_start = "0x0"
22
+ end
23
+ end
24
+
25
+ # Create the binary from an image file.
26
+ def imagefile=(image_file)
27
+ if image_file.kind_of?(String) && image_file =~ /^(ht|f)tps?:\/\//
28
+ self.filename = File.basename(image_file)
29
+ image_file = open(image_file)
30
+ else
31
+ self.filename = image_file.original_filename rescue File.basename(image_file.path)
32
+ end
33
+ self.content_type = image_file.content_type.chomp rescue "image/"+image_file.path.split(/\./).last.downcase.gsub(/jpg/,"jpeg") # ugly hack
34
+ set_image_data(image_file.read)
35
+ end
36
+
37
+ # Return the image hotspot
38
+ def hotspot
39
+ (self.hotspot?) ? self.hotspot : (Vector2d.new(self.size) * 0.5).round.to_s
40
+ end
41
+
42
+ # Check the image data
43
+ def set_image_data(data)
44
+ self.data = data
45
+ if self.data
46
+ image = Magick::ImageList.new.from_blob(self.data)
47
+ size = Vector2d.new(image.columns, image.rows)
48
+ if DynamicImage.crash_size
49
+ crashsize = Vector2d.new(DynamicImage.crash_size)
50
+ if (size.x > crashsize.x || size.y > crashsize.y)
51
+ raise "Image too large!"
52
+ end
53
+ end
54
+ if DynamicImage.max_size && !self.skip_maxsize
55
+ maxsize = Vector2d.new(DynamicImage.max_size)
56
+ if (size.x > maxsize.x || size.y > maxsize.y)
57
+ size = size.constrain_both(maxsize).round
58
+ image.resize!(size.x, size.y)
59
+ self.data = image.to_blob
60
+ end
61
+ end
62
+ # Convert image to a proper format
63
+ unless image.format =~ /(JPEG|PNG|GIF)/
64
+ self.data = image.to_blob{self.format = 'JPEG'; self.quality = 90}
65
+ self.filename += ".jpg"
66
+ self.content_type = "image/jpeg"
67
+ end
68
+ self.original_size = size.round.to_s
69
+ end
70
+ end
71
+
72
+ # Returns the image width
73
+ def original_width
74
+ Vector2d.new(self.original_size).x.to_i
75
+ end
76
+
77
+ # Returns the image height
78
+ def original_height
79
+ Vector2d.new(self.original_size).y.to_i
80
+ end
81
+
82
+ def crop_start_x
83
+ Vector2d.new(self.crop_start).x.to_i
84
+ end
85
+ def crop_start_y
86
+ Vector2d.new(self.crop_start).y.to_i
87
+ end
88
+ def crop_width
89
+ Vector2d.new(self.crop_size).x.to_i
90
+ end
91
+ def crop_height
92
+ Vector2d.new(self.crop_size).y.to_i
93
+ end
94
+
95
+ # Returns original or cropped size
96
+ def size
97
+ (self.cropped?) ? self.crop_size : self.original_size
98
+ end
99
+
100
+ def size=(new_size)
101
+ self.original_size = new_size
102
+ end
103
+
104
+ # Convert file name to a more file system friendly one.
105
+ # TODO: international chars
106
+ def friendly_file_name( file_name )
107
+ [["æ","ae"], ["ø","oe"], ["å","aa"]].each do |int|
108
+ file_name = file_name.gsub(int[0], int[1])
109
+ end
110
+ File.basename(file_name).gsub(/[^\w\d\.-]/, "_")
111
+ end
112
+
113
+ # Get the base part of a filename
114
+ def base_part_of(file_name)
115
+ name = File.basename(file_name)
116
+ name.gsub(/[ˆ\w._-]/, '')
117
+ end
118
+
119
+ # Rescale and crop the image, and return it as a blob.
120
+ def rescaled_and_cropped_data(*args)
121
+ DynamicImage.dirty_memory = true # Flag to perform GC
122
+ image_data = Magick::ImageList.new.from_blob(self.data)
123
+
124
+ if self.cropped?
125
+ cropped_start = Vector2d.new(self.crop_start).round
126
+ cropped_size = Vector2d.new(self.crop_size).round
127
+ image_data = image_data.crop(cropped_start.x, cropped_start.y, cropped_size.x, cropped_size.y, true)
128
+ end
129
+
130
+ size = Vector2d.new(self.size)
131
+ rescale_size = size.dup.constrain_one(args).round # Rescale dimensions
132
+ crop_to_size = Vector2d.new(args).round # Crop size
133
+ new_hotspot = Vector2d.new(hotspot) * (rescale_size / size) # Recalculated hotspot
134
+ rect = [(new_hotspot-(crop_to_size/2)).round, (new_hotspot+(crop_to_size/2)).round] # Array containing crop coords
135
+
136
+ # Adjustments
137
+ x = rect[0].x; rect.each{|r| r.x += (x.abs)} if x < 0
138
+ y = rect[0].y; rect.each{|r| r.y += (y.abs)} if y < 0
139
+ x = rect[1].x; rect.each{|r| r.x -= (x-rescale_size.x)} if x > rescale_size.x
140
+ y = rect[1].y; rect.each{|r| r.y -= (y-rescale_size.y)} if y > rescale_size.y
141
+
142
+ rect[0].round!
143
+ rect[1].round!
144
+
145
+ image_data = image_data.resize(rescale_size.x, rescale_size.y)
146
+ image_data = image_data.crop(rect[0].x, rect[0].y, crop_to_size.x, crop_to_size.y)
147
+ image_data.to_blob{self.quality = 90}
148
+ end
149
+
150
+ def constrain_size(*max_size)
151
+ Vector2d.new(self.size).constrain_both(max_size.flatten).round.to_s
152
+ end
153
+
154
+ # Get a duplicate image with resizing and filters applied.
155
+ def get_processed(size, filterset=nil)
156
+ size = Vector2d.new(size).round.to_s
157
+ processed_image = Image.new
158
+ processed_image.filterset = filterset || 'default'
159
+ processed_image.data = self.rescaled_and_cropped_data(size)
160
+ processed_image.size = size
161
+ processed_image.apply_filters
162
+ processed_image
163
+ end
164
+
165
+ # Apply filters to image data
166
+ def apply_filters
167
+ filterset_name = self.filterset || 'default'
168
+ filterset = DynamicImage::Filterset[filterset_name]
169
+ if filterset
170
+ DynamicImage.dirty_memory = true # Flag for GC
171
+ data = Magick::ImageList.new.from_blob(self.data)
172
+ data = filterset.process(data)
173
+ self.data = data.to_blob
174
+ end
175
+ end
176
+
177
+ # Decorates to_json with additional attributes
178
+ def to_json
179
+ attributes.merge({
180
+ :original_width => self.original_width,
181
+ :original_height => self.original_height,
182
+ :crop_width => self.crop_width,
183
+ :crop_height => self.crop_height,
184
+ :crop_start_x => self.crop_start_x,
185
+ :crop_start_y => self.crop_start_y
186
+ }).to_json
187
+ end
188
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,16 @@
1
+ # Rails 3 routes
2
+ Rails.application.routes.draw do
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'})
@@ -12,11 +12,32 @@ Gem::Specification.new do |s|
12
12
  s.date = "2014-06-12"
13
13
  s.email = "inge@elektronaut.no"
14
14
  s.extra_rdoc_files = [
15
+ "LICENSE",
15
16
  "README.md"
16
17
  ]
17
18
  s.files = [
19
+ "LICENSE",
18
20
  "README.md",
19
- "dynamic_image.gemspec"
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"
20
41
  ]
21
42
  s.homepage = "http://github.com/elektronaut/dynamic_image"
22
43
  s.require_paths = ["lib"]
@@ -0,0 +1,9 @@
1
+ {
2
+ "folders":
3
+ [
4
+ {
5
+ "follow_symlinks": true,
6
+ "path": "."
7
+ }
8
+ ]
9
+ }