Browse Source

Merge pull request #1753 from onevcat/feature/border-and-stroke

Add border and stroke processor
Wei Wang 4 years ago
parent
commit
75031ab83c

+ 2 - 0
Demo/Demo/Kingfisher-Demo/ViewControllers/ProcessorCollectionViewController.swift

@@ -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"),

+ 87 - 11
Sources/Image/ImageDrawing.swift

@@ -101,6 +101,7 @@ extension KingfisherWrapper where Base: KFCrossPlatformImage {
     #endif
     
     // MARK: Round Corner
+    
     /// Creates a round corner image from on `base` image.
     ///
     /// - Parameters:
@@ -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
@@ -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
@@ -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)
@@ -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 {
@@ -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.
     ///

+ 93 - 34
Sources/Image/ImageProcessor.swift

@@ -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.
 ///
@@ -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.
@@ -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)
@@ -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.
 ///
@@ -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
@@ -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