Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit aae841a

Browse files
authored
[image_picker_for_web] Added support for maxWidth, maxHeight and imageQuality (#4389)
1 parent 9e46048 commit aae841a

File tree

8 files changed

+382
-17
lines changed

8 files changed

+382
-17
lines changed

packages/image_picker/image_picker_for_web/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 2.1.4
2+
3+
* Implemented `maxWidth`, `maxHeight` and `imageQuality` when selecting images
4+
(except for gifs).
5+
16
## 2.1.3
27

38
* Add `implements` to pubspec.

packages/image_picker/image_picker_for_web/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ Each browser may implement `capture` any way they please, so it may (or may not)
4343
difference in your users' experience.
4444

4545
### pickImage()
46-
The arguments `maxWidth`, `maxHeight` and `imageQuality` are not supported on the web.
46+
The arguments `maxWidth`, `maxHeight` and `imageQuality` are not supported for gif images.
47+
The argument `imageQuality` only works for jpeg and webp images.
4748

4849
### pickVideo()
4950
The argument `maxDuration` is not supported on the web.
@@ -63,7 +64,7 @@ You should be able to use `package:image_picker` _almost_ as normal.
6364
Once the user has picked a file, the returned `PickedFile` instance will contain a
6465
`network`-accessible URL (pointing to a location within the browser).
6566

66-
The instace will also let you retrieve the bytes of the selected file across all platforms.
67+
The instance will also let you retrieve the bytes of the selected file across all platforms.
6768

6869
If you want to use the path directly, your code would need look like this:
6970

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:html' as html;
7+
import 'dart:typed_data';
8+
import 'dart:ui';
9+
10+
import 'package:flutter_test/flutter_test.dart';
11+
import 'package:image_picker_for_web/src/image_resizer.dart';
12+
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
13+
import 'package:integration_test/integration_test.dart';
14+
15+
//This is a sample 10x10 png image
16+
final String pngFileBase64Contents =
17+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKAQMAAAC3/F3+AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABlBMVEXqQzX+/v6lfubTAAAAAWJLR0QB/wIt3gAAAAlwSFlzAAAHEwAABxMBziAPCAAAAAd0SU1FB+UJHgsdDM0ErZoAAAALSURBVAjXY2DABwAAHgABboVHMgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMS0wOS0zMFQxMToyOToxMi0wNDowMHCDC24AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjEtMDktMzBUMTE6Mjk6MTItMDQ6MDAB3rPSAAAAAElFTkSuQmCC";
18+
19+
void main() {
20+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
21+
22+
// Under test...
23+
late ImageResizer imageResizer;
24+
late XFile pngFile;
25+
setUp(() {
26+
imageResizer = ImageResizer();
27+
final pngHtmlFile = _base64ToFile(pngFileBase64Contents, "pngImage.png");
28+
pngFile = XFile(html.Url.createObjectUrl(pngHtmlFile),
29+
name: pngHtmlFile.name, mimeType: pngHtmlFile.type);
30+
});
31+
32+
testWidgets("image is loaded correctly ", (WidgetTester tester) async {
33+
final imageElement = await imageResizer.loadImage(pngFile.path);
34+
expect(imageElement.width!, 10);
35+
expect(imageElement.height!, 10);
36+
});
37+
38+
testWidgets(
39+
"canvas is loaded with image's width and height when max width and max height are null",
40+
(widgetTester) async {
41+
final imageElement = await imageResizer.loadImage(pngFile.path);
42+
final canvas = imageResizer.resizeImageElement(imageElement, null, null);
43+
expect(canvas.width, imageElement.width);
44+
expect(canvas.height, imageElement.height);
45+
});
46+
47+
testWidgets(
48+
"canvas size is scaled when max width and max height are not null",
49+
(widgetTester) async {
50+
final imageElement = await imageResizer.loadImage(pngFile.path);
51+
final canvas = imageResizer.resizeImageElement(imageElement, 8, 8);
52+
expect(canvas.width, 8);
53+
expect(canvas.height, 8);
54+
});
55+
56+
testWidgets("resized image is returned after converting canvas to file",
57+
(widgetTester) async {
58+
final imageElement = await imageResizer.loadImage(pngFile.path);
59+
final canvas = imageResizer.resizeImageElement(imageElement, null, null);
60+
final resizedImage =
61+
await imageResizer.writeCanvasToFile(pngFile, canvas, null);
62+
expect(resizedImage.name, "scaled_${pngFile.name}");
63+
});
64+
65+
testWidgets("image is scaled when maxWidth is set",
66+
(WidgetTester tester) async {
67+
final scaledImage =
68+
await imageResizer.resizeImageIfNeeded(pngFile, 5, null, null);
69+
expect(scaledImage.name, "scaled_${pngFile.name}");
70+
final scaledImageSize = await _getImageSize(scaledImage);
71+
expect(scaledImageSize, Size(5, 5));
72+
});
73+
74+
testWidgets("image is scaled when maxHeight is set",
75+
(WidgetTester tester) async {
76+
final scaledImage =
77+
await imageResizer.resizeImageIfNeeded(pngFile, null, 6, null);
78+
expect(scaledImage.name, "scaled_${pngFile.name}");
79+
final scaledImageSize = await _getImageSize(scaledImage);
80+
expect(scaledImageSize, Size(6, 6));
81+
});
82+
83+
testWidgets("image is scaled when imageQuality is set",
84+
(WidgetTester tester) async {
85+
final scaledImage =
86+
await imageResizer.resizeImageIfNeeded(pngFile, null, null, 89);
87+
expect(scaledImage.name, "scaled_${pngFile.name}");
88+
});
89+
90+
testWidgets("image is scaled when maxWidth,maxHeight,imageQuality are set",
91+
(WidgetTester tester) async {
92+
final scaledImage =
93+
await imageResizer.resizeImageIfNeeded(pngFile, 3, 4, 89);
94+
expect(scaledImage.name, "scaled_${pngFile.name}");
95+
});
96+
97+
testWidgets("image is not scaled when maxWidth,maxHeight, is set",
98+
(WidgetTester tester) async {
99+
final scaledImage =
100+
await imageResizer.resizeImageIfNeeded(pngFile, null, null, null);
101+
expect(scaledImage.name, pngFile.name);
102+
});
103+
}
104+
105+
Future<Size> _getImageSize(XFile file) async {
106+
final completer = Completer<Size>();
107+
final image = html.ImageElement(src: file.path);
108+
image.onLoad.listen((event) {
109+
completer.complete(Size(image.width!.toDouble(), image.height!.toDouble()));
110+
});
111+
image.onError.listen((event) {
112+
completer.complete(Size(0, 0));
113+
});
114+
return completer.future;
115+
}
116+
117+
html.File _base64ToFile(String data, String fileName) {
118+
var arr = data.split(',');
119+
var bstr = html.window.atob(arr[1]);
120+
var n = bstr.length, u8arr = Uint8List(n);
121+
122+
while (n >= 1) {
123+
u8arr[n - 1] = bstr.codeUnitAt(n - 1);
124+
n--;
125+
}
126+
127+
return html.File([u8arr], fileName);
128+
}

packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:async';
66
import 'dart:html' as html;
77

88
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
9+
import 'package:image_picker_for_web/src/image_resizer.dart';
910
import 'package:meta/meta.dart';
1011
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
1112

@@ -23,10 +24,14 @@ class ImagePickerPlugin extends ImagePickerPlatform {
2324

2425
late html.Element _target;
2526

27+
late ImageResizer _imageResizer;
28+
2629
/// A constructor that allows tests to override the function that creates file inputs.
2730
ImagePickerPlugin({
2831
@visibleForTesting ImagePickerPluginTestOverrides? overrides,
32+
@visibleForTesting ImageResizer? imageResizer,
2933
}) : _overrides = overrides {
34+
_imageResizer = imageResizer ?? ImageResizer();
3035
_target = _ensureInitialized(_kImagePickerInputsDomId);
3136
}
3237

@@ -122,7 +127,12 @@ class ImagePickerPlugin extends ImagePickerPlatform {
122127
accept: _kAcceptImageMimeType,
123128
capture: capture,
124129
);
125-
return files.first;
130+
return _imageResizer.resizeImageIfNeeded(
131+
files.first,
132+
maxWidth,
133+
maxHeight,
134+
imageQuality,
135+
);
126136
}
127137

128138
/// Returns an [XFile] containing the video that was picked.
@@ -157,8 +167,21 @@ class ImagePickerPlugin extends ImagePickerPlatform {
157167
double? maxWidth,
158168
double? maxHeight,
159169
int? imageQuality,
160-
}) {
161-
return getFiles(accept: _kAcceptImageMimeType, multiple: true);
170+
}) async {
171+
final List<XFile> images = await getFiles(
172+
accept: _kAcceptImageMimeType,
173+
multiple: true,
174+
);
175+
final Iterable<Future<XFile>> resized = images.map(
176+
(image) => _imageResizer.resizeImageIfNeeded(
177+
image,
178+
maxWidth,
179+
maxHeight,
180+
imageQuality,
181+
),
182+
);
183+
184+
return Future.wait<XFile>(resized);
162185
}
163186

164187
/// Injects a file input with the specified accept+capture attributes, and
@@ -244,17 +267,17 @@ class ImagePickerPlugin extends ImagePickerPlatform {
244267
input.onChange.first.then((event) {
245268
final files = _handleOnChangeEvent(event);
246269
if (!_completer.isCompleted && files != null) {
247-
_completer.complete(files
248-
.map((file) => XFile(
249-
html.Url.createObjectUrl(file),
250-
name: file.name,
251-
length: file.size,
252-
lastModified: DateTime.fromMillisecondsSinceEpoch(
253-
file.lastModified ?? DateTime.now().millisecondsSinceEpoch,
254-
),
255-
mimeType: file.type,
256-
))
257-
.toList());
270+
_completer.complete(files.map((file) {
271+
return XFile(
272+
html.Url.createObjectUrl(file),
273+
name: file.name,
274+
length: file.size,
275+
lastModified: DateTime.fromMillisecondsSinceEpoch(
276+
file.lastModified ?? DateTime.now().millisecondsSinceEpoch,
277+
),
278+
mimeType: file.type,
279+
);
280+
}).toList());
258281
}
259282
});
260283
input.onError.first.then((event) {
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:math';
7+
import 'dart:ui';
8+
import 'package:image_picker_for_web/src/image_resizer_utils.dart';
9+
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
10+
import 'dart:html' as html;
11+
12+
/// Helper class that resizes images.
13+
class ImageResizer {
14+
/// Resizes the image if needed.
15+
/// (Does not support gif images)
16+
Future<XFile> resizeImageIfNeeded(XFile file, double? maxWidth,
17+
double? maxHeight, int? imageQuality) async {
18+
if (!imageResizeNeeded(maxWidth, maxHeight, imageQuality) ||
19+
file.mimeType == "image/gif") {
20+
// Implement maxWidth and maxHeight for image/gif
21+
return file;
22+
}
23+
try {
24+
final imageElement = await loadImage(file.path);
25+
final canvas = resizeImageElement(imageElement, maxWidth, maxHeight);
26+
final resizedImage = await writeCanvasToFile(file, canvas, imageQuality);
27+
html.Url.revokeObjectUrl(file.path);
28+
return resizedImage;
29+
} catch (e) {
30+
return file;
31+
}
32+
}
33+
34+
/// function that loads the blobUrl into an imageElement
35+
Future<html.ImageElement> loadImage(String blobUrl) {
36+
final imageLoadCompleter = Completer<html.ImageElement>();
37+
final imageElement = html.ImageElement();
38+
imageElement.src = blobUrl;
39+
40+
imageElement.onLoad.listen((event) {
41+
imageLoadCompleter.complete(imageElement);
42+
});
43+
imageElement.onError.listen((event) {
44+
final exception = ("Error while loading image.");
45+
imageElement.remove();
46+
imageLoadCompleter.completeError(exception);
47+
});
48+
return imageLoadCompleter.future;
49+
}
50+
51+
/// Draws image to a canvas while resizing the image to fit the [maxWidth],[maxHeight] constraints
52+
html.CanvasElement resizeImageElement(
53+
html.ImageElement source, double? maxWidth, double? maxHeight) {
54+
final newImageSize = calculateSizeOfDownScaledImage(
55+
Size(source.width!.toDouble(), source.height!.toDouble()),
56+
maxWidth,
57+
maxHeight);
58+
final canvas = html.CanvasElement();
59+
canvas.width = newImageSize.width.toInt();
60+
canvas.height = newImageSize.height.toInt();
61+
final context = canvas.context2D;
62+
if (maxHeight == null && maxWidth == null) {
63+
context.drawImage(source, 0, 0);
64+
} else {
65+
context.drawImageScaled(source, 0, 0, canvas.width!, canvas.height!);
66+
}
67+
return canvas;
68+
}
69+
70+
/// function that converts a canvas element to Xfile
71+
/// [imageQuality] is only supported for jpeg and webp images.
72+
Future<XFile> writeCanvasToFile(
73+
XFile originalFile, html.CanvasElement canvas, int? imageQuality) async {
74+
final calculatedImageQuality = ((min(imageQuality ?? 100, 100)) / 100.0);
75+
final blob =
76+
await canvas.toBlob(originalFile.mimeType, calculatedImageQuality);
77+
return XFile(html.Url.createObjectUrlFromBlob(blob),
78+
mimeType: originalFile.mimeType,
79+
name: "scaled_" + originalFile.name,
80+
lastModified: DateTime.now(),
81+
length: blob.size);
82+
}
83+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:math';
6+
import 'dart:ui';
7+
8+
import 'package:flutter/material.dart';
9+
10+
///a function that checks if an image needs to be resized or not
11+
bool imageResizeNeeded(double? maxWidth, double? maxHeight, int? imageQuality) {
12+
return imageQuality != null
13+
? isImageQualityValid(imageQuality)
14+
: (maxWidth != null || maxHeight != null);
15+
}
16+
17+
/// a function that checks if image quality is between 0 to 100
18+
bool isImageQualityValid(int imageQuality) {
19+
return (imageQuality >= 0 && imageQuality <= 100);
20+
}
21+
22+
/// a function that calculates the size of the downScaled image.
23+
/// imageWidth is the width of the image
24+
/// imageHeight is the height of the image
25+
/// maxWidth is the maximum width of the scaled image
26+
/// maxHeight is the maximum height of the scaled image
27+
Size calculateSizeOfDownScaledImage(
28+
Size imageSize, double? maxWidth, double? maxHeight) {
29+
double widthFactor = maxWidth != null ? imageSize.width / maxWidth : 1;
30+
double heightFactor = maxHeight != null ? imageSize.height / maxHeight : 1;
31+
double resizeFactor = max(widthFactor, heightFactor);
32+
return (resizeFactor > 1 ? imageSize ~/ resizeFactor : imageSize);
33+
}

packages/image_picker/image_picker_for_web/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: image_picker_for_web
22
description: Web platform implementation of image_picker
33
repository: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_for_web
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
5-
version: 2.1.3
5+
version: 2.1.4
66

77
environment:
88
sdk: ">=2.12.0 <3.0.0"

0 commit comments

Comments
 (0)