UIImage, EXIF и немного рантайма

в 6:53, , рубрики: exif, iOS, UIImage, Блог компании Luxoft, разработка под iOS, метки: , ,

image

Для обладателей 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

Выглядеть на устройстве будет так:
image

И так, теперь нам необходимо изменить код таким образом, чтобы получить доступ к исходному файлу фотографии, в котором помимо пикселей содержится метаинформация. На помощь нам приходит ключ 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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js