| // 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)); |
| } |
| }; |