Skip to content
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
4 changes: 3 additions & 1 deletion packages/flutter/lib/src/painting/decoration_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -376,12 +376,14 @@ void paintImage({
bool flipHorizontally = false,
bool invertColors = false,
FilterQuality filterQuality = FilterQuality.low,
bool isAntiAlias = false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we assert this isn't null? Here and below.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}) {
assert(canvas != null);
assert(image != null);
assert(alignment != null);
assert(repeat != null);
assert(flipHorizontally != null);
assert(isAntiAlias != null);
if (rect.isEmpty)
return;
Size outputSize = rect.size;
Expand Down Expand Up @@ -412,7 +414,7 @@ void paintImage({
// output rect with the image.
repeat = ImageRepeat.noRepeat;
}
final Paint paint = Paint()..isAntiAlias = false;
final Paint paint = Paint()..isAntiAlias = isAntiAlias;
if (colorFilter != null)
paint.colorFilter = colorFilter;
if (sourceSize != destinationSize) {
Expand Down
18 changes: 18 additions & 0 deletions packages/flutter/lib/src/rendering/image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@ class RenderImage extends RenderBox {
bool matchTextDirection = false,
TextDirection textDirection,
bool invertColors = false,
bool isAntiAlias = false,
FilterQuality filterQuality = FilterQuality.low,
}) : assert(scale != null),
assert(repeat != null),
assert(alignment != null),
assert(filterQuality != null),
assert(matchTextDirection != null),
assert(isAntiAlias != null),
_image = image,
_width = width,
_height = height,
Expand All @@ -57,6 +59,7 @@ class RenderImage extends RenderBox {
_matchTextDirection = matchTextDirection,
_invertColors = invertColors,
_textDirection = textDirection,
_isAntiAlias = isAntiAlias,
_filterQuality = filterQuality {
_updateColorFilter();
}
Expand Down Expand Up @@ -287,6 +290,20 @@ class RenderImage extends RenderBox {
_markNeedResolution();
}

/// Whether to paint the image with anti-aliasing.
///
/// Anti-aliasing alleviates the sawtooth artifact when the image is rotated.
bool get isAntiAlias => _isAntiAlias;
bool _isAntiAlias;
set isAntiAlias(bool value) {
if (_isAntiAlias == value) {
return;
}
assert(value != null);
_isAntiAlias = value;
markNeedsPaint();
}

/// Find a size for the render image within the given constraints.
///
/// - The dimensions of the RenderImage must fit within the constraints.
Expand Down Expand Up @@ -367,6 +384,7 @@ class RenderImage extends RenderBox {
flipHorizontally: _flipHorizontally,
invertColors: invertColors,
filterQuality: _filterQuality,
isAntiAlias: _isAntiAlias,
);
}

Expand Down
8 changes: 8 additions & 0 deletions packages/flutter/lib/src/widgets/basic.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5229,10 +5229,12 @@ class RawImage extends LeafRenderObjectWidget {
this.matchTextDirection = false,
this.invertColors = false,
this.filterQuality = FilterQuality.low,
this.isAntiAlias = false,
}) : assert(scale != null),
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null),
assert(isAntiAlias != null),
super(key: key);

/// The image to display.
Expand Down Expand Up @@ -5347,6 +5349,11 @@ class RawImage extends LeafRenderObjectWidget {
/// * [Paint.invertColors], for the dart:ui implementation.
final bool invertColors;

/// Whether to paint the image with anti-aliasing.
///
/// Anti-aliasing alleviates the sawtooth artifact when the image is rotated.
final bool isAntiAlias;

@override
RenderImage createRenderObject(BuildContext context) {
assert((!matchTextDirection && alignment is Alignment) || debugCheckHasDirectionality(context));
Expand All @@ -5365,6 +5372,7 @@ class RawImage extends LeafRenderObjectWidget {
textDirection: matchTextDirection || alignment is! Alignment ? Directionality.of(context) : null,
invertColors: invertColors,
filterQuality: filterQuality,
isAntiAlias: isAntiAlias,
);
}

Expand Down
16 changes: 16 additions & 0 deletions packages/flutter/lib/src/widgets/image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -332,12 +332,14 @@ class Image extends StatefulWidget {
this.centerSlice,
this.matchTextDirection = false,
this.gaplessPlayback = false,
this.isAntiAlias = false,
this.filterQuality = FilterQuality.low,
}) : assert(image != null),
assert(alignment != null),
assert(repeat != null),
assert(filterQuality != null),
assert(matchTextDirection != null),
assert(isAntiAlias != null),
super(key: key);

/// Creates a widget that displays an [ImageStream] obtained from the network.
Expand Down Expand Up @@ -393,6 +395,7 @@ class Image extends StatefulWidget {
this.matchTextDirection = false,
this.gaplessPlayback = false,
this.filterQuality = FilterQuality.low,
this.isAntiAlias = false,
Map<String, String> headers,
int cacheWidth,
int cacheHeight,
Expand All @@ -402,6 +405,7 @@ class Image extends StatefulWidget {
assert(matchTextDirection != null),
assert(cacheWidth == null || cacheWidth > 0),
assert(cacheHeight == null || cacheHeight > 0),
assert(isAntiAlias != null),
super(key: key);

/// Creates a widget that displays an [ImageStream] obtained from a [File].
Expand Down Expand Up @@ -446,6 +450,7 @@ class Image extends StatefulWidget {
this.centerSlice,
this.matchTextDirection = false,
this.gaplessPlayback = false,
this.isAntiAlias = false,
this.filterQuality = FilterQuality.low,
int cacheWidth,
int cacheHeight,
Expand All @@ -457,6 +462,7 @@ class Image extends StatefulWidget {
assert(matchTextDirection != null),
assert(cacheWidth == null || cacheWidth > 0),
assert(cacheHeight == null || cacheHeight > 0),
assert(isAntiAlias != null),
super(key: key);


Expand Down Expand Up @@ -609,6 +615,7 @@ class Image extends StatefulWidget {
this.centerSlice,
this.matchTextDirection = false,
this.gaplessPlayback = false,
this.isAntiAlias = false,
String package,
this.filterQuality = FilterQuality.low,
int cacheWidth,
Expand All @@ -623,6 +630,7 @@ class Image extends StatefulWidget {
assert(matchTextDirection != null),
assert(cacheWidth == null || cacheWidth > 0),
assert(cacheHeight == null || cacheHeight > 0),
assert(isAntiAlias != null),
super(key: key);

/// Creates a widget that displays an [ImageStream] obtained from a [Uint8List].
Expand Down Expand Up @@ -668,6 +676,7 @@ class Image extends StatefulWidget {
this.centerSlice,
this.matchTextDirection = false,
this.gaplessPlayback = false,
this.isAntiAlias = false,
this.filterQuality = FilterQuality.low,
int cacheWidth,
int cacheHeight,
Expand All @@ -678,6 +687,7 @@ class Image extends StatefulWidget {
assert(matchTextDirection != null),
assert(cacheWidth == null || cacheWidth > 0),
assert(cacheHeight == null || cacheHeight > 0),
assert(isAntiAlias != null),
super(key: key);

/// The image to display.
Expand Down Expand Up @@ -994,6 +1004,11 @@ class Image extends StatefulWidget {
/// application.
final bool excludeFromSemantics;

/// Whether to paint the image with anti-aliasing.
///
/// Anti-aliasing alleviates the sawtooth artifact when the image is rotated.
final bool isAntiAlias;

@override
_ImageState createState() => _ImageState();

Expand Down Expand Up @@ -1196,6 +1211,7 @@ class _ImageState extends State<Image> with WidgetsBindingObserver {
centerSlice: widget.centerSlice,
matchTextDirection: widget.matchTextDirection,
invertColors: _invertColors,
isAntiAlias: widget.isAntiAlias,
filterQuality: widget.filterQuality,
);

Expand Down
49 changes: 49 additions & 0 deletions packages/flutter/test/painting/image_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,52 @@ const List<int> kAnimatedGif = <int> [
0xf9, 0x04, 0x00, 0x0a, 0x00, 0xff, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x44, 0x01, 0x00, 0x3b,
];

// A PNG with 100x100 blue pixels.
//
// Constructed by the following code:
// ```dart
// Future<void> someTest(WidgetTester tester) async {
// Uint8List bytes;
// await tester.runAsync(() async {
// const int imageWidth = 100;
// const int imageHeight = 100;
// final Uint8List pixels = Uint8List.fromList(List<int>.generate(
// imageWidth * imageHeight * 4,
// (int i) => i % 4 < 2 ? 0x00 : 0xFF, // opaque blue
// ));
// final Completer<void> completer = Completer<void>();
// ui.decodeImageFromPixels(
// pixels, imageWidth, imageHeight, ui.PixelFormat.rgba8888,
// (ui.Image image) async {
// final ByteData byteData = await image.toByteData(
// format: ui.ImageByteFormat.png);
// bytes = byteData.buffer.asUint8List();
// completer.complete();
// },
// );
// await completer.future;
// });
// print(bytes);
// }
// ```
const List<int> kBlueRectPng = <int> [
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 100, 0,
0, 0, 100, 8, 6, 0, 0, 0, 112, 226, 149, 84, 0, 0, 0, 4, 115, 66, 73, 84, 8,
8, 8, 8, 124, 8, 100, 136, 0, 0, 1, 0, 73, 68, 65, 84, 120, 156, 237, 209, 65,
13, 0, 32, 16, 192, 176, 3, 255, 158, 225, 141, 2, 246, 104, 21, 44, 217, 154,
57, 103, 200, 216, 191, 3, 120, 25, 18, 99, 72, 140, 33, 49, 134, 196, 24, 18,
99, 72, 140, 33, 49, 134, 196, 24, 18, 99, 72, 140, 33, 49, 134, 196, 24, 18,
99, 72, 140, 33, 49, 134, 196, 24, 18, 99, 72, 140, 33, 49, 134, 196, 24, 18,
99, 72, 140, 33, 49, 134, 196, 24, 18, 99, 72, 140, 33, 49, 134, 196, 24, 18,
99, 72, 140, 33, 49, 134, 196, 24, 18, 99, 72, 140, 33, 49, 134, 196, 24, 18,
99, 72, 140, 33, 49, 134, 196, 24, 18, 99, 72, 140, 33, 49, 134, 196, 24, 18,
99, 72, 140, 33, 49, 134, 196, 24, 18, 99, 72, 140, 33, 49, 134, 196, 24, 18,
99, 72, 140, 33, 49, 134, 196, 24, 18, 99, 72, 140, 33, 49, 134, 196, 24, 18,
99, 72, 140, 33, 49, 134, 196, 24, 18, 99, 72, 140, 33, 49, 134, 196, 24, 18,
99, 72, 140, 33, 49, 134, 196, 24, 18, 99, 72, 140, 33, 49, 134, 196, 24, 18,
99, 72, 140, 33, 49, 134, 196, 24, 18, 99, 72, 140, 33, 49, 134, 196, 24, 18,
99, 72, 140, 33, 49, 134, 196, 24, 18, 99, 72, 140, 33, 49, 134, 196, 24, 18,
99, 72, 140, 33, 49, 134, 196, 24, 18, 99, 72, 204, 5, 234, 78, 2, 198, 180,
170, 48, 200, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130
];
58 changes: 58 additions & 0 deletions packages/flutter/test/widgets/image_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

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

Expand Down Expand Up @@ -1620,6 +1621,63 @@ void main() {

expect(tester.takeException(), 'threw');
});

Future<void> _testRotatedImage(WidgetTester tester, bool isAntiAlias) async {
final Key key = UniqueKey();
await tester.pumpWidget(RepaintBoundary(
key: key,
child: Transform.rotate(
angle: math.pi / 180,
child: Image.memory(Uint8List.fromList(kBlueRectPng), isAntiAlias: isAntiAlias),
),
));

// precacheImage is needed, or the image in the golden file will be empty.
if (!kIsWeb) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you instead just skip the test for the browser with skip: isBrowser below? It should also have a comment referencing why the test is skipped so we remember to come back and turn it on when the issue is resolved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do still want the most of this test to run on web to see how it behaves. The precacheImage is fixing the empty image issue probably caused by the non-web image cache (CC @dnfield for confirmation). Since I didn't see Skia gold to report any strange golden images on web, I think the web is fine with this test for now?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok! Can we still reference an issue here for the precacheImage with a TODO so we know why this is is not happening on web and can enable it when it is fixed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

final Finder allImages = find.byType(Image);
for (final Element e in allImages.evaluate()) {
await tester.runAsync(() async {
final Image image = e.widget as Image;
await precacheImage(image.image, e);
});
}
await tester.pumpAndSettle();
}

await expectLater(
find.byKey(key),
matchesGoldenFile('rotated_image_${isAntiAlias ? 'aa' : 'noaa'}.png'),
);
}

testWidgets(
'Rotated images',
(WidgetTester tester) async {
await _testRotatedImage(tester, true);
await _testRotatedImage(tester, false);
},
// TODO(hterkelson): figure out why web timed out with `await precacheImage`
// so we can enable this test on web.
//
// See https://github.com/flutter/flutter/issues/54292.
skip: kIsWeb,
);
}

class ImagePainter extends CustomPainter {
ImagePainter(this.image);

@override
void paint(ui.Canvas canvas, ui.Size size) {
canvas.drawImage(image, Offset.zero, Paint());
}

@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}

final ui.Image image;
}

class ConfigurationAwareKey {
Expand Down