Swift 中的图片处理(圆角、后台解码、剪裁等)

以下处理主要参考了 Kingfisher 的相关实现。

创建命名空间

利用协议扩展为现有类型添加前缀,避免自定义扩展污染类型的全局命名空间。

swift 复制代码
// 定义一个泛型结构体 YUWrapper,它将作为一个通用包装器,能够包装任何类型。
struct YUWrapper<Base> {
    let base: Base // 保存被包装的原始值
    init(_ base: Base) {
        self.base = base // 初始化时将原始值保存到属性中
    }
}
​
// 定义一个空的协议 YUCompatible,用于标记类类型(引用类型)的对象可以使用 YUWrapper 进行扩展。
protocol YUCompatible: AnyObject { }
​
// 定义一个空的协议 YUCompatibleValue,用于标记值类型的对象可以使用 YUWrapper 进行扩展。
protocol YUCompatibleValue { }
​
// 为 YUCompatible 协议扩展一个计算属性 yu,这使得任何遵循 YUCompatible 的类型(即类)都能通过 .yu 访问其包装器 YUWrapper 实例。
extension YUCompatible {
    var yu: YUWrapper<Self> {
        YUWrapper(self)
    }
    
    // 提供一个静态属性 yu,允许通过类型本身访问 YUWrapper,而不是类型的实例。
    static var yu: YUWrapper<Self>.Type {
        YUWrapper<Self>.self
    }
}
​
// 为 YUCompatibleValue 协议扩展一个计算属性 yu,这使得任何遵循 YUCompatibleValue 的类型(即值类型)也能通过 .yu 访问其包装器 YUWrapper 实例。
extension YUCompatibleValue {
    var yu: YUWrapper<Self> {
        YUWrapper(self)
    }
}
​
// 让 UIImage 类遵循 YUCompatible 协议,这意味着 UIImage 及其实例现在可以访问 .yu 属性,从而使用 YUWrapper 中定义的功能。
extension UIImage: YUCompatible { }
​
// 让 UIImageView 类遵循 YUCompatible 协议。
extension UIImageView: YUCompatible { }
​
// 让 CGSize 和 CGRect 这两个结构体遵循 YUCompatibleValue 协议。
extension CGSize: YUCompatibleValue { }
extension CGRect: YUCompatibleValue { }

圆角

首先定义两个枚举,分别用于配置圆角大小和图像内容模式。

swift 复制代码
enum Radius {
    case widthFraction(CGFloat)
    case heightFraction(CGFloat)
    case point(CGFloat)
​
    func compute(with size: CGSize) -> CGFloat {
        // 根据枚举值计算实际的圆角大小
        switch self {
        case .point(let point):
            // 直接指定圆角大小
            return point
        case .widthFraction(let widthFraction):
            // 根据容器宽度的百分比计算圆角
            return size.width * widthFraction
        case .heightFraction(let heightFraction):
            // 根据容器高度的百分比计算圆角
            return size.height * heightFraction
        }
    }
}
​
enum ContentMode: String {
    case none // 不进行调整
    case aspectFit // 保持纵横比缩放内容以适应容器大小,内容完整显示
    case aspectFill // 保持纵横比缩放内容填满容器,可能会裁剪内容
}

圆角处理:

swift 复制代码
extension YUWrapper where Base == CGSize {
    var aspectRatio: CGFloat {
        base.height == 0.0 ? 1.0 : base.width / base.height
    }
}
​
​
extension YUWrapper where Base == UIImage {
    /// 创建一个具有指定圆角的新图像。
    /// - Parameters:
    ///   - radius: 圆角大小,可以是具体的点数、容器宽度的百分比或容器高度的百分比。
    ///   - corners: 指定需要应用圆角的角。默认值是 .allCorners。
    ///   - backgroundColor: 图像背景颜色。如果提供,该颜色将填充图像的背景。
    /// - Returns: 处理后的新图像。
    func roundedImage(
        withRadius radius: Radius,
        roundingCorners corners: UIRectCorner = .allCorners,
        backgroundColor: UIColor? = nil
    ) -> UIImage {
        roundedImage(
            withRadius: radius, containerViewSize: base.size, containerViewContentMode: .none,
            roundingCorners: corners, backgroundColor: backgroundColor
        )
    }
    
    /// 根据容器的尺寸和内容模式创建一个新的具有指定圆角的图像。
    /// - Parameters:
    ///   - radius: 圆角大小,可以是具体的点数、容器宽度的百分比或容器高度的百分比。
    ///   - size: 容器的尺寸,如 imageView 的 size。
    ///   - contentMode: 决定图像如何适应容器的内容模式。
    ///   - corners: 指定需要应用圆角的角。默认值是 .allCorners。
    ///   - backgroundColor: 图像背景颜色。如果提供,该颜色将填充图像的背景。
    /// - Returns: 处理后的新图像。
    func roundedImage(
        withRadius radius: Radius,
        containerViewSize size: CGSize,
        containerViewContentMode contentMode: ContentMode,
        roundingCorners corners: UIRectCorner = .allCorners,
        backgroundColor: UIColor? = nil
    ) -> UIImage {
        // 确保图像基于 CGImage
        guard base.cgImage != nil else {
            assertionFailure("Round corner image only works for CG-based image.")
            return base
        }
        
        // 初始化偏移和缩放因子
        var xOffset: CGFloat = 0, yOffset: CGFloat = 0, scalingFactor: CGFloat = 1
        
        // 根据容器尺寸和内容模式调整图像尺寸和位置
        if size.yu.aspectRatio > base.size.yu.aspectRatio {
            if contentMode == .aspectFill {
                yOffset = (base.size.height - base.size.width / size.yu.aspectRatio) / 2
                scalingFactor = base.size.width / size.width
            } else if contentMode == .aspectFit {
                scalingFactor = base.size.height / size.height
            }
        } else {
            if contentMode == .aspectFill {
                xOffset = (base.size.width - base.size.height * size.yu.aspectRatio) / 2
                scalingFactor = base.size.height / size.height
            } else if contentMode == .aspectFit {
                scalingFactor = base.size.width / size.width
            }
        }
        
        // 计算实际的圆角大小
        let actualCornerRadius = radius.compute(with: base.size) * scalingFactor
        let drawSize = CGSize(width: base.size.width - 2 * xOffset, height: base.size.height - 2 * yOffset)
        
        // 绘制圆角图像
        return draw(to: drawSize) { _ in
            guard let context = UIGraphicsGetCurrentContext() else {
                assertionFailure("Failed to create CG context for image.")
                return false
            }
            
            // 如果指定了背景颜色,则先填充背景
            if let backgroundColor = backgroundColor {
                let rectPath = UIBezierPath(rect: CGRect(origin: .zero, size: drawSize))
                backgroundColor.setFill()
                rectPath.fill()
            }
            
            // 创建并应用圆角路径
            let path = pathForRoundCorner(rect: CGRect(origin: .zero, size: drawSize), cornerRadius: actualCornerRadius, corners: corners)
            context.addPath(path.cgPath)
            context.clip()
            base.draw(in: CGRect(x: -xOffset, y: -yOffset, width: base.size.width, height: base.size.height))
            
            return false
        }
    }
​
}
​
private extension YUWrapper where Base == UIImage {
    func pathForRoundCorner(rect: CGRect, cornerRadius: CGFloat, corners: UIRectCorner, offsetBase: CGFloat = 0) -> UIBezierPath {
        UIBezierPath(
            roundedRect: rect,
            byRoundingCorners: corners,
            cornerRadii: CGSize(
                width: cornerRadius - offsetBase / 2,
                height: cornerRadius - offsetBase / 2
            )
        )
    }
    
    // 使用Core Graphics进行自定义绘图,返回一个新的UIImage。
    // - size: 绘图的目标尺寸。
    // - inverting: 是否翻转绘图上下文。当绘制CGImage时,由于坐标系统的差异,有时需要翻转。
    // - scale: 绘制的比例因子。如果不指定,则使用原图像的scale。
    // - refImage: 用作参考的图像,当需要保留原图像的某些属性(如方向)时使用。
    // - draw: 一个闭包,传入CGContext,根据返回值决定是否使用refImage的方向。
    func draw(
        to size: CGSize,
        inverting: Bool = false,
        scale: CGFloat? = nil,
        refImage: UIImage? = nil,
        draw: (CGContext) -> Bool
    ) -> UIImage
    {
        // 创建一个图像渲染器,设置其格式和缩放比例。
        let format = UIGraphicsImageRendererFormat.preferred()
        format.scale = scale ?? base.scale
        let renderer = UIGraphicsImageRenderer(size: size, format: format)
        
        var useRefImage: Bool = false
        // 使用渲染器创建图像。
        let image = renderer.image { rendererContext in
            let context = rendererContext.cgContext
            // 如果需要翻转上下文(例如,绘制CGImage时)。
            if inverting {
                context.scaleBy(x: 1.0, y: -1.0)
                context.translateBy(x: 0, y: -size.height)
            }
            
            // 调用传入的闭包进行自定义绘图,决定是否使用参考图像的方向。
            useRefImage = draw(context)
        }
        // 根据是否使用参考图像,创建并返回最终的UIImage。
        if useRefImage {
            guard let cgImage = image.cgImage else {
                return base // 如果无法获取CGImage,返回原图。
            }
            let ref = refImage ?? base
            // 使用参考图像的方向创建UIImage。
            return UIImage(cgImage: cgImage, scale: format.scale, orientation: ref.imageOrientation)
        } else {
            // 不使用参考图像,直接返回绘制的图像。
            return image
        }
    }
}

为方便使用,给 UIImageView 添加一个扩展方法:

ini 复制代码
extension YUWrapper where Base == UIImageView {
    func setImage(_ image: UIImage, cornerRadius: Radius, roundingCorners corners: UIRectCorner = .allCorners, backgroundColor: UIColor? = nil) {
        var contentMode: ContentMode = .none
        if base.contentMode == .scaleAspectFit {
            contentMode = .aspectFit
        } else if base.contentMode == .scaleAspectFill {
            contentMode = .aspectFill
        }
        
        base.image = image.yu.roundedImage(
            withRadius: cornerRadius, containerViewSize: base.bounds.size,
            containerViewContentMode: contentMode, roundingCorners: corners, backgroundColor: backgroundColor
        )
    }
}

使用:

scss 复制代码
override func viewDidLoad() {
    super.viewDidLoad()
​
    let imageView1 = UIImageView()
    imageView1.backgroundColor = .systemPink
    imageView1.contentMode = .scaleAspectFit
    // ...
    imageViews.append(imageView1)
​
    let imageView2 = UIImageView()
    imageView2.backgroundColor = .systemPink
    imageView2.contentMode = .scaleAspectFill
    imageView2.clipsToBounds = true
    // ...
    imageViews.append(imageView2)
}
​
override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
​
    imageViews.forEach { imageView in
        // 在 imageView 有确定的 size 之后再调用
        imageView.yu.setImage(.panda, cornerRadius: .heightFraction(0.5))
    }
}

效果:

后台解码

swift 复制代码
extension YUWrapper where Base == UIImage {
    // 扩展UIImage,增加一个计算属性`decoded`用于返回解码后的图像。
    var decoded: UIImage {
        // 确保UIImage基于CGImage,因为解码操作是基于Core Graphics的CGImage进行的。
        guard let imageRef = base.cgImage else {
            assertionFailure("Decoding only works for CG-based image.")
            return base // 如果不是基于CGImage的图像,直接返回原图像。
        }
​
        // 计算原始图像的尺寸,考虑到了图像的scale。UIImage的scale属性标识了图像的像素密度。
        // 例如,@2x图像的scale为2,这意味着图像的宽度和高度应该除以scale,以获得正确的显示尺寸。
        let size = CGSize(width: CGFloat(imageRef.width) / base.scale, height: CGFloat(imageRef.height) / base.scale)
        // 调用`draw(to:inverting:scale:draw:)`方法进行解码操作。
        return draw(to: size, inverting: true, scale: base.scale) { context in
            // 在给定的上下文中绘制原始CGImage。
            // 这里创建了一个与原图相同尺寸的上下文,并将原图绘制进去,实现了解码。
            context.draw(imageRef, in: CGRect(origin: .zero, size: size))
            return true // 返回true表示使用绘制的图像。
        }
    }
}

使用:

csharp 复制代码
DispatchQueue.global().async {
    let decodedImage: UIImage = .panda.yu.decoded
    DispatchQueue.main.async {
        self.imageView.image = decodedImage
    }
}

调整尺寸

swift 复制代码
import CoreGraphics
import func AVFoundation.AVMakeRect
​
extension YUWrapper where Base == CGSize {
    // resize 方法接受一个 desiredSize 和 contentMode 参数,返回调整后的 CGSize。
    func resize(to desiredSize: CGSize, for contentMode: ContentMode = .aspectFit) -> CGSize {
        var targetSize: CGSize
        switch contentMode {
        case .none:
            // 如果 contentMode 为 .none,则直接使用 desiredSize 作为目标尺寸。
            targetSize = desiredSize
        case .aspectFit:
            // 如果 contentMode 为 .aspectFit,使用 AVMakeRect 计算适合内部矩形的宽高比。
            // 这保证了尺寸调整后的大小将完全适应指定区域,同时保持原始的宽高比。
            let rect = AVFoundation.AVMakeRect(aspectRatio: base, insideRect: .init(origin: .zero, size: desiredSize))
            targetSize = rect.size
        case .aspectFill:
            // 如果 contentMode 为 .aspectFill,计算缩放因子以填充目标区域,同时保持原始的宽高比。
            // 这可能会导致目标尺寸超出 desiredSize 指定的范围。
            let scalingFactor = max(desiredSize.width / base.width, desiredSize.height / base.height)
            targetSize = CGSize(width: base.width * scalingFactor, height: base.height * scalingFactor)
        }
        
        return targetSize
    }
}
​
extension YUWrapper where Base == UIImage {
    // resize 方法接受一个 desiredSize 和 contentMode 参数,返回调整后的 UIImage。
    func resize(to desiredSize: CGSize, for contentMode: ContentMode = .none) -> UIImage {
        // 使用 CGSize 扩展的 resize 方法计算目标尺寸。
        let targetSize = base.size.yu.resize(to: desiredSize, for: contentMode)
        return draw(to: targetSize) { _ in
            base.draw(in: CGRect(origin: .zero, size: targetSize))
            return false
        }
    }
}

使用:

less 复制代码
imageView.image = .panda.yu.resize(to: CGSize(width: 500, height: 500), for: .aspectFit)

裁剪

swift 复制代码
import CoreGraphics

extension YUWrapper where Base == CGSize {
    // 计算给定尺寸和锚点的约束矩形。
    func constrainedRect(for size: CGSize, anchor: CGPoint) -> CGRect {
        // 确保锚点的x和y值位于[0, 1]的范围内,防止超出范围的值。
        let unifiedAnchor = CGPoint(x: anchor.x.clamped(to: 0.0...1.0),
                                    y: anchor.y.clamped(to: 0.0...1.0))
        
        // 计算裁剪矩形的起始点。基于锚点和目标尺寸与基础尺寸的关系进行计算。
        let x = unifiedAnchor.x * base.width - unifiedAnchor.x * size.width
        let y = unifiedAnchor.y * base.height - unifiedAnchor.y * size.height
        // 创建裁剪矩形。
        let r = CGRect(x: x, y: y, width: size.width, height: size.height)
        
        // 创建一个原点为(0, 0),尺寸为基础尺寸的CGRect。
        let ori = CGRect(origin: .zero, size: base)
        // 返回两个矩形的交集,即最终的约束矩形。
        return ori.intersection(r)
    }
}

extension YUWrapper where Base == CGRect {
    // 根据给定的缩放比例,返回缩放后的CGRect。
    func scaled(_ scale: CGFloat) -> CGRect {
        CGRect(x: base.origin.x * scale, y: base.origin.y * scale, width: base.size.width * scale, height: base.size.height * scale)
    }
}

// 为所有可比较类型提供一个方法,将值限制在给定范围内。
private extension Comparable {
    // 将当前值限制在闭区间limits内。
    func clamped(to limits: ClosedRange<Self>) -> Self {
        min(max(self, limits.lowerBound), limits.upperBound)
    }
}

extension YUWrapper where Base == UIImage {
    // 定义一个裁剪方法,接受目标尺寸和一个可选的锚点参数,默认锚点在图像中心。
    func crop(to size: CGSize, anchorOn anchor: CGPoint = CGPoint(x: 0.5, y: 0.5)) -> UIImage {
        // 确保UIImage基于CGImage,因为裁剪操作是基于Core Graphics的CGImage进行的。
        guard let cgImage = base.cgImage else {
            assertionFailure("Crop only works for CG-based image.")
            return base // 如果不是基于CGImage的图像,直接返回原图像。
        }
        
        // 计算裁剪矩形。这一步涉及到根据指定的锚点和目标尺寸,计算出一个CGRect作为裁剪区域。
        let rect = base.size.yu.constrainedRect(for: size, anchor: anchor)
        
        // 使用Core Graphics的cropping方法进行裁剪。
        guard let image = cgImage.cropping(to: rect.yu.scaled(base.scale)) else {
            assertionFailure("Cropping image failed.")
            return base // 如果裁剪失败,返回原图像。
        }
        
        // 使用裁剪后的CGImage创建一个新的UIImage对象,保持原图像的scale和orientation不变。
        return UIImage(cgImage: image, scale: base.scale, orientation: base.imageOrientation)
    }
}

使用:

less 复制代码
imageView.image = .panda.yu.crop(to: CGSize(width: 1600, height: 800), anchorOn: .zero)

下采样

swift 复制代码
extension YUWrapper where Base == UIImage {
    static func downsample(data: Data, to pointSize: CGSize) -> UIImage? {
        // 设置图像源选项,指定不应在创建时缓存图像数据。
        let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
        
        // 获取屏幕的缩放比例。
        let scale = if #available(iOS 13, *) { UITraitCollection.current.displayScale } else { UIScreen.main.scale }
        
        // 计算目标尺寸的最大维度(以像素为单位),乘以屏幕的缩放比例。
        let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
        
        // 设置降采样选项。
        let downsampleOptions = [
            kCGImageSourceCreateThumbnailFromImageAlways: true,         // 生成缩略图
            kCGImageSourceShouldCacheImmediately: true,                 // 是否在创建图片时就进行解码
            kCGImageSourceCreateThumbnailWithTransform: true,           // 根据完整图像的方向和像素纵横比旋转和缩放缩略图
            kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels   // 指定缩略图的最大维度
        ] as CFDictionary
        
        // 尝试创建图像源,使用提供的图像数据和图像源选项。
        guard
            let imageSource = CGImageSourceCreateWithData(data as CFData, imageSourceOptions),
            // 尝试创建降采样图像,指定索引为0(通常图像数据只包含一个图像)和下采样选项。
            let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions)
        else { return nil } // 如果创建失败,返回nil。
        
        // 使用下采样后的CGImage创建并返回UIImage对象。
        return UIImage(cgImage: downsampledImage)
    }

    static func downsample(imageAt imageURL: URL, to pointSize: CGSize) -> UIImage? {
        let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
        let scale = if #available(iOS 13, *) { UITraitCollection.current.displayScale } else { UIScreen.main.scale }
        let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
        let downsampleOptions = [
            kCGImageSourceCreateThumbnailFromImageAlways: true,
            kCGImageSourceShouldCacheImmediately: true,
            kCGImageSourceCreateThumbnailWithTransform: true,
            kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
        ] as CFDictionary
        
        guard
            let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, imageSourceOptions),
            let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions)
        else { return nil }
        
        return UIImage(cgImage: downsampledImage)
    }
}

使用:

arduino 复制代码
let image: UIImage = .panda
imageView.image = UIImage.yu.downsample(data: image.pngData()!, to: imageView.bounds.size)
相关推荐
兔云程序8 小时前
消息称苹果iPhone系列将完全放弃LCD屏幕
ios·iphone
JackLee1810 小时前
Metal 之旅之MTLLibrary
ios·metal·图形绘制·视频渲染·mtllibrary
missmisslulu1 天前
电容笔值得买吗?2024精选盘点推荐五大惊艳平替电容笔!
学习·ios·电脑·平板
GEEKVIP1 天前
手机使用技巧:8 个 Android 锁屏移除工具 [解锁 Android]
android·macos·ios·智能手机·电脑·手机·iphone
GEEKVIP1 天前
如何在 Windows 10 上恢复未保存/删除的 Word 文档
macos·ios·智能手机·电脑·word·笔记本电脑·iphone
奇客软件1 天前
iPhone使用技巧:如何恢复变砖的 iPhone 或 iPad
数码相机·macos·ios·电脑·笔记本电脑·iphone·ipad
奇客软件3 天前
如何从相机的记忆棒(存储卡)中恢复丢失照片
深度学习·数码相机·ios·智能手机·电脑·笔记本电脑·iphone
GEEKVIP3 天前
如何修复变砖的手机并恢复丢失的数据
macos·ios·智能手机·word·手机·笔记本电脑·iphone
一丝晨光3 天前
继承、Lambda、Objective-C和Swift
开发语言·macos·ios·objective-c·swift·继承·lambda