Outsets for referenced SVG filters are always zero
https://bugs.webkit.org/show_bug.cgi?id=202826

Patch by Said Abou-Hallawa <sabouhallawa@apple.com> on 2019-10-14
Reviewed by Simon Fraser.

Source/WebCore:

Sometimes the filter is applied only to a part of the image, so we need
to enlarge the source rectangle such that the border of the result image
looks like as if the filter were applied to the whole image.

The filter outsets should only be calculated for the blur and the drop
shadow filters. The problem is FilterOperations::outsets() was returning
empty outsets always for the referenced filters.

Since the referenced filters must be software filters, the fix is to rely
on CSSFilter in calculating the filter outsets for software filters.

By adding a virtual function called outset() to the FilterEffect class,
we can loop through the CSSFilter::m_effects asking every FilterEffect
to return its outset(). This function is only overridden by FEDropShadow
and FEGaussianBlur.

This should work because RenderLayer builds the CSSFilter when the styleChanged()
happens through its updateFilterPaintingStrategy(). This will guarantee
that m_filters->filter()->outsets() will return the correct outsets for
the referenced and the non-referenced software filters.

We should keep FilterOperations::outsets() because it has be used for
the hardware non-referenced filters. In that case, RenderLayer does not
build the the filter effects. Therefore m_filters can't be used because
it is null in this case.

For accuracy, hasOutsets() implementation is deleted. Having a blur or
drop shadow filter effect is not enough to say hasOutsets() is true. We
need to calculate the outsets first and then ask if it isZero() or not.

Test: css3/filters/svg-blur-filter-clipped.html

* platform/graphics/IntRectExtent.h:
(WebCore::operator==):
(WebCore::operator+=):
(WebCore::IntRectExtent::expandRect const): Deleted.
IntRectExtent is only used as a filter outsets. So add the definition of
IntOutsets in IntRectExtent.h.

* platform/graphics/filters/FEDropShadow.cpp:
(WebCore::FEDropShadow::outsets const):
* platform/graphics/filters/FEDropShadow.h:
* platform/graphics/filters/FEGaussianBlur.cpp:
(WebCore::FEGaussianBlur::calculateOutsetSize):
(WebCore::FEGaussianBlur::outsets const):
* platform/graphics/filters/FEGaussianBlur.h:
* platform/graphics/filters/FilterEffect.h:
(WebCore::FilterEffect::outsets const):
* platform/graphics/filters/FilterOperations.cpp:
(WebCore::FilterOperations::outsets const):
(WebCore::outsetSizeForBlur): Deleted.
(WebCore::FilterOperations::hasOutsets const): Deleted.
* platform/graphics/filters/FilterOperations.h:
(WebCore::FilterOperations::hasOutsets const):
* platform/graphics/texmap/TextureMapperLayer.cpp:
(WebCore::TextureMapperLayer::computeOverlapRegions):

* rendering/CSSFilter.cpp:
(WebCore::CSSFilter::build):
(WebCore::CSSFilter::computeSourceImageRectForDirtyRect):
(WebCore::CSSFilter::outsets const):
Calculate m_outsets once and cache its value for later uses.

* rendering/CSSFilter.h:
* rendering/RenderLayer.cpp:
(WebCore::RenderLayer::setFilterBackendNeedsRepaintingInRect):
(WebCore::RenderLayer::hasAncestorWithFilterOutsets const):
(WebCore::transparencyClipBox):
(WebCore::RenderLayer::calculateClipRects const):
* rendering/RenderLayer.h:
* rendering/style/RenderStyle.h:
(WebCore::RenderStyle::filterOutsets const):
(WebCore::RenderStyle::hasFilterOutsets const): Deleted.

LayoutTests:

* css3/filters/svg-blur-filter-clipped-expected.html: Added.
* css3/filters/svg-blur-filter-clipped.html: Added.
* platform/ios/TestExpectations:

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@251119 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index 4b370a8..5c58c09 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,3 +1,14 @@
+2019-10-14  Said Abou-Hallawa  <sabouhallawa@apple.com>
+
+        Outsets for referenced SVG filters are always zero
+        https://bugs.webkit.org/show_bug.cgi?id=202826
+
+        Reviewed by Simon Fraser.
+
+        * css3/filters/svg-blur-filter-clipped-expected.html: Added.
+        * css3/filters/svg-blur-filter-clipped.html: Added.
+        * platform/ios/TestExpectations:
+
 2019-10-14  Myles C. Maxfield  <mmaxfield@apple.com>
 
         [Mac] Update fast/text/font-cursive-italic-cjk-2.html for Catalina
diff --git a/LayoutTests/css3/filters/svg-blur-filter-clipped-expected.html b/LayoutTests/css3/filters/svg-blur-filter-clipped-expected.html
new file mode 100644
index 0000000..b90cccc
--- /dev/null
+++ b/LayoutTests/css3/filters/svg-blur-filter-clipped-expected.html
@@ -0,0 +1,11 @@
+<style>
+    .container {
+        width: 100px;
+        height: 100px;
+        display: inline-block;
+        background-color: green;
+    }
+</style>
+<body>
+    <div class="container"></div>
+</body>
diff --git a/LayoutTests/css3/filters/svg-blur-filter-clipped.html b/LayoutTests/css3/filters/svg-blur-filter-clipped.html
new file mode 100644
index 0000000..6833115
--- /dev/null
+++ b/LayoutTests/css3/filters/svg-blur-filter-clipped.html
@@ -0,0 +1,27 @@
+<style>
+    .container {
+        width: 100px;
+        height: 100px;
+        overflow: hidden;
+        display: inline-block;
+    }
+    .box {
+        width: 800px;
+        height: 800px;
+        background-color: green;
+    }
+    .gaussian-blur {
+        filter: url(#gaussian-blur);
+        transform: translate(-300px, -300px);
+    }
+</style>
+<body>
+    <div class="container">
+        <div class="box gaussian-blur"></div>
+    </div>
+    <svg height="0">
+        <filter id="gaussian-blur">
+            <fegaussianblur stdDeviation="100">
+        </filter>
+    </svg>
+</body>
diff --git a/LayoutTests/platform/ios/TestExpectations b/LayoutTests/platform/ios/TestExpectations
index 6ed992f..5b98ece 100644
--- a/LayoutTests/platform/ios/TestExpectations
+++ b/LayoutTests/platform/ios/TestExpectations
@@ -3404,3 +3404,5 @@
 
 webkit.org/b/202518 imported/w3c/web-platform-tests/2dcontext/drawing-text-to-the-canvas/2d.text.measure.fontBoundingBox.html [ Failure ]
 webkit.org/b/202518 imported/w3c/web-platform-tests/2dcontext/drawing-text-to-the-canvas/2d.text.measure.emHeights.html [ Failure ]
+
+webkit.org/b/202958 css3/filters/svg-blur-filter-clipped.html [ Failure ]
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index b085e34..381daf2 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,84 @@
+2019-10-14  Said Abou-Hallawa  <sabouhallawa@apple.com>
+
+        Outsets for referenced SVG filters are always zero
+        https://bugs.webkit.org/show_bug.cgi?id=202826
+
+        Reviewed by Simon Fraser.
+
+        Sometimes the filter is applied only to a part of the image, so we need
+        to enlarge the source rectangle such that the border of the result image
+        looks like as if the filter were applied to the whole image.
+
+        The filter outsets should only be calculated for the blur and the drop
+        shadow filters. The problem is FilterOperations::outsets() was returning
+        empty outsets always for the referenced filters.
+
+        Since the referenced filters must be software filters, the fix is to rely
+        on CSSFilter in calculating the filter outsets for software filters.
+
+        By adding a virtual function called outset() to the FilterEffect class, 
+        we can loop through the CSSFilter::m_effects asking every FilterEffect
+        to return its outset(). This function is only overridden by FEDropShadow
+        and FEGaussianBlur.
+
+        This should work because RenderLayer builds the CSSFilter when the styleChanged()
+        happens through its updateFilterPaintingStrategy(). This will guarantee 
+        that m_filters->filter()->outsets() will return the correct outsets for
+        the referenced and the non-referenced software filters.
+
+        We should keep FilterOperations::outsets() because it has be used for
+        the hardware non-referenced filters. In that case, RenderLayer does not
+        build the the filter effects. Therefore m_filters can't be used because
+        it is null in this case.
+
+        For accuracy, hasOutsets() implementation is deleted. Having a blur or
+        drop shadow filter effect is not enough to say hasOutsets() is true. We
+        need to calculate the outsets first and then ask if it isZero() or not.
+
+        Test: css3/filters/svg-blur-filter-clipped.html
+
+        * platform/graphics/IntRectExtent.h:
+        (WebCore::operator==):
+        (WebCore::operator+=):
+        (WebCore::IntRectExtent::expandRect const): Deleted.
+        IntRectExtent is only used as a filter outsets. So add the definition of
+        IntOutsets in IntRectExtent.h.
+
+        * platform/graphics/filters/FEDropShadow.cpp:
+        (WebCore::FEDropShadow::outsets const):
+        * platform/graphics/filters/FEDropShadow.h:
+        * platform/graphics/filters/FEGaussianBlur.cpp:
+        (WebCore::FEGaussianBlur::calculateOutsetSize):
+        (WebCore::FEGaussianBlur::outsets const):
+        * platform/graphics/filters/FEGaussianBlur.h:
+        * platform/graphics/filters/FilterEffect.h:
+        (WebCore::FilterEffect::outsets const):
+        * platform/graphics/filters/FilterOperations.cpp:
+        (WebCore::FilterOperations::outsets const):
+        (WebCore::outsetSizeForBlur): Deleted.
+        (WebCore::FilterOperations::hasOutsets const): Deleted.
+        * platform/graphics/filters/FilterOperations.h:
+        (WebCore::FilterOperations::hasOutsets const):
+        * platform/graphics/texmap/TextureMapperLayer.cpp:
+        (WebCore::TextureMapperLayer::computeOverlapRegions):
+
+        * rendering/CSSFilter.cpp:
+        (WebCore::CSSFilter::build):
+        (WebCore::CSSFilter::computeSourceImageRectForDirtyRect):
+        (WebCore::CSSFilter::outsets const):
+        Calculate m_outsets once and cache its value for later uses.
+
+        * rendering/CSSFilter.h:
+        * rendering/RenderLayer.cpp:
+        (WebCore::RenderLayer::setFilterBackendNeedsRepaintingInRect):
+        (WebCore::RenderLayer::hasAncestorWithFilterOutsets const):
+        (WebCore::transparencyClipBox):
+        (WebCore::RenderLayer::calculateClipRects const):
+        * rendering/RenderLayer.h:
+        * rendering/style/RenderStyle.h:
+        (WebCore::RenderStyle::filterOutsets const):
+        (WebCore::RenderStyle::hasFilterOutsets const): Deleted.
+
 2019-10-14  Wenson Hsieh  <wenson_hsieh@apple.com>
 
         [Clipboard API] Refactor custom pasteboard writing codepaths to handle multiple items
diff --git a/Source/WebCore/platform/graphics/IntRectExtent.h b/Source/WebCore/platform/graphics/IntRectExtent.h
index 6117390..43c832e 100644
--- a/Source/WebCore/platform/graphics/IntRectExtent.h
+++ b/Source/WebCore/platform/graphics/IntRectExtent.h
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2013 Adobe Systems Incorporated. All rights reserved.
+ * Copyright (C) 2019 Apple Inc.  All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
@@ -27,22 +28,13 @@
  * SUCH DAMAGE.
  */
 
-#ifndef IntRectExtent_h
-#define IntRectExtent_h
-
-#include "LayoutRect.h"
+#pragma once
 
 namespace WebCore {
 
 class IntRectExtent {
 public:
-    IntRectExtent()
-        : m_top(0)
-        , m_right(0)
-        , m_bottom(0)
-        , m_left(0)
-    {
-    }
+    IntRectExtent() = default;
 
     IntRectExtent(int top, int right, int bottom, int left)
         : m_top(top)
@@ -64,30 +56,18 @@
     int left() const { return m_left; }
     void setLeft(int left) { m_left = left; }
 
-    void expandRect(LayoutRect& rect) const
-    {
-        if (isZero())
-            return;
-
-        rect.move(-left(), -top());
-        rect.expand(left() + right(), top() + bottom());
-    }
-
-private:
     bool isZero() const { return !left() && !right() && !top() && !bottom(); }
 
-    int m_top;
-    int m_right;
-    int m_bottom;
-    int m_left;
+private:
+    int m_top { 0 };
+    int m_right { 0 };
+    int m_bottom { 0 };
+    int m_left { 0 };
 };
 
 inline bool operator==(const IntRectExtent& a, const IntRectExtent& b)
 {
-    return a.top() == b.top()
-        && a.right() == b.right()
-        && a.bottom() == b.bottom()
-        && a.left() == b.left();
+    return a.top() == b.top() && a.right() == b.right() && a.bottom() == b.bottom() && a.left() == b.left();
 }
 
 inline bool operator!=(const IntRectExtent& a, const IntRectExtent& b)
@@ -95,14 +75,23 @@
     return !(a == b);
 }
 
-inline void operator+=(IntRectExtent& a, const IntRectExtent& b)
+template<typename RectType>
+inline RectType& operator+=(RectType& rect, const IntRectExtent& extent)
+{
+    rect.move(-extent.left(), -extent.top());
+    rect.expand(extent.left() + extent.right(), extent.top() + extent.bottom());
+    return rect;
+}
+
+inline IntRectExtent& operator+=(IntRectExtent& a, const IntRectExtent& b)
 {
     a.setTop(a.top() + b.top());
     a.setRight(a.right() + b.right());
     a.setBottom(a.bottom() + b.bottom());
     a.setLeft(a.left() + b.left());
+    return a;
 }
 
-} // namespace WebCore
+using IntOutsets = IntRectExtent;
 
-#endif // IntRectExtent_h
+} // namespace WebCore
diff --git a/Source/WebCore/platform/graphics/filters/FEDropShadow.cpp b/Source/WebCore/platform/graphics/filters/FEDropShadow.cpp
index a3744fa..889aa2d 100644
--- a/Source/WebCore/platform/graphics/filters/FEDropShadow.cpp
+++ b/Source/WebCore/platform/graphics/filters/FEDropShadow.cpp
@@ -115,6 +115,17 @@
     resultImage->context().drawImageBuffer(*sourceImage, drawingRegion);
 }
 
+IntOutsets FEDropShadow::outsets() const
+{
+    IntSize outsetSize = FEGaussianBlur::calculateOutsetSize({ m_stdX, m_stdY });
+    return {
+        std::max<int>(0, outsetSize.height() - m_dy),
+        std::max<int>(0, outsetSize.width() + m_dx),
+        std::max<int>(0, outsetSize.height() + m_dy),
+        std::max<int>(0, outsetSize.width() - m_dx)
+    };
+}
+
 TextStream& FEDropShadow::externalRepresentation(TextStream& ts, RepresentationType representation) const
 {
     ts << indent <<"[feDropShadow";
diff --git a/Source/WebCore/platform/graphics/filters/FEDropShadow.h b/Source/WebCore/platform/graphics/filters/FEDropShadow.h
index 57a2677..b4e0a20 100644
--- a/Source/WebCore/platform/graphics/filters/FEDropShadow.h
+++ b/Source/WebCore/platform/graphics/filters/FEDropShadow.h
@@ -56,6 +56,8 @@
 
     void determineAbsolutePaintRect() override;
 
+    IntOutsets outsets() const override;
+
     WTF::TextStream& externalRepresentation(WTF::TextStream&, RepresentationType) const override;
 
     float m_stdX;
diff --git a/Source/WebCore/platform/graphics/filters/FEGaussianBlur.cpp b/Source/WebCore/platform/graphics/filters/FEGaussianBlur.cpp
index 1180231..49ba841 100644
--- a/Source/WebCore/platform/graphics/filters/FEGaussianBlur.cpp
+++ b/Source/WebCore/platform/graphics/filters/FEGaussianBlur.cpp
@@ -487,6 +487,14 @@
     return calculateUnscaledKernelSize(filter.scaledByFilterResolution(stdDeviation));
 }
 
+IntSize FEGaussianBlur::calculateOutsetSize(FloatSize stdDeviation)
+{
+    auto kernelSize = calculateUnscaledKernelSize(stdDeviation);
+
+    // We take the half kernel size and multiply it with three, because we run box blur three times.
+    return { 3 * kernelSize.width() / 2, 3 * kernelSize.height() / 2 };
+}
+
 void FEGaussianBlur::determineAbsolutePaintRect()
 {
     IntSize kernelSize = calculateKernelSize(filter(), { m_stdX, m_stdY });
@@ -538,6 +546,12 @@
     platformApply(*resultPixelArray, *tmpImageData, kernelSize.width(), kernelSize.height(), paintSize);
 }
 
+IntOutsets FEGaussianBlur::outsets() const
+{
+    IntSize outsetSize = calculateOutsetSize({ m_stdX, m_stdY });
+    return { outsetSize.height(), outsetSize.width(), outsetSize.height(), outsetSize.width() };
+}
+
 TextStream& FEGaussianBlur::externalRepresentation(TextStream& ts, RepresentationType representation) const
 {
     ts << indent << "[feGaussianBlur";
diff --git a/Source/WebCore/platform/graphics/filters/FEGaussianBlur.h b/Source/WebCore/platform/graphics/filters/FEGaussianBlur.h
index 07efc7a..5fb97ff 100644
--- a/Source/WebCore/platform/graphics/filters/FEGaussianBlur.h
+++ b/Source/WebCore/platform/graphics/filters/FEGaussianBlur.h
@@ -42,6 +42,7 @@
 
     static IntSize calculateKernelSize(const Filter&, FloatSize stdDeviation);
     static IntSize calculateUnscaledKernelSize(FloatSize stdDeviation);
+    static IntSize calculateOutsetSize(FloatSize stdDeviation);
 
 private:
     FEGaussianBlur(Filter&, float, float, EdgeModeType);
@@ -67,6 +68,8 @@
 
     void determineAbsolutePaintRect() override;
 
+    IntOutsets outsets() const override;
+
     WTF::TextStream& externalRepresentation(WTF::TextStream&, RepresentationType) const override;
 
     static void platformApplyWorker(PlatformApplyParameters*);
diff --git a/Source/WebCore/platform/graphics/filters/FilterEffect.h b/Source/WebCore/platform/graphics/filters/FilterEffect.h
index 9b006e3..19f6fb4 100644
--- a/Source/WebCore/platform/graphics/filters/FilterEffect.h
+++ b/Source/WebCore/platform/graphics/filters/FilterEffect.h
@@ -24,6 +24,7 @@
 #include "ColorSpace.h"
 #include "FloatRect.h"
 #include "IntRect.h"
+#include "IntRectExtent.h"
 #include <JavaScriptCore/Uint8ClampedArray.h>
 #include <wtf/MathExtras.h>
 #include <wtf/RefCounted.h>
@@ -103,6 +104,8 @@
 
     virtual FilterEffectType filterEffectType() const { return FilterEffectTypeUnknown; }
 
+    virtual IntOutsets outsets() const { return IntOutsets(); }
+
     enum class RepresentationType { TestOutput, Debugging };
     virtual WTF::TextStream& externalRepresentation(WTF::TextStream&, RepresentationType = RepresentationType::TestOutput) const;
 
diff --git a/Source/WebCore/platform/graphics/filters/FilterOperations.cpp b/Source/WebCore/platform/graphics/filters/FilterOperations.cpp
index 36dfcf7..7f8974c 100644
--- a/Source/WebCore/platform/graphics/filters/FilterOperations.cpp
+++ b/Source/WebCore/platform/graphics/filters/FilterOperations.cpp
@@ -34,17 +34,6 @@
 
 namespace WebCore {
 
-static inline IntSize outsetSizeForBlur(float stdDeviation)
-{
-    auto kernelSize = FEGaussianBlur::calculateUnscaledKernelSize({ stdDeviation, stdDeviation });
-
-    // We take the half kernel size and multiply it with three, because we run box blur three times.
-    return {
-        3 * kernelSize.width() / 2,
-        3 * kernelSize.height() / 2
-    };
-}
-
 bool FilterOperations::operator==(const FilterOperations& other) const
 {
     size_t size = m_operations.size();
@@ -78,33 +67,24 @@
     return false;
 }
 
-bool FilterOperations::hasOutsets() const
+IntOutsets FilterOperations::outsets() const
 {
-    for (auto& operation : m_operations) {
-        auto type = operation->type();
-        if (type == FilterOperation::BLUR || type == FilterOperation::DROP_SHADOW)
-            return true;
-    }
-    return false;
-}
-
-FilterOutsets FilterOperations::outsets() const
-{
-    FilterOutsets totalOutsets;
+    IntOutsets totalOutsets;
     for (auto& operation : m_operations) {
         switch (operation->type()) {
         case FilterOperation::BLUR: {
             auto& blurOperation = downcast<BlurFilterOperation>(*operation);
             float stdDeviation = floatValueForLength(blurOperation.stdDeviation(), 0);
-            IntSize outsetSize = outsetSizeForBlur(stdDeviation);
-            FilterOutsets outsets(outsetSize.height(), outsetSize.width(), outsetSize.height(), outsetSize.width());
+            IntSize outsetSize = FEGaussianBlur::calculateOutsetSize({ stdDeviation, stdDeviation });
+            IntOutsets outsets(outsetSize.height(), outsetSize.width(), outsetSize.height(), outsetSize.width());
             totalOutsets += outsets;
             break;
         }
         case FilterOperation::DROP_SHADOW: {
             auto& dropShadowOperation = downcast<DropShadowFilterOperation>(*operation);
-            IntSize outsetSize = outsetSizeForBlur(dropShadowOperation.stdDeviation());
-            FilterOutsets outsets {
+            float stdDeviation = dropShadowOperation.stdDeviation();
+            IntSize outsetSize = FEGaussianBlur::calculateOutsetSize({ stdDeviation, stdDeviation });
+            IntOutsets outsets {
                 std::max(0, outsetSize.height() - dropShadowOperation.y()),
                 std::max(0, outsetSize.width() + dropShadowOperation.x()),
                 std::max(0, outsetSize.height() + dropShadowOperation.y()),
@@ -113,6 +93,9 @@
             totalOutsets += outsets;
             break;
         }
+        case FilterOperation::REFERENCE:
+            ASSERT_NOT_REACHED();
+            break;
         default:
             break;
         }
diff --git a/Source/WebCore/platform/graphics/filters/FilterOperations.h b/Source/WebCore/platform/graphics/filters/FilterOperations.h
index 930343c..3c77300 100644
--- a/Source/WebCore/platform/graphics/filters/FilterOperations.h
+++ b/Source/WebCore/platform/graphics/filters/FilterOperations.h
@@ -32,8 +32,6 @@
 
 namespace WebCore {
 
-using FilterOutsets = IntRectExtent;
-
 class FilterOperations {
     WTF_MAKE_FAST_ALLOCATED;
 public:
@@ -51,8 +49,8 @@
 
     bool operationsMatch(const FilterOperations&) const;
 
-    bool hasOutsets() const;
-    FilterOutsets outsets() const;
+    bool hasOutsets() const { return !outsets().isZero(); }
+    IntOutsets outsets() const;
 
     bool hasFilterThatAffectsOpacity() const;
     bool hasFilterThatMovesPixels() const;
diff --git a/Source/WebCore/platform/graphics/texmap/TextureMapperLayer.cpp b/Source/WebCore/platform/graphics/texmap/TextureMapperLayer.cpp
index 06f322d..99934b8 100644
--- a/Source/WebCore/platform/graphics/texmap/TextureMapperLayer.cpp
+++ b/Source/WebCore/platform/graphics/texmap/TextureMapperLayer.cpp
@@ -298,7 +298,7 @@
         boundingRect = m_state.contentsRect;
 
     if (m_currentFilters.hasOutsets()) {
-        FilterOutsets outsets = m_currentFilters.outsets();
+        auto outsets = m_currentFilters.outsets();
         IntRect unfilteredTargetRect(boundingRect);
         boundingRect.move(std::max(0, -outsets.left()), std::max(0, -outsets.top()));
         boundingRect.expand(outsets.left() + outsets.right(), outsets.top() + outsets.bottom());
diff --git a/Source/WebCore/rendering/CSSFilter.cpp b/Source/WebCore/rendering/CSSFilter.cpp
index 47981ae..6d595a2 100644
--- a/Source/WebCore/rendering/CSSFilter.cpp
+++ b/Source/WebCore/rendering/CSSFilter.cpp
@@ -133,10 +133,9 @@
 {
     m_hasFilterThatMovesPixels = operations.hasFilterThatMovesPixels();
     m_hasFilterThatShouldBeRestrictedBySecurityOrigin = operations.hasFilterThatShouldBeRestrictedBySecurityOrigin();
-    if (m_hasFilterThatMovesPixels)
-        m_outsets = operations.outsets();
 
     m_effects.clear();
+    m_outsets = { };
 
     RefPtr<FilterEffect> previousEffect = m_sourceGraphic.ptr();
     for (auto& operation : operations.operations()) {
@@ -374,12 +373,8 @@
 {
     // The result of this function is the area in the "filterBoxRect" that needs to be repainted, so that we fully cover the "dirtyRect".
     auto rectForRepaint = dirtyRect;
-    if (hasFilterThatMovesPixels()) {
-        // Note that the outsets are reversed here because we are going backwards -> we have the dirty rect and
-        // need to find out what is the rectangle that might influence the result inside that dirty rect.
-        rectForRepaint.move(-m_outsets.right(), -m_outsets.bottom());
-        rectForRepaint.expand(m_outsets.left() + m_outsets.right(), m_outsets.top() + m_outsets.bottom());
-    }
+    if (hasFilterThatMovesPixels())
+        rectForRepaint += outsets();
     rectForRepaint.intersect(filterBoxRect);
     return rectForRepaint;
 }
@@ -397,7 +392,6 @@
     m_graphicsBufferAttached = false;
 }
 
-
 void CSSFilter::setMaxEffectRects(const FloatRect& effectRect)
 {
     for (auto& effect : m_effects)
@@ -412,4 +406,17 @@
     return lastEffect.requestedRegionOfInputImageData(IntRect { m_filterRegion });
 }
 
+IntOutsets CSSFilter::outsets() const
+{
+    if (!m_hasFilterThatMovesPixels)
+        return { };
+
+    if (!m_outsets.isZero())
+        return m_outsets;
+
+    for (auto& effect : m_effects)
+        m_outsets += effect->outsets();
+    return m_outsets;
+}
+
 } // namespace WebCore
diff --git a/Source/WebCore/rendering/CSSFilter.h b/Source/WebCore/rendering/CSSFilter.h
index be8f7a6..683aa84 100644
--- a/Source/WebCore/rendering/CSSFilter.h
+++ b/Source/WebCore/rendering/CSSFilter.h
@@ -61,6 +61,7 @@
     bool hasFilterThatShouldBeRestrictedBySecurityOrigin() const { return m_hasFilterThatShouldBeRestrictedBySecurityOrigin; }
 
     void determineFilterPrimitiveSubregion();
+    IntOutsets outsets() const;
 
 private:
     CSSFilter();
@@ -93,7 +94,7 @@
     Ref<SourceGraphic> m_sourceGraphic;
     RefPtr<FilterEffect> m_sourceAlpha;
 
-    IntRectExtent m_outsets;
+    mutable IntOutsets m_outsets;
 
     bool m_graphicsBufferAttached { false };
     bool m_hasFilterThatMovesPixels { false };
diff --git a/Source/WebCore/rendering/RenderLayer.cpp b/Source/WebCore/rendering/RenderLayer.cpp
index 65b3f33..39d8a51 100644
--- a/Source/WebCore/rendering/RenderLayer.cpp
+++ b/Source/WebCore/rendering/RenderLayer.cpp
@@ -1929,7 +1929,7 @@
         return;
     
     LayoutRect rectForRepaint = rect;
-    renderer().style().filterOutsets().expandRect(rectForRepaint);
+    rectForRepaint += filterOutsets();
 
     m_filters->expandDirtySourceRect(rectForRepaint);
     
@@ -1964,7 +1964,7 @@
 bool RenderLayer::hasAncestorWithFilterOutsets() const
 {
     for (const RenderLayer* curr = this; curr; curr = curr->parent()) {
-        if (curr->renderer().style().hasFilterOutsets())
+        if (curr->hasFilterOutsets())
             return true;
     }
     return false;
@@ -2087,7 +2087,7 @@
         // paints unfragmented.
         LayoutRect clipRect = layer.boundingBox(&layer);
         expandClipRectForDescendantsAndReflection(clipRect, layer, &layer, transparencyBehavior, paintBehavior);
-        layer.renderer().style().filterOutsets().expandRect(clipRect);
+        clipRect += layer.filterOutsets();
         LayoutRect result = transform.mapRect(clipRect);
         if (!paginationLayer)
             return result;
@@ -2103,7 +2103,7 @@
     
     LayoutRect clipRect = layer.boundingBox(rootLayer, layer.offsetFromAncestor(rootLayer), transparencyBehavior == HitTestingTransparencyClipBox ? RenderLayer::UseFragmentBoxesIncludingCompositing : RenderLayer::UseFragmentBoxesExcludingCompositing);
     expandClipRectForDescendantsAndReflection(clipRect, layer, rootLayer, transparencyBehavior, paintBehavior);
-    layer.renderer().style().filterOutsets().expandRect(clipRect);
+    clipRect += layer.filterOutsets();
 
     return clipRect;
 }
@@ -6114,7 +6114,7 @@
         computeLayersUnion(*childLayer);
 
     if (flags.contains(IncludeFilterOutsets) || (flags.contains(IncludePaintedFilterOutsets) && paintsWithFilters()))
-        renderer().style().filterOutsets().expandRect(unionBounds);
+        unionBounds += filterOutsets();
 
     if ((flags & IncludeSelfTransform) && paintsWithTransform(PaintBehavior::Normal)) {
         TransformationMatrix* affineTrans = transform();
@@ -6837,6 +6837,13 @@
     renderer().repaint();
 }
 
+IntOutsets RenderLayer::filterOutsets() const
+{
+    if (m_filters)
+        return m_filters->filter() ? m_filters->filter()->outsets() : IntOutsets();
+    return renderer().style().filterOutsets();
+}
+
 bool RenderLayer::isTransparentRespectingParentFrames() const
 {
     static const double minimumVisibleOpacity = 0.01;
diff --git a/Source/WebCore/rendering/RenderLayer.h b/Source/WebCore/rendering/RenderLayer.h
index cf4289b..a97c3f9 100644
--- a/Source/WebCore/rendering/RenderLayer.h
+++ b/Source/WebCore/rendering/RenderLayer.h
@@ -790,6 +790,8 @@
 
     void filterNeedsRepaint();
     bool hasFilter() const { return renderer().hasFilter(); }
+    bool hasFilterOutsets() const { return !filterOutsets().isZero(); }
+    IntOutsets filterOutsets() const;
     bool hasBackdropFilter() const
     {
 #if ENABLE(FILTERS_LEVEL_2)
diff --git a/Source/WebCore/rendering/style/RenderStyle.h b/Source/WebCore/rendering/style/RenderStyle.h
index 45c832b..42ddd50 100644
--- a/Source/WebCore/rendering/style/RenderStyle.h
+++ b/Source/WebCore/rendering/style/RenderStyle.h
@@ -229,8 +229,7 @@
 
     LayoutBoxExtent maskBoxImageOutsets() const { return imageOutsets(maskBoxImage()); }
 
-    bool hasFilterOutsets() const { return hasFilter() && filter().hasOutsets(); }
-    FilterOutsets filterOutsets() const { return hasFilter() ? filter().outsets() : FilterOutsets(); }
+    IntOutsets filterOutsets() const { return hasFilter() ? filter().outsets() : IntOutsets(); }
 
     Order rtlOrdering() const { return static_cast<Order>(m_inheritedFlags.rtlOrdering); }
     void setRTLOrdering(Order ordering) { m_inheritedFlags.rtlOrdering = static_cast<unsigned>(ordering); }