Преглед изворни кода

Merge pull request #630 from onevcat/feature/crop-processor

Implement crop processor
Wei Wang пре 9 година
родитељ
комит
c99218536f

+ 56 - 0
Kingfisher.xcodeproj/project.pbxproj

@@ -372,6 +372,24 @@
 		D141465A1E5C7E86001476DF /* unicorn-resize-240-60-aspectFit.png in Resources */ = {isa = PBXBuildFile; fileRef = D14146361E5C7E86001476DF /* unicorn-resize-240-60-aspectFit.png */; };
 		D1679A461C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D1679A451C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
 		D1679A531C4E78B20020FD12 /* Kingfisher-watchOS-Demo.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = D1679A391C4E78B20020FD12 /* Kingfisher-watchOS-Demo.app */; };
+		D18C16A41E87FCF500673D57 /* kingfisher-cropping-50-50-anchor-center.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D18C16A11E87FCF500673D57 /* kingfisher-cropping-50-50-anchor-center.jpg */; };
+		D18C16A51E87FCF500673D57 /* kingfisher-cropping-50-50-anchor-center.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D18C16A11E87FCF500673D57 /* kingfisher-cropping-50-50-anchor-center.jpg */; };
+		D18C16A61E87FCF500673D57 /* kingfisher-cropping-50-50-anchor-center.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D18C16A11E87FCF500673D57 /* kingfisher-cropping-50-50-anchor-center.jpg */; };
+		D18C16A71E87FCF500673D57 /* onevcat-cropping-50-50-anchor-center.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D18C16A21E87FCF500673D57 /* onevcat-cropping-50-50-anchor-center.jpg */; };
+		D18C16A81E87FCF500673D57 /* onevcat-cropping-50-50-anchor-center.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D18C16A21E87FCF500673D57 /* onevcat-cropping-50-50-anchor-center.jpg */; };
+		D18C16A91E87FCF500673D57 /* onevcat-cropping-50-50-anchor-center.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D18C16A21E87FCF500673D57 /* onevcat-cropping-50-50-anchor-center.jpg */; };
+		D18C16AA1E87FCF500673D57 /* unicorn-cropping-50-50-anchor-center.png in Resources */ = {isa = PBXBuildFile; fileRef = D18C16A31E87FCF500673D57 /* unicorn-cropping-50-50-anchor-center.png */; };
+		D18C16AB1E87FCF500673D57 /* unicorn-cropping-50-50-anchor-center.png in Resources */ = {isa = PBXBuildFile; fileRef = D18C16A31E87FCF500673D57 /* unicorn-cropping-50-50-anchor-center.png */; };
+		D18C16AC1E87FCF500673D57 /* unicorn-cropping-50-50-anchor-center.png in Resources */ = {isa = PBXBuildFile; fileRef = D18C16A31E87FCF500673D57 /* unicorn-cropping-50-50-anchor-center.png */; };
+		D18C16B11E87FDA300673D57 /* kingfisher-cropping-50-50-anchor-center-mac.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D18C16AE1E87FDA300673D57 /* kingfisher-cropping-50-50-anchor-center-mac.jpg */; };
+		D18C16B21E87FDA300673D57 /* kingfisher-cropping-50-50-anchor-center-mac.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D18C16AE1E87FDA300673D57 /* kingfisher-cropping-50-50-anchor-center-mac.jpg */; };
+		D18C16B31E87FDA300673D57 /* kingfisher-cropping-50-50-anchor-center-mac.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D18C16AE1E87FDA300673D57 /* kingfisher-cropping-50-50-anchor-center-mac.jpg */; };
+		D18C16B41E87FDA300673D57 /* onevcat-cropping-50-50-anchor-center-mac.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D18C16AF1E87FDA300673D57 /* onevcat-cropping-50-50-anchor-center-mac.jpg */; };
+		D18C16B51E87FDA300673D57 /* onevcat-cropping-50-50-anchor-center-mac.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D18C16AF1E87FDA300673D57 /* onevcat-cropping-50-50-anchor-center-mac.jpg */; };
+		D18C16B61E87FDA300673D57 /* onevcat-cropping-50-50-anchor-center-mac.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D18C16AF1E87FDA300673D57 /* onevcat-cropping-50-50-anchor-center-mac.jpg */; };
+		D18C16B71E87FDA300673D57 /* unicorn-cropping-50-50-anchor-center-mac.png in Resources */ = {isa = PBXBuildFile; fileRef = D18C16B01E87FDA300673D57 /* unicorn-cropping-50-50-anchor-center-mac.png */; };
+		D18C16B81E87FDA300673D57 /* unicorn-cropping-50-50-anchor-center-mac.png in Resources */ = {isa = PBXBuildFile; fileRef = D18C16B01E87FDA300673D57 /* unicorn-cropping-50-50-anchor-center-mac.png */; };
+		D18C16B91E87FDA300673D57 /* unicorn-cropping-50-50-anchor-center-mac.png in Resources */ = {isa = PBXBuildFile; fileRef = D18C16B01E87FDA300673D57 /* unicorn-cropping-50-50-anchor-center-mac.png */; };
 		D1D2C32A1C70A3230018F2F9 /* single-frame.gif in Resources */ = {isa = PBXBuildFile; fileRef = D1D2C3291C70A3230018F2F9 /* single-frame.gif */; };
 		D1D2C32B1C70A3230018F2F9 /* single-frame.gif in Resources */ = {isa = PBXBuildFile; fileRef = D1D2C3291C70A3230018F2F9 /* single-frame.gif */; };
 		D1D2C32C1C70A3230018F2F9 /* single-frame.gif in Resources */ = {isa = PBXBuildFile; fileRef = D1D2C3291C70A3230018F2F9 /* single-frame.gif */; };
@@ -666,6 +684,12 @@
 		D16799EB1C4E74460020FD12 /* Kingfisher.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Kingfisher.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		D1679A391C4E78B20020FD12 /* Kingfisher-watchOS-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Kingfisher-watchOS-Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; };
 		D1679A451C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Kingfisher-watchOS-Demo Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
+		D18C16A11E87FCF500673D57 /* kingfisher-cropping-50-50-anchor-center.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "kingfisher-cropping-50-50-anchor-center.jpg"; sourceTree = "<group>"; };
+		D18C16A21E87FCF500673D57 /* onevcat-cropping-50-50-anchor-center.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "onevcat-cropping-50-50-anchor-center.jpg"; sourceTree = "<group>"; };
+		D18C16A31E87FCF500673D57 /* unicorn-cropping-50-50-anchor-center.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "unicorn-cropping-50-50-anchor-center.png"; sourceTree = "<group>"; };
+		D18C16AE1E87FDA300673D57 /* kingfisher-cropping-50-50-anchor-center-mac.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "kingfisher-cropping-50-50-anchor-center-mac.jpg"; sourceTree = "<group>"; };
+		D18C16AF1E87FDA300673D57 /* onevcat-cropping-50-50-anchor-center-mac.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "onevcat-cropping-50-50-anchor-center-mac.jpg"; sourceTree = "<group>"; };
+		D18C16B01E87FDA300673D57 /* unicorn-cropping-50-50-anchor-center-mac.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "unicorn-cropping-50-50-anchor-center-mac.png"; sourceTree = "<group>"; };
 		D1D2C3291C70A3230018F2F9 /* single-frame.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = "single-frame.gif"; sourceTree = "<group>"; };
 		D1DC4B401D60996D00DFDFAA /* StringExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExtensionTests.swift; sourceTree = "<group>"; };
 		D1ED2D0B1AD2CFA600CFC3EB /* Kingfisher-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Kingfisher-Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -794,6 +818,7 @@
 				4BB83E0F1E32075800B64183 /* Blur */,
 				4BB83E161E32075800B64183 /* ColorControl */,
 				4BB83E1D1E32075800B64183 /* Composition */,
+				D18C16A01E87FCF500673D57 /* Crop */,
 				4BB83E241E32075800B64183 /* Overlay */,
 				4BB83E311E32075800B64183 /* Resize */,
 				4BB83E381E32075800B64183 /* RoundCorner */,
@@ -1145,6 +1170,19 @@
 			path = "Demo/Kingfisher-watchOS-Demo Extension";
 			sourceTree = "<group>";
 		};
+		D18C16A01E87FCF500673D57 /* Crop */ = {
+			isa = PBXGroup;
+			children = (
+				D18C16A11E87FCF500673D57 /* kingfisher-cropping-50-50-anchor-center.jpg */,
+				D18C16A21E87FCF500673D57 /* onevcat-cropping-50-50-anchor-center.jpg */,
+				D18C16A31E87FCF500673D57 /* unicorn-cropping-50-50-anchor-center.png */,
+				D18C16AE1E87FDA300673D57 /* kingfisher-cropping-50-50-anchor-center-mac.jpg */,
+				D18C16AF1E87FDA300673D57 /* onevcat-cropping-50-50-anchor-center-mac.jpg */,
+				D18C16B01E87FDA300673D57 /* unicorn-cropping-50-50-anchor-center-mac.png */,
+			);
+			path = Crop;
+			sourceTree = "<group>";
+		};
 		D1ED2D021AD2CFA600CFC3EB = {
 			isa = PBXGroup;
 			children = (
@@ -1602,6 +1640,7 @@
 				4BB83EAF1E32075800B64183 /* onevcat-overlay-red.jpg in Resources */,
 				4BB83EBE1E32075800B64183 /* kingfisher-resize-120-mac.jpg in Resources */,
 				4BB83ECA1E32075800B64183 /* unicorn-resize-120-mac.png in Resources */,
+				D18C16B21E87FDA300673D57 /* kingfisher-cropping-50-50-anchor-center-mac.jpg in Resources */,
 				4BB83EB51E32075800B64183 /* unicorn-overlay-red-07.png in Resources */,
 				4BB83EC11E32075800B64183 /* kingfisher-resize-120.jpg in Resources */,
 				4BB83EB21E32075800B64183 /* unicorn-overlay-red-07-mac.png in Resources */,
@@ -1616,12 +1655,15 @@
 				4BB83ED31E32075800B64183 /* kingfisher-round-corner-40.jpg in Resources */,
 				4BB83EAC1E32075800B64183 /* onevcat-overlay-red-mac.jpg in Resources */,
 				4BB83EC41E32075800B64183 /* onevcat-resize-120-mac.jpg in Resources */,
+				D18C16B81E87FDA300673D57 /* unicorn-cropping-50-50-anchor-center-mac.png in Resources */,
 				D14146441E5C7E86001476DF /* onevcat-resize-240-60-aspectFill-mac.jpg in Resources */,
 				D14146411E5C7E86001476DF /* kingfisher-resize-240-60-aspectFit.jpg in Resources */,
 				D141464A1E5C7E86001476DF /* onevcat-resize-240-60-aspectFit-mac.jpg in Resources */,
+				D18C16AB1E87FCF500673D57 /* unicorn-cropping-50-50-anchor-center.png in Resources */,
 				4BB83F091E32075800B64183 /* onevcat.jpg in Resources */,
 				4BB83EC71E32075800B64183 /* onevcat-resize-120.jpg in Resources */,
 				D14146531E5C7E86001476DF /* unicorn-resize-240-60-aspectFill.png in Resources */,
+				D18C16A81E87FCF500673D57 /* onevcat-cropping-50-50-anchor-center.jpg in Resources */,
 				4BB83EF41E32075800B64183 /* kingfisher-tint-yellow-02-mac.jpg in Resources */,
 				4BB83EE51E32075800B64183 /* onevcat-round-corner-60-resize-100.jpg in Resources */,
 				4BB83E701E32075800B64183 /* unicorn-blur-10-mac.png in Resources */,
@@ -1632,8 +1674,10 @@
 				4BB83E731E32075800B64183 /* unicorn-blur-10.png in Resources */,
 				4BB83F061E32075800B64183 /* kingfisher.jpg in Resources */,
 				4BB83EFA1E32075800B64183 /* onevcat-tint-yellow-02-mac.jpg in Resources */,
+				D18C16B51E87FDA300673D57 /* onevcat-cropping-50-50-anchor-center-mac.jpg in Resources */,
 				D141463E1E5C7E86001476DF /* kingfisher-resize-240-60-aspectFit-mac.jpg in Resources */,
 				4BB83EBB1E32075800B64183 /* unicorn-overlay-red.png in Resources */,
+				D18C16A51E87FCF500673D57 /* kingfisher-cropping-50-50-anchor-center.jpg in Resources */,
 				D14146591E5C7E86001476DF /* unicorn-resize-240-60-aspectFit.png in Resources */,
 				4BB83E8E1E32075800B64183 /* onevcat-blur-4-round-corner-60-mac.jpg in Resources */,
 				4BB83E641E32075800B64183 /* kingfisher-blur-10-mac.jpg in Resources */,
@@ -1686,6 +1730,7 @@
 				4BB83EB01E32075800B64183 /* onevcat-overlay-red.jpg in Resources */,
 				4BB83EBF1E32075800B64183 /* kingfisher-resize-120-mac.jpg in Resources */,
 				4BB83ECB1E32075800B64183 /* unicorn-resize-120-mac.png in Resources */,
+				D18C16B31E87FDA300673D57 /* kingfisher-cropping-50-50-anchor-center-mac.jpg in Resources */,
 				4BB83EB61E32075800B64183 /* unicorn-overlay-red-07.png in Resources */,
 				4BB83EC21E32075800B64183 /* kingfisher-resize-120.jpg in Resources */,
 				4BB83EB31E32075800B64183 /* unicorn-overlay-red-07-mac.png in Resources */,
@@ -1700,12 +1745,15 @@
 				4BB83ED41E32075800B64183 /* kingfisher-round-corner-40.jpg in Resources */,
 				4BB83EAD1E32075800B64183 /* onevcat-overlay-red-mac.jpg in Resources */,
 				4BB83EC51E32075800B64183 /* onevcat-resize-120-mac.jpg in Resources */,
+				D18C16B91E87FDA300673D57 /* unicorn-cropping-50-50-anchor-center-mac.png in Resources */,
 				D14146451E5C7E86001476DF /* onevcat-resize-240-60-aspectFill-mac.jpg in Resources */,
 				D14146421E5C7E86001476DF /* kingfisher-resize-240-60-aspectFit.jpg in Resources */,
 				D141464B1E5C7E86001476DF /* onevcat-resize-240-60-aspectFit-mac.jpg in Resources */,
+				D18C16AC1E87FCF500673D57 /* unicorn-cropping-50-50-anchor-center.png in Resources */,
 				4BB83F0A1E32075800B64183 /* onevcat.jpg in Resources */,
 				4BB83EC81E32075800B64183 /* onevcat-resize-120.jpg in Resources */,
 				D14146541E5C7E86001476DF /* unicorn-resize-240-60-aspectFill.png in Resources */,
+				D18C16A91E87FCF500673D57 /* onevcat-cropping-50-50-anchor-center.jpg in Resources */,
 				4BB83EF51E32075800B64183 /* kingfisher-tint-yellow-02-mac.jpg in Resources */,
 				4BB83EE61E32075800B64183 /* onevcat-round-corner-60-resize-100.jpg in Resources */,
 				4BB83E711E32075800B64183 /* unicorn-blur-10-mac.png in Resources */,
@@ -1716,8 +1764,10 @@
 				4BB83E741E32075800B64183 /* unicorn-blur-10.png in Resources */,
 				4BB83F071E32075800B64183 /* kingfisher.jpg in Resources */,
 				4BB83EFB1E32075800B64183 /* onevcat-tint-yellow-02-mac.jpg in Resources */,
+				D18C16B61E87FDA300673D57 /* onevcat-cropping-50-50-anchor-center-mac.jpg in Resources */,
 				D141463F1E5C7E86001476DF /* kingfisher-resize-240-60-aspectFit-mac.jpg in Resources */,
 				4BB83EBC1E32075800B64183 /* unicorn-overlay-red.png in Resources */,
+				D18C16A61E87FCF500673D57 /* kingfisher-cropping-50-50-anchor-center.jpg in Resources */,
 				D141465A1E5C7E86001476DF /* unicorn-resize-240-60-aspectFit.png in Resources */,
 				4BB83E8F1E32075800B64183 /* onevcat-blur-4-round-corner-60-mac.jpg in Resources */,
 				4BB83E651E32075800B64183 /* kingfisher-blur-10-mac.jpg in Resources */,
@@ -1829,6 +1879,7 @@
 				4BB83EAE1E32075800B64183 /* onevcat-overlay-red.jpg in Resources */,
 				4BB83EBD1E32075800B64183 /* kingfisher-resize-120-mac.jpg in Resources */,
 				4BB83EC91E32075800B64183 /* unicorn-resize-120-mac.png in Resources */,
+				D18C16B11E87FDA300673D57 /* kingfisher-cropping-50-50-anchor-center-mac.jpg in Resources */,
 				4BB83EB41E32075800B64183 /* unicorn-overlay-red-07.png in Resources */,
 				4BB83EC01E32075800B64183 /* kingfisher-resize-120.jpg in Resources */,
 				4BB83EB11E32075800B64183 /* unicorn-overlay-red-07-mac.png in Resources */,
@@ -1843,12 +1894,15 @@
 				4BB83ED21E32075800B64183 /* kingfisher-round-corner-40.jpg in Resources */,
 				4BB83EAB1E32075800B64183 /* onevcat-overlay-red-mac.jpg in Resources */,
 				4BB83EC31E32075800B64183 /* onevcat-resize-120-mac.jpg in Resources */,
+				D18C16B71E87FDA300673D57 /* unicorn-cropping-50-50-anchor-center-mac.png in Resources */,
 				D14146431E5C7E86001476DF /* onevcat-resize-240-60-aspectFill-mac.jpg in Resources */,
 				D14146401E5C7E86001476DF /* kingfisher-resize-240-60-aspectFit.jpg in Resources */,
 				D14146491E5C7E86001476DF /* onevcat-resize-240-60-aspectFit-mac.jpg in Resources */,
+				D18C16AA1E87FCF500673D57 /* unicorn-cropping-50-50-anchor-center.png in Resources */,
 				4BB83F081E32075800B64183 /* onevcat.jpg in Resources */,
 				4BB83EC61E32075800B64183 /* onevcat-resize-120.jpg in Resources */,
 				D14146521E5C7E86001476DF /* unicorn-resize-240-60-aspectFill.png in Resources */,
+				D18C16A71E87FCF500673D57 /* onevcat-cropping-50-50-anchor-center.jpg in Resources */,
 				4BB83EF31E32075800B64183 /* kingfisher-tint-yellow-02-mac.jpg in Resources */,
 				4BB83EE41E32075800B64183 /* onevcat-round-corner-60-resize-100.jpg in Resources */,
 				4BB83E6F1E32075800B64183 /* unicorn-blur-10-mac.png in Resources */,
@@ -1859,8 +1913,10 @@
 				4BB83E721E32075800B64183 /* unicorn-blur-10.png in Resources */,
 				4BB83F051E32075800B64183 /* kingfisher.jpg in Resources */,
 				4BB83EF91E32075800B64183 /* onevcat-tint-yellow-02-mac.jpg in Resources */,
+				D18C16B41E87FDA300673D57 /* onevcat-cropping-50-50-anchor-center-mac.jpg in Resources */,
 				D141463D1E5C7E86001476DF /* kingfisher-resize-240-60-aspectFit-mac.jpg in Resources */,
 				4BB83EBA1E32075800B64183 /* unicorn-overlay-red.png in Resources */,
+				D18C16A41E87FCF500673D57 /* kingfisher-cropping-50-50-anchor-center.jpg in Resources */,
 				D14146581E5C7E86001476DF /* unicorn-resize-240-60-aspectFit.png in Resources */,
 				4BB83E8D1E32075800B64183 /* onevcat-blur-4-round-corner-60-mac.jpg in Resources */,
 				4BB83E631E32075800B64183 /* kingfisher-blur-10-mac.jpg in Resources */,

+ 40 - 5
Sources/Image.swift

@@ -141,11 +141,11 @@ extension Kingfisher where Base: Image {
     }
     #else
     static func image(cgImage: CGImage, scale: CGFloat, refImage: Image?) -> Image {
-    if let refImage = refImage {
-        return Image(cgImage: cgImage, scale: scale, orientation: refImage.imageOrientation)
-    } else {
-        return Image(cgImage: cgImage, scale: scale, orientation: .up)
-    }
+        if let refImage = refImage {
+            return Image(cgImage: cgImage, scale: scale, orientation: refImage.imageOrientation)
+        } else {
+            return Image(cgImage: cgImage, scale: scale, orientation: .up)
+        }
     }
     
     /**
@@ -430,6 +430,21 @@ extension Kingfisher where Base: Image {
         }
     }
     
+    public func crop(to size: CGSize, anchorOn anchor: CGPoint) -> Image {
+        guard let cgImage = cgImage else {
+            assertionFailure("[Kingfisher] Crop only works for CG-based image.")
+            return base
+        }
+        
+        let rect = self.size.kf.constrainedRect(for: size, anchor: anchor)
+        guard let image = cgImage.cropping(to: rect) else {
+            assertionFailure("[Kingfisher] Cropping image failed.")
+            return base
+        }
+        
+        return Kingfisher.image(cgImage: image, scale: scale, refImage: base)
+    }
+    
     // MARK: - Blur
     
     /// Create an image with blur effect based on `self`.
@@ -716,6 +731,26 @@ extension CGSizeProxy {
     private var aspectRatio: CGFloat {
         return base.height == 0.0 ? 1.0 : base.width / base.height
     }
+    
+    
+    func constrainedRect(for size: CGSize, anchor: CGPoint) -> CGRect {
+        
+        let unifiedAnchor = CGPoint(x: anchor.x.clamped(to: 0.0...1.0),
+                                    y: anchor.y.clamped(to: 0.0...1.0))
+        
+        let x = unifiedAnchor.x * base.width - unifiedAnchor.x * size.width
+        let y = unifiedAnchor.y * base.height - unifiedAnchor.y * size.height
+        let r = CGRect(x: x, y: y, width: size.width, height: size.height)
+        
+        let ori = CGRect(origin: CGPoint.zero, size: base)
+        return ori.intersection(r)
+    }
+}
+
+extension Comparable {
+    func clamped(to limits: ClosedRange<Self>) -> Self {
+        return min(max(self, limits.lowerBound), limits.upperBound)
+    }
 }
 
 extension Kingfisher where Base: Image {

+ 144 - 18
Sources/ImageProcessor.swift

@@ -73,7 +73,7 @@ public extension ImageProcessor {
     ///
     /// - parameter another: An `ImageProcessor` you want to append to `self`.
     ///
-    /// - returns: The new `ImageProcessor`. It will process the image in the order
+    /// - returns: The new `ImageProcessor` will process the image in the order
     ///            of the two processors concatenated.
     public func append(another: ImageProcessor) -> ImageProcessor {
         let newIdentifier = identifier.appending("|>\(another.identifier)")
@@ -104,13 +104,21 @@ public struct DefaultImageProcessor: ImageProcessor {
     /// A default `DefaultImageProcessor` could be used across.
     public static let `default` = DefaultImageProcessor()
     
+    /// Identifier of the processor.
+    /// - Note: See documentation of `ImageProcessor` protocol for more.
     public let identifier = ""
     
     /// Initialize a `DefaultImageProcessor`
-    ///
-    /// - returns: An initialized `DefaultImageProcessor`.
     public init() {}
     
+    /// Process an input `ImageProcessItem` item to an image for this processor.
+    ///
+    /// - parameter item:    Input item which will be processed by `self`
+    /// - parameter options: Options when processing the item.
+    ///
+    /// - returns: The processed image.
+    /// 
+    /// - Note: See documentation of `ImageProcessor` protocol for more.
     public func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? {
         switch item {
         case .image(let image):
@@ -128,6 +136,9 @@ public struct DefaultImageProcessor: ImageProcessor {
 /// 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.
 public struct RoundCornerImageProcessor: ImageProcessor {
+    
+    /// Identifier of the processor.
+    /// - Note: See documentation of `ImageProcessor` protocol for more.
     public let identifier: String
 
     /// Corner radius will be applied in processing.
@@ -142,8 +153,6 @@ public struct RoundCornerImageProcessor: ImageProcessor {
     /// - parameter targetSize:   Target size of output image should be. If `nil`, 
     ///                           the image will keep its original size after processing.
     ///                           Default is `nil`.
-    ///
-    /// - returns: An initialized `RoundCornerImageProcessor`.
     public init(cornerRadius: CGFloat, targetSize: CGSize? = nil) {
         self.cornerRadius = cornerRadius
         self.targetSize = targetSize
@@ -154,6 +163,14 @@ public struct RoundCornerImageProcessor: ImageProcessor {
         }
     }
     
+    /// Process an input `ImageProcessItem` item to an image for this processor.
+    ///
+    /// - parameter item:    Input item which will be processed by `self`
+    /// - parameter options: Options when processing the item.
+    ///
+    /// - returns: The processed image.
+    ///
+    /// - Note: See documentation of `ImageProcessor` protocol for more.
     public func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? {
         switch item {
         case .image(let image):
@@ -179,6 +196,9 @@ public enum ContentMode {
 
 /// Processor for resizing images. Only CG-based images are supported in macOS.
 public struct ResizingImageProcessor: ImageProcessor {
+    
+    /// Identifier of the processor.
+    /// - Note: See documentation of `ImageProcessor` protocol for more.
     public let identifier: String
     
     /// Target size of output image should be.
@@ -192,8 +212,6 @@ public struct ResizingImageProcessor: ImageProcessor {
     ///
     /// - parameter targetSize: Target size of output image should be.
     /// - parameter contentMode: Target content mode of output image should be.
-    ///
-    /// - returns: An initialized `ResizingImageProcessor`.
     public init(targetSize: CGSize, contentMode: ContentMode = .none) {
         self.targetSize = targetSize
         self.targetContentMode = contentMode
@@ -205,6 +223,14 @@ public struct ResizingImageProcessor: ImageProcessor {
         }
     }
     
+    /// Process an input `ImageProcessItem` item to an image for this processor.
+    ///
+    /// - parameter item:    Input item which will be processed by `self`
+    /// - parameter options: Options when processing the item.
+    ///
+    /// - returns: The processed image.
+    ///
+    /// - Note: See documentation of `ImageProcessor` protocol for more.
     public func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? {
         switch item {
         case .image(let image):
@@ -218,6 +244,9 @@ public struct ResizingImageProcessor: ImageProcessor {
 /// Processor for adding blur effect to images. `Accelerate.framework` is used underhood for 
 /// a better performance. A simulated Gaussian blur with specified blur radius will be applied.
 public struct BlurImageProcessor: ImageProcessor {
+    
+    /// Identifier of the processor.
+    /// - Note: See documentation of `ImageProcessor` protocol for more.
     public let identifier: String
     
     /// Blur radius for the simulated Gaussian blur.
@@ -226,13 +255,19 @@ public struct BlurImageProcessor: ImageProcessor {
     /// Initialize a `BlurImageProcessor`
     ///
     /// - parameter blurRadius: Blur radius for the simulated Gaussian blur.
-    ///
-    /// - returns: An initialized `BlurImageProcessor`.
     public init(blurRadius: CGFloat) {
         self.blurRadius = blurRadius
         self.identifier = "com.onevcat.Kingfisher.BlurImageProcessor(\(blurRadius))"
     }
     
+    /// Process an input `ImageProcessItem` item to an image for this processor.
+    ///
+    /// - parameter item:    Input item which will be processed by `self`
+    /// - parameter options: Options when processing the item.
+    ///
+    /// - returns: The processed image.
+    ///
+    /// - Note: See documentation of `ImageProcessor` protocol for more.
     public func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? {
         switch item {
         case .image(let image):
@@ -247,7 +282,9 @@ public struct BlurImageProcessor: ImageProcessor {
 /// Processor for adding an overlay to images. Only CG-based images are supported in macOS.
 public struct OverlayImageProcessor: ImageProcessor {
     
-    public var identifier: String
+    /// Identifier of the processor.
+    /// - Note: See documentation of `ImageProcessor` protocol for more.
+    public let identifier: String
     
     /// Overlay color will be used to overlay the input image.
     public let overlay: Color
@@ -260,14 +297,20 @@ public struct OverlayImageProcessor: ImageProcessor {
     /// - parameter overlay:  Overlay color will be used to overlay the input image.
     /// - parameter fraction: Fraction will be used when overlay the color to image. 
     ///                       From 0.0 to 1.0. 0.0 means solid color, 1.0 means transparent overlay.
-    ///
-    /// - returns: An initialized `OverlayImageProcessor`.
     public init(overlay: Color, fraction: CGFloat = 0.5) {
         self.overlay = overlay
         self.fraction = fraction
         self.identifier = "com.onevcat.Kingfisher.OverlayImageProcessor(\(overlay.hex)_\(fraction))"
     }
     
+    /// Process an input `ImageProcessItem` item to an image for this processor.
+    ///
+    /// - parameter item:    Input item which will be processed by `self`
+    /// - parameter options: Options when processing the item.
+    ///
+    /// - returns: The processed image.
+    ///
+    /// - Note: See documentation of `ImageProcessor` protocol for more.
     public func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? {
         switch item {
         case .image(let image):
@@ -281,6 +324,8 @@ public struct OverlayImageProcessor: ImageProcessor {
 /// Processor for tint images with color. Only CG-based images are supported.
 public struct TintImageProcessor: ImageProcessor {
     
+    /// Identifier of the processor.
+    /// - Note: See documentation of `ImageProcessor` protocol for more.
     public let identifier: String
     
     /// Tint color will be used to tint the input image.
@@ -289,13 +334,19 @@ public struct TintImageProcessor: ImageProcessor {
     /// Initialize a `TintImageProcessor`
     ///
     /// - parameter tint: Tint color will be used to tint the input image.
-    ///
-    /// - returns: An initialized `TintImageProcessor`.
     public init(tint: Color) {
         self.tint = tint
         self.identifier = "com.onevcat.Kingfisher.TintImageProcessor(\(tint.hex))"
     }
     
+    /// Process an input `ImageProcessItem` item to an image for this processor.
+    ///
+    /// - parameter item:    Input item which will be processed by `self`
+    /// - parameter options: Options when processing the item.
+    ///
+    /// - returns: The processed image.
+    ///
+    /// - Note: See documentation of `ImageProcessor` protocol for more.
     public func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? {
         switch item {
         case .image(let image):
@@ -310,6 +361,8 @@ public struct TintImageProcessor: ImageProcessor {
 /// watchOS is not supported.
 public struct ColorControlsProcessor: ImageProcessor {
     
+    /// Identifier of the processor.
+    /// - Note: See documentation of `ImageProcessor` protocol for more.
     public let identifier: String
     
     /// Brightness changing to image.
@@ -330,8 +383,6 @@ public struct ColorControlsProcessor: ImageProcessor {
     /// - parameter contrast:   Contrast changing to image.
     /// - parameter saturation: Saturation changing to image.
     /// - parameter inputEV:    InputEV changing to image.
-    ///
-    /// - returns: An initialized `ColorControlsProcessor`
     public init(brightness: CGFloat, contrast: CGFloat, saturation: CGFloat, inputEV: CGFloat) {
         self.brightness = brightness
         self.contrast = contrast
@@ -340,6 +391,14 @@ public struct ColorControlsProcessor: ImageProcessor {
         self.identifier = "com.onevcat.Kingfisher.ColorControlsProcessor(\(brightness)_\(contrast)_\(saturation)_\(inputEV))"
     }
     
+    /// Process an input `ImageProcessItem` item to an image for this processor.
+    ///
+    /// - parameter item:    Input item which will be processed by `self`
+    /// - parameter options: Options when processing the item.
+    ///
+    /// - returns: The processed image.
+    ///
+    /// - Note: See documentation of `ImageProcessor` protocol for more.
     public func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? {
         switch item {
         case .image(let image):
@@ -353,19 +412,86 @@ public struct ColorControlsProcessor: ImageProcessor {
 /// Processor for applying black and white effect to images. Only CG-based images are supported.
 /// watchOS is not supported.
 public struct BlackWhiteProcessor: ImageProcessor {
+    
+    /// Identifier of the processor.
+    /// - Note: See documentation of `ImageProcessor` protocol for more.
     public let identifier = "com.onevcat.Kingfisher.BlackWhiteProcessor"
     
     /// Initialize a `BlackWhiteProcessor`
-    ///
-    /// - returns: An initialized `BlackWhiteProcessor`
     public init() {}
     
+    /// Process an input `ImageProcessItem` item to an image for this processor.
+    ///
+    /// - parameter item:    Input item which will be processed by `self`
+    /// - parameter options: Options when processing the item.
+    ///
+    /// - returns: The processed image.
+    ///
+    /// - Note: See documentation of `ImageProcessor` protocol for more.
     public func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? {
         return ColorControlsProcessor(brightness: 0.0, contrast: 1.0, saturation: 0.0, inputEV: 0.7)
             .process(item: item, options: options)
     }
 }
 
+/// Processor for cropping an image. Only CG-based images are supported.
+/// watchOS is not supported.
+public struct CroppingImageProcessor: ImageProcessor {
+    
+    /// Identifier of the processor.
+    /// - Note: See documentation of `ImageProcessor` protocol for more.
+    public let identifier: String
+    
+    /// Target size of output image should be.
+    public let size: CGSize
+    
+    /// Anchor point from which the output size should be calculate.
+    /// The anchor point is consisted by two values between 0.0 and 1.0.
+    /// It indicates a related point in current image. 
+    /// See `CroppingImageProcessor.init(size:anchor:)` for more.
+    public let anchor: CGPoint
+    
+    /// Initialize a `CroppingImageProcessor`
+    ///
+    /// - Parameters:
+    ///   - size: Target size of output image should be.
+    ///   - anchor: The anchor point from which the size should be calculated.
+    ///             Default is `CGPoint(x: 0.5, y: 0.5)`, which means the center of input image.
+    /// - Note:
+    ///   The anchor point is consisted by two values between 0.0 and 1.0.
+    ///   It indicates a related point in current image, eg: (0.0, 0.0) for top-left
+    ///   corner, (0.5, 0.5) for center and (1.0, 1.0) for bottom-right corner.
+    ///   The `size` property of `CroppingImageProcessor` will be used along with
+    ///   `anchor` to calculate a target rectange in the size of image.
+    ///    
+    ///   The target size will be automatically calculated with a reasonable behavior.
+    ///   For example, when you have an image size of `CGSize(width: 100, height: 100)`,
+    ///   and a target size of `CGSize(width: 100, height: 100)`, with a (0.0, 0.0) anchor (top-left),
+    ///   the crop rect will be `{0, 0, 20, 20}`; with a (0.5, 0.5) anchor (center), it will be 
+    ///   `{40, 40, 20, 20}`; while with a (1.0, 1.0) anchor (bottom-right), it will be `{80, 80, 20, 20}`
+    public init(size: CGSize, anchor: CGPoint = CGPoint(x: 0.5, y: 0.5)) {
+        self.size = size
+        self.anchor = anchor
+        self.identifier = "com.onevcat.Kingfisher.CroppingImageProcessor(\(size)_\(anchor))"
+    }
+    
+    /// Process an input `ImageProcessItem` item to an image for this processor.
+    ///
+    /// - parameter item:    Input item which will be processed by `self`
+    /// - parameter options: Options when processing the item.
+    ///
+    /// - returns: The processed image.
+    ///
+    /// - Note: See documentation of `ImageProcessor` protocol for more.
+    public func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? {
+        switch item {
+        case .image(let image):
+            return image.kf.crop(to: size, anchorOn: anchor)
+        case .data(_): return (DefaultImageProcessor.default >> self).process(item: item, options: options)
+        }
+    }
+}
+
 /// Concatenate two `ImageProcessor`s. `ImageProcessor.appen(another:)` is used internally.
 ///
 /// - parameter left:  First processor.

+ 46 - 0
Tests/KingfisherTests/ImageExtensionTests.swift

@@ -138,4 +138,50 @@ class ImageExtensionTests: XCTestCase {
         XCTAssertEqual(resizeImage.size.width, 100)
         XCTAssertEqual(resizeImage.size.height, 50)
     }
+    
+    func testSizeConstraintByAnchor() {
+        let size = CGSize(width: 100, height: 100)
+        
+        let topLeft = CGPoint(x: 0, y: 0)
+        let top = CGPoint(x: 0.5, y: 0)
+        let topRight = CGPoint(x: 1, y: 0)
+        let center = CGPoint(x: 0.5, y: 0.5)
+        let bottomRight = CGPoint(x: 1, y: 1)
+        let invalidAnchor = CGPoint(x: -1, y: 2)
+        
+        let inSize = CGSize(width: 20, height: 20)
+        let outX = CGSize(width: 120, height: 20)
+        let outY = CGSize(width: 20, height: 120)
+        let outSize = CGSize(width: 120, height: 120)
+        
+        XCTAssertEqual(size.kf.constrainedRect(for: inSize, anchor: topLeft), CGRect(x: 0, y: 0, width: 20, height: 20))
+        XCTAssertEqual(size.kf.constrainedRect(for: outX, anchor: topLeft), CGRect(x: 0, y: 0, width: 100, height: 20))
+        XCTAssertEqual(size.kf.constrainedRect(for: outY, anchor: topLeft), CGRect(x: 0, y: 0, width: 20, height: 100))
+        XCTAssertEqual(size.kf.constrainedRect(for: outSize, anchor: topLeft), CGRect(x: 0, y: 0, width: 100, height: 100))
+        
+        XCTAssertEqual(size.kf.constrainedRect(for: inSize, anchor: top), CGRect(x: 40, y: 0, width: 20, height: 20))
+        XCTAssertEqual(size.kf.constrainedRect(for: outX, anchor: top), CGRect(x: 0, y: 0, width: 100, height: 20))
+        XCTAssertEqual(size.kf.constrainedRect(for: outY, anchor: top), CGRect(x: 40, y: 0, width: 20, height: 100))
+        XCTAssertEqual(size.kf.constrainedRect(for: outSize, anchor: top), CGRect(x: 0, y: 0, width: 100, height: 100))
+        
+        XCTAssertEqual(size.kf.constrainedRect(for: inSize, anchor: topRight), CGRect(x: 80, y: 0, width: 20, height: 20))
+        XCTAssertEqual(size.kf.constrainedRect(for: outX, anchor: topRight), CGRect(x: 0, y: 0, width: 100, height: 20))
+        XCTAssertEqual(size.kf.constrainedRect(for: outY, anchor: topRight), CGRect(x: 80, y: 0, width: 20, height: 100))
+        XCTAssertEqual(size.kf.constrainedRect(for: outSize, anchor: topRight), CGRect(x: 0, y: 0, width: 100, height: 100))
+        
+        XCTAssertEqual(size.kf.constrainedRect(for: inSize, anchor: center), CGRect(x: 40, y: 40, width: 20, height: 20))
+        XCTAssertEqual(size.kf.constrainedRect(for: outX, anchor: center), CGRect(x: 0, y: 40, width: 100, height: 20))
+        XCTAssertEqual(size.kf.constrainedRect(for: outY, anchor: center), CGRect(x: 40, y: 0, width: 20, height: 100))
+        XCTAssertEqual(size.kf.constrainedRect(for: outSize, anchor: center), CGRect(x: 0, y: 0, width: 100, height: 100))
+        
+        XCTAssertEqual(size.kf.constrainedRect(for: inSize, anchor: bottomRight), CGRect(x: 80, y: 80, width: 20, height: 20))
+        XCTAssertEqual(size.kf.constrainedRect(for: outX, anchor: bottomRight), CGRect(x: 0, y: 80, width: 100, height: 20))
+        XCTAssertEqual(size.kf.constrainedRect(for: outY, anchor: bottomRight), CGRect(x:80, y: 0, width: 20, height: 100))
+        XCTAssertEqual(size.kf.constrainedRect(for: outSize, anchor: bottomRight), CGRect(x: 0, y: 0, width: 100, height: 100))
+        
+        XCTAssertEqual(size.kf.constrainedRect(for: inSize, anchor: invalidAnchor), CGRect(x: 0, y: 80, width: 20, height: 20))
+        XCTAssertEqual(size.kf.constrainedRect(for: outX, anchor: invalidAnchor), CGRect(x: 0, y: 80, width: 100, height: 20))
+        XCTAssertEqual(size.kf.constrainedRect(for: outY, anchor: invalidAnchor), CGRect(x:0, y: 0, width: 20, height: 100))
+        XCTAssertEqual(size.kf.constrainedRect(for: outSize, anchor: invalidAnchor), CGRect(x: 0, y: 0, width: 100, height: 100))
+    }
 }

+ 8 - 2
Tests/KingfisherTests/ImageProcessorTests.swift

@@ -132,6 +132,12 @@ class ImageProcessorTests: XCTestCase {
         let p = TestCIImageProcessor(filter: .tint(Color.yellow.withAlphaComponent(0.2)))
         checkProcessor(p, with: "tint-yellow-02")
     }
+    
+    func testCroppingImageProcessor() {
+        let p = CroppingImageProcessor(size: CGSize(width: 50, height: 50), anchor: CGPoint(x: 0.5, y: 0.5))
+        XCTAssertEqual(p.identifier, "com.onevcat.Kingfisher.CroppingImageProcessor((50.0, 50.0)_(0.5, 0.5))")
+        checkProcessor(p, with: "cropping-50-50-anchor-center")
+    }
 }
 
 struct TestCIImageProcessor: CIImageProcessor {
@@ -149,7 +155,7 @@ extension ImageProcessorTests {
         
         let targetImages = filteredImageNames
             .map { $0.replacingOccurrences(of: ".", with: "-\(specifiedSuffix).") }
-            .map { Image(fileName: $0) }
+            .flatMap { Image(fileName: $0) }
         
         let resultImages = imageData(noAlpha: noAlpha).flatMap { p.process(item: .data($0), options: []) }
         
@@ -158,7 +164,7 @@ extension ImageProcessorTests {
     
     func checkImagesEqual(targetImages: [Image], resultImages: [Image], for suffix: String) {
         XCTAssertEqual(targetImages.count, resultImages.count)
-        
+
         for (i, (resultImage, targetImage)) in zip(resultImages, targetImages).enumerated() {
             guard resultImage.renderEqual(to: targetImage) else {
                 let originalName = imageNames[i]

+ 7 - 3
Tests/KingfisherTests/KingfisherTestHelper.swift

@@ -136,9 +136,9 @@ extension Image {
 }
 
 extension Image {
-    convenience init(fileName: String) {
+    convenience init?(fileName: String) {
         let data = Data(fileName: fileName)
-        self.init(data: data)!
+        self.init(data: data)
     }
     
     @discardableResult
@@ -160,6 +160,10 @@ extension Data {
     }
     
     init(named name: String, type: String) {
-        try! self.init(contentsOf: URL(fileURLWithPath: Bundle(for: ImageExtensionTests.self).path(forResource: name, ofType: type)!))
+        guard let path = Bundle(for: ImageExtensionTests.self).path(forResource: name, ofType: type) else {
+            self.init()
+            return
+        }
+        try! self.init(contentsOf: URL(fileURLWithPath: path))
     }
 }