From 0d159b9477c3bc4b563ad5f02774dd38f6e96fda Mon Sep 17 00:00:00 2001
From: winscripter <142818255+winscripter@users.noreply.github.com>
Date: Sat, 1 Aug 2026 23:44:39 +0400
Subject: [PATCH] Add CMS* prototype, XYB decoding, TOC, convolution and
coefficient ordering
* CMS - Color Management System
---
.../Formats/Jxl/Cms/JxlCieXyPrimaries.cs | 27 ++
.../Formats/Jxl/Cms/JxlColorEncoding.cs | 77 ++++
.../Formats/Jxl/Cms/JxlColorSpace.cs | 31 ++
.../Jxl/Cms/JxlCustomTransferFunction.cs | 109 +++++
src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs | 53 +++
.../Formats/Jxl/Cms/JxlPrimaries.cs | 27 ++
.../Formats/Jxl/Cms/JxlRenderingIntent.cs | 13 +
.../Formats/Jxl/Cms/JxlTransferFunction.cs | 45 ++
.../Formats/Jxl/Cms/JxlWhitePoint.cs | 33 ++
src/ImageSharp/Formats/Jxl/Cms/README.md | 4 +
.../Jxl/IO/Metadata/JxlImageMetadata.cs | 22 +
.../Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs | 8 +-
src/ImageSharp/Formats/Jxl/InlineArrays.cs | 9 +
.../Jxl/Processing/Decoder/JxlDecoderCore.cs | 157 +++++++
.../Jxl/Processing/Decoder/JxlNoiseDecoder.cs | 63 +++
.../Processing/Decoder/JxlOpsinParameters.cs | 18 +
.../Decoder/JxlOutputEncodingInfo.cs | 78 ++++
.../Jxl/Processing/Decoder/JxlXybDecoder.cs | 180 ++++++++
.../Jxl/Processing/JxlCoefficientOrder.cs | 91 ++++
.../Formats/Jxl/Processing/JxlConvolve.cs | 424 ++++++++++++++++++
.../Formats/Jxl/Processing/JxlDctScales.cs | 2 +-
.../Formats/Jxl/Processing/JxlToc.cs | 191 ++++++++
.../Formats/Jxl/Processing/JxlXorShift.cs | 2 +
23 files changed, 1661 insertions(+), 3 deletions(-)
create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlColorSpace.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlPrimaries.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlTransferFunction.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlWhitePoint.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Cms/README.md
create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs
diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs
new file mode 100644
index 000000000..7e5a4367e
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.ColorProfiles;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
+
+///
+/// RGB primaries for CIEXY
+///
+internal struct JxlCieXyPrimaries
+{
+ ///
+ /// Gets or sets the R component
+ ///
+ public CieXyChromaticityCoordinates R { get; set; }
+
+ ///
+ /// Gets or sets the G component
+ ///
+ public CieXyChromaticityCoordinates G { get; set; }
+
+ ///
+ /// Gets or sets the B component
+ ///
+ public CieXyChromaticityCoordinates B { get; set; }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs
new file mode 100644
index 000000000..9a87dccc2
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs
@@ -0,0 +1,77 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
+
+internal sealed class JxlColorEncoding
+{
+ public JxlWhitePoint WhitePoint { get; set; } = JxlWhitePoint.D65;
+
+ public JxlPrimaries Primaries { get; set; } = JxlPrimaries.SRgb;
+
+ public JxlRenderingIntent RenderingIntent { get; set; } = JxlRenderingIntent.Relative;
+
+ public bool HaveFields { get; set; } = true;
+
+ public JxlIccBytes? Icc { get; set; }
+
+ public JxlColorSpace ColorSpace { get; set; } = JxlColorSpace.Rgb;
+
+ public bool Cmyk { get; set; }
+
+ public JxlCustomTransferFunction TransferFunction { get; set; }
+
+ public JxlCustomXy White { get; set; }
+
+ public JxlCustomXy Red { get; set; }
+
+ public JxlCustomXy Green { get; set; }
+
+ public JxlCustomXy Blue { get; set; }
+
+ public bool HasPrimaries => this.ColorSpace is not (JxlColorSpace.Gray or JxlColorSpace.Xyb);
+
+ public int Channels => (this.ColorSpace == JxlColorSpace.Gray) ? 1 : 3;
+
+ public bool TryGetPrimaries(out JxlCieXyPrimaries xy)
+ {
+ xy = default;
+
+ if (!this.HasPrimaries || !this.HasPrimaries)
+ {
+ return false;
+ }
+
+ switch (this.Primaries)
+ {
+ case JxlPrimaries.Custom:
+ xy.R = this.Red.GetValue();
+ xy.G = this.Green.GetValue();
+ xy.B = this.Blue.GetValue();
+ break;
+
+ case JxlPrimaries.SRgb:
+ xy.R = new(0.639998686f, 0.330010138f);
+ xy.G = new(0.300003784f, 0.600003357f);
+ xy.B = new(0.150002046f, 0.059997204f);
+ break;
+
+ case JxlPrimaries.Bt2020:
+ xy.R = new(0.708f, 0.292f);
+ xy.G = new(0.170f, 0.797f);
+ xy.B = new(0.131f, 0.046f);
+ break;
+
+ case JxlPrimaries.P3:
+ xy.R = new(0.680f, 0.320f);
+ xy.G = new(0.265f, 0.690f);
+ xy.B = new(0.150f, 0.060f);
+ break;
+
+ default:
+ throw new InvalidOperationException("Invalid primaries: " + this.Primaries);
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlColorSpace.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlColorSpace.cs
new file mode 100644
index 000000000..b65cbbef9
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/JxlColorSpace.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
+
+///
+/// Supported, JPEG XL-specific color space types.
+///
+internal enum JxlColorSpace : byte
+{
+ ///
+ /// Trichromatic color data. This also includes CMYK if Black
+ /// ExtraChannelInfo is present.
+ ///
+ Rgb,
+
+ ///
+ /// Single-channel data.
+ ///
+ Gray,
+
+ ///
+ /// Like Rgb but fixed values for primaries.
+ ///
+ Xyb,
+
+ ///
+ /// Unknown color space
+ ///
+ Unknown
+}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs
new file mode 100644
index 000000000..acf049d4a
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs
@@ -0,0 +1,109 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
+
+internal struct JxlCustomTransferFunction
+{
+ private const uint MaxGamma = 8192;
+ private const uint GammaMultiplier = 10000000;
+
+ public JxlCustomTransferFunction()
+ {
+ }
+
+ public bool HaveGamma { get; set; }
+
+ public uint Gamma { get; set; }
+
+ public JxlTransferFunction TransferFunction { get; set; } = JxlTransferFunction.SRgb;
+
+ public readonly bool IsUnknown => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Unknown;
+
+ public readonly bool IsSrgb => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.SRgb;
+
+ public readonly bool IsLinear => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Linear;
+
+ public readonly bool IsPq => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Pq;
+
+ public readonly bool IsHlg => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Hlg;
+
+ public readonly bool Is709 => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Bt709;
+
+ public readonly bool IsDci => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Dci;
+
+ public readonly JxlTransferFunction GetTransferFunction()
+ {
+ if (this.HaveGamma)
+ {
+ return JxlTransferFunction.Unknown;
+ }
+
+ return this.TransferFunction;
+ }
+
+ public void SetTransferFunction(JxlTransferFunction tf)
+ {
+ this.HaveGamma = false;
+ this.TransferFunction = tf;
+ }
+
+ public readonly float GetGamma()
+ {
+ if (!this.HaveGamma)
+ {
+ return 0.0f;
+ }
+
+ return this.Gamma * (1.0f / GammaMultiplier);
+ }
+
+ public void SetGamma(float newGamma)
+ {
+ if (newGamma is < 1.0f / MaxGamma or > 1.0f)
+ {
+ throw new InvalidOperationException($"Invalid gamma {newGamma}");
+ }
+
+ this.HaveGamma = false;
+
+ if (IsAlmostEqual(newGamma, 1.0f))
+ {
+ this.TransferFunction = JxlTransferFunction.Linear;
+ return;
+ }
+
+ if (IsAlmostEqual(newGamma, 1.0f / 2.6f))
+ {
+ this.TransferFunction = JxlTransferFunction.Dci;
+ return;
+ }
+
+ // Don't translate 0.45.. to kSRGB nor k709 - that might change pixel
+ // values because those curves also have a linear part.
+ this.HaveGamma = true;
+ this.Gamma = (uint)MathF.Round((float)(newGamma * GammaMultiplier));
+ this.TransferFunction = JxlTransferFunction.Unknown;
+ }
+
+ public readonly bool IsSame(JxlCustomTransferFunction other)
+ {
+ if (this.HaveGamma != other.HaveGamma)
+ {
+ return false;
+ }
+
+ if (this.HaveGamma)
+ {
+ return this.Gamma == other.Gamma;
+ }
+
+ return this.TransferFunction == other.TransferFunction;
+ }
+
+ private static bool IsAlmostEqual(float a, float b)
+ {
+ const float dist = 1e-3f;
+ return MathF.Abs(a - b) < dist;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs
new file mode 100644
index 000000000..95652cdaf
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs
@@ -0,0 +1,53 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.ColorProfiles;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
+
+///
+/// A serializable form of CieXyChromaticityCoordinates
+///
+internal struct JxlCustomXy
+{
+ private const uint Multiplier = 1000000;
+ private const float RoughLimit = 4.0f;
+ private const int Min = -0x200000;
+ private const int Max = 0x1FFFFF;
+
+ public int X { get; set; }
+
+ public int Y { get; set; }
+
+ public readonly CieXyChromaticityCoordinates GetValue() => new(
+ x: this.X * (1.0f / Multiplier),
+ y: this.Y * (1.0f / Multiplier));
+
+ public bool SetValue(CieXyChromaticityCoordinates xy)
+ {
+ bool ok = (Math.Abs(xy.X) < RoughLimit) && (Math.Abs(xy.Y) < RoughLimit);
+
+ if (!ok)
+ {
+ throw new InvalidOperationException("X or Y is out of bounds");
+ }
+
+ this.X = (int)MathF.Round((float)(xy.X * Multiplier));
+
+ if (this.X is < Min or > Max)
+ {
+ throw new InvalidOperationException("X is out of bounds");
+ }
+
+ this.Y = (int)MathF.Round((float)(xy.Y * Multiplier));
+
+ if (this.Y is < Min or > Max)
+ {
+ throw new InvalidOperationException("Y is out of bounds");
+ }
+
+ return true;
+ }
+
+ public readonly bool IsSame(CieXyChromaticityCoordinates other) => this.X == other.X && this.Y == other.Y;
+}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlPrimaries.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlPrimaries.cs
new file mode 100644
index 000000000..48d4766fa
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/JxlPrimaries.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
+
+///
+/// JPEG XL primaries
+///
+internal enum JxlPrimaries : byte
+{
+ ///
+ /// Same as ITU-R BT.709
+ ///
+ SRgb = 1,
+
+ ///
+ /// Values encoded in separate fields
+ ///
+ Custom = 2,
+
+ ///
+ /// ITU-R BT.2020
+ ///
+ Bt2020 = 9,
+
+ P3 = 11,
+}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs
new file mode 100644
index 000000000..739dddccc
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
+
+internal enum JxlRenderingIntent : byte
+{
+ // Values match ICC sRGB encodings
+ Perceptual, // Good for photos, requires a profile with LUT
+ Relative, // Good for logos
+ Saturation, // Perhaps useful for CG with fully saturated colors
+ Absolute, // Leaves white point unchanged; good for proofing
+}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlTransferFunction.cs
new file mode 100644
index 000000000..cfee68176
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/JxlTransferFunction.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
+
+///
+/// JPEG XL transfer function type
+///
+internal enum JxlTransferFunction : byte
+{
+ ///
+ /// ITU-R BT.709
+ ///
+ Bt709 = 1,
+
+ ///
+ /// Unknown transfer function
+ ///
+ Unknown = 2,
+
+ ///
+ /// Linear transfer function
+ ///
+ Linear = 8,
+
+ ///
+ /// sRGB
+ ///
+ SRgb = 13,
+
+ ///
+ /// From ITU-R BT.2100
+ ///
+ Pq = 16,
+
+ ///
+ /// From SMPTE RP 431-2 reference projector
+ ///
+ Dci = 17,
+
+ ///
+ /// From ITU-R BT.2100
+ ///
+ Hlg = 18,
+}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlWhitePoint.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlWhitePoint.cs
new file mode 100644
index 000000000..c6c7e019f
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/JxlWhitePoint.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
+
+///
+/// White point from CICP Color Primaries.
+///
+// Note that we define a separate enum instead of using the ColorPrimaries
+// enum from CICP code because JPEG XL doesn't support all color primaries defined
+// by CICP.
+internal enum JxlWhitePoint : byte
+{
+ ///
+ /// sRGB/ITU-R BT.709/Display P3/ITU-R BT.2020
+ ///
+ D65 = 1,
+
+ ///
+ /// Actual values encoded in separate fields
+ ///
+ Custom = 2,
+
+ ///
+ /// XYZ
+ ///
+ E = 10,
+
+ ///
+ /// DCI-P3
+ ///
+ Dci = 11,
+}
diff --git a/src/ImageSharp/Formats/Jxl/Cms/README.md b/src/ImageSharp/Formats/Jxl/Cms/README.md
new file mode 100644
index 000000000..557a4c8b9
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Cms/README.md
@@ -0,0 +1,4 @@
+# CMS
+This is the JPEG XL Color Management System component.
+
+Not to be confused with Content Management System.
diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs
index d9bc419e9..09400f9a8 100644
--- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs
+++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Diagnostics;
+using SixLabors.ImageSharp.Formats.Jxl.Cms;
using SixLabors.ImageSharp.Formats.Jxl.Fields;
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
@@ -124,5 +125,26 @@ internal sealed class JxlImageMetadata : IJxlFields
this.Modular16BitBufferSufficient = bits <= 12;
}
+ public void SetIntensityTarget()
+ {
+ JxlCustomTransferFunction? tf = this.ColorEncoding?.TransferFunction;
+
+ if (tf is not null)
+ {
+ if (tf.Value.IsPq)
+ {
+ this.SetIntensityTarget(10000);
+ }
+ else if (tf.Value.IsHlg)
+ {
+ this.SetIntensityTarget(1000);
+ }
+ else
+ {
+ this.SetIntensityTarget(DefaultIntensityTarget);
+ }
+ }
+ }
+
public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
}
diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs
index 357ce27c3..dc44594d9 100644
--- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs
+++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs
@@ -12,9 +12,13 @@ internal sealed class JxlOpsinInverseMatrix : IJxlFields
public JxlMatrix3x3F InverseMatrix { get; set; }
- public InlineArray3 OpsinBiases { get; set; }
+ // Prefer arrays so we can set values like this:
+ // JxlOpsinInverseMatrix m = ...;
+ // m.OpsinBiases[0] = 1f;
+ // An InlineArray can't do that.
+ public float[] OpsinBiases { get; set; } = new float[3];
- public InlineArray4 QuantBiases { get; set; }
+ public float[] QuantBiases { get; set; } = new float[4];
public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
}
diff --git a/src/ImageSharp/Formats/Jxl/InlineArrays.cs b/src/ImageSharp/Formats/Jxl/InlineArrays.cs
index 9c19e9266..f006d2fd0 100644
--- a/src/ImageSharp/Formats/Jxl/InlineArrays.cs
+++ b/src/ImageSharp/Formats/Jxl/InlineArrays.cs
@@ -13,6 +13,15 @@ internal struct InlineArray3
private T first;
}
+///
+/// Used by JxlOpsinParameters
+///
+[InlineArray(36)]
+internal struct InlineArray36
+{
+ private T first;
+}
+
///
/// Used by JxlCustomTransformData
///
diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs
new file mode 100644
index 000000000..45c746324
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs
@@ -0,0 +1,157 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.IO;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
+
+internal sealed class JxlDecoderCore : ImageDecoderCore
+{
+ ///
+ /// Identifies the signature of the JPEG XL file.
+ ///
+ private enum JxlSignature : byte
+ {
+ ///
+ /// Error status indicating not enough bytes to detect the signature.
+ ///
+ NotEnoughBytes,
+
+ ///
+ /// A JPEG XL code stream.
+ ///
+ CodeStream,
+
+ ///
+ /// The signature is invalid.
+ ///
+ Invalid,
+
+ ///
+ /// Container format.
+ ///
+ Container
+ }
+
+ ///
+ /// Represents a data type.
+ ///
+ private enum JxlDataType : byte
+ {
+ ///
+ ///
+ ///
+ UInt8,
+
+ ///
+ ///
+ ///
+ UInt16,
+
+ ///
+ ///
+ ///
+ Float,
+
+ ///
+ ///
+ ///
+ Float16
+ }
+
+ public JxlDecoderCore(DecoderOptions options)
+ : base(options)
+ {
+ }
+
+ ///
+ /// Ensures that the coordinates are not out of bounds.
+ ///
+ /// First coordinate
+ /// Second coordinate
+ /// Image width
+ /// Boolean indicating whether the coordinates are out of bounds
+ private static bool IsOutOfBounds(int a, int b, int size)
+ {
+ int position = a + b;
+
+ return position > size || position < a;
+ }
+
+ private static int InitialBasicInfoSizeHint()
+ {
+ const int containerHeaderSize = 48;
+ const int maxCodestreamBasicInfoSize = 50;
+ return containerHeaderSize + maxCodestreamBasicInfoSize;
+ }
+
+ private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length, ref int position)
+ {
+ if (position >= length)
+ {
+ return JxlSignature.NotEnoughBytes;
+ }
+
+ buffer = buffer[position..];
+ length -= position;
+
+ // 0xFF 0x0A represents a codestream
+ if (length >= 1 && buffer[0] == 0xFF)
+ {
+ if (length < 2)
+ {
+ // We need at least two bytes for a valid codestream signature
+ return JxlSignature.NotEnoughBytes;
+ }
+ else if (buffer[1] == CodestreamMarker)
+ {
+ position += 2;
+ return JxlSignature.CodeStream;
+ }
+ else
+ {
+ return JxlSignature.Invalid;
+ }
+ }
+
+ // Container?
+ if (length >= 1 && buffer[0] == 0)
+ {
+ if (length < SignatureBox.Length)
+ {
+ return JxlSignature.NotEnoughBytes;
+ }
+ else if (buffer[SignatureBox.Length..].SequenceEqual(SignatureBox))
+ {
+ position += SignatureBox.Length;
+ return JxlSignature.Container;
+ }
+ else
+ {
+ return JxlSignature.Invalid;
+ }
+ }
+
+ // Signature is invalid
+ return JxlSignature.Invalid;
+ }
+
+ private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length)
+ {
+ int position = 0;
+ return DetectSignature(buffer, length, ref position);
+ }
+
+ private static int BitsPerChannel(JxlDataType dataType)
+ => dataType switch
+ {
+ JxlDataType.UInt8 => 8,
+ JxlDataType.UInt16 or JxlDataType.Float16 => 16,
+ JxlDataType.Float => 32,
+ _ => 0
+ };
+
+ protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) => throw new NotImplementedException();
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs
new file mode 100644
index 000000000..819517551
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs
@@ -0,0 +1,63 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
+
+internal static class JxlNoiseDecoder
+{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void BitsToFloatingPoint(ReadOnlySpan randomBits, Span floats)
+ {
+ Vector bits = new(randomBits);
+ Vector rand12 = ((bits >> 9) | new Vector(0x3F800000u)).As();
+ rand12.StoreUnsafe(ref MemoryMarshal.GetReference(floats));
+ }
+
+ public static void GenerateRandomImage(JxlXorShift rng, Rectangle rectangle, JxlImageF noise)
+ {
+ const int floatsPerBatch = JxlXorShift.Generators * sizeof(ulong) / sizeof(float);
+
+ int xSize = rectangle.Width;
+ int ySize = rectangle.Height;
+
+ Span batch64 = stackalloc ulong[JxlXorShift.Generators];
+ Span batch32 = stackalloc uint[JxlXorShift.Generators * 2];
+
+ // stackalloc doesn't zero-initialize, so clear values
+ batch64.Clear();
+ batch32.Clear();
+
+ int n = Vector.Count;
+
+ for (int y = 0; y < ySize; y++)
+ {
+ Span row = noise.GetRow(rectangle, y);
+ int x = 0;
+ for (; x + floatsPerBatch < xSize; x += floatsPerBatch)
+ {
+ rng.Fill(batch64);
+ MemoryMarshal.Cast(batch32).CopyTo(batch64);
+ for (int i = 0; i < floatsPerBatch; i += n)
+ {
+ BitsToFloatingPoint(batch32[i..], row[(x + i)..]);
+ }
+ }
+
+ rng.Fill(batch64);
+ MemoryMarshal.Cast(batch32).CopyTo(batch64);
+
+ int batchPos = 0;
+
+ for (; x < xSize; x += n)
+ {
+ BitsToFloatingPoint(batch32[batchPos..], row[x..]);
+ batchPos += n;
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs
new file mode 100644
index 000000000..c18a18ef0
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
+
+internal sealed class JxlOpsinParameters
+{
+ // Use arrays instead of InlineArrays because, with inline arrays we can't do:
+ // JxlOpsinParameters parameters = ...;
+ // parameters.OpsinBiasesCbrt[0] /* <-- error */ = 1.25f;
+ public float[] InverseOpsinMatrix { get; set; } = new float[36];
+
+ public float[] OpsinBiases { get; set; } = new float[4];
+
+ public float[] OpsinBiasesCbrt { get; set; } = new float[4];
+
+ public float[] QuantBiases { get; set; } = new float[4];
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs
new file mode 100644
index 000000000..bdec66643
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs
@@ -0,0 +1,78 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using SixLabors.ImageSharp.Formats.Jxl.Cms;
+using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
+
+// Prefer class instead of struct because it's too large for a struct
+// Note that this struct wasn't well documented, so it's not that easy to add
+// XML documentation here.
+internal sealed class JxlOutputEncodingInfo
+{
+ public JxlColorEncoding? OriginalColorEncoding { get; set; }
+
+ public float OriginalIntensityTarget { get; set; }
+
+ public JxlMatrix3x3F OriginalInverseMatrix { get; set; }
+
+ public bool DefaultTransform { get; set; }
+
+ public bool XybEncoded { get; set; }
+
+ ///
+ /// Gets or sets the requested color encoding.
+ ///
+ public JxlColorEncoding ColorEncoding { get; set; } = new();
+
+ public JxlColorEncoding LinearColorEncoding { get; set; } = new();
+
+ public bool ColorEncodingIsOriginal { get; set; }
+
+ public JxlOpsinParameters OpsinParameters { get; set; } = new();
+
+ public bool AllDefaultOpsin { get; set; }
+
+ public float InverseGamma { get; set; }
+
+ public Vector3 Luminances { get; set; }
+
+ public float DesiredIntensityTarget { get; set; }
+
+ public bool CmsSet { get; set; }
+
+ public JxlCmsInterface Cms { get; set; }
+
+ public void SetFromMetadata(JxlCodecMetadata metadata)
+ {
+ JxlImageMetadata imageMetadata = metadata.ImageMetadata ?? throw new InvalidOperationException("Missing image metadata");
+
+ this.OriginalColorEncoding = imageMetadata.ColorEncoding;
+ this.OriginalIntensityTarget = imageMetadata.IntensityTarget;
+ this.DesiredIntensityTarget = this.OriginalIntensityTarget;
+
+ JxlOpsinInverseMatrix inverseMatrix = metadata.CustomTransformData?.OpsinInverseMatrix ?? throw new InvalidOperationException("Missing Opsin inverse matrix or transform data");
+ this.OriginalInverseMatrix = inverseMatrix.InverseMatrix;
+ this.DefaultTransform = inverseMatrix.AllDefault;
+ this.XybEncoded = imageMetadata.XybEncoded;
+
+ JxlOpsinParameters parameters = this.OpsinParameters;
+
+ imageMetadata.OpsinBiases.CopyTo(parameters.OpsinBiases);
+ parameters.OpsinBiasesCbrt[0] = MathF.Cbrt(parameters.OpsinBiases[0]);
+ parameters.OpsinBiasesCbrt[1] = MathF.Cbrt(parameters.OpsinBiases[1]);
+ parameters.OpsinBiasesCbrt[2] = MathF.Cbrt(parameters.OpsinBiases[2]);
+
+ parameters.OpsinBiasesCbrt[3] = 1;
+ parameters.OpsinBiases[3] = 1;
+
+ inverseMatrix.QuantBiases.AsSpan().CopyTo(parameters.QuantBiases);
+
+ bool origOK = JxlXybDecoder.CanOutputToColorEncoding(this.OriginalColorEncoding ?? throw new InvalidCastException("Missing color encoding"));
+ bool origGrey = this.OriginalColorEncoding.IsGray;
+
+ return this.SetColorEncoding(!this.XybEncoded || origOK ? this.OriginalColorEncoding : JxlColorEncoding.LinearSrgb(origGrey));
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs
new file mode 100644
index 000000000..940b01170
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs
@@ -0,0 +1,180 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.Formats.Jxl.Cms;
+using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
+
+///
+/// Decodes the XYB color format (which JPEG XL uses) into RGB.
+///
+internal static class JxlXybDecoder
+{
+ ///
+ /// Converts XYB to RGB using SIMD, one vector at a time.
+ ///
+ /// X channel
+ /// Y channel
+ /// B channel
+ /// Opsin parameters & configuration
+ /// Output R
+ /// Output G
+ /// Output B
+ public static void ConvertXybToRgb(
+ Vector opsinX,
+ Vector opsinY,
+ Vector opsinB,
+ JxlOpsinParameters opsinParameters,
+ ref Vector linearR,
+ ref Vector linearG,
+ ref Vector linearB)
+ {
+ Vector negBiasR = new(opsinParameters.OpsinBiaases[0]);
+ Vector negBiasG = new(opsinParameters.OpsinBiaases[1]);
+ Vector negBiasB = new(opsinParameters.OpsinBiaases[2]);
+
+ Vector gammaR = opsinX + opsinY;
+ Vector gammaG = opsinY - opsinX;
+ Vector gammaB = opsinB;
+
+ Vector gammaR2 = gammaR * gammaR;
+ Vector gammaG2 = gammaG * gammaG;
+ Vector gammaB2 = gammaB * gammaB;
+
+ Vector mixedR = (gammaR2 * gammaR) + negBiasR;
+ Vector mixedG = (gammaG2 * gammaG) + negBiasG;
+ Vector mixedB = (gammaB2 * gammaB) + negBiasB;
+
+ Span inverseMatrix = opsinParameters.GetInverseOpsinMatrixSpan();
+
+ linearR = LoadDuplicate128(ref inverseMatrix[0 * 4]) * mixedR;
+ linearG = LoadDuplicate128(ref inverseMatrix[3 * 4]) * mixedR;
+ linearB = LoadDuplicate128(ref inverseMatrix[6 * 4]) * mixedR;
+
+ linearR = (LoadDuplicate128(ref inverseMatrix[1 * 4]) * mixedG) + linearR;
+ linearG = (LoadDuplicate128(ref inverseMatrix[4 * 4]) * mixedG) + linearG;
+ linearB = (LoadDuplicate128(ref inverseMatrix[7 * 4]) * mixedG) + linearB;
+
+ linearR = (LoadDuplicate128(ref inverseMatrix[2 * 4]) * mixedB) + linearR;
+ linearG = (LoadDuplicate128(ref inverseMatrix[5 * 4]) * mixedB) + linearG;
+ linearB = (LoadDuplicate128(ref inverseMatrix[8 * 4]) * mixedB) + linearB;
+ }
+
+ public static bool OpsinToLinear(JxlImage3F opsin, Rectangle rect, JxlImage3F linear, JxlOpsinParameters opsinParameters)
+ {
+ if (!SameSize(rect, linear))
+ {
+ return false;
+ }
+
+ if (Vector.Count < 4)
+ {
+ // TODO: support 64bit vectors or no SIMD?
+ throw new PlatformNotSupportedException("XYB to RGB conversion requires at least 128-bit SIMD");
+ }
+
+ // Reuse variables instead of creating them over
+ // and over again
+ Unsafe.SkipInit(out Vector linearR);
+ Unsafe.SkipInit(out Vector linearG);
+ Unsafe.SkipInit(out Vector linearB);
+
+ for (int y = 0; y < rect.Height; y++)
+ {
+ ReadOnlySpan rowOpsin0 = opsin.PlaneRow(rect, 0, y);
+ ReadOnlySpan rowOpsin1 = opsin.PlaneRow(rect, 1, y);
+ ReadOnlySpan rowOpsin2 = opsin.PlaneRow(rect, 2, y);
+
+ ref float rowOpsin0Reference = ref MemoryMarshal.GetReference(rowOpsin0);
+ ref float rowOpsin1Reference = ref MemoryMarshal.GetReference(rowOpsin1);
+ ref float rowOpsin2Reference = ref MemoryMarshal.GetReference(rowOpsin2);
+
+ Span rowLinear0 = linear.PlaneRow(0, y);
+ Span rowLinear1 = linear.PlaneRow(1, y);
+ Span rowLinear2 = linear.PlaneRow(2, y);
+
+ ref float rowLinear0Reference = ref MemoryMarshal.GetReference(rowLinear0);
+ ref float rowLinear1Reference = ref MemoryMarshal.GetReference(rowLinear1);
+ ref float rowLinear2Reference = ref MemoryMarshal.GetReference(rowLinear2);
+
+ for (int x = 0; x < rect.Height; x += Vector.Count)
+ {
+ Vector inOpsinX = Vector.LoadUnsafe(ref Unsafe.Add(ref rowOpsin0Reference, x));
+ Vector inOpsinY = Vector.LoadUnsafe(ref Unsafe.Add(ref rowOpsin1Reference, x));
+ Vector inOpsinB = Vector.LoadUnsafe(ref Unsafe.Add(ref rowOpsin2Reference, x));
+
+ ConvertXybToRgb(inOpsinX, inOpsinY, inOpsinB, opsinParameters, ref linearR, ref linearG, ref linearB);
+
+ linearR.StoreUnsafe(ref Unsafe.Add(ref rowLinear0Reference, x));
+ linearG.StoreUnsafe(ref Unsafe.Add(ref rowLinear1Reference, x));
+ linearB.StoreUnsafe(ref Unsafe.Add(ref rowLinear2Reference, x));
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// A SIMD utility method which reads next 128 bits
+ /// (which in this case happens to be next 4 floats),
+ /// and duplicates them to fit in the CPU vector size.
+ /// For example,
+ ///
+ /// 128 bit vectors: A B C D (as-is)
+ /// 256 bit vectors: A B C D A B C D (duplicate once)
+ /// 512 bit vectors: A B C D A B C D A B C D A B C D (duplicate three times)
+ ///
+ /// Vector<T> has support for arbitrarily large
+ /// vector sizes. For example, some ARM CPUs support 2048-bit
+ /// vectors through Vector<T>. In that specific case, this
+ /// method can be used for future-proofing.
+ ///
+ /// Note that this method, albeit future-proof, may be considered
+ /// slow for smaller vector sizes (think CPUs with 128bit or 256bit vectors).
+ ///
+ /// Reference to first element to load & duplicate.
+ /// Vector with first 128 bits duplicated across the vector width.
+ private static Vector LoadDuplicate128(ref float reference)
+ {
+ Span value = stackalloc float[Vector.Count];
+ Span values128 = [
+ reference,
+ Unsafe.Add(ref reference, 1),
+ Unsafe.Add(ref reference, 2),
+ Unsafe.Add(ref reference, 3)
+ ];
+
+ for (int i = 0; i < Vector.Count; i += 4)
+ {
+ values128[i..].CopyTo(value[i..]);
+ }
+
+ return new(value);
+ }
+
+ public static bool CanOutputToColorEncoding(JxlColorEncoding colorEncoding)
+ {
+ if (!colorEncoding.HaveFields)
+ {
+ return false;
+ }
+
+ JxlCustomTransferFunction tf = colorEncoding.TransferFunction;
+
+ if (!tf.IsPq && !tf.IsSrgb && !tf.HaveGamma && !tf.IsLinear && !tf.IsHlg && !tf.IsDci && !tf.Is709)
+ {
+ return false;
+ }
+
+ if (colorEncoding.IsGray && colorEncoding.WhitePoint != JxlWhitePoint.D65)
+ {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs
new file mode 100644
index 000000000..ccea59b2c
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs
@@ -0,0 +1,91 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using System.Security.Principal;
+using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy;
+using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+///
+/// Helps with reordering coefficients.
+///
+internal static class JxlCoefficientOrder
+{
+ public const int Limit = 6156;
+
+ public const int CoefficientOrderMaxSize = Limit * JxlFrameDimensions.DctBlockSize;
+
+ public const int PermutationContexts = 8;
+
+ ///
+ /// Gets the pattern which coefficients must follow to compute offsets.
+ ///
+ public static ReadOnlySpan CoefficientOrderOffsets =>
+ [
+ 0, 1, 2, 3, 4, 5, 6, 10, 14, 18,
+ 34, 50, 66, 68, 70, 72, 76, 80, 84, 92,
+ 100, 108, 172, 236, 300, 332, 364, 396, 652, 908,
+ 1164, 1292, 1420, 1548, 2572, 3596, 4620, 5132, 5644, Limit
+ ];
+
+ ///
+ /// Gets the pattern which coefficients must follow to compute offsets.
+ ///
+ public static ReadOnlySpan StrategyOrder =>
+ [
+ 0, 1, 1, 1, 2, 3, 4, 4, 5, 5, 6, 6, 1, 1,
+ 1, 1, 1, 1, 7, 8, 8, 9, 10, 10, 11, 12, 12,
+ ];
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int CoeffOrderOffset(int o, int c) => CoefficientOrderOffsets[(3 * o) + c] * JxlFrameDimensions.DctBlockSize;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static uint CoeffOrderContext(uint value)
+ {
+ uint token = 0;
+ uint nbits = 0;
+ uint bits = 0;
+
+ new JxlAnsHybridUIntConfiguration(0, 0, 0).Encode(value, ref token, ref nbits, ref bits);
+
+ return Math.Min(token, PermutationContexts - 1u);
+ }
+
+ public static bool ReadPermutation(int skip, int size, Span order, JxlBitReader bitReader, JxlAnsSymbolReader reader, Span contextMap)
+ {
+ Span lehmer = stackalloc uint[size];
+ lehmer.Clear();
+
+ Span temp = stackalloc uint[size * 2];
+ temp.Clear();
+
+ uint end = reader.ReadHybridUnsignedInteger(CoeffOrderContext((int)size), bitReader, contextMap) + skip;
+
+ if (end > size)
+ {
+ throw new InvalidOperationException("Invalid permutation size");
+ }
+
+ uint last = 0;
+
+ for (int i = skip; i < end; i++)
+ {
+ lehmer[i] = reader.ReadHybridUnsignedInteger(CoeffOrderContext(last), bitReader, contextMap);
+ last = lehmer[i];
+ if (lehmer[i] >= size - i)
+ {
+ throw new InvalidOperationException("Invalid lehmer code");
+ }
+ }
+
+ if (order.IsEmpty)
+ {
+ return true;
+ }
+
+ return JxlLehmerCode.DecodeLehmerCode(lehmer, temp, size, order);
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs
new file mode 100644
index 000000000..9bf6a7dcb
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs
@@ -0,0 +1,424 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.Intrinsics;
+using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+///
+/// Convolution filters
+///
+internal static class JxlConvolve
+{
+ ///
+ /// Weighted sum of 1x5 pixels around ix, iy with [wx2, wx1, wx0, wx1, wx2].
+ ///
+ public static float WeightedSumBorder(
+ JxlImageF input,
+ Func wrapY,
+ long ix,
+ long iy,
+ int width,
+ int height,
+ float wx0,
+ float wx1,
+ float wx2)
+ {
+ ReadOnlySpan row = input.GetRow(wrapY(iy, height));
+
+ float inM2 = row[WrapMirror(ix - 2, width)];
+ float inP2 = row[WrapMirror(ix + 2, width)];
+ float inM1 = row[WrapMirror(ix - 1, width)];
+ float inP1 = row[WrapMirror(ix + 1, width)];
+ float in00 = row[(int)ix];
+
+ float sum2 = wx2 * (inM2 + inP2);
+ float sum1 = wx1 * (inM1 + inP1);
+ float sum0 = wx0 * in00;
+
+ return sum2 + (sum1 + sum0);
+ }
+
+ public static Vector WeightedSum(
+ JxlImageF input,
+ Func wrapY,
+ int ix,
+ long iy,
+ int height,
+ Vector wx0,
+ Vector wx1,
+ Vector wx2)
+ {
+ ReadOnlySpan center = input.GetRow(wrapY(iy, height))[ix..];
+ ref float centerRef = ref MemoryMarshal.GetReference(center);
+
+ Vector inM2 = Vector.LoadUnsafe(ref Unsafe.Subtract(ref centerRef, 2));
+ Vector inP2 = Vector.LoadUnsafe(ref Unsafe.Add(ref centerRef, 2));
+ Vector inM1 = Vector.LoadUnsafe(ref Unsafe.Subtract(ref centerRef, 1));
+ Vector inP1 = Vector.LoadUnsafe(ref Unsafe.Add(ref centerRef, 1));
+ Vector in00 = Vector.LoadUnsafe(ref centerRef);
+
+ Vector sum2 = wx2 * (inM2 + inP2);
+ Vector sum1 = wx1 * (inM1 + inP1);
+ Vector sum0 = wx0 * in00;
+
+ return sum2 + (sum1 + sum0);
+ }
+
+ public static float Symmetric5Border(JxlImageF input, Func wrapY, long ix, long iy, JxlWeightsSymmetric5 weights)
+ {
+ float w0 = weights.GetCVector()[0];
+ float w1 = weights.GetRVector()[0];
+ float w2 = weights.GetR2Vector()[0];
+ float w4 = weights.GetDVector()[0];
+ float w5 = weights.GetCVector()[0];
+ float w8 = weights.GetD2Vector()[0];
+
+ int width = input.XSize;
+ int height = input.YSize;
+
+ float sum0 = WeightedSumBorder(input, wrapY, ix, iy, width, height, w0, w1, w2)
+ + WeightedSumBorder(input, wrapY, ix, iy - 2, width, height, w2, w5, w8);
+
+ float sum1 = WeightedSumBorder(input, wrapY, ix, iy + 2, width, height, w2, w5, w8);
+
+ sum0 += WeightedSumBorder(input, wrapY, ix, iy + 1, width, height, w1, w4, w5);
+ sum1 += WeightedSumBorder(input, wrapY, ix, iy - 1, width, height, w1, w4, w5);
+
+ return sum0 + sum1;
+ }
+
+ public static void Symmetric5Interior(
+ JxlImageF image,
+ int ix,
+ Func wrapY,
+ int rix,
+ long iy,
+ JxlWeightsSymmetric5 weights,
+ Span rowOut)
+ {
+ Vector w0 = LoadDuplicate128(weights.GetCVector()); // c
+ Vector w1 = LoadDuplicate128(weights.GetRVector()); // r
+ Vector w2 = LoadDuplicate128(weights.GetR2Vector()); // R
+ Vector w4 = LoadDuplicate128(weights.GetDVector()); // d
+ Vector w5 = LoadDuplicate128(weights.GetLVector()); // L
+ Vector w8 = LoadDuplicate128(weights.GetD2Vector()); // D
+
+ int height = image.YSize;
+ Vector sum0 = WeightedSum(image, wrapY, ix, iy, height, w0, w1, w2)
+ + WeightedSum(image, wrapY, ix, iy - 2, height, w2, w5, w8);
+
+ Vector sum1 = WeightedSum(image, wrapY, ix, iy + 2, height, w2, w5, w8);
+
+ sum0 += WeightedSum(image, wrapY, ix, iy - 1, height, w1, w4, w5);
+ sum1 += WeightedSum(image, wrapY, ix, iy + 1, height, w1, w4, w5);
+
+ (sum0 + sum1).StoreUnsafe(ref Unsafe.Add(ref MemoryMarshal.GetReference(rowOut), rix));
+ }
+
+ public static void Symmetric5Row(
+ JxlImageF image,
+ Func wrapY,
+ in Rectangle rect,
+ long iy,
+ JxlWeightsSymmetric5 weights,
+ Span rowOut)
+ {
+ const int radius = 2;
+ int xEnd = rect.Right;
+
+ int rix = 0;
+ int ix = rect.X;
+
+ int n = Vector.Count;
+ int alignedX = RoundUpTo(radius, n);
+
+ for (; ix < Math.Min(alignedX, xEnd); ix++, rix++)
+ {
+ rowOut[rix] = Symmetric5Border(image, wrapY, ix, iy, weights);
+ }
+
+ for (; ix + n + radius <= xEnd; ix += n, rix += n)
+ {
+ Symmetric5Interior(image, ix, wrapY, rix, iy, weights, rowOut);
+ }
+
+ for (; ix < xEnd; ix++, rix++)
+ {
+ rowOut[rix] = Symmetric5Border(image, wrapY, ix, iy, weights);
+ }
+ }
+
+ public static bool Symmetric5(
+ JxlImageF input,
+ in Rectangle rectangle,
+ JxlWeightsSymmetric5 weights,
+ JxlImageF output,
+ Rectangle outputRect)
+ {
+ if (rectangle.Width != outputRect.Width || rectangle.Height != outputRect.Height)
+ {
+ return false;
+ }
+
+ int height = rectangle.Height;
+
+ for (int riy = 0; riy < height; riy++)
+ {
+ int iy = rectangle.Y + riy;
+
+ if (iy < 2 || iy >= rectangle.Height - 2)
+ {
+ Symmetric5Row(input, WrapMirror, in rectangle, iy, weights, output.GetRow(outputRect, riy));
+ }
+ else
+ {
+ Symmetric5Row(input, in rectangle, iy, weights, output.GetRow(outputRect, riy));
+ }
+ }
+
+ return true;
+ }
+
+ public static float SlowSymmetric3Pixel(
+ JxlImageF image,
+ int x,
+ int y,
+ int width,
+ int height,
+ JxlWeightsSymmetric3 weights,
+ Func wrapX,
+ Func wrapY)
+ {
+ float sum = 0.0f;
+
+ float c0 = weights.GetCVector()[0];
+ float r0 = weights.GetRVector()[0];
+ float d0 = weights.GetDVector()[0];
+
+ for (int ky = -1; ky <= 1; ky++)
+ {
+ int yy = wrapY(y + ky, height);
+ ReadOnlySpan row = image.GetRow(yy);
+
+ float wc = (ky == 0) ? c0 : r0;
+ float wlr = (ky == 0) ? r0 : d0;
+
+ int xm1 = wrapX(x - 1, width);
+ int xp1 = wrapX(x + 1, width);
+
+ sum += (row[x] * wc) + ((row[xm1] + row[xp1]) * wlr);
+ }
+
+ return sum;
+ }
+
+ public static void SlowSymmetric3Row(
+ JxlImageF image,
+ int y,
+ int width,
+ int height,
+ JxlWeightsSymmetric3 weights,
+ Span outputRow,
+ Func wrapY)
+ {
+ outputRow[0] = SlowSymmetric3Pixel(
+ image,
+ 0,
+ y,
+ width,
+ height,
+ weights,
+ WrapMirror,
+ wrapY);
+
+ for (int x = 1; x < width - 1; x++)
+ {
+ outputRow[x] = SlowSymmetric3Pixel(
+ image,
+ x,
+ y,
+ width,
+ height,
+ weights,
+ WrapUnchanged,
+ wrapY);
+ }
+
+ outputRow[width - 1] = SlowSymmetric3Pixel(
+ image,
+ width - 1,
+ y,
+ width,
+ height,
+ weights,
+ WrapMirror,
+ wrapY);
+ }
+
+ public static void SlowSymmetric3(
+ JxlImageF input,
+ Rectangle rect,
+ JxlWeightsSymmetric3 weights,
+ JxlImageF output)
+ {
+ int width = rect.Width;
+ int height = rect.Height;
+
+ const int radius = 1;
+
+ for (int y = 0; y < height; y++)
+ {
+ Span rowOut = output.GetRow(y);
+
+ if (y < radius || y >= height - radius)
+ {
+ SlowSymmetric3Row(
+ input,
+ y,
+ width,
+ height,
+ weights,
+ rowOut,
+ WrapMirror);
+ }
+ else
+ {
+ SlowSymmetric3Row(
+ input,
+ y,
+ width,
+ height,
+ weights,
+ rowOut,
+ WrapUnchanged);
+ }
+ }
+ }
+
+ public static float SlowSeparablePixel(
+ JxlImageF image,
+ Rectangle rect,
+ int x,
+ int y,
+ int radius,
+ ReadOnlySpan horzWeights,
+ ReadOnlySpan vertWeights)
+ {
+ int width = image.XSize;
+ int height = image.YSize;
+
+ float sum = 0;
+
+ for (int dy = -radius; dy <= radius; dy++)
+ {
+ float wy = vertWeights[Math.Abs(dy) * 4];
+ int sy = WrapMirror(rect.Y + y + dy, height);
+ ReadOnlySpan row = image.GetRow(sy);
+
+ for (int dx = -radius; dx <= radius; dx++)
+ {
+ float wx = horzWeights[Math.Abs(dx) * 4];
+ int sx = WrapMirror(rect.X + x + dx, width);
+ sum += row[sx] * wx * wy;
+ }
+ }
+
+ return sum;
+ }
+
+ public static void SlowSeparable(
+ JxlImageF input,
+ Rectangle inputRect,
+ JxlWeightsSeparable5 weights,
+ JxlImageF output,
+ Rectangle outputRect,
+ int radius)
+ {
+ ReadOnlySpan horz = weights.Horizontal;
+ ReadOnlySpan vert = weights.Vertical;
+
+ for (int y = 0; y < inputRect.Height; y++)
+ {
+ Span rowOut = output.GetRow(outputRect, y);
+
+ for (int x = 0; x < inputRect.Width; x++)
+ {
+ rowOut[x] = SlowSeparablePixel(
+ input,
+ inputRect,
+ x,
+ y,
+ radius,
+ horz,
+ vert);
+ }
+ }
+ }
+
+ public static void SlowSeparable5(
+ JxlImageF input,
+ Rectangle inputRect,
+ JxlWeightsSeparable5 weights,
+ JxlImageF output,
+ Rectangle outputRect)
+ => SlowSeparable(input, inputRect, weights, output, outputRect, 2);
+
+ public static void FirstL1(ReadOnlySpan c, Span dst)
+ {
+ dst[0] = c[0];
+ for (int i = 1; i < dst.Length; i++)
+ {
+ dst[i] = c[i - 1];
+ }
+ }
+
+ public static void FirstL2(ReadOnlySpan c, Span dst)
+ {
+ dst[0] = c[1];
+ dst[1] = c[0];
+
+ for (int i = 2; i < dst.Length; i++)
+ {
+ dst[i] = c[i - 2];
+ }
+ }
+
+ ///
+ /// A SIMD utility method which takes in the 128 bit vector
+ /// and duplicates its values to fit in the CPU vector size.
+ /// For example,
+ ///
+ /// 128 bit vectors: A B C D (as-is)
+ /// 256 bit vectors: A B C D A B C D (duplicate once)
+ /// 512 bit vectors: A B C D A B C D A B C D A B C D (duplicate three times)
+ ///
+ /// Vector<T> has support for arbitrarily large
+ /// vector sizes. For example, some ARM CPUs support 2048-bit
+ /// vectors through Vector<T>. In that specific case, this
+ /// method can be used for future-proofing.
+ ///
+ /// Note that this method, albeit future-proof, may be considered
+ /// slow for smaller vector sizes (think CPUs with 256bit vectors).
+ ///
+ /// Vector to duplicate.
+ /// New vector that is duplicated across the width.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector LoadDuplicate128(Vector128 vec)
+ {
+ Span value = stackalloc float[Vector.Count];
+ for (int i = 0; i < Vector.Count; i += 4)
+ {
+ vec.CopyTo(value[i..]);
+ }
+
+ return new(value);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int RoundUpTo(int value, int multiple) => ((value + multiple - 1) / multiple) * multiple;
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs
index 84e7819bc..26bc7ee8e 100644
--- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs
@@ -5,7 +5,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
///
/// Read-only cosine lookups for the Discrete Cosine Transform (DCT),
-/// a mathematical function used for quantization.
+/// a mathematical function used for quantization and coefficient reordering.
///
internal static class JxlDctScales
{
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs
new file mode 100644
index 000000000..21fa5d23b
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs
@@ -0,0 +1,191 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
+using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
+
+///
+/// Table of Contents encoding
+///
+internal static class JxlToc
+{
+ private static readonly JxlU32Enc TocDistribution = new(
+ JxlFieldExpressions.Bits(10),
+ JxlFieldExpressions.BitsOffset(14, 2024),
+ JxlFieldExpressions.BitsOffset(22, 17408),
+ JxlFieldExpressions.BitsOffset(30, 4211712));
+
+ public static int AcGroupIndex(int pass, int group, int numGroups, int numDcGroups)
+ => 2 + numDcGroups + (pass * numGroups) + group;
+
+ public static int NumberOfTocEntries(int numGroups, int numDcGroups, int numPasses)
+ {
+ if (numGroups == 1 && numPasses == 1)
+ {
+ return 1;
+ }
+
+ return AcGroupIndex(0, 0, numGroups, numDcGroups) + (numGroups * numPasses);
+ }
+
+ private const int BitsPerByte = 8;
+ private const int MaxTocEntries = 65536;
+
+ public static bool ReadToc(
+ Configuration configuration,
+ int tocEntries,
+ JxlBitReader reader,
+ List sizes,
+ List permutation)
+ {
+ if (tocEntries > MaxTocEntries)
+ {
+ return false; // too many TOC entries
+ }
+
+ sizes.Clear();
+ sizes.Capacity = tocEntries;
+
+ for (int i = 0; i < tocEntries; i++)
+ {
+ sizes.Add(0);
+ }
+
+ if (reader.TotalBitsConsumed >= reader.TotalBytes * BitsPerByte)
+ {
+ return false; // not enough bytes
+ }
+
+ bool CheckBitBudget(int numEntries)
+ {
+ long minimalBitCost = numEntries * (2 + 10);
+ long bitBudget = reader.TotalBytes * BitsPerByte;
+ long expenses = reader.TotalBitsConsumed;
+
+ return expenses <= bitBudget &&
+ minimalBitCost <= bitBudget - expenses;
+ }
+
+ if (tocEntries <= 0)
+ {
+ return false;
+ }
+
+ if (reader.ReadBits32(1) == 1)
+ {
+ if (!CheckBitBudget(tocEntries))
+ {
+ return false;
+ }
+
+ permutation.Clear();
+
+ for (int i = 0; i < tocEntries; i++)
+ {
+ permutation.Add(default);
+ }
+
+ if (!DecodePermutation(
+ configuration,
+ 0,
+ tocEntries,
+ permutation,
+ reader))
+ {
+ return false;
+ }
+ }
+
+ if (!reader.JumpToByteBoundary())
+ {
+ return false;
+ }
+
+ if (!CheckBitBudget(tocEntries))
+ {
+ return false;
+ }
+
+ for (int i = 0; i < tocEntries; i++)
+ {
+ sizes[i] = JxlU32Coder.Read(TocDistribution, reader);
+ }
+
+ if (!reader.JumpToByteBoundary())
+ {
+ return false;
+ }
+
+ return CheckBitBudget(0);
+ }
+
+ public static bool ReadGroupOffsets(
+ Configuration configuration,
+ int tocEntries,
+ JxlBitReader reader,
+ List offsets,
+ List sizes,
+ out ulong totalSize)
+ {
+ totalSize = 0;
+
+ List permutation = [];
+
+ if (!ReadToc(
+ configuration,
+ tocEntries,
+ reader,
+ sizes,
+ permutation))
+ {
+ return false;
+ }
+
+ offsets.Clear();
+ offsets.Capacity = tocEntries;
+
+ for (int i = 0; i < tocEntries; i++)
+ {
+ offsets.Add(0);
+ }
+
+ ulong offset = 0;
+
+ for (int i = 0; i < tocEntries; i++)
+ {
+ ulong size = sizes[i];
+
+ if (offset + size < offset)
+ {
+ return false;
+ }
+
+ offsets[i] = offset;
+ offset += size;
+ }
+
+ totalSize = offset;
+
+ if (permutation.Count != 0)
+ {
+ List permutedOffsets = new(tocEntries);
+ List permutedSizes = new(tocEntries);
+
+ foreach (byte index in permutation)
+ {
+ permutedOffsets.Add(offsets[index]);
+ permutedSizes.Add(sizes[index]);
+ }
+
+ offsets.Clear();
+ offsets.AddRange(permutedOffsets);
+
+ sizes.Clear();
+ sizes.AddRange(permutedSizes);
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs
index b3849d69f..78d8900a5 100644
--- a/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs
+++ b/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs
@@ -7,6 +7,8 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
internal sealed class JxlXorShift
{
+ public const int Generators = 8;
+
private readonly ulong[] s0 = new ulong[8];
private readonly ulong[] s1 = new ulong[8];