diff --git a/src/ImageSharp.Drawing/Processing/Extensions/ClipPathExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/ClipPathExtensions.cs
index dea056b1..10ac9ba3 100644
--- a/src/ImageSharp.Drawing/Processing/Extensions/ClipPathExtensions.cs
+++ b/src/ImageSharp.Drawing/Processing/Extensions/ClipPathExtensions.cs
@@ -11,11 +11,16 @@ namespace SixLabors.ImageSharp.Drawing.Processing;
public static class ClipPathExtensions
{
///
- /// Applies the processing operation within the provided region defined by an .
+ /// Applies the processing operation within the region defined by an .
///
/// The source image processing context.
- /// The defining the region to operation within.
- /// The operation to perform.
+ ///
+ /// The defining the clip region. Only pixels inside the clip are affected.
+ ///
+ ///
+ /// The operation to perform. This executes in the clipped context so results are constrained to the
+ /// clip bounds.
+ ///
/// The to allow chaining of operations.
public static IImageProcessingContext Clip(
this IImageProcessingContext source,
diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClipPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClipPathProcessor{TPixel}.cs
index 65aecbaf..5e4089c6 100644
--- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClipPathProcessor{TPixel}.cs
+++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClipPathProcessor{TPixel}.cs
@@ -6,8 +6,8 @@
namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing;
///
-/// The main workhorse class. This has access to the pixel buffer but
-/// in an abstract/generic way.
+/// Applies a processing operation to a clipped path region by constraining the operation's input domain
+/// to the bounds of the path, then using the processed result as an image brush to fill the path.
///
/// The type of pixel.
internal class ClipPathProcessor : IImageProcessor
@@ -32,34 +32,41 @@ public void Dispose()
public void Execute()
{
- // Clone out our source image so we can apply various effects to it without mutating
- // the original yet.
- using Image clone = this.source.Clone(this.definition.Operation);
+ // Bounds in drawing are floating point. We must conservatively cover the entire shape bounds.
+ RectangleF boundsF = this.definition.Region.Bounds;
- // Use an image brush to apply cloned image as the source for filling the shape.
- // We pass explicit bounds to avoid the need to crop the clone;
- RectangleF bounds = this.definition.Region.Bounds;
+ int left = (int)MathF.Floor(boundsF.Left);
+ int top = (int)MathF.Floor(boundsF.Top);
+ int right = (int)MathF.Ceiling(boundsF.Right);
+ int bottom = (int)MathF.Ceiling(boundsF.Bottom);
- // add some clamping offsets to the brush to account for the target drawing location due to the cloned image not fill the image as expected
- int offsetX = 0;
- int offsetY = 0;
- if (bounds.X < 0)
- {
- offsetX = -(int)MathF.Floor(bounds.X);
- }
+ Rectangle crop = Rectangle.FromLTRB(left, top, right, bottom);
- if (bounds.Y < 0)
+ // Constrain the operation to the intersection of the requested bounds and source region.
+ Rectangle clipped = Rectangle.Intersect(this.sourceRectangle, crop);
+
+ if (clipped.Width <= 0 || clipped.Height <= 0)
{
- offsetY = -(int)MathF.Floor(bounds.Y);
+ return;
}
- ImageBrush brush = new(clone, bounds, new Point(offsetX, offsetY));
+ Action operation = this.definition.Operation;
- // Grab hold of an image processor that can fill paths with a brush to allow it to do the hard pixel pushing for us
- FillPathProcessor processor = new(this.definition.Options, brush, this.definition.Region);
- using IImageProcessor p = processor.CreatePixelSpecificProcessor(this.configuration, this.source, this.sourceRectangle);
+ // Run the operation on the clipped context so only pixels inside the clip are affected,
+ // matching the expected semantics of clipping in other graphics APIs.
+ using Image clone = this.source.Clone(ctx => operation(ctx.Crop(clipped)));
- // Fill the shape using the image brush
- p.Execute();
+ // Use the clone as a brush source so only the clipped result contributes to the fill,
+ // keeping the effect confined to the clipped region.
+ Point brushOffset = new(
+ clipped.X - (int)MathF.Floor(boundsF.Left),
+ clipped.Y - (int)MathF.Floor(boundsF.Top));
+
+ ImageBrush brush = new(clone, clone.Bounds, brushOffset);
+
+ // Fill the shape using the image brush.
+ FillPathProcessor processor = new(this.definition.Options, brush, this.definition.Region);
+ using IImageProcessor pixelProcessor = processor.CreatePixelSpecificProcessor(this.configuration, this.source, this.sourceRectangle);
+ pixelProcessor.Execute();
}
}
diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/ClipTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/ClipTests.cs
index 55b66494..c8892b87 100644
--- a/tests/ImageSharp.Drawing.Tests/Drawing/ClipTests.cs
+++ b/tests/ImageSharp.Drawing.Tests/Drawing/ClipTests.cs
@@ -36,6 +36,21 @@ public void Clip(TestImageProvider provider, float dx, float dy,
appendSourceFileOrDescription: false);
}
+ [Theory]
+ [WithFile(TestImages.Png.Ducky, PixelTypes.Rgba32)]
+ public void Clip_ConstrainsOperationToClipBounds(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ => provider.RunValidatingProcessorTest(
+ x =>
+ {
+ Size size = x.GetCurrentSize();
+ RectangleF rect = new(0, 0, size.Width / 2, size.Height / 2);
+ RectangularPolygon clipRect = new(rect);
+ x.Clip(clipRect, ctx => ctx.Flip(FlipMode.Vertical));
+ },
+ appendPixelTypeToFileName: false,
+ appendSourceFileOrDescription: false);
+
[Fact]
public void Issue250_Vertical_Horizontal_Count_Should_Match()
{
diff --git a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_ConstrainsOperationToClipBounds.png b/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_ConstrainsOperationToClipBounds.png
new file mode 100644
index 00000000..969d80f9
--- /dev/null
+++ b/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_ConstrainsOperationToClipBounds.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:790a9e156bee55ddb3d40dd743eafa2a4b0129c43618fea3e99ffd875bd1d551
+size 39092