Для обладателей iOS-устройств существует огромное количество web-сервисов, предоставляющих возможность публиковать фотографии на своих ресурсах. За примерами долго ходить не надо. Это и социальные сети ВКонтакте, Фейсбук — сервисы, если можно выразиться, широкого профиля, приложения которых установлены почти у всех пользователей. Так и узкоспециализированные, например, — FourSquare, Path.
Таких сервисов полно и для многих из них существует открытое API, с помощью которого сторонные разработчики (а это мы с вами) могут реализовывать приложения или их отдельные части, взаимодействующие с сервисом. Написать код, который достает из фотоальбомов снимки или делает новый снимок довольно просто. Рассмотрим первый вариант.
В .h-файле вашего контроллера:
@interface MYViewController : UIViewController<UIImagePickerControllerDelegate, UINavigationControllerDelegate>
...
@end
В .m-файле вашего контроллера:
@implementation MYViewController
- (IBAction)pickupImage:(id)sender
{
UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init];
imagePicker.delegate = self;
imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
[self presentModalViewController:imagePicker animated:YES];
[imagePicker release];
}
#pragma mark - UIImagePickerControllerDelegate
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
UIImage *image = [info objectForKey:UIImagePickerControllerOriginalImage];
// Используем полученный UIImage тут
// [serviceAPI postWithText:@"Чудесная погода!" withImage:image];
[picker dismissModalViewControllerAnimated:YES];
}
@end
Как-то так. Все просто, вы уже такое проделывали либо точно читали об этом в книжках. Все работает хорошо, пока в один прекрасный день web-сервис не начинает обрабатывать EXIF-данные снимков. Например, начинает выводить под каждой фотографией координаты места съемки и тип фотокамеры, на которую был сделан снимок, ориентацию фотографии. И тут оказывается, что никакие EXIF-данные вы и не отправляете.
В имени константы UIImagePickerControllerOriginalImage
Original не совсем Original. Оригинальный он лишь с той точки зрения, что снимок вам отдается целиком от точки (0,0) до точки (ширина-1, высота-1), поскольку есть возможность его обрезать перед тем, как будет вызван метод делегата, что осуществляется установкой флага
imagePicker.allowsEditing = YES
Выглядеть на устройстве будет так:
И так, теперь нам необходимо изменить код таким образом, чтобы получить доступ к исходному файлу фотографии, в котором помимо пикселей содержится метаинформация. На помощь нам приходит ключ UIImagePickerControllerReferenceURL и фреймворк AssetsLibrary.
Начиная с iOS SDK 4.1 параметр info
, который нам приходит в методе делегата, содержит значение по ключу UIImagePickerControllerReferenceURL в случае, когда мы обращаемся к фотографии из альбомов. Значение по этому ключу — ссылка на ассет (asset), файл, который нам нужен.
Код метода делегата будет выглядет так:
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
UIImage *image = [info objectForKey:UIImagePickerControllerOriginalImage];
UIImage *assetURL = [info objectForKey:UIImagePickerControllerReferenceURL];
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
[library assetForURL:assetURL
resultBlock:^(ALAsset *asset) {
CLLocation *location = [asset valueForProperty:ALAssetPropertyLocation];
// [serviceAPI postWithText:@"Чудесная погода!" withImage:image withMetadata:...];
[library autorelease];
}
failureBlock:^(NSError *error) {
}];
[picker dismissModalViewControllerAnimated:YES];
}
CLLocation *location = [asset valueForProperty:ALAssetPropertyLocation];
— тут мы обращаемся к геопозиции фотографии. В файле <AssetsLibrary/ALAsset.h> мы можем просмотреть остальные ключи, доступные разработчикам.
Тут и далее мы рассматриваем геоотметки, работа с остальными данными проходит по аналогии.
Как вы понимаете, в конечном счете на сторону сервиса мы сделаем POST-запрос с бинарным представлением картинки, которые мы получим методом UIImageJPEGRepresentation, именно эти данные надо будет дополнить метаинформацией. На помощь нам приходит библитека iphone-exif, которая поднимет нас на более высокий уровень абстракции при работе с exif.
Код, добавляющий к бинарному представлению картинки, геоотметку выглядит так:
#import "EXF.h"
#import "EXFUtils.h"
+ (NSData *)populateData:(NSData *)data byLocation:(CLLocation *)location {
EXFJpeg *exfJpeg = [[[EXFJpeg alloc] init] autorelease];
[exfJpeg scanImageData:data]; // сканируем данные, запоминаем графическое представление и метаданные
// добавляем абсолютное значение широты
EXFGPSLoc* gpsLocLatitude = [[[EXFGPSLoc alloc] init] autorelease];
[self populateGPS:gpsLocLatitude byValue:[self locationArrayForValue:location.coordinate.latitude]];
[exfJpeg.exifMetaData addTagValue:gpsLocLatitude forKey:[NSNumber numberWithInt:EXIF_GPSLatitude]];
// добавляем абсолютное значение долготы
EXFGPSLoc* gpsLocLongitude = [[[EXFGPSLoc alloc] init] autorelease];
[self populateGPS:gpsLocLongitude byValue:[self locationArrayForValue:location.coordinate.longitude]];
[exfJpeg.exifMetaData addTagValue:gpsLocLongitude forKey:[NSNumber numberWithInt:EXIF_GPSLongitude]];
// добавляем "знак" широты
NSString *refLatitude = (location.coordinate.latitude < 0 ? @"S" : @"N");
[exfJpeg.exifMetaData addTagValue:refLatitude forKey:[NSNumber numberWithInt:EXIF_GPSLatitudeRef]];
// добавляем "знак" долготы
NSString *refLongitude = (location.coordinate.longitude < 0 ? @"W" : @"E");
[exfJpeg.exifMetaData addTagValue:refLongitude forKey:[NSNumber numberWithInt:EXIF_GPSLongitudeRef]];
NSMutableData *dataWithExif = [NSMutableData data];
[exfJpeg populateImageData:dataWithExif];
return dataWithExif;
}
Казалось бы, мы в шаге от успеха. Добавляем метаданные к NSData, а потом, проделывая обрутную операцию, получаем UIImage. И нам даже не придется изменять сигнатуры методов, которые принимают только UIImage без каких-либо намеков на exif.
Увы, так сделать не получится. И вот почему.
Во-первых, к сожалению, методы вроде [UIImage imageWithData:data] теряют все метаданные (геопозоцию, в нашем случае), которые хранятся в NSData. Так что не получится держать координаты прямо «внутри» бинарного представления картинки на уровне UIImage.
Во-вторых, вы можете проделывать различные манипуляции с картинкой, скажем, уменьшить ее размер. Обычно это делается с помощью функций CoreGraphics, которые точно не знают ни о каких метаданных.
Выход
Получается, нам придется сопровождать картинку и ее метаданные вдоль всего пути от ее получения до тех пор, пока мы не получим объект класса NSData и сформируем POST-запрос на сервер. И еще нам лучше все сделать так, чтобы не пришлось из-за такого сопровождения (UIImage + NSData-Exif) менять все сигнатуры методов и функций на этом пути.
Можно было бы, конечно, завести объект класса NSMutableDictionary, который бы хранил соответствие между UIImage и NSData. Но это ни к чему, такую связь можно добавить прямо к UIImage.
Aссоциированные объекты
Начиная с iOS 4.0, в рантайм языка была добавлена возможность добавлять ассоциации объектов — связь, которую вы устанавливаете во время работы программы. Подробнее тут. Объявление методов можно рассмотреть в <objc/runtime.h>
Работает это так:
Ассоциацию можно установить вызовом функции:
void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)
Значения последнего аргумента будут вам знакомы по атрибутам свойств ObjC-классов:
/* objc_setAssociatedObject() options */
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
typedef uintptr_t objc_AssociationPolicy;
Указатель на объект можно получить методом:
id objc_getAssociatedObject(id object, void *key)
Так что после получения объекта UIImage c помошью ALAssetsLibrary, нам нужно будет сделать следующее:
#import <objc/runtime.h>
// где-то в начале файла. или даже не в этом
char kUIImageExifKey;
...
objc_setAssociatedObject(<#image#>, &kUIImageExifKey, <#DATA#>, OBJC_ASSOCIATION_RETAIN);
...
Путь UIImage и метаданных будет следующим
1) Получить фотографию и метаданные к ней;
2) Проассоциировать эти два объекта;
3) При каждом обновлении картинки сохранять ассоциацию с наиболее актуальным объектом UIImage (картинка может быть уменьшена, наложен фильтр и пр).
4) Непосредственно перед отправкой бинарных данных воспользоваться библитекой iphone-exif.
Это все.
Если будут просьбы, можно будет рассмотреть работу с только что сделанной фотографией и оформить это в «боевой» код, и выложить на github.
Автор: Silf