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..398587baac
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Formats.Jxl.IO;
+
+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.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ 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;
+ }
+
+ // 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..e6e673f9c8
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs
@@ -0,0 +1,89 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.IO;
+
+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;
+ }
+
+ ///
+ /// 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;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs
new file mode 100644
index 0000000000..aae11aa120
--- /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(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..3c4126c4b8
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs
@@ -0,0 +1,72 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Formats.Jxl.IO;
+
+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.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ 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).
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ 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;
+ }
+}
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..b0396579cc
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs
@@ -0,0 +1,132 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.IO;
+
+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)
+ {
+ int idxExtension = Num0BitsBelowLS1BitNonzero(remainingExtensions);
+ if (!this.U64(0, ref this.extensionBits[idxExtension]))
+ {
+ return false;
+ }
+
+ if (!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 (!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 this.ThrowIfEndOfStreamOrReturnTrue();
+ }
+
+ private bool ThrowIfEndOfStreamOrReturnTrue()
+ {
+ if (reader.IsEndOfStream)
+ {
+ DebugGuard.IsTrue(false, "Got an invalid end-of-stream");
+ this.notEnoughBytes = true;
+ return true;
+ }
+
+ 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..e909f7f74f
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs
@@ -0,0 +1,127 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Formats.Jxl.IO;
+
+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.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ 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)
+ {
+ int bitsRequired = 32 - 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)
+ {
+ DebugGuard.IsTrue(false, "No matching selector");
+
+ return false;
+ }
+
+ 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..ba121cafe2
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs
@@ -0,0 +1,106 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.IO;
+
+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;
+ }
+
+ ///
+ /// 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..7d2dffffbf
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs
@@ -0,0 +1,74 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+
+#pragma warning disable SA1405 // Debug.Assert should provide message text
+
+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)
+ {
+ // TODO: use DebugGuard
+ Debug.Assert(!this.extensionStates.IsBegun || this.extensionStates.IsEnded);
+ }
+
+ this.extensionStates.Pop();
+
+ // TODO: use DebugGuard
+ Debug.Assert(this.depth != 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;
+ }
+
+ // TODO: use DebugGuard
+ Debug.Assert(bits <= 1u);
+
+ 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/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..de61664146
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs
@@ -0,0 +1,39 @@
+// 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 readonly int[][] JpegOrders =
+ [
+ [0, 0, 0], // Grayscale
+ [1, 0, 2], // Y'Cb'Cr
+ [0, 1, 2], // None
+ [0, 1, 2] // Anything else
+ ];
+
+ public static ReadOnlySpan GetJpegOrder(JxlColorTransform transform, bool isGraysacle)
+ {
+ if (isGraysacle)
+ {
+ return JpegOrders[0];
+ }
+
+ if (transform == JxlColorTransform.YCbCr)
+ {
+ return JpegOrders[1];
+ }
+ else if (transform == JxlColorTransform.None)
+ {
+ return JpegOrders[2];
+ }
+ else
+ {
+ return JpegOrders[3];
+ }
+ }
+}
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;
+
+///
+/// Represents the kind of frame encoding.
+///
+internal enum JxlFrameEncoding : byte
+{
+ ///
+ /// Use VarDCT
+ ///
+ VarDct,
+
+ ///
+ /// Use Modular encoding
+ ///
+ Modular
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs
new file mode 100644
index 0000000000..a45c0e4710
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs
@@ -0,0 +1,743 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+// Disable IDE0032 for consistency with other fields.
+// We have to avoid auto properties for most fields
+// so we can use the ref keyword on them directly.
+#pragma warning disable IDE0032 // Use auto property
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+using SixLabors.ImageSharp.Formats.Jxl.Metadata;
+using SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Control information for a JPEG XL frame.
+///
+internal sealed class JxlFrameHeader : IJxlFields
+{
+ // The following are backing fields for properties.
+ private JxlFrameEncoding encoding = JxlFrameEncoding.Modular;
+ private JxlFrameType frameType = JxlFrameType.RegularFrame;
+ private ulong flags;
+ private JxlColorTransform colorTransform = JxlColorTransform.Xyb;
+ private JxlYCbCrChromaSubsampling? chromaSubsampling;
+ private uint groupSizeShift;
+ private uint xQmScale;
+ private uint bQmScale;
+ private string? name;
+ private bool customSizeOrOrigin;
+ private Size frameSize;
+ private uint upsampling;
+ private List extraChannelUpsampling = [];
+ private Point frameOrigin;
+ private JxlBlendingInfo? blendingInfo;
+ private List extraChannelBlendingInfo = [];
+ private readonly JxlAnimationFrame? animationFrame;
+ private bool isLast;
+ private uint saveAsReference;
+ private bool saveBeforeColorTransform;
+ private uint dcLevel;
+ private JxlCodecMetadata? metadata;
+ private JxlLoopFilter? loopFilter;
+ private ulong extensions;
+
+ private bool isPreviewFrame; // Non-serialized
+
+ ///
+ /// Gets or sets the frame encoding method (e.g., Modular or VarDCT).
+ ///
+ public JxlFrameEncoding Encoding
+ {
+ get => this.encoding;
+ set => this.encoding = value;
+ }
+
+ ///
+ /// Gets or sets the type of frame (e.g., RegularFrame).
+ ///
+ public JxlFrameType FrameType
+ {
+ get => this.frameType;
+ set => this.frameType = value;
+ }
+
+ ///
+ /// Gets or sets the frame flags.
+ ///
+ public ulong Flags
+ {
+ get => this.flags;
+ set => this.flags = value;
+ }
+
+ ///
+ /// Gets or sets the color transform used (e.g., XYB).
+ ///
+ public JxlColorTransform ColorTransform
+ {
+ get => this.colorTransform;
+ set => this.colorTransform = value;
+ }
+
+ ///
+ /// Gets or sets the chroma subsampling information.
+ ///
+ public JxlYCbCrChromaSubsampling? ChromaSubsampling
+ {
+ get => this.chromaSubsampling;
+ set => this.chromaSubsampling = value;
+ }
+
+ ///
+ /// Gets or sets the group size shift value.
+ ///
+ public uint GroupSizeShift
+ {
+ get => this.groupSizeShift;
+ set => this.groupSizeShift = value;
+ }
+
+ ///
+ /// Gets or sets the X quantization matrix scale.
+ ///
+ public uint XQmScale
+ {
+ get => this.xQmScale;
+ set => this.xQmScale = value;
+ }
+
+ ///
+ /// Gets or sets the B quantization matrix scale.
+ ///
+ public uint BQmScale
+ {
+ get => this.bQmScale;
+ set => this.bQmScale = value;
+ }
+
+ ///
+ /// Gets or sets the frame name.
+ ///
+ public string? Name
+ {
+ get => this.name;
+ set => this.name = value;
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the frame has a custom size or origin.
+ ///
+ public bool CustomSizeOrOrigin
+ {
+ get => this.customSizeOrOrigin;
+ set => this.customSizeOrOrigin = value;
+ }
+
+ ///
+ /// Gets or sets the frame size.
+ ///
+ public Size FrameSize
+ {
+ get => this.frameSize;
+ set => this.frameSize = value;
+ }
+
+ ///
+ /// Gets or sets the upsampling factor.
+ ///
+ public uint Upsampling
+ {
+ get => this.upsampling;
+ set => this.upsampling = value;
+ }
+
+ ///
+ /// Gets or sets the upsampling factors for extra channels.
+ ///
+ public List ExtraChannelUpsampling
+ {
+ get => this.extraChannelUpsampling;
+ set => this.extraChannelUpsampling = value;
+ }
+
+ ///
+ /// Gets or sets the frame origin point.
+ ///
+ public Point FrameOrigin
+ {
+ get => this.frameOrigin;
+ set => this.frameOrigin = value;
+ }
+
+ ///
+ /// Gets or sets the blending information for the frame.
+ ///
+ public JxlBlendingInfo? BlendingInfo
+ {
+ get => this.blendingInfo;
+ set => this.blendingInfo = value;
+ }
+
+ ///
+ /// Gets or sets the blending information for extra channels.
+ ///
+ public List ExtraChannelBlendingInfo
+ {
+ get => this.extraChannelBlendingInfo;
+ set => this.extraChannelBlendingInfo = value;
+ }
+
+ ///
+ /// Gets the associated animation frame, if any.
+ ///
+ public JxlAnimationFrame? AnimationFrame => this.animationFrame;
+
+ ///
+ /// Gets or sets a value indicating whether this is the last frame.
+ ///
+ public bool IsLast
+ {
+ get => this.isLast;
+ set => this.isLast = value;
+ }
+
+ ///
+ /// Gets or sets the reference frame index to save.
+ ///
+ public uint SaveAsReference
+ {
+ get => this.saveAsReference;
+ set => this.saveAsReference = value;
+ }
+
+ ///
+ /// Gets or sets a value indicating whether to save before color transform.
+ ///
+ public bool SaveBeforeColorTransform
+ {
+ get => this.saveBeforeColorTransform;
+ set => this.saveBeforeColorTransform = value;
+ }
+
+ ///
+ /// Gets or sets the DC level of the frame.
+ ///
+ public uint DcLevel
+ {
+ get => this.dcLevel;
+ set => this.dcLevel = value;
+ }
+
+ ///
+ /// Gets or sets the codec metadata.
+ ///
+ public JxlCodecMetadata? Metadata
+ {
+ get => this.metadata;
+ set => this.metadata = value;
+ }
+
+ ///
+ /// Gets or sets the loop filter applied to the frame.
+ ///
+ public JxlLoopFilter? LoopFilter
+ {
+ get => this.loopFilter;
+ set => this.loopFilter = value;
+ }
+
+ ///
+ /// Gets or sets a value indicating whether this is a preview frame. Non-serialized.
+ ///
+ public bool IsPreviewFrame
+ {
+ get => this.isPreviewFrame;
+ set => this.isPreviewFrame = value;
+ }
+
+ ///
+ /// Gets or sets the number of extensions.
+ ///
+ public ulong Extensions
+ {
+ get => this.extensions;
+ set => this.extensions = value;
+ }
+
+ public int DefaultXSize
+ {
+ get
+ {
+ if (this.metadata == null)
+ {
+ return 0;
+ }
+
+ if (this.isPreviewFrame)
+ {
+ return this.metadata.ImageMetadata?.PreviewSize?.XSize ?? 0;
+ }
+
+ return this.metadata.XSize;
+ }
+ }
+
+ public int DefaultYSize
+ {
+ get
+ {
+ if (this.metadata == null)
+ {
+ return 0;
+ }
+
+ if (this.isPreviewFrame)
+ {
+ return this.metadata.ImageMetadata?.PreviewSize?.YSize ?? 0;
+ }
+
+ return this.metadata.YSize;
+ }
+ }
+
+ public JxlFrameDimensions FrameDimensions
+ {
+ get
+ {
+ int xsize = this.DefaultXSize;
+ int ysize = this.DefaultYSize;
+
+ xsize = this.frameSize.Width != 0 ? this.frameSize.Width : xsize;
+ ysize = this.frameSize.Height != 0 ? this.frameSize.Height : ysize;
+
+ if (this.dcLevel != 0)
+ {
+ xsize = JxlMath.DivCeil(xsize, 1 << (3 * (int)this.dcLevel));
+ ysize = JxlMath.DivCeil(ysize, 1 << (3 * (int)this.dcLevel));
+ }
+
+ JxlFrameDimensions frameDim = new(
+ xsize,
+ ysize,
+ (int)this.groupSizeShift,
+ this.chromaSubsampling?.MaxHShift ?? 0,
+ this.chromaSubsampling?.MaxVShift ?? 0,
+ this.encoding == JxlFrameEncoding.Modular,
+ (int)this.upsampling);
+
+ return frameDim;
+ }
+ }
+
+ public bool NeedsColorTransform => !this.saveBeforeColorTransform ||
+ this.frameType == JxlFrameType.RegularFrame ||
+ this.frameType == JxlFrameType.SkipProgressive;
+
+ ///
+ /// Gets a value indicating whether this frame is supposed to be saved for future usage by other frames.
+ ///
+ public bool CanBeReferenced => // DC frames cannot be referenced. The last frame cannot be referenced.
+ // A duration 0 frame makes little sense if it is not referenced.
+ // A non-duration 0 frame may or may not be referenced.
+ !this.isLast &&
+ this.frameType != JxlFrameType.DcFrame &&
+ (this.animationFrame?.Duration == 0 || this.saveAsReference != 0);
+
+ private void UpdateFlag(bool condition, ulong flag)
+ {
+ if (condition)
+ {
+ this.flags |= flag;
+ }
+ else
+ {
+ this.flags &= ~flag;
+ }
+ }
+
+ public bool Visit(JxlVisitor visitor)
+ {
+ bool allDefault = false;
+ if (visitor.AllDefault(this, ref allDefault))
+ {
+ visitor.SetDefault(this);
+ return true;
+ }
+
+ if (!VisitFrameType(visitor, JxlFrameType.RegularFrame, ref this.frameType))
+ {
+ return false;
+ }
+
+ if (visitor.IsReading && this.isPreviewFrame && this.frameType != JxlFrameType.RegularFrame)
+ {
+ throw new InvalidOperationException("Only regular frame could be a preview");
+ }
+
+ // FrameEncoding
+ bool isModular = this.encoding == JxlFrameEncoding.Modular;
+ if (!visitor.Boolean(false, ref isModular))
+ {
+ return false;
+ }
+
+ this.encoding = isModular
+ ? JxlFrameEncoding.Modular
+ : JxlFrameEncoding.VarDct;
+
+ // Flags
+ if (!visitor.U64(0, ref this.flags))
+ {
+ return false;
+ }
+
+ // Color transform
+ bool xybEncoded = this.metadata?.ImageMetadata?.XybEncoded == true;
+ if (xybEncoded)
+ {
+ this.colorTransform = JxlColorTransform.Xyb;
+ }
+ else
+ {
+ bool alternate = this.colorTransform == JxlColorTransform.YCbCr;
+ if (!visitor.Boolean(false, ref alternate))
+ {
+ return false;
+ }
+
+ this.colorTransform = alternate
+ ? JxlColorTransform.YCbCr
+ : JxlColorTransform.None;
+ }
+
+ // Chroma subsampling
+ if (visitor.Conditional(this.colorTransform == JxlColorTransform.YCbCr &&
+ ((this.flags & (ulong)JxlFrameHeaderFlags.Dc) == 0)))
+ {
+ if (!visitor.VisitNested(this.chromaSubsampling!))
+ {
+ return false;
+ }
+ }
+
+ int numExtraChannels = this.metadata?.ImageMetadata?.ExtraChannelCount ?? 0;
+
+ // Upsampling
+ if (visitor.Conditional((this.flags & (ulong)JxlFrameHeaderFlags.Dc) == 0))
+ {
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.Value(2),
+ JxlFieldExpressions.Value(4),
+ JxlFieldExpressions.Value(8),
+ 1,
+ ref this.upsampling))
+ {
+ return false;
+ }
+
+ if (this.metadata != null && visitor.Conditional(numExtraChannels != 0))
+ {
+ List extraChannels = this.metadata!.ImageMetadata?.ExtraChannels ?? [];
+ this.extraChannelUpsampling = new List(extraChannels.Count);
+
+ for (int i = 0; i < extraChannels.Count; i++)
+ {
+ uint dimShift = (uint)extraChannels[i].DimensionShift;
+ uint ecUpsampling = 1;
+ ecUpsampling >>= (int)dimShift;
+
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.Value(2),
+ JxlFieldExpressions.Value(4),
+ JxlFieldExpressions.Value(8),
+ 1,
+ ref ecUpsampling))
+ {
+ return false;
+ }
+
+ ecUpsampling <<= (int)dimShift;
+
+ if (ecUpsampling < this.upsampling)
+ {
+ throw new InvalidOperationException("EC upsampling < color upsampling, invalid");
+ }
+
+ if (ecUpsampling > 8)
+ {
+ throw new InvalidOperationException("EC upsampling too large");
+ }
+
+ this.extraChannelUpsampling.Add(ecUpsampling);
+ }
+ }
+ else
+ {
+ this.extraChannelUpsampling.Clear();
+ }
+ }
+
+ // Modular / VarDCT specifics
+ if (visitor.Conditional(this.encoding == JxlFrameEncoding.Modular))
+ {
+ if (!visitor.Bits(2, 1, ref this.groupSizeShift))
+ {
+ return false;
+ }
+ }
+
+ if (visitor.Conditional(this.encoding == JxlFrameEncoding.VarDct &&
+ this.colorTransform == JxlColorTransform.Xyb))
+ {
+ if (!visitor.Bits(3, 3, ref this.xQmScale))
+ {
+ return false;
+ }
+
+ if (!visitor.Bits(3, 2, ref this.bQmScale))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ this.xQmScale = this.bQmScale = 2;
+ }
+
+ // Passes
+ if (visitor.Conditional(this.frameType != JxlFrameType.ReferenceOnly))
+ {
+ if (!visitor.VisitNested(this.passes))
+ {
+ return false;
+ }
+ }
+
+ // DC frame
+ if (visitor.Conditional(this.frameType == JxlFrameType.DcFrame))
+ {
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.Value(2),
+ JxlFieldExpressions.Value(3),
+ JxlFieldExpressions.Value(4),
+ 1,
+ ref this.dcLevel))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ this.dcLevel = 0;
+ }
+
+ // Custom size/origin
+ bool isPartialFrame = false;
+
+ if (visitor.Conditional(this.frameType != JxlFrameType.DcFrame))
+ {
+ if (!visitor.Boolean(false, ref this.customSizeOrOrigin))
+ {
+ return false;
+ }
+
+ if (visitor.Conditional(this.customSizeOrOrigin))
+ {
+ JxlU32Enc enc = new(
+ JxlFieldExpressions.Bits(8),
+ JxlFieldExpressions.BitsOffset(11, 256),
+ JxlFieldExpressions.BitsOffset(14, 2304),
+ JxlFieldExpressions.BitsOffset(30, 18688));
+
+ if (visitor.Conditional(this.frameType is JxlFrameType.RegularFrame or JxlFrameType.SkipProgressive))
+ {
+ uint ux0 = JxlPackSigned.PackUnsigned(this.frameOrigin.X);
+ uint uy0 = JxlPackSigned.PackUnsigned(this.frameOrigin.Y);
+
+ if (!visitor.U32(enc, 0, ref ux0))
+ {
+ return false;
+ }
+
+ if (!visitor.U32(enc, 0, ref uy0))
+ {
+ return false;
+ }
+
+ this.frameOrigin = new Point(JxlPackSigned.UnpackSigned(ux0), JxlPackSigned.UnpackSigned(uy0));
+ }
+
+ uint frameSizeWidth = (uint)this.frameSize.Width;
+ uint frameSizeHeight = (uint)this.frameSize.Height;
+
+ if (!visitor.U32(enc, 0, ref frameSizeWidth))
+ {
+ return false;
+ }
+
+ if (!visitor.U32(enc, 0, ref frameSizeHeight))
+ {
+ return false;
+ }
+
+ if (this.customSizeOrOrigin && (this.frameSize.Width == 0 || this.frameSize.Height == 0))
+ {
+ throw new InvalidOperationException("Invalid crop dimensions for frame");
+ }
+
+ int imageXSize = this.DefaultXSize;
+ int imageYSize = this.DefaultYSize;
+
+ if (this.frameType is JxlFrameType.RegularFrame or JxlFrameType.SkipProgressive)
+ {
+ isPartialFrame |= this.frameOrigin.X > 0;
+ isPartialFrame |= this.frameOrigin.Y > 0;
+ isPartialFrame |= (this.frameSize.Width + this.frameOrigin.X) < imageXSize;
+ isPartialFrame |= (this.frameSize.Height + this.frameOrigin.Y) < imageYSize;
+ }
+ }
+ }
+
+ // Blending, animation, last frame
+ if (visitor.Conditional(this.frameType is JxlFrameType.RegularFrame or JxlFrameType.SkipProgressive))
+ {
+ this.blendingInfo!.ExtraChannelCount = numExtraChannels;
+ this.blendingInfo.IsPartialFrame = isPartialFrame;
+
+ if (!visitor.VisitNested(this.blendingInfo))
+ {
+ return false;
+ }
+
+ bool replaceAll = this.blendingInfo.BlendMode == JxlBlendMode.Replace;
+
+ this.extraChannelBlendingInfo = new List(numExtraChannels);
+ for (int i = 0; i < numExtraChannels; i++)
+ {
+ JxlBlendingInfo ecBlendingInfo = new()
+ {
+ IsPartialFrame = isPartialFrame,
+ ExtraChannelCount = numExtraChannels
+ };
+
+ if (!visitor.VisitNested(ecBlendingInfo))
+ {
+ return false;
+ }
+
+ this.extraChannelBlendingInfo.Add(ecBlendingInfo);
+ replaceAll &= ecBlendingInfo.BlendMode == JxlBlendMode.Replace;
+ }
+
+ if (visitor.IsReading && this.isPreviewFrame)
+ {
+ if (!replaceAll || this.customSizeOrOrigin)
+ {
+ throw new InvalidOperationException("Preview is not compatible with blending");
+ }
+ }
+
+ if (visitor.Conditional(this.metadata?.ImageMetadata?.HaveAnimation == true))
+ {
+ this.animationFrame!.CodecMetadata = this.metadata;
+
+ if (!visitor.VisitNested(this.animationFrame!))
+ {
+ return false;
+ }
+ }
+
+ if (!visitor.Boolean(true, ref this.isLast))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ this.isLast = false;
+ }
+
+ // SaveAsReference
+ if (visitor.Conditional(this.frameType != JxlFrameType.DcFrame && !this.isLast))
+ {
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(0),
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.Value(2),
+ JxlFieldExpressions.Value(3),
+ 0,
+ ref this.saveAsReference))
+ {
+ return false;
+ }
+ }
+
+ // SaveBeforeColorTransform logic
+ if (this.frameType != JxlFrameType.DcFrame)
+ {
+ if (visitor.Conditional(
+ this.CanBeReferenced &&
+ this.blendingInfo?.BlendMode == JxlBlendMode.Replace &&
+ !isPartialFrame &&
+ (this.frameType == JxlFrameType.RegularFrame ||
+ this.frameType == JxlFrameType.SkipProgressive)))
+ {
+ if (!visitor.Boolean(false, ref this.saveBeforeColorTransform))
+ {
+ return false;
+ }
+ }
+ else if (visitor.Conditional(this.frameType == JxlFrameType.ReferenceOnly))
+ {
+ if (!visitor.Boolean(true, ref this.saveBeforeColorTransform))
+ {
+ return false;
+ }
+
+ int xsize = this.customSizeOrOrigin
+ ? this.frameSize.Width
+ : this.metadata!.XSize;
+
+ int ysize = this.customSizeOrOrigin
+ ? this.frameSize.Height
+ : this.metadata!.YSize;
+
+ if (!this.saveBeforeColorTransform &&
+ (xsize < this.metadata!.XSize ||
+ ysize < this.metadata!.YSize ||
+ this.frameOrigin.X != 0 ||
+ this.frameOrigin.Y != 0))
+ {
+ throw new InvalidOperationException("Non-patch reference frame with invalid crop");
+ }
+ }
+ }
+ else
+ {
+ this.saveBeforeColorTransform = true;
+ }
+
+ if (!VisitNameString(visitor, ref this.name))
+ {
+ return false;
+ }
+
+ this.loopFilter!.IsModular = isModular;
+ if (!visitor.VisitNested(this.loopFilter!))
+ {
+ return false;
+ }
+
+ if (!visitor.BeginExtensions(ref this.extensions))
+ {
+ return false;
+ }
+
+ return visitor.EndExtensions();
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeaderFlags.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeaderFlags.cs
new file mode 100644
index 0000000000..098bcb57fc
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeaderFlags.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Optional steps for postprocessing. These flags are the
+/// source of truth. Override must set/clear them rather than
+/// change their meaning. Values chosen such that typical flags
+/// are 0, encoded in only two bits.
+///
+[Flags]
+internal enum JxlFrameHeaderFlags : byte
+{
+ ///
+ /// Noise is injected into decoded output.
+ ///
+ Noise = 1,
+
+ ///
+ /// Overlay patches.
+ ///
+ Patches = 2,
+
+ ///
+ /// Overlay splines.
+ ///
+ Splines = 16,
+
+ ///
+ /// Implies skip adaptive DC smoothing.
+ ///
+ Dc = 32,
+
+ SkipAdaptiveDcSmoothing = 128,
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameType.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameType.cs
new file mode 100644
index 0000000000..6aa73eab26
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameType.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Defines the type of a JPEG XL frame.
+///
+internal enum JxlFrameType : byte
+{
+ ///
+ /// A regular frame. It might be a crop, and it will be blended
+ /// on a previous frame (if any) and likely displayed or blended in
+ /// future frames.
+ ///
+ RegularFrame,
+
+ ///
+ /// A DC frame. It is downsampled and only used as the DC
+ /// of a future and, possibly, preview frame. This cannot be cropped,
+ /// blended, or referenced by patches or blending modes. Frames using
+ /// DC cannot have non-default sizes.
+ ///
+ DcFrame,
+
+ ///
+ /// A PatchesSource frame. Can only be used as source frame for
+ /// taking patches. It can be cropped but can't have a non-(0, 0) x0/y0.
+ ///
+ ReferenceOnly = 2,
+
+ ///
+ /// Same as regular frame but not used for progressive rendering.
+ /// Implies no early display of DC.
+ ///
+ SkipProgressive,
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlPasses.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlPasses.cs
new file mode 100644
index 0000000000..87277414ac
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlPasses.cs
@@ -0,0 +1,220 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Used for decoding to lower resolutions.
+///
+internal sealed class JxlPasses : IJxlFields
+{
+ ///
+ /// Defines the maximum amount of passes, which is 11.
+ ///
+ private const int MaxPasses = 11;
+
+ private uint numPasses;
+ private uint numDownsample;
+
+ ///
+ /// Gets or sets the number of passes.
+ ///
+ public uint NumberOfPasses
+ {
+ get => this.numPasses;
+ set => this.numPasses = value;
+ }
+
+ ///
+ /// Gets or sets the number of downsamples.
+ ///
+ public uint NumberOfDownsamples
+ {
+ get => this.numDownsample;
+ set => this.numDownsample = value;
+ }
+
+ ///
+ /// Gets the downsample values.
+ ///
+ public uint[] Downsample { get; } = new uint[MaxPasses];
+
+ ///
+ /// Gets the last pass values.
+ ///
+ public uint[] LastPass { get; } = new uint[MaxPasses];
+
+ ///
+ /// Gets the shift values.
+ ///
+ public uint[] Shift { get; } = new uint[MaxPasses];
+
+ public void GetDownsamplingBracket(int pass, out int minShift, out int maxShift)
+ {
+ maxShift = 2;
+ minShift = 3;
+
+ for (int i = 0; ; i++)
+ {
+ for (int j = 0; j < this.numDownsample; ++j)
+ {
+ if (i == this.LastPass[j])
+ {
+ uint ds = this.Downsample[j];
+
+ if (ds == 8)
+ {
+ minShift = 3;
+ }
+
+ if (ds == 4)
+ {
+ minShift = 2;
+ }
+
+ if (ds == 2)
+ {
+ minShift = 1;
+ }
+
+ if (ds == 1)
+ {
+ minShift = 0;
+ }
+ }
+ }
+
+ if (i == this.numPasses - 1)
+ {
+ minShift = 0;
+ }
+
+ if (i == pass)
+ {
+ return;
+ }
+
+ maxShift = minShift - 1;
+ }
+ }
+
+ public uint GetDownsamplingTargetForCompletedPasses(int num)
+ {
+ if (num >= this.numPasses)
+ {
+ return 1;
+ }
+
+ uint result = 0;
+
+ for (int i = 0; i < this.numDownsample; i++)
+ {
+ if (num > this.LastPass[i])
+ {
+ result = Math.Min(result, this.Downsample[i]);
+ }
+ }
+
+ return result;
+ }
+
+ public bool Visit(JxlVisitor visitor)
+ {
+ if (visitor.U32(
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.Value(2),
+ JxlFieldExpressions.Value(2),
+ JxlFieldExpressions.BitsOffset(1, 3),
+ 0,
+ ref this.numPasses))
+ {
+ return false;
+ }
+
+ if (this.numPasses > MaxPasses)
+ {
+ return false;
+ }
+
+ if (visitor.Conditional(this.numPasses != 1))
+ {
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(0),
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.Value(2),
+ JxlFieldExpressions.BitsOffset(1, 3),
+ 0,
+ ref this.numDownsample))
+ {
+ return false;
+ }
+
+ if (this.numDownsample > 4)
+ {
+ return false;
+ }
+
+ if (this.numDownsample > this.numPasses)
+ {
+ throw new InvalidOperationException("Number of downsaples is greater than number of passes");
+ }
+
+ for (int i = 0; i < this.numPasses - 1; i++)
+ {
+ if (!visitor.Bits(2, 0u, ref this.Shift[i]))
+ {
+ return false;
+ }
+ }
+
+ this.Shift[this.numPasses - 1] = 0;
+
+ for (int i = 0; i < this.numDownsample; i++)
+ {
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.Value(2),
+ JxlFieldExpressions.Value(4),
+ JxlFieldExpressions.Value(8),
+ 1,
+ ref this.Downsample[i]))
+ {
+ return false;
+ }
+
+ if (i > 0 && this.Downsample[i] >= this.Downsample[i - 1])
+ {
+ throw new InvalidOperationException("Downsample sequence should decrease");
+ }
+ }
+
+ for (int i = 0; i < this.numDownsample; i++)
+ {
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(0),
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.Value(2),
+ JxlFieldExpressions.Value(3),
+ 0,
+ ref this.LastPass[i]))
+ {
+ return false;
+ }
+
+ if (i > 0 && this.LastPass[i] <= this.LastPass[i - 1])
+ {
+ throw new InvalidOperationException("Last pass sequence should increase");
+ }
+
+ if (this.LastPass[i] >= this.numPasses)
+ {
+ throw new InvalidOperationException("Last pass is greater than number of passes");
+ }
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlYCbCrChromaSubsampling.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlYCbCrChromaSubsampling.cs
new file mode 100644
index 0000000000..b10d8818aa
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlYCbCrChromaSubsampling.cs
@@ -0,0 +1,147 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Gets the Y'Cb'Cr chroma subsampling information as part
+/// of the JPEG XL Frame Header.
+///
+internal sealed class JxlYCbCrChromaSubsampling : IJxlFields
+{
+ private readonly int[] channelMode = new int[3];
+
+ private static ReadOnlySpan HShiftData => [0, 1, 1, 0];
+
+ private static ReadOnlySpan VShiftData => [0, 1, 0, 1];
+
+ public byte MaxHShift { get; private set; }
+
+ public byte MaxVShift { get; private set; }
+
+ ///
+ /// Gets a value indicating whether this 4:4:4 chroma subsampling.
+ ///
+ public bool Is444 =>
+ this.HShift(0) == 0 && this.VShift(0) == 0 && // Cb
+ this.HShift(2) == 0 && this.VShift(2) == 0 && // Cr
+ this.HShift(1) == 0 && this.VShift(1) == 0; // Y;
+
+ ///
+ /// Gets a value indicating whether this 4:2:0 chroma subsampling.
+ ///
+ public bool Is420 =>
+ this.HShift(0) == 1 && this.VShift(0) == 1 && // Cb
+ this.HShift(2) == 1 && this.VShift(2) == 1 && // Cr
+ this.HShift(1) == 0 && this.VShift(1) == 0; // Y
+
+ ///
+ /// Gets a value indicating whether this 4:2:2 chroma subsampling.
+ ///
+ public bool Is422 =>
+ this.HShift(0) == 1 && this.VShift(0) == 0 && // Cb
+ this.HShift(2) == 1 && this.VShift(2) == 0 && // Cr
+ this.HShift(1) == 0 && this.VShift(1) == 0; // Y
+
+ ///
+ /// Gets a value indicating whether this 4:4:0 chroma subsampling.
+ ///
+ public bool Is440 =>
+ this.HShift(0) == 0 && this.VShift(0) == 1 && // Cb
+ this.HShift(2) == 0 && this.VShift(2) == 1 && // Cr
+ this.HShift(1) == 0 && this.VShift(1) == 0; // Y
+
+ public byte RawHShift(int c) => HShiftData[this.channelMode[c]];
+
+ public byte RawVShift(int c) => VShiftData[this.channelMode[c]];
+
+ public byte HShift(int c) => (byte)(this.MaxHShift - HShiftData[this.channelMode[c]]);
+
+ public byte VShift(int c) => (byte)(this.MaxVShift - VShiftData[this.channelMode[c]]);
+
+ private void Recompute()
+ {
+ this.MaxHShift = 0;
+ this.MaxVShift = 0;
+
+ for (int i = 0; i < 3; i++)
+ {
+ int ch = this.channelMode[i];
+
+ this.MaxHShift = Math.Max(this.MaxHShift, HShiftData[ch]);
+ this.MaxVShift = Math.Max(this.MaxVShift, VShiftData[ch]);
+ }
+ }
+
+ public bool Set(ReadOnlySpan hsample, ReadOnlySpan vsample)
+ {
+ for (int c = 0; c < 3; c++)
+ {
+ int cjpeg = c < 2 ? (c ^ 1) : c;
+ int i = 0;
+
+ for (; i < 4; i++)
+ {
+ if (1 << HShiftData[i] == hsample[cjpeg] && 1 << VShiftData[i] == vsample[cjpeg])
+ {
+ this.channelMode[c] = i;
+ break;
+ }
+ }
+
+ if (i == 4)
+ {
+ return false;
+ }
+ }
+
+ this.Recompute();
+ return true;
+ }
+
+ public override string ToString()
+ {
+ if (this.Is444)
+ {
+ return "4:4:4";
+ }
+ else if (this.Is420)
+ {
+ return "4:2:0";
+ }
+ else if (this.Is422)
+ {
+ return "4:2:2";
+ }
+ else if (this.Is440)
+ {
+ return "4:4:0";
+ }
+ else
+ {
+ return $"[Custom] {this.channelMode[0]}:{this.channelMode[1]}:{this.channelMode[2]}";
+ }
+ }
+
+ public bool Visit(JxlVisitor visitor)
+ {
+ for (int i = 0; i < 3; i++)
+ {
+ int channel = this.channelMode[i];
+
+ uint unsignedChannel = (uint)channel;
+ bool wroteSuccessfully = visitor.Bits(2, 0, ref unsignedChannel);
+
+ if (!wroteSuccessfully)
+ {
+ return false;
+ }
+
+ this.channelMode[i] = (int)unsignedChannel;
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsConstants.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsConstants.cs
new file mode 100644
index 0000000000..dcd244ac7d
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsConstants.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO;
+
+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/JxlAnsEntry.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs
new file mode 100644
index 0000000000..25a9d12609
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/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;
+
+[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/JxlAnsHelper.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs
new file mode 100644
index 0000000000..35630eb103
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs
@@ -0,0 +1,242 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO;
+
+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)
+ {
+ Debug.Assert(length <= 0, "Length should be >= 0");
+ Debug.Assert(length > totalCount, "Length should be <= totalCount");
+
+ 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)
+ {
+ int range = 1 << (int)logRange;
+ int tableSize = 1 << logAlphaSize;
+
+ Debug.Assert(tableSize <= range, "table_size must be <= range");
+
+ 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)
+ {
+ Debug.Fail("Too many items in the distribution");
+
+ return false;
+ }
+
+ 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/JxlAnsHybridUIntConfiguration.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHybridUIntConfiguration.cs
new file mode 100644
index 0000000000..422eb2323f
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHybridUIntConfiguration.cs
@@ -0,0 +1,60 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO;
+
+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;
+
+ Debug.Assert(splitExponent >= msbInToken + lsbInToken, "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, ref uint token, ref uint bitCount, ref uint bits)
+ {
+ if (value < this.SplitToken)
+ {
+ token = value;
+ bitCount = 0;
+ bits = 0;
+ }
+ else
+ {
+ uint n = 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/JxlAnsLz77Parameters.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsLz77Parameters.cs
new file mode 100644
index 0000000000..ecf5cdc3fa
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsLz77Parameters.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO;
+
+internal sealed class JxlAnsLz77Parameters : IJxlFields
+{
+ public bool Enabled { get; set; }
+
+ public uint MinimumSymbol { get; set; }
+
+ public uint MinimumLength { get; set; }
+
+ public JxlAnsHybridUIntConfiguration LengthUintConfig { get; set; } = new(0, 0, 0);
+
+ public int NonserializedDistanceContext { get; set; }
+
+ public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs
new file mode 100644
index 0000000000..d28e98c029
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs
@@ -0,0 +1,256 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using System.Diagnostics;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO;
+
+internal static class JxlAnsReader
+{
+ // Prefer jagged arrays over multidimensional arrays
+ // for performance. Collection expressions help represent
+ // jagged arrays easily.
+ private static readonly byte[][] HuffmanLookup =
+ [
+ [3, 10], [7, 12], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5],
+ [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2],
+ [3, 10], [5, 0], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5],
+ [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2],
+ [3, 10], [6, 11], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5],
+ [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2],
+ [3, 10], [5, 0], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5],
+ [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2],
+ [3, 10], [7, 13], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5],
+ [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2],
+ [3, 10], [5, 0], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5],
+ [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2],
+ [3, 10], [6, 11], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5],
+ [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2],
+ [3, 10], [5, 0], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5],
+ [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2],
+ ];
+
+ public static uint DecodeVariableLengthUint8(JxlBitReader reader)
+ {
+ if (reader.ReadBoolean())
+ {
+ uint bitCount = reader.ReadBits32(3u);
+
+ return bitCount == 0
+ ? 1u
+ : (reader.ReadBits32(bitCount) + (1u << (int)bitCount));
+ }
+
+ return 0u;
+ }
+
+ public static uint DecodeVariableLengthUint16(JxlBitReader reader)
+ {
+ if (reader.ReadBoolean())
+ {
+ uint bitCount = reader.ReadBits32(4u);
+
+ return bitCount == 0
+ ? 1u
+ : (reader.ReadBits32(bitCount) + (1u << (int)bitCount));
+ }
+
+ return 0u;
+ }
+
+ // NOTE: this method returns null on failure.
+ // If the return value is a valid IMemoryOwner object,
+ // then it succeeded.
+ public static IMemoryOwner? ReadHistogram(Configuration configuration, int precisionBits, JxlBitReader reader)
+ {
+ int range = 1 << precisionBits;
+ bool isSimpleCode = reader.ReadBoolean();
+
+ IMemoryOwner counts;
+
+ if (isSimpleCode)
+ {
+ Span symbols = stackalloc uint[2];
+ symbols.Clear();
+
+ uint maxSymbol = 0u;
+ uint symCount = reader.ReadBits32(1u) + 1u;
+ for (uint i = 0; i < symCount; i++)
+ {
+ uint symbol = DecodeVariableLengthUint8(reader);
+ if (symbol > maxSymbol)
+ {
+ maxSymbol = symbol;
+ }
+
+ symbols[(int)i] = symbol;
+ }
+
+ // Up to 256 items
+ counts = configuration.MemoryAllocator.Allocate((int)maxSymbol + 1);
+ Span countsSpan = counts.Memory.Span;
+
+ if (symCount == 1)
+ {
+ countsSpan[(int)symbols[0]] = (uint)range;
+ }
+ else
+ {
+ if (symbols[0] == symbols[1])
+ {
+ Debug.Fail("Corrupt data");
+ counts.Dispose();
+ return null;
+ }
+
+ countsSpan[(int)symbols[0]] = reader.ReadBits32((uint)precisionBits);
+ countsSpan[(int)symbols[1]] = (uint)range - countsSpan[(int)symbols[0]];
+ }
+ }
+ else
+ {
+ bool isFlat = reader.ReadBoolean();
+
+ if (isFlat)
+ {
+ uint alphabetSize = DecodeVariableLengthUint8(reader) + 1u;
+ if (alphabetSize <= range)
+ {
+ return null;
+ }
+
+ counts = JxlAnsHelper.CreateFlatHistogram(configuration, (int)alphabetSize, range);
+ return counts;
+ }
+
+ int upperBoundLog = FloorLog2Nonzero(JxlAnsConstants.AnsLogTableSize + 1);
+ int log = 0;
+
+ for (; log < upperBoundLog; log++)
+ {
+ bool logIncrementBit = reader.ReadBoolean();
+
+ if (!logIncrementBit)
+ {
+ break;
+ }
+ }
+
+ uint shift = (reader.ReadBits32((uint)log) | (1u << log)) - 1u;
+
+ if (shift > JxlAnsConstants.AnsLogTableSize + 1)
+ {
+ Debug.Fail("Invalid shift");
+
+ return null;
+ }
+
+ uint length = DecodeVariableLengthUint8(reader) + 3u;
+
+ counts = configuration.MemoryAllocator.Allocate((int)length);
+ Span countsSpan = counts.Memory.Span;
+
+ uint totalCount = 0;
+
+ // The length variable can represent up to 258 elements,
+ // so it'd be more beneficial to allocate on the stack
+ // than pool an array.
+ Span logCounts = stackalloc int[(int)length];
+
+ // stackalloc doesn't zero-init, so clear just in case.
+ logCounts.Clear();
+
+ int omitLog = -1;
+ int omitPos = -1;
+
+ // See comments for logCounts definition
+ Span same = stackalloc int[(int)length];
+ same.Clear();
+
+ for (int i = 0; i < length; i++)
+ {
+ uint index = reader.PeekBits32(7);
+ reader.SkipBits32(HuffmanLookup[index][0]);
+ logCounts[i] = HuffmanLookup[index][1] - 1;
+
+ if (logCounts[i] == JxlAnsConstants.AnsLogTableSize)
+ {
+ uint rleLength = DecodeVariableLengthUint8(reader);
+ same[i] = (int)rleLength + 5;
+ i += (int)rleLength + 3;
+ continue;
+ }
+
+ if (logCounts[i] > omitLog)
+ {
+ omitLog = logCounts[i];
+ omitPos = i;
+ }
+ }
+
+ if (omitPos < 0)
+ {
+ Debug.Fail("The histogram is corrupt or invalid.");
+ counts.Dispose();
+ return null;
+ }
+
+ if (omitPos + 1 < length && logCounts[omitPos + 1] == JxlAnsConstants.AnsLogTableSize)
+ {
+ Debug.Fail("The histogram is corrupt or invalid.");
+ counts.Dispose();
+ return null;
+ }
+
+ int previous = 0;
+ int sameCount = 0;
+
+ for (int i = 0; i < length; i++)
+ {
+ if (same[i] > 0)
+ {
+ sameCount = same[i] - 1;
+ previous = i > 0 ? (int)countsSpan[i - 1] : 0;
+ }
+
+ if (sameCount > 0)
+ {
+ countsSpan[i] = (uint)previous;
+ sameCount--;
+ }
+ else
+ {
+ int code = logCounts[i];
+
+ if (i == omitPos || code < 0)
+ {
+ continue;
+ }
+ else if (shift == 0 || code == 0)
+ {
+ countsSpan[i] = 1u << code;
+ }
+ else
+ {
+ int bitCount = JxlAnsHelper.GetPopulationCountPrecision(code, (int)shift);
+ countsSpan[i] = (1u << code) + (reader.ReadBits32((uint)bitCount) << (code - bitCount));
+ }
+ }
+
+ totalCount += countsSpan[i];
+ }
+
+ countsSpan[omitPos] = (uint)range - totalCount;
+
+ if (countsSpan[omitPos] <= 0)
+ {
+ Debug.Fail("The histogram count is incorrect.");
+ counts.Dispose();
+ return null;
+ }
+ }
+
+ return counts;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsSymbol.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsSymbol.cs
new file mode 100644
index 0000000000..75afa94d9f
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/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;
+
+[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/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs
new file mode 100644
index 0000000000..50753321e7
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs
@@ -0,0 +1,176 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers.Binary;
+using System.Diagnostics;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO;
+
+///
+/// Represents a bitstream reader.
+///
+internal sealed class JxlBitReader(ReadOnlyMemory bytes)
+{
+ private ulong buffer;
+ private uint bufferRemainingBits;
+ private int pointer;
+
+ ///
+ /// Gets a value indicating whether this marks an end of stream.
+ ///
+ public bool IsEndOfStream { get; private set; }
+
+ ///
+ /// Gets the total number of bits consumed.
+ ///
+ public long TotalBitsConsumed => ((long)this.pointer * 8) + (64 - this.bufferRemainingBits);
+
+ ///
+ /// Fetches a new buffer.
+ ///
+ private void RefillCore()
+ {
+ ReadOnlySpan samplesSpan = bytes.Span;
+
+ int remaining = samplesSpan.Length - this.pointer;
+ if (remaining <= 0)
+ {
+ // we don't have any more data... mark an end of stream
+ this.buffer = 0;
+ this.bufferRemainingBits = 0;
+ this.IsEndOfStream = true;
+ return;
+ }
+
+ if (remaining >= 8)
+ {
+ this.buffer = BinaryPrimitives.ReadUInt64LittleEndian(samplesSpan[this.pointer..]);
+ this.bufferRemainingBits = 64u;
+ this.pointer += 8;
+ }
+ else
+ {
+ ulong value = 0;
+ for (int i = 0; i < remaining; i++)
+ {
+ value |= (ulong)samplesSpan[this.pointer + i] << (8 * i);
+ }
+
+ this.buffer = value;
+ this.bufferRemainingBits = (uint)(remaining * 8);
+ this.pointer += remaining;
+ }
+ }
+
+ private void MaybeRefill()
+ {
+ if (this.bufferRemainingBits <= 0)
+ {
+ this.RefillCore();
+ }
+ }
+
+ private ulong ReadBits64Core(uint n, bool peek = false)
+ {
+ Debug.Assert(n <= 64, "Too many bits to pack into ulong");
+ this.MaybeRefill();
+
+ if (this.IsEndOfStream)
+ {
+ JxlThrowHelper.ThrowEndOfStream();
+ }
+
+ if (n <= this.bufferRemainingBits)
+ {
+ ulong result = this.buffer & ((1UL << (int)n) - 1);
+
+ if (!peek)
+ {
+ this.buffer >>= (int)n;
+ this.bufferRemainingBits -= n;
+ }
+
+ return result;
+ }
+ else
+ {
+ uint bitsFromCurrent = this.bufferRemainingBits;
+ ulong part = this.buffer & ((1UL << (int)bitsFromCurrent) - 1);
+
+ this.buffer >>= (int)bitsFromCurrent;
+ this.bufferRemainingBits = 0;
+
+ this.RefillCore();
+
+ uint bitsFromNext = n - bitsFromCurrent;
+ ulong nextPart = this.buffer & ((1UL << (int)bitsFromNext) - 1);
+
+ if (!peek)
+ {
+ this.buffer >>= (int)bitsFromNext;
+ this.bufferRemainingBits -= bitsFromNext;
+ }
+
+ return part | (nextPart << (int)bitsFromCurrent);
+ }
+ }
+
+ private uint ReadBits32Core(uint n, bool peek = false)
+ {
+ Debug.Assert(n <= 32, "Too many bits to pack into uint");
+ this.MaybeRefill();
+
+ if (this.IsEndOfStream)
+ {
+ JxlThrowHelper.ThrowEndOfStream();
+ }
+
+ if (n <= this.bufferRemainingBits)
+ {
+ uint result = (uint)(this.buffer & ((1UL << (int)n) - 1));
+
+ if (!peek)
+ {
+ this.buffer >>= (int)n;
+ this.bufferRemainingBits -= n;
+ }
+
+ return result;
+ }
+ else
+ {
+ uint bitsFromCurrent = this.bufferRemainingBits;
+ uint part = (uint)(this.buffer & ((1UL << (int)bitsFromCurrent) - 1));
+
+ this.buffer >>= (int)bitsFromCurrent;
+ this.bufferRemainingBits = 0;
+
+ this.RefillCore();
+
+ uint bitsFromNext = n - bitsFromCurrent;
+ uint nextPart = (uint)(this.buffer & ((1UL << (int)bitsFromNext) - 1));
+
+ if (!peek)
+ {
+ this.buffer >>= (int)bitsFromNext;
+ this.bufferRemainingBits -= bitsFromNext;
+ }
+
+ return part | (nextPart << (int)bitsFromCurrent);
+ }
+ }
+
+ public uint ReadBits32(uint bits) => this.ReadBits32Core(bits, peek: false);
+
+ public uint PeekBits32(uint bits) => this.ReadBits32Core(bits, peek: true);
+
+ public void SkipBits32(uint bits) => _ = this.ReadBits32(bits);
+
+ public ulong ReadBits64(ulong bits) => this.ReadBits64Core((uint)bits, peek: false);
+
+ public ulong PeekBits64(ulong bits) => this.ReadBits64Core((uint)bits, peek: true);
+
+ public void SkipBits64(ulong bits) => _ = this.ReadBits64(bits);
+
+ public bool ReadBoolean() => this.ReadBits32Core(1, peek: false) == 1;
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlAnimationHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlAnimationHeader.cs
new file mode 100644
index 0000000000..647e8bbc7d
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlAnimationHeader.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
+
+internal sealed class JxlAnimationHeader : IJxlFields
+{
+ public int TpsNumerator { get; set; }
+
+ public int TpsDenominator { get; set; }
+
+ public int LoopCount { get; set; }
+
+ public bool ContainsTimecodes { get; set; }
+
+ public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepth.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepth.cs
new file mode 100644
index 0000000000..0a9db0f575
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepth.cs
@@ -0,0 +1,129 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
+
+///
+/// Represents the JPEG XL Bit Depth image metadata.
+///
+internal sealed class JxlBitDepth : IJxlFields
+{
+ private uint bitsPerSample;
+ private uint exponentBitsPerSample;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public JxlBitDepth() => JxlBundle.Init(this);
+
+ ///
+ /// Gets or sets a value indicating whether
+ /// the original (uncompressed) samples are floating point or
+ /// unsigned integer.
+ ///
+ public bool FloatingPointSample { get; set; }
+
+ ///
+ /// Gets or sets the bit depth of the original (uncompressed) image samples.
+ /// Must be in the range [1, 32].
+ ///
+ public uint BitsPerSample
+ {
+ get => this.bitsPerSample;
+ set => this.bitsPerSample = value;
+ }
+
+ ///
+ ///
+ /// Gets or sets floating point exponent bits of the original (uncompressed) image samples,
+ /// only used if is .
+ ///
+ ///
+ /// If used, the samples are floating point with:
+ ///
+ /// - 1 sign bit
+ /// - exponent bits
+ /// - ( - - 1) mantissa bits
+ ///
+ /// If used, must be in the range
+ /// [2, 8] and amount of mantissa bits must be in the range [2, 23].
+ ///
+ ///
+ public uint ExponentBitsPerSample
+ {
+ get => this.exponentBitsPerSample;
+ set => this.exponentBitsPerSample = value;
+ }
+
+ public bool Visit(JxlVisitor visitor)
+ {
+ if (!this.FloatingPointSample)
+ {
+ bool successful = visitor.U32(
+ JxlFieldExpressions.Value(8u),
+ JxlFieldExpressions.Value(10u),
+ JxlFieldExpressions.Value(12u),
+ JxlFieldExpressions.BitsOffset(6u, 1u),
+ 8u,
+ ref this.bitsPerSample);
+
+ if (!successful)
+ {
+ return false;
+ }
+
+ this.exponentBitsPerSample = 0;
+ }
+ else
+ {
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(32u),
+ JxlFieldExpressions.Value(16u),
+ JxlFieldExpressions.Value(24u),
+ JxlFieldExpressions.BitsOffset(6u, 1u),
+ 32u,
+ ref this.bitsPerSample))
+ {
+ return false;
+ }
+
+ this.exponentBitsPerSample--;
+
+ if (!visitor.Bits(4, 7, ref this.exponentBitsPerSample))
+ {
+ return false;
+ }
+
+ this.exponentBitsPerSample++;
+ }
+
+ if (this.FloatingPointSample)
+ {
+ if (this.exponentBitsPerSample is < 2 or > 8)
+ {
+ DebugGuard.IsTrue(false, "Invalid exponent_bits_per_sample");
+
+ return false;
+ }
+
+ int mantissaBits = (int)this.bitsPerSample - (int)this.exponentBitsPerSample - 1;
+
+ if (mantissaBits is < 2 or > 23)
+ {
+ DebugGuard.IsTrue(false, "Invalid bits_per_sample");
+
+ return false;
+ }
+ }
+ else if (this.bitsPerSample > 31)
+ {
+ DebugGuard.IsTrue(false, "Invalid bits_per_sample");
+
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCodecMetadata.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCodecMetadata.cs
new file mode 100644
index 0000000000..5a3aaeecb7
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCodecMetadata.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
+
+internal sealed class JxlCodecMetadata
+{
+ public JxlImageMetadata? ImageMetadata { get; set; }
+
+ public JxlSizeHeader? Size { get; set; }
+
+ public JxlCustomTransformData? CustomTransformData { get; set; }
+
+ public int XSize => this.Size?.XSize ?? 0;
+
+ public int YSize => this.Size?.YSize ?? 0;
+
+ public int GetOrientedPreviewXSize(bool keepOrientation)
+ {
+ if (this.ImageMetadata!.Orientation > 4 && !keepOrientation)
+ {
+ return this.ImageMetadata.PreviewSize.YSize;
+ }
+
+ return this.ImageMetadata.PreviewSize.XSize;
+ }
+
+ public int GetOrientedPreviewYSize(bool keepOrientation)
+ {
+ if (this.ImageMetadata!.Orientation > 4 && !keepOrientation)
+ {
+ return this.ImageMetadata.PreviewSize.XSize;
+ }
+
+ return this.ImageMetadata.PreviewSize.YSize;
+ }
+
+ public int GetOrientedXSize(bool keepOrientation)
+ {
+ if (this.ImageMetadata!.Orientation > 4 && !keepOrientation)
+ {
+ return this.YSize;
+ }
+
+ return this.XSize;
+ }
+
+ public int GetOrientedYSize(bool keepOrientation)
+ {
+ if (this.ImageMetadata!.Orientation > 4 && !keepOrientation)
+ {
+ return this.XSize;
+ }
+
+ return this.YSize;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs
new file mode 100644
index 0000000000..3b3c6bc200
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
+
+internal sealed class JxlCustomTransformData : IJxlFields
+{
+ public bool NonserializedXybEncoded { get; set; }
+
+ public bool AllDefault { get; set; }
+
+ public JxlOpsinInverseMatrix? OpsinInverseMatrix { get; set; }
+
+ public int CustomWeightsMask { get; set; }
+
+ public InlineArray15 Upsampling2Weights { get; set; }
+
+ public InlineArray55 Upsampling4Weights { get; set; }
+
+ public InlineArray210 Upsampling8Weights { get; set; }
+
+ public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExifOrientation.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExifOrientation.cs
new file mode 100644
index 0000000000..406e7e9a1a
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExifOrientation.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
+
+internal enum JxlExifOrientation : byte
+{
+ Identity = 1,
+ FlipHorizontal = 2,
+ Rotate180 = 3,
+ FlipVertical = 4,
+ Transponse = 5,
+ Rotate90 = 6,
+ AntiTranspose = 7,
+ Rotate270 = 8
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannel.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannel.cs
new file mode 100644
index 0000000000..8e2b842a56
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannel.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
+
+internal enum JxlExtraChannel : byte
+{
+ Alpha,
+ Depth,
+ SpotColor,
+ SelectionMask,
+ Black,
+ Cfa,
+ Thermal,
+ Reserved0,
+ Reserved1,
+ Reserved2,
+ Reserved3,
+ Reserved4,
+ Reserved5,
+ Reserved6,
+ Reserved7,
+ Unknown,
+ Optional
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs
new file mode 100644
index 0000000000..5b492b1026
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
+
+internal sealed class JxlExtraChannelInfo : IJxlFields
+{
+ public bool AllDefault { get; set; }
+
+ public JxlExtraChannel Type { get; set; }
+
+ public JxlBitDepth? BitDepth { get; set; }
+
+ public int DimensionShift { get; set; }
+
+ public string? Name { get; set; }
+
+ public bool AlphaAssociated { get; set; }
+
+ public InlineArray4 SpotColor { get; set; }
+
+ public int CfaChannel { get; set; }
+
+ public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs
new file mode 100644
index 0000000000..d9bc419e97
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs
@@ -0,0 +1,128 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
+
+internal sealed class JxlImageMetadata : IJxlFields
+{
+ public bool AllDefault { get; set; }
+
+ public JxlBitDepth? BitDepth { get; set; }
+
+ public bool Modular16BitBufferSufficient { get; set; } // Otherwise, 32 is
+
+ public bool XybEncoded { get; set; }
+
+ public JxlColorEncoding? ColorEncoding { get; set; }
+
+ public int Orientation { get; set; } = 1;
+
+ public bool HavePreview { get; set; }
+
+ public bool HaveAnimation { get; set; }
+
+ public bool HaveIntrinsicSize { get; set; }
+
+ public JxlSizeHeader IntrinsicSize { get; set; }
+
+ public JxlToneMapping? ToneMapping { get; set; }
+
+ public int ExtraChannelCount { get; set; }
+
+ public List ExtraChannels { get; set; } = [];
+
+ public JxlPreviewHeader PreviewSize { get; set; }
+
+ public JxlAnimationHeader Animation { get; set; }
+
+ public long Extensions { get; set; }
+
+ public bool NonserializedOnlyParseBasicInfos { get; set; }
+
+ public float IntensityTarget
+ {
+ get
+ {
+ float intensityTarget = this.ToneMapping?.IntensityTarget ?? 0f;
+
+ Debug.Assert(intensityTarget != 0f, "Intensity target should be present");
+
+ return intensityTarget;
+ }
+
+ set
+ {
+ if (this.ToneMapping != null)
+ {
+ this.ToneMapping.IntensityTarget = value;
+ }
+ }
+ }
+
+ public int AlphaBits
+ {
+ get
+ {
+ JxlExtraChannelInfo? ec = this.FindExtraChannel(JxlExtraChannel.Alpha);
+
+ if (ec == null)
+ {
+ return 0;
+ }
+
+ return ec.BitDepth?.BitsPerSample ?? 0;
+ }
+
+ set
+ {
+ }
+ }
+
+ public bool HasAlpha => this.AlphaBits != 0;
+
+ public JxlExtraChannelInfo? FindExtraChannel(JxlExtraChannel type)
+ => this.ExtraChannels.FirstOrDefault(eci => eci.Type == type);
+
+ public JxlExifOrientation GetExifOrientation() => (JxlExifOrientation)this.Orientation;
+
+ public void SetFloat16Samples()
+ {
+ if (this.BitDepth != null)
+ {
+ this.BitDepth.BitsPerSample = 16;
+ this.BitDepth.ExponentBitsPerSample = 5;
+ this.BitDepth.FloatingPointSample = true;
+ }
+
+ this.Modular16BitBufferSufficient = false;
+ }
+
+ public void SetFloat32Samples()
+ {
+ if (this.BitDepth != null)
+ {
+ this.BitDepth.BitsPerSample = 32;
+ this.BitDepth.ExponentBitsPerSample = 8;
+ this.BitDepth.FloatingPointSample = true;
+ }
+
+ this.Modular16BitBufferSufficient = false;
+ }
+
+ public void SetUIntSamples(int bits)
+ {
+ if (this.BitDepth != null)
+ {
+ this.BitDepth.BitsPerSample = bits;
+ this.BitDepth.ExponentBitsPerSample = 0;
+ this.BitDepth.FloatingPointSample = false;
+ }
+
+ this.Modular16BitBufferSufficient = bits <= 12;
+ }
+
+ public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs
new file mode 100644
index 0000000000..357ce27c39
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs
@@ -0,0 +1,20 @@
+// 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.Metadata;
+
+internal sealed class JxlOpsinInverseMatrix : IJxlFields
+{
+ public bool AllDefault { get; set; }
+
+ public JxlMatrix3x3F InverseMatrix { get; set; }
+
+ public InlineArray3 OpsinBiases { get; set; }
+
+ public InlineArray4 QuantBiases { get; set; }
+
+ public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlPreviewHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlPreviewHeader.cs
new file mode 100644
index 0000000000..e46763cfeb
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlPreviewHeader.cs
@@ -0,0 +1,68 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
+
+internal sealed class JxlPreviewHeader : IJxlFields
+{
+ private bool div8;
+ private int ySizeDiv8;
+ private int ySize;
+ private int ratio;
+ private int xSizeDiv8;
+ private int xSize;
+
+ public int YSize => this.div8 ? (this.ySizeDiv8 * 8) : this.ySize;
+
+ public int XSize
+ {
+ get
+ {
+ if (this.ratio != 0)
+ {
+ SignedRational signedRational = JxlAspectRatioHelpers.FixedAspectRatios(this.ratio);
+
+ return JxlAspectRatioHelpers.MultiplyTruncate(signedRational, this.YSize);
+ }
+
+ return this.div8 ? (this.xSizeDiv8 * 8) : this.xSize;
+ }
+ }
+
+ public void Set(int x, int y)
+ {
+ if (x == 0 || y == 0)
+ {
+ throw new ArgumentException("Empty preview");
+ }
+
+ this.div8 = ((x % JxlFrameDimensions.BlockDimensions) | (y % JxlFrameDimensions.BlockDimensions)) == 0;
+
+ if (this.div8)
+ {
+ this.ySizeDiv8 = y / 8;
+ }
+ else
+ {
+ this.ySize = y;
+ }
+
+ this.ratio = JxlAspectRatioHelpers.FindAspectRatio(x, y);
+
+ if (this.ratio == 0)
+ {
+ if (this.div8)
+ {
+ this.xSizeDiv8 = x / 8;
+ }
+ else
+ {
+ this.xSize = x;
+ }
+ }
+ }
+
+ public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlSizeHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlSizeHeader.cs
new file mode 100644
index 0000000000..5da30b8a0d
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlSizeHeader.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
+
+internal sealed class JxlSizeHeader : IJxlFields
+{
+ private bool isSmall;
+ private int ySizeDiv8Minus1;
+ private int ySize;
+ private int ratio;
+ private int xSizeDiv8Minus1;
+ private int xSize;
+
+ public int YSize => this.isSmall ? ((this.ySizeDiv8Minus1 + 1) * 8) : this.ySize;
+
+ public int XSize
+ {
+ get
+ {
+ if (this.ratio != 0)
+ {
+ SignedRational aspectRatio = JxlAspectRatioHelpers.FixedAspectRatios(this.ratio);
+
+ return JxlAspectRatioHelpers.MultiplyTruncate(aspectRatio, this.YSize);
+ }
+
+ return this.isSmall ? ((this.xSizeDiv8Minus1 + 1) * 8) : this.xSize;
+ }
+ }
+
+ public void Set(int x, int y)
+ {
+ if (x > int.MaxValue || y > int.MaxValue)
+ {
+ throw new ArgumentException("Image too large");
+ }
+
+ if (x == 0 || y == 0)
+ {
+ throw new ArgumentException("Empty image");
+ }
+
+ this.ratio = JxlAspectRatioHelpers.FindAspectRatio(x, y);
+ this.isSmall = y < 256 && (y % JxlFrameDimensions.BlockDimensions) == 0
+ && (this.ratio != 0 || (x <= 256 && (x % JxlFrameDimensions.BlockDimensions) == 0));
+
+ if (this.isSmall)
+ {
+ this.ySizeDiv8Minus1 = (y / 8) - 1;
+ }
+ else
+ {
+ this.ySize = y;
+ }
+
+ if (this.ratio == 0)
+ {
+ if (this.isSmall)
+ {
+ this.xSizeDiv8Minus1 = (x / 8) - 1;
+ }
+ else
+ {
+ this.xSize = x;
+ }
+ }
+ }
+
+ public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs
new file mode 100644
index 0000000000..9a399dc0b0
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
+
+internal sealed class JxlToneMapping : IJxlFields
+{
+ public bool AllDefault { get; set; }
+
+ public float IntensityTarget { get; set; }
+
+ public float LowerBoundIntensityLevel { get; set; }
+
+ public bool RelativeToMaxDisplay { get; set; }
+
+ public float LinearBelow { get; set; }
+
+ public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
+}
diff --git a/src/ImageSharp/Formats/Jxl/InlineArrays.cs b/src/ImageSharp/Formats/Jxl/InlineArrays.cs
new file mode 100644
index 0000000000..9c19e9266c
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/InlineArrays.cs
@@ -0,0 +1,50 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+
+#pragma warning disable SA1649 // File name should match first type name
+
+namespace SixLabors.ImageSharp.Formats.Jxl;
+
+[InlineArray(3)]
+internal struct InlineArray3
+{
+ private T first;
+}
+
+///
+/// Used by JxlCustomTransformData
+///
+[InlineArray(15)]
+internal struct InlineArray15
+{
+ private T first;
+}
+
+///
+/// Used by JxlCustomTransformData
+///
+[InlineArray(55)]
+internal struct InlineArray55
+{
+ private T first;
+}
+
+///
+/// Used by JxlCustomTransformData
+///
+[InlineArray(210)]
+internal struct InlineArray210
+{
+ private T first;
+}
+
+///
+/// Used by JxlWeightsSeparable5
+///
+[InlineArray(12)]
+internal struct InlineArray12
+{
+ private T first;
+}
diff --git a/src/ImageSharp/Formats/Jxl/JxlAspectRatioHelpers.cs b/src/ImageSharp/Formats/Jxl/JxlAspectRatioHelpers.cs
new file mode 100644
index 0000000000..ee5d1c134b
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/JxlAspectRatioHelpers.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl;
+
+internal static class JxlAspectRatioHelpers
+{
+ private static readonly SignedRational[] Ratios =
+ [
+ new(1, 1),
+ new(12, 10),
+ new(4, 3),
+ new(3, 2),
+ new(16, 9),
+ new(5, 4),
+ new(2, 1)
+ ];
+
+ public static SignedRational FixedAspectRatios(int ratio) => Ratios[ratio - 1];
+
+ public static int FindAspectRatio(int x, int y)
+ {
+ for (int i = 0; i < 7; i++)
+ {
+ if (x == MultiplyTruncate(Ratios[i], y))
+ {
+ return i;
+ }
+ }
+
+ return 0;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int MultiplyTruncate(SignedRational rational, int multiplicand) => (multiplicand * rational.Numerator) / rational.Denominator;
+}
diff --git a/src/ImageSharp/Formats/Jxl/JxlFormat.cs b/src/ImageSharp/Formats/Jxl/JxlFormat.cs
new file mode 100644
index 0000000000..00e7b66eb5
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/JxlFormat.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl;
+
+internal class JxlFormat : IImageFormat
+{
+ public string Name => "JPEG XL";
+
+ public string DefaultMimeType => "image/jxl";
+
+ IEnumerable IImageFormat.MimeTypes => new[] { "image/jxl" };
+
+ IEnumerable IImageFormat.FileExtensions => new[] { "jxl" };
+}
diff --git a/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs b/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs
new file mode 100644
index 0000000000..8a7416a573
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs
@@ -0,0 +1,82 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl;
+
+internal sealed class JxlFrameDimensions
+{
+ public const int BlockDimensions = 8;
+ public const int DctBlockSize = BlockDimensions * BlockDimensions;
+ public const int GroupDimensions = 256;
+ public const int GroupDimensionsInBlocks = GroupDimensions / BlockDimensions;
+
+ public JxlFrameDimensions(int xSizePixel, int ySizePixel, int groupSizeShift, int maxHorizontalShift, int maxVerticalShift, bool modularMode, int upsampling)
+ {
+ this.GroupDimension = (GroupDimensions >> 1) << groupSizeShift;
+ this.DcGroupDimension = this.GroupDimension * BlockDimensions;
+ this.XSizeUpsampled = xSizePixel;
+ this.YSizeUpsampled = ySizePixel;
+ this.XSize = DivCeil(xSizePixel, upsampling);
+ this.YSize = DivCeil(ySizePixel, upsampling);
+ this.XSizeBlocks = DivCeil(this.XSize, BlockDimensions << maxHorizontalShift) << maxHorizontalShift;
+ this.YSizeBlocks = DivCeil(this.YSize, BlockDimensions << maxVerticalShift) << maxVerticalShift;
+ this.XSizePadded = this.XSizeBlocks * BlockDimensions;
+ this.YSizePadded = this.YSizeBlocks * BlockDimensions;
+
+ if (modularMode)
+ {
+ this.XSizePadded = this.XSize;
+ this.YSizePadded = this.YSize;
+ }
+
+ this.XSizeUpsampledPadded = this.XSizePadded * upsampling;
+ this.YSizeUpsampledPadded = this.YSizePadded * upsampling;
+ this.XSizeGroups = DivCeil(this.XSize, GroupDimensions);
+ this.YSizeGroups = DivCeil(this.YSize, GroupDimensions);
+ this.XSizeDcGroups = DivCeil(this.XSizeBlocks, GroupDimensions);
+ this.YSizeDcGroups = DivCeil(this.YSizeBlocks, GroupDimensions);
+ this.NumGroups = this.XSizeGroups * this.YSizeGroups;
+ this.NumDcGroups = this.XSizeDcGroups * this.YSizeDcGroups;
+ }
+
+ public int XSize { get; set; }
+
+ public int YSize { get; set; }
+
+ public int XSizeUpsampled { get; set; }
+
+ public int YSizeUpsampled { get; set; }
+
+ public int XSizeUpsampledPadded { get; set; }
+
+ public int YSizeUpsampledPadded { get; set; }
+
+ public int XSizePadded { get; set; }
+
+ public int YSizePadded { get; set; }
+
+ public int XSizeBlocks { get; set; }
+
+ public int YSizeBlocks { get; set; }
+
+ public int XSizeGroups { get; set; }
+
+ public int YSizeGroups { get; set; }
+
+ public int XSizeDcGroups { get; set; }
+
+ public int YSizeDcGroups { get; set; }
+
+ public int NumGroups { get; set; }
+
+ public int NumDcGroups { get; set; }
+
+ public int GroupDimension { get; set; }
+
+ public int DcGroupDimension { get; set; }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int DivCeil(int x, int y) => x / y;
+}
diff --git a/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs b/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs
new file mode 100644
index 0000000000..a39dfe707c
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace SixLabors.ImageSharp.Formats.Jxl;
+
+internal static class JxlThrowHelper
+{
+ [DoesNotReturn]
+ public static void ThrowEndOfStream() => throw new EndOfStreamException();
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs
new file mode 100644
index 0000000000..63a5c59c9f
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+///
+/// Represents a three-plane, 2D raster image of type .
+///
+internal sealed class JxlImage3B : JxlImage3
+{
+ public JxlImage3B()
+ {
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs
new file mode 100644
index 0000000000..b456dfd9a7
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+///
+/// Represents a three-plane, 2D raster image of type .
+///
+internal sealed class JxlImage3F : JxlImage3
+{
+ public JxlImage3F()
+ {
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs
new file mode 100644
index 0000000000..0d9f6c8202
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+///
+/// Represents a three-plane, 2D raster image of type .
+///
+internal sealed class JxlImage3I : JxlImage3
+{
+ public JxlImage3I()
+ {
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs
new file mode 100644
index 0000000000..00615ff846
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+///
+/// Represents a three-plane, 2D raster image of type .
+///
+internal sealed class JxlImage3S : JxlImage3
+{
+ public JxlImage3S()
+ {
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs
new file mode 100644
index 0000000000..1921e86d33
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+///
+/// Represents a three-plane, 2D raster image of type .
+///
+internal sealed class JxlImage3U : JxlImage3
+{
+ public JxlImage3U()
+ {
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs
new file mode 100644
index 0000000000..0faba189ad
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+///
+/// Represents a single-plane, 2D raster image of type .
+///
+internal sealed class JxlImageB : JxlPlane
+{
+ public JxlImageB()
+ {
+ }
+
+ public JxlImageB(int width, int height)
+ : base(width, height)
+ {
+ }
+
+ public JxlImageB(Configuration configuration, int xSize, int ySize, int prePadding = 0)
+ : base(xSize, ySize)
+ => this.Allocate(configuration, prePadding);
+
+ public Memory GetRowBytesMemory(int y)
+ {
+ Debug.Assert(y < this.YSize, "Attempted to access out-of-bounds Y coordinate");
+
+ Memory row = this.Bytes[(y * this.BytesPerRow)..];
+
+ return row;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageF.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageF.cs
new file mode 100644
index 0000000000..6810e87df9
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageF.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+///
+/// Represents a single-plane, 2D raster image of type .
+///
+internal sealed class JxlImageF : JxlPlane
+{
+ public JxlImageF()
+ {
+ }
+
+ public JxlImageF(int width, int height)
+ : base(width, height)
+ {
+ }
+
+ public JxlImageF(Configuration configuration, int xSize, int ySize, int prePadding = 0)
+ : base(xSize, ySize)
+ => this.Allocate(configuration, prePadding);
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageI.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageI.cs
new file mode 100644
index 0000000000..0261ffd63c
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageI.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+///
+/// Represents a single-plane, 2D raster image of type .
+///
+internal sealed class JxlImageI : JxlPlane
+{
+ public JxlImageI()
+ {
+ }
+
+ public JxlImageI(int width, int height)
+ : base(width, height)
+ {
+ }
+
+ public JxlImageI(Configuration configuration, int xSize, int ySize, int prePadding = 0)
+ : base(xSize, ySize)
+ => this.Allocate(configuration, prePadding);
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageS.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageS.cs
new file mode 100644
index 0000000000..b53716f12f
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageS.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+///
+/// Represents a single-plane, 2D raster image of type .
+///
+internal sealed class JxlImageS : JxlPlane
+{
+ public JxlImageS()
+ {
+ }
+
+ public JxlImageS(int width, int height)
+ : base(width, height)
+ {
+ }
+
+ public JxlImageS(Configuration configuration, int xSize, int ySize, int prePadding = 0)
+ : base(xSize, ySize)
+ => this.Allocate(configuration, prePadding);
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageSB.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageSB.cs
new file mode 100644
index 0000000000..355c50785c
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageSB.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+///
+/// Represents a single-plane, 2D raster image of type .
+///
+internal sealed class JxlImageSB : JxlPlane
+{
+ public JxlImageSB()
+ {
+ }
+
+ public JxlImageSB(int width, int height)
+ : base(width, height)
+ {
+ }
+
+ public JxlImageSB(Configuration configuration, int xSize, int ySize, int prePadding = 0)
+ : base(xSize, ySize)
+ => this.Allocate(configuration, prePadding);
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageU.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageU.cs
new file mode 100644
index 0000000000..d41669e4d1
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageU.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+///
+/// Represents a single-plane, 2D raster image of type .
+///
+internal sealed class JxlImageU : JxlPlane
+{
+ public JxlImageU()
+ {
+ }
+
+ public JxlImageU(int width, int height)
+ : base(width, height)
+ {
+ }
+
+ public JxlImageU(Configuration configuration, int xSize, int ySize, int prePadding = 0)
+ : base(xSize, ySize)
+ => this.Allocate(configuration, prePadding);
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs
new file mode 100644
index 0000000000..37707b7ba4
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs
@@ -0,0 +1,95 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory;
+
+// NOTE: Do not seal this class.
+internal class JxlImage3 : IDisposable
+ where T : unmanaged
+{
+ private const int PlaneCount = 3;
+
+ private JxlPlane[] planes = new JxlPlane[3];
+
+ public JxlImage3()
+ {
+ }
+
+ public JxlImage3(JxlImage3 other)
+ {
+ for (int i = 0; i < PlaneCount; i++)
+ {
+ this.planes[i] = other.planes[i];
+ }
+ }
+
+ public int XSize => this.planes[0].XSize;
+
+ public int YSize => this.planes[0].YSize;
+
+ public int BytesPerRow => this.planes[0].BytesPerRow;
+
+ public int PixelsPerRow => this.planes[0].PixelsPerRow;
+
+ public Span PlaneRow(int plane, int row)
+ {
+ this.PlaneRowBoundsCheck(plane, row);
+
+ int rowOffset = row * this.planes[0].BytesPerRow;
+ Span rowSpan = MemoryMarshal.Cast(this.planes[plane].BytesSpan[rowOffset..]);
+
+ return rowSpan;
+ }
+
+ public JxlPlane Plane(int index) => this.planes[index];
+
+ public void Swap(JxlImage3 other)
+ {
+ for (int i = 0; i < PlaneCount; i++)
+ {
+ other.planes[i].Swap(this.planes[i]);
+ }
+ }
+
+ public static JxlImage3 Create(Configuration configuration, int xSize, int ySize)
+ {
+ JxlPlane plane0 = JxlPlane.Create(configuration, xSize, ySize);
+ JxlPlane plane1 = JxlPlane.Create(configuration, xSize, ySize);
+ JxlPlane plane2 = JxlPlane.Create(configuration, xSize, ySize);
+
+ return new JxlImage3()
+ {
+ planes = [plane0, plane1, plane2]
+ };
+ }
+
+ public bool ShrinkTo(int x, int y)
+ {
+ for (int i = 0; i < PlaneCount; i++)
+ {
+ if (!this.planes[i].ShrinkTo(x, y))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ [Conditional("DEBUG")]
+ private void PlaneRowBoundsCheck(int c, int y) =>
+ Debug.Assert(c < PlaneCount && y < this.YSize, "The bounds check has failed");
+
+ public void Dispose()
+ {
+ foreach (JxlPlane plane in this.planes)
+ {
+ plane.Dispose();
+ }
+
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs
new file mode 100644
index 0000000000..9948ae790f
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs
@@ -0,0 +1,115 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory;
+
+// NOTE: Do not seal this type.
+internal class JxlPlaneBase : IDisposable
+{
+ private IMemoryOwner? bytes;
+
+ public JxlPlaneBase(int xSize, int ySize, int sizeOfT)
+ {
+ this.XSize = xSize;
+ this.YSize = ySize;
+ this.OriginalXSize = xSize;
+ this.OriginalYSize = ySize;
+ this.BytesPerRow = 0;
+ this.Size = sizeOfT;
+ }
+
+ public JxlPlaneBase()
+ : this(0, 0, 0)
+ {
+ }
+
+ public int BytesPerRow { get; private set; }
+
+ public int XSize { get; private set; }
+
+ public int YSize { get; private set; }
+
+ public Memory Bytes =>
+#if DEBUG
+ this.bytes?.Memory ?? throw new InvalidOperationException("Bytes are missing");
+#else
+ return this.bytes!.Memory;
+#endif
+
+ public Span BytesSpan => this.Bytes.Span;
+
+ protected int Size { get; set; }
+
+ protected int OriginalXSize { get; set; }
+
+ protected int OriginalYSize { get; set; }
+
+ public bool Allocate(Configuration configuration, int prePadding)
+ {
+ if (this.bytes != null || this.BytesPerRow != 0)
+ {
+ return false;
+ }
+
+ if (this.XSize == 0 || this.YSize == 0)
+ {
+ return true;
+ }
+
+ int totalBytes = unchecked(this.YSize * this.BytesPerRow);
+
+ this.bytes = configuration.MemoryAllocator.Allocate(totalBytes + (prePadding * this.Size));
+
+ return true;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool ShrinkTo(int x, int y)
+ {
+ if (x <= this.OriginalXSize || y <= this.OriginalYSize)
+ {
+ return false;
+ }
+
+ Debug.Assert(x <= this.OriginalXSize, "ShrinkTo cannot expand memory");
+ Debug.Assert(y <= this.OriginalYSize, "ShrinkTo cannot expand memory");
+
+ this.XSize = x;
+ this.YSize = y;
+
+ return true;
+ }
+
+ protected Span GetRowBase(int y)
+ where T : unmanaged
+ {
+ Debug.Assert(y < this.YSize, "Attempted to access out-of-bounds Y coordinate");
+
+ Span row = this.Bytes.Span[(y * this.BytesPerRow)..];
+
+ return MemoryMarshal.Cast(row);
+ }
+
+ protected void SetBytes(IMemoryOwner bytes) => this.bytes = bytes;
+
+ public void Swap(JxlPlaneBase other)
+ {
+ (this.XSize, other.XSize) = (other.XSize, this.XSize);
+ (this.YSize, other.YSize) = (other.YSize, this.YSize);
+ (this.OriginalXSize, other.OriginalXSize) = (other.OriginalXSize, this.OriginalXSize);
+ (this.OriginalYSize, other.OriginalYSize) = (other.OriginalYSize, this.OriginalYSize);
+ (this.BytesPerRow, other.BytesPerRow) = (other.BytesPerRow, this.BytesPerRow);
+ (this.bytes, other.bytes) = (other.bytes, this.bytes);
+ }
+
+ public void Dispose()
+ {
+ this.bytes?.Dispose();
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs
new file mode 100644
index 0000000000..6bb09c6002
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory;
+
+// NOTE: Do not seal this class.
+internal class JxlPlane : JxlPlaneBase
+ where T : unmanaged
+{
+ public JxlPlane()
+ {
+ }
+
+ public unsafe JxlPlane(int width, int height)
+ : base(width, height, sizeof(T))
+ {
+ }
+
+ public unsafe int PixelsPerRow => this.BytesPerRow / sizeof(T);
+
+ public static JxlPlane Create(Configuration configuration, int xSize, int ySize, int prePadding = 0)
+ {
+ JxlPlane plane = new(xSize, ySize);
+
+ bool allocated = plane.Allocate(configuration, prePadding);
+
+ if (!allocated)
+ {
+ throw new InvalidOperationException("Failed to allocate a JPEG XL plane");
+ }
+
+ return plane;
+ }
+
+ public Span GetRow(int y) => this.GetRowBase(y);
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlSpanHelper.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlSpanHelper.cs
new file mode 100644
index 0000000000..fcf9b61aa5
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/JxlSpanHelper.cs
@@ -0,0 +1,50 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory;
+
+internal static class JxlSpanHelper
+{
+ public static T NthElement(this Span span, int n)
+ where T : IComparable
+ {
+ int left = 0;
+ int right = span.Length - 1;
+
+ while (true)
+ {
+ int pivotIndex = Partition(span, left, right);
+ if (pivotIndex == n)
+ {
+ return span[pivotIndex];
+ }
+ else if (n < pivotIndex)
+ {
+ right = pivotIndex - 1;
+ }
+ else
+ {
+ left = pivotIndex + 1;
+ }
+ }
+ }
+
+ private static int Partition(Span span, int left, int right)
+ where T : IComparable
+ {
+ T pivot = span[right];
+ int storeIndex = left;
+
+ for (int i = left; i < right; i++)
+ {
+ if (span[i].CompareTo(pivot) < 0)
+ {
+ (span[i], span[storeIndex]) = (span[storeIndex], span[i]);
+ storeIndex++;
+ }
+ }
+
+ (span[storeIndex], span[right]) = (span[right], span[storeIndex]);
+ return storeIndex;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/IJxlDctAcImage.cs b/src/ImageSharp/Formats/Jxl/Processing/IJxlDctAcImage.cs
new file mode 100644
index 0000000000..30969824cd
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/IJxlDctAcImage.cs
@@ -0,0 +1,68 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+///
+/// Base DCT AC coefficient image
+///
+internal interface IJxlDctAcImage
+{
+ ///
+ /// Gets the bit width of AC coefficients
+ ///
+ public JxlDctAcType Type { get; }
+
+ ///
+ /// Gets the number of pixels per row.
+ ///
+ public int PixelsPerRow { get; }
+
+ ///
+ /// Gets a value indicating whether the image is empty and doesn't
+ /// have anything within.
+ ///
+ public bool IsEmpty { get; }
+
+ ///
+ /// Returns a reference to the coefficients at the specified row.
+ ///
+ /// The plane index (which channel).
+ /// The row index within plane specified by .
+ /// The X offset at the specified row.
+ ///
+ /// A reference to the coefficients inside the channel
+ /// specified by index , at index of
+ /// the row specified by , with X offset
+ /// specified by .
+ ///
+ public JxlDctAcPointer GetPlaneRow(int channel, int y, int xBase = 0);
+
+ ///
+ /// Returns a reference to the coefficients at the specified row.
+ ///
+ /// The plane index (which channel).
+ /// The row index within plane specified by .
+ /// The X offset at the specified row.
+ ///
+ /// A reference to the coefficients inside the channel
+ /// specified by index , at index of
+ /// the row specified by , with X offset
+ /// specified by .
+ ///
+ ///
+ /// This is a read-only kind of .
+ ///
+ public JxlDctReadOnlyAcPointer GetReadOnlyPlaneRow(int channel, int y, int xBase = 0);
+
+ ///
+ /// Fills all planes with zero.
+ ///
+ public void Clear();
+
+ ///
+ /// Fills everything within the specified plane with zero.
+ ///
+ /// Desired index of the plane.
+ public void Clear(int plane = 0);
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs
new file mode 100644
index 0000000000..88c1e88f7b
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs
@@ -0,0 +1,53 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+#pragma warning disable SA1405 // Debug.Assert should provide message text
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+///
+/// AC context
+///
+internal static class JxlAcContext
+{
+ public const int DctOrderContextStart = 0;
+ public const int NonZeroBuckets = 37;
+ public const int ZeroDensityContextCount = 458;
+ public const int ZeroDensityContextLimit = 474;
+
+ public static ReadOnlySpan CoefficientFrequencyContext =>
+ [
+ 0xBAD, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22,
+ 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26,
+ 27, 27, 27, 27, 28, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30,
+ ];
+
+ public static ReadOnlySpan CoefficientNumNonzeroContext =>
+ [
+ 0xBAD, 0, 31, 62, 62, 93, 93, 93, 93, 123, 123, 123, 123,
+ 152, 152, 152, 152, 152, 152, 152, 152, 180, 180, 180, 180, 180,
+ 180, 180, 180, 180, 180, 180, 180, 206, 206, 206, 206, 206, 206,
+ 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206,
+ 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206,
+ ];
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int ZeroDensityContext(int nonZeroesLeft, int k, int coveredBlocks, int log2CoveredBlocks, int prev)
+ {
+ Debug.Assert((1 << log2CoveredBlocks) == coveredBlocks);
+
+ nonZeroesLeft = (nonZeroesLeft + coveredBlocks - 1) >> log2CoveredBlocks;
+ k >>= log2CoveredBlocks;
+
+ Debug.Assert(k > 0);
+ Debug.Assert(k < 64);
+ Debug.Assert(nonZeroesLeft > 0);
+ Debug.Assert(nonZeroesLeft < 64);
+
+ return ((CoefficientNumNonzeroContext[nonZeroesLeft] + CoefficientFrequencyContext[k]) * 2) + prev;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs
new file mode 100644
index 0000000000..84b2462591
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs
@@ -0,0 +1,188 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using static SixLabors.ImageSharp.Formats.Jxl.JxlFrameDimensions;
+
+#pragma warning disable SA1405 // Debug.Assert should provide message text
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+[StructLayout(LayoutKind.Sequential, Pack = 8)]
+internal struct JxlAcStrategy
+{
+ public const int MaximumCoefficientBlocks = 32;
+ public const int MaximumBlockDimension = BlockDimensions * MaximumCoefficientBlocks;
+ public const int MaximumCoefficientArea = MaximumBlockDimension * MaximumBlockDimension;
+ public const int NumberOfValidStrategies = 27;
+
+ private static readonly int MultiblockBits =
+ GetTypeBit(JxlAcStrategyType.DCT16X16) | GetTypeBit(JxlAcStrategyType.DCT32X32) |
+ GetTypeBit(JxlAcStrategyType.DCT16X8) | GetTypeBit(JxlAcStrategyType.DCT8X16) |
+ GetTypeBit(JxlAcStrategyType.DCT32X8) | GetTypeBit(JxlAcStrategyType.DCT8X32) |
+ GetTypeBit(JxlAcStrategyType.DCT16X32) | GetTypeBit(JxlAcStrategyType.DCT32X16) |
+ GetTypeBit(JxlAcStrategyType.DCT32X64) | GetTypeBit(JxlAcStrategyType.DCT64X32) |
+ GetTypeBit(JxlAcStrategyType.DCT64X64) | GetTypeBit(JxlAcStrategyType.DCT64X128) |
+ GetTypeBit(JxlAcStrategyType.DCT128X64) |
+ GetTypeBit(JxlAcStrategyType.DCT128X128) |
+ GetTypeBit(JxlAcStrategyType.DCT128X256) |
+ GetTypeBit(JxlAcStrategyType.DCT256X128) |
+ GetTypeBit(JxlAcStrategyType.DCT256X256);
+
+ private readonly bool isFirst;
+
+ public JxlAcStrategy(JxlAcStrategyType strategy, bool isFirst)
+ {
+ this.Strategy = strategy;
+ this.isFirst = isFirst;
+
+ Debug.Assert(this.IsMultiblock);
+ }
+
+ public JxlAcStrategy(JxlAcStrategyType strategy)
+ : this(strategy, true)
+ {
+ }
+
+ public JxlAcStrategy(int rawStrategy)
+ : this((JxlAcStrategyType)rawStrategy)
+ {
+ }
+
+ private static ReadOnlySpan CoveredBlocksXLookup =>
+ [
+ 1, 1, 1, 1, 2, 4, 1, 2, 1,
+ 4, 2, 4, 1, 1, 1, 1, 1, 1,
+ 8, 4, 8, 16, 8, 16, 32, 16, 32
+ ];
+
+ private static ReadOnlySpan CoveredBlocksYLookup =>
+ [
+ 1, 1, 1, 1, 2, 4, 2, 1, 4,
+ 1, 4, 2, 1, 1, 1, 1, 1, 1,
+ 8, 8, 4, 16, 16, 8, 32, 32, 16
+ ];
+
+ private static ReadOnlySpan Log2CoveredBlocksLookup =>
+ [
+ 0, 0, 0, 0, 2, 4, 1, 1, 2,
+ 2, 3, 3, 0, 0, 0, 0, 0, 0,
+ 6, 5, 5, 8, 7, 7, 10, 9, 9
+ ];
+
+ public readonly bool IsMultiblock => ((1 << (int)this.Strategy) & MultiblockBits) != 0;
+
+ public readonly int RawStrategy => (int)this.Strategy;
+
+ public readonly int CoveredBlocksX => CoveredBlocksXLookup[(int)this.Strategy];
+
+ public readonly int CoveredBlocksY => CoveredBlocksYLookup[(int)this.Strategy];
+
+ public readonly int Log2CoveredBlocks => Log2CoveredBlocksLookup[(int)this.Strategy];
+
+ public readonly bool IsFirstBlock => this.isFirst;
+
+ public readonly JxlAcStrategyType Strategy { get; }
+
+ public void ComputeNaturalCoefficientOrder(ref int order) => CoefficientOrderAndLookup(this, false, ref order);
+
+ public void ComputeNaturalCoefficientOrderLookup(ref int lookup) => CoefficientOrderAndLookup(this, true, ref lookup);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int GetTypeBit(JxlAcStrategyType type) => 1 << (int)type;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsRawStrategyValid(int rawStrategy) => rawStrategy is < NumberOfValidStrategies and >= 0;
+
+ private static void CoefficientOrderAndLookup(JxlAcStrategy strategy, bool isLookup, Span output)
+ {
+ // TODO: CoefficientLayout
+ // TODO: CeilLog2Nonzero
+ int cx = strategy.CoveredBlocksX;
+ int cy = strategy.CoveredBlocksY;
+
+ CoefficientLayout(ref cx, ref cy);
+
+ int xs = cx / cy;
+ int xsm = xs - 1;
+ int xss = CeilLog2Nonzero(xs);
+ int cur = cx * cy;
+
+ for (int i = 0; i < cx * BlockDimensions; i++)
+ {
+ for (int j = 0; j <= i; j++)
+ {
+ int x = j;
+ int y = i - j;
+
+ if ((i & 1) == 0)
+ {
+ // swap
+ (x, y) = (y, x);
+ }
+
+ if ((y & xsm) != 0)
+ {
+ continue;
+ }
+
+ y >>= xss;
+ int value = 0;
+
+ if (x < cx && y < cy)
+ {
+ value = (y * cx) + x;
+ }
+ else
+ {
+ value = cur++;
+ }
+
+ if (isLookup)
+ {
+ output[((y * cx) * BlockDimensions) + x] = value;
+ }
+ else
+ {
+ output[value] = ((y * cx) * BlockDimensions) + x;
+ }
+ }
+ }
+
+ for (int ip = (cx * BlockDimensions) - 1; ip > 0; ip--)
+ {
+ int i = ip - 1;
+
+ for (int j = 0; j <= i; j++)
+ {
+ int x = (cx * BlockDimensions) - 1 - (i - j);
+ int y = (cx * BlockDimensions) - 1 - j;
+
+ if ((i & 1) != 0)
+ {
+ // swap
+ (x, y) = (y, x);
+ }
+
+ if ((y & xsm) != 0)
+ {
+ continue;
+ }
+
+ y >>= xss;
+ int value = cur++;
+
+ if (isLookup)
+ {
+ output[((y * cx) * BlockDimensions) + x] = value;
+ }
+ else
+ {
+ output[value] = ((y * cx) * BlockDimensions) + x;
+ }
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs
new file mode 100644
index 0000000000..6070db54c9
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs
@@ -0,0 +1,119 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal sealed class JxlAcStrategyImage : IDisposable
+{
+ private const byte Invalid = byte.MaxValue; // 255
+
+ private JxlImageB? layers;
+ private Memory row;
+ private int stride;
+
+ public int XSize => this.layers!.XSize;
+
+ public int YSize => this.layers!.YSize;
+
+ public int PixelsPerRow => this.layers!.PixelsPerRow;
+
+ public JxlAcStrategyRow GetRow(int y, int xPrefix = 0)
+ {
+ ReadOnlyMemory layerRow = this.layers!.GetRowBytesMemory(y);
+ ReadOnlyMemory row = layerRow[xPrefix..];
+
+ return new JxlAcStrategyRow(row);
+ }
+
+ public static JxlAcStrategyImage Create(Configuration memoryManager, int xSize, int ySize)
+ {
+ JxlAcStrategyImage image = new()
+ {
+ layers = new JxlImageB(memoryManager, xSize, ySize)
+ };
+
+ image.row = image.layers.GetRowBytesMemory(0);
+ image.stride = image.layers.PixelsPerRow;
+
+ return image;
+ }
+
+ public int CountBlocks(JxlAcStrategyType type)
+ {
+ int value = 0;
+ int compare = ((int)type << 1) | 1;
+
+ for (int y = 0; y < this.layers!.YSize; y++)
+ {
+ ReadOnlySpan row = this.layers!.GetRow(y);
+
+ for (int x = 0; x < this.layers!.XSize; x++)
+ {
+ if (row[x] == compare)
+ {
+ value++;
+ }
+ }
+ }
+
+ return value;
+ }
+
+ public JxlAcStrategyRow GetRow(in Rectangle rect, int y) => this.GetRow(rect.Y + y, rect.X);
+
+ public bool IsValid(int x, int y) => this.row.Span[(y * this.stride) + x] != Invalid;
+
+ public bool SetNoBoundsChecks(int x, int y, JxlAcStrategyType type, bool check = true)
+ {
+ JxlAcStrategy strategy = new(type);
+ Span rowSpan = this.row.Span;
+ int rawType = (int)type;
+ int rawTypeTimes2 = rawType << 1;
+
+ for (int iy = 0; iy < strategy.CoveredBlocksX; iy++)
+ {
+ for (int ix = 0; ix < strategy.CoveredBlocksX; ix++)
+ {
+ int pos = ((y + iy) * this.stride) + x + ix;
+
+ if (check && rowSpan[pos] != Invalid)
+ {
+ Debug.Fail("Invalid AC strategy. Blocks overlap.");
+
+ return false;
+ }
+
+ rowSpan[pos] = (byte)(rawTypeTimes2 | ((iy | ix) == 0 ? 1 : 0));
+ }
+ }
+
+ return true;
+ }
+
+ public bool Set(int x, int y, JxlAcStrategyType type)
+ {
+#if DEBUG
+ JxlAcStrategy strategy = new(type);
+
+ Debug.Assert(y + strategy.CoveredBlocksY <= this.layers!.YSize, "Invalid range");
+ Debug.Assert(x + strategy.CoveredBlocksX <= this.layers.XSize, "Invalid range");
+#endif
+
+ return this.SetNoBoundsChecks(x, y, type, check: false);
+ }
+
+ public void FillDct8(in Rectangle rect) => this.FillPlane(((int)JxlAcStrategyType.DCT << 1) | 1, this.layers, in rect);
+
+ public void FillDct8() => this.FillDct8(in this.layers.GetRectangle());
+
+ public void FillInvalid() => this.FillImage(Invalid, this.layers);
+
+ public void Dispose()
+ {
+ this.layers?.Dispose();
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs
new file mode 100644
index 0000000000..a99b3654ac
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal sealed class JxlAcStrategyRow
+{
+ private readonly ReadOnlyMemory row;
+
+ public JxlAcStrategyRow(ReadOnlyMemory row) => this.row = row;
+
+ public JxlAcStrategy this[int x]
+ {
+ get
+ {
+ ReadOnlySpan span = this.row.Span;
+
+ Debug.Assert(x * 8 < span.Length, "Too many bytes of memory were requested");
+
+ ref byte first = ref MemoryMarshal.GetReference(span);
+ JxlAcStrategyType strategy = (JxlAcStrategyType)(Unsafe.Add(ref Unsafe.As(ref first), x) >> 1);
+ bool isFirst = Unsafe.Add(ref first, x) != 0;
+
+ return new JxlAcStrategy(strategy, isFirst);
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyType.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyType.cs
new file mode 100644
index 0000000000..e1b935fa2b
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyType.cs
@@ -0,0 +1,67 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal enum JxlAcStrategyType : ushort
+{
+ // Regular block size DCT
+ DCT = 0,
+
+ // Encode pixels without transforming
+ IDENTITY = 1,
+
+ // Use 2-by-2 DCT
+ DCT2X2 = 2,
+
+ // Use 4-by-4 DCT
+ DCT4X4 = 3,
+
+ // Use 16-by-16 DCT
+ DCT16X16 = 4,
+
+ // Use 32-by-32 DCT
+ DCT32X32 = 5,
+
+ // Use 16-by-8 DCT
+ DCT16X8 = 6,
+
+ // Use 8-by-16 DCT
+ DCT8X16 = 7,
+
+ // Use 32-by-8 DCT
+ DCT32X8 = 8,
+
+ // Use 8-by-32 DCT
+ DCT8X32 = 9,
+
+ // Use 32-by-16 DCT
+ DCT32X16 = 10,
+
+ // Use 16-by-32 DCT
+ DCT16X32 = 11,
+
+ // 4x8 and 8x4 DCT
+ DCT4X8 = 12,
+ DCT8X4 = 13,
+
+ // Corner-DCT.
+ AFV0 = 14,
+
+ AFV1 = 15,
+ AFV2 = 16,
+ AFV3 = 17,
+
+ // Larger DCTs
+ DCT64X64 = 18,
+ DCT64X32 = 19,
+ DCT32X64 = 20,
+
+ // No transforms smaller than 64x64 are allowed below.
+ DCT128X128 = 21,
+ DCT128X64 = 22,
+ DCT64X128 = 23,
+ DCT256X256 = 24,
+ DCT256X128 = 25,
+ DCT128X256 = 26
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingInputLayer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingInputLayer.cs
new file mode 100644
index 0000000000..4b49e32c11
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingInputLayer.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal sealed class JxlAlphaBlendingInputLayer
+{
+ public ReadOnlyMemory R { get; set; }
+
+ public ReadOnlyMemory G { get; set; }
+
+ public ReadOnlyMemory B { get; set; }
+
+ public ReadOnlyMemory A { get; set; }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs
new file mode 100644
index 0000000000..c5db8cb8a7
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal sealed class JxlAlphaBlendingOutput
+{
+ public Memory R { get; set; }
+
+ public Memory G { get; set; }
+
+ public Memory B { get; set; }
+
+ public Memory A { get; set; }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs
new file mode 100644
index 0000000000..641205ab21
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs
@@ -0,0 +1,192 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal sealed class JxlAlphaHelper
+{
+ // TODO: SIMD support
+ private const float SmallAlpha = 1f / (1 << 26);
+
+ // Force x to stay within the range of 0 through 1
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static float Clamp(float x) => Math.Clamp(x, 0f, 1f);
+
+ public static void PerformAlphaBlending(
+ JxlAlphaBlendingInputLayer background,
+ JxlAlphaBlendingInputLayer foreground,
+ JxlAlphaBlendingOutput output,
+ int pixelCount,
+ bool alphaIsPremultiplied,
+ bool clamp)
+ {
+ // Store all channels from all parameters into spans, because
+ // creating a Span from Memory is expensive, especially
+ // in a loop.
+ ReadOnlySpan fgR = foreground.R.Span;
+ ReadOnlySpan fgG = foreground.G.Span;
+ ReadOnlySpan fgB = foreground.B.Span;
+ ReadOnlySpan fgA = foreground.A.Span;
+
+ ReadOnlySpan bgR = background.R.Span;
+ ReadOnlySpan bgG = background.G.Span;
+ ReadOnlySpan bgB = background.B.Span;
+ ReadOnlySpan bgA = background.A.Span;
+
+ Span outR = output.R.Span;
+ Span outG = output.G.Span;
+ Span outB = output.B.Span;
+ Span outA = output.A.Span;
+
+ if (alphaIsPremultiplied)
+ {
+ for (int x = 0; x < pixelCount; x++)
+ {
+ float fga = clamp ? Clamp(fgA[x]) : fgA[x];
+ outR[x] = fgR[x] + (bgR[x] * (1f - fga));
+ outG[x] = fgG[x] + (bgG[x] * (1f - fga));
+ outB[x] = fgB[x] + (bgB[x] * (1f - fga));
+ outA[x] = 1f - ((1f - fga) * (1f - bgA[x]));
+ }
+ }
+ else
+ {
+ for (int x = 0; x < pixelCount; x++)
+ {
+ float fga = clamp ? Clamp(fgA[x]) : fgA[x];
+ float newA = 1f - ((1f - fga) * (1f - bgA[x]));
+ float rnewA = newA > 0 ? 1f / newA : 0f;
+ outR[x] = ((fgR[x] * fga) + (bgR[x] * bgA[x] * (1f - fga))) * rnewA;
+ outG[x] = ((fgG[x] * fga) + (bgG[x] * bgA[x] * (1f - fga))) * rnewA;
+ outB[x] = ((fgB[x] * fga) + (bgB[x] * bgA[x] * (1f - fga))) * rnewA;
+ outA[x] = newA;
+ }
+ }
+ }
+
+ public static void PerformAlphaBlending(
+ ReadOnlySpan bg,
+ ReadOnlySpan bga,
+ ReadOnlySpan fg,
+ ReadOnlySpan fga,
+ Span output,
+ int pixelCount,
+ bool alphaIsPremultiplied,
+ bool clamp)
+ {
+ if (bg == bga && fg == fga)
+ {
+ for (int x = 0; x < pixelCount; x++)
+ {
+ float fa = clamp ? Clamp(fga[x]) : fga[x];
+ output[x] = 1f - ((1f - fa) * (1f - bga[x]));
+ }
+ }
+ else
+ {
+ if (alphaIsPremultiplied)
+ {
+ for (int x = 0; x < pixelCount; x++)
+ {
+ float fa = clamp ? Clamp(fga[x]) : fga[x];
+ output[x] = fg[x] + (bg[x] * (1f - fa));
+ }
+ }
+ else
+ {
+ for (int x = 0; x < pixelCount; x++)
+ {
+ float fa = clamp ? Clamp(fga[x]) : fga[x];
+ float new_a = 1f - ((1f - fa) * (1f - bga[x]));
+ float rnew_a = new_a > 0 ? 1f / new_a : 0f;
+ output[x] = ((fg[x] * fa) + (bg[x] * bga[x] * (1f - fa))) * rnew_a;
+ }
+ }
+ }
+ }
+
+ public static void PerformAlphaWeightedAdd(
+ ReadOnlySpan bg,
+ ReadOnlySpan fg,
+ ReadOnlySpan fga,
+ Span output,
+ int pixelCount,
+ bool clamp)
+ {
+ if (fg == fga)
+ {
+ bg[pixelCount..].CopyTo(output);
+ }
+ else if (clamp)
+ {
+ for (int x = 0; x < pixelCount; x++)
+ {
+ output[x] = bg[x] + (fg[x] * Clamp(fga[x]));
+ }
+ }
+ else
+ {
+ for (int x = 0; x < pixelCount; ++x)
+ {
+ output[x] = bg[x] + (fg[x] * fga[x]);
+ }
+ }
+ }
+
+ public static void PerformMultiplyBlending(
+ ReadOnlySpan bg,
+ ReadOnlySpan fg,
+ Span output,
+ int pixelCount,
+ bool clamp)
+ {
+ if (clamp)
+ {
+ for (int x = 0; x < pixelCount; x++)
+ {
+ output[x] = bg[x] * Clamp(fg[x]);
+ }
+ }
+ else
+ {
+ for (int x = 0; x < pixelCount; x++)
+ {
+ output[x] = bg[x] * fg[x];
+ }
+ }
+ }
+
+ public static void PremultiplyAlpha(
+ Span r,
+ Span g,
+ Span b,
+ ReadOnlySpan a,
+ int pixelCount)
+ {
+ for (int x = 0; x < pixelCount; x++)
+ {
+ float multiplier = Math.Max(SmallAlpha, a[x]);
+ r[x] *= multiplier;
+ g[x] *= multiplier;
+ b[x] *= multiplier;
+ }
+ }
+
+ public static void UnpremultiplyAlpha(
+ Span r,
+ Span g,
+ Span b,
+ ReadOnlySpan a,
+ int pixelCount)
+ {
+ for (int x = 0; x < pixelCount; x++)
+ {
+ float multiplier = 1f / Math.Max(SmallAlpha, a[x]);
+ r[x] *= multiplier;
+ g[x] *= multiplier;
+ b[x] *= multiplier;
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs
new file mode 100644
index 0000000000..2642a2295f
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs
@@ -0,0 +1,71 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal sealed class JxlBlockContextMap
+{
+ public JxlBlockContextMap()
+ {
+ DefaultContextMap.CopyTo(this.ContextMap);
+ this.ContextCount = this.ContextMap.Max() + 1;
+ this.DcContextCount = 1;
+ }
+
+ private static ReadOnlySpan DefaultContextMap =>
+ [
+ 0, 1, 2, 2, 3, 3, 4, 5, 6, 6, 6, 6, 6,
+ 7, 8, 9, 9, 10, 11, 12, 13, 14, 14, 14, 14, 14,
+ 7, 8, 9, 9, 10, 11, 12, 13, 14, 14, 14, 14, 14,
+ ];
+
+ public List[] DcThresholds { get; } = [[], [], []];
+
+ public List QfThresholds { get; } = [];
+
+ public byte[] ContextMap { get; } = new byte[DefaultContextMap.Length];
+
+ public int ContextCount { get; set; }
+
+ public int DcContextCount { get; set; }
+
+ public int AcContextCount => (this.ContextCount * JxlAcContext.NonZeroBuckets) + JxlAcContext.ZeroDensityContextCount;
+
+ public int Context(int dcIndex, uint qf, int ord, int c)
+ {
+ int qfIndex = 0;
+ for (int i = 0; i < this.QfThresholds.Count; i++)
+ {
+ uint t = this.QfThresholds[i];
+
+ if (qf > t)
+ {
+ qfIndex++;
+ }
+ }
+
+ int idx = c < 2 ? c ^ 1 : 2;
+ idx = (idx * JxlForwardCoefficientOrder.OrderCount) + ord;
+ idx = (idx * (this.QfThresholds.Count + 1)) + qfIndex;
+ idx = (idx * this.DcContextCount) + dcIndex;
+ return this.ContextMap[idx];
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int ZeroDensityContextOffset(int blockContext) =>
+ (this.ContextCount * JxlAcContext.NonZeroBuckets) + JxlAcContext.ZeroDensityContextCount + blockContext;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int NonZeroContext(int nonZeroes, int blockContext)
+ {
+ if (nonZeroes >= 64)
+ {
+ nonZeroes = 64;
+ }
+
+ int ctx = nonZeroes < 8 ? nonZeroes : (4 + (nonZeroes / 2));
+ return (ctx * this.ContextCount) + blockContext;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs
new file mode 100644
index 0000000000..2052daee6e
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs
@@ -0,0 +1,63 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal sealed class JxlDctAcImage : IJxlDctAcImage, IDisposable
+ where T : unmanaged
+{
+ private readonly JxlImage3 image;
+
+ public unsafe JxlDctAcImage(Configuration configuration, int width, int height)
+ {
+ DebugGuard.IsTrue(sizeof(T) is 2 or 4, "The type must be 2 or 4 bytes");
+
+ this.image = JxlImage3.Create(configuration, width, height);
+ }
+
+ public unsafe JxlDctAcType Type => sizeof(T) == 4 ? JxlDctAcType.Ac32 : JxlDctAcType.Ac16;
+
+ public int PixelsPerRow => this.image.PixelsPerRow;
+
+ public bool IsEmpty => this.image.XSize == 0 || this.image.YSize == 0;
+
+ public void Clear() => JxlImageOperations.ClearImage(this.image);
+
+ public void Clear(int plane = 0) => JxlImageOperations.ClearImage(this.image);
+
+ public unsafe JxlDctAcPointer GetPlaneRow(int channel, int y, int xBase = 0)
+ {
+ if (sizeof(T) == 4)
+ {
+ Span span = (this.image as JxlImage3)!.PlaneRow(channel, y)[xBase..];
+ return new JxlDctAcPointer() { Pointer32 = span };
+ }
+ else
+ {
+ Span span = (this.image as JxlImage3)!.PlaneRow(channel, y)[xBase..];
+ return new JxlDctAcPointer() { Pointer16 = span };
+ }
+ }
+
+ public unsafe JxlDctReadOnlyAcPointer GetReadOnlyPlaneRow(int channel, int y, int xBase = 0)
+ {
+ if (sizeof(T) == 4)
+ {
+ ReadOnlySpan span = (this.image as JxlImage3)!.PlaneRow(channel, y)[xBase..];
+ return new JxlDctReadOnlyAcPointer(span);
+ }
+ else
+ {
+ ReadOnlySpan span = (this.image as JxlImage3)!.PlaneRow(channel, y)[xBase..];
+ return new JxlDctReadOnlyAcPointer(span);
+ }
+ }
+
+ public void Dispose()
+ {
+ this.image.Dispose();
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcPointer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcPointer.cs
new file mode 100644
index 0000000000..9710712d77
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcPointer.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+///
+/// Pointer to DCT AC coefficients
+///
+internal ref struct JxlDctAcPointer()
+{
+ ///
+ /// 16-bit pointer (if any)
+ ///
+ public Span Pointer16 = [];
+
+ ///
+ /// 32-bit pointer (if any)
+ ///
+ public Span Pointer32 = [];
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcType.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcType.cs
new file mode 100644
index 0000000000..a7e4ede67f
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcType.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+///
+/// Bit size for DCT AC coefficient
+///
+internal enum JxlDctAcType : byte
+{
+ ///
+ /// 16-bit coefficient
+ ///
+ Ac16,
+
+ ///
+ /// 32-bit coefficient
+ ///
+ Ac32
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctReadOnlyAcPointer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctReadOnlyAcPointer.cs
new file mode 100644
index 0000000000..c80d8eaa64
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctReadOnlyAcPointer.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+///
+/// Pointer to DCT AC coefficients. (Read-only)
+///
+internal readonly ref struct JxlDctReadOnlyAcPointer
+{
+ ///
+ /// 16-bit pointer (if any)
+ ///
+ public readonly ReadOnlySpan Pointer16;
+
+ ///
+ /// 32-bit pointer (if any)
+ ///
+ public readonly ReadOnlySpan Pointer32;
+
+ internal JxlDctReadOnlyAcPointer(ReadOnlySpan pointer16) => this.Pointer16 = pointer16;
+
+ internal JxlDctReadOnlyAcPointer(ReadOnlySpan pointer32) => this.Pointer32 = pointer32;
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs
new file mode 100644
index 0000000000..84e7819bc5
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs
@@ -0,0 +1,361 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+///
+/// Read-only cosine lookups for the Discrete Cosine Transform (DCT),
+/// a mathematical function used for quantization.
+///
+internal static class JxlDctScales
+{
+ ///
+ /// Gets 8x1 DCT resample scales.
+ ///
+ public static ReadOnlySpan ResampleScales8_1 =>
+ [
+ 1.000000000000000000f
+ ];
+
+ ///
+ /// Gets 16x2 DCT resample scales.
+ ///
+ public static ReadOnlySpan ResampleScales16_2 =>
+ [
+ 1.000000000000000000f,
+ 0.901764195028874394f,
+ ];
+
+ ///
+ /// Gets 32x4 DCT resample scales.
+ ///
+ public static ReadOnlySpan ResampleScales32_4 =>
+ [
+ 1.000000000000000000f,
+ 0.974886821136879522f,
+ 0.901764195028874394f,
+ 0.787054918159101335f,
+ ];
+
+ ///
+ /// Gets 64x8 DCT resample scales.
+ ///
+ public static ReadOnlySpan ResampleScales64_8 =>
+ [
+ 1.0000000000000000f,
+ 0.9936866130906366f,
+ 0.9748868211368796f,
+ 0.9440180941651672f,
+ 0.9017641950288744f,
+ 0.8490574973847023f,
+ 0.7870549181591013f,
+ 0.7171081282466044f,
+ ];
+
+ ///
+ /// Gets 128x16 DCT resample scales.
+ ///
+ public static ReadOnlySpan ResampleScales128_16 =>
+ [
+ 1.0f,
+ 0.9984194528776054f,
+ 0.9936866130906366f,
+ 0.9858278282666936f,
+ 0.9748868211368796f,
+ 0.9609244059440204f,
+ 0.9440180941651672f,
+ 0.9242615922757944f,
+ 0.9017641950288744f,
+ 0.8766500784429904f,
+ 0.8490574973847023f,
+ 0.8191378932865928f,
+ 0.7870549181591013f,
+ 0.7529833816270532f,
+ 0.7171081282466044f,
+ 0.6796228528314651f,
+ ];
+
+ ///
+ /// Gets 256x32 DCT resample scales.
+ ///
+ public static ReadOnlySpan ResampleScales256_32 =>
+ [
+ 1.0f,
+ 0.9996047255830407f,
+ 0.9984194528776054f,
+ 0.9964458326264695f,
+ 0.9936866130906366f,
+ 0.9901456355893141f,
+ 0.9858278282666936f,
+ 0.9807391980963174f,
+ 0.9748868211368796f,
+ 0.9682788310563117f,
+ 0.9609244059440204f,
+ 0.9528337534340876f,
+ 0.9440180941651672f,
+ 0.9344896436056892f,
+ 0.9242615922757944f,
+ 0.913348084400198f,
+ 0.9017641950288744f,
+ 0.8895259056651056f,
+ 0.8766500784429904f,
+ 0.8631544288990163f,
+ 0.8490574973847023f,
+ 0.8343786191696513f,
+ 0.8191378932865928f,
+ 0.8033561501721485f,
+ 0.7870549181591013f,
+ 0.7702563888779096f,
+ 0.7529833816270532f,
+ 0.7352593067735488f,
+ 0.7171081282466044f,
+ 0.6985543251889097f,
+ 0.6796228528314651f,
+ 0.6603391026591464f,
+ ];
+
+ ///
+ /// Gets 1x8 DCT resample scales.
+ ///
+ public static ReadOnlySpan ResampleScales1_8 =>
+ [
+ 1.000000000000000000f
+ ];
+
+ ///
+ /// Gets 2x16 DCT resample scales.
+ ///
+ public static ReadOnlySpan ResampleScales2_16 =>
+ [
+ 1.000000000000000000f,
+ 1.108937353592731823f,
+ ];
+
+ ///
+ /// Gets 4x32 DCT resample scales.
+ ///
+ public static ReadOnlySpan ResampleScales4_32 =>
+ [
+ 1.000000000000000000f,
+ 1.025760096781116015f,
+ 1.108937353592731823f,
+ 1.270559368765487251f,
+ ];
+
+ ///
+ /// Gets 8x64 DCT resample scales.
+ ///
+ public static ReadOnlySpan ResampleScales8_64 =>
+ [
+ 1.0000000000000000f,
+ 1.0063534990068217f,
+ 1.0257600967811158f,
+ 1.0593017296817173f,
+ 1.1089373535927318f,
+ 1.1777765381970435f,
+ 1.2705593687654873f,
+ 1.3944898413647777f,
+ ];
+
+ ///
+ /// Gets 16x128 DCT resample scales.
+ ///
+ public static ReadOnlySpan ResampleScales16_128 =>
+ [
+ 1.0f,
+ 1.0015830492062623f,
+ 1.0063534990068217f,
+ 1.0143759095928793f,
+ 1.0257600967811158f,
+ 1.0406645869480142f,
+ 1.0593017296817173f,
+ 1.0819447744633812f,
+ 1.1089373535927318f,
+ 1.1407059950032632f,
+ 1.1777765381970435f,
+ 1.2207956782315876f,
+ 1.2705593687654873f,
+ 1.3280505578213306f,
+ 1.3944898413647777f,
+ 1.4714043176061107f,
+ ];
+
+ ///
+ /// Gets 32x256 DCT resample scales.
+ ///
+ public static ReadOnlySpan ResampleScales32_256 =>
+ [
+ 1.0f,
+ 1.0003954307206069f,
+ 1.0015830492062623f,
+ 1.0035668445360069f,
+ 1.0063534990068217f,
+ 1.009952439375063f,
+ 1.0143759095928793f,
+ 1.0196390660647288f,
+ 1.0257600967811158f,
+ 1.0327603660498115f,
+ 1.0406645869480142f,
+ 1.049501024072585f,
+ 1.0593017296817173f,
+ 1.0701028169146336f,
+ 1.0819447744633812f,
+ 1.0948728278734026f,
+ 1.1089373535927318f,
+ 1.124194353004584f,
+ 1.1407059950032632f,
+ 1.158541237256391f,
+ 1.1777765381970435f,
+ 1.1984966740820495f,
+ 1.2207956782315876f,
+ 1.244777922949508f,
+ 1.2705593687654873f,
+ 1.2982690107339132f,
+ 1.3280505578213306f,
+ 1.3600643892400104f,
+ 1.3944898413647777f,
+ 1.4315278911623237f,
+ 1.4714043176061107f,
+ 1.5143734423314616f,
+ ];
+
+ ///
+ /// Gets the DCT multiplier constants
+ ///
+ public static ReadOnlySpan Multipliers4 =>
+ [
+ 0.541196100146197f,
+ 1.3065629648763764f,
+ ];
+
+ ///
+ /// Gets the DCT multiplier constants
+ ///
+ public static ReadOnlySpan Multipliers8 =>
+ [
+ 0.5097955791041592f,
+ 0.6013448869350453f,
+ 0.8999762231364156f,
+ 2.5629154477415055f,
+ ];
+
+ ///
+ /// Gets the DCT multiplier constants
+ ///
+ public static ReadOnlySpan Multipliers16 =>
+ [
+ 0.5024192861881557f, 0.5224986149396889f, 0.5669440348163577f,
+ 0.6468217833599901f, 0.7881546234512502f, 1.060677685990347f,
+ 1.7224470982383342f, 5.101148618689155f,
+ ];
+
+ ///
+ /// Gets the DCT multiplier constants
+ ///
+ public static ReadOnlySpan Multipliers32 =>
+ [
+ 0.5006029982351963f, 0.5054709598975436f, 0.5154473099226246f,
+ 0.5310425910897841f, 0.5531038960344445f, 0.5829349682061339f,
+ 0.6225041230356648f, 0.6748083414550057f, 0.7445362710022986f,
+ 0.8393496454155268f, 0.9725682378619608f, 1.1694399334328847f,
+ 1.4841646163141662f, 2.057781009953411f, 3.407608418468719f,
+ 10.190008123548033f,
+ ];
+
+ ///
+ /// Gets the DCT multiplier constants
+ ///
+ public static ReadOnlySpan Multipliers64 =>
+ [
+ 0.500150636020651f, 0.5013584524464084f, 0.5037887256810443f,
+ 0.5074711720725553f, 0.5124514794082247f, 0.5187927131053328f,
+ 0.52657731515427f, 0.535909816907992f, 0.5469204379855088f,
+ 0.5597698129470802f, 0.57465518403266f, 0.5918185358574165f,
+ 0.6115573478825099f, 0.6342389366884031f, 0.6603198078137061f,
+ 0.6903721282002123f, 0.7251205223771985f, 0.7654941649730891f,
+ 0.8127020908144905f, 0.8683447152233481f, 0.9345835970364075f,
+ 1.0144082649970547f, 1.1120716205797176f, 1.233832737976571f,
+ 1.3892939586328277f, 1.5939722833856311f, 1.8746759800084078f,
+ 2.282050068005162f, 2.924628428158216f, 4.084611078129248f,
+ 6.796750711673633f, 20.373878167231453f,
+ ];
+
+ ///
+ /// Gets the DCT multiplier constants
+ ///
+ public static ReadOnlySpan Multipliers128 =>
+ [
+ 0.5000376519155477f, 0.5003390374428216f, 0.5009427176380873f,
+ 0.5018505174842379f, 0.5030651913013697f, 0.5045904432216454f,
+ 0.5064309549285542f, 0.5085924210498143f, 0.5110815927066812f,
+ 0.5139063298475396f, 0.5170756631334912f, 0.5205998663018917f,
+ 0.524490540114724f, 0.5287607092074876f, 0.5334249333971333f,
+ 0.538499435291984f, 0.5440022463817783f, 0.549953374183236f,
+ 0.5563749934898856f, 0.5632916653417023f, 0.5707305880121454f,
+ 0.5787218851348208f, 0.5872989370937893f, 0.5964987630244563f,
+ 0.606362462272146f, 0.6169357260050706f, 0.6282694319707711f,
+ 0.6404203382416639f, 0.6534518953751283f, 0.6674352009263413f,
+ 0.6824501259764195f, 0.6985866506472291f, 0.7159464549705746f,
+ 0.7346448236478627f, 0.7548129391165311f, 0.776600658233963f,
+ 0.8001798956216941f, 0.8257487738627852f, 0.8535367510066064f,
+ 0.8838110045596234f, 0.9168844461846523f, 0.9531258743921193f,
+ 0.9929729612675466f, 1.036949040910389f, 1.0856850642580145f,
+ 1.1399486751015042f, 1.2006832557294167f, 1.2690611716991191f,
+ 1.346557628206286f, 1.4350550884414341f, 1.5369941008524954f,
+ 1.6555965242641195f, 1.7952052190778898f, 1.961817848571166f,
+ 2.163957818751979f, 2.4141600002500763f, 2.7316450287739396f,
+ 3.147462191781909f, 3.7152427383269746f, 4.5362909369693565f,
+ 5.827688377844654f, 8.153848602466814f, 13.58429025728446f,
+ 40.744688103351834f,
+ ];
+
+ ///
+ /// Gets the DCT multiplier constants
+ ///
+ public static ReadOnlySpan Multipliers256 =>
+ [
+ 0.5000094125358878f, 0.500084723455784f, 0.5002354020255269f,
+ 0.5004615618093246f, 0.5007633734146156f, 0.5011410648064231f,
+ 0.5015949217281668f, 0.502125288230386f, 0.5027325673091954f,
+ 0.5034172216566842f, 0.5041797745258774f, 0.5050208107132756f,
+ 0.5059409776624396f, 0.5069409866925212f, 0.5080216143561264f,
+ 0.509183703931388f, 0.5104281670536573f, 0.5117559854927805f,
+ 0.5131682130825206f, 0.5146659778093218f, 0.516250484068288f,
+ 0.5179230150949777f, 0.5196849355823947f, 0.5215376944933958f,
+ 0.5234828280796439f, 0.52552196311921f, 0.5276568203859896f,
+ 0.5298892183652453f, 0.5322210772308335f, 0.5346544231010253f,
+ 0.537191392591309f, 0.5398342376841637f, 0.5425853309375497f,
+ 0.545447171055775f, 0.5484223888484947f, 0.551513753605893f,
+ 0.554724179920619f, 0.5580567349898085f, 0.5615146464335654f,
+ 0.5651013106696203f, 0.5688203018875696f, 0.5726753816701664f,
+ 0.5766705093136241f, 0.5808098529038624f, 0.5850978012111273f,
+ 0.58953897647151f, 0.5941382481306648f, 0.5989007476325463f,
+ 0.6038318843443582f, 0.6089373627182432f, 0.614223200800649f,
+ 0.6196957502119484f, 0.6253617177319102f, 0.6312281886412079f,
+ 0.6373026519855411f, 0.6435930279473415f, 0.6501076975307724f,
+ 0.6568555347890955f, 0.6638459418498757f, 0.6710888870233562f,
+ 0.6785949463131795f, 0.6863753486870501f, 0.6944420255086364f,
+ 0.7028076645818034f, 0.7114857693151208f, 0.7204907235796304f,
+ 0.7298378629074134f, 0.7395435527641373f, 0.749625274727372f,
+ 0.7601017215162176f, 0.7709929019493761f, 0.7823202570613161f,
+ 0.7941067887834509f, 0.8063772028037925f, 0.8191580674598145f,
+ 0.83247799080191f, 0.8463678182968619f, 0.860860854031955f,
+ 0.8759931087426972f, 0.8918035785352535f, 0.9083345588266809f,
+ 0.9256319988042384f, 0.9437459026371479f, 0.962730784794803f,
+ 0.9826461881778968f, 1.0035572754078206f, 1.0255355056139732f,
+ 1.048659411496106f, 1.0730154944316674f, 1.0986992590905857f,
+ 1.1258164135986009f, 1.1544842669978943f, 1.184833362908442f,
+ 1.217009397314603f, 1.2511754798461228f, 1.287514812536712f,
+ 1.326233878832723f, 1.3675662599582539f, 1.411777227500661f,
+ 1.459169302866857f, 1.5100890297227016f, 1.5649352798258847f,
+ 1.6241695131835794f, 1.6883285509131505f, 1.7580406092704062f,
+ 1.8340456094306077f, 1.9172211551275689f, 2.0086161135167564f,
+ 2.1094945286246385f, 2.22139377701127f, 2.346202662531156f,
+ 2.486267909203593f, 2.644541877144861f, 2.824791402350551f,
+ 3.0318994541759925f, 3.2723115884254845f, 3.5547153325075804f,
+ 3.891107790700307f, 4.298537526449054f, 4.802076008665048f,
+ 5.440166215091329f, 6.274908408039339f, 7.413566756422303f,
+ 9.058751453879703f, 11.644627325175037f, 16.300023088031555f,
+ 27.163977662448232f, 81.48784219222516f,
+ ];
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlForwardCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlForwardCoefficientOrder.cs
new file mode 100644
index 0000000000..26a210e6fd
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlForwardCoefficientOrder.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal static class JxlForwardCoefficientOrder
+{
+ public const byte OrderCount = 13;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int CoefficientRows(int rows, int columns) => rows < columns ? rows : columns;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int CoefficientColumns(int rows, int columns) => rows < columns ? columns : rows;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CoefficientLayout(ref int rows, ref int columns)
+ {
+ rows = CoefficientRows(rows, columns);
+ columns = CoefficientColumns(rows, columns);
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs
new file mode 100644
index 0000000000..095e936d32
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs
@@ -0,0 +1,106 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal static class JxlLehmerCode
+{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int ValueOfLowest1Bit(int n) => n & -n;
+
+ public static bool ComputeLehmerCode(ReadOnlySpan permutation, Span temp, int n, Span code)
+ {
+ temp[(n + 1)..].Clear();
+
+ for (int idx = 0; idx < n; idx++)
+ {
+ int s = permutation[idx];
+
+ uint penalty = 0u;
+ uint i = (uint)s + 1u;
+
+ while (i != 0u)
+ {
+ penalty += temp[(int)i];
+ i &= i - 1u; // Clear lowest bit
+ }
+
+ if (s < penalty)
+ {
+ return false;
+ }
+
+ code[idx] = (uint)s - penalty;
+ i = (uint)s + 1u;
+
+ while (i < n + 1u)
+ {
+ temp[(int)i]++;
+ i += (uint)ValueOfLowest1Bit((int)i);
+ }
+ }
+
+ return true;
+ }
+
+ public static bool DecodeLehmerCode(ReadOnlySpan code, Span temp, int n, Span permutation)
+ {
+ if (n == 0)
+ {
+ return false;
+ }
+
+ int log2n = CeilLog2Nonzero(n);
+ int paddedN = 1 << log2n;
+
+ for (int i = 0; i < paddedN; i++)
+ {
+ int i1 = i + 1;
+ temp[i] = (uint)ValueOfLowest1Bit(i1);
+ }
+
+ for (int i = 0; i < n; i++)
+ {
+ if (code[i] + i >= n)
+ {
+ return false;
+ }
+
+ uint rank = code[i] + 1;
+
+ int bit = paddedN;
+ int next = 0;
+
+ for (int b = 0; b <= log2n; b++)
+ {
+ int cand = next + bit;
+
+ if (cand < 1)
+ {
+ return false;
+ }
+
+ bit >>= 1;
+
+ if (temp[cand - 1] < rank)
+ {
+ next = cand;
+ rank -= temp[cand - 1];
+ }
+ }
+
+ permutation[i] = next;
+
+ next++;
+ while (next <= paddedN)
+ {
+ temp[next - 1]--;
+ next += ValueOfLowest1Bit(next);
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs
new file mode 100644
index 0000000000..312138db43
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs
@@ -0,0 +1,263 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+///
+/// JPEG XL loop filter.
+///
+internal sealed class JxlLoopFilter : IJxlFields
+{
+ private const int SigmaBorder = 1;
+ private const int SigmaPadding = 2;
+
+ ///
+ /// 4 * (sqrt(0.5)-1), so that Weight(sigma) = 0.5
+ ///
+ private const float InverseSigmaNum = -1.1715728752538099024f;
+
+ ///
+ /// kInvSigmaNum / 0.3
+ ///
+ private const float MinSigma = -3.90524291751269967465540850526868f;
+
+ ///
+ /// Gets the number of EPF (Edge-preserving filter) sharp entries.
+ ///
+ public const int EpfSharpEntries = 8;
+
+ ///
+ /// Gets or sets a value indicating whether gaborish
+ /// convolution is preferred.
+ ///
+ public bool UseGaborishConvolution { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether custom
+ /// gaborish weights are used.
+ ///
+ public bool GaborishCustom { get; set; }
+
+ ///
+ /// Gets or sets the first custom X gaborish weight.
+ ///
+ public float GaborishXWeight1 { get; set; }
+
+ ///
+ /// Gets or sets the second custom X gaborish weight.
+ ///
+ public float GaborishXWeight2 { get; set; }
+
+ ///
+ /// Gets or sets the first custom Y gaborish weight.
+ ///
+ public float GaborishYWeight1 { get; set; }
+
+ ///
+ /// Gets or sets the second custom Y gaborish weight.
+ ///
+ public float GaborishYWeight2 { get; set; }
+
+ ///
+ /// Gets or sets the first custom B gaborish weight.
+ ///
+ public float GaborishBWeight1 { get; set; }
+
+ ///
+ /// Gets or sets the second custom B gaborish weight.
+ ///
+ public float GaborishBWeight2 { get; set; }
+
+ ///
+ /// Gets or sets the number of EPF (Edge-preserving filter) steps.
+ /// 0 means EPF is disabled, 1 applies only the first stage,
+ /// 2 applies both stages and 3 applies the first stage twice
+ /// and the second stage once.
+ ///
+ public int EpfIterations { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether custom EPF sharpness
+ /// is used.
+ ///
+ public bool EpfSharpCustom { get; set; }
+
+ ///
+ /// Gets or sets a value with 8 elements representing EPF sharpness lookup tables (LUTs).
+ ///
+ public float[] EpfSharpLookup { get; set; } = new float[8];
+
+ ///
+ /// Gets or sets a value indicating whether custom EPF weights are used.
+ ///
+ public bool EpfCustomWeights { get; set; }
+
+ ///
+ /// Gets or sets the relative weight of each channel.
+ ///
+ public float[] EpfChannelScale { get; set; } = new float[3];
+
+ ///
+ /// Gets or sets the value that represents the minimum weight for first pass.
+ ///
+ public float EpfPass1ZeroFlush { get; set; }
+
+ ///
+ /// Gets or sets the value that represents the minimum weight for second pass.
+ ///
+ public float EpfPass2ZeroFlush { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether custom sigma parameters are used for EPF.
+ ///
+ public bool EpfCustomSigma { get; set; }
+
+ ///
+ /// Gets or sets the quant multiplier.
+ ///
+ public float EpfQuantMultiplier { get; set; }
+
+ ///
+ /// Gets or sets the multiplier for sigma in pass 0.
+ ///
+ public float EpfPass0SigmaScale { get; set; }
+
+ ///
+ /// Gets or sets the multiplier for sigma in pass 2.
+ ///
+ public float EpfPass2SigmaScale { get; set; }
+
+ ///
+ /// Gets or sets the inverse multiplier for sigma on borders.
+ ///
+ public float EpfBorderSadMul { get; set; }
+
+ ///
+ /// Gets or sets the EPF sigma for modular.
+ ///
+ // NOTE: This value is not documented by libjxl.
+ public float EpfSigmaForModular { get; set; }
+
+ ///
+ /// Gets or sets the number of extensions.
+ ///
+ public long Extensions { get; set; }
+
+ ///
+ /// Mirror n floats starting at *span and store them before span.
+ ///
+ private static void LeftMirror(Span span, int n)
+ {
+ ref float p = ref MemoryMarshal.GetReference(span);
+ for (int i = 0; i < n; i++)
+ {
+ Unsafe.Add(ref p, -1 - i) = span[i];
+ }
+ }
+
+ ///
+ /// Mirror n floats starting at *(span - n) and store them at *span.
+ ///
+ private static void RightMirror(Span span, int n)
+ {
+ ref float p = ref MemoryMarshal.GetReference(span);
+ for (int i = 0; i < n; i++)
+ {
+ span[i] = Unsafe.Add(ref p, -1 - i);
+ }
+ }
+
+ public bool ComputeSigma(Rectangle blockRect, JxlPassesDecoderState state)
+ {
+ if (this.EpfIterations <= 0)
+ {
+ return false;
+ }
+
+ JxlAcStrategyImage acStrategy = state.Shared.AcStrategy;
+ float quantScale = state.Shared.Quantizer.Scale;
+
+ int sigmaStride = state.Sigma.PixelsPerRow;
+ int sharpnessStride = state.Shared.EpfSharpness.PixelsPerRow;
+
+ for (int by = 0; by < blockRect.Height; by++)
+ {
+ Span sigmaRow = state.Sigma.GetRowSpan(by);
+ Span sharpnessRow = state.Shared.EpfSharpness.GetRowSpan(by);
+ JxlAcStrategyRow acsRow = acStrategy.GetRow(in blockRect, by);
+ Span rowQuant = state.Shared.RawQuantField.GetRow(by);
+
+ for (int bx = 0; bx < blockRect.Width; bx++)
+ {
+ JxlAcStrategy acs = acsRow[bx];
+ int llfX = acs.CoveredBlocksX;
+
+ if (!acs.IsFirstBlock)
+ {
+ continue;
+ }
+
+ float sigmaQuant = this.EpfQuantMultiplier / (quantScale * rowQuant[bx] * InverseSigmaNum);
+
+ for (int iy = 0; iy < acs.CoveredBlocksY; iy++)
+ {
+ for (int ix = 0; ix < acs.CoveredBlocksY; ix++)
+ {
+ float sigma = sigmaQuant * this.EpfSharpLookup[sharpnessRow[bx + ix + iy + sharpnessStride]];
+ sigma = MathF.Min(-1e-4f, sigma);
+ sigmaRow[bx + ix + SigmaPadding + ((iy + SigmaPadding) * sigmaStride)] = 1.0f / sigma;
+ }
+ }
+
+ if (bx + blockRect.X == 0)
+ {
+ for (int iy = 0; iy < acs.CoveredBlocksY; iy++)
+ {
+ LeftMirror(sigmaRow[(SigmaPadding + ((iy + SigmaPadding) * sigmaStride))..], SigmaBorder);
+ }
+ }
+
+ if (bx + blockRect.X + llfX == state.Shared.FrameDimensions.XSizeBlocks)
+ {
+ for (int iy = 0; iy < acs.CoveredBlocksY; iy++)
+ {
+ RightMirror(sigmaRow[(SigmaPadding + bx + llfX + ((iy + SigmaPadding) * sigmaStride))..], SigmaBorder);
+ }
+ }
+
+ int offsetBefore = bx + blockRect.X == 0 ? 1 : bx + SigmaPadding;
+ int offsetAfter = bx + blockRect.X + llfX == state.Shared.FrameDimensions.XSizeBlocks
+ ? SigmaPadding + llfX + bx + SigmaBorder
+ : SigmaPadding + llfX + bx;
+
+ int num = offsetAfter - offsetBefore;
+
+ if (by + blockRect.Y == 0)
+ {
+ for (int iy = 0; iy < SigmaBorder; iy++)
+ {
+ sigmaRow.Slice(offsetBefore + ((SigmaPadding - 1 - iy) * sigmaStride), num)
+ .CopyTo(sigmaRow[(offsetBefore + ((SigmaPadding + iy) * sigmaStride))..]);
+ }
+ }
+
+ if (by + blockRect.Y + acs.CoveredBlocksX == state.Shared.FrameDimensions.YSizeBloks)
+ {
+ for (int iy = 0; iy < SigmaBorder; iy++)
+ {
+ sigmaRow[(offsetBefore + (sigmaStride * (acs.CoveredBlocksX + SigmaPadding + iy)))..]
+ .CopyTo(sigmaRow[(offsetBefore + (sigmaStride * (acs.CoveredBlocksY + SigmaPadding - 1 - iy)))..]);
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3.cs
new file mode 100644
index 0000000000..a01e2a6216
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3.cs
@@ -0,0 +1,142 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.InteropServices;
+
+#pragma warning disable IDE0044 // Add readonly modifier
+#pragma warning disable IDE0051 // Remove unused private members
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal struct JxlMatrix3x3
+{
+ ///
+ /// Represents the matrix element at 0,0.
+ ///
+ private double e00;
+
+ ///
+ /// Represents the matrix element at 0,1.
+ ///
+ private double e01;
+
+ ///
+ /// Represents the matrix element at 0,2.
+ ///
+ private double e02;
+
+ ///
+ /// Represents the matrix element at 1,0.
+ ///
+ private double e10;
+
+ ///
+ /// Represents the matrix element at 1,1.
+ ///
+ private double e11;
+
+ ///
+ /// Represents the matrix element at 1,2.
+ ///
+ private double e12;
+
+ ///
+ /// Represents the matrix element at 2,0.
+ ///
+ private double e20;
+
+ ///
+ /// Represents the matrix element at 2,1.
+ ///
+ private double e21;
+
+ ///
+ /// Represents the matrix element at 2,2.
+ ///
+ private double e22;
+
+ ///
+ /// Wraps all these values into a Span.
+ ///
+ /// A Span with all matrix elements.
+ public Span AsSpan() => MemoryMarshal.CreateSpan(ref this.e00, 9);
+
+ ///
+ /// Wraps all these values into a ReadOnlySpan.
+ ///
+ /// A ReadOnlySpan with all matrix elements.
+ public ReadOnlySpan AsReadOnlySpan() => MemoryMarshal.CreateReadOnlySpan(ref this.e00, 9);
+
+ public static void Multiply(in JxlMatrix3x3 a, in JxlMatrix3x3 b, ref JxlMatrix3x3 c)
+ {
+ ReadOnlySpan spanA = a.AsReadOnlySpan();
+ ReadOnlySpan spanB = b.AsReadOnlySpan();
+ Span spanC = c.AsSpan();
+
+ for (int row = 0; row < 3; row++)
+ {
+ int row3 = row * 3;
+ for (int col = 0; col < 3; col++)
+ {
+ double sum = 0d;
+ for (int k = 0; k < 3; k++)
+ {
+ sum += spanA[row3 + k] * spanB[(k * 3) + col];
+ }
+
+ spanC[row3 + col] = sum;
+ }
+ }
+ }
+
+ public static void Multiply(in JxlMatrix3x3 a, ReadOnlySpan b, Span c)
+ {
+ ReadOnlySpan spanA = a.AsReadOnlySpan();
+
+ for (int row = 0; row < 3; row++)
+ {
+ double sum = 0f;
+ int row3 = row * 3;
+ for (int col = 0; col < 3; col++)
+ {
+ sum += spanA[row3 + col] * b[col];
+ }
+
+ c[row] = sum;
+ }
+ }
+
+ public static bool Invert(ref JxlMatrix3x3 matrix)
+ {
+ ReadOnlySpan m = matrix.AsReadOnlySpan();
+ Span temp =
+ [
+ ((double)m[4] * m[8]) - ((double)m[5] * m[7]),
+ ((double)m[2] * m[7]) - ((double)m[1] * m[8]),
+ ((double)m[1] * m[5]) - ((double)m[2] * m[4]),
+ ((double)m[5] * m[6]) - ((double)m[3] * m[8]),
+ ((double)m[0] * m[8]) - ((double)m[2] * m[6]),
+ ((double)m[2] * m[3]) - ((double)m[0] * m[5]),
+ ((double)m[3] * m[7]) - ((double)m[4] * m[6]),
+ ((double)m[1] * m[6]) - ((double)m[0] * m[7]),
+ ((double)m[0] * m[4]) - ((double)m[1] * m[3]),
+ ];
+
+ double det = (m[0] * temp[0]) + (m[1] * temp[3]) + (m[2] * temp[6]);
+
+ if (Math.Abs(det) < 1e-10)
+ {
+ return false;
+ }
+
+ double idet = 1.0 / det;
+ Span spanM = matrix.AsSpan();
+
+ for (int i = 0; i < 9; i++)
+ {
+ spanM[i] = (double)(temp[i] * idet);
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs
new file mode 100644
index 0000000000..0a2d59a90f
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs
@@ -0,0 +1,155 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.InteropServices;
+
+#pragma warning disable IDE0044 // Add readonly modifier
+#pragma warning disable IDE0051 // Remove unused private members
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal struct JxlMatrix3x3F
+{
+ ///
+ /// Represents the matrix element at 0,0.
+ ///
+ private float e00;
+
+ ///
+ /// Represents the matrix element at 0,1.
+ ///
+ private float e01;
+
+ ///
+ /// Represents the matrix element at 0,2.
+ ///
+ private float e02;
+
+ ///
+ /// Represents the matrix element at 1,0.
+ ///
+ private float e10;
+
+ ///
+ /// Represents the matrix element at 1,1.
+ ///
+ private float e11;
+
+ ///
+ /// Represents the matrix element at 1,2.
+ ///
+ private float e12;
+
+ ///
+ /// Represents the matrix element at 2,0.
+ ///
+ private float e20;
+
+ ///
+ /// Represents the matrix element at 2,1.
+ ///
+ private float e21;
+
+ ///
+ /// Represents the matrix element at 2,2.
+ ///
+ private float e22;
+
+ internal JxlMatrix3x3F(float[][] array)
+ {
+ this.e00 = array[0][0];
+ this.e01 = array[0][1];
+ this.e02 = array[0][2];
+ this.e10 = array[1][0];
+ this.e11 = array[1][1];
+ this.e12 = array[1][2];
+ this.e20 = array[2][0];
+ this.e21 = array[2][1];
+ this.e22 = array[2][2];
+ }
+
+ ///
+ /// Wraps all these values into a Span.
+ ///
+ /// A Span with all matrix elements.
+ public Span AsSpan() => MemoryMarshal.CreateSpan(ref this.e00, 9);
+
+ ///
+ /// Wraps all these values into a ReadOnlySpan.
+ ///
+ /// A ReadOnlySpan with all matrix elements.
+ public ReadOnlySpan AsReadOnlySpan() => MemoryMarshal.CreateReadOnlySpan(ref this.e00, 9);
+
+ public static void Multiply(in JxlMatrix3x3F a, in JxlMatrix3x3F b, ref JxlMatrix3x3F c)
+ {
+ ReadOnlySpan spanA = a.AsReadOnlySpan();
+ ReadOnlySpan spanB = b.AsReadOnlySpan();
+ Span spanC = c.AsSpan();
+
+ for (int row = 0; row < 3; row++)
+ {
+ int row3 = row * 3;
+ for (int col = 0; col < 3; col++)
+ {
+ float sum = 0f;
+ for (int k = 0; k < 3; k++)
+ {
+ sum += spanA[row3 + k] * spanB[(k * 3) + col];
+ }
+
+ spanC[row3 + col] = sum;
+ }
+ }
+ }
+
+ public static void Multiply(in JxlMatrix3x3F a, ReadOnlySpan b, Span c)
+ {
+ ReadOnlySpan spanA = a.AsReadOnlySpan();
+
+ for (int row = 0; row < 3; row++)
+ {
+ float sum = 0f;
+ int row3 = row * 3;
+ for (int col = 0; col < 3; col++)
+ {
+ sum += spanA[row3 + col] * b[col];
+ }
+
+ c[row] = sum;
+ }
+ }
+
+ public static bool Invert(ref JxlMatrix3x3F matrix)
+ {
+ ReadOnlySpan m = matrix.AsReadOnlySpan();
+ Span temp =
+ [
+ ((double)m[4] * m[8]) - ((double)m[5] * m[7]),
+ ((double)m[2] * m[7]) - ((double)m[1] * m[8]),
+ ((double)m[1] * m[5]) - ((double)m[2] * m[4]),
+ ((double)m[5] * m[6]) - ((double)m[3] * m[8]),
+ ((double)m[0] * m[8]) - ((double)m[2] * m[6]),
+ ((double)m[2] * m[3]) - ((double)m[0] * m[5]),
+ ((double)m[3] * m[7]) - ((double)m[4] * m[6]),
+ ((double)m[1] * m[6]) - ((double)m[0] * m[7]),
+ ((double)m[0] * m[4]) - ((double)m[1] * m[3]),
+ ];
+
+ double det = (m[0] * temp[0]) + (m[1] * temp[3]) + (m[2] * temp[6]);
+
+ if (Math.Abs(det) < 1e-10)
+ {
+ return false;
+ }
+
+ double idet = 1.0 / det;
+ Span spanM = matrix.AsSpan();
+
+ for (int i = 0; i < 9; i++)
+ {
+ spanM[i] = (float)(temp[i] * idet);
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseHelper.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseHelper.cs
new file mode 100644
index 0000000000..f6432e24db
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseHelper.cs
@@ -0,0 +1,28 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal static class JxlNoiseHelper
+{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static JxlNoiseIndexAndFraction IndexAndFraction(float x)
+ {
+ const int scaleNumerator = JxlNoiseParameters.NoisePoints - 2;
+ const float scale = scaleNumerator / 1.0f;
+
+ float scaledX = MathF.Max(0f, x * scale);
+ float floorX = MathF.Floor(scaledX);
+ float fractionalX = scaledX - floorX;
+
+ if (scaledX >= scaleNumerator + 1)
+ {
+ floorX = scaleNumerator;
+ fractionalX = 1f;
+ }
+
+ return new((int)floorX, fractionalX);
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseIndexAndFraction.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseIndexAndFraction.cs
new file mode 100644
index 0000000000..c3ec1437c8
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseIndexAndFraction.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.Processing;
+
+[StructLayout(LayoutKind.Sequential)]
+internal struct JxlNoiseIndexAndFraction(int index, float fraction)
+{
+ public int Index = index;
+
+ public float Fraction = fraction;
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseLevel.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseLevel.cs
new file mode 100644
index 0000000000..d5feebd52b
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseLevel.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.Processing;
+
+[StructLayout(LayoutKind.Sequential)]
+internal struct JxlNoiseLevel(float noiseLevel, float intensity)
+{
+ public float NoiseLevel = noiseLevel;
+
+ public float Intensity = intensity;
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseParameters.cs
new file mode 100644
index 0000000000..3f51f85379
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseParameters.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal sealed class JxlNoiseParameters
+{
+ public const int NoisePoints = 8;
+
+ public float[] Lookup { get; set; } = new float[NoisePoints];
+
+ public bool ContainsAny => this.Lookup.Any(x => MathF.Abs(x) > 1e-3f);
+
+ public void Clear() => Array.Fill(this.Lookup, 0f);
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs
new file mode 100644
index 0000000000..322bcd1c64
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal static class JxlOpsinInverseParameters
+{
+ private const float M02 = 0.078f;
+ private const float M00 = 0.30f;
+ private const float M01 = 1.0f - M02 - M00;
+
+ private const float M12 = 0.078f;
+ private const float M10 = 0.23f;
+ private const float M11 = 1.0f - M12 - M10;
+
+ private const float M20 = 0.24342268924547819f;
+ private const float M21 = 0.20476744424496821f;
+ private const float M22 = 1.0f - M20 - M21;
+
+ private static readonly float[][] Matrix =
+ [
+ [M00, M01, M02],
+ [M10, M11, M12],
+ [M20, M21, M22]
+ ];
+
+ public static JxlMatrix3x3F GetOpsinAbsorbanceInverseMatrix()
+ {
+ JxlMatrix3x3F matrix = new(Matrix);
+ _ = JxlMatrix3x3F.Invert(ref matrix);
+ return matrix;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPackSigned.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPackSigned.cs
new file mode 100644
index 0000000000..f54b77f1d3
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPackSigned.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+///
+/// Provides PackSigned and UnpackSigned methods.
+///
+internal static class JxlPackSigned
+{
+ ///
+ /// Encodes non-negative (X) into (2 * X), negative (-X) into (2 * X - 1)
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static uint PackUnsigned(int x)
+ {
+ unchecked
+ {
+ uint value = (uint)x;
+ return (value << 1) ^ ((~value >> 31) - 1);
+ }
+ }
+
+ ///
+ /// Reverse to PackSigned, i.e. UnpackSigned(PackSigned(X)) == X.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int UnpackSigned(uint x)
+ {
+ unchecked
+ {
+ return (int)((x >> 1) ^ (((~x) & 1) - 1));
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs
new file mode 100644
index 0000000000..fa4dd20518
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+///
+/// Quantization mode.
+///
+internal enum JxlQuantMode : byte
+{
+ Library,
+ Id,
+ Dct2,
+ Dct4,
+ Dct4x8,
+ Afv,
+ Dct,
+ Raw
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantTable.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantTable.cs
new file mode 100644
index 0000000000..5fc8b49a8e
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantTable.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+///
+/// Specifies which quantization table to use depending on
+/// transform & block type.
+///
+internal enum JxlQuantTable : byte
+{
+ DCT = 0,
+ IDENTITY,
+ DCT2X2,
+ DCT4X4,
+ DCT16X16,
+ DCT32X32,
+
+ // DCT16X8
+ DCT8X16,
+
+ // DCT32X8
+ DCT8X32,
+
+ // DCT32X16
+ DCT16X32,
+ DCT4X8,
+
+ // DCT8X4
+ AFV0,
+
+ // AFV1
+ // AFV2
+ // AFV3
+ DCT64X64,
+
+ // DCT64X32,
+ DCT32X64,
+ DCT128X128,
+
+ // DCT128X64,
+ DCT64X128,
+ DCT256X256,
+
+ // DCT256X128,
+ DCT128X256
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs
new file mode 100644
index 0000000000..5a1405b350
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs
@@ -0,0 +1,394 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+using SixLabors.ImageSharp.Formats.Jxl.IO;
+using SixLabors.ImageSharp.Formats.Jxl.Memory;
+using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+///
+/// The quantizer for DCT DC/AC coefficients.
+///
+///
+/// The quantizer's primary role is to lower the value
+/// of coefficients. For example, during encoding,
+/// coefficients may be divided by 3 and have to be
+/// multiplied by 3 at decoding (which is lossy). Quantization is often
+/// useful for variable-length coding where the amount
+/// of bits depend on how large the number is.
+///
+internal sealed class JxlQuantizer
+{
+ ///
+ /// Denominator for the global_scale value.
+ ///
+ private const int GlobalScaleDenominator = 1 << 16;
+
+ ///
+ /// Numerator for the global_scale value.
+ ///
+ private const int GlobalScaleNumerator = 4096;
+
+ ///
+ /// Numerator for biases.
+ ///
+ private const float BiasNumerator = 0.145f;
+
+ ///
+ /// The default value of the quant.
+ ///
+ private const int DefaultQuant = 64;
+
+ ///
+ /// The maximum value for a quant. Quant cannot be greater than this -
+ /// if attempted to, it will be limited to this value.
+ ///
+ private const int MaxQuant = 256;
+
+ ///
+ /// Represents the multipliers for the DC coefficients.
+ ///
+ private readonly float[] mulDc = new float[4];
+
+ ///
+ /// Represents the inverse multipliers for the DC coefficients.
+ ///
+ private readonly float[] inverseMulDc = new float[4];
+
+ ///
+ /// Global scale
+ ///
+ private int globalScale;
+
+ ///
+ /// Quantizer DC
+ ///
+ private int quantDc;
+
+ ///
+ /// Inverse global scale
+ ///
+ private float inverseGlobalScale;
+
+ ///
+ /// Reciprocal of inverseGlobalScale
+ ///
+ private float globalScaleSingle;
+
+ ///
+ /// Inverse quantizer DC
+ ///
+ private float inverseQuantDc;
+
+ ///
+ /// The zero bias.
+ ///
+ private readonly float[] zeroBias = new float[3];
+
+ ///
+ /// The dequant matrices.
+ ///
+ private readonly JxlDequantMatrices? dequant;
+
+ ///
+ /// Initializes a new instance of the class using the default
+ /// DC quantizer & scale factors.
+ ///
+ /// The dequant.
+ public JxlQuantizer(JxlDequantMatrices dequant)
+ : this(dequant, DefaultQuant, GlobalScaleDenominator / DefaultQuant)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The dequant.
+ /// The DC quantizer.
+ /// The scale factor.
+ public JxlQuantizer(JxlDequantMatrices dequant, int quantDc, int globalScale)
+ {
+ this.dequant = dequant;
+ this.quantDc = quantDc;
+ this.globalScale = globalScale;
+
+ this.RecomputeFromGlobalScale();
+ this.inverseQuantDc = this.inverseGlobalScale / this.quantDc;
+
+ ZeroBiasDefault.CopyTo(this.zeroBias);
+ }
+
+ ///
+ /// Gets the scaling factor.
+ ///
+ public float Scale => this.globalScaleSingle;
+
+ ///
+ /// Gets the inverse scaling factor. It is a reciprocal of .
+ ///
+ public float InverseGlobalScale => this.inverseGlobalScale;
+
+ ///
+ /// Gets the inverse DC quantization base value.
+ ///
+ public float InverseQuantDc => this.inverseQuantDc;
+
+ public ReadOnlySpan MulDc => this.mulDc;
+
+ public ReadOnlySpan InverseMulDc => this.inverseMulDc;
+
+ ///
+ /// Gets the zero-biases for quantizing channels X, Y, and B.
+ ///
+ private static ReadOnlySpan ZeroBiasDefault => [0.5f, 0.5f, 0.5f];
+
+ ///
+ /// Gets the default bias for quant.
+ ///
+ private static ReadOnlySpan DefaultQuantBias =>
+ [
+ 1.0f - 0.05465007330715401f,
+ 1.0f - 0.07005449891748593f,
+ 1.0f - 0.049935103337343655f,
+ 0.145f,
+ ];
+
+ ///
+ /// Clears the and values,
+ /// setting their contents to 1.0f.
+ ///
+ public void ClearDcMultipliers()
+ {
+ Array.Fill(this.mulDc, 1f);
+ Array.Fill(this.inverseMulDc, 1f);
+ }
+
+ ///
+ /// Ensure that the input value stays within the range of 1..MaxQuant.
+ ///
+ /// Value to clamp
+ /// The input value clamped into the range of 1..MaxQuant.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int Clamp(float value) => (int)MathF.Max(1.0f, MathF.Min(value, MaxQuant));
+
+ ///
+ /// Scales the global scale value.
+ ///
+ /// The new scale
+ /// The scale value, scaled by the global scale.
+ private float ScaleGlobalScale(float scale)
+ {
+ int newGlobalScale = (int)MathF.Round(this.globalScale * scale, MidpointRounding.AwayFromZero);
+ float scaleOut = newGlobalScale * 1.0f / this.globalScale;
+ this.globalScale = newGlobalScale;
+
+ this.RecomputeFromGlobalScale();
+
+ return scaleOut;
+ }
+
+ ///
+ /// Recomputes quant values from the scale.
+ ///
+ public void RecomputeFromGlobalScale()
+ {
+ this.globalScaleSingle = this.globalScale * (1.0f / GlobalScaleDenominator);
+ this.inverseGlobalScale = 1.0f * GlobalScaleDenominator / this.globalScale;
+ this.inverseQuantDc = this.inverseGlobalScale / this.quantDc;
+
+ for (int c = 0; c < 3; c++)
+ {
+ this.mulDc[c] = this.GetDcStep(c);
+ this.inverseMulDc[c] = this.GetInverseDcStep(c);
+ }
+ }
+
+ ///
+ /// Returns the dequant matrix.
+ ///
+ /// The quant kind
+ /// The quantization index
+ /// The dequant matrix.
+ public ReadOnlySpan DequantMatrix(JxlAcStrategyType strategy, int c)
+ => this.dequant.Matrix(strategy, c);
+
+ ///
+ /// Returns the inverse dequant matrix.
+ ///
+ /// The quant kind
+ /// The quantization index
+ /// The inverse dequant matrix.
+ public ReadOnlySpan InverseDequantMatrix(JxlAcStrategyType strategy, int c)
+ => this.dequant.InverseMatrix(strategy, c);
+
+ ///
+ /// Returns the DC quantization step.
+ ///
+ /// The quantization index
+ /// The DC quantization step
+ public float GetDcStep(int c) => this.inverseQuantDc * this.dequant.DcQuant(c);
+
+ ///
+ /// Returns the inverse DC quantization step.
+ ///
+ /// The quantization index
+ /// The inverse DC quantization step
+ public float GetInverseDcStep(int c) => this.dequant.InverseDcQuant(c) * (this.globalScaleSingle * this.quantDc);
+
+ ///
+ /// Creates JXL quantizer parameters with values reflecting those in this quantizer instance.
+ ///
+ /// The quantizer parameters.
+ public JxlQuantizerParameters GetParameters() => new()
+ {
+ QuantDc = (uint)this.quantDc,
+ GlobalScale = (uint)this.globalScale
+ };
+
+ ///
+ /// Reads the quantizer values from the bit-stream.
+ ///
+ /// The bit reader.
+ /// Thrown when it is not possible to parse the quantizer parameters.
+ public void Decode(JxlBitReader reader)
+ {
+ JxlQuantizerParameters qp = new();
+ if (!JxlBundle.Read(reader, qp))
+ {
+ throw new IOException("Could not read quantizer parameters");
+ }
+
+ this.globalScale = (int)qp.GlobalScale;
+ this.quantDc = (int)qp.QuantDc;
+
+ this.RecomputeFromGlobalScale();
+ }
+
+ ///
+ /// Recomputes the scaling factors and quant.
+ ///
+ public void ComputeGlobalScaleAndQuant(float quantDc, float quantMedian, float quantMedianAbsd)
+ {
+ const int quantFieldTarget = 5;
+ float scale = GlobalScaleDenominator * (quantMedian - quantMedianAbsd) / quantFieldTarget;
+
+ if (scale < 1)
+ {
+ scale = 1;
+ }
+
+ if (scale > (1 << 15))
+ {
+ scale = 1 << 15;
+ }
+
+ int newGlobalScale = (int)scale;
+ int scaledQuantDc = (int)(quantDc * GlobalScaleNumerator * 1.6f);
+
+ if (newGlobalScale > scaledQuantDc)
+ {
+ newGlobalScale = scaledQuantDc;
+
+ if (newGlobalScale <= 0)
+ {
+ newGlobalScale = 1;
+ }
+ }
+
+ this.globalScale = newGlobalScale;
+
+ this.RecomputeFromGlobalScale();
+
+ float valueF = (quantDc * this.inverseGlobalScale) + 0.5f;
+ float clipValueF = MathF.Min(1 << 16, valueF);
+ int newQuant = (int)clipValueF;
+ this.quantDc = newQuant;
+
+ this.RecomputeFromGlobalScale();
+ }
+
+ ///
+ /// Quantizes the specified rectangular selection.
+ ///
+ public void SetQuantFieldRect(JxlImageF qf, in Rectangle rect, JxlImageI rawQuantField)
+ {
+ for (int y = 0; y < rect.Height; y++)
+ {
+ ReadOnlySpan rowQf = qf.GetRow(in rect, y);
+ Span rowQi = rawQuantField.GetRow(in rect, y);
+
+ for (int x = 0; x < rect.Width; x++)
+ {
+ int val = Clamp((rowQf[x] * this.inverseGlobalScale) + 0.5f);
+
+ rowQi[x] = val;
+ }
+ }
+ }
+
+ ///
+ /// Set the quant field.
+ ///
+ public bool SetQuantField(Configuration configuration, float quantDc, JxlImageF qf, JxlImageI? rawQuantField)
+ {
+ IMemoryOwner data = configuration.MemoryAllocator.Allocate(qf.XSize * qf.YSize);
+ Span dataSpan = data.Memory.Span;
+
+ for (int y = 0; y < qf.YSize; y++)
+ {
+ ReadOnlySpan rowQf = qf.GetRow(y);
+
+ for (int x = 0; x < qf.XSize; y++)
+ {
+ float quant = rowQf[x];
+
+ dataSpan[(qf.XSize * y) + x] = quant;
+ }
+ }
+
+ dataSpan[dataSpan.Length / 2] = JxlSpanHelper.NthElement(dataSpan, dataSpan.Length / 2);
+ float quantMedian = dataSpan[dataSpan.Length / 2];
+
+ IMemoryOwner deviations = configuration.MemoryAllocator.Allocate(dataSpan.Length);
+ Span deviationsSpan = deviations.Memory.Span;
+ for (int i = 0; i < dataSpan.Length; i++)
+ {
+ deviationsSpan[i] = MathF.Abs(dataSpan[i] - quantMedian);
+ }
+
+ deviationsSpan[deviationsSpan.Length / 2] = JxlSpanHelper.NthElement(deviationsSpan, deviationsSpan.Length / 2);
+ float quantMedianAbsd = deviationsSpan[deviationsSpan.Length / 2];
+
+ this.ComputeGlobalScaleAndQuant(quantDc, quantMedian, quantMedianAbsd);
+
+ if (rawQuantField != null)
+ {
+ if (rawQuantField.GetSize() != qf.GetSize())
+ {
+ data.Dispose();
+ deviations.Dispose();
+
+ return false;
+ }
+
+ this.SetQuantField(qf, qf.GetRectangle(), rawQuantField);
+ }
+
+ data.Dispose();
+ deviations.Dispose();
+
+ return true;
+ }
+
+ public void SetQuant(float quantDc, float quantAc, JxlImageI rawQuantField)
+ {
+ this.ComputeGlobalScaleAndQuant(quantDc, quantAc, 0);
+
+ int value = Clamp((quantAc * this.inverseGlobalScale) + 0.5f);
+ rawQuantField.Fill(value);
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerParameters.cs
new file mode 100644
index 0000000000..4faa558692
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerParameters.cs
@@ -0,0 +1,60 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+///
+/// Represents parameters for the JPEG XL quantizer.
+///
+internal sealed class JxlQuantizerParameters : IJxlFields
+{
+ private uint globalScale;
+ private uint quantDc;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public JxlQuantizerParameters() => JxlBundle.Init(this);
+
+ ///
+ /// Gets or sets the global scale value.
+ ///
+ public uint GlobalScale
+ {
+ get => this.globalScale;
+ set => this.globalScale = value;
+ }
+
+ ///
+ /// Gets or sets the quant DC value.
+ ///
+ public uint QuantDc
+ {
+ get => this.quantDc;
+ set => this.quantDc = value;
+ }
+
+ public bool Visit(JxlVisitor visitor)
+ {
+ if (!visitor.U32(
+ JxlFieldExpressions.BitsOffset(11, 1),
+ JxlFieldExpressions.BitsOffset(11, 2049),
+ JxlFieldExpressions.BitsOffset(12, 4097),
+ JxlFieldExpressions.BitsOffset(16, 8193),
+ 1,
+ ref this.globalScale))
+ {
+ return false;
+ }
+
+ return visitor.U32(
+ JxlFieldExpressions.Value(16),
+ JxlFieldExpressions.BitsOffset(5, 1),
+ JxlFieldExpressions.BitsOffset(8, 1),
+ JxlFieldExpressions.BitsOffset(16, 1),
+ 1,
+ ref this.quantDc);
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs
new file mode 100644
index 0000000000..249d1ec9a7
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal sealed class JxlWeightsSeparable5
+{
+ public InlineArray12 Horizontal { get; set; }
+
+ public InlineArray12 Vertical { get; set; }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric3.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric3.cs
new file mode 100644
index 0000000000..4d350b3ab8
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric3.cs
@@ -0,0 +1,52 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using System.Runtime.Intrinsics;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal sealed class JxlWeightsSymmetric3
+{
+ private InlineArray4 c;
+
+ private InlineArray4 r;
+
+ private InlineArray4 d;
+
+ public Vector128 GetCVector()
+ {
+ ref float first = ref Unsafe.AsRef(in this.c[0]);
+ return Vector128.LoadUnsafe(ref first);
+ }
+
+ public Vector128 GetRVector()
+ {
+ ref float first = ref Unsafe.AsRef(in this.r[0]);
+ return Vector128.LoadUnsafe(ref first);
+ }
+
+ public Vector128