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.
- metadata +6 -27
- data/CHANGELOG.rdoc +0 -22
- data/INSTALL +0 -26
- data/MIT-LICENSE +0 -20
- data/README +0 -18
- data/init.rb +0 -1
- data/lib/milton.rb +0 -78
- data/lib/milton/attachment.rb +0 -278
- data/lib/milton/is_image.rb +0 -31
- data/lib/milton/is_resizeable.rb +0 -212
- data/lib/milton/is_uploadable.rb +0 -124
- data/spec/fixtures/big-milton.jpg +0 -0
- data/spec/fixtures/milton.jpg +0 -0
- data/spec/fixtures/mini-milton.jpg +0 -0
- data/spec/fixtures/unsanitary .milton.jpg +0 -0
- data/spec/milton/attachment_spec.rb +0 -120
- data/spec/milton/is_image_spec.rb +0 -4
- data/spec/milton/is_resizeable_spec.rb +0 -150
- data/spec/milton/is_uploadable_spec.rb +0 -110
- data/spec/milton/milton_spec.rb +0 -4
- data/spec/schema.rb +0 -13
- data/spec/spec.opts +0 -7
- data/spec/spec_helper.rb +0 -29
data/lib/milton/is_image.rb
DELETED
@@ -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
|
data/lib/milton/is_resizeable.rb
DELETED
@@ -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
|
data/lib/milton/is_uploadable.rb
DELETED
@@ -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
|
data/spec/fixtures/milton.jpg
DELETED
Binary file
|
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
|