Skip to content

Commit

Permalink
Merge pull request onevcat#1753 from onevcat/feature/border-and-stroke
Browse files Browse the repository at this point in the history
Add border and stroke processor
  • Loading branch information
onevcat authored Jul 19, 2021
2 parents 6b86fad + b268130 commit 75031ab
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class ProcessorCollectionViewController: UICollectionViewController {
(ResizingImageProcessor(referenceSize: CGSize(width: 50, height: 50)), "Resizing"),
(RoundCornerImageProcessor(radius: .point(20)), "Round Corner"),
(RoundCornerImageProcessor(radius: .widthFraction(0.5), roundingCorners: [.topLeft, .bottomRight]), "Round Corner Partial"),
(BorderImageProcessor(border: .init(color: .systemBlue, lineWidth: 8)), "Border"),
(RoundCornerImageProcessor(radius: .widthFraction(0.2)) |> BorderImageProcessor(border: .init(color: .systemBlue.withAlphaComponent(0.7), lineWidth: 12, radius: .widthFraction(0.2))), "Round Border"),
(BlendImageProcessor(blendMode: .lighten, alpha: 1.0, backgroundColor: .red), "Blend"),
(BlurImageProcessor(blurRadius: 5), "Blur"),
(OverlayImageProcessor(overlay: .red, fraction: 0.5), "Overlay"),
Expand Down
98 changes: 87 additions & 11 deletions Sources/Image/ImageDrawing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ extension KingfisherWrapper where Base: KFCrossPlatformImage {
#endif

// MARK: Round Corner

/// Creates a round corner image from on `base` image.
///
/// - Parameters:
Expand All @@ -112,11 +113,14 @@ extension KingfisherWrapper where Base: KFCrossPlatformImage {
///
/// - Note: This method only works for CG-based image. The current image scale is kept.
/// For any non-CG-based image, `base` itself is returned.
public func image(withRoundRadius radius: CGFloat,
fit size: CGSize,
roundingCorners corners: RectCorner = .all,
backgroundColor: KFCrossPlatformColor? = nil) -> KFCrossPlatformImage
public func image(
withRadius radius: Radius,
fit size: CGSize,
roundingCorners corners: RectCorner = .all,
backgroundColor: KFCrossPlatformColor? = nil
) -> KFCrossPlatformImage
{

guard let _ = cgImage else {
assertionFailure("[Kingfisher] Round corner image only works for CG-based image.")
return base
Expand All @@ -131,8 +135,7 @@ extension KingfisherWrapper where Base: KFCrossPlatformImage {
rectPath.fill()
}

let path = NSBezierPath(roundedRect: rect, byRoundingCorners: corners, radius: radius)
path.windingRule = .evenOdd
let path = pathForRoundCorner(rect: rect, radius: radius, corners: corners)
path.addClip()
base.draw(in: rect)
#else
Expand All @@ -147,11 +150,7 @@ extension KingfisherWrapper where Base: KFCrossPlatformImage {
rectPath.fill()
}

let path = UIBezierPath(
roundedRect: rect,
byRoundingCorners: corners.uiRectCorner,
cornerRadii: CGSize(width: radius, height: radius)
)
let path = pathForRoundCorner(rect: rect, radius: radius, corners: corners)
context.addPath(path.cgPath)
context.clip()
base.draw(in: rect)
Expand All @@ -160,6 +159,48 @@ extension KingfisherWrapper where Base: KFCrossPlatformImage {
}
}

/// Creates a round corner image from on `base` image.
///
/// - Parameters:
/// - radius: The round corner radius of creating image.
/// - size: The target size of creating image.
/// - corners: The target corners which will be applied rounding.
/// - backgroundColor: The background color for the output image
/// - Returns: An image with round corner of `self`.
///
/// - Note: This method only works for CG-based image. The current image scale is kept.
/// For any non-CG-based image, `base` itself is returned.
public func image(
withRoundRadius radius: CGFloat,
fit size: CGSize,
roundingCorners corners: RectCorner = .all,
backgroundColor: KFCrossPlatformColor? = nil
) -> KFCrossPlatformImage
{
image(withRadius: .point(radius), fit: size, roundingCorners: corners, backgroundColor: backgroundColor)
}

#if os(macOS)
func pathForRoundCorner(rect: CGRect, radius: Radius, corners: RectCorner, offsetBase: CGFloat = 0) -> NSBezierPath {
let cornerRadius = radius.compute(with: rect.size)
let path = NSBezierPath(roundedRect: rect, byRoundingCorners: corners, radius: cornerRadius - offsetBase / 2)
path.windingRule = .evenOdd
return path
}
#else
func pathForRoundCorner(rect: CGRect, radius: Radius, corners: RectCorner, offsetBase: CGFloat = 0) -> UIBezierPath {
let cornerRadius = radius.compute(with: rect.size)
return UIBezierPath(
roundedRect: rect,
byRoundingCorners: corners.uiRectCorner,
cornerRadii: CGSize(
width: cornerRadius - offsetBase / 2,
height: cornerRadius - offsetBase / 2
)
)
}
#endif

#if os(iOS) || os(tvOS)
func resize(to size: CGSize, for contentMode: UIView.ContentMode) -> KFCrossPlatformImage {
switch contentMode {
Expand Down Expand Up @@ -329,6 +370,41 @@ extension KingfisherWrapper where Base: KFCrossPlatformImage {
return blurredImage
}

public func addingBorder(_ border: Border) -> KFCrossPlatformImage
{
guard let _ = cgImage else {
assertionFailure("[Kingfisher] Blend mode image only works for CG-based image.")
return base
}

let rect = CGRect(origin: .zero, size: size)
return draw(to: rect.size, inverting: false) { context in

#if os(macOS)
base.draw(in: rect)
#else
base.draw(in: rect, blendMode: .normal, alpha: 1.0)
#endif


let strokeRect = rect.insetBy(dx: border.lineWidth / 2, dy: border.lineWidth / 2)
context.setStrokeColor(border.color.cgColor)
context.setAlpha(border.color.rgba.a)

let line = pathForRoundCorner(
rect: strokeRect,
radius: border.radius,
corners: border.roundingCorners,
offsetBase: border.lineWidth
)
line.lineCapStyle = .square
line.lineWidth = border.lineWidth
line.stroke()

return false
}
}

// MARK: Overlay
/// Creates an image from `base` image with a color overlay layer.
///
Expand Down
127 changes: 93 additions & 34 deletions Sources/Image/ImageProcessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,42 @@ public struct CompositingImageProcessor: ImageProcessor {
}
#endif

/// Represents a radius specified in a `RoundCornerImageProcessor`.
public enum Radius {
/// The radius should be calculated as a fraction of the image width. Typically the associated value should be
/// between 0 and 0.5, where 0 represents no radius and 0.5 represents using half of the image width.
case widthFraction(CGFloat)
/// The radius should be calculated as a fraction of the image height. Typically the associated value should be
/// between 0 and 0.5, where 0 represents no radius and 0.5 represents using half of the image height.
case heightFraction(CGFloat)
/// Use a fixed point value as the round corner radius.
case point(CGFloat)

var radiusIdentifier: String {
switch self {
case .widthFraction(let f):
return "w_frac_\(f)"
case .heightFraction(let f):
return "h_frac_\(f)"
case .point(let p):
return p.description
}
}

public func compute(with size: CGSize) -> CGFloat {
let cornerRadius: CGFloat
switch self {
case .point(let point):
cornerRadius = point
case .widthFraction(let widthFraction):
cornerRadius = size.width * widthFraction
case .heightFraction(let heightFraction):
cornerRadius = size.height * heightFraction
}
return cornerRadius
}
}

/// Processor for making round corner images. Only CG-based images are supported in macOS,
/// if a non-CG image passed in, the processor will do nothing.
///
Expand All @@ -318,27 +354,7 @@ public struct CompositingImageProcessor: ImageProcessor {
public struct RoundCornerImageProcessor: ImageProcessor {

/// Represents a radius specified in a `RoundCornerImageProcessor`.
public enum Radius {
/// The radius should be calculated as a fraction of the image width. Typically the associated value should be
/// between 0 and 0.5, where 0 represents no radius and 0.5 represents using half of the image width.
case widthFraction(CGFloat)
/// The radius should be calculated as a fraction of the image height. Typically the associated value should be
/// between 0 and 0.5, where 0 represents no radius and 0.5 represents using half of the image height.
case heightFraction(CGFloat)
/// Use a fixed point value as the round corner radius.
case point(CGFloat)

var radiusIdentifier: String {
switch self {
case .widthFraction(let f):
return "w_frac_\(f)"
case .heightFraction(let f):
return "h_frac_\(f)"
case .point(let p):
return p.description
}
}
}
public typealias Radius = Kingfisher.Radius

/// Identifier of the processor.
/// - Note: See documentation of `ImageProcessor` protocol for more.
Expand Down Expand Up @@ -436,20 +452,9 @@ public struct RoundCornerImageProcessor: ImageProcessor {
switch item {
case .image(let image):
let size = targetSize ?? image.kf.size

let cornerRadius: CGFloat
switch radius {
case .point(let point):
cornerRadius = point
case .widthFraction(let widthFraction):
cornerRadius = size.width * widthFraction
case .heightFraction(let heightFraction):
cornerRadius = size.height * heightFraction
}

return image.kf.scaled(to: options.scaleFactor)
.kf.image(
withRoundRadius: cornerRadius,
withRadius: radius,
fit: size,
roundingCorners: roundingCorners,
backgroundColor: backgroundColor)
Expand All @@ -459,6 +464,52 @@ public struct RoundCornerImageProcessor: ImageProcessor {
}
}

public struct Border {
public var color: KFCrossPlatformColor
public var lineWidth: CGFloat

/// The radius will be applied in processing. Specify a certain point value with `.point`, or a fraction of the
/// target image with `.widthFraction`. or `.heightFraction`. For example, given a square image with width and
/// height equals, `.widthFraction(0.5)` means use half of the length of size and makes the final image a round one.
public var radius: Radius

/// The target corners which will be applied rounding.
public var roundingCorners: RectCorner

public init(
color: KFCrossPlatformColor = .black,
lineWidth: CGFloat = 4,
radius: Radius = .point(0),
roundingCorners: RectCorner = .all
) {
self.color = color
self.lineWidth = lineWidth
self.radius = radius
self.roundingCorners = roundingCorners
}

var identifier: String {
"\(color.hex)_\(lineWidth)_\(radius.radiusIdentifier)_\(roundingCorners.cornerIdentifier)"
}
}

public struct BorderImageProcessor: ImageProcessor {
public var identifier: String { "com.onevcat.Kingfisher.RoundCornerImageProcessor(\(border)" }
public let border: Border

public init(border: Border) {
self.border = border
}

public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
switch item {
case .image(let image):
return image.kf.addingBorder(border)
case .data:
return (DefaultImageProcessor.default |> self).process(item: item, options: options)
}
}
}

/// Represents how a size adjusts itself to fit a target size.
///
Expand Down Expand Up @@ -849,7 +900,8 @@ public func |>(left: ImageProcessor, right: ImageProcessor) -> ImageProcessor {
}

extension KFCrossPlatformColor {
var hex: String {

var rgba: (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) {
var r: CGFloat = 0
var g: CGFloat = 0
var b: CGFloat = 0
Expand All @@ -860,6 +912,13 @@ extension KFCrossPlatformColor {
#else
getRed(&r, green: &g, blue: &b, alpha: &a)
#endif

return (r, g, b, a)
}

var hex: String {

let (r, g, b, a) = rgba

let rInt = Int(r * 255) << 24
let gInt = Int(g * 255) << 16
Expand Down

0 comments on commit 75031ab

Please sign in to comment.