citrusbyte-milton 0.2.4 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,31 +0,0 @@
1
- module Citrusbyte
2
- module Milton
3
- module IsImage
4
- def self.included(base)
5
- base.extend IsMethods
6
- end
7
-
8
- module IsMethods
9
- # Stupid little helper for defining something as an image, this used to
10
- # have more functionality, it's just being kept around because it will
11
- # probably be useful in the future. For the time being it just allows
12
- # you to do:
13
- #
14
- # class Image < ActiveRecord::Base
15
- # is_image
16
- # end
17
- #
18
- # rather than:
19
- #
20
- # class Image < ActiveRecord::Base
21
- # is_uploadable
22
- # is_resizeable
23
- # end
24
- def is_image(options={})
25
- is_uploadable options
26
- is_resizeable options
27
- end
28
- end
29
- end
30
- end
31
- end
@@ -1,212 +0,0 @@
1
- module Citrusbyte
2
- module Milton
3
- module IsResizeable
4
- def self.included(base)
5
- base.extend IsMethods
6
- end
7
-
8
- module IsMethods
9
- def is_resizeable(options={})
10
- ensure_attachment_methods options
11
-
12
- require_column 'content_type', "Milton's is_resizeable requires a content_type column on #{class_name} table"
13
-
14
- self.milton_options.merge!(options)
15
-
16
- extend Citrusbyte::Milton::IsResizeable::ClassMethods
17
- include Citrusbyte::Milton::IsResizeable::InstanceMethods
18
- end
19
- end
20
-
21
- module ClassMethods
22
- end
23
-
24
- module InstanceMethods
25
- # Returns the content_type of this attachment, tries to determine it if
26
- # hasn't been determined yet or is not saved to the database
27
- def content_type
28
- return self[:content_type] unless self[:content_type].blank?
29
- self.content_type = file_reference.mime_type? if file_reference.respond_to?(:mime_type?)
30
- end
31
-
32
- # Sets the content type to the given type
33
- def content_type=(type)
34
- write_attribute :content_type, type.to_s.strip
35
- end
36
-
37
- protected
38
- def attached_file
39
- @attached_file ||= ResizeableFile.new(self, filename)
40
- end
41
- end
42
-
43
- # Generic view of an "Image", or rather, something with a width and a
44
- # height we care about =).
45
- class Image
46
- attr_accessor :width
47
- attr_accessor :height
48
-
49
- class << self
50
- # Instantiates a new image from the given path. Uses ImageMagick's
51
- # identify method to determine the width and height of the image with
52
- # the given path and returns a new Image with those dimensions.
53
- #
54
- # Raises a MissingFileError if the given path could not be identify'd
55
- # by ImageMagick (resulting in a height and width).
56
- def from_path(path)
57
- raise Citrusbyte::Milton::MissingFileError.new("Could not identify #{path} as an image, does the file exist?") unless `identify #{path}` =~ /.*? (\d+)x(\d+)\+\d+\+\d+/
58
- new($1, $2)
59
- end
60
-
61
- # Instantiates a new image from the given geometry string. A geometry
62
- # string is just something like 50x40. The first number is the width
63
- # and the second is the height.
64
- def from_geometry(geometry)
65
- new(*(geometry.split("x").collect(&:to_i)))
66
- end
67
- end
68
-
69
- # Instantiates a new Image with the given width and height
70
- def initialize(width=nil, height=nil)
71
- @width = width.to_i
72
- @height = height.to_i
73
- end
74
-
75
- # Returns the larger dimension of the Image
76
- def larger_dimension
77
- width > height ? width : height
78
- end
79
-
80
- # Returns true if the Image is wider than it is tall
81
- def wider?
82
- width > height
83
- end
84
-
85
- # Returns true if the Image is square
86
- def square?
87
- width == height
88
- end
89
- end
90
-
91
- class CropCalculator
92
- attr_reader :original, :target
93
-
94
- # Initializes a new CropCalculator with the two given Images.
95
- #
96
- # A CropCalculator is used to calculate the proper zoom/crop dimensions
97
- # to be passed to ImageMagick's convert method in order to transform
98
- # the original Image's dimensions into the target Image's dimensions
99
- # with sensible zoom/cropping.
100
- def initialize(original, target)
101
- @original = original
102
- @target = target
103
- end
104
-
105
- # Returns the geometry string to send to ImageMagick's convert -resize
106
- # argument -- that is, the dimensions that the original Image would
107
- # need to be resized to in order to result in the given target Image's
108
- # dimensions with cropping.
109
- def resizing_geometry
110
- case
111
- when original.wider? then "#{resized_width}x#{target.height}"
112
- when original.square? && target.wider? then "#{target.width}x#{resized_height}"
113
- when original.square? && !target.wider? then "#{resized_width}x#{target.height}"
114
- else "#{target.width}x#{resized_height}"
115
- end
116
- end
117
-
118
- # The geometry string to send to ImageMagick's convert -crop argument.
119
- def cropping_geometry
120
- "#{target.width}x#{target.height}+0+0"
121
- end
122
-
123
- # The gravity to use for cropping.
124
- def gravity
125
- original.wider? ? "center" : "north"
126
- end
127
-
128
- private
129
- def resized_width
130
- (target.height * original.width / original.height).to_i
131
- end
132
-
133
- def resized_height
134
- (target.width * original.height / original.width).to_i
135
- end
136
-
137
- # TODO: this is the old-school cropping w/ coords, need to implement
138
- # cropping w/ coords using the new system calls
139
- # def crop_with_coordinates(img, x, y, size, options={})
140
- # gravity = options[:gravity] || Magick::NorthGravity
141
- # cropped_img = nil
142
- # img = Magick::Image.read(img).first unless img.is_a?(Magick::Image)
143
- # szx, szy = img.columns, img.rows
144
- # sz = self.class.get_size_from_parameter(size)
145
- # # logger.info "crop_with_coordinates: img.crop!(#{x}, #{y}, #{sz[0]}, #{sz[1]}, true)"
146
- # # cropped_img = img.resize!(sz[0], sz[1]) # EEEEEK
147
- # cropped_img = img.crop!(x, y, szx, szy, true)
148
- # cropped_img.crop_resized!(sz[0], sz[1], gravity) # EEEEEK
149
- # self.temp_path = write_to_temp_file(cropped_img.to_blob)
150
- # end
151
- end
152
- end
153
-
154
- class ResizeableFile < AttachableFile
155
- class << self
156
- # Returns the given size as an array of two integers, width first. Can
157
- # handle:
158
- #
159
- # A fixnum argument, which results in a square sizing:
160
- # parse_size(40) => [40, 40]
161
- # An Array argument, which is simply returned:
162
- # parse_size([40, 40]) => [40, 40]
163
- # A String argument, which is split on 'x' and converted:
164
- # parse_size("40x40") => [40, 40]
165
- def parse_size(size)
166
- case size.class.to_s
167
- when "Fixnum" then [size.to_i, size.to_i]
168
- when "Array" then size
169
- when "String" then size.split('x').collect(&:to_i)
170
- end
171
- end
172
- end
173
-
174
- def initialize(attachment, filename)
175
- super attachment, filename
176
- end
177
-
178
- def path(options={})
179
- options = Derivative.options_from(options) if options.is_a?(String)
180
- return super if options.empty?
181
-
182
- derivative = Derivative.new(self, options)
183
- resize(derivative) unless derivative.exists?
184
- derivative.path
185
- end
186
-
187
- protected
188
- # For speed, any derivatives less than 640-wide are made from a
189
- # 640-wide version of the image (so you're not generating tiny
190
- # thumbnails from an 8-megapixel upload)
191
- def presize_options(derivative)
192
- image.width > 640 && IsResizeable::Image.from_geometry(derivative.options[:size]).width < 640 ? { :size => '640x' } : {}
193
- end
194
-
195
- def image
196
- @image ||= IsResizeable::Image.from_path(path)
197
- end
198
-
199
- def resize(derivative)
200
- raise "target size must be specified for resizing" unless derivative.options.has_key?(:size)
201
-
202
- if derivative.options[:crop]
203
- crop = IsResizeable::CropCalculator.new(image, IsResizeable::Image.from_geometry(derivative.options[:size]))
204
- size = crop.resizing_geometry
205
- conversion_options = %Q(-gravity #{crop.gravity} -crop #{crop.cropping_geometry})
206
- end
207
-
208
- system %Q(convert -geometry #{size || derivative.options[:size]} #{ResizeableFile.new(@attachment, @attachment.filename).path(presize_options(derivative))} #{conversion_options || ''} +repage "#{derivative.path}")
209
- end
210
- end
211
- end
212
- end
@@ -1,124 +0,0 @@
1
- module Citrusbyte
2
- module Milton
3
- module IsUploadable
4
- def self.included(base)
5
- base.extend IsMethods
6
- end
7
-
8
- module IsMethods
9
- def is_uploadable(options = {})
10
- # TODO: implement size validations
11
- # options[:min_size] ||= 1
12
- # options[:max_size] ||= 4.megabytes
13
- # options[:size] ||= (options[:min_size]..options[:max_size])
14
-
15
- ensure_attachment_methods options
16
-
17
- options[:tempfile_path] ||= File.join(RAILS_ROOT, "tmp", "milton")
18
-
19
- self.milton_options.merge!(options)
20
-
21
- after_create :save_uploaded_file
22
-
23
- extend Citrusbyte::Milton::IsUploadable::ClassMethods
24
- include Citrusbyte::Milton::IsUploadable::InstanceMethods
25
- end
26
- end
27
-
28
- module ClassMethods
29
- def self.extended(base)
30
- # Rails 2.1 fix for callbacks
31
- if defined?(::ActiveSupport::Callbacks)
32
- base.define_callbacks :before_file_saved, :after_file_saved
33
- end
34
- end
35
-
36
- unless defined?(::ActiveSupport::Callbacks)
37
- def before_file_saved(&block)
38
- write_inheritable_array(:before_file_saved, [block])
39
- end
40
-
41
- def after_file_saved(&block)
42
- write_inheritable_array(:after_file_saved, [block])
43
- end
44
- end
45
- end
46
-
47
- module InstanceMethods
48
- FILENAME_REGEX = /^[^\/\\]+$/
49
-
50
- def self.included(base)
51
- # Nasty rails 2.1 fix for callbacks
52
- base.define_callbacks *[:before_file_saved, :after_file_saved] if base.respond_to?(:define_callbacks)
53
- end
54
-
55
- def file=(file)
56
- return nil if file.nil? || file.size == 0
57
- @upload = UploadableFile.new(self, file)
58
- self.filename = @upload.filename
59
- self.size = @upload.size if respond_to?(:size=)
60
- self.content_type = @upload.content_type if respond_to?(:content_type=)
61
- end
62
-
63
- protected
64
- def save_uploaded_file
65
- unless @upload.saved?
66
- callback :before_file_saved
67
- @upload.save
68
- callback :after_file_saved
69
- end
70
- end
71
- end
72
- end
73
-
74
- class UploadableFile < AttachableFile
75
- attr_reader :content_type, :filename, :size
76
-
77
- class << self
78
- def write_to_temp_file(data_or_path, options)
79
- FileUtils.mkdir_p(options[:tempfile_path]) unless File.exists?(options[:tempfile_path])
80
-
81
- tempfile = Tempfile.new("#{rand(Time.now.to_i)}", options[:tempfile_path])
82
-
83
- if data_or_path.is_a?(StringIO)
84
- tempfile.binmode
85
- tempfile.write data_or_path.read
86
- tempfile.close
87
- else
88
- tempfile.close
89
- FileUtils.cp((data_or_path.respond_to?(:path) ? data_or_path.path : data_or_path), tempfile.path)
90
- end
91
-
92
- tempfile
93
- end
94
- end
95
-
96
- def initialize(attachment, data_or_path)
97
- @has_been_saved = false
98
- @content_type = data_or_path.content_type
99
- @filename = AttachableFile.sanitize_filename(data_or_path.original_filename, attachment.class.milton_options) if respond_to?(:filename)
100
- @tempfile = UploadableFile.write_to_temp_file(data_or_path, attachment.class.milton_options)
101
- @size = File.size(self.temp_path)
102
-
103
- super attachment, filename
104
- end
105
-
106
- def saved?
107
- @has_been_saved
108
- end
109
-
110
- def save
111
- return true if self.saved?
112
- recreate_directory
113
- File.cp(temp_path, path)
114
- File.chmod(milton_options[:chmod], path)
115
- @has_been_saved = true
116
- end
117
-
118
- protected
119
- def temp_path
120
- @tempfile.respond_to?(:path) ? @tempfile.path : @tempfile.to_s
121
- end
122
- end
123
- end
124
- end
Binary file
Binary file
@@ -1,120 +0,0 @@
1
- require File.dirname(__FILE__) + '/../spec_helper'
2
-
3
- describe Attachment do
4
- describe "setting options" do
5
- before :each do
6
- Attachment.class_eval("is_uploadable :file_system_path => 'foo'")
7
- Image.class_eval("is_uploadable :file_system_path => 'bar'")
8
- end
9
-
10
- it "should not overwrite Attachment's file_system_path setting with Image's" do
11
- Attachment.milton_options[:file_system_path].should eql('foo')
12
- end
13
-
14
- it "should not overwrite Image's file_system_path setting with Attachment's" do
15
- Image.milton_options[:file_system_path].should eql('bar')
16
- end
17
- end
18
-
19
- describe "creating attachment folder" do
20
- before :all do
21
- @output_path = File.join(File.dirname(__FILE__), '..', 'output')
22
- raise "Failed to create #{File.join(@output_path, 'exists')}" unless FileUtils.mkdir_p(File.join(@output_path, 'exists'))
23
- FileUtils.ln_s 'exists', File.join(@output_path, 'linked')
24
- raise "Failed to symlink #{File.join(@output_path, 'linked')}" unless File.symlink?(File.join(@output_path, 'linked'))
25
- end
26
-
27
- it "should create root path when root path does not exist" do
28
- Attachment.class_eval("is_uploadable :file_system_path => '#{File.join(@output_path, 'nonexistant')}'")
29
- @attachment = Attachment.create :file => upload('milton.jpg')
30
-
31
- File.exists?(@attachment.path).should be_true
32
- File.exists?(File.join(@output_path, 'nonexistant')).should be_true
33
- @attachment.path.should =~ /nonexistant/
34
- end
35
-
36
- it "should work when root path already exists" do
37
- Attachment.class_eval("is_uploadable :file_system_path => '#{File.join(@output_path, 'exists')}'")
38
- @attachment = Attachment.create :file => upload('milton.jpg')
39
-
40
- File.exists?(@attachment.path).should be_true
41
- @attachment.path.should =~ /exists/
42
- end
43
-
44
- it "should work when root path is a symlink" do
45
- Attachment.class_eval("is_uploadable :file_system_path => '#{File.join(@output_path, 'linked')}'")
46
- @attachment = Attachment.create :file => upload('milton.jpg')
47
-
48
- File.exists?(@attachment.path).should be_true
49
- @attachment.path.should =~ /linked/
50
- end
51
-
52
- after :all do
53
- Attachment.class_eval("is_uploadable :file_system_path => '#{@output_path}'")
54
- end
55
- end
56
-
57
- describe "being destroyed" do
58
- before :each do
59
- @attachment = Attachment.create :file => upload('milton.jpg')
60
- end
61
-
62
- it "should delete the underlying file from the filesystem" do
63
- @attachment.destroy
64
- File.exists?(@attachment.path).should be_false
65
- end
66
-
67
- # the partitioning algorithm ensures that each attachment model has its own
68
- # folder, so we can safely delete the folder, if you write a new
69
- # partitioner this might change!
70
- it "should delete the directory containing the file and all derivatives from the filesystem" do
71
- @attachment.destroy
72
- File.exists?(File.dirname(@attachment.path)).should be_false
73
- end
74
- end
75
-
76
- describe "instantiating" do
77
- before :each do
78
- @image = Image.new :file => upload('milton.jpg')
79
- end
80
-
81
- it "should have a file= method" do
82
- @image.should respond_to(:file=)
83
- end
84
-
85
- it "should set the filename from the uploaded file" do
86
- @image.filename.should eql('milton.jpg')
87
- end
88
-
89
- it "should strip seperator (.) from the filename and replace them with replacement (-)" do
90
- @image.filename = 'foo.bar.baz.jpg'
91
- @image.filename.should eql('foo-bar-baz.jpg')
92
- end
93
- end
94
-
95
- describe "path partitioning" do
96
- before :each do
97
- @image = Image.new :file => upload('milton.jpg')
98
- end
99
-
100
- it "should be stored in a partitioned folder based on its id" do
101
- @image.path.should =~ /^.*\/#{Citrusbyte::Milton::AttachableFile.partition(@image.id)}\/#{@image.filename}$/
102
- end
103
- end
104
-
105
- describe "public path helper" do
106
- before :each do
107
- @image = Image.new :file => upload('milton.jpg')
108
- end
109
-
110
- it "should give the path from public/ on to the filename" do
111
- @image.stub!(:path).and_return('/root/public/assets/1/milton.jpg')
112
- @image.public_path.should eql("/assets/1/milton.jpg")
113
- end
114
-
115
- it "should give the path from foo/ on to the filename" do
116
- @image.stub!(:path).and_return('/root/foo/assets/1/milton.jpg')
117
- @image.public_path({}, 'foo').should eql("/assets/1/milton.jpg")
118
- end
119
- end
120
- end