From fba5894390df826be5f3ca56843f059e4f1c4bc5 Mon Sep 17 00:00:00 2001 From: Ivan Smolin Date: Thu, 27 Apr 2017 16:48:45 +0300 Subject: [PATCH] new Image edit API --- LeadKit/LeadKit.xcodeproj/project.pbxproj | 4 + .../Extensions/CGSize/CGSize+Resize.swift | 8 +- .../UIImage/UIImage+Extensions.swift | 58 +++++------ .../UIImage/UIImage+SupportExtensions.swift | 99 ++++++++++--------- .../BorderDrawingOperation.swift | 2 +- .../CALayerDrawingOperation.swift | 3 - .../ImageDrawingOperation.swift | 14 ++- .../ResizeDrawingOperation.swift | 55 +++++++++++ 8 files changed, 154 insertions(+), 89 deletions(-) create mode 100644 LeadKit/LeadKit/Structures/DrawingOperations/ResizeDrawingOperation.swift diff --git a/LeadKit/LeadKit.xcodeproj/project.pbxproj b/LeadKit/LeadKit.xcodeproj/project.pbxproj index 087cd04c..fb5db045 100644 --- a/LeadKit/LeadKit.xcodeproj/project.pbxproj +++ b/LeadKit/LeadKit.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 67186B181EB1DC0500CFAFFB /* ResizeDrawingOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67186B171EB1DC0500CFAFFB /* ResizeDrawingOperation.swift */; }; 6727419D1E65B99E0075836A /* MappableUserDefaultsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6727419C1E65B99E0075836A /* MappableUserDefaultsTests.swift */; }; 672741A01E65C1E00075836A /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6727419F1E65C1E00075836A /* Post.swift */; }; 674743941E929A5A00B47671 /* PaginationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 674743931E929A5A00B47671 /* PaginationViewModelTests.swift */; }; @@ -117,6 +118,7 @@ /* Begin PBXFileReference section */ 12F36034A5278991B658B53E /* Pods_LeadKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LeadKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 67186B171EB1DC0500CFAFFB /* ResizeDrawingOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResizeDrawingOperation.swift; sourceTree = ""; }; 671FF1611EAA264B001B882C /* iOS.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = iOS.playground; sourceTree = ""; }; 6727419C1E65B99E0075836A /* MappableUserDefaultsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MappableUserDefaultsTests.swift; sourceTree = ""; }; 6727419F1E65C1E00075836A /* Post.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = ""; }; @@ -303,6 +305,7 @@ 67A7B19E1EAF646400E5BC59 /* PaddingDrawingOperation.swift */, 67A7B1A01EAF67AE00E5BC59 /* SolidFillDrawingOperation.swift */, 67A7B1A21EAF6B4600E5BC59 /* CALayerDrawingOperation.swift */, + 67186B171EB1DC0500CFAFFB /* ResizeDrawingOperation.swift */, ); path = DrawingOperations; sourceTree = ""; @@ -1015,6 +1018,7 @@ 95B39A861D9D51250057BD54 /* String+Localization.swift in Sources */, 78C36F7E1D801E3E00E7EBEA /* Double+Rounding.swift in Sources */, 67DC65061E979B70002F2FFF /* UIView+LoadingIndicator.swift in Sources */, + 67186B181EB1DC0500CFAFFB /* ResizeDrawingOperation.swift in Sources */, 787609221E1403830093CE36 /* Observable+DeferredJust.swift in Sources */, 67B305841E8A92E8008169CA /* XibView.swift in Sources */, 78C54AFD1E432EEF0051EFBA /* UIViewController+TopVisibleViewController.swift in Sources */, diff --git a/LeadKit/LeadKit/Extensions/CGSize/CGSize+Resize.swift b/LeadKit/LeadKit/Extensions/CGSize/CGSize+Resize.swift index 2c5f98cd..84f7909c 100644 --- a/LeadKit/LeadKit/Extensions/CGSize/CGSize+Resize.swift +++ b/LeadKit/LeadKit/Extensions/CGSize/CGSize+Resize.swift @@ -31,8 +31,8 @@ public extension CGSize { /// - resizeMode: Resize mode to use. /// - Returns: A new CGRect instance matching request parameters. public func resizeRect(forNewSize newSize: CGSize, resizeMode: ResizeMode) -> CGRect { - let horizontalRatio = newSize.width / CGFloat(width) - let verticalRatio = newSize.height / CGFloat(height) + let horizontalRatio = newSize.width / width + let verticalRatio = newSize.height / height let ratio: CGFloat @@ -45,8 +45,8 @@ public extension CGSize { ratio = min(horizontalRatio, verticalRatio) } - let newWidth = resizeMode == .scaleToFill ? newSize.width : CGFloat(width) * ratio - let newHeight = resizeMode == .scaleToFill ? newSize.height : CGFloat(height) * ratio + let newWidth = resizeMode == .scaleToFill ? newSize.width : width * ratio + let newHeight = resizeMode == .scaleToFill ? newSize.height : height * ratio let originX: CGFloat let originY: CGFloat diff --git a/LeadKit/LeadKit/Extensions/UIImage/UIImage+Extensions.swift b/LeadKit/LeadKit/Extensions/UIImage/UIImage+Extensions.swift index 4913c053..5de1f826 100644 --- a/LeadKit/LeadKit/Extensions/UIImage/UIImage+Extensions.swift +++ b/LeadKit/LeadKit/Extensions/UIImage/UIImage+Extensions.swift @@ -116,7 +116,7 @@ public extension UIImage { imageSize: size, radius: radius) - return operation.imageFromNewRenderer(scale: scale) + return operation.imageFromNewRenderer(scale: scale).redraw() } /// Creates a new circle image with a border. @@ -159,37 +159,24 @@ public extension UIImage { /// - Parameters: /// - newSize: The new size of the image. /// - contentMode: The way to handle the content in the new size. + /// - cropToImageBounds: Should output image size match resized image size. + /// Note: If passed true with ResizeMode.scaleAspectFit content mode it will give the original image. /// - Returns: A new image scaled to new size. - public func resize(newSize: CGSize, contentMode: ResizeMode = .scaleToFill) -> UIImage { + public func resize(newSize: CGSize, + contentMode: ResizeMode = .scaleToFill, + cropToImageBounds: Bool = false) -> UIImage { + guard let image = cgImage else { return self } - let resizedRect = size.resizeRect(forNewSize: newSize, resizeMode: contentMode) + let operation = ResizeDrawingOperation(image: image, + imageSize: size, + preferredNewSize: newSize, + resizeMode: contentMode, + cropToImageBounds: cropToImageBounds) - let operation = ImageDrawingOperation(image: image, - newSize: resizedRect.size, - origin: resizedRect.origin) - - return operation.imageFromNewRenderer(scale: scale) - } - - /// Creates a resized copy of image using scaleAspectFit strategy. - /// - /// - Parameter preferredNewSize: The preferred new size of image. - /// - Returns: A new image which size may be less then or equals to given preferredSize. - public func resizeAspectFit(preferredNewSize: CGSize) -> UIImage { - guard let image = cgImage else { - return self - } - - let resizedRect = size.resizeRect(forNewSize: preferredNewSize, resizeMode: .scaleAspectFit) - - let operation = ImageDrawingOperation(image: image, - newSize: resizedRect.size, - origin: .zero) - - return operation.imageFromNewRenderer(scale: scale) + return operation.imageFromNewRenderer(scale: scale).redraw() } /// Adds an alpha channel if UIImage doesn't already have one. @@ -204,7 +191,7 @@ public extension UIImage { newSize: size, opaque: false) - return operation.imageFromNewRenderer(scale: scale) + return operation.imageFromNewRenderer(scale: scale).redraw() } /// Creates a copy of the image with border of the given size added around its edges. @@ -218,6 +205,17 @@ public extension UIImage { let operation = PaddingDrawingOperation(image: image, imageSize: size, padding: padding) + return operation.imageFromNewRenderer(scale: scale).redraw() + } + + /// Workaround to fix flipped image rendering (by Y) + private func redraw() -> UIImage { + guard let image = cgImage else { + return self + } + + let operation = ImageDrawingOperation(image: image, newSize: size) + return operation.imageFromNewRenderer(scale: scale) } @@ -233,10 +231,8 @@ private extension DrawingOperation { format.opaque = opaque format.scale = scale - let renderer = UIGraphicsImageRenderer(size: size, format: format) - - return renderer.image { context in - self.apply(in: context.cgContext) + return UIGraphicsImageRenderer(size: size, format: format).image { + self.apply(in: $0.cgContext) } } diff --git a/LeadKit/LeadKit/Extensions/UIImage/UIImage+SupportExtensions.swift b/LeadKit/LeadKit/Extensions/UIImage/UIImage+SupportExtensions.swift index a3dd984d..3db45324 100644 --- a/LeadKit/LeadKit/Extensions/UIImage/UIImage+SupportExtensions.swift +++ b/LeadKit/LeadKit/Extensions/UIImage/UIImage+SupportExtensions.swift @@ -30,39 +30,39 @@ public extension Support where Base: UIImage { /// - color: The color to fill /// - size: The size of an new image. /// - Returns: A new instanse of UIImage with given size and color or nil if something goes wrong. - public static func imageWith(color: UIColor, size: CGSize) -> UIImage? { + public static func imageWith(color: UIColor, size: CGSize) -> Support? { let width = Int(ceil(size.width)) let height = Int(ceil(size.height)) let operation = SolidFillDrawingOperation(color: color.cgColor, width: width, height: height) - return operation.imageFromNewContext(scale: UIScreen.main.scale) + return operation.imageFromNewContext(scale: UIScreen.main.scale)?.support } /// Creates an image from a UIView. /// /// - Parameter fromView: The source view. /// - Returns: A new instance of UIImage or nil if something goes wrong. - public static func imageFrom(view: UIView) -> UIImage? { - let operation = CALayerDrawingOperation(layer: view.layer, size: view.bounds.size) + public static func imageFrom(view: UIView) -> Support? { + let layerDrawingOperation = CALayerDrawingOperation(layer: view.layer, size: view.bounds.size) - return operation.imageFromNewContext(scale: UIScreen.main.scale) + return layerDrawingOperation.imageFromNewContext(scale: UIScreen.main.scale)?.support.flipY() } /// Render current template UIImage into new image using given color. /// /// - Parameter color: Color to fill template image. /// - Returns: A new UIImage rendered with given color or nil if something goes wrong. - public func renderTemplate(withColor color: UIColor) -> UIImage? { + public func renderTemplate(withColor color: UIColor) -> Support? { guard let image = base.cgImage else { - return base + return Support(base) } let operation = TemplateDrawingOperation(image: image, imageSize: base.size, color: color.cgColor) - return operation.imageFromNewContext(scale: base.scale) + return operation.imageFromNewContext(scale: base.scale)?.support } /// Creates a new image with rounded corners and border. @@ -76,10 +76,10 @@ public extension Support where Base: UIImage { public func roundCorners(cornerRadius: CGFloat, borderWidth: CGFloat, color: UIColor, - extendSize: Bool = false) -> UIImage? { + extendSize: Bool = false) -> Support? { guard let image = base.cgImage else { - return base + return Support(base) } let roundOperation = RoundDrawingOperation(image: image, @@ -97,15 +97,15 @@ public extension Support where Base: UIImage { radius: cornerRadius, extendSize: extendSize) - return borderOperation.imageFromNewContext(scale: base.scale) + return borderOperation.imageFromNewContext(scale: base.scale)?.support } /// Creates a new circle image. /// /// - Returns: A new circled image or nil if something goes wrong. - public func roundCornersToCircle() -> UIImage? { + public func roundCornersToCircle() -> Support? { guard let image = base.cgImage else { - return base + return Support(base) } let radius = CGFloat(min(base.size.width, base.size.height) / 2) @@ -114,7 +114,7 @@ public extension Support where Base: UIImage { imageSize: base.size, radius: radius) - return operation.imageFromNewContext(scale: base.scale) + return operation.imageFromNewContext(scale: base.scale)?.support } /// Creates a new circle image with a border. @@ -126,10 +126,10 @@ public extension Support where Base: UIImage { /// - Returns: A new image with rounded corners or nil if something goes wrong. public func roundCornersToCircle(borderWidth: CGFloat, borderColor: UIColor, - extendSize: Bool = false) -> UIImage? { + extendSize: Bool = false) -> Support? { guard let image = base.cgImage else { - return base + return Support(base) } let radius = CGFloat(min(base.size.width, base.size.height) / 2) @@ -149,7 +149,7 @@ public extension Support where Base: UIImage { radius: radius, extendSize: extendSize) - return borderOperation.imageFromNewContext(scale: base.scale) + return borderOperation.imageFromNewContext(scale: base.scale)?.support } /// Creates a resized copy of an image. @@ -157,68 +157,69 @@ public extension Support where Base: UIImage { /// - Parameters: /// - newSize: The new size of the image. /// - contentMode: The way to handle the content in the new size. + /// - cropToImageBounds: Should output image size match resized image size. + /// Note: If passed true with ResizeMode.scaleAspectFit content mode it will give the original image. /// - Returns: A new image scaled to new size. - public func resize(newSize: CGSize, contentMode: ResizeMode = .scaleToFill) -> UIImage? { + public func resize(newSize: CGSize, + contentMode: ResizeMode = .scaleToFill, + cropToImageBounds: Bool = false) -> Support? { + guard let image = base.cgImage else { - return base + return Support(base) } - let resizedRect = base.size.resizeRect(forNewSize: newSize, resizeMode: contentMode) + let operation = ResizeDrawingOperation(image: image, + imageSize: base.size, + preferredNewSize: newSize, + resizeMode: contentMode, + cropToImageBounds: cropToImageBounds) - let operation = ImageDrawingOperation(image: image, - newSize: resizedRect.size, - origin: resizedRect.origin) - - return operation.imageFromNewContext(scale: base.scale) - } - - /// Creates a resized copy of image using scaleAspectFit strategy. - /// - /// - Parameter preferredNewSize: The preferred new size of image. - /// - Returns: A new image which size may be less then or equals to given preferredSize. - public func resizeAspectFit(preferredNewSize: CGSize) -> UIImage? { - guard let image = base.cgImage else { - return base - } - - let resizedRect = base.size.resizeRect(forNewSize: preferredNewSize, resizeMode: .scaleAspectFit) - - let operation = ImageDrawingOperation(image: image, - newSize: resizedRect.size, - origin: .zero) - - return operation.imageFromNewContext(scale: base.scale) + return operation.imageFromNewContext(scale: base.scale)?.support } /// Adds an alpha channel if UIImage doesn't already have one. /// /// - Returns: A copy of the given image, adding an alpha channel if it doesn't already have one. - public func applyAlpha() -> UIImage? { + public func applyAlpha() -> Support? { guard let image = base.cgImage, !image.hasAlpha else { - return base + return Support(base) } let operation = ImageDrawingOperation(image: image, newSize: base.size, opaque: false) - return operation.imageFromNewContext(scale: base.scale) + return operation.imageFromNewContext(scale: base.scale)?.support } /// Creates a copy of the image with border of the given size added around its edges. /// /// - Parameter padding: The padding amount. /// - Returns: A new padded image or nil if something goes wrong. - public func applyPadding(_ padding: CGFloat) -> UIImage? { + public func applyPadding(_ padding: CGFloat) -> Support? { guard let image = base.cgImage else { - return base + return Support(base) } let operation = PaddingDrawingOperation(image: image, imageSize: base.size, padding: padding) - return operation.imageFromNewContext(scale: base.scale) + return operation.imageFromNewContext(scale: base.scale)?.support + } + + private func flipY() -> Support? { + guard let image = base.cgImage else { + return Support(base) + } + + let flipOperation = ImageDrawingOperation(image: image, + newSize: base.size, + origin: .zero, + opaque: false, + flipY: true) + + return flipOperation.imageFromNewContext(scale: base.scale)?.support } } diff --git a/LeadKit/LeadKit/Structures/DrawingOperations/BorderDrawingOperation.swift b/LeadKit/LeadKit/Structures/DrawingOperations/BorderDrawingOperation.swift index 332d13fd..885449c4 100644 --- a/LeadKit/LeadKit/Structures/DrawingOperations/BorderDrawingOperation.swift +++ b/LeadKit/LeadKit/Structures/DrawingOperations/BorderDrawingOperation.swift @@ -52,7 +52,7 @@ struct BorderDrawingOperation: DrawingOperation { let width = imageSize.width + offset * 2 let height = imageSize.height + offset * 2 - return (width: Int(ceil(width)), height: Int(ceil(height))) + return CGSize(width: width, height: height).ceiledContextSize } public func apply(in context: CGContext) { diff --git a/LeadKit/LeadKit/Structures/DrawingOperations/CALayerDrawingOperation.swift b/LeadKit/LeadKit/Structures/DrawingOperations/CALayerDrawingOperation.swift index 2d8e8e2c..d6abf448 100644 --- a/LeadKit/LeadKit/Structures/DrawingOperations/CALayerDrawingOperation.swift +++ b/LeadKit/LeadKit/Structures/DrawingOperations/CALayerDrawingOperation.swift @@ -37,9 +37,6 @@ struct CALayerDrawingOperation: DrawingOperation { } public func apply(in context: CGContext) { - context.translateBy(x: 0, y: size.height) - context.scaleBy(x: 1.0, y: -1.0) - layer.render(in: context) } diff --git a/LeadKit/LeadKit/Structures/DrawingOperations/ImageDrawingOperation.swift b/LeadKit/LeadKit/Structures/DrawingOperations/ImageDrawingOperation.swift index aa0f654d..9831c050 100644 --- a/LeadKit/LeadKit/Structures/DrawingOperations/ImageDrawingOperation.swift +++ b/LeadKit/LeadKit/Structures/DrawingOperations/ImageDrawingOperation.swift @@ -28,12 +28,19 @@ struct ImageDrawingOperation: DrawingOperation { private let newSize: CGSize private let origin: CGPoint public let opaque: Bool + private let flipY: Bool + + public init(image: CGImage, + newSize: CGSize, + origin: CGPoint = .zero, + opaque: Bool = false, + flipY: Bool = false) { - public init(image: CGImage, newSize: CGSize, origin: CGPoint = .zero, opaque: Bool = true) { self.image = image self.newSize = newSize self.origin = origin self.opaque = opaque + self.flipY = flipY } public var contextSize: CGContextSize { @@ -41,6 +48,11 @@ struct ImageDrawingOperation: DrawingOperation { } public func apply(in context: CGContext) { + if flipY { + context.translateBy(x: 0, y: newSize.height) + context.scaleBy(x: 1.0, y: -1.0) + } + context.interpolationQuality = .high context.draw(image, in: CGRect(origin: origin, size: newSize)) } diff --git a/LeadKit/LeadKit/Structures/DrawingOperations/ResizeDrawingOperation.swift b/LeadKit/LeadKit/Structures/DrawingOperations/ResizeDrawingOperation.swift new file mode 100644 index 00000000..1f1dd6f7 --- /dev/null +++ b/LeadKit/LeadKit/Structures/DrawingOperations/ResizeDrawingOperation.swift @@ -0,0 +1,55 @@ +// +// Copyright (c) 2017 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import CoreGraphics + +struct ResizeDrawingOperation: DrawingOperation { + + private let image: CGImage + private let drawRect: CGRect + public let contextSize: CGContextSize + + public init(image: CGImage, + imageSize: CGSize, + preferredNewSize: CGSize, + resizeMode: ResizeMode, + cropToImageBounds: Bool = false) { + + self.image = image + + let resizedRect = imageSize.resizeRect(forNewSize: preferredNewSize, resizeMode: resizeMode) + + if cropToImageBounds { + drawRect = CGRect(origin: .zero, size: resizedRect.size) + contextSize = resizedRect.size.ceiledContextSize + } else { + drawRect = resizedRect + contextSize = preferredNewSize.ceiledContextSize + } + } + + public func apply(in context: CGContext) { + context.interpolationQuality = .high + context.draw(image, in: drawRect) + } + +}