blob: fe957e3340f12dcfd39aa361ecd1bfe6eaba8a74 [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:litetest/litetest.dart';
import 'package:path/path.dart' as path;
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);
expectAssertion(() => canvas.drawAtlas(image, <RSTransform>[transform], <Rect>[rect], <Color>[color], null, rect, paint));
});
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);
expectArgumentError(() => canvas.drawAtlas(image, <RSTransform>[transform], <Rect>[], <Color>[color], BlendMode.src, rect, paint));
expectArgumentError(() => canvas.drawAtlas(image, <RSTransform>[], <Rect>[rect], <Color>[color], BlendMode.src, rect, paint));
expectArgumentError(() => canvas.drawAtlas(image, <RSTransform>[transform], <Rect>[rect], <Color>[color, color], BlendMode.src, rect, paint));
expectArgumentError(() => canvas.drawAtlas(image, <RSTransform>[transform], <Rect>[rect, rect], <Color>[color], BlendMode.src, rect, paint));
expectArgumentError(() => canvas.drawAtlas(image, <RSTransform>[transform, transform], <Rect>[rect], <Color>[color], BlendMode.src, rect, paint));
expectArgumentError(() => canvas.drawRawAtlas(image, Float32List(3), Float32List(3), null, null, rect, paint));
expectArgumentError(() => canvas.drawRawAtlas(image, Float32List(4), Float32List(0), null, null, rect, paint));
expectArgumentError(() => canvas.drawRawAtlas(image, Float32List(0), Float32List(4), null, null, rect, paint));
expectArgumentError(() => canvas.drawRawAtlas(image, Float32List(4), Float32List(4), Int32List(2), BlendMode.src, rect, paint));
});
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, listEquals(dataSync));
});
test('Canvas.drawParagraph throws when Paragraph.layout was not called', () async {
// Regression test for https://github.com/flutter/flutter/issues/97172
bool assertsEnabled = false;
assert(() {
assertsEnabled = true;
return true;
}());
Object? error;
try {
await 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);
} catch (e) {
error = e;
}
if (assertsEnabled) {
expect(error, isNotNull);
} else {
expect(error, isNull);
}
});
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');
});
Matcher closeToTransform(Float64List expected) => (dynamic v) {
Expect.type<Float64List>(v);
final Float64List value = v as Float64List;
expect(expected.length, equals(16));
expect(value.length, equals(16));
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
final double vActual = value[r*4 + c];
final double vExpected = expected[r*4 + c];
if ((vActual - vExpected).abs() > 1e-10) {
Expect.fail('matrix mismatch at $r, $c, $vActual not close to $vExpected');
}
}
}
};
Matcher notCloseToTransform(Float64List expected) => (dynamic v) {
Expect.type<Float64List>(v);
final Float64List value = v as Float64List;
expect(expected.length, equals(16));
expect(value.length, equals(16));
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
final double vActual = value[r*4 + c];
final double vExpected = expected[r*4 + c];
if ((vActual - vExpected).abs() > 1e-10) {
return;
}
}
}
Expect.fail('$value is too close to $expected');
};
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, notCloseToTransform(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, notCloseToTransform(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, notCloseToTransform(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, notCloseToTransform(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, notCloseToTransform(matrix));
expect(curMatrix, closeToTransform(matrix));
});
Matcher closeToRect(Rect expected) => (dynamic v) {
Expect.type<Rect>(v);
final Rect value = v as Rect;
expect(value.left, closeTo(expected.left, 1e-6));
expect(value.top, closeTo(expected.top, 1e-6));
expect(value.right, closeTo(expected.right, 1e-6));
expect(value.bottom, closeTo(expected.bottom, 1e-6));
};
Matcher notCloseToRect(Rect expected) => (dynamic v) {
Expect.type<Rect>(v);
final Rect value = v as Rect;
if ((value.left - expected.left).abs() > 1e-6 ||
(value.top - expected.top).abs() > 1e-6 ||
(value.right - expected.right).abs() > 1e-6 ||
(value.bottom - expected.bottom).abs() > 1e-6) {
return;
}
Expect.fail('$value is too close to $expected');
};
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(), notCloseToRect(clipExpandedBounds));
expect(canvas.getDestinationClipBounds(), notCloseToRect(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(), notCloseToRect(clipExpandedBounds));
expect(canvas.getDestinationClipBounds(), notCloseToRect(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(), notCloseToRect(clipBounds));
expect(canvas.getDestinationClipBounds(), notCloseToRect(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(), notCloseToRect(clipExpandedBounds));
expect(canvas.getDestinationClipBounds(), notCloseToRect(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(), notCloseToRect(clipBounds));
expect(canvas.getDestinationClipBounds(), notCloseToRect(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');
});
}
Matcher listEquals(ByteData expected) => (dynamic v) {
Expect.type<ByteData>(v);
final ByteData value = v as ByteData;
expect(value.lengthInBytes, expected.lengthInBytes);
for (int i = 0; i < value.lengthInBytes; i++) {
expect(value.getUint8(i), expected.getUint8(i));
}
};