citrusbyte-milton 0.2.4 → 0.3.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.
@@ -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