diff --git a/Unicolour.Tests/EqualityTests.cs b/Unicolour.Tests/EqualityTests.cs index 14e929d..6ad781a 100644 --- a/Unicolour.Tests/EqualityTests.cs +++ b/Unicolour.Tests/EqualityTests.cs @@ -159,8 +159,9 @@ private static void AssertUnicoloursEqual(Unicolour unicolour1, Unicolour unicol AssertEqual(unicolour1.RgbLinear, unicolour2.RgbLinear); AssertEqual(unicolour1.Temperature, unicolour2.Temperature); AssertEqual(unicolour1.Tsl, unicolour2.Tsl); - AssertEqual(unicolour1.Xyz, unicolour2.Xyz); + AssertEqual(unicolour1.Xyb, unicolour2.Xyb); AssertEqual(unicolour1.Xyy, unicolour2.Xyy); + AssertEqual(unicolour1.Xyz, unicolour2.Xyz); AssertEqual(unicolour1.Ycbcr, unicolour2.Ycbcr); AssertEqual(unicolour1.Ycbcr, unicolour2.Ycbcr); AssertEqual(unicolour1.Ycgco, unicolour2.Ycgco); diff --git a/Unicolour.Tests/GreyscaleTests.cs b/Unicolour.Tests/GreyscaleTests.cs index 6ca1ef0..57de494 100644 --- a/Unicolour.Tests/GreyscaleTests.cs +++ b/Unicolour.Tests/GreyscaleTests.cs @@ -280,6 +280,17 @@ public class GreyscaleTests [TestCase(180.0, 0.5, 0.00000000001, false)] public void Tsl(double t, double s, double l, bool expected) => AssertUnicolour(new(ColourSpace.Tsl, t, s, l), expected); + [TestCase(0.0, 0.5, 0.0, true)] + [TestCase(0.00000000001, 0.5, 0.0, false)] + [TestCase(-0.00000000001, 0.5, 0.0, false)] + [TestCase(0.0, 0.5, 0.00000000001, false)] + [TestCase(0.0, 0.5, -0.00000000001, false)] + [TestCase(0.1, 0.0, -0.1, true)] + [TestCase(0.1, -0.00000000001, -0.1, true)] + [TestCase(0.1, 0.00000000001, -0.1, false)] + [TestCase(0.1, 1.0, -0.1, false)] + public void Xyb(double x, double y, double b, bool expected) => AssertUnicolour(new(ColourSpace.Xyb, x, y, b), expected); + [TestCase(0.5, 0.0, 0.0, true)] [TestCase(0.5, 0.00000000001, 0.0, false)] [TestCase(0.5, -0.00000000001, 0.0, false)] diff --git a/Unicolour.Tests/KnownXybTests.cs b/Unicolour.Tests/KnownXybTests.cs new file mode 100644 index 0000000..8c27693 --- /dev/null +++ b/Unicolour.Tests/KnownXybTests.cs @@ -0,0 +1,51 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class KnownXybTests +{ + private const double Tolerance = 0.00000000001; + + [Test] + public void Red() + { + var red = StandardRgb.Red; + TestUtils.AssertTriplet(red, new(0.028100083161277323, 0.4881882010413151, -0.01652922538774071), Tolerance); + } + + [Test] + public void Green() + { + var green = StandardRgb.Green; + TestUtils.AssertTriplet(green, new(-0.015386116472573375, 0.714781372724691, -0.2777046155146864), Tolerance); + } + + [Test] + public void Blue() + { + var blue = StandardRgb.Blue; + TestUtils.AssertTriplet(blue, new(0.0, 0.27812819734781813, 0.3880116647837879), Tolerance); + } + + [Test] + public void Black() + { + var black = StandardRgb.Black; + TestUtils.AssertTriplet(black, new(0.0, 0.0, 0.0), Tolerance); + } + + [Test] + public void White() + { + var white = StandardRgb.White; + TestUtils.AssertTriplet(white, new(0.0, 0.8453085619621622, 2.220446049250313e-16), Tolerance); + } + + [Test] + public void Grey() + { + var grey = StandardRgb.Grey; + TestUtils.AssertTriplet(grey, new(0.0, 0.4457393607565907, 0.0), Tolerance); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/MixHueNoComponentTests.cs b/Unicolour.Tests/MixHueNoComponentTests.cs index a5ddd9a..e1eabed 100644 --- a/Unicolour.Tests/MixHueNoComponentTests.cs +++ b/Unicolour.Tests/MixHueNoComponentTests.cs @@ -26,6 +26,7 @@ public class MixHueNoComponentTests : MixHueAgnosticTests new(ColourSpace.Yuv, new Range(0, 1), new Range(-0.436, 0.436), new Range(-0.614, 0.614)), new(ColourSpace.Yiq, new Range(0, 1), new Range(-0.595, 0.595), new Range(-0.522, 0.522)), new(ColourSpace.Ydbdr, new Range(0, 1), new Range(-1.333, 1.333), new Range(-1.333, 1.333)), + new(ColourSpace.Xyb, new Range(-0.03, 0.03), new Range(0, 1), new Range(-0.4, 0.4)), new(ColourSpace.Ipt, new Range(0, 1), new Range(-0.75, 0.75), new Range(-0.75, 0.75)), new(ColourSpace.Ictcp, new Range(0, 1), new Range(-0.5, 0.5), new Range(-0.5, 0.5)), new(ColourSpace.Jzazbz, new Range(0, 0.16), new Range(-0.1, 0.1), new Range(-0.1, 0.1)), diff --git a/Unicolour.Tests/NotNumberTests.cs b/Unicolour.Tests/NotNumberTests.cs index adc0de1..629f6ad 100644 --- a/Unicolour.Tests/NotNumberTests.cs +++ b/Unicolour.Tests/NotNumberTests.cs @@ -83,6 +83,9 @@ public class NotNumberTests [TestCaseSource(nameof(testCases))] public void Tsl(double t, double s, double l) => AssertUnicolour(new(ColourSpace.Tsl, t, s, l)); + [TestCaseSource(nameof(testCases))] + public void Xyb(double x, double y, double b) => AssertUnicolour(new(ColourSpace.Xyb, x, y, b)); + [TestCaseSource(nameof(testCases))] public void Ipt(double i, double p, double t) => AssertUnicolour(new(ColourSpace.Ipt, i, p, t)); diff --git a/Unicolour.Tests/RoundtripRgbLinearTests.cs b/Unicolour.Tests/RoundtripRgbLinearTests.cs index cb24452..66ee44e 100644 --- a/Unicolour.Tests/RoundtripRgbLinearTests.cs +++ b/Unicolour.Tests/RoundtripRgbLinearTests.cs @@ -13,8 +13,8 @@ public class RoundtripRgbLinearTests public void ViaXyz(ColourTriplet triplet) { var original = new RgbLinear(triplet.First, triplet.Second, triplet.Third); - var rgb = RgbLinear.ToXyz(original, RgbConfig, XyzConfig); - var roundtrip = RgbLinear.FromXyz(rgb, RgbConfig, XyzConfig); + var xyz = RgbLinear.ToXyz(original, RgbConfig, XyzConfig); + var roundtrip = RgbLinear.FromXyz(xyz, RgbConfig, XyzConfig); TestUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); } @@ -64,6 +64,15 @@ private static void AssertViaRgb(ColourTriplet triplet, RgbConfiguration rgbConf TestUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); } + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.RgbLinearTriplets))] + public void ViaXyb(ColourTriplet triplet) + { + var original = new RgbLinear(triplet.First, triplet.Second, triplet.Third); + var xyb = Xyb.FromRgbLinear(original); + var roundtrip = Xyb.ToRgbLinear(xyb); + TestUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } + private static void AssertBoundedRoundtrip(RgbLinear original, RgbLinear roundtrip, double lower, double upper) { var boundOriginal = new RgbLinear(original.R.Clamp(lower, upper), original.G.Clamp(lower, upper), original.B.Clamp(lower, upper)); diff --git a/Unicolour.Tests/RoundtripXybTests.cs b/Unicolour.Tests/RoundtripXybTests.cs new file mode 100644 index 0000000..a01fd52 --- /dev/null +++ b/Unicolour.Tests/RoundtripXybTests.cs @@ -0,0 +1,18 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class RoundtripXybTests +{ + private const double Tolerance = 0.0000000005; + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XybTriplets))] + public void ViaRgbLinear(ColourTriplet triplet) + { + var original = new Xyb(triplet.First, triplet.Second, triplet.Third); + var rgbLinear = Xyb.ToRgbLinear(original); + var roundtrip = Xyb.FromRgbLinear(rgbLinear); + TestUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/SmokeTests.cs b/Unicolour.Tests/SmokeTests.cs index e8ea3b4..f98fc80 100644 --- a/Unicolour.Tests/SmokeTests.cs +++ b/Unicolour.Tests/SmokeTests.cs @@ -74,6 +74,9 @@ public class SmokeTests new TestCaseData(ColourSpace.Tsl, 0, 0, 0, 0), new TestCaseData(ColourSpace.Tsl, 360, 1, 1, 1), new TestCaseData(ColourSpace.Tsl, 180, 0.4, 0.6, 0.5), + new TestCaseData(ColourSpace.Xyb, -0.03, 0, -0.4, 0), + new TestCaseData(ColourSpace.Xyb, 0.03, 1, 0.4, 1), + new TestCaseData(ColourSpace.Xyb, 0, 0.5, 0, 0.5), new TestCaseData(ColourSpace.Ipt, 0, -0.75, -0.75, 0), new TestCaseData(ColourSpace.Ipt, 1, 0.75, 0.75, 1), new TestCaseData(ColourSpace.Ipt, 0.5, -0.01, 0.01, 0.5), diff --git a/Unicolour.Tests/Utils/RandomColours.cs b/Unicolour.Tests/Utils/RandomColours.cs index 8884372..addad58 100644 --- a/Unicolour.Tests/Utils/RandomColours.cs +++ b/Unicolour.Tests/Utils/RandomColours.cs @@ -32,6 +32,7 @@ internal static class RandomColours public static readonly List YiqTriplets = new(); public static readonly List YdbdrTriplets = new(); public static readonly List TslTriplets = new(); + public static readonly List XybTriplets = new(); public static readonly List IptTriplets = new(); public static readonly List IctcpTriplets = new(); public static readonly List JzazbzTriplets = new(); @@ -96,6 +97,7 @@ static RandomColours() YiqTriplets.Add(Yiq()); YdbdrTriplets.Add(Ydbdr()); TslTriplets.Add(Tsl()); + XybTriplets.Add(Xyb()); IptTriplets.Add(Ipt()); IctcpTriplets.Add(Ictcp()); JzazbzTriplets.Add(Jzazbz()); @@ -140,6 +142,7 @@ private static ColourTriplet GetRandomTriplet(ColourSpace colourSpace) ColourSpace.Yiq => Yiq(), ColourSpace.Ydbdr => Ydbdr(), ColourSpace.Tsl => Tsl(), + ColourSpace.Xyb => Xyb(), ColourSpace.Ipt => Ipt(), ColourSpace.Ictcp => Ictcp(), ColourSpace.Jzazbz => Jzazbz(), @@ -179,6 +182,7 @@ private static ColourTriplet GetRandomTriplet(ColourSpace colourSpace) private static ColourTriplet Yiq() => new(Rng(), Rng(-0.595, 0.595), Rng(-0.522, 0.522)); private static ColourTriplet Ydbdr() => new(Rng(), Rng(-1.333, 1.333), Rng(-1.333, 1.333)); private static ColourTriplet Tsl() => new(Rng(0, 360), Rng(), Rng()); + private static ColourTriplet Xyb() => new(Rng(-0.03, 0.03), Rng(), Rng(-0.4, 0.4)); private static ColourTriplet Ipt() => new(Rng(), Rng(-0.75, 0.75), Rng(-0.75, 0.75)); private static ColourTriplet Ictcp() => new(Rng(), Rng(-0.5, 0.5), Rng(-0.5, 0.5)); private static ColourTriplet Jzazbz() => new(Rng(0, 0.17), Rng(-0.10, 0.11), Rng(-0.16, 0.12)); // from own test values since ranges suggested by paper (0->1, -0.5->0.5, -0.5->0.5) easily produce XYZ with NaNs [https://doi.org/10.1364/OE.25.015131] diff --git a/Unicolour.Tests/Utils/TestUtils.cs b/Unicolour.Tests/Utils/TestUtils.cs index bae89f6..20d2150 100644 --- a/Unicolour.Tests/Utils/TestUtils.cs +++ b/Unicolour.Tests/Utils/TestUtils.cs @@ -34,6 +34,7 @@ internal static class TestUtils new TestCaseData(ColourSpace.Yiq), new TestCaseData(ColourSpace.Ydbdr), new TestCaseData(ColourSpace.Tsl), + new TestCaseData(ColourSpace.Xyb), new TestCaseData(ColourSpace.Ipt), new TestCaseData(ColourSpace.Ictcp), new TestCaseData(ColourSpace.Jzazbz), @@ -206,6 +207,7 @@ void AccessProperties() AccessProperty(() => unicolour.RgbLinear); AccessProperty(() => unicolour.Temperature); AccessProperty(() => unicolour.Tsl); + AccessProperty(() => unicolour.Xyb); AccessProperty(() => unicolour.Xyy); AccessProperty(() => unicolour.Xyz); AccessProperty(() => unicolour.Ypbpr); @@ -274,6 +276,7 @@ internal static void AssertNotEqual(T object1, T object2) { typeof(Yiq), ColourSpace.Yiq }, { typeof(Ydbdr), ColourSpace.Ydbdr }, { typeof(Tsl), ColourSpace.Tsl }, + { typeof(Xyb), ColourSpace.Xyb }, { typeof(Ipt), ColourSpace.Ipt }, { typeof(Ictcp), ColourSpace.Ictcp }, { typeof(Jzazbz), ColourSpace.Jzazbz }, diff --git a/Unicolour/ColourSpace.cs b/Unicolour/ColourSpace.cs index c681624..e665442 100644 --- a/Unicolour/ColourSpace.cs +++ b/Unicolour/ColourSpace.cs @@ -24,6 +24,7 @@ public enum ColourSpace Yiq, Ydbdr, Tsl, + Xyb, Ipt, Ictcp, Jzazbz, diff --git a/Unicolour/Hct.cs b/Unicolour/Hct.cs index 9e9430f..c41cd87 100644 --- a/Unicolour/Hct.cs +++ b/Unicolour/Hct.cs @@ -23,6 +23,10 @@ internal Hct(double h, double c, double t, ColourHeritage heritage) : base(h, c, * (just a combination of LAB & CAM16, but with specific XYZ & CAM configuration, so can't reuse existing colour space calculations) * Forward: https://material.io/blog/science-of-color-design * Reverse: n/a - no published reverse transform and I don't want to port Google code, so using my own naive search + * + * ⚠️ + * this colour space is potentially defined relative to sRGB, but Unicolour does not currently enforce sRGB + * (using other RGB configs may lead to unexpected results, though it may be desirable to explore non-sRGB behaviour) */ private static readonly WhitePoint HctWhitePoint = Illuminant.D65.GetWhitePoint(Observer.Degree2); diff --git a/Unicolour/Hpluv.cs b/Unicolour/Hpluv.cs index 220f793..0a61d45 100644 --- a/Unicolour/Hpluv.cs +++ b/Unicolour/Hpluv.cs @@ -26,6 +26,10 @@ internal Hpluv(double h, double s, double l, ColourHeritage heritage) : base(h, * HPLUV is a transform of LCHUV * Forward: https://github.com/hsluv/hsluv-haxe/blob/master/src/hsluv/Hsluv.hx#L397 * Reverse: https://github.com/hsluv/hsluv-haxe/blob/master/src/hsluv/Hsluv.hx#L380 + * + * ⚠️ + * this colour space is potentially defined relative to sRGB, but Unicolour does not currently enforce sRGB + * (using other RGB configs may lead to unexpected results, though it may be desirable to explore non-sRGB behaviour) */ internal static Hpluv FromLchuv(Lchuv lchuv) diff --git a/Unicolour/Hsluv.cs b/Unicolour/Hsluv.cs index 39a74bc..e3ed5f8 100644 --- a/Unicolour/Hsluv.cs +++ b/Unicolour/Hsluv.cs @@ -26,6 +26,10 @@ internal Hsluv(double h, double s, double l, ColourHeritage heritage) : base(h, * HSLUV is a transform of LCHUV * Forward: https://github.com/hsluv/hsluv-haxe/blob/master/src/hsluv/Hsluv.hx#L363 * Reverse: https://github.com/hsluv/hsluv-haxe/blob/master/src/hsluv/Hsluv.hx#L346 + * + * ⚠️ + * this colour space is potentially defined relative to sRGB, but Unicolour does not currently enforce sRGB + * (using other RGB configs may lead to unexpected results, though it may be desirable to explore non-sRGB behaviour) */ internal static Hsluv FromLchuv(Lchuv lchuv) diff --git a/Unicolour/Okhsl.cs b/Unicolour/Okhsl.cs index a27e7ee..ac0d8ca 100644 --- a/Unicolour/Okhsl.cs +++ b/Unicolour/Okhsl.cs @@ -28,6 +28,10 @@ internal Okhsl(double h, double s, double l, ColourHeritage heritage) : base(h, * OKHSL is a transform of OKLAB * Forward: https://bottosson.github.io/posts/colorpicker/#hsl-2 * Reverse: https://bottosson.github.io/posts/colorpicker/#hsl-2 + * + * ⚠️ + * this colour space is potentially defined relative to sRGB, but Unicolour does not currently enforce sRGB + * (using other RGB configs may lead to unexpected results, though it may be desirable to explore non-sRGB behaviour) */ internal static Okhsl FromOklab(Oklab oklab, XyzConfiguration xyzConfig, RgbConfiguration rgbConfig) diff --git a/Unicolour/Okhsv.cs b/Unicolour/Okhsv.cs index 0ff6f1a..fc83947 100644 --- a/Unicolour/Okhsv.cs +++ b/Unicolour/Okhsv.cs @@ -28,6 +28,10 @@ internal Okhsv(double h, double s, double v, ColourHeritage heritage) : base(h, * OKHSV is a transform of OKLAB * Forward: https://bottosson.github.io/posts/colorpicker/#hsv-2 * Reverse: https://bottosson.github.io/posts/colorpicker/#hsv-2 + * + * ⚠️ + * this colour space is potentially defined relative to sRGB, but Unicolour does not currently enforce sRGB + * (using other RGB configs may lead to unexpected results, though it may be desirable to explore non-sRGB behaviour) */ internal static Okhsv FromOklab(Oklab oklab, XyzConfiguration xyzConfig, RgbConfiguration rgbConfig) diff --git a/Unicolour/Okhwb.cs b/Unicolour/Okhwb.cs index c2ccacf..7e5c47f 100644 --- a/Unicolour/Okhwb.cs +++ b/Unicolour/Okhwb.cs @@ -26,6 +26,10 @@ internal Okhwb(double h, double w, double b, ColourHeritage heritage) : base(h, * OKHWB is a transform of OKHSV * Forward: https://bottosson.github.io/posts/colorpicker/#okhwb * Reverse: https://bottosson.github.io/posts/colorpicker/#okhwb + * + * ⚠️ + * this colour space is potentially defined relative to sRGB, but Unicolour does not currently enforce sRGB + * (using other RGB configs may lead to unexpected results, though it may be desirable to explore non-sRGB behaviour) */ internal static Okhwb FromOkhsv(Okhsv okhsv) diff --git a/Unicolour/Oklab.cs b/Unicolour/Oklab.cs index 9acfa84..f2bce89 100644 --- a/Unicolour/Oklab.cs +++ b/Unicolour/Oklab.cs @@ -23,10 +23,12 @@ internal Oklab(double l, double a, double b, ColourHeritage heritage) : base(l, * OKLAB is a transform of XYZ * Forward: https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab * Reverse: https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab + * + * ⚠️ + * this colour space is potentially defined relative to sRGB, but Unicolour does not currently enforce sRGB + * (using other RGB configs may lead to unexpected results, though it may be desirable to explore non-sRGB behaviour) */ - private static readonly WhitePoint OklabWhitePoint = Illuminant.D65.GetWhitePoint(Observer.Degree2); - /* * NOTE: this definition of M1 is no longer used * internal static readonly Matrix M1 = new(new[,] @@ -52,6 +54,9 @@ internal Oklab(double l, double a, double b, ColourHeritage heritage) : base(l, * RgbToOklab = M1 * RgbToXyz * M1 = RgbToOklab * RgbToXyz^-1 */ + + private static readonly WhitePoint OklabWhitePoint = Illuminant.D65.GetWhitePoint(Observer.Degree2); + private static Matrix GetM1(Matrix rgbToXyzMatrix) => RgbToOklab.Multiply(rgbToXyzMatrix.Inverse()); private static readonly Matrix RgbToOklab = new(new[,] { diff --git a/Unicolour/Oklch.cs b/Unicolour/Oklch.cs index c4a94e6..4d84e07 100644 --- a/Unicolour/Oklch.cs +++ b/Unicolour/Oklch.cs @@ -24,6 +24,10 @@ internal Oklch(double l, double c, double h, ColourHeritage heritage) : base(l, * OKLCH is a transform of OKLAB * Forward: https://en.wikipedia.org/wiki/CIELAB_color_space#CIEHLC_cylindrical_model * Reverse: https://en.wikipedia.org/wiki/CIELAB_color_space#CIEHLC_cylindrical_model + * + * ⚠️ + * this colour space is potentially defined relative to sRGB, but Unicolour does not currently enforce sRGB + * (using other RGB configs may lead to unexpected results, though it may be desirable to explore non-sRGB behaviour) */ internal static Oklch FromOklab(Oklab oklab) diff --git a/Unicolour/Unicolour.Lookups.cs b/Unicolour/Unicolour.Lookups.cs index e0c754b..fcd8ab2 100644 --- a/Unicolour/Unicolour.Lookups.cs +++ b/Unicolour/Unicolour.Lookups.cs @@ -28,6 +28,7 @@ public ColourRepresentation GetRepresentation(ColourSpace colourSpace) ColourSpace.Yiq => Yiq, ColourSpace.Ydbdr => Ydbdr, ColourSpace.Tsl => Tsl, + ColourSpace.Xyb => Xyb, ColourSpace.Ipt => Ipt, ColourSpace.Ictcp => Ictcp, ColourSpace.Jzazbz => Jzazbz, @@ -71,6 +72,7 @@ private static ColourRepresentation CreateRepresentation( ColourSpace.Yiq => new Yiq(first, second, third, heritage), ColourSpace.Ydbdr => new Ydbdr(first, second, third, heritage), ColourSpace.Tsl => new Tsl(first, second, third, heritage), + ColourSpace.Xyb => new Xyb(first, second, third, heritage), ColourSpace.Ipt => new Ipt(first, second, third, heritage), ColourSpace.Ictcp => new Ictcp(first, second, third, heritage), ColourSpace.Jzazbz => new Jzazbz(first, second, third, heritage), @@ -142,6 +144,7 @@ private RgbLinear EvaluateRgbLinear() ColourSpace.Yiq => Rgb.ToRgbLinear(Rgb, Config.Rgb), ColourSpace.Ydbdr => Rgb.ToRgbLinear(Rgb, Config.Rgb), ColourSpace.Tsl => Rgb.ToRgbLinear(Rgb, Config.Rgb), + ColourSpace.Xyb => Xyb.ToRgbLinear(Xyb), _ => RgbLinear.FromXyz(Xyz, Config.Rgb, Config.Xyz) }; } @@ -209,6 +212,7 @@ private Xyz EvaluateXyz() ColourSpace.Yiq => RgbLinear.ToXyz(RgbLinear, Config.Rgb, Config.Xyz), ColourSpace.Ydbdr => RgbLinear.ToXyz(RgbLinear, Config.Rgb, Config.Xyz), ColourSpace.Tsl => RgbLinear.ToXyz(RgbLinear, Config.Rgb, Config.Xyz), + ColourSpace.Xyb => RgbLinear.ToXyz(RgbLinear, Config.Rgb, Config.Xyz), ColourSpace.Ipt => Ipt.ToXyz(Ipt, Config.Xyz), ColourSpace.Ictcp => Ictcp.ToXyz(Ictcp, Config.IctcpScalar, Config.Xyz), ColourSpace.Jzazbz => Jzazbz.ToXyz(Jzazbz, Config.JzazbzScalar, Config.Xyz), @@ -360,6 +364,15 @@ private Tsl EvaluateTsl() }; } + private Xyb EvaluateXyb() + { + return InitialColourSpace switch + { + ColourSpace.Xyb => (Xyb)InitialRepresentation, + _ => Xyb.FromRgbLinear(RgbLinear) + }; + } + private Ipt EvaluateIpt() { return InitialColourSpace switch diff --git a/Unicolour/Unicolour.cs b/Unicolour/Unicolour.cs index 6467025..95114ca 100644 --- a/Unicolour/Unicolour.cs +++ b/Unicolour/Unicolour.cs @@ -23,6 +23,7 @@ public partial class Unicolour : IEquatable private readonly Lazy yiq; private readonly Lazy ydbdr; private readonly Lazy tsl; + private readonly Lazy xyb; private readonly Lazy ipt; private readonly Lazy ictcp; private readonly Lazy jzazbz; @@ -62,6 +63,7 @@ public partial class Unicolour : IEquatable public Yiq Yiq => yiq.Value; public Ydbdr Ydbdr => ydbdr.Value; public Tsl Tsl => tsl.Value; + public Xyb Xyb => xyb.Value; public Ipt Ipt => ipt.Value; public Ictcp Ictcp => ictcp.Value; public Jzazbz Jzazbz => jzazbz.Value; @@ -124,6 +126,7 @@ internal Unicolour(Configuration config, ColourHeritage heritage, yiq = new Lazy(EvaluateYiq); ydbdr = new Lazy(EvaluateYdbdr); tsl = new Lazy(EvaluateTsl); + xyb = new Lazy(EvaluateXyb); ipt = new Lazy(EvaluateIpt); ictcp = new Lazy(EvaluateIctcp); jzazbz = new Lazy(EvaluateJzazbz); diff --git a/Unicolour/Xyb.cs b/Unicolour/Xyb.cs new file mode 100644 index 0000000..5696619 --- /dev/null +++ b/Unicolour/Xyb.cs @@ -0,0 +1,76 @@ +namespace Wacton.Unicolour; + +using static Utils; + +public record Xyb : ColourRepresentation +{ + protected override int? HueIndex => null; + public double X => First; + public double Y => Second; + public double B => Third; + + internal override bool IsGreyscale => Y <= 0.0 || (X.Equals(0.0) && B.Equals(0.0)); + + public Xyb(double x, double y, double b) : this(x, y, b, ColourHeritage.None) {} + internal Xyb(double x, double y, double b, ColourHeritage heritage) : base(x, y, b, heritage) {} + + protected override string FirstString => $"{X:+0.000;-0.000;0.000}"; + protected override string SecondString => $"{Y:F3}"; + protected override string ThirdString => $"{B:+0.000;-0.000;0.000}"; + public override string ToString() => base.ToString(); + + /* + * XYB is a transform of RGB + * Forward: https://ds.jpeg.org/whitepapers/jpeg-xl-whitepaper.pdf + * Reverse: n/a - not provided, using own implementation + * + * ⚠️ + * this colour space is potentially defined relative to sRGB, but Unicolour does not currently enforce sRGB + * (using other RGB configs may lead to unexpected results, though it may be desirable to explore non-sRGB behaviour) + */ + + /* + * NOTE: the final step of B -= Y is not documented in the JPEG XL white paper + * but apparently the intention is that B = sGamma should be B = sGamma - Y 🤷 + * (at least it makes the colour greyscale when X == 0 and B == 0, in the same way as LAB) + */ + + private const double Bias = 0.00379307325527544933; + private static readonly double CubeRootBias = CubeRoot(Bias); + + private static readonly Matrix RgbToLmsMatrix = new(new[,] + { + { 0.3, 0.622, 0.078 }, + { 0.23, 0.692, 0.078 }, + { 0.24342268924547819, 0.20476744424496821, 0.55180986650955360 } + }); + + private static readonly Matrix LmsToXybMatrix = new(new[,] + { + { 0.5, -0.5, 0.0 }, + { 0.5, 0.5, 0.0 }, + { 0.0, 0.0, 1.0 } + }); + + internal static Xyb FromRgbLinear(RgbLinear rgb) + { + var rgbMatrix = Matrix.FromTriplet(rgb.Triplet); + var lmsMixMatrix = RgbToLmsMatrix.Multiply(rgbMatrix).Select(value => value + Bias); + var lmsGammaMatrix = lmsMixMatrix.Select(mix => CubeRoot(mix) - CubeRootBias); + var xybMatrix = LmsToXybMatrix.Multiply(lmsGammaMatrix); + var (x, y, b) = xybMatrix.ToTriplet().Tuple; + b -= y; + return new Xyb(x, y, b, ColourHeritage.From(rgb)); + } + + internal static RgbLinear ToRgbLinear(Xyb xyb) + { + var (x, y, b) = xyb.Triplet; + b += y; + var xybMatrix = Matrix.FromTriplet(new(x, y, b)); + var lmsGammaMatrix = LmsToXybMatrix.Inverse().Multiply(xybMatrix); + var lmsMixMatrix = lmsGammaMatrix.Select(gamma => Math.Pow(gamma + CubeRootBias, 3)); + var rgbMatrix = RgbToLmsMatrix.Inverse().Multiply(lmsMixMatrix.Select(mix => mix - Bias)); + return new RgbLinear(rgbMatrix.ToTriplet(), ColourHeritage.From(xyb)); + } +} \ No newline at end of file