objective-ccore-graphicsquartz-2d

Swap color channels on UIImage from transparent png with quartz 2D


I have a weird issue. I want to swap red & green color channels of a UIImage I have loaded from a png. I looked up examples and I use the following code, which works fine for png images that have no transparency:

- (UIImage *)redToGreen {

CGImageRef imgRef = [self CGImage];

size_t width = CGImageGetWidth(imgRef);
size_t height = CGImageGetHeight(imgRef);

CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
size_t bitsPerComponent = 8;
size_t bytesPerPixel = 4;
size_t bytesPerRow = bytesPerPixel * width;
size_t totalBytes = bytesPerRow * height;


//Allocate Image space
uint8_t* rawData = malloc(totalBytes);

//Create Bitmap of same size
CGContextRef context = CGBitmapContextCreate(rawData, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);

//Draw our image to the context
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imgRef);

for ( int i = 0; i < totalBytes; i += 4 ) {

    uint8_t* red = rawData + i;
    uint8_t* green = rawData + (i + 1);
    uint8_t red_old = *red;

    *red = *green;
    *green = red_old;
}

//Create Image
CGImageRef newImg = CGBitmapContextCreateImage(context);

//Release Created Data Structs
CGColorSpaceRelease(colorSpace);
CGContextRelease(context);
free(rawData);

//Create UIImage struct around image
UIImage* newImage = [UIImage imageWithCGImage:newImg];

//Release our hold on the image
CGImageRelease(newImg);

//return new image!
return newImage;
}

However, when I try it on transparent pngs, I get weird artefacts in the background.

So this transparent image for example:

Original image

Loaded on black background shows up as:

After redToGreen

All I do in the code is:

[btn setImage:[[UIImage imageNamed:@"trash.png"] redToGreen] forState:UIControlStateNormal];

I tried changing the kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big since there are examples with differences, but nothing seems to work. Any ideas?

Update: I find it strange that this code has been copy-pasted without complaints. Apart from the problem with not clearing the buffer as indicated by the accepted answer, another flaw is that the scale is always set to 1, so if you open a @2x image, the result will have twice the point size. The correct call to make the UIImage would be:

UIImage* newImage = [UIImage imageWithCGImage:newImg scale:self.scale orientation:self.imageOrientation];

Solution

  • When you create the bitmap context, it doesn't clear the buffer. (It's perfectly sensible to create a bitmap context with a buffer that contains a preexisting image, for example.)

    There are two approaches:

    1) explicitly clear it before drawing the original image, using CGContextClearRect()

    2) set the context's blend mode to "copy" before drawing the original image, so that it "paints over" whatever junk may have been in the buffer; you would use CGContextSetBlendMode(context, kCGBlendModeCopy) to do that