[image_picker_for_web] Added support for maxWidth, maxHeight and imageQuality  (#4389)

diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md
index d11ead3..aee8b08 100644
--- a/packages/image_picker/image_picker_for_web/CHANGELOG.md
+++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 2.1.4
+
+* Implemented `maxWidth`, `maxHeight` and `imageQuality` when selecting images
+  (except for gifs).
+
 ## 2.1.3
 
 * Add `implements` to pubspec.
diff --git a/packages/image_picker/image_picker_for_web/README.md b/packages/image_picker/image_picker_for_web/README.md
index 73f2dfc..c8b85f2 100644
--- a/packages/image_picker/image_picker_for_web/README.md
+++ b/packages/image_picker/image_picker_for_web/README.md
@@ -43,7 +43,8 @@
 difference in your users' experience.
 
 ### pickImage()
-The arguments `maxWidth`, `maxHeight` and `imageQuality` are not supported on the web.
+The arguments `maxWidth`, `maxHeight` and `imageQuality` are not supported for gif images.
+The argument `imageQuality` only works for jpeg and webp images.
 
 ### pickVideo()
 The argument `maxDuration` is not supported on the web.
@@ -63,7 +64,7 @@
 Once the user has picked a file, the returned `PickedFile` instance will contain a
 `network`-accessible URL (pointing to a location within the browser).
 
-The instace will also let you retrieve the bytes of the selected file across all platforms.
+The instance will also let you retrieve the bytes of the selected file across all platforms.
 
 If you want to use the path directly, your code would need look like this:
 
diff --git a/packages/image_picker/image_picker_for_web/example/integration_test/image_resizer_test.dart b/packages/image_picker/image_picker_for_web/example/integration_test/image_resizer_test.dart
new file mode 100644
index 0000000..067c775
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/example/integration_test/image_resizer_test.dart
@@ -0,0 +1,128 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:html' as html;
+import 'dart:typed_data';
+import 'dart:ui';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:image_picker_for_web/src/image_resizer.dart';
+import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
+import 'package:integration_test/integration_test.dart';
+
+//This is a sample 10x10 png image
+final String pngFileBase64Contents =
+    "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKAQMAAAC3/F3+AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABlBMVEXqQzX+/v6lfubTAAAAAWJLR0QB/wIt3gAAAAlwSFlzAAAHEwAABxMBziAPCAAAAAd0SU1FB+UJHgsdDM0ErZoAAAALSURBVAjXY2DABwAAHgABboVHMgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMS0wOS0zMFQxMToyOToxMi0wNDowMHCDC24AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjEtMDktMzBUMTE6Mjk6MTItMDQ6MDAB3rPSAAAAAElFTkSuQmCC";
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  // Under test...
+  late ImageResizer imageResizer;
+  late XFile pngFile;
+  setUp(() {
+    imageResizer = ImageResizer();
+    final pngHtmlFile = _base64ToFile(pngFileBase64Contents, "pngImage.png");
+    pngFile = XFile(html.Url.createObjectUrl(pngHtmlFile),
+        name: pngHtmlFile.name, mimeType: pngHtmlFile.type);
+  });
+
+  testWidgets("image is loaded correctly ", (WidgetTester tester) async {
+    final imageElement = await imageResizer.loadImage(pngFile.path);
+    expect(imageElement.width!, 10);
+    expect(imageElement.height!, 10);
+  });
+
+  testWidgets(
+      "canvas is loaded with image's width and height when max width and max height are null",
+      (widgetTester) async {
+    final imageElement = await imageResizer.loadImage(pngFile.path);
+    final canvas = imageResizer.resizeImageElement(imageElement, null, null);
+    expect(canvas.width, imageElement.width);
+    expect(canvas.height, imageElement.height);
+  });
+
+  testWidgets(
+      "canvas size is scaled when max width and max height are not null",
+      (widgetTester) async {
+    final imageElement = await imageResizer.loadImage(pngFile.path);
+    final canvas = imageResizer.resizeImageElement(imageElement, 8, 8);
+    expect(canvas.width, 8);
+    expect(canvas.height, 8);
+  });
+
+  testWidgets("resized image is returned after converting canvas to file",
+      (widgetTester) async {
+    final imageElement = await imageResizer.loadImage(pngFile.path);
+    final canvas = imageResizer.resizeImageElement(imageElement, null, null);
+    final resizedImage =
+        await imageResizer.writeCanvasToFile(pngFile, canvas, null);
+    expect(resizedImage.name, "scaled_${pngFile.name}");
+  });
+
+  testWidgets("image is scaled when maxWidth is set",
+      (WidgetTester tester) async {
+    final scaledImage =
+        await imageResizer.resizeImageIfNeeded(pngFile, 5, null, null);
+    expect(scaledImage.name, "scaled_${pngFile.name}");
+    final scaledImageSize = await _getImageSize(scaledImage);
+    expect(scaledImageSize, Size(5, 5));
+  });
+
+  testWidgets("image is scaled when maxHeight is set",
+      (WidgetTester tester) async {
+    final scaledImage =
+        await imageResizer.resizeImageIfNeeded(pngFile, null, 6, null);
+    expect(scaledImage.name, "scaled_${pngFile.name}");
+    final scaledImageSize = await _getImageSize(scaledImage);
+    expect(scaledImageSize, Size(6, 6));
+  });
+
+  testWidgets("image is scaled when imageQuality is set",
+      (WidgetTester tester) async {
+    final scaledImage =
+        await imageResizer.resizeImageIfNeeded(pngFile, null, null, 89);
+    expect(scaledImage.name, "scaled_${pngFile.name}");
+  });
+
+  testWidgets("image is scaled when maxWidth,maxHeight,imageQuality are set",
+      (WidgetTester tester) async {
+    final scaledImage =
+        await imageResizer.resizeImageIfNeeded(pngFile, 3, 4, 89);
+    expect(scaledImage.name, "scaled_${pngFile.name}");
+  });
+
+  testWidgets("image is not scaled when maxWidth,maxHeight, is set",
+      (WidgetTester tester) async {
+    final scaledImage =
+        await imageResizer.resizeImageIfNeeded(pngFile, null, null, null);
+    expect(scaledImage.name, pngFile.name);
+  });
+}
+
+Future<Size> _getImageSize(XFile file) async {
+  final completer = Completer<Size>();
+  final image = html.ImageElement(src: file.path);
+  image.onLoad.listen((event) {
+    completer.complete(Size(image.width!.toDouble(), image.height!.toDouble()));
+  });
+  image.onError.listen((event) {
+    completer.complete(Size(0, 0));
+  });
+  return completer.future;
+}
+
+html.File _base64ToFile(String data, String fileName) {
+  var arr = data.split(',');
+  var bstr = html.window.atob(arr[1]);
+  var n = bstr.length, u8arr = Uint8List(n);
+
+  while (n >= 1) {
+    u8arr[n - 1] = bstr.codeUnitAt(n - 1);
+    n--;
+  }
+
+  return html.File([u8arr], fileName);
+}
diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart
index b170ee3..f13aeb1 100644
--- a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart
+++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart
@@ -6,6 +6,7 @@
 import 'dart:html' as html;
 
 import 'package:flutter_web_plugins/flutter_web_plugins.dart';
+import 'package:image_picker_for_web/src/image_resizer.dart';
 import 'package:meta/meta.dart';
 import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
 
@@ -23,10 +24,14 @@
 
   late html.Element _target;
 
+  late ImageResizer _imageResizer;
+
   /// A constructor that allows tests to override the function that creates file inputs.
   ImagePickerPlugin({
     @visibleForTesting ImagePickerPluginTestOverrides? overrides,
+    @visibleForTesting ImageResizer? imageResizer,
   }) : _overrides = overrides {
+    _imageResizer = imageResizer ?? ImageResizer();
     _target = _ensureInitialized(_kImagePickerInputsDomId);
   }
 
@@ -122,7 +127,12 @@
       accept: _kAcceptImageMimeType,
       capture: capture,
     );
-    return files.first;
+    return _imageResizer.resizeImageIfNeeded(
+      files.first,
+      maxWidth,
+      maxHeight,
+      imageQuality,
+    );
   }
 
   /// Returns an [XFile] containing the video that was picked.
@@ -157,8 +167,21 @@
     double? maxWidth,
     double? maxHeight,
     int? imageQuality,
-  }) {
-    return getFiles(accept: _kAcceptImageMimeType, multiple: true);
+  }) async {
+    final List<XFile> images = await getFiles(
+      accept: _kAcceptImageMimeType,
+      multiple: true,
+    );
+    final Iterable<Future<XFile>> resized = images.map(
+      (image) => _imageResizer.resizeImageIfNeeded(
+        image,
+        maxWidth,
+        maxHeight,
+        imageQuality,
+      ),
+    );
+
+    return Future.wait<XFile>(resized);
   }
 
   /// Injects a file input with the specified accept+capture attributes, and
@@ -244,17 +267,17 @@
     input.onChange.first.then((event) {
       final files = _handleOnChangeEvent(event);
       if (!_completer.isCompleted && files != null) {
-        _completer.complete(files
-            .map((file) => XFile(
-                  html.Url.createObjectUrl(file),
-                  name: file.name,
-                  length: file.size,
-                  lastModified: DateTime.fromMillisecondsSinceEpoch(
-                    file.lastModified ?? DateTime.now().millisecondsSinceEpoch,
-                  ),
-                  mimeType: file.type,
-                ))
-            .toList());
+        _completer.complete(files.map((file) {
+          return XFile(
+            html.Url.createObjectUrl(file),
+            name: file.name,
+            length: file.size,
+            lastModified: DateTime.fromMillisecondsSinceEpoch(
+              file.lastModified ?? DateTime.now().millisecondsSinceEpoch,
+            ),
+            mimeType: file.type,
+          );
+        }).toList());
       }
     });
     input.onError.first.then((event) {
diff --git a/packages/image_picker/image_picker_for_web/lib/src/image_resizer.dart b/packages/image_picker/image_picker_for_web/lib/src/image_resizer.dart
new file mode 100644
index 0000000..6ee7c5f
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/lib/src/image_resizer.dart
@@ -0,0 +1,83 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:math';
+import 'dart:ui';
+import 'package:image_picker_for_web/src/image_resizer_utils.dart';
+import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
+import 'dart:html' as html;
+
+/// Helper class that resizes images.
+class ImageResizer {
+  /// Resizes the image if needed.
+  /// (Does not support gif images)
+  Future<XFile> resizeImageIfNeeded(XFile file, double? maxWidth,
+      double? maxHeight, int? imageQuality) async {
+    if (!imageResizeNeeded(maxWidth, maxHeight, imageQuality) ||
+        file.mimeType == "image/gif") {
+      // Implement maxWidth and maxHeight for image/gif
+      return file;
+    }
+    try {
+      final imageElement = await loadImage(file.path);
+      final canvas = resizeImageElement(imageElement, maxWidth, maxHeight);
+      final resizedImage = await writeCanvasToFile(file, canvas, imageQuality);
+      html.Url.revokeObjectUrl(file.path);
+      return resizedImage;
+    } catch (e) {
+      return file;
+    }
+  }
+
+  /// function that loads the blobUrl into an imageElement
+  Future<html.ImageElement> loadImage(String blobUrl) {
+    final imageLoadCompleter = Completer<html.ImageElement>();
+    final imageElement = html.ImageElement();
+    imageElement.src = blobUrl;
+
+    imageElement.onLoad.listen((event) {
+      imageLoadCompleter.complete(imageElement);
+    });
+    imageElement.onError.listen((event) {
+      final exception = ("Error while loading image.");
+      imageElement.remove();
+      imageLoadCompleter.completeError(exception);
+    });
+    return imageLoadCompleter.future;
+  }
+
+  /// Draws image to a canvas while resizing the image to fit the [maxWidth],[maxHeight] constraints
+  html.CanvasElement resizeImageElement(
+      html.ImageElement source, double? maxWidth, double? maxHeight) {
+    final newImageSize = calculateSizeOfDownScaledImage(
+        Size(source.width!.toDouble(), source.height!.toDouble()),
+        maxWidth,
+        maxHeight);
+    final canvas = html.CanvasElement();
+    canvas.width = newImageSize.width.toInt();
+    canvas.height = newImageSize.height.toInt();
+    final context = canvas.context2D;
+    if (maxHeight == null && maxWidth == null) {
+      context.drawImage(source, 0, 0);
+    } else {
+      context.drawImageScaled(source, 0, 0, canvas.width!, canvas.height!);
+    }
+    return canvas;
+  }
+
+  /// function that converts a canvas element to Xfile
+  /// [imageQuality] is only supported for jpeg and webp images.
+  Future<XFile> writeCanvasToFile(
+      XFile originalFile, html.CanvasElement canvas, int? imageQuality) async {
+    final calculatedImageQuality = ((min(imageQuality ?? 100, 100)) / 100.0);
+    final blob =
+        await canvas.toBlob(originalFile.mimeType, calculatedImageQuality);
+    return XFile(html.Url.createObjectUrlFromBlob(blob),
+        mimeType: originalFile.mimeType,
+        name: "scaled_" + originalFile.name,
+        lastModified: DateTime.now(),
+        length: blob.size);
+  }
+}
diff --git a/packages/image_picker/image_picker_for_web/lib/src/image_resizer_utils.dart b/packages/image_picker/image_picker_for_web/lib/src/image_resizer_utils.dart
new file mode 100644
index 0000000..6ef7892
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/lib/src/image_resizer_utils.dart
@@ -0,0 +1,33 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:math';
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+
+///a function that checks if an image needs to be resized or not
+bool imageResizeNeeded(double? maxWidth, double? maxHeight, int? imageQuality) {
+  return imageQuality != null
+      ? isImageQualityValid(imageQuality)
+      : (maxWidth != null || maxHeight != null);
+}
+
+/// a function that checks if image quality is between 0 to 100
+bool isImageQualityValid(int imageQuality) {
+  return (imageQuality >= 0 && imageQuality <= 100);
+}
+
+/// a function that calculates the size of the downScaled image.
+/// imageWidth is the width of the image
+/// imageHeight is the height of  the image
+/// maxWidth is the maximum width of the scaled image
+/// maxHeight is the maximum height of the scaled image
+Size calculateSizeOfDownScaledImage(
+    Size imageSize, double? maxWidth, double? maxHeight) {
+  double widthFactor = maxWidth != null ? imageSize.width / maxWidth : 1;
+  double heightFactor = maxHeight != null ? imageSize.height / maxHeight : 1;
+  double resizeFactor = max(widthFactor, heightFactor);
+  return (resizeFactor > 1 ? imageSize ~/ resizeFactor : imageSize);
+}
diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml
index 895486f..d0d9701 100644
--- a/packages/image_picker/image_picker_for_web/pubspec.yaml
+++ b/packages/image_picker/image_picker_for_web/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Web platform implementation of image_picker
 repository: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_for_web
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
-version: 2.1.3
+version: 2.1.4
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
diff --git a/packages/image_picker/image_picker_for_web/test/image_resizer_utils_test.dart b/packages/image_picker/image_picker_for_web/test/image_resizer_utils_test.dart
new file mode 100644
index 0000000..352d2be
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/test/image_resizer_utils_test.dart
@@ -0,0 +1,92 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+import 'dart:ui';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:image_picker_for_web/src/image_resizer_utils.dart';
+
+void main() {
+  group('Image Resizer Utils', () {
+    group("calculateSizeOfScaledImage", () {
+      test(
+          "scaled image height and width are same if max width and max height are same as image's width and height",
+          () {
+        expect(calculateSizeOfDownScaledImage(Size(500, 300), 500, 300),
+            Size(500, 300));
+      });
+
+      test(
+          "scaled image height and width are same if max width and max height are null",
+          () {
+        expect(calculateSizeOfDownScaledImage(Size(500, 300), null, null),
+            Size(500, 300));
+      });
+
+      test("image size is scaled when maxWidth is set", () {
+        final imageSize = Size(500, 300);
+        final maxWidth = 400;
+        final scaledSize = calculateSizeOfDownScaledImage(
+            Size(imageSize.width, imageSize.height), maxWidth.toDouble(), null);
+        expect(scaledSize.height <= imageSize.height, true);
+        expect(scaledSize.width <= maxWidth, true);
+      });
+
+      test("image size is scaled when maxHeight is set", () {
+        final imageSize = Size(500, 300);
+        final maxHeight = 400;
+        final scaledSize = calculateSizeOfDownScaledImage(
+            Size(imageSize.width, imageSize.height),
+            null,
+            maxHeight.toDouble());
+        expect(scaledSize.height <= maxHeight, true);
+        expect(scaledSize.width <= imageSize.width, true);
+      });
+
+      test("image size is scaled when both maxWidth and maxHeight is set", () {
+        final imageSize = Size(1120, 2000);
+        final maxHeight = 1200;
+        final maxWidth = 99;
+        final scaledSize = calculateSizeOfDownScaledImage(
+            Size(imageSize.width, imageSize.height),
+            maxWidth.toDouble(),
+            maxHeight.toDouble());
+        expect(scaledSize.height <= maxHeight, true);
+        expect(scaledSize.width <= maxWidth, true);
+      });
+    });
+    group("imageResizeNeeded", () {
+      test("image needs to be resized when maxWidth is set", () {
+        expect(imageResizeNeeded(50, null, null), true);
+      });
+
+      test("image needs to be resized when maxHeight is set", () {
+        expect(imageResizeNeeded(null, 50, null), true);
+      });
+
+      test("image needs to be resized  when imageQuality is set", () {
+        expect(imageResizeNeeded(null, null, 100), true);
+      });
+
+      test("image will not be resized when imageQuality is not valid", () {
+        expect(imageResizeNeeded(null, null, 101), false);
+        expect(imageResizeNeeded(null, null, -1), false);
+      });
+    });
+
+    group("isImageQualityValid", () {
+      test("image quality is valid in 0 to 100", () {
+        expect(isImageQualityValid(50), true);
+        expect(isImageQualityValid(0), true);
+        expect(isImageQualityValid(100), true);
+      });
+
+      test(
+          "image quality is not valid when imageQuality is less than 0 or greater than 100",
+          () {
+        expect(isImageQualityValid(-1), false);
+        expect(isImageQualityValid(101), false);
+      });
+    });
+  });
+}