Add back screenshot extension that was temporarily disabled. (#21828)

Add back screenshot extension that was temporarily disabled.

Add matchesReferenceImage matcher to test that the screenshot extension
generates equivalent images to InspectorService.instance.screenshot.
diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart
index 09683a8..c9a06d9 100644
--- a/packages/flutter/lib/src/widgets/widget_inspector.dart
+++ b/packages/flutter/lib/src/widgets/widget_inspector.dart
@@ -13,6 +13,7 @@
         window,
         ClipOp,
         Image,
+        ImageByteFormat,
         Paragraph,
         Picture,
         PictureRecorder,
@@ -1026,6 +1027,36 @@
       name: 'isWidgetCreationTracked',
       callback: isWidgetCreationTracked,
     );
+    assert(() {
+      registerServiceExtension(
+        name: 'screenshot',
+        callback: (Map<String, String> parameters) async {
+          assert(parameters.containsKey('id'));
+          assert(parameters.containsKey('width'));
+          assert(parameters.containsKey('height'));
+
+          final ui.Image image = await screenshot(
+            toObject(parameters['id']),
+            width: double.parse(parameters['width']),
+            height: double.parse(parameters['height']),
+            margin: parameters.containsKey('margin') ?
+                double.parse(parameters['margin']) : 0.0,
+            maxPixelRatio: parameters.containsKey('maxPixelRatio') ?
+                double.parse(parameters['maxPixelRatio']) : 1.0,
+            debugPaint: parameters['debugPaint'] == 'true',
+          );
+          if (image == null) {
+            return <String, Object>{'result': null};
+          }
+          final ByteData byteData = await image.toByteData(format:ui.ImageByteFormat.png);
+
+          return <String, Object>{
+            'result': base64.encoder.convert(Uint8List.view(byteData.buffer)),
+          };
+        },
+      );
+      return true;
+    }());
   }
 
   /// Clear all InspectorService object references.
diff --git a/packages/flutter/test/foundation/service_extensions_test.dart b/packages/flutter/test/foundation/service_extensions_test.dart
index 1166e44..6fcb04f 100644
--- a/packages/flutter/test/foundation/service_extensions_test.dart
+++ b/packages/flutter/test/foundation/service_extensions_test.dart
@@ -512,7 +512,7 @@
 
     // If you add a service extension... TEST IT! :-)
     // ...then increment this number.
-    expect(binding.extensions.length, 37);
+    expect(binding.extensions.length, 38);
 
     expect(console, isEmpty);
     debugPrint = debugPrintThrottled;
diff --git a/packages/flutter/test/widgets/widget_inspector_test.dart b/packages/flutter/test/widgets/widget_inspector_test.dart
index 1816a30..e695a16 100644
--- a/packages/flutter/test/widgets/widget_inspector_test.dart
+++ b/packages/flutter/test/widgets/widget_inspector_test.dart
@@ -5,7 +5,7 @@
 import 'dart:async';
 import 'dart:convert';
 import 'dart:io' show Platform;
-import 'dart:ui' as ui show PictureRecorder;
+import 'dart:ui' as ui;
 
 import 'package:flutter/material.dart';
 import 'package:flutter/rendering.dart';
@@ -1545,18 +1545,48 @@
         matchesGoldenFile('inspector.clipRect_debugPaint.png'),
       );
 
+      final Element clipRect = find.byType(ClipRRect).evaluate().single;
+
+      final Future<ui.Image> clipRectScreenshot = service.screenshot(
+        clipRect,
+        width: 100.0,
+        height: 100.0,
+        margin: 20.0,
+        debugPaint: true,
+      );
       // Add a margin so that the clip icon shows up in the screenshot.
       // This golden image is platform dependent due to the clip icon.
       await expectLater(
-        service.screenshot(
-          find.byType(ClipRRect).evaluate().single,
-          width: 100.0,
-          height: 100.0,
-          margin: 20.0,
-          debugPaint: true,
-        ),
+        clipRectScreenshot,
         matchesGoldenFile('inspector.clipRect_debugPaint_margin.png'),
-        skip: !Platform.isLinux
+        skip: !Platform.isLinux,
+      );
+
+      // Verify we get the same image if we go through the service extension
+      // instead of invoking the screenshot method directly.
+      final Future<Object> base64ScreenshotFuture = service.testExtension(
+        'screenshot',
+        <String, String>{
+          'id': service.toId(clipRect, 'group'),
+          'width': '100.0',
+          'height': '100.0',
+          'margin': '20.0',
+          'debugPaint': 'true',
+        },
+      );
+
+      final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding
+          .ensureInitialized();
+      final ui.Image screenshotImage = await binding.runAsync<ui.Image>(() async {
+        final String base64Screenshot = await base64ScreenshotFuture;
+        final ui.Codec codec = await ui.instantiateImageCodec(base64.decode(base64Screenshot));
+        final ui.FrameInfo frame = await codec.getNextFrame();
+        return frame.image;
+      }, additionalTime: const Duration(seconds: 11));
+
+      await expectLater(
+        screenshotImage,
+        matchesReferenceImage(await clipRectScreenshot),
       );
 
       // Test with a very visible debug paint
diff --git a/packages/flutter_test/lib/src/matchers.dart b/packages/flutter_test/lib/src/matchers.dart
index 231cce3..c30759a 100644
--- a/packages/flutter_test/lib/src/matchers.dart
+++ b/packages/flutter_test/lib/src/matchers.dart
@@ -4,6 +4,7 @@
 
 import 'dart:async';
 import 'dart:math' as math;
+import 'dart:typed_data';
 import 'dart:ui' as ui;
 import 'dart:ui';
 
@@ -275,9 +276,11 @@
 /// See also:
 ///
 ///  * [goldenFileComparator], which acts as the backend for this matcher.
+///  * [matchesReferenceImage], which should be used instead if you want to
+///    verify that two different code paths create identical images.
 ///  * [flutter_test] for a discussion of test configurations, whereby callers
 ///    may swap out the backend for this matcher.
-Matcher matchesGoldenFile(dynamic key) {
+AsyncMatcher matchesGoldenFile(dynamic key) {
   if (key is Uri) {
     return _MatchesGoldenFile(key);
   } else if (key is String) {
@@ -286,6 +289,42 @@
   throw ArgumentError('Unexpected type for golden file: ${key.runtimeType}');
 }
 
+/// Asserts that a [Finder], [Future<ui.Image>], or [ui.Image] matches a
+/// reference image identified by [image].
+///
+/// For the case of a [Finder], the [Finder] must match exactly one widget and
+/// the rendered image of the first [RepaintBoundary] ancestor of the widget is
+/// treated as the image for the widget.
+///
+/// This is an asynchronous matcher, meaning that callers should use
+/// [expectLater] when using this matcher and await the future returned by
+/// [expectLater].
+///
+/// ## Sample code
+///
+/// ```dart
+/// final ui.Paint paint = ui.Paint()
+///   ..style = ui.PaintingStyle.stroke
+///   ..strokeWidth = 1.0;
+/// final ui.PictureRecorder recorder = ui.PictureRecorder();
+/// final ui.Canvas pictureCanvas = ui.Canvas(recorder);
+/// pictureCanvas.drawCircle(Offset.zero, 20.0, paint);
+/// final ui.Picture picture = recorder.endRecording();
+/// ui.Image referenceImage = picture.toImage(50, 50);
+///
+/// await expectLater(find.text('Save'), matchesReferenceImage(referenceImage));
+/// await expectLater(image, matchesReferenceImage(referenceImage);
+/// await expectLater(imageFuture, matchesReferenceImage(referenceImage));
+/// ```
+///
+/// See also:
+///
+///  * [matchesGoldenFile], which should be used instead if you need to verify
+///    that a [Finder] or [ui.Image] matches a golden image.
+AsyncMatcher matchesReferenceImage(ui.Image image) {
+  return _MatchesReferenceImage(image);
+}
+
 /// Asserts that a [SemanticsData] contains the specified information.
 ///
 /// If either the label, hint, value, textDirection, or rect fields are not
@@ -1513,6 +1552,76 @@
   return layer.toImage(renderObject.paintBounds);
 }
 
+int _countDifferentPixels(Uint8List imageA, Uint8List imageB) {
+  assert(imageA.length == imageB.length);
+  int delta = 0;
+  for (int i = 0; i < imageA.length; i+=4) {
+    if (imageA[i] != imageB[i] ||
+      imageA[i+1] != imageB[i+1] ||
+      imageA[i+2] != imageB[i+2] ||
+      imageA[i+3] != imageB[i+3]) {
+      delta++;
+    }
+  }
+  return delta;
+}
+
+class _MatchesReferenceImage extends AsyncMatcher {
+  const _MatchesReferenceImage(this.referenceImage);
+
+  final ui.Image referenceImage;
+
+  @override
+  Future<String> matchAsync(dynamic item) async {
+    Future<ui.Image> imageFuture;
+    if (item is Future<ui.Image>) {
+      imageFuture = item;
+    } else if (item is ui.Image) {
+      imageFuture = Future<ui.Image>.value(item);
+    } else {
+      final Finder finder = item;
+      final Iterable<Element> elements = finder.evaluate();
+      if (elements.isEmpty) {
+        return 'could not be rendered because no widget was found';
+      } else if (elements.length > 1) {
+        return 'matched too many widgets';
+      }
+      imageFuture = _captureImage(elements.single);
+    }
+
+    final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
+    return binding.runAsync<String>(() async {
+      final ui.Image image = await imageFuture;
+      final ByteData bytes = await image.toByteData()
+        .timeout(const Duration(seconds: 10), onTimeout: () => null);
+      if (bytes == null) {
+        return 'Failed to generate an image from engine within the 10,000ms timeout.';
+      }
+
+      final ByteData referenceBytes = await referenceImage.toByteData()
+        .timeout(const Duration(seconds: 10), onTimeout: () => null);
+      if (referenceBytes == null) {
+        return 'Failed to generate an image from engine within the 10,000ms timeout.';
+      }
+
+      if (referenceImage.height != image.height || referenceImage.width != image.width) {
+        return 'does not match as width or height do not match. $image != $referenceImage';
+      }
+
+      final int countDifferentPixels = _countDifferentPixels(
+        Uint8List.view(bytes.buffer),
+        Uint8List.view(referenceBytes.buffer),
+      );
+      return countDifferentPixels == 0 ? null : 'does not match on $countDifferentPixels pixels';
+    }, additionalTime: const Duration(seconds: 21));
+  }
+
+  @override
+  Description describe(Description description) {
+    return description.add('rasterized image matches that of a $referenceImage reference image');
+  }
+}
+
 class _MatchesGoldenFile extends AsyncMatcher {
   const _MatchesGoldenFile(this.key);
 
diff --git a/packages/flutter_test/test/reference_image_test.dart b/packages/flutter_test/test/reference_image_test.dart
new file mode 100644
index 0000000..5e97180
--- /dev/null
+++ b/packages/flutter_test/test/reference_image_test.dart
@@ -0,0 +1,68 @@
+// Copyright 2018 The Chromium 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:ui' as ui;
+
+import 'package:flutter_test/flutter_test.dart';
+
+ui.Image createTestImage(int width, int height, ui.Color color) {
+  final ui.Paint paint = ui.Paint()
+    ..style = ui.PaintingStyle.stroke
+    ..strokeWidth = 1.0
+    ..color = color;
+  final ui.PictureRecorder recorder = ui.PictureRecorder();
+  final ui.Canvas pictureCanvas = ui.Canvas(recorder);
+  pictureCanvas.drawCircle(Offset.zero, 20.0, paint);
+  final ui.Picture picture = recorder.endRecording();
+  return picture.toImage(width, height);
+}
+
+void main() {
+  const ui.Color red = ui.Color.fromARGB(255, 255, 0, 0);
+  const ui.Color green = ui.Color.fromARGB(255, 0, 255, 0);
+  const ui.Color transparentRed = ui.Color.fromARGB(128, 255, 0, 0);
+
+  group('succeeds', () {
+    testWidgets('when images have the same content', (WidgetTester tester) async {
+      await expectLater(
+        createTestImage(100, 100, red),
+        matchesReferenceImage(createTestImage(100, 100, red)),
+      );
+      await expectLater(
+        createTestImage(100, 100, green),
+        matchesReferenceImage(createTestImage(100, 100, green)),
+      );
+
+      await expectLater(
+        createTestImage(100, 100, transparentRed),
+        matchesReferenceImage(createTestImage(100, 100, transparentRed)),
+      );
+    });
+
+    testWidgets('when images are identical', (WidgetTester tester) async {
+      final ui.Image image = createTestImage(100, 100, red);
+      await expectLater(image, matchesReferenceImage(image));
+    });
+  });
+
+  group('fails', () {
+    testWidgets('when image sizes do not match', (WidgetTester tester) async {
+      expect(
+        await matchesReferenceImage(createTestImage(50, 50, red)).matchAsync(createTestImage(100, 100, red)),
+        equals('does not match as width or height do not match. [100×100] != [50×50]'),
+      );
+    });
+
+    testWidgets('when image pixels do not match', (WidgetTester tester) async {
+      expect(
+        await matchesReferenceImage(createTestImage(100, 100, red)).matchAsync(createTestImage(100, 100, transparentRed)),
+        equals('does not match on 53 pixels'),
+      );
+      expect(
+        await matchesReferenceImage(createTestImage(100, 100, red)).matchAsync(createTestImage(100, 100, green)),
+        equals('does not match on 57 pixels'),
+      );
+    });
+  });
+}