Skip to content

Commit

Permalink
Merge pull request #309 from SixLabors/sw/image-brush-offsets
Browse files Browse the repository at this point in the history
Ensure the negatively offset shapes are correctly offset the ImageBrush Texture during rendering.
  • Loading branch information
JimBobSquarePants authored Dec 11, 2023
2 parents 3fb38a4 + 184fe2e commit fda6302
Show file tree
Hide file tree
Showing 11 changed files with 155 additions and 15 deletions.
59 changes: 51 additions & 8 deletions src/ImageSharp.Drawing/Processing/ImageBrush.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ public class ImageBrush : Brush
/// </summary>
private readonly RectangleF region;

/// <summary>
/// The offet to apply to the source image while applying the imagebrush
/// </summary>
private readonly Point offset;

/// <summary>
/// Initializes a new instance of the <see cref="ImageBrush"/> class.
/// </summary>
Expand All @@ -33,12 +38,44 @@ public ImageBrush(Image image)
/// <summary>
/// Initializes a new instance of the <see cref="ImageBrush"/> class.
/// </summary>
/// <param name="image">The source image.</param>
/// <param name="region">The region of interest within the source image to draw.</param>
/// <param name="image">The image.</param>
/// <param name="offset">
/// An offset to apply the to image image while drawing apply the texture.
/// </param>
public ImageBrush(Image image, Point offset)
: this(image, image.Bounds, offset)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="ImageBrush"/> class.
/// </summary>
/// <param name="image">The image.</param>
/// <param name="region">
/// The region of interest.
/// This overrides any region used to initialize the brush applicator.
/// </param>
public ImageBrush(Image image, RectangleF region)
: this(image, region, Point.Empty)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="ImageBrush"/> class.
/// </summary>
/// <param name="image">The image.</param>
/// <param name="region">
/// The region of interest.
/// This overrides any region used to initialize the brush applicator.
/// </param>
/// <param name="offset">
/// An offset to apply the to image image while drawing apply the texture.
/// </param>
public ImageBrush(Image image, RectangleF region, Point offset)
{
this.image = image;
this.region = RectangleF.Intersect(image.Bounds, region);
this.offset = offset;
}

/// <inheritdoc />
Expand All @@ -64,11 +101,11 @@ public override BrushApplicator<TPixel> CreateApplicator<TPixel>(
{
if (this.image is Image<TPixel> specificImage)
{
return new ImageBrushApplicator<TPixel>(configuration, options, source, specificImage, region, this.region, false);
return new ImageBrushApplicator<TPixel>(configuration, options, source, specificImage, region, this.region, this.offset, false);
}

specificImage = this.image.CloneAs<TPixel>();
return new ImageBrushApplicator<TPixel>(configuration, options, source, specificImage, region, this.region, true);
return new ImageBrushApplicator<TPixel>(configuration, options, source, specificImage, region, this.region, this.offset, true);
}

/// <summary>
Expand Down Expand Up @@ -107,6 +144,7 @@ private class ImageBrushApplicator<TPixel> : BrushApplicator<TPixel>
/// <param name="image">The image.</param>
/// <param name="targetRegion">The region of the target image we will be drawing to.</param>
/// <param name="sourceRegion">The region of the source image we will be using to source pixels to draw from.</param>
/// <param name="offset">An offset to apply to the texture while drawing.</param>
/// <param name="shouldDisposeImage">Whether to dispose the image on disposal of the applicator.</param>
public ImageBrushApplicator(
Configuration configuration,
Expand All @@ -115,6 +153,7 @@ public ImageBrushApplicator(
Image<TPixel> image,
RectangleF targetRegion,
RectangleF sourceRegion,
Point offset,
bool shouldDisposeImage)
: base(configuration, options, target)
{
Expand All @@ -124,8 +163,8 @@ public ImageBrushApplicator(

this.sourceRegion = Rectangle.Intersect(image.Bounds, (Rectangle)sourceRegion);

this.offsetY = (int)MathF.Max(MathF.Floor(targetRegion.Top), 0);
this.offsetX = (int)MathF.Max(MathF.Floor(targetRegion.Left), 0);
this.offsetY = (int)MathF.Floor(targetRegion.Top) + offset.Y;
this.offsetX = (int)MathF.Floor(targetRegion.Left) + offset.X;
}

internal TPixel this[int x, int y]
Expand Down Expand Up @@ -166,14 +205,18 @@ public override void Apply(Span<float> scanline, int x, int y)
Span<TPixel> overlaySpan = overlay.Memory.Span;

int offsetX = x - this.offsetX;
int sourceY = ((y - this.offsetY) % this.sourceRegion.Height) + this.sourceRegion.Y;
int sourceY = ((((y - this.offsetY) % this.sourceRegion.Height) // clamp the number between -height and +height
+ this.sourceRegion.Height) % this.sourceRegion.Height) // clamp the number between 0 and +height
+ this.sourceRegion.Y;
Span<TPixel> sourceRow = this.sourceFrame.PixelBuffer.DangerousGetRowSpan(sourceY);

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

int sourceX = ((i + offsetX) % this.sourceRegion.Width) + this.sourceRegion.X;
int sourceX = ((((i + offsetX) % this.sourceRegion.Width) // clamp the number between -width and +width
+ this.sourceRegion.Width) % this.sourceRegion.Width) // clamp the number between 0 and +width
+ this.sourceRegion.X;

overlaySpan[i] = sourceRow[sourceX];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,21 @@ public void Execute()
// 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;
var brush = new ImageBrush(clone, bounds);

// 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
var offsetX = 0;
var offsetY = 0;
if (bounds.X < 0)
{
offsetX = -(int)MathF.Floor(bounds.X);
}

if (bounds.Y < 0)
{
offsetY = -(int)MathF.Floor(bounds.Y);
}

var brush = new ImageBrush(clone, bounds, new Point(offsetX, offsetY));

// Grab hold of an image processor that can fill paths with a brush to allow it to do the hard pixel pushing for us
var processor = new FillPathProcessor(this.definition.Options, brush, this.definition.Region);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,9 @@ public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuratio
var rect = (Rectangle)rectF;
if (!this.Options.GraphicsOptions.Antialias || rectF == rect)
{
var interest = Rectangle.Intersect(sourceRectangle, rect);

// Cast as in and back are the same or we are using anti-aliasing
return new FillProcessor(this.Options, this.Brush)
.CreatePixelSpecificProcessor(configuration, source, interest);
.CreatePixelSpecificProcessor(configuration, source, rect);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ protected override void OnFrameApply(ImageFrame<TPixel> source)
subpixelCount = Math.Max(subpixelCount, graphicsOptions.AntialiasSubpixelDepth);
}

using BrushApplicator<TPixel> applicator = brush.CreateApplicator(configuration, graphicsOptions, source, interest);
using BrushApplicator<TPixel> applicator = brush.CreateApplicator(configuration, graphicsOptions, source, this.bounds);
int scanlineWidth = interest.Width;
MemoryAllocator allocator = this.Configuration.MemoryAllocator;
bool scanlineDirty = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ protected override void OnFrameApply(ImageFrame<TPixel> source)
configuration,
options,
source,
interest);
this.SourceRectangle);

amount.Memory.Span.Fill(1F);

Expand Down
1 change: 1 addition & 0 deletions tests/ImageSharp.Drawing.Tests/Drawing/ClipTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public class ClipTests
[Theory]
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, 0, 0, 0.5)]
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, -20, -20, 0.5)]
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, -20, -100, 0.5)]
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, 20, 20, 0.5)]
[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, 40, 60, 0.2)]
public void Clip<TPixel>(TestImageProvider<TPixel> provider, float dx, float dy, float sizeMult)
Expand Down
74 changes: 73 additions & 1 deletion tests/ImageSharp.Drawing.Tests/Drawing/FillImageBrushTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Drawing;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
Expand Down Expand Up @@ -72,11 +74,81 @@ public void CanDrawPortraitImage<TPixel>(TestImageProvider<TPixel> provider)

overlay.Mutate(c => c.Crop(new Rectangle(0, 0, 90, 125)));

ImageBrush brush = new(overlay);
var brush = new ImageBrush(overlay);
background.Mutate(c => c.Fill(brush));

background.DebugSave(provider, appendSourceFileOrDescription: false);
background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false);
}

[Theory]
[WithTestPatternImage(400, 400, PixelTypes.Rgba32)]
public void CanOffsetImage<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes;
using Image<TPixel> background = provider.GetImage();
using Image overlay = Image.Load<Rgba32>(data);

var brush = new ImageBrush(overlay);
background.Mutate(c => c.Fill(brush, new RectangularPolygon(0, 0, 400, 200)));
background.Mutate(c => c.Fill(brush, new RectangularPolygon(-100, 200, 500, 200)));

background.DebugSave(provider, appendSourceFileOrDescription: false);
background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false);
}

[Theory]
[WithTestPatternImage(400, 400, PixelTypes.Rgba32)]
public void CanOffsetViaBrushImage<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes;
using Image<TPixel> background = provider.GetImage();
using Image overlay = Image.Load<Rgba32>(data);

var brush = new ImageBrush(overlay);
var brushOffset = new ImageBrush(overlay, new Point(100, 0));
background.Mutate(c => c.Fill(brush, new RectangularPolygon(0, 0, 400, 200)));
background.Mutate(c => c.Fill(brushOffset, new RectangularPolygon(0, 200, 400, 200)));

background.DebugSave(provider, appendSourceFileOrDescription: false);
background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false);
}

[Theory]
[WithSolidFilledImages(1000, 1000, "White", PixelTypes.Rgba32)]
public void CanDrawOffsetImage<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes;
using Image<TPixel> background = provider.GetImage();

using Image templateImage = Image.Load<Rgba32>(data);
using Image finalTexture = BuildMultiRowTexture(templateImage);

finalTexture.Mutate(c => c.Resize(100, 200));

ImageBrush brush = new(finalTexture);
background.Mutate(c => c.Fill(brush));

background.DebugSave(provider, appendSourceFileOrDescription: false);
background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false);

Image BuildMultiRowTexture(Image sourceTexture)
{
int halfWidth = sourceTexture.Width / 2;

Image final = sourceTexture.Clone(x => x.Resize(new ResizeOptions
{
Size = new Size(templateImage.Width, templateImage.Height * 2),
Position = AnchorPositionMode.TopLeft,
Mode = ResizeMode.Pad,
})
.DrawImage(templateImage, new Point(halfWidth, sourceTexture.Height), new Rectangle(0, 0, halfWidth, sourceTexture.Height), 1)
.DrawImage(templateImage, new Point(0, templateImage.Height), new Rectangle(halfWidth, 0, halfWidth, sourceTexture.Height), 1));
return final;
}
}

[Theory]
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
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit fda6302

Please sign in to comment.