4

I'm trying to change color of an image in a background thread.
Apple doc says UIGraphicsBeginImageContext can only be called from main thread, and I'm trying to use CGBitmapContextCreate:

context = CGBitmapContextCreate (bitmapData, pixelsWide, pixelsHigh, 8, // bits per component

                             bitmapBytesPerRow,
                             colorSpace,
                             kCGImageAlphaPremultipliedFirst);

I have two versions of "changeColor" first one using UIGraphisBeginImageContext, second one using CGBitmapContextCreate.

The first one correctly changes color, but second one doesn't.
Why is that?

- (UIImage*) changeColor: (UIColor*) aColor
{
    if(aColor == nil)
        return self;

    UIGraphicsBeginImageContext(self.size);

    CGRect bounds;
    bounds.origin = CGPointMake(0,0);
    bounds.size = self.size;
    [aColor set];

    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextTranslateCTM(context, 0, self.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);

    CGContextClipToMask(context, bounds, [self CGImage]);
    CGContextFillRect(context, bounds);

    UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return img;
}

- (UIImage*) changeColor: (UIColor*) aColor
{
    if(aColor == nil)
        return self;

    CGContextRef context = CreateARGBBitmapContext(self.size);

    CGRect bounds;
    bounds.origin = CGPointMake(0,0);
    bounds.size = self.size;

    CGColorRef colorRef = aColor.CGColor;
    const CGFloat *components = CGColorGetComponents(colorRef);
    float red = components[0];
    float green = components[1];
    float blue = components[2];

    CGContextSetRGBFillColor(context, red, green, blue, 1);


    CGContextClipToMask(context, bounds, [self CGImage]);
    CGContextFillRect(context, bounds);

    CGImageRef imageRef = CGBitmapContextCreateImage(context);
    UIImage* img = [UIImage imageWithCGImage: imageRef];
    unsigned char* data = (unsigned char*)CGBitmapContextGetData (context);  
    CGContextRelease(context);
    free(data);

    return img;
}

CGContextRef CreateARGBBitmapContext(CGSize size)
{
    CGContextRef    context = NULL;
    CGColorSpaceRef colorSpace;
    void *          bitmapData;
    int             bitmapByteCount;
    int             bitmapBytesPerRow;

    // Get image width, height. We'll use the entire image.                                                                                                                                                                                 
    size_t pixelsWide = size.width;
    size_t pixelsHigh = size.height;

    // Declare the number of bytes per row. Each pixel in the bitmap in this                                                                                                                                                                
    // example is represented by 4 bytes; 8 bits each of red, green, blue, and                                                                                                                                                              
    // alpha.                                                                                                                                                                                                                               
    bitmapBytesPerRow   = (pixelsWide * 4);
    bitmapByteCount     = (bitmapBytesPerRow * pixelsHigh);

    // Use the generic RGB color space.                                                                                                                                                                                                     
    colorSpace = CGColorSpaceCreateDeviceRGB();

    if (colorSpace == NULL)
    {
        fprintf(stderr, "Error allocating color space\n");
        return NULL;
    }

    // Allocate memory for image data. This is the destination in memory                                                                                                                                                                    
    // where any drawing to the bitmap context will be rendered.                                                                                                                                                                            
    bitmapData = malloc( bitmapByteCount );
    if (bitmapData == NULL)
    {
        fprintf (stderr, "Memory not allocated!");
        CGColorSpaceRelease( colorSpace );
        return NULL;
    }
    // Create the bitmap context. We want pre-multiplied ARGB, 8-bits                                                                                                                                                                       
    // per component. Regardless of what the source image format is                                                                                                                                                                         
    // (CMYK, Grayscale, and so on) it will be converted over to the format                                                                                                                                                                 
    // specified here by CGBitmapContextCreate.                                                                                                                                                                                             
    context = CGBitmapContextCreate (bitmapData,
                                     pixelsWide,
                                     pixelsHigh,
                                     8,      // bits per component                                                                                                                                                                          
                                     bitmapBytesPerRow,
                                     colorSpace,
                                     kCGImageAlphaPremultipliedFirst);
    if (context == NULL)
    {
        free (bitmapData);
        fprintf (stderr, "Context not created!");
    }

    // Make sure and release colorspace before returning                                                                                                                                                                                    
    CGColorSpaceRelease( colorSpace );

    return context;

}
Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
eugene
  • 39,839
  • 68
  • 255
  • 489
  • 1
    Actually, in iOS 4.0 UIKit drawing functions are now threadsafe: http://www.cocoabuilder.com/archive/cocoa/296299-drawing-thread-safety-in-ios.html . This appears to include `UIGraphicsBeginImageContext()`. You may not need the pure Core Graphics implementation in 4.0+. – Brad Larson Jan 13 '11 at 18:49
  • is the quote authoritative? http://stackoverflow.com/questions/4451855/uigraphicsbeginimagecontext-vs-cgbitmapcontextcreate-on-ios there are debates on it I guess?, and my test code died with UIGraphicsBeginImageContext() on separate thread. although I can't be 100% sure that's the real reason. – eugene Jan 13 '11 at 19:03
  • this is another question, when you say iOS 4.0, does it apply when I target iOS < 4.0 but compile with iOS >= 4.0 ? or the code should run on machine with iOS >=4.0 only? – eugene Jan 13 '11 at 19:09
  • 2
    David Duncan is an engineer at Apple, so I'd say his statement, combined with the release notes, is authoritative. This is only threadsafe when run on iOS 4.0+ devices, because building against the 4.x SDK still uses the local OS implementation if targeting older versions. If you need older OS support, you'll still need to go the pure Core Graphics route. – Brad Larson Jan 13 '11 at 20:03

1 Answers1

3

Your second method is doing work that the first never did. Here's an adjustment of the second method to more closely match the first one:

- (UIImage*) changeColor: (UIColor*) aColor
{
    if(aColor == nil)
        return self;

    CGContextRef context = CreateARGBBitmapContext(self.size);

    CGRect bounds;
    bounds.origin = CGPointMake(0,0);
    bounds.size = self.size;

    CGContextSetFillColorWithColor(aColor.CGColor);

    CGContextClipToMask(context, bounds, [self CGImage]);
    CGContextFillRect(context, bounds);

    CGImageRef imageRef = CGBitmapContextCreateImage(context);
    UIImage* img = [UIImage imageWithCGImage: imageRef];
    CGContextRelease(context);

    return img;
}

The two changes I made were I converted this to use CGContextSetFillColorWithColor(), and I removed the dangerous and incorrect free() of the backing data of the bitmap context. If this code snippet does not behave identically to the first one, then you will have to look at your implementation of CreateARGBBitmapContext() to verify that it is correct.

Of course, as Brad Larson mentioned in the comments, if you're targeting iOS 4.0 and above, the UIKit graphics methods are (according to the release notes) thread-safe and you should be able to use the first method just fine.

Lily Ballard
  • 182,031
  • 33
  • 381
  • 347
  • ah Thanks!.. i see memory footprint going up when I don't have the 'free', maybe I'm doing something wrong in CreateARGBBitmapContext? I've updated the question with the function, can you take a look at it please? – eugene Jan 14 '11 at 02:08
  • 2
    Ah, I see what's going on. You're malloc'ing your own data, then dropping your reference to it. So that data does indeed need to be released later. It just feels rather nasty to pull out the data from the context and call `free()` on that. If you want an alternative, instead of `malloc()` you could create an `NSMutableData` of the right size and grab `-mutableBytes`. That will give you the same buffer, but autoreleased. Of course, if you do that the context must be released before the autorelease pool pops (but you're doing that here anyway). – Lily Ballard Jan 14 '11 at 02:58
  • one more thing, should i release the imageRef with CGImageRelease? – eugene Jan 14 '11 at 04:48
  • hate apple's documentation, doesn't say clearly if i need to release it or not :( – eugene Jan 14 '11 at 04:57
  • 1
    Oh yes, you clearly should. `CGBitmapContextCreateImage()` gives you an owned reference. `+[UIImage imageWithCGImage:]` will retain the `CGImageRef` for the lifetime of the `UIImage` object. But you still have your owned reference, which you need to release with `CGImageRelease()`. – Lily Ballard Jan 14 '11 at 12:43
  • I was implementing CreateRGBBitmapContext and it was leaking memory like crazy because of that malloc. Good call with the `NSMutableData`. In case someone needs it, here are the two lines to replace the malloc call: `NSMutableData* mutableData = [NSMutableData dataWithLength: bitmapByteCount]; bitmapData = [mutableData mutableBytes];` – Kudit Mar 06 '13 at 19:26