public
Description: Treat an ActiveRecord model as a file attachment, storing its patch, size, content type, etc.
Homepage: http://weblog.techno-weenie.net
Clone URL: git://github.com/technoweenie/attachment_fu.git
Search Repo:
Click here to lend your support to: attachment_fu and make a donation at www.pledgie.com !
Add support for Core Image in attachment_fu via a new core_image_processor 
class that uses a vendor imported mini core image library.
Updated attachment_fu to automatically use core image when available and 
fall back to Image Science, Rmagick and MiniMagick when not available.
crafterm (author)
Thu Feb 21 00:16:53 -0800 2008
commit  d971859124fbefb4e7d9d1ea046789d1680c8ddc
tree    1ac90ea30ad8fe07cfaa05175fdf60fe9177bafc
parent  ca63a36dbc61b0b8dad91516bb0ae98f328d8be6
...
12
13
14
 
 
...
12
13
14
15
16
0
@@ -12,4 +12,6 @@
0
 ActiveRecord::Base.send(:extend, Technoweenie::AttachmentFu::ActMethods)
0
 Technoweenie::AttachmentFu.tempfile_path = ATTACHMENT_FU_TEMPFILE_PATH if Object.const_defined?(:ATTACHMENT_FU_TEMPFILE_PATH)
0
 FileUtils.mkdir_p Technoweenie::AttachmentFu.tempfile_path
0
+
0
+$:.unshift(File.dirname(__FILE__) + '/vendor')
...
1
2
3
 
4
5
6
...
1
2
 
3
4
5
6
0
@@ -1,6 +1,6 @@
0
 module Technoweenie # :nodoc:
0
   module AttachmentFu # :nodoc:
0
- @@default_processors = %w(ImageScience Rmagick MiniMagick)
0
+ @@default_processors = %w(CoreImage ImageScience Rmagick MiniMagick)
0
     @@tempfile_path = File.join(RAILS_ROOT, 'tmp', 'attachment_fu')
0
     @@content_types = ['image/jpeg', 'image/pjpeg', 'image/gif', 'image/png', 'image/x-png', 'image/jpg']
0
     mattr_reader :content_types, :tempfile_path, :default_processors
...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
0
@@ -1 +1,55 @@
0
+require 'red_artisan/core_image/processor'
0
+
0
+module Technoweenie # :nodoc:
0
+ module AttachmentFu # :nodoc:
0
+ module Processors
0
+ module CoreImageProcessor
0
+ def self.included(base)
0
+ base.send :extend, ClassMethods
0
+ base.alias_method_chain :process_attachment, :processing
0
+ end
0
+
0
+ module ClassMethods
0
+ def with_image(file, &block)
0
+ block.call OSX::CIImage.from(file)
0
+ end
0
+ end
0
+
0
+ protected
0
+ def process_attachment_with_processing
0
+ return unless process_attachment_without_processing
0
+ with_image do |img|
0
+ self.width = img.extent.size.width if respond_to?(:width)
0
+ self.height = img.extent.size.height if respond_to?(:height)
0
+ resize_image_or_thumbnail! img
0
+ callback_with_args :after_resize, img
0
+ end if image?
0
+ end
0
+
0
+ # Performs the actual resizing operation for a thumbnail
0
+ def resize_image(img, size)
0
+ processor = ::RedArtisan::CoreImage::Processor.new(img)
0
+ size = size.first if size.is_a?(Array) && size.length == 1
0
+ if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
0
+ if size.is_a?(Fixnum)
0
+ processor.fit(size)
0
+ else
0
+ processor.resize(size[0], size[1])
0
+ end
0
+ else
0
+ new_size = [img.extent.size.width, img.extent.size.height] / size.to_s
0
+ processor.resize(new_size[0], new_size[1])
0
+ end
0
+
0
+ processor.render do |result|
0
+ self.width = result.extent.size.width if respond_to?(:width)
0
+ self.height = result.extent.size.height if respond_to?(:height)
0
+ result.save self.temp_path, OSX::NSJPEGFileType
0
+ self.size = File.size(self.temp_path)
0
+ end
0
+ end
0
+ end
0
+ end
0
+ end
0
+end
...
95
96
97
 
 
 
 
 
 
 
 
 
 
98
99
100
...
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
0
@@ -95,6 +95,16 @@
0
 end
0
 
0
 begin
0
+ class CoreImageAttachment < ActiveRecord::Base
0
+ has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files',
0
+ :processor => :core_image, :thumbnails => { :thumb => [50, 51], :geometry => '31>' }, :resize_to => 55
0
+ end
0
+rescue MissingSourceFile
0
+ puts $!.message
0
+ puts "no CoreImage"
0
+end
0
+
0
+begin
0
   class MiniMagickAttachment < ActiveRecord::Base
0
     has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files',
0
       :processor => :mini_magick, :thumbnails => { :thumb => [50, 51], :geometry => '31>' }, :resize_to => 55
...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
0
@@ -1 +1,32 @@
0
+require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper'))
0
+
0
+class CoreImageTest < Test::Unit::TestCase
0
+ attachment_model CoreImageAttachment
0
+
0
+ if Object.const_defined?(:OSX)
0
+ def test_should_resize_image
0
+ attachment = upload_file :filename => '/files/rails.png'
0
+ assert_valid attachment
0
+ assert attachment.image?
0
+ # test core image thumbnail
0
+ assert_equal 42, attachment.width
0
+ assert_equal 55, attachment.height
0
+
0
+ thumb = attachment.thumbnails.detect { |t| t.filename =~ /_thumb/ }
0
+ geo = attachment.thumbnails.detect { |t| t.filename =~ /_geometry/ }
0
+
0
+ # test exact resize dimensions
0
+ assert_equal 50, thumb.width
0
+ assert_equal 51, thumb.height
0
+
0
+ # test geometry string
0
+ assert_equal 31, geo.width
0
+ assert_equal 41, geo.height
0
+ end
0
+ else
0
+ def test_flunk
0
+ puts "CoreImage not loaded, tests not running"
0
+ end
0
+ end
0
+end
...
34
35
36
 
 
 
 
 
 
 
 
 
 
 
37
38
39
...
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
0
@@ -34,6 +34,17 @@
0
     t.column :type, :string
0
   end
0
 
0
+ create_table :core_image_attachments, :force => true do |t|
0
+ t.column :parent_id, :integer
0
+ t.column :thumbnail, :string
0
+ t.column :filename, :string, :limit => 255
0
+ t.column :content_type, :string, :limit => 255
0
+ t.column :size, :integer
0
+ t.column :width, :integer
0
+ t.column :height, :integer
0
+ t.column :type, :string
0
+ end
0
+
0
   create_table :mini_magick_attachments, :force => true do |t|
0
     t.column :parent_id, :integer
0
     t.column :thumbnail, :string
...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
0
@@ -1 +1,28 @@
0
+module RedArtisan
0
+ module CoreImage
0
+ module Filters
0
+ module Color
0
+
0
+ def greyscale(color = nil, intensity = 1.00)
0
+ create_core_image_context(@original.extent.size.width, @original.extent.size.height)
0
+
0
+ color = OSX::CIColor.colorWithString("1.0 1.0 1.0 1.0") unless color
0
+
0
+ @original.color_monochrome :inputColor => color, :inputIntensity => intensity do |greyscale|
0
+ @target = greyscale
0
+ end
0
+ end
0
+
0
+ def sepia(intensity = 1.00)
0
+ create_core_image_context(@original.extent.size.width, @original.extent.size.height)
0
+
0
+ @original.sepia_tone :inputIntensity => intensity do |sepia|
0
+ @target = sepia
0
+ end
0
+ end
0
+
0
+ end
0
+ end
0
+ end
0
+end
...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
0
@@ -1 +1,32 @@
0
+module RedArtisan
0
+ module CoreImage
0
+ module Filters
0
+ module Effects
0
+
0
+ def spotlight(position, points_at, brightness, concentration, color)
0
+ create_core_image_context(@original.extent.size.width, @original.extent.size.height)
0
+
0
+ @original.spot_light :inputLightPosition => vector3(*position), :inputLightPointsAt => vector3(*points_at),
0
+ :inputBrightness => brightness, :inputConcentration => concentration, :inputColor => color do |spot|
0
+ @target = spot
0
+ end
0
+ end
0
+
0
+ def edges(intensity = 1.00)
0
+ create_core_image_context(@original.extent.size.width, @original.extent.size.height)
0
+
0
+ @original.edges :inputIntensity => intensity do |edged|
0
+ @target = edged
0
+ end
0
+ end
0
+
0
+ private
0
+
0
+ def vector3(x, y, w)
0
+ OSX::CIVector.vectorWithX_Y_Z(x, y, w)
0
+ end
0
+ end
0
+ end
0
+ end
0
+end
...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
0
@@ -1 +1,26 @@
0
+module RedArtisan
0
+ module CoreImage
0
+ module Filters
0
+ module Perspective
0
+
0
+ def perspective(top_left, top_right, bottom_left, bottom_right)
0
+ create_core_image_context(@original.extent.size.width, @original.extent.size.height)
0
+
0
+ @original.perspective_transform :inputTopLeft => top_left, :inputTopRight => top_right, :inputBottomLeft => bottom_left, :inputBottomRight => bottom_right do |transformed|
0
+ @target = transformed
0
+ end
0
+ end
0
+
0
+ def perspective_tiled(top_left, top_right, bottom_left, bottom_right)
0
+ create_core_image_context(@original.extent.size.width, @original.extent.size.height)
0
+
0
+ @original.perspective_tile :inputTopLeft => top_left, :inputTopRight => top_right, :inputBottomLeft => bottom_left, :inputBottomRight => bottom_right do |tiled|
0
+ @target = tiled
0
+ end
0
+ end
0
+
0
+ end
0
+ end
0
+ end
0
+end
...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
0
@@ -1 +1,26 @@
0
+module RedArtisan
0
+ module CoreImage
0
+ module Filters
0
+ module Quality
0
+
0
+ def reduce_noise(level = 0.02, sharpness = 0.4)
0
+ create_core_image_context(@original.extent.size.width, @original.extent.size.height)
0
+
0
+ @original.noise_reduction :inputNoiseLevel => level, :inputSharpness => sharpness do |noise_reduced|
0
+ @target = noise_reduced
0
+ end
0
+ end
0
+
0
+ def adjust_exposure(input_ev = 0.5)
0
+ create_core_image_context(@original.extent.size.width, @original.extent.size.height)
0
+
0
+ @original.exposure_adjust :inputEV => input_ev do |adjusted|
0
+ @target = adjusted
0
+ end
0
+ end
0
+
0
+ end
0
+ end
0
+ end
0
+end
...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
0
@@ -1 +1,48 @@
0
+module RedArtisan
0
+ module CoreImage
0
+ module Filters
0
+ module Scale
0
+
0
+ def resize(width, height)
0
+ create_core_image_context(width, height)
0
+
0
+ scale_x, scale_y = scale(width, height)
0
+
0
+ @original.affine_clamp :inputTransform => OSX::NSAffineTransform.transform do |clamped|
0
+ clamped.lanczos_scale_transform :inputScale => scale_x > scale_y ? scale_x : scale_y, :inputAspectRatio => scale_x / scale_y do |scaled|
0
+ scaled.crop :inputRectangle => vector(0, 0, width, height) do |cropped|
0
+ @target = cropped
0
+ end
0
+ end
0
+ end
0
+ end
0
+
0
+ def thumbnail(width, height)
0
+ create_core_image_context(width, height)
0
+
0
+ transform = OSX::NSAffineTransform.transform
0
+ transform.scaleXBy_yBy *scale(width, height)
0
+
0
+ @original.affine_transform :inputTransform => transform do |scaled|
0
+ @target = scaled
0
+ end
0
+ end
0
+
0
+ def fit(size)
0
+ original_size = @original.extent.size
0
+ scale = size.to_f / (original_size.width > original_size.height ? original_size.width : original_size.height)
0
+ resize (original_size.width * scale).to_i, (original_size.height * scale).to_i
0
+ end
0
+
0
+ private
0
+
0
+ def scale(width, height)
0
+ original_size = @original.extent.size
0
+ return width.to_f / original_size.width.to_f, height.to_f / original_size.height.to_f
0
+ end
0
+
0
+ end
0
+ end
0
+ end
0
+end
...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
0
@@ -1 +1,33 @@
0
+module RedArtisan
0
+ module CoreImage
0
+ module Filters
0
+ module Watermark
0
+
0
+ def watermark(watermark_image, tile = false, strength = 0.1)
0
+ create_core_image_context(@original.extent.size.width, @original.extent.size.height)
0
+
0
+ if watermark_image.respond_to? :to_str
0
+ watermark_image = OSX::CIImage.from(watermark_image.to_str)
0
+ end
0
+
0
+ if tile
0
+ tile_transform = OSX::NSAffineTransform.transform
0
+ tile_transform.scaleXBy_yBy 1.0, 1.0
0
+
0
+ watermark_image.affine_tile :inputTransform => tile_transform do |tiled|
0
+ tiled.crop :inputRectangle => vector(0, 0, @original.extent.size.width, @original.extent.size.height) do |tiled_watermark|
0
+ watermark_image = tiled_watermark
0
+ end
0
+ end
0
+ end
0
+
0
+ @original.dissolve_transition :inputTargetImage => watermark_image, :inputTime => strength do |watermarked|
0
+ @target = watermarked
0
+ end
0
+ end
0
+
0
+ end
0
+ end
0
+ end
0
+end
...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
0
@@ -1 +1,123 @@
0
+require 'rubygems'
0
+require 'osx/cocoa'
0
+require 'active_support'
0
+
0
+require 'red_artisan/core_image/filters/scale'
0
+require 'red_artisan/core_image/filters/color'
0
+require 'red_artisan/core_image/filters/watermark'
0
+require 'red_artisan/core_image/filters/quality'
0
+require 'red_artisan/core_image/filters/perspective'
0
+require 'red_artisan/core_image/filters/effects'
0
+
0
+# Generic image processor for scaling images based on CoreImage via RubyCocoa.
0
+#
0
+# Example usage:
0
+#
0
+# p = Processor.new OSX::CIImage.from(path_to_image)
0
+# p.resize(640, 480)
0
+# p.render do |result|
0
+# result.save('resized.jpg', OSX::NSJPEGFileType)
0
+# end
0
+#
0
+# This will resize the image to the given dimensions exactly, if you'd like to ensure that aspect ratio is preserved:
0
+#
0
+# p = Processor.new OSX::CIImage.from(path_to_image)
0
+# p.fit(640)
0
+# p.render do |result|
0
+# result.save('resized.jpg', OSX::NSJPEGFileType)
0
+# end
0
+#
0
+# fit(size) will attempt its best to resize the image so that the longest width/height (depending on image orientation) will match
0
+# the given size. The second axis will be calculated automatically based on the aspect ratio.
0
+#
0
+# Scaling is performed by first clamping the image so that its external bounds become infinite, this helps when scaling so that any
0
+# rounding discrepencies in dimensions don't affect the resultant image. We then perform a Lanczos transform on the image which scales
0
+# it to the target size. We then crop the image to the traget dimensions.
0
+#
0
+# If you are generating smaller images such as thumbnails where high quality rendering isn't as important, an additional method is
0
+# available:
0
+#
0
+# p = Processor.new OSX::CIImage.from(path_to_image)
0
+# p.thumbnail(100, 100)
0
+# p.render do |result|
0
+# result.save('resized.jpg', OSX::NSJPEGFileType)
0
+# end
0
+#
0
+# This will perform a straight affine transform and scale the X and Y boundaries to the requested size. Generally, this will be faster
0
+# than a lanczos scale transform, but with a scaling quality trade.
0
+#
0
+# More than welcome to intregrate any patches, improvements - feel free to mail me with ideas.
0
+#
0
+# Thanks to
0
+# * Satoshi Nakagawa for working out that OCObjWrapper needs inclusion when aliasing method_missing on existing OSX::* classes.
0
+# * Vasantha Crabb for general help and inspiration with Cocoa
0
+# * Ben Schwarz for example image data and collaboration during performance testing
0
+#
0
+# Copyright (c) Marcus Crafter <crafterm@redartisan.com> released under the MIT license
0
+#
0
+module RedArtisan
0
+ module CoreImage
0
+ class Processor
0
+
0
+ def initialize(original)
0
+ if original.respond_to? :to_str
0
+ @original = OSX::CIImage.from(original.to_str)
0
+ else
0
+ @original = original
0
+ end
0
+ end
0
+
0
+ def render(&block)
0
+ raise "unprocessed image: #{@original}" unless @target
0
+ block.call @target
0
+ end
0
+
0
+ include Filters::Scale, Filters::Color, Filters::Watermark, Filters::Quality, Filters::Perspective, Filters::Effects
0
+
0
+ private
0
+
0
+ def create_core_image_context(width, height)
0
+ output = OSX::NSBitmapImageRep.alloc.initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel(nil, width, height, 8, 4, true, false, OSX::NSDeviceRGBColorSpace, 0, 0)
0
+ context = OSX::NSGraphicsContext.graphicsContextWithBitmapImageRep(output)
0
+ OSX::NSGraphicsContext.setCurrentContext(context)
0
+ @ci_context = context.CIContext
0
+ end
0
+
0
+ def vector(x, y, w, h)
0
+ OSX::CIVector.vectorWithX_Y_Z_W(x, y, w, h)
0
+ end
0
+ end
0
+ end
0
+end
0
+
0
+module OSX
0
+ class CIImage
0
+ include OCObjWrapper
0
+
0
+ def method_missing_with_filter_processing(sym, *args, &block)
0
+ f = OSX::CIFilter.filterWithName("CI#{sym.to_s.camelize}")
0
+ return method_missing_without_filter_processing(sym, *args, &block) unless f
0
+
0
+ f.setDefaults if f.respond_to? :setDefaults
0
+ f.setValue_forKey(self, 'inputImage')
0
+ options = args.last.is_a?(Hash) ? args.last : {}
0
+ options.each { |k, v| f.setValue_forKey(v, k.to_s) }
0
+
0
+ block.call f.valueForKey('outputImage')
0
+ end
0
+
0
+ alias_method_chain :method_missing, :filter_processing
0
+
0
+ def save(target, format = OSX::NSJPEGFileType, properties = nil)
0
+ bitmapRep = OSX::NSBitmapImageRep.alloc.initWithCIImage(self)
0
+ blob = bitmapRep.representationUsingType_properties(format, properties)
0
+ blob.writeToFile_atomically(target, false)
0
+ end
0
+
0
+ def self.from(filepath)
0
+ raise Errno::ENOENT, "No such file or directory - #{filepath}" unless File.exists?(filepath)
0
+ OSX::CIImage.imageWithContentsOfURL(OSX::NSURL.fileURLWithPath(filepath))
0
+ end
0
+ end
0
+end

Comments

    No one has commented yet.