Visual_Novel_iOS/crush/Crush/Src/Utils/Extensions/UIImageExt.swift

547 lines
22 KiB
Swift
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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<UnitAngle>) -> 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.01.0
/// - verticalRatio: 0.01.0
/// - Returns: UIImage
func stretchableImage(horizontalRatio: CGFloat, verticalRatio: CGFloat) -> UIImage {
// 0.01.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)
}
}