fix(ios): preserving EXIF data (#712) master
authorScott Murphy <scott@alwaysvip.com>
Tue, 22 Mar 2022 07:23:56 +0000 (00:23 -0700)
committerGitHub <noreply@github.com>
Tue, 22 Mar 2022 07:23:56 +0000 (16:23 +0900)
src/ios/CDVCamera.m

index 2f2738b5786c652be9f32e38d2d6c3d860cbed12..6793da45b68cb20eb83f1e4e27fc9941a96ae41b 100644 (file)
@@ -29,6 +29,7 @@
 #import <ImageIO/CGImageDestination.h>
 #import <MobileCoreServices/UTCoreTypes.h>
 #import <objc/message.h>
+#import <Photos/Photos.h>
 
 #ifndef __CORDOVA_4_0_0
     #import <Cordova/NSData+Base64.h>
@@ -159,7 +160,7 @@ static NSString* toBase64(NSData* data) {
         if (pictureOptions.sourceType == UIImagePickerControllerSourceTypeCamera) {
             [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted)
              {
-                 if(!granted)
+                 if (!granted)
                  {
                      // Denied; show an alert
                      dispatch_async(dispatch_get_main_queue(), ^{
@@ -174,11 +175,32 @@ static NSString* toBase64(NSData* data) {
                          [weakSelf.viewController presentViewController:alertController animated:YES completion:nil];
                      });
                  } else {
-                     [weakSelf showCameraPicker:command.callbackId withOptions:pictureOptions];
+                     dispatch_async(dispatch_get_main_queue(), ^{
+                         [weakSelf showCameraPicker:command.callbackId withOptions:pictureOptions];
+                     });
                  }
              }];
         } else {
-            [weakSelf showCameraPicker:command.callbackId withOptions:pictureOptions];
+            [weakSelf options:pictureOptions requestPhotoPermissions:^(BOOL granted) {
+                if (!granted) {
+                    // Denied; show an alert
+                    dispatch_async(dispatch_get_main_queue(), ^{
+                        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"] message:NSLocalizedString(@"Access to the camera roll has been prohibited; please enable it in the Settings to continue.", nil) preferredStyle:UIAlertControllerStyleAlert];
+                        [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
+                            [weakSelf sendNoPermissionResult:command.callbackId];
+                        }]];
+                        [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Settings", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
+                            [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
+                            [weakSelf sendNoPermissionResult:command.callbackId];
+                        }]];
+                        [weakSelf.viewController presentViewController:alertController animated:YES completion:nil];
+                    });
+                } else {
+                    dispatch_async(dispatch_get_main_queue(), ^{
+                        [weakSelf showCameraPicker:command.callbackId withOptions:pictureOptions];
+                    });
+                }
+            }];
         }
     }];
 }
@@ -367,24 +389,51 @@ static NSString* toBase64(NSData* data) {
                 data = UIImageJPEGRepresentation(image, [options.quality floatValue] / 100.0f);
             }
 
-            if (options.usesGeolocation) {
-                NSDictionary* controllerMetadata = [info objectForKey:@"UIImagePickerControllerMediaMetadata"];
+            if (pickerController.sourceType == UIImagePickerControllerSourceTypeCamera) {
+                if (options.usesGeolocation) {
+                    NSDictionary* controllerMetadata = [info objectForKey:@"UIImagePickerControllerMediaMetadata"];
+                    if (controllerMetadata) {
+                        self.data = data;
+                        self.metadata = [[NSMutableDictionary alloc] init];
+
+                        NSMutableDictionary* EXIFDictionary = [[controllerMetadata objectForKey:(NSString*)kCGImagePropertyExifDictionary]mutableCopy];
+                        if (EXIFDictionary)    {
+                            [self.metadata setObject:EXIFDictionary forKey:(NSString*)kCGImagePropertyExifDictionary];
+                        }
+
+                        if (IsAtLeastiOSVersion(@"8.0")) {
+                            [[self locationManager] performSelector:NSSelectorFromString(@"requestWhenInUseAuthorization") withObject:nil afterDelay:0];
+                        }
+                        [[self locationManager] startUpdatingLocation];
+                    }
+                data = nil;
+                }
+            } else if (pickerController.sourceType == UIImagePickerControllerSourceTypePhotoLibrary) {
+                PHAsset* asset = [info objectForKey:@"UIImagePickerControllerPHAsset"];
+                NSDictionary* controllerMetadata = [self getImageMetadataFromAsset:asset];
+
+                self.data = data;
                 if (controllerMetadata) {
-                    self.data = data;
                     self.metadata = [[NSMutableDictionary alloc] init];
 
                     NSMutableDictionary* EXIFDictionary = [[controllerMetadata objectForKey:(NSString*)kCGImagePropertyExifDictionary]mutableCopy];
                     if (EXIFDictionary)    {
                         [self.metadata setObject:EXIFDictionary forKey:(NSString*)kCGImagePropertyExifDictionary];
                     }
-
-                    if (IsAtLeastiOSVersion(@"8.0")) {
-                        [[self locationManager] performSelector:NSSelectorFromString(@"requestWhenInUseAuthorization") withObject:nil afterDelay:0];
+                    NSMutableDictionary* TIFFDictionary = [[controllerMetadata objectForKey:(NSString*)kCGImagePropertyTIFFDictionary
+                    ]mutableCopy];
+                    if (TIFFDictionary)    {
+                        [self.metadata setObject:TIFFDictionary forKey:(NSString*)kCGImagePropertyTIFFDictionary];
+                    }
+                    NSMutableDictionary* GPSDictionary = [[controllerMetadata objectForKey:(NSString*)kCGImagePropertyGPSDictionary
+]mutableCopy];
+                    if (GPSDictionary)    {
+                        [self.metadata setObject:GPSDictionary forKey:(NSString*)kCGImagePropertyGPSDictionary
+];
                     }
-                    [[self locationManager] startUpdatingLocation];
                 }
-                data = nil;
             }
+
         }
             break;
         default:
@@ -394,6 +443,78 @@ static NSString* toBase64(NSData* data) {
     return data;
 }
 
+/* --------------------------------------------------------------
+-- get the metadata of the image from a PHAsset
+-------------------------------------------------------------- */
+- (NSDictionary*)getImageMetadataFromAsset:(PHAsset*)asset {
+
+    if(asset == nil) {
+        return nil;
+    }
+
+    // get photo info from this asset
+    __block NSDictionary *dict = nil;
+    PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions alloc] init];
+    imageRequestOptions.synchronous = YES;
+    [[PHImageManager defaultManager]
+     requestImageDataForAsset:asset
+     options:imageRequestOptions
+     resultHandler: ^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) {
+        dict = [self convertImageMetadata:imageData]; // as this imageData is in NSData format so we need a method to convert this NSData into NSDictionary
+     }];
+    return dict;
+}
+
+-(NSDictionary*)convertImageMetadata:(NSData*)imageData {
+    CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)(imageData), NULL);
+    if (imageSource) {
+        NSDictionary *options = @{(NSString *)kCGImageSourceShouldCache : [NSNumber numberWithBool:NO]};
+        CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, (__bridge CFDictionaryRef)options);
+        if (imageProperties) {
+            NSDictionary *metadata = (__bridge NSDictionary *)imageProperties;
+            CFRelease(imageProperties);
+            CFRelease(imageSource);
+            NSLog(@"Metadata of selected image%@", metadata);// image metadata after converting NSData into NSDictionary
+            return metadata;
+        }
+        CFRelease(imageSource);
+    }
+
+    NSLog(@"Can't read image metadata");
+    return nil;
+}
+
+- (void)options:(CDVPictureOptions*)options requestPhotoPermissions:(void (^)(BOOL auth))completion
+{
+    if((unsigned long)options.sourceType == 1){
+        completion(YES);
+    }
+    else{
+        PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
+
+        switch (status) {
+            case PHAuthorizationStatusAuthorized:
+                completion(YES);
+                break;
+            case PHAuthorizationStatusNotDetermined: {
+                [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus authorizationStatus) {
+                    if (authorizationStatus == PHAuthorizationStatusAuthorized) {
+                        completion(YES);
+                    } else {
+                        completion(NO);
+                    }
+                }];
+                break;
+            }
+            default:
+                completion(NO);
+                break;
+        }
+
+    }
+
+}
+
 - (NSString*)tempFilePath:(NSString*)extension
 {
     NSString* docsPath = [NSTemporaryDirectory()stringByStandardizingPath];
@@ -454,17 +575,48 @@ static NSString* toBase64(NSData* data) {
             image = [self retrieveImage:info options:options];
             NSData* data = [self processImage:image info:info options:options];
             if (data) {
+                if (pickerController.sourceType == UIImagePickerControllerSourceTypePhotoLibrary) {
+                    NSMutableData *imageDataWithExif = [NSMutableData data];
+                    if (self.metadata) {
+                        CGImageSourceRef sourceImage = CGImageSourceCreateWithData((__bridge CFDataRef)self.data, NULL);
+                        CFStringRef sourceType = CGImageSourceGetType(sourceImage);
+
+                        CGImageDestinationRef destinationImage = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageDataWithExif, sourceType, 1, NULL);
+                        CGImageDestinationAddImageFromSource(destinationImage, sourceImage, 0, (__bridge CFDictionaryRef)self.metadata);
+                        CGImageDestinationFinalize(destinationImage);
+
+                        CFRelease(sourceImage);
+                        CFRelease(destinationImage);
+                    } else {
+                        imageDataWithExif = [self.data mutableCopy];
+                    }
 
-                NSString* extension = options.encodingType == EncodingTypePNG? @"png" : @"jpg";
-                NSString* filePath = [self tempFilePath:extension];
-                NSError* err = nil;
+                    NSError* err = nil;
+                    NSString* extension = self.pickerController.pictureOptions.encodingType == EncodingTypePNG ? @"png":@"jpg";
+                    NSString* filePath = [self tempFilePath:extension];
 
-                // save file
-                if (![data writeToFile:filePath options:NSAtomicWrite error:&err]) {
-                    result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsString:[err localizedDescription]];
-                } else {
-                    result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:[[self urlTransformer:[NSURL fileURLWithPath:filePath]] absoluteString]];
+                    // save file
+                    if (![imageDataWithExif writeToFile:filePath options:NSAtomicWrite error:&err]) {
+                        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsString:[err localizedDescription]];
+                    }
+                    else {
+                        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:[[self urlTransformer:[NSURL fileURLWithPath:filePath]] absoluteString]];
+                    }
+                    
+                } else if (pickerController.sourceType != UIImagePickerControllerSourceTypeCamera || !options.usesGeolocation) {
+                    // No need to save file if usesGeolocation is true since it will be saved after the location is tracked
+                    NSString* extension = options.encodingType == EncodingTypePNG? @"png" : @"jpg";
+                    NSString* filePath = [self tempFilePath:extension];
+                    NSError* err = nil;
+
+                    // save file
+                    if (![data writeToFile:filePath options:NSAtomicWrite error:&err]) {
+                        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsString:[err localizedDescription]];
+                    } else {
+                        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:[[self urlTransformer:[NSURL fileURLWithPath:filePath]] absoluteString]];
+                    }
                 }
+
             }
         }
             break;
@@ -649,19 +801,23 @@ static NSString* toBase64(NSData* data) {
 {
     CDVPictureOptions* options = self.pickerController.pictureOptions;
     CDVPluginResult* result = nil;
+   
+    NSMutableData *imageDataWithExif = [NSMutableData data];
 
     if (self.metadata) {
         NSData* dataCopy = [self.data mutableCopy];
         CGImageSourceRef sourceImage = CGImageSourceCreateWithData((__bridge CFDataRef)dataCopy, NULL);
         CFStringRef sourceType = CGImageSourceGetType(sourceImage);
 
-        CGImageDestinationRef destinationImage = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)self.data, sourceType, 1, NULL);
+        CGImageDestinationRef destinationImage = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageDataWithExif, sourceType, 1, NULL);
         CGImageDestinationAddImageFromSource(destinationImage, sourceImage, 0, (__bridge CFDictionaryRef)self.metadata);
         CGImageDestinationFinalize(destinationImage);
 
         dataCopy = nil;
         CFRelease(sourceImage);
         CFRelease(destinationImage);
+    } else {
+        imageDataWithExif = [self.data mutableCopy];
     }
 
     switch (options.destinationType) {
@@ -695,7 +851,7 @@ static NSString* toBase64(NSData* data) {
     self.pickerController = nil;
     self.data = nil;
     self.metadata = nil;
-
+    imageDataWithExif = nil;
     if (options.saveToPhotoAlbum) {
         UIImageWriteToSavedPhotosAlbum([[UIImage alloc] initWithData:self.data], nil, nil, nil);
     }