You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS中经cropAlpha裁剪的透明UIImage存相册时背景变白问题

问题现象

参考公开实现编写了cropAlpha()方法,作用是基于图片透明通道裁剪多余的空白区域,方法实现代码如下:

func cropAlpha() -> UIImage {
    let cgImage = self.cgImage!
    
    let width = cgImage.width
    let height = cgImage.height
    
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let bytesPerPixel:Int = 4
    let bytesPerRow = bytesPerPixel * width
    let bitsPerComponent = 8
    let bitmapInfo: UInt32 = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue

    guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo),
          let ptr = context.data?.assumingMemoryBound(to: UInt8.self)
    else { return self }

    context.draw(self.cgImage!, in: CGRect(x: 0, y: 0, width: width, height: height))
    
    var minX = width
    var minY = height
    var maxX: Int = 0
    var maxY: Int = 0
    
    for x in 1 ..< width {
        for y in 1 ..< height {
            let i = bytesPerRow * Int(y) + bytesPerPixel * Int(x)
            let a = CGFloat(ptr[i + 3]) / 255.0
            
            if a == 1 {
                if (x < minX) { minX = x }
                if (x > maxX) { maxX = x }
                if (y < minY) { minY = y }
                if (y > maxY) { maxY = y }
            }
        }
    }
    
    let rect = CGRect(x: CGFloat(minX),y: CGFloat(minY), width: CGFloat(maxX - minX), height: CGFloat(maxY-minY))
    
    let croppedImage = self.cgImage!.cropping(to: rect)!
    let ret = UIImage(cgImage: croppedImage)

    return ret
}

该方法返回的带透明元素的图片,赋值给ImageView控件presenterImageView.image = imagePNG时显示效果完全符合预期,但调用UIImageWriteToSavedPhotosAlbum接口将图片保存到系统相册时,原本的透明背景会变为白色。保存逻辑代码如下:

let image = maskedImage?.cropAlpha()
    
let imagePNGData = image!.pngData()
let imagePNG = UIImage(data: imagePNGData!)

UIImageWriteToSavedPhotosAlbum(imagePNG!, nil, nil, nil)

如果不调用cropAlpha()方法,直接保存原图片可以得到预期的透明效果,但图片会保留大量无用的空白区域。

问题原因

这个问题由两个核心点共同导致:

  1. cropAlpha()方法生成最终UIImage时,没有保留原图片的scale和orientation参数,默认用scale=1、orientation=.up初始化,导致系统在处理保存逻辑时无法正确识别图片的alpha通道属性
  2. 部分iOS版本下,UIImageWriteToSavedPhotosAlbum接口接收UIImage对象做保存时,会先在内部做一次转码,自动将图片转为不支持透明通道的格式,透明区域会被自动填充为白色,哪怕传入的UIImage是由PNG数据初始化的。

另外原有cropAlpha()本身还有几个逻辑缺陷,会导致裁剪效果异常:

  • 像素遍历从x=1、y=1开始,漏掉了最边缘的1行1列像素,容易裁掉边缘内容
  • 仅判断alpha值等于1(完全不透明)的像素,半透明的抗锯齿边缘会被排除在裁剪区域外,最终裁剪出的图片会有生硬的锯齿边
  • 没有处理全透明图片的边界场景,此时计算出的裁剪区域宽高为负,会直接触发强制解包崩溃
解决方案

第一步:修复cropAlpha()方法的逻辑问题

修正后的代码如下:

func cropAlpha() -> UIImage {
    guard let cgImage = self.cgImage else { return self }
    
    let width = cgImage.width
    let height = cgImage.height
    guard width > 1, height > 1 else { return self }
    
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let bytesPerPixel:Int = 4
    let bytesPerRow = bytesPerPixel * width
    let bitsPerComponent = 8
    let bitmapInfo: UInt32 = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue

    guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo),
          let ptr = context.data?.assumingMemoryBound(to: UInt8.self)
    else { return self }

    context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
    
    var minX = width
    var minY = height
    var maxX: Int = 0
    var maxY: Int = 0
    
    // 遍历全量像素
    for x in 0 ..< width {
        for y in 0 ..< height {
            let i = bytesPerRow * y + bytesPerPixel * x
            let a = CGFloat(ptr[i + 3]) / 255.0
            // 只要不是完全透明的像素就纳入裁剪范围,可根据需求调整alpha阈值过滤边缘噪点
            if a > 0 {
                minX = min(x, minX)
                maxX = max(x, maxX)
                minY = min(y, minY)
                maxY = max(y, maxY)
            }
        }
    }
    
    // 全透明场景直接返回原图
    guard maxX > minX, maxY > minY else { return self }
    
    // 计算裁剪区域,补正像素索引差
    let rect = CGRect(x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1)
    
    guard let croppedImage = cgImage.cropping(to: rect) else { return self }
    // 保留原图的scale和方向信息
    let ret = UIImage(cgImage: croppedImage, scale: self.scale, orientation: self.imageOrientation)

    return ret
}

第二步:替换保存逻辑,直接保存PNG数据到相册

不要使用UIImageWriteToSavedPhotosAlbum传UIImage对象,改用PHPhotoLibrary直接写入PNG数据,避免系统自动转码丢失alpha通道:

  1. 首先在info.plist中添加相册写入权限:NSPhotoLibraryAddUsageDescription,填入对应的权限提示文案
  2. 替换保存代码:
import Photos

guard let image = maskedImage?.cropAlpha(),
      let pngData = image.pngData() else {
    // 自行处理裁剪/数据转换失败逻辑
    return
}

// 直接写入原始PNG数据到相册
PHPhotoLibrary.shared().performChanges({
    let creationRequest = PHAssetCreationRequest.forAsset()
    creationRequest.addResource(with: .photo, data: pngData, options: nil)
}) { success, error in
    DispatchQueue.main.async {
        if success {
            // 保存成功逻辑
        } else {
            // 保存失败逻辑
        }
    }
}

内容的提问来源于stack exchange,提问作者cybshot

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 06:57:21