new Image edit API

This commit is contained in:
Ivan Smolin 2017-04-27 16:48:45 +03:00
parent 020ea512f4
commit fba5894390
8 changed files with 154 additions and 89 deletions

View File

@ -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 = "<group>"; };
671FF1611EAA264B001B882C /* iOS.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = iOS.playground; sourceTree = "<group>"; };
6727419C1E65B99E0075836A /* MappableUserDefaultsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MappableUserDefaultsTests.swift; sourceTree = "<group>"; };
6727419F1E65C1E00075836A /* Post.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = "<group>"; };
@ -303,6 +305,7 @@
67A7B19E1EAF646400E5BC59 /* PaddingDrawingOperation.swift */,
67A7B1A01EAF67AE00E5BC59 /* SolidFillDrawingOperation.swift */,
67A7B1A21EAF6B4600E5BC59 /* CALayerDrawingOperation.swift */,
67186B171EB1DC0500CFAFFB /* ResizeDrawingOperation.swift */,
);
path = DrawingOperations;
sourceTree = "<group>";
@ -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 */,

View File

@ -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

View File

@ -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)
}
}

View File

@ -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<UIImage>? {
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<UIImage>? {
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<UIImage>? {
guard let image = base.cgImage else {
return base
return Support<UIImage>(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<UIImage>? {
guard let image = base.cgImage else {
return base
return Support<UIImage>(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<UIImage>? {
guard let image = base.cgImage else {
return base
return Support<UIImage>(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<UIImage>? {
guard let image = base.cgImage else {
return base
return Support<UIImage>(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<UIImage>? {
guard let image = base.cgImage else {
return base
return Support<UIImage>(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<UIImage>? {
guard let image = base.cgImage, !image.hasAlpha else {
return base
return Support<UIImage>(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<UIImage>? {
guard let image = base.cgImage else {
return base
return Support<UIImage>(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<UIImage>? {
guard let image = base.cgImage else {
return Support<UIImage>(base)
}
let flipOperation = ImageDrawingOperation(image: image,
newSize: base.size,
origin: .zero,
opaque: false,
flipY: true)
return flipOperation.imageFromNewContext(scale: base.scale)?.support
}
}

View File

@ -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) {

View File

@ -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)
}

View File

@ -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))
}

View File

@ -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)
}
}