Commit a4838a2a authored by sir-boformer's avatar sir-boformer Committed by Ian Hickson

Scale high-pixel-ratio images with BoxFit.none/BoxFit.scaleDown correctly (#20791)

* Scale high-pixel-ratio images with BoxFit.none/BoxFit.scaleDown correctly

* add test cases for image scale

* make analyzer happy

* Make sure that test images always have a scale to make all tests pass

* add a new author

* use new keyword in tests
parent 7a950eb3
...@@ -26,3 +26,4 @@ Noah Groß <gross@ngsger.de> ...@@ -26,3 +26,4 @@ Noah Groß <gross@ngsger.de>
Victor Choueiri <victor@ctrlanddev.com> Victor Choueiri <victor@ctrlanddev.com>
Christian Mürtz <teraarts@t-online.de> Christian Mürtz <teraarts@t-online.de>
Lukasz Piliszczuk <lukasz@intheloup.io> Lukasz Piliszczuk <lukasz@intheloup.io>
Felix Schmidt <felix.free@gmx.de>
...@@ -254,6 +254,7 @@ class DecorationImagePainter { ...@@ -254,6 +254,7 @@ class DecorationImagePainter {
canvas: canvas, canvas: canvas,
rect: rect, rect: rect,
image: _image.image, image: _image.image,
scale: _image.scale,
colorFilter: _details.colorFilter, colorFilter: _details.colorFilter,
fit: _details.fit, fit: _details.fit,
alignment: _details.alignment.resolve(configuration.textDirection), alignment: _details.alignment.resolve(configuration.textDirection),
...@@ -303,6 +304,8 @@ class DecorationImagePainter { ...@@ -303,6 +304,8 @@ class DecorationImagePainter {
/// ///
/// * `image`: The image to paint onto the canvas. /// * `image`: The image to paint onto the canvas.
/// ///
/// * `scale`: The number of image pixels for each logical pixel.
///
/// * `colorFilter`: If non-null, the color filter to apply when painting the /// * `colorFilter`: If non-null, the color filter to apply when painting the
/// image. /// image.
/// ///
...@@ -339,7 +342,7 @@ class DecorationImagePainter { ...@@ -339,7 +342,7 @@ class DecorationImagePainter {
/// when using this, to not flip images with integral shadows, text, or other /// when using this, to not flip images with integral shadows, text, or other
/// effects that will look incorrect when flipped. /// effects that will look incorrect when flipped.
/// ///
/// The `canvas`, `rect`, `image`, `alignment`, `repeat`, and `flipHorizontally` /// The `canvas`, `rect`, `image`, `scale`, `alignment`, `repeat`, and `flipHorizontally`
/// arguments must not be null. /// arguments must not be null.
/// ///
/// See also: /// See also:
...@@ -351,6 +354,7 @@ void paintImage({ ...@@ -351,6 +354,7 @@ void paintImage({
@required Canvas canvas, @required Canvas canvas,
@required Rect rect, @required Rect rect,
@required ui.Image image, @required ui.Image image,
double scale = 1.0,
ColorFilter colorFilter, ColorFilter colorFilter,
BoxFit fit, BoxFit fit,
Alignment alignment = Alignment.center, Alignment alignment = Alignment.center,
...@@ -378,8 +382,8 @@ void paintImage({ ...@@ -378,8 +382,8 @@ void paintImage({
} }
fit ??= centerSlice == null ? BoxFit.scaleDown : BoxFit.fill; fit ??= centerSlice == null ? BoxFit.scaleDown : BoxFit.fill;
assert(centerSlice == null || (fit != BoxFit.none && fit != BoxFit.cover)); assert(centerSlice == null || (fit != BoxFit.none && fit != BoxFit.cover));
final FittedSizes fittedSizes = applyBoxFit(fit, inputSize, outputSize); final FittedSizes fittedSizes = applyBoxFit(fit, inputSize / scale, outputSize);
final Size sourceSize = fittedSizes.source; final Size sourceSize = fittedSizes.source * scale;
Size destinationSize = fittedSizes.destination; Size destinationSize = fittedSizes.destination;
if (centerSlice != null) { if (centerSlice != null) {
outputSize += sliceBorder; outputSize += sliceBorder;
...@@ -421,7 +425,7 @@ void paintImage({ ...@@ -421,7 +425,7 @@ void paintImage({
} }
if (centerSlice == null) { if (centerSlice == null) {
final Rect sourceRect = alignment.inscribe( final Rect sourceRect = alignment.inscribe(
fittedSizes.source, Offset.zero & inputSize sourceSize, Offset.zero & inputSize
); );
for (Rect tileRect in _generateImageTileRects(rect, destinationRect, repeat)) for (Rect tileRect in _generateImageTileRects(rect, destinationRect, repeat))
canvas.drawImageRect(image, sourceRect, tileRect, paint); canvas.drawImageRect(image, sourceRect, tileRect, paint);
......
...@@ -324,6 +324,7 @@ class RenderImage extends RenderBox { ...@@ -324,6 +324,7 @@ class RenderImage extends RenderBox {
canvas: context.canvas, canvas: context.canvas,
rect: offset & size, rect: offset & size,
image: _image, image: _image,
scale: _scale,
colorFilter: _colorFilter, colorFilter: _colorFilter,
fit: _fit, fit: _fit,
alignment: _resolvedAlignment, alignment: _resolvedAlignment,
......
...@@ -347,4 +347,171 @@ void main() { ...@@ -347,4 +347,171 @@ void main() {
expect(Decoration.lerp(const FlutterLogoDecoration(), const BoxDecoration(), 0.75), isInstanceOf<BoxDecoration>()); // ignore: CONST_EVAL_THROWS_EXCEPTION expect(Decoration.lerp(const FlutterLogoDecoration(), const BoxDecoration(), 0.75), isInstanceOf<BoxDecoration>()); // ignore: CONST_EVAL_THROWS_EXCEPTION
expect(Decoration.lerp(const FlutterLogoDecoration(), const BoxDecoration(), 1.0), isInstanceOf<BoxDecoration>()); // ignore: CONST_EVAL_THROWS_EXCEPTION expect(Decoration.lerp(const FlutterLogoDecoration(), const BoxDecoration(), 1.0), isInstanceOf<BoxDecoration>()); // ignore: CONST_EVAL_THROWS_EXCEPTION
}); });
test('paintImage BoxFit.none scale test', () {
for (double scale = 1.0; scale <= 4.0; scale += 1.0) {
final TestCanvas canvas = new TestCanvas(<Invocation>[]);
final Rect outputRect = new Rect.fromLTWH(30.0, 30.0, 250.0, 250.0);
final ui.Image image = new TestImage();
paintImage(
canvas: canvas,
rect: outputRect,
image: image,
scale: scale,
alignment: Alignment.bottomRight,
fit: BoxFit.none,
repeat: ImageRepeat.noRepeat,
flipHorizontally: false,
);
const Size imageSize = Size(100.0, 100.0);
final Invocation call = canvas.invocations.firstWhere((Invocation call) => call.memberName == #drawImageRect);
expect(call.isMethod, isTrue);
expect(call.positionalArguments, hasLength(4));
expect(call.positionalArguments[0], isInstanceOf<TestImage>());
// sourceRect should contain all pixels of the source image
expect(call.positionalArguments[1], Offset.zero & imageSize);
// Image should be scaled down (divided by scale)
// and be positioned in the bottom right of the outputRect
final Size expectedTileSize = imageSize / scale;
final Rect expectedTileRect = new Rect.fromPoints(
outputRect.bottomRight.translate(-expectedTileSize.width, -expectedTileSize.height),
outputRect.bottomRight,
);
expect(call.positionalArguments[2], expectedTileRect);
expect(call.positionalArguments[3], isInstanceOf<Paint>());
}
});
test('paintImage BoxFit.scaleDown scale test', () {
for (double scale = 1.0; scale <= 4.0; scale += 1.0) {
final TestCanvas canvas = new TestCanvas(<Invocation>[]);
// container size > scaled image size
final Rect outputRect = new Rect.fromLTWH(30.0, 30.0, 250.0, 250.0);
final ui.Image image = new TestImage();
paintImage(
canvas: canvas,
rect: outputRect,
image: image,
scale: scale,
alignment: Alignment.bottomRight,
fit: BoxFit.scaleDown,
repeat: ImageRepeat.noRepeat,
flipHorizontally: false,
);
const Size imageSize = Size(100.0, 100.0);
final Invocation call = canvas.invocations.firstWhere((Invocation call) => call.memberName == #drawImageRect);
expect(call.isMethod, isTrue);
expect(call.positionalArguments, hasLength(4));
expect(call.positionalArguments[0], isInstanceOf<TestImage>());
// sourceRect should contain all pixels of the source image
expect(call.positionalArguments[1], Offset.zero & imageSize);
// Image should be scaled down (divided by scale)
// and be positioned in the bottom right of the outputRect
final Size expectedTileSize = imageSize / scale;
final Rect expectedTileRect = new Rect.fromPoints(
outputRect.bottomRight.translate(-expectedTileSize.width, -expectedTileSize.height),
outputRect.bottomRight,
);
expect(call.positionalArguments[2], expectedTileRect);
expect(call.positionalArguments[3], isInstanceOf<Paint>());
}
});
test('paintImage BoxFit.scaleDown test', () {
final TestCanvas canvas = new TestCanvas(<Invocation>[]);
// container height (20 px) < scaled image height (50 px)
final Rect outputRect = new Rect.fromLTWH(30.0, 30.0, 250.0, 20.0);
final ui.Image image = new TestImage();
paintImage(
canvas: canvas,
rect: outputRect,
image: image,
scale: 2.0,
alignment: Alignment.bottomRight,
fit: BoxFit.scaleDown,
repeat: ImageRepeat.noRepeat,
flipHorizontally: false,
);
const Size imageSize = Size(100.0, 100.0);
final Invocation call = canvas.invocations.firstWhere((Invocation call) => call.memberName == #drawImageRect);
expect(call.isMethod, isTrue);
expect(call.positionalArguments, hasLength(4));
expect(call.positionalArguments[0], isInstanceOf<TestImage>());
// sourceRect should contain all pixels of the source image
expect(call.positionalArguments[1], Offset.zero & imageSize);
// Image should be scaled down to fit in hejght
// and be positioned in the bottom right of the outputRect
const Size expectedTileSize = Size(20.0, 20.0);
final Rect expectedTileRect = new Rect.fromPoints(
outputRect.bottomRight.translate(-expectedTileSize.width, -expectedTileSize.height),
outputRect.bottomRight,
);
expect(call.positionalArguments[2], expectedTileRect);
expect(call.positionalArguments[3], isInstanceOf<Paint>());
});
test('paintImage boxFit, scale and alignment test', () {
const List<BoxFit> boxFits = <BoxFit>[
BoxFit.contain,
BoxFit.cover,
BoxFit.fitWidth,
BoxFit.fitWidth,
BoxFit.fitHeight,
BoxFit.none,
BoxFit.scaleDown,
];
for(BoxFit boxFit in boxFits) {
final TestCanvas canvas = new TestCanvas(<Invocation>[]);
final Rect outputRect = new Rect.fromLTWH(30.0, 30.0, 250.0, 250.0);
final ui.Image image = new TestImage();
paintImage(
canvas: canvas,
rect: outputRect,
image: image,
scale: 3.0,
alignment: Alignment.center,
fit: boxFit,
repeat: ImageRepeat.noRepeat,
flipHorizontally: false,
);
final Invocation call = canvas.invocations.firstWhere((Invocation call) => call.memberName == #drawImageRect);
expect(call.isMethod, isTrue);
expect(call.positionalArguments, hasLength(4));
// Image should be positioned in the center of the container
expect(call.positionalArguments[2].center, outputRect.center);
}
});
} }
...@@ -11,7 +11,7 @@ import 'package:flutter/foundation.dart'; ...@@ -11,7 +11,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
class TestImageInfo implements ImageInfo { class TestImageInfo implements ImageInfo {
const TestImageInfo(this.value, { this.image, this.scale }); const TestImageInfo(this.value, { this.image, this.scale = 1.0 });
@override @override
final ui.Image image; final ui.Image image;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment