UIImage from ALAsset: getting the right orientation

Published on — Filed under protip, cocoade

One of the most used features on Droplr 2.0 for iPhone is the option to upload the last photo (from the photo library).

To do that, you just need to fetch the asset group of type ALAssetsGroupSavedPhotos...

ALAssetsLibrary* library = [[ALAssetsLibrary alloc] init];
[library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos
                       usingBlock:enumerationBlock
                     failureBlock:^(NSError *error) {
  // handle error
}];

... and within that group, fetch the ALAsset at the last index:

void (^enumerationBlock)(ALAssetsGroup*, BOOL*);
enumerationBlock = ^(ALAssetsGroup* group, BOOL *stop) {
  [group setAssetsFilter:[ALAssetsFilter allPhotos]];

  // Get the photo at the last index
  NSUInteger index = [group numberOfAssets] - 1;
  NSIndexSet *lastPhotoIndexSet = [NSIndexSet indexSetWithIndex:index];
  [group enumerateAssetsAtIndexes:lastPhotoIndexSet
                          options:0 usingBlock:selectionBlock];
};

Finally, you need a block to handle the assets selected (should be just 1):

void (^selectionBlock)(ALAsset*, NSUInteger, BOOL*);
selectionBlock = ^(ALAsset *asset, NSUInteger index, BOOL *innerStop) {
  // The end of the enumeration is signaled by asset == nil.
  if (asset == nil) return;

  ALAssetRepresentation* representation = [asset defaultRepresentation];

  // Retrieve the image orientation from the ALAsset
  UIImageOrientation orientation = ...;

  CGFloat scale  = 1;
  UIImage *image = [UIImage
                    imageWithCGImage:[representation fullResolutionImage]
                    scale:scale orientation:orientation];

  // do something with the image
};

Figuring out the correct orientation for the image

Being one of those LastMinute™ features, I had the following code to fix the orientation of the image:

// Retrieve the image orientation from the EXIF data
UIImageOrientation orientation = UIImageOrientationUp;
NSNumber *orientationValue = [[representation metadata]
                              objectForKey:@"Orientation"];
if (orientationValue != nil) {
  orientation = [orientationValue intValue];
}

While the above code is apparently correct, it doesn't work — surprisingly, we never ran into any issues during the test phase...

Using the int value of the Orientation property reported by the EXIF metadata as the UIImageOrientation value, the image would get all borked, incorrectly rotated.

Figuring out the correct orientation for the image, done right

Turns out there's an ALAssetPropertyOrientation property in the ALAsset that has the correct value to be used as UIImageOrientation.

// Retrieve the image orientation from the ALAsset
UIImageOrientation orientation = UIImageOrientationUp;
NSNumber *orientationValue;
orientationValue = [asset valueForProperty:@"ALAssetPropertyOrientation"];
if (orientationValue != nil) {
  orientation = [orientationValue intValue];
}

Putting it all together

ALAssetsLibrary* library = [[ALAssetsLibrary alloc] init];

// Block called for every asset selected
void (^selectionBlock)(ALAsset*, NSUInteger, BOOL*);
selectionBlock = ^(ALAsset *asset, NSUInteger index, BOOL *innerStop) {
  // The end of the enumeration is signaled by asset == nil.
  if (asset == nil) return;

  ALAssetRepresentation* representation = [asset defaultRepresentation];

  // Retrieve the image orientation from the ALAsset
  UIImageOrientation orientation = UIImageOrientationUp
  NSNumber *orientationVal;
  orientationVal = [asset valueForProperty:@"ALAssetPropertyOrientation"];
  if (orientationVal != nil) {
    orientation = [orientationVal intValue];
  }

  CGFloat scale  = 1;
  UIImage* image = [UIImage
                    imageWithCGImage:[representation fullResolutionImage]
                    scale:scale orientation:orientation];

  // do something with the image
};

// Block called when enumerating asset groups
void (^enumerationBlock)(ALAssetsGroup*, BOOL*);
enumerationBlock = ^(ALAssetsGroup *group, BOOL *stop) {
  // Within the group enumeration block, filter to enumerate just photos.
  [group setAssetsFilter:[ALAssetsFilter allPhotos]];

  // Get the photo at the last index
  NSUInteger index = [group numberOfAssets] - 1;
  NSIndexSet *lastPhotoIndexSet = [NSIndexSet indexSetWithIndex:index];
  [group enumerateAssetsAtIndexes:lastPhotoIndexSet
                          options:0 usingBlock:selectionBlock];
};

// Enumerate just the photos and videos group by
// using ALAssetsGroupSavedPhotos.
[library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos
                       usingBlock:enumerationBlock
                     failureBlock:^(NSError *error) {
  // handle error
}];

Don't forget to add AssetsLibrary.framework to your project and:

#import <AssetsLibrary/AssetsLibrary.h>

Caveat: Location Services and ALAsset

By design, images retrieved via UIImagePickerController don't bear any metadata (apart from the correct orientation). This makes some sense, as it prevents applications from reading sensitive information — namely GPS data — on your library items' metadata.

Accessing these items via ALAssetLibrary, however, will give your app unrestricted access to metadata. Thus, iOS will present a UIAlertView to the user on behalf of your app, asking for permissions to access Location Services

Yup, the app will need Location Services access. This means that both Location Services must be enabled and your app authorized to use it. It does make sense as it would be possible for an application to know where you've been just by analyzing your photos.

Might be a little awkward to the user, though; getting a request to access location services when all he wants is to upload a photo...