Search code examples
iosswiftuiuiimage

`kCMPhotoError_UnsupportedPixelFormat` when using image created using `ImageRenderer`


I'm using ImageRenderer to render a SwiftUI view to a UIImage. I am able to successfully render a PNG from that using pngData(). But if I call jpegData() I get the following error:

writeImageAtIndex:867: *** CMPhotoCompressionSessionAddImage: err = kCMPhotoError_UnsupportedPixelFormat [-16995] (codec: 'jpeg')

I get the same error in the log if I send the UIImage instance to UIActivityViewController and choose Messages (and Messages does not show the image in its UI). If I choose Mail, it crashes the process with

-[MFMailComposeInternalViewController addAttachmentData:mimeType:fileName:] attachment must not be nil.

The problem seems to be something about the rendered image being incompatible with the JPEG encoder. However, if I share this image using AirDrop, it sends a jpeg file with perfectly-normal-seeming attributes.

I also tried rendering to a CGImage and creating the UIImage from that, and I get the same result.


Solution

  • This seems to be specific to running on an actual device—in Simulator I was not able to reproduce the issue.

    The cause of the "unsupported pixel format" turned out to be a 24-bit color depth in the rendered image, and possibly also the presence of an alpha layer. The jpeg encoder is reported to require 8-bit color depth and no transparency (though I would have expected the UIImage implementation to convert the color depth automatically).

    The following converts the color depth to 8-bit:

    extension View {
        @MainActor func uiImageSnapshot() -> UIImage? {
            let renderer = ImageRenderer(content: self)
    
            // jpeg encoder requires that the image not have an alpha layer
            renderer.isOpaque = true
    
            guard let image = renderer.uiImage else { return nil }
    
            // jpeg encoder requires an 8-bit color depth (including the encoder used in Mail, Messages, etc.)
            return image.convertedToColorDepth(8)
        }
    }
    
    extension UIImage {
        func convertedToColorDepth(_ colorDepth: Int) -> UIImage? {
            guard let cgImage else {
                return nil
            }
    
            let colorSpace = CGColorSpaceCreateDeviceRGB()
            let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue)
    
            guard let context = CGContext(data: nil, width: cgImage.width, height: cgImage.height, bitsPerComponent: colorDepth, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo.rawValue) else {
                return nil
            }
    
            let rect = CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height)
            context.draw(cgImage, in: rect)
    
            guard let newCGImage = context.makeImage() else {
                return nil
            }
    
            return UIImage(cgImage: newCGImage)
        }
    }