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 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
49ed8d4
Use HtmlImageElement for image decoding in skia
harryterkelsen May 24, 2024
6b248d1
start touching up tests
harryterkelsen May 29, 2024
d780fe3
Merge branch 'main' into canvaskit-html-img-element-decode
harryterkelsen May 29, 2024
4e43312
eagerly decode
harryterkelsen May 31, 2024
9b1a448
Merge branch 'main' into canvaskit-html-img-element-decode
harryterkelsen May 31, 2024
e6d4d44
wip
harryterkelsen Jun 4, 2024
8ea4865
Merge branch 'main' into canvaskit-html-img-element-decode
harryterkelsen Jun 4, 2024
88d685d
wip
harryterkelsen Jul 2, 2024
23cb86b
Merge branch 'main' into canvaskit-html-img-element-decode
harryterkelsen Jul 2, 2024
7883559
Merge branch 'main' into canvaskit-html-img-element-decode
harryterkelsen Jul 3, 2024
8a1a7ce
WIP
harryterkelsen Jul 10, 2024
389369d
Merge branch 'main' into canvaskit-html-img-element-decode
harryterkelsen Jul 10, 2024
d900ded
Fix ImageBitmap toByteData case
harryterkelsen Jul 11, 2024
355891b
Fall back to Skia decoding for GIF and WEBP images
harryterkelsen Jul 11, 2024
cfc1c4a
Merge branch 'main' into canvaskit-html-img-element-decode
harryterkelsen Jul 11, 2024
d317302
Undo semantics change
harryterkelsen Jul 11, 2024
c579c8e
Remove unused readImageElementPixelsUnmodified method
harryterkelsen Jul 11, 2024
dd5c8d6
remove outdated comment in test
harryterkelsen Jul 11, 2024
f3997c1
delete old unused tests
harryterkelsen Jul 11, 2024
52b8330
fix analysis errors
harryterkelsen Jul 11, 2024
b207ee7
Merge branch 'main' into canvaskit-html-img-element-decode
harryterkelsen Jul 16, 2024
4f5f863
Merge branch 'main' into pr/harryterkelsen/53201
harryterkelsen Jul 17, 2024
7c6ab13
Respond to review comments
harryterkelsen Jul 17, 2024
0b426fe
Merge branch 'main' into canvaskit-html-img-element-decode
harryterkelsen Jul 18, 2024
c06797c
Merge branch 'canvaskit-html-img-element-decode' of github.com:harryt…
harryterkelsen Jul 18, 2024
e7aff46
Remove unnecessary `await`
harryterkelsen Jul 18, 2024
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
Prev Previous commit
Next Next commit
eagerly decode
  • Loading branch information
harryterkelsen committed May 31, 2024
commit 4e43312eccd84c8d7033d5c4b5fdcc97cd2ed822
42 changes: 39 additions & 3 deletions lib/web_ui/lib/src/engine/canvaskit/image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import 'dart:async';
import 'dart:js_interop';
import 'dart:math' as math;
import 'dart:typed_data';

import 'package:ui/src/engine.dart';
Expand All @@ -14,16 +15,20 @@ import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
Future<ui.Codec> skiaInstantiateImageCodec(Uint8List list,
[int? targetWidth, int? targetHeight]) async {
ui.Codec codec;
print('browser supports image decoder? $browserSupportsImageDecoder');
// ImageDecoder does not detect image type automatically. It requires us to
// tell it what the image type is.
final String contentType = tryDetectContentType(list, 'encoded image bytes');

if (browserSupportsImageDecoder) {
codec = await CkBrowserImageDecoder.create(
data: list,
contentType: contentType,
debugSource: 'encoded image bytes',
);
} else {
// TODO(harryterkelsen): If the image is animated, then use Skia to decode.
final DomBlob blob = createDomBlob(<dynamic>[list.buffer]);
codec = CkImageBlobCodec(blob);
codec = await decodeBlobToCkImage(blob);
}
return ResizingCodec(
codec,
Expand Down Expand Up @@ -90,6 +95,13 @@ class CkImageBlobCodec extends HtmlBlobCodec {
}
}

/// Creates and decodes an image using HtmlImageElement.
Future<CkImageBlobCodec> decodeBlobToCkImage(DomBlob blob) async {
final CkImageBlobCodec codec = CkImageBlobCodec(blob);
await codec.decode();
return codec;
}

void skiaDecodeImageFromPixels(
Uint8List pixels,
int width,
Expand Down Expand Up @@ -227,8 +239,10 @@ const String _kNetworkImageMessage = 'Failed to load network image.';
Future<ui.Codec> skiaInstantiateWebImageCodec(
String url, ui_web.ImageCodecChunkCallback? chunkCallback) async {
final Uint8List list = await fetchImage(url, chunkCallback);
final String contentType = tryDetectContentType(list, url);
if (browserSupportsImageDecoder) {
return CkBrowserImageDecoder.create(data: list, debugSource: url);
return CkBrowserImageDecoder.create(
data: list, contentType: contentType, debugSource: url);
} else {
return CkAnimatedImage.decodeFromBytes(list, url);
}
Expand Down Expand Up @@ -455,3 +469,25 @@ class CkImage implements ui.Image, StackTraceDebugger {
return '[$width\u00D7$height]';
}
}

/// Detect the content type or throw an error if content type can't be detected.
String tryDetectContentType(Uint8List data, String debugSource) {
// ImageDecoder does not detect image type automatically. It requires us to
// tell it what the image type is.
final String? contentType = detectContentType(data);

if (contentType == null) {
final String fileHeader;
if (data.isNotEmpty) {
fileHeader =
'[${bytesToHexString(data.sublist(0, math.min(10, data.length)))}]';
} else {
fileHeader = 'empty';
}
throw ImageCodecException(
'Failed to detect image file format using the file header.\n'
'File header was $fileHeader.\n'
'Image source: $debugSource');
}
return contentType;
}
20 changes: 1 addition & 19 deletions lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import 'dart:async';
import 'dart:convert' show base64;
import 'dart:js_interop';
import 'dart:math' as math;
import 'dart:typed_data';

import 'package:ui/src/engine.dart';
Expand All @@ -27,26 +26,9 @@ class CkBrowserImageDecoder extends BrowserImageDecoder {

static Future<CkBrowserImageDecoder> create({
required Uint8List data,
required String contentType,
required String debugSource,
}) async {
// ImageDecoder does not detect image type automatically. It requires us to
// tell it what the image type is.
final String? contentType = detectContentType(data);

if (contentType == null) {
final String fileHeader;
if (data.isNotEmpty) {
fileHeader = '[${bytesToHexString(data.sublist(0, math.min(10, data.length)))}]';
} else {
fileHeader = 'empty';
}
throw ImageCodecException(
'Failed to detect image file format using the file header.\n'
'File header was $fileHeader.\n'
'Image source: $debugSource'
);
}

final CkBrowserImageDecoder decoder = CkBrowserImageDecoder._(
contentType: contentType,
dataSource: data.toJS,
Expand Down
104 changes: 63 additions & 41 deletions lib/web_ui/lib/src/engine/html_image_element_codec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import 'dart:async';

import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;

Expand All @@ -24,50 +25,46 @@ final bool _supportsDecode = _jsImageDecodeFunction != null;
typedef WebOnlyImageCodecChunkCallback = ui_web.ImageCodecChunkCallback;

abstract class HtmlImageElementCodec implements ui.Codec {
HtmlImageElementCodec(this.src, {this.chunkCallback});
HtmlImageElementCodec(this.src, {this.chunkCallback, this.debugSource});

final String src;
final ui_web.ImageCodecChunkCallback? chunkCallback;
final String? debugSource;

@override
int get frameCount => 1;

@override
int get repetitionCount => 0;

@override
Future<ui.FrameInfo> getNextFrame() async {
final Completer<ui.FrameInfo> completer = Completer<ui.FrameInfo>();
/// The Image() element backing this codec.
DomHTMLImageElement? imgElement;

/// A Future which completes when the Image element backing this codec has
/// been loaded and decoded.
Future<void>? decodeFuture;

Future<void> decode() async {
if (decodeFuture != null) {
return decodeFuture;
}
final Completer<void> completer = Completer<void>();
decodeFuture = completer.future;
// Currently there is no way to watch decode progress, so
// we add 0/100 , 100/100 progress callbacks to enable loading progress
// builders to create UI.
chunkCallback?.call(0, 100);
if (_supportsDecode) {
final DomHTMLImageElement imgElement = createDomHTMLImageElement();
imgElement.src = src;
setJsProperty<String>(imgElement, 'decoding', 'async');
imgElement = createDomHTMLImageElement();
imgElement!.src = src;
setJsProperty<String>(imgElement!, 'decoding', 'async');

// Ignoring the returned future on purpose because we're communicating
// through the `completer`.
// ignore: unawaited_futures
imgElement.decode().then((dynamic _) {
imgElement!.decode().then((dynamic _) {
chunkCallback?.call(100, 100);
int naturalWidth = imgElement.naturalWidth.toInt();
int naturalHeight = imgElement.naturalHeight.toInt();
// Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=700533.
if (naturalWidth == 0 &&
naturalHeight == 0 &&
ui_web.browser.browserEngine == ui_web.BrowserEngine.firefox) {
const int kDefaultImageSizeFallback = 300;
naturalWidth = kDefaultImageSizeFallback;
naturalHeight = kDefaultImageSizeFallback;
}
final ui.Image image = createImageFromHTMLImageElement(
imgElement,
naturalWidth,
naturalHeight,
);
completer.complete(SingleFrameInfo(image));
completer.complete();
}).catchError((dynamic e) {
// This code path is hit on Chrome 80.0.3987.16 when too many
// images are on the page (~1000).
Expand All @@ -80,8 +77,31 @@ abstract class HtmlImageElementCodec implements ui.Codec {
return completer.future;
}

void _decodeUsingOnLoad(Completer<ui.FrameInfo> completer) {
final DomHTMLImageElement imgElement = createDomHTMLImageElement();
@override
Future<ui.FrameInfo> getNextFrame() async {
await decode();
int naturalWidth = imgElement!.naturalWidth.toInt();
int naturalHeight = imgElement!.naturalHeight.toInt();
// Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=700533.
if (naturalWidth == 0 &&
naturalHeight == 0 &&
ui_web.browser.browserEngine == ui_web.BrowserEngine.firefox) {
const int kDefaultImageSizeFallback = 300;
naturalWidth = kDefaultImageSizeFallback;
naturalHeight = kDefaultImageSizeFallback;
}
final ui.Image image = createImageFromHTMLImageElement(
imgElement!,
naturalWidth,
naturalHeight,
);
return SingleFrameInfo(image);
}

// TODO(harryterkelsen): All browsers support Image.decode now. Should we
// remove this code path?
void _decodeUsingOnLoad(Completer<void> completer) {
imgElement = createDomHTMLImageElement();
// If the browser doesn't support asynchronous decoding of an image,
// then use the `onload` event to decide when it's ready to paint to the
// DOM. Unfortunately, this will cause the image to be decoded synchronously
Expand All @@ -90,27 +110,25 @@ abstract class HtmlImageElementCodec implements ui.Codec {
DomEventListener? loadListener;
errorListener = createDomEventListener((DomEvent event) {
if (loadListener != null) {
imgElement.removeEventListener('load', loadListener);
imgElement!.removeEventListener('load', loadListener);
}
imgElement.removeEventListener('error', errorListener);
completer.completeError(event);
imgElement!.removeEventListener('error', errorListener);
completer.completeError(ImageCodecException(
'Failed to decode image data.\n'
'Image source: $debugSource',
));
});
imgElement.addEventListener('error', errorListener);
imgElement!.addEventListener('error', errorListener);
loadListener = createDomEventListener((DomEvent event) {
if (chunkCallback != null) {
chunkCallback!(100, 100);
}
imgElement.removeEventListener('load', loadListener);
imgElement.removeEventListener('error', errorListener);
final ui.Image image = createImageFromHTMLImageElement(
imgElement,
imgElement.naturalWidth.toInt(),
imgElement.naturalHeight.toInt(),
);
completer.complete(SingleFrameInfo(image));
imgElement!.removeEventListener('load', loadListener);
imgElement!.removeEventListener('error', errorListener);
completer.complete();
});
imgElement.addEventListener('load', loadListener);
imgElement.src = src;
imgElement!.addEventListener('load', loadListener);
imgElement!.src = src;
}

/// Creates a [ui.Image] from an [HTMLImageElement] that has been loaded.
Expand All @@ -125,7 +143,11 @@ abstract class HtmlImageElementCodec implements ui.Codec {
}

abstract class HtmlBlobCodec extends HtmlImageElementCodec {
HtmlBlobCodec(this.blob) : super(domWindow.URL.createObjectURL(blob));
HtmlBlobCodec(this.blob)
: super(
domWindow.URL.createObjectURL(blob),
debugSource: 'encoded image bytes',
);

final DomBlob blob;

Expand Down
1 change: 1 addition & 0 deletions lib/web_ui/test/canvaskit/embedded_views_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,7 @@ void testMain() {
await createPlatformView(0, 'test-platform-view');

final CkBrowserImageDecoder image = await CkBrowserImageDecoder.create(
contentType: 'image/gif',
data: kAnimatedGif,
debugSource: 'test',
);
Expand Down
Loading