Skip to content

Commit

Permalink
Merge pull request #10 from Alamofire/feature/composite_image_filters
Browse files Browse the repository at this point in the history
  • Loading branch information
cnoon committed Sep 16, 2015
2 parents 42c590e + 8ab53f6 commit adadac9
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 109 deletions.
173 changes: 81 additions & 92 deletions Source/ImageFilter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) -
Expand Down Expand Up @@ -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: -
Expand Down Expand Up @@ -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.

Expand All @@ -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.

Expand All @@ -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
11 changes: 8 additions & 3 deletions Source/UIImage+AlamofireImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
23 changes: 13 additions & 10 deletions Tests/ImageFilterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -62,22 +62,22 @@ 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)

// When
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
Expand Down Expand Up @@ -124,14 +124,17 @@ 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)

// 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() {
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions Tests/UIImageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit adadac9

Please sign in to comment.