Skip to content

Commit e7aa1b6

Browse files
Merge pull request #376 from SixLabors/js/clip-path-enhancements
Constrain Image processing operations to clip bounds.
2 parents 46b1688 + a7eaa1b commit e7aa1b6

File tree

4 files changed

+56
-26
lines changed

4 files changed

+56
-26
lines changed

src/ImageSharp.Drawing/Processing/Extensions/ClipPathExtensions.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,16 @@ namespace SixLabors.ImageSharp.Drawing.Processing;
1111
public static class ClipPathExtensions
1212
{
1313
/// <summary>
14-
/// Applies the processing operation within the provided region defined by an <see cref="IPath"/>.
14+
/// Applies the processing operation within the region defined by an <see cref="IPath"/>.
1515
/// </summary>
1616
/// <param name="source">The source image processing context.</param>
17-
/// <param name="region">The <see cref="IPath"/> defining the region to operation within.</param>
18-
/// <param name="operation">The operation to perform.</param>
17+
/// <param name="region">
18+
/// The <see cref="IPath"/> defining the clip region. Only pixels inside the clip are affected.
19+
/// </param>
20+
/// <param name="operation">
21+
/// The operation to perform. This executes in the clipped context so results are constrained to the
22+
/// clip bounds.
23+
/// </param>
1924
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
2025
public static IImageProcessingContext Clip(
2126
this IImageProcessingContext source,

src/ImageSharp.Drawing/Processing/Processors/Drawing/ClipPathProcessor{TPixel}.cs

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing;
77

88
/// <summary>
9-
/// The main workhorse class. This has access to the pixel buffer but
10-
/// in an abstract/generic way.
9+
/// Applies a processing operation to a clipped path region by constraining the operation's input domain
10+
/// to the bounds of the path, then using the processed result as an image brush to fill the path.
1111
/// </summary>
1212
/// <typeparam name="TPixel">The type of pixel.</typeparam>
1313
internal class ClipPathProcessor<TPixel> : IImageProcessor<TPixel>
@@ -32,34 +32,41 @@ public void Dispose()
3232

3333
public void Execute()
3434
{
35-
// Clone out our source image so we can apply various effects to it without mutating
36-
// the original yet.
37-
using Image<TPixel> clone = this.source.Clone(this.definition.Operation);
35+
// Bounds in drawing are floating point. We must conservatively cover the entire shape bounds.
36+
RectangleF boundsF = this.definition.Region.Bounds;
3837

39-
// Use an image brush to apply cloned image as the source for filling the shape.
40-
// We pass explicit bounds to avoid the need to crop the clone;
41-
RectangleF bounds = this.definition.Region.Bounds;
38+
int left = (int)MathF.Floor(boundsF.Left);
39+
int top = (int)MathF.Floor(boundsF.Top);
40+
int right = (int)MathF.Ceiling(boundsF.Right);
41+
int bottom = (int)MathF.Ceiling(boundsF.Bottom);
4242

43-
// 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
44-
int offsetX = 0;
45-
int offsetY = 0;
46-
if (bounds.X < 0)
47-
{
48-
offsetX = -(int)MathF.Floor(bounds.X);
49-
}
43+
Rectangle crop = Rectangle.FromLTRB(left, top, right, bottom);
5044

51-
if (bounds.Y < 0)
45+
// Constrain the operation to the intersection of the requested bounds and source region.
46+
Rectangle clipped = Rectangle.Intersect(this.sourceRectangle, crop);
47+
48+
if (clipped.Width <= 0 || clipped.Height <= 0)
5249
{
53-
offsetY = -(int)MathF.Floor(bounds.Y);
50+
return;
5451
}
5552

56-
ImageBrush brush = new(clone, bounds, new Point(offsetX, offsetY));
53+
Action<IImageProcessingContext> operation = this.definition.Operation;
5754

58-
// Grab hold of an image processor that can fill paths with a brush to allow it to do the hard pixel pushing for us
59-
FillPathProcessor processor = new(this.definition.Options, brush, this.definition.Region);
60-
using IImageProcessor<TPixel> p = processor.CreatePixelSpecificProcessor(this.configuration, this.source, this.sourceRectangle);
55+
// Run the operation on the clipped context so only pixels inside the clip are affected,
56+
// matching the expected semantics of clipping in other graphics APIs.
57+
using Image<TPixel> clone = this.source.Clone(ctx => operation(ctx.Crop(clipped)));
6158

62-
// Fill the shape using the image brush
63-
p.Execute();
59+
// Use the clone as a brush source so only the clipped result contributes to the fill,
60+
// keeping the effect confined to the clipped region.
61+
Point brushOffset = new(
62+
clipped.X - (int)MathF.Floor(boundsF.Left),
63+
clipped.Y - (int)MathF.Floor(boundsF.Top));
64+
65+
ImageBrush brush = new(clone, clone.Bounds, brushOffset);
66+
67+
// Fill the shape using the image brush.
68+
FillPathProcessor processor = new(this.definition.Options, brush, this.definition.Region);
69+
using IImageProcessor<TPixel> pixelProcessor = processor.CreatePixelSpecificProcessor(this.configuration, this.source, this.sourceRectangle);
70+
pixelProcessor.Execute();
6471
}
6572
}

tests/ImageSharp.Drawing.Tests/Drawing/ClipTests.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,21 @@ public void Clip<TPixel>(TestImageProvider<TPixel> provider, float dx, float dy,
3636
appendSourceFileOrDescription: false);
3737
}
3838

39+
[Theory]
40+
[WithFile(TestImages.Png.Ducky, PixelTypes.Rgba32)]
41+
public void Clip_ConstrainsOperationToClipBounds<TPixel>(TestImageProvider<TPixel> provider)
42+
where TPixel : unmanaged, IPixel<TPixel>
43+
=> provider.RunValidatingProcessorTest(
44+
x =>
45+
{
46+
Size size = x.GetCurrentSize();
47+
RectangleF rect = new(0, 0, size.Width / 2, size.Height / 2);
48+
RectangularPolygon clipRect = new(rect);
49+
x.Clip(clipRect, ctx => ctx.Flip(FlipMode.Vertical));
50+
},
51+
appendPixelTypeToFileName: false,
52+
appendSourceFileOrDescription: false);
53+
3954
[Fact]
4055
public void Issue250_Vertical_Horizontal_Count_Should_Match()
4156
{
Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)