diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.cs b/src/ImageSharp/Common/Helpers/SimdUtils.cs
index 2b8f58b086..7e78581c33 100644
--- a/src/ImageSharp/Common/Helpers/SimdUtils.cs
+++ b/src/ImageSharp/Common/Helpers/SimdUtils.cs
@@ -1,9 +1,10 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86;
@@ -71,6 +72,107 @@ internal static partial class SimdUtils
return val_2p23_f32 | sign;
}
+ ///
+ /// Estimates the reciprocal of this vector.
+ ///
+ /// The vector to get reciprocal estimate of.
+ /// An estimated reciprocal of each element in the vector.
+ internal static Vector ReciprocalEstimate(this Vector v)
+ {
+ // TODO: System.Runtime.Intrinsics.Arm has Sve and Sve2
+ // support but is for evaluation purposes only; add SVE/SVE2
+ // support when possible
+ if (Avx512F.IsSupported && Vector.Count == 16)
+ {
+ // x86
+ return Avx512F.Reciprocal14(v.AsVector512()).AsVector();
+ }
+ else if (Avx.IsSupported && Vector.Count == 8)
+ {
+ // x86
+ return Avx.Reciprocal(v.AsVector256()).AsVector();
+ }
+ else if (AdvSimd.IsSupported && Vector.Count == 4)
+ {
+ // ARM
+ return AdvSimd.ReciprocalEstimate(v.AsVector128()).AsVector();
+ }
+ else if (Sse.IsSupported && Vector.Count == 4)
+ {
+ // x86
+ return Sse.Reciprocal(v.AsVector128()).AsVector();
+ }
+ else
+ {
+ // Exact reciprocal fallback (slower)
+ return Vector.One / v;
+ }
+ }
+
+ ///
+ /// Finds the leading zero count on each item of the vector.
+ ///
+ /// The vector to compute leading zero count for.
+ /// Input vector with leading zero count on each element.
+ internal static Vector Lzcnt(Vector vector)
+ {
+ if (Avx512CD.IsSupported && Vector.Count == 16)
+ {
+ Vector512 v512 = vector.AsVector512();
+ Vector512 lzcnt = Avx512CD.LeadingZeroCount(v512);
+ return lzcnt.AsVector();
+ }
+ else if (AdvSimd.IsSupported && Vector.Count == 4)
+ {
+ Vector128 v128 = vector.AsVector128();
+ Vector128 lzcnt = AdvSimd.LeadingZeroCount(v128);
+ return lzcnt.AsVector();
+ }
+ else
+ {
+ Span data = stackalloc int[Vector.Count];
+ ref int dataPtr = ref MemoryMarshal.GetReference(data);
+ vector.StoreUnsafe(ref dataPtr);
+
+ for (int i = 0; i < data.Length; i++)
+ {
+ Unsafe.Add(ref dataPtr, i) = BitOperations.LeadingZeroCount((nuint)Unsafe.Add(ref dataPtr, i));
+ }
+
+ return Vector.LoadUnsafe(ref dataPtr);
+ }
+ }
+
+ ///
+ /// Raises 2 to the power of each item in the vector.
+ ///
+ /// Input vector.
+ /// 2^x for each item in the vector.
+ internal static Vector Pow2(Vector vector)
+ {
+ if (Avx512F.IsSupported && Vector.Count == 16)
+ {
+ return Avx512F.ShiftLeftLogicalVariable(Vector512.One, vector.AsVector512().AsUInt32()).AsVector();
+ }
+ else if (Avx2.IsSupported && Vector.Count == 8)
+ {
+ return Avx2.ShiftLeftLogicalVariable(Vector256.One, vector.AsVector256().AsUInt32()).AsVector();
+ }
+ else
+ {
+ Span data = stackalloc int[Vector.Count];
+ ref int dataPtr = ref MemoryMarshal.GetReference(data);
+ vector.StoreUnsafe(ref dataPtr);
+
+ for (int i = 0; i < data.Length; i++)
+ {
+ Unsafe.Add(ref dataPtr, i) = 1 << Unsafe.Add(ref dataPtr, i);
+ }
+
+ return Vector.LoadUnsafe(ref dataPtr);
+ }
+ }
+
[Conditional("DEBUG")]
private static void DebugVerifySpanInput(ReadOnlySpan source, ReadOnlySpan dest, int shouldBeDivisibleBy)
{
diff --git a/src/ImageSharp/Common/Helpers/Vector128Utilities.cs b/src/ImageSharp/Common/Helpers/Vector128Utilities.cs
index 6bb1f59ef8..0e9a40dd1b 100644
--- a/src/ImageSharp/Common/Helpers/Vector128Utilities.cs
+++ b/src/ImageSharp/Common/Helpers/Vector128Utilities.cs
@@ -859,4 +859,84 @@ internal static class Vector128_
Vector128 unpacked = Vector128.Create(left.GetLower(), right.GetLower());
return Vector128.ShuffleNative(unpacked, Vector128.Create(0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15));
}
+
+ ///
+ /// Interleaves the lower half of the vector.
+ ///
+ /// First vector
+ /// Second vector
+ ///
+ /// { a[0], b[0], a[1], b[1] }
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 InterleaveLower(Vector128 a, Vector128 b)
+ {
+ Vector128 shuffledA = Vector128.Shuffle(a, Vector128.Create(0, 0, 1, 1));
+ Vector128 shuffledB = Vector128.Shuffle(b, Vector128.Create(0, 0, 1, 1));
+
+ Vector128 maskA = Vector128.Create(-1, 0, -1, 0);
+ Vector128 maskB = Vector128.Create(0, -1, 0, -1);
+
+ return (shuffledA & maskA) | (shuffledB & maskB);
+ }
+
+ ///
+ /// Interleaves the upper half of the vector.
+ ///
+ /// First vector
+ /// Second vector
+ ///
+ /// { a[2], b[2], a[3], b[3] }
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 InterleaveUpper(Vector128 a, Vector128 b)
+ {
+ Vector128 shuffledA = Vector128.Shuffle(a, Vector128.Create(2, 2, 3, 3));
+ Vector128 shuffledB = Vector128.Shuffle(b, Vector128.Create(2, 2, 3, 3));
+
+ Vector128 maskA = Vector128.Create(-1, 0, -1, 0);
+ Vector128 maskB = Vector128.Create(0, -1, 0, -1);
+
+ return (shuffledA & maskA) | (shuffledB & maskB);
+ }
+
+ ///
+ /// Interleaves the lower half of the vector.
+ ///
+ /// First vector
+ /// Second vector
+ ///
+ /// { a[0], b[0], a[1], b[1] }
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 InterleaveLower(Vector128 a, Vector128 b)
+ {
+ Vector128 shuffledA = Vector128.Shuffle(a, Vector128.Create(0, 0, 1, 1));
+ Vector128 shuffledB = Vector128.Shuffle(b, Vector128.Create(0, 0, 1, 1));
+
+ Vector128 maskA = Vector128.Create(-1, 0, -1, 0).AsSingle();
+ Vector128 maskB = Vector128.Create(0, -1, 0, -1).AsSingle();
+
+ return (shuffledA & maskA) | (shuffledB & maskB);
+ }
+
+ ///
+ /// Interleaves the upper half of the vector.
+ ///
+ /// First vector
+ /// Second vector
+ ///
+ /// { a[2], b[2], a[3], b[3] }
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 InterleaveUpper(Vector128 a, Vector128 b)
+ {
+ Vector128 shuffledA = Vector128.Shuffle(a, Vector128.Create(2, 2, 3, 3));
+ Vector128 shuffledB = Vector128.Shuffle(b, Vector128.Create(2, 2, 3, 3));
+
+ Vector128 maskA = Vector128.Create(-1, 0, -1, 0).AsSingle();
+ Vector128 maskB = Vector128.Create(0, -1, 0, -1).AsSingle();
+
+ return (shuffledA & maskA) | (shuffledB & maskB);
+ }
}
diff --git a/src/ImageSharp/Common/Helpers/Vector256Utilities.cs b/src/ImageSharp/Common/Helpers/Vector256Utilities.cs
index 1dd7122713..bb11108869 100644
--- a/src/ImageSharp/Common/Helpers/Vector256Utilities.cs
+++ b/src/ImageSharp/Common/Helpers/Vector256Utilities.cs
@@ -397,4 +397,210 @@ internal static class Vector256_
return Vector256.Create(lo, hi);
}
+
+ ///
+ /// Multiplies only the even indices of the two 256-bit vectors,
+ /// producing half as many elements of twice the element width.
+ ///
+ /// Left vector to multiply.
+ /// Right vector to multiply
+ ///
+ ///
+ /// {
+ /// A[0] * B[0],
+ /// A[2] * B[2],
+ /// A[4] * B[4],
+ /// A[6] * B[6]
+ /// }
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector256 MultiplyEven(Vector256 left, Vector256 right)
+ => Vector256.Create(
+ left[0] * right[0],
+ left[2] * right[2],
+ left[4] * right[4],
+ left[6] * right[6]);
+
+ ///
+ /// Multiplies only the odd indices of the two 256-bit vectors,
+ /// producing half as many elements of twice the element width.
+ ///
+ /// Left vector to multiply.
+ /// Right vector to multiply
+ ///
+ ///
+ /// {
+ /// A[1] * B[1],
+ /// A[3] * B[3],
+ /// A[5] * B[5],
+ /// A[7] * B[7]
+ /// }
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector256 MultiplyOdd(Vector256 left, Vector256 right)
+ => Vector256.Create(
+ left[1] * right[1],
+ left[3] * right[3],
+ left[5] * right[5],
+ left[7] * right[7]);
+
+ ///
+ /// Produces a vector by interleaving the even-indexed elements
+ /// of the left and right vectors.
+ ///
+ /// Left vector to interleave.
+ /// Right vector to interleave.
+ ///
+ ///
+ /// {
+ /// A[0], B[0],
+ /// A[2], B[2],
+ /// A[4], B[4],
+ /// A[6], B[6]
+ /// }
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector256 InterleaveEven(
+ Vector256 left,
+ Vector256 right)
+ => Vector256.Create(
+ left[0],
+ right[0],
+ left[2],
+ right[2],
+ left[4],
+ right[4],
+ left[6],
+ right[6]);
+
+ ///
+ /// Produces a vector by interleaving the odd-indexed elements
+ /// of the left and right vectors.
+ ///
+ /// Left vector to interleave.
+ /// Right vector to interleave.
+ ///
+ ///
+ /// {
+ /// A[1], B[1],
+ /// A[3], B[3],
+ /// A[5], B[5],
+ /// A[7], B[7]
+ /// }
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector256 InterleaveOdd(
+ Vector256 left,
+ Vector256 right)
+ => Vector256.Create(
+ left[1],
+ right[1],
+ left[3],
+ right[3],
+ left[5],
+ right[5],
+ left[7],
+ right[7]);
+
+ ///
+ /// Produces a vector with masks where 0xFFFFFFFF specifies
+ /// that the left value does not equal to the right value and
+ /// 0x00000000 specifies that the value equals to the
+ /// right value.
+ ///
+ /// Left vector to compare for inequality.
+ /// Right vector to compare for inequality.
+ ///
+ /// 0xFFFFFFFF for values that aren't equal, 0x00000000 for
+ /// values that are equal. This is essentially the inverse of
+ /// .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector256 NotEqual(
+ Vector256 left,
+ Vector256 right) => ~Vector256.Equals(left, right);
+
+ ///
+ /// Interleaves the lower half of the vector.
+ ///
+ /// First vector
+ /// Second vector
+ ///
+ /// { a[0], b[0], a[1], b[1], a[2], b[2], a[3], b[3] }
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector256 InterleaveLower(Vector256 a, Vector256 b)
+ {
+ Vector256 shuffledA = Vector256.Shuffle(a, Vector256.Create(0, 0, 1, 1, 2, 2, 3, 3));
+ Vector256 shuffledB = Vector256.Shuffle(b, Vector256.Create(0, 0, 1, 1, 2, 2, 3, 3));
+
+ Vector256 maskA = Vector256.Create(-1, 0, -1, 0, -1, 0, -1, 0);
+ Vector256 maskB = Vector256.Create(0, -1, 0, -1, 0, -1, 0, -1);
+
+ return (shuffledA & maskA) | (shuffledB & maskB);
+ }
+
+ ///
+ /// Interleaves the upper half of the vector.
+ ///
+ /// First vector
+ /// Second vector
+ ///
+ /// { a[4], b[4], a[5], b[5], a[6], b[6], a[7], b[7] }
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector256 InterleaveUpper(Vector256 a, Vector256 b)
+ {
+ Vector256 shuffledA = Vector256.Shuffle(a, Vector256.Create(4, 4, 5, 5, 6, 6, 7, 7));
+ Vector256 shuffledB = Vector256.Shuffle(b, Vector256.Create(4, 4, 5, 5, 6, 6, 7, 7));
+
+ Vector256 maskA = Vector256.Create(-1, 0, -1, 0, -1, 0, -1, 0);
+ Vector256 maskB = Vector256.Create(0, -1, 0, -1, 0, -1, 0, -1);
+
+ return (shuffledA & maskA) | (shuffledB & maskB);
+ }
+
+ ///
+ /// Interleaves the lower half of the vector.
+ ///
+ /// First vector
+ /// Second vector
+ ///
+ /// { a[0], b[0], a[1], b[1], a[2], b[2], a[3], b[3] }
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector256 InterleaveLower(Vector256 a, Vector256 b)
+ {
+ Vector256 shuffledA = Vector256.Shuffle(a, Vector256.Create(0, 0, 1, 1, 2, 2, 3, 3));
+ Vector256 shuffledB = Vector256.Shuffle(b, Vector256.Create(0, 0, 1, 1, 2, 2, 3, 3));
+
+ Vector256 maskA = Vector256.Create(-1, 0, -1, 0, -1, 0, -1, 0).AsSingle();
+ Vector256 maskB = Vector256.Create(0, -1, 0, -1, 0, -1, 0, -1).AsSingle();
+
+ return (shuffledA & maskA) | (shuffledB & maskB);
+ }
+
+ ///
+ /// Interleaves the upper half of the vector.
+ ///
+ /// First vector
+ /// Second vector
+ ///
+ /// { a[4], b[4], a[5], b[5], a[6], b[6], a[7], b[7] }
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector256 InterleaveUpper(Vector256 a, Vector256 b)
+ {
+ Vector256 shuffledA = Vector256.Shuffle(a, Vector256.Create(4, 4, 5, 5, 6, 6, 7, 7));
+ Vector256 shuffledB = Vector256.Shuffle(b, Vector256.Create(4, 4, 5, 5, 6, 6, 7, 7));
+
+ Vector256 maskA = Vector256.Create(-1, 0, -1, 0, -1, 0, -1, 0).AsSingle();
+ Vector256 maskB = Vector256.Create(0, -1, 0, -1, 0, -1, 0, -1).AsSingle();
+
+ return (shuffledA & maskA) | (shuffledB & maskB);
+ }
}
diff --git a/src/ImageSharp/Common/InlineArray.cs b/src/ImageSharp/Common/InlineArray.cs
index 700551a8f3..a72e3f5394 100644
--- a/src/ImageSharp/Common/InlineArray.cs
+++ b/src/ImageSharp/Common/InlineArray.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
//
@@ -44,6 +44,15 @@ internal struct InlineArray16
private T t;
}
+///
+/// Represents a safe, fixed sized buffer of 17 elements.
+///
+[InlineArray(17)]
+internal struct InlineArray17
+{
+ private T t;
+}
+
///
/// Represents a safe, fixed sized buffer of 18 elements.
///
@@ -71,6 +80,24 @@ internal struct InlineArray26
private T t;
}
+///
+/// Represents a safe, fixed sized buffer of 32 elements.
+///
+[InlineArray(32)]
+internal struct InlineArray32
+{
+ private T t;
+}
+
+///
+/// Represents a safe, fixed sized buffer of 33 elements.
+///
+[InlineArray(33)]
+internal struct InlineArray33
+{
+ private T t;
+}
+
///
/// Represents a safe, fixed sized buffer of 36 elements.
///
@@ -88,3 +115,5 @@ internal struct InlineArray256
{
private T t;
}
+
+
diff --git a/src/ImageSharp/Common/InlineArray.tt b/src/ImageSharp/Common/InlineArray.tt
index d689b0469a..d15650592a 100644
--- a/src/ImageSharp/Common/InlineArray.tt
+++ b/src/ImageSharp/Common/InlineArray.tt
@@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp;
<#GenerateInlineArrays();#>
<#+
-private static int[] Lengths = [4, 8, 14, 16, 18, 19, 26, 36, 256];
+private static int[] Lengths = [4, 8, 14, 16, 17, 18, 19, 26, 32, 33, 36, 256];
void GenerateInlineArrays()
{
diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs
new file mode 100644
index 0000000000..7e5a4367e1
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.ColorProfiles;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
+
+///
+/// RGB primaries for CIEXY
+///
+internal struct JxlCieXyPrimaries
+{
+ ///
+ /// Gets or sets the R component
+ ///
+ public CieXyChromaticityCoordinates R { get; set; }
+
+ ///
+ /// Gets or sets the G component
+ ///
+ public CieXyChromaticityCoordinates G { get; set; }
+
+ ///
+ /// Gets or sets the B component
+ ///
+ public CieXyChromaticityCoordinates B { get; set; }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs
new file mode 100644
index 0000000000..9a87dccc2d
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs
@@ -0,0 +1,77 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
+
+internal sealed class JxlColorEncoding
+{
+ public JxlWhitePoint WhitePoint { get; set; } = JxlWhitePoint.D65;
+
+ public JxlPrimaries Primaries { get; set; } = JxlPrimaries.SRgb;
+
+ public JxlRenderingIntent RenderingIntent { get; set; } = JxlRenderingIntent.Relative;
+
+ public bool HaveFields { get; set; } = true;
+
+ public JxlIccBytes? Icc { get; set; }
+
+ public JxlColorSpace ColorSpace { get; set; } = JxlColorSpace.Rgb;
+
+ public bool Cmyk { get; set; }
+
+ public JxlCustomTransferFunction TransferFunction { get; set; }
+
+ public JxlCustomXy White { get; set; }
+
+ public JxlCustomXy Red { get; set; }
+
+ public JxlCustomXy Green { get; set; }
+
+ public JxlCustomXy Blue { get; set; }
+
+ public bool HasPrimaries => this.ColorSpace is not (JxlColorSpace.Gray or JxlColorSpace.Xyb);
+
+ public int Channels => (this.ColorSpace == JxlColorSpace.Gray) ? 1 : 3;
+
+ public bool TryGetPrimaries(out JxlCieXyPrimaries xy)
+ {
+ xy = default;
+
+ if (!this.HasPrimaries || !this.HasPrimaries)
+ {
+ return false;
+ }
+
+ switch (this.Primaries)
+ {
+ case JxlPrimaries.Custom:
+ xy.R = this.Red.GetValue();
+ xy.G = this.Green.GetValue();
+ xy.B = this.Blue.GetValue();
+ break;
+
+ case JxlPrimaries.SRgb:
+ xy.R = new(0.639998686f, 0.330010138f);
+ xy.G = new(0.300003784f, 0.600003357f);
+ xy.B = new(0.150002046f, 0.059997204f);
+ break;
+
+ case JxlPrimaries.Bt2020:
+ xy.R = new(0.708f, 0.292f);
+ xy.G = new(0.170f, 0.797f);
+ xy.B = new(0.131f, 0.046f);
+ break;
+
+ case JxlPrimaries.P3:
+ xy.R = new(0.680f, 0.320f);
+ xy.G = new(0.265f, 0.690f);
+ xy.B = new(0.150f, 0.060f);
+ break;
+
+ default:
+ throw new InvalidOperationException("Invalid primaries: " + this.Primaries);
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlColorSpace.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlColorSpace.cs
new file mode 100644
index 0000000000..b65cbbef98
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/JxlColorSpace.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
+
+///
+/// Supported, JPEG XL-specific color space types.
+///
+internal enum JxlColorSpace : byte
+{
+ ///
+ /// Trichromatic color data. This also includes CMYK if Black
+ /// ExtraChannelInfo is present.
+ ///
+ Rgb,
+
+ ///
+ /// Single-channel data.
+ ///
+ Gray,
+
+ ///
+ /// Like Rgb but fixed values for primaries.
+ ///
+ Xyb,
+
+ ///
+ /// Unknown color space
+ ///
+ Unknown
+}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs
new file mode 100644
index 0000000000..acf049d4a7
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs
@@ -0,0 +1,109 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
+
+internal struct JxlCustomTransferFunction
+{
+ private const uint MaxGamma = 8192;
+ private const uint GammaMultiplier = 10000000;
+
+ public JxlCustomTransferFunction()
+ {
+ }
+
+ public bool HaveGamma { get; set; }
+
+ public uint Gamma { get; set; }
+
+ public JxlTransferFunction TransferFunction { get; set; } = JxlTransferFunction.SRgb;
+
+ public readonly bool IsUnknown => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Unknown;
+
+ public readonly bool IsSrgb => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.SRgb;
+
+ public readonly bool IsLinear => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Linear;
+
+ public readonly bool IsPq => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Pq;
+
+ public readonly bool IsHlg => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Hlg;
+
+ public readonly bool Is709 => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Bt709;
+
+ public readonly bool IsDci => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Dci;
+
+ public readonly JxlTransferFunction GetTransferFunction()
+ {
+ if (this.HaveGamma)
+ {
+ return JxlTransferFunction.Unknown;
+ }
+
+ return this.TransferFunction;
+ }
+
+ public void SetTransferFunction(JxlTransferFunction tf)
+ {
+ this.HaveGamma = false;
+ this.TransferFunction = tf;
+ }
+
+ public readonly float GetGamma()
+ {
+ if (!this.HaveGamma)
+ {
+ return 0.0f;
+ }
+
+ return this.Gamma * (1.0f / GammaMultiplier);
+ }
+
+ public void SetGamma(float newGamma)
+ {
+ if (newGamma is < 1.0f / MaxGamma or > 1.0f)
+ {
+ throw new InvalidOperationException($"Invalid gamma {newGamma}");
+ }
+
+ this.HaveGamma = false;
+
+ if (IsAlmostEqual(newGamma, 1.0f))
+ {
+ this.TransferFunction = JxlTransferFunction.Linear;
+ return;
+ }
+
+ if (IsAlmostEqual(newGamma, 1.0f / 2.6f))
+ {
+ this.TransferFunction = JxlTransferFunction.Dci;
+ return;
+ }
+
+ // Don't translate 0.45.. to kSRGB nor k709 - that might change pixel
+ // values because those curves also have a linear part.
+ this.HaveGamma = true;
+ this.Gamma = (uint)MathF.Round((float)(newGamma * GammaMultiplier));
+ this.TransferFunction = JxlTransferFunction.Unknown;
+ }
+
+ public readonly bool IsSame(JxlCustomTransferFunction other)
+ {
+ if (this.HaveGamma != other.HaveGamma)
+ {
+ return false;
+ }
+
+ if (this.HaveGamma)
+ {
+ return this.Gamma == other.Gamma;
+ }
+
+ return this.TransferFunction == other.TransferFunction;
+ }
+
+ private static bool IsAlmostEqual(float a, float b)
+ {
+ const float dist = 1e-3f;
+ return MathF.Abs(a - b) < dist;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs
new file mode 100644
index 0000000000..95652cdaf4
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs
@@ -0,0 +1,53 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.ColorProfiles;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
+
+///
+/// A serializable form of CieXyChromaticityCoordinates
+///
+internal struct JxlCustomXy
+{
+ private const uint Multiplier = 1000000;
+ private const float RoughLimit = 4.0f;
+ private const int Min = -0x200000;
+ private const int Max = 0x1FFFFF;
+
+ public int X { get; set; }
+
+ public int Y { get; set; }
+
+ public readonly CieXyChromaticityCoordinates GetValue() => new(
+ x: this.X * (1.0f / Multiplier),
+ y: this.Y * (1.0f / Multiplier));
+
+ public bool SetValue(CieXyChromaticityCoordinates xy)
+ {
+ bool ok = (Math.Abs(xy.X) < RoughLimit) && (Math.Abs(xy.Y) < RoughLimit);
+
+ if (!ok)
+ {
+ throw new InvalidOperationException("X or Y is out of bounds");
+ }
+
+ this.X = (int)MathF.Round((float)(xy.X * Multiplier));
+
+ if (this.X is < Min or > Max)
+ {
+ throw new InvalidOperationException("X is out of bounds");
+ }
+
+ this.Y = (int)MathF.Round((float)(xy.Y * Multiplier));
+
+ if (this.Y is < Min or > Max)
+ {
+ throw new InvalidOperationException("Y is out of bounds");
+ }
+
+ return true;
+ }
+
+ public readonly bool IsSame(CieXyChromaticityCoordinates other) => this.X == other.X && this.Y == other.Y;
+}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs
new file mode 100644
index 0000000000..2b304885d1
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
+
+///
+/// Opsin constants used by the color management system
+///
+internal static class JxlOpsinConstants
+{
+ public const float BScale = 1f;
+
+ // The following constants are used for XYB.
+ // They can be adjusted to change how Y<->B ratio
+ // works. For example, YToBRatio works better
+ // with 0.50017729543783418.
+ public const float YToBRatio = 1f;
+ public const float BToYRatio = 1f / YToBRatio;
+
+ // Adjusting these constants influences the opsin absorbance.
+ public const float OpsinAbsorbanceBias0 = 0.0037930732552754493f;
+ public const float OpsinAbsorbanceBias1 = OpsinAbsorbanceBias0;
+ public const float OpsinAbsorbanceBias2 = OpsinAbsorbanceBias0;
+
+ public static ReadOnlySpan OpsinAbsorbanceBias => [OpsinAbsorbanceBias0, OpsinAbsorbanceBias1, OpsinAbsorbanceBias2];
+}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlPrimaries.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlPrimaries.cs
new file mode 100644
index 0000000000..48d4766fa6
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/JxlPrimaries.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
+
+///
+/// JPEG XL primaries
+///
+internal enum JxlPrimaries : byte
+{
+ ///
+ /// Same as ITU-R BT.709
+ ///
+ SRgb = 1,
+
+ ///
+ /// Values encoded in separate fields
+ ///
+ Custom = 2,
+
+ ///
+ /// ITU-R BT.2020
+ ///
+ Bt2020 = 9,
+
+ P3 = 11,
+}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs
new file mode 100644
index 0000000000..739dddcccd
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
+
+internal enum JxlRenderingIntent : byte
+{
+ // Values match ICC sRGB encodings
+ Perceptual, // Good for photos, requires a profile with LUT
+ Relative, // Good for logos
+ Saturation, // Perhaps useful for CG with fully saturated colors
+ Absolute, // Leaves white point unchanged; good for proofing
+}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlTransferFunction.cs
new file mode 100644
index 0000000000..cfee681765
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/JxlTransferFunction.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
+
+///
+/// JPEG XL transfer function type
+///
+internal enum JxlTransferFunction : byte
+{
+ ///
+ /// ITU-R BT.709
+ ///
+ Bt709 = 1,
+
+ ///
+ /// Unknown transfer function
+ ///
+ Unknown = 2,
+
+ ///
+ /// Linear transfer function
+ ///
+ Linear = 8,
+
+ ///
+ /// sRGB
+ ///
+ SRgb = 13,
+
+ ///
+ /// From ITU-R BT.2100
+ ///
+ Pq = 16,
+
+ ///
+ /// From SMPTE RP 431-2 reference projector
+ ///
+ Dci = 17,
+
+ ///
+ /// From ITU-R BT.2100
+ ///
+ Hlg = 18,
+}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlWhitePoint.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlWhitePoint.cs
new file mode 100644
index 0000000000..c6c7e019f5
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/JxlWhitePoint.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
+
+///
+/// White point from CICP Color Primaries.
+///
+// Note that we define a separate enum instead of using the ColorPrimaries
+// enum from CICP code because JPEG XL doesn't support all color primaries defined
+// by CICP.
+internal enum JxlWhitePoint : byte
+{
+ ///
+ /// sRGB/ITU-R BT.709/Display P3/ITU-R BT.2020
+ ///
+ D65 = 1,
+
+ ///
+ /// Actual values encoded in separate fields
+ ///
+ Custom = 2,
+
+ ///
+ /// XYZ
+ ///
+ E = 10,
+
+ ///
+ /// DCI-P3
+ ///
+ Dci = 11,
+}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/README.md b/src/ImageSharp/Formats/Jxl/Cms/README.md
new file mode 100644
index 0000000000..557a4c8b91
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/README.md
@@ -0,0 +1,4 @@
+# CMS
+This is the JPEG XL Color Management System component.
+
+Not to be confused with Content Management System.
diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlBt709TransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlBt709TransferFunction.cs
new file mode 100644
index 0000000000..dfc9de66ea
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlBt709TransferFunction.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions;
+
+///
+/// ITU-R BT.709 transfer function
+///
+internal static class JxlBt709TransferFunction
+{
+ // Encoded From Display constants
+ private const float Threshold = 0.018f;
+ private const float MulLow = 4.5f;
+ private const float MulHi = 1.099f;
+ private const float PowHi = 0.45f;
+ private const float Sub = -0.099f;
+
+ // Display From Encoded constants
+ private const float InverseThreshold = 0.081f;
+ private const float InverseMulLow = 1f / 4.5f;
+ private const float InverseMulHi = 1f / 1.099f;
+ private const float InversePowHi = 1f / 0.45f;
+ private const float InverseAdd = 0.099f * InverseMulHi;
+
+ public static float EncodedFromDisplay(float d)
+ {
+ if (d < Threshold)
+ {
+ return MulLow * d;
+ }
+
+ return (MulHi * MathF.Pow(d, PowHi)) + Sub;
+ }
+
+ public static Vector EncodedFromDisplay(Vector x)
+ {
+ Vector low = Vector.Create(MulLow) * x;
+ Vector high = (Vector.Create(MulHi) * JxlSimdUtils.FastPowf(x, Vector.Create(PowHi))) + Vector.Create(Sub);
+ return Vector.ConditionalSelect(
+ Vector.LessThanOrEqual(x, Vector.Create(Threshold)),
+ low,
+ high);
+ }
+
+ public static Vector DisplayFromEncoded(Vector x)
+ {
+ Vector low = Vector.Create(InverseMulLow) * x;
+ Vector high = JxlSimdUtils.FastPowf((x * Vector.Create(InverseMulHi)) + Vector.Create(InverseAdd), Vector.Create(InversePowHi));
+ return Vector.ConditionalSelect(
+ Vector.LessThan(x, Vector.Create(InverseThreshold)),
+ low,
+ high);
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHlgTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHlgTransferFunction.cs
new file mode 100644
index 0000000000..053a2a5cfb
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHlgTransferFunction.cs
@@ -0,0 +1,159 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions;
+
+///
+/// Hybrid Log Gamma transfer function.
+///
+internal sealed class JxlHlgTransferFunction
+{
+ // Shared constants used by transfer functions
+ private const float A = 0.17883277f;
+ private const float RA = 1.0f / A;
+ private const float B = 1 - (4 * A);
+ private const float C = 0.5599107295f;
+ private const float Inverse12 = 1.0f / 12.0f;
+ private const float HiAdd = B * Inverse12;
+ private const float HiMul = 0.003639807079052639f; // MathF.Exp(-C * RA) * Inverse12
+ private const float HiPow = 8.067285659607931f; // RA * JxlMath.InverseLog2E
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// Use static methods. Don't instantiate this class.
+ ///
+ private JxlHlgTransferFunction()
+ {
+ }
+
+ public static Vector EncodedFromDisplay(Vector x)
+ {
+ Vector sign = Vector.Create(0x80000000u).As();
+ Vector originalSign = x & sign;
+ x = Vector.AndNot(sign, x);
+ Vector belowInverse12 = Vector.LessThan(x, Vector.Create(Inverse12));
+
+ Vector lo = Vector.SquareRoot(Vector.Create(3.0f) * x);
+ Vector hi = (Vector.Create(A * JxlMath.InverseLog2E) * Vector.Log2((Vector.Create(12f) * x) + Vector.Create(-B))) + Vector.Create(C);
+ Vector magnitude = Vector.ConditionalSelect(belowInverse12, lo, hi);
+ return Vector.AndNot(sign, magnitude) | originalSign;
+ }
+
+ public static Vector DisplayFromEncoded(Vector x)
+ {
+ Vector sign = Vector.Create(0x80000000u).As();
+ Vector originalSign = x & sign;
+ x = Vector.AndNot(sign, x);
+ Vector below05 = Vector.LessThan(x, Vector.Create(0.5f));
+
+ Vector lo = x * (x * Vector.Create(1f / 3f));
+ Vector hi = (Pow2(x * Vector.Create(HiPow)) * Vector.Create(HiMul)) + Vector.Create(HiAdd);
+ Vector magnitude = Vector.ConditionalSelect(below05, lo, hi);
+
+ return Vector.AndNot(sign, magnitude) | originalSign;
+ }
+
+ private static Vector Pow2(Vector x) => JxlSimdUtils.FastPow2f(x);
+
+ ///
+ /// Converts encoded signal to display signal.
+ ///
+ /// The encoded signal
+ /// The display signal
+ public static double DisplayFromEncoded(double encoded) => Ootf(InverseOotf(encoded));
+
+ ///
+ /// Converts display signal to encoded signal.
+ ///
+ /// The display signal
+ /// The encoded signal
+ public static double EncodedFromDisplay(double display) => Oetf(InverseOetf(display));
+
+ ///
+ /// Opto-Electronic Transfer Function - converts
+ /// real-world scene light (s) into a digital video
+ /// signal inside a camera.
+ ///
+ ///
+ ///
+ ///
+ /// Scene light
+ /// Digital video signal
+ public static double Oetf(double s)
+ {
+ if (s == 0)
+ {
+ return 0;
+ }
+
+ double originalSign = s;
+
+ s = Math.Abs(s);
+
+ if (s <= Inverse12)
+ {
+ return Math.CopySign(Math.Sqrt(3.0 * s), originalSign);
+ }
+
+ double e = (A * Math.Log((12 * s) - B)) + C;
+ DebugGuard.MustBeGreaterThan(e, 0.0, nameof(e));
+
+ return Math.CopySign(e, originalSign);
+ }
+
+ ///
+ /// Inverse Opto-Electronic Transfer Function - converts
+ /// digital video signal into a real-world scene light.
+ ///
+ ///
+ ///
+ ///
+ /// Digital video signal
+ /// Scene light
+ public static double InverseOetf(double e)
+ {
+ if (e == 0)
+ {
+ return 0;
+ }
+
+ double originalSign = e;
+
+ e = Math.Abs(e);
+
+ if (e <= 0.5)
+ {
+ return Math.CopySign(e * e * (1.0 / 3), originalSign);
+ }
+
+ double s = (Math.Exp((e - C) * RA) + B) * Inverse12;
+ DebugGuard.MustBeGreaterThan(s, 0.0, nameof(s));
+
+ return Math.CopySign(s, originalSign);
+ }
+
+ ///
+ /// Opto-Optical Transfer Function - as-is.
+ ///
+ ///
+ ///
+ ///
+ /// Input signal
+ /// Digital video signal
+ public static double Ootf(double s) => s;
+
+ ///
+ /// Inverse Opto-Optical Transfer Function - as-is.
+ ///
+ ///
+ ///
+ ///
+ /// Digital video signal
+ /// Scene light
+ public static double InverseOotf(double s) => s;
+}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPqTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPqTransferFunction.cs
new file mode 100644
index 0000000000..f0df5c00c7
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPqTransferFunction.cs
@@ -0,0 +1,141 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions;
+
+///
+/// Perceptual Quantization transfer function.
+///
+internal struct JxlPqTransferFunction(float displayIntensityTarget = DefaultIntensityTarget)
+{
+ private const double M1 = 2610.0 / 16384;
+ private const double M2 = (2523.0 / 4096) * 128;
+ private const double C1 = 3424.0 / 4096;
+ private const double C2 = (2413.0 / 4096) * 32;
+ private const double C3 = (2392.0 / 4096) * 32;
+
+ private readonly float scaleFactorTo10000Nits = displayIntensityTarget * (1.0f / 10000.0f);
+ private readonly float scaleFactorFrom10000Nits = 10000.0f / displayIntensityTarget;
+
+ private static ReadOnlySpan DisplayP =>
+ [
+ 2.62975656e-04f, 2.62975656e-04f, 2.62975656e-04f, 2.62975656e-04f,
+ -6.23553089e-03f, -6.23553089e-03f, -6.23553089e-03f, -6.23553089e-03f,
+ 7.38602301e-01f, 7.38602301e-01f, 7.38602301e-01f, 7.38602301e-01f,
+ 2.64553172e+00f, 2.64553172e+00f, 2.64553172e+00f, 2.64553172e+00f,
+ 5.50034862e-01f, 5.50034862e-01f, 5.50034862e-01f, 5.50034862e-01f
+ ];
+
+ private static ReadOnlySpan DisplayQ =>
+ [
+ 4.21350107e+02f, 4.21350107e+02f, 4.21350107e+02f, 4.21350107e+02f,
+ -4.28736818e+02f, -4.28736818e+02f, -4.28736818e+02f, -4.28736818e+02f,
+ 1.74364667e+02f, 1.74364667e+02f, 1.74364667e+02f, 1.74364667e+02f,
+ -3.39078883e+01f, -3.39078883e+01f, -3.39078883e+01f, -3.39078883e+01f,
+ 2.67718770e+00f, 2.67718770e+00f, 2.67718770e+00f, 2.67718770e+00f
+ ];
+
+ private static ReadOnlySpan EncodedP =>
+ [
+ 1.351392e-02f, 1.351392e-02f, 1.351392e-02f, 1.351392e-02f,
+ -1.095778e+00f, -1.095778e+00f, -1.095778e+00f, -1.095778e+00f,
+ 5.522776e+01f, 5.522776e+01f, 5.522776e+01f, 5.522776e+01f,
+ 1.492516e+02f, 1.492516e+02f, 1.492516e+02f, 1.492516e+02f,
+ 4.838434e+01f, 4.838434e+01f, 4.838434e+01f, 4.838434e+01f
+ ];
+
+ private static ReadOnlySpan EncodedQ =>
+ [
+ 1.012416e+00f, 1.012416e+00f, 1.012416e+00f, 1.012416e+00f,
+ 2.016708e+01f, 2.016708e+01f, 2.016708e+01f, 2.016708e+01f,
+ 9.263710e+01f, 9.263710e+01f, 9.263710e+01f, 9.263710e+01f,
+ 1.120607e+02f, 1.120607e+02f, 1.120607e+02f, 1.120607e+02f,
+ 2.590418e+01f, 2.590418e+01f, 2.590418e+01f, 2.590418e+01f
+ ];
+
+ private static ReadOnlySpan EncodedPLo =>
+ [
+ 9.863406e-06f, 9.863406e-06f, 9.863406e-06f, 9.863406e-06f,
+ 3.881234e-01f, 3.881234e-01f, 3.881234e-01f, 3.881234e-01f,
+ 1.352821e+02f, 1.352821e+02f, 1.352821e+02f, 1.352821e+02f,
+ 6.889862e+04f, 6.889862e+04f, 6.889862e+04f, 6.889862e+04f,
+ -2.864824e+05f, -2.864824e+05f, -2.864824e+05f, -2.864824e+05f
+ ];
+
+ private static ReadOnlySpan EncodedQLo =>
+ [
+ 3.371868e+01f, 3.371868e+01f, 3.371868e+01f, 3.371868e+01f,
+ 1.477719e+03f, 1.477719e+03f, 1.477719e+03f, 1.477719e+03f,
+ 1.608477e+04f, 1.608477e+04f, 1.608477e+04f, 1.608477e+04f,
+ -4.389884e+04f, -4.389884e+04f, -4.389884e+04f, -4.389884e+04f,
+ -2.072546e+05f, -2.072546e+05f, -2.072546e+05f, -2.072546e+05f
+ ];
+
+ public static double DisplayFromEncoded(float displayIntensityTarget, double e)
+ {
+ if (e == 0.0)
+ {
+ return 0.0;
+ }
+
+ double originalSign = e;
+
+ e = Math.Abs(e);
+
+ double xp = Math.Pow(e, 1.0 / M2);
+ double num = Math.Max(xp - C1, 0.0);
+ double den = C2 - (C3 * xp);
+ double d = Math.Pow(num / den, 1.0 / M1);
+
+ return Math.CopySign(d * (10000.0 / displayIntensityTarget), originalSign);
+ }
+
+ public static double EncodedFromDisplay(float displayIntensityTarget, double d)
+ {
+ if (d == 0.0)
+ {
+ return 0.0;
+ }
+
+ double originalSign = d;
+
+ d = Math.Abs(d);
+
+ double xp = Math.Pow(d * (displayIntensityTarget * (1 / 10000)), M1);
+ double num = C1 + (xp * C2);
+ double den = 1.0 + (xp * C3);
+ double e = Math.Pow(num / den, M2);
+
+ return Math.CopySign(e, originalSign);
+ }
+
+ public Vector DisplayFromEncoded(Vector x)
+ {
+ Vector sign = Vector.Create(0x80000000u).As();
+ Vector originalSign = x & sign;
+ x = Vector.AndNot(sign, x);
+
+ Vector xpxx = (x * x) + x;
+ Vector magnitude = EvaluateRationalPolynomial(xpxx, DisplayP, DisplayQ);
+
+ return Vector.AndNot(sign, magnitude * Vector.Create(this.scaleFactorFrom10000Nits)) | originalSign;
+ }
+
+ public Vector EncodedFromDisplay(Vector x)
+ {
+ Vector sign = Vector.Create(0x80000000u).As();
+ Vector originalSign = x & sign;
+ x = Vector.AndNot(sign, x);
+
+ Vector xto025 = Vector.SquareRoot(Vector.SquareRoot(x * Vector.Create(this.scaleFactorTo10000Nits)));
+
+ Vector magnitude = Vector.ConditionalSelect(
+ Vector.LessThan(x, Vector.Create(1e-4f)),
+ EvaluateRationalPolynomial(xto025, EncodedPLo, EncodedQLo),
+ EvaluateRationalPolynomial(xto025, EncodedP, EncodedQ));
+
+ return Vector.AndNot(sign, magnitude) | originalSign;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlSRgbTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlSRgbTransferFunction.cs
new file mode 100644
index 0000000000..adc817ff78
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlSRgbTransferFunction.cs
@@ -0,0 +1,82 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions;
+
+internal static class JxlSRgbTransferFunction
+{
+ private const float ThresholdSRGBToLinear = 0.04045f;
+ private const float ThresholdLinearToSRGB = 0.0031308f;
+ private const float LowDiv = 12.92f;
+ private const float LowDivInverse = 1.0f / LowDiv;
+
+ private static ReadOnlySpan DisplayP =>
+ [
+ 2.200248328e-04f, 2.200248328e-04f, 2.200248328e-04f, 2.200248328e-04f,
+ 1.043637593e-02f, 1.043637593e-02f, 1.043637593e-02f, 1.043637593e-02f,
+ 1.624820318e-01f, 1.624820318e-01f, 1.624820318e-01f, 1.624820318e-01f,
+ 7.961564959e-01f, 7.961564959e-01f, 7.961564959e-01f, 7.961564959e-01f,
+ 8.210152774e-01f, 8.210152774e-01f, 8.210152774e-01f, 8.210152774e-01f
+ ];
+
+ private static ReadOnlySpan DisplayQ =>
+ [
+ 2.631846970e-01f, 2.631846970e-01f, 2.631846970e-01f, 2.631846970e-01f,
+ 1.076976492e+00f, 1.076976492e+00f, 1.076976492e+00f, 1.076976492e+00f,
+ 4.987528350e-01f, 4.987528350e-01f, 4.987528350e-01f, 4.987528350e-01f,
+ -5.512498495e-02f, -5.512498495e-02f, -5.512498495e-02f, -5.512498495e-02f,
+ 6.521209011e-03f, 6.521209011e-03f, 6.521209011e-03f, 6.521209011e-03f
+ ];
+
+ private static ReadOnlySpan EncodedP =>
+ [
+ -5.135152395e-04f, -5.135152395e-04f, -5.135152395e-04f, -5.135152395e-04f,
+ 5.287254571e-03f, 5.287254571e-03f, 5.287254571e-03f, 5.287254571e-03f,
+ 3.903842876e-01f, 3.903842876e-01f, 3.903842876e-01f, 3.903842876e-01f,
+ 1.474205315e+00f, 1.474205315e+00f, 1.474205315e+00f, 1.474205315e+00f,
+ 7.352629620e-01f, 7.352629620e-01f, 7.352629620e-01f, 7.352629620e-01f
+ ];
+
+ private static ReadOnlySpan EncodedQ =>
+ [
+ 1.004519624e-02f, 1.004519624e-02f, 1.004519624e-02f, 1.004519624e-02f,
+ 3.036675394e-01f, 3.036675394e-01f, 3.036675394e-01f, 3.036675394e-01f,
+ 1.340816930e+00f, 1.340816930e+00f, 1.340816930e+00f, 1.340816930e+00f,
+ 9.258482155e-01f, 9.258482155e-01f, 9.258482155e-01f, 9.258482155e-01f,
+ 2.424867759e-02f, 2.424867759e-02f, 2.424867759e-02f, 2.424867759e-02f
+ ];
+
+ public static Vector DisplayFromEncoded(Vector x)
+ {
+ Vector sign = Vector.Create(0x80000000u).As();
+ Vector originalSign = x & sign;
+ x = Vector.AndNot(sign, x);
+
+ Vector linear = x * Vector.Create(LowDivInverse);
+ Vector poly = EvaluateRationalPolynomial(x, DisplayP, DisplayQ);
+ Vector magnitude = Vector.ConditionalSelect(
+ Vector.GreaterThan(x, Vector.Create(ThresholdSRGBToLinear)),
+ poly,
+ linear);
+
+ return Vector.AndNot(sign, magnitude) | originalSign;
+ }
+
+ public static Vector EncodedFromDisplay(Vector x)
+ {
+ Vector sign = Vector.Create(0x80000000u).As();
+ Vector originalSign = x & sign;
+ x = Vector.AndNot(sign, x);
+
+ Vector linear = x * Vector.Create(LowDiv);
+ Vector poly = EvaluateRationalPolynomial(Vector.SquareRoot(x), DisplayP, DisplayQ);
+ Vector magnitude = Vector.ConditionalSelect(
+ Vector.GreaterThan(x, Vector.Create(ThresholdLinearToSRGB)),
+ poly,
+ linear);
+
+ return Vector.AndNot(sign, magnitude) | originalSign;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlTransferUtils.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlTransferUtils.cs
new file mode 100644
index 0000000000..8b9760c73d
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlTransferUtils.cs
@@ -0,0 +1,71 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions;
+
+internal static class JxlTransferUtils
+{
+ public static Vector FastLinearToSrgb(Vector v)
+ {
+ Vector v025_05 = ((v.As() & new Vector(0x3effffff)) | new Vector(0x3e800000)).As();
+
+ Vector d1 = (v025_05 * Vector.Create(0.059914046f)) - Vector.Create(0.108894556f);
+ Vector d2 = (d1 * v025_05) + Vector.Create(0.107963754f);
+ Vector pow = (d2 * v025_05) + Vector.Create(0.018092343f);
+
+ const uint baseBits = 0x40000000;
+
+ ReadOnlySpan powers25to18 =
+ [
+ 0x00, 0x0a, 0x19, 0x26,
+ 0x32, 0x41, 0x4d, 0x5c,
+ 0x68, 0x75, 0x83, 0x8f,
+ 0xa0, 0xaa, 0xb9, 0xc6
+ ];
+
+ ReadOnlySpan powers17to10 =
+ [
+ 0x00, 0xb7, 0x04, 0x0d,
+ 0xcb, 0xe7, 0x41, 0x68,
+ 0x51, 0xd1, 0xeb, 0xf2,
+ 0x00, 0xb7, 0x04, 0x0d
+ ];
+
+ Vector bits = Vector.AsVectorInt32(v);
+ Vector exp = (bits >> 23) - Vector.Create(118);
+
+ exp &= new Vector(0xf);
+
+ Vector p25to18 = GatherByteTable(exp, powers25to18);
+ Vector p17to10 = GatherByteTable(exp, powers17to10);
+
+ Vector mulBits =
+ (p25to18 << 18) |
+ (p17to10 << 10) |
+ new Vector((int)baseBits);
+
+ Vector mul = Vector.AsVectorSingle(mulBits);
+
+ Vector cutoff = new(0.0031308f);
+
+ return Vector.ConditionalSelect(
+ Vector.LessThan(v, cutoff),
+ v * new Vector(12.92f),
+ (pow * mul) - new Vector(0.055f));
+ }
+
+ private static Vector GatherByteTable(Vector indices, ReadOnlySpan table)
+ {
+ int count = Vector.Count;
+ Span result = stackalloc int[count];
+
+ for (int i = 0; i < count; i++)
+ {
+ result[i] = table[indices[i]];
+ }
+
+ return new Vector(result);
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/IJxlFields.cs b/src/ImageSharp/Formats/Jxl/Fields/IJxlFields.cs
new file mode 100644
index 0000000000..bdc09f5f41
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/IJxlFields.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+///
+/// Abstracts enumeration of all fields into a visitor.
+///
+internal interface IJxlFields
+{
+ ///
+ /// Visits all fields into the specified JXL visitor.
+ ///
+ /// The visitor to use to visit all fields.
+ /// Status of the visit operation.
+ public bool Visit(JxlVisitor visitor);
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs
new file mode 100644
index 0000000000..cfe1430193
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+internal sealed class JxlAllDefaultVisitor : JxlVisitorBase
+{
+ public bool IsAllDefault { get; private set; } = true;
+
+ public override bool Bits(int bits, uint defaultValue, ref uint value)
+ {
+ this.IsAllDefault = value == defaultValue;
+ return true;
+ }
+
+ public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value)
+ {
+ this.IsAllDefault = value == defaultValue;
+ return true;
+ }
+
+ public override bool U64(ulong defaultValue, ref ulong value)
+ {
+ this.IsAllDefault = value == defaultValue;
+ return true;
+ }
+
+ public override bool F16(float defaultValue, ref float value)
+ {
+ this.IsAllDefault = MathF.Abs(value - defaultValue) < 1E-6f;
+ return true;
+ }
+
+ public override bool AllDefault(IJxlFields fields, ref bool allDefault) => false;
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs
new file mode 100644
index 0000000000..787d16d088
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs
@@ -0,0 +1,50 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
+using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+///
+/// Raw bits coder
+///
+internal static class JxlBitsCoder
+{
+ ///
+ /// Maximum number of encodeable bits. Since this coder encodes
+ /// bits raw, this happens to be whatever is passed to it.
+ ///
+ // Looks like that's what the function does (fields.cc:406):
+ // it returns whatever is passed to it.
+ public static int MaxEncodedBits(int bits) => bits;
+
+ public static bool CanEncode(int bits, uint value, ref int encodedBits)
+ {
+ encodedBits = bits;
+ if (value >= (1 << bits))
+ {
+ DebugGuard.IsTrue(false, "Value is too large");
+
+ return false;
+ }
+
+ return true;
+ }
+
+ public static void Write(int bits, uint value, JxlBitWriter writer)
+ {
+ if (value >= (1u << bits))
+ {
+ throw new InvalidOperationException("Value is too large to encode in " + bits + " bits");
+ }
+
+ writer.Write(bits, value);
+ }
+
+ // NOTE: BitsCoder::Read (fields.cc:418) returns a uint32_t,
+ // suggesting the input bit size does not exceed 32 bits.
+ public static uint Read(uint bits, JxlBitReader reader) => reader.ReadBits32(bits);
+
+ public static uint Read(int bits, JxlBitReader reader) => reader.ReadBits32((uint)bits);
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs
new file mode 100644
index 0000000000..f7b8c921b1
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs
@@ -0,0 +1,174 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
+using SixLabors.ImageSharp.Formats.Jxl.Processing;
+using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
+using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder;
+using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput;
+using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+///
+/// An helper.
+///
+internal static class JxlBundle
+{
+ ///
+ /// Initializes the specified JXL fields.
+ ///
+ /// The JXL fields.
+ public static void Init(IJxlFields fields)
+ {
+ JxlInitVisitor initVisitor = new();
+
+ if (!initVisitor.Visit(fields))
+ {
+ DebugGuard.IsTrue(false, "Init should never fail");
+ }
+ }
+
+ ///
+ /// Sets all JXL fields provided by the input value to their defaults.
+ ///
+ /// The JXL fields.
+ public static void SetDefault(IJxlFields fields)
+ {
+ JxlSetDefaultVisitor visitor = new();
+
+ if (!visitor.Visit(fields))
+ {
+ DebugGuard.IsTrue(false, "SetDefault should never fail");
+ }
+ }
+
+ ///
+ /// Returns a value indicating whether every value provided by this
+ /// field is a default value. If at least one field isn't a default
+ /// value, the method returns false.
+ ///
+ /// The JXL fields.
+ /// A boolean indicating whether or not are all values initialized to their default values.
+ public static bool AllDefault(IJxlFields fields)
+ {
+ JxlAllDefaultVisitor allDefaultVisitor = new();
+
+ if (!allDefaultVisitor.Visit(fields))
+ {
+ DebugGuard.IsTrue(false, "AllDefault should never fail");
+ }
+
+ return allDefaultVisitor.IsAllDefault;
+ }
+
+ ///
+ /// Reads the fields from a bit-reader.
+ ///
+ /// The bit-reader.
+ /// The fields.
+ /// Status of the read operation.
+ public static bool Read(JxlBitReader reader, IJxlFields fields)
+ {
+ JxlReadVisitor visitor = new(reader);
+ if (!visitor.Visit(fields))
+ {
+ return false;
+ }
+
+ return visitor.OK;
+ }
+
+ public static bool CanEncode(IJxlFields fields, ref int extensionBits, ref long totalBits)
+ {
+ JxlCanEncodeVisitor canEncodeVisitor = new();
+
+ if (!canEncodeVisitor.Visit(fields))
+ {
+ return false;
+ }
+
+ if (!canEncodeVisitor.GetSizes(ref extensionBits, ref totalBits))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Tries to read the fields from a bit-reader.
+ ///
+ /// The bit-reader.
+ /// The fields.
+ /// Status of the read operation.
+ public static bool CanRead(JxlBitReader reader, IJxlFields fields)
+ {
+ JxlReadVisitor visitor = new(reader);
+ _ = visitor.Visit(fields);
+ return visitor.OK;
+ }
+
+ public static bool Write(IJxlFields fields, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOutput)
+ {
+ int extensionBits = 0;
+ long totalBits = 0;
+ if (!CanEncode(fields, ref extensionBits, ref totalBits))
+ {
+ return false;
+ }
+
+ return writer.WithMaxBits(totalBits, () =>
+ {
+ JxlWriteVisitor visitor = new(extensionBits, writer);
+
+ if (!visitor.Visit(fields))
+ {
+ return false;
+ }
+
+ return visitor.OK();
+ });
+ }
+
+ public static bool WriteCodestreamHeaders(JxlCodecMetadata metadata, JxlBitWriter writer, JxlAuxiliaryOutput auxOut)
+ {
+ // Marker/signature
+ if (!writer.WithMaxBits(16, () =>
+ {
+ writer.Write(8, 0xFF);
+ writer.Write(8, JxlShared.CodestreamMarker);
+ return true;
+ }))
+ {
+ return false;
+ }
+
+ if (!WriteSizeHeader(metadata.Size!, writer, JxlLayerType.Header, auxOut))
+ {
+ return false;
+ }
+
+ if (!WriteImageMetadata(metadata.ImageMetadata!, writer, JxlLayerType.Header, auxOut))
+ {
+ return false;
+ }
+
+ metadata.CustomTransformData!.NonserializedXybEncoded = metadata.ImageMetadata!.XybEncoded;
+
+ return Write(metadata.CustomTransformData!, writer, JxlLayerType.Header, auxOut);
+ }
+
+ public static bool WriteFrameHeader(JxlFrameHeader frame, JxlBitWriter writer, JxlAuxiliaryOutput auxOut)
+ => Write(frame, writer, JxlLayerType.Header, auxOut);
+
+ public static bool WriteImageMetadata(JxlImageMetadata metadata, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOut)
+ => Write(metadata, writer, layer, auxOut);
+
+ public static bool WriteQuantizerParameters(JxlQuantizerParameters metadata, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOut)
+ => Write(metadata, writer, layer, auxOut);
+
+ public static bool WriteSizeHeader(JxlSizeHeader header, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOut)
+ => Write(header, writer, layer, auxOut);
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs
new file mode 100644
index 0000000000..87192bfefc
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs
@@ -0,0 +1,118 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+internal sealed class JxlCanEncodeVisitor : JxlVisitorBase
+{
+ private long encodedBits;
+ private ulong extensions;
+ private long posAfterExt;
+
+ public bool OK { get; set; } = true;
+
+ public override bool Bits(int bits, uint defaultValue, ref uint value)
+ {
+ int enc = 0;
+ this.OK &= JxlBitsCoder.CanEncode(bits, value, ref enc);
+ this.encodedBits += enc;
+ return true;
+ }
+
+ public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value)
+ {
+ int encBits = 0;
+ this.OK &= JxlU32Coder.CanEncode(enc, value, ref encBits);
+ this.encodedBits += encBits;
+ return true;
+ }
+
+ public override bool U64(ulong defaultValue, ref ulong value)
+ {
+ int encBits = 0;
+ this.OK &= JxlU64Coder.CanEncode(value, ref encBits);
+ this.encodedBits += encBits;
+ return true;
+ }
+
+ public override bool F16(float defaultValue, ref float value)
+ {
+ int encBits = 0;
+ this.OK &= JxlF16Coder.CanEncode(value, ref encBits);
+ this.encodedBits += encBits;
+ return true;
+ }
+
+ public override bool AllDefault(IJxlFields fields, ref bool allDefault)
+ {
+ allDefault = JxlBundle.AllDefault(fields);
+ if (!this.Boolean(true, ref allDefault))
+ {
+ return false;
+ }
+
+ return allDefault;
+ }
+
+ public override bool BeginExtensions(ref ulong extensions)
+ {
+ if (!base.BeginExtensions(ref extensions))
+ {
+ return false;
+ }
+
+ this.extensions = extensions;
+
+ if (extensions != 0uL)
+ {
+ if (this.posAfterExt != 0)
+ {
+ return false;
+ }
+
+ this.posAfterExt = this.encodedBits;
+
+ if (this.posAfterExt == 0)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public bool GetSizes(ref int extensionBits, ref long totalBits)
+ {
+ if (!this.OK)
+ {
+ return false;
+ }
+
+ extensionBits = 0;
+ totalBits = this.encodedBits;
+
+ if (this.posAfterExt != 0)
+ {
+ if (this.encodedBits < this.posAfterExt)
+ {
+ return false;
+ }
+
+ extensionBits = (int)this.encodedBits - (int)this.posAfterExt;
+ int encodedBits = 0;
+ this.OK &= JxlU64Coder.CanEncode((ulong)extensionBits, ref encodedBits);
+ totalBits += encodedBits;
+
+ for (int i = 1; i < BitOperations.PopCount(this.extensions); i++)
+ {
+ encodedBits = 0;
+ this.OK &= JxlU64Coder.CanEncode(0, ref encodedBits);
+ totalBits += encodedBits;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlExtensionStates.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlExtensionStates.cs
new file mode 100644
index 0000000000..2cc3c82581
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlExtensionStates.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+internal sealed class JxlExtensionStates
+{
+ private ulong begun;
+ private ulong ended;
+
+ public bool IsBegun => (this.begun & 1) != 0;
+
+ public bool IsEnded => (this.ended & 1) != 0;
+
+ public void Push()
+ {
+ this.begun <<= 1;
+ this.ended <<= 1;
+ }
+
+ public void Pop()
+ {
+ this.begun >>= 1;
+ this.ended >>= 1;
+ }
+
+ public void Begin()
+ {
+ DebugGuard.IsFalse(this.IsBegun, nameof(this.IsBegun), "This must be false.");
+ DebugGuard.IsFalse(this.IsEnded, nameof(this.IsEnded), "This must be false.");
+
+ this.begun++;
+ }
+
+ public void End()
+ {
+ DebugGuard.IsTrue(this.IsBegun, nameof(this.IsBegun), "This must be true.");
+ DebugGuard.IsFalse(this.IsEnded, nameof(this.IsEnded), "This must be false.");
+
+ this.ended++;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs
new file mode 100644
index 0000000000..d5b0d30fd2
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs
@@ -0,0 +1,136 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
+using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+///
+/// Represents the Half-precision Floating-point number coder.
+///
+internal static class JxlF16Coder
+{
+ ///
+ /// Always returns 16, which is the maximum possible encoded bits.
+ /// The F16 coder always reads 16 bits from the bitstream.
+ ///
+ public static int MaxEncodedBits() => 16;
+
+ ///
+ /// Returns a boolean indicating whether the input float
+ /// can be represented properly when encoded into a bit-stream.
+ /// Also stores the maximum encodeable bits into encodedBits (which is
+ /// always 16).
+ ///
+ public static bool CanEncode(float value, ref int encodedBits)
+ {
+ encodedBits = MaxEncodedBits();
+ if (float.IsNaN(value) || float.IsInfinity(value))
+ {
+ return false; // NaN and Infinity are not valid
+ }
+
+ return MathF.Abs(value) <= 65504.0f;
+ }
+
+ public static bool Read(JxlBitReader reader, ref float value)
+ {
+ uint bits16 = reader.ReadBits32(16u);
+ uint sign = bits16 >> 15;
+ uint biasedExponent = (bits16 >> 10) & 0x1Fu;
+ uint mantissa = bits16 & 0x3FFu;
+
+ if (biasedExponent == 31u)
+ {
+ // NaN and Infinity are not valid
+ return false;
+ }
+
+ if (biasedExponent == 0u)
+ {
+ // Subnormal or zero.
+ value = (1.0f / 16384) * (mantissa * (1.0f / 1024));
+ if (sign != 0u)
+ {
+ value = -value;
+ }
+
+ return true;
+ }
+
+ uint biasedExp32 = biasedExponent + (127u - 15u);
+ uint mantissa32 = mantissa << (23 - 10);
+ uint bits32 = (sign << 31) | (biasedExp32 << 23) | mantissa32;
+
+ value = BitConverter.UInt32BitsToSingle(bits32);
+
+ return true;
+ }
+
+ public static bool Write(float value, JxlBitWriter writer)
+ {
+ uint bits32 = BitConverter.SingleToUInt32Bits(value);
+
+ uint sign = bits32 >> 31;
+ uint biasedExp32 = (bits32 >> 23) & 0xFF;
+ uint mantissa32 = bits32 & 0x7FFFFF;
+
+ int exp = (int)biasedExp32 - 127;
+ if (exp > 15)
+ {
+ throw new InvalidOperationException("Too big to encode, CanEncode should return false");
+ }
+
+ // Tiny or zero => zero.
+ if (exp < -24)
+ {
+ writer.Write(16, 0);
+ return true;
+ }
+
+ uint biasedExp16 = 0;
+ uint mantissa16 = 0;
+
+ if (exp < -14)
+ {
+ biasedExp16 = 0;
+
+ uint subExp = unchecked((uint)(-14 - exp));
+
+ if (subExp is not (>= 1 and < 11))
+ {
+ return false;
+ }
+
+ mantissa16 = (1u << (int)(10 - subExp)) + (mantissa32 >> (int)(13 + subExp));
+ }
+ else
+ {
+ // exp = [-14, 15]
+ biasedExp16 = unchecked((uint)(exp + 15));
+
+ if (biasedExp16 is not (>= 1 and < 31))
+ {
+ return false;
+ }
+
+ mantissa16 = mantissa32 >> 13;
+ }
+
+ if (mantissa16 >= 1024)
+ {
+ return false;
+ }
+
+ uint bits16 = (sign << 15) | (biasedExp16 << 10) | mantissa16;
+
+ if (bits16 >= 0x10000)
+ {
+ return false;
+ }
+
+ writer.Write(16, bits16);
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlFieldExpressions.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlFieldExpressions.cs
new file mode 100644
index 0000000000..8d74c81bb8
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlFieldExpressions.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+internal static class JxlFieldExpressions
+{
+ public static JxlU32Distribution Value(uint value)
+ {
+ const uint directConstant = JxlU32Distribution.DirectConstant;
+
+ return new(value | directConstant);
+ }
+
+ public static JxlU32Distribution BitsOffset(uint bits, uint offset)
+ => new(((bits - 1u) & 0x1Fu) + ((offset & 0x3FFFFFFu) << 5));
+
+ public static JxlU32Distribution Bits(uint value) => BitsOffset(value, 0u);
+
+ public static int MakeBit(int index) => 1 << index;
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs
new file mode 100644
index 0000000000..28c16c4e46
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+///
+/// This variant of sets values
+/// to be the default value.
+///
+internal sealed class JxlInitVisitor : JxlVisitorBase
+{
+ public override bool Bits(int bits, uint defaultValue, ref uint value)
+ {
+ value = defaultValue;
+ return true;
+ }
+
+ public override bool U32(JxlU32Distribution d0, JxlU32Distribution d1, JxlU32Distribution d2, JxlU32Distribution d3, uint defaultValue, ref uint value)
+ {
+ value = defaultValue;
+ return true;
+ }
+
+ public override bool U64(ulong defaultValue, ref ulong value)
+ {
+ value = defaultValue;
+ return true;
+ }
+
+ public override bool Boolean(bool defaultValue, ref bool value)
+ {
+ value = defaultValue;
+ return true;
+ }
+
+ public override bool F16(float defaultValue, ref float value)
+ {
+ value = defaultValue;
+ return true;
+ }
+
+ public override bool Conditional(bool condition) => true;
+
+ public override bool AllDefault(IJxlFields fields, ref bool allDefault)
+ {
+ _ = this.Boolean(true, ref allDefault);
+ return false;
+ }
+
+ public override bool VisitNested(IJxlFields fields) => true;
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs
new file mode 100644
index 0000000000..d54dba6706
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs
@@ -0,0 +1,121 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Processing;
+using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+internal sealed class JxlReadVisitor(JxlBitReader reader) : JxlVisitorBase
+{
+ private ulong totalExtensionBits;
+ private bool notEnoughBytes;
+ private long posAfterExtSize;
+ private readonly ulong[] extensionBits = new ulong[JxlBundle.MaxExtensions];
+
+ public bool OK { get; private set; }
+
+ public override bool IsReading => true;
+
+ public override bool Bits(int bits, uint defaultValue, ref uint value)
+ {
+ value = JxlBitsCoder.Read(bits, reader);
+ return this.ThrowIfEndOfStreamOrReturnTrue();
+ }
+
+ public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value)
+ {
+ value = JxlU32Coder.Read(enc, reader);
+ return this.ThrowIfEndOfStreamOrReturnTrue();
+ }
+
+ public override bool U64(ulong defaultValue, ref ulong value)
+ {
+ value = JxlU64Coder.Read(reader);
+ return this.ThrowIfEndOfStreamOrReturnTrue();
+ }
+
+ public override bool F16(float defaultValue, ref float value)
+ {
+ this.OK &= JxlF16Coder.Read(reader, ref value);
+ return this.ThrowIfEndOfStreamOrReturnTrue();
+ }
+
+ public override void SetDefault(IJxlFields fields) => JxlBundle.SetDefault(fields);
+
+ public override bool BeginExtensions(ref ulong extensions)
+ {
+ if (!base.BeginExtensions(ref extensions))
+ {
+ return false;
+ }
+
+ if (extensions == 0)
+ {
+ return true;
+ }
+
+ for (ulong remainingExtensions = extensions; remainingExtensions != 0; remainingExtensions &= remainingExtensions - 1)
+ {
+ ulong idxExtension = JxlMath.Num0BitsBelowLS1Bit_Nonzero(remainingExtensions);
+ if (!this.U64(0, ref this.extensionBits[idxExtension]))
+ {
+ return false;
+ }
+
+ if (!JxlMath.SafeAdd(this.totalExtensionBits, this.extensionBits[idxExtension], ref this.totalExtensionBits))
+ {
+ DebugGuard.IsTrue(false, "Extension bits overflow; the codestream is not valid");
+
+ return false;
+ }
+ }
+
+ this.posAfterExtSize = reader.TotalBitsConsumed;
+ return this.posAfterExtSize != 0;
+ }
+
+ public override bool EndExtensions()
+ {
+ if (!base.EndExtensions())
+ {
+ return false;
+ }
+
+ if (this.posAfterExtSize == 0)
+ {
+ return true;
+ }
+
+ if (this.notEnoughBytes)
+ {
+ return true;
+ }
+
+ long bitsRead = reader.TotalBitsConsumed;
+
+ long end = 0;
+ if (!JxlMath.SafeAdd(this.posAfterExtSize, this.totalExtensionBits, ref end))
+ {
+ DebugGuard.IsTrue(false, "Invalid extension size.");
+
+ return false;
+ }
+
+ if (bitsRead > end)
+ {
+ DebugGuard.IsTrue(false, "Read more extension bits than budgeted");
+
+ return false;
+ }
+
+ long remainingBits = end - bitsRead;
+
+ if (remainingBits != 0)
+ {
+ reader.SkipBits64((uint)remainingBits);
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs
new file mode 100644
index 0000000000..aab1bc90ba
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+///
+/// This is similar to InitVisitor but also initializes
+/// nested fields.
+///
+internal sealed class JxlSetDefaultVisitor : JxlVisitorBase
+{
+ public override bool Bits(int bits, uint defaultValue, ref uint value)
+ {
+ value = defaultValue;
+ return true;
+ }
+
+ public override bool U32(JxlU32Distribution d0, JxlU32Distribution d1, JxlU32Distribution d2, JxlU32Distribution d3, uint defaultValue, ref uint value)
+ {
+ value = defaultValue;
+ return true;
+ }
+
+ public override bool U64(ulong defaultValue, ref ulong value)
+ {
+ value = defaultValue;
+ return true;
+ }
+
+ public override bool Boolean(bool defaultValue, ref bool value)
+ {
+ value = defaultValue;
+ return true;
+ }
+
+ public override bool F16(float defaultValue, ref float value)
+ {
+ value = defaultValue;
+ return true;
+ }
+
+ public override bool Conditional(bool condition) => true;
+
+ public override bool AllDefault(IJxlFields fields, ref bool allDefault)
+ {
+ _ = this.Boolean(true, ref allDefault);
+ return false;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs
new file mode 100644
index 0000000000..3fa3a3ab79
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs
@@ -0,0 +1,154 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Processing;
+using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
+using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+///
+/// Unsigned 32-bit variable-length integer coder.
+///
+internal static class JxlU32Coder
+{
+ ///
+ /// Maximum number of writeable and/or readable bits in a variable-length integer.
+ ///
+ public static int MaxEncodedBits(in JxlU32Enc enc)
+ {
+ int extraBits = 0;
+
+ for (int selector = 0; selector < 4; selector++)
+ {
+ JxlU32Distribution distr = enc.GetDistribution(selector);
+
+ if (distr.IsDirect)
+ {
+ continue;
+ }
+ else
+ {
+ extraBits = Math.Max(extraBits, (int)distr.ExtraBits);
+ }
+ }
+
+ return 2 + extraBits;
+ }
+
+ ///
+ /// Verifies that the value can be encoded.
+ ///
+ public static bool CanEncode(in JxlU32Enc enc, uint value, ref int encodedBits)
+ {
+ uint selector = 0;
+ int totalBits = 0;
+
+ bool isOk = ChooseSelector(in enc, value, ref selector, ref totalBits);
+
+ encodedBits = isOk ? totalBits : 0;
+
+ return isOk;
+ }
+
+ ///
+ /// Reads the U32 coded value.
+ ///
+ public static uint Read(in JxlU32Enc enc, JxlBitReader reader)
+ {
+ uint selector = reader.ReadBits32(2u);
+ JxlU32Distribution dist = enc.GetDistribution((int)selector);
+
+ if (dist.IsDirect)
+ {
+ return dist.Direct;
+ }
+ else
+ {
+ return reader.ReadBits32(dist.ExtraBits) + dist.Offset;
+ }
+ }
+
+ ///
+ /// Tries to find the best one of the four selectors based on the value.
+ ///
+ public static bool ChooseSelector(in JxlU32Enc enc, uint value, ref uint selector, ref int totalBits)
+ {
+ uint bitsRequired = 32 - JxlMath.Num0BitsAboveMS1Bit(value);
+
+ if (bitsRequired > 32)
+ {
+ return false;
+ }
+
+ selector = 0;
+ totalBits = 64;
+
+ for (int s = 0; s < 4; s++)
+ {
+ JxlU32Distribution dist = enc.GetDistribution(s);
+
+ if (dist.IsDirect)
+ {
+ if (dist.Direct == value)
+ {
+ selector = (uint)s;
+ totalBits = 2;
+ return true;
+ }
+
+ continue;
+ }
+
+ uint extraBits = dist.ExtraBits;
+ uint offset = dist.Offset;
+
+ if (value < offset || value >= offset + (1u << (int)extraBits))
+ {
+ continue;
+ }
+
+ if (2 + extraBits < totalBits)
+ {
+ selector = (uint)s;
+ totalBits = 2 + (int)extraBits;
+ }
+ }
+
+ if (totalBits == 64)
+ {
+ throw new InvalidOperationException("No matching selector");
+ }
+
+ return true;
+ }
+
+ public static bool Write(in JxlU32Enc enc, uint value, JxlBitWriter writer)
+ {
+ uint selector = 0;
+ int totalBits = 0;
+
+ if (!ChooseSelector(in enc, value, ref selector, ref totalBits))
+ {
+ return false;
+ }
+
+ writer.Write(2, selector);
+
+ JxlU32Distribution d = enc.GetDistribution((int)selector);
+
+ if (!d.IsDirect)
+ {
+ uint offset = d.Offset;
+
+ if (value < offset)
+ {
+ return false;
+ }
+
+ writer.WriteBits(totalBits - 2, value - offset);
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Distribution.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Distribution.cs
new file mode 100644
index 0000000000..355b6f533f
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Distribution.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+internal struct JxlU32Distribution(uint d)
+{
+ public const uint DirectConstant = 0x80000000u;
+
+ public readonly bool IsDirect => (d & DirectConstant) != 0;
+
+ public readonly uint Direct => d & (DirectConstant - 1u);
+
+ public readonly uint ExtraBits => (d & 0x1Fu) + 1u;
+
+ public readonly uint Offset => (d >> 5) & 0x3FFFFFF;
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs
new file mode 100644
index 0000000000..5b6e10b847
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+internal readonly struct JxlU32Enc
+{
+ private readonly InlineArray4 d = default;
+
+ public JxlU32Enc(JxlU32Distribution d0, JxlU32Distribution d1, JxlU32Distribution d2, JxlU32Distribution d3)
+ {
+ this.d[0] = d0;
+ this.d[1] = d1;
+ this.d[2] = d2;
+ this.d[3] = d3;
+ }
+
+ public JxlU32Distribution GetDistribution(int selector)
+ {
+ // This stuff is internal, so if argument check
+ // fails it's not a user error.
+ DebugGuard.MustBeLessThan(selector, 4, nameof(selector));
+
+ return this.d[selector];
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs
new file mode 100644
index 0000000000..3336ecf0f2
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs
@@ -0,0 +1,160 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
+using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+///
+/// Unsigned 64-bit variable-length coding.
+///
+internal static class JxlU64Coder
+{
+ ///
+ /// Reads the variable-length, unsigned 64-bit integer.
+ ///
+ public static ulong Read(JxlBitReader reader)
+ {
+ uint selector = reader.ReadBits32(2u);
+
+ if (selector == 0u)
+ {
+ return 0u;
+ }
+ else if (selector == 1u)
+ {
+ return 1u + reader.ReadBits32(4u);
+ }
+ else if (selector == 2u)
+ {
+ return 17u + reader.ReadBits32(8u);
+ }
+
+ // Selector 3...
+ ulong result = reader.ReadBits32(12u);
+ int shift = 12;
+
+ while (reader.ReadBoolean())
+ {
+ if (shift == 60)
+ {
+ result |= (ulong)reader.ReadBits32(4u) << shift;
+ break;
+ }
+
+ result |= (ulong)reader.ReadBits32(8u) << shift;
+ shift += 8;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Returns a value indicating whether can the value be encoded,
+ /// as well as the number of encoded bits.
+ ///
+ public static bool CanEncode(ulong value, ref int encodedBits)
+ {
+ if (value == 0)
+ {
+ // 2 selector bits
+ encodedBits = 2;
+ }
+ else if (value <= 16)
+ {
+ // 2 selector bits + 4 payload bits
+ encodedBits = 2 + 4;
+ }
+ else if (value <= 272)
+ {
+ // 2 selector bits + 8 payload bits
+ encodedBits = 2 + 8;
+ }
+ else
+ {
+ // 2 selector bits + 12 payload bits
+ encodedBits = 2 + 12;
+ value >>= 12;
+ int shift = 12;
+ while (value > 0 && shift < 60)
+ {
+ // 1 continuation bit + 8 payload bits
+ encodedBits += 1 + 8;
+ value >>= 8;
+ shift += 8;
+ }
+
+ if (value > 0)
+ {
+ // 1 continuation bit + 4 payload bits
+ encodedBits += 1 + 4;
+ }
+ else
+ {
+ // 1 stop bit
+ encodedBits += 1;
+ }
+ }
+
+ return true;
+ }
+
+ public static bool Write(ulong value, JxlBitWriter writer)
+ {
+ if (value == 0)
+ {
+ // Selector: use 0 bits, value 0
+ writer.Write(2, 0);
+ }
+ else if (value <= 16)
+ {
+ // Selector: use 4 bits, value 1..16
+ writer.Write(2, 1);
+ writer.Write(4, value - 1);
+ }
+ else if (value <= 272)
+ {
+ // Selector: use 8 bits, value 17..272
+ writer.Write(2, 2);
+ writer.Write(8, value - 17);
+ }
+ else
+ {
+ // Selector: varint, first a 12-bit group, after that per 8-bit group.
+ writer.Write(2, 3);
+ writer.Write(12, value & 4095);
+ value >>= 12;
+ int shift = 12;
+ while (value > 0 && shift < 60)
+ {
+ // Indicate varint not done
+ writer.Write(1, 1);
+ writer.Write(8, value & 255);
+ value >>= 8;
+ shift += 8;
+ }
+
+ if (value > 0)
+ {
+ // This only could happen if shift == N - 4.
+ writer.Write(1, 1);
+ writer.Write(4, value & 15);
+
+ // Implicitly closed sequence, no extra stop bit is required.
+ }
+ else
+ {
+ // Indicate end of varint
+ writer.Write(1, 0);
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Always returns 73.
+ ///
+ public static int MaxEncodedBits() => 73;
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs
new file mode 100644
index 0000000000..7fa79f75d2
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs
@@ -0,0 +1,92 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+///
+/// Base JPEG XL visitor that can visit all fields of a class.
+/// This is highly similar to the following Reflection code, but with
+/// lower overhead:
+///
+/// // Pseudocode
+/// void Visit(Type type)
+/// {
+/// foreach (PropertyInfo property in type.GetProperties())
+/// {
+/// /* visitor implementation */(property);
+/// }
+/// }
+///
+///
+internal class JxlVisitor
+{
+ public virtual bool IsReading => false;
+
+ public virtual bool Visit(IJxlFields fields) => false;
+
+ public virtual bool Boolean(bool defaultValue, ref bool value) => false;
+
+ public virtual bool U32(JxlU32Enc enc, uint defaultValue, ref uint value) => false;
+
+ public virtual bool U32(
+ JxlU32Distribution d0,
+ JxlU32Distribution d1,
+ JxlU32Distribution d2,
+ JxlU32Distribution d3,
+ uint defaultValue,
+ ref uint value)
+ => this.U32(new JxlU32Enc(d0, d1, d2, d3), value, ref defaultValue);
+
+ public virtual unsafe bool Enum(T defaultValue, ref T value)
+ where T : unmanaged
+ {
+ DebugGuard.IsTrue(sizeof(T) == 4, "We use unsafe bit casting so anything beside 4 bytes will break memory layout");
+
+ ref uint u32 = ref Unsafe.As(ref value);
+ if (!this.U32(
+ JxlFieldExpressions.Value(0),
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.BitsOffset(4, 2),
+ JxlFieldExpressions.BitsOffset(6, 18),
+ Unsafe.BitCast(defaultValue),
+ ref u32))
+ {
+ return false;
+ }
+
+ return System.Enum.IsDefined(typeof(T), value);
+ }
+
+ public virtual bool Bits(int bits, uint defaultValue, ref uint value) => false;
+
+ public virtual bool U64(ulong defaultValue, ref ulong value) => false;
+
+ public virtual bool F16(float defaultValue, ref float value) => false;
+
+ public virtual bool Conditional(bool condition) => condition;
+
+ public virtual bool AllDefault(IJxlFields fields, ref bool allDefault)
+ {
+ // Do not remove the fields parameter, derived classes
+ // use it.
+ if (!this.Boolean(true, ref allDefault))
+ {
+ return false;
+ }
+
+ return allDefault;
+ }
+
+ public virtual void SetDefault(IJxlFields fields)
+ {
+ // Used by derived methods.
+ }
+
+ public virtual bool VisitNested(IJxlFields fields) => this.Visit(fields);
+
+ public virtual bool BeginExtensions(ref ulong extensions) => false;
+
+ public virtual bool EndExtensions() => false;
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs
new file mode 100644
index 0000000000..ff3ee5a3d5
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs
@@ -0,0 +1,76 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+internal class JxlVisitorBase : JxlVisitor
+{
+ private readonly JxlExtensionStates extensionStates = new();
+ private int depth;
+
+ public override bool Visit(IJxlFields fields)
+ {
+ if (this.depth >= JxlBundle.MaxExtensions)
+ {
+ return false;
+ }
+
+ this.depth++;
+ this.extensionStates.Push();
+
+ bool visited = fields.Visit(this);
+
+ if (visited)
+ {
+ if (!(!this.extensionStates.IsBegun || this.extensionStates.IsEnded))
+ {
+ throw new InvalidOperationException("Invalid extension state");
+ }
+ }
+
+ this.extensionStates.Pop();
+
+ if (this.depth == 0)
+ {
+ throw new InvalidOperationException("Depth must not be 0");
+ }
+ this.depth--;
+
+ return visited;
+ }
+
+ public override bool Boolean(bool defaultValue, ref bool value)
+ {
+ uint bits = value ? 1u : 0u;
+ if (!this.Bits(1, defaultValue ? 1u : 0u, ref bits))
+ {
+ return false;
+ }
+
+ if (bits > 1u)
+ {
+ throw new InvalidOperationException("Invalid bits");
+ }
+
+ value = bits == 1u;
+
+ return true;
+ }
+
+ public override bool BeginExtensions(ref ulong extensions)
+ {
+ if (!this.U64(0uL, ref extensions))
+ {
+ return false;
+ }
+
+ this.extensionStates.Begin();
+ return true;
+ }
+
+ public override bool EndExtensions()
+ {
+ this.extensionStates.End();
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs
new file mode 100644
index 0000000000..a5b07c45f6
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs
@@ -0,0 +1,320 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers.Binary;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO;
+
+///
+/// Reads primitives from streams with correct endianness.
+///
+// TODO: move this class into the IO or Common folder?
+internal static class BinaryUtils
+{
+ ///
+ /// Reads a
+ /// from the specified stream in little-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static Int16 ReadInt16LittleEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(Int16)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadInt16LittleEndian(data);
+ }
+
+ ///
+ /// Reads a
+ /// from the specified stream in big-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static Int16 ReadInt16BigEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(Int16)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadInt16BigEndian(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in little-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteInt16LittleEndian(Stream stream, Int16 value)
+ {
+ Span data = stackalloc byte[sizeof(Int16)];
+ BinaryPrimitives.WriteInt16LittleEndian(data, value);
+ stream.Write(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in big-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteInt16BigEndian(Stream stream, Int16 value)
+ {
+ Span data = stackalloc byte[sizeof(Int16)];
+ BinaryPrimitives.WriteInt16BigEndian(data, value);
+ stream.Write(data);
+ }
+ ///
+ /// Reads a
+ /// from the specified stream in little-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static UInt16 ReadUInt16LittleEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(UInt16)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadUInt16LittleEndian(data);
+ }
+
+ ///
+ /// Reads a
+ /// from the specified stream in big-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static UInt16 ReadUInt16BigEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(UInt16)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadUInt16BigEndian(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in little-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteUInt16LittleEndian(Stream stream, UInt16 value)
+ {
+ Span data = stackalloc byte[sizeof(UInt16)];
+ BinaryPrimitives.WriteUInt16LittleEndian(data, value);
+ stream.Write(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in big-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteUInt16BigEndian(Stream stream, UInt16 value)
+ {
+ Span data = stackalloc byte[sizeof(UInt16)];
+ BinaryPrimitives.WriteUInt16BigEndian(data, value);
+ stream.Write(data);
+ }
+ ///
+ /// Reads a
+ /// from the specified stream in little-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static Int32 ReadInt32LittleEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(Int32)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadInt32LittleEndian(data);
+ }
+
+ ///
+ /// Reads a
+ /// from the specified stream in big-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static Int32 ReadInt32BigEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(Int32)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadInt32BigEndian(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in little-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteInt32LittleEndian(Stream stream, Int32 value)
+ {
+ Span data = stackalloc byte[sizeof(Int32)];
+ BinaryPrimitives.WriteInt32LittleEndian(data, value);
+ stream.Write(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in big-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteInt32BigEndian(Stream stream, Int32 value)
+ {
+ Span data = stackalloc byte[sizeof(Int32)];
+ BinaryPrimitives.WriteInt32BigEndian(data, value);
+ stream.Write(data);
+ }
+ ///
+ /// Reads a
+ /// from the specified stream in little-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static UInt32 ReadUInt32LittleEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(UInt32)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadUInt32LittleEndian(data);
+ }
+
+ ///
+ /// Reads a
+ /// from the specified stream in big-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static UInt32 ReadUInt32BigEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(UInt32)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadUInt32BigEndian(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in little-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteUInt32LittleEndian(Stream stream, UInt32 value)
+ {
+ Span data = stackalloc byte[sizeof(UInt32)];
+ BinaryPrimitives.WriteUInt32LittleEndian(data, value);
+ stream.Write(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in big-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteUInt32BigEndian(Stream stream, UInt32 value)
+ {
+ Span data = stackalloc byte[sizeof(UInt32)];
+ BinaryPrimitives.WriteUInt32BigEndian(data, value);
+ stream.Write(data);
+ }
+ ///
+ /// Reads a
+ /// from the specified stream in little-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static Int64 ReadInt64LittleEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(Int64)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadInt64LittleEndian(data);
+ }
+
+ ///
+ /// Reads a
+ /// from the specified stream in big-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static Int64 ReadInt64BigEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(Int64)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadInt64BigEndian(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in little-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteInt64LittleEndian(Stream stream, Int64 value)
+ {
+ Span data = stackalloc byte[sizeof(Int64)];
+ BinaryPrimitives.WriteInt64LittleEndian(data, value);
+ stream.Write(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in big-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteInt64BigEndian(Stream stream, Int64 value)
+ {
+ Span data = stackalloc byte[sizeof(Int64)];
+ BinaryPrimitives.WriteInt64BigEndian(data, value);
+ stream.Write(data);
+ }
+ ///
+ /// Reads a
+ /// from the specified stream in little-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static UInt64 ReadUInt64LittleEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(UInt64)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadUInt64LittleEndian(data);
+ }
+
+ ///
+ /// Reads a
+ /// from the specified stream in big-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static UInt64 ReadUInt64BigEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(UInt64)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.ReadUInt64BigEndian(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in little-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteUInt64LittleEndian(Stream stream, UInt64 value)
+ {
+ Span data = stackalloc byte[sizeof(UInt64)];
+ BinaryPrimitives.WriteUInt64LittleEndian(data, value);
+ stream.Write(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in big-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void WriteUInt64BigEndian(Stream stream, UInt64 value)
+ {
+ Span data = stackalloc byte[sizeof(UInt64)];
+ BinaryPrimitives.WriteUInt64BigEndian(data, value);
+ stream.Write(data);
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt
new file mode 100644
index 0000000000..ba98aca4ad
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt
@@ -0,0 +1,85 @@
+<#@ template language="C#" #>
+<#@ import namespace="System" #>
+<#@ import namespace="System.IO" #>
+<#@ import namespace="System.Collections.Generic" #>
+<#@ output extension=".Generated.cs" #>
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers.Binary;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO;
+
+<#
+ List types = [
+ typeof(short),
+ typeof(ushort),
+ typeof(int),
+ typeof(uint),
+ typeof(long),
+ typeof(ulong)
+ ];
+#>
+///
+/// Reads primitives from streams with correct endianness.
+///
+// TODO: move this class into the IO or Common folder?
+internal static class BinaryUtils
+{
+<#
+ foreach (Type type in types)
+ {
+#>
+ ///
+ /// Reads a
+ /// from the specified stream in little-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static <#= type.Name #> Read<#= type.Name#>LittleEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(<#= type.Name #>)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.Read<#= type.Name #>LittleEndian(data);
+ }
+
+ ///
+ /// Reads a
+ /// from the specified stream in big-endian order.
+ ///
+ /// The stream where the will be read from.
+ ///
+ public static <#= type.Name #> Read<#= type.Name#>BigEndian(Stream stream)
+ {
+ Span data = stackalloc byte[sizeof(<#= type.Name #>)];
+ stream.ReadExactly(data);
+ return BinaryPrimitives.Read<#= type.Name #>BigEndian(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in little-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void Write<#= type.Name #>LittleEndian(Stream stream, <#= type.Name #> value)
+ {
+ Span data = stackalloc byte[sizeof(<#= type.Name #>)];
+ BinaryPrimitives.Write<#= type.Name #>LittleEndian(data, value);
+ stream.Write(data);
+ }
+
+ ///
+ /// Writes a
+ /// into the specified stream in big-endian order.
+ ///
+ /// The stream where the will be written to.
+ /// Value which will be written to the stream.
+ public static void Write<#= type.Name #>BigEndian(Stream stream, <#= type.Name #> value)
+ {
+ Span data = stackalloc byte[sizeof(<#= type.Name #>)];
+ BinaryPrimitives.Write<#= type.Name #>BigEndian(data, value);
+ stream.Write(data);
+ }
+<# } #>
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs
new file mode 100644
index 0000000000..8e3600bb5e
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs
@@ -0,0 +1,158 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers.Binary;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Container;
+
+///
+/// Header for JPEG XL container format.
+///
+[StructLayout(LayoutKind.Sequential, Size = 16)]
+internal struct JxlBoxHeader
+{
+ ///
+ /// Box size in bytes.
+ ///
+ public ulong Size;
+
+ ///
+ /// Type of the box.
+ ///
+ public uint Type;
+
+ ///
+ /// True if the size field extends until the end of the file.
+ ///
+ public bool SizeExtendsTillEnd;
+
+ ///
+ /// True if the size is 64-bit.
+ ///
+ public bool ContainsLargeSize;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The size of the box.
+ /// The type of the box.
+ /// Does the box size extend till the end of the file?
+ /// Is there a 64-bit size field?
+ public JxlBoxHeader(ulong size, uint type, bool sizeExtendsTillEnd, bool containsLargeSize)
+ {
+ this.Size = size;
+ this.Type = type;
+ this.SizeExtendsTillEnd = sizeExtendsTillEnd;
+ this.ContainsLargeSize = containsLargeSize;
+ }
+
+ ///
+ /// Converts a 4-character ASCII string (e.g. "jxlc") into a uint type code.
+ ///
+ /// Input type string to convert
+ /// Unsigned integer representation of the type string
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static uint TypeFromString(string typeString)
+ {
+ if (typeString.Length != 4)
+ {
+ throw new ArgumentException("Box type must be exactly 4 characters", nameof(typeString));
+ }
+
+ return ((uint)typeString[0] << 24) |
+ ((uint)typeString[1] << 16) |
+ ((uint)typeString[2] << 8) |
+ typeString[3];
+ }
+
+ ///
+ /// Converts a uint type code back into a 4-character ASCII string.
+ ///
+ /// Unsigned integer representation of the type string
+ /// The string representing the type code.
+ public static string TypeToString(uint typeCode)
+ {
+ return typeCode switch
+ {
+ 0x6A786C20 => "jxl ",
+ 0x6A786C70 => "jxlp",
+ 0x6A786C63 => "jxlc",
+ 0x66747970 => "ftyp",
+ 0x6A627264 => "jbrd",
+ 0x45786966 => "Exif",
+ 0x786D6C20 => "xml ",
+ 0x6A756D62 => "jumb",
+ _ => Fallback(typeCode)
+ };
+
+ static string Fallback(uint typeCode)
+ {
+ // The box type is not known
+ Span buffer = stackalloc byte[4];
+ BinaryPrimitives.WriteUInt32BigEndian(buffer, typeCode);
+
+ return Encoding.ASCII.GetString(buffer);
+ }
+ }
+
+ ///
+ /// Parses the JPEG XL box header.
+ ///
+ /// A stream to parse the header from.
+ /// The box header.
+ /// Thrown when the header is invalid.
+ public static JxlBoxHeader ReadHeader(Stream stream)
+ {
+ ulong size = BinaryUtils.ReadUInt32BigEndian(stream);
+ bool haveSize64 = false;
+
+ if (size == 1)
+ {
+ // When the size value is equal to 1, a new 64-bit
+ // size field follows.
+ haveSize64 = true;
+ size = BinaryUtils.ReadUInt64BigEndian(stream);
+ }
+
+ // Read the 4-byte type field.
+ uint type = BinaryUtils.ReadUInt32BigEndian(stream);
+
+ if (haveSize64)
+ {
+ // When the 64-bit largesize was read,
+ // the size cannot proceed till the end of the file.
+ if (size is 0 or 1)
+ {
+ throw new InvalidOperationException("Large size cannot have another large size or extend till the end of the file");
+ }
+
+ return new JxlBoxHeader(size, type, sizeExtendsTillEnd: false, containsLargeSize: true);
+ }
+ else
+ {
+ return new JxlBoxHeader(size, type, sizeExtendsTillEnd: size == 0, containsLargeSize: false);
+ }
+ }
+
+ ///
+ /// Writes the box header to the specified stream.
+ ///
+ /// The stream to write the box header to.
+ public readonly void WriteHeader(Stream writer)
+ {
+ if (this.Size is > uint.MaxValue or 1)
+ {
+ BinaryUtils.WriteUInt32BigEndian(writer, 1); // Indicates a large size is present
+ BinaryUtils.WriteUInt64BigEndian(writer, this.Size);
+ }
+ else
+ {
+ BinaryUtils.WriteUInt32BigEndian(writer, (uint)this.Size);
+ }
+
+ BinaryUtils.WriteUInt32BigEndian(writer, this.Type);
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Container/JxlFileTypeBox.cs b/src/ImageSharp/Formats/Jxl/IO/Container/JxlFileTypeBox.cs
new file mode 100644
index 0000000000..ca2adb1ca4
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Container/JxlFileTypeBox.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Container;
+
+///
+/// A ftyp box payload.
+///
+internal sealed class JxlFileTypeBox(string majorBrand, uint minorVersion)
+{
+ ///
+ /// Gets or sets the primary format. Has to be "jxl " for JPEG XL.
+ ///
+ public string MajorBrand { get; set; } = majorBrand;
+
+ ///
+ /// Gets or sets the revision of the major brand.
+ ///
+ public uint MinorVersion { get; set; } = minorVersion;
+
+ ///
+ /// Gets or sets the list of other brands the file is compatible with.
+ ///
+ public List CompatibleBrands { get; set; } = [];
+
+ public int GetPayloadSize() => 8 + (this.CompatibleBrands.Count * 4);
+
+ public static JxlFileTypeBox Parse(Stream stream, ulong boxSize)
+ {
+ string majorBrand = JxlBoxHeader.TypeToString(BinaryUtils.ReadUInt32BigEndian(stream));
+ uint minorVersion = BinaryUtils.ReadUInt32BigEndian(stream);
+ boxSize -= 8;
+
+ List compatibleBrands = [];
+ for (ulong i = 0; i < boxSize; i += 4)
+ {
+ compatibleBrands.Add(JxlBoxHeader.TypeToString(BinaryUtils.ReadUInt32BigEndian(stream)));
+ }
+
+ JxlFileTypeBox ftyp = new(majorBrand, minorVersion)
+ {
+ CompatibleBrands = compatibleBrands
+ };
+
+ return ftyp;
+ }
+
+ public void WritePayload(Stream stream)
+ {
+ BinaryUtils.WriteUInt32BigEndian(stream, JxlBoxHeader.TypeFromString(this.MajorBrand));
+ BinaryUtils.WriteUInt32BigEndian(stream, this.MinorVersion);
+
+ foreach (string compatibleBrand in this.CompatibleBrands)
+ {
+ BinaryUtils.WriteUInt32BigEndian(stream, JxlBoxHeader.TypeFromString(compatibleBrand));
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsConstants.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsConstants.cs
new file mode 100644
index 0000000000..d9b6d1150d
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsConstants.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy;
+
+internal static class JxlAnsConstants
+{
+ public const int AnsLogTableSize = 12;
+ public const int AnsTableSize = 1 << AnsLogTableSize;
+ public const int AnsTabMask = AnsTableSize - 1;
+ public const int PrefixMaxAlphabetSize = 4096;
+ public const int AnsMaxAlphabetSize = 256;
+ public const int PrefixMaxBits = 15;
+ public const int AnsSignature = 0x13;
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsEntry.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsEntry.cs
new file mode 100644
index 0000000000..7ec8c7cd4c
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsEntry.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy;
+
+[StructLayout(LayoutKind.Sequential)]
+internal struct JxlAnsEntry
+{
+ public byte Cutoff;
+ public byte RightValue;
+ public ushort Frequency0;
+ public ushort Offsets1;
+ public ushort Frequency1XorFrequency0;
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs
new file mode 100644
index 0000000000..7feda2848d
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs
@@ -0,0 +1,239 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy;
+
+internal static class JxlAnsHelper
+{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int GetPopulationCountPrecision(int logCount, int shift)
+ => Math.Max(0, Math.Min(logCount, shift - ((JxlAnsConstants.AnsLogTableSize - logCount) >> 1)));
+
+ // NOTE: The result may potentially be large, so prefer using a memory allocator
+ public static IMemoryOwner CreateFlatHistogram(Configuration configuration, int length, int totalCount)
+ {
+ DebugGuard.MustBeLessThanOrEqualTo(length, 0, nameof(length));
+ DebugGuard.MustBeGreaterThan(length, totalCount, nameof(length));
+
+ int count = totalCount / length;
+ IMemoryOwner result = configuration.MemoryAllocator.Allocate(length);
+ Span resultSpan = result.Memory.Span;
+ uint unsignedCount = (uint)count;
+
+ for (int i = 0; i < length; i++)
+ {
+ resultSpan[i] = unsignedCount;
+ }
+
+ int remCounts = totalCount % length;
+ for (int i = 0; i < remCounts; i++)
+ {
+ resultSpan[i]++;
+ }
+
+ return result;
+ }
+
+ public static JxlAnsSymbol Lookup(ReadOnlySpan table, int value, int logEntrySize, int entrySizeMinus1)
+ {
+ int i = value >> logEntrySize;
+ int pos = value & entrySizeMinus1;
+
+ JxlAnsEntry entry = table[i];
+
+ int cutoff = entry.Cutoff;
+ int rightValue = entry.RightValue;
+ int freq0 = entry.Frequency0;
+
+ bool greater = pos >= cutoff;
+
+ int offsets1or0 = greater ? entry.Offsets1 : 0;
+ int freq1xorfreq0or0 = greater ? entry.Frequency1XorFrequency0 : 0;
+
+ JxlAnsSymbol symbol = new()
+ {
+ Value = greater ? rightValue : i,
+ Offset = offsets1or0 + pos,
+ Frequency = freq0 ^ freq1xorfreq0or0
+ };
+
+ return symbol;
+ }
+
+ public static bool InitAliasTable(Span preDistribution, uint logRange, int logAlphaSize, Span entries)
+ {
+ DebugGuard.MustBeLessThan(logAlphaSize, (int)logRange, nameof(logAlphaSize));
+
+ int range = 1 << (int)logRange;
+ int tableSize = 1 << logAlphaSize;
+
+ int distributionPointer = preDistribution.Length - 1;
+
+ while (distributionPointer >= 0 && preDistribution[distributionPointer] == 0)
+ {
+ distributionPointer--;
+ }
+
+ if (distributionPointer < 0)
+ {
+ preDistribution[0] = range;
+ distributionPointer = 0;
+ }
+
+ Span distribution = preDistribution[..(distributionPointer + 1)];
+
+ if (distribution.Length > tableSize)
+ {
+ throw new InvalidOperationException("Too many items in the distribution");
+ }
+
+ int entrySize = range >> logAlphaSize;
+ int singleSymbol = -1;
+ int sum = 0;
+
+ for (int sym = 0; sym < distribution.Length; sym++)
+ {
+ int value = distribution[sym];
+ sum += value;
+
+ if (value == JxlAnsConstants.AnsTableSize)
+ {
+ if (singleSymbol != -1)
+ {
+ return false;
+ }
+
+ singleSymbol = sym;
+ }
+ }
+
+ if (sum != range)
+ {
+ return false;
+ }
+
+ if (singleSymbol != -1)
+ {
+ byte sym = (byte)singleSymbol;
+ if (singleSymbol != sym)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < tableSize; i++)
+ {
+ ref JxlAnsEntry jxlEntry = ref entries[i];
+
+ jxlEntry.RightValue = sym;
+ jxlEntry.Cutoff = 0;
+ jxlEntry.Offsets1 = (ushort)(entrySize * i);
+ jxlEntry.Frequency0 = 0;
+ jxlEntry.Frequency1XorFrequency0 = JxlAnsConstants.AnsTableSize;
+ }
+
+ return true;
+ }
+
+ Span underfullPosn = stackalloc uint[distribution.Length];
+ Span overfullPosn = stackalloc uint[distribution.Length];
+ Span cutoffs = stackalloc uint[1 << logAlphaSize];
+
+ int underfullPointer = 0;
+ int overfullPointer = 0;
+
+ for (int i = 0; i < distribution.Length; i++)
+ {
+ uint currentCutoff = (uint)distribution[i];
+
+ cutoffs[i] = currentCutoff;
+
+ if (currentCutoff > entrySize)
+ {
+ overfullPosn[overfullPointer] = (uint)i;
+ overfullPointer++;
+ }
+ else if (currentCutoff < entrySize)
+ {
+ underfullPosn[underfullPointer] = (uint)i;
+ underfullPointer++;
+ }
+ }
+
+ for (int i = distribution.Length; i < tableSize; i++)
+ {
+ cutoffs[i] = 0;
+ underfullPosn[underfullPointer] = (uint)i;
+ underfullPointer++;
+ }
+
+ uint unsignedEntrySize = (uint)entrySize;
+
+ while (overfullPointer >= 0)
+ {
+ uint overfullIndex = overfullPosn[overfullPointer];
+ overfullPointer--;
+
+ if (underfullPointer <= -1)
+ {
+ return false;
+ }
+
+ uint underfullIndex = underfullPosn[underfullPointer];
+ underfullPointer--;
+
+ int signedOverfullIndex = (int)overfullIndex;
+ int signedUnderfullIndex = (int)underfullIndex;
+
+ uint underfullBy = unsignedEntrySize - cutoffs[signedUnderfullIndex];
+ cutoffs[signedOverfullIndex] -= underfullBy;
+
+ ref JxlAnsEntry currentEntry = ref entries[signedUnderfullIndex];
+
+ currentEntry.RightValue = unchecked((byte)overfullIndex);
+ currentEntry.Offsets1 = unchecked((ushort)cutoffs[signedOverfullIndex]);
+
+ uint currentCutoff = cutoffs[signedOverfullIndex];
+
+ if (currentCutoff < entrySize)
+ {
+ underfullPosn[underfullPointer] = overfullIndex;
+ underfullPointer++;
+ }
+ else if (currentCutoff > entrySize)
+ {
+ overfullPosn[overfullPointer] = overfullIndex;
+ overfullPointer++;
+ }
+ }
+
+ for (uint i = 0; i < tableSize; i++)
+ {
+ uint currentCutoff = cutoffs[(int)i];
+ ref JxlAnsEntry entry = ref entries[(int)i];
+
+ if (currentCutoff == entrySize)
+ {
+ entry.RightValue = (byte)i;
+ entry.Offsets1 = 0;
+ entry.Cutoff = 0;
+ }
+ else
+ {
+ entry.Offsets1 -= (ushort)currentCutoff;
+ entry.Cutoff = (byte)currentCutoff;
+ }
+
+ int freq0 = i < distribution.Length ? distribution[(int)i] : 0;
+ int i1 = entry.RightValue;
+ int freq1 = i1 < distribution.Length ? distribution[i1] : 0;
+
+ entry.Frequency0 = (ushort)freq0;
+ entry.Frequency1XorFrequency0 = (ushort)(freq1 ^ freq0);
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs
new file mode 100644
index 0000000000..9ff4306522
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs
@@ -0,0 +1,63 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+using SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy;
+
+internal sealed class JxlAnsHybridUIntConfiguration : IJxlFields
+{
+ public JxlAnsHybridUIntConfiguration(uint splitExponent = 4, uint msbInToken = 2, uint lsbInToken = 0)
+ {
+ this.SplitExponent = splitExponent;
+ this.SplitToken = 1u << (int)splitExponent;
+ this.MsbInToken = msbInToken;
+ this.LsbInToken = lsbInToken;
+
+ if (splitExponent < msbInToken + lsbInToken)
+ {
+ throw new InvalidOperationException("Split exponent should be < msbInToken + lsbInToken");
+ }
+ }
+
+ public uint SplitExponent { get; set; }
+
+ public uint SplitToken { get; set; }
+
+ public uint MsbInToken { get; set; } // Most significant bit
+
+ public uint LsbInToken { get; set; } // Least significant bit
+
+ public uint LsbMask => (1u << (int)this.LsbInToken) - 1;
+
+ public void Encode(uint value, out uint token, out uint bitCount, out uint bits)
+ {
+ if (value < this.SplitToken)
+ {
+ token = value;
+ bitCount = 0;
+ bits = 0;
+ }
+ else
+ {
+ uint n = JxlMath.FloorLog2Nonzero(value);
+ uint m = value - (1u << (int)n);
+
+ unchecked
+ {
+ // The following expression is quite complex.
+ // See https://github.com/libjxl/libjxl/blob/main/lib/jxl/dec_ans.h#L83C16-L86C47.
+ token = this.SplitToken +
+ (uint)(((n - this.SplitExponent) << (int)(this.MsbInToken + this.LsbInToken)) +
+ ((m >> (int)(n - this.MsbInToken)) << (int)this.LsbInToken) +
+ (m & ((1 << (int)this.LsbInToken) - 1)));
+
+ bitCount = n - this.MsbInToken - this.LsbInToken;
+ bits = (value >> (int)this.LsbInToken) & ((1u << (int)bitCount) - 1);
+ }
+ }
+ }
+
+ public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsLz77Parameters.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsLz77Parameters.cs
new file mode 100644
index 0000000000..01016f4dd6
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsLz77Parameters.cs
@@ -0,0 +1,81 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy;
+
+internal sealed class JxlAnsLz77Parameters : IJxlFields
+{
+ private bool enabled;
+ private uint minimumSymbol;
+ private uint minimumLength;
+ private JxlAnsHybridUIntConfiguration lengthUintConfig = new(0, 0, 0);
+
+ public JxlAnsLz77Parameters() => JxlBundle.Init(this);
+
+ public bool Enabled
+ {
+ get => this.enabled;
+ set => this.enabled = value;
+ }
+
+ public uint MinimumSymbol
+ {
+ get => this.minimumSymbol;
+ set => this.minimumSymbol = value;
+ }
+
+ public uint MinimumLength
+ {
+ get => this.minimumLength;
+ set => this.minimumLength = value;
+ }
+
+ public JxlAnsHybridUIntConfiguration LengthUintConfig
+ {
+ get => this.lengthUintConfig;
+ set => this.lengthUintConfig = value;
+ }
+
+ public int NonserializedDistanceContext { get; set; }
+
+ public ref JxlAnsHybridUIntConfiguration GetLengthUIntConfigReference() => ref this.lengthUintConfig;
+
+ public bool Visit(JxlVisitor visitor)
+ {
+ if (!visitor.Boolean(false, ref this.enabled))
+ {
+ return false;
+ }
+
+ if (!visitor.Conditional(this.enabled))
+ {
+ return true;
+ }
+
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(224u),
+ JxlFieldExpressions.Value(512u),
+ JxlFieldExpressions.Value(4096u),
+ JxlFieldExpressions.BitsOffset(15u, 8u),
+ 224u,
+ ref this.minimumSymbol))
+ {
+ return false;
+ }
+
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(3u),
+ JxlFieldExpressions.Value(4u),
+ JxlFieldExpressions.BitsOffset(2u, 5u),
+ JxlFieldExpressions.BitsOffset(8u, 9u),
+ 3u,
+ ref this.minimumLength))
+ {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsSymbol.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsSymbol.cs
new file mode 100644
index 0000000000..240b2463eb
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsSymbol.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy;
+
+[StructLayout(LayoutKind.Sequential)]
+internal struct JxlAnsSymbol(int value, int offset, int frequency)
+{
+ public int Value = value;
+ public int Offset = offset;
+ public int Frequency = frequency;
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs
new file mode 100644
index 0000000000..1854b4ab67
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs
@@ -0,0 +1,74 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+using SixLabors.ImageSharp.Formats.Jxl.Metadata;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Describes duration of frames that make up an animation.
+///
+internal sealed class JxlAnimationFrame : IJxlFields
+{
+ ///
+ /// See .
+ ///
+ private uint duration;
+
+ ///
+ /// See .
+ ///
+ private uint timecode;
+
+ ///
+ /// Gets or sets the duration of the animation.
+ ///
+ public uint Duration
+ {
+ get => this.duration;
+ set => this.duration = value;
+ }
+
+ ///
+ /// Gets or sets the timecode of the animation. The
+ /// format is 0xHHMMSSFF.
+ ///
+ public uint Timecode
+ {
+ get => this.timecode;
+ set => this.timecode = value;
+ }
+
+ ///
+ /// Gets or sets the optional codec metadata.
+ ///
+ public JxlCodecMetadata? CodecMetadata { get; set; }
+
+ public bool Visit(JxlVisitor visitor)
+ {
+ if (visitor.Conditional(this.CodecMetadata?.ImageMetadata?.HaveAnimation == true))
+ {
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(0),
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.Bits(8),
+ JxlFieldExpressions.Bits(32),
+ 0,
+ ref this.duration))
+ {
+ return false;
+ }
+ }
+
+ if (visitor.Conditional(this.CodecMetadata?.ImageMetadata?.Animation?.ContainsTimecodes == true))
+ {
+ if (!visitor.Bits(32, 0u, ref this.timecode))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendMode.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendMode.cs
new file mode 100644
index 0000000000..035a48edd5
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendMode.cs
@@ -0,0 +1,64 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Represents the blending mode describing how to combine
+/// current frame with previously saved frame.
+///
+internal enum JxlBlendMode : byte
+{
+ ///
+ /// New values replace old ones.
+ ///
+ /// sample = new
+ ///
+ ///
+ Replace,
+
+ ///
+ /// New values add to the old ones.
+ ///
+ /// sample = old + new
+ ///
+ ///
+ Add,
+
+ ///
+ /// New values replace old ones if alpha>0:
+ ///
+ /// alpha = old + new * (1 - old)
+ ///
+ /// For other channels if !alpha_associated:
+ ///
+ /// sample = ((1 - newAlpha) * old * oldAlpha + newAlpha * new) / alpha
+ ///
+ /// For other channels if alpha_associated:
+ ///
+ /// sample = (1 - newAlpha) * old + new
+ ///
+ ///
+ Blend,
+
+ ///
+ /// New values are added to the old ones if alpha>0:
+ /// For the alpha channel that is used as source:
+ ///
+ /// sample = old + new * (1 - old)
+ ///
+ /// Otherwise:
+ ///
+ /// sample = old + alpha * new
+ ///
+ ///
+ AlphaWeightedBlend,
+
+ ///
+ /// New values are multiplied by old ones:
+ ///
+ /// sample = old * new
+ ///
+ ///
+ Multiply
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendingInfo.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendingInfo.cs
new file mode 100644
index 0000000000..58c79d22db
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendingInfo.cs
@@ -0,0 +1,146 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Provides options and instructions that tell the decoder the proper
+/// way to blend the current and previous frame together.
+///
+internal sealed class JxlBlendingInfo : IJxlFields
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public JxlBlendingInfo() => JxlBundle.Init(this);
+
+ ///
+ /// Gets or sets the blending mode. See .
+ ///
+ public JxlBlendMode BlendMode { get; set; }
+
+ ///
+ /// Gets or sets the value that indicates which extra channel
+ /// to use as alpha channel for blending.
+ ///
+ public uint AlphaChannel { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the alpha or channel values
+ /// must be clamped* to the 0 through 1 range.
+ ///
+ ///
+ /// Clamped - must be limited to the specified range.
+ ///
+ public bool Clamp { get; set; }
+
+ ///
+ /// Gets or sets the frame ID to copy from (0 through 3).
+ ///
+ ///
+ /// If is equal to ,
+ /// the value of this property is ignored.
+ ///
+ public uint Source { get; set; }
+
+ ///
+ /// Gets or sets the total number of extra channels.
+ ///
+ public int ExtraChannelCount { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the frame is partial.
+ ///
+ public bool IsPartialFrame { get; set; }
+
+ public bool Visit(JxlVisitor visitor)
+ {
+ JxlBlendMode mode = this.BlendMode;
+ if (!VisitBlendMode(visitor, JxlBlendMode.Replace, ref mode))
+ {
+ return false;
+ }
+
+ this.BlendMode = mode;
+
+ if (visitor.Conditional(this.ExtraChannelCount > 0 && mode is JxlBlendMode.Blend or JxlBlendMode.AlphaWeightedBlend))
+ {
+ uint alphaChannel = this.AlphaChannel;
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(0u),
+ JxlFieldExpressions.Value(1u),
+ JxlFieldExpressions.Value(2u),
+ JxlFieldExpressions.BitsOffset(3u, 3u),
+ 0,
+ ref alphaChannel))
+ {
+ return false;
+ }
+
+ this.AlphaChannel = alphaChannel;
+
+ if (visitor.IsReading && alphaChannel >= this.ExtraChannelCount)
+ {
+ throw new InvalidOperationException("Invalid alpha channel for blending");
+ }
+ }
+
+ if (visitor.Conditional((this.ExtraChannelCount > 0 && mode is JxlBlendMode.Blend or JxlBlendMode.AlphaWeightedBlend) || mode == JxlBlendMode.Multiply))
+ {
+ bool clamp = this.Clamp;
+
+ if (!visitor.Boolean(false, ref clamp))
+ {
+ return false;
+ }
+
+ this.Clamp = clamp;
+ }
+
+ if (visitor.Conditional(mode != JxlBlendMode.Replace || this.IsPartialFrame))
+ {
+ uint source = this.Source;
+
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(0),
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.Value(2),
+ JxlFieldExpressions.Value(3),
+ 0,
+ ref source))
+ {
+ return false;
+ }
+
+ this.Source = source;
+ }
+
+ return true;
+ }
+
+ private static bool VisitBlendMode(JxlVisitor visitor, JxlBlendMode defaultValue, ref JxlBlendMode valueToEncode)
+ {
+ uint unsignedBackingValue = (uint)valueToEncode;
+
+ if (!visitor.U32(
+ JxlFieldExpressions.Value((uint)JxlBlendMode.Replace),
+ JxlFieldExpressions.Value((uint)JxlBlendMode.Add),
+ JxlFieldExpressions.Value((uint)JxlBlendMode.Blend),
+ JxlFieldExpressions.BitsOffset(2u, 3u),
+ (uint)defaultValue,
+ ref unsignedBackingValue))
+ {
+ return false;
+ }
+
+ if (unsignedBackingValue > (uint)JxlBlendMode.Multiply)
+ {
+ throw new InvalidOperationException("Invalid blend mode");
+ }
+
+ valueToEncode = (JxlBlendMode)unsignedBackingValue;
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransform.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransform.cs
new file mode 100644
index 0000000000..f874983486
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransform.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Represents the type of JPEG XL color transform.
+///
+internal enum JxlColorTransform : byte
+{
+ ///
+ /// Use XYB encoding
+ ///
+ Xyb,
+
+ ///
+ /// Encode according to the attached color profile.
+ ///
+ None,
+
+ ///
+ /// Encode according to the attached color profile but
+ /// transformed into Y'Cb'Cr.
+ ///
+ YCbCr,
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs
new file mode 100644
index 0000000000..81bc941870
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Helper methods associated with JxlColorTransform.
+///
+internal static class JxlColorTransformHelpers
+{
+ private static ReadOnlySpan Grayscale => [0, 0, 0];
+
+ private static ReadOnlySpan YCbCr => [1, 0, 2];
+
+ private static ReadOnlySpan None => [0, 1, 2];
+
+ public static ReadOnlySpan GetJpegOrder(JxlColorTransform transform, bool isGraysacle)
+ {
+ if (isGraysacle)
+ {
+ return Grayscale;
+ }
+
+ if (transform == JxlColorTransform.YCbCr)
+ {
+ return YCbCr;
+ }
+
+ return None;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameEncoding.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameEncoding.cs
new file mode 100644
index 0000000000..77ae964ac9
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameEncoding.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///