diff --git a/Source/ImageFilter.swift b/Source/ImageFilter.swift index c1788df1..ebbf618e 100644 --- a/Source/ImageFilter.swift +++ b/Source/ImageFilter.swift @@ -78,17 +78,6 @@ extension ImageFilter where Self: Roundable { } } -extension ImageFilter where Self: Sizable, Self: Roundable { - /// The unique idenitifier for an `ImageFilter` conforming to both the `Sizable` and `Roundable` protocols. - public var identifier: String { - let width = Int64(round(size.width)) - let height = Int64(round(size.height)) - let radius = Int64(round(self.radius)) - - return "\(self.dynamicType)-size:(\(width)x\(height))-radius:(\(radius))" - } -} - #if os(iOS) || os(watchOS) // MARK: - Single Pass Image Filters (iOS and watchOS only) - @@ -177,23 +166,41 @@ public struct RoundedCornersFilter: ImageFilter, Roundable { /// The radius of the filter. public let radius: CGFloat + /// Whether to divide the radius by the image scale. + public let divideRadiusByImageScale: Bool + /** Initializes the `RoundedCornersFilter` instance with the given radius. - - parameter radius: The radius. + - parameter radius: The radius. + - parameter divideRadiusByImageScale: Whether to divide the radius by the image scale. Set to `true` when the + image has the same resolution for all screen scales such as @1x, @2x and + @3x (i.e. single image from web server). Set to `false` for images loaded + from an asset catalog with varying resolutions for each screen scale. + `false` by default. - returns: The new `RoundedCornersFilter` instance. */ - public init(radius: CGFloat) { + public init(radius: CGFloat, divideRadiusByImageScale: Bool = false) { self.radius = radius + self.divideRadiusByImageScale = divideRadiusByImageScale } /// The filter closure used to create the modified representation of the given image. public var filter: Image -> Image { return { image in - return image.af_imageWithRoundedCornerRadius(self.radius) + return image.af_imageWithRoundedCornerRadius( + self.radius, + divideRadiusByImageScale: self.divideRadiusByImageScale + ) } } + + /// The unique idenitifier for an `ImageFilter` conforming to the `Roundable` protocol. + public var identifier: String { + let radius = Int64(round(self.radius)) + return "\(self.dynamicType)-radius:(\(radius))-divided:(\(divideRadiusByImageScale))" + } } // MARK: - @@ -246,85 +253,89 @@ public struct BlurFilter: ImageFilter { #endif -// MARK: - Multi-Pass Image Filters (iOS and watchOS only) - +// MARK: - Composite Image Filters (iOS and watchOS only) - -/// Scales an image to a specified size, then rounds the corners to the specified radius. -public struct ScaledToSizeWithRoundedCornersFilter: ImageFilter, Sizable, Roundable { - /// The size of the filter. - public let size: CGSize +/// The `CompositeImageFilter` protocol defines an additional `filters` property to support multiple composite filters. +public protocol CompositeImageFilter: ImageFilter { + /// The image filters to apply to the image in sequential order. + var filters: [ImageFilter] { get } +} - /// The radius of the filter. - public let radius: CGFloat +public extension CompositeImageFilter { + /// The unique idenitifier for any `CompositeImageFilter` type. + var identifier: String { + return filters.map { $0.identifier }.joinWithSeparator("_") + } + + /// The filter closure for any `CompositeImageFilter` type. + var filter: Image -> Image { + return { image in + return self.filters.reduce(image) { $1.filter($0) } + } + } +} +// MARK: - + +/// Scales an image to a specified size, then rounds the corners to the specified radius. +public struct ScaledToSizeWithRoundedCornersFilter: CompositeImageFilter { /** Initializes the `ScaledToSizeWithRoundedCornersFilter` instance with the given size and radius. - - parameter size: The size. - - parameter radius: The radius. + - parameter size: The size. + - parameter radius: The radius. + - parameter divideRadiusByImageScale: Whether to divide the radius by the image scale. Set to `true` when the + image has the same resolution for all screen scales such as @1x, @2x and + @3x (i.e. single image from web server). Set to `false` for images loaded + from an asset catalog with varying resolutions for each screen scale. + `false` by default. - returns: The new `ScaledToSizeWithRoundedCornersFilter` instance. */ - public init(size: CGSize, radius: CGFloat) { - self.size = size - self.radius = radius + public init(size: CGSize, radius: CGFloat, divideRadiusByImageScale: Bool = false) { + self.filters = [ + ScaledToSizeFilter(size: size), + RoundedCornersFilter(radius: radius, divideRadiusByImageScale: divideRadiusByImageScale) + ] } - /// The filter closure used to create the modified representation of the given image. - public var filter: Image -> Image { - return { image in - let scaledImage = image.af_imageScaledToSize(self.size) - let roundedAndScaledImage = scaledImage.af_imageWithRoundedCornerRadius(self.radius * image.scale) - - return roundedAndScaledImage - } - } + /// The image filters to apply to the image in sequential order. + public let filters: [ImageFilter] } // MARK: - /// Scales an image from the center while maintaining the aspect ratio to fit within a specified size, then rounds the /// corners to the specified radius. -public struct AspectScaledToFillSizeWithRoundedCornersFilter: ImageFilter, Sizable, Roundable { - /// The size of the filter. - public let size: CGSize - - /// The radius of the filter. - public let radius: CGFloat - +public struct AspectScaledToFillSizeWithRoundedCornersFilter: CompositeImageFilter { /** Initializes the `AspectScaledToFillSizeWithRoundedCornersFilter` instance with the given size and radius. - - parameter size: The size. - - parameter radius: The radius. + - parameter size: The size. + - parameter radius: The radius. + - parameter divideRadiusByImageScale: Whether to divide the radius by the image scale. Set to `true` when the + image has the same resolution for all screen scales such as @1x, @2x and + @3x (i.e. single image from web server). Set to `false` for images loaded + from an asset catalog with varying resolutions for each screen scale. + `false` by default. - returns: The new `AspectScaledToFillSizeWithRoundedCornersFilter` instance. */ - public init(size: CGSize, radius: CGFloat) { - self.size = size - self.radius = radius + public init(size: CGSize, radius: CGFloat, divideRadiusByImageScale: Bool = false) { + self.filters = [ + AspectScaledToFillSizeFilter(size: size), + RoundedCornersFilter(radius: radius, divideRadiusByImageScale: divideRadiusByImageScale) + ] } - /// The filter closure used to create the modified representation of the given image. - public var filter: Image -> Image { - return { image in - let scaledImage = image.af_imageAspectScaledToFillSize(self.size) - let roundedAndScaledImage = scaledImage.af_imageWithRoundedCornerRadius(self.radius * image.scale) - - return roundedAndScaledImage - } - } + /// The image filters to apply to the image in sequential order. + public let filters: [ImageFilter] } // MARK: - /// Scales an image to a specified size, then rounds the corners into a circle. -public struct ScaledToSizeCircleFilter: ImageFilter, Sizable, Roundable { - /// The size of the filter. - public let size: CGSize - - /// The radius of the filter. - public let radius: CGFloat - +public struct ScaledToSizeCircleFilter: CompositeImageFilter { /** Initializes the `ScaledToSizeCircleFilter` instance with the given size. @@ -333,32 +344,18 @@ public struct ScaledToSizeCircleFilter: ImageFilter, Sizable, Roundable { - returns: The new `ScaledToSizeCircleFilter` instance. */ public init(size: CGSize) { - self.size = size - self.radius = min(size.width, size.height) / 2.0 + self.filters = [ScaledToSizeFilter(size: size), CircleFilter()] } - /// The filter closure used to create the modified representation of the given image. - public var filter: Image -> Image { - return { image in - let scaledImage = image.af_imageScaledToSize(self.size) - let scaledCircleImage = scaledImage.af_imageRoundedIntoCircle() - - return scaledCircleImage - } - } + /// The image filters to apply to the image in sequential order. + public let filters: [ImageFilter] } // MARK: - /// Scales an image from the center while maintaining the aspect ratio to fit within a specified size, then rounds the /// corners into a circle. -public struct AspectScaledToFillSizeCircleFilter: ImageFilter, Sizable, Roundable { - /// The size of the filter. - public let size: CGSize - - /// The radius of the filter. - public let radius: CGFloat - +public struct AspectScaledToFillSizeCircleFilter: CompositeImageFilter { /** Initializes the `AspectScaledToFillSizeCircleFilter` instance with the given size. @@ -367,19 +364,11 @@ public struct AspectScaledToFillSizeCircleFilter: ImageFilter, Sizable, Roundabl - returns: The new `AspectScaledToFillSizeCircleFilter` instance. */ public init(size: CGSize) { - self.size = size - self.radius = min(size.width, size.height) / 2.0 + self.filters = [AspectScaledToFillSizeFilter(size: size), CircleFilter()] } - /// The filter closure used to create the modified representation of the given image. - public var filter: Image -> Image { - return { image in - let scaledImage = image.af_imageAspectScaledToFillSize(self.size) - let scaledCircleImage = scaledImage.af_imageRoundedIntoCircle() - - return scaledCircleImage - } - } + /// The image filters to apply to the image in sequential order. + public let filters: [ImageFilter] } #endif diff --git a/Source/UIImage+AlamofireImage.swift b/Source/UIImage+AlamofireImage.swift index 3d75d8d4..fbb9d957 100644 --- a/Source/UIImage+AlamofireImage.swift +++ b/Source/UIImage+AlamofireImage.swift @@ -201,14 +201,19 @@ extension UIImage { /** Returns a new version of the image with the corners rounded to the specified radius. - - parameter radius: The radius to use when rounding the new image. + - parameter radius: The radius to use when rounding the new image. + - parameter divideRadiusByImageScale: Whether to divide the radius by the image scale. Set to `true` when the + image has the same resolution for all screen scales such as @1x, @2x and + @3x (i.e. single image from web server). Set to `false` for images loaded + from an asset catalog with varying resolutions for each screen scale. + `false` by default. - returns: A new image object. */ - public func af_imageWithRoundedCornerRadius(radius: CGFloat) -> UIImage { + public func af_imageWithRoundedCornerRadius(radius: CGFloat, divideRadiusByImageScale: Bool = false) -> UIImage { UIGraphicsBeginImageContextWithOptions(size, false, 0.0) - let scaledRadius = radius / scale + let scaledRadius = divideRadiusByImageScale ? radius / scale : radius let clippingPath = UIBezierPath(roundedRect: CGRect(origin: CGPointZero, size: size), cornerRadius: scaledRadius) clippingPath.addClip() diff --git a/Tests/ImageFilterTests.swift b/Tests/ImageFilterTests.swift index 3bf55ee2..fb0d9e53 100644 --- a/Tests/ImageFilterTests.swift +++ b/Tests/ImageFilterTests.swift @@ -30,7 +30,7 @@ class ImageFilterTestCase: BaseTestCase { let largeSquareSize = CGSize(width: 100, height: 100) let scale = Int(round(UIScreen.mainScreen().scale)) - // MARK: - Protocol Extension Identifiers + // MARK: - ImageFilter Protocol Extension Identifiers func testThatImageFilterIdentifierIsImplemented() { // Given @@ -62,10 +62,13 @@ class ImageFilterTestCase: BaseTestCase { let identifier = filter.identifier // Then - XCTAssertEqual(identifier, "RoundedCornersFilter-radius:(12)", "identifier does not match expected value") + let expectedIdentifier = "RoundedCornersFilter-radius:(12)-divided:(false)" + XCTAssertEqual(identifier, expectedIdentifier, "identifier does not match expected value") } - func testThatImageFilterWhereSelfIsSizableAndRoundableIdentifierIsImplemented() { + // MARK: - CompositeImageFilter Protocol Extension Identifiers + + func testThatCompositeImageFilterIdentifierIsImplemented() { // Given let filter = ScaledToSizeWithRoundedCornersFilter(size: CGSize(width: 200, height: 100), radius: 20.0123) @@ -73,11 +76,8 @@ class ImageFilterTestCase: BaseTestCase { let identifier = filter.identifier // Then - XCTAssertEqual( - identifier, - "ScaledToSizeWithRoundedCornersFilter-size:(200x100)-radius:(20)", - "identifier does not match expected value" - ) + let expectedIdentifier = "ScaledToSizeFilter-size:(200x100)_RoundedCornersFilter-radius:(20)-divided:(false)" + XCTAssertEqual(identifier, expectedIdentifier, "identifier does not match expected value") } // MARK: - Single Pass Image Filter Tests @@ -124,7 +124,7 @@ class ImageFilterTestCase: BaseTestCase { func testThatRoundedCornersFilterReturnsCorrectFilteredImage() { // Given let image = imageForResource("pirate", withExtension: "jpg") - let filter = RoundedCornersFilter(radius: 20) + let filter = RoundedCornersFilter(radius: 20, divideRadiusByImageScale: true) // When let filteredImage = filter.filter(image) @@ -132,6 +132,9 @@ class ImageFilterTestCase: BaseTestCase { // Then let expectedFilteredImage = imageForResource("pirate-radius-20", withExtension: "png") XCTAssertTrue(filteredImage.af_isEqualToImage(expectedFilteredImage), "filtered image pixels do not match") + + let expectedIdentifier = "RoundedCornersFilter-radius:(20)-divided:(true)" + XCTAssertEqual(filter.identifier, expectedIdentifier, "filter identifier does not match") } func testThatCircleFilterReturnsCorrectFilteredImage() { @@ -163,7 +166,7 @@ class ImageFilterTestCase: BaseTestCase { XCTAssertTrue(pixelsMatch, "pixels match should be true") } - // MARK: - Multi-Pass Image Filter Tests + // MARK: - Composite Image Filter Tests func testThatScaledToSizeWithRoundedCornersFilterReturnsCorrectFilteredImage() { // Given diff --git a/Tests/UIImageTests.swift b/Tests/UIImageTests.swift index 91e6d190..0fe89ab3 100644 --- a/Tests/UIImageTests.swift +++ b/Tests/UIImageTests.swift @@ -261,10 +261,10 @@ class UIImageTestCase: BaseTestCase { let r = Int(round(radius)) // When - let roundedAppleImage = appleImage.af_imageWithRoundedCornerRadius(radius) - let roundedPirateImage = pirateImage.af_imageWithRoundedCornerRadius(radius) - let roundedRainbowImage = rainbowImage.af_imageWithRoundedCornerRadius(radius) - let roundedUnicornImage = unicornImage.af_imageWithRoundedCornerRadius(radius) + let roundedAppleImage = appleImage.af_imageWithRoundedCornerRadius(radius, divideRadiusByImageScale: true) + let roundedPirateImage = pirateImage.af_imageWithRoundedCornerRadius(radius, divideRadiusByImageScale: true) + let roundedRainbowImage = rainbowImage.af_imageWithRoundedCornerRadius(radius, divideRadiusByImageScale: true) + let roundedUnicornImage = unicornImage.af_imageWithRoundedCornerRadius(radius, divideRadiusByImageScale: true) // Then let expectedAppleImage = imageForResource("apple-radius-\(r)", withExtension: "png")