Question

Given a UIImage of any dimension, I wish to generate a square "icon" sized version, px pixels to a side, without any distortion (stretching). However, I'm running into a little snag. Not quite sure where the problem is. Here's what I'm doing so far.

First, given a UImage size, I determine three things: the ratio to use when scaling down the image; a delta (the difference between our desired icon size and the longest side), and an offset (which is used to figure out our origin coordinate when clipping the image):

 if (size.width > size.height) {
   ratio = px / size.width;
   delta = (ratio*size.width - ratio*size.height);
   offset = CGPointMake(delta/2, 0);
 } else {
   ratio = px / size.height;
   delta = (ratio*size.height - ratio*size.width);
   offset = CGPointMake(0, delta/2);
 }

Now, let's say you have an image 640px wide by 480px high, and we want to get a 50px x 50px icon out of this. The width is greater than the height, so our calculations are:

ratio = 50px / 640px = 0.078125
delta = (ratio * 640px) - (ratio * 480px) = 50px - 37.5px = 12.5px
offset = {x=6.25, y=0}

Next, I create a CGRect rect that is large enough to be cropped down to our desired icon size without distortion, plus a clipRect for clipping purposes:

CGRect rect = CGRectMake(0.0, 0.0, (ratio * size.width) + delta,
                                   (ratio * size.height) + delta);
CGRect clipRect = CGRectMake(offset.x, offset.y, px, px);

Substituting our values from above, we get:

rect = origin {x=0.0, y=0.0}, size {width=62.5, height=50.0}
clipRect = origin {x=6.25, y=0}, size {width=50.0, height=50.0}

So now we have a 62.5px wide by 50px high rect to work with, and a clipping rectangle that grabs the "middle" 50x50 portion.

On to the home stretch! Next, we set up our image context, draw the UIImage (called myImage here) into the rect, set the clipping rectangle, get the (presumably now-clipped) image, use it, and finally clean up our image context:

 UIGraphicsBeginImageContext(rect.size);
 [myImage drawInRect:rect]; 
 UIRectClip(clipRect);
 UIImage *icon = UIGraphicsGetImageFromCurrentImageContext();

 // Do something with the icon here ...

 UIGraphicsEndImageContext();

Only one problem: The clipping never occurs! I end up with an image 63px wide x 50px high. :(

Perhaps I'm misusing/misunderstanding UIRectClip? I've tried shuffling various things around: swapping the use of rect and clipRect, moving UIRectClip before drawInRect:. No dice.

I tried searching for an example of this method online as well, to no avail. For the record, UIRectClip is defined as:

Modifies the current clipping path by intersecting it with the specified rectangle.

Shuffling things around gets us a little bit closer:

UIGraphicsBeginImageContext(clipRect.size);
UIRectClip(rect);
[myImage drawInRect:rect];

Now we don't have distortion, but the clipped image isn't centered on the original as I expected. Still, at least the image is 50x50, though the variable names are now fouled up as a result of said shuffling. (I'll respectfully leave renaming as an exercise for the reader.)

Was it helpful?

Solution

Eureka! I had things a little mixed up. This works:

CGRect clipRect = CGRectMake(-offset.x, -offset.y,
                             (ratio * size.width) + delta,
                             (ratio * size.height) + delta);
UIGraphicsBeginImageContext(CGSizeMake(px, px));
UIRectClip(clipRect);
[myImage drawInRect:clipRect];
UIImage *icon = UIGraphicsGetImageFromCurrentImageContext();

// Do something with the icon here ...

UIGraphicsEndImageContext();

No more need for rect. The trick appears to be using a negative offset in the clipping rectangle, thereby lining up the origin of where we want to grab our 50 x 50 image (in this example).

Perhaps there's an easier way. If so, please weigh in!

OTHER TIPS

I wanted to achieve a similar thing but found the answer from by the original poster didn't quite work. It distorted the image. This may well be solely because he didn't post the whole solution and had changed some of how the variables are initialised:

 (if (size.width > size.height) 
       ratio = px / size.width;

Was wrong for my solution (which wanted to use the largest possible square from the source image). Also it is not necessary to use UIClipRect - if you make the context the size of the image you want to extract, no actual drawing will be done outside that rect anyway. It is just a matter of scaling the size of the image rect and offsetting one of the origin coordinates. I have posted my solution below:

+(UIImage *)makeIconImage:(UIImage *)image
{
    CGFloat destSize = 400.0;
    CGRect rect = CGRectMake(0, 0, destSize, destSize);

    UIGraphicsBeginImageContext(rect.size);

    if(image.size.width != image.size.height)
    {
        CGFloat ratio;
        CGRect destRect;

        if (image.size.width > image.size.height)
        {
            ratio = destSize / image.size.height;

            CGFloat destWidth = image.size.width * ratio;
            CGFloat destX = (destWidth - destSize) / 2.0;

            destRect = CGRectMake(-destX, 0, destWidth, destSize);

        }
        else
        {
            ratio = destSize / image.size.width;

            CGFloat destHeight = image.size.height * ratio;
            CGFloat destY = (destHeight - destSize) / 2.0;

            destRect = CGRectMake(0, destY, destSize, destHeight);
        }
        [image drawInRect:destRect];
    }
    else
    {
        [image drawInRect:rect];
    }
    UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    return scaledImage;
}

wheeliebin answers is correct but he forgot a minus sign in front of destY

destRect = CGRectMake(0, -destY, destSize, destHeight);
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top