Swift: Custom Camera Save Modified Metadata with Image

Swift: Custom camera save modified metadata with image

I ended up figuring out how to get everything to work the way I needed it to. The thing that helped me the most was finding out that a CFDictionary can be cast as a NSMutableDictionary.

Here is my final code:

As you can see I add a property to the EXIF dictionary for the date digitized, and changed the orientation value.

private func snapPhoto(success: (UIImage, NSMutableDictionary) -> Void, errorMessage: String -> Void) {
guard !self.stillImageOutput.capturingStillImage,
let videoConnection = stillImageOutput.connectionWithMediaType(AVMediaTypeVideo) else { return }

videoConnection.fixVideoOrientation()

stillImageOutput.captureStillImageAsynchronouslyFromConnection(videoConnection) {
(imageDataSampleBuffer, error) -> Void in
guard imageDataSampleBuffer != nil && error == nil else {
errorMessage("Couldn't snap photo")
return
}

let data = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(imageDataSampleBuffer)

let rawMetadata = CMCopyDictionaryOfAttachments(nil, imageDataSampleBuffer, CMAttachmentMode(kCMAttachmentMode_ShouldPropagate))
let metadata = CFDictionaryCreateMutableCopy(nil, 0, rawMetadata) as NSMutableDictionary

let exifData = metadata.valueForKey(kCGImagePropertyExifDictionary as String) as? NSMutableDictionary
exifData?.setValue(NSDate().toString("yyyy:MM:dd HH:mm:ss"), forKey: kCGImagePropertyExifDateTimeDigitized as String)

metadata.setValue(exifData, forKey: kCGImagePropertyExifDictionary as String)
metadata.setValue(1, forKey: kCGImagePropertyOrientation as String)

guard let image = UIImage(data: data)?.fixOrientation() else {
errorMessage("Couldn't create image")
return
}

success(image, metadata)
}
}

And my final code for saving the image with the metadata:

Lots of guard statements, which I hate, but it is better than force unwrapping.

func saveImage(withMetadata image: UIImage, metadata: NSMutableDictionary) {
let filePath = "\(self.documentsPath)/image1.jpg"

guard let jpgData = UIImageJPEGRepresentation(image, 1) else { return }

// Add metadata to jpgData
guard let source = CGImageSourceCreateWithData(jpgData, nil),
let uniformTypeIdentifier = CGImageSourceGetType(source) else { return }
let finalData = NSMutableData(data: jpgData)
guard let destination = CGImageDestinationCreateWithData(finalData, uniformTypeIdentifier, 1, nil) else { return }
CGImageDestinationAddImageFromSource(destination, source, 0, metadata)
guard CGImageDestinationFinalize(destination) else { return }

// Save image that now has metadata
self.fileService.save(filePath, data: finalData)
}

Here is my updated save method (Not the exact same that I was using when I wrote this question, since I have updated to Swift 2.3, but the concept is the same):

public func save(fileAt path: NSURL, with data: NSData) throws -> Bool {
guard let pathString = path.absoluteString else { return false }
let directory = (pathString as NSString).stringByDeletingLastPathComponent

if !self.fileManager.fileExistsAtPath(directory) {
try self.makeDirectory(at: NSURL(string: directory)!)
}

if self.fileManager.fileExistsAtPath(pathString) {
try self.delete(fileAt: path)
}

return self.fileManager.createFileAtPath(pathString, contents: data, attributes: [NSFileProtectionKey: NSFileProtectionComplete])
}

Save CUSTOM metadata in an image taken from AVFoundation in iOS

UIImage and CIImage don't hold all the metadata. In fact, converting it from NSData to either of those two formats will strip a lot of that metadata out, so IMHO, UIImage and CIImage should only be used if you are planning on displaying it in the UI or passing it to CoreImage.

You can read the image properties like this:

- (NSDictionary *)getImageProperties:(NSData *)imageData {
// get the original image metadata
CGImageSourceRef imageSourceRef = CGImageSourceCreateWithData((__bridge CFDataRef) imageData, NULL);
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSourceRef, 0, NULL);
NSDictionary *props = (__bridge_transfer NSDictionary *) properties;
CFRelease(imageSourceRef);

return props;
}

You can then convert your NSDictionary to a NSMutableDictionary:

NSDictionary *props = [self getImageProperties:imageData];
NSMutableDictionary *mutableDictionary = [NSMutableDictionary dictionaryWithDictionary:props];

After that, add whatever fields you would like. Note that EXIF data is found by grabbing the kCGImagePropertyExifDictionary, GPS data by the kCGImagePropertyGPSDictionary, and so on:

NSMutableDictionary *exifDictionary = properties[(__bridge NSString *) kCGImagePropertyExifDictionary];

Look at the CGImageProperties header file for all the keys available.

Okay, so now that you have made whatever changes you needed to the metadata, adding it back takes a couple of more steps:

// incoming image data
NSData *imageData;

// create the image ref
CGDataProviderRef imgDataProvider = CGDataProviderCreateWithCFData((__bridge CFDataRef) imageData);
CGImageRef imageRef = CGImageCreateWithJPEGDataProvider(imgDataProvider, NULL, true, kCGRenderingIntentDefault);

// the updated properties from above
NSMutableDictionary *mutableDictionary;

// create the new output data
CFMutableDataRef newImageData = CFDataCreateMutable(NULL, 0);
// my code assumes JPEG type since the input is from the iOS device camera
CFStringRef type = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge CFStringRef) @"image/jpg", kUTTypeImage);
// create the destination
CGImageDestinationRef destination = CGImageDestinationCreateWithData(newImageData, type, 1, NULL);
// add the image to the destination
CGImageDestinationAddImage(destination, imageRef, (__bridge CFDictionaryRef) mutableDictionary);
// finalize the write
CGImageDestinationFinalize(destination);

// memory cleanup
CGDataProviderRelease(imgDataProvider);
CGImageRelease(imageRef);
CFRelease(type);
CFRelease(destination);

// your new image data
NSData *newImage = (__bridge_transfer NSData *)newImageData;

And there you have it.

Swift how to modify exif info in images taken from mobile camera

Yes! Finally I made a trick to modify the EXIF info. At first, you can get EXIF info from info[UIImagePickerControllerMediaMetadata] and NSData without EXIF from picked UIImage by UIImageJPEGRepresentation. Then, you can create a new NSDictionary with modified EXIF info. After that, call my function in the following, you can get image NSData with modified EXIF!

func saveImageWithImageData(data: NSData, properties: NSDictionary, completion: (data: NSData, path: NSURL) -> Void) {

let imageRef: CGImageSourceRef = CGImageSourceCreateWithData((data as CFDataRef), nil)!
let uti: CFString = CGImageSourceGetType(imageRef)!
let dataWithEXIF: NSMutableData = NSMutableData(data: data)
let destination: CGImageDestinationRef = CGImageDestinationCreateWithData((dataWithEXIF as CFMutableDataRef), uti, 1, nil)!

CGImageDestinationAddImageFromSource(destination, imageRef, 0, (properties as CFDictionaryRef))
CGImageDestinationFinalize(destination)

var paths: [AnyObject] = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
let savePath: String = paths[0].stringByAppendingPathComponent("exif.jpg")

let manager: NSFileManager = NSFileManager.defaultManager()
manager.createFileAtPath(savePath, contents: dataWithEXIF, attributes: nil)

completion(data: dataWithEXIF,path: NSURL(string: savePath)!)

print("image with EXIF info converting to NSData: Done! Ready to upload! ")

}

How to write exif metadata to an image (not the camera roll, just a UIImage or JPEG)

UIImage does not contain metadata information (it is stripped). So if you want to save it without using the imagepicker method (not in camera roll):

Follow the answer here to write to a file with the metadata intact:

Problem setting exif data for an image

no idea why would this be downvoted but here is the method:

In this case im getting the image through AVFoundation and this is what goes in the

[[self stillImageOutput] captureStillImageAsynchronouslyFromConnection:videoConnection 
completionHandler:^(CMSampleBufferRef imageSampleBuffer, NSError *error)
{
// code here
}

block code:

    CFDictionaryRef metaDict = CMCopyDictionaryOfAttachments(NULL, imageSampleBuffer, kCMAttachmentMode_ShouldPropagate);

CFMutableDictionaryRef mutable = CFDictionaryCreateMutableCopy(NULL, 0, metaDict);

// Create formatted date
NSTimeZone *timeZone = [NSTimeZone timeZoneWithName:@"UTC"];
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setTimeZone:timeZone];
[formatter setDateFormat:@"HH:mm:ss.SS"];

// Create GPS Dictionary
NSDictionary *gpsDict = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithFloat:fabs(loc.coordinate.latitude)], kCGImagePropertyGPSLatitude
, ((loc.coordinate.latitude >= 0) ? @"N" : @"S"), kCGImagePropertyGPSLatitudeRef
, [NSNumber numberWithFloat:fabs(loc.coordinate.longitude)], kCGImagePropertyGPSLongitude
, ((loc.coordinate.longitude >= 0) ? @"E" : @"W"), kCGImagePropertyGPSLongitudeRef
, [formatter stringFromDate:[loc timestamp]], kCGImagePropertyGPSTimeStamp
, [NSNumber numberWithFloat:fabs(loc.altitude)], kCGImagePropertyGPSAltitude
, nil];

// The gps info goes into the gps metadata part

CFDictionarySetValue(mutable, kCGImagePropertyGPSDictionary, (__bridge void *)gpsDict);

// Here just as an example im adding the attitude matrix in the exif comment metadata

CMRotationMatrix m = att.rotationMatrix;
GLKMatrix4 attMat = GLKMatrix4Make(m.m11, m.m12, m.m13, 0, m.m21, m.m22, m.m23, 0, m.m31, m.m32, m.m33, 0, 0, 0, 0, 1);

NSMutableDictionary *EXIFDictionary = (__bridge NSMutableDictionary*)CFDictionaryGetValue(mutable, kCGImagePropertyExifDictionary);

[EXIFDictionary setValue:NSStringFromGLKMatrix4(attMat) forKey:(NSString *)kCGImagePropertyExifUserComment];

CFDictionarySetValue(mutable, kCGImagePropertyExifDictionary, (__bridge void *)EXIFDictionary);

NSData *jpeg = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageSampleBuffer] ;

After this code you will have your image in the jpeg nsdata and the correspoding dictionary for that image in the mutable cfdictionary.

All you have to do now is:

    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)jpeg, NULL);

CFStringRef UTI = CGImageSourceGetType(source); //this is the type of image (e.g., public.jpeg)

NSMutableData *dest_data = [NSMutableData data];

CGImageDestinationRef destination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)dest_data,UTI,1,NULL);

if(!destination) {
NSLog(@"***Could not create image destination ***");
}

//add the image contained in the image source to the destination, overidding the old metadata with our modified metadata
CGImageDestinationAddImageFromSource(destination,source,0, (CFDictionaryRef) mutable);

//tell the destination to write the image data and metadata into our data object.
//It will return false if something goes wrong
BOOL success = CGImageDestinationFinalize(destination);

if(!success) {
NSLog(@"***Could not create data from image destination ***");
}

//now we have the data ready to go, so do whatever you want with it
//here we just write it to disk at the same path we were passed

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0]; // Get documents folder
NSString *dataPath = [documentsDirectory stringByAppendingPathComponent:@"ImagesFolder"];

NSError *error;
if (![[NSFileManager defaultManager] fileExistsAtPath:dataPath])
[[NSFileManager defaultManager] createDirectoryAtPath:dataPath withIntermediateDirectories:NO attributes:nil error:&error]; //Create folder

// NSString *imageName = @"ImageName";

NSString *fullPath = [dataPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.jpg", name]]; //add our image to the path

[dest_data writeToFile:fullPath atomically:YES];

//cleanup

CFRelease(destination);
CFRelease(source);

Note how I'm not saving using the ALAssets but directly into a folder of my choice.

Btw most of this code can be found in the link I posted at first.

Modifing metadata from existing phAsset seems not working

Sorry this isn't a full answer but it covers one part of your question. I noticed you are placing the StarRating in the wrong place. You need to place it in a IPTC dictionary. Also the properties data is stored as strings. Given you have the imageProperties you can add the star rating as follows and read it back using the following two functions

func setIPTCStarRating(imageProperties : NSMutableDictionary, rating : Int) {
if let iptc = imageProperties[kCGImagePropertyIPTCDictionary] as? NSMutableDictionary {
iptc[kCGImagePropertyIPTCStarRating] = String(rating)
} else {
let iptc = NSMutableDictionary()
iptc[kCGImagePropertyIPTCStarRating] = String(rating)
imageProperties[kCGImagePropertyIPTCDictionary] = iptc
}
}

func getIPTCStarRating(imageProperties : NSMutableDictionary) -> Int? {
if let iptc = imageProperties[kCGImagePropertyIPTCDictionary] as? NSDictionary {
if let starRating = iptc[kCGImagePropertyIPTCStarRating] as? String {
return Int(starRating)
}
}
return nil
}

As the imageProperties you get from the image are not mutable you need to create a mutable copy of these properties first before you can call the functions above. When you create your image to save use the mutable properties in your call to CGImageDestinationAddImage()

if let mutableProperties = imageProperties.mutableCopy() as? NSMutableDictionary {
setIPTCStarRating(imageProperties:mutableProperties, rating:rating)
}

One other point you are creating an unnecessary UIImage. If you use CGImageDestinationAddImageFromSource() instead of CGImageDestinationAddImage() you can use the imageSource you created earlier instead of loading the image data into a UIImage.

Failure to save image with custom Metadata

I did some testing, and from what I can tell, the short answer is 'you can't do that.' It looks like the metadata has to conform to specific EXIF Metadata keys. You could look up the available TIFF Metadata keys and see if there are any values you want to set/overwrite. You could try, for example, using kCGImagePropertyTIFFImageDescription to store your data.

NSMutableDictionary *tiffDictionary= [[NSMutableDictionary alloc] init];
NSMutableDictionary *myMetadata = [[NSMutableDictionary alloc] init];
[tiffDictionary setObject:@"My Metadata" forKey:(NSString*)kCGImagePropertyTIFFImageDescription];
[myMetadata setObject:tiffDictionary forKey:(NSString*)kCGImagePropertyTIFFDictionary];

... and save myMetadata with the image.

For other keys, see this:
http://developer.apple.com/library/mac/#documentation/GraphicsImaging/Reference/CGImageProperties_Reference/Reference/reference.html

Otherwise, what I would do is create an NSDictionary that uses an image's unique identifier as a key, and store the metadata object as the value. Save/Load this NSDictionary whenever you save/load an image.



Related Topics



Leave a reply



Submit