Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit e6e6a02

Browse files
authored
implement targetWidth and targetHeight (#38028)
* implement targetWidth and targetHeight * blank lines and typo * addressed comments * add warning to tests
1 parent ec211d2 commit e6e6a02

File tree

5 files changed

+229
-26
lines changed

5 files changed

+229
-26
lines changed

lib/web_ui/lib/src/engine/canvaskit/image.dart

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,29 @@ import '../dom.dart';
1111
import '../html_image_codec.dart';
1212
import '../safe_browser_api.dart';
1313
import '../util.dart';
14+
import 'canvas.dart';
1415
import 'canvaskit_api.dart';
1516
import 'image_wasm_codecs.dart';
1617
import 'image_web_codecs.dart';
18+
import 'painting.dart';
19+
import 'picture.dart';
20+
import 'picture_recorder.dart';
1721
import 'skia_object_cache.dart';
1822

1923
/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia.
20-
// TODO(yjbanov): Implement targetWidth and targetHeight support.
21-
// https://github.com/flutter/flutter/issues/34075
2224
FutureOr<ui.Codec> skiaInstantiateImageCodec(Uint8List list,
2325
[int? targetWidth, int? targetHeight]) {
24-
if (browserSupportsImageDecoder) {
26+
// If we have either a target width or target height, use canvaskit to decode.
27+
if (browserSupportsImageDecoder && (targetWidth == null && targetHeight == null)) {
2528
return CkBrowserImageDecoder.create(
2629
data: list,
2730
debugSource: 'encoded image bytes',
28-
targetWidth: targetWidth,
29-
targetHeight: targetHeight,
3031
);
3132
} else {
32-
return CkAnimatedImage.decodeFromBytes(list, 'encoded image bytes');
33+
return CkAnimatedImage.decodeFromBytes(list, 'encoded image bytes', targetWidth: targetWidth, targetHeight: targetHeight);
3334
}
3435
}
3536

36-
// TODO(yjbanov): add support for targetWidth/targetHeight (https://github.com/flutter/flutter/issues/34075)
3737
void skiaDecodeImageFromPixels(
3838
Uint8List pixels,
3939
int width,
@@ -45,6 +45,13 @@ void skiaDecodeImageFromPixels(
4545
int? targetHeight,
4646
bool allowUpscaling = true,
4747
}) {
48+
if (targetWidth != null) {
49+
assert(allowUpscaling || targetWidth <= width);
50+
}
51+
if (targetHeight != null) {
52+
assert(allowUpscaling || targetHeight <= height);
53+
}
54+
4855
// Run in a timer to avoid janking the current frame by moving the decoding
4956
// work outside the frame event.
5057
Timer.run(() {
@@ -65,10 +72,88 @@ void skiaDecodeImageFromPixels(
6572
return;
6673
}
6774

75+
if (targetWidth != null || targetHeight != null) {
76+
if (!validUpscale(allowUpscaling, targetWidth, targetHeight, width, height)) {
77+
domWindow.console.warn('Cannot apply targetWidth/targetHeight when allowUpscaling is false.');
78+
} else {
79+
return callback(scaleImage(skImage, targetWidth, targetHeight));
80+
}
81+
}
6882
return callback(CkImage(skImage));
6983
});
7084
}
7185

86+
// An invalid upscale happens when allowUpscaling is false AND either the given
87+
// targetWidth is larger than the originalWidth OR the targetHeight is larger than originalHeight.
88+
bool validUpscale(bool allowUpscaling, int? targetWidth, int? targetHeight, int originalWidth, int originalHeight) {
89+
if (allowUpscaling) {
90+
return true;
91+
}
92+
final bool targetWidthFits;
93+
final bool targetHeightFits;
94+
if (targetWidth != null) {
95+
targetWidthFits = targetWidth <= originalWidth;
96+
} else {
97+
targetWidthFits = true;
98+
}
99+
100+
if (targetHeight != null) {
101+
targetHeightFits = targetHeight <= originalHeight;
102+
} else {
103+
targetHeightFits = true;
104+
}
105+
return targetWidthFits && targetHeightFits;
106+
}
107+
108+
/// Creates a scaled [CkImage] from an [SkImage] by drawing the [SkImage] to a canvas.
109+
///
110+
/// This function will only be called if either a targetWidth or targetHeight is not null
111+
///
112+
/// If only one of targetWidth or targetHeight are specified, the other
113+
/// dimension will be scaled according to the aspect ratio of the supplied
114+
/// dimension.
115+
///
116+
/// If either targetWidth or targetHeight is less than or equal to zero, it
117+
/// will be treated as if it is null.
118+
CkImage scaleImage(SkImage image, int? targetWidth, int? targetHeight) {
119+
assert(targetWidth != null || targetHeight != null);
120+
if (targetWidth != null && targetWidth <= 0) {
121+
targetWidth = null;
122+
}
123+
if (targetHeight != null && targetHeight <= 0) {
124+
targetHeight = null;
125+
}
126+
if (targetWidth == null && targetHeight != null) {
127+
targetWidth = (targetHeight * (image.width() / image.height())).round();
128+
targetHeight = targetHeight;
129+
} else if (targetHeight == null && targetWidth != null) {
130+
targetWidth = targetWidth;
131+
targetHeight = targetWidth ~/ (image.width() / image.height());
132+
}
133+
134+
assert(targetWidth != null);
135+
assert(targetHeight != null);
136+
137+
final CkPictureRecorder recorder = CkPictureRecorder();
138+
final CkCanvas canvas = recorder.beginRecording(ui.Rect.largest);
139+
140+
canvas.drawImageRect(
141+
CkImage(image),
142+
ui.Rect.fromLTWH(0, 0, image.width(), image.height()),
143+
ui.Rect.fromLTWH(0, 0, targetWidth!.toDouble(), targetHeight!.toDouble()),
144+
CkPaint()
145+
);
146+
147+
final CkPicture picture = recorder.endRecording();
148+
final ui.Image finalImage = picture.toImageSync(
149+
targetWidth,
150+
targetHeight
151+
);
152+
153+
final CkImage ckImage = finalImage as CkImage;
154+
return ckImage;
155+
}
156+
72157
/// Thrown when the web engine fails to decode an image, either due to a
73158
/// network issue, corrupted image contents, or missing codec.
74159
class ImageCodecException implements Exception {

lib/web_ui/lib/src/engine/canvaskit/image_wasm_codecs.dart

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import 'dart:typed_data';
1414

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

17+
import '../util.dart';
1718
import 'canvaskit_api.dart';
1819
import 'image.dart';
1920
import 'skia_object_cache.dart';
@@ -24,7 +25,7 @@ import 'skia_object_cache.dart';
2425
class CkAnimatedImage extends ManagedSkiaObject<SkAnimatedImage>
2526
implements ui.Codec {
2627
/// Decodes an image from a list of encoded bytes.
27-
CkAnimatedImage.decodeFromBytes(this._bytes, this.src);
28+
CkAnimatedImage.decodeFromBytes(this._bytes, this.src, {this.targetWidth, this.targetHeight});
2829

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

38+
final int? targetWidth;
39+
final int? targetHeight;
40+
3741
@override
3842
SkAnimatedImage createDefault() {
39-
final SkAnimatedImage? animatedImage =
43+
SkAnimatedImage? animatedImage =
4044
canvasKit.MakeAnimatedImageFromEncoded(_bytes);
4145
if (animatedImage == null) {
4246
throw ImageCodecException(
@@ -45,6 +49,20 @@ class CkAnimatedImage extends ManagedSkiaObject<SkAnimatedImage>
4549
);
4650
}
4751

52+
if (targetWidth != null || targetHeight != null) {
53+
if (animatedImage.getFrameCount() > 1) {
54+
printWarning('targetWidth and targetHeight for multi-frame images not supported');
55+
} else {
56+
animatedImage = _resizeAnimatedImage(animatedImage, targetWidth, targetHeight);
57+
if (animatedImage == null) {
58+
throw ImageCodecException(
59+
'Failed to decode re-sized image data.\n'
60+
'Image source: $src',
61+
);
62+
}
63+
}
64+
}
65+
4866
_frameCount = animatedImage.getFrameCount().toInt();
4967
_repetitionCount = animatedImage.getRepetitionCount().toInt();
5068

@@ -61,6 +79,19 @@ class CkAnimatedImage extends ManagedSkiaObject<SkAnimatedImage>
6179
return animatedImage;
6280
}
6381

82+
SkAnimatedImage? _resizeAnimatedImage(SkAnimatedImage animatedImage, int? targetWidth, int? targetHeight) {
83+
final SkImage image = animatedImage.makeImageAtCurrentFrame();
84+
final CkImage ckImage = scaleImage(image, targetWidth, targetHeight);
85+
final Uint8List? resizedBytes = ckImage.skImage.encodeToBytes();
86+
87+
if (resizedBytes == null) {
88+
throw ImageCodecException('Failed to re-size image');
89+
}
90+
91+
final SkAnimatedImage? resizedAnimatedImage = canvasKit.MakeAnimatedImageFromEncoded(resizedBytes);
92+
return resizedAnimatedImage;
93+
}
94+
6495
@override
6596
SkAnimatedImage resurrect() => createDefault();
6697

lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,13 @@ void debugRestoreWebDecoderExpireDuration() {
4444
class CkBrowserImageDecoder implements ui.Codec {
4545
CkBrowserImageDecoder._({
4646
required this.contentType,
47-
required this.targetWidth,
48-
required this.targetHeight,
4947
required this.data,
5048
required this.debugSource,
5149
});
5250

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

7773
final CkBrowserImageDecoder decoder = CkBrowserImageDecoder._(
7874
contentType: contentType,
79-
targetWidth: targetWidth,
80-
targetHeight: targetHeight,
8175
data: data,
8276
debugSource: debugSource,
8377
);
@@ -88,8 +82,6 @@ class CkBrowserImageDecoder implements ui.Codec {
8882
}
8983

9084
final String contentType;
91-
final int? targetWidth;
92-
final int? targetHeight;
9385
final Uint8List data;
9486
final String debugSource;
9587

@@ -160,9 +152,6 @@ class CkBrowserImageDecoder implements ui.Codec {
160152

161153
// Flutter always uses premultiplied alpha when decoding.
162154
premultiplyAlpha: 'premultiply',
163-
desiredWidth: targetWidth,
164-
desiredHeight: targetHeight,
165-
166155
// "default" gives the browser the liberty to convert to display-appropriate
167156
// color space, typically SRGB, which is what we want.
168157
colorSpaceConversion: 'default',

lib/web_ui/lib/src/engine/safe_browser_api.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,8 @@ class ImageDecoderOptions {
280280
required String type,
281281
required Uint8List data,
282282
required String premultiplyAlpha,
283-
required int? desiredWidth,
284-
required int? desiredHeight,
283+
int? desiredWidth,
284+
int? desiredHeight,
285285
required String colorSpaceConversion,
286286
required bool preferAnimation,
287287
});

0 commit comments

Comments
 (0)