From dfa39f3ee51d37c07f6a812017fdafdcac58f47e Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Sat, 15 Jun 2019 10:20:45 -0700 Subject: [PATCH] Separate web and io implementations of network image (#34112) * add web and io implemenations of network and asset image * fix foundation import * update to remove extra asset image indirection * skip chunk test * address comments * disable non-functional test * disable all golden tests * address comments --- .../lib/src/painting/_network_image_io.dart | 124 ++++++++++++++++++ .../lib/src/painting/_network_image_web.dart | 73 +++++++++++ .../lib/src/painting/image_provider.dart | 106 ++------------- .../lib/src/painting/image_resolution.dart | 10 +- .../test/painting/image_provider_test.dart | 2 +- .../test/painting/image_resolution_test.dart | 1 - .../test/widgets/image_headers_test.dart | 2 +- 7 files changed, 216 insertions(+), 102 deletions(-) create mode 100644 packages/flutter/lib/src/painting/_network_image_io.dart create mode 100644 packages/flutter/lib/src/painting/_network_image_web.dart diff --git a/packages/flutter/lib/src/painting/_network_image_io.dart b/packages/flutter/lib/src/painting/_network_image_io.dart new file mode 100644 index 0000000000..5f9129b50d --- /dev/null +++ b/packages/flutter/lib/src/painting/_network_image_io.dart @@ -0,0 +1,124 @@ +// Copyright 2019 The Chromium 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:async'; +import 'dart:io'; +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/foundation.dart'; + +import 'binding.dart'; +import 'debug.dart'; +import 'image_provider.dart' as image_provider; +import 'image_stream.dart'; + +/// The dart:io implemenation of [image_provider.NetworkImage]. +class NetworkImage extends image_provider.ImageProvider implements image_provider.NetworkImage { + /// Creates an object that fetches the image at the given URL. + /// + /// The arguments [url] and [scale] must not be null. + const NetworkImage(this.url, { this.scale = 1.0, this.headers }) + : assert(url != null), + assert(scale != null); + + @override + final String url; + + @override + final double scale; + + @override + final Map headers; + + @override + Future obtainKey(image_provider.ImageConfiguration configuration) { + return SynchronousFuture(this); + } + + @override + ImageStreamCompleter load(image_provider.NetworkImage key) { + // 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 chunkEvents = StreamController(); + + return MultiFrameImageStreamCompleter( + codec: _loadAsync(key, chunkEvents), + chunkEvents: chunkEvents.stream, + scale: key.scale, + informationCollector: () { + return [ + DiagnosticsProperty('Image provider', this), + DiagnosticsProperty('Image key', key), + ]; + }, + ); + } + + // Do not access this field directly; use [_httpClient] instead. + // We set `autoUncompress` to false to ensure that we can trust the value of + // the `Content-Length` HTTP header. We automatically uncompress the content + // in our call to [consolidateHttpClientResponseBytes]. + static final HttpClient _sharedHttpClient = HttpClient()..autoUncompress = false; + + static HttpClient get _httpClient { + HttpClient client = _sharedHttpClient; + assert(() { + if (debugNetworkImageHttpClientProvider != null) + client = debugNetworkImageHttpClientProvider(); + return true; + }()); + return client; + } + + Future _loadAsync( + NetworkImage key, + StreamController chunkEvents, + ) async { + try { + assert(key == this); + + final Uri resolved = Uri.base.resolve(key.url); + final HttpClientRequest request = await _httpClient.getUrl(resolved); + headers?.forEach((String name, String value) { + request.headers.add(name, value); + }); + final HttpClientResponse response = await request.close(); + if (response.statusCode != HttpStatus.ok) + throw Exception('HTTP request failed, statusCode: ${response?.statusCode}, $resolved'); + + final Uint8List bytes = await consolidateHttpClientResponseBytes( + response, + onBytesReceived: (int cumulative, int total) { + chunkEvents.add(ImageChunkEvent( + cumulativeBytesLoaded: cumulative, + expectedTotalBytes: total, + )); + }, + ); + if (bytes.lengthInBytes == 0) + throw Exception('NetworkImage is an empty file: $resolved'); + + return PaintingBinding.instance.instantiateImageCodec(bytes); + } finally { + chunkEvents.close(); + } + } + + @override + bool operator ==(dynamic other) { + if (other.runtimeType != runtimeType) + return false; + final NetworkImage typedOther = other; + return url == typedOther.url + && scale == typedOther.scale; + } + + @override + int get hashCode => ui.hashValues(url, scale); + + @override + String toString() => '$runtimeType("$url", scale: $scale)'; +} diff --git a/packages/flutter/lib/src/painting/_network_image_web.dart b/packages/flutter/lib/src/painting/_network_image_web.dart new file mode 100644 index 0000000000..6b566dec2c --- /dev/null +++ b/packages/flutter/lib/src/painting/_network_image_web.dart @@ -0,0 +1,73 @@ +// Copyright 2019 The Chromium 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:async'; +import 'dart:ui' as ui; + +import 'package:flutter/foundation.dart'; + +import 'image_provider.dart' as image_provider; +import 'image_stream.dart'; + +/// The dart:html implemenation of [image_provider.NetworkImage]. +class NetworkImage extends image_provider.ImageProvider implements image_provider.NetworkImage { + /// Creates an object that fetches the image at the given URL. + /// + /// The arguments [url] and [scale] must not be null. + const NetworkImage(this.url, {this.scale = 1.0, this.headers}) + : assert(url != null), + assert(scale != null); + + @override + final String url; + + @override + final double scale; + + @override + final Map headers; + + @override + Future obtainKey(image_provider.ImageConfiguration configuration) { + return SynchronousFuture(this); + } + + @override + ImageStreamCompleter load(image_provider.NetworkImage key) { + return MultiFrameImageStreamCompleter( + codec: _loadAsync(key), + scale: key.scale, + informationCollector: () { + return [ + DiagnosticsProperty('Image provider', this), + DiagnosticsProperty('Image key', key), + ]; + }, + ); + } + + Future _loadAsync(NetworkImage key) async { + assert(key == this); + + final Uri resolved = Uri.base.resolve(key.url); + // This API only exists in the web engine implementation and is not + // contained in the analyzer summary for Flutter. + return ui.webOnlyInstantiateImageCodecFromUrl(resolved); // ignore: undefined_function + } + + @override + bool operator ==(dynamic other) { + if (other.runtimeType != runtimeType) { + return false; + } + final NetworkImage typedOther = other; + return url == typedOther.url && scale == typedOther.scale; + } + + @override + int get hashCode => ui.hashValues(url, scale); + + @override + String toString() => '$runtimeType("$url", scale: $scale)'; +} diff --git a/packages/flutter/lib/src/painting/image_provider.dart b/packages/flutter/lib/src/painting/image_provider.dart index 170fde073e..e77d79e86c 100644 --- a/packages/flutter/lib/src/painting/image_provider.dart +++ b/packages/flutter/lib/src/painting/image_provider.dart @@ -11,8 +11,9 @@ import 'dart:ui' show Size, Locale, TextDirection, hashValues; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import '_network_image_io.dart' + if (dart.library.html) '_network_image_web.dart' as network_image; import 'binding.dart'; -import 'debug.dart'; import 'image_cache.dart'; import 'image_stream.dart'; @@ -478,110 +479,25 @@ abstract class AssetBundleImageProvider extends ImageProvider { +abstract class NetworkImage extends ImageProvider { /// Creates an object that fetches the image at the given URL. /// - /// The arguments must not be null. - const NetworkImage(this.url, { this.scale = 1.0, this.headers }) - : assert(url != null), - assert(scale != null); + /// The arguments [url] and [scale] must not be null. + const factory NetworkImage(String url, { double scale, Map headers }) = network_image.NetworkImage; /// The URL from which the image will be fetched. - final String url; + String get url; /// The scale to place in the [ImageInfo] object of the image. - final double scale; + double get scale; /// The HTTP headers that will be used with [HttpClient.get] to fetch image from network. - final Map headers; + /// + /// When running flutter on the web, headers are not used. + Map get headers; @override - Future obtainKey(ImageConfiguration configuration) { - return SynchronousFuture(this); - } - - @override - ImageStreamCompleter load(NetworkImage key) { - // 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 chunkEvents = StreamController(); - - return MultiFrameImageStreamCompleter( - codec: _loadAsync(key, chunkEvents), - chunkEvents: chunkEvents.stream, - scale: key.scale, - informationCollector: () sync* { - yield DiagnosticsProperty('Image provider', this); - yield DiagnosticsProperty('Image key', key); - }, - ); - } - - // Do not access this field directly; use [_httpClient] instead. - // We set `autoUncompress` to false to ensure that we can trust the value of - // the `Content-Length` HTTP header. We automatically uncompress the content - // in our call to [consolidateHttpClientResponseBytes]. - static final HttpClient _sharedHttpClient = HttpClient()..autoUncompress = false; - - static HttpClient get _httpClient { - HttpClient client = _sharedHttpClient; - assert(() { - if (debugNetworkImageHttpClientProvider != null) - client = debugNetworkImageHttpClientProvider(); - return true; - }()); - return client; - } - - Future _loadAsync( - NetworkImage key, - StreamController chunkEvents, - ) async { - try { - assert(key == this); - - final Uri resolved = Uri.base.resolve(key.url); - final HttpClientRequest request = await _httpClient.getUrl(resolved); - headers?.forEach((String name, String value) { - request.headers.add(name, value); - }); - final HttpClientResponse response = await request.close(); - if (response.statusCode != HttpStatus.ok) - throw Exception('HTTP request failed, statusCode: ${response?.statusCode}, $resolved'); - - final Uint8List bytes = await consolidateHttpClientResponseBytes( - response, - onBytesReceived: (int cumulative, int total) { - chunkEvents.add(ImageChunkEvent( - cumulativeBytesLoaded: cumulative, - expectedTotalBytes: total, - )); - }, - ); - if (bytes.lengthInBytes == 0) - throw Exception('NetworkImage is an empty file: $resolved'); - - return PaintingBinding.instance.instantiateImageCodec(bytes); - } finally { - chunkEvents.close(); - } - } - - @override - bool operator ==(dynamic other) { - if (other.runtimeType != runtimeType) - return false; - final NetworkImage typedOther = other; - return url == typedOther.url - && scale == typedOther.scale; - } - - @override - int get hashCode => hashValues(url, scale); - - @override - String toString() => '$runtimeType("$url", scale: $scale)'; + ImageStreamCompleter load(NetworkImage key); } /// Decodes the given [File] object as an image, associating it with the given diff --git a/packages/flutter/lib/src/painting/image_resolution.dart b/packages/flutter/lib/src/painting/image_resolution.dart index 1c5b7ec773..7aec422f08 100644 --- a/packages/flutter/lib/src/painting/image_resolution.dart +++ b/packages/flutter/lib/src/painting/image_resolution.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'dart:collection'; import 'dart:convert'; -import 'dart:io'; import 'dart:ui' show hashValues; import 'package:flutter/foundation.dart'; @@ -265,10 +264,13 @@ class AssetImage extends AssetBundleImageProvider { return _naturalResolution; } - final File assetPath = File(key); - final Directory assetDir = assetPath.parent; + final Uri assetUri = Uri.parse(key); + String directoryPath = ''; + if (assetUri.pathSegments.length > 1) { + directoryPath = assetUri.pathSegments[assetUri.pathSegments.length - 2]; + } - final Match match = _extractRatioRegExp.firstMatch(assetDir.path); + final Match match = _extractRatioRegExp.firstMatch(directoryPath); if (match != null && match.groupCount > 0) return double.parse(match.group(1)); return _naturalResolution; // i.e. default to 1.0x diff --git a/packages/flutter/test/painting/image_provider_test.dart b/packages/flutter/test/painting/image_provider_test.dart index 448330b86a..8f7589a190 100644 --- a/packages/flutter/test/painting/image_provider_test.dart +++ b/packages/flutter/test/painting/image_provider_test.dart @@ -253,7 +253,7 @@ void main() { expect(events[i].cumulativeBytesLoaded, math.min((i + 1) * chunkSize, kTransparentImage.length)); expect(events[i].expectedTotalBytes, kTransparentImage.length); } - }); + }, skip: isBrowser); }); }); } diff --git a/packages/flutter/test/painting/image_resolution_test.dart b/packages/flutter/test/painting/image_resolution_test.dart index 2d22052d7e..2b9c73b12a 100644 --- a/packages/flutter/test/painting/image_resolution_test.dart +++ b/packages/flutter/test/painting/image_resolution_test.dart @@ -11,7 +11,6 @@ import 'package:flutter/painting.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - class TestAssetBundle extends CachingAssetBundle { TestAssetBundle(this._assetBundleMap); diff --git a/packages/flutter/test/widgets/image_headers_test.dart b/packages/flutter/test/widgets/image_headers_test.dart index e7b70e4ccf..68bc680cdb 100644 --- a/packages/flutter/test/widgets/image_headers_test.dart +++ b/packages/flutter/test/widgets/image_headers_test.dart @@ -41,7 +41,7 @@ void main() { }); return client; }); - }); + }, skip: isBrowser); } class MockHttpClient extends Mock implements HttpClient {}