diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcContext.cs b/src/ImageSharp/Formats/Jxl/Ac/JxlAcContext.cs
new file mode 100644
index 0000000000..98f86534b2
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Ac/JxlAcContext.cs
@@ -0,0 +1,53 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+#pragma warning disable SA1405 // Debug.Assert should provide message text
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Ac;
+
+///
+/// AC context
+///
+internal static class JxlAcContext
+{
+ public const int DctOrderContextStart = 0;
+ public const int NonZeroBuckets = 37;
+ public const int ZeroDensityContextCount = 458;
+ public const int ZeroDensityContextLimit = 474;
+
+ public static ReadOnlySpan CoefficientFrequencyContext =>
+ [
+ 0xBAD, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22,
+ 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26,
+ 27, 27, 27, 27, 28, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30,
+ ];
+
+ public static ReadOnlySpan CoefficientNumNonzeroContext =>
+ [
+ 0xBAD, 0, 31, 62, 62, 93, 93, 93, 93, 123, 123, 123, 123,
+ 152, 152, 152, 152, 152, 152, 152, 152, 180, 180, 180, 180, 180,
+ 180, 180, 180, 180, 180, 180, 180, 206, 206, 206, 206, 206, 206,
+ 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206,
+ 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206,
+ ];
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int ZeroDensityContext(int nonZeroesLeft, int k, int coveredBlocks, int log2CoveredBlocks, int prev)
+ {
+ Debug.Assert((1 << log2CoveredBlocks) == coveredBlocks);
+
+ nonZeroesLeft = (nonZeroesLeft + coveredBlocks - 1) >> log2CoveredBlocks;
+ k >>= log2CoveredBlocks;
+
+ Debug.Assert(k > 0);
+ Debug.Assert(k < 64);
+ Debug.Assert(nonZeroesLeft > 0);
+ Debug.Assert(nonZeroesLeft < 64);
+
+ return ((CoefficientNumNonzeroContext[nonZeroesLeft] + CoefficientFrequencyContext[k]) * 2) + prev;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategy.cs b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategy.cs
new file mode 100644
index 0000000000..7c704d8e91
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategy.cs
@@ -0,0 +1,186 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using static SixLabors.ImageSharp.Formats.Jxl.JxlFrameDimensions;
+
+#pragma warning disable SA1405 // Debug.Assert should provide message text
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Ac;
+
+[StructLayout(LayoutKind.Sequential, Pack = 8)]
+internal struct JxlAcStrategy
+{
+ public const int MaximumCoefficientBlocks = 32;
+ public const int MaximumBlockDimension = BlockDimensions * MaximumCoefficientBlocks;
+ public const int MaximumCoefficientArea = MaximumBlockDimension * MaximumBlockDimension;
+ public const int NumberOfValidStrategies = 27;
+
+ private static readonly int MultiblockBits =
+ GetTypeBit(JxlAcStrategyType.DCT16X16) | GetTypeBit(JxlAcStrategyType.DCT32X32) |
+ GetTypeBit(JxlAcStrategyType.DCT16X8) | GetTypeBit(JxlAcStrategyType.DCT8X16) |
+ GetTypeBit(JxlAcStrategyType.DCT32X8) | GetTypeBit(JxlAcStrategyType.DCT8X32) |
+ GetTypeBit(JxlAcStrategyType.DCT16X32) | GetTypeBit(JxlAcStrategyType.DCT32X16) |
+ GetTypeBit(JxlAcStrategyType.DCT32X64) | GetTypeBit(JxlAcStrategyType.DCT64X32) |
+ GetTypeBit(JxlAcStrategyType.DCT64X64) | GetTypeBit(JxlAcStrategyType.DCT64X128) |
+ GetTypeBit(JxlAcStrategyType.DCT128X64) |
+ GetTypeBit(JxlAcStrategyType.DCT128X128) |
+ GetTypeBit(JxlAcStrategyType.DCT128X256) |
+ GetTypeBit(JxlAcStrategyType.DCT256X128) |
+ GetTypeBit(JxlAcStrategyType.DCT256X256);
+
+ private readonly bool isFirst;
+
+ public JxlAcStrategy(JxlAcStrategyType strategy, bool isFirst)
+ {
+ this.Strategy = strategy;
+ this.isFirst = isFirst;
+
+ Debug.Assert(this.IsMultiblock);
+ }
+
+ public JxlAcStrategy(JxlAcStrategyType strategy)
+ : this(strategy, true)
+ {
+ }
+
+ public JxlAcStrategy(int rawStrategy)
+ : this((JxlAcStrategyType)rawStrategy)
+ {
+ }
+
+ private static ReadOnlySpan CoveredBlocksXLookup =>
+ [
+ 1, 1, 1, 1, 2, 4, 1, 2, 1,
+ 4, 2, 4, 1, 1, 1, 1, 1, 1,
+ 8, 4, 8, 16, 8, 16, 32, 16, 32
+ ];
+
+ private static ReadOnlySpan CoveredBlocksYLookup =>
+ [
+ 1, 1, 1, 1, 2, 4, 2, 1, 4,
+ 1, 4, 2, 1, 1, 1, 1, 1, 1,
+ 8, 8, 4, 16, 16, 8, 32, 32, 16
+ ];
+
+ private static ReadOnlySpan Log2CoveredBlocksLookup =>
+ [
+ 0, 0, 0, 0, 2, 4, 1, 1, 2,
+ 2, 3, 3, 0, 0, 0, 0, 0, 0,
+ 6, 5, 5, 8, 7, 7, 10, 9, 9
+ ];
+
+ public readonly bool IsMultiblock => ((1 << (int)this.Strategy) & MultiblockBits) != 0;
+
+ public readonly int RawStrategy => (int)this.Strategy;
+
+ public readonly int CoveredBlocksX => CoveredBlocksXLookup[(int)this.Strategy];
+
+ public readonly int CoveredBlocksY => CoveredBlocksYLookup[(int)this.Strategy];
+
+ public readonly int Log2CoveredBlocks => Log2CoveredBlocksLookup[(int)this.Strategy];
+
+ public readonly JxlAcStrategyType Strategy { get; }
+
+ public void ComputeNaturalCoefficientOrder(ref int order) => CoefficientOrderAndLookup(this, false, ref order);
+
+ public void ComputeNaturalCoefficientOrderLookup(ref int lookup) => CoefficientOrderAndLookup(this, true, ref lookup);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int GetTypeBit(JxlAcStrategyType type) => 1 << (int)type;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsRawStrategyValid(int rawStrategy) => rawStrategy is < NumberOfValidStrategies and >= 0;
+
+ private static void CoefficientOrderAndLookup(JxlAcStrategy strategy, bool isLookup, Span output)
+ {
+ // TODO: CoefficientLayout
+ // TODO: CeilLog2Nonzero
+ int cx = strategy.CoveredBlocksX;
+ int cy = strategy.CoveredBlocksY;
+
+ CoefficientLayout(ref cx, ref cy);
+
+ int xs = cx / cy;
+ int xsm = xs - 1;
+ int xss = CeilLog2Nonzero(xs);
+ int cur = cx * cy;
+
+ for (int i = 0; i < cx * BlockDimensions; i++)
+ {
+ for (int j = 0; j <= i; j++)
+ {
+ int x = j;
+ int y = i - j;
+
+ if ((i & 1) == 0)
+ {
+ // swap
+ (x, y) = (y, x);
+ }
+
+ if ((y & xsm) != 0)
+ {
+ continue;
+ }
+
+ y >>= xss;
+ int value = 0;
+
+ if (x < cx && y < cy)
+ {
+ value = (y * cx) + x;
+ }
+ else
+ {
+ value = cur++;
+ }
+
+ if (isLookup)
+ {
+ output[((y * cx) * BlockDimensions) + x] = value;
+ }
+ else
+ {
+ output[value] = ((y * cx) * BlockDimensions) + x;
+ }
+ }
+ }
+
+ for (int ip = (cx * BlockDimensions) - 1; ip > 0; ip--)
+ {
+ int i = ip - 1;
+
+ for (int j = 0; j <= i; j++)
+ {
+ int x = (cx * BlockDimensions) - 1 - (i - j);
+ int y = (cx * BlockDimensions) - 1 - j;
+
+ if ((i & 1) != 0)
+ {
+ // swap
+ (x, y) = (y, x);
+ }
+
+ if ((y & xsm) != 0)
+ {
+ continue;
+ }
+
+ y >>= xss;
+ int value = cur++;
+
+ if (isLookup)
+ {
+ output[((y * cx) * BlockDimensions) + x] = value;
+ }
+ else
+ {
+ output[value] = ((y * cx) * BlockDimensions) + x;
+ }
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs
new file mode 100644
index 0000000000..0b2bdb74ae
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs
@@ -0,0 +1,119 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Ac;
+
+internal sealed class JxlAcStrategyImage : IDisposable
+{
+ private const byte Invalid = byte.MaxValue; // 255
+
+ private JxlImageB? layers;
+ private Memory row;
+ private int stride;
+
+ public int XSize => this.layers!.XSize;
+
+ public int YSize => this.layers!.YSize;
+
+ public int PixelsPerRow => this.layers!.PixelsPerRow;
+
+ public JxlAcStrategyRow GetRow(int y, int xPrefix = 0)
+ {
+ ReadOnlyMemory layerRow = this.layers!.GetRowBytesMemory(y);
+ ReadOnlyMemory row = layerRow[xPrefix..];
+
+ return new JxlAcStrategyRow(row);
+ }
+
+ public static JxlAcStrategyImage Create(Configuration memoryManager, int xSize, int ySize)
+ {
+ JxlAcStrategyImage image = new()
+ {
+ layers = new JxlImageB(memoryManager, xSize, ySize)
+ };
+
+ image.row = image.layers.GetRowBytesMemory(0);
+ image.stride = image.layers.PixelsPerRow;
+
+ return image;
+ }
+
+ public int CountBlocks(JxlAcStrategyType type)
+ {
+ int value = 0;
+ int compare = ((int)type << 1) | 1;
+
+ for (int y = 0; y < this.layers!.YSize; y++)
+ {
+ ReadOnlySpan row = this.layers!.GetRow(y);
+
+ for (int x = 0; x < this.layers!.XSize; x++)
+ {
+ if (row[x] == compare)
+ {
+ value++;
+ }
+ }
+ }
+
+ return value;
+ }
+
+ public JxlAcStrategyRow GetRow(in Rectangle rect, int y) => this.GetRow(rect.Y + y, rect.X);
+
+ public bool IsValid(int x, int y) => this.row.Span[(y * this.stride) + x] != Invalid;
+
+ public bool SetNoBoundsChecks(int x, int y, JxlAcStrategyType type, bool check = true)
+ {
+ JxlAcStrategy strategy = new(type);
+ Span rowSpan = this.row.Span;
+ int rawType = (int)type;
+ int rawTypeTimes2 = rawType << 1;
+
+ for (int iy = 0; iy < strategy.CoveredBlocksX; iy++)
+ {
+ for (int ix = 0; ix < strategy.CoveredBlocksX; ix++)
+ {
+ int pos = ((y + iy) * this.stride) + x + ix;
+
+ if (check && rowSpan[pos] != Invalid)
+ {
+ Debug.Fail("Invalid AC strategy. Blocks overlap.");
+
+ return false;
+ }
+
+ rowSpan[pos] = (byte)(rawTypeTimes2 | ((iy | ix) == 0 ? 1 : 0));
+ }
+ }
+
+ return true;
+ }
+
+ public bool Set(int x, int y, JxlAcStrategyType type)
+ {
+#if DEBUG
+ JxlAcStrategy strategy = new(type);
+
+ Debug.Assert(y + strategy.CoveredBlocksY <= this.layers!.YSize, "Invalid range");
+ Debug.Assert(x + strategy.CoveredBlocksX <= this.layers.XSize, "Invalid range");
+#endif
+
+ return this.SetNoBoundsChecks(x, y, type, check: false);
+ }
+
+ public void FillDct8(in Rectangle rect) => this.FillPlane(((int)JxlAcStrategyType.DCT << 1) | 1, this.layers, in rect);
+
+ public void FillDct8() => this.FillDct8(in this.layers.GetRectangle());
+
+ public void FillInvalid() => this.FillImage(Invalid, this.layers);
+
+ public void Dispose()
+ {
+ this.layers?.Dispose();
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyRow.cs b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyRow.cs
new file mode 100644
index 0000000000..668876f711
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyRow.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Ac;
+
+internal sealed class JxlAcStrategyRow
+{
+ private readonly ReadOnlyMemory row;
+
+ public JxlAcStrategyRow(ReadOnlyMemory row) => this.row = row;
+
+ public JxlAcStrategy this[int x]
+ {
+ get
+ {
+ ReadOnlySpan span = this.row.Span;
+
+ Debug.Assert(x * 8 < span.Length, "Too many bytes of memory were requested");
+
+ ref byte first = ref MemoryMarshal.GetReference(span);
+ JxlAcStrategyType strategy = (JxlAcStrategyType)(Unsafe.Add(ref Unsafe.As(ref first), x) >> 1);
+ bool isFirst = Unsafe.Add(ref first, x) != 0;
+
+ return new JxlAcStrategy(strategy, isFirst);
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyType.cs b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyType.cs
new file mode 100644
index 0000000000..3412d10f46
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyType.cs
@@ -0,0 +1,67 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Ac;
+
+internal enum JxlAcStrategyType : ushort
+{
+ // Regular block size DCT
+ DCT = 0,
+
+ // Encode pixels without transforming
+ IDENTITY = 1,
+
+ // Use 2-by-2 DCT
+ DCT2X2 = 2,
+
+ // Use 4-by-4 DCT
+ DCT4X4 = 3,
+
+ // Use 16-by-16 DCT
+ DCT16X16 = 4,
+
+ // Use 32-by-32 DCT
+ DCT32X32 = 5,
+
+ // Use 16-by-8 DCT
+ DCT16X8 = 6,
+
+ // Use 8-by-16 DCT
+ DCT8X16 = 7,
+
+ // Use 32-by-8 DCT
+ DCT32X8 = 8,
+
+ // Use 8-by-32 DCT
+ DCT8X32 = 9,
+
+ // Use 32-by-16 DCT
+ DCT32X16 = 10,
+
+ // Use 16-by-32 DCT
+ DCT16X32 = 11,
+
+ // 4x8 and 8x4 DCT
+ DCT4X8 = 12,
+ DCT8X4 = 13,
+
+ // Corner-DCT.
+ AFV0 = 14,
+
+ AFV1 = 15,
+ AFV2 = 16,
+ AFV3 = 17,
+
+ // Larger DCTs
+ DCT64X64 = 18,
+ DCT64X32 = 19,
+ DCT32X64 = 20,
+
+ // No transforms smaller than 64x64 are allowed below.
+ DCT128X128 = 21,
+ DCT128X64 = 22,
+ DCT64X128 = 23,
+ DCT256X256 = 24,
+ DCT256X128 = 25,
+ DCT128X256 = 26
+}
diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlBlockContextMap.cs b/src/ImageSharp/Formats/Jxl/Ac/JxlBlockContextMap.cs
new file mode 100644
index 0000000000..3b7e75605f
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Ac/JxlBlockContextMap.cs
@@ -0,0 +1,72 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Formats.Jxl.Coefficients;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Ac;
+
+internal sealed class JxlBlockContextMap
+{
+ public JxlBlockContextMap()
+ {
+ DefaultContextMap.CopyTo(this.ContextMap);
+ this.ContextCount = this.ContextMap.Max() + 1;
+ this.DcContextCount = 1;
+ }
+
+ private static ReadOnlySpan DefaultContextMap =>
+ [
+ 0, 1, 2, 2, 3, 3, 4, 5, 6, 6, 6, 6, 6,
+ 7, 8, 9, 9, 10, 11, 12, 13, 14, 14, 14, 14, 14,
+ 7, 8, 9, 9, 10, 11, 12, 13, 14, 14, 14, 14, 14,
+ ];
+
+ public List[] DcThresholds { get; } = [[], [], []];
+
+ public List QfThresholds { get; } = [];
+
+ public byte[] ContextMap { get; } = new byte[DefaultContextMap.Length];
+
+ public int ContextCount { get; set; }
+
+ public int DcContextCount { get; set; }
+
+ public int AcContextCount => (this.ContextCount * JxlAcContext.NonZeroBuckets) + JxlAcContext.ZeroDensityContextCount;
+
+ public int Context(int dcIndex, uint qf, int ord, int c)
+ {
+ int qfIndex = 0;
+ for (int i = 0; i < this.QfThresholds.Count; i++)
+ {
+ uint t = this.QfThresholds[i];
+
+ if (qf > t)
+ {
+ qfIndex++;
+ }
+ }
+
+ int idx = c < 2 ? c ^ 1 : 2;
+ idx = (idx * JxlForwardCoefficientOrder.OrderCount) + ord;
+ idx = (idx * (this.QfThresholds.Count + 1)) + qfIndex;
+ idx = (idx * this.DcContextCount) + dcIndex;
+ return this.ContextMap[idx];
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int ZeroDensityContextOffset(int blockContext) =>
+ (this.ContextCount * JxlAcContext.NonZeroBuckets) + JxlAcContext.ZeroDensityContextCount + blockContext;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int NonZeroContext(int nonZeroes, int blockContext)
+ {
+ if (nonZeroes >= 64)
+ {
+ nonZeroes = 64;
+ }
+
+ int ctx = nonZeroes < 8 ? nonZeroes : (4 + (nonZeroes / 2));
+ return (ctx * this.ContextCount) + blockContext;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Coefficients/JxlForwardCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Coefficients/JxlForwardCoefficientOrder.cs
new file mode 100644
index 0000000000..04cacb176a
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Coefficients/JxlForwardCoefficientOrder.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Coefficients;
+
+internal static class JxlForwardCoefficientOrder
+{
+ public const byte OrderCount = 13;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int CoefficientRows(int rows, int columns) => rows < columns ? rows : columns;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int CoefficientColumns(int rows, int columns) => rows < columns ? columns : rows;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void CoefficientLayout(ref int rows, ref int columns)
+ {
+ rows = CoefficientRows(rows, columns);
+ columns = CoefficientColumns(rows, columns);
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/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/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/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..9583f57dac
--- /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.
+
+using System.Diagnostics;
+
+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)
+ {
+ Debug.Assert(selector < 4, "Selector out of range");
+
+ return this.d[selector];
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsConstants.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsConstants.cs
new file mode 100644
index 0000000000..dcd244ac7d
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsConstants.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO;
+
+internal static class JxlAnsConstants
+{
+ public const int AnsLogTableSize = 12;
+ public const int AnsTableSize = 1 << AnsLogTableSize;
+ public const int AnsTabMask = AnsTableSize - 1;
+ public const int PrefixMaxAlphabetSize = 4096;
+ public const int AnsMaxAlphabetSize = 256;
+ public const int PrefixMaxBits = 15;
+ public const int AnsSignature = 0x13;
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs
new file mode 100644
index 0000000000..25a9d12609
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO;
+
+[StructLayout(LayoutKind.Sequential)]
+internal struct JxlAnsEntry
+{
+ public byte Cutoff;
+ public byte RightValue;
+ public ushort Frequency0;
+ public ushort Offsets1;
+ public ushort Frequency1XorFrequency0;
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs
new file mode 100644
index 0000000000..35630eb103
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs
@@ -0,0 +1,242 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO;
+
+internal static class JxlAnsHelper
+{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int GetPopulationCountPrecision(int logCount, int shift)
+ => Math.Max(0, Math.Min(logCount, shift - ((JxlAnsConstants.AnsLogTableSize - logCount) >> 1)));
+
+ // NOTE: The result may potentially be large, so prefer using a memory allocator
+ public static IMemoryOwner CreateFlatHistogram(Configuration configuration, int length, int totalCount)
+ {
+ Debug.Assert(length <= 0, "Length should be >= 0");
+ Debug.Assert(length > totalCount, "Length should be <= totalCount");
+
+ int count = totalCount / length;
+ IMemoryOwner result = configuration.MemoryAllocator.Allocate(length);
+ Span resultSpan = result.Memory.Span;
+ uint unsignedCount = (uint)count;
+
+ for (int i = 0; i < length; i++)
+ {
+ resultSpan[i] = unsignedCount;
+ }
+
+ int remCounts = totalCount % length;
+ for (int i = 0; i < remCounts; i++)
+ {
+ resultSpan[i]++;
+ }
+
+ return result;
+ }
+
+ public static JxlAnsSymbol Lookup(ReadOnlySpan table, int value, int logEntrySize, int entrySizeMinus1)
+ {
+ int i = value >> logEntrySize;
+ int pos = value & entrySizeMinus1;
+
+ JxlAnsEntry entry = table[i];
+
+ int cutoff = entry.Cutoff;
+ int rightValue = entry.RightValue;
+ int freq0 = entry.Frequency0;
+
+ bool greater = pos >= cutoff;
+
+ int offsets1or0 = greater ? entry.Offsets1 : 0;
+ int freq1xorfreq0or0 = greater ? entry.Frequency1XorFrequency0 : 0;
+
+ JxlAnsSymbol symbol = new()
+ {
+ Value = greater ? rightValue : i,
+ Offset = offsets1or0 + pos,
+ Frequency = freq0 ^ freq1xorfreq0or0
+ };
+
+ return symbol;
+ }
+
+ public static bool InitAliasTable(Span preDistribution, uint logRange, int logAlphaSize, Span entries)
+ {
+ int range = 1 << (int)logRange;
+ int tableSize = 1 << logAlphaSize;
+
+ Debug.Assert(tableSize <= range, "table_size must be <= range");
+
+ int distributionPointer = preDistribution.Length - 1;
+
+ while (distributionPointer >= 0 && preDistribution[distributionPointer] == 0)
+ {
+ distributionPointer--;
+ }
+
+ if (distributionPointer < 0)
+ {
+ preDistribution[0] = range;
+ distributionPointer = 0;
+ }
+
+ Span distribution = preDistribution[..(distributionPointer + 1)];
+
+ if (distribution.Length > tableSize)
+ {
+ Debug.Fail("Too many items in the distribution");
+
+ return false;
+ }
+
+ int entrySize = range >> logAlphaSize;
+ int singleSymbol = -1;
+ int sum = 0;
+
+ for (int sym = 0; sym < distribution.Length; sym++)
+ {
+ int value = distribution[sym];
+ sum += value;
+
+ if (value == JxlAnsConstants.AnsTableSize)
+ {
+ if (singleSymbol != -1)
+ {
+ return false;
+ }
+
+ singleSymbol = sym;
+ }
+ }
+
+ if (sum != range)
+ {
+ return false;
+ }
+
+ if (singleSymbol != -1)
+ {
+ byte sym = (byte)singleSymbol;
+ if (singleSymbol != sym)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < tableSize; i++)
+ {
+ ref JxlAnsEntry jxlEntry = ref entries[i];
+
+ jxlEntry.RightValue = sym;
+ jxlEntry.Cutoff = 0;
+ jxlEntry.Offsets1 = (ushort)(entrySize * i);
+ jxlEntry.Frequency0 = 0;
+ jxlEntry.Frequency1XorFrequency0 = JxlAnsConstants.AnsTableSize;
+ }
+
+ return true;
+ }
+
+ Span underfullPosn = stackalloc uint[distribution.Length];
+ Span overfullPosn = stackalloc uint[distribution.Length];
+ Span cutoffs = stackalloc uint[1 << logAlphaSize];
+
+ int underfullPointer = 0;
+ int overfullPointer = 0;
+
+ for (int i = 0; i < distribution.Length; i++)
+ {
+ uint currentCutoff = (uint)distribution[i];
+
+ cutoffs[i] = currentCutoff;
+
+ if (currentCutoff > entrySize)
+ {
+ overfullPosn[overfullPointer] = (uint)i;
+ overfullPointer++;
+ }
+ else if (currentCutoff < entrySize)
+ {
+ underfullPosn[underfullPointer] = (uint)i;
+ underfullPointer++;
+ }
+ }
+
+ for (int i = distribution.Length; i < tableSize; i++)
+ {
+ cutoffs[i] = 0;
+ underfullPosn[underfullPointer] = (uint)i;
+ underfullPointer++;
+ }
+
+ uint unsignedEntrySize = (uint)entrySize;
+
+ while (overfullPointer >= 0)
+ {
+ uint overfullIndex = overfullPosn[overfullPointer];
+ overfullPointer--;
+
+ if (underfullPointer <= -1)
+ {
+ return false;
+ }
+
+ uint underfullIndex = underfullPosn[underfullPointer];
+ underfullPointer--;
+
+ int signedOverfullIndex = (int)overfullIndex;
+ int signedUnderfullIndex = (int)underfullIndex;
+
+ uint underfullBy = unsignedEntrySize - cutoffs[signedUnderfullIndex];
+ cutoffs[signedOverfullIndex] -= underfullBy;
+
+ ref JxlAnsEntry currentEntry = ref entries[signedUnderfullIndex];
+
+ currentEntry.RightValue = unchecked((byte)overfullIndex);
+ currentEntry.Offsets1 = unchecked((ushort)cutoffs[signedOverfullIndex]);
+
+ uint currentCutoff = cutoffs[signedOverfullIndex];
+
+ if (currentCutoff < entrySize)
+ {
+ underfullPosn[underfullPointer] = overfullIndex;
+ underfullPointer++;
+ }
+ else if (currentCutoff > entrySize)
+ {
+ overfullPosn[overfullPointer] = overfullIndex;
+ overfullPointer++;
+ }
+ }
+
+ for (uint i = 0; i < tableSize; i++)
+ {
+ uint currentCutoff = cutoffs[(int)i];
+ ref JxlAnsEntry entry = ref entries[(int)i];
+
+ if (currentCutoff == entrySize)
+ {
+ entry.RightValue = (byte)i;
+ entry.Offsets1 = 0;
+ entry.Cutoff = 0;
+ }
+ else
+ {
+ entry.Offsets1 -= (ushort)currentCutoff;
+ entry.Cutoff = (byte)currentCutoff;
+ }
+
+ int freq0 = i < distribution.Length ? distribution[(int)i] : 0;
+ int i1 = entry.RightValue;
+ int freq1 = i1 < distribution.Length ? distribution[i1] : 0;
+
+ entry.Frequency0 = (ushort)freq0;
+ entry.Frequency1XorFrequency0 = (ushort)(freq1 ^ freq0);
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHybridUIntConfiguration.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHybridUIntConfiguration.cs
new file mode 100644
index 0000000000..422eb2323f
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHybridUIntConfiguration.cs
@@ -0,0 +1,60 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO;
+
+internal sealed class JxlAnsHybridUIntConfiguration : IJxlFields
+{
+ public JxlAnsHybridUIntConfiguration(uint splitExponent = 4, uint msbInToken = 2, uint lsbInToken = 0)
+ {
+ this.SplitExponent = splitExponent;
+ this.SplitToken = 1u << (int)splitExponent;
+ this.MsbInToken = msbInToken;
+ this.LsbInToken = lsbInToken;
+
+ Debug.Assert(splitExponent >= msbInToken + lsbInToken, "Split exponent should be < msbInToken + lsbInToken");
+ }
+
+ public uint SplitExponent { get; set; }
+
+ public uint SplitToken { get; set; }
+
+ public uint MsbInToken { get; set; } // Most significant bit
+
+ public uint LsbInToken { get; set; } // Least significant bit
+
+ public uint LsbMask => (1u << (int)this.LsbInToken) - 1;
+
+ public void Encode(uint value, ref uint token, ref uint bitCount, ref uint bits)
+ {
+ if (value < this.SplitToken)
+ {
+ token = value;
+ bitCount = 0;
+ bits = 0;
+ }
+ else
+ {
+ uint n = FloorLog2Nonzero(value);
+ uint m = value - (1u << (int)n);
+
+ unchecked
+ {
+ // The following expression is quite complex.
+ // See https://github.com/libjxl/libjxl/blob/main/lib/jxl/dec_ans.h#L83C16-L86C47.
+ token = this.SplitToken +
+ (uint)(((n - this.SplitExponent) << (int)(this.MsbInToken + this.LsbInToken)) +
+ ((m >> (int)(n - this.MsbInToken)) << (int)this.LsbInToken) +
+ (m & ((1 << (int)this.LsbInToken) - 1)));
+
+ bitCount = n - this.MsbInToken - this.LsbInToken;
+ bits = (value >> (int)this.LsbInToken) & ((1u << (int)bitCount) - 1);
+ }
+ }
+ }
+
+ public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsLz77Parameters.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsLz77Parameters.cs
new file mode 100644
index 0000000000..ecf5cdc3fa
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsLz77Parameters.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO;
+
+internal sealed class JxlAnsLz77Parameters : IJxlFields
+{
+ public bool Enabled { get; set; }
+
+ public uint MinimumSymbol { get; set; }
+
+ public uint MinimumLength { get; set; }
+
+ public JxlAnsHybridUIntConfiguration LengthUintConfig { get; set; } = new(0, 0, 0);
+
+ public int NonserializedDistanceContext { get; set; }
+
+ public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs
new file mode 100644
index 0000000000..d28e98c029
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs
@@ -0,0 +1,256 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using System.Diagnostics;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO;
+
+internal static class JxlAnsReader
+{
+ // Prefer jagged arrays over multidimensional arrays
+ // for performance. Collection expressions help represent
+ // jagged arrays easily.
+ private static readonly byte[][] HuffmanLookup =
+ [
+ [3, 10], [7, 12], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5],
+ [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2],
+ [3, 10], [5, 0], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5],
+ [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2],
+ [3, 10], [6, 11], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5],
+ [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2],
+ [3, 10], [5, 0], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5],
+ [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2],
+ [3, 10], [7, 13], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5],
+ [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2],
+ [3, 10], [5, 0], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5],
+ [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2],
+ [3, 10], [6, 11], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5],
+ [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2],
+ [3, 10], [5, 0], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5],
+ [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2],
+ ];
+
+ public static uint DecodeVariableLengthUint8(JxlBitReader reader)
+ {
+ if (reader.ReadBoolean())
+ {
+ uint bitCount = reader.ReadBits32(3u);
+
+ return bitCount == 0
+ ? 1u
+ : (reader.ReadBits32(bitCount) + (1u << (int)bitCount));
+ }
+
+ return 0u;
+ }
+
+ public static uint DecodeVariableLengthUint16(JxlBitReader reader)
+ {
+ if (reader.ReadBoolean())
+ {
+ uint bitCount = reader.ReadBits32(4u);
+
+ return bitCount == 0
+ ? 1u
+ : (reader.ReadBits32(bitCount) + (1u << (int)bitCount));
+ }
+
+ return 0u;
+ }
+
+ // NOTE: this method returns null on failure.
+ // If the return value is a valid IMemoryOwner object,
+ // then it succeeded.
+ public static IMemoryOwner? ReadHistogram(Configuration configuration, int precisionBits, JxlBitReader reader)
+ {
+ int range = 1 << precisionBits;
+ bool isSimpleCode = reader.ReadBoolean();
+
+ IMemoryOwner counts;
+
+ if (isSimpleCode)
+ {
+ Span symbols = stackalloc uint[2];
+ symbols.Clear();
+
+ uint maxSymbol = 0u;
+ uint symCount = reader.ReadBits32(1u) + 1u;
+ for (uint i = 0; i < symCount; i++)
+ {
+ uint symbol = DecodeVariableLengthUint8(reader);
+ if (symbol > maxSymbol)
+ {
+ maxSymbol = symbol;
+ }
+
+ symbols[(int)i] = symbol;
+ }
+
+ // Up to 256 items
+ counts = configuration.MemoryAllocator.Allocate((int)maxSymbol + 1);
+ Span countsSpan = counts.Memory.Span;
+
+ if (symCount == 1)
+ {
+ countsSpan[(int)symbols[0]] = (uint)range;
+ }
+ else
+ {
+ if (symbols[0] == symbols[1])
+ {
+ Debug.Fail("Corrupt data");
+ counts.Dispose();
+ return null;
+ }
+
+ countsSpan[(int)symbols[0]] = reader.ReadBits32((uint)precisionBits);
+ countsSpan[(int)symbols[1]] = (uint)range - countsSpan[(int)symbols[0]];
+ }
+ }
+ else
+ {
+ bool isFlat = reader.ReadBoolean();
+
+ if (isFlat)
+ {
+ uint alphabetSize = DecodeVariableLengthUint8(reader) + 1u;
+ if (alphabetSize <= range)
+ {
+ return null;
+ }
+
+ counts = JxlAnsHelper.CreateFlatHistogram(configuration, (int)alphabetSize, range);
+ return counts;
+ }
+
+ int upperBoundLog = FloorLog2Nonzero(JxlAnsConstants.AnsLogTableSize + 1);
+ int log = 0;
+
+ for (; log < upperBoundLog; log++)
+ {
+ bool logIncrementBit = reader.ReadBoolean();
+
+ if (!logIncrementBit)
+ {
+ break;
+ }
+ }
+
+ uint shift = (reader.ReadBits32((uint)log) | (1u << log)) - 1u;
+
+ if (shift > JxlAnsConstants.AnsLogTableSize + 1)
+ {
+ Debug.Fail("Invalid shift");
+
+ return null;
+ }
+
+ uint length = DecodeVariableLengthUint8(reader) + 3u;
+
+ counts = configuration.MemoryAllocator.Allocate((int)length);
+ Span countsSpan = counts.Memory.Span;
+
+ uint totalCount = 0;
+
+ // The length variable can represent up to 258 elements,
+ // so it'd be more beneficial to allocate on the stack
+ // than pool an array.
+ Span logCounts = stackalloc int[(int)length];
+
+ // stackalloc doesn't zero-init, so clear just in case.
+ logCounts.Clear();
+
+ int omitLog = -1;
+ int omitPos = -1;
+
+ // See comments for logCounts definition
+ Span same = stackalloc int[(int)length];
+ same.Clear();
+
+ for (int i = 0; i < length; i++)
+ {
+ uint index = reader.PeekBits32(7);
+ reader.SkipBits32(HuffmanLookup[index][0]);
+ logCounts[i] = HuffmanLookup[index][1] - 1;
+
+ if (logCounts[i] == JxlAnsConstants.AnsLogTableSize)
+ {
+ uint rleLength = DecodeVariableLengthUint8(reader);
+ same[i] = (int)rleLength + 5;
+ i += (int)rleLength + 3;
+ continue;
+ }
+
+ if (logCounts[i] > omitLog)
+ {
+ omitLog = logCounts[i];
+ omitPos = i;
+ }
+ }
+
+ if (omitPos < 0)
+ {
+ Debug.Fail("The histogram is corrupt or invalid.");
+ counts.Dispose();
+ return null;
+ }
+
+ if (omitPos + 1 < length && logCounts[omitPos + 1] == JxlAnsConstants.AnsLogTableSize)
+ {
+ Debug.Fail("The histogram is corrupt or invalid.");
+ counts.Dispose();
+ return null;
+ }
+
+ int previous = 0;
+ int sameCount = 0;
+
+ for (int i = 0; i < length; i++)
+ {
+ if (same[i] > 0)
+ {
+ sameCount = same[i] - 1;
+ previous = i > 0 ? (int)countsSpan[i - 1] : 0;
+ }
+
+ if (sameCount > 0)
+ {
+ countsSpan[i] = (uint)previous;
+ sameCount--;
+ }
+ else
+ {
+ int code = logCounts[i];
+
+ if (i == omitPos || code < 0)
+ {
+ continue;
+ }
+ else if (shift == 0 || code == 0)
+ {
+ countsSpan[i] = 1u << code;
+ }
+ else
+ {
+ int bitCount = JxlAnsHelper.GetPopulationCountPrecision(code, (int)shift);
+ countsSpan[i] = (1u << code) + (reader.ReadBits32((uint)bitCount) << (code - bitCount));
+ }
+ }
+
+ totalCount += countsSpan[i];
+ }
+
+ countsSpan[omitPos] = (uint)range - totalCount;
+
+ if (countsSpan[omitPos] <= 0)
+ {
+ Debug.Fail("The histogram count is incorrect.");
+ counts.Dispose();
+ return null;
+ }
+ }
+
+ return counts;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsSymbol.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsSymbol.cs
new file mode 100644
index 0000000000..75afa94d9f
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsSymbol.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO;
+
+[StructLayout(LayoutKind.Sequential)]
+internal struct JxlAnsSymbol(int value, int offset, int frequency)
+{
+ public int Value = value;
+ public int Offset = offset;
+ public int Frequency = frequency;
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs
new file mode 100644
index 0000000000..99b1599915
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs
@@ -0,0 +1,167 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers.Binary;
+using System.Diagnostics;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.IO;
+
+///
+/// Represents a bitstream reader.
+///
+internal sealed class JxlBitReader(ReadOnlyMemory bytes)
+{
+ private ulong buffer;
+ private uint bufferRemainingBits;
+ private int pointer;
+ private bool endOfStream;
+
+ ///
+ /// Fetches a new buffer.
+ ///
+ private void RefillCore()
+ {
+ ReadOnlySpan samplesSpan = bytes.Span;
+
+ int remaining = samplesSpan.Length - this.pointer;
+ if (remaining <= 0)
+ {
+ // we don't have any more data... mark an end of stream
+ this.buffer = 0;
+ this.bufferRemainingBits = 0;
+ this.endOfStream = true;
+ return;
+ }
+
+ if (remaining >= 8)
+ {
+ this.buffer = BinaryPrimitives.ReadUInt64LittleEndian(samplesSpan[this.pointer..]);
+ this.bufferRemainingBits = 64u;
+ this.pointer += 8;
+ }
+ else
+ {
+ ulong value = 0;
+ for (int i = 0; i < remaining; i++)
+ {
+ value |= (ulong)samplesSpan[this.pointer + i] << (8 * i);
+ }
+
+ this.buffer = value;
+ this.bufferRemainingBits = (uint)(remaining * 8);
+ this.pointer += remaining;
+ }
+ }
+
+ private void MaybeRefill()
+ {
+ if (this.bufferRemainingBits <= 0)
+ {
+ this.RefillCore();
+ }
+ }
+
+ private ulong ReadBits64Core(uint n, bool peek = false)
+ {
+ Debug.Assert(n <= 64, "Too many bits to pack into ulong");
+ this.MaybeRefill();
+
+ if (this.endOfStream)
+ {
+ JxlThrowHelper.ThrowEndOfStream();
+ }
+
+ if (n <= this.bufferRemainingBits)
+ {
+ ulong result = this.buffer & ((1UL << (int)n) - 1);
+
+ if (!peek)
+ {
+ this.buffer >>= (int)n;
+ this.bufferRemainingBits -= n;
+ }
+
+ return result;
+ }
+ else
+ {
+ uint bitsFromCurrent = this.bufferRemainingBits;
+ ulong part = this.buffer & ((1UL << (int)bitsFromCurrent) - 1);
+
+ this.buffer >>= (int)bitsFromCurrent;
+ this.bufferRemainingBits = 0;
+
+ this.RefillCore();
+
+ uint bitsFromNext = n - bitsFromCurrent;
+ ulong nextPart = this.buffer & ((1UL << (int)bitsFromNext) - 1);
+
+ if (!peek)
+ {
+ this.buffer >>= (int)bitsFromNext;
+ this.bufferRemainingBits -= bitsFromNext;
+ }
+
+ return part | (nextPart << (int)bitsFromCurrent);
+ }
+ }
+
+ private uint ReadBits32Core(uint n, bool peek = false)
+ {
+ Debug.Assert(n <= 32, "Too many bits to pack into uint");
+ this.MaybeRefill();
+
+ if (this.endOfStream)
+ {
+ JxlThrowHelper.ThrowEndOfStream();
+ }
+
+ if (n <= this.bufferRemainingBits)
+ {
+ uint result = (uint)(this.buffer & ((1UL << (int)n) - 1));
+
+ if (!peek)
+ {
+ this.buffer >>= (int)n;
+ this.bufferRemainingBits -= n;
+ }
+
+ return result;
+ }
+ else
+ {
+ uint bitsFromCurrent = this.bufferRemainingBits;
+ uint part = (uint)(this.buffer & ((1UL << (int)bitsFromCurrent) - 1));
+
+ this.buffer >>= (int)bitsFromCurrent;
+ this.bufferRemainingBits = 0;
+
+ this.RefillCore();
+
+ uint bitsFromNext = n - bitsFromCurrent;
+ uint nextPart = (uint)(this.buffer & ((1UL << (int)bitsFromNext) - 1));
+
+ if (!peek)
+ {
+ this.buffer >>= (int)bitsFromNext;
+ this.bufferRemainingBits -= bitsFromNext;
+ }
+
+ return part | (nextPart << (int)bitsFromCurrent);
+ }
+ }
+
+ public uint ReadBits32(uint bits) => this.ReadBits32Core(bits, peek: false);
+
+ public uint PeekBits32(uint bits) => this.ReadBits32Core(bits, peek: true);
+
+ public void SkipBits32(uint bits) => _ = this.ReadBits32(bits);
+
+ public ulong ReadBits64(uint bits) => this.ReadBits64Core(bits, peek: false);
+
+ public ulong PeekBits64(uint bits) => this.ReadBits64Core(bits, peek: true);
+
+ public void SkipBits64(uint bits) => _ = this.ReadBits64(bits);
+
+ public bool ReadBoolean() => this.ReadBits32Core(1, peek: false) == 1;
+}
diff --git a/src/ImageSharp/Formats/Jxl/InlineArrays.cs b/src/ImageSharp/Formats/Jxl/InlineArrays.cs
new file mode 100644
index 0000000000..e89b924681
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/InlineArrays.cs
@@ -0,0 +1,41 @@
+// 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;
+}
diff --git a/src/ImageSharp/Formats/Jxl/JxlAspectRatioHelpers.cs b/src/ImageSharp/Formats/Jxl/JxlAspectRatioHelpers.cs
new file mode 100644
index 0000000000..ee5d1c134b
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/JxlAspectRatioHelpers.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl;
+
+internal static class JxlAspectRatioHelpers
+{
+ private static readonly SignedRational[] Ratios =
+ [
+ new(1, 1),
+ new(12, 10),
+ new(4, 3),
+ new(3, 2),
+ new(16, 9),
+ new(5, 4),
+ new(2, 1)
+ ];
+
+ public static SignedRational FixedAspectRatios(int ratio) => Ratios[ratio - 1];
+
+ public static int FindAspectRatio(int x, int y)
+ {
+ for (int i = 0; i < 7; i++)
+ {
+ if (x == MultiplyTruncate(Ratios[i], y))
+ {
+ return i;
+ }
+ }
+
+ return 0;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int MultiplyTruncate(SignedRational rational, int multiplicand) => (multiplicand * rational.Numerator) / rational.Denominator;
+}
diff --git a/src/ImageSharp/Formats/Jxl/JxlFormat.cs b/src/ImageSharp/Formats/Jxl/JxlFormat.cs
new file mode 100644
index 0000000000..00e7b66eb5
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/JxlFormat.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl;
+
+internal class JxlFormat : IImageFormat
+{
+ public string Name => "JPEG XL";
+
+ public string DefaultMimeType => "image/jxl";
+
+ IEnumerable IImageFormat.MimeTypes => new[] { "image/jxl" };
+
+ IEnumerable IImageFormat.FileExtensions => new[] { "jxl" };
+}
diff --git a/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs b/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs
new file mode 100644
index 0000000000..8a7416a573
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs
@@ -0,0 +1,82 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl;
+
+internal sealed class JxlFrameDimensions
+{
+ public const int BlockDimensions = 8;
+ public const int DctBlockSize = BlockDimensions * BlockDimensions;
+ public const int GroupDimensions = 256;
+ public const int GroupDimensionsInBlocks = GroupDimensions / BlockDimensions;
+
+ public JxlFrameDimensions(int xSizePixel, int ySizePixel, int groupSizeShift, int maxHorizontalShift, int maxVerticalShift, bool modularMode, int upsampling)
+ {
+ this.GroupDimension = (GroupDimensions >> 1) << groupSizeShift;
+ this.DcGroupDimension = this.GroupDimension * BlockDimensions;
+ this.XSizeUpsampled = xSizePixel;
+ this.YSizeUpsampled = ySizePixel;
+ this.XSize = DivCeil(xSizePixel, upsampling);
+ this.YSize = DivCeil(ySizePixel, upsampling);
+ this.XSizeBlocks = DivCeil(this.XSize, BlockDimensions << maxHorizontalShift) << maxHorizontalShift;
+ this.YSizeBlocks = DivCeil(this.YSize, BlockDimensions << maxVerticalShift) << maxVerticalShift;
+ this.XSizePadded = this.XSizeBlocks * BlockDimensions;
+ this.YSizePadded = this.YSizeBlocks * BlockDimensions;
+
+ if (modularMode)
+ {
+ this.XSizePadded = this.XSize;
+ this.YSizePadded = this.YSize;
+ }
+
+ this.XSizeUpsampledPadded = this.XSizePadded * upsampling;
+ this.YSizeUpsampledPadded = this.YSizePadded * upsampling;
+ this.XSizeGroups = DivCeil(this.XSize, GroupDimensions);
+ this.YSizeGroups = DivCeil(this.YSize, GroupDimensions);
+ this.XSizeDcGroups = DivCeil(this.XSizeBlocks, GroupDimensions);
+ this.YSizeDcGroups = DivCeil(this.YSizeBlocks, GroupDimensions);
+ this.NumGroups = this.XSizeGroups * this.YSizeGroups;
+ this.NumDcGroups = this.XSizeDcGroups * this.YSizeDcGroups;
+ }
+
+ public int XSize { get; set; }
+
+ public int YSize { get; set; }
+
+ public int XSizeUpsampled { get; set; }
+
+ public int YSizeUpsampled { get; set; }
+
+ public int XSizeUpsampledPadded { get; set; }
+
+ public int YSizeUpsampledPadded { get; set; }
+
+ public int XSizePadded { get; set; }
+
+ public int YSizePadded { get; set; }
+
+ public int XSizeBlocks { get; set; }
+
+ public int YSizeBlocks { get; set; }
+
+ public int XSizeGroups { get; set; }
+
+ public int YSizeGroups { get; set; }
+
+ public int XSizeDcGroups { get; set; }
+
+ public int YSizeDcGroups { get; set; }
+
+ public int NumGroups { get; set; }
+
+ public int NumDcGroups { get; set; }
+
+ public int GroupDimension { get; set; }
+
+ public int DcGroupDimension { get; set; }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int DivCeil(int x, int y) => x / y;
+}
diff --git a/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs b/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs
new file mode 100644
index 0000000000..a39dfe707c
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace SixLabors.ImageSharp.Formats.Jxl;
+
+internal static class JxlThrowHelper
+{
+ [DoesNotReturn]
+ public static void ThrowEndOfStream() => throw new EndOfStreamException();
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs
new file mode 100644
index 0000000000..63a5c59c9f
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+///
+/// Represents a three-plane, 2D raster image of type .
+///
+internal sealed class JxlImage3B : JxlImage3
+{
+ public JxlImage3B()
+ {
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs
new file mode 100644
index 0000000000..b456dfd9a7
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+///
+/// Represents a three-plane, 2D raster image of type .
+///
+internal sealed class JxlImage3F : JxlImage3
+{
+ public JxlImage3F()
+ {
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs
new file mode 100644
index 0000000000..0d9f6c8202
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+///
+/// Represents a three-plane, 2D raster image of type .
+///
+internal sealed class JxlImage3I : JxlImage3
+{
+ public JxlImage3I()
+ {
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs
new file mode 100644
index 0000000000..00615ff846
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+///
+/// Represents a three-plane, 2D raster image of type .
+///
+internal sealed class JxlImage3S : JxlImage3
+{
+ public JxlImage3S()
+ {
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs
new file mode 100644
index 0000000000..1921e86d33
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+///
+/// Represents a three-plane, 2D raster image of type .
+///
+internal sealed class JxlImage3U : JxlImage3
+{
+ public JxlImage3U()
+ {
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs
new file mode 100644
index 0000000000..0faba189ad
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+///
+/// Represents a single-plane, 2D raster image of type .
+///
+internal sealed class JxlImageB : JxlPlane
+{
+ public JxlImageB()
+ {
+ }
+
+ public JxlImageB(int width, int height)
+ : base(width, height)
+ {
+ }
+
+ public JxlImageB(Configuration configuration, int xSize, int ySize, int prePadding = 0)
+ : base(xSize, ySize)
+ => this.Allocate(configuration, prePadding);
+
+ public Memory GetRowBytesMemory(int y)
+ {
+ Debug.Assert(y < this.YSize, "Attempted to access out-of-bounds Y coordinate");
+
+ Memory row = this.Bytes[(y * this.BytesPerRow)..];
+
+ return row;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageF.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageF.cs
new file mode 100644
index 0000000000..6810e87df9
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageF.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+///
+/// Represents a single-plane, 2D raster image of type .
+///
+internal sealed class JxlImageF : JxlPlane
+{
+ public JxlImageF()
+ {
+ }
+
+ public JxlImageF(int width, int height)
+ : base(width, height)
+ {
+ }
+
+ public JxlImageF(Configuration configuration, int xSize, int ySize, int prePadding = 0)
+ : base(xSize, ySize)
+ => this.Allocate(configuration, prePadding);
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageI.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageI.cs
new file mode 100644
index 0000000000..0261ffd63c
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageI.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+///
+/// Represents a single-plane, 2D raster image of type .
+///
+internal sealed class JxlImageI : JxlPlane
+{
+ public JxlImageI()
+ {
+ }
+
+ public JxlImageI(int width, int height)
+ : base(width, height)
+ {
+ }
+
+ public JxlImageI(Configuration configuration, int xSize, int ySize, int prePadding = 0)
+ : base(xSize, ySize)
+ => this.Allocate(configuration, prePadding);
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageS.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageS.cs
new file mode 100644
index 0000000000..b53716f12f
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageS.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+///
+/// Represents a single-plane, 2D raster image of type .
+///
+internal sealed class JxlImageS : JxlPlane
+{
+ public JxlImageS()
+ {
+ }
+
+ public JxlImageS(int width, int height)
+ : base(width, height)
+ {
+ }
+
+ public JxlImageS(Configuration configuration, int xSize, int ySize, int prePadding = 0)
+ : base(xSize, ySize)
+ => this.Allocate(configuration, prePadding);
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageSB.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageSB.cs
new file mode 100644
index 0000000000..355c50785c
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageSB.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+///
+/// Represents a single-plane, 2D raster image of type .
+///
+internal sealed class JxlImageSB : JxlPlane
+{
+ public JxlImageSB()
+ {
+ }
+
+ public JxlImageSB(int width, int height)
+ : base(width, height)
+ {
+ }
+
+ public JxlImageSB(Configuration configuration, int xSize, int ySize, int prePadding = 0)
+ : base(xSize, ySize)
+ => this.Allocate(configuration, prePadding);
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageU.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageU.cs
new file mode 100644
index 0000000000..d41669e4d1
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageU.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+///
+/// Represents a single-plane, 2D raster image of type .
+///
+internal sealed class JxlImageU : JxlPlane
+{
+ public JxlImageU()
+ {
+ }
+
+ public JxlImageU(int width, int height)
+ : base(width, height)
+ {
+ }
+
+ public JxlImageU(Configuration configuration, int xSize, int ySize, int prePadding = 0)
+ : base(xSize, ySize)
+ => this.Allocate(configuration, prePadding);
+}
diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs
new file mode 100644
index 0000000000..f46f6767b8
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs
@@ -0,0 +1,85 @@
+// 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
+ 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");
+}
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/Metadata/JxlAnimationHeader.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlAnimationHeader.cs
new file mode 100644
index 0000000000..3a11ff88f7
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlAnimationHeader.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.IO;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.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/Metadata/JxlBitDepth.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs
new file mode 100644
index 0000000000..0df657f44d
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.IO;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Metadata;
+
+internal sealed class JxlBitDepth : IJxlFields
+{
+ ///
+ /// 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 int BitsPerSample { get; set; }
+
+ ///
+ ///
+ /// 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 int ExponentBitsPerSample { get; set; }
+
+ public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
+}
diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlCodecMetadata.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlCodecMetadata.cs
new file mode 100644
index 0000000000..d26d79d9a5
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlCodecMetadata.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Metadata;
+
+internal sealed class JxlCodecMetadata
+{
+ public JxlImageMetadata? ImageMetadata { get; set; }
+
+ public SizeHeader Size { get; set; }
+
+ public JxlCustomTransformData? CustomTransformData { get; set; }
+
+ public int XSize => this.Size.XSize;
+
+ public int YSize => this.Size.YSize;
+
+ 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/Metadata/JxlCustomTransformData.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs
new file mode 100644
index 0000000000..78c0e9ba81
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.IO;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.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/Metadata/JxlExifOrientation.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs
new file mode 100644
index 0000000000..b5f52d67fe
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.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/Metadata/JxlExtraChannel.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs
new file mode 100644
index 0000000000..1fb39d8604
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.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/Metadata/JxlExtraChannelInfo.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannelInfo.cs
new file mode 100644
index 0000000000..0279c2d7ab
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannelInfo.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.IO;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.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/Metadata/JxlImageMetadata.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlImageMetadata.cs
new file mode 100644
index 0000000000..10855235b2
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/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.IO;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.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/Metadata/JxlOpsinInverseMatrix.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs
new file mode 100644
index 0000000000..ccf2b25303
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.IO;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Metadata;
+
+internal sealed class JxlOpsinInverseMatrix : IJxlFields
+{
+ public bool AllDefault { get; set; }
+
+ public JxlMatrix3x3 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/Metadata/JxlPreviewHeader.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlPreviewHeader.cs
new file mode 100644
index 0000000000..0c2bbab2ed
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlPreviewHeader.cs
@@ -0,0 +1,68 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.IO;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.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/Metadata/JxlSizeHeader.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlSizeHeader.cs
new file mode 100644
index 0000000000..9c6e66dbef
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlSizeHeader.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.IO;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.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/Metadata/JxlToneMapping.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlToneMapping.cs
new file mode 100644
index 0000000000..1698b07016
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlToneMapping.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.IO;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.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/Processing/JxlAlphaBlendingInputLayer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingInputLayer.cs
new file mode 100644
index 0000000000..4b49e32c11
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingInputLayer.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal sealed class JxlAlphaBlendingInputLayer
+{
+ public ReadOnlyMemory R { get; set; }
+
+ public ReadOnlyMemory G { get; set; }
+
+ public ReadOnlyMemory B { get; set; }
+
+ public ReadOnlyMemory A { get; set; }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs
new file mode 100644
index 0000000000..c5db8cb8a7
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal sealed class JxlAlphaBlendingOutput
+{
+ public Memory R { get; set; }
+
+ public Memory G { get; set; }
+
+ public Memory B { get; set; }
+
+ public Memory A { get; set; }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs
new file mode 100644
index 0000000000..641205ab21
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs
@@ -0,0 +1,192 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal sealed class JxlAlphaHelper
+{
+ // TODO: SIMD support
+ private const float SmallAlpha = 1f / (1 << 26);
+
+ // Force x to stay within the range of 0 through 1
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static float Clamp(float x) => Math.Clamp(x, 0f, 1f);
+
+ public static void PerformAlphaBlending(
+ JxlAlphaBlendingInputLayer background,
+ JxlAlphaBlendingInputLayer foreground,
+ JxlAlphaBlendingOutput output,
+ int pixelCount,
+ bool alphaIsPremultiplied,
+ bool clamp)
+ {
+ // Store all channels from all parameters into spans, because
+ // creating a Span from Memory is expensive, especially
+ // in a loop.
+ ReadOnlySpan fgR = foreground.R.Span;
+ ReadOnlySpan fgG = foreground.G.Span;
+ ReadOnlySpan fgB = foreground.B.Span;
+ ReadOnlySpan fgA = foreground.A.Span;
+
+ ReadOnlySpan bgR = background.R.Span;
+ ReadOnlySpan bgG = background.G.Span;
+ ReadOnlySpan bgB = background.B.Span;
+ ReadOnlySpan bgA = background.A.Span;
+
+ Span outR = output.R.Span;
+ Span outG = output.G.Span;
+ Span outB = output.B.Span;
+ Span outA = output.A.Span;
+
+ if (alphaIsPremultiplied)
+ {
+ for (int x = 0; x < pixelCount; x++)
+ {
+ float fga = clamp ? Clamp(fgA[x]) : fgA[x];
+ outR[x] = fgR[x] + (bgR[x] * (1f - fga));
+ outG[x] = fgG[x] + (bgG[x] * (1f - fga));
+ outB[x] = fgB[x] + (bgB[x] * (1f - fga));
+ outA[x] = 1f - ((1f - fga) * (1f - bgA[x]));
+ }
+ }
+ else
+ {
+ for (int x = 0; x < pixelCount; x++)
+ {
+ float fga = clamp ? Clamp(fgA[x]) : fgA[x];
+ float newA = 1f - ((1f - fga) * (1f - bgA[x]));
+ float rnewA = newA > 0 ? 1f / newA : 0f;
+ outR[x] = ((fgR[x] * fga) + (bgR[x] * bgA[x] * (1f - fga))) * rnewA;
+ outG[x] = ((fgG[x] * fga) + (bgG[x] * bgA[x] * (1f - fga))) * rnewA;
+ outB[x] = ((fgB[x] * fga) + (bgB[x] * bgA[x] * (1f - fga))) * rnewA;
+ outA[x] = newA;
+ }
+ }
+ }
+
+ public static void PerformAlphaBlending(
+ ReadOnlySpan bg,
+ ReadOnlySpan bga,
+ ReadOnlySpan fg,
+ ReadOnlySpan fga,
+ Span output,
+ int pixelCount,
+ bool alphaIsPremultiplied,
+ bool clamp)
+ {
+ if (bg == bga && fg == fga)
+ {
+ for (int x = 0; x < pixelCount; x++)
+ {
+ float fa = clamp ? Clamp(fga[x]) : fga[x];
+ output[x] = 1f - ((1f - fa) * (1f - bga[x]));
+ }
+ }
+ else
+ {
+ if (alphaIsPremultiplied)
+ {
+ for (int x = 0; x < pixelCount; x++)
+ {
+ float fa = clamp ? Clamp(fga[x]) : fga[x];
+ output[x] = fg[x] + (bg[x] * (1f - fa));
+ }
+ }
+ else
+ {
+ for (int x = 0; x < pixelCount; x++)
+ {
+ float fa = clamp ? Clamp(fga[x]) : fga[x];
+ float new_a = 1f - ((1f - fa) * (1f - bga[x]));
+ float rnew_a = new_a > 0 ? 1f / new_a : 0f;
+ output[x] = ((fg[x] * fa) + (bg[x] * bga[x] * (1f - fa))) * rnew_a;
+ }
+ }
+ }
+ }
+
+ public static void PerformAlphaWeightedAdd(
+ ReadOnlySpan bg,
+ ReadOnlySpan fg,
+ ReadOnlySpan fga,
+ Span output,
+ int pixelCount,
+ bool clamp)
+ {
+ if (fg == fga)
+ {
+ bg[pixelCount..].CopyTo(output);
+ }
+ else if (clamp)
+ {
+ for (int x = 0; x < pixelCount; x++)
+ {
+ output[x] = bg[x] + (fg[x] * Clamp(fga[x]));
+ }
+ }
+ else
+ {
+ for (int x = 0; x < pixelCount; ++x)
+ {
+ output[x] = bg[x] + (fg[x] * fga[x]);
+ }
+ }
+ }
+
+ public static void PerformMultiplyBlending(
+ ReadOnlySpan bg,
+ ReadOnlySpan fg,
+ Span output,
+ int pixelCount,
+ bool clamp)
+ {
+ if (clamp)
+ {
+ for (int x = 0; x < pixelCount; x++)
+ {
+ output[x] = bg[x] * Clamp(fg[x]);
+ }
+ }
+ else
+ {
+ for (int x = 0; x < pixelCount; x++)
+ {
+ output[x] = bg[x] * fg[x];
+ }
+ }
+ }
+
+ public static void PremultiplyAlpha(
+ Span r,
+ Span g,
+ Span b,
+ ReadOnlySpan a,
+ int pixelCount)
+ {
+ for (int x = 0; x < pixelCount; x++)
+ {
+ float multiplier = Math.Max(SmallAlpha, a[x]);
+ r[x] *= multiplier;
+ g[x] *= multiplier;
+ b[x] *= multiplier;
+ }
+ }
+
+ public static void UnpremultiplyAlpha(
+ Span r,
+ Span g,
+ Span b,
+ ReadOnlySpan a,
+ int pixelCount)
+ {
+ for (int x = 0; x < pixelCount; x++)
+ {
+ float multiplier = 1f / Math.Max(SmallAlpha, a[x]);
+ r[x] *= multiplier;
+ g[x] *= multiplier;
+ b[x] *= multiplier;
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs
new file mode 100644
index 0000000000..095e936d32
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs
@@ -0,0 +1,106 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal static class JxlLehmerCode
+{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int ValueOfLowest1Bit(int n) => n & -n;
+
+ public static bool ComputeLehmerCode(ReadOnlySpan permutation, Span temp, int n, Span code)
+ {
+ temp[(n + 1)..].Clear();
+
+ for (int idx = 0; idx < n; idx++)
+ {
+ int s = permutation[idx];
+
+ uint penalty = 0u;
+ uint i = (uint)s + 1u;
+
+ while (i != 0u)
+ {
+ penalty += temp[(int)i];
+ i &= i - 1u; // Clear lowest bit
+ }
+
+ if (s < penalty)
+ {
+ return false;
+ }
+
+ code[idx] = (uint)s - penalty;
+ i = (uint)s + 1u;
+
+ while (i < n + 1u)
+ {
+ temp[(int)i]++;
+ i += (uint)ValueOfLowest1Bit((int)i);
+ }
+ }
+
+ return true;
+ }
+
+ public static bool DecodeLehmerCode(ReadOnlySpan code, Span temp, int n, Span permutation)
+ {
+ if (n == 0)
+ {
+ return false;
+ }
+
+ int log2n = CeilLog2Nonzero(n);
+ int paddedN = 1 << log2n;
+
+ for (int i = 0; i < paddedN; i++)
+ {
+ int i1 = i + 1;
+ temp[i] = (uint)ValueOfLowest1Bit(i1);
+ }
+
+ for (int i = 0; i < n; i++)
+ {
+ if (code[i] + i >= n)
+ {
+ return false;
+ }
+
+ uint rank = code[i] + 1;
+
+ int bit = paddedN;
+ int next = 0;
+
+ for (int b = 0; b <= log2n; b++)
+ {
+ int cand = next + bit;
+
+ if (cand < 1)
+ {
+ return false;
+ }
+
+ bit >>= 1;
+
+ if (temp[cand - 1] < rank)
+ {
+ next = cand;
+ rank -= temp[cand - 1];
+ }
+ }
+
+ permutation[i] = next;
+
+ next++;
+ while (next <= paddedN)
+ {
+ temp[next - 1]--;
+ next += ValueOfLowest1Bit(next);
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseHelper.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseHelper.cs
new file mode 100644
index 0000000000..f6432e24db
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseHelper.cs
@@ -0,0 +1,28 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal static class JxlNoiseHelper
+{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static JxlNoiseIndexAndFraction IndexAndFraction(float x)
+ {
+ const int scaleNumerator = JxlNoiseParameters.NoisePoints - 2;
+ const float scale = scaleNumerator / 1.0f;
+
+ float scaledX = MathF.Max(0f, x * scale);
+ float floorX = MathF.Floor(scaledX);
+ float fractionalX = scaledX - floorX;
+
+ if (scaledX >= scaleNumerator + 1)
+ {
+ floorX = scaleNumerator;
+ fractionalX = 1f;
+ }
+
+ return new((int)floorX, fractionalX);
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseIndexAndFraction.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseIndexAndFraction.cs
new file mode 100644
index 0000000000..c3ec1437c8
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseIndexAndFraction.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+[StructLayout(LayoutKind.Sequential)]
+internal struct JxlNoiseIndexAndFraction(int index, float fraction)
+{
+ public int Index = index;
+
+ public float Fraction = fraction;
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseLevel.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseLevel.cs
new file mode 100644
index 0000000000..d5feebd52b
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseLevel.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+[StructLayout(LayoutKind.Sequential)]
+internal struct JxlNoiseLevel(float noiseLevel, float intensity)
+{
+ public float NoiseLevel = noiseLevel;
+
+ public float Intensity = intensity;
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseParameters.cs
new file mode 100644
index 0000000000..3f51f85379
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseParameters.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal sealed class JxlNoiseParameters
+{
+ public const int NoisePoints = 8;
+
+ public float[] Lookup { get; set; } = new float[NoisePoints];
+
+ public bool ContainsAny => this.Lookup.Any(x => MathF.Abs(x) > 1e-3f);
+
+ public void Clear() => Array.Fill(this.Lookup, 0f);
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs
new file mode 100644
index 0000000000..b3849d69f0
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+internal sealed class JxlXorShift
+{
+ private readonly ulong[] s0 = new ulong[8];
+ private readonly ulong[] s1 = new ulong[8];
+
+ public void XorShift128Plus(ulong seed)
+ {
+ this.s0[0] = SplitMix64(seed + 0x9E3779B97F4A7C15L);
+ this.s1[0] = SplitMix64(this.s0[0]);
+ for (int i = 1; i < 8; ++i)
+ {
+ this.s0[i] = SplitMix64(this.s1[i - 1]);
+ this.s1[i] = SplitMix64(this.s0[i]);
+ }
+ }
+
+ public void XorShift128Plus(uint seed1, uint seed2, uint seed3, uint seed4)
+ {
+ this.s0[0] = SplitMix64((((ulong)seed1 << 32) + seed2) + 0x9E3779B97F4A7C15uL);
+ this.s1[0] = SplitMix64((((ulong)seed3 << 32) + seed4) + 0x9E3779B97F4A7C15uL);
+ for (int i = 1; i < 8; ++i)
+ {
+ this.s0[i] = SplitMix64(this.s0[i - 1]);
+ this.s1[i] = SplitMix64(this.s1[i - 1]);
+ }
+ }
+
+ // TODO: SIMD
+ public void Fill(Span randomBits)
+ {
+ for (int i = 0; i < 8; ++i)
+ {
+ ulong s1 = this.s0[i];
+ ulong s0 = this.s1[i];
+ ulong bits = s1 + s0;
+ this.s0[i] = s0;
+ s1 ^= s1 << 23;
+ randomBits[i] = bits;
+ s1 ^= s0 ^ (s1 >> 18) ^ (s0 >> 5);
+ this.s1[i] = s1;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static ulong SplitMix64(ulong z)
+ {
+ z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9uL;
+ z = (z ^ (z >> 27)) * 0x94D049BB133111EBuL;
+ return z ^ (z >> 31);
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlControlPoint.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlControlPoint.cs
new file mode 100644
index 0000000000..74f1a0c3a8
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Splines/JxlControlPoint.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Splines;
+
+///
+/// A simple pair of first and second 32-bit signed integers
+/// that represent a single control point.
+///
+[StructLayout(LayoutKind.Sequential)]
+internal struct JxlControlPoint(int first, int second)
+{
+ public int First = first;
+ public int Second = second;
+}
diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs
new file mode 100644
index 0000000000..42aed25f8a
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs
@@ -0,0 +1,343 @@
+// 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.Splines;
+
+internal sealed class JxlQuantizedSpline : IDisposable
+{
+ private IMemoryOwner? memoryOwner;
+
+ public JxlQuantizedSpline()
+ {
+ for (int i = 0; i < 3; i++)
+ {
+ this.ColorDct[i] = new int[32];
+ }
+ }
+
+ private static ReadOnlySpan Loops => [1, 0, 2];
+
+ private static ReadOnlySpan ChannelWeight => [0.0042f, 0.075f, 0.07f, .3333f];
+
+ public Memory ControlPoints { get; set; }
+
+ // NOTE: Do not use Configuration.MemoryAllocator.Allocate2D. This is
+ // a 3x32 array, and renting memory introduces too much overhead for
+ // 384 bytes of memory.
+ // Additionally, prefer jagged arrays instead of multidimensional arrays
+ // for performance.
+ public int[][] ColorDct { get; set; } = new int[3][];
+
+ public int[] SigmaDct { get; set; } = new int[32];
+
+ public void Dispose()
+ {
+ this.memoryOwner?.Dispose();
+ this.ControlPoints = Memory.Empty;
+ GC.SuppressFinalize(this);
+ }
+
+ public void ReserveControlPoints(Configuration configuration, int n)
+ {
+ this.memoryOwner = configuration.MemoryAllocator.Allocate(n);
+
+ this.ControlPoints = this.memoryOwner.Memory;
+ }
+
+ public static JxlQuantizedSpline Create(Configuration configuration, JxlSpline original, int quantizationAdjustment, float yToX, float yToB)
+ {
+ JxlQuantizedSpline spline = new();
+
+ spline.ReserveControlPoints(configuration, original.ControlPoints.Count - 1);
+
+ PointF startingPoint = original.ControlPoints.First();
+ int previousX = (int)MathF.Round(startingPoint.X);
+ int previousY = (int)MathF.Round(startingPoint.Y);
+ int previousDx = 0; // D stands for delta
+ int previousDy = 0; // D stands for delta
+
+ int length = original.ControlPoints.Length;
+ IMemoryOwner newControls = configuration.MemoryAllocator.Allocate(length);
+ Span controlsSpan = newControls.Memory.Span;
+
+ for (int i = 0; i < length; i++)
+ {
+ PointF controlPoint = original.ControlPoints[i];
+
+ int newX = (int)MathF.Round(controlPoint.X);
+ int newY = (int)MathF.Round(controlPoint.Y);
+ int newDx = newX - previousX; // D stands for delta
+ int newDy = newY - previousY; // D stands for delta
+
+ controlsSpan[i] = new(newDx - previousDx, newDy - previousDy);
+
+ previousDx = newDx;
+ previousDy = newDy;
+ previousX = newX;
+ previousY = newY;
+ }
+
+ float quant = AdjustedQuant(quantizationAdjustment);
+ float inverseQuant = InverseAdjustedQuant(quantizationAdjustment);
+
+ for (int j = 0; j < 3; j++)
+ {
+ int c = Loops[j];
+
+ float factor = (c == 0) ? yToX : (c == 1) ? 0 : yToB;
+
+ // TODO: lower amount of branches by duplicating code
+ // for i=0 and adding a separate loop for i=1..31
+ for (int i = 0; i < 32; i++)
+ {
+ float dctFactor = (i == 0) ? Sqrt2 : 1.0f;
+ float inverseDctFactor = (i == 0) ? Sqrt05 : 1.0f;
+ float restoredY = spline.ColorDct[1][i] * inverseDctFactor * ChannelWeight[1] * inverseQuant;
+ float decorrelated = spline.ColorDct[c][i] - (factor * restoredY);
+ spline.ColorDct[c][i] = ConvertToInteger(decorrelated * dctFactor * quant / ChannelWeight[c]);
+ }
+ }
+
+ for (int i = 0; i < 32; i++)
+ {
+ float dctFactor = (i == 0) ? Sqrt2 : 1.0f;
+ spline.SigmaDct[i] = ConvertToInteger(original.SigmaDct[i] * dctFactor * quant / ChannelWeight[1]);
+ }
+
+ return spline;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static int ConvertToInteger(float value)
+ {
+ const float max = int.MaxValue - 127f;
+ const float min = -max;
+
+ return (int)MathF.Round(Math.Clamp(value, min, max));
+ }
+ }
+
+ public bool Dequantize(
+ Configuration configuration,
+ PointF startingPoint,
+ int quantizationAdjustment,
+ float yToX,
+ float yToB,
+ long imageSize,
+ ref long totalEstimatedAreaReached,
+ JxlSpline result)
+ {
+ long areaLimit = Math.Min(1024 * imageSize * (1L << 32), 1L << 42);
+ result.ClearControlPoints();
+ result.ReserveControlPoints(configuration, this.ControlPoints.Length + 1);
+
+ float px = MathF.Round(startingPoint.X);
+ float py = MathF.Round(startingPoint.Y);
+
+ if (!this.ValidateSplinePointPos(px, py))
+ {
+ Debug.Fail("Spline points out of range");
+
+ return false;
+ }
+
+ int currentX = (int)px;
+ int currentY = (int)py;
+
+ Span controlPoints = result.ControlPoints.Span;
+ Span thisControlPoints = this.ControlPoints.Span;
+
+ controlPoints[0] = new(currentX, currentY);
+
+ int currentDx = 0; // D stands for delta
+ int currentDy = 0; // D stands for delta
+
+ long manhattanDistance = 0;
+
+ int length = this.ControlPoints.Length;
+
+ for (int i = 0; i < length; i++)
+ {
+ JxlControlPoint point = thisControlPoints[i];
+ currentDx += point.First;
+ currentDy += point.Second;
+ manhattanDistance = Math.Abs(currentDx) + Math.Abs(currentDy);
+ if (manhattanDistance > areaLimit)
+ {
+ Debug.Fail("Manhattan distance is too large");
+
+ return false;
+ }
+
+ if (!ValidateSplinePointPos(currentDx, currentDy))
+ {
+ Debug.Fail("Delta points out of range");
+
+ return false;
+ }
+
+ currentX += currentDx;
+ currentY += currentDy;
+
+ if (!ValidateSplinePointPos(currentX, currentY))
+ {
+ Debug.Fail("Current points out of range");
+
+ return false;
+ }
+
+ controlPoints[i + 1] = new(currentX, currentY);
+ }
+
+ float inverseQuant = InverseAdjustedQuant(quantizationAdjustment);
+
+ for (int c = 0; c < 3; c++)
+ {
+ for (int i = 0; i < 32; i++)
+ {
+ float inverseDctFactor = (i == 0) ? Sqrt05 : 1.0f;
+ result.ColorDct[c][i] = this.ColorDct[c][i] * inverseDctFactor * ChannelWeight[c] * inverseQuant;
+ }
+ }
+
+ for (int i = 0; i < 32; i++)
+ {
+ result.ColorDct[0][i] += yToX * result.ColorDct[1][i];
+ result.ColorDct[2][i] += yToB * result.ColorDct[1][i];
+ }
+
+ long widthEstimate = 0;
+ Span color = stackalloc long[3];
+
+ for (int c = 0; c < 3; c++)
+ {
+ for (int i = 0; i < 32; i++)
+ {
+ color[c] += (long)MathF.Ceiling(inverseQuant * MathF.Abs(this.ColorDct[c][i]));
+ }
+ }
+
+ color[0] += (long)MathF.Ceiling(MathF.Abs(yToX)) * color[1];
+ color[2] += (long)MathF.Ceiling(MathF.Abs(yToB)) * color[1];
+
+ long maxColor = Math.Max(color[1], Math.Max(color[0], color[2]));
+ long logColor = Math.Max(1L, (long)CeilLog2Nonzero(1L + maxColor));
+ float weightLimit = MathF.Ceiling(MathF.Sqrt((float)areaLimit / logColor) / MathF.Max(1, manhattanDistance));
+
+ for (int i = 0; i < 32; i++)
+ {
+ float inverseDctFactor = (i == 0) ? Sqrt05 : 1.0f;
+ result.SigmaDct[i] = this.SigmaDct[i] * inverseDctFactor * ChannelWeight[3] * inverseQuant;
+ float weightF = MathF.Ceiling(inverseQuant * MathF.Abs(this.SigmaDct[i]));
+ long weight = (long)Math.Min(weightLimit, Math.Max(1.0f, weightF));
+ widthEstimate += weight * weight * logColor;
+ }
+
+ totalEstimatedAreaReached = widthEstimate * manhattanDistance;
+ if (totalEstimatedAreaReached > areaLimit)
+ {
+ Debug.Fail("Total estimated area is too large");
+
+ return false;
+ }
+
+ return true;
+ }
+
+ public bool Decode(
+ Configuration configuration,
+ Span contextMap,
+ JxlAnsSymbolReader decoder,
+ JxlBitReader br,
+ int maxControlPoints,
+ ref int totalControlPoints)
+ {
+ int numControlPoints = decoder.ReadHybridUnsignedInteger(NumControlPointsContext, br, contextMap);
+ if (numControlPoints > maxControlPoints)
+ {
+ Debug.Fail("Too many control points");
+
+ return false;
+ }
+
+ totalControlPoints += numControlPoints;
+
+ if (totalControlPoints >= maxControlPoints)
+ {
+ Debug.Fail("Too many control points");
+
+ return false;
+ }
+
+ this.ResizeControlPoints(configuration, numControlPoints);
+
+ const long deltaLimit = 1L << 30;
+ Span controlPoints = this.ControlPoints.Span;
+
+ int length = this.ControlPoints.Length;
+
+ for (int i = 0; i < length; i++)
+ {
+ ref JxlControlPoint controlPoint = ref controlPoints[i];
+
+ controlPoint.First = UnpackSigned(decoder.ReadHybridUnsignedInteger(ControlPointsContext, br, contextMap));
+ controlPoint.Second = UnpackSigned(decoder.ReadHybridUnsignedInteger(ControlPointsContext, br, contextMap));
+
+ if (controlPoint.First >= deltaLimit || controlPoint.First <= -deltaLimit ||
+ controlPoint.Second >= deltaLimit || controlPoint.Second <= -deltaLimit)
+ {
+ Debug.Fail("Spline delta-delta is out of bounds");
+
+ return false;
+ }
+ }
+
+ for (int i = 0; i < this.ColorDct.Length; i++)
+ {
+ if (!TryDecodeDct(contextMap, this.ColorDct[i]))
+ {
+ return false;
+ }
+ }
+
+ if (!TryDecodeDct(contextMap, this.SigmaDct))
+ {
+ return false;
+ }
+
+ return true;
+
+ bool TryDecodeDct(ReadOnlySpan contextMap, Span dct)
+ {
+ const int invalidCoefficient = int.MinValue;
+
+ for (int i = 0; i < 32; i++)
+ {
+ dct[i] = UnpackSigned(decoder.ReadHybridUnsignedInteger(DctContext, br, contextMap));
+ if (dct[i] == invalidCoefficient)
+ {
+ Debug.Fail("The DCT coefficient is invalid");
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static float AdjustedQuant(int adjustment)
+ => (adjustment >= 0)
+ ? (1f + (.125f * adjustment))
+ : 1f / (1f - (.125f * adjustment));
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static float InverseAdjustedQuant(int adjustment)
+ => (adjustment >= 0)
+ ? 1f / (1f + (.125f * adjustment))
+ : (1f - (.125f * adjustment));
+}
diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs
new file mode 100644
index 0000000000..ef65c1d95e
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Splines;
+
+internal sealed class JxlSpline : IDisposable
+{
+ private IMemoryOwner? controlPoints;
+
+ public Memory ControlPoints { get; private set; }
+
+ public JxlDct32[] ColorDct { get; set; } = [];
+
+ public JxlDct32 SigmaDct { get; set; }
+
+ public void ClearControlPoints()
+ {
+ this.controlPoints?.Dispose();
+ this.controlPoints = null;
+
+ this.ControlPoints = Memory.Empty;
+ }
+
+ public void ReserveControlPoints(Configuration configuration, int n)
+ {
+ this.ClearControlPoints();
+
+ this.controlPoints = configuration.MemoryAllocator.Allocate(n);
+ this.ControlPoints = this.controlPoints.Memory;
+ }
+
+ public void Dispose()
+ {
+ this.ClearControlPoints();
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineDataView.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineDataView.cs
new file mode 100644
index 0000000000..1884b85c68
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineDataView.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Splines;
+
+internal sealed class JxlSplineDataView
+{
+ public List Splines { get; set; } = [];
+
+ public List StartingPoints { get; set; } = [];
+
+ public bool HasAny => this.Splines.Count > 0;
+}
diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs
new file mode 100644
index 0000000000..adb619f5ff
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Splines;
+
+internal enum JxlSplineEntropyContext : byte
+{
+ QuantizationAdjustment,
+ StartingPosition,
+ NumSplines,
+ NumControlPoints,
+ ControlPoints,
+ Dct,
+ NumSplineContexts
+}
diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegment.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegment.cs
new file mode 100644
index 0000000000..7e8bef1beb
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegment.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Splines;
+
+internal struct JxlSplineSegment
+{
+ public PointF Center { get; set; }
+
+ public float MaximumDistance { get; set; }
+
+ public float InverseSigma { get; set; }
+
+ public float SigmaOver4TimesIntensity { get; set; }
+
+ public InlineArray3 Color { get; set; }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegmentSpan.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegmentSpan.cs
new file mode 100644
index 0000000000..b4bd52af4f
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegmentSpan.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Splines;
+
+internal struct JxlSplineSegmentSpan(int startInclusive, int endInclusive)
+{
+ public int StartInclusive { get; set; } = startInclusive;
+
+ public int EndInclusive { get; set; } = endInclusive;
+}