dm-paperclip-r3 2.4.1

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,117 @@
1
+ module Paperclip
2
+
3
+ # Defines the geometry of an image.
4
+ class Geometry
5
+ attr_accessor :height, :width, :modifier
6
+
7
+ # Gives a Geometry representing the given height and width
8
+ def initialize width = nil, height = nil, modifier = nil
9
+ height = nil if height == ''
10
+ width = nil if width == ''
11
+ @height = (height || width).to_f
12
+ @width = (width || height).to_f
13
+ @modifier = modifier
14
+ end
15
+
16
+ # Uses ImageMagick to determing the dimensions of a file, passed in as either a
17
+ # File or path.
18
+ def self.from_file file
19
+ file = file.path if file.respond_to? "path"
20
+ geometry = begin
21
+ Paperclip.run("identify", %Q[-format "%wx%h" "#{file}"[0]])
22
+ rescue PaperclipCommandLineError
23
+ ""
24
+ end
25
+ parse(geometry) ||
26
+ raise(NotIdentifiedByImageMagickError.new("#{file} is not recognized by the 'identify' command."))
27
+ end
28
+
29
+ # Parses a "WxH" formatted string, where W is the width and H is the height.
30
+ def self.parse string
31
+ if match = (string && string.match(/\b(\d*)x?(\d*)\b([\>\<\#\@\%^!])?/i))
32
+ Geometry.new(*match[1,3])
33
+ end
34
+ end
35
+
36
+ # True if the dimensions represent a square
37
+ def square?
38
+ height == width
39
+ end
40
+
41
+ # True if the dimensions represent a horizontal rectangle
42
+ def horizontal?
43
+ height < width
44
+ end
45
+
46
+ # True if the dimensions represent a vertical rectangle
47
+ def vertical?
48
+ height > width
49
+ end
50
+
51
+ # The aspect ratio of the dimensions.
52
+ def aspect
53
+ width / height
54
+ end
55
+
56
+ # Returns the larger of the two dimensions
57
+ def larger
58
+ [height, width].max
59
+ end
60
+
61
+ # Returns the smaller of the two dimensions
62
+ def smaller
63
+ [height, width].min
64
+ end
65
+
66
+ # Returns the width and height in a format suitable to be passed to Geometry.parse
67
+ def to_s
68
+ s = ""
69
+ s << width.to_i.to_s if width > 0
70
+ s << "x#{height.to_i}" if height > 0
71
+ s << modifier.to_s
72
+ s
73
+ end
74
+
75
+ # Same as to_s
76
+ def inspect
77
+ to_s
78
+ end
79
+
80
+ # Returns the scaling and cropping geometries (in string-based ImageMagick format)
81
+ # neccessary to transform this Geometry into the Geometry given. If crop is true,
82
+ # then it is assumed the destination Geometry will be the exact final resolution.
83
+ # In this case, the source Geometry is scaled so that an image containing the
84
+ # destination Geometry would be completely filled by the source image, and any
85
+ # overhanging image would be cropped. Useful for square thumbnail images. The cropping
86
+ # is weighted at the center of the Geometry.
87
+ def transformation_to dst, crop = false
88
+ if crop
89
+ ratio = Geometry.new( dst.width / self.width, dst.height / self.height )
90
+ scale_geometry, scale = scaling(dst, ratio)
91
+ crop_geometry = cropping(dst, ratio, scale)
92
+ else
93
+ scale_geometry = dst.to_s
94
+ end
95
+
96
+ [ scale_geometry, crop_geometry ]
97
+ end
98
+
99
+ private
100
+
101
+ def scaling dst, ratio
102
+ if ratio.horizontal? || ratio.square?
103
+ [ "%dx" % dst.width, ratio.width ]
104
+ else
105
+ [ "x%d" % dst.height, ratio.height ]
106
+ end
107
+ end
108
+
109
+ def cropping dst, ratio, scale
110
+ if ratio.horizontal? || ratio.square?
111
+ "%dx%d+%d+%d" % [ dst.width, dst.height, 0, (self.height * scale - dst.height) / 2 ]
112
+ else
113
+ "%dx%d+%d+%d" % [ dst.width, dst.height, (self.width * scale - dst.width) / 2, 0 ]
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,123 @@
1
+ module Paperclip
2
+ # This module contains all the methods that are available for interpolation
3
+ # in paths and urls. To add your own (or override an existing one), you
4
+ # can either open this module and define it, or call the
5
+ # Paperclip.interpolates method.
6
+ module Interpolations
7
+ extend self
8
+
9
+ # Hash assignment of interpolations. Included only for compatability,
10
+ # and is not intended for normal use.
11
+ def self.[]= name, block
12
+ define_method(name, &block)
13
+ end
14
+
15
+ # Hash access of interpolations. Included only for compatability,
16
+ # and is not intended for normal use.
17
+ def self.[] name
18
+ method(name)
19
+ end
20
+
21
+ # Returns a sorted list of all interpolations.
22
+ def self.all
23
+ self.instance_methods(false).sort
24
+ end
25
+
26
+ # Perform the actual interpolation. Takes the pattern to interpolate
27
+ # and the arguments to pass, which are the attachment and style name.
28
+ def self.interpolate pattern, *args
29
+ all.reverse.inject( pattern.dup ) do |result, tag|
30
+ result.gsub(/:#{tag}/) do |match|
31
+ send( tag, *args )
32
+ end
33
+ end
34
+ end
35
+
36
+ # Returns the filename, the same way as ":basename.:extension" would.
37
+ def filename attachment, style
38
+ "#{basename(attachment, style)}.#{extension(attachment, style)}"
39
+ end
40
+
41
+ # Returns the interpolated URL. Will raise an error if the url itself
42
+ # contains ":url" to prevent infinite recursion. This interpolation
43
+ # is used in the default :path to ease default specifications.
44
+ def url attachment, style
45
+ raise InfiniteInterpolationError if attachment.options[:url].include?(":url")
46
+ attachment.url(style, false)
47
+ end
48
+
49
+ # Returns the timestamp as defined by the <attachment>_updated_at field
50
+ def timestamp attachment, style
51
+ attachment.instance_read(:updated_at).to_s
52
+ end
53
+
54
+ def web_root attachment, style
55
+ if Object.const_defined?('Merb')
56
+ merb_root(attachment, style)
57
+ elsif Object.const_defined("RAILS_ROOT")
58
+ rails_root(attachment, style)
59
+ else
60
+ ""
61
+ end
62
+ end
63
+
64
+ # Returns the RAILS_ROOT constant.
65
+ def rails_root attachment, style
66
+ Object.const_defined?('RAILS_ROOT') ? RAILS_ROOT : nil
67
+ end
68
+
69
+ # Returns the RAILS_ENV constant.
70
+ def rails_env attachment, style
71
+ Object.const_defined?('RAILS_ENV') ? RAILS_ENV : nil
72
+ end
73
+
74
+ def merb_root attachment, style
75
+ Object.const_defined?('Merb') ? Merb.root : nil
76
+ end
77
+
78
+ def merb_env attachment, style
79
+ Object.const_defined?('Merb') ? Merb.env : nil
80
+ end
81
+
82
+ # Returns the snake cased, pluralized version of the class name.
83
+ # e.g. "users" for the User class.
84
+ def class attachment, style
85
+ attachment.instance.class.to_s.snake_case.pluralize
86
+ end
87
+
88
+ # Returns the basename of the file. e.g. "file" for "file.jpg"
89
+ def basename attachment, style
90
+ attachment.original_filename.gsub(/#{File.extname(attachment.original_filename)}$/, "")
91
+ end
92
+
93
+ # Returns the extension of the file. e.g. "jpg" for "file.jpg"
94
+ # If the style has a format defined, it will return the format instead
95
+ # of the actual extension.
96
+ def extension attachment, style
97
+ ((style = attachment.styles[style]) && style[:format]) ||
98
+ File.extname(attachment.original_filename).gsub(/^\.+/, "")
99
+ end
100
+
101
+ # Returns the id of the instance.
102
+ def id attachment, style
103
+ attachment.instance.id
104
+ end
105
+
106
+ # Returns the id of the instance in a split path form. e.g. returns
107
+ # 000/001/234 for an id of 1234.
108
+ def id_partition attachment, style
109
+ ("%09d" % attachment.instance.id).scan(/\d{3}/).join("/")
110
+ end
111
+
112
+ # Returns the pluralized form of the attachment name. e.g.
113
+ # "avatars" for an attachment of :avatar
114
+ def attachment attachment, style
115
+ attachment.name.to_s.downcase.pluralize
116
+ end
117
+
118
+ # Returns the style, or the default style if nil is supplied.
119
+ def style attachment, style
120
+ style || attachment.default_style
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,46 @@
1
+ # Provides method that can be included on File-type objects (IO, StringIO, Tempfile, etc) to allow stream copying
2
+ # and Tempfile conversion.
3
+ module IOStream
4
+
5
+ # Returns a Tempfile containing the contents of the readable object.
6
+ def to_tempfile(object)
7
+ return object.to_tempfile if object.respond_to?(:to_tempfile)
8
+ name = object.respond_to?(:original_filename) ? object.original_filename : (object.respond_to?(:path) ? object.path : "stream")
9
+ tempfile = Paperclip::Tempfile.new(["stream", File.extname(name)])
10
+ tempfile.binmode
11
+ stream_to(object, tempfile)
12
+ end
13
+
14
+ # Copies one read-able object from one place to another in blocks, obviating the need to load
15
+ # the whole thing into memory. Defaults to 8k blocks. Returns a File if a String is passed
16
+ # in as the destination and returns the IO or Tempfile as passed in if one is sent as the destination.
17
+ def stream_to object, path_or_file, in_blocks_of = 8192
18
+ dstio = case path_or_file
19
+ when String then File.new(path_or_file, "wb+")
20
+ when IO then path_or_file
21
+ when Tempfile then path_or_file
22
+ end
23
+ buffer = ""
24
+ object.rewind
25
+ while object.read(in_blocks_of, buffer) do
26
+ dstio.write(buffer)
27
+ end
28
+ dstio.rewind
29
+ dstio
30
+ end
31
+ end
32
+
33
+ # Corrects a bug in Windows when asking for Tempfile size.
34
+ if defined? Tempfile
35
+ class Tempfile
36
+ def size
37
+ if @tmpfile
38
+ @tmpfile.fsync
39
+ @tmpfile.flush
40
+ @tmpfile.stat.size
41
+ else
42
+ 0
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,49 @@
1
+ module Paperclip
2
+ # Paperclip processors allow you to modify attached files when they are
3
+ # attached in any way you are able. Paperclip itself uses command-line
4
+ # programs for its included Thumbnail processor, but custom processors
5
+ # are not required to follow suit.
6
+ #
7
+ # Processors are required to be defined inside the Paperclip module and
8
+ # are also required to be a subclass of Paperclip::Processor. There is
9
+ # only one method you *must* implement to properly be a subclass:
10
+ # #make, but #initialize may also be of use. Both methods accept 3
11
+ # arguments: the file that will be operated on (which is an instance of
12
+ # File), a hash of options that were defined in has_attached_file's
13
+ # style hash, and the Paperclip::Attachment itself.
14
+ #
15
+ # All #make needs to return is an instance of File (Tempfile is
16
+ # acceptable) which contains the results of the processing.
17
+ #
18
+ # See Paperclip.run for more information about using command-line
19
+ # utilities from within Processors.
20
+ class Processor
21
+ attr_accessor :file, :options, :attachment
22
+
23
+ def initialize file, options = {}, attachment = nil
24
+ @file = file
25
+ @options = options
26
+ @attachment = attachment
27
+ end
28
+
29
+ def make
30
+ end
31
+
32
+ def self.make file, options = {}, attachment = nil
33
+ new(file, options, attachment).make
34
+ end
35
+ end
36
+
37
+ # Due to how ImageMagick handles its image format conversion and how Tempfile
38
+ # handles its naming scheme, it is necessary to override how Tempfile makes
39
+ # its names so as to allow for file extensions. Idea taken from the comments
40
+ # on this blog post:
41
+ # http://marsorange.com/archives/of-mogrify-ruby-tempfile-dynamic-class-definitions
42
+ class Tempfile < ::Tempfile
43
+ # Replaces Tempfile's +make_tmpname+ with one that honors file extensions.
44
+ def make_tmpname(basename, n)
45
+ extension = File.extname(basename)
46
+ sprintf("%s,%d,%d%s", File.basename(basename, extension), $$, n, extension)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,257 @@
1
+ module Paperclip
2
+ module Storage
3
+
4
+ # The default place to store attachments is in the filesystem. Files on the local
5
+ # filesystem can be very easily served by Apache without requiring a hit to your app.
6
+ # They also can be processed more easily after they've been saved, as they're just
7
+ # normal files. There is one Filesystem-specific option for has_attached_file.
8
+ # * +path+: The location of the repository of attachments on disk. This can (and, in
9
+ # almost all cases, should) be coordinated with the value of the +url+ option to
10
+ # allow files to be saved into a place where Apache can serve them without
11
+ # hitting your app. Defaults to
12
+ # ":rails_root/public/:attachment/:id/:style/:basename.:extension"
13
+ # By default this places the files in the app's public directory which can be served
14
+ # directly. If you are using capistrano for deployment, a good idea would be to
15
+ # make a symlink to the capistrano-created system directory from inside your app's
16
+ # public directory.
17
+ # See Paperclip::Attachment#interpolate for more information on variable interpolaton.
18
+ # :path => "/var/app/attachments/:class/:id/:style/:basename.:extension"
19
+ module Filesystem
20
+ def self.extended base
21
+ end
22
+
23
+ def exists?(style_name = default_style)
24
+ if original_filename
25
+ File.exist?(path(style_name))
26
+ else
27
+ false
28
+ end
29
+ end
30
+
31
+ # Returns representation of the data of the file assigned to the given
32
+ # style, in the format most representative of the current storage.
33
+ def to_file style_name = default_style
34
+ @queued_for_write[style_name] || (File.new(path(style_name), 'rb') if exists?(style_name))
35
+ end
36
+
37
+ def flush_writes #:nodoc:
38
+ @queued_for_write.each do |style_name, file|
39
+ file.close
40
+ FileUtils.mkdir_p(File.dirname(path(style_name)))
41
+ log("saving #{path(style_name)}")
42
+ FileUtils.mv(file.path, path(style_name))
43
+ FileUtils.chmod(0644, path(style_name))
44
+ end
45
+ @queued_for_write = {}
46
+ end
47
+
48
+ def flush_deletes #:nodoc:
49
+ @queued_for_delete.each do |path|
50
+ begin
51
+ log("deleting #{path}")
52
+ FileUtils.rm(path) if File.exist?(path)
53
+ rescue Errno::ENOENT => e
54
+ # ignore file-not-found, let everything else pass
55
+ end
56
+ begin
57
+ while(true)
58
+ path = File.dirname(path)
59
+ FileUtils.rmdir(path)
60
+ end
61
+ rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR
62
+ # Stop trying to remove parent directories
63
+ rescue SystemCallError => e
64
+ log("There was an unexpected error while deleting directories: #{e.class}")
65
+ # Ignore it
66
+ end
67
+ end
68
+ @queued_for_delete = []
69
+ end
70
+ end
71
+
72
+ # Amazon's S3 file hosting service is a scalable, easy place to store files for
73
+ # distribution. You can find out more about it at http://aws.amazon.com/s3
74
+ # There are a few S3-specific options for has_attached_file:
75
+ # * +s3_credentials+: Takes a path, a File, or a Hash. The path (or File) must point
76
+ # to a YAML file containing the +access_key_id+ and +secret_access_key+ that Amazon
77
+ # gives you. You can 'environment-space' this just like you do to your
78
+ # database.yml file, so different environments can use different accounts:
79
+ # development:
80
+ # access_key_id: 123...
81
+ # secret_access_key: 123...
82
+ # test:
83
+ # access_key_id: abc...
84
+ # secret_access_key: abc...
85
+ # production:
86
+ # access_key_id: 456...
87
+ # secret_access_key: 456...
88
+ # This is not required, however, and the file may simply look like this:
89
+ # access_key_id: 456...
90
+ # secret_access_key: 456...
91
+ # In which case, those access keys will be used in all environments. You can also
92
+ # put your bucket name in this file, instead of adding it to the code directly.
93
+ # This is useful when you want the same account but a different bucket for
94
+ # development versus production.
95
+ # * +s3_permissions+: This is a String that should be one of the "canned" access
96
+ # policies that S3 provides (more information can be found here:
97
+ # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html#RESTCannedAccessPolicies)
98
+ # The default for Paperclip is :public_read.
99
+ # * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either
100
+ # 'http' or 'https'. Defaults to 'http' when your :s3_permissions are :public_read (the
101
+ # default), and 'https' when your :s3_permissions are anything else.
102
+ # * +s3_headers+: A hash of headers such as {'Expires' => 1.year.from_now.httpdate}
103
+ # * +bucket+: This is the name of the S3 bucket that will store your files. Remember
104
+ # that the bucket must be unique across all of Amazon S3. If the bucket does not exist
105
+ # Paperclip will attempt to create it. The bucket name will not be interpolated.
106
+ # You can define the bucket as a Proc if you want to determine it's name at runtime.
107
+ # Paperclip will call that Proc with attachment as the only argument.
108
+ # * +s3_host_alias+: The fully-qualified domain name (FQDN) that is the alias to the
109
+ # S3 domain of your bucket. Used with the :s3_alias_url url interpolation. See the
110
+ # link in the +url+ entry for more information about S3 domains and buckets.
111
+ # * +url+: There are three options for the S3 url. You can choose to have the bucket's name
112
+ # placed domain-style (bucket.s3.amazonaws.com) or path-style (s3.amazonaws.com/bucket).
113
+ # Lastly, you can specify a CNAME (which requires the CNAME to be specified as
114
+ # :s3_alias_url. You can read more about CNAMEs and S3 at
115
+ # http://docs.amazonwebservices.com/AmazonS3/latest/index.html?VirtualHosting.html
116
+ # Normally, this won't matter in the slightest and you can leave the default (which is
117
+ # path-style, or :s3_path_url). But in some cases paths don't work and you need to use
118
+ # the domain-style (:s3_domain_url). Anything else here will be treated like path-style.
119
+ # NOTE: If you use a CNAME for use with CloudFront, you can NOT specify https as your
120
+ # :s3_protocol; This is *not supported* by S3/CloudFront. Finally, when using the host
121
+ # alias, the :bucket parameter is ignored, as the hostname is used as the bucket name
122
+ # by S3.
123
+ # * +path+: This is the key under the bucket in which the file will be stored. The
124
+ # URL will be constructed from the bucket and the path. This is what you will want
125
+ # to interpolate. Keys should be unique, like filenames, and despite the fact that
126
+ # S3 (strictly speaking) does not support directories, you can still use a / to
127
+ # separate parts of your file name.
128
+ module S3
129
+ def self.extended base
130
+ begin
131
+ require 'aws/s3'
132
+ rescue LoadError => e
133
+ e.message << " (You may need to install the aws-s3 gem)"
134
+ raise e
135
+ end
136
+
137
+ base.instance_eval do
138
+ @s3_credentials = parse_credentials(@options[:s3_credentials])
139
+ @bucket = @options[:bucket] || @s3_credentials[:bucket]
140
+ @bucket = @bucket.call(self) if @bucket.is_a?(Proc)
141
+ @s3_options = @options[:s3_options] || {}
142
+ @s3_permissions = @options[:s3_permissions] || :public_read
143
+ @s3_protocol = @options[:s3_protocol] || (@s3_permissions == :public_read ? 'http' : 'https')
144
+ @s3_headers = @options[:s3_headers] || {}
145
+ @s3_host_alias = @options[:s3_host_alias]
146
+ @url = ":s3_path_url" unless @url.to_s.match(/^:s3.*url$/)
147
+ AWS::S3::Base.establish_connection!( @s3_options.merge(
148
+ :access_key_id => @s3_credentials[:access_key_id],
149
+ :secret_access_key => @s3_credentials[:secret_access_key]
150
+ ))
151
+ end
152
+ Paperclip.interpolates(:s3_alias_url) do |attachment, style|
153
+ "#{attachment.s3_protocol}://#{attachment.s3_host_alias}/#{attachment.path(style).gsub(%r{^/}, "")}"
154
+ end
155
+ Paperclip.interpolates(:s3_path_url) do |attachment, style|
156
+ "#{attachment.s3_protocol}://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
157
+ end
158
+ Paperclip.interpolates(:s3_domain_url) do |attachment, style|
159
+ "#{attachment.s3_protocol}://#{attachment.bucket_name}.s3.amazonaws.com/#{attachment.path(style).gsub(%r{^/}, "")}"
160
+ end
161
+ end
162
+
163
+ def expiring_url(time = 3600)
164
+ AWS::S3::S3Object.url_for(path, bucket_name, :expires_in => time )
165
+ end
166
+
167
+ def bucket_name
168
+ @bucket
169
+ end
170
+
171
+ def s3_host_alias
172
+ @s3_host_alias
173
+ end
174
+
175
+ def parse_credentials creds
176
+ creds = find_credentials(creds).to_mash.stringify_keys!
177
+ if defined? Merb && Merb.respond_to?(:env)
178
+ (creds[Merb.env] || creds).symbolize_keys
179
+ elsif defined? RAILS_ENV
180
+ (creds[RAILS_ENV] || creds).symbolize_keys
181
+ elsif defined? Rails && Rails.respond_to(:env)
182
+ (creds[Rails.env] || creds).symbolize_keys
183
+ elsif defined? RACK_ENV
184
+ (creds[RACK_ENV] || creds).symbolize_keys
185
+ else
186
+ creds.symbolize_keys
187
+ end
188
+ end
189
+
190
+ def exists?(style = default_style)
191
+ if original_filename
192
+ AWS::S3::S3Object.exists?(path(style), bucket_name)
193
+ else
194
+ false
195
+ end
196
+ end
197
+
198
+ def s3_protocol
199
+ @s3_protocol
200
+ end
201
+
202
+ # Returns representation of the data of the file assigned to the given
203
+ # style, in the format most representative of the current storage.
204
+ def to_file style = default_style
205
+ return @queued_for_write[style] if @queued_for_write[style]
206
+ file = Tempfile.new(path(style))
207
+ file.write(AWS::S3::S3Object.value(path(style), bucket_name))
208
+ file.rewind
209
+ return file
210
+ end
211
+
212
+ def flush_writes #:nodoc:
213
+ @queued_for_write.each do |style, file|
214
+ begin
215
+ log("saving #{path(style)}")
216
+ AWS::S3::S3Object.store(path(style),
217
+ file,
218
+ bucket_name,
219
+ {:content_type => instance_read(:content_type),
220
+ :access => @s3_permissions,
221
+ }.merge(@s3_headers))
222
+ rescue AWS::S3::ResponseError => e
223
+ raise
224
+ end
225
+ end
226
+ @queued_for_write = {}
227
+ end
228
+
229
+ def flush_deletes #:nodoc:
230
+ @queued_for_delete.each do |path|
231
+ begin
232
+ log("deleting #{path}")
233
+ AWS::S3::S3Object.delete(path, bucket_name)
234
+ rescue AWS::S3::ResponseError
235
+ # Ignore this.
236
+ end
237
+ end
238
+ @queued_for_delete = []
239
+ end
240
+
241
+ def find_credentials creds
242
+ case creds
243
+ when File
244
+ YAML::load(ERB.new(File.read(creds.path)).result)
245
+ when String
246
+ YAML::load(ERB.new(File.read(creds)).result)
247
+ when Hash
248
+ creds
249
+ else
250
+ raise ArgumentError, "Credentials are not a path, file, or hash."
251
+ end
252
+ end
253
+ private :find_credentials
254
+
255
+ end
256
+ end
257
+ end
@@ -0,0 +1,70 @@
1
+ module Paperclip
2
+ # Handles thumbnailing images that are uploaded.
3
+ class Thumbnail < Processor
4
+
5
+ attr_accessor :current_geometry, :target_geometry, :format, :whiny, :convert_options
6
+
7
+ # Creates a Thumbnail object set to work on the +file+ given. It
8
+ # will attempt to transform the image into one defined by +target_geometry+
9
+ # which is a "WxH"-style string. +format+ will be inferred from the +file+
10
+ # unless specified. Thumbnail creation will raise no errors unless
11
+ # +whiny+ is true (which it is, by default. If +convert_options+ is
12
+ # set, the options will be appended to the convert command upon image conversion
13
+ def initialize file, options = {}, attachment = nil
14
+ super
15
+ geometry = options[:geometry]
16
+ @file = file
17
+ @crop = geometry[-1,1] == '#'
18
+ @target_geometry = Geometry.parse geometry
19
+ @current_geometry = Geometry.from_file @file
20
+ @convert_options = options[:convert_options]
21
+ @whiny = options[:whiny].nil? ? true : options[:whiny]
22
+ @format = options[:format]
23
+
24
+ @current_format = File.extname(@file.path)
25
+ @basename = File.basename(@file.path, @current_format)
26
+ end
27
+
28
+ # Returns true if the +target_geometry+ is meant to crop.
29
+ def crop?
30
+ @crop
31
+ end
32
+
33
+ # Returns true if the image is meant to make use of additional convert options.
34
+ def convert_options?
35
+ not @convert_options.blank?
36
+ end
37
+
38
+ # Performs the conversion of the +file+ into a thumbnail. Returns the Tempfile
39
+ # that contains the new image.
40
+ def make
41
+ src = @file
42
+ dst = Tempfile.new([@basename, @format].compact.join("."))
43
+ dst.binmode
44
+
45
+ command = <<-end_command
46
+ "#{ File.expand_path(src.path) }[0]"
47
+ #{ transformation_command }
48
+ "#{ File.expand_path(dst.path) }"
49
+ end_command
50
+
51
+ begin
52
+ success = Paperclip.run("convert", command.gsub(/\s+/, " "))
53
+ rescue PaperclipCommandLineError
54
+ raise PaperclipError, "There was an error processing the thumbnail for #{@basename}" if @whiny
55
+ end
56
+
57
+ dst
58
+ end
59
+
60
+ # Returns the command ImageMagick's +convert+ needs to transform the image
61
+ # into the thumbnail.
62
+ def transformation_command
63
+ scale, crop = @current_geometry.transformation_to(@target_geometry, crop?)
64
+ trans = "-resize \"#{scale}\""
65
+ trans << " -crop \"#{crop}\" +repage" if crop
66
+ trans << " #{convert_options}" if convert_options?
67
+ trans
68
+ end
69
+ end
70
+ end