在iPhone的照片库中写入带有元数据(EXIF,GPS,TIFF)的UIImage

16

我正在开发一个项目,其要求如下: - 用户将通过应用程序打开摄像头 - 拍摄照片后,一些数据将被附加到所捕获的图像元数据中。 我查看了一些论坛。我尝试编写此逻辑。我想我已经达到了这一点,但是由于我无法看到我附加到图像的元数据,所以似乎还缺少某些内容。 我的代码是:

- (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);    
}
请帮我完成这个需求并得到一些正面的结果。 看看最后一行,我是不是正在保存有我的元数据的图像? 在那一点上图像已经被保存了,但是我附加的元数据没有被保存。 提前致谢。

你找到答案了吗? - Rushi trivedi
8个回答

13

苹果已更新他们的文章,解决了这个问题(技术 Q&A QA1622)。如果你使用的是旧版本的 Xcode,则可能仍有一篇文章说,或多或少地,很遗憾,如果不对图像数据进行低级解析,你无法完成此操作。

https://developer.apple.com/library/ios/#qa/qa1622/_index.html

我根据那里的代码进行了调整:

- (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


6

该函数:UIImageWriteToSavePhotosAlbum 仅写入图像数据。

您需要了解ALAssetsLibrary

您最终想要调用的方法是:

 ALAssetsLibrary *library = [[ALAssetsLibrary alloc]
 [library writeImageToSavedPhotosAlbum:metadata:completionBlock];

1
ALAssetsLibrary在iOS9中已被弃用。现在应使用PhotoKit - Mudlabs

5
如果有人想在您的应用程序中使用相机拍照,并将图像文件保存到带有GPS元数据的相机胶卷中,我有一个使用Photos APISwift解决方案,因为iOS 9.0起ALAssetsLibrary已被弃用

如rickster在答案中所述,即使您设置新资产的.location属性,Photos API也不会直接嵌入位置数据到JPG图像文件中。

给定一个 CMSampleBuffer 样本缓冲区 buffer,一些 CLLocation location,并使用 Morty 的 suggestion 来使用 CMSetAttachments 以避免复制图像,我们可以执行以下操作。扩展 CLLocation 的 gpsMetadata 方法可以在此处找到here
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 并且想要使用 Photos API,则必须创建一个临时文件,然后从该 URL 创建一个资产,以便正确嵌入 GPS 元数据(PHAssetChangeRequest.creationRequestForAssetFromImage:atFileURL)。仅设置 PHAsset 的 .location 不会将新元数据嵌入实际文件中。

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元数据的图像保存到您的临时文件或文档中(而不是相机胶卷/照片库),则可以跳过使用Photos 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
}

2
这很好,但是我如何从UIImage获取CMSampleBuffer? - Chris
@Chris 在我的情况下,我设置了一个变量用于 AVCaptureStillImageOutput,从中我们可以获取一个 AVCaptureConnection(.connection(withMediaType:)。然后,在 captureStillImageAsynchronously(from:) 的完成块中有一个 CMSampleBuffer? 和一个错误。请查看 https://dev59.com/vZrga4cB1Zd3GeqPrcDU 获取一些示例代码。 - Undrea
savePhoto函数中似乎没有使用location参数。 - Efren
这很好,但它并没有回答问题。我们不是从样本缓冲区开始,而是从刚拍摄照片的UIImagePickerController开始。 - matt
@matt 我建议使用 AVFoundation,因为它非常适合复杂的文件处理,并允许更多的相机自定义。AVCaptureStillImageOutputcaptureStillImageAsynchronously 会生成示例缓冲区。这里这里 列出了 AVFoundation 相对于 UIImagePickerController 的优点。OP 给出的两个要求都没有明确说明必须使用 UIImagePickerController - Undrea
他的代码的第一行是- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingImage:,他说他开始使用的是UIImage,所以我认为这就是我们需要解决的问题。无论如何,当我遇到你的答案时,这就是我正在寻找解决方案的问题。 - matt

2

在应用程序中从摄像头捕获的图像中获取元数据:

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);

1
使用PHPhotoLibrary会是一个解决方案,因为ALAssetsLibrary从iOS 9.0开始已经被弃用。 - IndexOutOfDevelopersException

2

我不小心点了踩!对不起!这是一个很棒的要点。 - Jehan

1

有许多框架可以处理图像和元数据。

Assets Framework已被弃用,并由Photos Library framework取代。如果您使用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。我在这里写了关于这个主题的文章

1
我们要解决的问题是:用户刚刚使用UIImagePickerController相机拍了一张照片。我们得到的是UIImage。既然我们没有AssetsLibrary框架,我们该如何将元数据折叠到UIImage中并将其保存到相机胶卷(照片库)中?
答案(据我所知)是:使用ImageIO框架。从UIImage中提取JPEG数据,将其用作源并将其和元数据字典写入目标,然后将目标数据保存为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中,并保存到照片库中。

如何将图像的旋转(方向)存储在EXIF格式中? - IndexOutOfDevelopersException
我使用了你的一些代码来编写一个更完整的答案,链接在这里:https://dev59.com/HG435IYBdhLWcg3w7k6b#56634582 - lenooh

1
这是@matt答案的微小变化。
以下代码仅使用一个CGImageDestination,并且更有趣的是允许在iOS11+上以HEIC格式保存。
请注意,在添加图像之前,压缩质量已添加到元数据中。 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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接