Skip to content

Commit

Permalink
Separate web and io implementations of network image (#34112)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jonahwilliams committed Jun 15, 2019
1 parent fd1291f commit dfa39f3
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 102 deletions.
124 changes: 124 additions & 0 deletions packages/flutter/lib/src/painting/_network_image_io.dart
Original file line number Diff line number Diff line change
@@ -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<image_provider.NetworkImage> 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<String, String> headers;

@override
Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
return SynchronousFuture<NetworkImage>(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<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();

return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, chunkEvents),
chunkEvents: chunkEvents.stream,
scale: key.scale,
informationCollector: () {
return <DiagnosticsNode>[
DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this),
DiagnosticsProperty<image_provider.NetworkImage>('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<ui.Codec> _loadAsync(
NetworkImage key,
StreamController<ImageChunkEvent> 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)';
}
73 changes: 73 additions & 0 deletions packages/flutter/lib/src/painting/_network_image_web.dart
Original file line number Diff line number Diff line change
@@ -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<image_provider.NetworkImage> 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<String, String> headers;

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

@override
ImageStreamCompleter load(image_provider.NetworkImage key) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key),
scale: key.scale,
informationCollector: () {
return <DiagnosticsNode>[
DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this),
DiagnosticsProperty<NetworkImage>('Image key', key),
];
},
);
}

Future<ui.Codec> _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)';
}
106 changes: 11 additions & 95 deletions packages/flutter/lib/src/painting/image_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -478,110 +479,25 @@ abstract class AssetBundleImageProvider extends ImageProvider<AssetBundleImageKe
// TODO(ianh): Find some way to honor cache headers to the extent that when the
// last reference to an image is released, we proactively evict the image from
// our cache if the headers describe the image as having expired at that point.
class NetworkImage extends ImageProvider<NetworkImage> {
abstract class NetworkImage extends ImageProvider<NetworkImage> {
/// 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<String, String> 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<String, String> headers;

@override
Future<NetworkImage> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<NetworkImage>(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<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();

return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, chunkEvents),
chunkEvents: chunkEvents.stream,
scale: key.scale,
informationCollector: () sync* {
yield DiagnosticsProperty<ImageProvider>('Image provider', this);
yield DiagnosticsProperty<NetworkImage>('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<ui.Codec> _loadAsync(
NetworkImage key,
StreamController<ImageChunkEvent> 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);
///
/// When running flutter on the web, headers are not used.
Map<String, String> get headers;

@override
String toString() => '$runtimeType("$url", scale: $scale)';
ImageStreamCompleter load(NetworkImage key);
}

/// Decodes the given [File] object as an image, associating it with the given
Expand Down
10 changes: 6 additions & 4 deletions packages/flutter/lib/src/painting/image_resolution.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter/test/painting/image_provider_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
}
Expand Down
1 change: 0 additions & 1 deletion packages/flutter/test/painting/image_resolution_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion packages/flutter/test/widgets/image_headers_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ void main() {
});
return client;
});
});
}, skip: isBrowser);
}

class MockHttpClient extends Mock implements HttpClient {}
Expand Down

0 comments on commit dfa39f3

Please sign in to comment.