mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[canvaskit] improve image error handling and messaging (#22951)
This commit is contained in:
parent
1749dbcc1b
commit
d0b6e4269f
@ -8,32 +8,80 @@ part of engine;
|
||||
/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia.
|
||||
ui.Codec skiaInstantiateImageCodec(Uint8List list,
|
||||
[int? width, int? height, int? format, int? rowBytes]) {
|
||||
return CkAnimatedImage.decodeFromBytes(list);
|
||||
return CkAnimatedImage.decodeFromBytes(list, 'encoded image bytes');
|
||||
}
|
||||
|
||||
/// Thrown when the web engine fails to decode an image, either due to a
|
||||
/// network issue, corrupted image contents, or missing codec.
|
||||
class ImageCodecException implements Exception {
|
||||
ImageCodecException(this._message);
|
||||
|
||||
final String _message;
|
||||
|
||||
@override
|
||||
String toString() => 'ImageCodecException: $_message';
|
||||
}
|
||||
|
||||
const String _kNetworkImageMessage = 'Failed to load network image.';
|
||||
|
||||
typedef HttpRequestFactory = html.HttpRequest Function();
|
||||
HttpRequestFactory httpRequestFactory = () => html.HttpRequest();
|
||||
void debugRestoreHttpRequestFactory() {
|
||||
httpRequestFactory = () => html.HttpRequest();
|
||||
}
|
||||
|
||||
/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia after
|
||||
/// requesting from URI.
|
||||
Future<ui.Codec> skiaInstantiateWebImageCodec(
|
||||
String uri, WebOnlyImageCodecChunkCallback? chunkCallback) {
|
||||
String url, WebOnlyImageCodecChunkCallback? chunkCallback) {
|
||||
Completer<ui.Codec> completer = Completer<ui.Codec>();
|
||||
//TODO: Switch to using MakeImageFromCanvasImageSource when animated images are supported.
|
||||
html.HttpRequest.request(uri, responseType: "arraybuffer",
|
||||
onProgress: (html.ProgressEvent event) {
|
||||
if (event.lengthComputable) {
|
||||
chunkCallback?.call(event.loaded!, event.total!);
|
||||
}
|
||||
}).then((html.HttpRequest response) {
|
||||
if (response.status != 200) {
|
||||
completer.completeError(Exception(
|
||||
'Network image request failed with status: ${response.status}'));
|
||||
}
|
||||
final Uint8List list =
|
||||
new Uint8List.view((response.response as ByteBuffer));
|
||||
final CkAnimatedImage codec = CkAnimatedImage.decodeFromBytes(list);
|
||||
completer.complete(codec);
|
||||
}, onError: (dynamic error) {
|
||||
completer.completeError(error);
|
||||
|
||||
final html.HttpRequest request = httpRequestFactory();
|
||||
request.open('GET', url, async: true);
|
||||
request.responseType = 'arraybuffer';
|
||||
if (chunkCallback != null) {
|
||||
request.onProgress.listen((html.ProgressEvent event) {
|
||||
chunkCallback.call(event.loaded!, event.total!);
|
||||
});
|
||||
}
|
||||
|
||||
request.onError.listen((html.ProgressEvent event) {
|
||||
completer.completeError(ImageCodecException(
|
||||
'$_kNetworkImageMessage\n'
|
||||
'Image URL: $url\n'
|
||||
'Trying to load an image from another domain? Find answers at:\n'
|
||||
'https://flutter.dev/docs/development/platform-integration/web-images'
|
||||
));
|
||||
});
|
||||
|
||||
request.onLoad.listen((html.ProgressEvent event) {
|
||||
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.completeError(ImageCodecException(
|
||||
'$_kNetworkImageMessage\n'
|
||||
'Image URL: $url\n'
|
||||
'Server response code: $status'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final Uint8List list =
|
||||
new Uint8List.view((request.response as ByteBuffer));
|
||||
final CkAnimatedImage codec = CkAnimatedImage.decodeFromBytes(list, url);
|
||||
completer.complete(codec);
|
||||
} catch (error, stackTrace) {
|
||||
completer.completeError(error, stackTrace);
|
||||
}
|
||||
});
|
||||
|
||||
request.send();
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@ -42,15 +90,19 @@ Future<ui.Codec> skiaInstantiateWebImageCodec(
|
||||
/// Wraps `SkAnimatedImage`.
|
||||
class CkAnimatedImage extends ManagedSkiaObject<SkAnimatedImage> implements ui.Codec {
|
||||
/// Decodes an image from a list of encoded bytes.
|
||||
CkAnimatedImage.decodeFromBytes(this._bytes);
|
||||
CkAnimatedImage.decodeFromBytes(this._bytes, this.src);
|
||||
|
||||
final String src;
|
||||
final Uint8List _bytes;
|
||||
|
||||
@override
|
||||
SkAnimatedImage createDefault() {
|
||||
final SkAnimatedImage? animatedImage = canvasKit.MakeAnimatedImageFromEncoded(_bytes);
|
||||
if (animatedImage == null) {
|
||||
throw Exception('Failed to decode image');
|
||||
throw ImageCodecException(
|
||||
'Failed to decode image data.\n'
|
||||
'Image source: $src',
|
||||
);
|
||||
}
|
||||
return animatedImage;
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
// @dart = 2.6
|
||||
import 'dart:html' show ProgressEvent;
|
||||
import 'dart:html' as html;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:test/bootstrap/browser.dart';
|
||||
@ -23,8 +23,12 @@ void testMain() {
|
||||
group('CanvasKit image', () {
|
||||
setUpCanvasKitTest();
|
||||
|
||||
tearDown(() {
|
||||
debugRestoreHttpRequestFactory();
|
||||
});
|
||||
|
||||
test('CkAnimatedImage can be explicitly disposed of', () {
|
||||
final CkAnimatedImage image = CkAnimatedImage.decodeFromBytes(kTransparentImage);
|
||||
final CkAnimatedImage image = CkAnimatedImage.decodeFromBytes(kTransparentImage, 'test');
|
||||
expect(image.debugDisposed, false);
|
||||
image.dispose();
|
||||
expect(image.debugDisposed, true);
|
||||
@ -99,13 +103,6 @@ void testMain() {
|
||||
testCollector.collectNow();
|
||||
});
|
||||
|
||||
test('skiaInstantiateWebImageCodec throws exception if given invalid URL',
|
||||
() async {
|
||||
expect(skiaInstantiateWebImageCodec('invalid-url', null),
|
||||
throwsA(isA<ProgressEvent>()));
|
||||
testCollector.collectNow();
|
||||
});
|
||||
|
||||
test('CkImage toByteData', () async {
|
||||
final SkImage skImage =
|
||||
canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)
|
||||
@ -116,14 +113,210 @@ void testMain() {
|
||||
testCollector.collectNow();
|
||||
});
|
||||
|
||||
test('Reports error when failing to decode image', () async {
|
||||
test('skiaInstantiateWebImageCodec loads an image from the network',
|
||||
() async {
|
||||
httpRequestFactory = () {
|
||||
return TestHttpRequest()
|
||||
..status = 200
|
||||
..onLoad = Stream<html.ProgressEvent>.fromIterable(<html.ProgressEvent>[
|
||||
html.ProgressEvent('test error'),
|
||||
])
|
||||
..response = kTransparentImage.buffer;
|
||||
};
|
||||
final ui.Codec codec = await skiaInstantiateWebImageCodec('http://image-server.com/picture.jpg', null);
|
||||
expect(codec.frameCount, 1);
|
||||
final ui.Image image = (await codec.getNextFrame()).image;
|
||||
expect(image.height, 1);
|
||||
expect(image.width, 1);
|
||||
testCollector.collectNow();
|
||||
});
|
||||
|
||||
test('skiaInstantiateWebImageCodec throws exception on request error',
|
||||
() async {
|
||||
httpRequestFactory = () {
|
||||
return TestHttpRequest()
|
||||
..onError = Stream<html.ProgressEvent>.fromIterable(<html.ProgressEvent>[
|
||||
html.ProgressEvent('test error'),
|
||||
]);
|
||||
};
|
||||
try {
|
||||
await skiaInstantiateWebImageCodec('url-does-not-matter', null);
|
||||
fail('Expected to throw');
|
||||
} on ImageCodecException catch (exception) {
|
||||
expect(
|
||||
exception.toString(),
|
||||
'ImageCodecException: Failed to load network image.\n'
|
||||
'Image URL: url-does-not-matter\n'
|
||||
'Trying to load an image from another domain? Find answers at:\n'
|
||||
'https://flutter.dev/docs/development/platform-integration/web-images',
|
||||
);
|
||||
}
|
||||
testCollector.collectNow();
|
||||
});
|
||||
|
||||
test('skiaInstantiateWebImageCodec throws exception on HTTP error',
|
||||
() async {
|
||||
try {
|
||||
await skiaInstantiateWebImageCodec('/does-not-exist.jpg', null);
|
||||
fail('Expected to throw');
|
||||
} on ImageCodecException catch (exception) {
|
||||
expect(
|
||||
exception.toString(),
|
||||
'ImageCodecException: Failed to load network image.\n'
|
||||
'Image URL: /does-not-exist.jpg\n'
|
||||
'Server response code: 404',
|
||||
);
|
||||
}
|
||||
testCollector.collectNow();
|
||||
});
|
||||
|
||||
test('skiaInstantiateWebImageCodec includes URL in the error for malformed image',
|
||||
() async {
|
||||
httpRequestFactory = () {
|
||||
return TestHttpRequest()
|
||||
..status = 200
|
||||
..onLoad = Stream<html.ProgressEvent>.fromIterable(<html.ProgressEvent>[
|
||||
html.ProgressEvent('test error'),
|
||||
])
|
||||
..response = Uint8List(0).buffer;
|
||||
};
|
||||
try {
|
||||
await skiaInstantiateWebImageCodec('http://image-server.com/picture.jpg', null);
|
||||
fail('Expected to throw');
|
||||
} on ImageCodecException catch (exception) {
|
||||
expect(
|
||||
exception.toString(),
|
||||
'ImageCodecException: Failed to decode image data.\n'
|
||||
'Image source: http://image-server.com/picture.jpg',
|
||||
);
|
||||
}
|
||||
testCollector.collectNow();
|
||||
});
|
||||
|
||||
test('Reports error when failing to decode image data', () async {
|
||||
try {
|
||||
await ui.instantiateImageCodec(Uint8List(0));
|
||||
fail('Expected to throw');
|
||||
} on Exception catch (exception) {
|
||||
expect(exception.toString(), 'Exception: Failed to decode image');
|
||||
} on ImageCodecException catch (exception) {
|
||||
expect(
|
||||
exception.toString(),
|
||||
'ImageCodecException: Failed to decode image data.\n'
|
||||
'Image source: encoded image bytes'
|
||||
);
|
||||
}
|
||||
});
|
||||
// TODO: https://github.com/flutter/flutter/issues/60040
|
||||
}, skip: isIosSafari);
|
||||
}
|
||||
|
||||
class TestHttpRequest implements html.HttpRequest {
|
||||
@override
|
||||
String responseType;
|
||||
|
||||
@override
|
||||
int timeout = 10;
|
||||
|
||||
@override
|
||||
bool withCredentials = false;
|
||||
|
||||
@override
|
||||
void abort() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
void addEventListener(String type, listener, [bool useCapture]) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
bool dispatchEvent(html.Event event) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
String getAllResponseHeaders() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
String getResponseHeader(String name) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
html.Events get on => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Stream<html.ProgressEvent> get onAbort => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Stream<html.ProgressEvent> onError = Stream<html.ProgressEvent>.fromIterable(<html.ProgressEvent>[]);
|
||||
|
||||
@override
|
||||
Stream<html.ProgressEvent> onLoad = Stream<html.ProgressEvent>.fromIterable(<html.ProgressEvent>[]);
|
||||
|
||||
@override
|
||||
Stream<html.ProgressEvent> get onLoadEnd => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Stream<html.ProgressEvent> get onLoadStart => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Stream<html.ProgressEvent> get onProgress => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Stream<html.Event> get onReadyStateChange => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Stream<html.ProgressEvent> get onTimeout => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
void open(String method, String url, {bool async, String user, String password}) {}
|
||||
|
||||
@override
|
||||
void overrideMimeType(String mime) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
int get readyState => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
void removeEventListener(String type, listener, [bool useCapture]) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic response;
|
||||
|
||||
@override
|
||||
Map<String, String> get responseHeaders => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
String get responseText => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
String get responseUrl => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
html.Document get responseXml => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
void send([dynamic bodyOrData]) {
|
||||
}
|
||||
|
||||
@override
|
||||
void setRequestHeader(String name, String value) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
int status = -1;
|
||||
|
||||
@override
|
||||
String get statusText => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
html.HttpRequestUpload get upload => throw UnimplementedError();
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user