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()方法,直接保存原图片可以得到预期的透明效果,但图片会保留大量无用的空白区域。
问题原因
这个问题由两个核心点共同导致:
cropAlpha()方法生成最终UIImage时,没有保留原图片的scale和orientation参数,默认用scale=1、orientation=.up初始化,导致系统在处理保存逻辑时无法正确识别图片的alpha通道属性- 部分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通道:
- 首先在info.plist中添加相册写入权限:
NSPhotoLibraryAddUsageDescription,填入对应的权限提示文案 - 替换保存代码:
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
相关产品推荐
相关产品推荐

