diff --git a/init.rb b/init.rb index 0239e56b..dadcb078 100644 --- a/init.rb +++ b/init.rb @@ -11,4 +11,6 @@ def make_tmpname(basename, n) require 'geometry' ActiveRecord::Base.send(:extend, Technoweenie::AttachmentFu::ActMethods) Technoweenie::AttachmentFu.tempfile_path = ATTACHMENT_FU_TEMPFILE_PATH if Object.const_defined?(:ATTACHMENT_FU_TEMPFILE_PATH) -FileUtils.mkdir_p Technoweenie::AttachmentFu.tempfile_path \ No newline at end of file +FileUtils.mkdir_p Technoweenie::AttachmentFu.tempfile_path + +$:.unshift(File.dirname(__FILE__) + '/vendor') diff --git a/lib/technoweenie/attachment_fu.rb b/lib/technoweenie/attachment_fu.rb index 013e225f..864aad70 100644 --- a/lib/technoweenie/attachment_fu.rb +++ b/lib/technoweenie/attachment_fu.rb @@ -1,6 +1,6 @@ module Technoweenie # :nodoc: module AttachmentFu # :nodoc: - @@default_processors = %w(ImageScience Rmagick MiniMagick) + @@default_processors = %w(CoreImage ImageScience Rmagick MiniMagick) @@tempfile_path = File.join(RAILS_ROOT, 'tmp', 'attachment_fu') @@content_types = ['image/jpeg', 'image/pjpeg', 'image/gif', 'image/png', 'image/x-png', 'image/jpg'] mattr_reader :content_types, :tempfile_path, :default_processors diff --git a/lib/technoweenie/attachment_fu/processors/core_image_processor.rb b/lib/technoweenie/attachment_fu/processors/core_image_processor.rb new file mode 100644 index 00000000..8120b69b --- /dev/null +++ b/lib/technoweenie/attachment_fu/processors/core_image_processor.rb @@ -0,0 +1,56 @@ +require 'red_artisan/core_image/processor' + +module Technoweenie # :nodoc: + module AttachmentFu # :nodoc: + module Processors + module CoreImageProcessor + def self.included(base) + base.send :extend, ClassMethods + base.alias_method_chain :process_attachment, :processing + end + + module ClassMethods + def with_image(file, &block) + block.call OSX::CIImage.from(file) + end + end + + protected + def process_attachment_with_processing + return unless process_attachment_without_processing + with_image do |img| + self.width = img.extent.size.width if respond_to?(:width) + self.height = img.extent.size.height if respond_to?(:height) + resize_image_or_thumbnail! img + callback_with_args :after_resize, img + end if image? + end + + # Performs the actual resizing operation for a thumbnail + def resize_image(img, size) + processor = ::RedArtisan::CoreImage::Processor.new(img) + size = size.first if size.is_a?(Array) && size.length == 1 + if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum)) + if size.is_a?(Fixnum) + processor.fit(size) + else + processor.resize(size[0], size[1]) + end + else + new_size = [img.extent.size.width, img.extent.size.height] / size.to_s + processor.resize(new_size[0], new_size[1]) + end + + processor.render do |result| + self.width = result.extent.size.width if respond_to?(:width) + self.height = result.extent.size.height if respond_to?(:height) + result.save self.temp_path, OSX::NSJPEGFileType + self.size = File.size(self.temp_path) + end + end + end + end + end +end + + diff --git a/test/fixtures/attachment.rb b/test/fixtures/attachment.rb index 77d60c3f..59258dd3 100644 --- a/test/fixtures/attachment.rb +++ b/test/fixtures/attachment.rb @@ -94,6 +94,16 @@ class ImageScienceAttachment < ActiveRecord::Base puts "no ImageScience" end +begin + class CoreImageAttachment < ActiveRecord::Base + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files', + :processor => :core_image, :thumbnails => { :thumb => [50, 51], :geometry => '31>' }, :resize_to => 55 + end +rescue MissingSourceFile + puts $!.message + puts "no CoreImage" +end + begin class MiniMagickAttachment < ActiveRecord::Base has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files', diff --git a/test/processors/core_image_test.rb b/test/processors/core_image_test.rb new file mode 100644 index 00000000..09a9bbd5 --- /dev/null +++ b/test/processors/core_image_test.rb @@ -0,0 +1,31 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper')) + +class CoreImageTest < Test::Unit::TestCase + attachment_model CoreImageAttachment + + if Object.const_defined?(:OSX) + def test_should_resize_image + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + assert attachment.image? + # test core image thumbnail + assert_equal 42, attachment.width + assert_equal 55, attachment.height + + thumb = attachment.thumbnails.detect { |t| t.filename =~ /_thumb/ } + geo = attachment.thumbnails.detect { |t| t.filename =~ /_geometry/ } + + # test exact resize dimensions + assert_equal 50, thumb.width + assert_equal 51, thumb.height + + # test geometry string + assert_equal 31, geo.width + assert_equal 41, geo.height + end + else + def test_flunk + puts "CoreImage not loaded, tests not running" + end + end +end \ No newline at end of file diff --git a/test/schema.rb b/test/schema.rb index b2e284d5..5d7f7ada 100644 --- a/test/schema.rb +++ b/test/schema.rb @@ -34,6 +34,17 @@ t.column :type, :string end + create_table :core_image_attachments, :force => true do |t| + t.column :parent_id, :integer + t.column :thumbnail, :string + t.column :filename, :string, :limit => 255 + t.column :content_type, :string, :limit => 255 + t.column :size, :integer + t.column :width, :integer + t.column :height, :integer + t.column :type, :string + end + create_table :mini_magick_attachments, :force => true do |t| t.column :parent_id, :integer t.column :thumbnail, :string diff --git a/vendor/red_artisan/core_image/filters/color.rb b/vendor/red_artisan/core_image/filters/color.rb new file mode 100644 index 00000000..f593b909 --- /dev/null +++ b/vendor/red_artisan/core_image/filters/color.rb @@ -0,0 +1,27 @@ +module RedArtisan + module CoreImage + module Filters + module Color + + def greyscale(color = nil, intensity = 1.00) + create_core_image_context(@original.extent.size.width, @original.extent.size.height) + + color = OSX::CIColor.colorWithString("1.0 1.0 1.0 1.0") unless color + + @original.color_monochrome :inputColor => color, :inputIntensity => intensity do |greyscale| + @target = greyscale + end + end + + def sepia(intensity = 1.00) + create_core_image_context(@original.extent.size.width, @original.extent.size.height) + + @original.sepia_tone :inputIntensity => intensity do |sepia| + @target = sepia + end + end + + end + end + end +end \ No newline at end of file diff --git a/vendor/red_artisan/core_image/filters/effects.rb b/vendor/red_artisan/core_image/filters/effects.rb new file mode 100644 index 00000000..2e0f2443 --- /dev/null +++ b/vendor/red_artisan/core_image/filters/effects.rb @@ -0,0 +1,31 @@ +module RedArtisan + module CoreImage + module Filters + module Effects + + def spotlight(position, points_at, brightness, concentration, color) + create_core_image_context(@original.extent.size.width, @original.extent.size.height) + + @original.spot_light :inputLightPosition => vector3(*position), :inputLightPointsAt => vector3(*points_at), + :inputBrightness => brightness, :inputConcentration => concentration, :inputColor => color do |spot| + @target = spot + end + end + + def edges(intensity = 1.00) + create_core_image_context(@original.extent.size.width, @original.extent.size.height) + + @original.edges :inputIntensity => intensity do |edged| + @target = edged + end + end + + private + + def vector3(x, y, w) + OSX::CIVector.vectorWithX_Y_Z(x, y, w) + end + end + end + end +end diff --git a/vendor/red_artisan/core_image/filters/perspective.rb b/vendor/red_artisan/core_image/filters/perspective.rb new file mode 100644 index 00000000..6160dd82 --- /dev/null +++ b/vendor/red_artisan/core_image/filters/perspective.rb @@ -0,0 +1,25 @@ +module RedArtisan + module CoreImage + module Filters + module Perspective + + def perspective(top_left, top_right, bottom_left, bottom_right) + create_core_image_context(@original.extent.size.width, @original.extent.size.height) + + @original.perspective_transform :inputTopLeft => top_left, :inputTopRight => top_right, :inputBottomLeft => bottom_left, :inputBottomRight => bottom_right do |transformed| + @target = transformed + end + end + + def perspective_tiled(top_left, top_right, bottom_left, bottom_right) + create_core_image_context(@original.extent.size.width, @original.extent.size.height) + + @original.perspective_tile :inputTopLeft => top_left, :inputTopRight => top_right, :inputBottomLeft => bottom_left, :inputBottomRight => bottom_right do |tiled| + @target = tiled + end + end + + end + end + end +end \ No newline at end of file diff --git a/vendor/red_artisan/core_image/filters/quality.rb b/vendor/red_artisan/core_image/filters/quality.rb new file mode 100644 index 00000000..018690f0 --- /dev/null +++ b/vendor/red_artisan/core_image/filters/quality.rb @@ -0,0 +1,25 @@ +module RedArtisan + module CoreImage + module Filters + module Quality + + def reduce_noise(level = 0.02, sharpness = 0.4) + create_core_image_context(@original.extent.size.width, @original.extent.size.height) + + @original.noise_reduction :inputNoiseLevel => level, :inputSharpness => sharpness do |noise_reduced| + @target = noise_reduced + end + end + + def adjust_exposure(input_ev = 0.5) + create_core_image_context(@original.extent.size.width, @original.extent.size.height) + + @original.exposure_adjust :inputEV => input_ev do |adjusted| + @target = adjusted + end + end + + end + end + end +end \ No newline at end of file diff --git a/vendor/red_artisan/core_image/filters/scale.rb b/vendor/red_artisan/core_image/filters/scale.rb new file mode 100644 index 00000000..f729b59a --- /dev/null +++ b/vendor/red_artisan/core_image/filters/scale.rb @@ -0,0 +1,47 @@ +module RedArtisan + module CoreImage + module Filters + module Scale + + def resize(width, height) + create_core_image_context(width, height) + + scale_x, scale_y = scale(width, height) + + @original.affine_clamp :inputTransform => OSX::NSAffineTransform.transform do |clamped| + clamped.lanczos_scale_transform :inputScale => scale_x > scale_y ? scale_x : scale_y, :inputAspectRatio => scale_x / scale_y do |scaled| + scaled.crop :inputRectangle => vector(0, 0, width, height) do |cropped| + @target = cropped + end + end + end + end + + def thumbnail(width, height) + create_core_image_context(width, height) + + transform = OSX::NSAffineTransform.transform + transform.scaleXBy_yBy *scale(width, height) + + @original.affine_transform :inputTransform => transform do |scaled| + @target = scaled + end + end + + def fit(size) + original_size = @original.extent.size + scale = size.to_f / (original_size.width > original_size.height ? original_size.width : original_size.height) + resize (original_size.width * scale).to_i, (original_size.height * scale).to_i + end + + private + + def scale(width, height) + original_size = @original.extent.size + return width.to_f / original_size.width.to_f, height.to_f / original_size.height.to_f + end + + end + end + end +end \ No newline at end of file diff --git a/vendor/red_artisan/core_image/filters/watermark.rb b/vendor/red_artisan/core_image/filters/watermark.rb new file mode 100644 index 00000000..3c3a1ad1 --- /dev/null +++ b/vendor/red_artisan/core_image/filters/watermark.rb @@ -0,0 +1,32 @@ +module RedArtisan + module CoreImage + module Filters + module Watermark + + def watermark(watermark_image, tile = false, strength = 0.1) + create_core_image_context(@original.extent.size.width, @original.extent.size.height) + + if watermark_image.respond_to? :to_str + watermark_image = OSX::CIImage.from(watermark_image.to_str) + end + + if tile + tile_transform = OSX::NSAffineTransform.transform + tile_transform.scaleXBy_yBy 1.0, 1.0 + + watermark_image.affine_tile :inputTransform => tile_transform do |tiled| + tiled.crop :inputRectangle => vector(0, 0, @original.extent.size.width, @original.extent.size.height) do |tiled_watermark| + watermark_image = tiled_watermark + end + end + end + + @original.dissolve_transition :inputTargetImage => watermark_image, :inputTime => strength do |watermarked| + @target = watermarked + end + end + + end + end + end +end \ No newline at end of file diff --git a/vendor/red_artisan/core_image/processor.rb b/vendor/red_artisan/core_image/processor.rb new file mode 100644 index 00000000..965e70cc --- /dev/null +++ b/vendor/red_artisan/core_image/processor.rb @@ -0,0 +1,123 @@ +require 'rubygems' +require 'osx/cocoa' +require 'active_support' + +require 'red_artisan/core_image/filters/scale' +require 'red_artisan/core_image/filters/color' +require 'red_artisan/core_image/filters/watermark' +require 'red_artisan/core_image/filters/quality' +require 'red_artisan/core_image/filters/perspective' +require 'red_artisan/core_image/filters/effects' + +# Generic image processor for scaling images based on CoreImage via RubyCocoa. +# +# Example usage: +# +# p = Processor.new OSX::CIImage.from(path_to_image) +# p.resize(640, 480) +# p.render do |result| +# result.save('resized.jpg', OSX::NSJPEGFileType) +# end +# +# This will resize the image to the given dimensions exactly, if you'd like to ensure that aspect ratio is preserved: +# +# p = Processor.new OSX::CIImage.from(path_to_image) +# p.fit(640) +# p.render do |result| +# result.save('resized.jpg', OSX::NSJPEGFileType) +# end +# +# fit(size) will attempt its best to resize the image so that the longest width/height (depending on image orientation) will match +# the given size. The second axis will be calculated automatically based on the aspect ratio. +# +# Scaling is performed by first clamping the image so that its external bounds become infinite, this helps when scaling so that any +# rounding discrepencies in dimensions don't affect the resultant image. We then perform a Lanczos transform on the image which scales +# it to the target size. We then crop the image to the traget dimensions. +# +# If you are generating smaller images such as thumbnails where high quality rendering isn't as important, an additional method is +# available: +# +# p = Processor.new OSX::CIImage.from(path_to_image) +# p.thumbnail(100, 100) +# p.render do |result| +# result.save('resized.jpg', OSX::NSJPEGFileType) +# end +# +# This will perform a straight affine transform and scale the X and Y boundaries to the requested size. Generally, this will be faster +# than a lanczos scale transform, but with a scaling quality trade. +# +# More than welcome to intregrate any patches, improvements - feel free to mail me with ideas. +# +# Thanks to +# * Satoshi Nakagawa for working out that OCObjWrapper needs inclusion when aliasing method_missing on existing OSX::* classes. +# * Vasantha Crabb for general help and inspiration with Cocoa +# * Ben Schwarz for example image data and collaboration during performance testing +# +# Copyright (c) Marcus Crafter released under the MIT license +# +module RedArtisan + module CoreImage + class Processor + + def initialize(original) + if original.respond_to? :to_str + @original = OSX::CIImage.from(original.to_str) + else + @original = original + end + end + + def render(&block) + raise "unprocessed image: #{@original}" unless @target + block.call @target + end + + include Filters::Scale, Filters::Color, Filters::Watermark, Filters::Quality, Filters::Perspective, Filters::Effects + + private + + def create_core_image_context(width, height) + 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) + context = OSX::NSGraphicsContext.graphicsContextWithBitmapImageRep(output) + OSX::NSGraphicsContext.setCurrentContext(context) + @ci_context = context.CIContext + end + + def vector(x, y, w, h) + OSX::CIVector.vectorWithX_Y_Z_W(x, y, w, h) + end + end + end +end + +module OSX + class CIImage + include OCObjWrapper + + def method_missing_with_filter_processing(sym, *args, &block) + f = OSX::CIFilter.filterWithName("CI#{sym.to_s.camelize}") + return method_missing_without_filter_processing(sym, *args, &block) unless f + + f.setDefaults if f.respond_to? :setDefaults + f.setValue_forKey(self, 'inputImage') + options = args.last.is_a?(Hash) ? args.last : {} + options.each { |k, v| f.setValue_forKey(v, k.to_s) } + + block.call f.valueForKey('outputImage') + end + + alias_method_chain :method_missing, :filter_processing + + def save(target, format = OSX::NSJPEGFileType, properties = nil) + bitmapRep = OSX::NSBitmapImageRep.alloc.initWithCIImage(self) + blob = bitmapRep.representationUsingType_properties(format, properties) + blob.writeToFile_atomically(target, false) + end + + def self.from(filepath) + raise Errno::ENOENT, "No such file or directory - #{filepath}" unless File.exists?(filepath) + OSX::CIImage.imageWithContentsOfURL(OSX::NSURL.fileURLWithPath(filepath)) + end + end +end +