Skip to content

Commit

Permalink
Merge branch 'main' into js/update-refs
Browse files Browse the repository at this point in the history
  • Loading branch information
JimBobSquarePants authored Dec 12, 2023
2 parents 60760c0 + fda6302 commit bf6482a
Show file tree
Hide file tree
Showing 12 changed files with 157 additions and 17 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,14 @@ jobs:

- name: DotNet Setup
if: ${{ matrix.options.sdk-preview != true }}
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
6.0.x
- name: DotNet Setup Preview
if: ${{ matrix.options.sdk-preview == true }}
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
7.0.x
Expand Down
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 bf6482a

Please sign in to comment.