Skip to content

Commit 5743bf9

Browse files
committed
enable image brush to be only draw a portion of the source image
1 parent 07c3bf1 commit 5743bf9

7 files changed

Lines changed: 68 additions & 31 deletions

File tree

src/ImageSharp.Drawing/Processing/ImageBrush.cs

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,19 @@ public class ImageBrush : IBrush
1818
/// </summary>
1919
private readonly Image image;
2020

21-
private readonly RectangleF? region;
21+
/// <summary>
22+
/// The region of the source image we will be using to paint.
23+
/// </summary>
24+
private readonly RectangleF region;
2225

2326
/// <summary>
2427
/// Initializes a new instance of the <see cref="ImageBrush"/> class.
2528
/// </summary>
2629
/// <param name="image">The image.</param>
2730
public ImageBrush(Image image)
28-
=> this.image = image;
31+
: this(image, image.Bounds())
32+
{
33+
}
2934

3035
/// <summary>
3136
/// Initializes a new instance of the <see cref="ImageBrush"/> class.
@@ -49,16 +54,13 @@ public BrushApplicator<TPixel> CreateApplicator<TPixel>(
4954
RectangleF region)
5055
where TPixel : unmanaged, IPixel<TPixel>
5156
{
52-
RectangleF interest = this.region ?? region;
53-
5457
if (this.image is Image<TPixel> specificImage)
5558
{
56-
return new ImageBrushApplicator<TPixel>(configuration, options, source, specificImage, interest, false);
59+
return new ImageBrushApplicator<TPixel>(configuration, options, source, specificImage, region, this.region, false);
5760
}
5861

5962
specificImage = this.image.CloneAs<TPixel>();
60-
61-
return new ImageBrushApplicator<TPixel>(configuration, options, source, specificImage, interest, true);
63+
return new ImageBrushApplicator<TPixel>(configuration, options, source, specificImage, region, this.region, true);
6264
}
6365

6466
/// <summary>
@@ -75,14 +77,9 @@ private class ImageBrushApplicator<TPixel> : BrushApplicator<TPixel>
7577
private readonly bool shouldDisposeImage;
7678

7779
/// <summary>
78-
/// The y-length.
80+
/// The region of the source image we will be using to draw from.
7981
/// </summary>
80-
private readonly int yLength;
81-
82-
/// <summary>
83-
/// The x-length.
84-
/// </summary>
85-
private readonly int xLength;
82+
private readonly Rectangle sourceRegion;
8683

8784
/// <summary>
8885
/// The Y offset.
@@ -93,7 +90,6 @@ private class ImageBrushApplicator<TPixel> : BrushApplicator<TPixel>
9390
/// The X offset.
9491
/// </summary>
9592
private readonly int offsetX;
96-
9793
private bool isDisposed;
9894

9995
/// <summary>
@@ -103,32 +99,35 @@ private class ImageBrushApplicator<TPixel> : BrushApplicator<TPixel>
10399
/// <param name="options">The graphics options.</param>
104100
/// <param name="target">The target image.</param>
105101
/// <param name="image">The image.</param>
106-
/// <param name="region">The region.</param>
102+
/// <param name="targetRegion">The region of the target image we will be drawing to.</param>
103+
/// <param name="sourceRegion">The region of the source image we will be using to source pixels to draw from.</param>
107104
/// <param name="shouldDisposeImage">Whether to dispose the image on disposal of the applicator.</param>
108105
public ImageBrushApplicator(
109106
Configuration configuration,
110107
GraphicsOptions options,
111108
ImageFrame<TPixel> target,
112109
Image<TPixel> image,
113-
RectangleF region,
110+
RectangleF targetRegion,
111+
RectangleF sourceRegion,
114112
bool shouldDisposeImage)
115113
: base(configuration, options, target)
116114
{
117115
this.sourceImage = image;
118116
this.sourceFrame = image.Frames.RootFrame;
119117
this.shouldDisposeImage = shouldDisposeImage;
120-
this.xLength = image.Width;
121-
this.yLength = image.Height;
122-
this.offsetY = (int)MathF.Max(MathF.Floor(region.Top), 0);
123-
this.offsetX = (int)MathF.Max(MathF.Floor(region.Left), 0);
118+
119+
this.sourceRegion = Rectangle.Intersect(image.Bounds(), (Rectangle)sourceRegion);
120+
121+
this.offsetY = (int)MathF.Max(MathF.Floor(targetRegion.Top), 0);
122+
this.offsetX = (int)MathF.Max(MathF.Floor(targetRegion.Left), 0);
124123
}
125124

126125
internal TPixel this[int x, int y]
127126
{
128127
get
129128
{
130-
int srcX = (x - this.offsetX) % this.xLength;
131-
int srcY = (y - this.offsetY) % this.yLength;
129+
int srcX = ((x - this.offsetX) % this.sourceRegion.Width) + this.sourceRegion.X;
130+
int srcY = ((y - this.offsetY) % this.sourceRegion.Width) + this.sourceRegion.Y;
132131
return this.sourceFrame[srcX, srcY];
133132
}
134133
}
@@ -161,15 +160,16 @@ public override void Apply(Span<float> scanline, int x, int y)
161160
Span<float> amountSpan = amountBuffer.Memory.Span;
162161
Span<TPixel> overlaySpan = overlay.Memory.Span;
163162

164-
int sourceY = (y - this.offsetY) % this.yLength;
165163
int offsetX = x - this.offsetX;
164+
int sourceY = ((y - this.offsetY) % this.sourceRegion.Width) + this.sourceRegion.Y;
166165
Span<TPixel> sourceRow = this.sourceFrame.GetPixelRowSpan(sourceY);
167166

168167
for (int i = 0; i < scanline.Length; i++)
169168
{
170169
amountSpan[i] = scanline[i] * this.Options.BlendPercentage;
171170

172-
int sourceX = (i + offsetX) % this.xLength;
171+
int sourceX = ((i + offsetX) % this.sourceRegion.Width) + this.sourceRegion.X;
172+
173173
overlaySpan[i] = sourceRow[sourceX];
174174
}
175175

src/ImageSharp.Drawing/Processing/Processors/Drawing/RecursiveImageProcessor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public void Execute()
8383
// Use an image brush to apply cloned image as the source for filling the shape.
8484
// We pass explicit bounds to avoid the need to crop the clone;
8585
RectangleF bounds = this.recursiveImageProcessor.Path.Bounds;
86-
var brush = new ImageBrush(clone, new RectangleF(0, 0, bounds.Width, bounds.Height));
86+
var brush = new ImageBrush(clone, bounds);
8787

8888
// Grab hold of an image processor that can fill paths with a brush to allow it to do the hard pixel pushing for us
8989
var processor = new FillPathProcessor(this.recursiveImageProcessor.Options, brush, this.recursiveImageProcessor.Path);

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,19 @@ namespace SixLabors.ImageSharp.Drawing.Tests.Drawing
1414
public class ClipTests
1515
{
1616
[Theory]
17-
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, 0, 0)]
18-
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, -20, -20)]
19-
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, 20, 20)]
20-
public void Clip<TPixel>(TestImageProvider<TPixel> provider, float dx, float dy)
17+
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, 0, 0, 0.5)]
18+
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, -20, -20, 0.5)]
19+
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, 20, 20, 0.5)]
20+
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, 40, 60, 0.2)]
21+
public void Clip<TPixel>(TestImageProvider<TPixel> provider, float dx, float dy, float sizeMult)
2122
where TPixel : unmanaged, IPixel<TPixel>
2223
{
2324
FormattableString testDetails = $"offset_x{dx}_y{dy}";
2425
provider.RunValidatingProcessorTest(
2526
x =>
2627
{
2728
Size size = x.GetCurrentSize();
28-
int outerRadii = Math.Min(size.Width, size.Height) / 2;
29+
int outerRadii = (int)(Math.Min(size.Width, size.Height) * sizeMult);
2930
var star = new Star(new PointF(size.Width / 2, size.Height / 2), 5, outerRadii / 2, outerRadii);
3031

3132
var builder = Matrix3x2.CreateTranslation(new Vector2(dx, dy));

tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,33 @@ public void FillPolygon_ImageBrush<TPixel>(TestImageProvider<TPixel> provider, s
224224
}
225225
}
226226

227+
[Theory]
228+
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, TestImages.Png.Ducky)]
229+
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, TestImages.Bmp.Car)]
230+
public void FillPolygon_ImageBrush_Rect<TPixel>(TestImageProvider<TPixel> provider, string brushImageName)
231+
where TPixel : unmanaged, IPixel<TPixel>
232+
{
233+
PointF[] simplePath =
234+
{
235+
new Vector2(10, 10), new Vector2(200, 50), new Vector2(50, 200)
236+
};
237+
238+
using (var brushImage = Image.Load<TPixel>(TestFile.Create(brushImageName).Bytes))
239+
{
240+
float top = brushImage.Height / 4;
241+
float left = brushImage.Width / 4;
242+
float height = top * 2;
243+
float width = left * 2;
244+
245+
var brush = new ImageBrush(brushImage, new RectangleF(left, top, width, height));
246+
247+
provider.RunValidatingProcessorTest(
248+
c => c.FillPolygon(brush, simplePath),
249+
System.IO.Path.GetFileNameWithoutExtension(brushImageName) + "_rect",
250+
appendSourceFileOrDescription: false);
251+
}
252+
}
253+
227254
[Theory]
228255
[WithBasicTestPatternImages(250, 250, PixelTypes.Rgba32)]
229256
public void Fill_RectangularPolygon<TPixel>(TestImageProvider<TPixel> provider)
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)