##| ##| REALLY HANDY STUFF! ##| many of these methods are translated from: ##| ##| ##| class UIImage class << self # Easily create a UIImage by using this factory method, and do your drawing # in a block. The core graphics context will be passed to the block you # provide. To create a canvas based on an image, use the instance method. # # @example # white_square = UIImage.canvas([100, 100]) do |context| # :white.uicolor.set # CGContextAddRect(context, [[0, 0], [100, 100]]) # CGContextDrawPath(context, KCGPathFill) # end # :size is required, :scale defaults to the screen scale, and :opaque is # false by default. # # The first argument can be a size, or an options dict def canvas(options_or_size={}, more_options={}, &block) if options_or_size.is_a?(NSDictionary) options = options_or_size size = options[:size] else options = more_options size = options_or_size end raise ":size is required in #{self.name}##canvas" unless size scale = options[:scale] || UIScreen.mainScreen.scale opaque = options.fetch(:opaque, false) UIGraphicsBeginImageContextWithOptions(size, opaque, scale) block.call(UIGraphicsGetCurrentContext()) if block new_image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return new_image end end # Merges the two images. The target is drawn first, `image` is drawn on top. # The two images are centered, and the maximum size is used so that both # images fit on the canvas. def <<(image) self.merge(image, at: :center, stretch: true) end # Draw an image on top of the receiver. The `:at` option provides either an # absolute location (Array or CGPoint) or relative location (Symbol, one of # :top_left, :top, :top_right, :left, :center (default), :right, :bottom_left, # :bottom, :bottom_right). The `:stretch` option increases the canvas so # there is room for both images, otherwise the target image's size is used. def merge(image, options={}) image_position = options.fetch(:at, :center) stretch = options.fetch(:stretch, false) size = self.size if stretch if image.size.width > size.width size.width = image.size.width end if image.size.height > size.height size.height = image.size.height end end my_left = image_left = 0 my_top = image_top = 0 my_right = (size.width - self.size.width) my_bottom = (size.height - self.size.height) image_right = (size.width - image.size.width) image_bottom = (size.height - image.size.height) my_cx = my_right / 2.0 my_cy = my_bottom / 2.0 image_cx = image_right / 2.0 image_cy = image_bottom / 2.0 case image_position when :top_left, :topleft, :tl my_position = [my_right, my_bottom] image_position = [image_left, image_top] when :top, :t my_position = [my_cx, my_bottom] image_position = [image_cx, image_top] when :top_right, :topright, :tr my_position = [my_left, my_bottom] image_position = [image_right, image_top] when :left, :l my_position = [my_right, my_cy] image_position = [image_left, image_cy] when :center, :c my_position = [my_cx, my_cy] image_position = [image_cx, image_cy] when :right, :r my_position = [my_left, my_cy] image_position = [image_right, image_cy] when :bottom_left, :bottomleft, :bl my_position = [my_right, my_top] image_position = [image_left, image_bottom] when :bottom, :b my_position = [my_cx, my_top] image_position = [image_cx, image_bottom] when :bottom_right, :bottomright, :br my_position = [my_left, my_top] image_position = [image_right, image_bottom] end return self.draw(size: size, at: my_position) do image.drawAtPoint(image_position) end end # Returns a cropped UIImage. The easiest way is to check for a CGImage # backing, but if this image uses a CIImage backing, we draw a new (cropped) # image. def crop(rect) rect = SugarCube::CoreGraphics::Rect(rect) if self.CGImage if self.scale > 1.0 rect = CGRectMake(rect.origin.x * self.scale, rect.origin.y * self.scale, rect.size.width * self.scale, rect.size.height * self.scale) end cgimage = CGImageCreateWithImageInRect(self.CGImage, rect) return UIImage.imageWithCGImage(cgimage, scale:self.scale, orientation:self.imageOrientation) else return self.canvas(size: rect.size) do |context| self.drawAtPoint(CGPoint.new(-rect.origin.x, -rect.origin.y)) end end end ##| ##| image scaling ##| # This method is used to crop an image. Scale (retina or non-retina) is preserved. # # @param rect [CGRect] the portion of the image to return # @return [UIImage] def in_rect(rect) imageRef = CGImageCreateWithImageInRect(self.CGImage, rect) sub_image = UIImage.imageWithCGImage(imageRef, scale:self.scale, orientation:self.imageOrientation) return sub_image end # Scales an image to fit within the given size, stretching one or both # dimensions so that it completely fills the area. The current aspect ratio # is maintained. If you want to place an image inside a container image, this # is the method to use. # # You can specify a `position` property, which can be a symbol or a point. It # specifies where you want the image located if it has to be cropped. # Specifying the top-left corner will display the top-left corner of the # image, likewise specifing the bottom-right corner will display *that* # corner. If you want the image centered, you can use the 'position-less' # version of this method (`scale_to_fit()`) or specify the point at the center # of the image (`scale_to_fit(size, position:[w/2, h/2])`), or use a symbol # (`scale_to_fit(size, position: :center)`). # # @param new_size [CGSize] Minimum dimensions of desired image. The returned image is # guaranteed to fit within these dimensions. # @param position [Symbol, CGPoint] Where to position the resized image. Valid symbols # are: `[:top_left, :top, :top_right, :left, :center, :right, :bottom_left, # :bottom, :bottom_right]` (if you forget the underscore, like # `topleft`, that'll work, too) # @param scale [Numeric] image scale # @return [UIImage] def scale_to_fill(new_size, options={}) new_size = SugarCube::CoreGraphics::Size(new_size) position = options[:position] || :center scale = options[:scale] || self.scale my_size = self.size if new_size.width == my_size.width && new_size.height == my_size.height && self.scale == scale return self end # first, scale down; then we'll scale back up if we went too far if my_size.width > new_size.width my_size.height *= new_size.width / my_size.width my_size.width = new_size.width end if my_size.height > new_size.height my_size.width *= new_size.height / my_size.height my_size.height = new_size.height end if my_size.width < new_size.width my_size.height *= new_size.width / my_size.width my_size.width = new_size.width end if my_size.height < new_size.height my_size.width *= new_size.height / my_size.height my_size.height = new_size.height end if self.size.width == my_size.width && self.size.height == my_size.height return self end if position.is_a?(Symbol) min_x = 0 min_y = 0 max_x = my_size.width max_y = my_size.height mid_x = max_x / 2 mid_y = max_y / 2 case position when :top_left, :topleft, :tl position = CGPoint.new(min_x, min_y) when :top, :t position = CGPoint.new(mid_x, min_y) when :top_right, :topright, :tr position = CGPoint.new(max_x, min_y) when :left, :l position = CGPoint.new(min_x, mid_x) when :center, :c position = CGPoint.new(mid_x, mid_x) when :right, :r position = CGPoint.new(max_x, mid_x) when :bottom_left, :bottomleft, :bl position = CGPoint.new(min_x, max_y) when :bottom, :b position = CGPoint.new(mid_x, max_y) when :bottom_right, :bottomright, :br position = CGPoint.new(max_x, max_y) else raise "Unknown position #{position.inspect}" end else position = SugarCube::CoreGraphics::Point(position) end thumbnail_x = position.x * (new_size.width - my_size.width) / my_size.width thumbnail_y = position.y * (new_size.height - my_size.height) / my_size.height new_image = self.canvas(size: new_size) do thumbnail_rect = CGRect.new([0, 0], [0, 0]) thumbnail_rect.origin = [thumbnail_x, thumbnail_y] thumbnail_rect.size = my_size self.drawInRect(thumbnail_rect) end raise "could not scale image" unless new_image return new_image end # This method is similar to `scale_to`, except it doesn't pad the image, it # just scales the image so that it will fit inside the new bounds. def scale_within(new_size) target_size = SugarCube::CoreGraphics::Size(new_size) image_size = self.size if CGSizeEqualToSize(target_size, self.size) return self end width = image_size.width height = image_size.height target_width = target_size.width target_height = target_size.height width_factor = target_width / width height_factor = target_height / height if width_factor < height_factor scale_factor = width_factor else scale_factor = height_factor end if scale_factor == 1 return self end scaled_size = CGSize.new(width * scale_factor, height * scale_factor) return scale_to(scaled_size) end # Delegates to scale_to(background:), specifying background color of `nil` def scale_to(new_size) scale_to(new_size, background:nil) end # Scales an image to fit within the given size. Its current aspect ratio is # maintained, but the image is padded so that it fills the entire area. If the # image is too small, it will be scaled up to fit. If you specify a # background that color will be used, otherwise the background will be # transparent. # # @param new_size [CGSize] Maximum dimensions of desired image. The returned image is # guaranteed to fit within these dimensions. # @param background [UIColor] Color to fill padded areas. Default is transparent. # @return [UIImage] def scale_to(new_size, background:background) new_size = SugarCube::CoreGraphics::Size(new_size) image_size = self.size if CGSizeEqualToSize(image_size, new_size) return self end new_image = nil width = image_size.width height = image_size.height target_width = new_size.width target_height = new_size.height scale_factor = 0.0 scaled_width = target_width scaled_height = target_height thumbnail_point = CGPoint.new(0.0, 0.0) width_factor = target_width / width height_factor = target_height / height if width_factor < height_factor scale_factor = width_factor else scale_factor = height_factor end scaled_width = width * scale_factor scaled_height = height * scale_factor # center the image if width_factor < height_factor thumbnail_point.y = (target_height - scaled_height) * 0.5 elsif width_factor > height_factor thumbnail_point.x = (target_width - scaled_width) * 0.5 end # this is actually the interesting part: new_image = self.canvas(size: new_size) do |context| if background background = background.uicolor background.setFill CGContextAddRect(context, [[0, 0], new_size]) CGContextDrawPath(context, KCGPathFill) end thumbnail_rect = CGRect.new([0, 0], [0, 0]) thumbnail_rect.origin = thumbnail_point thumbnail_rect.size.width = scaled_width thumbnail_rect.size.height = scaled_height self.drawInRect(thumbnail_rect) end raise "could not scale image" unless new_image return new_image end ##| ##| image modifications ##| def rounded(corner_radius=5) return self.canvas do path = UIBezierPath.bezierPathWithRoundedRect([[0, 0], size], cornerRadius:corner_radius) path.addClip self.drawInRect([[0, 0], size]) end end # Returns a CIImage with the filter applied to the receiver. The return value # is a CIImage object, which also defines `|` to work the same way, so filters # can be chained. # # @example # image = 'test'.uiimage # new_image = image.apply_filter(CIFilter.gaussian_blur) def apply_filter(filter) filter.setValue(self.ciimage, forKey: 'inputImage') return filter.valueForKey('outputImage') end # Returns a CGImageRef. Alias for `CGImage`. def cgimage return self.CGImage end # Returns a CIImage. def ciimage return CIImage.imageWithCGImage(self.CGImage) end # Accepts two options: brightness (default: 0.0) and saturation (default: 0.0) # Returns a darkened version of the image. def darken(options={}) filter_options = { 'inputSaturation' => options[:saturation] || 0, 'inputBrightness' => options[:brightness] || 0, } darken_filter = CIFilter.color_controls(filter_options) output = self.apply_filter(darken_filter) return UIImage.imageWithCIImage(output, scale:self.scale, orientation:self.imageOrientation) end # Apply a gaussian filter # @options radius, default: 10 # # @example # image.gaussian_blur(radius: 5) # image.gaussian_blur(5) # :radius is the default option def gaussian_blur(*args) output = self.apply_filter(CIFilter.gaussian_blur(*args)) return UIImage.imageWithCIImage(output, scale:self.scale, orientation:self.imageOrientation) end # Apply a color overlay to the image (very practical with PNG button images) # # @example # image.overlay(UIColor.redColor) def overlay(color) image_rect = CGRectMake(0, 0, self.size.width, self.size.height) UIImage.canvas(size: self.size, scale: self.scale) do |ctx| self.drawInRect(image_rect) CGContextSetFillColorWithColor(ctx, color.uicolor.CGColor) CGContextSetAlpha(ctx, 1) CGContextSetBlendMode(ctx, KCGBlendModeSourceAtop) CGContextFillRect(ctx, image_rect) image_ref = CGBitmapContextCreateImage(ctx) new_image = UIImage.imageWithCGImage(image_ref, scale:self.scale, orientation:self.imageOrientation) end return new_image end ##| ##| rotate images ##| def rotate(angle_or_direction) case angle_or_direction when :left radian = -90.degrees when :right radian = 90.degrees when :flip radian = 180.degrees when Numeric radian = angle_or_direction else raise "Unknown angle/direction #{angle_or_direction.inspect}" end w = (self.size.width * Math.cos(radian)).abs + (self.size.height * Math.sin(radian)).abs h = (self.size.height * Math.cos(radian)).abs + (self.size.width * Math.sin(radian)).abs new_size = CGSize.new(w, h) new_size = self.size return self.canvas(size: new_size) do |context| # Move the origin to the middle of the image so we will rotate and scale around the center. CGContextTranslateCTM(context, new_size.width / 2, new_size.height / 2) # Rotate the image context CGContextRotateCTM(context, radian) # otherwise it'll be upside down: CGContextScaleCTM(context, 1.0, -1.0) # Now, draw the rotated/scaled image into the context CGContextDrawImage(context, CGRectMake(-new_size.width / 2, -new_size.height / 2, new_size.width, new_size.height), self.CGImage) end end ##| ##| resizableImageWithCapInsets ##| def tileable(insets=UIEdgeInsetsZero) # not necessary, since we don't modify/examine the insets # insets = SugarCube::CoreGraphics::EdgeInsets(insets) resizableImageWithCapInsets(insets, resizingMode:UIImageResizingModeTile) end def stretchable(insets=UIEdgeInsetsZero) # not necessary, since we don't modify/examine the insets # insets = SugarCube::CoreGraphics::EdgeInsets(insets) resizableImageWithCapInsets(insets, resizingMode:UIImageResizingModeStretch) end ##| ##| imageWithAlignmentRectInsets ##| def alignment_rect(insets=UIEdgeInsetsZero) imageWithAlignmentRectInsets(insets) end ##| ##| CGImageCreateWithMask ##| # The mask image cannot have ANY transparency. Instead, transparent areas must # be white or some value between black and white. The more white a pixel is # the more transparent it becomes. # black .. white # opaque .. transparent def masked(mask_image) mask_image = mask_image.CGImage width = CGImageGetWidth(mask_image) height = CGImageGetHeight(mask_image) component_bits = CGImageGetBitsPerComponent(mask_image) pixel_bits = CGImageGetBitsPerPixel(mask_image) row_bytes = CGImageGetBytesPerRow(mask_image) data_provider = CGImageGetDataProvider(mask_image) mask = CGImageMaskCreate(width, height, component_bits, pixel_bits, row_bytes, data_provider,nil, false) masked = CGImageCreateWithMask(self.CGImage, mask) UIImage.imageWithCGImage(masked, scale:self.scale, orientation:self.imageOrientation) end # Oddly enough, this method doesn't seem to have retina support def color_at(point) point = SugarCube::CoreGraphics::Point(point) point.x *= self.scale point.y *= self.scale # First get the image into your data buffer cgimage = self.CGImage width = CGImageGetWidth(cgimage) height = CGImageGetHeight(cgimage) bytes_per_pixel = 4 bits_per_component = 8 bytes_per_row = bytes_per_pixel * width @raw_data || begin color_space = CGColorSpaceCreateDeviceRGB() @raw_data = Pointer.new(:uchar, height * width * 4) context = CGBitmapContextCreate(@raw_data, width, height, bits_per_component, bytes_per_row, color_space, KCGImageAlphaPremultipliedLast | KCGBitmapByteOrder32Big) CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgimage) end # Now @raw_data contains the image data in the RGBA8888 pixel format. xx = point.x.round yy = point.y.round byte_index = (bytes_per_row * yy) + xx * bytes_per_pixel red = @raw_data[byte_index] green = @raw_data[byte_index + 1] blue = @raw_data[byte_index + 2] alpha = @raw_data[byte_index + 3] return [red, green, blue].uicolor(alpha / 255.0) end def avg_color colorSpace = CGColorSpaceCreateDeviceRGB() rgba = Pointer.new(:uchar, 4) context = CGBitmapContextCreate(rgba, 1, 1, 8, 4, colorSpace, KCGImageAlphaPremultipliedLast | KCGBitmapByteOrder32Big) CGContextDrawImage(context, CGRectMake(0, 0, 1, 1), self.CGImage) if rgba[3] > 0 alpha = rgba[3] / 255.0 multiplier = alpha / 255.0 return UIColor.colorWithRed(rgba[0] * multiplier, green:rgba[1] * multiplier, blue:rgba[2] * multiplier, alpha:alpha) else return UIColor.colorWithRed(rgba[0] / 255.0, green:rgba[1] / 255.0, blue:rgba[2] / 255.0, alpha:rgba[3] / 255.0) end end def at_scale(scale) if scale == self.scale return self end new_size = self.size new_size.width = new_size.width * self.scale / scale new_size.height = new_size.height * self.scale / scale return self.canvas(size: new_size, scale: scale) do thumbnail_rect = CGRect.new([0, 0], new_size) self.drawInRect(thumbnail_rect) end end # Using the image as the background, you can use this method to draw anything # on top, like text or other images. def draw(options={}, &block) at = options[:at] || [0, 0] return self.canvas(options) do |context| self.drawAtPoint(at) block.call(context) if block end end # the first argument can be a size, or an options dict def canvas(options_or_size={}, more_options={}, &block) if options_or_size.is_a?(NSDictionary) options = options_or_size else options = more_options options[:size] = options_or_size end unless options[:size] options[:size] = self.size end unless options[:scale] options = options.merge(scale: self.scale) end self.class.canvas(options) do |context| block.call(context) if block end end end