popel-attachment_fu 1.0.4

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 (33) hide show
  1. data/README +200 -0
  2. data/VERSION.yml +4 -0
  3. data/lib/geometry.rb +93 -0
  4. data/lib/technoweenie/attachment_fu.rb +528 -0
  5. data/lib/technoweenie/attachment_fu/backends/db_file_backend.rb +39 -0
  6. data/lib/technoweenie/attachment_fu/backends/file_system_backend.rb +126 -0
  7. data/lib/technoweenie/attachment_fu/backends/s3_backend.rb +394 -0
  8. data/lib/technoweenie/attachment_fu/processors/core_image_processor.rb +59 -0
  9. data/lib/technoweenie/attachment_fu/processors/gd2_processor.rb +54 -0
  10. data/lib/technoweenie/attachment_fu/processors/image_science_processor.rb +61 -0
  11. data/lib/technoweenie/attachment_fu/processors/mini_magick_processor.rb +132 -0
  12. data/lib/technoweenie/attachment_fu/processors/rmagick_processor.rb +57 -0
  13. data/test/backends/db_file_test.rb +16 -0
  14. data/test/backends/file_system_test.rb +143 -0
  15. data/test/backends/remote/s3_test.rb +119 -0
  16. data/test/base_attachment_tests.rb +77 -0
  17. data/test/basic_test.rb +70 -0
  18. data/test/database.yml +18 -0
  19. data/test/extra_attachment_test.rb +67 -0
  20. data/test/fixtures/attachment.rb +215 -0
  21. data/test/fixtures/files/fake/rails.png +0 -0
  22. data/test/fixtures/files/foo.txt +1 -0
  23. data/test/fixtures/files/rails.png +0 -0
  24. data/test/geometry_test.rb +108 -0
  25. data/test/processors/core_image_test.rb +37 -0
  26. data/test/processors/gd2_test.rb +31 -0
  27. data/test/processors/image_science_test.rb +31 -0
  28. data/test/processors/mini_magick_test.rb +103 -0
  29. data/test/processors/rmagick_test.rb +255 -0
  30. data/test/schema.rb +121 -0
  31. data/test/test_helper.rb +150 -0
  32. data/test/validation_test.rb +55 -0
  33. metadata +95 -0
@@ -0,0 +1,59 @@
1
+ require 'red_artisan/core_image/processor'
2
+
3
+ module Technoweenie # :nodoc:
4
+ module AttachmentFu # :nodoc:
5
+ module Processors
6
+ module CoreImageProcessor
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
+ def with_image(file, &block)
14
+ block.call OSX::CIImage.from(file)
15
+ end
16
+ end
17
+
18
+ protected
19
+ def process_attachment_with_processing
20
+ return unless process_attachment_without_processing
21
+ with_image do |img|
22
+ self.width = img.extent.size.width if respond_to?(:width)
23
+ self.height = img.extent.size.height if respond_to?(:height)
24
+ resize_image_or_thumbnail! img
25
+ callback_with_args :after_resize, img
26
+ end if image?
27
+ end
28
+
29
+ # Performs the actual resizing operation for a thumbnail
30
+ def resize_image(img, size)
31
+ processor = ::RedArtisan::CoreImage::Processor.new(img)
32
+ size = size.first if size.is_a?(Array) && size.length == 1
33
+ if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
34
+ if size.is_a?(Fixnum)
35
+ processor.fit(size)
36
+ else
37
+ processor.resize(size[0], size[1])
38
+ end
39
+ else
40
+ new_size = [img.extent.size.width, img.extent.size.height] / size.to_s
41
+ processor.resize(new_size[0], new_size[1])
42
+ end
43
+
44
+ processor.render do |result|
45
+ self.width = result.extent.size.width if respond_to?(:width)
46
+ self.height = result.extent.size.height if respond_to?(:height)
47
+
48
+ # Get a new temp_path for the image before saving
49
+ temp_paths.unshift Tempfile.new(random_tempfile_filename, Technoweenie::AttachmentFu.tempfile_path).path
50
+ result.save self.temp_path, OSX::NSJPEGFileType
51
+ self.size = File.size(self.temp_path)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+
@@ -0,0 +1,54 @@
1
+ require 'rubygems'
2
+ require 'gd2'
3
+ module Technoweenie # :nodoc:
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
54
+ end
@@ -0,0 +1,61 @@
1
+ require 'image_science'
2
+ module Technoweenie # :nodoc:
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
61
+ end
@@ -0,0 +1,132 @@
1
+ require 'mini_magick'
2
+ module Technoweenie # :nodoc:
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
132
+ end
@@ -0,0 +1,57 @@
1
+ require 'RMagick'
2
+ module Technoweenie # :nodoc:
3
+ module AttachmentFu # :nodoc:
4
+ module Processors
5
+ module RmagickProcessor
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 RMagick Image for the given binary data.
13
+ def with_image(file, &block)
14
+ begin
15
+ binary_data = file.is_a?(Magick::Image) ? file : Magick::Image.read(file).first unless !Object.const_defined?(:Magick)
16
+ rescue
17
+ # Log the failure to load the image. This should match ::Magick::ImageMagickError
18
+ # but that would cause acts_as_attachment to require rmagick.
19
+ logger.debug("Exception working with image: #{$!}")
20
+ binary_data = nil
21
+ end
22
+ block.call binary_data if block && binary_data
23
+ ensure
24
+ !binary_data.nil?
25
+ end
26
+ end
27
+
28
+ protected
29
+ def process_attachment_with_processing
30
+ return unless process_attachment_without_processing
31
+ with_image do |img|
32
+ resize_image_or_thumbnail! img
33
+ self.width = img.columns if respond_to?(:width)
34
+ self.height = img.rows if respond_to?(:height)
35
+ callback_with_args :after_resize, img
36
+ end if image?
37
+ end
38
+
39
+ # Performs the actual resizing operation for a thumbnail
40
+ def resize_image(img, size)
41
+ size = size.first if size.is_a?(Array) && size.length == 1 && !size.first.is_a?(Fixnum)
42
+ if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
43
+ size = [size, size] if size.is_a?(Fixnum)
44
+ img.thumbnail!(*size)
45
+ elsif size.is_a?(String) && size =~ /^c.*$/ # Image cropping - example geometry string: c75x75
46
+ dimensions = size[1..size.size].split("x")
47
+ img.crop_resized!(dimensions[0].to_i, dimensions[1].to_i)
48
+ else
49
+ img.change_geometry(size.to_s) { |cols, rows, image| image.resize!(cols<1 ? 1 : cols, rows<1 ? 1 : rows) }
50
+ end
51
+ img.strip! unless attachment_options[:keep_profile]
52
+ temp_paths.unshift write_to_temp_file(img.to_blob)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,16 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper'))
2
+
3
+ class DbFileTest < Test::Unit::TestCase
4
+ include BaseAttachmentTests
5
+ attachment_model Attachment
6
+
7
+ def test_should_call_after_attachment_saved(klass = Attachment)
8
+ attachment_model.saves = 0
9
+ assert_created do
10
+ upload_file :filename => '/files/rails.png'
11
+ end
12
+ assert_equal 1, attachment_model.saves
13
+ end
14
+
15
+ test_against_subclass :test_should_call_after_attachment_saved, Attachment
16
+ end
@@ -0,0 +1,143 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper'))
2
+ require 'digest/sha2'
3
+
4
+ class FileSystemTest < Test::Unit::TestCase
5
+ include BaseAttachmentTests
6
+ attachment_model FileAttachment
7
+
8
+ def test_filesystem_size_for_file_attachment(klass = FileAttachment)
9
+ attachment_model klass
10
+ assert_created 1 do
11
+ attachment = upload_file :filename => '/files/rails.png'
12
+ assert_equal attachment.size, File.open(attachment.full_filename).stat.size
13
+ end
14
+ end
15
+
16
+ test_against_subclass :test_filesystem_size_for_file_attachment, FileAttachment
17
+
18
+ def test_should_not_overwrite_file_attachment(klass = FileAttachment)
19
+ attachment_model klass
20
+ assert_created 2 do
21
+ real = upload_file :filename => '/files/rails.png'
22
+ assert_valid real
23
+ assert !real.new_record?, real.errors.full_messages.join("\n")
24
+ assert !real.size.zero?
25
+
26
+ fake = upload_file :filename => '/files/fake/rails.png'
27
+ assert_valid fake
28
+ assert !fake.size.zero?
29
+
30
+ assert_not_equal File.open(real.full_filename).stat.size, File.open(fake.full_filename).stat.size
31
+ end
32
+ end
33
+
34
+ test_against_subclass :test_should_not_overwrite_file_attachment, FileAttachment
35
+
36
+ def test_should_store_file_attachment_in_filesystem(klass = FileAttachment)
37
+ attachment_model klass
38
+ attachment = nil
39
+ assert_created do
40
+ attachment = upload_file :filename => '/files/rails.png'
41
+ assert_valid attachment
42
+ assert File.exists?(attachment.full_filename), "#{attachment.full_filename} does not exist"
43
+ end
44
+ attachment
45
+ end
46
+
47
+ test_against_subclass :test_should_store_file_attachment_in_filesystem, FileAttachment
48
+
49
+ def test_should_delete_old_file_when_updating(klass = FileAttachment)
50
+ attachment_model klass
51
+ attachment = upload_file :filename => '/files/rails.png'
52
+ old_filename = attachment.full_filename
53
+ assert_not_created do
54
+ use_temp_file 'files/rails.png' do |file|
55
+ attachment.filename = 'rails2.png'
56
+ attachment.temp_paths.unshift File.join(fixture_path, file)
57
+ attachment.save!
58
+ assert File.exists?(attachment.full_filename), "#{attachment.full_filename} does not exist"
59
+ assert !File.exists?(old_filename), "#{old_filename} still exists"
60
+ end
61
+ end
62
+ end
63
+
64
+ test_against_subclass :test_should_delete_old_file_when_updating, FileAttachment
65
+
66
+ def test_should_delete_old_file_when_renaming(klass = FileAttachment)
67
+ attachment_model klass
68
+ attachment = upload_file :filename => '/files/rails.png'
69
+ old_filename = attachment.full_filename
70
+ assert_not_created do
71
+ attachment.filename = 'rails2.png'
72
+ attachment.save
73
+ assert File.exists?(attachment.full_filename), "#{attachment.full_filename} does not exist"
74
+ assert !File.exists?(old_filename), "#{old_filename} still exists"
75
+ assert !attachment.reload.size.zero?
76
+ assert_equal 'rails2.png', attachment.filename
77
+ end
78
+ end
79
+
80
+ test_against_subclass :test_should_delete_old_file_when_renaming, FileAttachment
81
+
82
+ def test_path_partitioning_works_on_integer_id(klass = FileAttachment)
83
+ attachment_model klass
84
+
85
+ # Create a random attachment object, doesn't matter what.
86
+ attachment = upload_file :filename => '/files/rails.png'
87
+ old_id = attachment.id
88
+ attachment.id = 1
89
+
90
+ begin
91
+ assert_equal ["0000", "0001", "bar.txt"], attachment.send(:partitioned_path, "bar.txt")
92
+ ensure
93
+ attachment.id = old_id
94
+ end
95
+ end
96
+
97
+ test_against_subclass :test_path_partitioning_works_on_integer_id, FileAttachment
98
+
99
+ def test_path_partitioning_with_string_id_works_by_generating_hash(klass = FileAttachmentWithStringId)
100
+ attachment_model klass
101
+
102
+ # Create a random attachment object, doesn't matter what.
103
+ attachment = upload_file :filename => '/files/rails.png'
104
+ old_id = attachment.id
105
+ attachment.id = "hello world some long string"
106
+ hash = Digest::SHA512.hexdigest("hello world some long string")
107
+
108
+ begin
109
+ assert_equal [
110
+ hash[0..31],
111
+ hash[32..63],
112
+ hash[64..95],
113
+ hash[96..127],
114
+ "bar.txt"
115
+ ], attachment.send(:partitioned_path, "bar.txt")
116
+ ensure
117
+ attachment.id = old_id
118
+ end
119
+ end
120
+
121
+ test_against_subclass :test_path_partitioning_with_string_id_works_by_generating_hash, FileAttachmentWithStringId
122
+
123
+ def test_path_partition_string_id_hashing_is_turned_off_if_id_is_uuid(klass = FileAttachmentWithUuid)
124
+ attachment_model klass
125
+
126
+ # Create a random attachment object, doesn't matter what.
127
+ attachment = upload_file :filename => '/files/rails.png'
128
+ old_id = attachment.id
129
+ attachment.id = "0c0743b698483569dc65909a8cdb3bf9"
130
+
131
+ begin
132
+ assert_equal [
133
+ "0c0743b698483569",
134
+ "dc65909a8cdb3bf9",
135
+ "bar.txt"
136
+ ], attachment.send(:partitioned_path, "bar.txt")
137
+ ensure
138
+ attachment.id = old_id
139
+ end
140
+ end
141
+
142
+ test_against_subclass :test_path_partition_string_id_hashing_is_turned_off_if_id_is_uuid, FileAttachmentWithUuid
143
+ end