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.
- 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
|