Browse Source

Add CMYK

pull/2739/head
James Jackson-South 2 years ago
parent
commit
b8c73e121d
  1. 10
      src/ImageSharp/ColorProfiles/CieXyy.cs
  2. 164
      src/ImageSharp/ColorProfiles/Cmyk.cs
  3. 55
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs
  4. 55
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs
  5. 7
      tests/ImageSharp.Tests/ColorProfiles/ApproximateColorProfileComparer.cs
  6. 70
      tests/ImageSharp.Tests/ColorProfiles/CieLabAndCmykConversionTests.cs
  7. 70
      tests/ImageSharp.Tests/ColorProfiles/CmykAndYCbCrConversionTests.cs
  8. 77
      tests/ImageSharp.Tests/ColorProfiles/RgbAndCmykConversionTest.cs

10
src/ImageSharp/ColorProfiles/CieXyy.cs

@ -18,7 +18,7 @@ public readonly struct CieXyy : IColorProfile<CieXyy, CieXyz>
/// <param name="x">The x chroma component.</param>
/// <param name="y">The y chroma component.</param>
/// <param name="yl">The y luminance component.</param>
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieXyy(float x, float y, float yl)
{
// Not clamping as documentation about this space only indicates "usual" ranges
@ -31,7 +31,7 @@ public readonly struct CieXyy : IColorProfile<CieXyy, CieXyz>
/// Initializes a new instance of the <see cref="CieXyy"/> struct.
/// </summary>
/// <param name="vector">The vector representing the x, y, Y components.</param>
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieXyy(Vector3 vector)
: this()
{
@ -67,7 +67,7 @@ public readonly struct CieXyy : IColorProfile<CieXyy, CieXyz>
/// <returns>
/// True if the current left is equal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(CieXyy left, CieXyy right) => left.Equals(right);
/// <summary>
@ -78,7 +78,7 @@ public readonly struct CieXyy : IColorProfile<CieXyy, CieXyz>
/// <returns>
/// True if the current left is unequal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(CieXyy left, CieXyy right) => !left.Equals(right);
/// <inheritdoc/>
@ -148,7 +148,7 @@ public readonly struct CieXyy : IColorProfile<CieXyy, CieXyz>
public override bool Equals(object? obj) => obj is CieXyy other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(CieXyy other)
=> this.X.Equals(other.X)
&& this.Y.Equals(other.Y)

164
src/ImageSharp/ColorProfiles/Cmyk.cs

@ -0,0 +1,164 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.ColorProfiles;
/// <summary>
/// Represents an CMYK (cyan, magenta, yellow, keyline) color.
/// </summary>
public readonly struct Cmyk : IColorProfile<Cmyk, Rgb>
{
private static readonly Vector4 Min = Vector4.Zero;
private static readonly Vector4 Max = Vector4.One;
/// <summary>
/// Initializes a new instance of the <see cref="Cmyk"/> struct.
/// </summary>
/// <param name="c">The cyan component.</param>
/// <param name="m">The magenta component.</param>
/// <param name="y">The yellow component.</param>
/// <param name="k">The keyline black component.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Cmyk(float c, float m, float y, float k)
: this(new Vector4(c, m, y, k))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Cmyk"/> struct.
/// </summary>
/// <param name="vector">The vector representing the c, m, y, k components.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Cmyk(Vector4 vector)
{
vector = Numerics.Clamp(vector, Min, Max);
this.C = vector.X;
this.M = vector.Y;
this.Y = vector.Z;
this.K = vector.W;
}
/// <summary>
/// Gets the cyan color component.
/// <remarks>A value ranging between 0 and 1.</remarks>
/// </summary>
public readonly float C { get; }
/// <summary>
/// Gets the magenta color component.
/// <remarks>A value ranging between 0 and 1.</remarks>
/// </summary>
public readonly float M { get; }
/// <summary>
/// Gets the yellow color component.
/// <remarks>A value ranging between 0 and 1.</remarks>
/// </summary>
public readonly float Y { get; }
/// <summary>
/// Gets the keyline black color component.
/// <remarks>A value ranging between 0 and 1.</remarks>
/// </summary>
public readonly float K { get; }
/// <summary>
/// Compares two <see cref="Cmyk"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="Cmyk"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="Cmyk"/> 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>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(Cmyk left, Cmyk right) => left.Equals(right);
/// <summary>
/// Compares two <see cref="Cmyk"/> objects for inequality.
/// </summary>
/// <param name="left">The <see cref="Cmyk"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="Cmyk"/> 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>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(Cmyk left, Cmyk right) => !left.Equals(right);
/// <inheritdoc/>
public static Cmyk FromProfileConnectingSpace(ColorConversionOptions options, in Rgb source)
{
// To CMY
Vector3 cmy = Vector3.One - source.ToScaledVector3();
// To CMYK
Vector3 k = new(MathF.Min(cmy.X, MathF.Min(cmy.Y, cmy.Z)));
if (MathF.Abs(k.X - 1F) < Constants.Epsilon)
{
return new Cmyk(0, 0, 0, 1F);
}
cmy = (cmy - k) / (Vector3.One - k);
return new Cmyk(cmy.X, cmy.Y, cmy.Z, k.X);
}
/// <inheritdoc/>
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<Rgb> source, Span<Cmyk> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
// TODO: We can optimize this by using SIMD
for (int i = 0; i < source.Length; i++)
{
Rgb rgb = source[i];
destination[i] = FromProfileConnectingSpace(options, in rgb);
}
}
/// <inheritdoc/>
public Rgb ToProfileConnectingSpace(ColorConversionOptions options)
{
Vector3 rgb = (Vector3.One - new Vector3(this.C, this.M, this.Y)) * (Vector3.One - new Vector3(this.K));
return Rgb.FromScaledVector3(rgb);
}
/// <inheritdoc/>
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<Cmyk> source, Span<Rgb> destination)
{
// TODO: We can possibly optimize this by using SIMD
for (int i = 0; i < source.Length; i++)
{
Cmyk cmyk = source[i];
destination[i] = cmyk.ToProfileConnectingSpace(options);
}
}
/// <inheritdoc/>
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource()
=> ChromaticAdaptionWhitePointSource.RgbWorkingSpace;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode()
=> HashCode.Combine(this.C, this.M, this.Y, this.K);
/// <inheritdoc/>
public override string ToString()
=> FormattableString.Invariant($"Cmyk({this.C:#0.##}, {this.M:#0.##}, {this.Y:#0.##}, {this.K:#0.##})");
/// <inheritdoc/>
public override bool Equals(object? obj)
=> obj is Cmyk other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(Cmyk other)
=> this.C.Equals(other.C)
&& this.M.Equals(other.M)
&& this.Y.Equals(other.Y)
&& this.K.Equals(other.K);
}

55
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs

@ -0,0 +1,55 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsCieLabCieLab
{
public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, TFrom source)
where TFrom : struct, IColorProfile<TFrom, CieLab>
where TTo : struct, IColorProfile<TTo, CieLab>
{
ColorConversionOptions options = converter.Options;
// Convert to input PCS
CieLab pcsFromA = source.ToProfileConnectingSpace(options);
CieXyz pcsFromB = pcsFromA.ToProfileConnectingSpace(options);
// Adapt to target white point
pcsFromB = VonKriesChromaticAdaptation.Transform<TFrom, TTo>(options, in pcsFromB);
// Convert between PCS
CieLab pcsTo = CieLab.FromProfileConnectingSpace(options, in pcsFromB);
// Convert to output from PCS
return TTo.FromProfileConnectingSpace(options, pcsTo);
}
public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
where TFrom : struct, IColorProfile<TFrom, CieLab>
where TTo : struct, IColorProfile<TTo, CieLab>
{
ColorConversionOptions options = converter.Options;
// Convert to input PCS.
using IMemoryOwner<CieLab> pcsFromToOwner = options.MemoryAllocator.Allocate<CieLab>(source.Length);
Span<CieLab> pcsFromTo = pcsFromToOwner.GetSpan();
TFrom.ToProfileConnectionSpace(options, source, pcsFromTo);
using IMemoryOwner<CieXyz> pcsFromOwner = options.MemoryAllocator.Allocate<CieXyz>(source.Length);
Span<CieXyz> pcsFrom = pcsFromOwner.GetSpan();
CieLab.ToProfileConnectionSpace(options, pcsFromTo, pcsFrom);
// Adapt to target white point
VonKriesChromaticAdaptation.Transform<TFrom, TTo>(options, pcsFrom, pcsFrom);
// Convert between PCS.
CieLab.FromProfileConnectionSpace(options, pcsFrom, pcsFromTo);
// Convert to output from PCS
TTo.FromProfileConnectionSpace(options, pcsFromTo, destination);
}
}

55
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs

@ -0,0 +1,55 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsRgbRgb
{
public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, TFrom source)
where TFrom : struct, IColorProfile<TFrom, Rgb>
where TTo : struct, IColorProfile<TTo, Rgb>
{
ColorConversionOptions options = converter.Options;
// Convert to input PCS
Rgb pcsFromA = source.ToProfileConnectingSpace(options);
CieXyz pcsFromB = pcsFromA.ToProfileConnectingSpace(options);
// Adapt to target white point
pcsFromB = VonKriesChromaticAdaptation.Transform<TFrom, TTo>(options, in pcsFromB);
// Convert between PCS
Rgb pcsTo = Rgb.FromProfileConnectingSpace(options, in pcsFromB);
// Convert to output from PCS
return TTo.FromProfileConnectingSpace(options, pcsTo);
}
public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
where TFrom : struct, IColorProfile<TFrom, Rgb>
where TTo : struct, IColorProfile<TTo, Rgb>
{
ColorConversionOptions options = converter.Options;
// Convert to input PCS.
using IMemoryOwner<Rgb> pcsFromToOwner = options.MemoryAllocator.Allocate<Rgb>(source.Length);
Span<Rgb> pcsFromTo = pcsFromToOwner.GetSpan();
TFrom.ToProfileConnectionSpace(options, source, pcsFromTo);
using IMemoryOwner<CieXyz> pcsFromOwner = options.MemoryAllocator.Allocate<CieXyz>(source.Length);
Span<CieXyz> pcsFrom = pcsFromOwner.GetSpan();
Rgb.ToProfileConnectionSpace(options, pcsFromTo, pcsFrom);
// Adapt to target white point
VonKriesChromaticAdaptation.Transform<TFrom, TTo>(options, pcsFrom, pcsFrom);
// Convert between PCS.
Rgb.FromProfileConnectionSpace(options, pcsFrom, pcsFromTo);
// Convert to output from PCS
TTo.FromProfileConnectionSpace(options, pcsFromTo, destination);
}
}

7
tests/ImageSharp.Tests/ColorProfiles/ApproximateColorProfileComparer.cs

@ -18,7 +18,8 @@ internal readonly struct ApproximateColorProfileComparer :
IEqualityComparer<YCbCr>,
IEqualityComparer<CieLchuv>,
IEqualityComparer<CieLuv>,
IEqualityComparer<CieXyy>
IEqualityComparer<CieXyy>,
IEqualityComparer<Cmyk>
{
private readonly float epsilon;
@ -46,6 +47,8 @@ internal readonly struct ApproximateColorProfileComparer :
public bool Equals(CieXyy x, CieXyy y) => this.Equals(x.X, y.X) && this.Equals(x.Y, y.Y) && this.Equals(x.Yl, y.Yl);
public bool Equals(Cmyk x, Cmyk y) => this.Equals(x.C, y.C) && this.Equals(x.M, y.M) && this.Equals(x.Y, y.Y) && this.Equals(x.K, y.K);
public int GetHashCode([DisallowNull] CieLab obj) => obj.GetHashCode();
public int GetHashCode([DisallowNull] CieXyz obj) => obj.GetHashCode();
@ -64,6 +67,8 @@ internal readonly struct ApproximateColorProfileComparer :
public int GetHashCode([DisallowNull] CieXyy obj) => obj.GetHashCode();
public int GetHashCode([DisallowNull] Cmyk obj) => obj.GetHashCode();
private bool Equals(float x, float y)
{
float d = x - y;

70
tests/ImageSharp.Tests/ColorProfiles/CieLabAndCmykConversionTests.cs

@ -0,0 +1,70 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.ColorProfiles;
namespace SixLabors.ImageSharp.Tests.ColorProfiles.Conversion;
/// <summary>
/// Tests <see cref="CieLab"/>-<see cref="Cmyk"/> conversions.
/// </summary>
public class CieLabAndCmykConversionTests
{
private static readonly ApproximateColorProfileComparer Comparer = new(.0002F);
[Theory]
[InlineData(0, 0, 0, 1, 0, 0, 0)]
[InlineData(0, 1, 0.6156551, 5.960464E-08, 55.063, 82.54871, 23.16506)]
public void Convert_Cmyk_to_CieLab(float c, float m, float y, float k, float l, float a, float b)
{
// Arrange
Cmyk input = new(c, m, y, k);
CieLab expected = new(l, a, b);
ColorProfileConverter converter = new();
Span<Cmyk> inputSpan = new Cmyk[5];
inputSpan.Fill(input);
Span<CieLab> actualSpan = new CieLab[5];
// Act
CieLab actual = converter.Convert<Cmyk, CieLab>(input);
converter.Convert<Cmyk, CieLab>(inputSpan, actualSpan);
// Assert
Assert.Equal(expected, actual, Comparer);
for (int i = 0; i < actualSpan.Length; i++)
{
Assert.Equal(expected, actualSpan[i], Comparer);
}
}
[Theory]
[InlineData(0, 0, 0, 0, 0, 0, 1)]
[InlineData(36.0555, 303.6901, 10.01514, 0, 1, 0.597665966, 0)]
public void Convert_CieLab_to_Cmyk(float l, float a, float b, float c, float m, float y, float k)
{
// Arrange
CieLab input = new(l, a, b);
Cmyk expected = new(c, m, y, k);
ColorProfileConverter converter = new();
Span<CieLab> inputSpan = new CieLab[5];
inputSpan.Fill(input);
Span<Cmyk> actualSpan = new Cmyk[5];
// Act
Cmyk actual = converter.Convert<CieLab, Cmyk>(input);
converter.Convert<CieLab, Cmyk>(inputSpan, actualSpan);
// Assert
Assert.Equal(expected, actual, Comparer);
for (int i = 0; i < actualSpan.Length; i++)
{
Assert.Equal(expected, actualSpan[i], Comparer);
}
}
}

70
tests/ImageSharp.Tests/ColorProfiles/CmykAndYCbCrConversionTests.cs

@ -0,0 +1,70 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.ColorProfiles;
namespace SixLabors.ImageSharp.Tests.ColorProfiles.Conversion;
/// <summary>
/// Tests <see cref="Cmyk"/>-<see cref="YCbCr"/> conversions.
/// </summary>
public class CmykAndYCbCrConversionTests
{
private static readonly ApproximateColorProfileComparer Comparer = new(.0002F);
[Theory]
[InlineData(0, 0, 0, 0, 255, 128, 128)]
[InlineData(0.360555, 0.1036901, 0.818514, 0.274615, 136.5134, 69.90555, 114.9948)]
public void Convert_Cmyk_to_YCbCr(float c, float m, float y, float k, float y2, float cb, float cr)
{
// Arrange
Cmyk input = new(c, m, y, k);
YCbCr expected = new(y2, cb, cr);
ColorProfileConverter converter = new();
Span<Cmyk> inputSpan = new Cmyk[5];
inputSpan.Fill(input);
Span<YCbCr> actualSpan = new YCbCr[5];
// Act
YCbCr actual = converter.Convert<Cmyk, YCbCr>(input);
converter.Convert<Cmyk, YCbCr>(inputSpan, actualSpan);
// Assert
Assert.Equal(expected, actual, Comparer);
for (int i = 0; i < actualSpan.Length; i++)
{
Assert.Equal(expected, actualSpan[i], Comparer);
}
}
[Theory]
[InlineData(255, 128, 128, 0, 0, 0, 5.960464E-08)]
[InlineData(136.5134, 69.90555, 114.9948, 0.2891567, 0, 0.7951807, 0.3490196)]
public void Convert_YCbCr_to_Cmyk(float y2, float cb, float cr, float c, float m, float y, float k)
{
// Arrange
YCbCr input = new(y2, cb, cr);
Cmyk expected = new(c, m, y, k);
ColorProfileConverter converter = new();
Span<YCbCr> inputSpan = new YCbCr[5];
inputSpan.Fill(input);
Span<Cmyk> actualSpan = new Cmyk[5];
// Act
Cmyk actual = converter.Convert<YCbCr, Cmyk>(input);
converter.Convert<YCbCr, Cmyk>(inputSpan, actualSpan);
// Assert
Assert.Equal(expected, actual, Comparer);
for (int i = 0; i < actualSpan.Length; i++)
{
Assert.Equal(expected, actualSpan[i], Comparer);
}
}
}

77
tests/ImageSharp.Tests/ColorProfiles/RgbAndCmykConversionTest.cs

@ -0,0 +1,77 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.ColorProfiles;
namespace SixLabors.ImageSharp.Tests.ColorProfiles.Conversion;
/// <summary>
/// Tests <see cref="Rgb"/>-<see cref="Cmyk"/> conversions.
/// </summary>
/// <remarks>
/// Test data generated using:
/// <see href="http://www.colorhexa.com"/>
/// <see href="http://www.rapidtables.com/convert/color/cmyk-to-rgb.htm"/>
/// </remarks>
public class RgbAndCmykConversionTest
{
private static readonly ApproximateColorProfileComparer Comparer = new(.0001F);
[Theory]
[InlineData(1, 1, 1, 1, 0, 0, 0)]
[InlineData(0, 0, 0, 0, 1, 1, 1)]
[InlineData(0, 0.84, 0.037, 0.365, 0.635, 0.1016, 0.6115)]
public void Convert_Cmyk_To_Rgb(float c, float m, float y, float k, float r, float g, float b)
{
// Arrange
Cmyk input = new(c, m, y, k);
Rgb expected = new(r, g, b);
ColorProfileConverter converter = new();
Span<Cmyk> inputSpan = new Cmyk[5];
inputSpan.Fill(input);
Span<Rgb> actualSpan = new Rgb[5];
// Act
Rgb actual = converter.Convert<Cmyk, Rgb>(input);
converter.Convert<Cmyk, Rgb>(inputSpan, actualSpan);
// Assert
Assert.Equal(expected, actual, Comparer);
for (int i = 0; i < actualSpan.Length; i++)
{
Assert.Equal(expected, actualSpan[i], Comparer);
}
}
[Theory]
[InlineData(1, 1, 1, 0, 0, 0, 0)]
[InlineData(0, 0, 0, 0, 0, 0, 1)]
[InlineData(0.635, 0.1016, 0.6115, 0, 0.84, 0.037, 0.365)]
public void Convert_Rgb_To_Cmyk(float r, float g, float b, float c, float m, float y, float k)
{
// Arrange
Rgb input = new(r, g, b);
Cmyk expected = new(c, m, y, k);
ColorProfileConverter converter = new();
Span<Rgb> inputSpan = new Rgb[5];
inputSpan.Fill(input);
Span<Cmyk> actualSpan = new Cmyk[5];
// Act
Cmyk actual = converter.Convert<Rgb, Cmyk>(input);
converter.Convert<Rgb, Cmyk>(inputSpan, actualSpan);
// Assert
Assert.Equal(expected, actual, Comparer);
for (int i = 0; i < actualSpan.Length; i++)
{
Assert.Equal(expected, actualSpan[i], Comparer);
}
}
}
Loading…
Cancel
Save