compensate for DPR in canvas blur (#18484)

* compensate for DPR in canvas blur
diff --git a/lib/web_ui/dev/goldens_lock.yaml b/lib/web_ui/dev/goldens_lock.yaml
index 906f324..36047fa 100644
--- a/lib/web_ui/dev/goldens_lock.yaml
+++ b/lib/web_ui/dev/goldens_lock.yaml
@@ -1,2 +1,2 @@
 repository: https://github.com/flutter/goldens.git
-revision: 04c9d19f106cf0b1bf409bf691bd5334950553ea
+revision: f80e019e225915e220f0f37884c8cd1b942d9bc2
diff --git a/lib/web_ui/lib/src/engine/bitmap_canvas.dart b/lib/web_ui/lib/src/engine/bitmap_canvas.dart
index 831307f..0f94c43 100644
--- a/lib/web_ui/lib/src/engine/bitmap_canvas.dart
+++ b/lib/web_ui/lib/src/engine/bitmap_canvas.dart
@@ -973,7 +973,9 @@
     'WebKit (Safari) does not support `filter` canvas property.',
   );
   if (maskFilter != null) {
-    return 'blur(${maskFilter.webOnlySigma}px)';
+    // Multiply by device-pixel ratio because the canvas' pixel width and height
+    // are larger than its CSS width and height by device-pixel ratio.
+    return 'blur(${maskFilter.webOnlySigma * window.devicePixelRatio}px)';
   } else {
     return 'none';
   }
diff --git a/lib/web_ui/lib/src/engine/surface/painting.dart b/lib/web_ui/lib/src/engine/surface/painting.dart
index 5a14d93..cce4564 100644
--- a/lib/web_ui/lib/src/engine/surface/painting.dart
+++ b/lib/web_ui/lib/src/engine/surface/painting.dart
@@ -246,4 +246,45 @@
       ..strokeJoin = strokeJoin
       ..strokeCap = strokeCap;
   }
+
+  @override
+  String toString() {
+    if (!assertionsEnabled) {
+      return super.toString();
+    } else {
+      final StringBuffer buffer = StringBuffer('SurfacePaintData(');
+      if (blendMode != null) {
+        buffer.write('blendMode = $blendMode; ');
+      }
+      if (style != null) {
+        buffer.write('style = $style; ');
+      }
+      if (strokeWidth != null) {
+        buffer.write('strokeWidth = $strokeWidth; ');
+      }
+      if (strokeCap != null) {
+        buffer.write('strokeCap = $strokeCap; ');
+      }
+      if (strokeJoin != null) {
+        buffer.write('strokeJoin = $strokeJoin; ');
+      }
+      if (color != null) {
+        buffer.write('color = ${colorToCssString(color)}; ');
+      }
+      if (shader != null) {
+        buffer.write('shader = $shader; ');
+      }
+      if (maskFilter != null) {
+        buffer.write('maskFilter = $maskFilter; ');
+      }
+      if (filterQuality != null) {
+        buffer.write('filterQuality = $filterQuality; ');
+      }
+      if (colorFilter != null) {
+        buffer.write('colorFilter = $colorFilter; ');
+      }
+      buffer.write('isAntiAlias = $isAntiAlias)');
+      return buffer.toString();
+    }
+  }
 }
diff --git a/lib/web_ui/test/golden_tests/engine/canvas_mask_filter_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_mask_filter_test.dart
index 77b86bf..a808a57 100644
--- a/lib/web_ui/test/golden_tests/engine/canvas_mask_filter_test.dart
+++ b/lib/web_ui/test/golden_tests/engine/canvas_mask_filter_test.dart
@@ -7,7 +7,7 @@
 import 'dart:math' as math;
 import 'dart:typed_data';
 
-import 'package:ui/ui.dart' hide TextStyle;
+import 'package:ui/ui.dart' as ui;
 import 'package:ui/src/engine.dart';
 import 'package:test/test.dart';
 
@@ -15,7 +15,7 @@
 
 void main() async {
   // Commit a recording canvas to a bitmap, and compare with the expected
-  Future<void> _checkScreenshot(RecordingCanvas rc, String fileName, Rect screenRect) async {
+  Future<void> _checkScreenshot(RecordingCanvas rc, String fileName, ui.Rect screenRect, {bool write = false}) async {
     final EngineCanvas engineCanvas = BitmapCanvas(screenRect);
 
     rc.endRecording();
@@ -26,7 +26,7 @@
     try {
       sceneElement.append(engineCanvas.rootElement);
       html.document.body.append(sceneElement);
-      await matchGoldenFile('$fileName.png', region: screenRect, maxDiffRatePercent: 0.0);
+      await matchGoldenFile('$fileName.png', region: screenRect, maxDiffRatePercent: 0.0, write: write);
     } finally {
       // The page is reused across tests, so remove the element after taking the
       // Scuba screenshot.
@@ -35,10 +35,10 @@
   }
 
   setUp(() async {
-    debugEmulateFlutterTesterEnvironment = true;
-    await webOnlyInitializePlatform();
-    webOnlyFontCollection.debugRegisterTestFonts();
-    await webOnlyFontCollection.ensureFontsLoaded();
+    ui.debugEmulateFlutterTesterEnvironment = true;
+    await ui.webOnlyInitializePlatform();
+    ui.webOnlyFontCollection.debugRegisterTestFonts();
+    await ui.webOnlyFontCollection.ensureFontsLoaded();
   });
 
   tearDown(() {
@@ -52,37 +52,37 @@
     test('renders MaskFilter.blur in $browser', () async {
       const double screenWidth = 800.0;
       const double screenHeight = 150.0;
-      const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight);
+      const ui.Rect screenRect = ui.Rect.fromLTWH(0, 0, screenWidth, screenHeight);
 
       ContextStateHandle.debugEmulateWebKitMaskFilter = isSafariMode;
       final RecordingCanvas rc = RecordingCanvas(screenRect);
       rc.translate(0, 75);
 
       final SurfacePaint paint = SurfacePaint()
-          ..maskFilter = MaskFilter.blur(BlurStyle.normal, 5);
+          ..maskFilter = ui.MaskFilter.blur(ui.BlurStyle.normal, 5);
 
       rc.translate(50, 0);
       rc.drawRect(
-        Rect.fromCircle(center: Offset.zero, radius: 30),
+        ui.Rect.fromCircle(center: ui.Offset.zero, radius: 30),
         paint,
       );
 
       rc.translate(100, 0);
-      paint.color = Color(0xFF00FF00);
+      paint.color = ui.Color(0xFF00FF00);
       rc.drawRRect(
-        RRect.fromRectAndRadius(
-          Rect.fromCircle(center: Offset.zero, radius: 30),
-          Radius.circular(20),
+        ui.RRect.fromRectAndRadius(
+          ui.Rect.fromCircle(center: ui.Offset.zero, radius: 30),
+          ui.Radius.circular(20),
         ),
         paint,
       );
 
       rc.translate(100, 0);
-      paint.color = Color(0xFF0000FF);
-      rc.drawCircle(Offset.zero, 30, paint);
+      paint.color = ui.Color(0xFF0000FF);
+      rc.drawCircle(ui.Offset.zero, 30, paint);
 
       rc.translate(100, 0);
-      paint.color = Color(0xFF00FFFF);
+      paint.color = ui.Color(0xFF00FFFF);
       rc.drawPath(
         SurfacePath()
           ..moveTo(-20, 0)
@@ -94,39 +94,39 @@
       );
 
       rc.translate(100, 0);
-      paint.color = Color(0xFFFF00FF);
+      paint.color = ui.Color(0xFFFF00FF);
       rc.drawOval(
-        Rect.fromCenter(center: Offset.zero, width: 40, height: 100),
+        ui.Rect.fromCenter(center: ui.Offset.zero, width: 40, height: 100),
         paint,
       );
 
       rc.translate(100, 0);
-      paint.color = Color(0xFF888800);
+      paint.color = ui.Color(0xFF888800);
       paint.strokeWidth = 5;
       rc.drawLine(
-        Offset(-20, -50),
-        Offset(20, 50),
+        ui.Offset(-20, -50),
+        ui.Offset(20, 50),
         paint,
       );
 
       rc.translate(100, 0);
-      paint.color = Color(0xFF888888);
+      paint.color = ui.Color(0xFF888888);
       rc.drawDRRect(
-        RRect.fromRectAndRadius(
-          Rect.fromCircle(center: Offset.zero, radius: 35),
-          Radius.circular(20),
+        ui.RRect.fromRectAndRadius(
+          ui.Rect.fromCircle(center: ui.Offset.zero, radius: 35),
+          ui.Radius.circular(20),
         ),
-        RRect.fromRectAndRadius(
-          Rect.fromCircle(center: Offset.zero, radius: 15),
-          Radius.circular(7),
+        ui.RRect.fromRectAndRadius(
+          ui.Rect.fromCircle(center: ui.Offset.zero, radius: 15),
+          ui.Radius.circular(7),
         ),
         paint,
       );
 
       rc.translate(100, 0);
-      paint.color = Color(0xFF6500C9);
+      paint.color = ui.Color(0xFF6500C9);
       rc.drawRawPoints(
-        PointMode.points,
+        ui.PointMode.points,
         Float32List.fromList([-10, -10, -10, 10, 10, -10, 10, 10]),
         paint,
       );
@@ -137,31 +137,31 @@
     test('renders transformed MaskFilter.blur in $browser', () async {
       const double screenWidth = 300.0;
       const double screenHeight = 300.0;
-      const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight);
+      const ui.Rect screenRect = ui.Rect.fromLTWH(0, 0, screenWidth, screenHeight);
 
       ContextStateHandle.debugEmulateWebKitMaskFilter = isSafariMode;
       final RecordingCanvas rc = RecordingCanvas(screenRect);
       rc.translate(150, 150);
 
       final SurfacePaint paint = SurfacePaint()
-          ..maskFilter = MaskFilter.blur(BlurStyle.normal, 5);
+          ..maskFilter = ui.MaskFilter.blur(ui.BlurStyle.normal, 5);
 
-      final List<Color> colors = <Color>[
-        Color(0xFF000000),
-        Color(0xFF00FF00),
-        Color(0xFF0000FF),
-        Color(0xFF00FFFF),
-        Color(0xFFFF00FF),
-        Color(0xFF888800),
-        Color(0xFF888888),
-        Color(0xFF6500C9),
+      final List<ui.Color> colors = <ui.Color>[
+        ui.Color(0xFF000000),
+        ui.Color(0xFF00FF00),
+        ui.Color(0xFF0000FF),
+        ui.Color(0xFF00FFFF),
+        ui.Color(0xFFFF00FF),
+        ui.Color(0xFF888800),
+        ui.Color(0xFF888888),
+        ui.Color(0xFF6500C9),
       ];
 
-      for (Color color in colors) {
+      for (ui.Color color in colors) {
         paint.color = color;
         rc.rotate(math.pi / 4);
         rc.drawRect(
-          Rect.fromCircle(center: const Offset(90, 0), radius: 20),
+          ui.Rect.fromCircle(center: const ui.Offset(90, 0), radius: 20),
           paint,
         );
       }
@@ -172,4 +172,26 @@
 
   testMaskFilterBlur(isSafariMode: false);
   testMaskFilterBlur(isSafariMode: true);
+
+  for (int testDpr in <int>[1, 2, 4]) {
+    test('MaskFilter.blur blurs correctly for device-pixel ratio $testDpr', () async {
+      window.debugOverrideDevicePixelRatio(testDpr.toDouble());
+      const ui.Rect screenRect = ui.Rect.fromLTWH(0, 0, 150, 150);
+
+      final RecordingCanvas rc = RecordingCanvas(screenRect);
+      rc.translate(0, 75);
+
+      final SurfacePaint paint = SurfacePaint()
+          ..maskFilter = ui.MaskFilter.blur(ui.BlurStyle.normal, 5);
+
+      rc.translate(75, 0);
+      rc.drawRect(
+        ui.Rect.fromCircle(center: ui.Offset.zero, radius: 30),
+        paint,
+      );
+
+      await _checkScreenshot(rc, 'mask_filter_blur_dpr_$testDpr', screenRect);
+      window.debugOverrideDevicePixelRatio(1.0);
+    });
+  }
 }