Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,16 @@ namespace SixLabors.ImageSharp.Drawing.Processing;
public static class ClipPathExtensions
{
/// <summary>
/// Applies the processing operation within the provided region defined by an <see cref="IPath"/>.
/// Applies the processing operation within the region defined by an <see cref="IPath"/>.
/// </summary>
/// <param name="source">The source image processing context.</param>
/// <param name="region">The <see cref="IPath"/> defining the region to operation within.</param>
/// <param name="operation">The operation to perform.</param>
/// <param name="region">
/// The <see cref="IPath"/> defining the clip region. Only pixels inside the clip are affected.
/// </param>
/// <param name="operation">
/// The operation to perform. This executes in the clipped context so results are constrained to the
/// clip bounds.
/// </param>
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext Clip(
this IImageProcessingContext source,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing;

/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TPixel">The type of pixel.</typeparam>
internal class ClipPathProcessor<TPixel> : IImageProcessor<TPixel>
Expand All @@ -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<TPixel> 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<IImageProcessingContext> 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<TPixel> 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<TPixel> 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<TPixel> pixelProcessor = processor.CreatePixelSpecificProcessor(this.configuration, this.source, this.sourceRectangle);
pixelProcessor.Execute();
}
}
15 changes: 15 additions & 0 deletions tests/ImageSharp.Drawing.Tests/Drawing/ClipTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ public void Clip<TPixel>(TestImageProvider<TPixel> provider, float dx, float dy,
appendSourceFileOrDescription: false);
}

[Theory]
[WithFile(TestImages.Png.Ducky, PixelTypes.Rgba32)]
public void Clip_ConstrainsOperationToClipBounds<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
=> 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()
{
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading