Напишите 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. Я построил простой проект, используя этот код:

https://github.com/5teev/MetaPhotoSave

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

https://gist.github.com/phildow/6043486

Вот небольшой вариант ответа @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.

Я написал об этой теме здесь.

Другие вопросы по тегам