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); + }); + }); + }); +}