#import "FLTImageCropperPlugin.h" #import #import #import #import @interface FLTImageCropperPlugin() @end @implementation FLTImageCropperPlugin { FlutterResult _result; NSDictionary *_arguments; UIViewController *_viewController; float _compressQuality; NSString *_compressFormat; } + (void)registerWithRegistrar:(NSObject*)registrar { FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@"plugins.hunghd.vn/image_cropper" binaryMessenger:[registrar messenger]]; UIViewController *viewController = [UIApplication sharedApplication].delegate.window.rootViewController; FLTImageCropperPlugin* instance = [[FLTImageCropperPlugin alloc] initWithViewController:viewController]; [registrar addMethodCallDelegate:instance channel:channel]; } - (instancetype)initWithViewController:(UIViewController *)viewController { self = [super init]; if (self) { _viewController = viewController; } return self; } - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { if ([@"cropImage" isEqualToString:call.method]) { _result = result; _arguments = call.arguments; NSString *sourcePath = call.arguments[@"source_path"]; NSNumber *ratioX = call.arguments[@"ratio_x"]; NSNumber *ratioY = call.arguments[@"ratio_y"]; NSString *cropStyle = call.arguments[@"crop_style"]; NSArray *aspectRatioPresets = call.arguments[@"aspect_ratio_presets"]; NSNumber *compressQuality = call.arguments[@"compress_quality"]; NSString *compressFormat = call.arguments[@"compress_format"]; UIImage *image = [UIImage imageWithContentsOfFile:sourcePath]; TOCropViewController *cropViewController; if ([@"circle" isEqualToString:cropStyle]) { cropViewController = [[TOCropViewController alloc] initWithCroppingStyle:TOCropViewCroppingStyleCircular image:image]; } else { cropViewController = [[TOCropViewController alloc] initWithImage:image]; } cropViewController.delegate = self; if (compressQuality && [compressQuality isKindOfClass:[NSNumber class]]) { _compressQuality = compressQuality.intValue * 1.0f / 100; } else { _compressQuality = 0.9f; } if (compressFormat && [compressFormat isKindOfClass:[NSString class]]) { _compressFormat = compressFormat; } else { _compressFormat = @"jpg"; } NSMutableArray *allowedAspectRatios = [NSMutableArray new]; for (NSString *preset in aspectRatioPresets) { if (preset) { [allowedAspectRatios addObject:@([self parseAspectRatioPresetFromName:preset])]; } } cropViewController.allowedAspectRatios = allowedAspectRatios; if (ratioX != (id)[NSNull null] && ratioY != (id)[NSNull null]) { cropViewController.customAspectRatio = CGSizeMake([ratioX floatValue], [ratioY floatValue]); cropViewController.resetAspectRatioEnabled = NO; cropViewController.aspectRatioPickerButtonHidden = YES; cropViewController.aspectRatioLockDimensionSwapEnabled = YES; cropViewController.aspectRatioLockEnabled = YES; } [self setupUiCustomizedOptions:call.arguments forViewController:cropViewController]; UIWindow *window = [UIApplication sharedApplication].delegate.window; if (!window && @available(iOS 13.0, *)) { for (UIWindowScene* scene in [UIApplication sharedApplication].connectedScenes) { if (scene.activationState == UISceneActivationStateForegroundActive) { for (UIWindow *w in scene.windows) { if (w.isKeyWindow) { window = w; break; } } } } } [window.rootViewController presentViewController:cropViewController animated:YES completion:nil]; } else { result(FlutterMethodNotImplemented); } } - (void)setupUiCustomizedOptions:(id)options forViewController:(TOCropViewController*)controller { NSNumber *minimumAspectRatio = options[@"ios.minimum_aspect_ratio"]; NSNumber *rectX = options[@"ios.rect_x"]; NSNumber *rectY = options[@"ios.rect_y"]; NSNumber *rectWidth = options[@"ios.rect_width"]; NSNumber *rectHeight = options[@"ios.rect_height"]; NSNumber *showActivitySheetOnDone = options[@"ios.show_activity_sheet_on_done"]; NSNumber *showCancelConfirmationDialog = options[@"ios.show_cancel_confirmation_dialog"]; NSNumber *rotateClockwiseButtonHidden = options[@"ios.rotate_clockwise_button_hidden"]; NSNumber *hidesNavigationBar = options[@"ios.hides_navigation_bar"]; NSNumber *rotateButtonHidden = options[@"ios.rotate_button_hidden"]; NSNumber *resetButtonHidden = options[@"ios.reset_button_hidden"]; NSNumber *aspectRatioPickerButtonHidden = options[@"ios.aspect_ratio_picker_button_hidden"]; NSNumber *resetAspectRatioEnabled = options[@"ios.reset_aspect_ratio_enabled"]; NSNumber *aspectRatioLockDimensionSwapEnabled = options[@"ios.aspect_ratio_lock_dimension_swap_enabled"]; NSNumber *aspectRatioLockEnabled = options[@"ios.aspect_ratio_lock_enabled"]; NSString *title = options[@"ios.title"]; NSString *doneButtonTitle = options[@"ios.done_button_title"]; NSString *cancelButtonTitle = options[@"ios.cancel_button_title"]; if (minimumAspectRatio && [minimumAspectRatio isKindOfClass:[NSNumber class]]) { controller.minimumAspectRatio = minimumAspectRatio.floatValue; } if (rectX && [rectX isKindOfClass:[NSNumber class]] && rectY && [rectY isKindOfClass:[NSNumber class]] && rectWidth && [rectWidth isKindOfClass:[NSNumber class]] && rectHeight && [rectHeight isKindOfClass:[NSNumber class]]) { controller.imageCropFrame = CGRectMake(rectX.floatValue, rectY.floatValue, rectWidth.floatValue, rectHeight.floatValue); } if (showActivitySheetOnDone && [showActivitySheetOnDone isKindOfClass:[NSNumber class]]) { controller.showActivitySheetOnDone = showActivitySheetOnDone.boolValue; } if (showCancelConfirmationDialog && [showCancelConfirmationDialog isKindOfClass:[NSNumber class]]) { controller.showCancelConfirmationDialog = showCancelConfirmationDialog.boolValue; } if (rotateClockwiseButtonHidden && [rotateClockwiseButtonHidden isKindOfClass:[NSNumber class]]) { controller.rotateClockwiseButtonHidden = rotateClockwiseButtonHidden.boolValue; } if (hidesNavigationBar && [hidesNavigationBar isKindOfClass:[NSNumber class]]) { controller.hidesNavigationBar = hidesNavigationBar.boolValue; } if (rotateButtonHidden && [rotateButtonHidden isKindOfClass:[NSNumber class]]) { controller.rotateButtonsHidden = rotateButtonHidden.boolValue; } if (resetButtonHidden && [resetButtonHidden isKindOfClass:[NSNumber class]]) { controller.resetButtonHidden = resetButtonHidden.boolValue; } if (aspectRatioPickerButtonHidden && [aspectRatioPickerButtonHidden isKindOfClass:[NSNumber class]]) { controller.aspectRatioPickerButtonHidden = aspectRatioPickerButtonHidden.boolValue; } if (resetAspectRatioEnabled && [resetAspectRatioEnabled isKindOfClass:[NSNumber class]]) { controller.resetAspectRatioEnabled = resetAspectRatioEnabled.boolValue; } if (aspectRatioLockDimensionSwapEnabled && [aspectRatioLockDimensionSwapEnabled isKindOfClass:[NSNumber class]]) { controller.aspectRatioLockDimensionSwapEnabled = aspectRatioLockDimensionSwapEnabled.boolValue; } if (aspectRatioLockEnabled && [aspectRatioLockEnabled isKindOfClass:[NSNumber class]]) { controller.aspectRatioLockEnabled = aspectRatioLockEnabled.boolValue; } if (title && [title isKindOfClass:[NSString class]]) { controller.title = title; } if (doneButtonTitle && [doneButtonTitle isKindOfClass:[NSString class]]) { controller.doneButtonTitle = doneButtonTitle; } if (cancelButtonTitle && [cancelButtonTitle isKindOfClass:[NSString class]]) { controller.cancelButtonTitle = cancelButtonTitle; } } - (TOCropViewControllerAspectRatioPreset)parseAspectRatioPresetFromName:(NSString*)name { if ([@"square" isEqualToString:name]) { return TOCropViewControllerAspectRatioPresetSquare; } else if ([@"original" isEqualToString:name]) { return TOCropViewControllerAspectRatioPresetOriginal; } else if ([@"3x2" isEqualToString:name]) { return TOCropViewControllerAspectRatioPreset3x2; } else if ([@"4x3" isEqualToString:name]) { return TOCropViewControllerAspectRatioPreset4x3; } else if ([@"5x3" isEqualToString:name]) { return TOCropViewControllerAspectRatioPreset5x3; } else if ([@"5x4" isEqualToString:name]) { return TOCropViewControllerAspectRatioPreset5x4; } else if ([@"7x5" isEqualToString:name]) { return TOCropViewControllerAspectRatioPreset7x5; } else if ([@"16x9" isEqualToString:name]) { return TOCropViewControllerAspectRatioPreset16x9; } else { return TOCropViewControllerAspectRatioPresetOriginal; } } # pragma TOCropViewControllerDelegate - (void)cropViewController:(TOCropViewController *)cropViewController didCropToImage:(UIImage *)image withRect:(CGRect)cropRect angle:(NSInteger)angle { image = [self normalizedImage:image]; NSNumber *maxWidth = [_arguments objectForKey:@"max_width"]; NSNumber *maxHeight = [_arguments objectForKey:@"max_height"]; if (maxWidth != (id)[NSNull null] && maxHeight != (id)[NSNull null]) { image = [self scaledImage:image maxWidth:maxWidth maxHeight:maxHeight]; } NSString *guid = [[NSProcessInfo processInfo] globallyUniqueString]; NSData *data; NSString *tmpFile; if ([@"png" isEqualToString:_compressFormat]) { data = UIImagePNGRepresentation(image); tmpFile = [NSString stringWithFormat:@"image_cropper_%@.png", guid]; } else { data = UIImageJPEGRepresentation(image, _compressQuality); tmpFile = [NSString stringWithFormat:@"image_cropper_%@.jpg", guid]; } NSString *tmpDirectory = NSTemporaryDirectory(); NSString *tmpPath = [tmpDirectory stringByAppendingPathComponent:tmpFile]; if (_result) { 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]); } [cropViewController dismissViewControllerAnimated:YES completion:nil]; _result = nil; _arguments = nil; } } - (void)cropViewController:(TOCropViewController *)cropViewController didFinishCancelled:(BOOL)cancelled { [cropViewController dismissViewControllerAnimated:YES completion:nil]; _result(nil); _result = nil; _arguments = nil; } // The way we save images to the tmp dir currently throws away all EXIF data // (including the orientation of the image). That means, pics taken in portrait // will not be orientated correctly as is. To avoid that, we rotate the actual // image data. // TODO(goderbauer): investigate how to preserve EXIF data. - (UIImage *)normalizedImage:(UIImage *)image { if (image.imageOrientation == UIImageOrientationUp) return image; UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale); [image drawInRect:(CGRect){0, 0, image.size}]; UIImage *normalizedImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return normalizedImage; } - (UIImage *)scaledImage:(UIImage *)image maxWidth:(NSNumber *)maxWidth maxHeight:(NSNumber *)maxHeight { double originalWidth = image.size.width; double originalHeight = image.size.height; bool hasMaxWidth = maxWidth != (id)[NSNull null]; bool hasMaxHeight = maxHeight != (id)[NSNull null]; double width = hasMaxWidth ? MIN([maxWidth doubleValue], originalWidth) : originalWidth; double height = hasMaxHeight ? MIN([maxHeight doubleValue], originalHeight) : originalHeight; bool shouldDownscaleWidth = hasMaxWidth && [maxWidth doubleValue] < originalWidth; bool shouldDownscaleHeight = hasMaxHeight && [maxHeight doubleValue] < originalHeight; bool shouldDownscale = shouldDownscaleWidth || shouldDownscaleHeight; if (shouldDownscale) { double downscaledWidth = (height / originalHeight) * originalWidth; double downscaledHeight = (width / originalWidth) * originalHeight; if (width < height) { if (!hasMaxWidth) { width = downscaledWidth; } else { height = downscaledHeight; } } else if (height < width) { if (!hasMaxHeight) { height = downscaledHeight; } else { width = downscaledWidth; } } else { if (originalWidth < originalHeight) { width = downscaledWidth; } else if (originalHeight < originalWidth) { height = downscaledHeight; } } } UIGraphicsBeginImageContextWithOptions(CGSizeMake(width, height), NO, 1.0); [image drawInRect:CGRectMake(0, 0, width, height)]; UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return scaledImage; } @end