ImageSharp/ImageSharp.Drawing/Processing/ImageBrush.cs
2026-08-03 22:31:27 +02:00

269 lines
10 KiB
C#

// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Drawing.Processing {
/// <summary>
/// Provides an implementation of an image brush for painting images within areas.
/// </summary>
/// <typeparam name="TPixel">The pixel format of the source image.</typeparam>
public sealed class ImageBrush<TPixel> : ImageBrush
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="ImageBrush{TPixel}"/> class.
/// </summary>
/// <param name="image">The source image to draw.</param>
public ImageBrush(Image<TPixel> image)
: base(image)
=> this.SourceImage = image;
/// <summary>
/// Initializes a new instance of the <see cref="ImageBrush{TPixel}"/> class.
/// </summary>
/// <param name="image">The source image to draw.</param>
/// <param name="offset">An offset to apply to the image while drawing the texture.</param>
public ImageBrush(Image<TPixel> image, Point offset)
: base(image, offset)
=> this.SourceImage = image;
/// <summary>
/// Initializes a new instance of the <see cref="ImageBrush{TPixel}"/> class.
/// </summary>
/// <param name="image">The source image to draw.</param>
/// <param name="region">The region of interest within the source image.</param>
public ImageBrush(Image<TPixel> image, RectangleF region)
: base(image, region)
=> this.SourceImage = image;
/// <summary>
/// Initializes a new instance of the <see cref="ImageBrush{TPixel}"/> class.
/// </summary>
/// <param name="image">The source image to draw.</param>
/// <param name="region">The region of interest within the source image.</param>
/// <param name="offset">An offset to apply to the image while drawing the texture.</param>
public ImageBrush(Image<TPixel> image, RectangleF region, Point offset)
: base(image, region, offset)
=> this.SourceImage = image;
/// <summary>
/// Gets the typed source image used by this brush.
/// </summary>
public Image<TPixel> SourceImage { get; }
}
/// <summary>
/// The untyped base class for image brushes, used to support non-generic brush references in drawing contexts.
/// </summary>
public abstract class ImageBrush : Brush
{
/// <summary>
/// Initializes a new instance of the <see cref="ImageBrush"/> class.
/// </summary>
/// <param name="image">The source image to draw.</param>
protected ImageBrush(Image image)
: this(image, image.Bounds)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ImageBrush"/> class.
/// </summary>
/// <param name="image">The image.</param>
/// <param name="offset">
/// An offset to apply the to image image while drawing apply the texture.
/// </param>
protected 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>
protected 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>
protected ImageBrush(Image image, RectangleF region, Point offset)
{
this.UntypedImage = image;
this.SourceRegion = RectangleF.Intersect(image.Bounds, region);
this.Offset = offset;
}
/// <summary>
/// Gets the source image used by this brush.
/// </summary>
public Image UntypedImage { get; }
/// <summary>
/// Gets the source region within the image.
/// </summary>
public RectangleF SourceRegion { get; }
/// <summary>
/// Gets the offset applied to the brush origin.
/// </summary>
public Point Offset { get; }
/// <inheritdoc />
public override bool Equals(Brush? other)
{
if (other is ImageBrush ib)
{
return ib.UntypedImage == this.UntypedImage && ib.SourceRegion == this.SourceRegion;
}
return false;
}
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.UntypedImage, this.SourceRegion);
/// <inheritdoc />
public override BrushRenderer<TPixel> CreateRenderer<TPixel>(
Configuration configuration,
GraphicsOptions options,
int canvasWidth,
RectangleF region)
{
if (this.UntypedImage is Image<TPixel> image)
{
return new ImageBrushRenderer<TPixel>(configuration, options, canvasWidth, image, region, this.SourceRegion, this.Offset);
}
// This will never be hit as the brush is always normalized by the drawing canvas
// but we do it to satisfy the type system.
ThrowIfInvalidImagePixelFormat();
return null;
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowIfInvalidImagePixelFormat()
=> throw new UnreachableException("The pixel format of the image is not supported by this brush renderer");
/// <summary>
/// The image brush applicator.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
private sealed class ImageBrushRenderer<TPixel> : BrushRenderer<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly ImageFrame<TPixel> sourceFrame;
/// <summary>
/// The region of the source image we will be using to draw from.
/// </summary>
private readonly Rectangle sourceRegion;
/// <summary>
/// The Y offset.
/// </summary>
private readonly int offsetY;
/// <summary>
/// The X offset.
/// </summary>
private readonly int offsetX;
/// <summary>
/// Initializes a new instance of the <see cref="ImageBrushRenderer{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration instance to use when performing operations.</param>
/// <param name="options">The graphics options.</param>
/// <param name="canvasWidth">The canvas width for the current render pass.</param>
/// <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>
public ImageBrushRenderer(
Configuration configuration,
GraphicsOptions options,
int canvasWidth,
Image<TPixel> image,
RectangleF targetRegion,
RectangleF sourceRegion,
Point offset)
: base(configuration, options, canvasWidth)
{
this.sourceFrame = image.Frames.RootFrame;
this.sourceRegion = Rectangle.Intersect(image.Bounds, (Rectangle)sourceRegion);
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]
{
get
{
int srcX = ((x - this.offsetX) % this.sourceRegion.Width) + this.sourceRegion.X;
int srcY = ((y - this.offsetY) % this.sourceRegion.Height) + this.sourceRegion.Y;
return this.sourceFrame[srcX, srcY];
}
}
/// <inheritdoc />
public override void Apply(
Span<TPixel> destinationRow,
ReadOnlySpan<float> scanline,
int x,
int y,
BrushWorkspace<TPixel> workspace)
{
Span<float> amountSpan = workspace.GetAmounts(scanline.Length);
Span<TPixel> overlaySpan = workspace.GetOverlays(scanline.Length);
int offsetX = x - this.offsetX;
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) // 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];
}
this.Blender.Blend(
this.Configuration,
destinationRow,
destinationRow,
overlaySpan,
amountSpan,
workspace.GetBlendScratch(scanline.Length, 3));
}
}
}
}