diff --git a/src/ImageSharp/ColorProfiles/ChromaticAdaptionWhitePointSource.cs b/src/ImageSharp/ColorProfiles/ChromaticAdaptionWhitePointSource.cs new file mode 100644 index 0000000000..7e4a9c413b --- /dev/null +++ b/src/ImageSharp/ColorProfiles/ChromaticAdaptionWhitePointSource.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.ColorProfiles; + +/// +/// Enumerate the possible sources of the white point used in chromatic adaptation. +/// +public enum ChromaticAdaptionWhitePointSource +{ + /// + /// The white point of the source color space. + /// + WhitePoint, + + /// + /// The white point of the source working space. + /// + RgbWorkingSpace +} diff --git a/src/ImageSharp/ColorProfiles/CieLab.cs b/src/ImageSharp/ColorProfiles/CieLab.cs index 248559f731..3578e27139 100644 --- a/src/ImageSharp/ColorProfiles/CieLab.cs +++ b/src/ImageSharp/ColorProfiles/CieLab.cs @@ -172,4 +172,7 @@ public readonly struct CieLab : IProfileConnectingSpace destination[i] = lab.ToProfileConnectingSpace(options); } } + + /// + public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() => ChromaticAdaptionWhitePointSource.WhitePoint; } diff --git a/src/ImageSharp/ColorProfiles/CieLch.cs b/src/ImageSharp/ColorProfiles/CieLch.cs index 4b48e1c8d9..23f250d0c8 100644 --- a/src/ImageSharp/ColorProfiles/CieLch.cs +++ b/src/ImageSharp/ColorProfiles/CieLch.cs @@ -163,4 +163,7 @@ public readonly struct CieLch : IColorProfile destination[i] = lch.ToProfileConnectingSpace(options); } } + + /// + public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() => ChromaticAdaptionWhitePointSource.WhitePoint; } diff --git a/src/ImageSharp/ColorProfiles/CieXyChromaticityCoordinates.cs b/src/ImageSharp/ColorProfiles/CieXyChromaticityCoordinates.cs new file mode 100644 index 0000000000..b55abdd05a --- /dev/null +++ b/src/ImageSharp/ColorProfiles/CieXyChromaticityCoordinates.cs @@ -0,0 +1,83 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +// ReSharper disable CompareOfFloatsByEqualityOperator +namespace SixLabors.ImageSharp.ColorProfiles; + +/// +/// Represents the coordinates of CIEXY chromaticity space. +/// +public readonly struct CieXyChromaticityCoordinates : IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// Chromaticity coordinate x (usually from 0 to 1) + /// Chromaticity coordinate y (usually from 0 to 1) + [MethodImpl(InliningOptions.ShortMethod)] + public CieXyChromaticityCoordinates(float x, float y) + { + this.X = x; + this.Y = y; + } + + /// + /// Gets the chromaticity X-coordinate. + /// + /// + /// Ranges usually from 0 to 1. + /// + public readonly float X { get; } + + /// + /// Gets the chromaticity Y-coordinate + /// + /// + /// Ranges usually from 0 to 1. + /// + public readonly float Y { get; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public static bool operator ==(CieXyChromaticityCoordinates left, CieXyChromaticityCoordinates right) + => left.Equals(right); + + /// + /// Compares two objects for inequality + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public static bool operator !=(CieXyChromaticityCoordinates left, CieXyChromaticityCoordinates right) + => !left.Equals(right); + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public override int GetHashCode() + => HashCode.Combine(this.X, this.Y); + + /// + public override string ToString() + => FormattableString.Invariant($"CieXyChromaticityCoordinates({this.X:#0.##}, {this.Y:#0.##})"); + + /// + public override bool Equals(object? obj) + => obj is CieXyChromaticityCoordinates other && this.Equals(other); + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool Equals(CieXyChromaticityCoordinates other) + => this.X.Equals(other.X) && this.Y.Equals(other.Y); +} diff --git a/src/ImageSharp/ColorProfiles/CieXyz.cs b/src/ImageSharp/ColorProfiles/CieXyz.cs index b22ab16f92..ec28a232da 100644 --- a/src/ImageSharp/ColorProfiles/CieXyz.cs +++ b/src/ImageSharp/ColorProfiles/CieXyz.cs @@ -121,4 +121,7 @@ public readonly struct CieXyz : IProfileConnectingSpace Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); source.CopyTo(destination[..source.Length]); } + + /// + public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() => ChromaticAdaptionWhitePointSource.WhitePoint; } diff --git a/src/ImageSharp/ColorProfiles/ColorConversionOptions.cs b/src/ImageSharp/ColorProfiles/ColorConversionOptions.cs index de32aa54d3..384ab6a7f7 100644 --- a/src/ImageSharp/ColorProfiles/ColorConversionOptions.cs +++ b/src/ImageSharp/ColorProfiles/ColorConversionOptions.cs @@ -2,6 +2,8 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.ColorProfiles; @@ -33,6 +35,16 @@ public class ColorConversionOptions /// public CieXyz TargetWhitePoint { get; init; } = Illuminants.D50; + /// + /// Gets the source working space used for companding in conversions from/to XYZ color space. + /// + public RgbWorkingSpace RgbWorkingSpace { get; init; } = RgbWorkingSpaces.SRgb; + + /// + /// Gets the destination working space used for companding in conversions from/to XYZ color space. + /// + public RgbWorkingSpace TargetRgbWorkingSpace { get; init; } = RgbWorkingSpaces.SRgb; + /// /// Gets the transformation matrix used in conversion to perform chromatic adaptation. /// diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs index 481280b85e..8860777cd4 100644 --- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs @@ -21,7 +21,7 @@ internal static class ColorProfileConverterExtensionsCieLabCieXyz CieXyz pcsTo = pcsFrom.ToProfileConnectingSpace(options); // Adapt to target white point - VonKriesChromaticAdaptation.Transform(options, in pcsTo); + VonKriesChromaticAdaptation.Transform(options, in pcsTo); // Convert to output from PCS return TTo.FromProfileConnectingSpace(options, pcsTo); @@ -44,7 +44,7 @@ internal static class ColorProfileConverterExtensionsCieLabCieXyz CieLab.ToProfileConnectionSpace(options, pcsFrom, pcsTo); // Adapt to target white point - VonKriesChromaticAdaptation.Transform(options, pcsTo, pcsTo); + VonKriesChromaticAdaptation.Transform(options, pcsTo, pcsTo); // Convert to output from PCS TTo.FromProfileConnectionSpace(options, pcsTo, destination); diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs index 949cd6c8e9..50317311c0 100644 --- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Buffers; @@ -18,7 +18,7 @@ internal static class ColorProfileConverterExtensionsCieXyzCieLab CieXyz pcsFrom = source.ToProfileConnectingSpace(options); // Adapt to target white point - VonKriesChromaticAdaptation.Transform(options, in pcsFrom); + VonKriesChromaticAdaptation.Transform(options, in pcsFrom); // Convert between PCS CieLab pcsTo = CieLab.FromProfileConnectingSpace(options, in pcsFrom); @@ -39,7 +39,7 @@ internal static class ColorProfileConverterExtensionsCieXyzCieLab TFrom.ToProfileConnectionSpace(options, source, pcsFrom); // Adapt to target white point - VonKriesChromaticAdaptation.Transform(options, pcsFrom, pcsFrom); + VonKriesChromaticAdaptation.Transform(options, pcsFrom, pcsFrom); // Convert between PCS. using IMemoryOwner pcsToOwner = options.MemoryAllocator.Allocate(source.Length); diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs index ebe07003e9..2380bb15eb 100644 --- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs @@ -18,7 +18,7 @@ internal static class ColorProfileConverterExtensionsCieXyzCieXyz CieXyz pcsFrom = source.ToProfileConnectingSpace(options); // Adapt to target white point - VonKriesChromaticAdaptation.Transform(options, in pcsFrom); + VonKriesChromaticAdaptation.Transform(options, in pcsFrom); // Convert between PCS CieXyz pcsTo = CieXyz.FromProfileConnectingSpace(options, in pcsFrom); @@ -39,7 +39,7 @@ internal static class ColorProfileConverterExtensionsCieXyzCieXyz TFrom.ToProfileConnectionSpace(options, source, pcsFrom); // Adapt to target white point - VonKriesChromaticAdaptation.Transform(options, pcsFrom, pcsFrom); + VonKriesChromaticAdaptation.Transform(options, pcsFrom, pcsFrom); // Convert between PCS. using IMemoryOwner pcsToOwner = options.MemoryAllocator.Allocate(source.Length); diff --git a/src/ImageSharp/ColorProfiles/Companding/CompandingUtilities.cs b/src/ImageSharp/ColorProfiles/Companding/CompandingUtilities.cs new file mode 100644 index 0000000000..44dae35a98 --- /dev/null +++ b/src/ImageSharp/ColorProfiles/Companding/CompandingUtilities.cs @@ -0,0 +1,171 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Concurrent; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp.ColorProfiles.Companding; + +/// +/// Companding utilities that allow the accelerated compression-expansion of color channels. +/// +public static class CompandingUtilities +{ + private const int Length = Scale + 2; // 256kb @ 16bit precision. + private const int Scale = (1 << 16) - 1; + private static readonly ConcurrentDictionary<(Type, double), Lazy> LookupTables = new(); + + /// + /// Lazily creates and stores a companding lookup table using the given function and modifier. + /// + /// The type of companding function. + /// The companding function. + /// A modifier to pass to the function. + /// The array. + public static Lazy GetLookupTable(Func compandingFunction, double modifier = 0) + => LookupTables.GetOrAdd((typeof(T), modifier), args => new(() => CreateLookupTableImpl(compandingFunction, args.Item2), true)); + + /// + /// Creates a companding lookup table using the given function. + /// + /// The companding function. + /// A modifier to pass to the function. + /// The array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float[] CreateLookupTableImpl(Func compandingFunction, double modifier = 0) + { + float[] result = new float[Length]; + + for (int i = 0; i < result.Length; i++) + { + double d = (double)i / Scale; + d = compandingFunction(d, modifier); + result[i] = (float)d; + } + + return result; + } + + /// + /// Performs the companding operation on the given vectors using the given table. + /// + /// The span of vectors. + /// The lookup table. + public static void Compand(Span vectors, float[] table) + { + DebugGuard.MustBeGreaterThanOrEqualTo(table.Length, Length, nameof(table)); + + if (Avx2.IsSupported && vectors.Length >= 2) + { + CompandAvx2(vectors, table); + + if (Numerics.Modulo2(vectors.Length) != 0) + { + // Vector4 fits neatly in pairs. Any overlap has to be equal to 1. + ref Vector4 last = ref MemoryMarshal.GetReference(vectors[^1..]); + last = Compand(last, table); + } + } + else + { + CompandScalar(vectors, table); + } + } + + /// + /// Performs the companding operation on the given vector using the given table. + /// + /// The vector. + /// The lookup table. + /// The + public static Vector4 Compand(Vector4 vector, float[] table) + { + DebugGuard.MustBeGreaterThanOrEqualTo(table.Length, Length, nameof(table)); + + Vector4 zero = Vector4.Zero; + Vector4 scale = new(Scale); + + Vector4 multiplied = Numerics.Clamp(vector * Scale, zero, scale); + + float f0 = multiplied.X; + float f1 = multiplied.Y; + float f2 = multiplied.Z; + + uint i0 = (uint)f0; + uint i1 = (uint)f1; + uint i2 = (uint)f2; + + // Alpha is already a linear representation of opacity so we do not want to convert it. + vector.X = Numerics.Lerp(table[i0], table[i0 + 1], f0 - (int)i0); + vector.Y = Numerics.Lerp(table[i1], table[i1 + 1], f1 - (int)i1); + vector.Z = Numerics.Lerp(table[i2], table[i2 + 1], f2 - (int)i2); + + return vector; + } + + private static unsafe void CompandAvx2(Span vectors, float[] table) + { + fixed (float* tablePointer = &MemoryMarshal.GetArrayDataReference(table)) + { + Vector256 scale = Vector256.Create((float)Scale); + Vector256 zero = Vector256.Zero; + Vector256 offset = Vector256.Create(1); + + // Divide by 2 as 4 elements per Vector4 and 8 per Vector256 + ref Vector256 vectorsBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vectors)); + ref Vector256 vectorsLast = ref Unsafe.Add(ref vectorsBase, (uint)vectors.Length / 2u); + + while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsLast)) + { + Vector256 multiplied = Avx.Multiply(scale, vectorsBase); + multiplied = Avx.Min(Avx.Max(zero, multiplied), scale); + + Vector256 truncated = Avx.ConvertToVector256Int32WithTruncation(multiplied); + Vector256 truncatedF = Avx.ConvertToVector256Single(truncated); + + Vector256 low = Avx2.GatherVector256(tablePointer, truncated, sizeof(float)); + Vector256 high = Avx2.GatherVector256(tablePointer, Avx2.Add(truncated, offset), sizeof(float)); + + // Alpha is already a linear representation of opacity so we do not want to convert it. + Vector256 companded = Numerics.Lerp(low, high, Avx.Subtract(multiplied, truncatedF)); + vectorsBase = Avx.Blend(companded, vectorsBase, Numerics.BlendAlphaControl); + vectorsBase = ref Unsafe.Add(ref vectorsBase, 1); + } + } + } + + private static unsafe void CompandScalar(Span vectors, float[] table) + { + fixed (float* tablePointer = &MemoryMarshal.GetArrayDataReference(table)) + { + Vector4 zero = Vector4.Zero; + Vector4 scale = new(Scale); + ref Vector4 vectorsBase = ref MemoryMarshal.GetReference(vectors); + ref Vector4 vectorsLast = ref Unsafe.Add(ref vectorsBase, (uint)vectors.Length); + + while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsLast)) + { + Vector4 multiplied = Numerics.Clamp(vectorsBase * Scale, zero, scale); + + float f0 = multiplied.X; + float f1 = multiplied.Y; + float f2 = multiplied.Z; + + uint i0 = (uint)f0; + uint i1 = (uint)f1; + uint i2 = (uint)f2; + + // Alpha is already a linear representation of opacity so we do not want to convert it. + vectorsBase.X = Numerics.Lerp(tablePointer[i0], tablePointer[i0 + 1], f0 - (int)i0); + vectorsBase.Y = Numerics.Lerp(tablePointer[i1], tablePointer[i1 + 1], f1 - (int)i1); + vectorsBase.Z = Numerics.Lerp(tablePointer[i2], tablePointer[i2 + 1], f2 - (int)i2); + + vectorsBase = ref Unsafe.Add(ref vectorsBase, 1); + } + } + } +} diff --git a/src/ImageSharp/ColorProfiles/Companding/GammaCompanding.cs b/src/ImageSharp/ColorProfiles/Companding/GammaCompanding.cs new file mode 100644 index 0000000000..28e9a46414 --- /dev/null +++ b/src/ImageSharp/ColorProfiles/Companding/GammaCompanding.cs @@ -0,0 +1,56 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.ColorProfiles.Companding; + +/// +/// Implements gamma companding. +/// +/// +/// +/// +/// +public static class GammaCompanding +{ + private static Func CompressFunction => (d, m) => Math.Pow(d, 1 / m); + + private static Func ExpandFunction => Math.Pow; + + /// + /// Compresses the linear vectors to their nonlinear equivalents with respect to the energy. + /// + /// The span of vectors. + /// The gamma value. + public static void Compress(Span vectors, double gamma) + => CompandingUtilities.Compand(vectors, CompandingUtilities.GetLookupTable(CompressFunction, gamma).Value); + + /// + /// Expands the nonlinear vectors to their linear equivalents with respect to the energy. + /// + /// The span of vectors. + /// The gamma value. + public static void Expand(Span vectors, double gamma) + => CompandingUtilities.Compand(vectors, CompandingUtilities.GetLookupTable(ExpandFunction, gamma).Value); + + /// + /// Compresses the linear vector to its nonlinear equivalent with respect to the energy. + /// + /// The vector. + /// The gamma value. + /// The . + public static Vector4 Compress(Vector4 vector, double gamma) + => CompandingUtilities.Compand(vector, CompandingUtilities.GetLookupTable(CompressFunction, gamma).Value); + + /// + /// Compresses the linear vector to its nonlinear equivalent with respect to the energy. + /// + /// The vector. + /// The gamma value. + /// The . + public static Vector4 Expand(Vector4 vector, double gamma) + => CompandingUtilities.Compand(vector, CompandingUtilities.GetLookupTable(ExpandFunction, gamma).Value); + + private class GammaCompandingKey; +} diff --git a/src/ImageSharp/ColorProfiles/Companding/LCompanding.cs b/src/ImageSharp/ColorProfiles/Companding/LCompanding.cs new file mode 100644 index 0000000000..79349d1c53 --- /dev/null +++ b/src/ImageSharp/ColorProfiles/Companding/LCompanding.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.ColorProfiles.Companding; + +/// +/// Implements L* companding. +/// +/// +/// For more info see: +/// +/// +/// +public static class LCompanding +{ + private static Func CompressFunction + => (d, _) => + { + if (d <= CieConstants.Epsilon) + { + return (d * CieConstants.Kappa) / 100; + } + + return (1.16 * Math.Pow(d, 0.3333333)) - 0.16; + }; + + private static Func ExpandFunction + => (d, _) => + { + if (d <= 0.08) + { + return (100 * d) / CieConstants.Kappa; + } + + return Numerics.Pow3(((float)(d + 0.16f)) / 1.16f); + }; + + /// + /// Compresses the linear vectors to their nonlinear equivalents with respect to the energy. + /// + /// The span of vectors. + public static void Compress(Span vectors) + => CompandingUtilities.Compand(vectors, CompandingUtilities.GetLookupTable(CompressFunction).Value); + + /// + /// Expands the nonlinear vectors to their linear equivalents with respect to the energy. + /// + /// The span of vectors. + public static void Expand(Span vectors) + => CompandingUtilities.Compand(vectors, CompandingUtilities.GetLookupTable(ExpandFunction).Value); + + /// + /// Compresses the linear vector to its nonlinear equivalent with respect to the energy. + /// + /// The vector. + /// The . + public static Vector4 Compress(Vector4 vector) + => CompandingUtilities.Compand(vector, CompandingUtilities.GetLookupTable(CompressFunction).Value); + + /// + /// Compresses the linear vector to its nonlinear equivalent with respect to the energy. + /// + /// The vector. + /// The . + public static Vector4 Expand(Vector4 vector) + => CompandingUtilities.Compand(vector, CompandingUtilities.GetLookupTable(ExpandFunction).Value); + + private class LCompandingKey; +} diff --git a/src/ImageSharp/ColorProfiles/Companding/Rec2020Companding.cs b/src/ImageSharp/ColorProfiles/Companding/Rec2020Companding.cs new file mode 100644 index 0000000000..9beab3a86c --- /dev/null +++ b/src/ImageSharp/ColorProfiles/Companding/Rec2020Companding.cs @@ -0,0 +1,75 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.ColorProfiles.Companding; + +/// +/// Implements Rec. 2020 companding function. +/// +/// +/// +/// +public static class Rec2020Companding +{ + private const double Alpha = 1.09929682680944; + private const double AlphaMinusOne = Alpha - 1; + private const double Beta = 0.018053968510807; + private const double InverseBeta = Beta * 4.5; + private const double Epsilon = 1 / 0.45; + + private static Func CompressFunction + => (d, _) => + { + if (d < Beta) + { + return 4.5 * d; + } + + return (Alpha * Math.Pow(d, 0.45)) - AlphaMinusOne; + }; + + private static Func ExpandFunction + => (d, _) => + { + if (d < InverseBeta) + { + return d / 4.5; + } + + return Math.Pow((d + AlphaMinusOne) / Alpha, Epsilon); + }; + + /// + /// Compresses the linear vectors to their nonlinear equivalents with respect to the energy. + /// + /// The span of vectors. + public static void Compress(Span vectors) + => CompandingUtilities.Compand(vectors, CompandingUtilities.GetLookupTable(CompressFunction).Value); + + /// + /// Expands the nonlinear vectors to their linear equivalents with respect to the energy. + /// + /// The span of vectors. + public static void Expand(Span vectors) + => CompandingUtilities.Compand(vectors, CompandingUtilities.GetLookupTable(ExpandFunction).Value); + + /// + /// Compresses the linear vector to its nonlinear equivalent with respect to the energy. + /// + /// The vector. + /// The . + public static Vector4 Compress(Vector4 vector) + => CompandingUtilities.Compand(vector, CompandingUtilities.GetLookupTable(CompressFunction).Value); + + /// + /// Compresses the linear vector to its nonlinear equivalent with respect to the energy. + /// + /// The vector. + /// The . + public static Vector4 Expand(Vector4 vector) + => CompandingUtilities.Compand(vector, CompandingUtilities.GetLookupTable(ExpandFunction).Value); + + private class Rec2020CompandingKey; +} diff --git a/src/ImageSharp/ColorProfiles/Companding/Rec709Companding.cs b/src/ImageSharp/ColorProfiles/Companding/Rec709Companding.cs new file mode 100644 index 0000000000..93a32672c7 --- /dev/null +++ b/src/ImageSharp/ColorProfiles/Companding/Rec709Companding.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.ColorProfiles.Companding; + +/// +/// Implements the Rec. 709 companding function. +/// +/// +/// http://en.wikipedia.org/wiki/Rec._709 +/// +public static class Rec709Companding +{ + private const double Epsilon = 1 / 0.45; + + private static Func CompressFunction + => (d, _) => + { + if (d < 0.018) + { + return 4.5 * d; + } + + return (1.099 * Math.Pow(d, 0.45)) - 0.099; + }; + + private static Func ExpandFunction + => (d, _) => + { + if (d < 0.081) + { + return d / 4.5; + } + + return Math.Pow((d + 0.099) / 1.099, Epsilon); + }; + + /// + /// Compresses the linear vectors to their nonlinear equivalents with respect to the energy. + /// + /// The span of vectors. + public static void Compress(Span vectors) + => CompandingUtilities.Compand(vectors, CompandingUtilities.GetLookupTable(CompressFunction).Value); + + /// + /// Expands the nonlinear vectors to their linear equivalents with respect to the energy. + /// + /// The span of vectors. + public static void Expand(Span vectors) + => CompandingUtilities.Compand(vectors, CompandingUtilities.GetLookupTable(ExpandFunction).Value); + + /// + /// Compresses the linear vector to its nonlinear equivalent with respect to the energy. + /// + /// The vector. + /// The . + public static Vector4 Compress(Vector4 vector) + => CompandingUtilities.Compand(vector, CompandingUtilities.GetLookupTable(CompressFunction).Value); + + /// + /// Compresses the linear vector to its nonlinear equivalent with respect to the energy. + /// + /// The vector. + /// The . + public static Vector4 Expand(Vector4 vector) + => CompandingUtilities.Compand(vector, CompandingUtilities.GetLookupTable(ExpandFunction).Value); + + private class Rec2020CompandingKey; +} diff --git a/src/ImageSharp/ColorProfiles/Companding/SRgbCompanding.cs b/src/ImageSharp/ColorProfiles/Companding/SRgbCompanding.cs new file mode 100644 index 0000000000..04e4007a8c --- /dev/null +++ b/src/ImageSharp/ColorProfiles/Companding/SRgbCompanding.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.ColorProfiles.Companding; + +/// +/// Implements sRGB companding. +/// +/// +/// For more info see: +/// +/// +/// +public static class SRgbCompanding +{ + private static Func CompressFunction + => (d, _) => + { + if (d <= (0.04045 / 12.92)) + { + return d * 12.92; + } + + return (1.055 * Math.Pow(d, 1.0 / 2.4)) - 0.055; + }; + + private static Func ExpandFunction + => (d, _) => + { + if (d <= 0.04045) + { + return d / 12.92; + } + + return Math.Pow((d + 0.055) / 1.055, 2.4); + }; + + /// + /// Compresses the linear vectors to their nonlinear equivalents with respect to the energy. + /// + /// The span of vectors. + public static void Compress(Span vectors) + => CompandingUtilities.Compand(vectors, CompandingUtilities.GetLookupTable(CompressFunction).Value); + + /// + /// Expands the nonlinear vectors to their linear equivalents with respect to the energy. + /// + /// The span of vectors. + public static void Expand(Span vectors) + => CompandingUtilities.Compand(vectors, CompandingUtilities.GetLookupTable(ExpandFunction).Value); + + /// + /// Compresses the linear vector to its nonlinear equivalent with respect to the energy. + /// + /// The vector. + /// The . + public static Vector4 Compress(Vector4 vector) + => CompandingUtilities.Compand(vector, CompandingUtilities.GetLookupTable(CompressFunction).Value); + + /// + /// Compresses the linear vector to its nonlinear equivalent with respect to the energy. + /// + /// The vector. + /// The . + public static Vector4 Expand(Vector4 vector) + => CompandingUtilities.Compand(vector, CompandingUtilities.GetLookupTable(ExpandFunction).Value); + + private class SRgbCompandingKey; +} diff --git a/src/ImageSharp/ColorProfiles/IColorProfile{TSelf,TProfileSpace}.cs b/src/ImageSharp/ColorProfiles/IColorProfile.cs similarity index 82% rename from src/ImageSharp/ColorProfiles/IColorProfile{TSelf,TProfileSpace}.cs rename to src/ImageSharp/ColorProfiles/IColorProfile.cs index e1d4cae355..ac4b425fe2 100644 --- a/src/ImageSharp/ColorProfiles/IColorProfile{TSelf,TProfileSpace}.cs +++ b/src/ImageSharp/ColorProfiles/IColorProfile.cs @@ -1,24 +1,29 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. namespace SixLabors.ImageSharp.ColorProfiles; +/// +/// Defines the contract for all color profiles. +/// +public interface IColorProfile +{ + /// + /// Gets the chromatic adaption white point source. + /// + /// The . + public static abstract ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource(); +} + /// /// Defines the contract for all color profiles. /// /// The type of color profile. /// The type of color profile connecting space. -public interface IColorProfile : IEquatable +public interface IColorProfile : IColorProfile, IEquatable where TSelf : IColorProfile where TProfileSpace : struct, IProfileConnectingSpace { - /// - /// Converts the color to the profile connection space. - /// - /// The color profile conversion options. - /// The . - public TProfileSpace ToProfileConnectingSpace(ColorConversionOptions options); - #pragma warning disable CA1000 // Do not declare static members on generic types /// /// Converts the color from the profile connection space. @@ -28,14 +33,6 @@ public interface IColorProfile : IEquatable /// The . public static abstract TSelf FromProfileConnectingSpace(ColorConversionOptions options, in TProfileSpace source); - /// - /// Converts the span of colors to the profile connection space. - /// - /// The color profile conversion options. - /// The color span to convert from. - /// The color profile span to write the results to. - public static abstract void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination); - /// /// Converts the span of colors from the profile connection space. /// @@ -43,5 +40,20 @@ public interface IColorProfile : IEquatable /// The color profile span to convert from. /// The color span to write the results to. public static abstract void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination); + + /// + /// Converts the color to the profile connection space. + /// + /// The color profile conversion options. + /// The . + public TProfileSpace ToProfileConnectingSpace(ColorConversionOptions options); + + /// + /// Converts the span of colors to the profile connection space. + /// + /// The color profile conversion options. + /// The color span to convert from. + /// The color profile span to write the results to. + public static abstract void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination); #pragma warning restore CA1000 // Do not declare static members on generic types } diff --git a/src/ImageSharp/ColorProfiles/Lms.cs b/src/ImageSharp/ColorProfiles/Lms.cs index e11c1ca87a..cb060185df 100644 --- a/src/ImageSharp/ColorProfiles/Lms.cs +++ b/src/ImageSharp/ColorProfiles/Lms.cs @@ -133,4 +133,7 @@ internal readonly struct Lms : IColorProfile destination[i] = lms.ToProfileConnectingSpace(options); } } + + /// + public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() => ChromaticAdaptionWhitePointSource.WhitePoint; } diff --git a/src/ImageSharp/ColorProfiles/Rgb.cs b/src/ImageSharp/ColorProfiles/Rgb.cs new file mode 100644 index 0000000000..2669d2c963 --- /dev/null +++ b/src/ImageSharp/ColorProfiles/Rgb.cs @@ -0,0 +1,246 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces; + +namespace SixLabors.ImageSharp.ColorProfiles; + +internal readonly struct Rgb : IProfileConnectingSpace +{ + private static readonly Vector3 Min = Vector3.Zero; + private static readonly Vector3 Max = Vector3.One; + + /// + /// Initializes a new instance of the struct. + /// + /// The red component ranging between 0 and 1. + /// The green component ranging between 0 and 1. + /// The blue component ranging between 0 and 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgb(float r, float g, float b) + : this(new Vector3(r, g, b)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the r, g, b components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Rgb(Vector3 source) + { + source = Vector3.Clamp(source, Min, Max); + this.R = source.X; + this.G = source.Y; + this.B = source.Z; + } + + /// + /// Gets the red component. + /// A value usually ranging between 0 and 1. + /// + public readonly float R { get; } + + /// + /// Gets the green component. + /// A value usually ranging between 0 and 1. + /// + public readonly float G { get; } + + /// + /// Gets the blue component. + /// A value usually ranging between 0 and 1. + /// + public readonly float B { get; } + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Rgb left, Rgb right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Rgb left, Rgb right) => !left.Equals(right); + + /// + public override int GetHashCode() => HashCode.Combine(this.R, this.G, this.B); + + /// + public override string ToString() => FormattableString.Invariant($"Rgb({this.R:#0.##}, {this.G:#0.##}, {this.B:#0.##})"); + + /// + public override bool Equals(object? obj) => obj is Rgb other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Rgb other) + => this.R.Equals(other.R) + && this.G.Equals(other.G) + && this.B.Equals(other.B); + + /// + public static Rgb FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source) + { + // Convert to linear rgb then compress. + Rgb linear = new(Vector3.Transform(source.ToVector3(), GetCieXyzToRgbMatrix(options.TargetRgbWorkingSpace))); + return FromScaledVector4(options.TargetRgbWorkingSpace.Compress(linear.ToScaledVector4())); + } + + /// + public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + Matrix4x4 matrix = GetCieXyzToRgbMatrix(options.TargetRgbWorkingSpace); + for (int i = 0; i < source.Length; i++) + { + // Convert to linear rgb then compress. + Rgb linear = new(Vector3.Transform(source[i].ToVector3(), matrix)); + Vector4 nonlinear = options.TargetRgbWorkingSpace.Compress(linear.ToScaledVector4()); + destination[i] = FromScaledVector4(nonlinear); + } + } + + /// + public CieXyz ToProfileConnectingSpace(ColorConversionOptions options) + { + // First expand to linear rgb + Rgb linear = FromScaledVector4(options.RgbWorkingSpace.Expand(this.ToScaledVector4())); + + // Then convert to xyz + return new CieXyz(Vector3.Transform(linear.ToScaledVector3(), GetRgbToCieXyzMatrix(options.RgbWorkingSpace))); + } + + /// + public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan source, Span destination) + { + Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); + + Matrix4x4 matrix = GetRgbToCieXyzMatrix(options.RgbWorkingSpace); + for (int i = 0; i < source.Length; i++) + { + Rgb rgb = source[i]; + + // First expand to linear rgb + Rgb linear = FromScaledVector4(options.RgbWorkingSpace.Expand(rgb.ToScaledVector4())); + + // Then convert to xyz + destination[i] = new CieXyz(Vector3.Transform(linear.ToScaledVector3(), matrix)); + } + } + + /// + public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() => ChromaticAdaptionWhitePointSource.RgbWorkingSpace; + + /// + /// Initializes the pixel instance from a generic scaled . + /// + /// The vector to load the pixel from. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb FromScaledVector3(Vector3 source) => new(source); + + /// + /// Initializes the pixel instance from a generic scaled . + /// + /// The vector to load the pixel from. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb FromScaledVector4(Vector4 source) => new(source.X, source.Y, source.Z); + + /// + /// Expands the color into a generic ("scaled") representation + /// with values scaled and clamped between 0 and 1. + /// The vector components are typically expanded in least to greatest significance order. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector3 ToScaledVector3() => new(this.R, this.G, this.B); + + /// + /// Expands the color into a generic ("scaled") representation + /// with values scaled and clamped between 0 and 1. + /// The vector components are typically expanded in least to greatest significance order. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 ToScaledVector4() => new(this.R, this.G, this.B, 1f); + + private static Matrix4x4 GetCieXyzToRgbMatrix(RgbWorkingSpace workingSpace) + { + Matrix4x4 matrix = GetRgbToCieXyzMatrix(workingSpace); + Matrix4x4.Invert(matrix, out Matrix4x4 inverseMatrix); + return inverseMatrix; + } + + private static Matrix4x4 GetRgbToCieXyzMatrix(RgbWorkingSpace workingSpace) + { + DebugGuard.NotNull(workingSpace, nameof(workingSpace)); + RgbPrimariesChromaticityCoordinates chromaticity = workingSpace.ChromaticityCoordinates; + + float xr = chromaticity.R.X; + float xg = chromaticity.G.X; + float xb = chromaticity.B.X; + float yr = chromaticity.R.Y; + float yg = chromaticity.G.Y; + float yb = chromaticity.B.Y; + + float mXr = xr / yr; + float mZr = (1 - xr - yr) / yr; + + float mXg = xg / yg; + float mZg = (1 - xg - yg) / yg; + + float mXb = xb / yb; + float mZb = (1 - xb - yb) / yb; + + Matrix4x4 xyzMatrix = new() + { + M11 = mXr, + M21 = mXg, + M31 = mXb, + M12 = 1F, + M22 = 1F, + M32 = 1F, + M13 = mZr, + M23 = mZg, + M33 = mZb, + M44 = 1F + }; + + Matrix4x4.Invert(xyzMatrix, out Matrix4x4 inverseXyzMatrix); + + Vector3 vector = Vector3.Transform(workingSpace.WhitePoint.ToVector3(), inverseXyzMatrix); + + // Use transposed Rows/Columns + // TODO: Is there a built in method for this multiplication? + return new Matrix4x4 + { + M11 = vector.X * mXr, + M21 = vector.Y * mXg, + M31 = vector.Z * mXb, + M12 = vector.X * 1, + M22 = vector.Y * 1, + M32 = vector.Z * 1, + M13 = vector.X * mZr, + M23 = vector.Y * mZg, + M33 = vector.Z * mZb, + M44 = 1F + }; + } +} diff --git a/src/ImageSharp/ColorProfiles/RgbPrimariesChromaticityCoordinates.cs b/src/ImageSharp/ColorProfiles/RgbPrimariesChromaticityCoordinates.cs new file mode 100644 index 0000000000..90a5a57f9c --- /dev/null +++ b/src/ImageSharp/ColorProfiles/RgbPrimariesChromaticityCoordinates.cs @@ -0,0 +1,82 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces; + +namespace SixLabors.ImageSharp.ColorProfiles; + +/// +/// Represents the chromaticity coordinates of RGB primaries. +/// One of the specifiers of . +/// +public readonly struct RgbPrimariesChromaticityCoordinates : IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// The chromaticity coordinates of the red channel. + /// The chromaticity coordinates of the green channel. + /// The chromaticity coordinates of the blue channel. + public RgbPrimariesChromaticityCoordinates(CieXyChromaticityCoordinates r, CieXyChromaticityCoordinates g, CieXyChromaticityCoordinates b) + { + this.R = r; + this.G = g; + this.B = b; + } + + /// + /// Gets the chromaticity coordinates of the red channel. + /// + public CieXyChromaticityCoordinates R { get; } + + /// + /// Gets the chromaticity coordinates of the green channel. + /// + public CieXyChromaticityCoordinates G { get; } + + /// + /// Gets the chromaticity coordinates of the blue channel. + /// + public CieXyChromaticityCoordinates B { get; } + + /// + /// Compares two objects for equality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + public static bool operator ==(RgbPrimariesChromaticityCoordinates left, RgbPrimariesChromaticityCoordinates right) + => left.Equals(right); + + /// + /// Compares two objects for inequality + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + public static bool operator !=(RgbPrimariesChromaticityCoordinates left, RgbPrimariesChromaticityCoordinates right) + => !left.Equals(right); + + /// + public override bool Equals(object? obj) + => obj is RgbPrimariesChromaticityCoordinates other && this.Equals(other); + + /// + public bool Equals(RgbPrimariesChromaticityCoordinates other) + => this.R.Equals(other.R) && this.G.Equals(other.G) && this.B.Equals(other.B); + + /// + public override int GetHashCode() => HashCode.Combine(this.R, this.G, this.B); +} diff --git a/src/ImageSharp/ColorProfiles/RgbWorkingSpaces.cs b/src/ImageSharp/ColorProfiles/RgbWorkingSpaces.cs new file mode 100644 index 0000000000..c0d7f50746 --- /dev/null +++ b/src/ImageSharp/ColorProfiles/RgbWorkingSpaces.cs @@ -0,0 +1,114 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Companding; +using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces; + +namespace SixLabors.ColorProfiles; + +/// +/// Chromaticity coordinates based on: +/// +public static class RgbWorkingSpaces +{ + /// + /// sRgb working space. + /// + /// + /// Uses proper companding function, according to: + /// + /// + public static readonly RgbWorkingSpace SRgb = new SRgbWorkingSpace(Illuminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6400F, 0.3300F), new CieXyChromaticityCoordinates(0.3000F, 0.6000F), new CieXyChromaticityCoordinates(0.1500F, 0.0600F))); + + /// + /// Simplified sRgb working space (uses gamma companding instead of ). + /// See also . + /// + public static readonly RgbWorkingSpace SRgbSimplified = new GammaWorkingSpace(2.2F, Illuminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6400F, 0.3300F), new CieXyChromaticityCoordinates(0.3000F, 0.6000F), new CieXyChromaticityCoordinates(0.1500F, 0.0600F))); + + /// + /// Rec. 709 (ITU-R Recommendation BT.709) working space. + /// + public static readonly RgbWorkingSpace Rec709 = new Rec709WorkingSpace(Illuminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.64F, 0.33F), new CieXyChromaticityCoordinates(0.30F, 0.60F), new CieXyChromaticityCoordinates(0.15F, 0.06F))); + + /// + /// Rec. 2020 (ITU-R Recommendation BT.2020F) working space. + /// + public static readonly RgbWorkingSpace Rec2020 = new Rec2020WorkingSpace(Illuminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.708F, 0.292F), new CieXyChromaticityCoordinates(0.170F, 0.797F), new CieXyChromaticityCoordinates(0.131F, 0.046F))); + + /// + /// ECI Rgb v2 working space. + /// + public static readonly RgbWorkingSpace ECIRgbv2 = new LWorkingSpace(Illuminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6700F, 0.3300F), new CieXyChromaticityCoordinates(0.2100F, 0.7100F), new CieXyChromaticityCoordinates(0.1400F, 0.0800F))); + + /// + /// Adobe Rgb (1998) working space. + /// + public static readonly RgbWorkingSpace AdobeRgb1998 = new GammaWorkingSpace(2.2F, Illuminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6400F, 0.3300F), new CieXyChromaticityCoordinates(0.2100F, 0.7100F), new CieXyChromaticityCoordinates(0.1500F, 0.0600F))); + + /// + /// Apple sRgb working space. + /// + public static readonly RgbWorkingSpace ApplesRgb = new GammaWorkingSpace(1.8F, Illuminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6250F, 0.3400F), new CieXyChromaticityCoordinates(0.2800F, 0.5950F), new CieXyChromaticityCoordinates(0.1550F, 0.0700F))); + + /// + /// Best Rgb working space. + /// + public static readonly RgbWorkingSpace BestRgb = new GammaWorkingSpace(2.2F, Illuminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.7347F, 0.2653F), new CieXyChromaticityCoordinates(0.2150F, 0.7750F), new CieXyChromaticityCoordinates(0.1300F, 0.0350F))); + + /// + /// Beta Rgb working space. + /// + public static readonly RgbWorkingSpace BetaRgb = new GammaWorkingSpace(2.2F, Illuminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6888F, 0.3112F), new CieXyChromaticityCoordinates(0.1986F, 0.7551F), new CieXyChromaticityCoordinates(0.1265F, 0.0352F))); + + /// + /// Bruce Rgb working space. + /// + public static readonly RgbWorkingSpace BruceRgb = new GammaWorkingSpace(2.2F, Illuminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6400F, 0.3300F), new CieXyChromaticityCoordinates(0.2800F, 0.6500F), new CieXyChromaticityCoordinates(0.1500F, 0.0600F))); + + /// + /// CIE Rgb working space. + /// + public static readonly RgbWorkingSpace CIERgb = new GammaWorkingSpace(2.2F, Illuminants.E, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.7350F, 0.2650F), new CieXyChromaticityCoordinates(0.2740F, 0.7170F), new CieXyChromaticityCoordinates(0.1670F, 0.0090F))); + + /// + /// ColorMatch Rgb working space. + /// + public static readonly RgbWorkingSpace ColorMatchRgb = new GammaWorkingSpace(1.8F, Illuminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6300F, 0.3400F), new CieXyChromaticityCoordinates(0.2950F, 0.6050F), new CieXyChromaticityCoordinates(0.1500F, 0.0750F))); + + /// + /// Don Rgb 4 working space. + /// + public static readonly RgbWorkingSpace DonRgb4 = new GammaWorkingSpace(2.2F, Illuminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6960F, 0.3000F), new CieXyChromaticityCoordinates(0.2150F, 0.7650F), new CieXyChromaticityCoordinates(0.1300F, 0.0350F))); + + /// + /// Ekta Space PS5 working space. + /// + public static readonly RgbWorkingSpace EktaSpacePS5 = new GammaWorkingSpace(2.2F, Illuminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6950F, 0.3050F), new CieXyChromaticityCoordinates(0.2600F, 0.7000F), new CieXyChromaticityCoordinates(0.1100F, 0.0050F))); + + /// + /// NTSC Rgb working space. + /// + public static readonly RgbWorkingSpace NTSCRgb = new GammaWorkingSpace(2.2F, Illuminants.C, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6700F, 0.3300F), new CieXyChromaticityCoordinates(0.2100F, 0.7100F), new CieXyChromaticityCoordinates(0.1400F, 0.0800F))); + + /// + /// PAL/SECAM Rgb working space. + /// + public static readonly RgbWorkingSpace PALSECAMRgb = new GammaWorkingSpace(2.2F, Illuminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6400F, 0.3300F), new CieXyChromaticityCoordinates(0.2900F, 0.6000F), new CieXyChromaticityCoordinates(0.1500F, 0.0600F))); + + /// + /// ProPhoto Rgb working space. + /// + public static readonly RgbWorkingSpace ProPhotoRgb = new GammaWorkingSpace(1.8F, Illuminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.7347F, 0.2653F), new CieXyChromaticityCoordinates(0.1596F, 0.8404F), new CieXyChromaticityCoordinates(0.0366F, 0.0001F))); + + /// + /// SMPTE-C Rgb working space. + /// + public static readonly RgbWorkingSpace SMPTECRgb = new GammaWorkingSpace(2.2F, Illuminants.D65, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.6300F, 0.3400F), new CieXyChromaticityCoordinates(0.3100F, 0.5950F), new CieXyChromaticityCoordinates(0.1550F, 0.0700F))); + + /// + /// Wide Gamut Rgb working space. + /// + public static readonly RgbWorkingSpace WideGamutRgb = new GammaWorkingSpace(2.2F, Illuminants.D50, new RgbPrimariesChromaticityCoordinates(new CieXyChromaticityCoordinates(0.7350F, 0.2650F), new CieXyChromaticityCoordinates(0.1150F, 0.8260F), new CieXyChromaticityCoordinates(0.1570F, 0.0180F))); +} diff --git a/src/ImageSharp/ColorProfiles/VonKriesChromaticAdaptation.cs b/src/ImageSharp/ColorProfiles/VonKriesChromaticAdaptation.cs index 0505395deb..4719750b7c 100644 --- a/src/ImageSharp/ColorProfiles/VonKriesChromaticAdaptation.cs +++ b/src/ImageSharp/ColorProfiles/VonKriesChromaticAdaptation.cs @@ -19,16 +19,25 @@ public static class VonKriesChromaticAdaptation /// /// Performs a linear transformation of a source color in to the destination color. /// + /// The type of color profile to convert from. + /// The type of color profile to convert to. /// Doesn't crop the resulting color space coordinates (e.g. allows negative values for XYZ coordinates). /// The color profile conversion options. /// The source color. /// The - public static CieXyz Transform(ColorConversionOptions options, in CieXyz source) + public static CieXyz Transform(ColorConversionOptions options, in CieXyz source) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile { - CieXyz sourceWhitePoint = options.WhitePoint; - CieXyz destinationWhitePoint = options.TargetWhitePoint; + CieXyz sourceWhitePoint = TFrom.GetChromaticAdaptionWhitePointSource() == ChromaticAdaptionWhitePointSource.WhitePoint + ? options.WhitePoint + : options.RgbWorkingSpace.WhitePoint; - if (sourceWhitePoint.Equals(destinationWhitePoint)) + CieXyz targetWhitePoint = TTo.GetChromaticAdaptionWhitePointSource() == ChromaticAdaptionWhitePointSource.WhitePoint + ? options.TargetWhitePoint + : options.TargetRgbWorkingSpace.WhitePoint; + + if (sourceWhitePoint.Equals(targetWhitePoint)) { return new(source.X, source.Y, source.Z); } @@ -37,7 +46,7 @@ public static class VonKriesChromaticAdaptation Vector3 sourceColorLms = Vector3.Transform(source.ToVector3(), matrix); Vector3 sourceWhitePointLms = Vector3.Transform(sourceWhitePoint.ToVector3(), matrix); - Vector3 targetWhitePointLms = Vector3.Transform(destinationWhitePoint.ToVector3(), matrix); + Vector3 targetWhitePointLms = Vector3.Transform(targetWhitePoint.ToVector3(), matrix); Vector3 vector = targetWhitePointLms / sourceWhitePointLms; Vector3 targetColorLms = Vector3.Multiply(vector, sourceColorLms); @@ -49,19 +58,28 @@ public static class VonKriesChromaticAdaptation /// /// Performs a bulk linear transformation of a source color in to the destination color. /// + /// The type of color profile to convert from. + /// The type of color profile to convert to. /// Doesn't crop the resulting color space coordinates (e. g. allows negative values for XYZ coordinates). /// The color profile conversion options. /// The span to the source colors. /// The span to the destination colors. - public static void Transform(ColorConversionOptions options, ReadOnlySpan source, Span destination) + public static void Transform(ColorConversionOptions options, ReadOnlySpan source, Span destination) + where TFrom : struct, IColorProfile + where TTo : struct, IColorProfile { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); int count = source.Length; - CieXyz sourceWhitePoint = options.WhitePoint; - CieXyz destinationWhitePoint = options.TargetWhitePoint; + CieXyz sourceWhitePoint = TFrom.GetChromaticAdaptionWhitePointSource() == ChromaticAdaptionWhitePointSource.WhitePoint + ? options.WhitePoint + : options.RgbWorkingSpace.WhitePoint; + + CieXyz targetWhitePoint = TTo.GetChromaticAdaptionWhitePointSource() == ChromaticAdaptionWhitePointSource.WhitePoint + ? options.TargetWhitePoint + : options.TargetRgbWorkingSpace.WhitePoint; - if (sourceWhitePoint.Equals(destinationWhitePoint)) + if (sourceWhitePoint.Equals(targetWhitePoint)) { source.CopyTo(destination[..count]); return; @@ -73,16 +91,18 @@ public static class VonKriesChromaticAdaptation ref CieXyz sourceBase = ref MemoryMarshal.GetReference(source); ref CieXyz destinationBase = ref MemoryMarshal.GetReference(destination); + Vector3 sourceWhitePointLms = Vector3.Transform(sourceWhitePoint.ToVector3(), matrix); + Vector3 targetWhitePointLms = Vector3.Transform(targetWhitePoint.ToVector3(), matrix); + + Vector3 vector = targetWhitePointLms / sourceWhitePointLms; + for (nuint i = 0; i < (uint)count; i++) { ref CieXyz sp = ref Unsafe.Add(ref sourceBase, i); ref CieXyz dp = ref Unsafe.Add(ref destinationBase, i); Vector3 sourceColorLms = Vector3.Transform(sp.ToVector3(), matrix); - Vector3 sourceWhitePointLms = Vector3.Transform(sourceWhitePoint.ToVector3(), matrix); - Vector3 targetWhitePointLms = Vector3.Transform(destinationWhitePoint.ToVector3(), matrix); - Vector3 vector = targetWhitePointLms / sourceWhitePointLms; Vector3 targetColorLms = Vector3.Multiply(vector, sourceColorLms); dp = new CieXyz(Vector3.Transform(targetColorLms, inverseMatrix)); } diff --git a/src/ImageSharp/ColorProfiles/WorkingSpaces/GammaWorkingSpace.cs b/src/ImageSharp/ColorProfiles/WorkingSpaces/GammaWorkingSpace.cs new file mode 100644 index 0000000000..91fa426241 --- /dev/null +++ b/src/ImageSharp/ColorProfiles/WorkingSpaces/GammaWorkingSpace.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles.Companding; + +namespace SixLabors.ImageSharp.ColorProfiles.WorkingSpaces; + +/// +/// The gamma working space. +/// +public sealed class GammaWorkingSpace : RgbWorkingSpace +{ + /// + /// Initializes a new instance of the class. + /// + /// The gamma value. + /// The reference white point. + /// The chromaticity of the rgb primaries. + public GammaWorkingSpace(float gamma, CieXyz referenceWhite, RgbPrimariesChromaticityCoordinates chromaticityCoordinates) + : base(referenceWhite, chromaticityCoordinates) => this.Gamma = gamma; + + /// + /// Gets the gamma value. + /// + public float Gamma { get; } + + /// + public override void Compress(Span vectors) => GammaCompanding.Compress(vectors, this.Gamma); + + /// + public override void Expand(Span vectors) => GammaCompanding.Expand(vectors, this.Gamma); + + /// + public override Vector4 Compress(Vector4 vector) => GammaCompanding.Compress(vector, this.Gamma); + + /// + public override Vector4 Expand(Vector4 vector) => GammaCompanding.Expand(vector, this.Gamma); + + /// + public override bool Equals(object? obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is GammaWorkingSpace other) + { + return this.Gamma.Equals(other.Gamma) + && this.WhitePoint.Equals(other.WhitePoint) + && this.ChromaticityCoordinates.Equals(other.ChromaticityCoordinates); + } + + return false; + } + + /// + public override int GetHashCode() => HashCode.Combine( + this.WhitePoint, + this.ChromaticityCoordinates, + this.Gamma); +} diff --git a/src/ImageSharp/ColorProfiles/WorkingSpaces/LWorkingSpace.cs b/src/ImageSharp/ColorProfiles/WorkingSpaces/LWorkingSpace.cs new file mode 100644 index 0000000000..77dc2c06d9 --- /dev/null +++ b/src/ImageSharp/ColorProfiles/WorkingSpaces/LWorkingSpace.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles.Companding; + +namespace SixLabors.ImageSharp.ColorProfiles.WorkingSpaces; + +/// +/// L* working space. +/// +public sealed class LWorkingSpace : RgbWorkingSpace +{ + /// + /// Initializes a new instance of the class. + /// + /// The reference white point. + /// The chromaticity of the rgb primaries. + public LWorkingSpace(CieXyz referenceWhite, RgbPrimariesChromaticityCoordinates chromaticityCoordinates) + : base(referenceWhite, chromaticityCoordinates) + { + } + + /// + public override void Compress(Span vectors) => LCompanding.Compress(vectors); + + /// + public override void Expand(Span vectors) => LCompanding.Expand(vectors); + + /// + public override Vector4 Compress(Vector4 vector) => LCompanding.Compress(vector); + + /// + public override Vector4 Expand(Vector4 vector) => LCompanding.Expand(vector); +} diff --git a/src/ImageSharp/ColorProfiles/WorkingSpaces/Rec2020WorkingSpace.cs b/src/ImageSharp/ColorProfiles/WorkingSpaces/Rec2020WorkingSpace.cs new file mode 100644 index 0000000000..970d103029 --- /dev/null +++ b/src/ImageSharp/ColorProfiles/WorkingSpaces/Rec2020WorkingSpace.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles.Companding; + +namespace SixLabors.ImageSharp.ColorProfiles.WorkingSpaces; + +/// +/// Rec. 2020 (ITU-R Recommendation BT.2020F) working space. +/// +public sealed class Rec2020WorkingSpace : RgbWorkingSpace +{ + /// + /// Initializes a new instance of the class. + /// + /// The reference white point. + /// The chromaticity of the rgb primaries. + public Rec2020WorkingSpace(CieXyz referenceWhite, RgbPrimariesChromaticityCoordinates chromaticityCoordinates) + : base(referenceWhite, chromaticityCoordinates) + { + } + + /// + public override void Compress(Span vectors) => Rec2020Companding.Compress(vectors); + + /// + public override void Expand(Span vectors) => Rec2020Companding.Expand(vectors); + + /// + public override Vector4 Compress(Vector4 vector) => Rec2020Companding.Compress(vector); + + /// + public override Vector4 Expand(Vector4 vector) => Rec2020Companding.Expand(vector); +} diff --git a/src/ImageSharp/ColorProfiles/WorkingSpaces/Rec709WorkingSpace.cs b/src/ImageSharp/ColorProfiles/WorkingSpaces/Rec709WorkingSpace.cs new file mode 100644 index 0000000000..011da58077 --- /dev/null +++ b/src/ImageSharp/ColorProfiles/WorkingSpaces/Rec709WorkingSpace.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles.Companding; + +namespace SixLabors.ImageSharp.ColorProfiles.WorkingSpaces; + +/// +/// Rec. 709 (ITU-R Recommendation BT.709) working space. +/// +public sealed class Rec709WorkingSpace : RgbWorkingSpace +{ + /// + /// Initializes a new instance of the class. + /// + /// The reference white point. + /// The chromaticity of the rgb primaries. + public Rec709WorkingSpace(CieXyz referenceWhite, RgbPrimariesChromaticityCoordinates chromaticityCoordinates) + : base(referenceWhite, chromaticityCoordinates) + { + } + + /// + public override void Compress(Span vectors) => Rec709Companding.Compress(vectors); + + /// + public override void Expand(Span vectors) => Rec709Companding.Expand(vectors); + + /// + public override Vector4 Compress(Vector4 vector) => Rec709Companding.Compress(vector); + + /// + public override Vector4 Expand(Vector4 vector) => Rec709Companding.Expand(vector); +} diff --git a/src/ImageSharp/ColorProfiles/WorkingSpaces/RgbWorkingSpace.cs b/src/ImageSharp/ColorProfiles/WorkingSpaces/RgbWorkingSpace.cs new file mode 100644 index 0000000000..97069b856b --- /dev/null +++ b/src/ImageSharp/ColorProfiles/WorkingSpaces/RgbWorkingSpace.cs @@ -0,0 +1,85 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.ColorProfiles.WorkingSpaces; + +/// +/// Base class for all implementations of . +/// +public abstract class RgbWorkingSpace +{ + /// + /// Initializes a new instance of the class. + /// + /// The reference white point. + /// The chromaticity of the rgb primaries. + protected RgbWorkingSpace(CieXyz referenceWhite, RgbPrimariesChromaticityCoordinates chromaticityCoordinates) + { + this.WhitePoint = referenceWhite; + this.ChromaticityCoordinates = chromaticityCoordinates; + } + + /// + /// Gets the reference white point + /// + public CieXyz WhitePoint { get; } + + /// + /// Gets the chromaticity of the rgb primaries. + /// + public RgbPrimariesChromaticityCoordinates ChromaticityCoordinates { get; } + + /// + /// Compresses the linear vectors to their nonlinear equivalents with respect to the energy. + /// + /// The span of vectors. + public abstract void Compress(Span vectors); + + /// + /// Expands the nonlinear vectors to their linear equivalents with respect to the energy. + /// + /// The span of vectors. + public abstract void Expand(Span vectors); + + /// + /// Compresses the linear vector to its nonlinear equivalent with respect to the energy. + /// + /// The vector. + /// The . + public abstract Vector4 Compress(Vector4 vector); + + /// + /// Compresses the linear vector to its nonlinear equivalent with respect to the energy. + /// + /// The vector. + /// The . + public abstract Vector4 Expand(Vector4 vector); + + /// + public override bool Equals(object? obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is RgbWorkingSpace other) + { + return this.WhitePoint.Equals(other.WhitePoint) + && this.ChromaticityCoordinates.Equals(other.ChromaticityCoordinates); + } + + return false; + } + + /// + public override int GetHashCode() + => HashCode.Combine(this.WhitePoint, this.ChromaticityCoordinates); +} diff --git a/src/ImageSharp/ColorProfiles/WorkingSpaces/SRgbWorkingSpace.cs b/src/ImageSharp/ColorProfiles/WorkingSpaces/SRgbWorkingSpace.cs new file mode 100644 index 0000000000..b88e6c8983 --- /dev/null +++ b/src/ImageSharp/ColorProfiles/WorkingSpaces/SRgbWorkingSpace.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.ColorProfiles.Companding; + +namespace SixLabors.ImageSharp.ColorProfiles.WorkingSpaces; + +/// +/// The sRgb working space. +/// +public sealed class SRgbWorkingSpace : RgbWorkingSpace +{ + /// + /// Initializes a new instance of the class. + /// + /// The reference white point. + /// The chromaticity of the rgb primaries. + public SRgbWorkingSpace(CieXyz referenceWhite, RgbPrimariesChromaticityCoordinates chromaticityCoordinates) + : base(referenceWhite, chromaticityCoordinates) + { + } + + /// + public override void Compress(Span vectors) => SRgbCompanding.Compress(vectors); + + /// + public override void Expand(Span vectors) => SRgbCompanding.Expand(vectors); + + /// + public override Vector4 Compress(Vector4 vector) => SRgbCompanding.Compress(vector); + + /// + public override Vector4 Expand(Vector4 vector) => SRgbCompanding.Expand(vector); +}