Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure the negatively offset shapes are correctly offset the ImageBrush Texture during rendering. #309

Merged
merged 4 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
61 changes: 52 additions & 9 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>
public ImageBrush(Image image, RectangleF region)
/// <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>
internal ImageBrush(Image image, RectangleF region)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes the constructor internal again. Was that intentional?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, I had failed to update my working branch when I started the PR. Fixed.

: 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>
internal 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.
Loading