coryodaniel-milton 0.3.7

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.
@@ -0,0 +1,202 @@
1
+ require 'fileutils'
2
+ require 'milton/derivatives/derivative'
3
+
4
+ module Milton
5
+ module Attachment
6
+ # Call is_attachment with your options in order to add attachment
7
+ # functionality to your ActiveRecord model.
8
+ #
9
+ # TODO: list options
10
+ def is_attachment(options={})
11
+ # Check to see that it hasn't already been extended so that subclasses
12
+ # can redefine is_attachment from their superclasses and overwrite
13
+ # options w/o losing the superclass options.
14
+ unless respond_to?(:has_attachment_methods)
15
+ extend Milton::Attachment::AttachmentMethods
16
+ class_inheritable_accessor :milton_options
17
+ end
18
+ has_attachment_methods(options)
19
+ end
20
+
21
+ module AttachmentMethods
22
+ def require_column(column, message)
23
+ begin
24
+ raise message unless column_names.include?(column)
25
+ rescue ActiveRecord::StatementInvalid => i
26
+ # table doesn't exist yet, i.e. hasn't been migrated in...
27
+ end
28
+ end
29
+
30
+ def has_attachment_methods(options={})
31
+ require_column 'filename', "Milton requires a filename column on #{class_name} table"
32
+
33
+ # It's possible that this is being included from a class and a sub
34
+ # class of that class, in which case we need to merge the existing
35
+ # options up.
36
+ self.milton_options ||= {}
37
+ milton_options.merge!(options)
38
+
39
+ # Character used to seperate a filename from its derivative options, this
40
+ # character will be stripped from all incoming filenames and replaced by
41
+ # replacement
42
+ milton_options[:separator] ||= '.'
43
+ milton_options[:replacement] ||= '-'
44
+ milton_options[:tempfile_path] ||= File.join(Rails.root, "tmp", "milton")
45
+ milton_options[:storage] ||= :disk
46
+ milton_options[:storage_options] ||= {}
47
+ milton_options[:processors] ||= {}
48
+ milton_options[:uploading] ||= true
49
+
50
+ # Set to true to allow on-demand processing of derivatives. This can
51
+ # be rediculously slow because it requires that the existance of the
52
+ # derivative is checked each time it's requested -- throw in S3 and
53
+ # that becomes a huge lag. Reccommended only for prototyping.
54
+ milton_options[:postproccess] ||= false
55
+
56
+ # TODO: Write about recipes
57
+ # * They're refered to by name in #path
58
+ # * They're an order of derivations to make against this attachment
59
+ # * They run in the order defined
60
+ # * They are created and run when the AR model is created
61
+ # * They're necessary when +:postprocessing+ is turned off
62
+ milton_options[:recipes] ||= {}
63
+ milton_options[:recipes].each do |name, steps|
64
+ steps = [steps] unless steps.is_a?(Array)
65
+ steps.each do |step|
66
+ step.each { |processor, options| Milton.try_require "milton/derivatives/#{processor}", "No '#{processor}' processor found for Milton" }
67
+ end
68
+ end
69
+
70
+ # TODO: Write about storage options
71
+ # * Late binding (so right_aws is only req'd if you use S3)
72
+ Milton.try_require "milton/storage/#{milton_options[:storage]}_file", "No '#{milton_options[:storage]}' storage found for Milton"
73
+
74
+ # TODO: initialize these options in DiskFile
75
+ if milton_options[:storage] == :disk
76
+ # root of where the underlying files are stored (or will be stored)
77
+ # on the file system
78
+ milton_options[:storage_options][:root] ||= File.join(Rails.root, "public", table_name)
79
+ milton_options[:storage_options][:root] = File.expand_path(milton_options[:storage_options][:root])
80
+ # mode to set on stored files and created directories
81
+ milton_options[:storage_options][:chmod] ||= 0755
82
+ end
83
+
84
+ validates_presence_of :filename
85
+
86
+ after_destroy :destroy_attached_file
87
+ after_create :create_derivatives
88
+
89
+ include Milton::Attachment::InstanceMethods
90
+
91
+ if milton_options[:uploading]
92
+ require 'milton/uploading'
93
+ extend Milton::Uploading::ClassMethods
94
+ include Milton::Uploading::InstanceMethods
95
+ end
96
+ end
97
+ end
98
+
99
+ # These get mixed in to your model when you use Milton
100
+ module InstanceMethods
101
+ # Sets the filename to the given filename (sanitizes the given filename
102
+ # as well)
103
+ #
104
+ # TODO: change the filename on the underlying file system on save so as
105
+ # not to orphan the file
106
+ def filename=(name)
107
+ write_attribute :filename, Storage::StoredFile.sanitize_filename(name, self.class.milton_options)
108
+ end
109
+
110
+ # Returns the content_type of this attachment, tries to determine it if
111
+ # hasn't been determined yet or is not saved to the database
112
+ def content_type
113
+ return self[:content_type] unless self[:content_type].blank?
114
+ self.content_type = attached_file.mime_type
115
+ end
116
+
117
+ # Sets the content type to the given type
118
+ def content_type=(type)
119
+ write_attribute :content_type, type.to_s.strip
120
+ end
121
+
122
+ # Simple helper, same as path except returns the directory from
123
+ # .../public/ on, i.e. for showing images in your views.
124
+ #
125
+ # @asset.path => /var/www/site/public/assets/000/000/001/313/milton.jpg
126
+ # @asset.public_path => /assets/000/000/001/313/milton.jpg
127
+ #
128
+ # Can send a different base path than public if you want to give the
129
+ # path from that base on, useful if you change your root path to
130
+ # somewhere else.
131
+ def public_path(options={}, base='public')
132
+ path(options).gsub(/.*?\/#{base}/, '')
133
+ end
134
+
135
+ # The path to the file.
136
+ def path(options=nil)
137
+ return attached_file.path if options.nil?
138
+ process(options).path
139
+ end
140
+
141
+ protected
142
+
143
+ # Meant to be used as an after_create filter -- loops over all the
144
+ # recipes and processes them to create the derivatives.
145
+ def create_derivatives
146
+ milton_options[:recipes].each{ |name, recipe| process(name, true) } if milton_options[:recipes].any?
147
+ end
148
+
149
+ # Process the given options to produce a final derivative. +options+
150
+ # takes a Hash of options to process or the name of a pre-defined
151
+ # recipe which will be looked up and processed.
152
+ #
153
+ # Pass +force = true+ to force processing regardless of if
154
+ # +:postprocessing+ is turned on or not.
155
+ #
156
+ # Returns the final Derivative of all processors in the recipe.
157
+ def process(options, force=false)
158
+ options = milton_options[:recipes][options] unless options.is_a?(Hash)
159
+ options = [options] unless options.is_a?(Array)
160
+
161
+ source = attached_file
162
+ options.each do |recipe|
163
+ recipe.each do |processor, opts|
164
+ source = Derivative.factory(processor, source, opts, self.class.milton_options).process_if(process? || force).file
165
+ end
166
+ end
167
+ source
168
+ end
169
+
170
+ # Returns true if derivaties of the attachment should be processed,
171
+ # returns false if no processing should be done when a derivative is
172
+ # requested.
173
+ #
174
+ # No processing also means the derivative won't be checked for
175
+ # existance (since that can be slow) so w/o postprocessing things will
176
+ # be much faster but #path will happily return the paths to Derivatives
177
+ # which don't exist.
178
+ #
179
+ # It is highly recommended that you turn +:postprocessing+ off for
180
+ # anything but prototyping, and instead use recipes and refer to them
181
+ # via #path. +:postprocessing+ relies on checking for existance which
182
+ # will kill any real application.
183
+ def process?
184
+ self.class.milton_options[:postprocessing]
185
+ end
186
+
187
+ # A reference to the attached file, this is probably what you want to
188
+ # overwrite to introduce a new behavior
189
+ #
190
+ # i.e.
191
+ # have attached_file return a ResizeableFile, or a TranscodableFile
192
+ def attached_file
193
+ @attached_file ||= Storage::StoredFile.adapter(self.class.milton_options[:storage]).new(filename, id, self.class.milton_options)
194
+ end
195
+
196
+ # Clean the file from the filesystem
197
+ def destroy_attached_file
198
+ attached_file.destroy
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,22 @@
1
+ begin
2
+ require 'mimetype_fu'
3
+ rescue MissingSourceFile
4
+ end
5
+
6
+ module Milton
7
+ class File < ::File
8
+ class << self
9
+ def extension(filename)
10
+ extension = extname(filename)
11
+ extension.slice(1, extension.length-1)
12
+ end
13
+
14
+ # File respond_to?(:mime_type) is true if mimetype_fu is installed, so
15
+ # this way we always have File.mime_type? available but it favors
16
+ # mimetype_fu's implementation.
17
+ def mime_type?(file)
18
+ ::File.respond_to?(:mime_type?) ? super(file.filename) : file.content_type
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,44 @@
1
+ require 'tempfile'
2
+
3
+ module Milton
4
+ # For lack of a better name, a MiltonTempfile adds some helpful
5
+ # functionality to Ruby's Tempfile
6
+ class Tempfile < ::Tempfile
7
+ class << self
8
+ def create(data_or_path, tempfile_path)
9
+ FileUtils.mkdir_p(tempfile_path) unless File.exists?(tempfile_path)
10
+
11
+ tempfile = new(basename, tempfile_path)
12
+
13
+ if data_or_path.is_a?(StringIO)
14
+ tempfile.binmode
15
+ tempfile.write data_or_path.read
16
+ tempfile.close
17
+ else
18
+ tempfile.close
19
+ FileUtils.cp((data_or_path.respond_to?(:path) ? data_or_path.path : data_or_path), tempfile.path)
20
+ end
21
+
22
+ tempfile
23
+ end
24
+
25
+ def basename
26
+ "#{rand(Time.now.to_i)}"
27
+ end
28
+
29
+ def filename(extension)
30
+ "#{basename}.#{extension}"
31
+ end
32
+
33
+ def path(tempfile_path, extension)
34
+ File.join(tempfile_path, filename(extension))
35
+ end
36
+
37
+ # Simple helper that returns a path to a tempfile with a uniquely
38
+ # generated basename and same extension as the given source.
39
+ def from(source)
40
+ filename(Milton::File.extension(source))
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,106 @@
1
+ module Milton
2
+ # Represents a file created on the file system that is a derivative of the
3
+ # one referenced by the model, i.e. a thumbnail of an image, or a transcode
4
+ # of a video.
5
+ #
6
+ # Provides a container for options and a uniform API to dealing with
7
+ # passing options for the creation of derivatives.
8
+ #
9
+ # Files created as derivatives have their creation options appended into
10
+ # their filenames so it can be checked later if a file w/ the given
11
+ # options already exists (so as not to create it again).
12
+ #
13
+ class Derivative
14
+ class << self
15
+ # Given a string of attachment options, splits them out into a hash,
16
+ # useful for things that take options on the query string or from
17
+ # filenames
18
+ def options_from(string)
19
+ Hash[*(string.split('_').collect { |option|
20
+ key, value = option.split('=')
21
+ [ key.to_sym, value || true ] # nothing on RHS of = means it's a boolean true
22
+ }).flatten]
23
+ end
24
+
25
+ # Given a filename (presumably with options embedded in it) parses out
26
+ # the options and returns them as a hash.
27
+ def extract_options_from(filename, options)
28
+ File.basename(filename, File.extname(filename))[(filename.rindex(options[:separator]) + 1)..-1]
29
+ end
30
+
31
+ # Creates a new Derivative from the given filename by extracting the
32
+ # options.
33
+ def from_filename(filename)
34
+ Derivative.new(filename, options_from(extract_options_from(filename)))
35
+ end
36
+
37
+ def process(source, options={}, settings={})
38
+ returning(derivative = new(source, options, settings)) do
39
+ derivative.process unless derivative.exists?
40
+ end
41
+ end
42
+
43
+ def factory(type, source, options={}, settings={})
44
+ begin
45
+ klass = "Milton::#{type.to_s.classify}".constantize
46
+ rescue NameError
47
+ begin
48
+ require "milton/derivatives/#{type.to_s}"
49
+ rescue MissingSourceFile => e
50
+ raise MissingSourceFile.new("#{e.message} (milton: couldn't find the processor '#{type}' you were trying to load)", e.path)
51
+ end
52
+ klass = "Milton::#{type.to_s.classify}".constantize
53
+ end
54
+
55
+ klass.new(source, options, settings)
56
+ end
57
+ end
58
+
59
+ attr_reader :options, :settings
60
+
61
+ # Instantiate a new Derivative:
62
+ # * +source+: a reference to the Storage::StoredFile this will be a Derivative of
63
+ # * +options+: options to generate the Derivative using
64
+ # * +settings+: settings about how to create Derivatives
65
+ def initialize(source, options={}, settings={})
66
+ @source = source
67
+ @options = options.is_a?(String) ? self.class.options_from(options) : options
68
+ @settings = settings
69
+ end
70
+
71
+ # The resulting filename of this Derivative with embedded options.
72
+ def filename
73
+ # ignore false booleans and don't output =true for true booleans,
74
+ # otherwise just k=v
75
+ append = options.reject{ |k, v| v.is_a?(FalseClass) }.collect { |k, v| v === true ? k.to_s : "#{k}=#{v}" }.sort.join('_')
76
+ extension = File.extname(@source.path)
77
+ File.basename(@source.path, extension) + (append.blank? ? '' : "#{settings[:separator]}#{append}") + extension
78
+ end
79
+
80
+ # The full path and filename to this Derivative.
81
+ def path
82
+ file.path
83
+ end
84
+
85
+ # Returns true if this Derivative has already been generated and stored.
86
+ def exists?
87
+ file.exists?
88
+ end
89
+
90
+ # Overwrite this to provide your derivatives processing.
91
+ def process;end;
92
+
93
+ # Convenience method, only runs process if the given condition is true.
94
+ # Returns the derivative so it's chainable.
95
+ def process_if(condition)
96
+ process if condition && !exists?
97
+ return self
98
+ end
99
+
100
+ # Returns the StoredFile which represents the Derivative (which is a copy
101
+ # of the source w/ a different filename).
102
+ def file
103
+ @file ||= @source.clone(filename)
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,39 @@
1
+ require 'milton/derivatives/thumbnail/image'
2
+ require 'milton/derivatives/thumbnail/crop_calculator'
3
+
4
+ module Milton
5
+ class Thumbnail < Derivative
6
+ def process
7
+ raise "target size must be specified for resizing" unless options.has_key?(:size)
8
+
9
+ temp_dst = File.join(settings[:tempfile_path], Milton::Tempfile.from(@source.filename))
10
+ temp_src = File.join(settings[:tempfile_path], Milton::Tempfile.from(@source.filename))
11
+
12
+ @source.copy(temp_src)
13
+ image = Image.from_path(temp_src)
14
+
15
+ # TODO: this really only makes sense for processing recipes, reimplement
16
+ # once it's setup to build all derivatives then push to storage
17
+ #
18
+ # For speed, any derivatives less than 640-wide are made from a
19
+ # 640-wide version of the image (so you're not generating tiny
20
+ # thumbnails from an 8-megapixel upload)
21
+ # source = if image.width > 640 && Image.from_geometry(options[:size]).width < 640
22
+ # Thumbnail.process(@source, { :size => '640x' }, settings).file
23
+ # else
24
+ # @source
25
+ # end
26
+
27
+ if options[:crop]
28
+ crop = CropCalculator.new(image, Image.from_geometry(options[:size]))
29
+ size = crop.resizing_geometry
30
+ conversion_options = %Q(-gravity #{crop.gravity} -crop #{crop.cropping_geometry})
31
+ end
32
+
33
+ Milton.syscall!(%Q{convert #{temp_src} -geometry #{size || options[:size]} #{conversion_options || ''} +repage "#{temp_dst}"})
34
+
35
+ # TODO: raise if the store fails
36
+ file.store(temp_dst)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,64 @@
1
+ module Milton
2
+ class CropCalculator
3
+ attr_reader :original, :target
4
+
5
+ # Initializes a new CropCalculator with the two given Images.
6
+ #
7
+ # A CropCalculator is used to calculate the proper zoom/crop dimensions
8
+ # to be passed to ImageMagick's convert method in order to transform
9
+ # the original Image's dimensions into the target Image's dimensions
10
+ # with sensible zoom/cropping.
11
+ def initialize(original, target)
12
+ @original = original
13
+ @target = target
14
+ end
15
+
16
+ # Returns the geometry string to send to ImageMagick's convert -resize
17
+ # argument -- that is, the dimensions that the original Image would
18
+ # need to be resized to in order to result in the given target Image's
19
+ # dimensions with cropping.
20
+ def resizing_geometry
21
+ case
22
+ when original.wider? then "#{resized_width}x#{target.height}"
23
+ when original.square? && target.wider? then "#{target.width}x#{resized_height}"
24
+ when original.square? && !target.wider? then "#{resized_width}x#{target.height}"
25
+ else "#{target.width}x#{resized_height}"
26
+ end
27
+ end
28
+
29
+ # The geometry string to send to ImageMagick's convert -crop argument.
30
+ def cropping_geometry
31
+ "#{target.width}x#{target.height}+0+0"
32
+ end
33
+
34
+ # The gravity to use for cropping.
35
+ def gravity
36
+ original.wider? ? "center" : "north"
37
+ end
38
+
39
+ private
40
+
41
+ def resized_width
42
+ (target.height * original.width / original.height).to_i
43
+ end
44
+
45
+ def resized_height
46
+ (target.width * original.height / original.width).to_i
47
+ end
48
+
49
+ # TODO: this is the old-school cropping w/ coords, need to implement
50
+ # cropping w/ coords using the new system calls
51
+ # def crop_with_coordinates(img, x, y, size, options={})
52
+ # gravity = options[:gravity] || Magick::NorthGravity
53
+ # cropped_img = nil
54
+ # img = Magick::Image.read(img).first unless img.is_a?(Magick::Image)
55
+ # szx, szy = img.columns, img.rows
56
+ # sz = self.class.get_size_from_parameter(size)
57
+ # # logger.info "crop_with_coordinates: img.crop!(#{x}, #{y}, #{sz[0]}, #{sz[1]}, true)"
58
+ # # cropped_img = img.resize!(sz[0], sz[1]) # EEEEEK
59
+ # cropped_img = img.crop!(x, y, szx, szy, true)
60
+ # cropped_img.crop_resized!(sz[0], sz[1], gravity) # EEEEEK
61
+ # self.temp_path = write_to_temp_file(cropped_img.to_blob)
62
+ # end
63
+ end
64
+ end