| // 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'; | 
 |  | 
 | 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()))); | 
 |   }); | 
 | } | 
 |  | 
 | /// @returns true When the images are reasonably similar. | 
 | /// @todo Make the search actually fuzzy to a certain degree. | 
 | Future<bool> fuzzyCompareImages(Image golden, Image img) async { | 
 |   if (golden.width != img.width || golden.height != img.height) { | 
 |     return false; | 
 |   } | 
 |   int getPixel(ByteData data, int x, int y) => data.getUint32((x + y * golden.width) * 4); | 
 |   final ByteData goldenData = (await golden.toByteData())!; | 
 |   final ByteData imgData = (await img.toByteData())!; | 
 |   for (int y = 0; y < golden.height; y++) { | 
 |     for (int x = 0; x < golden.width; x++) { | 
 |       if (getPixel(goldenData, x, y) != getPixel(imgData, x, y)) { | 
 |         return false; | 
 |       } | 
 |     } | 
 |   } | 
 |   return true; | 
 | } | 
 |  | 
 | Future<void> saveTestImage(Image image, String filename) async { | 
 |   final String imagesPath = path.join('flutter', 'testing', 'resources'); | 
 |   final ByteData pngData = (await image.toByteData(format: ImageByteFormat.png))!; | 
 |   final String outPath = path.join(imagesPath, filename); | 
 |   File(outPath).writeAsBytesSync(pngData.buffer.asUint8List()); | 
 |   print('wrote: $outPath'); | 
 | } | 
 |  | 
 | /// @returns true When the images are reasonably similar. | 
 | Future<bool> fuzzyGoldenImageCompare( | 
 |     Image image, String goldenImageName) async { | 
 |   final String imagesPath = path.join('flutter', 'testing', 'resources'); | 
 |   final File file = File(path.join(imagesPath, goldenImageName)); | 
 |  | 
 |   bool areEqual = false; | 
 |  | 
 |   if (file.existsSync()) { | 
 |     final Uint8List goldenData = await file.readAsBytes(); | 
 |  | 
 |     final Codec codec = await instantiateImageCodec(goldenData); | 
 |     final FrameInfo frame = await codec.getNextFrame(); | 
 |     expect(frame.image.height, equals(image.height)); | 
 |     expect(frame.image.width, equals(image.width)); | 
 |  | 
 |     areEqual = await fuzzyCompareImages(frame.image, image); | 
 |   } | 
 |  | 
 |   if (!areEqual) { | 
 |     saveTestImage(image, 'found_$goldenImageName'); | 
 |   } | 
 |   return areEqual; | 
 | } | 
 |  | 
 | void main() { | 
 |   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)); | 
 |  | 
 |     final bool areEqual = | 
 |         await fuzzyGoldenImageCompare(image, 'canvas_test_toImage.png'); | 
 |     expect(areEqual, true); | 
 |   }); | 
 |  | 
 |   Gradient makeGradient() { | 
 |     return Gradient.linear( | 
 |       Offset.zero, | 
 |       const Offset(100, 100), | 
 |       const <Color>[Color(0xFF4C4D52), Color(0xFF202124)], | 
 |     ); | 
 |   } | 
 |  | 
 |   test('Simple gradient', () async { | 
 |     // TODO(matanl): While deprecated, we still don't want to accidentally | 
 |     // change the behavior of the old API, | 
 |     // https://github.com/flutter/flutter/issues/112498. | 
 |     // ignore: deprecated_member_use | 
 |     Paint.enableDithering = false; | 
 |     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)); | 
 |  | 
 |     final bool areEqual = | 
 |         await fuzzyGoldenImageCompare(image, 'canvas_test_gradient.png'); | 
 |     expect(areEqual, true); | 
 |   }, skip: !Platform.isLinux); // https://github.com/flutter/flutter/issues/53784 | 
 |  | 
 |   test('Simple dithered gradient', () async { | 
 |     // TODO(matanl): Reword this test once we remove the deprecated API. | 
 |     // ignore: deprecated_member_use | 
 |     Paint.enableDithering = true; | 
 |     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)); | 
 |  | 
 |     final bool areEqual = | 
 |         await fuzzyGoldenImageCompare(image, 'canvas_test_dithered_gradient.png'); | 
 |     expect(areEqual, true); | 
 |   }, skip: !Platform.isLinux); // https://github.com/flutter/flutter/issues/53784 | 
 |  | 
 |   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 fuzzyCompareImages(incrementalMatrixImage, combinedMatrixImage); | 
 |  | 
 |     if (!areEqual) { | 
 |       saveTestImage(incrementalMatrixImage, 'incremental_3D_transform_test_image.png'); | 
 |       saveTestImage(combinedMatrixImage, 'combined_3D_transform_test_image.png'); | 
 |     } | 
 |     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)); | 
 |  | 
 |     final bool areEqual = | 
 |         await fuzzyGoldenImageCompare(image, 'dotted_path_effect_mixed_with_stroked_geometry.png'); | 
 |     expect(areEqual, true); | 
 |   }, skip: !Platform.isLinux); // https://github.com/flutter/flutter/issues/53784 | 
 |  | 
 |   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)); | 
 |  | 
 |     final bool areEqual = | 
 |     await fuzzyGoldenImageCompare(image, 'text_with_gradient_with_matrix.png'); | 
 |     expect(areEqual, true); | 
 |   }, skip: !Platform.isLinux); // https://github.com/flutter/flutter/issues/53784 | 
 |  | 
 |   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); | 
 |  | 
 |     // 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('flutter', 'testing', 'resources', 'RobotoSlab-VariableFont_wght.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 fuzzyCompareImages(tabImage, spaceImage); | 
 |     final bool tabToTofuComparison = await fuzzyCompareImages(tabImage, tofuImage); | 
 |  | 
 |     expect(tabToSpaceComparison, isTrue); | 
 |     expect(tabToTofuComparison, isFalse); | 
 |   }); | 
 |  | 
 |   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(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); | 
 |     canvas.clipRect(clipBounds); | 
 |  | 
 |     // 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.clipRect(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); | 
 |     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.save(); | 
 |     canvas.clipRect(const Rect.fromLTRB(0, 0, 15, 15)); | 
 |     // 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.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)); | 
 |   }); | 
 | } | 
 |  | 
 | 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)); | 
 |   } | 
 | }; |