futuresinc-attachment_fu 1.0.4 → 1.0.5

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.
Files changed (28) hide show
  1. data/VERSION.yml +1 -1
  2. data/attachment_fu.gemspec +11 -11
  3. data/init.rb +1 -1
  4. data/lib/attachment_fu/backends/cloud_file_backend.rb +209 -0
  5. data/lib/attachment_fu/backends/db_file_backend.rb +37 -0
  6. data/lib/attachment_fu/backends/file_system_backend.rb +124 -0
  7. data/lib/attachment_fu/backends/s3_backend.rb +392 -0
  8. data/lib/attachment_fu/processors/core_image_processor.rb +55 -0
  9. data/lib/attachment_fu/processors/gd2_processor.rb +53 -0
  10. data/lib/attachment_fu/processors/image_science_processor.rb +60 -0
  11. data/lib/attachment_fu/processors/mini_magick_processor.rb +131 -0
  12. data/lib/attachment_fu/processors/rmagick_processor.rb +56 -0
  13. data/lib/attachment_fu.rb +543 -0
  14. data/test/backends/remote/cloudfiles_test.rb +3 -3
  15. data/test/backends/remote/s3_test.rb +3 -3
  16. data/test/basic_test.rb +2 -2
  17. data/test/extra_attachment_test.rb +2 -2
  18. metadata +11 -11
  19. data/lib/technoweenie/attachment_fu/backends/cloud_file_backend.rb +0 -211
  20. data/lib/technoweenie/attachment_fu/backends/db_file_backend.rb +0 -39
  21. data/lib/technoweenie/attachment_fu/backends/file_system_backend.rb +0 -126
  22. data/lib/technoweenie/attachment_fu/backends/s3_backend.rb +0 -394
  23. data/lib/technoweenie/attachment_fu/processors/core_image_processor.rb +0 -59
  24. data/lib/technoweenie/attachment_fu/processors/gd2_processor.rb +0 -54
  25. data/lib/technoweenie/attachment_fu/processors/image_science_processor.rb +0 -61
  26. data/lib/technoweenie/attachment_fu/processors/mini_magick_processor.rb +0 -132
  27. data/lib/technoweenie/attachment_fu/processors/rmagick_processor.rb +0 -57
  28. data/lib/technoweenie/attachment_fu.rb +0 -545
@@ -0,0 +1,392 @@
1
+ module AttachmentFu # :nodoc:
2
+ module Backends
3
+ # = AWS::S3 Storage Backend
4
+ #
5
+ # Enables use of {Amazon's Simple Storage Service}[http://aws.amazon.com/s3] as a storage mechanism
6
+ #
7
+ # == Requirements
8
+ #
9
+ # Requires the {AWS::S3 Library}[http://amazon.rubyforge.org] for S3 by Marcel Molina Jr. installed either
10
+ # as a gem or a as a Rails plugin.
11
+ #
12
+ # == Configuration
13
+ #
14
+ # Configuration is done via <tt>RAILS_ROOT/config/amazon_s3.yml</tt> and is loaded according to the <tt>RAILS_ENV</tt>.
15
+ # The minimum connection options that you must specify are a bucket name, your access key id and your secret access key.
16
+ # If you don't already have your access keys, all you need to sign up for the S3 service is an account at Amazon.
17
+ # You can sign up for S3 and get access keys by visiting http://aws.amazon.com/s3.
18
+ #
19
+ # If you wish to use Amazon CloudFront to serve the files, you can also specify a distibution domain for the bucket.
20
+ # To read more about CloudFront, visit http://aws.amazon.com/cloudfront
21
+ #
22
+ # Example configuration (RAILS_ROOT/config/amazon_s3.yml)
23
+ #
24
+ # development:
25
+ # bucket_name: appname_development
26
+ # access_key_id: <your key>
27
+ # secret_access_key: <your key>
28
+ # distribution_domain: XXXX.cloudfront.net
29
+ #
30
+ # test:
31
+ # bucket_name: appname_test
32
+ # access_key_id: <your key>
33
+ # secret_access_key: <your key>
34
+ # distribution_domain: XXXX.cloudfront.net
35
+ #
36
+ # production:
37
+ # bucket_name: appname
38
+ # access_key_id: <your key>
39
+ # secret_access_key: <your key>
40
+ # distribution_domain: XXXX.cloudfront.net
41
+ #
42
+ # You can change the location of the config path by passing a full path to the :s3_config_path option.
43
+ #
44
+ # has_attachment :storage => :s3, :s3_config_path => (RAILS_ROOT + '/config/s3.yml')
45
+ #
46
+ # === Required configuration parameters
47
+ #
48
+ # * <tt>:access_key_id</tt> - The access key id for your S3 account. Provided by Amazon.
49
+ # * <tt>:secret_access_key</tt> - The secret access key for your S3 account. Provided by Amazon.
50
+ # * <tt>:bucket_name</tt> - A unique bucket name (think of the bucket_name as being like a database name).
51
+ #
52
+ # If any of these required arguments is missing, a MissingAccessKey exception will be raised from AWS::S3.
53
+ #
54
+ # == About bucket names
55
+ #
56
+ # Bucket names have to be globaly unique across the S3 system. And you can only have up to 100 of them,
57
+ # so it's a good idea to think of a bucket as being like a database, hence the correspondance in this
58
+ # implementation to the development, test, and production environments.
59
+ #
60
+ # The number of objects you can store in a bucket is, for all intents and purposes, unlimited.
61
+ #
62
+ # === Optional configuration parameters
63
+ #
64
+ # * <tt>:server</tt> - The server to make requests to. Defaults to <tt>s3.amazonaws.com</tt>.
65
+ # * <tt>:port</tt> - The port to the requests should be made on. Defaults to 80 or 443 if <tt>:use_ssl</tt> is set.
66
+ # * <tt>:use_ssl</tt> - If set to true, <tt>:port</tt> will be implicitly set to 443, unless specified otherwise. Defaults to false.
67
+ # * <tt>:distribution_domain</tt> - The CloudFront distribution domain for the bucket. This can either be the assigned
68
+ # distribution domain (ie. XXX.cloudfront.net) or a chosen domain using a CNAME. See CloudFront for more details.
69
+ #
70
+ # == Usage
71
+ #
72
+ # To specify S3 as the storage mechanism for a model, set the acts_as_attachment <tt>:storage</tt> option to <tt>:s3</tt>.
73
+ #
74
+ # class Photo < ActiveRecord::Base
75
+ # has_attachment :storage => :s3
76
+ # end
77
+ #
78
+ # === Customizing the path
79
+ #
80
+ # By default, files are prefixed using a pseudo hierarchy in the form of <tt>:table_name/:id</tt>, which results
81
+ # in S3 urls that look like: http(s)://:server/:bucket_name/:table_name/:id/:filename with :table_name
82
+ # representing the customizable portion of the path. You can customize this prefix using the <tt>:path_prefix</tt>
83
+ # option:
84
+ #
85
+ # class Photo < ActiveRecord::Base
86
+ # has_attachment :storage => :s3, :path_prefix => 'my/custom/path'
87
+ # end
88
+ #
89
+ # Which would result in URLs like <tt>http(s)://:server/:bucket_name/my/custom/path/:id/:filename.</tt>
90
+ #
91
+ # === Using different bucket names on different models
92
+ #
93
+ # By default the bucket name that the file will be stored to is the one specified by the
94
+ # <tt>:bucket_name</tt> key in the amazon_s3.yml file. You can use the <tt>:bucket_key</tt> option
95
+ # to overide this behavior on a per model basis. For instance if you want a bucket that will hold
96
+ # only Photos you can do this:
97
+ #
98
+ # class Photo < ActiveRecord::Base
99
+ # has_attachment :storage => :s3, :bucket_key => :photo_bucket_name
100
+ # end
101
+ #
102
+ # And then your amazon_s3.yml file needs to look like this.
103
+ #
104
+ # development:
105
+ # bucket_name: appname_development
106
+ # access_key_id: <your key>
107
+ # secret_access_key: <your key>
108
+ #
109
+ # test:
110
+ # bucket_name: appname_test
111
+ # access_key_id: <your key>
112
+ # secret_access_key: <your key>
113
+ #
114
+ # production:
115
+ # bucket_name: appname
116
+ # photo_bucket_name: appname_photos
117
+ # access_key_id: <your key>
118
+ # secret_access_key: <your key>
119
+ #
120
+ # If the bucket_key you specify is not there in a certain environment then attachment_fu will
121
+ # default to the <tt>bucket_name</tt> key. This way you only have to create special buckets
122
+ # this can be helpful if you only need special buckets in certain environments.
123
+ #
124
+ # === Permissions
125
+ #
126
+ # By default, files are stored on S3 with public access permissions. You can customize this using
127
+ # the <tt>:s3_access</tt> option to <tt>has_attachment</tt>. Available values are
128
+ # <tt>:private</tt>, <tt>:public_read_write</tt>, and <tt>:authenticated_read</tt>.
129
+ #
130
+ # === Other options
131
+ #
132
+ # Of course, all the usual configuration options apply, such as content_type and thumbnails:
133
+ #
134
+ # class Photo < ActiveRecord::Base
135
+ # has_attachment :storage => :s3, :content_type => ['application/pdf', :image], :resize_to => 'x50'
136
+ # has_attachment :storage => :s3, :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
137
+ # end
138
+ #
139
+ # === Accessing S3 URLs
140
+ #
141
+ # You can get an object's URL using the s3_url accessor. For example, assuming that for your postcard app
142
+ # you had a bucket name like 'postcard_world_development', and an attachment model called Photo:
143
+ #
144
+ # @postcard.s3_url # => http(s)://s3.amazonaws.com/postcard_world_development/photos/1/mexico.jpg
145
+ #
146
+ # The resulting url is in the form: http(s)://:server/:bucket_name/:table_name/:id/:file.
147
+ # The optional thumbnail argument will output the thumbnail's filename (if any).
148
+ #
149
+ # Additionally, you can get an object's base path relative to the bucket root using
150
+ # <tt>base_path</tt>:
151
+ #
152
+ # @photo.file_base_path # => photos/1
153
+ #
154
+ # And the full path (including the filename) using <tt>full_filename</tt>:
155
+ #
156
+ # @photo.full_filename # => photos/
157
+ #
158
+ # Niether <tt>base_path</tt> or <tt>full_filename</tt> include the bucket name as part of the path.
159
+ # You can retrieve the bucket name using the <tt>bucket_name</tt> method.
160
+ #
161
+ # === Accessing CloudFront URLs
162
+ #
163
+ # You can get an object's CloudFront URL using the cloudfront_url accessor. Using the example from above:
164
+ # @postcard.cloudfront_url # => http://XXXX.cloudfront.net/photos/1/mexico.jpg
165
+ #
166
+ # The resulting url is in the form: http://:distribution_domain/:table_name/:id/:file
167
+ #
168
+ # If you set :cloudfront to true in your model, the public_filename will be the CloudFront
169
+ # URL, not the S3 URL.
170
+ module S3Backend
171
+ class RequiredLibraryNotFoundError < StandardError; end
172
+ class ConfigFileNotFoundError < StandardError; end
173
+
174
+ def self.included(base) #:nodoc:
175
+ mattr_reader :bucket_name, :s3_config
176
+
177
+ begin
178
+ require 'aws/s3'
179
+ include AWS::S3
180
+ rescue LoadError
181
+ raise RequiredLibraryNotFoundError.new('AWS::S3 could not be loaded')
182
+ end
183
+
184
+ begin
185
+ @@s3_config_path = base.attachment_options[:s3_config_path] || (RAILS_ROOT + '/config/amazon_s3.yml')
186
+ @@s3_config = @@s3_config = YAML.load(ERB.new(File.read(@@s3_config_path)).result)[RAILS_ENV].symbolize_keys
187
+ #rescue
188
+ # raise ConfigFileNotFoundError.new('File %s not found' % @@s3_config_path)
189
+ end
190
+
191
+ bucket_key = base.attachment_options[:bucket_key]
192
+
193
+ if bucket_key and s3_config[bucket_key.to_sym]
194
+ eval_string = "def bucket_name()\n \"#{s3_config[bucket_key.to_sym]}\"\nend"
195
+ else
196
+ eval_string = "def bucket_name()\n \"#{s3_config[:bucket_name]}\"\nend"
197
+ end
198
+ base.class_eval(eval_string, __FILE__, __LINE__)
199
+
200
+ Base.establish_connection!(s3_config.slice(:access_key_id, :secret_access_key, :server, :port, :use_ssl, :persistent, :proxy))
201
+
202
+ # Bucket.create(@@bucket_name)
203
+
204
+ base.before_update :rename_file
205
+ end
206
+
207
+ def self.protocol
208
+ @protocol ||= s3_config[:use_ssl] ? 'https://' : 'http://'
209
+ end
210
+
211
+ def self.hostname
212
+ @hostname ||= s3_config[:server] || AWS::S3::DEFAULT_HOST
213
+ end
214
+
215
+ def self.port_string
216
+ @port_string ||= (s3_config[:port].nil? || s3_config[:port] == (s3_config[:use_ssl] ? 443 : 80)) ? '' : ":#{s3_config[:port]}"
217
+ end
218
+
219
+ def self.distribution_domain
220
+ @distribution_domain = s3_config[:distribution_domain]
221
+ end
222
+
223
+ module ClassMethods
224
+ def s3_protocol
225
+ AttachmentFu::Backends::S3Backend.protocol
226
+ end
227
+
228
+ def s3_hostname
229
+ AttachmentFu::Backends::S3Backend.hostname
230
+ end
231
+
232
+ def s3_port_string
233
+ AttachmentFu::Backends::S3Backend.port_string
234
+ end
235
+
236
+ def cloudfront_distribution_domain
237
+ AttachmentFu::Backends::S3Backend.distribution_domain
238
+ end
239
+ end
240
+
241
+ # Overwrites the base filename writer in order to store the old filename
242
+ def filename=(value)
243
+ @old_filename = filename unless filename.nil? || @old_filename
244
+ write_attribute :filename, sanitize_filename(value)
245
+ end
246
+
247
+ # The attachment ID used in the full path of a file
248
+ def attachment_path_id
249
+ ((respond_to?(:parent_id) && parent_id) || id).to_s
250
+ end
251
+
252
+ # The pseudo hierarchy containing the file relative to the bucket name
253
+ # Example: <tt>:table_name/:id</tt>
254
+ def base_path
255
+ File.join(attachment_options[:path_prefix], attachment_path_id)
256
+ end
257
+
258
+ # The full path to the file relative to the bucket name
259
+ # Example: <tt>:table_name/:id/:filename</tt>
260
+ def full_filename(thumbnail = nil)
261
+ File.join(base_path, thumbnail_name_for(thumbnail))
262
+ end
263
+
264
+ # All public objects are accessible via a GET request to the S3 servers. You can generate a
265
+ # url for an object using the s3_url method.
266
+ #
267
+ # @photo.s3_url
268
+ #
269
+ # The resulting url is in the form: <tt>http(s)://:server/:bucket_name/:table_name/:id/:file</tt> where
270
+ # the <tt>:server</tt> variable defaults to <tt>AWS::S3 URL::DEFAULT_HOST</tt> (s3.amazonaws.com) and can be
271
+ # set using the configuration parameters in <tt>RAILS_ROOT/config/amazon_s3.yml</tt>.
272
+ #
273
+ # The optional thumbnail argument will output the thumbnail's filename (if any).
274
+ def s3_url(thumbnail = nil)
275
+ File.join(s3_protocol + s3_hostname + s3_port_string, bucket_name, full_filename(thumbnail))
276
+ end
277
+
278
+ # All public objects are accessible via a GET request to CloudFront. You can generate a
279
+ # url for an object using the cloudfront_url method.
280
+ #
281
+ # @photo.cloudfront_url
282
+ #
283
+ # The resulting url is in the form: <tt>http://:distribution_domain/:table_name/:id/:file</tt> using
284
+ # the <tt>:distribution_domain</tt> variable set in the configuration parameters in <tt>RAILS_ROOT/config/amazon_s3.yml</tt>.
285
+ #
286
+ # The optional thumbnail argument will output the thumbnail's filename (if any).
287
+ def cloudfront_url(thumbnail = nil)
288
+ "http://" + cloudfront_distribution_domain + "/" + full_filename(thumbnail)
289
+ end
290
+
291
+ def public_filename(*args)
292
+ if attachment_options[:cloudfront]
293
+ cloudfront_url(args)
294
+ else
295
+ s3_url(args)
296
+ end
297
+ end
298
+
299
+ # All private objects are accessible via an authenticated GET request to the S3 servers. You can generate an
300
+ # authenticated url for an object like this:
301
+ #
302
+ # @photo.authenticated_s3_url
303
+ #
304
+ # By default authenticated urls expire 5 minutes after they were generated.
305
+ #
306
+ # Expiration options can be specified either with an absolute time using the <tt>:expires</tt> option,
307
+ # or with a number of seconds relative to now with the <tt>:expires_in</tt> option:
308
+ #
309
+ # # Absolute expiration date (October 13th, 2025)
310
+ # @photo.authenticated_s3_url(:expires => Time.mktime(2025,10,13).to_i)
311
+ #
312
+ # # Expiration in five hours from now
313
+ # @photo.authenticated_s3_url(:expires_in => 5.hours)
314
+ #
315
+ # You can specify whether the url should go over SSL with the <tt>:use_ssl</tt> option.
316
+ # By default, the ssl settings for the current connection will be used:
317
+ #
318
+ # @photo.authenticated_s3_url(:use_ssl => true)
319
+ #
320
+ # Finally, the optional thumbnail argument will output the thumbnail's filename (if any):
321
+ #
322
+ # @photo.authenticated_s3_url('thumbnail', :expires_in => 5.hours, :use_ssl => true)
323
+ def authenticated_s3_url(*args)
324
+ options = args.extract_options!
325
+ options[:expires_in] = options[:expires_in].to_i if options[:expires_in]
326
+ thumbnail = args.shift
327
+ S3Object.url_for(full_filename(thumbnail), bucket_name, options)
328
+ end
329
+
330
+ def create_temp_file
331
+ write_to_temp_file current_data
332
+ end
333
+
334
+ def current_data
335
+ S3Object.value full_filename, bucket_name
336
+ end
337
+
338
+ def s3_protocol
339
+ AttachmentFu::Backends::S3Backend.protocol
340
+ end
341
+
342
+ def s3_hostname
343
+ AttachmentFu::Backends::S3Backend.hostname
344
+ end
345
+
346
+ def s3_port_string
347
+ AttachmentFu::Backends::S3Backend.port_string
348
+ end
349
+
350
+ def cloudfront_distribution_domain
351
+ AttachmentFu::Backends::S3Backend.distribution_domain
352
+ end
353
+
354
+ protected
355
+ # Called in the after_destroy callback
356
+ def destroy_file
357
+ S3Object.delete full_filename, bucket_name
358
+ end
359
+
360
+ def rename_file
361
+ return unless @old_filename && @old_filename != filename
362
+
363
+ old_full_filename = File.join(base_path, @old_filename)
364
+
365
+ S3Object.rename(
366
+ old_full_filename,
367
+ full_filename,
368
+ bucket_name,
369
+ :access => attachment_options[:s3_access]
370
+ )
371
+
372
+ @old_filename = nil
373
+ true
374
+ end
375
+
376
+ def save_to_storage
377
+ if save_attachment?
378
+ S3Object.store(
379
+ full_filename,
380
+ (temp_path ? File.open(temp_path) : temp_data),
381
+ bucket_name,
382
+ :content_type => content_type,
383
+ :access => attachment_options[:s3_access]
384
+ )
385
+ end
386
+
387
+ @old_filename = nil
388
+ true
389
+ end
390
+ end
391
+ end
392
+ end
@@ -0,0 +1,55 @@
1
+ require 'red_artisan/core_image/processor'
2
+
3
+ module AttachmentFu # :nodoc:
4
+ module Processors
5
+ module CoreImageProcessor
6
+ def self.included(base)
7
+ base.send :extend, ClassMethods
8
+ base.alias_method_chain :process_attachment, :processing
9
+ end
10
+
11
+ module ClassMethods
12
+ def with_image(file, &block)
13
+ block.call OSX::CIImage.from(file)
14
+ end
15
+ end
16
+
17
+ protected
18
+ def process_attachment_with_processing
19
+ return unless process_attachment_without_processing
20
+ with_image do |img|
21
+ self.width = img.extent.size.width if respond_to?(:width)
22
+ self.height = img.extent.size.height if respond_to?(:height)
23
+ resize_image_or_thumbnail! img
24
+ callback_with_args :after_resize, img
25
+ end if image?
26
+ end
27
+
28
+ # Performs the actual resizing operation for a thumbnail
29
+ def resize_image(img, size)
30
+ processor = ::RedArtisan::CoreImage::Processor.new(img)
31
+ size = size.first if size.is_a?(Array) && size.length == 1
32
+ if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
33
+ if size.is_a?(Fixnum)
34
+ processor.fit(size)
35
+ else
36
+ processor.resize(size[0], size[1])
37
+ end
38
+ else
39
+ new_size = [img.extent.size.width, img.extent.size.height] / size.to_s
40
+ processor.resize(new_size[0], new_size[1])
41
+ end
42
+
43
+ processor.render do |result|
44
+ self.width = result.extent.size.width if respond_to?(:width)
45
+ self.height = result.extent.size.height if respond_to?(:height)
46
+
47
+ # Get a new temp_path for the image before saving
48
+ temp_paths.unshift Tempfile.new(random_tempfile_filename, AttachmentFu.tempfile_path).path
49
+ result.save self.temp_path, OSX::NSJPEGFileType
50
+ self.size = File.size(self.temp_path)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,53 @@
1
+ require 'rubygems'
2
+ require 'gd2'
3
+
4
+ module AttachmentFu # :nodoc:
5
+ module Processors
6
+ module Gd2Processor
7
+ def self.included(base)
8
+ base.send :extend, ClassMethods
9
+ base.alias_method_chain :process_attachment, :processing
10
+ end
11
+
12
+ module ClassMethods
13
+ # Yields a block containing a GD2 Image for the given binary data.
14
+ def with_image(file, &block)
15
+ im = GD2::Image.import(file)
16
+ block.call(im)
17
+ end
18
+ end
19
+
20
+ protected
21
+ def process_attachment_with_processing
22
+ return unless process_attachment_without_processing && image?
23
+ with_image do |img|
24
+ resize_image_or_thumbnail! img
25
+ self.width = img.width
26
+ self.height = img.height
27
+ callback_with_args :after_resize, img
28
+ end
29
+ end
30
+
31
+ # Performs the actual resizing operation for a thumbnail
32
+ def resize_image(img, size)
33
+ size = size.first if size.is_a?(Array) && size.length == 1
34
+ if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
35
+ if size.is_a?(Fixnum)
36
+ # Borrowed from image science's #thumbnail method and adapted
37
+ # for this.
38
+ scale = size.to_f / (img.width > img.height ? img.width.to_f : img.height.to_f)
39
+ img.resize!((img.width * scale).round(1), (img.height * scale).round(1), false)
40
+ else
41
+ img.resize!(size.first, size.last, false)
42
+ end
43
+ else
44
+ w, h = [img.width, img.height] / size.to_s
45
+ img.resize!(w, h, false)
46
+ end
47
+ temp_paths.unshift random_tempfile_filename
48
+ self.size = img.export(self.temp_path)
49
+ end
50
+
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,60 @@
1
+ require 'image_science'
2
+
3
+ module AttachmentFu # :nodoc:
4
+ module Processors
5
+ module ImageScienceProcessor
6
+ def self.included(base)
7
+ base.send :extend, ClassMethods
8
+ base.alias_method_chain :process_attachment, :processing
9
+ end
10
+
11
+ module ClassMethods
12
+ # Yields a block containing an Image Science image for the given binary data.
13
+ def with_image(file, &block)
14
+ ::ImageScience.with_image file, &block
15
+ end
16
+ end
17
+
18
+ protected
19
+ def process_attachment_with_processing
20
+ return unless process_attachment_without_processing && image?
21
+ with_image do |img|
22
+ self.width = img.width if respond_to?(:width)
23
+ self.height = img.height if respond_to?(:height)
24
+ resize_image_or_thumbnail! img
25
+ end
26
+ end
27
+
28
+ # Performs the actual resizing operation for a thumbnail
29
+ def resize_image(img, size)
30
+ # create a dummy temp file to write to
31
+ # ImageScience doesn't handle all gifs properly, so it converts them to
32
+ # pngs for thumbnails. It has something to do with trying to save gifs
33
+ # with a larger palette than 256 colors, which is all the gif format
34
+ # supports.
35
+ filename.sub! /gif$/, 'png'
36
+ content_type.sub!(/gif$/, 'png')
37
+ temp_paths.unshift write_to_temp_file(filename)
38
+ grab_dimensions = lambda do |img|
39
+ self.width = img.width if respond_to?(:width)
40
+ self.height = img.height if respond_to?(:height)
41
+ img.save self.temp_path
42
+ self.size = File.size(self.temp_path)
43
+ callback_with_args :after_resize, img
44
+ end
45
+
46
+ size = size.first if size.is_a?(Array) && size.length == 1
47
+ if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
48
+ if size.is_a?(Fixnum)
49
+ img.thumbnail(size, &grab_dimensions)
50
+ else
51
+ img.resize(size[0], size[1], &grab_dimensions)
52
+ end
53
+ else
54
+ new_size = [img.width, img.height] / size.to_s
55
+ img.resize(new_size[0], new_size[1], &grab_dimensions)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,131 @@
1
+ require 'mini_magick'
2
+
3
+ module AttachmentFu # :nodoc:
4
+ module Processors
5
+ module MiniMagickProcessor
6
+ def self.included(base)
7
+ base.send :extend, ClassMethods
8
+ base.alias_method_chain :process_attachment, :processing
9
+ end
10
+
11
+ module ClassMethods
12
+ # Yields a block containing an MiniMagick Image for the given binary data.
13
+ def with_image(file, &block)
14
+ begin
15
+ binary_data = file.is_a?(MiniMagick::Image) ? file : MiniMagick::Image.from_file(file) unless !Object.const_defined?(:MiniMagick)
16
+ rescue
17
+ # Log the failure to load the image.
18
+ logger.debug("Exception working with image: #{$!}")
19
+ binary_data = nil
20
+ end
21
+ block.call binary_data if block && binary_data
22
+ ensure
23
+ !binary_data.nil?
24
+ end
25
+ end
26
+
27
+ protected
28
+ def process_attachment_with_processing
29
+ return unless process_attachment_without_processing
30
+ with_image do |img|
31
+ resize_image_or_thumbnail! img
32
+ self.width = img[:width] if respond_to?(:width)
33
+ self.height = img[:height] if respond_to?(:height)
34
+ callback_with_args :after_resize, img
35
+ end if image?
36
+ end
37
+
38
+ # Performs the actual resizing operation for a thumbnail
39
+ def resize_image(img, size)
40
+ size = size.first if size.is_a?(Array) && size.length == 1
41
+ img.combine_options do |commands|
42
+ commands.strip unless attachment_options[:keep_profile]
43
+
44
+ # gif are not handled correct, this is a hack, but it seems to work.
45
+ if img.output =~ / GIF /
46
+ img.format("png")
47
+ end
48
+
49
+ if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
50
+ if size.is_a?(Fixnum)
51
+ size = [size, size]
52
+ commands.resize(size.join('x'))
53
+ else
54
+ commands.resize(size.join('x') + '!')
55
+ end
56
+ # extend to thumbnail size
57
+ elsif size.is_a?(String) and size =~ /e$/
58
+ size = size.gsub(/e/, '')
59
+ commands.resize(size.to_s + '>')
60
+ commands.background('#ffffff')
61
+ commands.gravity('center')
62
+ commands.extent(size)
63
+ # crop thumbnail, the smart way
64
+ elsif size.is_a?(String) and size =~ /c$/
65
+ size = size.gsub(/c/, '')
66
+
67
+ # calculate sizes and aspect ratio
68
+ thumb_width, thumb_height = size.split("x")
69
+ thumb_width = thumb_width.to_f
70
+ thumb_height = thumb_height.to_f
71
+
72
+ thumb_aspect = thumb_width.to_f / thumb_height.to_f
73
+ image_width, image_height = img[:width].to_f, img[:height].to_f
74
+ image_aspect = image_width / image_height
75
+
76
+ # only crop if image is not smaller in both dimensions
77
+ unless image_width < thumb_width and image_height < thumb_height
78
+ command = calculate_offset(image_width,image_height,image_aspect,thumb_width,thumb_height,thumb_aspect)
79
+
80
+ # crop image
81
+ commands.extract(command)
82
+ end
83
+
84
+ # don not resize if image is not as height or width then thumbnail
85
+ if image_width < thumb_width or image_height < thumb_height
86
+ commands.background('#ffffff')
87
+ commands.gravity('center')
88
+ commands.extent(size)
89
+ # resize image
90
+ else
91
+ commands.resize("#{size.to_s}")
92
+ end
93
+ # crop end
94
+ else
95
+ commands.resize(size.to_s)
96
+ end
97
+ end
98
+ temp_paths.unshift img
99
+ end
100
+
101
+ def calculate_offset(image_width,image_height,image_aspect,thumb_width,thumb_height,thumb_aspect)
102
+ # only crop if image is not smaller in both dimensions
103
+
104
+ # special cases, image smaller in one dimension then thumbsize
105
+ if image_width < thumb_width
106
+ offset = (image_height / 2) - (thumb_height / 2)
107
+ command = "#{image_width}x#{thumb_height}+0+#{offset}"
108
+ elsif image_height < thumb_height
109
+ offset = (image_width / 2) - (thumb_width / 2)
110
+ command = "#{thumb_width}x#{image_height}+#{offset}+0"
111
+
112
+ # normal thumbnail generation
113
+ # calculate height and offset y, width is fixed
114
+ elsif (image_aspect <= thumb_aspect or image_width < thumb_width) and image_height > thumb_height
115
+ height = image_width / thumb_aspect
116
+ offset = (image_height / 2) - (height / 2)
117
+ command = "#{image_width}x#{height}+0+#{offset}"
118
+ # calculate width and offset x, height is fixed
119
+ else
120
+ width = image_height * thumb_aspect
121
+ offset = (image_width / 2) - (width / 2)
122
+ command = "#{width}x#{image_height}+#{offset}+0"
123
+ end
124
+ # crop image
125
+ command
126
+ end
127
+
128
+
129
+ end
130
+ end
131
+ end