Écrire UIImage avec les métadonnées (EXIF, GPS, TIFF) dans la photothèque de l'iPhone
je développe un projet, où les exigences sont: - L'utilisateur ouvrira la caméra à travers l'application - Lors de la capture d'une Image, certaines données seront ajoutées aux métadonnées de l'image capturée. Je suis passé par quelques-uns des forums. J'ai essayé de code de cette logique. Je suppose que j'en suis arrivé là, mais il manque quelque chose, car je ne suis pas en mesure de voir les métadonnées que je joins à l'image. Mon code est:
- (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);
}
Aidez-moi à faire et pour obtenir quelque chose de positif. Regardez la dernière ligne, je suis l'enregistrement de l'image avec mon métadonnées? L'image est sauvegardée à ce moment-là, mais les métadonnées que j'y ajoute ne sont pas sauvegardées.
Merci d'avance.
7 réponses
la fonction: UIImageWriteToSavePhotosAlbum
n'écrit que les données de l'image.
vous devez lire sur le ALAssetsLibrary
la méthode que vous voulez finalement appeler est:
ALAssetsLibrary *library = [[ALAssetsLibrary alloc]
[library writeImageToSavedPhotosAlbum:metadata:completionBlock];
Apple a mis à jour leur article traitant de cette question (Q&A technique QA1622). Si vous utilisez une ancienne version de Xcode, vous pouvez toujours avoir l'article qui dit, plus ou moins, pas de chance, vous ne pouvez pas le faire sans parsing de bas niveau des données d'image.
https://developer.apple.com/library/ios/#qa/qa1622/_index.html
j'y ai adapté le code comme suit:
- (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];
}
et j'appelle ce de
imagePickerController:didFinishPickingMediaWithInfo:
qui est la méthode de délégué pour le capteur d'image.
j'utilise une méthode d'aide (adaptée de GusUtils ) pour construire un dictionnaire de métadonnées GPS à partir d'un emplacement:
- (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];
}
Jusqu'à présent cela fonctionne bien pour moi sur les appareils iOS4 et iOS5.
Update : et dispositifs iOS6/iOS7. J'ai construit un projet simple en utilisant ce code:
pour toute personne qui vient ici en essayant de prendre une photo avec l'appareil photo dans votre application et en sauvegardant le fichier image sur le rouleau de l'appareil photo avec des métadonnées GPS, j'ai un solution Swift qui utilise le API Photos depuis Bibliothèque Alassetsli est déprécié à partir de iOS 9.0 .
comme mentionné par rickster sur ce réponse , L'API Photos ne not embed les données de localisation directement dans un fichier image JPG même si vous définissez le .emplacement propriété du nouvel actif.
avec un tampon D'échantillon CMSampleBuffer buffer
, quelques Cllocations location
, et en utilisant la suggestion de Morty pour utiliser CMSetAttachments
afin d'éviter la duplication de l'image, nous pouvons faire ce qui suit. La méthode gpsMetadata
étendant CLLocation peut être trouvée ici .
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)
la méthode savePhoto
est décrite ci-dessous. Notez que la méthode pratique addResource:with:data:options
n'est disponible qu'en iOS 9. Si vous supportez un iOS antérieur et que vous voulez utiliser L'API Photos, alors vous devez créer un fichier temporaire puis créer un actif à partir du fichier à cette URL si vous voulez avoir les métadonnées GPS correctement intégrées ( PHAssetChangeRequest.creationRequestForAssetFromImage:atFileURL
). Seule la mise en PHAsset .l'emplacement N'intégrera pas vos nouvelles métadonnées dans le fichier lui-même.
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?()
}
})
}
mis à part: Si vous voulez simplement sauvegarder l'image avec des métadonnées GPS dans vos fichiers ou documents temporaires (par opposition à la photothèque), vous pouvez sauter en utilisant l'API Photos et écrire directement l'imageData à une 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
}
une partie de ceci implique la génération des métadonnées GPS. Voici une catégorie sur CLLocation pour faire exactement cela:
Obtenir des méta-données à partir de cam image capturée au sein d'une application:
UIImage *pTakenImage= [info objectForKey:@"UIImagePickerControllerOriginalImage"];
NSMutableDictionary *imageMetadata = [[NSMutableDictionary alloc] initWithDictionary:[info objectForKey:UIImagePickerControllerMediaMetadata]];
maintenant pour sauvegarder l'image dans la bibliothèque avec les métadonnées extraites:
ALAssetsLibrary* library = [[ALAssetsLibrary alloc] init];
[library writeImageToSavedPhotosAlbum:[sourceImage CGImage] metadata:imageMetadata completionBlock:Nil];
[library release];
ou voulez sauvegarder dans le répertoire local
CGImageDestinationAddImageFromSource(destinationPath,sourceImage,0, (CFDictionaryRef)imageMetadata);
le problème que nous essayons de résoudre est que l'utilisateur vient de prendre une photo avec l'appareil photo UIImagePickerController. Ce que nous obtenons est une UIImage. Comment plier des métadonnées dans que UIImage comme nous l'enregistrer dans la pellicule (photothèque), maintenant que nous n'avons pas les AssetsLibrary cadre?
la réponse (pour autant que je sache) est: utilisez le cadre ImageIO. Extraire les données JPEG de L'UIImage, les utiliser comme source et les écrire ainsi que le dictionnaire de métadonnées dans la destination, et enregistrer les données de destination comme un PHAsset dans le rouleau de caméra.
dans cet exemple, im
est L'UIImage et meta
est le dictionnaire de métadonnées:
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)
})
une bonne façon de tester - et un cas d'utilisation commune - est de recevoir les métadonnées photo de l'UIImagePickerController délégué info
dictionnaire à travers le UIImagePickerControllerMediaMetadata
clé et le plier dans le châssis que nous enregistrons dans la photothèque.
il existe de nombreux cadres qui traitent de l'image et des métadonnées.
Actifs Cadre est obsolète et remplacé par une Bibliothèque de Photos-cadre. Si vous avez implémenté AVCapturePhotoCaptureDelegate
pour capturer des photos, vous pouvez le faire:
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)
...
}
Les métadonnées est un dictionnaire de dictionnaires, et vous devez vous référer à CGImageProperties .
j'ai écrit sur ce sujet ici .