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..0b2640467b
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+///
+/// Raw bits coder
+///
+internal static class JxlBitsCoder
+{
+ ///
+ /// Maximum number of encodeable bits. Since this coder encodes
+ /// bits raw, this happens to be whatever is passed to it.
+ ///
+ // Looks like that's what the function does (fields.cc:406):
+ // it returns whatever is passed to it.
+ public static int MaxEncodedBits(int bits) => bits;
+
+ public static bool CanEncode(int bits, uint value, ref int encodedBits)
+ {
+ encodedBits = bits;
+ if (value >= (1 << bits))
+ {
+ DebugGuard.IsTrue(false, "Value is too large");
+
+ return false;
+ }
+
+ return true;
+ }
+
+ // 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..d45b0968af
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs
@@ -0,0 +1,70 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+///
+/// Represents the Half-precision Floating-point number coder.
+///
+internal static class JxlF16Coder
+{
+ ///
+ /// Always returns 16, which is the maximum possible encoded bits.
+ /// The F16 coder always reads 16 bits from the bitstream.
+ ///
+ public static int MaxEncodedBits() => 16;
+
+ ///
+ /// Returns a boolean indicating whether the input float
+ /// can be represented properly when encoded into a bit-stream.
+ /// Also stores the maximum encodeable bits into encodedBits (which is
+ /// always 16).
+ ///
+ public static bool CanEncode(float value, ref int encodedBits)
+ {
+ encodedBits = MaxEncodedBits();
+ if (float.IsNaN(value) || float.IsInfinity(value))
+ {
+ return false; // NaN and Infinity are not valid
+ }
+
+ return MathF.Abs(value) <= 65504.0f;
+ }
+
+ public static bool Read(JxlBitReader reader, ref float value)
+ {
+ uint bits16 = reader.ReadBits32(16u);
+ uint sign = bits16 >> 15;
+ uint biasedExponent = (bits16 >> 10) & 0x1Fu;
+ uint mantissa = bits16 & 0x3FFu;
+
+ if (biasedExponent == 31u)
+ {
+ // NaN and Infinity are not valid
+ return false;
+ }
+
+ if (biasedExponent == 0u)
+ {
+ // Subnormal or zero.
+ value = (1.0f / 16384) * (mantissa * (1.0f / 1024));
+ if (sign != 0u)
+ {
+ value = -value;
+ }
+
+ return true;
+ }
+
+ uint biasedExp32 = biasedExponent + (127u - 15u);
+ uint mantissa32 = mantissa << (23 - 10);
+ uint bits32 = (sign << 31) | (biasedExp32 << 23) | mantissa32;
+
+ value = BitConverter.UInt32BitsToSingle(bits32);
+
+ return true;
+ }
+}
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..8cd2fe7ea3
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs
@@ -0,0 +1,125 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+///
+/// Unsigned 32-bit variable-length integer coder.
+///
+internal static class JxlU32Coder
+{
+ ///
+ /// Maximum number of writeable and/or readable bits in a variable-length integer.
+ ///
+ public static int MaxEncodedBits(in JxlU32Enc enc)
+ {
+ int extraBits = 0;
+
+ for (int selector = 0; selector < 4; selector++)
+ {
+ JxlU32Distribution distr = enc.GetDistribution(selector);
+
+ if (distr.IsDirect)
+ {
+ continue;
+ }
+ else
+ {
+ extraBits = Math.Max(extraBits, (int)distr.ExtraBits);
+ }
+ }
+
+ return 2 + extraBits;
+ }
+
+ ///
+ /// Verifies that the value can be encoded.
+ ///
+ public static bool CanEncode(in JxlU32Enc enc, uint value, ref int encodedBits)
+ {
+ uint selector = 0;
+ int totalBits = 0;
+
+ bool isOk = ChooseSelector(in enc, value, ref selector, ref totalBits);
+
+ encodedBits = isOk ? totalBits : 0;
+
+ return isOk;
+ }
+
+ ///
+ /// Reads the U32 coded value.
+ ///
+ public static uint Read(in JxlU32Enc enc, JxlBitReader reader)
+ {
+ uint selector = reader.ReadBits32(2u);
+ JxlU32Distribution dist = enc.GetDistribution((int)selector);
+
+ if (dist.IsDirect)
+ {
+ return dist.Direct;
+ }
+ else
+ {
+ return reader.ReadBits32(dist.ExtraBits) + dist.Offset;
+ }
+ }
+
+ ///
+ /// Tries to find the best one of the four selectors based on the value.
+ ///
+ public static bool ChooseSelector(in JxlU32Enc enc, uint value, ref uint selector, ref int totalBits)
+ {
+ 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..659b6f994d
--- /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.Processing.Decoder;
+
+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/Entropy/JxlAnsConstants.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsConstants.cs
new file mode 100644
index 0000000000..d9b6d1150d
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsConstants.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy;
+
+internal static class JxlAnsConstants
+{
+ public const int AnsLogTableSize = 12;
+ public const int AnsTableSize = 1 << AnsLogTableSize;
+ public const int AnsTabMask = AnsTableSize - 1;
+ public const int PrefixMaxAlphabetSize = 4096;
+ public const int AnsMaxAlphabetSize = 256;
+ public const int PrefixMaxBits = 15;
+ public const int AnsSignature = 0x13;
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsEntry.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsEntry.cs
new file mode 100644
index 0000000000..7ec8c7cd4c
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsEntry.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy;
+
+[StructLayout(LayoutKind.Sequential)]
+internal struct JxlAnsEntry
+{
+ public byte Cutoff;
+ public byte RightValue;
+ public ushort Frequency0;
+ public ushort Offsets1;
+ public ushort Frequency1XorFrequency0;
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs
new file mode 100644
index 0000000000..8e775050e1
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/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.Entropy;
+
+internal static class JxlAnsHelper
+{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int GetPopulationCountPrecision(int logCount, int shift)
+ => Math.Max(0, Math.Min(logCount, shift - ((JxlAnsConstants.AnsLogTableSize - logCount) >> 1)));
+
+ // NOTE: The result may potentially be large, so prefer using a memory allocator
+ public static IMemoryOwner CreateFlatHistogram(Configuration configuration, int length, int totalCount)
+ {
+ 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/Entropy/JxlAnsHybridUIntConfiguration.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs
new file mode 100644
index 0000000000..b8609f9163
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/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.Entropy;
+
+internal sealed class JxlAnsHybridUIntConfiguration : IJxlFields
+{
+ public JxlAnsHybridUIntConfiguration(uint splitExponent = 4, uint msbInToken = 2, uint lsbInToken = 0)
+ {
+ this.SplitExponent = splitExponent;
+ this.SplitToken = 1u << (int)splitExponent;
+ this.MsbInToken = msbInToken;
+ this.LsbInToken = lsbInToken;
+
+ 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/Entropy/JxlAnsLz77Parameters.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsLz77Parameters.cs
new file mode 100644
index 0000000000..cbf6d3a4e4
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/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.Entropy;
+
+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/Entropy/JxlAnsSymbol.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsSymbol.cs
new file mode 100644
index 0000000000..240b2463eb
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsSymbol.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy;
+
+[StructLayout(LayoutKind.Sequential)]
+internal struct JxlAnsSymbol(int value, int offset, int frequency)
+{
+ public int Value = value;
+ public int Offset = offset;
+ public int Frequency = frequency;
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs
new file mode 100644
index 0000000000..1854b4ab67
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs
@@ -0,0 +1,74 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+using SixLabors.ImageSharp.Formats.Jxl.Metadata;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Describes duration of frames that make up an animation.
+///
+internal sealed class JxlAnimationFrame : IJxlFields
+{
+ ///
+ /// See .
+ ///
+ private uint duration;
+
+ ///
+ /// See .
+ ///
+ private uint timecode;
+
+ ///
+ /// Gets or sets the duration of the animation.
+ ///
+ public uint Duration
+ {
+ get => this.duration;
+ set => this.duration = value;
+ }
+
+ ///
+ /// Gets or sets the timecode of the animation. The
+ /// format is 0xHHMMSSFF.
+ ///
+ public uint Timecode
+ {
+ get => this.timecode;
+ set => this.timecode = value;
+ }
+
+ ///
+ /// Gets or sets the optional codec metadata.
+ ///
+ public JxlCodecMetadata? CodecMetadata { get; set; }
+
+ public bool Visit(JxlVisitor visitor)
+ {
+ if (visitor.Conditional(this.CodecMetadata?.ImageMetadata?.HaveAnimation == true))
+ {
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(0),
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.Bits(8),
+ JxlFieldExpressions.Bits(32),
+ 0,
+ ref this.duration))
+ {
+ return false;
+ }
+ }
+
+ if (visitor.Conditional(this.CodecMetadata?.ImageMetadata?.Animation?.ContainsTimecodes == true))
+ {
+ if (!visitor.Bits(32, 0u, ref this.timecode))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendMode.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendMode.cs
new file mode 100644
index 0000000000..035a48edd5
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendMode.cs
@@ -0,0 +1,64 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Represents the blending mode describing how to combine
+/// current frame with previously saved frame.
+///
+internal enum JxlBlendMode : byte
+{
+ ///
+ /// New values replace old ones.
+ ///
+ /// sample = new
+ ///
+ ///
+ Replace,
+
+ ///
+ /// New values add to the old ones.
+ ///
+ /// sample = old + new
+ ///
+ ///
+ Add,
+
+ ///
+ /// New values replace old ones if alpha>0:
+ ///
+ /// alpha = old + new * (1 - old)
+ ///
+ /// For other channels if !alpha_associated:
+ ///
+ /// sample = ((1 - newAlpha) * old * oldAlpha + newAlpha * new) / alpha
+ ///
+ /// For other channels if alpha_associated:
+ ///
+ /// sample = (1 - newAlpha) * old + new
+ ///
+ ///
+ Blend,
+
+ ///
+ /// New values are added to the old ones if alpha>0:
+ /// For the alpha channel that is used as source:
+ ///
+ /// sample = old + new * (1 - old)
+ ///
+ /// Otherwise:
+ ///
+ /// sample = old + alpha * new
+ ///
+ ///
+ AlphaWeightedBlend,
+
+ ///
+ /// New values are multiplied by old ones:
+ ///
+ /// sample = old * new
+ ///
+ ///
+ Multiply
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendingInfo.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendingInfo.cs
new file mode 100644
index 0000000000..58c79d22db
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendingInfo.cs
@@ -0,0 +1,146 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Provides options and instructions that tell the decoder the proper
+/// way to blend the current and previous frame together.
+///
+internal sealed class JxlBlendingInfo : IJxlFields
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public JxlBlendingInfo() => JxlBundle.Init(this);
+
+ ///
+ /// Gets or sets the blending mode. See .
+ ///
+ public JxlBlendMode BlendMode { get; set; }
+
+ ///
+ /// Gets or sets the value that indicates which extra channel
+ /// to use as alpha channel for blending.
+ ///
+ public uint AlphaChannel { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the alpha or channel values
+ /// must be clamped* to the 0 through 1 range.
+ ///
+ ///
+ /// Clamped - must be limited to the specified range.
+ ///
+ public bool Clamp { get; set; }
+
+ ///
+ /// Gets or sets the frame ID to copy from (0 through 3).
+ ///
+ ///
+ /// If is equal to ,
+ /// the value of this property is ignored.
+ ///
+ public uint Source { get; set; }
+
+ ///
+ /// Gets or sets the total number of extra channels.
+ ///
+ public int ExtraChannelCount { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the frame is partial.
+ ///
+ public bool IsPartialFrame { get; set; }
+
+ public bool Visit(JxlVisitor visitor)
+ {
+ JxlBlendMode mode = this.BlendMode;
+ if (!VisitBlendMode(visitor, JxlBlendMode.Replace, ref mode))
+ {
+ return false;
+ }
+
+ this.BlendMode = mode;
+
+ if (visitor.Conditional(this.ExtraChannelCount > 0 && mode is JxlBlendMode.Blend or JxlBlendMode.AlphaWeightedBlend))
+ {
+ uint alphaChannel = this.AlphaChannel;
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(0u),
+ JxlFieldExpressions.Value(1u),
+ JxlFieldExpressions.Value(2u),
+ JxlFieldExpressions.BitsOffset(3u, 3u),
+ 0,
+ ref alphaChannel))
+ {
+ return false;
+ }
+
+ this.AlphaChannel = alphaChannel;
+
+ if (visitor.IsReading && alphaChannel >= this.ExtraChannelCount)
+ {
+ throw new InvalidOperationException("Invalid alpha channel for blending");
+ }
+ }
+
+ if (visitor.Conditional((this.ExtraChannelCount > 0 && mode is JxlBlendMode.Blend or JxlBlendMode.AlphaWeightedBlend) || mode == JxlBlendMode.Multiply))
+ {
+ bool clamp = this.Clamp;
+
+ if (!visitor.Boolean(false, ref clamp))
+ {
+ return false;
+ }
+
+ this.Clamp = clamp;
+ }
+
+ if (visitor.Conditional(mode != JxlBlendMode.Replace || this.IsPartialFrame))
+ {
+ uint source = this.Source;
+
+ if (!visitor.U32(
+ JxlFieldExpressions.Value(0),
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.Value(2),
+ JxlFieldExpressions.Value(3),
+ 0,
+ ref source))
+ {
+ return false;
+ }
+
+ this.Source = source;
+ }
+
+ return true;
+ }
+
+ private static bool VisitBlendMode(JxlVisitor visitor, JxlBlendMode defaultValue, ref JxlBlendMode valueToEncode)
+ {
+ uint unsignedBackingValue = (uint)valueToEncode;
+
+ if (!visitor.U32(
+ JxlFieldExpressions.Value((uint)JxlBlendMode.Replace),
+ JxlFieldExpressions.Value((uint)JxlBlendMode.Add),
+ JxlFieldExpressions.Value((uint)JxlBlendMode.Blend),
+ JxlFieldExpressions.BitsOffset(2u, 3u),
+ (uint)defaultValue,
+ ref unsignedBackingValue))
+ {
+ return false;
+ }
+
+ if (unsignedBackingValue > (uint)JxlBlendMode.Multiply)
+ {
+ throw new InvalidOperationException("Invalid blend mode");
+ }
+
+ valueToEncode = (JxlBlendMode)unsignedBackingValue;
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransform.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransform.cs
new file mode 100644
index 0000000000..f874983486
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransform.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Represents the type of JPEG XL color transform.
+///
+internal enum JxlColorTransform : byte
+{
+ ///
+ /// Use XYB encoding
+ ///
+ Xyb,
+
+ ///
+ /// Encode according to the attached color profile.
+ ///
+ None,
+
+ ///
+ /// Encode according to the attached color profile but
+ /// transformed into Y'Cb'Cr.
+ ///
+ YCbCr,
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs
new file mode 100644
index 0000000000..81bc941870
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// Helper methods associated with JxlColorTransform.
+///
+internal static class JxlColorTransformHelpers
+{
+ private static ReadOnlySpan Grayscale => [0, 0, 0];
+
+ private static ReadOnlySpan YCbCr => [1, 0, 2];
+
+ private static ReadOnlySpan None => [0, 1, 2];
+
+ public static ReadOnlySpan GetJpegOrder(JxlColorTransform transform, bool isGraysacle)
+ {
+ if (isGraysacle)
+ {
+ return Grayscale;
+ }
+
+ if (transform == JxlColorTransform.YCbCr)
+ {
+ return YCbCr;
+ }
+
+ return None;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameEncoding.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameEncoding.cs
new file mode 100644
index 0000000000..77ae964ac9
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameEncoding.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
+
+///
+/// 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..43d7045041
--- /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.IO.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/JxlHuffman.cs b/src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs
new file mode 100644
index 0000000000..678321bab8
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs
@@ -0,0 +1,184 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO;
+
+///
+/// Shared Huffman I/O utilities.
+///
+internal static class JxlHuffman
+{
+ ///
+ /// Returns Reverse(Reverse(Key, Len) + 1, Len). The
+ /// Reverse(Key, Len) function performs bitwise reversal
+ /// of the len least significant bits of the key value.
+ ///
+ public static uint GetNextKey(uint key, int len)
+ {
+ uint step = 1u << (len - 1);
+ while ((key & step) != 0)
+ {
+ step >>= 1;
+ }
+
+ return (key & (step - 1)) + step;
+ }
+
+ ///
+ /// Replicates into every
+ /// times with the upper bound of .
+ ///
+ public static void ReplicateValue(Span table, int step, int end, JxlHuffmanCode code)
+ {
+ do
+ {
+ end -= step;
+ table[end] = code;
+ }
+ while (end > 0);
+ }
+
+ ///
+ /// Returns the table width of the next 2nd level table.
+ ///
+ /// The histogram of bit lengths for remaining symbols
+ /// Code length of the next processed symbol
+ /// Amount of bits for the root symbol
+ /// Table width for the 2nd level table.
+ public static int NextTableBitSize(ReadOnlySpan count, int length, int rootBits)
+ {
+ uint left = 1u << (length - rootBits);
+
+ while (length < JxlAnsConstants.PrefixMaxBits)
+ {
+ if (left <= count[length])
+ {
+ break;
+ }
+
+ left -= count[length];
+ length++;
+ left <<= 1;
+ }
+
+ return length - rootBits;
+ }
+
+ public static uint BuildHuffmanTable(
+ Span rootTable,
+ int rootBits,
+ ReadOnlySpan codeLengths,
+ Span count)
+ {
+ if (codeLengths.Length > (1u << JxlAnsConstants.PrefixMaxBits))
+ {
+ return 0u;
+ }
+
+ Span offset = stackalloc ushort[JxlAnsConstants.PrefixMaxBits + 1];
+
+ Span sortedStorage = stackalloc ushort[codeLengths.Length];
+
+ int maxLength = 1;
+ ushort sum = 0;
+ int len, symbol;
+ for (len = 1; len <= JxlAnsConstants.PrefixMaxBits; len++)
+ {
+ offset[len] = sum;
+
+ if (count[len] != 0)
+ {
+ sum = (ushort)(sum + count[len]);
+ maxLength = len;
+ }
+ }
+
+ for (symbol = 0; symbol < codeLengths.Length; symbol++)
+ {
+ if (codeLengths[symbol] != 0)
+ {
+ sortedStorage[offset[codeLengths[symbol]]++] = (ushort)symbol;
+ }
+ }
+
+ Span table = rootTable;
+ int tableBits = rootBits;
+ uint tableSize = 1u << tableBits;
+ uint totalSize = tableSize;
+
+ JxlHuffmanCode code = default;
+
+ if (offset[JxlAnsConstants.PrefixMaxBits] == 1)
+ {
+ code.Bits = 0;
+ code.Value = sortedStorage[0];
+
+ for (int i = 0; i < totalSize; i++)
+ {
+ table[i] = code;
+ }
+ }
+
+ if (tableBits > maxLength)
+ {
+ tableBits = maxLength;
+ tableSize = 1u << tableBits;
+ }
+
+ int key = 0;
+ code.Bits = 0;
+ int step = 2;
+
+ do
+ {
+ for (; count[code.Bits] != 0; --count[code.Bits])
+ {
+ code.Value = sortedStorage[symbol++];
+ ReplicateValue(table[key..], step, (int)tableSize, code);
+ key = (int)GetNextKey((uint)key, code.Bits);
+ }
+
+ step <<= 1;
+ }
+ while (++code.Bits <= tableBits);
+
+ while (totalSize != tableSize)
+ {
+ table[..(int)tableSize].CopyTo(table[(int)tableSize..]);
+ tableSize <<= 1;
+ }
+
+ uint mask = totalSize - 1u;
+ int low = -1;
+
+ uint tableOffset = 0;
+ for (step = 2; len <= maxLength; len++, step <<= 1)
+ {
+ for (; count[len] != 0; --count[len])
+ {
+ if ((key & mask) != low)
+ {
+ tableOffset += tableSize;
+ table = table[(int)tableSize..];
+ tableBits = NextTableBitSize(count, len, rootBits);
+ tableSize = 1u << tableBits;
+ totalSize += tableSize;
+ low = key & (int)mask;
+
+ rootTable[low].Bits = (byte)(tableBits + rootBits);
+ rootTable[low].Value = (ushort)(tableOffset - low);
+ }
+
+ code.Bits = (byte)(len - rootBits);
+ code.Value = sortedStorage[symbol++];
+
+ ReplicateValue(table[(key >> rootBits)..], step, (int)tableSize, code);
+ key = (int)GetNextKey((uint)key, len);
+ }
+ }
+
+ return totalSize;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlHuffmanCode.cs b/src/ImageSharp/Formats/Jxl/IO/JxlHuffmanCode.cs
new file mode 100644
index 0000000000..30bca3446d
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/JxlHuffmanCode.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO;
+
+///
+/// A single Huffman code.
+///
+internal struct JxlHuffmanCode
+{
+ ///
+ /// Number of bits for this symbol.
+ ///
+ public byte Bits;
+
+ ///
+ /// Symbol value/offset.
+ ///
+ public ushort Value;
+}
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/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/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/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs
new file mode 100644
index 0000000000..eb25583ecf
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs
@@ -0,0 +1,2333 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.Formats.Jxl.Memory;
+using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli;
+
+///
+/// Implementation of Google Butteraugli, an advanced
+/// image comparison system. Unlike PSNR or SSIM,
+/// Butteraugli compares images the way humans may
+/// spot differences instead of pixelwise.
+///
+internal static class Butteraugli
+{
+ // NOTE: The meaning of those constants
+ // is not perfectly understood.
+ public const float WMfMalta = 37.0819870399f;
+ public const float Norm1Mf = 130262059.556f;
+ public const float WMfMaltaX = 8246.75321353f;
+ public const float Norm1MfX = 1009002.70582f;
+
+ public const float WHfMalta = 18.7237414387f;
+ public const float Norm1Hf = 4498534.45232f;
+ public const float WHfMaltaX = 6923.99476109f;
+ public const float Norm1HfX = 8051.15833247f;
+
+ public const float WUhfMalta = 1.10039032555f;
+ public const float Norm1Uhf = 71.7800275169f;
+ public const float WUhfMaltaX = 173.5f;
+ public const float Norm1UhfX = 5.0f;
+
+ private const float IntensityTargetNormalizationHack = 0.79079917404f;
+
+ private static readonly float InternalGoodQualityThreshold =
+ 17.83f * IntensityTargetNormalizationHack;
+
+ private static readonly float GlobalScale =
+ 1.0f / InternalGoodQualityThreshold;
+
+ public static ReadOnlySpan Wmul =>
+ [
+ 400.0, 1.50815703118, 0,
+ 2150.0, 10.6195433239, 16.2176043152,
+ 29.2353797994, 0.844626970982, 0.703646627719,
+ ];
+
+ public static ReadOnlySpan ComputeKernel(float sigma)
+ {
+ const float m = 2.25f; // Accuracy increases when m is increased
+ float scaler = -1.0f / (2.0f * sigma * sigma);
+ int diff = Math.Max(1, (int)(m * MathF.Abs(sigma)));
+
+ // Use new because there's only up to 3 elements
+ float[] kernel = new float[(2 * diff) + 1];
+
+ for (int i = -diff; i <= diff; i++)
+ {
+ kernel[i + diff] = MathF.Exp(scaler * i * i);
+ }
+
+ return kernel;
+ }
+
+ public static void ConvolveBorderColumn(
+ JxlImageF input,
+ ReadOnlySpan kernel,
+ int x,
+ Span rowOut)
+ {
+ int offset = kernel.Length / 2;
+
+ int minX = x < offset ? 0 : x - offset;
+ int maxX = Math.Min(input.XSize - 1, x + offset);
+
+ float weight = 0.0f;
+ for (int j = minX; j <= maxX; j++)
+ {
+ weight += kernel[j - x + offset];
+ }
+
+ float scale = 1.0f / weight;
+
+ for (int y = 0; y < input.YSize; y++)
+ {
+ Span rowIn = input.GetRow(y);
+
+ float sum = 0.0f;
+
+ for (int j = minX; j <= maxX; j++)
+ {
+ sum += rowIn[j] * kernel[j - x + offset];
+ }
+
+ rowOut[y] = sum * scale;
+ }
+ }
+
+ public static bool ConvolutionWithTranspose(
+ JxlImageF input,
+ ReadOnlySpan kernel,
+ JxlImageF output)
+ {
+ if (output.XSize != input.YSize)
+ {
+ return false;
+ }
+
+ if (output.YSize != input.XSize)
+ {
+ return false;
+ }
+
+ int len = kernel.Length;
+ int offset = len / 2;
+
+ float weightNoBorder = 0.0f;
+
+ for (int j = 0; j < len; j++)
+ {
+ weightNoBorder += kernel[j];
+ }
+
+ float scaleNoBorder = 1.0f / weightNoBorder;
+
+ int border1 = Math.Min(input.XSize, offset);
+ int border2 = input.XSize > offset ? input.XSize - offset : 0;
+
+ Span scaledKernel = stackalloc float[(len / 2) + 1];
+
+ for (int i = 0; i <= len / 2; i++)
+ {
+ scaledKernel[i] = kernel[i] * scaleNoBorder;
+ }
+
+ // Middle
+ switch (len)
+ {
+ case 7:
+ {
+ float sk0 = scaledKernel[0];
+ float sk1 = scaledKernel[1];
+ float sk2 = scaledKernel[2];
+ float sk3 = scaledKernel[3];
+
+ for (int y = 0; y < input.YSize; y++)
+ {
+ Span rowIn = input.GetRow(y);
+
+ for (int x = border1; x < border2; x++)
+ {
+ int i = x - border1;
+
+ float sum0 = (rowIn[i + 0] + rowIn[i + 6]) * sk0;
+ float sum1 = (rowIn[i + 1] + rowIn[i + 5]) * sk1;
+ float sum2 = (rowIn[i + 2] + rowIn[i + 4]) * sk2;
+ float sum = (rowIn[i + 3] * sk3) + sum0 + sum1 + sum2;
+
+ output.GetRow(x)[y] = sum;
+ }
+ }
+
+ break;
+ }
+
+ case 13:
+ {
+ for (int y = 0; y < input.YSize; y++)
+ {
+ Span rowIn = input.GetRow(y);
+
+ for (int x = border1; x < border2; x++)
+ {
+ int i = x - border1;
+
+ float sum0 = (rowIn[i + 0] + rowIn[i + 12]) * scaledKernel[0];
+ float sum1 = (rowIn[i + 1] + rowIn[i + 11]) * scaledKernel[1];
+ float sum2 = (rowIn[i + 2] + rowIn[i + 10]) * scaledKernel[2];
+ float sum3 = (rowIn[i + 3] + rowIn[i + 9]) * scaledKernel[3];
+
+ sum0 += (rowIn[i + 4] + rowIn[i + 8]) * scaledKernel[4];
+ sum1 += (rowIn[i + 5] + rowIn[i + 7]) * scaledKernel[5];
+
+ float sum = rowIn[i + 6] * scaledKernel[6];
+
+ output.GetRow(x)[y] = sum + sum0 + sum1 + sum2 + sum3;
+ }
+ }
+
+ break;
+ }
+
+ case 15:
+ {
+ for (int y = 0; y < input.YSize; y++)
+ {
+ Span rowIn = input.GetRow(y);
+
+ for (int x = border1; x < border2; x++)
+ {
+ int i = x - border1;
+
+ float sum0 = (rowIn[i + 0] + rowIn[i + 14]) * scaledKernel[0];
+ float sum1 = (rowIn[i + 1] + rowIn[i + 13]) * scaledKernel[1];
+ float sum2 = (rowIn[i + 2] + rowIn[i + 12]) * scaledKernel[2];
+ float sum3 = (rowIn[i + 3] + rowIn[i + 11]) * scaledKernel[3];
+
+ sum0 += (rowIn[i + 4] + rowIn[i + 10]) * scaledKernel[4];
+ sum1 += (rowIn[i + 5] + rowIn[i + 9]) * scaledKernel[5];
+ sum2 += (rowIn[i + 6] + rowIn[i + 8]) * scaledKernel[6];
+
+ float sum = rowIn[i + 7] * scaledKernel[7];
+
+ output.GetRow(x)[y] = sum + sum0 + sum1 + sum2 + sum3;
+ }
+ }
+
+ break;
+ }
+
+ case 33:
+ {
+ for (int y = 0; y < input.YSize; y++)
+ {
+ Span rowIn = input.GetRow(y);
+
+ for (int x = border1; x < border2; x++)
+ {
+ int i = x - border1;
+
+ float sum0 = (rowIn[i + 0] + rowIn[i + 32]) * scaledKernel[0];
+ float sum1 = (rowIn[i + 1] + rowIn[i + 31]) * scaledKernel[1];
+ float sum2 = (rowIn[i + 2] + rowIn[i + 30]) * scaledKernel[2];
+ float sum3 = (rowIn[i + 3] + rowIn[i + 29]) * scaledKernel[3];
+
+ sum0 += (rowIn[i + 4] + rowIn[i + 28]) * scaledKernel[4];
+ sum1 += (rowIn[i + 5] + rowIn[i + 27]) * scaledKernel[5];
+ sum2 += (rowIn[i + 6] + rowIn[i + 26]) * scaledKernel[6];
+ sum3 += (rowIn[i + 7] + rowIn[i + 25]) * scaledKernel[7];
+
+ sum0 += (rowIn[i + 8] + rowIn[i + 24]) * scaledKernel[8];
+ sum1 += (rowIn[i + 9] + rowIn[i + 23]) * scaledKernel[9];
+ sum2 += (rowIn[i + 10] + rowIn[i + 22]) * scaledKernel[10];
+ sum3 += (rowIn[i + 11] + rowIn[i + 21]) * scaledKernel[11];
+
+ sum0 += (rowIn[i + 12] + rowIn[i + 20]) * scaledKernel[12];
+ sum1 += (rowIn[i + 13] + rowIn[i + 19]) * scaledKernel[13];
+ sum2 += (rowIn[i + 14] + rowIn[i + 18]) * scaledKernel[14];
+ sum3 += (rowIn[i + 15] + rowIn[i + 17]) * scaledKernel[15];
+
+ float sum = rowIn[i + 16] * scaledKernel[16];
+
+ output.GetRow(x)[y] = sum + sum0 + sum1 + sum2 + sum3;
+ }
+ }
+
+ break;
+ }
+
+ default:
+ throw new NotSupportedException($"Kernel size {len} not implemented.");
+ }
+
+ // Left border
+ for (int x = 0; x < border1; x++)
+ {
+ ConvolveBorderColumn(input, kernel, x, output.GetRow(x));
+ }
+
+ // Right border
+ for (int x = border2; x < input.XSize; x++)
+ {
+ ConvolveBorderColumn(input, kernel, x, output.GetRow(x));
+ }
+
+ return true;
+ }
+
+ private static bool Blur(
+ JxlImageF input,
+ float sigma,
+ in ButteraugliParameters parameters,
+ ButteraugliBlurTemp temp,
+ JxlImageF output)
+ {
+ ReadOnlySpan kernel = ComputeKernel(sigma);
+
+ // Separable5 does an in-place convolution, so this fast path is not safe
+ // if input aliases output.
+ if (kernel.Length == 5 && !ReferenceEquals(input, output))
+ {
+ float sumWeights = 0.0f;
+
+ foreach (float w in kernel)
+ {
+ sumWeights += w;
+ }
+
+ float scale = 1.0f / sumWeights;
+
+ float w0 = kernel[2] * scale;
+ float w1 = kernel[1] * scale;
+ float w2 = kernel[0] * scale;
+
+ JxlWeightsSeparable5 weights = default;
+ FillRep4(ref weights.Horizontal, w0, w1, w2);
+ FillRep4(ref weights.Vertical, w0, w1, w2);
+
+ if (!Separable5(input, input.GetRectangle(), weights, null, output))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ if (!temp.GetTransposed(input, out JxlImageF tempT))
+ {
+ return false;
+ }
+
+ if (!ConvolutionWithTranspose(input, kernel, tempT))
+ {
+ return false;
+ }
+
+ if (!ConvolutionWithTranspose(tempT, kernel, output))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Equivalent to HWY_REP4.
+ ///
+ private static void FillRep4(ref InlineArray12 values, float a, float b, float c)
+ {
+ for (int i = 0; i < 4; i++)
+ {
+ values[i] = a;
+ values[4 + i] = b;
+ values[8 + i] = c;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector MaximumClamp(Vector value, float maximum)
+ {
+ Vector multiplier = new(0.724216145665f);
+ Vector maximumValue = new(maximum);
+ Vector ifPositive = ((value - maximumValue) * multiplier) + maximumValue;
+ Vector ifNegative = ((value + maximumValue) * multiplier) - maximumValue;
+ Vector positiveOrValue = Vector.ConditionalSelect(Vector.GreaterThan(value, maximumValue), ifPositive, value);
+ Vector result = Vector.ConditionalSelect(Vector.LessThan(value, Vector.Negate(maximumValue)), ifNegative, positiveOrValue);
+
+ return result;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector RemoveRangeAroundZero(Vector x, float width)
+ {
+ Vector w = new(width);
+
+ return Vector.ConditionalSelect(
+ Vector.GreaterThan(x, w),
+ x - w,
+ Vector.ConditionalSelect(
+ Vector.LessThan(x, -w),
+ x + w,
+ Vector.Zero));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector AmplifyRangeAroundZero(Vector x, float width)
+ {
+ Vector w = new(width);
+
+ return Vector.ConditionalSelect(
+ Vector.GreaterThan(x, w),
+ x + w,
+ Vector.ConditionalSelect(
+ Vector.LessThan(x, -w),
+ x - w,
+ x + x));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void XybLowFrequencyToValues(
+ Vector x,
+ Vector y,
+ Vector bArg,
+ out Vector valX,
+ out Vector valY,
+ out Vector valB)
+ {
+ Vector xMul = new(33.832837186260f);
+ Vector yMul = new(14.458268100570f);
+ Vector bMul = new(49.87984651440f);
+ Vector yToBMul = new(-0.362267051518f);
+
+ Vector b = (yToBMul * y) + bArg;
+
+ valB = b * bMul;
+ valX = x * xMul;
+ valY = y * yMul;
+ }
+
+ public static void XybLowFrequencyToValues(JxlImage3F xybLf)
+ {
+ int lanes = Vector.Count;
+
+ for (int y = 0; y < xybLf.YSize; y++)
+ {
+ Span rowX = xybLf.PlaneRow(0, y);
+ Span rowY = xybLf.PlaneRow(1, y);
+ Span rowB = xybLf.PlaneRow(2, y);
+
+ for (int x = 0; x < xybLf.XSize; x += lanes)
+ {
+ Vector valX = new(rowX.Slice(x, lanes));
+ Vector valY = new(rowY.Slice(x, lanes));
+ Vector valB = new(rowB.Slice(x, lanes));
+
+ XybLowFrequencyToValues(
+ valX,
+ valY,
+ valB,
+ out valX,
+ out valY,
+ out valB);
+
+ valX.CopyTo(rowX.Slice(x, lanes));
+ valY.CopyTo(rowY.Slice(x, lanes));
+ valB.CopyTo(rowB.Slice(x, lanes));
+ }
+ }
+ }
+
+ public static bool SuppressXByY(JxlImageF inY, JxlImageF inOutX)
+ {
+ if (!SameSize(inOutX, inY))
+ {
+ return false;
+ }
+
+ int xSize = inY.XSize;
+ int ySize = inY.YSize;
+ int lanes = Vector.Count;
+
+ const float suppress = 46.0f;
+ const float s = 0.653020556257f;
+
+ Vector sv = new(s);
+ Vector oneMinusS = new(1.0f - s);
+ Vector ywv = new(suppress);
+
+ for (int y = 0; y < ySize; y++)
+ {
+ ReadOnlySpan rowY = inY.GetRow(y);
+ Span rowX = inOutX.GetRow(y);
+
+ for (int x = 0; x < xSize; x += lanes)
+ {
+ Vector vx = new(rowX.Slice(x, lanes));
+ Vector vy = new(rowY.Slice(x, lanes));
+
+ Vector scaler =
+ ((ywv / ((vy * vy) + ywv)) * oneMinusS) + sv;
+
+ (scaler * vx).CopyTo(rowX.Slice(x, lanes));
+ }
+ }
+
+ return true;
+ }
+
+ public static void Subtract(JxlPlane a, JxlPlane b, JxlPlane c)
+ {
+ int lanes = Vector.Count;
+
+ for (int y = 0; y < a.YSize; y++)
+ {
+ ReadOnlySpan rowA = a.GetRow(y);
+ ReadOnlySpan rowB = b.GetRow(y);
+ Span rowC = c.GetRow(y);
+
+ for (int x = 0; x < a.XSize; x += lanes)
+ {
+ Vector va = new(rowA.Slice(x, lanes));
+ Vector vb = new(rowB.Slice(x, lanes));
+
+ (va - vb).CopyTo(rowC.Slice(x, lanes));
+ }
+ }
+ }
+
+ public static bool SeparateLFAndMF(
+ in ButteraugliParameters parameters,
+ JxlImage3F xyb,
+ JxlImage3F lf,
+ JxlImage3F mf,
+ ButteraugliBlurTemp blurTemp)
+ {
+ const float sigmaLf = 7.15593339443f;
+
+ for (int i = 0; i < 3; i++)
+ {
+ if (!Blur(
+ xyb.Plane(i),
+ sigmaLf,
+ parameters,
+ blurTemp,
+ lf.Plane(i)))
+ {
+ return false;
+ }
+
+ Subtract(
+ xyb.Plane(i),
+ lf.Plane(i),
+ mf.Plane(i));
+ }
+
+ XybLowFrequencyToValues(lf);
+
+ return true;
+ }
+
+ public static bool SeparateMfAndHf(
+ Configuration configuration,
+ in ButteraugliParameters parameters,
+ JxlImage3F mf,
+ JxlImageF[] hf,
+ BlurTemp blurTemp)
+ {
+ const float sigmaHf = 3.22489901262f;
+
+ int xSize = mf.XSize;
+ int ySize = mf.YSize;
+
+ hf[0] = new JxlImageF(configuration, xSize, ySize);
+ hf[1] = new JxlImageF(configuration, xSize, ySize);
+
+ int lanes = Vector.Count;
+
+ for (int i = 0; i < 3; i++)
+ {
+ if (i == 2)
+ {
+ if (!Blur(mf.Plane(i), sigmaHf, parameters, blurTemp, mf.Plane(i)))
+ {
+ return false;
+ }
+
+ break;
+ }
+
+ for (int y = 0; y < ySize; y++)
+ {
+ Span rowMf = mf.PlaneRow(i, y);
+ Span rowHf = hf[i].GetRow(y);
+
+ for (int x = 0; x < xSize; x += lanes)
+ {
+ new Vector(rowMf.Slice(x, lanes))
+ .CopyTo(rowHf.Slice(x, lanes));
+ }
+ }
+
+ if (!Blur(mf.Plane(i), sigmaHf, parameters, blurTemp, mf.Plane(i)))
+ {
+ return false;
+ }
+
+ if (i == 0)
+ {
+ const float removeMfRange = 0.29f;
+
+ for (int y = 0; y < ySize; y++)
+ {
+ Span rowMf = mf.PlaneRow(0, y);
+ Span rowHf = hf[0].GetRow(y);
+
+ for (int x = 0; x < xSize; x += lanes)
+ {
+ Vector mfv = new(rowMf.Slice(x, lanes));
+ Vector hfv = new Vector(rowHf.Slice(x, lanes)) - mfv;
+
+ mfv = RemoveRangeAroundZero(mfv, removeMfRange);
+
+ mfv.CopyTo(rowMf.Slice(x, lanes));
+ hfv.CopyTo(rowHf.Slice(x, lanes));
+ }
+ }
+ }
+ else
+ {
+ const float addMfRange = 0.1f;
+
+ for (int y = 0; y < ySize; y++)
+ {
+ Span rowMf = mf.PlaneRow(1, y);
+ Span rowHf = hf[1].GetRow(y);
+
+ for (int x = 0; x < xSize; x += lanes)
+ {
+ Vector mfv = new(rowMf.Slice(x, lanes));
+ Vector hfv = new Vector(rowHf.Slice(x, lanes)) - mfv;
+
+ mfv = AmplifyRangeAroundZero(mfv, addMfRange);
+
+ mfv.CopyTo(rowMf.Slice(x, lanes));
+ hfv.CopyTo(rowHf.Slice(x, lanes));
+ }
+ }
+ }
+ }
+
+ return SuppressXByY(hf[1], hf[0]);
+ }
+
+ public static bool SeparateHFAndUHF(
+ in ButteraugliParameters parameters,
+ JxlImageF[] hf,
+ JxlImageF[] uhf,
+ JxlBlurTemp blurTemp)
+ {
+ const float sigmaUhf = 1.56416327805f;
+
+ int xSize = hf[0].XSize;
+ int ySize = hf[0].YSize;
+
+ uhf[0] = new JxlImageF(xSize, ySize);
+ uhf[1] = new JxlImageF(xSize, ySize);
+
+ int lanes = Vector.Count;
+
+ for (int i = 0; i < 2; i++)
+ {
+ for (int y = 0; y < ySize; y++)
+ {
+ Span rowUhf = uhf[i].GetRow(y);
+ Span rowHf = hf[i].GetRow(y);
+
+ for (int x = 0; x < xSize; x += lanes)
+ {
+ new Vector(rowHf.Slice(x, lanes)).CopyTo(rowUhf.Slice(x, lanes));
+ }
+ }
+
+ if (!Blur(hf[i], sigmaUhf, parameters, blurTemp, hf[i]))
+ {
+ return false;
+ }
+
+ if (i == 0)
+ {
+ const float removeHfRange = 1.5f;
+ const float removeUhfRange = 0.04f;
+
+ for (int y = 0; y < ySize; y++)
+ {
+ Span rowUhf = uhf[0].GetRow(y);
+ Span rowHf = hf[0].GetRow(y);
+
+ for (int x = 0; x < xSize; x += lanes)
+ {
+ Vector hfv = new(rowHf.Slice(x, lanes));
+ Vector uhfv = new Vector(rowUhf.Slice(x, lanes)) - hfv;
+
+ hfv = RemoveRangeAroundZero(hfv, removeHfRange);
+ uhfv = RemoveRangeAroundZero(uhfv, removeUhfRange);
+
+ hfv.CopyTo(rowHf.Slice(x, lanes));
+ uhfv.CopyTo(rowUhf.Slice(x, lanes));
+ }
+ }
+ }
+ else
+ {
+ const float addHfRange = 0.132f;
+ const float maxClampHf = 28.4691806922f;
+ const float maxClampUhf = 5.19175294647f;
+ const float mulYHf = 2.155f;
+ const float mulYUhf = 2.69313763794f;
+
+ Vector mulHf = new(mulYHf);
+ Vector mulUhf = new(mulYUhf);
+
+ for (int y = 0; y < ySize; y++)
+ {
+ Span rowUhf = uhf[1].GetRow(y);
+ Span rowHf = hf[1].GetRow(y);
+
+ for (int x = 0; x < xSize; x += lanes)
+ {
+ Vector hfv = new(rowHf.Slice(x, lanes));
+ hfv = MaximumClamp(hfv, maxClampHf);
+
+ Vector uhfv = new Vector(rowUhf.Slice(x, lanes)) - hfv;
+
+ uhfv = MaximumClamp(uhfv, maxClampUhf);
+ uhfv *= mulUhf;
+
+ uhfv.CopyTo(rowUhf.Slice(x, lanes));
+
+ hfv *= mulHf;
+ hfv = AmplifyRangeAroundZero(hfv, addHfRange);
+
+ hfv.CopyTo(rowHf.Slice(x, lanes));
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public static void DeallocateHFAndUHF(JxlImageF[] hf, JxlImageF[] uhf)
+ {
+ for (int i = 0; i < 2; i++)
+ {
+ hf[i] = new JxlImageF();
+ uhf[i] = new JxlImageF();
+ }
+ }
+
+ public static bool SeparateFrequencies(
+ Configuration configuration,
+ in ButteraugliParameters parameters,
+ BlurTemp blurTemp,
+ JxlImage3F xyb,
+ PsychoImage ps)
+ {
+ ps.Lf = JxlImage3F.Create(
+ configuration,
+ xyb.XSize,
+ xyb.YSize);
+
+ ps.Mf = JxlImage3F.Create(
+ configuration,
+ xyb.XSize,
+ xyb.YSize);
+
+ if (!SeparateLFAndMF(
+ parameters,
+ xyb,
+ ps.Lf,
+ ps.Mf,
+ blurTemp))
+ {
+ return false;
+ }
+
+ if (!SeparateMfAndHf(
+ parameters,
+ ps.Mf,
+ ps.Hf,
+ blurTemp))
+ {
+ return false;
+ }
+
+ if (!SeparateHFAndUHF(
+ parameters,
+ ps.Hf,
+ ps.Uhf,
+ blurTemp))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector Sum(
+ Vector a,
+ Vector b,
+ Vector c,
+ Vector d)
+ => (a + b) + (c + d);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector Sum(
+ Vector a,
+ Vector b,
+ Vector c,
+ Vector d,
+ Vector e)
+ => Sum(a, b, c, d + e);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector Sum(
+ Vector a,
+ Vector b,
+ Vector c,
+ Vector d,
+ Vector e,
+ Vector f,
+ Vector g)
+ => Sum(a, b, c, Sum(d, e, f, g));
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector Sum(
+ Vector a,
+ Vector b,
+ Vector c,
+ Vector d,
+ Vector e,
+ Vector f,
+ Vector g,
+ Vector h,
+ Vector i)
+ => (Sum(a, b, c, d) + Sum(e, f, g, h)) + i;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector MaltaUnitLF(
+ ReadOnlySpan row,
+ int index,
+ int xs)
+ {
+ // helper
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static Vector Load(ReadOnlySpan row, int index)
+ => new(row.Slice(index, Vector.Count));
+
+ int xs3 = 3 * xs;
+
+ Vector center = Load(row, index);
+
+ Vector sumYConst = Sum(
+ Load(row, index - 4),
+ Load(row, index - 2),
+ center,
+ Load(row, index + 2),
+ Load(row, index + 4));
+
+ Vector retval = sumYConst * sumYConst;
+
+ Vector sum;
+
+ sum = Sum(
+ Load(row, index - xs3 - xs),
+ Load(row, index - xs - xs),
+ center,
+ Load(row, index + xs + xs),
+ Load(row, index + xs3 + xs));
+ retval = (sum * sum) + retval;
+
+ sum = Sum(
+ Load(row, index - xs3 - 3),
+ Load(row, index - xs - xs - 2),
+ center,
+ Load(row, index + xs + xs + 2),
+ Load(row, index + xs3 + 3));
+ retval = (sum * sum) + retval;
+
+ sum = Sum(
+ Load(row, index - xs3 + 3),
+ Load(row, index - xs - xs + 2),
+ center,
+ Load(row, index + xs + xs - 2),
+ Load(row, index + xs3 - 3));
+ retval = (sum * sum) + retval;
+
+ sum = Sum(
+ Load(row, index - xs3 - xs + 1),
+ Load(row, index - xs - xs + 1),
+ center,
+ Load(row, index + xs + xs - 1),
+ Load(row, index + xs3 + xs - 1));
+ retval = (sum * sum) + retval;
+
+ sum = Sum(
+ Load(row, index - xs3 - xs - 1),
+ Load(row, index - xs - xs - 1),
+ center,
+ Load(row, index + xs + xs + 1),
+ Load(row, index + xs3 + xs + 1));
+ retval = (sum * sum) + retval;
+
+ sum = Sum(
+ Load(row, index - 4 - xs),
+ Load(row, index - 2 - xs),
+ center,
+ Load(row, index + 2 + xs),
+ Load(row, index + 4 + xs));
+ retval = (sum * sum) + retval;
+
+ sum = Sum(
+ Load(row, index - 4 + xs),
+ Load(row, index - 2 + xs),
+ center,
+ Load(row, index + 2 - xs),
+ Load(row, index + 4 - xs));
+ retval = (sum * sum) + retval;
+
+ sum = Sum(
+ Load(row, index - xs3 - 2),
+ Load(row, index - xs - xs - 1),
+ center,
+ Load(row, index + xs + xs + 1),
+ Load(row, index + xs3 + 2));
+ retval = (sum * sum) + retval;
+
+ sum = Sum(
+ Load(row, index - xs3 + 2),
+ Load(row, index - xs - xs + 1),
+ center,
+ Load(row, index + xs + xs - 1),
+ Load(row, index + xs3 - 2));
+ retval = (sum * sum) + retval;
+
+ sum = Sum(
+ Load(row, index - xs - xs - 3),
+ Load(row, index - xs - 2),
+ center,
+ Load(row, index + xs + 2),
+ Load(row, index + xs + xs + 3));
+ retval = (sum * sum) + retval;
+
+ sum = Sum(
+ Load(row, index - xs - xs + 3),
+ Load(row, index - xs + 2),
+ center,
+ Load(row, index + xs - 2),
+ Load(row, index + xs + xs - 3));
+ retval = (sum * sum) + retval;
+
+ sum = Sum(
+ Load(row, index + xs + xs - 4),
+ Load(row, index + xs - 2),
+ center,
+ Load(row, index - xs + 2),
+ Load(row, index - xs - xs + 4));
+ retval = (sum * sum) + retval;
+
+ sum = Sum(
+ Load(row, index - xs - xs - 4),
+ Load(row, index - xs - 2),
+ center,
+ Load(row, index + xs + 2),
+ Load(row, index + xs + xs + 4));
+ retval = (sum * sum) + retval;
+
+ sum = Sum(
+ Load(row, index - xs3 - xs - 2),
+ Load(row, index - xs - xs - 1),
+ center,
+ Load(row, index + xs + xs + 1),
+ Load(row, index + xs3 + xs + 2));
+ retval = (sum * sum) + retval;
+
+ sum = Sum(
+ Load(row, index - xs3 - xs + 2),
+ Load(row, index - xs - xs + 1),
+ center,
+ Load(row, index + xs + xs - 1),
+ Load(row, index + xs3 + xs - 2));
+ retval = (sum * sum) + retval;
+
+ return retval;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector MaltaUnit(
+ ReadOnlySpan row,
+ int index,
+ int xs)
+ {
+ // helper
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static Vector Load(ReadOnlySpan row, int index)
+ => new(row.Slice(index, Vector.Count));
+
+ int xs3 = 3 * xs;
+
+ Vector center = Load(row, index);
+
+ Vector sumYConst = Sum(
+ Load(row, index - 4),
+ Load(row, index - 3),
+ Load(row, index - 2),
+ Load(row, index - 1),
+ center,
+ Load(row, index + 1),
+ Load(row, index + 2),
+ Load(row, index + 3),
+ Load(row, index + 4));
+
+ Vector retval = sumYConst * sumYConst;
+
+ Vector sum;
+
+ sum = Sum(
+ Load(row, index - xs3 - xs),
+ Load(row, index - xs3),
+ Load(row, index - xs - xs),
+ Load(row, index - xs),
+ center,
+ Load(row, index + xs),
+ Load(row, index + xs + xs),
+ Load(row, index + xs3),
+ Load(row, index + xs3 + xs));
+ retval += sum * sum;
+
+ sum = Sum(
+ Load(row, index - xs3 - 3),
+ Load(row, index - xs - xs - 2),
+ Load(row, index - xs - 1),
+ center,
+ Load(row, index + xs + 1),
+ Load(row, index + xs + xs + 2),
+ Load(row, index + xs3 + 3));
+ retval += sum * sum;
+
+ sum = Sum(
+ Load(row, index - xs3 + 3),
+ Load(row, index - xs - xs + 2),
+ Load(row, index - xs + 1),
+ center,
+ Load(row, index + xs - 1),
+ Load(row, index + xs + xs - 2),
+ Load(row, index + xs3 - 3));
+ retval += sum * sum;
+
+ sum = Sum(
+ Load(row, index - xs3 - xs + 1),
+ Load(row, index - xs3 + 1),
+ Load(row, index - xs - xs + 1),
+ Load(row, index - xs),
+ center,
+ Load(row, index + xs),
+ Load(row, index + xs + xs - 1),
+ Load(row, index + xs3 - 1),
+ Load(row, index + xs3 + xs - 1));
+ retval += sum * sum;
+
+ sum = Sum(
+ Load(row, index - xs3 - xs - 1),
+ Load(row, index - xs3 - 1),
+ Load(row, index - xs - xs - 1),
+ Load(row, index - xs),
+ center,
+ Load(row, index + xs),
+ Load(row, index + xs + xs + 1),
+ Load(row, index + xs3 + 1),
+ Load(row, index + xs3 + xs + 1));
+ retval += sum * sum;
+
+ sum = Sum(
+ Load(row, index - 4 - xs),
+ Load(row, index - 3 - xs),
+ Load(row, index - 2 - xs),
+ Load(row, index - 1),
+ center,
+ Load(row, index + 1),
+ Load(row, index + 2 + xs),
+ Load(row, index + 3 + xs),
+ Load(row, index + 4 + xs));
+ retval += sum * sum;
+
+ sum = Sum(
+ Load(row, index - 4 + xs),
+ Load(row, index - 3 + xs),
+ Load(row, index - 2 + xs),
+ Load(row, index - 1),
+ center,
+ Load(row, index + 1),
+ Load(row, index + 2 - xs),
+ Load(row, index + 3 - xs),
+ Load(row, index + 4 - xs));
+ retval += sum * sum;
+
+ sum = Sum(
+ Load(row, index - xs3 - 2),
+ Load(row, index - xs - xs - 1),
+ Load(row, index - xs - 1),
+ center,
+ Load(row, index + xs + 1),
+ Load(row, index + xs + xs + 1),
+ Load(row, index + xs3 + 2));
+ retval += sum * sum;
+
+ sum = Sum(
+ Load(row, index - xs3 + 2),
+ Load(row, index - xs - xs + 1),
+ Load(row, index - xs + 1),
+ center,
+ Load(row, index + xs - 1),
+ Load(row, index + xs + xs - 1),
+ Load(row, index + xs3 - 2));
+ retval += sum * sum;
+
+ sum = Sum(
+ Load(row, index - xs - xs - 3),
+ Load(row, index - xs - 2),
+ Load(row, index - xs - 1),
+ center,
+ Load(row, index + xs + 1),
+ Load(row, index + xs + 2),
+ Load(row, index + xs + xs + 3));
+ retval += sum * sum;
+
+ sum = Sum(
+ Load(row, index - xs - xs + 3),
+ Load(row, index - xs + 2),
+ Load(row, index - xs + 1),
+ center,
+ Load(row, index + xs - 1),
+ Load(row, index + xs - 2),
+ Load(row, index + xs + xs - 3));
+ retval += sum * sum;
+
+ sum = Sum(
+ Load(row, index + xs - 4),
+ Load(row, index + xs - 3),
+ Load(row, index + xs - 2),
+ Load(row, index - 1),
+ center,
+ Load(row, index + 1),
+ Load(row, index - xs + 2),
+ Load(row, index - xs + 3),
+ Load(row, index - xs + 4));
+ retval += sum * sum;
+
+ sum = Sum(
+ Load(row, index - xs - 4),
+ Load(row, index - xs - 3),
+ Load(row, index - xs - 2),
+ Load(row, index - 1),
+ center,
+ Load(row, index + 1),
+ Load(row, index + xs + 2),
+ Load(row, index + xs + 3),
+ Load(row, index + xs + 4));
+ retval += sum * sum;
+
+ sum = Sum(
+ Load(row, index - xs3 - xs - 1),
+ Load(row, index - xs3 - 1),
+ Load(row, index - xs - xs - 1),
+ Load(row, index - xs),
+ center,
+ Load(row, index + xs),
+ Load(row, index + xs + xs + 1),
+ Load(row, index + xs3 + 1),
+ Load(row, index + xs3 + xs + 1));
+ retval += sum * sum;
+
+ sum = Sum(
+ Load(row, index - xs3 - xs + 1),
+ Load(row, index - xs3 + 1),
+ Load(row, index - xs - xs + 1),
+ Load(row, index - xs),
+ center,
+ Load(row, index + xs),
+ Load(row, index + xs + xs - 1),
+ Load(row, index + xs3 - 1),
+ Load(row, index + xs3 + xs - 1));
+ retval += sum * sum;
+
+ return retval;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static float PaddedMaltaUnit(
+ JxlImageF diffs,
+ int x0,
+ int y0,
+ bool isLF)
+ {
+ if (x0 >= 4 &&
+ y0 >= 4 &&
+ x0 < diffs.XSize - 4 &&
+ y0 < diffs.YSize - 4)
+ {
+ return isLF
+ ? MaltaUnitLF(
+ diffs.GetRow(y0),
+ x0,
+ diffs.PixelsPerRow)[0]
+ : MaltaUnit(
+ diffs.GetRow(y0),
+ x0,
+ diffs.PixelsPerRow)[0];
+ }
+
+ Span borderImage = stackalloc float[12 * 9];
+
+ for (int dy = 0; dy < 9; dy++)
+ {
+ int y = y0 + dy - 4;
+
+ if (y < 0 || y >= diffs.YSize)
+ {
+ borderImage.Slice(dy * 12, 12).Clear();
+ continue;
+ }
+
+ ReadOnlySpan rowDiffs = diffs.GetRow(y);
+
+ for (int dx = 0; dx < 9; dx++)
+ {
+ int x = x0 + dx - 4;
+
+ borderImage[(dy * 12) + dx] =
+ x < 0 || x >= diffs.XSize
+ ? 0.0f
+ : rowDiffs[x];
+ }
+
+ borderImage.Slice((dy * 12) + 9, 3).Clear();
+ }
+
+ return isLF
+ ? MaltaUnitLF(
+ diffs.GetRow(y0),
+ x0,
+ diffs.PixelsPerRow)[0]
+ : MaltaUnit(
+ borderImage,
+ (4 * 12) + 4,
+ 12)[0];
+ }
+
+ public static bool MaltaDiffMap(
+ bool isLf,
+ JxlImageF lum0,
+ JxlImageF lum1,
+ float w0Gt1,
+ float w0Lt1,
+ float norm1,
+ float len,
+ float mulli,
+ JxlImageF diffs,
+ JxlImageF blockDiffAc)
+ {
+ if (!SameSize(lum0, lum1) || !SameSize(lum0, diffs))
+ {
+ return false;
+ }
+
+ int width = lum0.XSize;
+ int height = lum0.YSize;
+
+ const float weight0 = 0.5f;
+ const float weight1 = 0.33f;
+
+ float norm2_0Gt1 =
+ (float)(mulli * MathF.Sqrt(weight0 * w0Gt1) / ((len * 2) + 1) * norm1);
+
+ float norm2_0Lt1 =
+ (float)(mulli * MathF.Sqrt(weight1 * w0Lt1) / ((len * 2) + 1) * norm1);
+
+ for (int y = 0; y < height; y++)
+ {
+ ReadOnlySpan row0 = lum0.GetRow(y);
+ ReadOnlySpan row1 = lum1.GetRow(y);
+ Span rowDiffs = diffs.GetRow(y);
+
+ for (int x = 0; x < width; x++)
+ {
+ float absVal = 0.5f *
+ (MathF.Abs(row0[x]) + MathF.Abs(row1[x]));
+
+ float diff = row0[x] - row1[x];
+
+ float scaler = norm2_0Gt1 / ((float)norm1 + absVal);
+
+ rowDiffs[x] = scaler * diff;
+
+ float scaler2 = norm2_0Lt1 / ((float)norm1 + absVal);
+
+ float fabs0 = MathF.Abs(row0[x]);
+
+ float tooSmall = 0.55f * fabs0;
+ float tooBig = 1.05f * fabs0;
+
+ if (row0[x] < 0)
+ {
+ if (row1[x] > -tooSmall)
+ {
+ rowDiffs[x] -= scaler2 * (row1[x] + tooSmall);
+ }
+ else if (row1[x] < -tooBig)
+ {
+ rowDiffs[x] += scaler2 * (-row1[x] - tooBig);
+ }
+ }
+ else
+ {
+ if (row1[x] < tooSmall)
+ {
+ rowDiffs[x] += scaler2 * (tooSmall - row1[x]);
+ }
+ else if (row1[x] > tooBig)
+ {
+ rowDiffs[x] -= scaler2 * (row1[x] - tooBig);
+ }
+ }
+ }
+ }
+
+ int y0 = 0;
+
+ for (; y0 < 4; y0++)
+ {
+ Span row = blockDiffAc.GetRow(y0);
+
+ for (int x = 0; x < width; x++)
+ {
+ row[x] += PaddedMaltaUnit(diffs, x, y0, isLf);
+ }
+ }
+
+ int lanes = Vector.Count;
+ int alignedX = Math.Max(4, lanes);
+
+ int stride = diffs.PixelsPerRow;
+
+ for (; y0 < height - 4; y0++)
+ {
+ ReadOnlySpan input = diffs.GetRow(y0);
+ Span output = blockDiffAc.GetRow(y0);
+ ref float outputReference = ref MemoryMarshal.GetReference(output);
+
+ int x = 0;
+
+ for (; x < alignedX; x++)
+ {
+ output[x] += PaddedMaltaUnit(diffs, x, y0, isLf);
+ }
+
+ for (; x + lanes + 4 <= width; x += lanes)
+ {
+ Vector value = Vector.LoadUnsafe(ref Unsafe.Add(ref outputReference, x));
+
+ Vector