From 9b88dbc539927fa0f4c8ae9aa8792043ecd0beed Mon Sep 17 00:00:00 2001 From: Balvinder Singh Gambhir Date: Wed, 6 Mar 2024 09:51:18 +0530 Subject: [PATCH] [image_picker_for_web] migrates to package:web (#5799) Updates the web implementation of `image_picker_for_web` to `package:web`. ### Issues * Fixes https://github.com/flutter/flutter/issues/139751 --- .../image_picker_for_web/CHANGELOG.md | 5 +- .../image_picker_for_web_test.dart | 108 ++++++++++-------- .../integration_test/image_resizer_test.dart | 47 ++++---- .../image_picker_for_web/example/pubspec.yaml | 5 +- .../lib/image_picker_for_web.dart | 81 +++++++------ .../lib/src/image_resizer.dart | 99 +++++++++------- .../lib/src/pkg_web_tweaks.dart | 14 +++ .../image_picker_for_web/pubspec.yaml | 7 +- 8 files changed, 211 insertions(+), 155 deletions(-) create mode 100644 packages/image_picker/image_picker_for_web/lib/src/pkg_web_tweaks.dart diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md index bc80d9584d9..2cfa022e0b9 100644 --- a/packages/image_picker/image_picker_for_web/CHANGELOG.md +++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 3.0.3 -* Updates minimum supported SDK version to Flutter 3.13/Dart 3.1. +* Migrates package and tests to `package:web`. +* Updates minimum supported SDK version to Flutter 3.19/Dart 3.3. ## 3.0.2 diff --git a/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart index 94adce2411e..a467a189537 100644 --- a/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart +++ b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart @@ -3,25 +3,29 @@ // found in the LICENSE file. import 'dart:convert'; -import 'dart:html' as html; +import 'dart:js_interop'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:image_picker_for_web/image_picker_for_web.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:web/web.dart' as web; const String expectedStringContents = 'Hello, world!'; const String otherStringContents = 'Hello again, world!'; final Uint8List bytes = const Utf8Encoder().convert(expectedStringContents); final Uint8List otherBytes = const Utf8Encoder().convert(otherStringContents); -final Map options = { - 'type': 'text/plain', - 'lastModified': DateTime.utc(2017, 12, 13).millisecondsSinceEpoch, -}; -final html.File textFile = html.File([bytes], 'hello.txt', options); -final html.File secondTextFile = - html.File([otherBytes], 'secondFile.txt'); +// TODO(dit): When web:0.6.0 lands, move `type` to the [web.FilePropertyBag] constructor. +// See: https://github.com/dart-lang/web/pull/197 +final web.FilePropertyBag options = web.FilePropertyBag( + lastModified: DateTime.utc(2017, 12, 13).millisecondsSinceEpoch, +)..type = 'text/plain'; + +final web.File textFile = + web.File([bytes.toJS].toJS, 'hello.txt', options); +final web.File secondTextFile = + web.File([otherBytes.toJS].toJS, 'secondFile.txt'); void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -36,12 +40,12 @@ void main() { testWidgets('getImageFromSource can select a file', ( WidgetTester _, ) async { - final html.FileUploadInputElement mockInput = html.FileUploadInputElement(); - + final web.HTMLInputElement mockInput = web.HTMLInputElement() + ..type = 'file'; final ImagePickerPluginTestOverrides overrides = ImagePickerPluginTestOverrides() ..createInputElement = ((_, __) => mockInput) - ..getMultipleFilesFromInput = ((_) => [textFile]); + ..getMultipleFilesFromInput = ((_) => [textFile]); final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides); @@ -50,11 +54,12 @@ void main() { source: ImageSource.camera, ); - expect(html.querySelector('flt-image-picker-inputs')?.children.isEmpty, - isFalse); + expect( + web.document.querySelector('flt-image-picker-inputs')?.children.length, + isNonZero); // Mock the browser behavior of selecting a file... - mockInput.dispatchEvent(html.Event('change')); + mockInput.dispatchEvent(web.Event('change')); // Now the file should be available expect(image, completes); @@ -69,22 +74,24 @@ void main() { expect( file.lastModified(), completion( - DateTime.fromMillisecondsSinceEpoch(textFile.lastModified!), + DateTime.fromMillisecondsSinceEpoch(textFile.lastModified), )); - expect(html.querySelector('flt-image-picker-inputs')?.children.isEmpty, - isTrue); + expect( + web.document.querySelector('flt-image-picker-inputs')?.children.length, + isZero); }); testWidgets('getMultiImageWithOptions can select multiple files', ( WidgetTester _, ) async { - final html.FileUploadInputElement mockInput = html.FileUploadInputElement(); + final web.HTMLInputElement mockInput = web.HTMLInputElement() + ..type = 'file'; final ImagePickerPluginTestOverrides overrides = ImagePickerPluginTestOverrides() ..createInputElement = ((_, __) => mockInput) ..getMultipleFilesFromInput = - ((_) => [textFile, secondTextFile]); + ((_) => [textFile, secondTextFile]); final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides); @@ -92,7 +99,7 @@ void main() { final Future> files = plugin.getMultiImageWithOptions(); // Mock the browser behavior of selecting a file... - mockInput.dispatchEvent(html.Event('change')); + mockInput.dispatchEvent(web.Event('change')); // Now the file should be available expect(files, completes); @@ -108,13 +115,14 @@ void main() { }); testWidgets('getMedia can select multiple files', (WidgetTester _) async { - final html.FileUploadInputElement mockInput = html.FileUploadInputElement(); + final web.HTMLInputElement mockInput = web.HTMLInputElement() + ..type = 'file'; final ImagePickerPluginTestOverrides overrides = ImagePickerPluginTestOverrides() ..createInputElement = ((_, __) => mockInput) ..getMultipleFilesFromInput = - ((_) => [textFile, secondTextFile]); + ((_) => [textFile, secondTextFile]); final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides); @@ -123,7 +131,7 @@ void main() { plugin.getMedia(options: const MediaOptions(allowMultiple: true)); // Mock the browser behavior of selecting a file... - mockInput.dispatchEvent(html.Event('change')); + mockInput.dispatchEvent(web.Event('change')); // Now the file should be available expect(files, completes); @@ -139,20 +147,20 @@ void main() { }); group('cancel event', () { - late html.FileUploadInputElement mockInput; + late web.HTMLInputElement mockInput; late ImagePickerPluginTestOverrides overrides; late ImagePickerPlugin plugin; setUp(() { - mockInput = html.FileUploadInputElement(); + mockInput = web.HTMLInputElement()..type = 'file'; overrides = ImagePickerPluginTestOverrides() ..createInputElement = ((_, __) => mockInput) - ..getMultipleFilesFromInput = ((_) => [textFile]); + ..getMultipleFilesFromInput = ((_) => [textFile]); plugin = ImagePickerPlugin(overrides: overrides); }); void mockCancel() { - mockInput.dispatchEvent(html.Event('cancel')); + mockInput.dispatchEvent(web.Event('cancel')); } testWidgets('getFiles - returns empty list', (WidgetTester _) async { @@ -226,61 +234,61 @@ void main() { group('createInputElement', () { testWidgets('accept: any, capture: null', (WidgetTester tester) async { - final html.Element input = plugin.createInputElement('any', null); + final web.Element input = plugin.createInputElement('any', null); - expect(input.attributes, containsPair('accept', 'any')); - expect(input.attributes, isNot(contains('capture'))); - expect(input.attributes, isNot(contains('multiple'))); + expect(input.getAttribute('accept'), 'any'); + expect(input.hasAttribute('capture'), false); + expect(input.hasAttribute('multiple'), false); }); testWidgets('accept: any, capture: something', (WidgetTester tester) async { - final html.Element input = plugin.createInputElement('any', 'something'); + final web.Element input = plugin.createInputElement('any', 'something'); - expect(input.attributes, containsPair('accept', 'any')); - expect(input.attributes, containsPair('capture', 'something')); - expect(input.attributes, isNot(contains('multiple'))); + expect(input.getAttribute('accept'), 'any'); + expect(input.getAttribute('capture'), 'something'); + expect(input.hasAttribute('multiple'), false); }); testWidgets('accept: any, capture: null, multi: true', (WidgetTester tester) async { - final html.Element input = + final web.Element input = plugin.createInputElement('any', null, multiple: true); - expect(input.attributes, containsPair('accept', 'any')); - expect(input.attributes, isNot(contains('capture'))); - expect(input.attributes, contains('multiple')); + expect(input.getAttribute('accept'), 'any'); + expect(input.hasAttribute('capture'), false); + expect(input.hasAttribute('multiple'), true); }); testWidgets('accept: any, capture: something, multi: true', (WidgetTester tester) async { - final html.Element input = + final web.Element input = plugin.createInputElement('any', 'something', multiple: true); - expect(input.attributes, containsPair('accept', 'any')); - expect(input.attributes, containsPair('capture', 'something')); - expect(input.attributes, contains('multiple')); + expect(input.getAttribute('accept'), 'any'); + expect(input.getAttribute('capture'), 'something'); + expect(input.hasAttribute('multiple'), true); }); }); group('Deprecated methods', () { - late html.FileUploadInputElement mockInput; + late web.HTMLInputElement mockInput; late ImagePickerPluginTestOverrides overrides; late ImagePickerPlugin plugin; setUp(() { - mockInput = html.FileUploadInputElement(); + mockInput = web.HTMLInputElement()..type = 'file'; overrides = ImagePickerPluginTestOverrides() ..createInputElement = ((_, __) => mockInput) - ..getMultipleFilesFromInput = ((_) => [textFile]); + ..getMultipleFilesFromInput = ((_) => [textFile]); plugin = ImagePickerPlugin(overrides: overrides); }); void mockCancel() { - mockInput.dispatchEvent(html.Event('cancel')); + mockInput.dispatchEvent(web.Event('cancel')); } void mockChange() { - mockInput.dispatchEvent(html.Event('change')); + mockInput.dispatchEvent(web.Event('change')); } group('getImage', () { @@ -306,7 +314,7 @@ void main() { expect( file.lastModified(), completion( - DateTime.fromMillisecondsSinceEpoch(textFile.lastModified!), + DateTime.fromMillisecondsSinceEpoch(textFile.lastModified), )); }); @@ -326,7 +334,7 @@ void main() { testWidgets('can select multiple files', (WidgetTester _) async { // Override the returned files... overrides.getMultipleFilesFromInput = - (_) => [textFile, secondTextFile]; + (_) => [textFile, secondTextFile]; // ignore: deprecated_member_use final Future> files = plugin.getMultiImage(); 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 index 0ff6d238000..dc01ef6d197 100644 --- 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 @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:html' as html; +import 'dart:js_interop'; import 'dart:typed_data'; import 'dart:ui'; @@ -11,6 +11,8 @@ 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'; +import 'package:web/helpers.dart'; +import 'package:web/web.dart' as web; //This is a sample 10x10 png image const String pngFileBase64Contents = @@ -24,14 +26,13 @@ void main() { late XFile pngFile; setUp(() { imageResizer = ImageResizer(); - final html.File pngHtmlFile = - _base64ToFile(pngFileBase64Contents, 'pngImage.png'); - pngFile = XFile(html.Url.createObjectUrl(pngHtmlFile), - name: pngHtmlFile.name, mimeType: pngHtmlFile.type); + final web.Blob pngHtmlFile = _base64ToBlob(pngFileBase64Contents); + pngFile = XFile(web.URL.createObjectURL(pngHtmlFile), + name: 'pngImage.png', mimeType: 'image/png'); }); testWidgets('image is loaded correctly ', (WidgetTester tester) async { - final html.ImageElement imageElement = + final web.HTMLImageElement imageElement = await imageResizer.loadImage(pngFile.path); expect(imageElement.width, 10); expect(imageElement.height, 10); @@ -40,9 +41,9 @@ void main() { testWidgets( "canvas is loaded with image's width and height when max width and max height are null", (WidgetTester widgetTester) async { - final html.ImageElement imageElement = + final web.HTMLImageElement imageElement = await imageResizer.loadImage(pngFile.path); - final html.CanvasElement canvas = + final web.HTMLCanvasElement canvas = imageResizer.resizeImageElement(imageElement, null, null); expect(canvas.width, imageElement.width); expect(canvas.height, imageElement.height); @@ -51,9 +52,9 @@ void main() { testWidgets( 'canvas size is scaled when max width and max height are not null', (WidgetTester widgetTester) async { - final html.ImageElement imageElement = + final web.HTMLImageElement imageElement = await imageResizer.loadImage(pngFile.path); - final html.CanvasElement canvas = + final web.HTMLCanvasElement canvas = imageResizer.resizeImageElement(imageElement, 8, 8); expect(canvas.width, 8); expect(canvas.height, 8); @@ -61,9 +62,9 @@ void main() { testWidgets('resized image is returned after converting canvas to file', (WidgetTester widgetTester) async { - final html.ImageElement imageElement = + final web.HTMLImageElement imageElement = await imageResizer.loadImage(pngFile.path); - final html.CanvasElement canvas = + final web.HTMLCanvasElement canvas = imageResizer.resizeImageElement(imageElement, null, null); final XFile resizedImage = await imageResizer.writeCanvasToFile(pngFile, canvas, null); @@ -112,19 +113,21 @@ void main() { Future _getImageSize(XFile file) async { final Completer completer = Completer(); - final html.ImageElement image = html.ImageElement(src: file.path); - image.onLoad.listen((html.Event event) { - completer.complete(Size(image.width!.toDouble(), image.height!.toDouble())); - }); - image.onError.listen((html.Event event) { - completer.complete(Size.zero); - }); + final web.HTMLImageElement image = web.HTMLImageElement(); + image + ..onLoad.listen((web.Event event) { + completer.complete(Size(image.width.toDouble(), image.height.toDouble())); + }) + ..onError.listen((web.Event event) { + completer.complete(Size.zero); + }) + ..src = file.path; return completer.future; } -html.File _base64ToFile(String data, String fileName) { +web.Blob _base64ToBlob(String data) { final List arr = data.split(','); - final String bstr = html.window.atob(arr[1]); + final String bstr = web.window.atob(arr[1]); int n = bstr.length; final Uint8List u8arr = Uint8List(n); @@ -133,5 +136,5 @@ html.File _base64ToFile(String data, String fileName) { n--; } - return html.File([u8arr], fileName); + return Blob([u8arr.toJS].toJS); } diff --git a/packages/image_picker/image_picker_for_web/example/pubspec.yaml b/packages/image_picker/image_picker_for_web/example/pubspec.yaml index 2efe50fa637..f4f85645c58 100644 --- a/packages/image_picker/image_picker_for_web/example/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/example/pubspec.yaml @@ -2,8 +2,8 @@ name: image_picker_for_web_integration_tests publish_to: none environment: - sdk: ^3.1.0 - flutter: ">=3.13.0" + sdk: ^3.3.0 + flutter: ">=3.19.0" dependencies: flutter: @@ -11,6 +11,7 @@ dependencies: image_picker_for_web: path: ../ image_picker_platform_interface: ^2.8.0 + web: ^0.5.1 dev_dependencies: flutter_test: 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 315b7ddd446..0241e0750a7 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 @@ -3,14 +3,16 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:html' as html; +import 'dart:js_interop'; import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; import 'package:mime/mime.dart' as mime; +import 'package:web/web.dart' as web; import 'src/image_resizer.dart'; +import 'src/pkg_web_tweaks.dart'; const String _kImagePickerInputsDomId = '__image_picker_web-file-input'; const String _kAcceptImageMimeType = 'image/*'; @@ -33,7 +35,7 @@ class ImagePickerPlugin extends ImagePickerPlatform { bool get _hasOverrides => _overrides != null; - late html.Element _target; + late web.Element _target; late ImageResizer _imageResizer; @@ -151,14 +153,16 @@ class ImagePickerPlugin extends ImagePickerPlatform { String? capture, bool multiple = false, }) { - final html.FileUploadInputElement input = createInputElement( + final web.HTMLInputElement input = createInputElement( accept, capture, multiple: multiple, - ) as html.FileUploadInputElement; + ); _injectAndActivate(input); - return _getSelectedXFiles(input).whenComplete(input.remove); + return _getSelectedXFiles(input).whenComplete(() { + input.remove(); + }); } // Deprecated methods follow... @@ -226,51 +230,52 @@ class ImagePickerPlugin extends ImagePickerPlatform { return null; } - List? _getFilesFromInput(html.FileUploadInputElement input) { + List? _getFilesFromInput(web.HTMLInputElement input) { if (_hasOverrides) { return _overrides!.getMultipleFilesFromInput(input); } - return input.files; + return input.files?.toList; } /// Handles the OnChange event from a FileUploadInputElement object /// Returns a list of selected files. - List? _handleOnChangeEvent(html.Event event) { - final html.FileUploadInputElement? input = - event.target as html.FileUploadInputElement?; + List? _handleOnChangeEvent(web.Event event) { + final web.HTMLInputElement? input = event.target as web.HTMLInputElement?; return input == null ? null : _getFilesFromInput(input); } /// Monitors an and returns the selected file(s). - Future> _getSelectedXFiles(html.FileUploadInputElement input) { + Future> _getSelectedXFiles(web.HTMLInputElement input) { final Completer> completer = Completer>(); + // TODO(dit): Migrate all this to Streams (onChange, onError, onCancel) when onCancel is available. + // See: https://github.com/dart-lang/web/issues/199 // Observe the input until we can return something - input.onChange.first.then((html.Event event) { - final List? files = _handleOnChangeEvent(event); + input.onchange = (web.Event event) { + final List? files = _handleOnChangeEvent(event); if (!completer.isCompleted && files != null) { - completer.complete(files.map((html.File file) { + completer.complete(files.map((web.File file) { return XFile( - html.Url.createObjectUrl(file), + web.URL.createObjectURL(file), name: file.name, length: file.size, lastModified: DateTime.fromMillisecondsSinceEpoch( - file.lastModified ?? DateTime.now().millisecondsSinceEpoch, + file.lastModified, ), mimeType: file.type, ); }).toList()); } - }); + }.toJS; - input.addEventListener('cancel', (html.Event _) { + input.oncancel = (web.Event _) { completer.complete([]); - }); + }.toJS; - input.onError.first.then((html.Event event) { + input.onerror = (web.Event event) { if (!completer.isCompleted) { completer.completeError(event); } - }); + }.toJS; // Note that we don't bother detaching from these streams, since the // "input" gets re-created in the DOM every time the user needs to // pick a file. @@ -278,13 +283,13 @@ class ImagePickerPlugin extends ImagePickerPlatform { } /// Initializes a DOM container where we can host input elements. - html.Element _ensureInitialized(String id) { - html.Element? target = html.querySelector('#$id'); + web.Element _ensureInitialized(String id) { + web.Element? target = web.document.querySelector('#$id'); if (target == null) { - final html.Element targetElement = - html.Element.tag('flt-image-picker-inputs')..id = id; - - html.querySelector('body')!.children.add(targetElement); + final web.Element targetElement = + web.document.createElement('flt-image-picker-inputs')..id = id; + // TODO(ditman): Append inside the `view` of the running app. + web.document.body!.append(targetElement); target = targetElement; } return target; @@ -293,7 +298,7 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// Creates an input element that accepts certain file types, and /// allows to `capture` from the device's cameras (where supported) @visibleForTesting - html.Element createInputElement( + web.HTMLInputElement createInputElement( String? accept, String? capture, { bool multiple = false, @@ -302,10 +307,14 @@ class ImagePickerPlugin extends ImagePickerPlatform { return _overrides!.createInputElement(accept, capture); } - final html.Element element = html.FileUploadInputElement() - ..accept = accept + final web.HTMLInputElement element = web.HTMLInputElement() + ..type = 'file' ..multiple = multiple; + if (accept != null) { + element.accept = accept; + } + if (capture != null) { element.setAttribute('capture', capture); } @@ -314,9 +323,9 @@ class ImagePickerPlugin extends ImagePickerPlatform { } /// Injects the file input element, and clicks on it - void _injectAndActivate(html.Element element) { - _target.children.clear(); - _target.children.add(element); + void _injectAndActivate(web.HTMLElement element) { + _target.replaceChildren([].toJS); + _target.append(element); // TODO(dit): Reimplement this with the showPicker() API, https://github.com/flutter/flutter/issues/130365 element.click(); } @@ -325,15 +334,15 @@ class ImagePickerPlugin extends ImagePickerPlatform { // Some tools to override behavior for unit-testing /// A function that creates a file input with the passed in `accept` and `capture` attributes. @visibleForTesting -typedef OverrideCreateInputFunction = html.Element Function( +typedef OverrideCreateInputFunction = web.HTMLInputElement Function( String? accept, String? capture, ); /// A function that extracts list of files from the file `input` passed in. @visibleForTesting -typedef OverrideExtractMultipleFilesFromInputFunction = List - Function(html.Element? input); +typedef OverrideExtractMultipleFilesFromInputFunction = List Function( + web.HTMLInputElement? input); /// Overrides for some of the functionality above. @visibleForTesting 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 index 7cca935c6c9..7b32a451b67 100644 --- 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 @@ -3,88 +3,107 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:html' as html; +import 'dart:js_interop'; import 'dart:math'; import 'dart:ui'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:web/helpers.dart'; +import 'package:web/web.dart' as web; import 'image_resizer_utils.dart'; /// Helper class that resizes images. class ImageResizer { /// Resizes the image if needed. + /// /// (Does not support gif images) - Future resizeImageIfNeeded(XFile file, double? maxWidth, - double? maxHeight, int? imageQuality) async { + Future 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 html.ImageElement imageElement = await loadImage(file.path); - final html.CanvasElement canvas = + final web.HTMLImageElement imageElement = await loadImage(file.path); + final web.HTMLCanvasElement canvas = resizeImageElement(imageElement, maxWidth, maxHeight); final XFile resizedImage = await writeCanvasToFile(file, canvas, imageQuality); - html.Url.revokeObjectUrl(file.path); + web.URL.revokeObjectURL(file.path); return resizedImage; } catch (e) { return file; } } - /// function that loads the blobUrl into an imageElement - Future loadImage(String blobUrl) { - final Completer imageLoadCompleter = - Completer(); - final html.ImageElement imageElement = html.ImageElement(); - // ignore: unsafe_html - imageElement.src = blobUrl; - - imageElement.onLoad.listen((html.Event event) { - imageLoadCompleter.complete(imageElement); - }); - imageElement.onError.listen((html.Event event) { - const String exception = 'Error while loading image.'; - imageElement.remove(); - imageLoadCompleter.completeError(exception); - }); + /// Loads the `blobUrl` into a [web.HTMLImageElement]. + Future loadImage(String blobUrl) { + final Completer imageLoadCompleter = + Completer(); + final web.HTMLImageElement imageElement = web.HTMLImageElement(); + imageElement + // ignore: unsafe_html + ..src = blobUrl + ..onLoad.listen((web.Event event) { + imageLoadCompleter.complete(imageElement); + }) + ..onError.listen((web.Event event) { + const String 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) { + /// Resizing the image in a canvas to fit the [maxWidth], [maxHeight] constraints. + web.HTMLCanvasElement resizeImageElement( + web.HTMLImageElement source, + double? maxWidth, + double? maxHeight, + ) { final Size newImageSize = calculateSizeOfDownScaledImage( - Size(source.width!.toDouble(), source.height!.toDouble()), + Size(source.width.toDouble(), source.height.toDouble()), maxWidth, maxHeight); - final html.CanvasElement canvas = html.CanvasElement(); - canvas.width = newImageSize.width.toInt(); - canvas.height = newImageSize.height.toInt(); - final html.CanvasRenderingContext2D context = canvas.context2D; + final web.HTMLCanvasElement canvas = web.HTMLCanvasElement() + ..width = newImageSize.width.toInt() + ..height = newImageSize.height.toInt(); + final web.CanvasRenderingContext2D context = canvas.context2D; if (maxHeight == null && maxWidth == null) { context.drawImage(source, 0, 0); } else { - context.drawImageScaled(source, 0, 0, canvas.width!, canvas.height!); + context.drawImageScaled( + source, 0, 0, canvas.width.toDouble(), canvas.height.toDouble()); } return canvas; } - /// function that converts a canvas element to Xfile + /// Converts a canvas element to [XFile]. + /// /// [imageQuality] is only supported for jpeg and webp images. Future writeCanvasToFile( - XFile originalFile, html.CanvasElement canvas, int? imageQuality) async { + XFile originalFile, + web.HTMLCanvasElement canvas, + int? imageQuality, + ) async { final double calculatedImageQuality = (min(imageQuality ?? 100, 100)) / 100.0; - final html.Blob 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); + final Completer completer = Completer(); + final web.BlobCallback blobCallback = (web.Blob blob) { + completer.complete(XFile(web.URL.createObjectURL(blob), + mimeType: originalFile.mimeType, + name: 'scaled_${originalFile.name}', + lastModified: DateTime.now(), + length: blob.size)); + }.toJS; + canvas.toBlob( + blobCallback, originalFile.mimeType ?? '', calculatedImageQuality.toJS); + return completer.future; } } diff --git a/packages/image_picker/image_picker_for_web/lib/src/pkg_web_tweaks.dart b/packages/image_picker/image_picker_for_web/lib/src/pkg_web_tweaks.dart new file mode 100644 index 00000000000..1152b01b2d4 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/lib/src/pkg_web_tweaks.dart @@ -0,0 +1,14 @@ +// 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 'package:web/web.dart' as web; + +/// Adds a `toList` method to [web.FileList] objects. +extension WebFileListToDartList on web.FileList { + /// Converts a [web.FileList] into a [List] of [web.File]. + /// + /// This method makes a copy. + List get toList => + [for (int i = 0; i < length; i++) item(i)!]; +} diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml index b4394843b87..85567a58209 100644 --- a/packages/image_picker/image_picker_for_web/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -2,11 +2,11 @@ name: image_picker_for_web description: Web platform implementation of image_picker repository: https://github.com/flutter/packages/tree/main/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: 3.0.2 +version: 3.0.3 environment: - sdk: ^3.1.0 - flutter: ">=3.13.0" + sdk: ^3.3.0 + flutter: ">=3.19.0" flutter: plugin: @@ -23,6 +23,7 @@ dependencies: sdk: flutter image_picker_platform_interface: ^2.9.0 mime: ^1.0.4 + web: ^0.5.1 dev_dependencies: flutter_test: