// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. import UIKit.UIImage public extension UIImage { func normalizedImage() -> UIImage { guard imageOrientation != .up else { return self } // The actual resize: draw the image on a new context, applying a transform matrix let bounds: CGRect = CGRect(x: 0, y: 0, width: size.width, height: size.height) let format = UIGraphicsImageRendererFormat() format.scale = self.scale format.opaque = false // Note: We use the UIImage.draw function here instead of using the CGContext because UIImage // automatically deals with orientations so we don't have to return UIGraphicsImageRenderer(bounds: bounds, format: format).image { _ in self.draw(in: bounds) } } func resized(to targetSize: CGSize) -> UIImage? { guard let imgRef: CGImage = self.cgImage else { return nil } // the below values are regardless of orientation : for UIImages from Camera, width>height (landscape) // // Note: Not equivalent to self.size (which is dependant on the imageOrientation)! let srcSize: CGSize = CGSize(width: imgRef.width, height: imgRef.height) var dstSize: CGSize = targetSize // Don't resize if we already meet the required destination size guard dstSize != srcSize else { return self } let scaleRatio: CGFloat = (dstSize.width / srcSize.width) let orient: UIImage.Orientation = self.imageOrientation var transform: CGAffineTransform = .identity switch orient { case .up: break // EXIF = 1 case .upMirrored: // EXIF = 2 transform = CGAffineTransform(translationX: srcSize.width, y: 0) .scaledBy(x: -1, y: 1) case .down: // EXIF = 3 transform = CGAffineTransform(translationX: srcSize.width, y: srcSize.height) .rotated(by: CGFloat.pi) case .downMirrored: // EXIF = 4 transform = CGAffineTransform(translationX: 0, y: srcSize.height) .scaledBy(x: 1, y: -1) case .leftMirrored: // EXIF = 5 dstSize = CGSize(width: dstSize.height, height: dstSize.width) transform = CGAffineTransform(translationX: srcSize.height, y: srcSize.width) .scaledBy(x: -1, y: 1) .rotated(by: (3 * (CGFloat.pi / 2))) case .left: // EXIF = 6 dstSize = CGSize(width: dstSize.height, height: dstSize.width) transform = CGAffineTransform(translationX: 0, y: srcSize.width) .scaledBy(x: -1, y: 1) .rotated(by: (3 * (CGFloat.pi / 2))) case .rightMirrored: // EXIF = 7 dstSize = CGSize(width: dstSize.height, height: dstSize.width) transform = CGAffineTransform(scaleX: -1, y: 1) .rotated(by: (CGFloat.pi / 2)) case .right: // EXIF = 8 dstSize = CGSize(width: dstSize.height, height: dstSize.width) transform = CGAffineTransform(translationX: srcSize.height, y: 0) .rotated(by: (CGFloat.pi / 2)) @unknown default: return nil } // The actual resize: draw the image on a new context, applying a transform matrix let bounds: CGRect = CGRect(x: 0, y: 0, width: dstSize.width, height: dstSize.height) let format = UIGraphicsImageRendererFormat() format.scale = self.scale format.opaque = false let renderer: UIGraphicsImageRenderer = UIGraphicsImageRenderer(bounds: bounds, format: format) return renderer.image { rendererContext in rendererContext.cgContext.interpolationQuality = .high switch orient { case .right, .left: rendererContext.cgContext.scaleBy(x: -scaleRatio, y: scaleRatio) rendererContext.cgContext.translateBy(x: -srcSize.height, y: 0) default: rendererContext.cgContext.scaleBy(x: scaleRatio, y: -scaleRatio) rendererContext.cgContext.translateBy(x: 0, y: -srcSize.height) } rendererContext.cgContext.concatenate(transform) // we use srcSize (and not dstSize) as the size to specify is in user space (and we use the CTM to apply a // scaleRatio) rendererContext.cgContext.draw( imgRef, in: CGRect(x: 0, y: 0, width: srcSize.width, height: srcSize.height), byTiling: false ) } } func resized(toFillPixelSize dstSize: CGSize) -> UIImage { let normalized: UIImage = self.normalizedImage() guard let normalizedRef: CGImage = normalized.cgImage, let imgRef: CGImage = self.cgImage else { return self } // Get the size in pixels, not points let srcSize: CGSize = CGSize(width: normalizedRef.width, height: normalizedRef.height) let widthRatio: CGFloat = (srcSize.width / srcSize.height) let heightRatio: CGFloat = (srcSize.height / srcSize.height) let drawRect: CGRect = { guard widthRatio <= heightRatio else { let targetWidth: CGFloat = (dstSize.height * srcSize.width / srcSize.height) return CGRect( x: (targetWidth - dstSize.width) * -0.5, y: 0, width: targetWidth, height: dstSize.height ) } let targetHeight: CGFloat = (dstSize.width * srcSize.height / srcSize.width) return CGRect( x: 0, y: (targetHeight - dstSize.height) * -0.5, width: dstSize.width, height: targetHeight ) }() let bounds: CGRect = CGRect(x: 0, y: 0, width: dstSize.width, height: dstSize.height) let format = UIGraphicsImageRendererFormat() format.scale = 1 // We are specifying a specific pixel size rather than a point size format.opaque = false let renderer: UIGraphicsImageRenderer = UIGraphicsImageRenderer(bounds: bounds, format: format) return renderer.image { rendererContext in rendererContext.cgContext.interpolationQuality = .high // we use srcSize (and not dstSize) as the size to specify is in user space (and we use the CTM to apply a // scaleRatio) rendererContext.cgContext.draw(imgRef, in: drawRect, byTiling: false) } } func resized(maxDimensionPoints: CGFloat) -> UIImage? { let originalSize: CGSize = self.size let maxOriginalDimensionPoints: CGFloat = max(originalSize.width, originalSize.height) guard originalSize.width > 0 && originalSize.height > 0 else { return nil } // Don't bother scaling an image that is already smaller than the max dimension. guard maxOriginalDimensionPoints > maxDimensionPoints else { return self } let thumbnailSize: CGSize = { guard originalSize.width <= originalSize.height else { return CGSize( width: maxDimensionPoints, height: round(maxDimensionPoints * originalSize.height / originalSize.width) ) } return CGSize( width: round(maxDimensionPoints * originalSize.width / originalSize.height), height: maxDimensionPoints ) }() guard thumbnailSize.width > 0 && thumbnailSize.height > 0 else { return nil } return resized(to: thumbnailSize) } }