_network_image_web.dart 7.75 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6
import 'dart:js_interop';
7 8 9 10
import 'dart:ui' as ui;

import 'package:flutter/foundation.dart';

11
import '../services/dom.dart';
12 13 14
import 'image_provider.dart' as image_provider;
import 'image_stream.dart';

15
/// Creates a type for an overridable factory function for testing purposes.
16
typedef HttpRequestFactory = DomXMLHttpRequest Function();
17 18

/// Default HTTP client.
19
DomXMLHttpRequest _httpClient() {
20
  return DomXMLHttpRequest();
21 22 23 24 25 26 27 28 29 30
}

/// Creates an overridable factory function.
HttpRequestFactory httpRequestFactory = _httpClient;

/// Restores to the default HTTP request factory.
void debugRestoreHttpRequestFactory() {
  httpRequestFactory = _httpClient;
}

31
/// The web implementation of [image_provider.NetworkImage].
32 33
///
/// NetworkImage on the web does not support decoding to a specified size.
34
@immutable
35 36 37
class NetworkImage
    extends image_provider.ImageProvider<image_provider.NetworkImage>
    implements image_provider.NetworkImage {
38 39 40
  /// Creates an object that fetches the image at the given URL.
  ///
  /// The arguments [url] and [scale] must not be null.
41
  const NetworkImage(this.url, {this.scale = 1.0, this.headers});
42 43 44 45 46 47 48 49

  @override
  final String url;

  @override
  final double scale;

  @override
50
  final Map<String, String>? headers;
51 52

  @override
53
  Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
54 55 56 57
    return SynchronousFuture<NetworkImage>(this);
  }

  @override
58
  ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
59 60 61 62 63 64
    // Ownership of this controller is handed off to [_loadAsync]; it is that
    // method's responsibility to close the controller's stream when the image
    // has been loaded or an error is thrown.
    final StreamController<ImageChunkEvent> chunkEvents =
        StreamController<ImageChunkEvent>();

65
    return MultiFrameImageStreamCompleter(
66
      chunkEvents: chunkEvents.stream,
67
      codec: _loadAsync(key as NetworkImage, null, null, decode, chunkEvents),
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
      scale: key.scale,
      debugLabel: key.url,
      informationCollector: _imageStreamInformationCollector(key),
    );
  }

  @override
  ImageStreamCompleter loadBuffer(image_provider.NetworkImage key, image_provider.DecoderBufferCallback decode) {
    // Ownership of this controller is handed off to [_loadAsync]; it is that
    // method's responsibility to close the controller's stream when the image
    // has been loaded or an error is thrown.
    final StreamController<ImageChunkEvent> chunkEvents =
        StreamController<ImageChunkEvent>();

    return MultiFrameImageStreamCompleter(
      chunkEvents: chunkEvents.stream,
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
      codec: _loadAsync(key as NetworkImage, null, decode, null, chunkEvents),
      scale: key.scale,
      debugLabel: key.url,
      informationCollector: _imageStreamInformationCollector(key),
    );
  }

  @override
  ImageStreamCompleter loadImage(image_provider.NetworkImage key, image_provider.ImageDecoderCallback decode) {
    // Ownership of this controller is handed off to [_loadAsync]; it is that
    // method's responsibility to close the controller's stream when the image
    // has been loaded or an error is thrown.
    final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();

    return MultiFrameImageStreamCompleter(
      chunkEvents: chunkEvents.stream,
      codec: _loadAsync(key as NetworkImage, decode, null, null, chunkEvents),
101 102 103 104
      scale: key.scale,
      debugLabel: key.url,
      informationCollector: _imageStreamInformationCollector(key),
    );
105 106
  }

107
  InformationCollector? _imageStreamInformationCollector(image_provider.NetworkImage key) {
108
    InformationCollector? collector;
109
    assert(() {
110 111 112 113
      collector = () => <DiagnosticsNode>[
        DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this),
        DiagnosticsProperty<NetworkImage>('Image key', key as NetworkImage),
      ];
114 115 116
      return true;
    }());
    return collector;
117 118
  }

119
  // Html renderer does not support decoding network images to a specified size. The decode parameter
120 121
  // here is ignored and the web-only `ui.webOnlyInstantiateImageCodecFromUrl` will be used
  // directly in place of the typical `instantiateImageCodec` method.
122
  Future<ui.Codec> _loadAsync(
123
    NetworkImage key,
124 125 126
    image_provider.ImageDecoderCallback? decode,
    image_provider.DecoderBufferCallback? decodeBufferDeprecated,
    image_provider.DecoderCallback? decodeDeprecated,
127
    StreamController<ImageChunkEvent> chunkEvents,
128
  ) async {
129 130 131
    assert(key == this);

    final Uri resolved = Uri.base.resolve(key.url);
132

133 134
    final bool containsNetworkImageHeaders = key.headers?.isNotEmpty ?? false;

135 136
    // We use a different method when headers are set because the
    // `ui.webOnlyInstantiateImageCodecFromUrl` method is not capable of handling headers.
137
    if (isCanvasKit || containsNetworkImageHeaders) {
138 139 140
      final Completer<DomXMLHttpRequest> completer =
          Completer<DomXMLHttpRequest>();
      final DomXMLHttpRequest request = httpRequestFactory();
141

142
      request.open('GET', key.url, true);
143
      request.responseType = 'arraybuffer';
144 145 146 147 148
      if (containsNetworkImageHeaders) {
        key.headers!.forEach((String header, String value) {
          request.setRequestHeader(header, value);
        });
      }
149

150
      request.addEventListener('load', createDomEventListener((DomEvent e) {
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
        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: request.status ?? 400, uri: resolved);
        }
166
      }));
167

168 169
      request.addEventListener('error',
          createDomEventListener(completer.completeError));
170 171 172 173 174

      request.send();

      await completer.future;

175
      final Uint8List bytes = (request.response! as JSArrayBuffer).toDart.asUint8List();
176

177
      if (bytes.lengthInBytes == 0) {
178 179
        throw image_provider.NetworkImageLoadException(
            statusCode: request.status!, uri: resolved);
180
      }
181

182 183 184
      if (decode != null) {
        final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
        return decode(buffer);
185 186 187
      } else if (decodeBufferDeprecated != null) {
        final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
        return decodeBufferDeprecated(buffer);
188
      } else {
189 190
        assert(decodeDeprecated != null);
        return decodeDeprecated!(bytes);
191
      }
192 193 194 195 196 197 198 199 200 201 202 203
    } else {
      // This API only exists in the web engine implementation and is not
      // contained in the analyzer summary for Flutter.
      // ignore: undefined_function, avoid_dynamic_calls
      return ui.webOnlyInstantiateImageCodecFromUrl(
        resolved,
        chunkCallback: (int bytes, int total) {
          chunkEvents.add(ImageChunkEvent(
              cumulativeBytesLoaded: bytes, expectedTotalBytes: total));
        },
      ) as Future<ui.Codec>;
    }
204 205 206
  }

  @override
207
  bool operator ==(Object other) {
208 209 210
    if (other.runtimeType != runtimeType) {
      return false;
    }
211
    return other is NetworkImage && other.url == url && other.scale == scale;
212 213 214
  }

  @override
215
  int get hashCode => Object.hash(url, scale);
216 217

  @override
218 219
  String toString() =>
      '${objectRuntimeType(this, 'NetworkImage')}("$url", scale: $scale)';
220
}