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

146 lines
5.6 KiB
C#

// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Numerics;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Drawing.Processing {
/// <summary>
/// Provides an implementation of a brush that can recolor an image
/// </summary>
public sealed class RecolorBrush : Brush
{
/// <summary>
/// Initializes a new instance of the <see cref="RecolorBrush" /> class.
/// </summary>
/// <param name="sourceColor">Color of the source.</param>
/// <param name="targetColor">Color of the target.</param>
/// <param name="threshold">The threshold as a value between 0 and 1.</param>
public RecolorBrush(Color sourceColor, Color targetColor, float threshold)
{
this.SourceColor = sourceColor;
this.Threshold = threshold;
this.TargetColor = targetColor;
}
/// <summary>
/// Gets the threshold.
/// </summary>
public float Threshold { get; }
/// <summary>
/// Gets the source color.
/// </summary>
public Color SourceColor { get; }
/// <summary>
/// Gets the target color.
/// </summary>
public Color TargetColor { get; }
/// <inheritdoc />
public override BrushRenderer<TPixel> CreateRenderer<TPixel>(
Configuration configuration,
GraphicsOptions options,
int canvasWidth,
RectangleF region)
=> new RecolorBrushRenderer<TPixel>(
configuration,
options,
canvasWidth,
this.SourceColor.ToPixel<TPixel>(),
this.TargetColor.ToPixel<TPixel>(),
this.Threshold);
/// <inheritdoc />
public override bool Equals(Brush? other)
{
if (other is RecolorBrush brush)
{
return this.SourceColor.Equals(brush.SourceColor)
&& this.TargetColor.Equals(brush.TargetColor)
&& this.Threshold == brush.Threshold;
}
return false;
}
/// <inheritdoc/>
public override int GetHashCode()
=> HashCode.Combine(this.Threshold, this.SourceColor, this.TargetColor);
/// <summary>
/// The recolor brush applicator.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
private sealed class RecolorBrushRenderer<TPixel> : BrushRenderer<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly Vector4 sourceColor;
private readonly float threshold;
private readonly TPixel targetColorPixel;
/// <summary>
/// Initializes a new instance of the <see cref="RecolorBrushRenderer{TPixel}" /> class.
/// </summary>
/// <param name="configuration">The configuration instance to use when performing operations.</param>
/// <param name="options">The options</param>
/// <param name="canvasWidth">The canvas width for the current render pass.</param>
/// <param name="sourceColor">Color of the source.</param>
/// <param name="targetColor">Color of the target.</param>
/// <param name="threshold">The threshold .</param>
public RecolorBrushRenderer(
Configuration configuration,
GraphicsOptions options,
int canvasWidth,
TPixel sourceColor,
TPixel targetColor,
float threshold)
: base(configuration, options, canvasWidth)
{
this.sourceColor = sourceColor.ToScaledVector4();
this.targetColorPixel = targetColor;
// TODO: Review this. We can skip the conversion from/to Vector4.
// Lets hack a min max extremes for a color space by letting the IPackedPixel clamp our values to something in the correct spaces :)
TPixel maxColor = TPixel.FromVector4(new Vector4(float.MaxValue));
TPixel minColor = TPixel.FromVector4(new Vector4(float.MinValue));
this.threshold = Vector4.DistanceSquared(maxColor.ToVector4(), minColor.ToVector4()) * threshold;
}
/// <inheritdoc />
public override void Apply(
Span<TPixel> destinationRow,
ReadOnlySpan<float> scanline,
int x,
int y,
BrushWorkspace<TPixel> workspace)
{
Span<float> amounts = workspace.GetAmounts(scanline.Length);
Span<TPixel> overlays = workspace.GetOverlays(scanline.Length);
for (int i = 0; i < scanline.Length; i++)
{
amounts[i] = scanline[i] * this.Options.BlendPercentage;
TPixel result = destinationRow[i];
Vector4 background = result.ToVector4();
float distance = Vector4.DistanceSquared(background, this.sourceColor);
overlays[i] = distance <= this.threshold
? this.Blender.Blend(result, this.targetColorPixel, (this.threshold - distance) / this.threshold)
: result;
}
this.Blender.Blend(
this.Configuration,
destinationRow,
destinationRow,
overlays,
amounts,
workspace.GetBlendScratch(scanline.Length, 3));
}
}
}
}