[skif] Take dst bounds into account for layer fills in FilterResult

When the image fully covers what needs to be drawn, pixels that would
be non-transparent due to a color filter or non-decal tile mode don't
matter. Previously, the fills_layer_bounds function was just checking
state and would always block compacting operations, unless the layer
bounds also hid the CF/tiling effects.

Bug: skia:9296
Change-Id: I02b8b585dc723de6d9ad99fce11a21e5348601e4
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/717297
Reviewed-by: Robert Phillips <robertphillips@google.com>
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
diff --git a/src/core/SkImageFilterTypes.cpp b/src/core/SkImageFilterTypes.cpp
index 9443c3e..d1575aa 100644
--- a/src/core/SkImageFilterTypes.cpp
+++ b/src/core/SkImageFilterTypes.cpp
@@ -207,10 +207,6 @@
 
 #endif
 
-bool fills_layer_bounds(const SkColorFilter* colorFilter) {
-    return colorFilter && as_CFB(colorFilter)->affectsTransparentBlack();
-}
-
 // AutoSurface manages an SkSpecialSurface and canvas state to draw to a layer-space bounding box,
 // and then snap it into a FilterResult. It provides operators to be used directly as a canvas,
 // assuming surface creation succeeded. Usage:
@@ -545,11 +541,33 @@
     return image;
 }
 
+
+bool FilterResult::modifiesPixelsBeyondImage(const LayerSpace<SkIRect>& dstBounds) const {
+    // If there is no transparency-affecting color filter and it's just decal tiling, it doesn't
+    // matter how the image geometry overlaps with the dst bounds.
+    if (!(fColorFilter && as_CFB(fColorFilter)->affectsTransparentBlack())) {
+        // TODO: add "&& fTileMode == SkTileMode::kDecal) {""
+        return false;
+    }
+
+    // If the base image completely covers the render bounds then the effects of tiling won't be
+    // visible and it doesn't matter if any color filter affects transparent black.
+    if (SkRectPriv::QuadContainsRect(SkMatrix(fTransform),
+                                     SkIRect::MakeSize(fImage->dimensions()),
+                                     SkIRect(dstBounds))) {
+        return false;
+    }
+
+    // Otherwise tiling or transparency-affecting color filters will modify the pixels beyond
+    // the image bounds that are still within render bounds.
+    return true;
+}
+
 bool FilterResult::isCropped(const LayerSpace<SkMatrix>& xtraTransform,
                              const LayerSpace<SkIRect>& dstBounds) const {
     // Tiling and color-filtering can completely fill 'fLayerBounds' in which case its edge is
     // a transition from possibly non-transparent to definitely transparent color.
-    bool fillsLayerBounds = fills_layer_bounds(fColorFilter.get());
+    bool fillsLayerBounds = this->modifiesPixelsBeyondImage(dstBounds);
     if (!fillsLayerBounds) {
         // When that's not the case, 'fLayerBounds' may still be important if it crops the
         // edges of the original transformed image itself.
@@ -589,7 +607,7 @@
     }
 
     LayerSpace<SkIPoint> origin;
-    if (!fills_layer_bounds(fColorFilter.get()) &&
+    if (!this->modifiesPixelsBeyondImage(tightBounds) &&
          is_nearly_integer_translation(fTransform, &origin)) {
         // We can lift the crop to earlier in the order of operations and apply it to the image
         // subset directly. This does not rely on resolve() to call extract_subset() because it
@@ -808,12 +826,12 @@
     SkSurfaceProps props = {};
     AutoSurface surface{ctx, dstBounds, /*renderInParameterSpace=*/false, &props};
     if (surface) {
-        this->draw(surface.canvas());
+        this->draw(surface.canvas(), dstBounds);
     }
     return surface.snap();
 }
 
-void FilterResult::draw(SkCanvas* canvas) const {
+void FilterResult::draw(SkCanvas* canvas, const LayerSpace<SkIRect>& dstBounds) const {
     if (!fImage) {
         return;
     }
@@ -838,7 +856,7 @@
         sampling = {};
     }
 
-    if (fills_layer_bounds(fColorFilter.get())) {
+    if (this->modifiesPixelsBeyondImage(dstBounds)) {
 #ifdef SK_ENABLE_SKSL
         // apply_decal consumes the transform, so we don't modify the canvas
         paint.setShader(apply_decal(fTransform, fImage, fLayerBounds, sampling));
@@ -1096,7 +1114,7 @@
                      input.fSampling == kDefaultSampling &&
                      input.fFlags == ShaderFlags::kNone);
             surface->save();
-            input.fImage.draw(surface.canvas());
+            input.fImage.draw(surface.canvas(), outputBounds);
             surface->restore();
         }
     }
diff --git a/src/core/SkImageFilterTypes.h b/src/core/SkImageFilterTypes.h
index 52367fd..d2d4b58 100644
--- a/src/core/SkImageFilterTypes.h
+++ b/src/core/SkImageFilterTypes.h
@@ -767,6 +767,11 @@
     std::pair<sk_sp<SkSpecialImage>, LayerSpace<SkIPoint>>
     resolve(const Context& ctx, LayerSpace<SkIRect> dstBounds) const;
 
+    // Returns true if tiling and color filtering affect pixels outside of the image's bounds that
+    // are within the layer bounds (limited to 'dstBounds'). This does not consider the layer bounds
+    // which are considered separately in isCropped().
+    bool modifiesPixelsBeyondImage(const LayerSpace<SkIRect>& dstBounds) const;
+
     // Returns true if the effects of the fLayerBounds crop are visible when this image is drawn
     // with 'xtraTransform' restricted to 'dstBounds'.
     bool isCropped(const LayerSpace<SkMatrix>& xtraTransform,
@@ -774,7 +779,7 @@
 
     // Draw directly to the canvas, which draws the same image as produced by resolve() but can be
     // useful if multiple operations need to be performed on the canvas.
-    void draw(SkCanvas* canvas) const;
+    void draw(SkCanvas* canvas, const LayerSpace<SkIRect>& dstBounds) const;
 
     // Returns the FilterResult as a shader, ideally without resolving to an axis-aligned image.
     // 'xtraSampling' is the sampling that any parent shader applies to the FilterResult.
diff --git a/tests/FilterResultTest.cpp b/tests/FilterResultTest.cpp
index ec4254a..4fb3e40 100644
--- a/tests/FilterResultTest.cpp
+++ b/tests/FilterResultTest.cpp
@@ -126,7 +126,7 @@
                 canvas->drawPaint(paint);
             } else {
                 SkASSERT(fMethod == Method::kDrawToCanvas);
-                image.draw(canvas);
+                image.draw(canvas, ctx.desiredOutput());
             }
 
             return {surface->makeImageSnapshot(), SkIPoint(ctx.desiredOutput().topLeft())};