From c519383bcccfd73657f20887f9fdb0c346fb2da7 Mon Sep 17 00:00:00 2001
From: Harry Terkelsen <1961493+harryterkelsen@users.noreply.github.com>
Date: Wed, 27 Nov 2024 17:15:59 -0800
Subject: [PATCH] [web] On the web platform, use an tag to show an image
if it can't be accessed with CORS (#157755)
When using `Image.network`, if the given URL points to an image that is
cross-origin and not set up to allow CORS access, then we are unable to
make a [ui.Image] from it. In this case, render the image using a
platform view.
This is the last remaining checklist item for
https://github.com/flutter/flutter/issues/145954
Fixes https://github.com/flutter/flutter/issues/149843
## Pre-launch Checklist
- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.
If you need help, consider asking for advice on the #hackers-new channel
on [Discord].
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
---
.../lib/src/painting/_network_image_web.dart | 295 +++++++++++---
.../lib/src/painting/_web_image_info_io.dart | 40 ++
.../lib/src/painting/_web_image_info_web.dart | 70 ++++
.../lib/src/painting/image_stream.dart | 19 +
packages/flutter/lib/src/web.dart | 19 +
.../lib/src/widgets/_web_image_io.dart | 50 +++
.../lib/src/widgets/_web_image_web.dart | 382 ++++++++++++++++++
packages/flutter/lib/src/widgets/image.dart | 63 ++-
.../painting/_network_image_test_web.dart | 215 +++++++++-
.../test/painting/_test_http_request.dart | 66 +++
.../image_provider_network_image_test.dart | 2 +-
11 files changed, 1131 insertions(+), 90 deletions(-)
create mode 100644 packages/flutter/lib/src/painting/_web_image_info_io.dart
create mode 100644 packages/flutter/lib/src/painting/_web_image_info_web.dart
create mode 100644 packages/flutter/lib/src/widgets/_web_image_io.dart
create mode 100644 packages/flutter/lib/src/widgets/_web_image_web.dart
diff --git a/packages/flutter/lib/src/painting/_network_image_web.dart b/packages/flutter/lib/src/painting/_network_image_web.dart
index 257e48e047..eecfc9a6b3 100644
--- a/packages/flutter/lib/src/painting/_network_image_web.dart
+++ b/packages/flutter/lib/src/painting/_network_image_web.dart
@@ -10,28 +10,52 @@ import 'dart:ui_web' as ui_web;
import 'package:flutter/foundation.dart';
import '../web.dart' as web;
+import '_web_image_info_web.dart';
import 'image_provider.dart' as image_provider;
import 'image_stream.dart';
-/// Creates a type for an overridable factory function for testing purposes.
+/// The type for an overridable factory function for creating an HTTP request,
+/// used for testing purposes.
typedef HttpRequestFactory = web.XMLHttpRequest Function();
+/// The type for an overridable factory function for creating elements,
+/// used for testing purposes.
+typedef ImgElementFactory = web.HTMLImageElement Function();
+
// Method signature for _loadAsync decode callbacks.
typedef _SimpleDecoderCallback = Future Function(ui.ImmutableBuffer buffer);
-/// Default HTTP client.
+/// The default HTTP client.
web.XMLHttpRequest _httpClient() {
return web.XMLHttpRequest();
}
/// Creates an overridable factory function.
+@visibleForTesting
HttpRequestFactory httpRequestFactory = _httpClient;
-/// Restores to the default HTTP request factory.
+/// Restores the default HTTP request factory.
+@visibleForTesting
void debugRestoreHttpRequestFactory() {
httpRequestFactory = _httpClient;
}
+/// The default element factory.
+web.HTMLImageElement _imgElementFactory() {
+ return web.document.createElement('img') as web.HTMLImageElement;
+}
+
+/// The factory function that creates elements, can be overridden for
+/// tests.
+@visibleForTesting
+ImgElementFactory imgElementFactory = _imgElementFactory;
+
+/// Restores the default element factory.
+@visibleForTesting
+void debugRestoreImgElementFactory() {
+ imgElementFactory = _imgElementFactory;
+}
+
/// The web implementation of [image_provider.NetworkImage].
///
/// NetworkImage on the web does not support decoding to a specified size.
@@ -64,12 +88,14 @@ class NetworkImage
final StreamController chunkEvents =
StreamController();
- return MultiFrameImageStreamCompleter(
- chunkEvents: chunkEvents.stream,
- codec: _loadAsync(key as NetworkImage, decode, chunkEvents),
- scale: key.scale,
- debugLabel: key.url,
+ return _ForwardingImageStreamCompleter(
+ _loadAsync(
+ key as NetworkImage,
+ decode,
+ chunkEvents,
+ ),
informationCollector: _imageStreamInformationCollector(key),
+ debugLabel: key.url,
);
}
@@ -80,12 +106,14 @@ class NetworkImage
// has been loaded or an error is thrown.
final StreamController chunkEvents = StreamController();
- return MultiFrameImageStreamCompleter(
- chunkEvents: chunkEvents.stream,
- codec: _loadAsync(key as NetworkImage, decode, chunkEvents),
- scale: key.scale,
- debugLabel: key.url,
+ return _ForwardingImageStreamCompleter(
+ _loadAsync(
+ key as NetworkImage,
+ decode,
+ chunkEvents,
+ ),
informationCollector: _imageStreamInformationCollector(key),
+ debugLabel: key.url,
);
}
@@ -101,10 +129,10 @@ class NetworkImage
return collector;
}
- // Html renderer does not support decoding network images to a specified size. The decode parameter
+ // HTML renderer does not support decoding network images to a specified size. The decode parameter
// here is ignored and `ui_web.createImageCodecFromUrl` will be used directly
// in place of the typical `instantiateImageCodec` method.
- Future _loadAsync(
+ Future _loadAsync(
NetworkImage key,
_SimpleDecoderCallback decode,
StreamController chunkEvents,
@@ -117,62 +145,143 @@ class NetworkImage
// We use a different method when headers are set because the
// `ui_web.createImageCodecFromUrl` method is not capable of handling headers.
- if (isSkiaWeb || containsNetworkImageHeaders) {
- final Completer completer =
- Completer();
- final web.XMLHttpRequest request = httpRequestFactory();
+ if (containsNetworkImageHeaders) {
+ // It is not possible to load an element and pass the headers with
+ // the request to fetch the image. Since the user has provided headers,
+ // this function should assume the headers are required to resolve to
+ // the correct resource and should not attempt to load the image in an
+ // tag without the headers.
- request.open('GET', key.url, true);
- request.responseType = 'arraybuffer';
- if (containsNetworkImageHeaders) {
- key.headers!.forEach((String header, String value) {
- request.setRequestHeader(header, value);
- });
+ // Resolve the Codec before passing it to
+ // [MultiFrameImageStreamCompleter] so any errors aren't reported
+ // twice (once from the MultiFrameImageStreamCompleter and again
+ // from the wrapping [ForwardingImageStreamCompleter]).
+ final ui.Codec codec = await _fetchImageBytes(decode);
+ return MultiFrameImageStreamCompleter(
+ chunkEvents: chunkEvents.stream,
+ codec: Future.value(codec),
+ scale: key.scale,
+ debugLabel: key.url,
+ informationCollector: _imageStreamInformationCollector(key),
+ );
+ } else if (isSkiaWeb) {
+ try {
+ // Resolve the Codec before passing it to
+ // [MultiFrameImageStreamCompleter] so any errors aren't reported
+ // twice (once from the MultiFrameImageStreamCompleter and again
+ // from the wrapping [ForwardingImageStreamCompleter]).
+ final ui.Codec codec = await _fetchImageBytes(decode);
+ return MultiFrameImageStreamCompleter(
+ chunkEvents: chunkEvents.stream,
+ codec: Future.value(codec),
+ scale: key.scale,
+ debugLabel: key.url,
+ informationCollector: _imageStreamInformationCollector(key),
+ );
+ } catch (e) {
+ // If we failed to fetch the bytes, try to load the image in an
+ // element instead.
+ final web.HTMLImageElement imageElement = imgElementFactory();
+ imageElement.src = key.url;
+ // Decode the element before creating the ImageStreamCompleter
+ // to avoid double reporting the error.
+ await imageElement.decode().toDart;
+ return OneFrameImageStreamCompleter(
+ Future.value(
+ WebImageInfo(
+ imageElement,
+ debugLabel: key.url,
+ ),
+ ),
+ informationCollector: _imageStreamInformationCollector(key),
+ )..debugLabel = key.url;
}
-
- request.addEventListener('load', (web.Event e) {
- final int status = request.status;
- final bool accepted = status >= 200 && status < 300;
- final bool fileUri = status == 0; // file:// URIs have status of 0.
- final bool notModified = status == 304;
- final bool unknownRedirect = status > 307 && status < 400;
- final bool success =
- accepted || fileUri || notModified || unknownRedirect;
-
- if (success) {
- completer.complete(request);
- } else {
- completer.completeError(e);
- throw image_provider.NetworkImageLoadException(
- statusCode: status, uri: resolved);
- }
- }.toJS);
-
- request.addEventListener('error',
- ((JSObject e) => completer.completeError(e)).toJS);
-
- request.send();
-
- await completer.future;
-
- final Uint8List bytes = (request.response! as JSArrayBuffer).toDart.asUint8List();
-
- if (bytes.lengthInBytes == 0) {
- throw image_provider.NetworkImageLoadException(
- statusCode: request.status, uri: resolved);
- }
- return decode(await ui.ImmutableBuffer.fromUint8List(bytes));
} else {
- return ui_web.createImageCodecFromUrl(
- resolved,
- chunkCallback: (int bytes, int total) {
- chunkEvents.add(ImageChunkEvent(
- cumulativeBytesLoaded: bytes, expectedTotalBytes: total));
- },
+ // This branch is only hit by the HTML renderer, which is deprecated. The
+ // HTML renderer supports loading images with CORS restrictions, so we
+ // don't need to catch errors and try loading the image in an tag
+ // in this case.
+
+ // Resolve the Codec before passing it to
+ // [MultiFrameImageStreamCompleter] so any errors aren't reported
+ // twice (once from the MultiFrameImageStreamCompleter) and again
+ // from the wrapping [ForwardingImageStreamCompleter].
+ final ui.Codec codec = await ui_web.createImageCodecFromUrl(
+ resolved,
+ chunkCallback: (int bytes, int total) {
+ chunkEvents.add(ImageChunkEvent(
+ cumulativeBytesLoaded: bytes, expectedTotalBytes: total));
+ },
+ );
+ return MultiFrameImageStreamCompleter(
+ chunkEvents: chunkEvents.stream,
+ codec: Future.value(codec),
+ scale: key.scale,
+ debugLabel: key.url,
+ informationCollector: _imageStreamInformationCollector(key),
);
}
}
+ Future _fetchImageBytes(
+ _SimpleDecoderCallback decode,
+ ) async {
+ final Uri resolved = Uri.base.resolve(url);
+
+ final bool containsNetworkImageHeaders = headers?.isNotEmpty ?? false;
+
+ final Completer completer =
+ Completer();
+ final web.XMLHttpRequest request = httpRequestFactory();
+
+ request.open('GET', url, true);
+ request.responseType = 'arraybuffer';
+ if (containsNetworkImageHeaders) {
+ headers!.forEach((String header, String value) {
+ request.setRequestHeader(header, value);
+ });
+ }
+
+ request.addEventListener('load', (web.Event e) {
+ final int status = request.status;
+ final bool accepted = status >= 200 && status < 300;
+ final bool fileUri = status == 0; // file:// URIs have status of 0.
+ final bool notModified = status == 304;
+ final bool unknownRedirect = status > 307 && status < 400;
+ final bool success =
+ accepted || fileUri || notModified || unknownRedirect;
+
+ if (success) {
+ completer.complete(request);
+ } else {
+ completer.completeError(image_provider.NetworkImageLoadException(
+ statusCode: status, uri: resolved));
+ }
+ }.toJS);
+
+ request.addEventListener(
+ 'error',
+ ((JSObject e) =>
+ completer.completeError(image_provider.NetworkImageLoadException(
+ statusCode: request.status,
+ uri: resolved,
+ ))).toJS,
+ );
+
+ request.send();
+
+ await completer.future;
+
+ final Uint8List bytes = (request.response! as JSArrayBuffer).toDart.asUint8List();
+
+ if (bytes.lengthInBytes == 0) {
+ throw image_provider.NetworkImageLoadException(
+ statusCode: request.status, uri: resolved);
+ }
+
+ return decode(await ui.ImmutableBuffer.fromUint8List(bytes));
+ }
+
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
@@ -187,3 +296,61 @@ class NetworkImage
@override
String toString() => '${objectRuntimeType(this, 'NetworkImage')}("$url", scale: ${scale.toStringAsFixed(1)})';
}
+
+/// An [ImageStreamCompleter] that delegates to another [ImageStreamCompleter]
+/// that is loaded asynchronously.
+///
+/// This completer keeps its child completer alive until this completer is disposed.
+class _ForwardingImageStreamCompleter extends ImageStreamCompleter {
+ _ForwardingImageStreamCompleter(this.task,
+ {InformationCollector? informationCollector, String? debugLabel}) {
+ this.debugLabel = debugLabel;
+ task.then((ImageStreamCompleter value) {
+ resolved = true;
+ if (_disposed) {
+ // Add a listener since the delegate completer won't dispose if it never
+ // had a listener.
+ value.addListener(ImageStreamListener((_, __) {}));
+ value.maybeDispose();
+ return;
+ }
+ completer = value;
+ handle = completer.keepAlive();
+ completer.addListener(ImageStreamListener(
+ (ImageInfo image, bool synchronousCall) {
+ setImage(image);
+ },
+ onChunk: (ImageChunkEvent event) {
+ reportImageChunkEvent(event);
+ },
+ onError:(Object exception, StackTrace? stackTrace) {
+ reportError(exception: exception, stack: stackTrace);
+ },
+ ));
+ }, onError: (Object error, StackTrace stack) {
+ reportError(
+ context: ErrorDescription('resolving an image stream completer'),
+ exception: error,
+ stack: stack,
+ informationCollector: informationCollector,
+ silent: true,
+ );
+ });
+ }
+
+ final Future task;
+ bool resolved = false;
+ late final ImageStreamCompleter completer;
+ late final ImageStreamCompleterHandle handle;
+
+ bool _disposed = false;
+
+ @override
+ void onDisposed() {
+ if (resolved) {
+ handle.dispose();
+ }
+ _disposed = true;
+ super.onDisposed();
+ }
+}
diff --git a/packages/flutter/lib/src/painting/_web_image_info_io.dart b/packages/flutter/lib/src/painting/_web_image_info_io.dart
new file mode 100644
index 0000000000..a88b52b70e
--- /dev/null
+++ b/packages/flutter/lib/src/painting/_web_image_info_io.dart
@@ -0,0 +1,40 @@
+// Copyright 2014 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' as ui;
+
+import 'image_stream.dart';
+
+/// An [ImageInfo] object indicating that the image can only be displayed in
+/// an element, and no [dart:ui.Image] can be created for it.
+///
+/// This occurs on the web when the image resource is from a different origin
+/// and is not configured for CORS. Since the image bytes cannot be directly
+/// fetched, [ui.Image]s cannot be created from it. However, the image can
+/// still be displayed if an element is used.
+class WebImageInfo implements ImageInfo {
+ @override
+ ImageInfo clone() => _unsupported();
+
+ @override
+ String? get debugLabel => _unsupported();
+
+ @override
+ void dispose() => _unsupported();
+
+ @override
+ ui.Image get image => _unsupported();
+
+ @override
+ bool isCloneOf(ImageInfo other) => _unsupported();
+
+ @override
+ double get scale => _unsupported();
+
+ @override
+ int get sizeBytes => _unsupported();
+
+ Never _unsupported() => throw UnsupportedError(
+ 'WebImageInfo should never be instantiated in a non-web context.');
+}
diff --git a/packages/flutter/lib/src/painting/_web_image_info_web.dart b/packages/flutter/lib/src/painting/_web_image_info_web.dart
new file mode 100644
index 0000000000..ee8468351c
--- /dev/null
+++ b/packages/flutter/lib/src/painting/_web_image_info_web.dart
@@ -0,0 +1,70 @@
+// Copyright 2014 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 '../web.dart' as web;
+import 'image_stream.dart';
+
+/// An [ImageInfo] object indicating that the image can only be displayed in
+/// an element, and no [dart:ui.Image] can be created for it.
+///
+/// This occurs on the web when the image resource is from a different origin
+/// and is not configured for CORS. Since the image bytes cannot be directly
+/// fetched, [ui.Image]s cannot be created from it. However, the image can
+/// still be displayed if an element is used.
+class WebImageInfo implements ImageInfo {
+ /// Creates a new [WebImageInfo] from a given element.
+ WebImageInfo(this.htmlImage, {this.debugLabel});
+
+ /// The element used to display this image. This element has
+ /// already been decoded, so size information can be retrieved from it.
+ final web.HTMLImageElement htmlImage;
+
+ @override
+ final String? debugLabel;
+
+ @override
+ WebImageInfo clone() {
+ // There is no need to actually clone the element here. We create
+ // another reference to the element and let the browser garbage
+ // collect it when there are no more live references.
+ return WebImageInfo(
+ htmlImage,
+ debugLabel: debugLabel,
+ );
+ }
+
+ @override
+ void dispose() {
+ // There is nothing to do here. There is no way to delete an element
+ // directly, the most we can do is remove it from the DOM. But the
+ // element here is never even added to the DOM. The browser will
+ // automatically garbage collect the element when there are no longer any
+ // live references to it.
+ }
+
+ @override
+ Image get image => throw UnsupportedError(
+ 'Could not create image data for this image because access to it is '
+ 'restricted by the Same-Origin Policy.\n'
+ 'See https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy');
+
+ @override
+ bool isCloneOf(ImageInfo other) {
+ if (other is! WebImageInfo) {
+ return false;
+ }
+
+ // It is a clone if it points to the same element.
+ return other.htmlImage == htmlImage && other.debugLabel == debugLabel;
+ }
+
+ @override
+ double get scale => 1.0;
+
+ @override
+ int get sizeBytes =>
+ (4 * htmlImage.naturalWidth * htmlImage.naturalHeight).toInt();
+}
diff --git a/packages/flutter/lib/src/painting/image_stream.dart b/packages/flutter/lib/src/painting/image_stream.dart
index f43942a8c3..8cb03f2af7 100644
--- a/packages/flutter/lib/src/painting/image_stream.dart
+++ b/packages/flutter/lib/src/painting/image_stream.dart
@@ -690,6 +690,24 @@ abstract class ImageStreamCompleter with Diagnosticable {
bool _disposed = false;
+ /// Called when this [ImageStreamCompleter] has actually been disposed.
+ ///
+ /// Subclasses should override this if they need to clean up resources when
+ /// they are disposed.
+ @mustCallSuper
+ @protected
+ void onDisposed() {}
+
+ /// Disposes this [ImageStreamCompleter] unless:
+ /// 1. It has never had a listener
+ /// 2. It is already disposed
+ /// 3. It has listeners.
+ /// 4. It has active "keep alive" handles.
+ @nonVirtual
+ void maybeDispose() {
+ _maybeDispose();
+ }
+
@mustCallSuper
void _maybeDispose() {
if (!_hadAtLeastOneListener || _disposed || _listeners.isNotEmpty || _keepAliveHandles != 0) {
@@ -700,6 +718,7 @@ abstract class ImageStreamCompleter with Diagnosticable {
_currentImage?.dispose();
_currentImage = null;
_disposed = true;
+ onDisposed();
}
void _checkDisposed() {
diff --git a/packages/flutter/lib/src/web.dart b/packages/flutter/lib/src/web.dart
index 9bfb3e3497..05de1572fa 100644
--- a/packages/flutter/lib/src/web.dart
+++ b/packages/flutter/lib/src/web.dart
@@ -67,6 +67,7 @@ extension type DOMTokenList._(JSObject _) implements JSObject {
extension type Element._(JSObject _) implements Node, JSObject {
external DOMTokenList get classList;
external void append(JSAny nodes);
+ external void remove();
}
extension type Event._(JSObject _) implements JSObject {}
@@ -83,6 +84,7 @@ extension type HTMLElement._(JSObject _) implements Element, JSObject {
external String get innerText;
external set innerText(String value);
external CSSStyleDeclaration get style;
+ external HTMLElement cloneNode(bool deep);
}
extension type HTMLHeadElement._(JSObject _) implements HTMLElement, JSObject {}
@@ -91,6 +93,22 @@ extension type HTMLStyleElement._(JSObject _) implements HTMLElement, JSObject {
external CSSStyleSheet? get sheet;
}
+extension type HTMLImageElement._(JSObject _) implements HTMLElement, JSObject {
+ external String get src;
+ external set src(String value);
+ external num get naturalWidth;
+ external num get naturalHeight;
+ external JSPromise decode();
+}
+
+extension type HTMLCanvasElement._(JSObject _) implements HTMLElement, JSObject {
+ external int get width;
+ external set width(int value);
+ external int get height;
+ external set height(int value);
+ external String toDataURL();
+}
+
extension type MediaQueryList._(JSObject _) implements EventTarget, JSObject {
external bool get matches;
}
@@ -120,6 +138,7 @@ extension type Window._(JSObject _) implements EventTarget, JSObject {
external Navigator get navigator;
external MediaQueryList matchMedia(String query);
external Selection? getSelection();
+ external String get origin;
}
extension type XMLHttpRequest._(JSObject _)
diff --git a/packages/flutter/lib/src/widgets/_web_image_io.dart b/packages/flutter/lib/src/widgets/_web_image_io.dart
new file mode 100644
index 0000000000..0023629593
--- /dev/null
+++ b/packages/flutter/lib/src/widgets/_web_image_io.dart
@@ -0,0 +1,50 @@
+// Copyright 2014 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 '../painting/_web_image_info_io.dart';
+import 'basic.dart';
+import 'framework.dart';
+
+/// A [Widget] that displays an image that is backed by an element.
+class RawWebImage extends StatelessWidget {
+ /// Creates a [RawWebImage].
+ RawWebImage({
+ super.key,
+ required this.image,
+ this.debugImageLabel,
+ this.width,
+ this.height,
+ this.fit,
+ this.alignment = Alignment.center,
+ this.matchTextDirection = false,
+ }) {
+ throw UnsupportedError('Cannot create a $RawWebImage when not running on the web');
+ }
+
+ /// The underlying `` element to be displayed.
+ final WebImageInfo image;
+
+ /// A debug label explaining the image.
+ final String? debugImageLabel;
+
+ /// The requested width for this widget.
+ final double? width;
+
+ /// The requested height for this widget.
+ final double? height;
+
+ /// How the `` should be inscribed in the box constraining it.
+ final BoxFit? fit;
+
+ /// How the image should be aligned in the box constraining it.
+ final AlignmentGeometry alignment;
+
+ /// Whether or not the alignment of the image should match the text direction.
+ final bool matchTextDirection;
+
+ @override
+ Widget build(BuildContext context) {
+ throw UnsupportedError('It is impossible to instantiate a RawWebImage when not running on the web');
+ }
+}
diff --git a/packages/flutter/lib/src/widgets/_web_image_web.dart b/packages/flutter/lib/src/widgets/_web_image_web.dart
new file mode 100644
index 0000000000..ae7af6c42f
--- /dev/null
+++ b/packages/flutter/lib/src/widgets/_web_image_web.dart
@@ -0,0 +1,382 @@
+// Copyright 2014 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_web' as ui_web;
+
+import 'package:flutter/foundation.dart';
+
+import '../painting/_web_image_info_web.dart';
+import '../rendering/box.dart';
+import '../rendering/shifted_box.dart';
+import '../web.dart' as web;
+import 'basic.dart';
+import 'framework.dart';
+import 'platform_view.dart';
+
+/// Displays an `` element with `src` set to [src].
+class ImgElementPlatformView extends StatelessWidget {
+ /// Creates a platform view backed with an `` element.
+ ImgElementPlatformView(this.src, {super.key}) {
+ if (!_registered) {
+ _register();
+ }
+ }
+
+ static const String _viewType = 'Flutter__ImgElementImage__';
+ static bool _registered = false;
+
+ static void _register() {
+ assert(!_registered);
+ _registered = true;
+ ui_web.platformViewRegistry.registerViewFactory(_viewType, (int viewId,
+ {Object? params}) {
+ final Map