mirror of https://github.com/SixLabors/ImageSharp
16 changed files with 664 additions and 3 deletions
@ -0,0 +1,142 @@ |
|||
// <copyright file="ColorSpaceConverter.cs" company="James Jackson-South">
|
|||
// Copyright (c) James Jackson-South and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
// </copyright>
|
|||
|
|||
namespace ImageSharp.Colors.Spaces.Conversion |
|||
{ |
|||
using ImageSharp.Colors.Spaces; |
|||
using ImageSharp.Colors.Spaces.Conversion.Implementation.Hsl; |
|||
|
|||
/// <summary>
|
|||
/// Converts between color spaces ensuring that the color is adapted using chromatic adaptation.
|
|||
/// </summary>
|
|||
public partial class ColorSpaceConverter |
|||
{ |
|||
private static readonly HslAndRgbConverter HslAndRgbConverter = new HslAndRgbConverter(); |
|||
|
|||
/// <summary>
|
|||
/// Converts a <see cref="CieLab"/> into a <see cref="Hsl"/>
|
|||
/// </summary>
|
|||
/// <param name="color">The color to convert.</param>
|
|||
/// <returns>The <see cref="Hsl"/></returns>
|
|||
public Hsl ToHsl(CieLab color) |
|||
{ |
|||
Guard.NotNull(color, nameof(color)); |
|||
|
|||
CieXyz xyzColor = this.ToCieXyz(color); |
|||
|
|||
return this.ToHsl(xyzColor); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts a <see cref="CieLch"/> into a <see cref="Hsl"/>
|
|||
/// </summary>
|
|||
/// <param name="color">The color to convert.</param>
|
|||
/// <returns>The <see cref="Hsl"/></returns>
|
|||
public Hsl ToHsl(CieLch color) |
|||
{ |
|||
Guard.NotNull(color, nameof(color)); |
|||
|
|||
CieXyz xyzColor = this.ToCieXyz(color); |
|||
|
|||
return this.ToHsl(xyzColor); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts a <see cref="CieXyy"/> into a <see cref="Hsl"/>
|
|||
/// </summary>
|
|||
/// <param name="color">The color to convert.</param>
|
|||
/// <returns>The <see cref="Hsl"/></returns>
|
|||
public Hsl ToHsl(CieXyy color) |
|||
{ |
|||
Guard.NotNull(color, nameof(color)); |
|||
|
|||
CieXyz xyzColor = this.ToCieXyz(color); |
|||
|
|||
return this.ToHsl(xyzColor); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts a <see cref="CieXyz"/> into a <see cref="Hsl"/>
|
|||
/// </summary>
|
|||
/// <param name="color">The color to convert.</param>
|
|||
/// <returns>The <see cref="Hsl"/></returns>
|
|||
public Hsl ToHsl(CieXyz color) |
|||
{ |
|||
Guard.NotNull(color, nameof(color)); |
|||
|
|||
Rgb rgb = this.ToRgb(color); |
|||
|
|||
return HslAndRgbConverter.Convert(rgb); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts a <see cref="Cmyk"/> into a <see cref="Hsl"/>
|
|||
/// </summary>
|
|||
/// <param name="color">The color to convert.</param>
|
|||
/// <returns>The <see cref="Hsl"/></returns>
|
|||
public Hsl ToHsl(Cmyk color) |
|||
{ |
|||
Guard.NotNull(color, nameof(color)); |
|||
|
|||
Rgb rgb = this.ToRgb(color); |
|||
|
|||
return HslAndRgbConverter.Convert(rgb); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts a <see cref="HunterLab"/> into a <see cref="Hsl"/>
|
|||
/// </summary>
|
|||
/// <param name="color">The color to convert.</param>
|
|||
/// <returns>The <see cref="Hsl"/></returns>
|
|||
public Hsl ToHsl(HunterLab color) |
|||
{ |
|||
Guard.NotNull(color, nameof(color)); |
|||
|
|||
CieXyz xyzColor = this.ToCieXyz(color); |
|||
|
|||
return this.ToHsl(xyzColor); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts a <see cref="LinearRgb"/> into a <see cref="Hsl"/>
|
|||
/// </summary>
|
|||
/// <param name="color">The color to convert.</param>
|
|||
/// <returns>The <see cref="Hsl"/></returns>
|
|||
public Hsl ToHsl(LinearRgb color) |
|||
{ |
|||
Guard.NotNull(color, nameof(color)); |
|||
|
|||
Rgb rgb = this.ToRgb(color); |
|||
|
|||
return HslAndRgbConverter.Convert(rgb); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts a <see cref="Lms"/> into a <see cref="Hsl"/>
|
|||
/// </summary>
|
|||
/// <param name="color">The color to convert.</param>
|
|||
/// <returns>The <see cref="Hsl"/></returns>
|
|||
public Hsl ToHsl(Lms color) |
|||
{ |
|||
Guard.NotNull(color, nameof(color)); |
|||
|
|||
CieXyz xyzColor = this.ToCieXyz(color); |
|||
|
|||
return this.ToHsl(xyzColor); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts a <see cref="Rgb"/> into a <see cref="Hsl"/>
|
|||
/// </summary>
|
|||
/// <param name="color">The color to convert.</param>
|
|||
/// <returns>The <see cref="Hsl"/></returns>
|
|||
public Hsl ToHsl(Rgb color) |
|||
{ |
|||
Guard.NotNull(color, nameof(color)); |
|||
|
|||
return HslAndRgbConverter.Convert(color); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,153 @@ |
|||
// <copyright file="HslAndRgbConverter.cs" company="James Jackson-South">
|
|||
// Copyright (c) James Jackson-South and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
// </copyright>
|
|||
|
|||
namespace ImageSharp.Colors.Spaces.Conversion.Implementation.Hsl |
|||
{ |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
using ImageSharp.Colors.Spaces; |
|||
|
|||
/// <summary>
|
|||
/// Color converter between HSL and Rgb
|
|||
/// See <see href="http://www.poynton.com/PDFs/coloureq.pdf"/> for formulas.
|
|||
/// </summary>
|
|||
internal class HslAndRgbConverter : IColorConversion<Hsl, Rgb>, IColorConversion<Rgb, Hsl> |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public Rgb Convert(Hsl input) |
|||
{ |
|||
float rangedH = input.H / 360F; |
|||
float r = 0; |
|||
float g = 0; |
|||
float b = 0; |
|||
float s = input.S; |
|||
float l = input.L; |
|||
|
|||
if (MathF.Abs(l) > Constants.Epsilon) |
|||
{ |
|||
if (MathF.Abs(s) < Constants.Epsilon) |
|||
{ |
|||
r = g = b = l; |
|||
} |
|||
else |
|||
{ |
|||
float temp2 = (l < .5F) ? l * (1F + s) : l + s - (l * s); |
|||
float temp1 = (2F * l) - temp2; |
|||
|
|||
r = GetColorComponent(temp1, temp2, rangedH + 0.3333333F); |
|||
g = GetColorComponent(temp1, temp2, rangedH); |
|||
b = GetColorComponent(temp1, temp2, rangedH - 0.3333333F); |
|||
} |
|||
} |
|||
|
|||
return new Rgb(r, g, b); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public Hsl Convert(Rgb input) |
|||
{ |
|||
float r = input.R; |
|||
float g = input.G; |
|||
float b = input.B; |
|||
|
|||
float max = MathF.Max(r, MathF.Max(g, b)); |
|||
float min = MathF.Min(r, MathF.Min(g, b)); |
|||
float chroma = max - min; |
|||
float h = 0F; |
|||
float s = 0F; |
|||
float l = (max + min) / 2F; |
|||
|
|||
if (MathF.Abs(chroma) < Constants.Epsilon) |
|||
{ |
|||
return new Hsl(0F, s, l); |
|||
} |
|||
|
|||
if (MathF.Abs(r - max) < Constants.Epsilon) |
|||
{ |
|||
h = (g - b) / chroma; |
|||
} |
|||
else if (MathF.Abs(g - max) < Constants.Epsilon) |
|||
{ |
|||
h = 2F + ((b - r) / chroma); |
|||
} |
|||
else if (MathF.Abs(b - max) < Constants.Epsilon) |
|||
{ |
|||
h = 4F + ((r - g) / chroma); |
|||
} |
|||
|
|||
h *= 60F; |
|||
if (h < 0F) |
|||
{ |
|||
h += 360F; |
|||
} |
|||
|
|||
if (l <= .5F) |
|||
{ |
|||
s = chroma / (max + min); |
|||
} |
|||
else |
|||
{ |
|||
s = chroma / (2F - chroma); |
|||
} |
|||
|
|||
return new Hsl(h, s, l); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the color component from the given values.
|
|||
/// </summary>
|
|||
/// <param name="first">The first value.</param>
|
|||
/// <param name="second">The second value.</param>
|
|||
/// <param name="third">The third value.</param>
|
|||
/// <returns>
|
|||
/// The <see cref="float"/>.
|
|||
/// </returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private static float GetColorComponent(float first, float second, float third) |
|||
{ |
|||
third = MoveIntoRange(third); |
|||
if (third < 0.1666667F) |
|||
{ |
|||
return first + ((second - first) * 6F * third); |
|||
} |
|||
|
|||
if (third < .5F) |
|||
{ |
|||
return second; |
|||
} |
|||
|
|||
if (third < 0.6666667F) |
|||
{ |
|||
return first + ((second - first) * (0.6666667F - third) * 6F); |
|||
} |
|||
|
|||
return first; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Moves the specific value within the acceptable range for
|
|||
/// conversion.
|
|||
/// <remarks>Used for converting <see cref="Hsl"/> colors to this type.</remarks>
|
|||
/// </summary>
|
|||
/// <param name="value">The value to shift.</param>
|
|||
/// <returns>
|
|||
/// The <see cref="float"/>.
|
|||
/// </returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private static float MoveIntoRange(float value) |
|||
{ |
|||
if (value < 0F) |
|||
{ |
|||
value += 1F; |
|||
} |
|||
else if (value > 1F) |
|||
{ |
|||
value -= 1F; |
|||
} |
|||
|
|||
return value; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,157 @@ |
|||
// <copyright file="Hsl.cs" company="James Jackson-South">
|
|||
// Copyright (c) James Jackson-South and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
// </copyright>
|
|||
|
|||
namespace ImageSharp.Colors.Spaces |
|||
{ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.Numerics; |
|||
|
|||
/// <summary>
|
|||
/// Represents a Hsl (hue, saturation, lightness) color.
|
|||
/// </summary>
|
|||
public struct Hsl : IColorVector, IEquatable<Hsl>, IAlmostEquatable<Hsl, float> |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a <see cref="Hsl"/> that has H, S, and L values set to zero.
|
|||
/// </summary>
|
|||
public static readonly Hsl Empty = default(Hsl); |
|||
|
|||
/// <summary>
|
|||
/// Max range used for clamping
|
|||
/// </summary>
|
|||
private static readonly Vector3 VectorMax = new Vector3(360, 1, 1); |
|||
|
|||
/// <summary>
|
|||
/// The backing vector for SIMD support.
|
|||
/// </summary>
|
|||
private readonly Vector3 backingVector; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="Hsl"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="h">The h hue component.</param>
|
|||
/// <param name="s">The s saturation component.</param>
|
|||
/// <param name="l">The l value (lightness) component.</param>
|
|||
public Hsl(float h, float s, float l) |
|||
: this(new Vector3(h, s, l)) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="Hsl"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="vector">The vector representing the h, s, l components.</param>
|
|||
public Hsl(Vector3 vector) |
|||
{ |
|||
this.backingVector = Vector3.Clamp(vector, Vector3.Zero, VectorMax); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the hue component.
|
|||
/// <remarks>A value ranging between 0 and 360.</remarks>
|
|||
/// </summary>
|
|||
public float H => this.backingVector.X; |
|||
|
|||
/// <summary>
|
|||
/// Gets the saturation component.
|
|||
/// <remarks>A value ranging between 0 and 1.</remarks>
|
|||
/// </summary>
|
|||
public float S => this.backingVector.Y; |
|||
|
|||
/// <summary>
|
|||
/// Gets the lightness component.
|
|||
/// <remarks>A value ranging between 0 and 1.</remarks>
|
|||
/// </summary>
|
|||
public float L => this.backingVector.Z; |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether this <see cref="Hsl"/> is empty.
|
|||
/// </summary>
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public bool IsEmpty => this.Equals(Empty); |
|||
|
|||
/// <inheritdoc/>
|
|||
public Vector3 Vector => this.backingVector; |
|||
|
|||
/// <summary>
|
|||
/// Compares two <see cref="Hsl"/> objects for equality.
|
|||
/// </summary>
|
|||
/// <param name="left">
|
|||
/// The <see cref="Hsl"/> on the left side of the operand.
|
|||
/// </param>
|
|||
/// <param name="right">
|
|||
/// The <see cref="Hsl"/> on the right side of the operand.
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// True if the current left is equal to the <paramref name="right"/> parameter; otherwise, false.
|
|||
/// </returns>
|
|||
public static bool operator ==(Hsl left, Hsl right) |
|||
{ |
|||
return left.Equals(right); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Compares two <see cref="Hsl"/> objects for inequality.
|
|||
/// </summary>
|
|||
/// <param name="left">
|
|||
/// The <see cref="Hsl"/> on the left side of the operand.
|
|||
/// </param>
|
|||
/// <param name="right">
|
|||
/// The <see cref="Hsl"/> on the right side of the operand.
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// True if the current left is unequal to the <paramref name="right"/> parameter; otherwise, false.
|
|||
/// </returns>
|
|||
public static bool operator !=(Hsl left, Hsl right) |
|||
{ |
|||
return !left.Equals(right); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override int GetHashCode() |
|||
{ |
|||
return this.backingVector.GetHashCode(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override string ToString() |
|||
{ |
|||
if (this.IsEmpty) |
|||
{ |
|||
return "Hsl [ Empty ]"; |
|||
} |
|||
|
|||
return $"Hsl [ H={this.H:#0.##}, S={this.S:#0.##}, L={this.L:#0.##} ]"; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool Equals(object obj) |
|||
{ |
|||
if (obj is Hsl) |
|||
{ |
|||
return this.Equals((Hsl)obj); |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool Equals(Hsl other) |
|||
{ |
|||
return this.backingVector.Equals(other.backingVector); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool AlmostEquals(Hsl other, float precision) |
|||
{ |
|||
Vector3 result = Vector3.Abs(this.backingVector - other.backingVector); |
|||
|
|||
return result.X <= precision |
|||
&& result.Y <= precision |
|||
&& result.Z <= precision; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,72 @@ |
|||
namespace ImageSharp.Tests.Colors.Colorspaces |
|||
{ |
|||
using System.Collections.Generic; |
|||
|
|||
using ImageSharp.Colors.Spaces; |
|||
using ImageSharp.Colors.Spaces.Conversion; |
|||
|
|||
using Xunit; |
|||
|
|||
/// <summary>
|
|||
/// Tests <see cref="Rgb"/>-<see cref="Hsl"/> conversions.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Test data generated using:
|
|||
/// <see href="http://www.colorhexa.com"/>
|
|||
/// <see href="http://www.rapidtables.com/convert/color/hsl-to-rgb"/>
|
|||
/// </remarks>
|
|||
public class RgbAndHslConversionTest |
|||
{ |
|||
private static readonly IEqualityComparer<float> FloatRoundingComparer = new FloatRoundingComparer(4); |
|||
|
|||
private static readonly ColorSpaceConverter Converter = new ColorSpaceConverter(); |
|||
|
|||
/// <summary>
|
|||
/// Tests conversion from <see cref="Hsl"/> to <see cref="Rgb"/>.
|
|||
/// </summary>
|
|||
[Theory] |
|||
[InlineData(0, 0, 0, 0, 0, 0)] |
|||
[InlineData(0, 1, 1, 1, 1, 1)] |
|||
[InlineData(360, 1, 1, 1, 1, 1)] |
|||
[InlineData(0, 1, .5F, 1, 0, 0)] |
|||
[InlineData(120, 1, .5F, 0, 1, 0)] |
|||
[InlineData(240, 1, .5F, 0, 0, 1)] |
|||
public void Convert_Hsl_To_Rgb(float h, float s, float l, float r, float g, float b) |
|||
{ |
|||
// Arrange
|
|||
Hsl input = new Hsl(h, s, l); |
|||
|
|||
// Act
|
|||
Rgb output = Converter.ToRgb(input); |
|||
|
|||
// Assert
|
|||
Assert.Equal(Rgb.DefaultWorkingSpace, output.WorkingSpace); |
|||
Assert.Equal(r, output.R, FloatRoundingComparer); |
|||
Assert.Equal(g, output.G, FloatRoundingComparer); |
|||
Assert.Equal(b, output.B, FloatRoundingComparer); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tests conversion from <see cref="Rgb"/> to <see cref="Hsl"/>.
|
|||
/// </summary>
|
|||
[Theory] |
|||
[InlineData(0, 0, 0, 0, 0, 0)] |
|||
[InlineData(1, 1, 1, 0, 0, 1)] |
|||
[InlineData(1, 0, 0, 0, 1, .5F)] |
|||
[InlineData(0, 1, 0, 120, 1, .5F)] |
|||
[InlineData(0, 0, 1, 240, 1, .5F)] |
|||
public void Convert_Rgb_To_Hsl(float r, float g, float b, float h, float s, float l) |
|||
{ |
|||
// Arrange
|
|||
Rgb input = new Rgb(r, g, b); |
|||
|
|||
// Act
|
|||
Hsl output = Converter.ToHsl(input); |
|||
|
|||
// Assert
|
|||
Assert.Equal(h, output.H, FloatRoundingComparer); |
|||
Assert.Equal(s, output.S, FloatRoundingComparer); |
|||
Assert.Equal(l, output.L, FloatRoundingComparer); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue