// // UIImage+Ext.swift // LegendTeam // // Created by dong on 2021/12/15. // import UIKit public extension UIImage { class func withColor(color: UIColor, size: CGSize) -> UIImage { UIGraphicsBeginImageContextWithOptions(size, false, 0.0) let context = UIGraphicsGetCurrentContext() context?.setFillColor(color.cgColor) context?.fill(CGRect(x: 0, y: 0, width: size.width, height: size.height)) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image! } class func withColor(color: UIColor) -> UIImage { return UIImage.withColor(color: color, size: CGSize(width: 1, height: 1)) } static func gradientHImageWithSize(size: CGSize, colors: [UIColor]) -> UIImage { let gradientLayer = CAGradientLayer() gradientLayer.frame = CGRect(origin: .zero, size: size) var cgColors : [CGColor] = [] for per in colors { cgColors.append(per.cgColor) } gradientLayer.colors = cgColors gradientLayer.startPoint = CGPoint(x: 0, y: 1) gradientLayer.endPoint = CGPoint(x: 0.5, y: 0.5) gradientLayer.locations = [0, 1] UIGraphicsBeginImageContext(gradientLayer.bounds.size) gradientLayer.render(in: UIGraphicsGetCurrentContext()!) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image! } static func gradientVImageWithSize(size: CGSize, colors: [UIColor]) -> UIImage { let gradientLayer = CAGradientLayer() gradientLayer.frame = CGRect(origin: .zero, size: size) var cgColors : [CGColor] = [] for per in colors { cgColors.append(per.cgColor) } gradientLayer.colors = cgColors gradientLayer.startPoint = CGPoint(x: 0.5, y: 0) gradientLayer.endPoint = CGPoint(x: 0.5, y: 1) gradientLayer.locations = [0, 1] UIGraphicsBeginImageContext(gradientLayer.bounds.size) gradientLayer.render(in: UIGraphicsGetCurrentContext()!) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image! } static func gradientImageWithSize(size: CGSize, colors: [CGColor], startPoint: CGPoint, endPoint: CGPoint) -> UIImage { let gradientLayer = CAGradientLayer() gradientLayer.frame = CGRect(origin: .zero, size: size) gradientLayer.colors = colors gradientLayer.startPoint = startPoint gradientLayer.endPoint = endPoint if(colors.count == 2){ gradientLayer.locations = [0, 1] }else if(colors.count == 3){ gradientLayer.locations = [0, 0.5, 1] } UIGraphicsBeginImageContextWithOptions(size, false, 0.0) defer { UIGraphicsEndImageContext() } guard let context = UIGraphicsGetCurrentContext() else { return UIImage() } gradientLayer.render(in: context) guard let image = UIGraphicsGetImageFromCurrentImageContext() else { return UIImage() } return image } func compress(toByte maxLength: Int) -> UIImage { var compression: CGFloat = 1 guard var data = jpegData(compressionQuality: compression), data.count > maxLength else { return self } dlog("压缩前:\(data.count / 1024) KB") // Compress by size var max: CGFloat = 1 var min: CGFloat = 0 for _ in 0 ..< 6 { compression = (max + min) / 2 data = jpegData(compressionQuality: compression)! if CGFloat(data.count) < CGFloat(maxLength) * 0.9 { min = compression } else if data.count > maxLength { max = compression } else { break } } dlog("压缩(by size)后1:\(data.count / 1024) KB") var resultImage = UIImage(data: data)! if data.count < maxLength { return resultImage } // Compress by size var lastDataLength: Int = 0 while data.count > maxLength, data.count != lastDataLength { lastDataLength = data.count let ratio = CGFloat(maxLength) / CGFloat(data.count) let size = CGSize(width: Int(resultImage.size.width * sqrt(ratio)), height: Int(resultImage.size.height * sqrt(ratio))) UIGraphicsBeginImageContext(size) resultImage.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) resultImage = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() data = resultImage.jpegData(compressionQuality: compression)! } dlog("压缩(by size)后2:\(data.count / 1024) KB") return resultImage } static func circleImage(withColor color: UIColor, bounds: CGRect) -> UIImage? { UIGraphicsBeginImageContextWithOptions(bounds.size, false, UIScreen.main.scale) guard let context = UIGraphicsGetCurrentContext() else { return nil } context.setStrokeColor(UIColor.clear.cgColor) context.addEllipse(in: bounds) context.clip() context.setFillColor(color.cgColor) context.fill(bounds) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image } } // properties public extension UIImage { /// Size in bytes of UIImage var bytesSize: Int { return jpegData(compressionQuality: 1)?.count ?? 0 } /// Size in kilo bytes of UIImage var kilobytesSize: Int { return (jpegData(compressionQuality: 1)?.count ?? 0) / 1024 } /// UIImage with .alwaysOriginal rendering mode. var original: UIImage { return withRenderingMode(.alwaysOriginal) } /// UIImage with .alwaysTemplate rendering mode. var template: UIImage { return withRenderingMode(.alwaysTemplate) } var pixelSize:CGSize{ return CGSize(width: size.width*scale, height: size.height*scale) } /// Jpeg: default: 0.8 func imageToBase64(_ image: UIImage, format: ImageFormat = .jpeg(0.8)) -> String? { var imageData: Data? switch format { case .png: imageData = image.pngData() case .jpeg(let quality): imageData = image.jpegData(compressionQuality: quality) } return imageData?.base64EncodedString() } enum ImageFormat { case png case jpeg(CGFloat) } } // methods public extension UIImage { /// Compressed UIImage from original UIImage. /// /// - Parameter quality: The quality of the resulting JPEG image, expressed as a value from 0.0 to 1.0. The value 0.0 represents the maximum compression (or lowest quality) while the value 1.0 represents the least compression (or best quality), (default is 0.5). /// - Returns: optional UIImage (if applicable). func compressed(quality: CGFloat = 0.5) -> UIImage? { guard let data = jpegData(compressionQuality: quality) else { return nil } return UIImage(data: data) } /// Compressed UIImage data from original UIImage. /// /// - Parameter quality: The quality of the resulting JPEG image, expressed as a value from 0.0 to 1.0. The value 0.0 represents the maximum compression (or lowest quality) while the value 1.0 represents the least compression (or best quality), (default is 0.5). /// - Returns: optional Data (if applicable). func compressedData(quality: CGFloat = 0.5) -> Data? { return jpegData(compressionQuality: quality) } /// UIImage Cropped to CGRect. /// /// - Parameter rect: CGRect to crop UIImage to. /// - Returns: cropped UIImage func cropped(to rect: CGRect) -> UIImage { guard rect.size.width <= size.width && rect.size.height <= size.height else { return self } guard let image: CGImage = cgImage?.cropping(to: rect) else { return self } return UIImage(cgImage: image) } func cropImageTop(with scale: CGFloat) -> UIImage? { // Original image guard let image = self as UIImage? else { return nil } // Get CGImage guard let imageRef = image.cgImage else { return nil } // Calculate height var height = image.size.width * scale if height > image.size.height { height = image.size.height } // Define crop area (full width, dynamic height from top) let cutArea = CGRect(x: 0, y: 0, width: image.size.width, height: height) // Crop the image guard let cgImage = imageRef.cropping(to: cutArea) else { return nil } // Create new UIImage from cropped CGImage let cutImage = UIImage(cgImage: cgImage) return cutImage } /// UIImage scaled to height with respect to aspect ratio. /// /// - Parameters: /// - toHeight: new height. /// - opaque: flag indicating whether the bitmap is opaque. /// - Returns: optional scaled UIImage (if applicable). func scaled(toHeight: CGFloat, opaque: Bool = false) -> UIImage? { let scale = toHeight / size.height let newWidth = size.width * scale UIGraphicsBeginImageContextWithOptions(CGSize(width: newWidth, height: toHeight), opaque, UIScreen.main.scale) // draw(in: CGRect(x: 0, y: 0, width: newWidth, height: toHeight)) let newImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return newImage } /// UIImage scaled to width with respect to aspect ratio. /// /// - Parameters: /// - toWidth: new width. /// - opaque: flag indicating whether the bitmap is opaque. /// - Returns: optional scaled UIImage (if applicable). func scaled(toWidth: CGFloat, opaque: Bool = false) -> UIImage? { let scale = toWidth / size.width let newHeight = size.height * scale UIGraphicsBeginImageContextWithOptions(CGSize(width: toWidth, height: newHeight), opaque, self.scale) draw(in: CGRect(x: 0, y: 0, width: toWidth, height: newHeight)) let newImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return newImage } /// Creates a copy of the receiver rotated by the given angle. /// /// // Rotate the image by 180° /// image.rotated(by: Measurement(value: 180, unit: .degrees)) /// /// - Parameter angle: The angle measurement by which to rotate the image. /// - Returns: A new image rotated by the given angle. @available(iOS 10.0, tvOS 10.0, watchOS 3.0, *) func rotated(by angle: Measurement) -> UIImage? { let radians = CGFloat(angle.converted(to: .radians).value) let destRect = CGRect(origin: .zero, size: size) .applying(CGAffineTransform(rotationAngle: radians)) let roundedDestRect = CGRect(x: destRect.origin.x.rounded(), y: destRect.origin.y.rounded(), width: destRect.width.rounded(), height: destRect.height.rounded()) UIGraphicsBeginImageContext(roundedDestRect.size) guard let contextRef = UIGraphicsGetCurrentContext() else { return nil } contextRef.translateBy(x: roundedDestRect.width / 2, y: roundedDestRect.height / 2) contextRef.rotate(by: radians) draw(in: CGRect(origin: CGPoint(x: -size.width / 2, y: -size.height / 2), size: size)) let newImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return newImage } /// Creates a copy of the receiver rotated by the given angle (in radians). /// /// // Rotate the image by 180° /// image.rotated(by: .pi) /// /// - Parameter radians: The angle, in radians, by which to rotate the image. /// - Returns: A new image rotated by the given angle. func rotated(by radians: CGFloat) -> UIImage? { let destRect = CGRect(origin: .zero, size: size) .applying(CGAffineTransform(rotationAngle: radians)) let roundedDestRect = CGRect(x: destRect.origin.x.rounded(), y: destRect.origin.y.rounded(), width: destRect.width.rounded(), height: destRect.height.rounded()) UIGraphicsBeginImageContext(roundedDestRect.size) guard let contextRef = UIGraphicsGetCurrentContext() else { return nil } contextRef.translateBy(x: roundedDestRect.width / 2, y: roundedDestRect.height / 2) contextRef.rotate(by: radians) draw(in: CGRect(origin: CGPoint(x: -size.width / 2, y: -size.height / 2), size: size)) let newImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return newImage } /// UIImage filled with color /// /// - Parameter color: color to fill image with. /// - Returns: UIImage filled with given color. func filled(withColor color: UIColor) -> UIImage { #if !os(watchOS) if #available(iOS 10, tvOS 10, *) { let format = UIGraphicsImageRendererFormat() format.scale = scale let renderer = UIGraphicsImageRenderer(size: size, format: format) return renderer.image { context in color.setFill() context.fill(CGRect(origin: .zero, size: size)) } } #endif UIGraphicsBeginImageContextWithOptions(size, false, scale) color.setFill() guard let context = UIGraphicsGetCurrentContext() else { return self } context.translateBy(x: 0, y: size.height) context.scaleBy(x: 1.0, y: -1.0) context.setBlendMode(CGBlendMode.normal) let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) guard let mask = cgImage else { return self } context.clip(to: rect, mask: mask) context.fill(rect) let newImage = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() return newImage } /// UIImage tinted with color /// /// - Parameters: /// - color: color to tint image with. /// - blendMode: how to blend the tint /// - Returns: UIImage tinted with given color. func tint(_ color: UIColor, blendMode: CGBlendMode, alpha: CGFloat = 1.0) -> UIImage { let drawRect = CGRect(origin: .zero, size: size) #if !os(watchOS) if #available(iOS 10.0, tvOS 10.0, *) { let format = UIGraphicsImageRendererFormat() format.scale = scale return UIGraphicsImageRenderer(size: size, format: format).image { context in color.setFill() context.fill(drawRect) draw(in: drawRect, blendMode: blendMode, alpha: alpha) } } #endif UIGraphicsBeginImageContextWithOptions(size, false, scale) defer { UIGraphicsEndImageContext() } let context = UIGraphicsGetCurrentContext() color.setFill() context?.fill(drawRect) draw(in: drawRect, blendMode: blendMode, alpha: alpha) return UIGraphicsGetImageFromCurrentImageContext()! } /// UImage with background color /// /// - Parameters: /// - backgroundColor: Color to use as background color /// - Returns: UIImage with a background color that is visible where alpha < 1 func withBackgroundColor(_ backgroundColor: UIColor) -> UIImage { #if !os(watchOS) if #available(iOS 10.0, tvOS 10.0, *) { let format = UIGraphicsImageRendererFormat() format.scale = scale return UIGraphicsImageRenderer(size: size, format: format).image { context in backgroundColor.setFill() context.fill(context.format.bounds) draw(at: .zero) } } #endif UIGraphicsBeginImageContextWithOptions(size, false, scale) defer { UIGraphicsEndImageContext() } backgroundColor.setFill() UIRectFill(CGRect(origin: .zero, size: size)) draw(at: .zero) return UIGraphicsGetImageFromCurrentImageContext()! } /// UIImage with rounded corners /// /// - Parameters: /// - radius: corner radius (optional), resulting image will be round if unspecified /// - Returns: UIImage with all corners rounded func withRoundedCorners(radius: CGFloat? = nil) -> UIImage? { let maxRadius = min(size.width, size.height) / 2 let cornerRadius: CGFloat if let radius = radius, radius > 0 && radius <= maxRadius { cornerRadius = radius } else { cornerRadius = maxRadius } UIGraphicsBeginImageContextWithOptions(size, false, scale) let rect = CGRect(origin: .zero, size: size) UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).addClip() draw(in: rect) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image } /// Base 64 encoded PNG data of the image. /// /// - returns: Base 64 encoded PNG data of the image as a String. func pngBase64String() -> String? { return pngData()?.base64EncodedString() } /// Base 64 encoded JPEG data of the image. /// /// - parameter compressionQuality: The quality of the resulting JPEG image, expressed as a value from 0.0 to 1.0. The value 0.0 represents the maximum compression (or lowest quality) while the value 1.0 represents the least compression (or best quality). /// - returns: Base 64 encoded JPEG data of the image as a String. func jpegBase64String(compressionQuality: CGFloat) -> String? { return jpegData(compressionQuality: compressionQuality)?.base64EncodedString() } /// 创建一个可拉伸的UIImage,指定水平和垂直拉伸位置 /// - Parameters: /// - horizontalRatio: 水平拉伸点比例(0.0到1.0),表示拉伸点的相对位置 /// - verticalRatio: 垂直拉伸点比例(0.0到1.0),表示拉伸点的相对位置 /// - Returns: 可拉伸的UIImage func stretchableImage(horizontalRatio: CGFloat, verticalRatio: CGFloat) -> UIImage { // 确保比例在0.0到1.0之间 let clampedHorizontalRatio = max(0.0, min(1.0, horizontalRatio)) let clampedVerticalRatio = max(0.0, min(1.0, verticalRatio)) // 计算拉伸区域 let width = self.size.width let height = self.size.height let leftCap = floor(width * clampedHorizontalRatio) let topCap = floor(height * clampedVerticalRatio) // 创建可拉伸图片 return self.resizableImage(withCapInsets: UIEdgeInsets( top: topCap, left: leftCap, bottom: height - topCap - 1, right: width - leftCap - 1 ), resizingMode: .stretch) } } public extension UIImage { /// Create UIImage from color and size. /// /// - Parameters: /// - color: image fill color. /// - size: image size. convenience init(color: UIColor, size: CGSize) { UIGraphicsBeginImageContextWithOptions(size, false, 1) defer { UIGraphicsEndImageContext() } color.setFill() UIRectFill(CGRect(origin: .zero, size: size)) guard let aCgImage = UIGraphicsGetImageFromCurrentImageContext()?.cgImage else { self.init() return } self.init(cgImage: aCgImage) } /// Create a new image from a base 64 string. /// /// - Parameters: /// - base64String: a base-64 `String`, representing the image /// - scale: The scale factor to assume when interpreting the image data created from the base-64 string. Applying a scale factor of 1.0 results in an image whose size matches the pixel-based dimensions of the image. Applying a different scale factor changes the size of the image as reported by the `size` property. convenience init?(base64String: String, scale: CGFloat = 1.0) { guard let data = Data(base64Encoded: base64String) else { return nil } self.init(data: data, scale: scale) } /// Create a new image from a URL /// /// - Important: /// Use this method to convert data:// URLs to UIImage objects. /// Don't use this synchronous initializer to request network-based URLs. For network-based URLs, this method can block the current thread for tens of seconds on a slow network, resulting in a poor user experience, and in iOS, may cause your app to be terminated. /// Instead, for non-file URLs, consider using this in an asynchronous way, using `dataTask(with:completionHandler:)` method of the URLSession class or a library such as `AlamofireImage`, `Kingfisher`, `SDWebImage`, or others to perform asynchronous network image loading. /// - Parameters: /// - url: a `URL`, representing the image location /// - scale: The scale factor to assume when interpreting the image data created from the URL. Applying a scale factor of 1.0 results in an image whose size matches the pixel-based dimensions of the image. Applying a different scale factor changes the size of the image as reported by the `size` property. convenience init?(url: URL, scale: CGFloat = 1.0) throws { let data = try Data(contentsOf: url) self.init(data: data, scale: scale) } }