Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 177 additions & 22 deletions packages/image_picker/ios/Classes/ImagePickerPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,22 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result
case SOURCE_CAMERA:
[self showCamera];
break;
case SOURCE_GALLERY:
[self showPhotoLibrary];
case SOURCE_GALLERY: {
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
if (status == PHAuthorizationStatusAuthorized) {
[self showPhotoLibrary];
} else {
result(nil);
}
}];
break;
default:
}
default: {
result([FlutterError errorWithCode:@"invalid_source"
message:@"Invalid image source."
details:nil]);
break;
}
}
} else if ([@"pickVideo" isEqualToString:call.method]) {
_imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext;
Expand All @@ -90,14 +98,22 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result
case SOURCE_CAMERA:
[self showCamera];
break;
case SOURCE_GALLERY:
[self showPhotoLibrary];
case SOURCE_GALLERY: {
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
if (status == PHAuthorizationStatusAuthorized) {
[self showPhotoLibrary];
} else {
result(nil);
}
}];
break;
default:
}
default: {
result([FlutterError errorWithCode:@"invalid_source"
message:@"Invalid video source."
details:nil]);
break;
}
}
} else {
result(FlutterMethodNotImplemented);
Expand Down Expand Up @@ -150,6 +166,11 @@ - (void)imagePickerController:(UIImagePickerController *)picker
details:nil]);
}
} else {
NSURL *assetURL = [info objectForKey:UIImagePickerControllerReferenceURL];
PHFetchResult<PHAsset *> *result = [PHAsset fetchAssetsWithALAssetURLs:@[ assetURL ]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assetURL can be nil if user take a fresh picture in the image_picker, which leads to crash here.

options:nil];
PHAsset *asset = result.firstObject;

if (image == nil) {
image = [info objectForKey:UIImagePickerControllerOriginalImage];
}
Expand All @@ -158,27 +179,71 @@ - (void)imagePickerController:(UIImagePickerController *)picker
NSNumber *maxWidth = [_arguments objectForKey:@"maxWidth"];
NSNumber *maxHeight = [_arguments objectForKey:@"maxHeight"];

bool isScaled = false;

if (maxWidth != (id)[NSNull null] || maxHeight != (id)[NSNull null]) {
isScaled = true;
image = [self scaledImage:image maxWidth:maxWidth maxHeight:maxHeight];
}

NSData *data = UIImageJPEGRepresentation(image, 1.0);
NSString *guid = [[NSProcessInfo processInfo] globallyUniqueString];
NSString *tmpFile = [NSString stringWithFormat:@"image_picker_%@.jpg", guid];
NSString *tmpDirectory = NSTemporaryDirectory();
NSString *tmpPath = [tmpDirectory stringByAppendingPathComponent:tmpFile];

if ([[NSFileManager defaultManager] createFileAtPath:tmpPath contents:data attributes:nil]) {
_result(tmpPath);
} else {
_result([FlutterError errorWithCode:@"create_error"
message:@"Temporary file could not be created"
details:nil]);
}
[[PHImageManager defaultManager]
requestImageDataForAsset:asset
options:nil
resultHandler:^(NSData *_Nullable imageData, NSString *_Nullable dataUTI,
UIImageOrientation orientation, NSDictionary *_Nullable info) {
NSString *guid = [[NSProcessInfo processInfo] globallyUniqueString];
NSString *tmpFile = [NSString stringWithFormat:@"image_picker_%@", guid];
NSString *tmpDirectory = NSTemporaryDirectory();
NSString *tmpPath = [tmpDirectory stringByAppendingPathComponent:tmpFile];

uint8_t c;
[imageData getBytes:&c length:1];
switch (c) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this an enum?
e.g, create a method like:

- (FlutterImagePickerFileType)getFileTypeFromFirstByte:(unit8_t c) {
   switch(c) {
      case 0x47:
        return FlutterImagePickerFileTypeGIF;
     .....
     .....
  }
}

case 0x47:
// image/gif
if (isScaled == true) {
// TODO When scaled, animation disappears. Drawing as JPEG.
[self createImageFileAtPath:[tmpPath stringByAppendingString:@".gif"]
contents:UIImageJPEGRepresentation(image, 1)];
} else {
[self createImageFileAtPath:[tmpPath stringByAppendingString:@".gif"]
contents:imageData];
}
break;
case 0x89:
// image/png
[self createImageFileAtPath:[tmpPath stringByAppendingString:@".png"]
contents:UIImagePNGRepresentation(image)];
break;
case 0xff:
// image/jpeg
[self createImageFileAtPath:[tmpPath stringByAppendingString:@".jpeg"]
contents:UIImageJPEGRepresentation(image, 1)
with:asset];
break;
case 0x49:
// image/tiff
// TODO Drawing as JPEG.
[self createImageFileAtPath:[tmpPath stringByAppendingString:@".tiff"]
contents:UIImageJPEGRepresentation(image, 1)
with:asset];
break;
case 0x4d:
// image/tiff
// TODO Drawing as JPEG.
[self createImageFileAtPath:[tmpPath stringByAppendingString:@".tiff"]
contents:UIImageJPEGRepresentation(image, 1)
with:asset];
break;
default:
// Drawing as JPEG.
[self createImageFileAtPath:[tmpPath stringByAppendingString:@".jpeg"]
contents:UIImageJPEGRepresentation(image, 1)
with:asset];
break;
}
}];
}

_result = nil;
_arguments = nil;
}

- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
Expand Down Expand Up @@ -254,4 +319,94 @@ - (UIImage *)scaledImage:(UIImage *)image
return scaledImage;
}

- (void)createImageFileAtPath:(NSString *)path contents:(NSData *)data {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
- (void)createImageFileAtPath:(NSString *)path contents:(NSData *)data {
- (void)createImageFileAtPath:(NSString *)path data:(NSData *)data {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mimics the signature of NSFileManager class.
So I feel good with contents.
However, I leave it to you.

- (BOOL)createFileAtPath:(NSString *)path contents:(nullable NSData *)data attributes:(nullable NSDictionary<NSFileAttributeKey, id> *)attr;

if ([[NSFileManager defaultManager] createFileAtPath:path contents:data attributes:nil]) {
_result(path);
} else {
_result([FlutterError errorWithCode:@"create_error"
message:@"Temporary file could not be created"
details:nil]);
}
_result = nil;
_arguments = nil;
}

- (void)createImageFileAtPath:(NSString *)path contents:(NSData *)data with:(PHAsset *)asset {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
- (void)createImageFileAtPath:(NSString *)path contents:(NSData *)data with:(PHAsset *)asset {
- (void)createImageFileAtPath:(NSString *)path data:(NSData *)data asset:(PHAsset *)asset {

NSMutableDictionary *exifDict = [self fetchExifFrom:asset];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manually parsing the meta data seems to be risky. Is it possible to get the meta data from the original image directly and pass down to the new image?
Maybe when fetching image in requestImageDataForAsset, we can call (NSDictionary *)CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(cgImage,0,NULL)); to get the whole meta data of the old image, and then somehow get a diff between the new image and the old image, then add the necessary stuff to the new image?
This way we don't manually parse the information like formatting time, adding hard coded GPS keys etc.

NSMutableDictionary *gpsDict = [self fetchGpsFrom:asset];

NSMutableData *imageData = [NSMutableData data];
CGImageSourceRef cgImage = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
CGImageDestinationRef destination = CGImageDestinationCreateWithData(
(__bridge CFMutableDataRef)imageData, CGImageSourceGetType(cgImage), 1, nil);
CGImageDestinationAddImageFromSource(
destination, cgImage, 0,
(__bridge CFDictionaryRef)[NSDictionary
dictionaryWithObjectsAndKeys:exifDict,
(__bridge NSString *)kCGImagePropertyExifDictionary, gpsDict,
(__bridge NSString *)kCGImagePropertyGPSDictionary, nil]);
CGImageDestinationFinalize(destination);

[imageData writeToFile:path atomically:YES];

CFRelease(cgImage);
CFRelease(destination);

_result(path);
_result = nil;
_arguments = nil;
}

- (NSMutableDictionary *)fetchExifFrom:(PHAsset *)asset {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a manual parser is inevitable, let's put the meta data handling into a separate file/class.

NSMutableDictionary *result = [NSMutableDictionary dictionary];
NSDate *creationDate = asset.creationDate;

NSDateFormatter *outputFormatter = [[NSDateFormatter alloc] init];
[outputFormatter setDateFormat:@"yyyy:MM:dd HH:mm:ss"];
NSString *original = [outputFormatter stringFromDate:creationDate];
[result setObject:original forKey:(__bridge NSString *)kCGImagePropertyExifDateTimeOriginal];
[result setObject:original forKey:(__bridge NSString *)kCGImagePropertyExifDateTimeDigitized];
return result;
}

- (NSMutableDictionary *)fetchGpsFrom:(PHAsset *)asset {
NSMutableDictionary *result = [NSMutableDictionary dictionary];
CLLocation *location = asset.location;
CGFloat latitude = location.coordinate.latitude;
NSString *gpsLatitudeRef;
if (latitude < 0) {
latitude = -latitude;
gpsLatitudeRef = @"S";
} else {
gpsLatitudeRef = @"N";
}
[result setObject:gpsLatitudeRef forKey:(__bridge NSString *)kCGImagePropertyGPSLatitudeRef];
[result setObject:@(latitude) forKey:(__bridge NSString *)kCGImagePropertyGPSLatitude];

CGFloat longitude = location.coordinate.longitude;
NSString *gpsLongitudeRef;
if (longitude < 0) {
longitude = -longitude;
gpsLongitudeRef = @"W";
} else {
gpsLongitudeRef = @"E";
}
[result setObject:gpsLongitudeRef forKey:(__bridge NSString *)kCGImagePropertyGPSLongitudeRef];
[result setObject:@(longitude) forKey:(__bridge NSString *)kCGImagePropertyGPSLongitude];

CGFloat altitude = location.altitude;
if (!isnan(altitude)) {
NSString *gpsAltitudeRef;
if (altitude < 0) {
altitude = -altitude;
gpsAltitudeRef = @"1";
} else {
gpsAltitudeRef = @"0";
}
[result setObject:gpsAltitudeRef forKey:(__bridge NSString *)kCGImagePropertyGPSAltitudeRef];
[result setObject:@(altitude) forKey:(__bridge NSString *)kCGImagePropertyGPSAltitude];
}
return result;
}

@end