@@ -10,28 +10,52 @@ import 'dart:ui_web' as ui_web;
10
10
import 'package:flutter/foundation.dart' ;
11
11
12
12
import '../web.dart' as web;
13
+ import '_web_image_info_web.dart' ;
13
14
import 'image_provider.dart' as image_provider;
14
15
import 'image_stream.dart' ;
15
16
16
- /// Creates a type for an overridable factory function for testing purposes.
17
+ /// The type for an overridable factory function for creating an HTTP request,
18
+ /// used for testing purposes.
17
19
typedef HttpRequestFactory = web.XMLHttpRequest Function ();
18
20
21
+ /// The type for an overridable factory function for creating <img> elements,
22
+ /// used for testing purposes.
23
+ typedef ImgElementFactory = web.HTMLImageElement Function ();
24
+
19
25
// Method signature for _loadAsync decode callbacks.
20
26
typedef _SimpleDecoderCallback = Future <ui.Codec > Function (ui.ImmutableBuffer buffer);
21
27
22
- /// Default HTTP client.
28
+ /// The default HTTP client.
23
29
web.XMLHttpRequest _httpClient () {
24
30
return web.XMLHttpRequest ();
25
31
}
26
32
27
33
/// Creates an overridable factory function.
34
+ @visibleForTesting
28
35
HttpRequestFactory httpRequestFactory = _httpClient;
29
36
30
- /// Restores to the default HTTP request factory.
37
+ /// Restores the default HTTP request factory.
38
+ @visibleForTesting
31
39
void debugRestoreHttpRequestFactory () {
32
40
httpRequestFactory = _httpClient;
33
41
}
34
42
43
+ /// The default <img> element factory.
44
+ web.HTMLImageElement _imgElementFactory () {
45
+ return web.document.createElement ('img' ) as web.HTMLImageElement ;
46
+ }
47
+
48
+ /// The factory function that creates <img> elements, can be overridden for
49
+ /// tests.
50
+ @visibleForTesting
51
+ ImgElementFactory imgElementFactory = _imgElementFactory;
52
+
53
+ /// Restores the default <img> element factory.
54
+ @visibleForTesting
55
+ void debugRestoreImgElementFactory () {
56
+ imgElementFactory = _imgElementFactory;
57
+ }
58
+
35
59
/// The web implementation of [image_provider.NetworkImage] .
36
60
///
37
61
/// NetworkImage on the web does not support decoding to a specified size.
@@ -64,12 +88,14 @@ class NetworkImage
64
88
final StreamController <ImageChunkEvent > chunkEvents =
65
89
StreamController <ImageChunkEvent >();
66
90
67
- return MultiFrameImageStreamCompleter (
68
- chunkEvents: chunkEvents.stream,
69
- codec: _loadAsync (key as NetworkImage , decode, chunkEvents),
70
- scale: key.scale,
71
- debugLabel: key.url,
91
+ return _ForwardingImageStreamCompleter (
92
+ _loadAsync (
93
+ key as NetworkImage ,
94
+ decode,
95
+ chunkEvents,
96
+ ),
72
97
informationCollector: _imageStreamInformationCollector (key),
98
+ debugLabel: key.url,
73
99
);
74
100
}
75
101
@@ -80,12 +106,14 @@ class NetworkImage
80
106
// has been loaded or an error is thrown.
81
107
final StreamController <ImageChunkEvent > chunkEvents = StreamController <ImageChunkEvent >();
82
108
83
- return MultiFrameImageStreamCompleter (
84
- chunkEvents: chunkEvents.stream,
85
- codec: _loadAsync (key as NetworkImage , decode, chunkEvents),
86
- scale: key.scale,
87
- debugLabel: key.url,
109
+ return _ForwardingImageStreamCompleter (
110
+ _loadAsync (
111
+ key as NetworkImage ,
112
+ decode,
113
+ chunkEvents,
114
+ ),
88
115
informationCollector: _imageStreamInformationCollector (key),
116
+ debugLabel: key.url,
89
117
);
90
118
}
91
119
@@ -101,10 +129,10 @@ class NetworkImage
101
129
return collector;
102
130
}
103
131
104
- // Html renderer does not support decoding network images to a specified size. The decode parameter
132
+ // HTML renderer does not support decoding network images to a specified size. The decode parameter
105
133
// here is ignored and `ui_web.createImageCodecFromUrl` will be used directly
106
134
// in place of the typical `instantiateImageCodec` method.
107
- Future <ui. Codec > _loadAsync (
135
+ Future <ImageStreamCompleter > _loadAsync (
108
136
NetworkImage key,
109
137
_SimpleDecoderCallback decode,
110
138
StreamController <ImageChunkEvent > chunkEvents,
@@ -117,60 +145,141 @@ class NetworkImage
117
145
118
146
// We use a different method when headers are set because the
119
147
// `ui_web.createImageCodecFromUrl` method is not capable of handling headers.
120
- if (isSkiaWeb || containsNetworkImageHeaders) {
121
- final Completer <web.XMLHttpRequest > completer =
122
- Completer <web.XMLHttpRequest >();
123
- final web.XMLHttpRequest request = httpRequestFactory ();
124
-
125
- request.open ('GET' , key.url, true );
126
- request.responseType = 'arraybuffer' ;
127
- if (containsNetworkImageHeaders) {
128
- key.headers! .forEach ((String header, String value) {
129
- request.setRequestHeader (header, value);
130
- });
148
+ if (containsNetworkImageHeaders) {
149
+ // It is not possible to load an <img> element and pass the headers with
150
+ // the request to fetch the image. Since the user has provided headers,
151
+ // this function should assume the headers are required to resolve to
152
+ // the correct resource and should not attempt to load the image in an
153
+ // <img> tag without the headers.
154
+
155
+ // Resolve the Codec before passing it to
156
+ // [MultiFrameImageStreamCompleter] so any errors aren't reported
157
+ // twice (once from the MultiFrameImageStreamCompleter and again
158
+ // from the wrapping [ForwardingImageStreamCompleter]).
159
+ final ui.Codec codec = await _fetchImageBytes (decode);
160
+ return MultiFrameImageStreamCompleter (
161
+ chunkEvents: chunkEvents.stream,
162
+ codec: Future <ui.Codec >.value (codec),
163
+ scale: key.scale,
164
+ debugLabel: key.url,
165
+ informationCollector: _imageStreamInformationCollector (key),
166
+ );
167
+ } else if (isSkiaWeb) {
168
+ try {
169
+ // Resolve the Codec before passing it to
170
+ // [MultiFrameImageStreamCompleter] so any errors aren't reported
171
+ // twice (once from the MultiFrameImageStreamCompleter and again
172
+ // from the wrapping [ForwardingImageStreamCompleter]).
173
+ final ui.Codec codec = await _fetchImageBytes (decode);
174
+ return MultiFrameImageStreamCompleter (
175
+ chunkEvents: chunkEvents.stream,
176
+ codec: Future <ui.Codec >.value (codec),
177
+ scale: key.scale,
178
+ debugLabel: key.url,
179
+ informationCollector: _imageStreamInformationCollector (key),
180
+ );
181
+ } catch (e) {
182
+ // If we failed to fetch the bytes, try to load the image in an <img>
183
+ // element instead.
184
+ final web.HTMLImageElement imageElement = imgElementFactory ();
185
+ imageElement.src = key.url;
186
+ // Decode the <img> element before creating the ImageStreamCompleter
187
+ // to avoid double reporting the error.
188
+ await imageElement.decode ().toDart;
189
+ return OneFrameImageStreamCompleter (
190
+ Future <ImageInfo >.value (
191
+ WebImageInfo (
192
+ imageElement,
193
+ debugLabel: key.url,
194
+ ),
195
+ ),
196
+ informationCollector: _imageStreamInformationCollector (key),
197
+ )..debugLabel = key.url;
131
198
}
199
+ } else {
200
+ // This branch is only hit by the HTML renderer, which is deprecated. The
201
+ // HTML renderer supports loading images with CORS restrictions, so we
202
+ // don't need to catch errors and try loading the image in an <img> tag
203
+ // in this case.
204
+
205
+ // Resolve the Codec before passing it to
206
+ // [MultiFrameImageStreamCompleter] so any errors aren't reported
207
+ // twice (once from the MultiFrameImageStreamCompleter) and again
208
+ // from the wrapping [ForwardingImageStreamCompleter].
209
+ final ui.Codec codec = await ui_web.createImageCodecFromUrl (
210
+ resolved,
211
+ chunkCallback: (int bytes, int total) {
212
+ chunkEvents.add (ImageChunkEvent (
213
+ cumulativeBytesLoaded: bytes, expectedTotalBytes: total));
214
+ },
215
+ );
216
+ return MultiFrameImageStreamCompleter (
217
+ chunkEvents: chunkEvents.stream,
218
+ codec: Future <ui.Codec >.value (codec),
219
+ scale: key.scale,
220
+ debugLabel: key.url,
221
+ informationCollector: _imageStreamInformationCollector (key),
222
+ );
223
+ }
224
+ }
132
225
133
- request.addEventListener ('load' , (web.Event e) {
134
- final int status = request.status;
135
- final bool accepted = status >= 200 && status < 300 ;
136
- final bool fileUri = status == 0 ; // file:// URIs have status of 0.
137
- final bool notModified = status == 304 ;
138
- final bool unknownRedirect = status > 307 && status < 400 ;
139
- final bool success =
140
- accepted || fileUri || notModified || unknownRedirect;
226
+ Future <ui.Codec > _fetchImageBytes (
227
+ _SimpleDecoderCallback decode,
228
+ ) async {
229
+ final Uri resolved = Uri .base .resolve (url);
141
230
142
- if (success) {
143
- completer.complete (request);
144
- } else {
145
- completer.completeError (e);
146
- throw image_provider.NetworkImageLoadException (
147
- statusCode: status, uri: resolved);
148
- }
149
- }.toJS);
231
+ final bool containsNetworkImageHeaders = headers? .isNotEmpty ?? false ;
150
232
151
- request.addEventListener ('error' ,
152
- ((JSObject e) => completer.completeError (e)).toJS);
233
+ final Completer <web.XMLHttpRequest > completer =
234
+ Completer <web.XMLHttpRequest >();
235
+ final web.XMLHttpRequest request = httpRequestFactory ();
153
236
154
- request.send ();
237
+ request.open ('GET' , url, true );
238
+ request.responseType = 'arraybuffer' ;
239
+ if (containsNetworkImageHeaders) {
240
+ headers! .forEach ((String header, String value) {
241
+ request.setRequestHeader (header, value);
242
+ });
243
+ }
155
244
156
- await completer.future;
245
+ request.addEventListener ('load' , (web.Event e) {
246
+ final int status = request.status;
247
+ final bool accepted = status >= 200 && status < 300 ;
248
+ final bool fileUri = status == 0 ; // file:// URIs have status of 0.
249
+ final bool notModified = status == 304 ;
250
+ final bool unknownRedirect = status > 307 && status < 400 ;
251
+ final bool success =
252
+ accepted || fileUri || notModified || unknownRedirect;
253
+
254
+ if (success) {
255
+ completer.complete (request);
256
+ } else {
257
+ completer.completeError (image_provider.NetworkImageLoadException (
258
+ statusCode: status, uri: resolved));
259
+ }
260
+ }.toJS);
261
+
262
+ request.addEventListener (
263
+ 'error' ,
264
+ ((JSObject e) =>
265
+ completer.completeError (image_provider.NetworkImageLoadException (
266
+ statusCode: request.status,
267
+ uri: resolved,
268
+ ))).toJS,
269
+ );
157
270
158
- final Uint8List bytes = ( request.response ! as JSArrayBuffer ).toDart. asUint8List ();
271
+ request.send ();
159
272
160
- if (bytes.lengthInBytes == 0 ) {
161
- throw image_provider.NetworkImageLoadException (
162
- statusCode: request.status, uri: resolved);
163
- }
164
- return decode (await ui.ImmutableBuffer .fromUint8List (bytes));
165
- } else {
166
- return ui_web.createImageCodecFromUrl (
167
- resolved,
168
- chunkCallback: (int bytes, int total) {
169
- chunkEvents.add (ImageChunkEvent (
170
- cumulativeBytesLoaded: bytes, expectedTotalBytes: total));
171
- },
172
- );
273
+ await completer.future;
274
+
275
+ final Uint8List bytes = (request.response! as JSArrayBuffer ).toDart.asUint8List ();
276
+
277
+ if (bytes.lengthInBytes == 0 ) {
278
+ throw image_provider.NetworkImageLoadException (
279
+ statusCode: request.status, uri: resolved);
173
280
}
281
+
282
+ return decode (await ui.ImmutableBuffer .fromUint8List (bytes));
174
283
}
175
284
176
285
@override
@@ -187,3 +296,61 @@ class NetworkImage
187
296
@override
188
297
String toString () => '${objectRuntimeType (this , 'NetworkImage' )}("$url ", scale: ${scale .toStringAsFixed (1 )})' ;
189
298
}
299
+
300
+ /// An [ImageStreamCompleter] that delegates to another [ImageStreamCompleter]
301
+ /// that is loaded asynchronously.
302
+ ///
303
+ /// This completer keeps its child completer alive until this completer is disposed.
304
+ class _ForwardingImageStreamCompleter extends ImageStreamCompleter {
305
+ _ForwardingImageStreamCompleter (this .task,
306
+ {InformationCollector ? informationCollector, String ? debugLabel}) {
307
+ this .debugLabel = debugLabel;
308
+ task.then ((ImageStreamCompleter value) {
309
+ resolved = true ;
310
+ if (_disposed) {
311
+ // Add a listener since the delegate completer won't dispose if it never
312
+ // had a listener.
313
+ value.addListener (ImageStreamListener ((_, __) {}));
314
+ value.maybeDispose ();
315
+ return ;
316
+ }
317
+ completer = value;
318
+ handle = completer.keepAlive ();
319
+ completer.addListener (ImageStreamListener (
320
+ (ImageInfo image, bool synchronousCall) {
321
+ setImage (image);
322
+ },
323
+ onChunk: (ImageChunkEvent event) {
324
+ reportImageChunkEvent (event);
325
+ },
326
+ onError: (Object exception, StackTrace ? stackTrace) {
327
+ reportError (exception: exception, stack: stackTrace);
328
+ },
329
+ ));
330
+ }, onError: (Object error, StackTrace stack) {
331
+ reportError (
332
+ context: ErrorDescription ('resolving an image stream completer' ),
333
+ exception: error,
334
+ stack: stack,
335
+ informationCollector: informationCollector,
336
+ silent: true ,
337
+ );
338
+ });
339
+ }
340
+
341
+ final Future <ImageStreamCompleter > task;
342
+ bool resolved = false ;
343
+ late final ImageStreamCompleter completer;
344
+ late final ImageStreamCompleterHandle handle;
345
+
346
+ bool _disposed = false ;
347
+
348
+ @override
349
+ void onDisposed () {
350
+ if (resolved) {
351
+ handle.dispose ();
352
+ }
353
+ _disposed = true ;
354
+ super .onDisposed ();
355
+ }
356
+ }
0 commit comments