vImageConverter_CreateWithCGImageFormat Fails with kvImageInvalidImageFormat When Trying to Convert CMYK to RGB

So I get JPEG data in my app. Previously I was using the higher level NSBitmapImageRep API and just feeding the JPEG data to it.

But now I've noticed on Sonoma If I get a JPEG in the CMYK color space the NSBitmapImageRep renders mostly black and is corrupted. So I'm trying to drop down to the lower level APIs. Specifically I grab a CGImageRef and and trying to use the Accelerate API to convert it to another format (to hopefully workaround the issue...

   CGImageRef sourceCGImage = `CGImageCreateWithJPEGDataProvider(jpegDataProvider,`
                                                                   NULL,
                                                                   shouldInterpolate,
                                                                   kCGRenderingIntentDefault);


Now I use vImageConverter_CreateWithCGImageFormat... with the following values for source and destination formats:

Source format: (derived from sourceCGImage) bitsPerComponent = 8 bitsPerPixel = 32 colorSpace = (kCGColorSpaceICCBased; kCGColorSpaceModelCMYK; Generic CMYK Profile) bitmapInfo = kCGBitmapByteOrderDefault version = 0 decode = 0x000060000147f780 renderingIntent = kCGRenderingIntentDefault

Destination format: bitsPerComponent = 8 bitsPerPixel = 24 colorSpace = (DeviceRBG) bitmapInfo = 8197 version = 0 decode = 0x0000000000000000 renderingIntent = kCGRenderingIntentDefault

But vImageConverter_CreateWithCGImageFormat fails with kvImageInvalidImageFormat. Now if I change the destination format to use 32 bitsPerpixel and use alpha in the bitmap info the vImageConverter_CreateWithCGImageFormat does not return an error but I get a black image just like NSBitmapImageRep

Answered by Engineer in 803772022

bitmapInfo = 8197 indicates CGBitmapInfo.byteOrder32Little.rawValue, but you're specifying 8-bit per-channel. The folllowing code creates a CMYK -> RGB converter for 8-bit per channel:

var source = vImage_CGImageFormat(bitsPerComponent: 8,
                                  bitsPerPixel: 32,
                                  colorSpace: CGColorSpaceCreateDeviceCMYK(),
                                  bitmapInfo: CGBitmapInfo.byteOrderDefault,
                                  renderingIntent: CGColorRenderingIntent.defaultIntent)!

var dest = vImage_CGImageFormat(bitsPerComponent: 8,
                                bitsPerPixel: 24,
                                colorSpace: CGColorSpaceCreateDeviceRGB(),
                                bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue),
                                renderingIntent: CGColorRenderingIntent.defaultIntent)!

var error = vImage_Error()

let converter = vImageConverter_CreateWithCGImageFormat(&source, &dest, nil, 0, &error)

Are you saying that you've called CGImageGetColorSpace(sourceCGImage) and it correctly reports that the image is CMYK?

Yes. Everything in the source image format struct is derived from the sourceCGImage. Color space is retrieved using CGImageGetColorSpace, width is retrieved using CGImageGetWidth, etc.

I filed FB15114920

So if I put a breakpoint right after CGImageCreateWithJPEGDataProvider and Quicklook preview sourceCGImage, the CGimageRef rendered in the Quicklook preview in the Xcode debugger is correct.

But once I try to pass sourceCGImage to any higher level API it goes black. That includes NSBitmapImageRep, NSImage, CIImage, etc.

I also tried creating a bitmap context and drawing the sourceCGImage in the bitmap context, but haven't been able to get that to work. Previewing the sourceCGImage right after CGImageCreateWithJPEGDataProvider from Xcode's debugger when I hit that breakpoint is the only time the image is rendered correctly. I wonder what API is being used to render the CGImage from the Xcode debugger?

To workaround the issue, if I iterate over the pixel data of the CMYK image I'm able to manually convert to RGB and write it into another buffer and create a CGImage with that RGB data.

Then I can use CGBitmapContextCreate with the RGB buffer to generate a new image. So there appears to be an issue with how CMYK color space is being handled.

Had to take a peek back in the Quartz 2D Programming Guide in the "Documentation Archive" for a refresh on some of these APIs I haven't used in a long time...

bitmapInfo = 8197 indicates CGBitmapInfo.byteOrder32Little.rawValue, but you're specifying 8-bit per-channel. The folllowing code creates a CMYK -> RGB converter for 8-bit per channel:

var source = vImage_CGImageFormat(bitsPerComponent: 8,
                                  bitsPerPixel: 32,
                                  colorSpace: CGColorSpaceCreateDeviceCMYK(),
                                  bitmapInfo: CGBitmapInfo.byteOrderDefault,
                                  renderingIntent: CGColorRenderingIntent.defaultIntent)!

var dest = vImage_CGImageFormat(bitsPerComponent: 8,
                                bitsPerPixel: 24,
                                colorSpace: CGColorSpaceCreateDeviceRGB(),
                                bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue),
                                renderingIntent: CGColorRenderingIntent.defaultIntent)!

var error = vImage_Error()

let converter = vImageConverter_CreateWithCGImageFormat(&source, &dest, nil, 0, &error)

I was trying all sorts of different combinations while I was hitting errors. It looks like I messed that up. Changing destination image format to use kCGImageAlphaNone like you've shown with 24 bits per pixel does in fact fix the kvImageInvalidImageFormat problem but when I convert the image I still get a black image after the conversion.

I also tied changing destination format to 32 bpp with kCGImageAlphaNoneSkipLast | kCGBitmapByteOrder32Host) but I still get a black CGImageRef from vImageCreateCGImageFromBuffer (though I am able to create a vImageConverter without error).

Only thing I've been able to get working is iterating over the pixels and converting to RGB, then make another CGImage like so:

CGContextRef context = CGBitmapContextCreate(nowRGBConvertedFromCMYK, 
                                                 sourceWidth,
                                                 sourceHeight,
                                                 sourceBitsPerCompontent, // 8
                                                 rgbBytesPerRow, 
                                                 rgbColorSpace,  // deviceRGB
                                                 kCGImageAlphaNoneSkipLast | kCGBitmapByteOrderDefault);

// Create a new RGB image from the context
 CGImageRef rgbImage = CGBitmapContextCreateImage(context);

I've added the code below that takes an image, creates a CMYK vImage PixelBuffer (which is a Swift overlay to the vImage_buffer) and converts to RGB. This conversion works as expected. The properties you set in CGBitmapContextCreate need to match the properties of destFormat - with this vImage workflow, I'm able to use the same rgbFormat structure.

I hope this helps:

let sourceImage = #imageLiteral(resourceName: "image.jpg").cgImage(forProposedRect: nil, context: nil, hints: nil)!

var cmykFormat = vImage_CGImageFormat(
    bitsPerComponent: 8,
    bitsPerPixel: 32,
    colorSpace: CGColorSpaceCreateDeviceCMYK(),
    bitmapInfo: CGBitmapInfo.byteOrderDefault,
    renderingIntent: CGColorRenderingIntent.defaultIntent)!

var rgbFormat = vImage_CGImageFormat(
    bitsPerComponent: 8,
    bitsPerPixel: 24,
    colorSpace: CGColorSpaceCreateDeviceRGB(),
    bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue),
    renderingIntent: CGColorRenderingIntent.defaultIntent)!

let sourceBuffer = try! vImage.PixelBuffer<vImage.Interleaved8x4>(
    cgImage: sourceImage,
    cgImageFormat: &cmykFormat)

let destBuffer = vImage.PixelBuffer<vImage.Interleaved8x3>(
    size: sourceBuffer.size)

vImageConverter.make(sourceFormat: cmykFormat, destinationFormat: rgbFormat)

try! cmykFormatToRGB.convert(from: sourceBuffer, to: destBuffer)

let cmykImage = sourceBuffer.makeCGImage(cgImageFormat: cmykFormat)

let rgbResult = destBuffer.makeCGImage(cgImageFormat: rgbFormat)

Thanks for sharing that code. That Swift code wouldn't compile for me. I'm using Objective-C in my project but your Swift code doesn't appear like it would produce anything different than the code I'm experimenting with now (unless I'm missing something?):

CGImageRef doConvertCMYKCGImageToRGB (CGImageRef sourceImage)
{
    size_t sourceBitsPerComponent = CGImageGetBitsPerComponent(sourceImage);
    size_t sourceBitsPerPixel = CGImageGetBitsPerPixel(sourceImage);
    CGColorSpaceRef sourceColorSpace = CGImageGetColorSpace(sourceImage);
    CGBitmapInfo sourceBitmapInfo = CGImageGetBitmapInfo(sourceImage);
    CGColorRenderingIntent sourceRenderingIntent = CGImageGetRenderingIntent(sourceImage);
    const CGFloat *sourceDecode = CGImageGetDecode(sourceImage);
    
    vImage_CGImageFormat cmykFormat = {
        .bitsPerComponent = (uint32_t)sourceBitsPerComponent, // 8
        .bitsPerPixel = (uint32_t)sourceBitsPerPixel, // 32
        .colorSpace = sourceColorSpace, // (kCGColorSpaceICCBased; kCGColorSpaceModelCMYK; Generic CMYK Profile)
        .bitmapInfo = sourceBitmapInfo, // kCGBitmapByteOrderDefault
        .renderingIntent = sourceRenderingIntent, // kCGRenderingIntentDefault
        .version = 0,
        .decode = sourceDecode
    };

    
    vImage_CGImageFormat rgbFormat = {
        .bitsPerComponent = 8,
        .bitsPerPixel = 24,
        .colorSpace = CGColorSpaceCreateDeviceRGB(),
        .bitmapInfo = (CGBitmapInfo)kCGImageAlphaNone,
        .renderingIntent = kCGRenderingIntentDefault
    };
    
    /*
    Also tried:
    vImage_CGImageFormat rgbFormat = {
        .bitsPerComponent = 8,
        .bitsPerPixel = 32,
        .colorSpace = CGColorSpaceCreateDeviceRGB(),
        .bitmapInfo = (CGBitmapInfo)kCGImageAlphaNoneSkipLast | kCGBitmapByteOrderDefault,
        .renderingIntent = kCGRenderingIntentDefault
    };*/

    // Initialize source buffer
    vImage_Buffer sourceBuffer;
    //  You are responsible for releasing the memory pointed to by buf->data back to the system using free().
    vImage_Error error = vImageBuffer_InitWithCGImage(
        &sourceBuffer, &cmykFormat, NULL, sourceImage, kvImageNoFlags);
    if (error != kvImageNoError) 
    {
        os_log_error(OS_LOG_DEFAULT,"Error initializing source buffer: %ld", error);
        return NULL;
    }

    // Initialize destination buffer..
//    You are responsible for releasing the memory pointed to by buf->data back to
//   the system when you are done with it using free(). If no such allocation is desired, pass
//    *  kvImageNoAllocate in the flags to cause buf->data to be set to NULL and the preferred alignment
//    *  to be returned from the left hand side of the function.
    vImage_Buffer destBuffer;
    error = vImageBuffer_Init(&destBuffer, sourceBuffer.height, sourceBuffer.width, rgbFormat.bitsPerPixel, kvImageNoFlags);
    if (error != kvImageNoError) 
    {
        os_log_error(OS_LOG_DEFAULT,"Error initializing destination buffer: %ld", error);
        free(sourceBuffer.data);
        return NULL;
    }

    // Create a converter
    vImageConverterRef cmykToRGBConverter = vImageConverter_CreateWithCGImageFormat(&cmykFormat, &rgbFormat, NULL, kvImageNoFlags, &error);
    if (cmykToRGBConverter == NULL) 
    {
        os_log_error(OS_LOG_DEFAULT,"Error creating vImage converter: %ld", error);
        free(destBuffer.data);  // Free destBuffer.data.
        free(sourceBuffer.data);  // Free sourceBuffer.data.
        return NULL;
    }

    // Perform the conversion
    error = vImageConvert_AnyToAny(cmykToRGBConverter, &sourceBuffer, &destBuffer, NULL, kvImageNoFlags);
    
    CGImageRef rgbResult = NULL;
    
    if (error == kvImageNoError)
    {
        // Create RGB result image
        rgbResult = vImageCreateCGImageFromBuffer(&destBuffer, &rgbFormat, NULL, NULL, kvImageNoFlags, &error);
    }
    else
    {
        os_log_error(OS_LOG_DEFAULT,"Error converting image: %ld", error);
    }

    // Create CMYK image (not needed)...
    //CGImageRef cmykImage = vImageCreateCGImageFromBuffer(&sourceBuffer, &cmykFormat, NULL, NULL, kvImageNoFlags, &error);

    // Clean up
    vImageConverter_Release(cmykToRGBConverter);
    free(destBuffer.data);  // Free destBuffer.data
    free(sourceBuffer.data);  // Free destBuffer.data

    return rgbResult;
}

Using that with image I provided in my feedback as sourceImage produces a black box (same when I pass the image data I provided in my feedback directly to higher level APIs like NSBitmapImageRep).

What values are in CGImageGetDecode(sourceImage)? Have you tried passing nil?

1.000000 0.000000, 1.000000, 0.000000, 1.000000, 0.000000, 1.000000, 0.000000

-- I did try passing in NULL but still end up with a black box.

Hi, I think we should continue this with https://feedbackassistant.apple.com - please can you attach a minimal project and image that replicates the issue.

Thanks in advance

Hi, I think we should continue this with https://feedbackassistant.apple.com - please can you attach a minimal project and image that replicates the issue.

I already filed FB15114920. In that sample I just loaded the CMYK image using NSBitmapImageRep...if I remember correctly. But if you use that same source image in that sample project and try use the Accelerate APIs to convert it to RGB you'll get the same black box NSBitmapImageRep gives you.

vImageConverter_CreateWithCGImageFormat Fails with kvImageInvalidImageFormat When Trying to Convert CMYK to RGB
 
 
Q