Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 71 additions & 19 deletions lib/web_ui/lib/src/engine/canvaskit/image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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!);

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;
}
}).then((html.HttpRequest response) {
if (response.status != 200) {
completer.completeError(Exception(
'Network image request failed with status: ${response.status}'));

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);
}
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);
});

request.send();
return completer.future;
}

Expand All @@ -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;
}
Expand Down
217 changes: 205 additions & 12 deletions lib/web_ui/test/canvaskit/image_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand Down Expand Up @@ -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)
Expand All @@ -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();
}