diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index ed5ca81f020ab..9abff13f9e5a8 100644
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -52,6 +52,9 @@ FILE: ../../../flutter/flow/layers/elevated_container_layer.cc
 FILE: ../../../flutter/flow/layers/elevated_container_layer.h
 FILE: ../../../flutter/flow/layers/fuchsia_system_composited_layer.cc
 FILE: ../../../flutter/flow/layers/fuchsia_system_composited_layer.h
+FILE: ../../../flutter/flow/layers/image_filter_layer.cc
+FILE: ../../../flutter/flow/layers/image_filter_layer.h
+FILE: ../../../flutter/flow/layers/image_filter_layer_unittests.cc
 FILE: ../../../flutter/flow/layers/layer.cc
 FILE: ../../../flutter/flow/layers/layer.h
 FILE: ../../../flutter/flow/layers/layer_tree.cc
diff --git a/flow/BUILD.gn b/flow/BUILD.gn
index f3ed537fe9113..dad0635fa760a 100644
--- a/flow/BUILD.gn
+++ b/flow/BUILD.gn
@@ -30,6 +30,8 @@ source_set("flow") {
     "layers/container_layer.h",
     "layers/elevated_container_layer.cc",
     "layers/elevated_container_layer.h",
+    "layers/image_filter_layer.cc",
+    "layers/image_filter_layer.h",
     "layers/layer.cc",
     "layers/layer.h",
     "layers/layer_tree.cc",
@@ -139,6 +141,7 @@ executable("flow_unittests") {
     "layers/clip_rrect_layer_unittests.cc",
     "layers/color_filter_layer_unittests.cc",
     "layers/container_layer_unittests.cc",
+    "layers/image_filter_layer_unittests.cc",
     "layers/layer_tree_unittests.cc",
     "layers/opacity_layer_unittests.cc",
     "layers/performance_overlay_layer_unittests.cc",
diff --git a/flow/layers/color_filter_layer_unittests.cc b/flow/layers/color_filter_layer_unittests.cc
index 42a65f255cd1e..af1e5788b6e34 100644
--- a/flow/layers/color_filter_layer_unittests.cc
+++ b/flow/layers/color_filter_layer_unittests.cc
@@ -29,7 +29,7 @@ TEST_F(ColorFilterLayerTest, PaintingEmptyLayerDies) {
                             "needs_painting\\(\\)");
 }
 
-TEST_F(ColorFilterLayerTest, PaintBeforePreollDies) {
+TEST_F(ColorFilterLayerTest, PaintBeforePrerollDies) {
   const SkRect child_bounds = SkRect::MakeLTRB(5.0f, 6.0f, 20.5f, 21.5f);
   const SkPath child_path = SkPath().addRect(child_bounds);
   auto mock_layer = std::make_shared<MockLayer>(child_path);
diff --git a/flow/layers/image_filter_layer.cc b/flow/layers/image_filter_layer.cc
new file mode 100644
index 0000000000000..119fddc181fb8
--- /dev/null
+++ b/flow/layers/image_filter_layer.cc
@@ -0,0 +1,56 @@
+// 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.
+
+#include "flutter/flow/layers/image_filter_layer.h"
+
+namespace flutter {
+
+ImageFilterLayer::ImageFilterLayer(sk_sp<SkImageFilter> filter)
+    : filter_(std::move(filter)) {}
+
+void ImageFilterLayer::Preroll(PrerollContext* context,
+                               const SkMatrix& matrix) {
+  Layer::AutoPrerollSaveLayerState save =
+      Layer::AutoPrerollSaveLayerState::Create(context);
+  ContainerLayer::Preroll(context, matrix);
+
+  if (!context->has_platform_view && context->raster_cache &&
+      SkRect::Intersects(context->cull_rect, paint_bounds())) {
+    SkMatrix ctm = matrix;
+#ifndef SUPPORT_FRACTIONAL_TRANSLATION
+    ctm = RasterCache::GetIntegralTransCTM(ctm);
+#endif
+    context->raster_cache->Prepare(context, this, ctm);
+  }
+}
+
+void ImageFilterLayer::Paint(PaintContext& context) const {
+  TRACE_EVENT0("flutter", "ImageFilterLayer::Paint");
+  FML_DCHECK(needs_painting());
+
+#ifndef SUPPORT_FRACTIONAL_TRANSLATION
+  SkAutoCanvasRestore save(context.leaf_nodes_canvas, true);
+  context.leaf_nodes_canvas->setMatrix(RasterCache::GetIntegralTransCTM(
+      context.leaf_nodes_canvas->getTotalMatrix()));
+#endif
+
+  if (context.raster_cache) {
+    const SkMatrix& ctm = context.leaf_nodes_canvas->getTotalMatrix();
+    RasterCacheResult layer_cache =
+        context.raster_cache->Get((Layer*)this, ctm);
+    if (layer_cache.is_valid()) {
+      layer_cache.draw(*context.leaf_nodes_canvas);
+      return;
+    }
+  }
+
+  SkPaint paint;
+  paint.setImageFilter(filter_);
+
+  Layer::AutoSaveLayer save_layer =
+      Layer::AutoSaveLayer::Create(context, paint_bounds(), &paint);
+  PaintChildren(context);
+}
+
+}  // namespace flutter
diff --git a/flow/layers/image_filter_layer.h b/flow/layers/image_filter_layer.h
new file mode 100644
index 0000000000000..30ec99935ff0a
--- /dev/null
+++ b/flow/layers/image_filter_layer.h
@@ -0,0 +1,30 @@
+// 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.
+
+#ifndef FLUTTER_FLOW_LAYERS_IMAGE_FILTER_LAYER_H_
+#define FLUTTER_FLOW_LAYERS_IMAGE_FILTER_LAYER_H_
+
+#include "flutter/flow/layers/container_layer.h"
+
+#include "third_party/skia/include/core/SkImageFilter.h"
+
+namespace flutter {
+
+class ImageFilterLayer : public ContainerLayer {
+ public:
+  ImageFilterLayer(sk_sp<SkImageFilter> filter);
+
+  void Preroll(PrerollContext* context, const SkMatrix& matrix) override;
+
+  void Paint(PaintContext& context) const override;
+
+ private:
+  sk_sp<SkImageFilter> filter_;
+
+  FML_DISALLOW_COPY_AND_ASSIGN(ImageFilterLayer);
+};
+
+}  // namespace flutter
+
+#endif  // FLUTTER_FLOW_LAYERS_IMAGE_FILTER_LAYER_H_
diff --git a/flow/layers/image_filter_layer_unittests.cc b/flow/layers/image_filter_layer_unittests.cc
new file mode 100644
index 0000000000000..63357fbd89ce8
--- /dev/null
+++ b/flow/layers/image_filter_layer_unittests.cc
@@ -0,0 +1,236 @@
+// 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.
+
+#include "flutter/flow/layers/image_filter_layer.h"
+
+#include "flutter/flow/testing/layer_test.h"
+#include "flutter/flow/testing/mock_layer.h"
+#include "flutter/fml/macros.h"
+#include "flutter/testing/mock_canvas.h"
+#include "third_party/skia/include/core/SkImageFilter.h"
+
+namespace flutter {
+namespace testing {
+
+using ImageFilterLayerTest = LayerTest;
+
+#ifndef NDEBUG
+TEST_F(ImageFilterLayerTest, PaintingEmptyLayerDies) {
+  auto layer = std::make_shared<ImageFilterLayer>(sk_sp<SkImageFilter>());
+
+  layer->Preroll(preroll_context(), SkMatrix());
+  EXPECT_EQ(layer->paint_bounds(), kEmptyRect);
+  EXPECT_FALSE(layer->needs_painting());
+  EXPECT_FALSE(layer->needs_system_composite());
+
+  EXPECT_DEATH_IF_SUPPORTED(layer->Paint(paint_context()),
+                            "needs_painting\\(\\)");
+}
+
+TEST_F(ImageFilterLayerTest, PaintBeforePrerollDies) {
+  const SkRect child_bounds = SkRect::MakeLTRB(5.0f, 6.0f, 20.5f, 21.5f);
+  const SkPath child_path = SkPath().addRect(child_bounds);
+  auto mock_layer = std::make_shared<MockLayer>(child_path);
+  auto layer = std::make_shared<ImageFilterLayer>(sk_sp<SkImageFilter>());
+  layer->Add(mock_layer);
+
+  EXPECT_EQ(layer->paint_bounds(), kEmptyRect);
+  EXPECT_DEATH_IF_SUPPORTED(layer->Paint(paint_context()),
+                            "needs_painting\\(\\)");
+}
+#endif
+
+TEST_F(ImageFilterLayerTest, EmptyFilter) {
+  const SkMatrix initial_transform = SkMatrix::MakeTrans(0.5f, 1.0f);
+  const SkRect child_bounds = SkRect::MakeLTRB(5.0f, 6.0f, 20.5f, 21.5f);
+  const SkPath child_path = SkPath().addRect(child_bounds);
+  const SkPaint child_paint = SkPaint(SkColors::kYellow);
+  auto mock_layer = std::make_shared<MockLayer>(child_path, child_paint);
+  auto layer = std::make_shared<ImageFilterLayer>(nullptr);
+  layer->Add(mock_layer);
+
+  layer->Preroll(preroll_context(), initial_transform);
+  EXPECT_EQ(layer->paint_bounds(), child_bounds);
+  EXPECT_TRUE(layer->needs_painting());
+  EXPECT_EQ(mock_layer->parent_matrix(), initial_transform);
+
+  SkPaint filter_paint;
+  filter_paint.setImageFilter(nullptr);
+  layer->Paint(paint_context());
+  EXPECT_EQ(mock_canvas().draw_calls(),
+            std::vector({
+                MockCanvas::DrawCall{0, MockCanvas::SaveData{1}},
+                MockCanvas::DrawCall{1, MockCanvas::SetMatrixData{SkMatrix()}},
+                MockCanvas::DrawCall{
+                    1, MockCanvas::SaveLayerData{child_bounds, filter_paint,
+                                                 nullptr, 2}},
+                MockCanvas::DrawCall{
+                    2, MockCanvas::DrawPathData{child_path, child_paint}},
+                MockCanvas::DrawCall{2, MockCanvas::RestoreData{1}},
+                MockCanvas::DrawCall{1, MockCanvas::RestoreData{0}},
+            }));
+}
+
+TEST_F(ImageFilterLayerTest, SimpleFilter) {
+  const SkMatrix initial_transform = SkMatrix::MakeTrans(0.5f, 1.0f);
+  const SkRect child_bounds = SkRect::MakeLTRB(5.0f, 6.0f, 20.5f, 21.5f);
+  const SkPath child_path = SkPath().addRect(child_bounds);
+  const SkPaint child_paint = SkPaint(SkColors::kYellow);
+  auto layer_filter = SkImageFilter::MakeMatrixFilter(
+      SkMatrix(), SkFilterQuality::kMedium_SkFilterQuality, nullptr);
+  auto mock_layer = std::make_shared<MockLayer>(child_path, child_paint);
+  auto layer = std::make_shared<ImageFilterLayer>(layer_filter);
+  layer->Add(mock_layer);
+
+  layer->Preroll(preroll_context(), initial_transform);
+  EXPECT_EQ(layer->paint_bounds(), child_bounds);
+  EXPECT_TRUE(layer->needs_painting());
+  EXPECT_EQ(mock_layer->parent_matrix(), initial_transform);
+
+  SkPaint filter_paint;
+  filter_paint.setImageFilter(layer_filter);
+  layer->Paint(paint_context());
+  EXPECT_EQ(mock_canvas().draw_calls(),
+            std::vector({
+                MockCanvas::DrawCall{0, MockCanvas::SaveData{1}},
+                MockCanvas::DrawCall{1, MockCanvas::SetMatrixData{SkMatrix()}},
+                MockCanvas::DrawCall{
+                    1, MockCanvas::SaveLayerData{child_bounds, filter_paint,
+                                                 nullptr, 2}},
+                MockCanvas::DrawCall{
+                    2, MockCanvas::DrawPathData{child_path, child_paint}},
+                MockCanvas::DrawCall{2, MockCanvas::RestoreData{1}},
+                MockCanvas::DrawCall{1, MockCanvas::RestoreData{0}},
+            }));
+}
+
+TEST_F(ImageFilterLayerTest, MultipleChildren) {
+  const SkMatrix initial_transform = SkMatrix::MakeTrans(0.5f, 1.0f);
+  const SkRect child_bounds = SkRect::MakeLTRB(5.0f, 6.0f, 2.5f, 3.5f);
+  const SkPath child_path1 = SkPath().addRect(child_bounds);
+  const SkPath child_path2 =
+      SkPath().addRect(child_bounds.makeOffset(3.0f, 0.0f));
+  const SkPaint child_paint1 = SkPaint(SkColors::kYellow);
+  const SkPaint child_paint2 = SkPaint(SkColors::kCyan);
+  auto layer_filter = SkImageFilter::MakeMatrixFilter(
+      SkMatrix(), SkFilterQuality::kMedium_SkFilterQuality, nullptr);
+  auto mock_layer1 = std::make_shared<MockLayer>(child_path1, child_paint1);
+  auto mock_layer2 = std::make_shared<MockLayer>(child_path2, child_paint2);
+  auto layer = std::make_shared<ImageFilterLayer>(layer_filter);
+  layer->Add(mock_layer1);
+  layer->Add(mock_layer2);
+
+  SkRect children_bounds = child_path1.getBounds();
+  children_bounds.join(child_path2.getBounds());
+  layer->Preroll(preroll_context(), initial_transform);
+  EXPECT_EQ(mock_layer1->paint_bounds(), child_path1.getBounds());
+  EXPECT_EQ(mock_layer2->paint_bounds(), child_path2.getBounds());
+  EXPECT_EQ(layer->paint_bounds(), children_bounds);
+  EXPECT_TRUE(mock_layer1->needs_painting());
+  EXPECT_TRUE(mock_layer2->needs_painting());
+  EXPECT_TRUE(layer->needs_painting());
+  EXPECT_EQ(mock_layer1->parent_matrix(), initial_transform);
+  EXPECT_EQ(mock_layer2->parent_matrix(), initial_transform);
+
+  SkPaint filter_paint;
+  filter_paint.setImageFilter(layer_filter);
+  layer->Paint(paint_context());
+  EXPECT_EQ(mock_canvas().draw_calls(),
+            std::vector(
+                {MockCanvas::DrawCall{0, MockCanvas::SaveData{1}},
+                 MockCanvas::DrawCall{1, MockCanvas::SetMatrixData{SkMatrix()}},
+                 MockCanvas::DrawCall{
+                     1, MockCanvas::SaveLayerData{children_bounds, filter_paint,
+                                                  nullptr, 2}},
+                 MockCanvas::DrawCall{
+                     2, MockCanvas::DrawPathData{child_path1, child_paint1}},
+                 MockCanvas::DrawCall{
+                     2, MockCanvas::DrawPathData{child_path2, child_paint2}},
+                 MockCanvas::DrawCall{2, MockCanvas::RestoreData{1}},
+                 MockCanvas::DrawCall{1, MockCanvas::RestoreData{0}}}));
+}
+
+TEST_F(ImageFilterLayerTest, Nested) {
+  const SkMatrix initial_transform = SkMatrix::MakeTrans(0.5f, 1.0f);
+  const SkRect child_bounds = SkRect::MakeLTRB(5.0f, 6.0f, 2.5f, 3.5f);
+  const SkPath child_path1 = SkPath().addRect(child_bounds);
+  const SkPath child_path2 =
+      SkPath().addRect(child_bounds.makeOffset(3.0f, 0.0f));
+  const SkPaint child_paint1 = SkPaint(SkColors::kYellow);
+  const SkPaint child_paint2 = SkPaint(SkColors::kCyan);
+  auto layer_filter1 = SkImageFilter::MakeMatrixFilter(
+      SkMatrix(), SkFilterQuality::kMedium_SkFilterQuality, nullptr);
+  auto layer_filter2 = SkImageFilter::MakeMatrixFilter(
+      SkMatrix(), SkFilterQuality::kMedium_SkFilterQuality, nullptr);
+  auto mock_layer1 = std::make_shared<MockLayer>(child_path1, child_paint1);
+  auto mock_layer2 = std::make_shared<MockLayer>(child_path2, child_paint2);
+  auto layer1 = std::make_shared<ImageFilterLayer>(layer_filter1);
+  auto layer2 = std::make_shared<ImageFilterLayer>(layer_filter2);
+  layer2->Add(mock_layer2);
+  layer1->Add(mock_layer1);
+  layer1->Add(layer2);
+
+  SkRect children_bounds = child_path1.getBounds();
+  children_bounds.join(child_path2.getBounds());
+  layer1->Preroll(preroll_context(), initial_transform);
+  EXPECT_EQ(mock_layer1->paint_bounds(), child_path1.getBounds());
+  EXPECT_EQ(mock_layer2->paint_bounds(), child_path2.getBounds());
+  EXPECT_EQ(layer1->paint_bounds(), children_bounds);
+  EXPECT_EQ(layer2->paint_bounds(), mock_layer2->paint_bounds());
+  EXPECT_TRUE(mock_layer1->needs_painting());
+  EXPECT_TRUE(mock_layer2->needs_painting());
+  EXPECT_TRUE(layer1->needs_painting());
+  EXPECT_TRUE(layer2->needs_painting());
+  EXPECT_EQ(mock_layer1->parent_matrix(), initial_transform);
+  EXPECT_EQ(mock_layer2->parent_matrix(), initial_transform);
+
+  SkPaint filter_paint1, filter_paint2;
+  filter_paint1.setImageFilter(layer_filter1);
+  filter_paint2.setImageFilter(layer_filter2);
+  layer1->Paint(paint_context());
+  EXPECT_EQ(mock_canvas().draw_calls(),
+            std::vector({
+                MockCanvas::DrawCall{0, MockCanvas::SaveData{1}},
+                MockCanvas::DrawCall{1, MockCanvas::SetMatrixData{SkMatrix()}},
+                MockCanvas::DrawCall{
+                    1, MockCanvas::SaveLayerData{children_bounds, filter_paint1,
+                                                 nullptr, 2}},
+                MockCanvas::DrawCall{
+                    2, MockCanvas::DrawPathData{child_path1, child_paint1}},
+                MockCanvas::DrawCall{2, MockCanvas::SaveData{3}},
+                MockCanvas::DrawCall{3, MockCanvas::SetMatrixData{SkMatrix()}},
+                MockCanvas::DrawCall{
+                    3, MockCanvas::SaveLayerData{child_path2.getBounds(),
+                                                 filter_paint2, nullptr, 4}},
+                MockCanvas::DrawCall{
+                    4, MockCanvas::DrawPathData{child_path2, child_paint2}},
+                MockCanvas::DrawCall{4, MockCanvas::RestoreData{3}},
+                MockCanvas::DrawCall{3, MockCanvas::RestoreData{2}},
+                MockCanvas::DrawCall{2, MockCanvas::RestoreData{1}},
+                MockCanvas::DrawCall{1, MockCanvas::RestoreData{0}},
+            }));
+}
+
+TEST_F(ImageFilterLayerTest, Readback) {
+  auto layer_filter = SkImageFilter::MakeMatrixFilter(
+      SkMatrix(), SkFilterQuality::kMedium_SkFilterQuality, nullptr);
+  auto initial_transform = SkMatrix();
+
+  // ImageFilterLayer does not read from surface
+  auto layer = std::make_shared<ImageFilterLayer>(layer_filter);
+  preroll_context()->surface_needs_readback = false;
+  layer->Preroll(preroll_context(), initial_transform);
+  EXPECT_FALSE(preroll_context()->surface_needs_readback);
+
+  // ImageFilterLayer blocks child with readback
+  auto mock_layer =
+      std::make_shared<MockLayer>(SkPath(), SkPaint(), false, false, true);
+  layer->Add(mock_layer);
+  preroll_context()->surface_needs_readback = false;
+  layer->Preroll(preroll_context(), initial_transform);
+  EXPECT_FALSE(preroll_context()->surface_needs_readback);
+}
+
+}  // namespace testing
+}  // namespace flutter
diff --git a/lib/ui/compositing.dart b/lib/ui/compositing.dart
index f2b36ca95212c..cf73966e06e20 100644
--- a/lib/ui/compositing.dart
+++ b/lib/ui/compositing.dart
@@ -139,6 +139,15 @@ class ColorFilterEngineLayer extends _EngineLayerWrapper {
   ColorFilterEngineLayer._(EngineLayer nativeLayer) : super._(nativeLayer);
 }
 
+/// An opaque handle to an image filter engine layer.
+///
+/// Instances of this class are created by [SceneBuilder.pushImageFilter].
+///
+/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility}
+class ImageFilterEngineLayer extends _EngineLayerWrapper {
+  ImageFilterEngineLayer._(EngineLayer nativeLayer) : super._(nativeLayer);
+}
+
 /// An opaque handle to a backdrop filter engine layer.
 ///
 /// Instances of this class are created by [SceneBuilder.pushBackdropFilter].
@@ -431,6 +440,31 @@ class SceneBuilder extends NativeFieldWrapperClass2 {
 
   EngineLayer _pushColorFilter(_ColorFilter filter) native 'SceneBuilder_pushColorFilter';
 
+  /// Pushes an image filter operation onto the operation stack.
+  ///
+  /// The given filter is applied to the children's rasterization before compositing them into
+  /// the scene.
+  ///
+  /// {@macro dart.ui.sceneBuilder.oldLayer}
+  ///
+  /// {@macro dart.ui.sceneBuilder.oldLayerVsRetained}
+  ///
+  /// See [pop] for details about the operation stack.
+  ImageFilterEngineLayer pushImageFilter(
+    ImageFilter filter, {
+    ImageFilterEngineLayer oldLayer,
+  }) {
+    assert(filter != null);
+    assert(_debugCheckCanBeUsedAsOldLayer(oldLayer, 'pushImageFilter'));
+    final _ImageFilter nativeFilter = filter._toNativeImageFilter();
+    assert(nativeFilter != null);
+    final ImageFilterEngineLayer layer = ImageFilterEngineLayer._(_pushImageFilter(nativeFilter));
+    assert(_debugPushLayer(layer));
+    return layer;
+  }
+
+  EngineLayer _pushImageFilter(_ImageFilter filter) native 'SceneBuilder_pushImageFilter';
+
   /// Pushes a backdrop filter operation onto the operation stack.
   ///
   /// The given filter is applied to the current contents of the scene prior to
diff --git a/lib/ui/compositing/scene_builder.cc b/lib/ui/compositing/scene_builder.cc
index 216e1c9284874..467f187400255 100644
--- a/lib/ui/compositing/scene_builder.cc
+++ b/lib/ui/compositing/scene_builder.cc
@@ -10,6 +10,7 @@
 #include "flutter/flow/layers/clip_rrect_layer.h"
 #include "flutter/flow/layers/color_filter_layer.h"
 #include "flutter/flow/layers/container_layer.h"
+#include "flutter/flow/layers/image_filter_layer.h"
 #include "flutter/flow/layers/layer.h"
 #include "flutter/flow/layers/layer_tree.h"
 #include "flutter/flow/layers/opacity_layer.h"
@@ -49,6 +50,7 @@ IMPLEMENT_WRAPPERTYPEINFO(ui, SceneBuilder);
   V(SceneBuilder, pushClipPath)                     \
   V(SceneBuilder, pushOpacity)                      \
   V(SceneBuilder, pushColorFilter)                  \
+  V(SceneBuilder, pushImageFilter)                  \
   V(SceneBuilder, pushBackdropFilter)               \
   V(SceneBuilder, pushShaderMask)                   \
   V(SceneBuilder, pushPhysicalShape)                \
@@ -152,6 +154,14 @@ fml::RefPtr<EngineLayer> SceneBuilder::pushColorFilter(
   return EngineLayer::MakeRetained(layer);
 }
 
+fml::RefPtr<EngineLayer> SceneBuilder::pushImageFilter(
+    const ImageFilter* image_filter) {
+  auto layer =
+      std::make_shared<flutter::ImageFilterLayer>(image_filter->filter());
+  PushLayer(layer);
+  return EngineLayer::MakeRetained(layer);
+}
+
 fml::RefPtr<EngineLayer> SceneBuilder::pushBackdropFilter(ImageFilter* filter) {
   auto layer = std::make_shared<flutter::BackdropFilterLayer>(filter->filter());
   PushLayer(layer);
diff --git a/lib/ui/compositing/scene_builder.h b/lib/ui/compositing/scene_builder.h
index a634087174e2f..51ae76e651ce0 100644
--- a/lib/ui/compositing/scene_builder.h
+++ b/lib/ui/compositing/scene_builder.h
@@ -50,6 +50,7 @@ class SceneBuilder : public RefCountedDartWrappable<SceneBuilder> {
                                         int clipBehavior);
   fml::RefPtr<EngineLayer> pushOpacity(int alpha, double dx = 0, double dy = 0);
   fml::RefPtr<EngineLayer> pushColorFilter(const ColorFilter* color_filter);
+  fml::RefPtr<EngineLayer> pushImageFilter(const ImageFilter* image_filter);
   fml::RefPtr<EngineLayer> pushBackdropFilter(ImageFilter* filter);
   fml::RefPtr<EngineLayer> pushShaderMask(Shader* shader,
                                           double maskRectLeft,
diff --git a/lib/ui/painting.dart b/lib/ui/painting.dart
index 33d209486f6d1..9c6d3f9df3c95 100644
--- a/lib/ui/painting.dart
+++ b/lib/ui/painting.dart
@@ -2746,8 +2746,11 @@ class _ColorFilter extends NativeFieldWrapperClass2 {
 /// See also:
 ///
 ///  * [BackdropFilter], a widget that applies [ImageFilter] to its rendering.
+///  * [ImageFiltered], a widget that applies [ImageFilter] to its children.
 ///  * [SceneBuilder.pushBackdropFilter], which is the low-level API for using
-///    this class.
+///    this class as a backdrop filter.
+///  * [SceneBuilder.pushImageFilter], which is the low-level API for using
+///    this class as a child layer filter.
 class ImageFilter {
   /// Creates an image filter that applies a Gaussian blur.
   ImageFilter.blur({ double sigmaX = 0.0, double sigmaY = 0.0 })
diff --git a/lib/ui/painting/image_filter.h b/lib/ui/painting/image_filter.h
index f95430f2c5460..c44243016b9cc 100644
--- a/lib/ui/painting/image_filter.h
+++ b/lib/ui/painting/image_filter.h
@@ -26,7 +26,7 @@ class ImageFilter : public RefCountedDartWrappable<ImageFilter> {
   void initBlur(double sigma_x, double sigma_y);
   void initMatrix(const tonic::Float64List& matrix4, int filter_quality);
 
-  const sk_sp<SkImageFilter>& filter() { return filter_; }
+  const sk_sp<SkImageFilter>& filter() const { return filter_; }
 
   static void RegisterNatives(tonic::DartLibraryNatives* natives);
 
diff --git a/lib/web_ui/lib/src/engine/compositor/layer_scene_builder.dart b/lib/web_ui/lib/src/engine/compositor/layer_scene_builder.dart
index ff71900d5fe31..bcef97cc35222 100644
--- a/lib/web_ui/lib/src/engine/compositor/layer_scene_builder.dart
+++ b/lib/web_ui/lib/src/engine/compositor/layer_scene_builder.dart
@@ -143,6 +143,14 @@ class LayerSceneBuilder implements ui.SceneBuilder {
     throw UnimplementedError();
   }
 
+  ui.ImageFilterEngineLayer pushImageFilter(
+    ui.ImageFilter filter, {
+    ui.ImageFilterEngineLayer oldLayer,
+  }) {
+    assert(filter != null);
+    throw UnimplementedError();
+  }
+
   @override
   ui.OffsetEngineLayer pushOffset(
     double dx,
diff --git a/lib/web_ui/lib/src/engine/surface/scene_builder.dart b/lib/web_ui/lib/src/engine/surface/scene_builder.dart
index 52e8d01420a3d..bda572b06145d 100644
--- a/lib/web_ui/lib/src/engine/surface/scene_builder.dart
+++ b/lib/web_ui/lib/src/engine/surface/scene_builder.dart
@@ -173,6 +173,25 @@ class SurfaceSceneBuilder implements ui.SceneBuilder {
     throw UnimplementedError();
   }
 
+  /// Pushes an image filter operation onto the operation stack.
+  ///
+  /// The given filter is applied to the children's rasterization before compositing them into
+  /// the scene.
+  ///
+  /// {@macro dart.ui.sceneBuilder.oldLayer}
+  ///
+  /// {@macro dart.ui.sceneBuilder.oldLayerVsRetained}
+  ///
+  /// See [pop] for details about the operation stack.
+  @override
+  ui.ImageFilterEngineLayer pushImageFilter(
+    ui.ImageFilter filter, {
+    ui.ImageFilterEngineLayer oldLayer,
+  }) {
+    assert(filter != null);
+    throw UnimplementedError();
+  }
+
   /// Pushes a backdrop filter operation onto the operation stack.
   ///
   /// The given filter is applied to the current contents of the scene prior to
diff --git a/lib/web_ui/lib/src/ui/compositing.dart b/lib/web_ui/lib/src/ui/compositing.dart
index d77249eb51775..4574304add353 100644
--- a/lib/web_ui/lib/src/ui/compositing.dart
+++ b/lib/web_ui/lib/src/ui/compositing.dart
@@ -74,6 +74,13 @@ abstract class OpacityEngineLayer implements EngineLayer {}
 /// {@macro dart.ui.sceneBuilder.oldLayerCompatibility}
 abstract class ColorFilterEngineLayer implements EngineLayer {}
 
+/// An opaque handle to an image filter engine layer.
+///
+/// Instances of this class are created by [SceneBuilder.pushImageFilter].
+///
+/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility}
+abstract class ImageFilterEngineLayer implements EngineLayer {}
+
 /// An opaque handle to a backdrop filter engine layer.
 ///
 /// Instances of this class are created by [SceneBuilder.pushBackdropFilter].
@@ -196,6 +203,21 @@ abstract class SceneBuilder {
     ColorFilterEngineLayer oldLayer,
   });
 
+  /// Pushes an image filter operation onto the operation stack.
+  ///
+  /// The given filter is applied to the children's rasterization before compositing them into
+  /// the scene.
+  ///
+  /// {@macro dart.ui.sceneBuilder.oldLayer}
+  ///
+  /// {@macro dart.ui.sceneBuilder.oldLayerVsRetained}
+  ///
+  /// See [pop] for details about the operation stack.
+  ImageFilterEngineLayer pushImageFilter(
+    ImageFilter filter, {
+    ImageFilterEngineLayer oldLayer,
+  });
+
   /// Pushes a backdrop filter operation onto the operation stack.
   ///
   /// The given filter is applied to the current contents of the scene prior to
diff --git a/testing/dart/compositing_test.dart b/testing/dart/compositing_test.dart
index 53812d9be4bfb..42f47c10e504a 100644
--- a/testing/dart/compositing_test.dart
+++ b/testing/dart/compositing_test.dart
@@ -359,6 +359,23 @@ void main() {
         oldLayer: oldLayer,
       );
     });
+    testNoSharing((SceneBuilder builder, EngineLayer oldLayer) {
+      return builder.pushImageFilter(
+        ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
+        oldLayer: oldLayer,
+      );
+    });
+    testNoSharing((SceneBuilder builder, EngineLayer oldLayer) {
+      return builder.pushImageFilter(
+        ImageFilter.matrix(Float64List.fromList(<double>[
+          1, 0, 0, 0,
+          0, 1, 0, 0,
+          0, 0, 1, 0,
+          0, 0, 0, 1,
+        ])),
+        oldLayer: oldLayer,
+      );
+    });
   });
 }