Напишите UIImage вместе с метаданными (EXIF, GPS, TIFF) в библиотеке фотографий iPhone
Я занимаюсь разработкой проекта, требования которого следующие: - Пользователь откроет камеру через приложение. - После захвата изображения некоторые данные будут добавлены к метаданным захваченного изображения. Я прошел через некоторые форумы. Я пытался закодировать эту логику. Я думаю, я дошел до сути, но чего-то не хватает, так как я не могу видеть метаданные, которые я добавляю к изображению. Мой код:
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingImage:(UIImage *)image editingInfo:(NSDictionary *)dictionary
{
[picker dismissModalViewControllerAnimated:YES];
NSData *dataOfImageFromGallery = UIImageJPEGRepresentation (image,0.5);
NSLog(@"Image length: %d", [dataOfImageFromGallery length]);
CGImageSourceRef source;
source = CGImageSourceCreateWithData((CFDataRef)dataOfImageFromGallery, NULL);
NSDictionary *metadata = (NSDictionary *) CGImageSourceCopyPropertiesAtIndex(source, 0, NULL);
NSMutableDictionary *metadataAsMutable = [[metadata mutableCopy]autorelease];
[metadata release];
NSMutableDictionary *EXIFDictionary = [[[metadataAsMutable objectForKey:(NSString *)kCGImagePropertyExifDictionary]mutableCopy]autorelease];
NSMutableDictionary *GPSDictionary = [[[metadataAsMutable objectForKey:(NSString *)kCGImagePropertyGPSDictionary]mutableCopy]autorelease];
if(!EXIFDictionary)
{
//if the image does not have an EXIF dictionary (not all images do), then create one for us to use
EXIFDictionary = [NSMutableDictionary dictionary];
}
if(!GPSDictionary)
{
GPSDictionary = [NSMutableDictionary dictionary];
}
//Setup GPS dict -
//I am appending my custom data just to test the logic……..
[GPSDictionary setValue:[NSNumber numberWithFloat:1.1] forKey:(NSString*)kCGImagePropertyGPSLatitude];
[GPSDictionary setValue:[NSNumber numberWithFloat:2.2] forKey:(NSString*)kCGImagePropertyGPSLongitude];
[GPSDictionary setValue:@"lat_ref" forKey:(NSString*)kCGImagePropertyGPSLatitudeRef];
[GPSDictionary setValue:@"lon_ref" forKey:(NSString*)kCGImagePropertyGPSLongitudeRef];
[GPSDictionary setValue:[NSNumber numberWithFloat:3.3] forKey:(NSString*)kCGImagePropertyGPSAltitude];
[GPSDictionary setValue:[NSNumber numberWithShort:4.4] forKey:(NSString*)kCGImagePropertyGPSAltitudeRef];
[GPSDictionary setValue:[NSNumber numberWithFloat:5.5] forKey:(NSString*)kCGImagePropertyGPSImgDirection];
[GPSDictionary setValue:@"_headingRef" forKey:(NSString*)kCGImagePropertyGPSImgDirectionRef];
[EXIFDictionary setValue:@"xml_user_comment" forKey:(NSString *)kCGImagePropertyExifUserComment];
//add our modified EXIF data back into the image’s metadata
[metadataAsMutable setObject:EXIFDictionary forKey:(NSString *)kCGImagePropertyExifDictionary];
[metadataAsMutable setObject:GPSDictionary forKey:(NSString *)kCGImagePropertyGPSDictionary];
CFStringRef UTI = CGImageSourceGetType(source);
NSMutableData *dest_data = [NSMutableData data];
CGImageDestinationRef destination = CGImageDestinationCreateWithData((CFMutableDataRef) dest_data, UTI, 1, NULL);
if(!destination)
{
NSLog(@"--------- Could not create image destination---------");
}
CGImageDestinationAddImageFromSource(destination, source, 0, (CFDictionaryRef) metadataAsMutable);
BOOL success = NO;
success = CGImageDestinationFinalize(destination);
if(!success)
{
NSLog(@"-------- could not create data from image destination----------");
}
UIImage * image1 = [[UIImage alloc] initWithData:dest_data];
UIImageWriteToSavedPhotosAlbum (image1, self, nil, nil);
}
Пожалуйста, помогите мне сделать это и получите что-то позитивное. Посмотрите на последнюю строку, сохраняю ли я изображение с метаданными? В этот момент изображение сохраняется, но метаданные, которые я к нему добавляю, не сохраняются.
Заранее спасибо.
7 ответов
Функция: UIImageWriteToSavePhotosAlbum
только пишет данные изображения.
Вы должны прочитать о ALAssetsLibrary
В конечном итоге вы хотите вызвать метод:
ALAssetsLibrary *library = [[ALAssetsLibrary alloc]
[library writeImageToSavedPhotosAlbum:metadata:completionBlock];
Apple обновила свою статью, посвященную этой проблеме (Технические вопросы и ответы QA1622). Если вы используете более старую версию XCode, у вас все еще может быть статья, в которой говорится, более или менее, неудача, вы не можете сделать это без низкоуровневого анализа данных изображения.
https://developer.apple.com/library/ios/
Я адаптировал код там следующим образом:
- (void) saveImage:(UIImage *)imageToSave withInfo:(NSDictionary *)info
{
// Get the assets library
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
// Get the image metadata (EXIF & TIFF)
NSMutableDictionary * imageMetadata = [[info objectForKey:UIImagePickerControllerMediaMetadata] mutableCopy];
// add GPS data
CLLocation * loc = <•••>; // need a location here
if ( loc ) {
[imageMetadata setObject:[self gpsDictionaryForLocation:loc] forKey:(NSString*)kCGImagePropertyGPSDictionary];
}
ALAssetsLibraryWriteImageCompletionBlock imageWriteCompletionBlock =
^(NSURL *newURL, NSError *error) {
if (error) {
NSLog( @"Error writing image with metadata to Photo Library: %@", error );
} else {
NSLog( @"Wrote image %@ with metadata %@ to Photo Library",newURL,imageMetadata);
}
};
// Save the new image to the Camera Roll
[library writeImageToSavedPhotosAlbum:[imageToSave CGImage]
metadata:imageMetadata
completionBlock:imageWriteCompletionBlock];
[imageMetadata release];
[library release];
}
и я называю это из
imagePickerController:didFinishPickingMediaWithInfo:
который является методом делегата для средства выбора изображений.
Я использую вспомогательный метод (адаптированный из GusUtils) для создания словаря метаданных GPS из местоположения:
- (NSDictionary *) gpsDictionaryForLocation:(CLLocation *)location
{
CLLocationDegrees exifLatitude = location.coordinate.latitude;
CLLocationDegrees exifLongitude = location.coordinate.longitude;
NSString * latRef;
NSString * longRef;
if (exifLatitude < 0.0) {
exifLatitude = exifLatitude * -1.0f;
latRef = @"S";
} else {
latRef = @"N";
}
if (exifLongitude < 0.0) {
exifLongitude = exifLongitude * -1.0f;
longRef = @"W";
} else {
longRef = @"E";
}
NSMutableDictionary *locDict = [[NSMutableDictionary alloc] init];
[locDict setObject:location.timestamp forKey:(NSString*)kCGImagePropertyGPSTimeStamp];
[locDict setObject:latRef forKey:(NSString*)kCGImagePropertyGPSLatitudeRef];
[locDict setObject:[NSNumber numberWithFloat:exifLatitude] forKey:(NSString *)kCGImagePropertyGPSLatitude];
[locDict setObject:longRef forKey:(NSString*)kCGImagePropertyGPSLongitudeRef];
[locDict setObject:[NSNumber numberWithFloat:exifLongitude] forKey:(NSString *)kCGImagePropertyGPSLongitude];
[locDict setObject:[NSNumber numberWithFloat:location.horizontalAccuracy] forKey:(NSString*)kCGImagePropertyGPSDOP];
[locDict setObject:[NSNumber numberWithFloat:location.altitude] forKey:(NSString*)kCGImagePropertyGPSAltitude];
return [locDict autorelease];
}
Пока это работает хорошо для меня на устройствах iOS4 и iOS5.
Обновление: и устройства iOS6/iOS7. Я построил простой проект, используя этот код:
Для тех, кто приезжает сюда, пытаясь сделать фотографию с помощью камеры в вашем приложении и сохранить файл изображения в рулон камеры с метаданными GPS, у меня есть решение Swift, использующее API фотографий, поскольку ALAssetsLibrary устарела с iOS 9.0.
Как уже упоминал Рикстер в этом ответе, API Фотографии не встраивает данные о местоположении непосредственно в файл изображения JPG, даже если вы установите свойство.location для нового ресурса.
Имеется пример буфера CMSampleBuffer buffer
, некоторые CLLocation location
и используя предложение Морти использовать CMSetAttachments
чтобы избежать дублирования изображения, мы можем сделать следующее. gpsMetadata
Метод расширения CLLocation можно найти здесь.
if let location = location {
// Get the existing metadata dictionary (if there is one)
var metaDict = CMCopyDictionaryOfAttachments(nil, buffer, kCMAttachmentMode_ShouldPropagate) as? Dictionary<String, Any> ?? [:]
// Append the GPS metadata to the existing metadata
metaDict[kCGImagePropertyGPSDictionary as String] = location.gpsMetadata()
// Save the new metadata back to the buffer without duplicating any data
CMSetAttachments(buffer, metaDict as CFDictionary, kCMAttachmentMode_ShouldPropagate)
}
// Get JPG image Data from the buffer
guard let imageData = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(buffer) else {
// There was a problem; handle it here
}
// Now save this image to the Camera Roll (will save with GPS metadata embedded in the file)
self.savePhoto(withData: imageData, completion: completion)
savePhoto
Метод ниже. Обратите внимание, что удобный addResource:with:data:options
метод доступен только в iOS 9. Если вы поддерживаете более раннюю iOS и хотите использовать API Photos, то вы должны создать временный файл, а затем создать ресурс из файла по этому URL, если вы хотите, чтобы метаданные GPS были правильно встроенный (PHAssetChangeRequest.creationRequestForAssetFromImage:atFileURL
). Только установка.location PHAsset НЕ будет встраивать ваши новые метаданные в сам файл.
func savePhoto(withData data: Data, completion: (() -> Void)? = nil) {
// Note that using the Photos API .location property on a request does NOT embed GPS metadata into the image file itself
PHPhotoLibrary.shared().performChanges({
if #available(iOS 9.0, *) {
// For iOS 9+ we can skip the temporary file step and write the image data from the buffer directly to an asset
let request = PHAssetCreationRequest.forAsset()
request.addResource(with: PHAssetResourceType.photo, data: data, options: nil)
request.creationDate = Date()
} else {
// Fallback on earlier versions; write a temporary file and then add this file to the Camera Roll using the Photos API
let tmpURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent("tempPhoto").appendingPathExtension("jpg")
do {
try data.write(to: tmpURL)
let request = PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: tmpURL)
request?.creationDate = Date()
} catch {
// Error writing the data; photo is not appended to the camera roll
}
}
}, completionHandler: { _ in
DispatchQueue.main.async {
completion?()
}
})
}
Кроме того: если вы просто хотите сохранить изображение с метаданными GPS во временные файлы или документы (в отличие от фотопленки / библиотеки фотографий), вы можете пропустить использование API фотографий и напрямую записать imageData в URL.
// Write photo to temporary files with the GPS metadata embedded in the file
let tmpURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent("tempPhoto").appendingPathExtension("jpg")
do {
try data.write(to: tmpURL)
// Do more work here...
} catch {
// Error writing the data; handle it here
}
Получение метаданных с изображения, снятого камерой, в приложении:
UIImage *pTakenImage= [info objectForKey:@"UIImagePickerControllerOriginalImage"];
NSMutableDictionary *imageMetadata = [[NSMutableDictionary alloc] initWithDictionary:[info objectForKey:UIImagePickerControllerMediaMetadata]];
Теперь для сохранения изображения в библиотеку с извлеченными метаданными:
ALAssetsLibrary* library = [[ALAssetsLibrary alloc] init];
[library writeImageToSavedPhotosAlbum:[sourceImage CGImage] metadata:imageMetadata completionBlock:Nil];
[library release];
или хотите сохранить в локальном каталоге
CGImageDestinationAddImageFromSource(destinationPath,sourceImage,0, (CFDictionaryRef)imageMetadata);
Часть этого включает создание метаданных GPS. Вот категория на CLLocation, чтобы сделать именно это:
Вот небольшой вариант ответа @matt.
Следующий код использует только одно CGImageDestination и, что более интересно, позволяет сохранять в формате HEIC на iOS11+.
Обратите внимание, что качество сжатия добавляется к метаданным перед добавлением изображения. 0.8 - это примерно качество сжатия оригинального сохранения камеры.
//img is the UIImage and metadata the metadata received from the picker
NSMutableDictionary *meta_plus = metadata.mutableCopy;
//with CGimage, one can set compression quality in metadata
meta_plus[(NSString *)kCGImageDestinationLossyCompressionQuality] = @(0.8);
NSMutableData *img_data = [NSMutableData new];
NSString *type;
if (@available(iOS 11.0, *)) type = AVFileTypeHEIC;
else type = @"public.jpeg";
CGImageDestinationRef dest = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)img_data, (__bridge CFStringRef)type, 1, nil);
CGImageDestinationAddImage(dest, img.CGImage, (__bridge CFDictionaryRef)meta_plus);
CGImageDestinationFinalize(dest);
CFRelease(dest); //image is in img_data
//go for the PHLibrary change request
Проблема, которую мы пытаемся решить: пользователь только что сделал снимок с помощью камеры UIImagePickerController. То, что мы получаем, это UIImage. Как мы складываем метаданные в этот UIImage, когда мы сохраняем его в рулон камеры (фото-библиотеку), теперь, когда у нас нет инфраструктуры AssetsLibrary?
Ответ (насколько я могу разобрать): использовать каркас ImageIO. Извлеките данные JPEG из UIImage, используйте их в качестве источника, запишите их и словарь метаданных в место назначения и сохраните данные места назначения как PHAsset в рулоне камеры.
В этом примере im
это UIImage и meta
это словарь метаданных:
let jpeg = UIImageJPEGRepresentation(im, 1)!
let src = CGImageSourceCreateWithData(jpeg as CFData, nil)!
let data = NSMutableData()
let uti = CGImageSourceGetType(src)!
let dest = CGImageDestinationCreateWithData(data as CFMutableData, uti, 1, nil)!
CGImageDestinationAddImageFromSource(dest, src, 0, meta)
CGImageDestinationFinalize(dest)
let lib = PHPhotoLibrary.shared()
lib.performChanges({
let req = PHAssetCreationRequest.forAsset()
req.addResource(with: .photo, data: data as Data, options: nil)
})
Хороший способ проверить - и распространенный вариант использования - это получить метаданные фотографии от делегата UIImagePickerController. info
словарь через UIImagePickerControllerMediaMetadata
ключ и сложите его в PHAsset, как мы сохраняем его в библиотеку фотографий.
Есть много структур, которые работают с изображениями и метаданными.
Структура ресурсов устарела и заменена структурой библиотеки фотографий. Если вы реализовали AVCapturePhotoCaptureDelegate
чтобы сделать снимок, вы можете сделать это:
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
var metadata = photo.metadata
metadata[kCGImagePropertyGPSDictionary as String] = gpsMetadata
photoData = photo.fileDataRepresentation(withReplacementMetadata: metadata,
replacementEmbeddedThumbnailPhotoFormat: photo.embeddedThumbnailPhotoFormat,
replacementEmbeddedThumbnailPixelBuffer: nil,
replacementDepthData: photo.depthData)
...
}
Метаданные - это словарь словарей, и вы должны ссылаться на CGImageProperties.
Я написал об этой теме здесь.