Alright, I have a rant. Bear with me…

Layers just came out this past Monday, and it has this great feature that allows you to add a layer to a drawing from your iPhone’s photo library. Simple enough – right? Apple provides the UIImagePicker API, we call a couple functions and get an image back.

For most purposes, that would work great! Write some code, test, commit, done. The problem is, the picker interface allows the user to adjust the scale/positioning of the image, and the cropped image is always returned at 320x320px (or less). 320px is really quite pathetic, and it means the images are smaller than the 512x512px drawing canvas in Layers. I could scale up each photo when it’s added to the drawing, but that’d be pretty lame.

The UIImagePicker API provides an editInfo dictionary containing the original image and the cropping rect information, so I decided to grab the original and re-perform the adjustments. Using the cropRect provided by the API, I could just re-crop the large, original image to 512×512… right?

Unfortunately, no. Photos taken with the iPhone’s camera use the industry-standard EXIF orientation flag to store rotation information. That means that the image data is always saved upright, and it’s the client application’s job to realize it should be rotated 90º, 180º or 270º because the user was holding the camera upside down or sideways.

Technically, this is great. The problem is, the editInfo dictionary contains (1) the original image and (2) the crop rect, defined in the coordinate space of the image after the EXIF orientation flag is taken into account. You can’t just jump in and crop the original image, because one has had transformations applied and the other hasn’t. So there are two options:

- Option 1: Rotate the original image you’re given based on the EXIF data, and then crop it using the cropRect. This is slow because you have to rotate the entire image and then you end up throwing most of it away. For extremely large images (which can be added into your photo library via Mail attachments), it fails entirely.
- Option 2: Adjust the cropping rect and undo the transformations that have been applied to it based on the EXIF data. This is better, but it requires writing some nasty CGRect transformations and lots of boxes drawn on paper.

I decided to go with option 2. I wrote a nice big switch statement to undo the transformations for each of the eight possible EXIF values. But then I discovered something else:

iPhone Photos App with Different EXIF Orientation FlagsSomebody was lazy. The iPhone’s Photos application only understands EXIF orientations 1, 3, 6, and 8. These correspond to the common orientations: UIImageOrientationUp, UIImageOrientationDown, UIImageOrientationLeft, and UIImageOrientationRight. Photos with the other four orientations (the “mirrored” ones) appear unrotated in the photo browser. (See screenshot at right. Numbers on the images correspond to their EXIF orientation values).

I want the user to get what they expect to get when they add a photo – even if it isn’t what they want. If the image is sideways while they’re cropping it, it should still be sideways when they press OK. I promptly deleted code for the other orientations so as to handle them as badly as the photo browser. Cool. moving on…

iPhone Photos App - Weird Vertical Pan BehaviorThe image picker allows the user to zoom in on the image of their choosing and pan around it, but the pan functionality is broken. You can pan beyond the edge of the image along the vertical axis, so that image is only partially visible within the gray cropping rectangle. This can lead to some strange results. A cropRect of (0,0,320,200) for an image of size (512,512), for example, indicates that a black gap is present at the top of the crop region. After playing around with this for a while, I was able to figure out how to differentiate between the cropRect values and properly draw the image to appear exactly as it did in the preview.

Fixing this problem took almost 5 hours – and it really shouldn’t have. Here’s the code that takes the original image and cropRect and re-performs the adjustments to yield a 512×512 cropped image that matches exactly what the user saw when they clicked “Choose” in the picker:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingImage:(UIImage *)img editingInfo:(NSDictionary *)editInfo
{
    if ([picker sourceType] == UIImagePickerControllerSourceTypeCamera){
        // save the image to the photo library
        UIImageWriteToSavedPhotosAlbum(img, nil, nil, nil);
    }
 
	[self dismissModalViewControllerAnimated:YES];
 
    NSDictionary * assets = [NSDictionary dictionaryWithObjectsAndKeys:img, @"smallCroppedImage", editInfo, @"editInfo", nil];
    [self performSelector:@selector(imagePickerControllerDidFinishThreaded:) withObject:assets afterDelay:0.05];
}
 
- (void)imagePickerControllerDidFinishThreaded:(NSDictionary*)assets
{
    NSDictionary        * editInfo = [assets objectForKey: @"editInfo"];
    CGRect                editCropRect = [[editInfo valueForKey:UIImagePickerControllerCropRect] CGRectValue];
 
    // 1. Determine original image orientation and size
    UIImage             * originalImage = [editInfo valueForKey: UIImagePickerControllerOriginalImage];
    UIImageOrientation    originalOrientation = originalImage.imageOrientation;
    CGSize                originalSize = originalImage.size;
    CGSize                desiredSize = CGSizeMake(512,512);
 
    // 2. Modify crop rect to reflect image orientation
    CGFloat oldY = editCropRect.origin.y;
    CGFloat oldOriginalW = originalSize.width;
    CGFloat tmp;
 
    switch (originalOrientation) {
        case UIImageOrientationUp:      //EXIF 1
            break;
 
        case UIImageOrientationDown:    //EXIF 3
            // X flipped horizontally
            // Y flipped vertically
            editCropRect.origin.x = originalSize.width - (editCropRect.size.width + editCropRect.origin.x);
            editCropRect.origin.y = originalSize.height - (editCropRect.size.height + editCropRect.origin.y);
            break;
 
        case UIImageOrientationLeft:    //EXIF 6
            // fix info for original image.
            originalSize.width = originalSize.height;
            originalSize.height = oldOriginalW;
 
            // fix crop rect
			tmp = editCropRect.size.height;
			editCropRect.size.height = editCropRect.size.width;
			editCropRect.size.width = tmp;
 
            // rotation to the left
            editCropRect.origin.y = originalSize.height - (editCropRect.origin.x + editCropRect.size.height);
            editCropRect.origin.x = oldY;
            break;
 
        case UIImageOrientationRight:   //EXIF 8
            // fix info for original image.
            originalSize.width = originalSize.height;
            originalSize.height = oldOriginalW;
 
            // fix crop rect
			tmp = editCropRect.size.height;
			editCropRect.size.height = editCropRect.size.width;
			editCropRect.size.width = tmp;
 
            // rotate to the right
            editCropRect.origin.y = editCropRect.origin.x;
            editCropRect.origin.x = originalSize.height - oldY;
            break;
 
        default:
            break;
    }
 
    // 2.5. make the damn thing square if it's ALMOST square
    if (fabs((editCropRect.size.height - editCropRect.size.width) / fminf(originalSize.height, originalSize.width)) < 0.0295){
        editCropRect.size.width = fminf(editCropRect.size.width, editCropRect.size.height);
        editCropRect.size.height = editCropRect.size.width;
    }
 
    // 3. Crop image using crop rect
    UIGraphicsBeginImageContext(desiredSize);
	CGContextRef context = UIGraphicsGetCurrentContext();
	CGImageRef image = CGImageCreateWithImageInRect([originalImage CGImage], editCropRect);
    CGRect imageRect = CGRectMake(0.0f, 0.0f, desiredSize.width, desiredSize.height);
 
    // Image width < Image height. Just center vertically
    if (editCropRect.size.width / editCropRect.size.height < 1){
        imageRect.origin.x = (desiredSize.width - editCropRect.size.width * desiredSize.height/editCropRect.size.height)/2;
        imageRect.size.width -= imageRect.origin.x * 2;
 
    // Image width > Image height
    } else if (editCropRect.size.width / editCropRect.size.height > 1){
        float extraHeight = desiredSize.height - editCropRect.size.height * (desiredSize.width / editCropRect.size.width);
 
        // If the crop rect's origin is at the top of the screen, some of it might be clear (IE, the user may
        // have dragged "too far" and have some white space at the top of the preview box
        if (editCropRect.origin.y == 0) {
            imageRect.size.height -= extraHeight;
            if (roundf(editCropRect.size.height) == roundf(originalSize.height))
                imageRect.origin.y = extraHeight / 2;
            else
                imageRect.origin.y = 0;
 
        // User dragged "too far" down, and white space is visible at the bottom of preview box
        } else if (fabs(editCropRect.origin.y - (originalSize.height - roundf(editCropRect.size.height))) <= 1.1) {
            imageRect.origin.y = extraHeight;
            imageRect.size.height -= extraHeight;
 
        }else {
            imageRect.origin.y = (desiredSize.height - editCropRect.size.height * desiredSize.width/editCropRect.size.width)/2;
            imageRect.size.height -= imageRect.origin.y * 2;
        }
    }
 
    CGContextClearRect(context, CGRectMake(0,0,desiredSize.width,desiredSize.height));
	CGContextDrawImage(context, imageRect, image);
	UIImage* croppedImage = UIGraphicsGetImageFromCurrentImageContext();
	UIGraphicsEndImageContext();
	CGImageRelease(image);
 
    // 4. Perform image rotation
    UIImage * finalImage = [self rotateImage: croppedImage byOrientationFlag: originalOrientation];
 
    // DO SOMETHING WITH finalImage!
}
 
#pragma mark Convenience Functions for Image Picking
 
- (UIImage*)rotateImage:(UIImage*)img byOrientationFlag:(UIImageOrientation)orient
{
	CGImageRef          imgRef = img.CGImage;
	CGFloat             width = CGImageGetWidth(imgRef);
	CGFloat             height = CGImageGetHeight(imgRef);
	CGAffineTransform   transform = CGAffineTransformIdentity;
	CGRect              bounds = CGRectMake(0, 0, width, height);
    CGSize              imageSize = bounds.size;
	CGFloat             boundHeight;
 
	switch(orient) {
 
		case UIImageOrientationUp: //EXIF = 1
			transform = CGAffineTransformIdentity;
			break;
 
		case UIImageOrientationDown: //EXIF = 3
			transform = CGAffineTransformMakeTranslation(imageSize.width, imageSize.height);
			transform = CGAffineTransformRotate(transform, M_PI);
			break;
 
		case UIImageOrientationLeft: //EXIF = 6
			boundHeight = bounds.size.height;
			bounds.size.height = bounds.size.width;
			bounds.size.width = boundHeight;
			transform = CGAffineTransformMakeTranslation(imageSize.height, imageSize.width);
			transform = CGAffineTransformScale(transform, -1.0, 1.0);
			transform = CGAffineTransformRotate(transform, 3.0 * M_PI / 2.0);
			break;
 
		case UIImageOrientationRight: //EXIF = 8
			boundHeight = bounds.size.height;
			bounds.size.height = bounds.size.width;
			bounds.size.width = boundHeight;
			transform = CGAffineTransformMakeTranslation(0.0, imageSize.width);
			transform = CGAffineTransformRotate(transform, 3.0 * M_PI / 2.0);
			break;
 
		default:
            // image is not auto-rotated by the photo picker, so whatever the user
            // sees is what they expect to get. No modification necessary
            transform = CGAffineTransformIdentity;
            break;
 
	}
 
	UIGraphicsBeginImageContext(bounds.size);
	CGContextRef context = UIGraphicsGetCurrentContext();
 
    if ((orient == UIImageOrientationDown) || (orient == UIImageOrientationRight) || (orient == UIImageOrientationUp)){
        // flip the coordinate space upside down
        CGContextScaleCTM(context, 1, -1);
        CGContextTranslateCTM(context, 0, -height);
    }
 
	CGContextConcatCTM(context, transform);
	CGContextDrawImage(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, width, height), imgRef);
	UIImage *imageCopy = UIGraphicsGetImageFromCurrentImageContext();
	UIGraphicsEndImageContext();
 
	return imageCopy;
}

I hope that it saves you time – please leave a comment below if you find it useful! Also – I’ve attached a ZIP file with eight images that can be used to debug problems with EXIF orientation handling. Each image has a different EXIF orientation flag value, and a giant number in the center of the image lets you know what it is. On the Mac desktop, all eight will appear to be vertical because QuickLook properly adjusts them based on their EXIF values. Other apps, like Fireworks, will open them sideways, upside-down, backwards, etc… Enjoy!

Download EXIF Orientation Sample Images (44MB)