_network_image_web.dart 6.39 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
import 'dart:ui' as ui;
8
import 'dart:ui_web' as ui_web;
9 10

import 'package:flutter/foundation.dart';
11
import 'package:web/web.dart' as web;
12 13 14 15

import 'image_provider.dart' as image_provider;
import 'image_stream.dart';

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

19 20 21
// Method signature for _loadAsync decode callbacks.
typedef _SimpleDecoderCallback = Future<ui.Codec> Function(ui.ImmutableBuffer buffer);

22
/// Default HTTP client.
23 24
web.XMLHttpRequest _httpClient() {
  return web.XMLHttpRequest();
25 26 27 28 29 30 31 32 33 34
}

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

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

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

  @override
  final String url;

  @override
  final double scale;

  @override
52
  final Map<String, String>? headers;
53 54

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

59 60 61 62 63 64 65 66 67 68
  @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,
69
      codec: _loadAsync(key as NetworkImage, decode, chunkEvents),
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
      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,
85
      codec: _loadAsync(key as NetworkImage, decode, chunkEvents),
86 87 88 89
      scale: key.scale,
      debugLabel: key.url,
      informationCollector: _imageStreamInformationCollector(key),
    );
90 91
  }

92
  InformationCollector? _imageStreamInformationCollector(image_provider.NetworkImage key) {
93
    InformationCollector? collector;
94
    assert(() {
95 96 97 98
      collector = () => <DiagnosticsNode>[
        DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this),
        DiagnosticsProperty<NetworkImage>('Image key', key as NetworkImage),
      ];
99 100 101
      return true;
    }());
    return collector;
102 103
  }

104
  // Html renderer does not support decoding network images to a specified size. The decode parameter
105 106
  // here is ignored and `ui_web.createImageCodecFromUrl` will be used directly
  // in place of the typical `instantiateImageCodec` method.
107
  Future<ui.Codec> _loadAsync(
108
    NetworkImage key,
109
    _SimpleDecoderCallback decode,
110
    StreamController<ImageChunkEvent> chunkEvents,
111
  ) async {
112 113 114
    assert(key == this);

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

116 117
    final bool containsNetworkImageHeaders = key.headers?.isNotEmpty ?? false;

118
    // We use a different method when headers are set because the
119
    // `ui_web.createImageCodecFromUrl` method is not capable of handling headers.
120
    if (isCanvasKit || containsNetworkImageHeaders) {
121 122 123
      final Completer<web.XMLHttpRequest> completer =
          Completer<web.XMLHttpRequest>();
      final web.XMLHttpRequest request = httpRequestFactory();
124

125
      request.open('GET', key.url, true);
126
      request.responseType = 'arraybuffer';
127 128 129 130 131
      if (containsNetworkImageHeaders) {
        key.headers!.forEach((String header, String value) {
          request.setRequestHeader(header, value);
        });
      }
132

133 134 135
      request.addEventListener('load', (web.Event e) {
        final int status = request.status;
        final bool accepted = status >= 200 && status < 300;
136 137 138 139 140 141 142 143 144 145 146
        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(
147
              statusCode: status, uri: resolved);
148
        }
149
      }.toJS);
150

151 152
      request.addEventListener('error',
          ((JSObject e) => completer.completeError(e)).toJS);
153 154 155 156 157

      request.send();

      await completer.future;

158
      final Uint8List bytes = (request.response! as JSArrayBuffer).toDart.asUint8List();
159

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 168 169 170 171
        resolved,
        chunkCallback: (int bytes, int total) {
          chunkEvents.add(ImageChunkEvent(
              cumulativeBytesLoaded: bytes, expectedTotalBytes: total));
        },
172
      );
173
    }
174 175 176
  }

  @override
177
  bool operator ==(Object other) {
178 179 180
    if (other.runtimeType != runtimeType) {
      return false;
    }
181
    return other is NetworkImage && other.url == url && other.scale == scale;
182 183 184
  }

  @override
185
  int get hashCode => Object.hash(url, scale);
186 187

  @override
188
  String toString() => '${objectRuntimeType(this, 'NetworkImage')}("$url", scale: ${scale.toStringAsFixed(1)})';
189
}