blob: 3e980842eda243f5a56acdbcb55d48f774e2cce0 [file] [log] [blame]
// Copyright 2013 The Flutter 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:html' as html;
import 'dart:typed_data';
import 'package:ui/ui.dart' as ui;
import '../html_image_codec.dart';
import '../safe_browser_api.dart';
import '../util.dart';
import 'canvaskit_api.dart';
import 'image_wasm_codecs.dart';
import 'image_web_codecs.dart';
import 'skia_object_cache.dart';
/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia.
// TODO(yjbanov): Implement targetWidth and targetHeight support.
// https://github.com/flutter/flutter/issues/34075
FutureOr<ui.Codec> skiaInstantiateImageCodec(Uint8List list,
[int? targetWidth, int? targetHeight]) {
if (browserSupportsImageDecoder) {
return CkBrowserImageDecoder.create(
data: list,
debugSource: 'encoded image bytes',
targetWidth: targetWidth,
targetHeight: targetHeight,
);
} else {
return CkAnimatedImage.decodeFromBytes(list, 'encoded image bytes');
}
}
// TODO(yjbanov): add support for targetWidth/targetHeight (https://github.com/flutter/flutter/issues/34075)
void skiaDecodeImageFromPixels(
Uint8List pixels,
int width,
int height,
ui.PixelFormat format,
ui.ImageDecoderCallback callback, {
int? rowBytes,
int? targetWidth,
int? targetHeight,
bool allowUpscaling = true,
}) {
// Run in a timer to avoid janking the current frame by moving the decoding
// work outside the frame event.
Timer.run(() {
final SkImage? skImage = canvasKit.MakeImage(
SkImageInfo(
width: width,
height: height,
colorType: format == ui.PixelFormat.rgba8888 ? canvasKit.ColorType.RGBA_8888 : canvasKit.ColorType.BGRA_8888,
alphaType: canvasKit.AlphaType.Premul,
colorSpace: SkColorSpaceSRGB,
),
pixels,
rowBytes ?? 4 * width,
);
if (skImage == null) {
html.window.console.warn('Failed to create image from pixels.');
return;
}
return callback(CkImage(skImage));
});
}
/// 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();
// ignore: prefer_function_declarations_over_variables
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 url, WebOnlyImageCodecChunkCallback? chunkCallback) async {
final Uint8List list = await fetchImage(url, chunkCallback);
if (browserSupportsImageDecoder) {
return CkBrowserImageDecoder.create(data: list, debugSource: url.toString());
} else {
return CkAnimatedImage.decodeFromBytes(list, url);
}
}
/// Sends a request to fetch image data.
Future<Uint8List> fetchImage(
String url, WebOnlyImageCodecChunkCallback? chunkCallback) {
final Completer<Uint8List> completer = Completer<Uint8List>();
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;
}
completer.complete(Uint8List.view(request.response as ByteBuffer));
});
request.send();
return completer.future;
}
/// A [ui.Image] backed by an `SkImage` from Skia.
class CkImage implements ui.Image, StackTraceDebugger {
CkImage(SkImage skImage, { this.videoFrame }) {
if (assertionsEnabled) {
_debugStackTrace = StackTrace.current;
}
if (browserSupportsFinalizationRegistry) {
box = SkiaObjectBox<CkImage, SkImage>(this, skImage);
} else {
// If finalizers are not supported we need to be able to resurrect the
// image if it was temporarily deleted. To do that, we keep the original
// pixels and ask the SkiaObjectBox to make an image from them when
// resurrecting.
//
// IMPORTANT: the alphaType, colorType, and colorSpace passed to
// _encodeImage and to canvasKit.MakeImage must be the same. Otherwise
// Skia will misinterpret the pixels and corrupt the image.
final ByteData? originalBytes = _encodeImage(
skImage: skImage,
format: ui.ImageByteFormat.rawRgba,
alphaType: canvasKit.AlphaType.Premul,
colorType: canvasKit.ColorType.RGBA_8888,
colorSpace: SkColorSpaceSRGB,
);
if (originalBytes == null) {
printWarning('Unable to encode image to bytes. We will not '
'be able to resurrect it once it has been garbage collected.');
return;
}
final int originalWidth = skImage.width();
final int originalHeight = skImage.height();
box = SkiaObjectBox<CkImage, SkImage>.resurrectable(this, skImage, () {
final SkImage? skImage = canvasKit.MakeImage(
SkImageInfo(
alphaType: canvasKit.AlphaType.Premul,
colorType: canvasKit.ColorType.RGBA_8888,
colorSpace: SkColorSpaceSRGB,
width: originalWidth,
height: originalHeight,
),
originalBytes.buffer.asUint8List(),
4 * originalWidth,
);
if (skImage == null) {
throw ImageCodecException(
'Failed to resurrect image from pixels.'
);
}
return skImage;
});
}
}
CkImage.cloneOf(this.box) {
if (assertionsEnabled) {
_debugStackTrace = StackTrace.current;
}
box.ref(this);
}
@override
StackTrace get debugStackTrace => _debugStackTrace!;
StackTrace? _debugStackTrace;
// Use a box because `SkImage` may be deleted either due to this object
// being garbage-collected, or by an explicit call to [delete].
late final SkiaObjectBox<CkImage, SkImage> box;
/// For browsers that support `ImageDecoder` this field holds the video frame
/// from which this image was created.
///
/// Skia owns the video frame and will close it when it's no longer used.
/// However, Flutter co-owns the [SkImage] and therefore it's safe to access
/// the video frame until the image is [dispose]d of.
VideoFrame? videoFrame;
/// The underlying Skia image object.
///
/// Do not store the returned value. It is memory-managed by [SkiaObjectBox].
/// Storing it may result in use-after-free bugs.
SkImage get skImage => box.skiaObject;
bool _disposed = false;
bool _debugCheckIsNotDisposed() {
assert(!_disposed, 'This image has been disposed.');
return true;
}
@override
void dispose() {
assert(
!_disposed,
'Cannot dispose an image that has already been disposed.',
);
_disposed = true;
box.unref(this);
}
@override
bool get debugDisposed {
if (assertionsEnabled) {
return _disposed;
}
throw StateError(
'Image.debugDisposed is only available when asserts are enabled.');
}
@override
CkImage clone() {
assert(_debugCheckIsNotDisposed());
return CkImage.cloneOf(box);
}
@override
bool isCloneOf(ui.Image other) {
assert(_debugCheckIsNotDisposed());
return other is CkImage && other.skImage.isAliasOf(skImage);
}
@override
List<StackTrace>? debugGetOpenHandleStackTraces() =>
box.debugGetStackTraces();
@override
int get width {
assert(_debugCheckIsNotDisposed());
return skImage.width();
}
@override
int get height {
assert(_debugCheckIsNotDisposed());
return skImage.height();
}
@override
Future<ByteData> toByteData({
ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba,
}) {
assert(_debugCheckIsNotDisposed());
if (videoFrame != null) {
return readPixelsFromVideoFrame(videoFrame!, format);
} else {
return _readPixelsFromSkImage(format);
}
}
Future<ByteData> _readPixelsFromSkImage(ui.ImageByteFormat format) {
final SkAlphaType alphaType = format == ui.ImageByteFormat.rawStraightRgba ? canvasKit.AlphaType.Unpremul : canvasKit.AlphaType.Premul;
final ByteData? data = _encodeImage(
skImage: skImage,
format: format,
alphaType: alphaType,
colorType: canvasKit.ColorType.RGBA_8888,
colorSpace: SkColorSpaceSRGB,
);
if (data == null) {
return Future<ByteData>.error('Failed to encode the image into bytes.');
} else {
return Future<ByteData>.value(data);
}
}
static ByteData? _encodeImage({
required SkImage skImage,
required ui.ImageByteFormat format,
required SkAlphaType alphaType,
required SkColorType colorType,
required ColorSpace colorSpace,
}) {
Uint8List? bytes;
if (format == ui.ImageByteFormat.rawRgba || format == ui.ImageByteFormat.rawStraightRgba) {
final SkImageInfo imageInfo = SkImageInfo(
alphaType: alphaType,
colorType: colorType,
colorSpace: colorSpace,
width: skImage.width(),
height: skImage.height(),
);
bytes = skImage.readPixels(0, 0, imageInfo);
} else {
bytes = skImage.encodeToBytes(); // defaults to PNG 100%
}
return bytes?.buffer.asByteData(0, bytes.length);
}
@override
String toString() {
assert(_debugCheckIsNotDisposed());
return '[$width\u00D7$height]';
}
}
/// Data for a single frame of an animated image.
class AnimatedImageFrameInfo implements ui.FrameInfo {
final Duration _duration;
final CkImage _image;
AnimatedImageFrameInfo(this._duration, this._image);
@override
Duration get duration => _duration;
@override
ui.Image get image => _image;
}