blob: 604a6ce4832852387a008729abf598b26a794205 [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:io';
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import 'package:vector_math/vector_math_64.dart';
import 'goldens.dart';
import 'impeller_enabled.dart';
typedef CanvasCallback = void Function(Canvas canvas);
Future<Image> createImage(int width, int height) {
final Completer<Image> completer = Completer<Image>();
decodeImageFromPixels(
Uint8List.fromList(List<int>.generate(
width * height * 4,
(int pixel) => pixel % 255,
)),
width,
height,
PixelFormat.rgba8888,
(Image image) {
completer.complete(image);
},
);
return completer.future;
}
void testCanvas(CanvasCallback callback) {
try {
callback(Canvas(PictureRecorder(), Rect.zero));
} catch (error) {} // ignore: empty_catches
}
Future<Image> toImage(CanvasCallback callback, int width, int height) {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(
recorder, Rect.fromLTRB(0, 0, width.toDouble(), height.toDouble()));
callback(canvas);
final Picture picture = recorder.endRecording();
return picture.toImage(width, height);
}
void testNoCrashes() {
test('canvas APIs should not crash', () async {
final Paint paint = Paint();
const Rect rect =
Rect.fromLTRB(double.nan, double.nan, double.nan, double.nan);
final RRect rrect = RRect.fromRectAndCorners(rect);
const Offset offset = Offset(double.nan, double.nan);
final Path path = Path();
const Color color = Color(0x00000000);
final Paragraph paragraph = ParagraphBuilder(ParagraphStyle()).build();
final PictureRecorder recorder = PictureRecorder();
final Canvas recorderCanvas = Canvas(recorder);
recorderCanvas.scale(1.0, 1.0);
final Picture picture = recorder.endRecording();
final Image image = await picture.toImage(1, 1);
try {
Canvas(PictureRecorder());
} catch (error) {} // ignore: empty_catches
try {
Canvas(PictureRecorder(), rect);
} catch (error) {} // ignore: empty_catches
try {
PictureRecorder()
..endRecording()
..endRecording()
..endRecording();
} catch (error) {} // ignore: empty_catches
testCanvas((Canvas canvas) => canvas.clipPath(path));
testCanvas((Canvas canvas) => canvas.clipRect(rect));
testCanvas((Canvas canvas) => canvas.clipRRect(rrect));
testCanvas((Canvas canvas) => canvas.drawArc(rect, 0.0, 0.0, false, paint));
testCanvas((Canvas canvas) => canvas.drawAtlas(image, <RSTransform>[],
<Rect>[], <Color>[], BlendMode.src, rect, paint));
testCanvas((Canvas canvas) => canvas.drawCircle(offset, double.nan, paint));
testCanvas((Canvas canvas) => canvas.drawColor(color, BlendMode.src));
testCanvas((Canvas canvas) => canvas.drawDRRect(rrect, rrect, paint));
testCanvas((Canvas canvas) => canvas.drawImage(image, offset, paint));
testCanvas(
(Canvas canvas) => canvas.drawImageNine(image, rect, rect, paint));
testCanvas(
(Canvas canvas) => canvas.drawImageRect(image, rect, rect, paint));
testCanvas((Canvas canvas) => canvas.drawLine(offset, offset, paint));
testCanvas((Canvas canvas) => canvas.drawOval(rect, paint));
testCanvas((Canvas canvas) => canvas.drawPaint(paint));
testCanvas((Canvas canvas) => canvas.drawParagraph(paragraph, offset));
testCanvas((Canvas canvas) => canvas.drawPath(path, paint));
testCanvas((Canvas canvas) => canvas.drawPicture(picture));
testCanvas((Canvas canvas) =>
canvas.drawPoints(PointMode.points, <Offset>[], paint));
testCanvas((Canvas canvas) => canvas.drawRawAtlas(image, Float32List(0),
Float32List(0), Int32List(0), BlendMode.src, rect, paint));
testCanvas((Canvas canvas) =>
canvas.drawRawPoints(PointMode.points, Float32List(0), paint));
testCanvas((Canvas canvas) => canvas.drawRect(rect, paint));
testCanvas((Canvas canvas) => canvas.drawRRect(rrect, paint));
testCanvas(
(Canvas canvas) => canvas.drawShadow(path, color, double.nan, false));
testCanvas(
(Canvas canvas) => canvas.drawShadow(path, color, double.nan, true));
testCanvas((Canvas canvas) => canvas.drawVertices(
Vertices(VertexMode.triangles, <Offset>[]), BlendMode.screen, paint));
testCanvas((Canvas canvas) => canvas.getSaveCount());
testCanvas((Canvas canvas) => canvas.restore());
testCanvas((Canvas canvas) => canvas.rotate(double.nan));
testCanvas((Canvas canvas) => canvas.save());
testCanvas((Canvas canvas) => canvas.saveLayer(rect, paint));
testCanvas((Canvas canvas) => canvas.saveLayer(null, paint));
testCanvas((Canvas canvas) => canvas.scale(double.nan, double.nan));
testCanvas((Canvas canvas) => canvas.skew(double.nan, double.nan));
testCanvas((Canvas canvas) => canvas.transform(Float64List(16)));
testCanvas((Canvas canvas) => canvas.translate(double.nan, double.nan));
testCanvas((Canvas canvas) => canvas.drawVertices(
Vertices(VertexMode.triangles, <Offset>[], indices: <int>[]),
BlendMode.screen,
paint));
testCanvas((Canvas canvas) => canvas.drawVertices(
Vertices(VertexMode.triangles, <Offset>[])..dispose(),
BlendMode.screen,
paint));
// Regression test for https://github.com/flutter/flutter/issues/115143
testCanvas((Canvas canvas) => canvas.drawPaint(Paint()
..imageFilter =
const ColorFilter.mode(Color(0x00000000), BlendMode.xor)));
// Regression test for https://github.com/flutter/flutter/issues/120278
testCanvas((Canvas canvas) => canvas.drawPaint(Paint()
..imageFilter = ImageFilter.compose(
outer: ImageFilter.matrix(Matrix4.identity().storage),
inner: ImageFilter.blur())));
});
}
const String kFlutterBuildDirectory = 'kFlutterBuildDirectory';
String get _flutterBuildPath {
const String buildPath = String.fromEnvironment(kFlutterBuildDirectory);
if (buildPath.isEmpty) {
throw StateError('kFlutterBuildDirectory -D variable is not set.');
}
return buildPath;
}
void main() async {
final ImageComparer comparer = await ImageComparer.create();
testNoCrashes();
test('Simple .toImage', () async {
final Image image = await toImage((Canvas canvas) {
final Path circlePath = Path()
..addOval(
Rect.fromCircle(center: const Offset(40.0, 40.0), radius: 20.0));
final Paint paint = Paint()
..isAntiAlias = false
..style = PaintingStyle.fill;
canvas.drawPath(circlePath, paint);
}, 100, 100);
expect(image.width, equals(100));
expect(image.height, equals(100));
await comparer.addGoldenImage(image, 'canvas_test_toImage.png');
});
Gradient makeGradient() {
return Gradient.linear(
Offset.zero,
const Offset(100, 100),
const <Color>[Color(0xFF4C4D52), Color(0xFF202124)],
);
}
test('Simple gradient, which is implicitly dithered', () async {
final Image image = await toImage((Canvas canvas) {
final Paint paint = Paint()..shader = makeGradient();
canvas.drawPaint(paint);
}, 100, 100);
expect(image.width, equals(100));
expect(image.height, equals(100));
await comparer.addGoldenImage(image, 'canvas_test_dithered_gradient.png');
});
test('Null values allowed for drawAtlas methods', () async {
final Image image = await createImage(100, 100);
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
const Rect rect = Rect.fromLTWH(0, 0, 100, 100);
final RSTransform transform = RSTransform(1, 0, 0, 0);
const Color color = Color(0x00000000);
final Paint paint = Paint();
canvas.drawAtlas(image, <RSTransform>[transform], <Rect>[rect],
<Color>[color], BlendMode.src, rect, paint);
canvas.drawAtlas(image, <RSTransform>[transform], <Rect>[rect],
<Color>[color], BlendMode.src, null, paint);
canvas.drawAtlas(image, <RSTransform>[transform], <Rect>[rect], <Color>[],
null, rect, paint);
canvas.drawAtlas(
image, <RSTransform>[transform], <Rect>[rect], null, null, rect, paint);
canvas.drawRawAtlas(image, Float32List(0), Float32List(0), Int32List(0),
BlendMode.src, rect, paint);
canvas.drawRawAtlas(image, Float32List(0), Float32List(0), Int32List(0),
BlendMode.src, null, paint);
canvas.drawRawAtlas(
image, Float32List(0), Float32List(0), null, null, rect, paint);
expect(
() => canvas.drawAtlas(image, <RSTransform>[transform], <Rect>[rect],
<Color>[color], null, rect, paint),
throwsA(isA<AssertionError>()),
);
});
test('Data lengths must match for drawAtlas methods', () async {
final Image image = await createImage(100, 100);
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
const Rect rect = Rect.fromLTWH(0, 0, 100, 100);
final RSTransform transform = RSTransform(1, 0, 0, 0);
const Color color = Color(0x00000000);
final Paint paint = Paint();
canvas.drawAtlas(image, <RSTransform>[transform], <Rect>[rect],
<Color>[color], BlendMode.src, rect, paint);
canvas.drawAtlas(image, <RSTransform>[transform, transform],
<Rect>[rect, rect], <Color>[color, color], BlendMode.src, rect, paint);
canvas.drawAtlas(image, <RSTransform>[transform], <Rect>[rect], <Color>[],
null, rect, paint);
canvas.drawAtlas(
image, <RSTransform>[transform], <Rect>[rect], null, null, rect, paint);
canvas.drawRawAtlas(image, Float32List(0), Float32List(0), Int32List(0),
BlendMode.src, rect, paint);
canvas.drawRawAtlas(image, Float32List(4), Float32List(4), Int32List(1),
BlendMode.src, rect, paint);
canvas.drawRawAtlas(
image, Float32List(4), Float32List(4), null, null, rect, paint);
expect(
() => canvas.drawAtlas(image, <RSTransform>[transform], <Rect>[],
<Color>[color], BlendMode.src, rect, paint),
throwsArgumentError);
expect(
() => canvas.drawAtlas(image, <RSTransform>[], <Rect>[rect],
<Color>[color], BlendMode.src, rect, paint),
throwsArgumentError);
expect(
() => canvas.drawAtlas(image, <RSTransform>[transform], <Rect>[rect],
<Color>[color, color], BlendMode.src, rect, paint),
throwsArgumentError);
expect(
() => canvas.drawAtlas(image, <RSTransform>[transform],
<Rect>[rect, rect], <Color>[color], BlendMode.src, rect, paint),
throwsArgumentError);
expect(
() => canvas.drawAtlas(image, <RSTransform>[transform, transform],
<Rect>[rect], <Color>[color], BlendMode.src, rect, paint),
throwsArgumentError);
expect(
() => canvas.drawRawAtlas(
image, Float32List(3), Float32List(3), null, null, rect, paint),
throwsArgumentError);
expect(
() => canvas.drawRawAtlas(
image, Float32List(4), Float32List(0), null, null, rect, paint),
throwsArgumentError);
expect(
() => canvas.drawRawAtlas(
image, Float32List(0), Float32List(4), null, null, rect, paint),
throwsArgumentError);
expect(
() => canvas.drawRawAtlas(image, Float32List(4), Float32List(4),
Int32List(2), BlendMode.src, rect, paint),
throwsArgumentError);
});
test('Canvas preserves perspective data in Matrix4', () async {
const double rotateAroundX = pi / 6; // 30 degrees
const double rotateAroundY = pi / 9; // 20 degrees
const int width = 150;
const int height = 150;
const Color black = Color.fromARGB(255, 0, 0, 0);
const Color green = Color.fromARGB(255, 0, 255, 0);
void paint(Canvas canvas, CanvasCallback rotate) {
canvas.translate(width * 0.5, height * 0.5);
rotate(canvas);
const double width3 = width / 3.0;
const double width5 = width / 5.0;
const double width10 = width / 10.0;
canvas.drawRect(const Rect.fromLTRB(-width3, -width3, width3, width3),
Paint()..color = green);
canvas.drawRect(const Rect.fromLTRB(-width5, -width5, -width10, width5),
Paint()..color = black);
canvas.drawRect(const Rect.fromLTRB(-width5, -width5, width5, -width10),
Paint()..color = black);
}
final Image incrementalMatrixImage = await toImage((Canvas canvas) {
paint(canvas, (Canvas canvas) {
final Matrix4 matrix = Matrix4.identity();
matrix.setEntry(3, 2, 0.001);
canvas.transform(matrix.storage);
matrix.setRotationX(rotateAroundX);
canvas.transform(matrix.storage);
matrix.setRotationY(rotateAroundY);
canvas.transform(matrix.storage);
});
}, width, height);
final Image combinedMatrixImage = await toImage((Canvas canvas) {
paint(canvas, (Canvas canvas) {
final Matrix4 matrix = Matrix4.identity();
matrix.setEntry(3, 2, 0.001);
matrix.rotateX(rotateAroundX);
matrix.rotateY(rotateAroundY);
canvas.transform(matrix.storage);
});
}, width, height);
final bool areEqual = await comparer.fuzzyCompareImages(
incrementalMatrixImage, combinedMatrixImage);
expect(areEqual, true);
});
test('Path effects from Paragraphs do not affect further rendering',
() async {
void drawText(Canvas canvas, String content, Offset offset,
{TextDecorationStyle style = TextDecorationStyle.solid}) {
final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle());
builder.pushStyle(TextStyle(
decoration: TextDecoration.underline,
decorationColor: const Color(0xFF0000FF),
fontFamily: 'Ahem',
fontSize: 10,
color: const Color(0xFF000000),
decorationStyle: style,
));
builder.addText(content);
final Paragraph paragraph = builder.build();
paragraph.layout(const ParagraphConstraints(width: 100));
canvas.drawParagraph(paragraph, offset);
}
final Image image = await toImage((Canvas canvas) {
canvas.drawColor(const Color(0xFFFFFFFF), BlendMode.srcOver);
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 5;
drawText(canvas, 'Hello World', const Offset(20, 10));
canvas.drawCircle(
const Offset(150, 25), 15, paint..color = const Color(0xFF00FF00));
drawText(canvas, 'Regular text', const Offset(20, 60));
canvas.drawCircle(
const Offset(150, 75), 15, paint..color = const Color(0xFFFFFF00));
drawText(canvas, 'Dotted text', const Offset(20, 110),
style: TextDecorationStyle.dotted);
canvas.drawCircle(
const Offset(150, 125), 15, paint..color = const Color(0xFFFF0000));
drawText(canvas, 'Dashed text', const Offset(20, 160),
style: TextDecorationStyle.dashed);
canvas.drawCircle(
const Offset(150, 175), 15, paint..color = const Color(0xFFFF0000));
drawText(canvas, 'Wavy text', const Offset(20, 210),
style: TextDecorationStyle.wavy);
canvas.drawCircle(
const Offset(150, 225), 15, paint..color = const Color(0xFFFF0000));
}, 200, 250);
expect(image.width, equals(200));
expect(image.height, equals(250));
await comparer.addGoldenImage(
image, 'dotted_path_effect_mixed_with_stroked_geometry.png');
});
test('Gradients with matrices in Paragraphs render correctly', () async {
final Image image = await toImage((Canvas canvas) {
final Paint p = Paint();
final Float64List transform = Float64List.fromList(<double>[
86.80000129342079,
0.0,
0.0,
0.0,
0.0,
94.5,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
60.0,
224.310302734375,
0.0,
1.0
]);
p.shader = Gradient.radial(
const Offset(2.5, 0.33),
0.8,
<Color>[
const Color(0xffff0000),
const Color(0xff00ff00),
const Color(0xff0000ff),
const Color(0xffff00ff)
],
<double>[0.0, 0.3, 0.7, 0.9],
TileMode.mirror,
transform,
const Offset(2.55, 0.4));
final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle());
builder.pushStyle(TextStyle(
foreground: p,
fontSize: 200,
));
builder.addText('Woodstock!');
final Paragraph paragraph = builder.build();
paragraph.layout(const ParagraphConstraints(width: 1000));
canvas.drawParagraph(paragraph, const Offset(10, 150));
}, 600, 400);
expect(image.width, equals(600));
expect(image.height, equals(400));
await comparer.addGoldenImage(image, 'text_with_gradient_with_matrix.png');
});
test('toImageSync - too big', () async {
PictureRecorder recorder = PictureRecorder();
Canvas canvas = Canvas(recorder);
canvas.drawPaint(Paint()..color = const Color(0xFF123456));
final Picture picture = recorder.endRecording();
final Image image = picture.toImageSync(300000, 4000000);
picture.dispose();
expect(image.width, 300000);
expect(image.height, 4000000);
recorder = PictureRecorder();
canvas = Canvas(recorder);
if (impellerEnabled) {
// Impeller tries to automagically scale this. See
// https://github.com/flutter/flutter/issues/128885
canvas.drawImage(image, Offset.zero, Paint());
return;
}
// On a slower CI machine, the raster thread may get behind the UI thread
// here. However, once the image is in an error state it will immediately
// throw on subsequent attempts.
bool caughtException = false;
for (int iterations = 0; iterations < 1000; iterations += 1) {
try {
canvas.drawImage(image, Offset.zero, Paint());
} on PictureRasterizationException catch (e) {
caughtException = true;
expect(
e.message,
contains(
'unable to create bitmap render target at specified size ${image.width}x${image.height}'),
);
break;
}
// Let the event loop turn.
await Future<void>.delayed(const Duration(milliseconds: 1));
}
expect(caughtException, true);
expect(
() => canvas.drawImageRect(image, Rect.zero, Rect.zero, Paint()),
throwsException,
);
expect(
() => canvas.drawImageNine(image, Rect.zero, Rect.zero, Paint()),
throwsException,
);
expect(
() => canvas.drawAtlas(
image, <RSTransform>[], <Rect>[], null, null, null, Paint()),
throwsException,
);
});
test('toImageSync - succeeds', () async {
PictureRecorder recorder = PictureRecorder();
Canvas canvas = Canvas(recorder);
canvas.drawPaint(Paint()..color = const Color(0xFF123456));
final Picture picture = recorder.endRecording();
final Image image = picture.toImageSync(30, 40);
picture.dispose();
expect(image.width, 30);
expect(image.height, 40);
recorder = PictureRecorder();
canvas = Canvas(recorder);
expect(
() => canvas.drawImage(image, Offset.zero, Paint()),
returnsNormally,
);
expect(
() => canvas.drawImageRect(image, Rect.zero, Rect.zero, Paint()),
returnsNormally,
);
expect(
() => canvas.drawImageNine(image, Rect.zero, Rect.zero, Paint()),
returnsNormally,
);
expect(
() => canvas.drawAtlas(
image, <RSTransform>[], <Rect>[], null, null, null, Paint()),
returnsNormally,
);
});
test('toImageSync - toByteData', () async {
const Color color = Color(0xFF123456);
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.drawPaint(Paint()..color = color);
final Picture picture = recorder.endRecording();
final Image image = picture.toImageSync(6, 8);
picture.dispose();
expect(image.width, 6);
expect(image.height, 8);
final ByteData? data = await image.toByteData();
expect(data, isNotNull);
expect(data!.lengthInBytes, 6 * 8 * 4);
expect(data.buffer.asUint8List()[0], 0x12);
expect(data.buffer.asUint8List()[1], 0x34);
expect(data.buffer.asUint8List()[2], 0x56);
expect(data.buffer.asUint8List()[3], 0xFF);
});
test('toImage and toImageSync have identical contents', () async {
// Note: on linux this stil seems to be different.
// TODO(jonahwilliams): https://github.com/flutter/flutter/issues/108835
if (Platform.isLinux) {
return;
}
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.drawRect(
const Rect.fromLTWH(20, 20, 100, 100),
Paint()..color = const Color(0xA0FF6D00),
);
final Picture picture = recorder.endRecording();
final Image toImageImage = await picture.toImage(200, 200);
final Image toImageSyncImage = picture.toImageSync(200, 200);
// To trigger observable difference in alpha, draw image
// on a second canvas.
Future<ByteData> drawOnCanvas(Image image) async {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.drawPaint(Paint()..color = const Color(0x4FFFFFFF));
canvas.drawImage(image, Offset.zero, Paint());
final Image resultImage = await recorder.endRecording().toImage(200, 200);
return (await resultImage.toByteData())!;
}
final ByteData dataSync = await drawOnCanvas(toImageImage);
final ByteData data = await drawOnCanvas(toImageSyncImage);
expect(data.buffer.asUint8List(), equals(dataSync.buffer.asUint8List()));
});
test('Canvas.drawParagraph throws when Paragraph.layout was not called',
() async {
// Regression test for https://github.com/flutter/flutter/issues/97172
expect(() {
toImage((Canvas canvas) {
final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle());
builder.addText('Woodstock!');
final Paragraph woodstock = builder.build();
canvas.drawParagraph(woodstock, const Offset(0, 50));
}, 100, 100);
}, throwsA(isA<AssertionError>()));
});
Future<Image> drawText(String text) {
return toImage((Canvas canvas) {
final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(
fontFamily: 'RobotoSerif',
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
fontSize: 15.0,
));
builder.pushStyle(TextStyle(color: const Color(0xFF0000FF)));
builder.addText(text);
final Paragraph paragraph = builder.build();
paragraph.layout(const ParagraphConstraints(width: 20 * 5.0));
canvas.drawParagraph(paragraph, Offset.zero);
}, 100, 100);
}
test('Canvas.drawParagraph renders tab as space instead of tofu', () async {
// Skia renders a tofu if the font does not have a glyph for a character.
// However, Flutter opts-in to a Skia feature to render tabs as a single space.
// See: https://github.com/flutter/flutter/issues/79153
final File file = File(path.join(_flutterBuildPath, 'flutter',
'third_party', 'txt', 'assets', 'Roboto-Regular.ttf'));
final Uint8List fontData = await file.readAsBytes();
await loadFontFromList(fontData, fontFamily: 'RobotoSerif');
// The backspace character, \b, does not have a corresponding glyph and is rendered as a tofu.
final Image tabImage = await drawText('>\t<');
final Image spaceImage = await drawText('> <');
final Image tofuImage = await drawText('>\b<');
// The tab's image should be identical to the space's image but not the tofu's image.
final bool tabToSpaceComparison =
await comparer.fuzzyCompareImages(tabImage, spaceImage);
final bool tabToTofuComparison =
await comparer.fuzzyCompareImages(tabImage, tofuImage);
expect(tabToSpaceComparison, isTrue);
expect(tabToTofuComparison, isFalse);
});
test('drawRect, drawOval, and clipRect render with unsorted rectangles',
() async {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.drawColor(const Color(0xFFE0E0E0), BlendMode.src);
void draw(Rect rect, double x, double y, Color color) {
final Paint paint = Paint()
..color = color
..strokeWidth = 5.0;
final Rect tallThin = Rect.fromLTRB(
min(rect.left, rect.right) - 10,
rect.top,
min(rect.left, rect.right) - 10,
rect.bottom,
);
final Rect wideThin = Rect.fromLTRB(
rect.left,
min(rect.top, rect.bottom) - 10,
rect.right,
min(rect.top, rect.bottom) - 10,
);
canvas.save();
canvas.translate(x, y);
paint.style = PaintingStyle.fill;
canvas.drawRect(rect, paint);
canvas.drawRect(tallThin, paint);
canvas.drawRect(wideThin, paint);
canvas.save();
canvas.translate(0, 100);
paint.style = PaintingStyle.stroke;
canvas.drawRect(rect, paint);
canvas.drawRect(tallThin, paint);
canvas.drawRect(wideThin, paint);
canvas.restore();
canvas.save();
canvas.translate(100, 0);
paint.style = PaintingStyle.fill;
canvas.drawOval(rect, paint);
canvas.drawOval(tallThin, paint);
canvas.drawOval(wideThin, paint);
canvas.restore();
canvas.save();
canvas.translate(100, 100);
paint.style = PaintingStyle.stroke;
canvas.drawOval(rect, paint);
canvas.drawOval(tallThin, paint);
canvas.drawOval(wideThin, paint);
canvas.restore();
canvas.save();
canvas.translate(50, 50);
canvas.save();
canvas.clipRect(rect);
canvas.drawPaint(paint);
canvas.restore();
canvas.save();
canvas.clipRect(tallThin);
canvas.drawPaint(paint);
canvas.restore();
canvas.save();
canvas.clipRect(wideThin);
canvas.drawPaint(paint);
canvas.restore();
canvas.restore();
canvas.restore();
}
draw(const Rect.fromLTRB(10, 10, 40, 40), 50, 50, const Color(0xFF2196F3));
draw(const Rect.fromLTRB(40, 10, 10, 40), 250, 50, const Color(0xFF4CAF50));
draw(const Rect.fromLTRB(10, 40, 40, 10), 50, 250, const Color(0xFF9C27B0));
draw(
const Rect.fromLTRB(40, 40, 10, 10), 250, 250, const Color(0xFFFF9800));
final Picture picture = recorder.endRecording();
final Image image = await picture.toImage(450, 450);
await comparer.addGoldenImage(image, 'render_unordered_rects.png');
});
test('Canvas.translate affects canvas.getTransform', () async {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.translate(12, 14.5);
final Float64List matrix = Matrix4.translationValues(12, 14.5, 0).storage;
final Float64List curMatrix = canvas.getTransform();
expect(curMatrix, closeToTransform(matrix));
canvas.translate(10, 10);
final Float64List newCurMatrix = canvas.getTransform();
expect(newCurMatrix, isNot(closeToTransform(matrix)));
expect(curMatrix, closeToTransform(matrix));
});
test('Canvas.scale affects canvas.getTransform', () async {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.scale(12, 14.5);
final Float64List matrix = Matrix4.diagonal3Values(12, 14.5, 1).storage;
final Float64List curMatrix = canvas.getTransform();
expect(curMatrix, closeToTransform(matrix));
canvas.scale(10, 10);
final Float64List newCurMatrix = canvas.getTransform();
expect(newCurMatrix, isNot(closeToTransform(matrix)));
expect(curMatrix, closeToTransform(matrix));
});
test('Canvas.rotate affects canvas.getTransform', () async {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.rotate(pi);
final Float64List matrix = Matrix4.rotationZ(pi).storage;
final Float64List curMatrix = canvas.getTransform();
expect(curMatrix, closeToTransform(matrix));
canvas.rotate(pi / 2);
final Float64List newCurMatrix = canvas.getTransform();
expect(newCurMatrix, isNot(closeToTransform(matrix)));
expect(curMatrix, closeToTransform(matrix));
});
test('Canvas.skew affects canvas.getTransform', () async {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.skew(12, 14.5);
final Float64List matrix = (Matrix4.identity()
..setEntry(0, 1, 12)
..setEntry(1, 0, 14.5))
.storage;
final Float64List curMatrix = canvas.getTransform();
expect(curMatrix, closeToTransform(matrix));
canvas.skew(10, 10);
final Float64List newCurMatrix = canvas.getTransform();
expect(newCurMatrix, isNot(closeToTransform(matrix)));
expect(curMatrix, closeToTransform(matrix));
});
test('Canvas.transform affects canvas.getTransform', () async {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
final Float64List matrix = (Matrix4.identity()
..translate(12.0, 14.5)
..scale(12.0, 14.5))
.storage;
canvas.transform(matrix);
final Float64List curMatrix = canvas.getTransform();
expect(curMatrix, closeToTransform(matrix));
canvas.translate(10, 10);
final Float64List newCurMatrix = canvas.getTransform();
expect(newCurMatrix, isNot(closeToTransform(matrix)));
expect(curMatrix, closeToTransform(matrix));
});
test('Canvas.clipRect affects canvas.getClipBounds', () async {
void testRect(Rect clipRect, bool doAA) {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.clipRect(clipRect, doAntiAlias: doAA);
final Rect clipSortedBounds = Rect.fromLTRB(
min(clipRect.left, clipRect.right),
min(clipRect.top, clipRect.bottom),
max(clipRect.left, clipRect.right),
max(clipRect.top, clipRect.bottom),
);
Rect clipExpandedBounds;
if (doAA) {
clipExpandedBounds = Rect.fromLTRB(
clipSortedBounds.left.floorToDouble(),
clipSortedBounds.top.floorToDouble(),
clipSortedBounds.right.ceilToDouble(),
clipSortedBounds.bottom.ceilToDouble(),
);
} else {
clipExpandedBounds = clipSortedBounds;
}
// Save initial return values for testing restored values
final Rect initialLocalBounds = canvas.getLocalClipBounds();
final Rect initialDestinationBounds = canvas.getDestinationClipBounds();
expect(initialLocalBounds, closeToRect(clipExpandedBounds));
expect(initialDestinationBounds, closeToRect(clipExpandedBounds));
canvas.save();
canvas.clipRect(const Rect.fromLTRB(0, 0, 15, 15));
// Both clip bounds have changed
expect(
canvas.getLocalClipBounds(), isNot(closeToRect(clipExpandedBounds)));
expect(canvas.getDestinationClipBounds(),
isNot(closeToRect(clipExpandedBounds)));
// Previous return values have not changed
expect(initialLocalBounds, closeToRect(clipExpandedBounds));
expect(initialDestinationBounds, closeToRect(clipExpandedBounds));
canvas.restore();
// save/restore returned the values to their original values
expect(canvas.getLocalClipBounds(), initialLocalBounds);
expect(canvas.getDestinationClipBounds(), initialDestinationBounds);
canvas.save();
canvas.scale(2, 2);
final Rect scaledExpandedBounds = Rect.fromLTRB(
clipExpandedBounds.left / 2.0,
clipExpandedBounds.top / 2.0,
clipExpandedBounds.right / 2.0,
clipExpandedBounds.bottom / 2.0,
);
expect(canvas.getLocalClipBounds(), closeToRect(scaledExpandedBounds));
// Destination bounds are unaffected by transform
expect(
canvas.getDestinationClipBounds(), closeToRect(clipExpandedBounds));
canvas.restore();
// save/restore returned the values to their original values
expect(canvas.getLocalClipBounds(), initialLocalBounds);
expect(canvas.getDestinationClipBounds(), initialDestinationBounds);
}
testRect(const Rect.fromLTRB(10.2, 11.3, 20.4, 25.7), false);
testRect(const Rect.fromLTRB(10.2, 11.3, 20.4, 25.7), true);
// LR swapped
testRect(const Rect.fromLTRB(20.4, 11.3, 10.2, 25.7), false);
testRect(const Rect.fromLTRB(20.4, 11.3, 10.2, 25.7), true);
// TB swapped
testRect(const Rect.fromLTRB(10.2, 25.7, 20.4, 11.3), false);
testRect(const Rect.fromLTRB(10.2, 25.7, 20.4, 11.3), true);
// LR and TB swapped
testRect(const Rect.fromLTRB(20.4, 25.7, 10.2, 11.3), false);
testRect(const Rect.fromLTRB(20.4, 25.7, 10.2, 11.3), true);
});
test('Canvas.clipRect with matrix affects canvas.getClipBounds', () async {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
const Rect clipBounds1 = Rect.fromLTRB(0.0, 0.0, 10.0, 10.0);
const Rect clipBounds2 = Rect.fromLTRB(10.0, 10.0, 20.0, 20.0);
canvas.save();
canvas.clipRect(clipBounds1, doAntiAlias: false);
canvas.translate(0, 10.0);
canvas.clipRect(clipBounds1, doAntiAlias: false);
expect(canvas.getDestinationClipBounds().isEmpty, isTrue);
canvas.restore();
canvas.save();
canvas.clipRect(clipBounds1, doAntiAlias: false);
canvas.translate(-10.0, -10.0);
canvas.clipRect(clipBounds2, doAntiAlias: false);
expect(canvas.getDestinationClipBounds(), clipBounds1);
canvas.restore();
});
test('Canvas.clipRRect(doAA=true) affects canvas.getClipBounds', () async {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
const Rect clipBounds = Rect.fromLTRB(10.2, 11.3, 20.4, 25.7);
const Rect clipExpandedBounds = Rect.fromLTRB(10, 11, 21, 26);
final RRect clip =
RRect.fromRectAndRadius(clipBounds, const Radius.circular(3));
canvas.clipRRect(clip);
// Save initial return values for testing restored values
final Rect initialLocalBounds = canvas.getLocalClipBounds();
final Rect initialDestinationBounds = canvas.getDestinationClipBounds();
expect(initialLocalBounds, closeToRect(clipExpandedBounds));
expect(initialDestinationBounds, closeToRect(clipExpandedBounds));
canvas.save();
canvas.clipRect(const Rect.fromLTRB(0, 0, 15, 15));
// Both clip bounds have changed
expect(canvas.getLocalClipBounds(), isNot(closeToRect(clipExpandedBounds)));
expect(canvas.getDestinationClipBounds(),
isNot(closeToRect(clipExpandedBounds)));
// Previous return values have not changed
expect(initialLocalBounds, closeToRect(clipExpandedBounds));
expect(initialDestinationBounds, closeToRect(clipExpandedBounds));
canvas.restore();
// save/restore returned the values to their original values
expect(canvas.getLocalClipBounds(), initialLocalBounds);
expect(canvas.getDestinationClipBounds(), initialDestinationBounds);
canvas.save();
canvas.scale(2, 2);
const Rect scaledExpandedBounds = Rect.fromLTRB(5, 5.5, 10.5, 13);
expect(canvas.getLocalClipBounds(), closeToRect(scaledExpandedBounds));
// Destination bounds are unaffected by transform
expect(canvas.getDestinationClipBounds(), closeToRect(clipExpandedBounds));
canvas.restore();
// save/restore returned the values to their original values
expect(canvas.getLocalClipBounds(), initialLocalBounds);
expect(canvas.getDestinationClipBounds(), initialDestinationBounds);
});
test('Canvas.clipRRect(doAA=false) affects canvas.getClipBounds', () async {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
const Rect clipBounds = Rect.fromLTRB(10.2, 11.3, 20.4, 25.7);
final RRect clip =
RRect.fromRectAndRadius(clipBounds, const Radius.circular(3));
canvas.clipRRect(clip, doAntiAlias: false);
// Save initial return values for testing restored values
final Rect initialLocalBounds = canvas.getLocalClipBounds();
final Rect initialDestinationBounds = canvas.getDestinationClipBounds();
expect(initialLocalBounds, closeToRect(clipBounds));
expect(initialDestinationBounds, closeToRect(clipBounds));
canvas.save();
canvas.clipRect(const Rect.fromLTRB(0, 0, 15, 15), doAntiAlias: false);
// Both clip bounds have changed
expect(canvas.getLocalClipBounds(), isNot(closeToRect(clipBounds)));
expect(canvas.getDestinationClipBounds(), isNot(closeToRect(clipBounds)));
// Previous return values have not changed
expect(initialLocalBounds, closeToRect(clipBounds));
expect(initialDestinationBounds, closeToRect(clipBounds));
canvas.restore();
// save/restore returned the values to their original values
expect(canvas.getLocalClipBounds(), initialLocalBounds);
expect(canvas.getDestinationClipBounds(), initialDestinationBounds);
canvas.save();
canvas.scale(2, 2);
const Rect scaledClipBounds = Rect.fromLTRB(5.1, 5.65, 10.2, 12.85);
expect(canvas.getLocalClipBounds(), closeToRect(scaledClipBounds));
// Destination bounds are unaffected by transform
expect(canvas.getDestinationClipBounds(), closeToRect(clipBounds));
canvas.restore();
// save/restore returned the values to their original values
expect(canvas.getLocalClipBounds(), initialLocalBounds);
expect(canvas.getDestinationClipBounds(), initialDestinationBounds);
});
test('Canvas.clipRRect with matrix affects canvas.getClipBounds', () async {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
const Rect clipBounds1 = Rect.fromLTRB(0.0, 0.0, 10.0, 10.0);
const Rect clipBounds2 = Rect.fromLTRB(10.0, 10.0, 20.0, 20.0);
final RRect clip1 =
RRect.fromRectAndRadius(clipBounds1, const Radius.circular(3));
final RRect clip2 =
RRect.fromRectAndRadius(clipBounds2, const Radius.circular(3));
canvas.save();
canvas.clipRRect(clip1, doAntiAlias: false);
canvas.translate(0, 10.0);
canvas.clipRRect(clip1, doAntiAlias: false);
expect(canvas.getDestinationClipBounds().isEmpty, isTrue);
canvas.restore();
canvas.save();
canvas.clipRRect(clip1, doAntiAlias: false);
canvas.translate(-10.0, -10.0);
canvas.clipRRect(clip2, doAntiAlias: false);
expect(canvas.getDestinationClipBounds(), clipBounds1);
canvas.restore();
});
test('Canvas.clipPath(doAA=true) affects canvas.getClipBounds', () async {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
const Rect clipBounds = Rect.fromLTRB(10.2, 11.3, 20.4, 25.7);
const Rect clipExpandedBounds = Rect.fromLTRB(10, 11, 21, 26);
final Path clip = Path()
..addRect(clipBounds)
..addOval(clipBounds);
canvas.clipPath(clip);
// Save initial return values for testing restored values
final Rect initialLocalBounds = canvas.getLocalClipBounds();
final Rect initialDestinationBounds = canvas.getDestinationClipBounds();
expect(initialLocalBounds, closeToRect(clipExpandedBounds));
expect(initialDestinationBounds, closeToRect(clipExpandedBounds));
canvas.save();
canvas.clipRect(const Rect.fromLTRB(0, 0, 15, 15));
// Both clip bounds have changed
expect(canvas.getLocalClipBounds(), isNot(closeToRect(clipExpandedBounds)));
expect(canvas.getDestinationClipBounds(),
isNot(closeToRect(clipExpandedBounds)));
// Previous return values have not changed
expect(initialLocalBounds, closeToRect(clipExpandedBounds));
expect(initialDestinationBounds, closeToRect(clipExpandedBounds));
canvas.restore();
// save/restore returned the values to their original values
expect(canvas.getLocalClipBounds(), initialLocalBounds);
expect(canvas.getDestinationClipBounds(), initialDestinationBounds);
canvas.save();
canvas.scale(2, 2);
const Rect scaledExpandedBounds = Rect.fromLTRB(5, 5.5, 10.5, 13);
expect(canvas.getLocalClipBounds(), closeToRect(scaledExpandedBounds));
// Destination bounds are unaffected by transform
expect(canvas.getDestinationClipBounds(), closeToRect(clipExpandedBounds));
canvas.restore();
// save/restore returned the values to their original values
expect(canvas.getLocalClipBounds(), initialLocalBounds);
expect(canvas.getDestinationClipBounds(), initialDestinationBounds);
});
test('Canvas.clipPath(doAA=false) affects canvas.getClipBounds', () async {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
const Rect clipBounds = Rect.fromLTRB(10.2, 11.3, 20.4, 25.7);
final Path clip = Path()
..addRect(clipBounds)
..addOval(clipBounds);
canvas.clipPath(clip, doAntiAlias: false);
// Save initial return values for testing restored values
final Rect initialLocalBounds = canvas.getLocalClipBounds();
final Rect initialDestinationBounds = canvas.getDestinationClipBounds();
expect(initialLocalBounds, closeToRect(clipBounds));
expect(initialDestinationBounds, closeToRect(clipBounds));
canvas.save();
canvas.clipRect(const Rect.fromLTRB(0, 0, 15, 15), doAntiAlias: false);
// Both clip bounds have changed
expect(canvas.getLocalClipBounds(), isNot(closeToRect(clipBounds)));
expect(canvas.getDestinationClipBounds(), isNot(closeToRect(clipBounds)));
// Previous return values have not changed
expect(initialLocalBounds, closeToRect(clipBounds));
expect(initialDestinationBounds, closeToRect(clipBounds));
canvas.restore();
// save/restore returned the values to their original values
expect(canvas.getLocalClipBounds(), initialLocalBounds);
expect(canvas.getDestinationClipBounds(), initialDestinationBounds);
canvas.save();
canvas.scale(2, 2);
const Rect scaledClipBounds = Rect.fromLTRB(5.1, 5.65, 10.2, 12.85);
expect(canvas.getLocalClipBounds(), closeToRect(scaledClipBounds));
// Destination bounds are unaffected by transform
expect(canvas.getDestinationClipBounds(), closeToRect(clipBounds));
canvas.restore();
// save/restore returned the values to their original values
expect(canvas.getLocalClipBounds(), initialLocalBounds);
expect(canvas.getDestinationClipBounds(), initialDestinationBounds);
});
test('Canvas.clipPath with matrix affects canvas.getClipBounds', () async {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
const Rect clipBounds1 = Rect.fromLTRB(0.0, 0.0, 10.0, 10.0);
const Rect clipBounds2 = Rect.fromLTRB(10.0, 10.0, 20.0, 20.0);
final Path clip1 = Path()
..addRect(clipBounds1)
..addOval(clipBounds1);
final Path clip2 = Path()
..addRect(clipBounds2)
..addOval(clipBounds2);
canvas.save();
canvas.clipPath(clip1, doAntiAlias: false);
canvas.translate(0, 10.0);
canvas.clipPath(clip1, doAntiAlias: false);
expect(canvas.getDestinationClipBounds().isEmpty, isTrue);
canvas.restore();
canvas.save();
canvas.clipPath(clip1, doAntiAlias: false);
canvas.translate(-10.0, -10.0);
canvas.clipPath(clip2, doAntiAlias: false);
expect(canvas.getDestinationClipBounds(), clipBounds1);
canvas.restore();
});
test('Canvas.clipRect(diff) does not affect canvas.getClipBounds', () async {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
const Rect clipBounds = Rect.fromLTRB(10.2, 11.3, 20.4, 25.7);
canvas.clipRect(clipBounds, doAntiAlias: false);
// Save initial return values for testing restored values
final Rect initialLocalBounds = canvas.getLocalClipBounds();
final Rect initialDestinationBounds = canvas.getDestinationClipBounds();
expect(initialLocalBounds, closeToRect(clipBounds));
expect(initialDestinationBounds, closeToRect(clipBounds));
canvas.clipRect(const Rect.fromLTRB(0, 0, 15, 15),
clipOp: ClipOp.difference, doAntiAlias: false);
expect(canvas.getLocalClipBounds(), initialLocalBounds);
expect(canvas.getDestinationClipBounds(), initialDestinationBounds);
});
test('RestoreToCount can work', () async {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.save();
canvas.save();
canvas.save();
canvas.save();
canvas.save();
expect(canvas.getSaveCount(), equals(6));
canvas.restoreToCount(2);
expect(canvas.getSaveCount(), equals(2));
canvas.restore();
expect(canvas.getSaveCount(), equals(1));
});
test('RestoreToCount count less than 1, the stack should be reset', () async {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.save();
canvas.save();
canvas.save();
canvas.save();
canvas.save();
expect(canvas.getSaveCount(), equals(6));
canvas.restoreToCount(0);
expect(canvas.getSaveCount(), equals(1));
});
test(
'RestoreToCount count greater than current [getSaveCount], nothing would happend',
() async {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.save();
canvas.save();
canvas.save();
canvas.save();
canvas.save();
expect(canvas.getSaveCount(), equals(6));
canvas.restoreToCount(canvas.getSaveCount() + 1);
expect(canvas.getSaveCount(), equals(6));
});
test('TextDecoration renders non-solid lines', () async {
final File file = File(path.join(_flutterBuildPath, 'flutter',
'third_party', 'txt', 'assets', 'Roboto-Regular.ttf'));
final Uint8List fontData = await file.readAsBytes();
await loadFontFromList(fontData, fontFamily: 'RobotoSlab');
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
for (final (int index, TextDecorationStyle style)
in TextDecorationStyle.values.indexed) {
final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle());
builder.pushStyle(TextStyle(
decoration: TextDecoration.underline,
decorationStyle: style,
decorationThickness: 1.0,
decorationColor: const Color(0xFFFF0000),
fontFamily: 'RobotoSlab',
fontSize: 24.0,
foreground: Paint()..color = const Color(0xFF0000FF),
));
builder.addText(style.name);
final Paragraph paragraph = builder.build();
paragraph.layout(const ParagraphConstraints(width: 1000));
// Draw and layout based on the index vertically.
canvas.drawParagraph(paragraph, Offset(0, index * 40.0));
}
final Picture picture = recorder.endRecording();
final Image image = await picture.toImage(200, 200);
await comparer.addGoldenImage(image, 'text_decoration.png');
});
test('Paint, when copied, has equivalent fields', () {
final Paint paint = Paint()
..color = const Color(0xFF0000FF)
..strokeWidth = 10.0
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..style = PaintingStyle.stroke
..blendMode = BlendMode.srcOver
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 10.0)
..filterQuality = FilterQuality.high
..colorFilter = const ColorFilter.mode(Color(0xFF00FF00), BlendMode.color)
..imageFilter = ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0);
final Paint paintCopy = Paint.from(paint);
expect(paintCopy.color, equals(const Color(0xFF0000FF)));
expect(paintCopy.strokeWidth, equals(10.0));
expect(paintCopy.strokeCap, equals(StrokeCap.round));
expect(paintCopy.strokeJoin, equals(StrokeJoin.round));
expect(paintCopy.style, equals(PaintingStyle.stroke));
expect(paintCopy.blendMode, equals(BlendMode.srcOver));
expect(paintCopy.maskFilter,
equals(const MaskFilter.blur(BlurStyle.normal, 10.0)));
expect(paintCopy.filterQuality, equals(FilterQuality.high));
expect(paintCopy.colorFilter,
equals(const ColorFilter.mode(Color(0xFF00FF00), BlendMode.color)));
expect(paintCopy.imageFilter,
equals(ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0)));
});
test('Paint, when copied, does not mutate the original instance', () {
final Paint paint = Paint()
..color = const Color(0xFF0000FF)
..strokeWidth = 10.0
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..style = PaintingStyle.stroke
..blendMode = BlendMode.srcOver
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 10.0)
..filterQuality = FilterQuality.high
..colorFilter = const ColorFilter.mode(Color(0xFF00FF00), BlendMode.color)
..imageFilter = ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0);
// Make a copy, and change every field of the copy.
Paint.from(paint)
..color = const Color(0xFF00FF00)
..strokeWidth = 20.0
..strokeCap = StrokeCap.butt
..strokeJoin = StrokeJoin.bevel
..style = PaintingStyle.fill
..blendMode = BlendMode.srcIn
..maskFilter = const MaskFilter.blur(BlurStyle.solid, 20.0)
..filterQuality = FilterQuality.none
..colorFilter =
const ColorFilter.mode(Color(0xFFFF0000), BlendMode.modulate)
..imageFilter = ImageFilter.blur(sigmaX: 20.0, sigmaY: 20.0);
// The original paint should not have changed.
expect(paint.color, equals(const Color(0xFF0000FF)));
expect(paint.strokeWidth, equals(10.0));
expect(paint.strokeCap, equals(StrokeCap.round));
expect(paint.strokeJoin, equals(StrokeJoin.round));
expect(paint.style, equals(PaintingStyle.stroke));
expect(paint.blendMode, equals(BlendMode.srcOver));
expect(paint.maskFilter,
equals(const MaskFilter.blur(BlurStyle.normal, 10.0)));
expect(paint.filterQuality, equals(FilterQuality.high));
expect(paint.colorFilter,
equals(const ColorFilter.mode(Color(0xFF00FF00), BlendMode.color)));
expect(paint.imageFilter,
equals(ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0)));
});
test('Paint, when copied, the original changing does not mutate the copy',
() {
final Paint paint = Paint()
..color = const Color(0xFF0000FF)
..strokeWidth = 10.0
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..style = PaintingStyle.stroke
..blendMode = BlendMode.srcOver
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 10.0)
..filterQuality = FilterQuality.high
..colorFilter = const ColorFilter.mode(Color(0xFF00FF00), BlendMode.color)
..imageFilter = ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0);
// Make a copy, and change every field of the original.
final Paint paintCopy = Paint.from(paint);
paint
..color = const Color(0xFF00FF00)
..strokeWidth = 20.0
..strokeCap = StrokeCap.butt
..strokeJoin = StrokeJoin.bevel
..style = PaintingStyle.fill
..blendMode = BlendMode.srcIn
..maskFilter = const MaskFilter.blur(BlurStyle.solid, 20.0)
..filterQuality = FilterQuality.none
..colorFilter =
const ColorFilter.mode(Color(0xFFFF0000), BlendMode.modulate)
..imageFilter = ImageFilter.blur(sigmaX: 20.0, sigmaY: 20.0);
// The copy should not have changed.
expect(paintCopy.color, equals(const Color(0xFF0000FF)));
expect(paintCopy.strokeWidth, equals(10.0));
expect(paintCopy.strokeCap, equals(StrokeCap.round));
expect(paintCopy.strokeJoin, equals(StrokeJoin.round));
expect(paintCopy.style, equals(PaintingStyle.stroke));
expect(paintCopy.blendMode, equals(BlendMode.srcOver));
expect(paintCopy.maskFilter,
equals(const MaskFilter.blur(BlurStyle.normal, 10.0)));
expect(paintCopy.filterQuality, equals(FilterQuality.high));
expect(paintCopy.colorFilter,
equals(const ColorFilter.mode(Color(0xFF00FF00), BlendMode.color)));
expect(paintCopy.imageFilter,
equals(ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0)));
});
test('DrawAtlas correctly copies color values into display list format',
() async {
final Image testImage = await createTestImage();
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
// Make a drawAtlas call that should be solid red.
canvas.drawAtlas(
testImage,
[
RSTransform.fromComponents(
rotation: 0,
scale: 10,
anchorX: 0,
anchorY: 0,
translateX: 0,
translateY: 0,
),
],
[const Rect.fromLTWH(0, 0, 1, 1)],
[const Color.fromARGB(255, 255, 0, 0)],
BlendMode.dst,
null,
Paint(),
);
final Image resultImage = await recorder.endRecording().toImage(1, 1);
final ByteData? data = await resultImage.toByteData();
if (data == null) {
fail('Expected non-null byte data');
}
final int rgba = data.buffer.asUint32List()[0];
expect(rgba, 0xFF0000FF);
});
test('DrawAtlas with no colors does not crash',
() async {
final Image testImage = await createTestImage();
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
// Make a drawAtlas call that should be solid red.
canvas.drawAtlas(
testImage,
[
RSTransform.fromComponents(
rotation: 0,
scale: 10,
anchorX: 0,
anchorY: 0,
translateX: 0,
translateY: 0,
),
],
[const Rect.fromLTWH(0, 0, 1, 1)],
[],
BlendMode.dst,
null,
Paint(),
);
final Image resultImage = await recorder.endRecording().toImage(1, 1);
final ByteData? data = await resultImage.toByteData();
expect(data, isNotNull);
});
Image makeCheckerBoard(int width, int height) {
final recorder = PictureRecorder();
final canvas = Canvas(recorder);
const double left = 0;
final double centerX = width * 0.5;
final double right = width.toDouble();
const double top = 0;
final double centerY = height * 0.5;
final double bottom = height.toDouble();
canvas.drawRect(Rect.fromLTRB(left, top, centerX, centerY),
Paint()..color = const Color.fromARGB(255, 0, 255, 0));
canvas.drawRect(Rect.fromLTRB(centerX, top, right, centerY),
Paint()..color = const Color.fromARGB(255, 255, 255, 0));
canvas.drawRect(Rect.fromLTRB(left, centerY, centerX, bottom),
Paint()..color = const Color.fromARGB(255, 0, 0, 255));
canvas.drawRect(Rect.fromLTRB(centerX, centerY, right, bottom),
Paint()..color = const Color.fromARGB(255, 255, 0, 0));
final picture = recorder.endRecording();
return picture.toImageSync(width, height);
}
Image renderingOpsWithTileMode(TileMode? tileMode) {
final recorder = PictureRecorder();
final canvas = Canvas(recorder);
canvas.drawColor(const Color.fromARGB(255, 224, 224, 224), BlendMode.src);
const Rect zone = Rect.fromLTWH(15, 15, 20, 20);
final Rect arena = zone.inflate(15);
const Rect ovalZone = Rect.fromLTWH(20, 15, 10, 20);
final gradient = Gradient.linear(
zone.topLeft,
zone.bottomRight,
<Color>[
const Color.fromARGB(255, 0, 255, 0),
const Color.fromARGB(255, 0, 0, 255),
],
<double>[0, 1],
);
final filter = ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0, tileMode: tileMode);
final Paint white = Paint()..color = const Color.fromARGB(255, 255, 255, 255);
final Paint grey = Paint()..color = const Color.fromARGB(255, 127, 127, 127);
final Paint unblurredFill = Paint()..shader = gradient;
final Paint blurredFill = Paint.from(unblurredFill)
..imageFilter = filter;
final Paint unblurredStroke = Paint.from(unblurredFill)
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..strokeWidth = 10;
final Paint blurredStroke = Paint.from(unblurredStroke)
..imageFilter = filter;
final Image image = makeCheckerBoard(20, 20);
const Rect imageBounds = Rect.fromLTRB(0, 0, 20, 20);
const Rect imageCenter = Rect.fromLTRB(5, 5, 9, 9);
final points = <Offset>[
zone.topLeft,
zone.topCenter,
zone.topRight,
zone.centerLeft,
zone.center,
zone.centerRight,
zone.bottomLeft,
zone.bottomCenter,
zone.bottomRight,
];
final vertices = Vertices(
VertexMode.triangles,
<Offset> [
zone.topLeft,
zone.bottomRight,
zone.topRight,
zone.topLeft,
zone.bottomRight,
zone.bottomLeft,
],
colors: <Color>[
const Color.fromARGB(255, 0, 255, 0),
const Color.fromARGB(255, 255, 0, 0),
const Color.fromARGB(255, 255, 255, 0),
const Color.fromARGB(255, 0, 255, 0),
const Color.fromARGB(255, 255, 0, 0),
const Color.fromARGB(255, 0, 0, 255),
],
);
final atlasXforms = <RSTransform>[
RSTransform.fromComponents(
rotation: 0.0,
scale: 1.0,
anchorX: 0,
anchorY: 0,
translateX: zone.topLeft.dx,
translateY: zone.topLeft.dy,
),
RSTransform.fromComponents(
rotation: pi / 2,
scale: 1.0,
anchorX: 0,
anchorY: 0,
translateX: zone.topRight.dx,
translateY: zone.topRight.dy,
),
RSTransform.fromComponents(
rotation: pi,
scale: 1.0,
anchorX: 0,
anchorY: 0,
translateX: zone.bottomRight.dx,
translateY: zone.bottomRight.dy,
),
RSTransform.fromComponents(
rotation: pi * 3 / 2,
scale: 1.0,
anchorX: 0,
anchorY: 0,
translateX: zone.bottomLeft.dx,
translateY: zone.bottomLeft.dy,
),
RSTransform.fromComponents(
rotation: pi / 4,
scale: 1.0,
anchorX: 4,
anchorY: 4,
translateX: zone.center.dx,
translateY: zone.center.dy,
),
];
const atlasRects = <Rect>[
Rect.fromLTRB(6, 6, 14, 14),
Rect.fromLTRB(6, 6, 14, 14),
Rect.fromLTRB(6, 6, 14, 14),
Rect.fromLTRB(6, 6, 14, 14),
Rect.fromLTRB(6, 6, 14, 14),
];
const double pad = 10;
final double offset = arena.width + pad;
const int columns = 5;
final Rect pairArena = Rect.fromLTRB(arena.left - 3, arena.top - 3,
arena.right + 3, arena.bottom + offset + 3);
final List<void Function(Canvas canvas, Paint fill, Paint stroke)> renderers = [
(canvas, fill, stroke) {
canvas.saveLayer(zone.inflate(5), fill);
canvas.drawLine(zone.topLeft, zone.bottomRight, unblurredStroke);
canvas.drawLine(zone.topRight, zone.bottomLeft, unblurredStroke);
canvas.restore();
},
(canvas, fill, stroke) => canvas.drawLine(zone.topLeft, zone.bottomRight, stroke),
(canvas, fill, stroke) => canvas.drawRect(zone, fill),
(canvas, fill, stroke) => canvas.drawOval(ovalZone, fill),
(canvas, fill, stroke) => canvas.drawCircle(zone.center, zone.width * 0.5, fill),
(canvas, fill, stroke) => canvas.drawRRect(RRect.fromRectXY(zone, 4.0, 4.0), fill),
(canvas, fill, stroke) => canvas.drawDRRect(RRect.fromRectXY(zone, 4.0, 4.0),
RRect.fromRectXY(zone.deflate(4), 4.0, 4.0),
fill),
(canvas, fill, stroke) => canvas.drawArc(zone, pi / 4, pi * 3 / 2, true, fill),
(canvas, fill, stroke) => canvas.drawPath(Path()
..moveTo(zone.left, zone.top)
..lineTo(zone.right, zone.top)
..lineTo(zone.left, zone.bottom)
..lineTo(zone.right, zone.bottom),
stroke),
(canvas, fill, stroke) => canvas.drawImage(image, zone.topLeft, fill),
(canvas, fill, stroke) => canvas.drawImageRect(image, imageBounds, zone.inflate(2), fill),
(canvas, fill, stroke) => canvas.drawImageNine(image, imageCenter, zone.inflate(2), fill),
(canvas, fill, stroke) => canvas.drawPoints(PointMode.points, points, stroke),
(canvas, fill, stroke) => canvas.drawVertices(vertices, BlendMode.dstOver, fill),
(canvas, fill, stroke) => canvas.drawAtlas(image, atlasXforms, atlasRects,
null, null, null, fill),
];
canvas.save();
canvas.translate(pad, pad);
int renderIndex = 0;
int rows = 0;
while (renderIndex < renderers.length) {
rows += 2;
canvas.save();
for (int col = 0; col < columns && renderIndex < renderers.length; col++) {
final renderer = renderers[renderIndex++];
canvas.drawRect(pairArena, grey);
canvas.drawRect(arena, white);
renderer(canvas, unblurredFill, unblurredStroke);
canvas.save();
canvas.translate(0, offset);
canvas.drawRect(arena, white);
renderer(canvas, blurredFill, blurredStroke);
canvas.restore();
canvas.translate(offset, 0);
}
canvas.restore();
canvas.translate(0, offset * 2);
}
canvas.restore();
final picture = recorder.endRecording();
return picture.toImageSync((offset * columns + pad).round(),
(offset * rows + pad).round());
}
test('Rendering ops with ImageFilter blur with default tile mode', () async {
final image = renderingOpsWithTileMode(null);
await comparer.addGoldenImage(image, 'canvas_test_blurred_rendering_with_default_tile_mode.png');
});
test('Rendering ops with ImageFilter blur with clamp tile mode', () async {
final image = renderingOpsWithTileMode(TileMode.clamp);
await comparer.addGoldenImage(image, 'canvas_test_blurred_rendering_with_clamp_tile_mode.png');
});
test('Rendering ops with ImageFilter blur with mirror tile mode', () async {
final image = renderingOpsWithTileMode(TileMode.mirror);
await comparer.addGoldenImage(image, 'canvas_test_blurred_rendering_with_mirror_tile_mode.png');
});
test('Rendering ops with ImageFilter blur with repeated tile mode', () async {
final image = renderingOpsWithTileMode(TileMode.repeated);
await comparer.addGoldenImage(image, 'canvas_test_blurred_rendering_with_repeated_tile_mode.png');
});
test('Rendering ops with ImageFilter blur with decal tile mode', () async {
final image = renderingOpsWithTileMode(TileMode.decal);
await comparer.addGoldenImage(image, 'canvas_test_blurred_rendering_with_decal_tile_mode.png');
});
}
Future<Image> createTestImage() async {
final PictureRecorder recorder = PictureRecorder();
final Canvas recorderCanvas = Canvas(recorder);
recorderCanvas.scale(1.0, 1.0);
final Picture picture = recorder.endRecording();
return picture.toImage(1, 1);
}
Matcher closeToRect(Rect rect) => _CloseToRectMatcher(rect);
final class _CloseToRectMatcher extends Matcher {
const _CloseToRectMatcher(this._expectedRect);
final Rect _expectedRect;
@override
bool matches(Object? item, Map<Object?, Object?> matchState) {
if (item is! Rect) {
return false;
}
return (item.left - _expectedRect.left).abs() < 1e-6 &&
(item.top - _expectedRect.top).abs() < 1e-6 &&
(item.right - _expectedRect.right).abs() < 1e-6 &&
(item.bottom - _expectedRect.bottom).abs() < 1e-6;
}
@override
Description describe(Description description) {
return description.add('Rect is close (within 1e-6) to $_expectedRect');
}
}
Matcher closeToTransform(Float64List expected) =>
_CloseToTransformMatcher(expected);
final class _CloseToTransformMatcher extends Matcher {
_CloseToTransformMatcher(this._expected);
final Float64List _expected;
@override
bool matches(Object? item, Map<Object?, Object?> matchState) {
if (item is! Float64List) {
return false;
}
if (item.length != 16 || _expected.length != 16) {
return false;
}
for (int i = 0; i < 16; i++) {
if ((item[i] - _expected[i]).abs() > 1e-10) {
return false;
}
}
return true;
}
@override
Description describe(Description description) {
return description.add('Transform is close (within 1e-10) to $_expected');
}
}