From 97b1622a166d3fa07eb1643ca2ab3e2b17dd2bda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20H=C3=BCther?= Date: Tue, 23 Apr 2024 15:39:56 +0200 Subject: [PATCH 1/4] Workaround for Android 14 image picker stripping EXIF information - Do not use the new picker because it strips EXIF information - Do not request Storage permission, because its unnecessary with file Intent on newer Android versions --- src/android/CameraLauncher.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/android/CameraLauncher.java b/src/android/CameraLauncher.java index 18b0d66f1..4f37e9969 100644 --- a/src/android/CameraLauncher.java +++ b/src/android/CameraLauncher.java @@ -205,7 +205,7 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo else if ((this.srcType == PHOTOLIBRARY) || (this.srcType == SAVEDPHOTOALBUM)) { // FIXME: Stop always requesting the permission String[] permissions = getPermissions(true, mediaType); - if(!hasPermissions(permissions)) { + if(!hasPermissions(permissions) && Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { PermissionHelper.requestPermissions(this, SAVE_TO_ALBUM_SEC, permissions); } else { this.getImage(this.srcType, destType); @@ -456,7 +456,11 @@ public void getImage(int srcType, int returnType) { croppedUri = Uri.fromFile(photo); intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, croppedUri); } else { - intent.setAction(Intent.ACTION_GET_CONTENT); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + intent.setAction(Intent.ACTION_OPEN_DOCUMENT); + } else { + intent.setAction(Intent.ACTION_GET_CONTENT); + } intent.addCategory(Intent.CATEGORY_OPENABLE); } } else if (this.mediaType == VIDEO) { From 83201a37a75f73a5d7e0e9402bd3e7a9d0e95583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20H=C3=BCther?= Date: Fri, 31 May 2024 15:32:40 +0200 Subject: [PATCH 2/4] extend workaround for image picker to android 13 --- src/android/CameraLauncher.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/android/CameraLauncher.java b/src/android/CameraLauncher.java index 4f37e9969..597c808a3 100644 --- a/src/android/CameraLauncher.java +++ b/src/android/CameraLauncher.java @@ -205,7 +205,7 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo else if ((this.srcType == PHOTOLIBRARY) || (this.srcType == SAVEDPHOTOALBUM)) { // FIXME: Stop always requesting the permission String[] permissions = getPermissions(true, mediaType); - if(!hasPermissions(permissions) && Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + if(!hasPermissions(permissions) && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { PermissionHelper.requestPermissions(this, SAVE_TO_ALBUM_SEC, permissions); } else { this.getImage(this.srcType, destType); @@ -456,7 +456,7 @@ public void getImage(int srcType, int returnType) { croppedUri = Uri.fromFile(photo); intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, croppedUri); } else { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.setAction(Intent.ACTION_OPEN_DOCUMENT); } else { intent.setAction(Intent.ACTION_GET_CONTENT); From 5e96c3076cb455c81d8e33bcbd1142b9620b9935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20H=C3=BCther?= Date: Mon, 3 Jun 2024 19:09:23 +0200 Subject: [PATCH 3/4] Add Exif handling for HEIC images --- src/android/CameraLauncher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/android/CameraLauncher.java b/src/android/CameraLauncher.java index 597c808a3..6a856ec62 100644 --- a/src/android/CameraLauncher.java +++ b/src/android/CameraLauncher.java @@ -1097,7 +1097,7 @@ private Bitmap getScaledAndRotatedBitmap(String imageUrl, boolean unknownSources } try { String mimeType = FileHelper.getMimeType(imageUrl.toString(), cordova); - if (JPEG_MIME_TYPE.equalsIgnoreCase(mimeType)) { + if (JPEG_MIME_TYPE.equalsIgnoreCase(mimeType) || HEIC_MIME_TYPE.equalsIgnoreCase(mimeType)) { // ExifInterface doesn't like the file:// prefix String filePath = galleryUri.toString().replace("file://", ""); // read exifData of source From 09fb0bc6ad51a98dffdc034e17377abdc8bc7951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20H=C3=BCther?= Date: Thu, 17 Oct 2024 13:44:13 +0200 Subject: [PATCH 4/4] add picker for Finder when running on macOS --- src/ios/CDVCamera.h | 5 +- src/ios/CDVCamera.m | 166 +++++++++++++++++++++++++++++++------------- 2 files changed, 119 insertions(+), 52 deletions(-) diff --git a/src/ios/CDVCamera.h b/src/ios/CDVCamera.h index 0eff7a9ce..99853629c 100644 --- a/src/ios/CDVCamera.h +++ b/src/ios/CDVCamera.h @@ -68,7 +68,7 @@ typedef NSUInteger CDVMediaType; API_AVAILABLE(ios(14)) @interface CDVGalleryPicker : NSObject -@property (strong) PHPickerViewController* pickerViewController; +@property (strong) UIViewController* pickerViewController; @property (strong) CDVPictureOptions* pictureOptions; @property (copy) NSString* callbackId; @@ -97,7 +97,8 @@ API_AVAILABLE(ios(14)) UINavigationControllerDelegate, UIPopoverControllerDelegate, CLLocationManagerDelegate, - PHPickerViewControllerDelegate> + PHPickerViewControllerDelegate, + UIDocumentPickerDelegate> {} @property (strong) CDVCameraPicker* pickerController; @property (strong) CDVGalleryPicker* galleryPicker; diff --git a/src/ios/CDVCamera.m b/src/ios/CDVCamera.m index aad3ed1ef..ad59b1a66 100644 --- a/src/ios/CDVCamera.m +++ b/src/ios/CDVCamera.m @@ -31,6 +31,7 @@ Licensed to the Apache Software Foundation (ASF) under one #import #import #import +#import #ifndef __CORDOVA_4_0_0 #import @@ -218,7 +219,17 @@ - (void)takePicture:(CDVInvokedUrlCommand*)command CDVGalleryPicker* picker = [CDVGalleryPicker createFromPictureOptions:pictureOptions]; picker.callbackId = command.callbackId; weakSelf.galleryPicker = picker; - picker.pickerViewController.delegate = weakSelf; + + if ([picker.pickerViewController isKindOfClass:[PHPickerViewController class]]) { + PHPickerViewController* controller = (PHPickerViewController*) picker.pickerViewController; + controller.delegate = weakSelf; + } else if ([picker.pickerViewController isKindOfClass:[UIDocumentPickerViewController class]]) { + UIDocumentPickerViewController* controller = (UIDocumentPickerViewController*) picker.pickerViewController; + controller.delegate = weakSelf; + } else { + NSLog(@"FIA: no class matched"); + } + [weakSelf.viewController presentViewController:picker.pickerViewController animated:true completion:^{ weakSelf.hasPendingOperation = NO; @@ -459,56 +470,99 @@ - (NSData*)processImage:(UIImage*)image info:(NSDictionary*)info options:(CDVPic return data; } -- (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray *)results API_AVAILABLE(ios(14)) { +- (void)documentPicker:(UIDocumentPickerViewController *)controller +didPickDocumentsAtURLs:(NSArray *)urls; { __weak CDVCamera* weakSelf = self; + if (urls.count > 0) { + NSURL *fileURL = [urls firstObject]; + + NSError *error; + NSData *data = [NSData dataWithContentsOfURL:fileURL options:NSDataReadingMappedIfSafe error:&error]; + + if (error) { + [self sendErrorResultWithMessage]; + return; + } + + // Create a UIImage from the data + UIImage *original = [[UIImage alloc] initWithData:data]; + [self handleImageFromPicker:original withData:data]; + } else { + [self sendErrorResultWithMessage]; + } +} + +- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller; { + [self sendErrorResultWithMessage]; +} + +- (void)sendErrorResultWithMessage { + CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"No Image Selected"]; + [self.commandDelegate sendPluginResult:result callbackId:self.galleryPicker.callbackId]; + + // Reset any pending operation state + self.hasPendingOperation = NO; + self.galleryPicker = nil; +} + +- (void)handleImageFromPicker:(UIImage *)original withData:(NSData *)data { + + __weak CDVCamera* weakSelf = self; + + UIImage* image = [weakSelf conformImage:original toOptions:weakSelf.galleryPicker.pictureOptions]; + + NSData* processedImageData = nil; + switch (weakSelf.galleryPicker.pictureOptions.encodingType) { + case EncodingTypePNG: + processedImageData = UIImagePNGRepresentation(image); + break; + case EncodingTypeJPEG: + processedImageData = UIImageJPEGRepresentation(image, weakSelf.galleryPicker.pictureOptions.quality.floatValue / 100.0f); + break; + default: + NSAssert(NO, @"Missing implementation for encoding type (fallback to jpg)"); + processedImageData = UIImageJPEGRepresentation(image, weakSelf.galleryPicker.pictureOptions.quality.floatValue / 100.0f); + } + CGImageSourceRef processedImageSource = CGImageSourceCreateWithData((__bridge CFDataRef) processedImageData, NULL); + + NSDictionary* completeMetadata = [self convertImageMetadata:data]; + NSDictionary* metadata = [self filterImageMetadataFrom:completeMetadata]; + + NSMutableData* imageDataWithExif = [NSMutableData data]; + CFStringRef fileFormat = kuTTypeFromCDVEncodingType(weakSelf.galleryPicker.pictureOptions.encodingType); + + CGImageDestinationRef destinationImage = CGImageDestinationCreateWithData((__bridge CFMutableDataRef) imageDataWithExif, fileFormat, 1, NULL); + CGImageDestinationAddImageFromSource(destinationImage, processedImageSource, 0, (__bridge CFDictionaryRef) metadata); + CGImageDestinationFinalize(destinationImage); + + CFRelease(processedImageSource); + CFRelease(destinationImage); + + NSString* extension = ExtensionFromCDVEncodingType(weakSelf.galleryPicker.pictureOptions.encodingType); + NSString* filePath = [self tempFilePath:extension]; + NSError* err = nil; + + CDVPluginResult* result = nil; + 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]]; + } + + [weakSelf.commandDelegate sendPluginResult:result callbackId:weakSelf.galleryPicker.callbackId]; + weakSelf.hasPendingOperation = NO; + weakSelf.galleryPicker = nil; +} + +- (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray *)results API_AVAILABLE(ios(14)) { + + [picker dismissViewControllerAnimated:YES completion:^{ if (results.count > 0 && [results.firstObject.itemProvider hasItemConformingToTypeIdentifier:(NSString*)kUTTypeImage]) { [results.firstObject.itemProvider loadDataRepresentationForTypeIdentifier:(NSString*)kUTTypeImage completionHandler:^(NSData* data, NSError* error) { UIImage* original = [[UIImage alloc] initWithData:data]; - UIImage* image = [weakSelf conformImage:original toOptions:weakSelf.galleryPicker.pictureOptions]; - - NSData* processedImageData = nil; - switch (weakSelf.galleryPicker.pictureOptions.encodingType) { - case EncodingTypePNG: - processedImageData = UIImagePNGRepresentation(image); - break; - case EncodingTypeJPEG: - processedImageData = UIImageJPEGRepresentation(image, weakSelf.galleryPicker.pictureOptions.quality.floatValue / 100.0f); - break; - default: - NSAssert(NO, @"Missing implementation for encoding type (fallback to jpg)"); - processedImageData = UIImageJPEGRepresentation(image, weakSelf.galleryPicker.pictureOptions.quality.floatValue / 100.0f); - } - CGImageSourceRef processedImageSource = CGImageSourceCreateWithData((__bridge CFDataRef) processedImageData, NULL); - - NSDictionary* completeMetadata = [self convertImageMetadata:data]; - NSDictionary* metadata = [self filterImageMetadataFrom:completeMetadata]; - - NSMutableData* imageDataWithExif = [NSMutableData data]; - CFStringRef fileFormat = kuTTypeFromCDVEncodingType(weakSelf.galleryPicker.pictureOptions.encodingType); - - CGImageDestinationRef destinationImage = CGImageDestinationCreateWithData((__bridge CFMutableDataRef) imageDataWithExif, fileFormat, 1, NULL); - CGImageDestinationAddImageFromSource(destinationImage, processedImageSource, 0, (__bridge CFDictionaryRef) metadata); - CGImageDestinationFinalize(destinationImage); - - CFRelease(processedImageSource); - CFRelease(destinationImage); - - NSString* extension = ExtensionFromCDVEncodingType(weakSelf.galleryPicker.pictureOptions.encodingType); - NSString* filePath = [self tempFilePath:extension]; - NSError* err = nil; - - CDVPluginResult* result = nil; - 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]]; - } - - [weakSelf.commandDelegate sendPluginResult:result callbackId:weakSelf.galleryPicker.callbackId]; - weakSelf.hasPendingOperation = NO; - weakSelf.galleryPicker = nil; + [self handleImageFromPicker:original withData:data]; }]; } else { CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"No Image Selected"]; @@ -969,11 +1023,23 @@ + (instancetype) createFromPictureOptions:(CDVPictureOptions*)pictureOptions { CDVGalleryPicker* instance = [[CDVGalleryPicker alloc] init]; instance.pictureOptions = pictureOptions; - PHPickerConfiguration* singleImage = [[PHPickerConfiguration alloc] initWithPhotoLibrary:PHPhotoLibrary.sharedPhotoLibrary]; - singleImage.filter = PHPickerFilter.imagesFilter; - singleImage.selectionLimit = 1; - instance.pickerViewController = [[PHPickerViewController alloc] initWithConfiguration:singleImage]; - instance.pickerViewController.presentationController.delegate = instance; + NSProcessInfo *processInfo = [NSProcessInfo processInfo]; + + if ((processInfo.isMacCatalystApp || processInfo.iOSAppOnMac)) { + // Running on macOS under Mac Catalyst, use UIDocumentPickerViewController for image selection + UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initForOpeningContentTypes:@[UTTypeImage]]; + documentPicker.allowsMultipleSelection = NO; + instance.pickerViewController = documentPicker; // Store as generic view controller + instance.pickerViewController.presentationController.delegate = instance; + } else { + // Use PHPickerViewController for image selection + PHPickerConfiguration* singleImage = [[PHPickerConfiguration alloc] initWithPhotoLibrary:PHPhotoLibrary.sharedPhotoLibrary]; + singleImage.filter = PHPickerFilter.imagesFilter; + singleImage.selectionLimit = 1; + PHPickerViewController *photoPicker = [[PHPickerViewController alloc] initWithConfiguration:singleImage]; + instance.pickerViewController = photoPicker; // Store as generic view controller + instance.pickerViewController.presentationController.delegate = instance; + } return instance; }