Add ability to scale images with ImagePicker plugin (#242)

* Added width and height arguments to the pickImage method.

* Implemented scaling images in native Android side.

* Removed the redundant dart:ui import.

* Implemented scaling images in iOS side.

* Added some documentation for the pickImage method.

* Made sure that the width or height can't be negative.

* Added some tests to the Dart side of the plugin.

* Refactored the API to be maxWidth and maxHeight instead.

* Removed unused imports.

* Implemented aspect ratio aware scaling in Android side.

* Implemented aspect ratio aware scaling in iOS side.

* Removed leftover debug code.

* Fixed formatting.

* Update version in pubspec.yaml

* Update CHANGELOG.md
diff --git a/packages/image_picker/CHANGELOG.md b/packages/image_picker/CHANGELOG.md
index 0ac6fd1..57016fa 100644
--- a/packages/image_picker/CHANGELOG.md
+++ b/packages/image_picker/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.1.2
+
+* Added optional maxWidth and maxHeight arguments to pickImage.
+
 ## 0.1.1
 
 * Updated Gradle repositories declaration to avoid the need for manual configuration
diff --git a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java
index 986a9c0..413618f 100644
--- a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java
+++ b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java
@@ -6,6 +6,8 @@
 
 import android.app.Activity;
 import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
 import com.esafirm.imagepicker.features.ImagePicker;
 import com.esafirm.imagepicker.features.camera.DefaultCameraModule;
 import com.esafirm.imagepicker.features.camera.OnImageReadyListener;
@@ -16,6 +18,10 @@
 import io.flutter.plugin.common.MethodChannel.Result;
 import io.flutter.plugin.common.PluginRegistry;
 import io.flutter.plugin.common.PluginRegistry.ActivityResultListener;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -33,6 +39,7 @@
 
   // Pending method call to obtain an image
   private Result pendingResult;
+  private MethodCall methodCall;
 
   public static void registerWith(PluginRegistry.Registrar registrar) {
     final MethodChannel channel = new MethodChannel(registrar.messenger(), CHANNEL);
@@ -51,7 +58,10 @@
       result.error("ALREADY_ACTIVE", "Image picker is already active", null);
       return;
     }
+
     pendingResult = result;
+    methodCall = call;
+
     if (call.method.equals("pickImage")) {
       ImagePicker.create(activity).single().start(REQUEST_CODE_PICK);
     } else if (call.method.equals("captureImage")) {
@@ -70,6 +80,7 @@
       } else {
         pendingResult.error("PICK_ERROR", "Error picking image", null);
         pendingResult = null;
+        methodCall = null;
       }
       return true;
     }
@@ -91,10 +102,79 @@
 
   private void handleResult(Image image) {
     if (pendingResult != null) {
-      pendingResult.success(image.getPath());
+      Double maxWidth = methodCall.argument("maxWidth");
+      Double maxHeight = methodCall.argument("maxHeight");
+      boolean shouldScale = maxWidth != null || maxHeight != null;
+
+      if (!shouldScale) {
+        pendingResult.success(image.getPath());
+      } else {
+        try {
+          File imageFile = scaleImage(image, maxWidth, maxHeight);
+          pendingResult.success(imageFile.getPath());
+        } catch (IOException e) {
+          throw new RuntimeException(e);
+        }
+      }
+
       pendingResult = null;
+      methodCall = null;
     } else {
       throw new IllegalStateException("Received images from picker that were not requested");
     }
   }
+
+  private File scaleImage(Image image, Double maxWidth, Double maxHeight) throws IOException {
+    Bitmap bmp = BitmapFactory.decodeFile(image.getPath());
+    double originalWidth = bmp.getWidth() * 1.0;
+    double originalHeight = bmp.getHeight() * 1.0;
+
+    boolean hasMaxWidth = maxWidth != null;
+    boolean hasMaxHeight = maxHeight != null;
+
+    Double width = hasMaxWidth ? Math.min(originalWidth, maxWidth) : originalWidth;
+    Double height = hasMaxHeight ? Math.min(originalHeight, maxHeight) : originalHeight;
+
+    boolean shouldDownscaleWidth = hasMaxWidth && maxWidth < originalWidth;
+    boolean shouldDownscaleHeight = hasMaxHeight && maxHeight < originalHeight;
+    boolean 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;
+        }
+      }
+    }
+
+    Bitmap scaledBmp = Bitmap.createScaledBitmap(bmp, width.intValue(), height.intValue(), false);
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    scaledBmp.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
+
+    String scaledCopyPath = image.getPath().replace(image.getName(), "scaled_" + image.getName());
+    File imageFile = new File(scaledCopyPath);
+
+    FileOutputStream fileOutput = new FileOutputStream(imageFile);
+    fileOutput.write(outputStream.toByteArray());
+    fileOutput.close();
+
+    return imageFile;
+  }
 }
diff --git a/packages/image_picker/ios/Classes/ImagePickerPlugin.m b/packages/image_picker/ios/Classes/ImagePickerPlugin.m
index 9a762ca..ed642cd 100644
--- a/packages/image_picker/ios/Classes/ImagePickerPlugin.m
+++ b/packages/image_picker/ios/Classes/ImagePickerPlugin.m
@@ -11,6 +11,7 @@
 
 @implementation ImagePickerPlugin {
   FlutterResult _result;
+  NSDictionary *_arguments;
   UIImagePickerController *_imagePickerController;
   UIViewController *_viewController;
 }
@@ -45,6 +46,7 @@
     _imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext;
     _imagePickerController.delegate = self;
     _result = result;
+    _arguments = call.arguments;
 
     UIAlertControllerStyle style = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad
                                        ? UIAlertControllerStyleAlert
@@ -101,6 +103,14 @@
     image = [info objectForKey:UIImagePickerControllerOriginalImage];
   }
   image = [self normalizedImage:image];
+
+  NSNumber *maxWidth = [_arguments objectForKey:@"maxWidth"];
+  NSNumber *maxHeight = [_arguments objectForKey:@"maxHeight"];
+
+  if (maxWidth != (id)[NSNull null] || maxHeight != (id)[NSNull null]) {
+    image = [self scaledImage:image maxWidth:maxWidth maxHeight:maxHeight];
+  }
+
   NSData *data = UIImageJPEGRepresentation(image, 1.0);
   NSString *tmpDirectory = NSTemporaryDirectory();
   NSString *guid = [[NSProcessInfo processInfo] globallyUniqueString];
@@ -116,6 +126,7 @@
                                 details:nil]);
   }
   _result = nil;
+  _arguments = nil;
 }
 
 // The way we save images to the tmp dir currently throws away all EXIF data
@@ -133,4 +144,54 @@
   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
diff --git a/packages/image_picker/lib/image_picker.dart b/packages/image_picker/lib/image_picker.dart
index 5c66208..f9fd2aa 100755
--- a/packages/image_picker/lib/image_picker.dart
+++ b/packages/image_picker/lib/image_picker.dart
@@ -10,9 +10,33 @@
 class ImagePicker {
   static const MethodChannel _channel = const MethodChannel('image_picker');
 
-  // Returns the URL of the picked image
-  static Future<File> pickImage() async {
-    final String path = await _channel.invokeMethod('pickImage');
+  /// Returns a [File] object pointing to the image that was picked.
+  ///
+  /// On both Android & iOS, the user can choose to either:
+  ///
+  /// * pick an image from the gallery
+  /// * take a photo using the device camera.
+  ///
+  /// If specified, the image will be at most [maxWidth] wide and
+  /// [maxHeight] tall. Otherwise the image will be returned at it's
+  /// original width and height.
+  static Future<File> pickImage({double maxWidth, double maxHeight}) async {
+    if (maxWidth != null && maxWidth < 0) {
+      throw new ArgumentError.value(maxWidth, 'maxWidth can\'t be negative');
+    }
+
+    if (maxHeight != null && maxHeight < 0) {
+      throw new ArgumentError.value(maxHeight, 'maxHeight can\'t be negative');
+    }
+
+    final String path = await _channel.invokeMethod(
+      'pickImage',
+      <String, double>{
+        'maxWidth': maxWidth,
+        'maxHeight': maxHeight,
+      },
+    );
+
     return new File(path);
   }
 }
diff --git a/packages/image_picker/pubspec.yaml b/packages/image_picker/pubspec.yaml
index 28b9207..83bdec0 100755
--- a/packages/image_picker/pubspec.yaml
+++ b/packages/image_picker/pubspec.yaml
@@ -3,7 +3,7 @@
 description: Flutter plugin that shows an image picker and uses the camera.
 author: Flutter Team <flutter-dev@googlegroups.com>
 homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker
-version: 0.1.1
+version: 0.1.2
 
 flutter:
   plugin:
@@ -14,5 +14,8 @@
   flutter:
     sdk: flutter
 
+dev_dependencies:
+  test: 0.12.21
+
 environment:
     sdk: ">=1.8.0 <2.0.0"
diff --git a/packages/image_picker/test/image_picker_test.dart b/packages/image_picker/test/image_picker_test.dart
new file mode 100644
index 0000000..7092a9e
--- /dev/null
+++ b/packages/image_picker/test/image_picker_test.dart
@@ -0,0 +1,61 @@
+import 'package:flutter/services.dart';
+import 'package:image_picker/image_picker.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('$ImagePicker', () {
+    const MethodChannel channel = const MethodChannel('image_picker');
+
+    final List<MethodCall> log = <MethodCall>[];
+
+    setUp(() {
+      channel.setMockMethodCallHandler((MethodCall methodCall) async {
+        log.add(methodCall);
+        return '';
+      });
+
+      log.clear();
+    });
+
+    group('#pickImage', () {
+      test('passes the width and height arguments correctly', () async {
+        await ImagePicker.pickImage();
+        await ImagePicker.pickImage(maxWidth: 10.0);
+        await ImagePicker.pickImage(maxHeight: 10.0);
+        await ImagePicker.pickImage(
+          maxWidth: 10.0,
+          maxHeight: 20.0,
+        );
+
+        expect(
+          log,
+          equals(
+            <MethodCall>[
+              const MethodCall('pickImage', const <String, double>{
+                'maxWidth': null,
+                'maxHeight': null,
+              }),
+              const MethodCall('pickImage', const <String, double>{
+                'maxWidth': 10.0,
+                'maxHeight': null,
+              }),
+              const MethodCall('pickImage', const <String, double>{
+                'maxWidth': null,
+                'maxHeight': 10.0,
+              }),
+              const MethodCall('pickImage', const <String, double>{
+                'maxWidth': 10.0,
+                'maxHeight': 20.0,
+              }),
+            ],
+          ),
+        );
+      });
+
+      test('does not accept a negative width or height argument', () {
+        expect(ImagePicker.pickImage(maxWidth: -1.0), throwsArgumentError);
+        expect(ImagePicker.pickImage(maxHeight: -1.0), throwsArgumentError);
+      });
+    });
+  });
+}