Robin Daugherty
Robin Daugherty

Reputation: 7524

`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.

Upvotes: 1

Views: 338

Answers (1)

Robin Daugherty
Robin Daugherty

Reputation: 7524

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)
    }
}

Upvotes: 3

Related Questions