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