Skip to content

Commit 8c5874b

Browse files
dkwingsmtmaheshj01
authored andcommitted
[Web] Allow specifying the strategy on when to use <img> element to display images (flutter#159917)
This PR follows the discussion under flutter#157755 and adds a flag to determine when `<img>` elements are used. By default the feature is turned off. Instead of just a boolean for on & off, I made it an enum to accept multiple options. Notably, an `always` option can be useful when the developer wants a unified experience regardless of the image origin (such as when displaying an image from arbitrary URLs.) ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [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
1 parent 9b4831a commit 8c5874b

10 files changed

+378
-84
lines changed

packages/flutter/lib/src/painting/_network_image_io.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ typedef _SimpleDecoderCallback = Future<ui.Codec> Function(ui.ImmutableBuffer bu
2121
class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkImage>
2222
implements image_provider.NetworkImage {
2323
/// Creates an object that fetches the image at the given URL.
24-
const NetworkImage(this.url, {this.scale = 1.0, this.headers});
24+
const NetworkImage(
25+
this.url, {
26+
this.scale = 1.0,
27+
this.headers,
28+
this.webHtmlElementStrategy = image_provider.WebHtmlElementStrategy.never,
29+
});
2530

2631
@override
2732
final String url;
@@ -32,6 +37,9 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
3237
@override
3338
final Map<String, String>? headers;
3439

40+
@override
41+
final image_provider.WebHtmlElementStrategy webHtmlElementStrategy;
42+
3543
@override
3644
Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
3745
return SynchronousFuture<NetworkImage>(this);

packages/flutter/lib/src/painting/_network_image_web.dart

Lines changed: 65 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ import 'image_stream.dart';
1818
/// used for testing purposes.
1919
typedef HttpRequestFactory = web.XMLHttpRequest Function();
2020

21-
/// The type for an overridable factory function for creating <img> elements,
22-
/// used for testing purposes.
23-
typedef ImgElementFactory = web.HTMLImageElement Function();
21+
/// The type for an overridable factory function for creating HTML elements to
22+
/// display images, used for testing purposes.
23+
typedef HtmlElementFactory = web.HTMLImageElement Function();
2424

2525
// Method signature for _loadAsync decode callbacks.
2626
typedef _SimpleDecoderCallback = Future<ui.Codec> Function(ui.ImmutableBuffer buffer);
@@ -40,17 +40,17 @@ void debugRestoreHttpRequestFactory() {
4040
httpRequestFactory = _httpClient;
4141
}
4242

43-
/// The default <img> element factory.
43+
/// The default HTML element factory.
4444
web.HTMLImageElement _imgElementFactory() {
4545
return web.document.createElement('img') as web.HTMLImageElement;
4646
}
4747

48-
/// The factory function that creates <img> elements, can be overridden for
48+
/// The factory function that creates HTML elements, can be overridden for
4949
/// tests.
5050
@visibleForTesting
51-
ImgElementFactory imgElementFactory = _imgElementFactory;
51+
HtmlElementFactory imgElementFactory = _imgElementFactory;
5252

53-
/// Restores the default <img> element factory.
53+
/// Restores the default HTML element factory.
5454
@visibleForTesting
5555
void debugRestoreImgElementFactory() {
5656
imgElementFactory = _imgElementFactory;
@@ -63,7 +63,12 @@ void debugRestoreImgElementFactory() {
6363
class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkImage>
6464
implements image_provider.NetworkImage {
6565
/// Creates an object that fetches the image at the given URL.
66-
const NetworkImage(this.url, {this.scale = 1.0, this.headers});
66+
const NetworkImage(
67+
this.url, {
68+
this.scale = 1.0,
69+
this.headers,
70+
this.webHtmlElementStrategy = image_provider.WebHtmlElementStrategy.never,
71+
});
6772

6873
@override
6974
final String url;
@@ -74,6 +79,9 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
7479
@override
7580
final Map<String, String>? headers;
7681

82+
@override
83+
final image_provider.WebHtmlElementStrategy webHtmlElementStrategy;
84+
7785
@override
7886
Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
7987
return SynchronousFuture<NetworkImage>(this);
@@ -136,19 +144,7 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
136144
) async {
137145
assert(key == this);
138146

139-
final Uri resolved = Uri.base.resolve(key.url);
140-
141-
final bool containsNetworkImageHeaders = key.headers?.isNotEmpty ?? false;
142-
143-
// We use a different method when headers are set because the
144-
// `ui_web.createImageCodecFromUrl` method is not capable of handling headers.
145-
if (containsNetworkImageHeaders) {
146-
// It is not possible to load an <img> element and pass the headers with
147-
// the request to fetch the image. Since the user has provided headers,
148-
// this function should assume the headers are required to resolve to
149-
// the correct resource and should not attempt to load the image in an
150-
// <img> tag without the headers.
151-
147+
Future<ImageStreamCompleter> loadViaDecode() async {
152148
// Resolve the Codec before passing it to
153149
// [MultiFrameImageStreamCompleter] so any errors aren't reported
154150
// twice (once from the MultiFrameImageStreamCompleter and again
@@ -161,34 +157,38 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
161157
debugLabel: key.url,
162158
informationCollector: _imageStreamInformationCollector(key),
163159
);
164-
} else if (isSkiaWeb) {
165-
try {
166-
// Resolve the Codec before passing it to
167-
// [MultiFrameImageStreamCompleter] so any errors aren't reported
168-
// twice (once from the MultiFrameImageStreamCompleter and again
169-
// from the wrapping [ForwardingImageStreamCompleter]).
170-
final ui.Codec codec = await _fetchImageBytes(decode);
171-
return MultiFrameImageStreamCompleter(
172-
chunkEvents: chunkEvents.stream,
173-
codec: Future<ui.Codec>.value(codec),
174-
scale: key.scale,
175-
debugLabel: key.url,
176-
informationCollector: _imageStreamInformationCollector(key),
177-
);
178-
} catch (e) {
179-
// If we failed to fetch the bytes, try to load the image in an <img>
180-
// element instead.
181-
final web.HTMLImageElement imageElement = imgElementFactory();
182-
imageElement.src = key.url;
183-
// Decode the <img> element before creating the ImageStreamCompleter
184-
// to avoid double reporting the error.
185-
await imageElement.decode().toDart;
186-
return OneFrameImageStreamCompleter(
187-
Future<ImageInfo>.value(WebImageInfo(imageElement, debugLabel: key.url)),
188-
informationCollector: _imageStreamInformationCollector(key),
189-
)..debugLabel = key.url;
190-
}
191-
} else {
160+
}
161+
162+
Future<ImageStreamCompleter> loadViaImgElement() async {
163+
// If we failed to fetch the bytes, try to load the image in an <img>
164+
// element instead.
165+
final web.HTMLImageElement imageElement = imgElementFactory();
166+
imageElement.src = key.url;
167+
// Decode the <img> element before creating the ImageStreamCompleter
168+
// to avoid double reporting the error.
169+
await imageElement.decode().toDart;
170+
return OneFrameImageStreamCompleter(
171+
Future<ImageInfo>.value(WebImageInfo(imageElement, debugLabel: key.url)),
172+
informationCollector: _imageStreamInformationCollector(key),
173+
)..debugLabel = key.url;
174+
}
175+
176+
final bool containsNetworkImageHeaders = key.headers?.isNotEmpty ?? false;
177+
// When headers are set, the image can only be loaded by decoding.
178+
//
179+
// For the HTML renderer, `ui_web.createImageCodecFromUrl` method is not
180+
// capable of handling headers.
181+
//
182+
// For CanvasKit and Skwasm, it is not possible to load an <img> element and
183+
// pass the headers with the request to fetch the image. Since the user has
184+
// provided headers, this function should assume the headers are required to
185+
// resolve to the correct resource and should not attempt to load the image
186+
// in an <img> tag without the headers.
187+
if (containsNetworkImageHeaders) {
188+
return loadViaDecode();
189+
}
190+
191+
if (!isSkiaWeb) {
192192
// This branch is only hit by the HTML renderer, which is deprecated. The
193193
// HTML renderer supports loading images with CORS restrictions, so we
194194
// don't need to catch errors and try loading the image in an <img> tag
@@ -198,6 +198,7 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
198198
// [MultiFrameImageStreamCompleter] so any errors aren't reported
199199
// twice (once from the MultiFrameImageStreamCompleter) and again
200200
// from the wrapping [ForwardingImageStreamCompleter].
201+
final Uri resolved = Uri.base.resolve(key.url);
201202
final ui.Codec codec = await ui_web.createImageCodecFromUrl(
202203
resolved,
203204
chunkCallback: (int bytes, int total) {
@@ -212,6 +213,21 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
212213
informationCollector: _imageStreamInformationCollector(key),
213214
);
214215
}
216+
217+
switch (webHtmlElementStrategy) {
218+
case image_provider.WebHtmlElementStrategy.never:
219+
return loadViaDecode();
220+
case image_provider.WebHtmlElementStrategy.prefer:
221+
return loadViaImgElement();
222+
case image_provider.WebHtmlElementStrategy.fallback:
223+
try {
224+
// Await here so that errors occurred during the asynchronous process
225+
// of `loadViaDecode` are caught and triggers `loadViaImgElement`.
226+
return await loadViaDecode();
227+
} catch (e) {
228+
return loadViaImgElement();
229+
}
230+
}
215231
}
216232

217233
Future<ui.Codec> _fetchImageBytes(_SimpleDecoderCallback decode) async {

packages/flutter/lib/src/painting/_web_image_info_io.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import 'dart:ui' as ui;
77
import 'image_stream.dart';
88

99
/// An [ImageInfo] object indicating that the image can only be displayed in
10-
/// an <img> element, and no [dart:ui.Image] can be created for it.
10+
/// an HTML element, and no [dart:ui.Image] can be created for it.
1111
///
1212
/// This occurs on the web when the image resource is from a different origin
1313
/// and is not configured for CORS. Since the image bytes cannot be directly
1414
/// fetched, [ui.Image]s cannot be created from it. However, the image can
15-
/// still be displayed if an <img> element is used.
15+
/// still be displayed if an HTML element is used.
1616
class WebImageInfo implements ImageInfo {
1717
@override
1818
ImageInfo clone() => _unsupported();

packages/flutter/lib/src/painting/_web_image_info_web.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@ import '../web.dart' as web;
88
import 'image_stream.dart';
99

1010
/// An [ImageInfo] object indicating that the image can only be displayed in
11-
/// an <img> element, and no [dart:ui.Image] can be created for it.
11+
/// an HTML element, and no [dart:ui.Image] can be created for it.
1212
///
1313
/// This occurs on the web when the image resource is from a different origin
1414
/// and is not configured for CORS. Since the image bytes cannot be directly
1515
/// fetched, [Image]s cannot be created from it. However, the image can
16-
/// still be displayed if an <img> element is used.
16+
/// still be displayed if an HTML element is used.
1717
class WebImageInfo implements ImageInfo {
18-
/// Creates a new [WebImageInfo] from a given <img> element.
18+
/// Creates a new [WebImageInfo] from a given HTML element.
1919
WebImageInfo(this.htmlImage, {this.debugLabel});
2020

21-
/// The <img> element used to display this image. This <img> element has
22-
/// already been decoded, so size information can be retrieved from it.
21+
/// The HTML element used to display this image. This HTML element has already
22+
/// decoded the image, so size information can be retrieved from it.
2323
final web.HTMLImageElement htmlImage;
2424

2525
@override

packages/flutter/lib/src/painting/image_provider.dart

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
// late BuildContext context;
77

88
/// @docImport 'package:flutter/widgets.dart';
9+
/// @docImport '_web_image_info_io.dart';
910
library;
1011

1112
import 'dart:async';
@@ -1467,10 +1468,46 @@ class ResizeImage extends ImageProvider<ResizeImageKey> {
14671468
}
14681469
}
14691470

1471+
/// The strategy for [Image.network] and [NetworkImage] to decide whether to
1472+
/// display images in HTML elements contained in a platform view instead of
1473+
/// fetching bytes.
1474+
///
1475+
/// See [Image.network] for more explanation on the impact.
1476+
///
1477+
/// This option is only effective on the Web platform. Other platforms always
1478+
/// display network images by fetching bytes.
1479+
enum WebHtmlElementStrategy {
1480+
/// Only show images by fetching bytes, and report errors if the fetch
1481+
/// encounters errors.
1482+
never,
1483+
1484+
/// Prefer fetching bytes to display images, and fall back to HTML elements
1485+
/// when fetching bytes is not available.
1486+
///
1487+
/// This strategy uses HTML elements only if `headers` is empty and the fetch
1488+
/// encounters errors. Errors may still be reported if neither approach works.
1489+
fallback,
1490+
1491+
/// Prefer HTML elements to display images, and fall back to fetching bytes
1492+
/// when HTML elements do not work.
1493+
///
1494+
/// This strategy fetches bytes only if `headers` is not empty, since HTML
1495+
/// elements do not support headers. Errors may still be reported if neither
1496+
/// approach works.
1497+
prefer,
1498+
}
1499+
14701500
/// Fetches the given URL from the network, associating it with the given scale.
14711501
///
14721502
/// The image will be cached regardless of cache headers from the server.
14731503
///
1504+
/// Typically this class resolves to an image stream that ultimately produces
1505+
/// [dart:ui.Image]s. On the Web platform, the [webHtmlElementStrategy]
1506+
/// parameter can be used to make the image stream ultimately produce an
1507+
/// [WebImageInfo] instead, which makes [Image.network] display the image as an
1508+
/// HTML element in a platform view. The feature is by default turned off
1509+
/// ([WebHtmlElementStrategy.never]). See [Image.network] for more explanation.
1510+
///
14741511
/// See also:
14751512
///
14761513
/// * [Image.network] for a shorthand of an [Image] widget backed by [NetworkImage].
@@ -1484,8 +1521,15 @@ abstract class NetworkImage extends ImageProvider<NetworkImage> {
14841521
///
14851522
/// The [scale] argument is the linear scale factor for drawing this image at
14861523
/// its intended size. See [ImageInfo.scale] for more information.
1487-
const factory NetworkImage(String url, {double scale, Map<String, String>? headers}) =
1488-
network_image.NetworkImage;
1524+
///
1525+
/// The [webHtmlElementStrategy] option is by default
1526+
/// [WebHtmlElementStrategy.never].
1527+
const factory NetworkImage(
1528+
String url, {
1529+
double scale,
1530+
Map<String, String>? headers,
1531+
WebHtmlElementStrategy webHtmlElementStrategy,
1532+
}) = network_image.NetworkImage;
14891533

14901534
/// The URL from which the image will be fetched.
14911535
String get url;
@@ -1498,6 +1542,17 @@ abstract class NetworkImage extends ImageProvider<NetworkImage> {
14981542
/// When running Flutter on the web, headers are not used.
14991543
Map<String, String>? get headers;
15001544

1545+
/// On the Web platform, specifies when the image is loaded as a
1546+
/// [WebImageInfo], which causes [Image.network] to display the image in an
1547+
/// HTML element in a platform view.
1548+
///
1549+
/// See [Image.network] for more explanation.
1550+
///
1551+
/// Defaults to [WebHtmlElementStrategy.never].
1552+
///
1553+
/// Has no effect on other platforms, which always fetch bytes.
1554+
WebHtmlElementStrategy get webHtmlElementStrategy;
1555+
15011556
@override
15021557
ImageStreamCompleter loadBuffer(NetworkImage key, DecoderBufferCallback decode);
15031558

packages/flutter/lib/src/widgets/_web_image_io.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import '../painting/_web_image_info_io.dart';
66
import 'basic.dart';
77
import 'framework.dart';
88

9-
/// A [Widget] that displays an image that is backed by an <img> element.
9+
/// A [Widget] that displays an image that is backed by an HTML element.
1010
class RawWebImage extends StatelessWidget {
1111
/// Creates a [RawWebImage].
1212
RawWebImage({
@@ -22,7 +22,7 @@ class RawWebImage extends StatelessWidget {
2222
throw UnsupportedError('Cannot create a $RawWebImage when not running on the web');
2323
}
2424

25-
/// The underlying `<img>` element to be displayed.
25+
/// The underlying HTML element to be displayed.
2626
final WebImageInfo image;
2727

2828
/// A debug label explaining the image.
@@ -34,7 +34,7 @@ class RawWebImage extends StatelessWidget {
3434
/// The requested height for this widget.
3535
final double? height;
3636

37-
/// How the `<img>` should be inscribed in the box constraining it.
37+
/// How the HTML element should be inscribed in the box constraining it.
3838
final BoxFit? fit;
3939

4040
/// How the image should be aligned in the box constraining it.

packages/flutter/lib/src/widgets/_web_image_web.dart

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ class ImgElementPlatformView extends StatelessWidget {
5454
}
5555
}
5656

57-
/// A widget which displays and lays out an underlying `<img>` platform view.
57+
/// A widget which displays and lays out an underlying HTML element in a
58+
/// platform view.
5859
class RawWebImage extends SingleChildRenderObjectWidget {
5960
/// Creates a [RawWebImage].
6061
RawWebImage({
@@ -68,7 +69,7 @@ class RawWebImage extends SingleChildRenderObjectWidget {
6869
this.matchTextDirection = false,
6970
}) : super(child: ImgElementPlatformView(image.htmlImage.src));
7071

71-
/// The underlying `<img>` element to be displayed.
72+
/// The underlying HTML element to be displayed.
7273
final WebImageInfo image;
7374

7475
/// A debug label explaining the image.
@@ -80,7 +81,7 @@ class RawWebImage extends SingleChildRenderObjectWidget {
8081
/// The requested height for this widget.
8182
final double? height;
8283

83-
/// How the `<img>` should be inscribed in the box constraining it.
84+
/// How the HTML element should be inscribed in the box constraining it.
8485
final BoxFit? fit;
8586

8687
/// How the image should be aligned in the box constraining it.
@@ -117,7 +118,7 @@ class RawWebImage extends SingleChildRenderObjectWidget {
117118
}
118119
}
119120

120-
/// Lays out and positions the child `<img>` element similarly to [RenderImage].
121+
/// Lays out and positions the child HTML element similarly to [RenderImage].
121122
class RenderWebImage extends RenderShiftedBox {
122123
/// Creates a new [RenderWebImage].
123124
RenderWebImage({

0 commit comments

Comments
 (0)