Skip to content

Commit

Permalink
implement targetWidth and targetHeight (#38028)
Browse files Browse the repository at this point in the history
* implement targetWidth and targetHeight

* blank lines and typo

* addressed comments

* add warning to tests
  • Loading branch information
alanwutang11 committed Dec 6, 2022
1 parent ec211d2 commit e6e6a02
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 26 deletions.
99 changes: 92 additions & 7 deletions lib/web_ui/lib/src/engine/canvaskit/image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,29 @@ import '../dom.dart';
import '../html_image_codec.dart';
import '../safe_browser_api.dart';
import '../util.dart';
import 'canvas.dart';
import 'canvaskit_api.dart';
import 'image_wasm_codecs.dart';
import 'image_web_codecs.dart';
import 'painting.dart';
import 'picture.dart';
import 'picture_recorder.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) {
// If we have either a target width or target height, use canvaskit to decode.
if (browserSupportsImageDecoder && (targetWidth == null && targetHeight == null)) {
return CkBrowserImageDecoder.create(
data: list,
debugSource: 'encoded image bytes',
targetWidth: targetWidth,
targetHeight: targetHeight,
);
} else {
return CkAnimatedImage.decodeFromBytes(list, 'encoded image bytes');
return CkAnimatedImage.decodeFromBytes(list, 'encoded image bytes', targetWidth: targetWidth, targetHeight: targetHeight);
}
}

// TODO(yjbanov): add support for targetWidth/targetHeight (https://github.com/flutter/flutter/issues/34075)
void skiaDecodeImageFromPixels(
Uint8List pixels,
int width,
Expand All @@ -45,6 +45,13 @@ void skiaDecodeImageFromPixels(
int? targetHeight,
bool allowUpscaling = true,
}) {
if (targetWidth != null) {
assert(allowUpscaling || targetWidth <= width);
}
if (targetHeight != null) {
assert(allowUpscaling || targetHeight <= height);
}

// Run in a timer to avoid janking the current frame by moving the decoding
// work outside the frame event.
Timer.run(() {
Expand All @@ -65,10 +72,88 @@ void skiaDecodeImageFromPixels(
return;
}

if (targetWidth != null || targetHeight != null) {
if (!validUpscale(allowUpscaling, targetWidth, targetHeight, width, height)) {
domWindow.console.warn('Cannot apply targetWidth/targetHeight when allowUpscaling is false.');
} else {
return callback(scaleImage(skImage, targetWidth, targetHeight));
}
}
return callback(CkImage(skImage));
});
}

// An invalid upscale happens when allowUpscaling is false AND either the given
// targetWidth is larger than the originalWidth OR the targetHeight is larger than originalHeight.
bool validUpscale(bool allowUpscaling, int? targetWidth, int? targetHeight, int originalWidth, int originalHeight) {
if (allowUpscaling) {
return true;
}
final bool targetWidthFits;
final bool targetHeightFits;
if (targetWidth != null) {
targetWidthFits = targetWidth <= originalWidth;
} else {
targetWidthFits = true;
}

if (targetHeight != null) {
targetHeightFits = targetHeight <= originalHeight;
} else {
targetHeightFits = true;
}
return targetWidthFits && targetHeightFits;
}

/// Creates a scaled [CkImage] from an [SkImage] by drawing the [SkImage] to a canvas.
///
/// This function will only be called if either a targetWidth or targetHeight is not null
///
/// If only one of targetWidth or targetHeight are specified, the other
/// dimension will be scaled according to the aspect ratio of the supplied
/// dimension.
///
/// If either targetWidth or targetHeight is less than or equal to zero, it
/// will be treated as if it is null.
CkImage scaleImage(SkImage image, int? targetWidth, int? targetHeight) {
assert(targetWidth != null || targetHeight != null);
if (targetWidth != null && targetWidth <= 0) {
targetWidth = null;
}
if (targetHeight != null && targetHeight <= 0) {
targetHeight = null;
}
if (targetWidth == null && targetHeight != null) {
targetWidth = (targetHeight * (image.width() / image.height())).round();
targetHeight = targetHeight;
} else if (targetHeight == null && targetWidth != null) {
targetWidth = targetWidth;
targetHeight = targetWidth ~/ (image.width() / image.height());
}

assert(targetWidth != null);
assert(targetHeight != null);

final CkPictureRecorder recorder = CkPictureRecorder();
final CkCanvas canvas = recorder.beginRecording(ui.Rect.largest);

canvas.drawImageRect(
CkImage(image),
ui.Rect.fromLTWH(0, 0, image.width(), image.height()),
ui.Rect.fromLTWH(0, 0, targetWidth!.toDouble(), targetHeight!.toDouble()),
CkPaint()
);

final CkPicture picture = recorder.endRecording();
final ui.Image finalImage = picture.toImageSync(
targetWidth,
targetHeight
);

final CkImage ckImage = finalImage as CkImage;
return ckImage;
}

/// 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 {
Expand Down
35 changes: 33 additions & 2 deletions lib/web_ui/lib/src/engine/canvaskit/image_wasm_codecs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'dart:typed_data';

import 'package:ui/ui.dart' as ui;

import '../util.dart';
import 'canvaskit_api.dart';
import 'image.dart';
import 'skia_object_cache.dart';
Expand All @@ -24,7 +25,7 @@ import 'skia_object_cache.dart';
class CkAnimatedImage extends ManagedSkiaObject<SkAnimatedImage>
implements ui.Codec {
/// Decodes an image from a list of encoded bytes.
CkAnimatedImage.decodeFromBytes(this._bytes, this.src);
CkAnimatedImage.decodeFromBytes(this._bytes, this.src, {this.targetWidth, this.targetHeight});

final String src;
final Uint8List _bytes;
Expand All @@ -34,9 +35,12 @@ class CkAnimatedImage extends ManagedSkiaObject<SkAnimatedImage>
/// Current frame index.
int _currentFrameIndex = 0;

final int? targetWidth;
final int? targetHeight;

@override
SkAnimatedImage createDefault() {
final SkAnimatedImage? animatedImage =
SkAnimatedImage? animatedImage =
canvasKit.MakeAnimatedImageFromEncoded(_bytes);
if (animatedImage == null) {
throw ImageCodecException(
Expand All @@ -45,6 +49,20 @@ class CkAnimatedImage extends ManagedSkiaObject<SkAnimatedImage>
);
}

if (targetWidth != null || targetHeight != null) {
if (animatedImage.getFrameCount() > 1) {
printWarning('targetWidth and targetHeight for multi-frame images not supported');
} else {
animatedImage = _resizeAnimatedImage(animatedImage, targetWidth, targetHeight);
if (animatedImage == null) {
throw ImageCodecException(
'Failed to decode re-sized image data.\n'
'Image source: $src',
);
}
}
}

_frameCount = animatedImage.getFrameCount().toInt();
_repetitionCount = animatedImage.getRepetitionCount().toInt();

Expand All @@ -61,6 +79,19 @@ class CkAnimatedImage extends ManagedSkiaObject<SkAnimatedImage>
return animatedImage;
}

SkAnimatedImage? _resizeAnimatedImage(SkAnimatedImage animatedImage, int? targetWidth, int? targetHeight) {
final SkImage image = animatedImage.makeImageAtCurrentFrame();
final CkImage ckImage = scaleImage(image, targetWidth, targetHeight);
final Uint8List? resizedBytes = ckImage.skImage.encodeToBytes();

if (resizedBytes == null) {
throw ImageCodecException('Failed to re-size image');
}

final SkAnimatedImage? resizedAnimatedImage = canvasKit.MakeAnimatedImageFromEncoded(resizedBytes);
return resizedAnimatedImage;
}

@override
SkAnimatedImage resurrect() => createDefault();

Expand Down
11 changes: 0 additions & 11 deletions lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,13 @@ void debugRestoreWebDecoderExpireDuration() {
class CkBrowserImageDecoder implements ui.Codec {
CkBrowserImageDecoder._({
required this.contentType,
required this.targetWidth,
required this.targetHeight,
required this.data,
required this.debugSource,
});

static Future<CkBrowserImageDecoder> create({
required Uint8List data,
required String debugSource,
int? targetWidth,
int? targetHeight,
}) async {
// ImageDecoder does not detect image type automatically. It requires us to
// tell it what the image type is.
Expand All @@ -76,8 +72,6 @@ class CkBrowserImageDecoder implements ui.Codec {

final CkBrowserImageDecoder decoder = CkBrowserImageDecoder._(
contentType: contentType,
targetWidth: targetWidth,
targetHeight: targetHeight,
data: data,
debugSource: debugSource,
);
Expand All @@ -88,8 +82,6 @@ class CkBrowserImageDecoder implements ui.Codec {
}

final String contentType;
final int? targetWidth;
final int? targetHeight;
final Uint8List data;
final String debugSource;

Expand Down Expand Up @@ -160,9 +152,6 @@ class CkBrowserImageDecoder implements ui.Codec {

// Flutter always uses premultiplied alpha when decoding.
premultiplyAlpha: 'premultiply',
desiredWidth: targetWidth,
desiredHeight: targetHeight,

// "default" gives the browser the liberty to convert to display-appropriate
// color space, typically SRGB, which is what we want.
colorSpaceConversion: 'default',
Expand Down
4 changes: 2 additions & 2 deletions lib/web_ui/lib/src/engine/safe_browser_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,8 @@ class ImageDecoderOptions {
required String type,
required Uint8List data,
required String premultiplyAlpha,
required int? desiredWidth,
required int? desiredHeight,
int? desiredWidth,
int? desiredHeight,
required String colorSpaceConversion,
required bool preferAnimation,
});
Expand Down

0 comments on commit e6e6a02

Please sign in to comment.