From 38eb97d60f96b90d75e106347ca42451946815b7 Mon Sep 17 00:00:00 2001 From: Ivan Smolin Date: Wed, 20 Dec 2017 16:09:26 +0300 Subject: [PATCH] image rotation --- LeadKit.xcodeproj/project.pbxproj | 28 +++ ...oatingPoint+DegreesRadiansConvertion.swift | 39 ++++ .../UIImage/UIImage+Extensions.swift | 195 ++++++++-------- .../UIImage/UIImage+SupportExtensions.swift | 215 ++++++++++-------- .../RotateDrawingOperation.swift | 60 +++++ 5 files changed, 354 insertions(+), 183 deletions(-) create mode 100644 Sources/Extensions/FloatingPoint/FloatingPoint+DegreesRadiansConvertion.swift create mode 100644 Sources/Structures/DrawingOperations/RotateDrawingOperation.swift diff --git a/LeadKit.xcodeproj/project.pbxproj b/LeadKit.xcodeproj/project.pbxproj index bc355587..e2d33475 100644 --- a/LeadKit.xcodeproj/project.pbxproj +++ b/LeadKit.xcodeproj/project.pbxproj @@ -274,6 +274,14 @@ 671463CF1EB34B1E00EAB194 /* TestView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 671463B71EB34B1E00EAB194 /* TestView.xib */; }; 67186B311EB248F100CFAFFB /* LeadKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 67186B281EB248F100CFAFFB /* LeadKit.framework */; }; 67186B3F1EB24A1900CFAFFB /* LeadKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 67186B201EB247A200CFAFFB /* LeadKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 672CC2A81FEA6A6A00EBFB0A /* RotateDrawingOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 672CC2A71FEA6A6A00EBFB0A /* RotateDrawingOperation.swift */; }; + 672CC2A91FEA6A7400EBFB0A /* RotateDrawingOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 672CC2A71FEA6A6A00EBFB0A /* RotateDrawingOperation.swift */; }; + 672CC2AA1FEA6A7500EBFB0A /* RotateDrawingOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 672CC2A71FEA6A6A00EBFB0A /* RotateDrawingOperation.swift */; }; + 672CC2AB1FEA6A7600EBFB0A /* RotateDrawingOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 672CC2A71FEA6A6A00EBFB0A /* RotateDrawingOperation.swift */; }; + 672CC2B31FEA72A000EBFB0A /* FloatingPoint+DegreesRadiansConvertion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 672CC2B21FEA72A000EBFB0A /* FloatingPoint+DegreesRadiansConvertion.swift */; }; + 672CC2B41FEA72A000EBFB0A /* FloatingPoint+DegreesRadiansConvertion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 672CC2B21FEA72A000EBFB0A /* FloatingPoint+DegreesRadiansConvertion.swift */; }; + 672CC2B51FEA72A000EBFB0A /* FloatingPoint+DegreesRadiansConvertion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 672CC2B21FEA72A000EBFB0A /* FloatingPoint+DegreesRadiansConvertion.swift */; }; + 672CC2B61FEA72A000EBFB0A /* FloatingPoint+DegreesRadiansConvertion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 672CC2B21FEA72A000EBFB0A /* FloatingPoint+DegreesRadiansConvertion.swift */; }; 6740D5D21FABDA46006BB7C0 /* DataSourceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6740D5D11FABDA46006BB7C0 /* DataSourceProtocol.swift */; }; 6740D5D31FABDA46006BB7C0 /* DataSourceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6740D5D11FABDA46006BB7C0 /* DataSourceProtocol.swift */; }; 6740D5D41FABDA46006BB7C0 /* DataSourceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6740D5D11FABDA46006BB7C0 /* DataSourceProtocol.swift */; }; @@ -558,6 +566,8 @@ 67186B301EB248F100CFAFFB /* LeadKit iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "LeadKit iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 67186B411EB24AA000CFAFFB /* iOS.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = iOS.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 67186C1A1EB24B7800CFAFFB /* Info-iOS.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-iOS.plist"; sourceTree = ""; }; + 672CC2A71FEA6A6A00EBFB0A /* RotateDrawingOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RotateDrawingOperation.swift; sourceTree = ""; }; + 672CC2B21FEA72A000EBFB0A /* FloatingPoint+DegreesRadiansConvertion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FloatingPoint+DegreesRadiansConvertion.swift"; sourceTree = ""; }; 6740D5D11FABDA46006BB7C0 /* DataSourceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSourceProtocol.swift; sourceTree = ""; }; 674AF55B1EC45B1600038A8F /* UIActivityIndicatorView+LoadingIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIActivityIndicatorView+LoadingIndicator.swift"; sourceTree = ""; }; 6771DFD71EE99EBA002DCDAE /* DateFormattingService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateFormattingService.swift; sourceTree = ""; }; @@ -776,6 +786,7 @@ 671461DA1EB3396E00EAB194 /* Extensions */ = { isa = PBXGroup; children = ( + 672CC2B11FEA727D00EBFB0A /* FloatingPoint */, 678B43F41FBA3D7C00D1F77D /* Views */, 67CB1BF81FAB793F0089D1B1 /* Pagination */, 67F139FF1FAB4FCC008175B4 /* Rx */, @@ -1028,6 +1039,7 @@ 6714623B1EB3396E00EAB194 /* ImageDrawingOperation.swift */, 6714623C1EB3396E00EAB194 /* PaddingDrawingOperation.swift */, 6714623D1EB3396E00EAB194 /* ResizeDrawingOperation.swift */, + 672CC2A71FEA6A6A00EBFB0A /* RotateDrawingOperation.swift */, 6714623E1EB3396E00EAB194 /* RoundDrawingOperation.swift */, 6714623F1EB3396E00EAB194 /* SolidFillDrawingOperation.swift */, 671462401EB3396E00EAB194 /* TemplateDrawingOperation.swift */, @@ -1112,6 +1124,14 @@ path = Tests; sourceTree = ""; }; + 672CC2B11FEA727D00EBFB0A /* FloatingPoint */ = { + isa = PBXGroup; + children = ( + 672CC2B21FEA72A000EBFB0A /* FloatingPoint+DegreesRadiansConvertion.swift */, + ); + path = FloatingPoint; + sourceTree = ""; + }; 674AF55A1EC45B1600038A8F /* UIActivityIndicatorView */ = { isa = PBXGroup; children = ( @@ -2184,6 +2204,7 @@ 6714639E1EB33AEB00EAB194 /* NetworkService+ActivityIndicator.swift in Sources */, A6E0DDEF1F8A6C57002CA74E /* CellSeparatorType.swift in Sources */, 6714634C1EB3396E00EAB194 /* ReuseIdentifierProtocol.swift in Sources */, + 672CC2A81FEA6A6A00EBFB0A /* RotateDrawingOperation.swift in Sources */, 671462F01EB3396E00EAB194 /* UIImage+SupportExtensions.swift in Sources */, 6771DFDE1EE99F6F002DCDAE /* DateFormattingArguments.swift in Sources */, 671462681EB3396E00EAB194 /* NetworkService.swift in Sources */, @@ -2195,6 +2216,7 @@ 67F13A431FAB6256008175B4 /* PaginationLoadingViewModel.swift in Sources */, 67A1FF8F1EBCA09B00D6C89F /* UIImage+Spinner.swift in Sources */, 67CB1BFE1FAB79EE0089D1B1 /* UICollectionView+PaginationWrappable.swift in Sources */, + 672CC2B31FEA72A000EBFB0A /* FloatingPoint+DegreesRadiansConvertion.swift in Sources */, 671462901EB3396E00EAB194 /* CGImage+Crop.swift in Sources */, 671462FC1EB3396E00EAB194 /* UIView+XibNameProtocol.swift in Sources */, 671463841EB3396E00EAB194 /* ResizeDrawingOperation.swift in Sources */, @@ -2321,6 +2343,7 @@ A676AE4D1F9810C1001F9214 /* Any+Cast.swift in Sources */, 67F13A3B1FAB60DE008175B4 /* GeneralLoadingViewModelConfiguration.swift in Sources */, EFBE57D21EC35EF20040E00A /* Array+Extensions.swift in Sources */, + 672CC2A91FEA6A7400EBFB0A /* RotateDrawingOperation.swift in Sources */, 67F139F71FAB4F22008175B4 /* TotalCountCursor.swift in Sources */, 671462821EB3396E00EAB194 /* AlamofireRequest+Extensions.swift in Sources */, 671463561EB3396E00EAB194 /* StaticViewHeightProtocol.swift in Sources */, @@ -2333,6 +2356,7 @@ 6714634E1EB3396E00EAB194 /* ReuseIdentifierProtocol.swift in Sources */, 6714626A1EB3396E00EAB194 /* NetworkService.swift in Sources */, 671463421EB3396E00EAB194 /* ModuleConfigurator.swift in Sources */, + 672CC2B51FEA72A000EBFB0A /* FloatingPoint+DegreesRadiansConvertion.swift in Sources */, 67F13A171FAB5A87008175B4 /* LoadingState.swift in Sources */, 671462921EB3396E00EAB194 /* CGImage+Crop.swift in Sources */, 671463861EB3396E00EAB194 /* ResizeDrawingOperation.swift in Sources */, @@ -2399,6 +2423,7 @@ 671462831EB3396E00EAB194 /* AlamofireRequest+Extensions.swift in Sources */, 671463571EB3396E00EAB194 /* StaticViewHeightProtocol.swift in Sources */, 67F13A371FAB60C0008175B4 /* GeneralLoadingViewModel.swift in Sources */, + 672CC2B61FEA72A000EBFB0A /* FloatingPoint+DegreesRadiansConvertion.swift in Sources */, 67F139FE1FAB4F7E008175B4 /* TotalCountCursorConfiguration.swift in Sources */, 671463631EB3396E00EAB194 /* SupportProtocol.swift in Sources */, 671462871EB3396E00EAB194 /* CGContext+Initializers.swift in Sources */, @@ -2454,6 +2479,7 @@ 6714625F1EB3396E00EAB194 /* LogFormatter.swift in Sources */, 6714630B1EB3396E00EAB194 /* UIView+Rotation.swift in Sources */, 6714626F1EB3396E00EAB194 /* XibView.swift in Sources */, + 672CC2AA1FEA6A7500EBFB0A /* RotateDrawingOperation.swift in Sources */, 6714637F1EB3396E00EAB194 /* ImageDrawingOperation.swift in Sources */, 671463371EB3396E00EAB194 /* DrawingOperation.swift in Sources */, 67F13A221FAB5AE8008175B4 /* LoadingConfiguration.swift in Sources */, @@ -2521,6 +2547,7 @@ 6771DFEB1EEA7CB8002DCDAE /* DateFormattingService+MappingTransform.swift in Sources */, 67F13A3F1FAB614D008175B4 /* PaginationLoadingViewModelConfiguration.swift in Sources */, 67A1FF951EBCA65E00D6C89F /* CABasicAnimation+Rotation.swift in Sources */, + 672CC2AB1FEA6A7600EBFB0A /* RotateDrawingOperation.swift in Sources */, 671462811EB3396E00EAB194 /* AlamofireRequest+Extensions.swift in Sources */, 671463551EB3396E00EAB194 /* StaticViewHeightProtocol.swift in Sources */, 671463611EB3396E00EAB194 /* SupportProtocol.swift in Sources */, @@ -2536,6 +2563,7 @@ 67051ADC1EBC7C36008EADC0 /* SpinnerView.swift in Sources */, 671462FD1EB3396E00EAB194 /* UIView+XibNameProtocol.swift in Sources */, 671463851EB3396E00EAB194 /* ResizeDrawingOperation.swift in Sources */, + 672CC2B41FEA72A000EBFB0A /* FloatingPoint+DegreesRadiansConvertion.swift in Sources */, 671462D11EB3396E00EAB194 /* UIScrollView+Support.swift in Sources */, 671463911EB3396E00EAB194 /* TemplateDrawingOperation.swift in Sources */, 67F13A201FAB5AE8008175B4 /* LoadingConfiguration.swift in Sources */, diff --git a/Sources/Extensions/FloatingPoint/FloatingPoint+DegreesRadiansConvertion.swift b/Sources/Extensions/FloatingPoint/FloatingPoint+DegreesRadiansConvertion.swift new file mode 100644 index 00000000..e6be27de --- /dev/null +++ b/Sources/Extensions/FloatingPoint/FloatingPoint+DegreesRadiansConvertion.swift @@ -0,0 +1,39 @@ +// +// 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. +// + +public extension FloatingPoint { + + /// Converts degrees to radians + /// + /// - Returns: radians + func degreesToRadians() -> Self { + return self * .pi / 180 + } + + /// Converts radians to degrees + /// + /// - Returns: degrees + func radiansToDegrees() -> Self { + return self * 180 / .pi + } + +} diff --git a/Sources/Extensions/UIImage/UIImage+Extensions.swift b/Sources/Extensions/UIImage/UIImage+Extensions.swift index 73204065..5adb9571 100644 --- a/Sources/Extensions/UIImage/UIImage+Extensions.swift +++ b/Sources/Extensions/UIImage/UIImage+Extensions.swift @@ -44,7 +44,7 @@ public extension UIImage { /// Creates an image from a UIView. /// /// - Parameter fromView: The source view. - /// - Returns: A new instance of UIImage or nil if something goes wrong. + /// - Returns: A new instance of UIImage. static func imageFrom(view: UIView) -> UIImage { let operation = CALayerDrawingOperation(layer: view.layer, size: view.bounds.size) @@ -54,17 +54,15 @@ public extension UIImage { /// 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. + /// - Returns: A new UIImage rendered with given color or original image if something goes wrong. func renderTemplate(withColor color: UIColor) -> UIImage { - guard let image = cgImage else { - return self + return withCGImage { image in + let operation = TemplateDrawingOperation(image: image, + imageSize: size, + color: color.cgColor) + + return operation.imageFromNewRenderer(scale: scale) } - - let operation = TemplateDrawingOperation(image: image, - imageSize: size, - color: color.cgColor) - - return operation.imageFromNewRenderer(scale: scale) } /// Creates a new image with rounded corners and border. @@ -74,49 +72,45 @@ public extension UIImage { /// - borderWidth: The size of the border. /// - color: The color of the border. /// - extendSize: Extend result image size and don't overlap source image by border. - /// - Returns: A new image with rounded corners. + /// - Returns: A new image with rounded corners or original image if something goes wrong. func roundCorners(cornerRadius: CGFloat, borderWidth: CGFloat, color: UIColor, extendSize: Bool = false) -> UIImage { - guard let image = cgImage else { - return self + return withCGImage { image in + let roundOperation = RoundDrawingOperation(image: image, + imageSize: size, + radius: cornerRadius) + + guard let roundImage = roundOperation.imageFromNewRenderer(scale: scale).cgImage else { + return self + } + + let borderOperation = BorderDrawingOperation(image: roundImage, + imageSize: size, + border: borderWidth, + color: color.cgColor, + radius: cornerRadius, + extendSize: extendSize) + + return borderOperation.imageFromNewRenderer(scale: scale) } - - let roundOperation = RoundDrawingOperation(image: image, - imageSize: size, - radius: cornerRadius) - - guard let roundImage = roundOperation.imageFromNewRenderer(scale: scale).cgImage else { - return self - } - - let borderOperation = BorderDrawingOperation(image: roundImage, - imageSize: size, - border: borderWidth, - color: color.cgColor, - radius: cornerRadius, - extendSize: extendSize) - - return borderOperation.imageFromNewRenderer(scale: scale) } /// Creates a new circle image. /// - /// - Returns: A new circled image. + /// - Returns: A new circled image or original image if something goes wrong. func roundCornersToCircle() -> UIImage { - guard let image = cgImage else { - return self + return withCGImage { image in + let radius = CGFloat(min(size.width, size.height) / 2) + + let operation = RoundDrawingOperation(image: image, + imageSize: size, + radius: radius) + + return operation.imageFromNewRenderer(scale: scale).redraw() } - - let radius = CGFloat(min(size.width, size.height) / 2) - - let operation = RoundDrawingOperation(image: image, - imageSize: size, - radius: radius) - - return operation.imageFromNewRenderer(scale: scale).redraw() } /// Creates a new circle image with a border. @@ -125,33 +119,31 @@ public extension UIImage { /// - borderWidth: The size of the border. /// - borderColor: The color of the border. /// - extendSize: Extend result image size and don't overlap source image by border (default = false). - /// - Returns: A new image with rounded corners or nil if something goes wrong. + /// - Returns: A new image with rounded corners or original image if something goes wrong. func roundCornersToCircle(borderWidth: CGFloat, borderColor: UIColor, extendSize: Bool = false) -> UIImage { - guard let image = cgImage else { - return self + return withCGImage { image in + let radius = CGFloat(min(size.width, size.height) / 2) + + let roundOperation = RoundDrawingOperation(image: image, + imageSize: size, + radius: radius) + + guard let roundImage = roundOperation.imageFromNewRenderer(scale: scale).cgImage else { + return self + } + + let borderOperation = BorderDrawingOperation(image: roundImage, + imageSize: size, + border: borderWidth, + color: borderColor.cgColor, + radius: radius, + extendSize: extendSize) + + return borderOperation.imageFromNewRenderer(scale: scale) } - - let radius = CGFloat(min(size.width, size.height) / 2) - - let roundOperation = RoundDrawingOperation(image: image, - imageSize: size, - radius: radius) - - guard let roundImage = roundOperation.imageFromNewRenderer(scale: scale).cgImage else { - return self - } - - let borderOperation = BorderDrawingOperation(image: roundImage, - imageSize: size, - border: borderWidth, - color: borderColor.cgColor, - radius: radius, - extendSize: extendSize) - - return borderOperation.imageFromNewRenderer(scale: scale) } /// Creates a resized copy of an image. @@ -161,62 +153,85 @@ public extension UIImage { /// - 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. + /// - Returns: A new image scaled to new size or original image if something goes wrong. func resize(newSize: CGSize, contentMode: ResizeMode = .scaleToFill, cropToImageBounds: Bool = false) -> UIImage { - guard let image = cgImage else { - return self + return withCGImage { image in + let operation = ResizeDrawingOperation(image: image, + imageSize: size, + preferredNewSize: newSize, + resizeMode: contentMode, + cropToImageBounds: cropToImageBounds) + + return operation.imageFromNewRenderer(scale: scale).redraw() } - - let operation = ResizeDrawingOperation(image: image, - imageSize: size, - preferredNewSize: newSize, - resizeMode: contentMode, - cropToImageBounds: cropToImageBounds) - - return operation.imageFromNewRenderer(scale: scale).redraw() } /// 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. func applyAlpha() -> UIImage { - guard let image = cgImage, !image.hasAlpha else { - return self + return withCGImage { image in + guard !image.hasAlpha else { + return self + } + + let operation = ImageDrawingOperation(image: image, + newSize: size, + opaque: false) + + return operation.imageFromNewRenderer(scale: scale).redraw() } - - let operation = ImageDrawingOperation(image: image, - newSize: size, - opaque: false) - - return operation.imageFromNewRenderer(scale: scale).redraw() } /// 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. + /// - Returns: A new padded image or original image if something goes wrong. func applyPadding(_ padding: CGFloat) -> UIImage { - guard let image = cgImage else { - return self + return withCGImage { image in + let operation = PaddingDrawingOperation(image: image, imageSize: size, padding: padding) + + return operation.imageFromNewRenderer(scale: scale).redraw() } + } - let operation = PaddingDrawingOperation(image: image, imageSize: size, padding: padding) + /// Creates a copy of the image rotated by the given amount of degrees. + /// + /// - Parameters: + /// - degrees: The number of degrees. + /// - clockwise: Should rotate image clockwise. + /// - Returns: A new rotated image or original image if something goes wrong. + func rotate(degrees: CGFloat, clockwise: Bool = true) -> UIImage { + return withCGImage { image in + let radians = degrees.degreesToRadians() - return operation.imageFromNewRenderer(scale: scale).redraw() + let operation = RotateDrawingOperation(image: image, + imageSize: size, + radians: radians, + clockwise: clockwise) + + return operation.imageFromNewRenderer(scale: scale) + } } /// Workaround to fix flipped image rendering (by Y) private func redraw() -> UIImage { + return withCGImage { image in + let operation = ImageDrawingOperation(image: image, newSize: size) + + return operation.imageFromNewRenderer(scale: scale) + } + } + + private func withCGImage(_ actionClosure: (CGImage) -> UIImage) -> UIImage { guard let image = cgImage else { return self } - let operation = ImageDrawingOperation(image: image, newSize: size) - - return operation.imageFromNewRenderer(scale: scale) + return actionClosure(image) } } diff --git a/Sources/Extensions/UIImage/UIImage+SupportExtensions.swift b/Sources/Extensions/UIImage/UIImage+SupportExtensions.swift index f7bd9762..816b0fd4 100644 --- a/Sources/Extensions/UIImage/UIImage+SupportExtensions.swift +++ b/Sources/Extensions/UIImage/UIImage+SupportExtensions.swift @@ -44,9 +44,15 @@ public extension Support where Base: UIImage { /// - Parameter fromView: The source view. /// - Returns: A new instance of UIImage or nil if something goes wrong. static func imageFrom(view: UIView) -> Support? { - let layerDrawingOperation = CALayerDrawingOperation(layer: view.layer, size: view.bounds.size) + let operation = CALayerDrawingOperation(layer: view.layer, size: view.bounds.size) - return layerDrawingOperation.imageFromNewContext(scale: UIScreen.main.scale)?.support.flipY() + guard let rotatedImage = operation.imageFromNewContext(scale: UIScreen.main.scale) else { + return nil + } + + let flipOperation = rotatedImage.cgImage?.flipYOperation(size: rotatedImage.size) + + return flipOperation?.imageFromNewContext(scale: rotatedImage.scale)?.support } /// Render current template UIImage into new image using given color. @@ -54,15 +60,19 @@ public extension Support where Base: UIImage { /// - Parameter color: Color to fill template image. /// - Returns: A new UIImage rendered with given color or nil if something goes wrong. func renderTemplate(withColor color: UIColor) -> Support? { - guard let image = base.cgImage else { - return Support(base) + return withCGImage { image in + let operation = TemplateDrawingOperation(image: image, + imageSize: base.size, + color: color.cgColor) + + guard let templateImage = operation.imageFromNewContext(scale: base.scale) else { + return nil + } + + let flipOperation = templateImage.cgImage?.flipYOperation(size: templateImage.size) + + return flipOperation?.imageFromNewContext(scale: templateImage.scale) } - - let operation = TemplateDrawingOperation(image: image, - imageSize: base.size, - color: color.cgColor) - - return operation.imageFromNewContext(scale: base.scale)?.support.flipY() } /// Creates a new image with rounded corners and border. @@ -78,43 +88,39 @@ public extension Support where Base: UIImage { color: UIColor, extendSize: Bool = false) -> Support? { - guard let image = base.cgImage else { - return Support(base) + return withCGImage { image in + let roundOperation = RoundDrawingOperation(image: image, + imageSize: base.size, + radius: cornerRadius) + + guard let roundImage = roundOperation.cgImageFromNewContext(scale: base.scale) else { + return nil + } + + let borderOperation = BorderDrawingOperation(image: roundImage, + imageSize: base.size, + border: borderWidth, + color: color.cgColor, + radius: cornerRadius, + extendSize: extendSize) + + return borderOperation.imageFromNewContext(scale: base.scale) } - - let roundOperation = RoundDrawingOperation(image: image, - imageSize: base.size, - radius: cornerRadius) - - guard let roundImage = roundOperation.cgImageFromNewContext(scale: base.scale) else { - return nil - } - - let borderOperation = BorderDrawingOperation(image: roundImage, - imageSize: base.size, - border: borderWidth, - color: color.cgColor, - radius: cornerRadius, - extendSize: extendSize) - - return borderOperation.imageFromNewContext(scale: base.scale)?.support } /// Creates a new circle image. /// /// - Returns: A new circled image or nil if something goes wrong. func roundCornersToCircle() -> Support? { - guard let image = base.cgImage else { - return Support(base) + return withCGImage { image in + let radius = CGFloat(min(base.size.width, base.size.height) / 2) + + let operation = RoundDrawingOperation(image: image, + imageSize: base.size, + radius: radius) + + return operation.imageFromNewContext(scale: base.scale) } - - let radius = CGFloat(min(base.size.width, base.size.height) / 2) - - let operation = RoundDrawingOperation(image: image, - imageSize: base.size, - radius: radius) - - return operation.imageFromNewContext(scale: base.scale)?.support } /// Creates a new circle image with a border. @@ -128,28 +134,26 @@ public extension Support where Base: UIImage { borderColor: UIColor, extendSize: Bool = false) -> Support? { - guard let image = base.cgImage else { - return Support(base) + return withCGImage { image in + let radius = CGFloat(min(base.size.width, base.size.height) / 2) + + let roundOperation = RoundDrawingOperation(image: image, + imageSize: base.size, + radius: radius) + + guard let roundImage = roundOperation.cgImageFromNewContext(scale: base.scale) else { + return nil + } + + let borderOperation = BorderDrawingOperation(image: roundImage, + imageSize: base.size, + border: borderWidth, + color: borderColor.cgColor, + radius: radius, + extendSize: extendSize) + + return borderOperation.imageFromNewContext(scale: base.scale) } - - let radius = CGFloat(min(base.size.width, base.size.height) / 2) - - let roundOperation = RoundDrawingOperation(image: image, - imageSize: base.size, - radius: radius) - - guard let roundImage = roundOperation.cgImageFromNewContext(scale: base.scale) else { - return nil - } - - let borderOperation = BorderDrawingOperation(image: roundImage, - imageSize: base.size, - border: borderWidth, - color: borderColor.cgColor, - radius: radius, - extendSize: extendSize) - - return borderOperation.imageFromNewContext(scale: base.scale)?.support } /// Creates a resized copy of an image. @@ -164,32 +168,28 @@ public extension Support where Base: UIImage { contentMode: ResizeMode = .scaleToFill, cropToImageBounds: Bool = false) -> Support? { - guard let image = base.cgImage else { - return Support(base) + return withCGImage { image in + let operation = ResizeDrawingOperation(image: image, + imageSize: base.size, + preferredNewSize: newSize, + resizeMode: contentMode, + cropToImageBounds: cropToImageBounds) + + return operation.imageFromNewContext(scale: base.scale) } - - let operation = ResizeDrawingOperation(image: image, - imageSize: base.size, - preferredNewSize: newSize, - resizeMode: contentMode, - cropToImageBounds: cropToImageBounds) - - 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. func applyAlpha() -> Support? { - guard let image = base.cgImage, !image.hasAlpha else { - return Support(base) + return withCGImage { image in + let operation = ImageDrawingOperation(image: image, + newSize: base.size, + opaque: false) + + return operation.imageFromNewContext(scale: base.scale) } - - let operation = ImageDrawingOperation(image: image, - newSize: base.size, - opaque: false) - - return operation.imageFromNewContext(scale: base.scale)?.support } /// Creates a copy of the image with border of the given size added around its edges. @@ -197,29 +197,58 @@ public extension Support where Base: UIImage { /// - Parameter padding: The padding amount. /// - Returns: A new padded image or nil if something goes wrong. func applyPadding(_ padding: CGFloat) -> Support? { - guard let image = base.cgImage else { - return Support(base) + return withCGImage { image in + let operation = PaddingDrawingOperation(image: image, + imageSize: base.size, + padding: padding) + + return operation.imageFromNewContext(scale: base.scale) } - - let operation = PaddingDrawingOperation(image: image, - imageSize: base.size, - padding: padding) - - return operation.imageFromNewContext(scale: base.scale)?.support } - private func flipY() -> Support? { + /// Creates a copy of the image rotated by the given amount of degrees. + /// + /// - Parameters: + /// - degrees: The number of degrees. + /// - clockwise: Should rotate image clockwise. + /// - Returns: A new rotated image or nil if something goes wrong. + func rotate(degrees: CGFloat, clockwise: Bool = true) -> Support? { + return withCGImage { image in + let radians = degrees.degreesToRadians() + + let operation = RotateDrawingOperation(image: image, + imageSize: base.size, + radians: radians, + clockwise: clockwise) + + guard let rotatedImage = operation.imageFromNewContext(scale: base.scale) else { + return nil + } + + let flipOperation = rotatedImage.cgImage?.flipYOperation(size: rotatedImage.size) + + return flipOperation?.imageFromNewContext(scale: rotatedImage.scale) + } + } + + private func withCGImage(_ actionClosure: (CGImage) -> UIImage?) -> 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 actionClosure(image)?.support + } - return flipOperation.imageFromNewContext(scale: base.scale)?.support +} + +private extension CGImage { + + func flipYOperation(size: CGSize) -> ImageDrawingOperation { + return ImageDrawingOperation(image: self, + newSize: size, + origin: .zero, + opaque: false, + flipY: true) } } diff --git a/Sources/Structures/DrawingOperations/RotateDrawingOperation.swift b/Sources/Structures/DrawingOperations/RotateDrawingOperation.swift new file mode 100644 index 00000000..f9e5e139 --- /dev/null +++ b/Sources/Structures/DrawingOperations/RotateDrawingOperation.swift @@ -0,0 +1,60 @@ +// +// 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 RotateDrawingOperation: DrawingOperation { + + private let image: CGImage + private let imageSize: CGSize + private let radians: CGFloat + + private let translateRect: CGRect + + public init(image: CGImage, imageSize: CGSize, radians: CGFloat, clockwise: Bool = true) { + self.image = image + self.imageSize = imageSize + self.radians = clockwise ? radians : -radians + + let transform = CGAffineTransform(rotationAngle: radians) + let imageRect = CGRect(origin: .zero, size: imageSize) + + translateRect = CGRect(origin: .zero, size: imageRect.applying(transform).size) + } + + public var contextSize: CGContextSize { + return translateRect.size.ceiledContextSize + } + + public func apply(in context: CGContext) { + context.translateBy(x: translateRect.midX, y: translateRect.midY) + context.rotate(by: radians) + + context.scaleBy(x: 1.0, y: -1.0) + + let imageLocation = CGRect(origin: CGPoint(x: -imageSize.width / 2, y: -imageSize.height / 2), + size: imageSize) + + context.draw(image, in: imageLocation) + } + +}