5

I am currently using AVCaptureStillImageOutput to get a full resolution picture. I am also able to get the exif metadata using the following code:

[self.stillImageOutput captureStillImageAsynchronouslyFromConnection:videoConnection completionHandler: ^(CMSampleBufferRef imageSampleBuffer, NSError *error)
     { 

         CFDictionaryRef metaDict = CMCopyDictionaryOfAttachments(NULL, imageSampleBuffer, kCMAttachmentMode_ShouldPropagate);
         CFMutableDictionaryRef mutableDict = CFDictionaryCreateMutableCopy(NULL, 0, metaDict);

         NSLog(@"test attachments %@", mutableDict);

         // set the dictionary back to the buffer
         CMSetAttachments(imageSampleBuffer, mutableDict, kCMAttachmentMode_ShouldPropagate);

         NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageSampleBuffer];

         UIImage *image = [[UIImage alloc] initWithData:imageData];    
         [self.delegate frameReadyToSave:image withExifAttachments: mutableDict];
     }];

The metadata being located in the mutableDict variable. Now, I want to save this picture in two different places, with the metadata. I want to save it on the disk in the application folders and in the Photo Library.

Now, I tried to save the image, in another method, using the following (the image variable you see is a custom object):

NSData* imageData = UIImageJPEGRepresentation(image.image, 1.0f);

[imageData writeToFile:image.filePath atomically:YES];

UIImageWriteToSavedPhotosAlbum(image.image, nil, nil, nil);

Now, the image is properly saved but does not contain any Exif metadata.

From what I have read, I need to use the PHPhotoLibrary to do so but the documentation isn't too loquacious on that. Here's what I found:

[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
    PHAssetChangeRequest *createAssetRequest = [PHAssetChangeRequest creationRequestForAssetFromImage:image.image];      

} completionHandler:nil];

But how do I save the metadata with it?

Etienne Noël
  • 5,988
  • 6
  • 48
  • 75

2 Answers2

2

I would suggest you use ImageIO to accomplish that:

-(void)frameReadyToSave:(UIImage*)image withExifAttachments:(NSMutableDictionary*)mutableDict
{
    NSData* imageData = UIImageJPEGRepresentation(image, 1.0f);
    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef) imageData, NULL);
    __block NSURL* tmpURL = [NSURL fileURLWithPath:@"example.jpg"]; //modify to your needs
    CGImageDestinationRef destination = CGImageDestinationCreateWithURL((__bridge CFURLRef) tmpURL, kUTTypeJPEG, 1, NULL);
    CGImageDestinationAddImageFromSource(destination, source, 0, (__bridge CFDictionaryRef) mutableDict);
    CGImageDestinationFinalize(destination);
    CFRelease(source);
    CFRelease(destination);
    [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
        [PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:tmpURL];
    }   completionHandler:^(BOOL success, NSError *error) {
        //cleanup the tmp file after import, if needed
    }];
}
holtmann
  • 6,043
  • 32
  • 44
  • This saves the metadata in the EXIF part of the JPEG but it doesn't show up in Photos (on the Mac). – Ortwin Gentz May 03 '16 at 19:04
  • [PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:tmpURL] should on import read out the EXIF metadata data of the photo and populate the iCloud Photo Library database with the necessary data (recording date, location data). Does the photo appear in the iOS Photos App with the correct timestamp? – holtmann May 09 '16 at 21:15
  • Indeed, it works for date and location. It does not work however for title, description, and keywords. At least not with the EXIF fields I tried. – Ortwin Gentz May 09 '16 at 21:21
  • 1
    Title, description and keywords are not part of the iOS Photo Library database (and not editable on iOS - neither in the Photos App or as properties of PHAsset). It's correct that these properties are editable using the Photos App on the Mac. Title, description and keywords seem not to be synced over iCloud (yet). – holtmann May 10 '16 at 08:14
  • Is there no way to save photo directly to camera roll without creating a local file first? – Deepak Sharma May 13 '17 at 13:39
  • 1
    Did anyone find a solution for this? and if so, any way possible to save the image with its new metadata as uiimage before saving it in photo library? Thanks – Reza.Ab Jan 26 '18 at 02:30
1

Use this to merge metadata into image data and save it to Photo Library:

func saveImageData(data: Data, metadata: NSDictionary? = nil, album: PHAssetCollection, completion:((PHAsset?)->())? = nil) {
    var placeholder: PHObjectPlaceholder?

    PHPhotoLibrary.shared().performChanges({
        var changeRequest: PHAssetChangeRequest

        if let metadata = metadata {
            let newImageData = UIImage.mergeImageData(imageData: data, with: metadata)
            changeRequest = PHAssetCreationRequest.forAsset()
            (changeRequest as! PHAssetCreationRequest).addResource(with: .photo, data: newImageData as Data, options: nil)
        }
        else {
            changeRequest = PHAssetChangeRequest.creationRequestForAsset(from: UIImage(data: data)!)
        }

        guard let albumChangeRequest = PHAssetCollectionChangeRequest(for: album),
            let photoPlaceholder = changeRequest.placeholderForCreatedAsset else { return }

        placeholder = photoPlaceholder
        let fastEnumeration = NSArray(array: [photoPlaceholder] as [PHObjectPlaceholder])
        albumChangeRequest.addAssets(fastEnumeration)
    }, completionHandler: { success, error in
        guard let placeholder = placeholder else {
            completion?(nil)
            return
        }

        if success {
            let assets:PHFetchResult<PHAsset> =  PHAsset.fetchAssets(withLocalIdentifiers: [placeholder.localIdentifier], options: nil)
            let asset:PHAsset? = assets.firstObject
            completion?(asset)
        }
        else {
            completion?(nil)
        }
    })
}

func mergeImageData(imageData: Data, with metadata: NSDictionary) -> Data {
    let source: CGImageSource = CGImageSourceCreateWithData(imageData as NSData, nil)!
    let UTI: CFString = CGImageSourceGetType(source)!
    let newImageData =  NSMutableData()
    let cgImage = UIImage(data: imageData)!.cgImage
    let imageDestination: CGImageDestination = CGImageDestinationCreateWithData((newImageData as CFMutableData), UTI, 1, nil)!
    CGImageDestinationAddImage(imageDestination, cgImage!, metadata as CFDictionary)
    CGImageDestinationFinalize(imageDestination)

    return newImageData as Data
}
Clouds
  • 399
  • 1
  • 12