From decc0b232c33fa0f18d7db50f01e743af6410521 Mon Sep 17 00:00:00 2001
From: winscripter <142818255+winscripter@users.noreply.github.com>
Date: Sat, 18 Jul 2026 16:07:46 +0400
Subject: [PATCH] Fully implement JPEG XL fields
See fields.h and fields.cc
---
.../Jxl/Fields/JxlAllDefaultVisitor.cs | 35 +++++
.../Formats/Jxl/Fields/JxlBitsCoder.cs | 41 ++++++
.../Formats/Jxl/Fields/JxlBundle.cs | 89 ++++++++++++
.../Formats/Jxl/Fields/JxlCanEncodeVisitor.cs | 118 ++++++++++++++++
.../Formats/Jxl/Fields/JxlExtensionStates.cs | 42 ++++++
.../Formats/Jxl/Fields/JxlF16Coder.cs | 73 ++++++++++
.../Formats/Jxl/Fields/JxlInitVisitor.cs | 51 +++++++
.../Formats/Jxl/Fields/JxlReadVisitor.cs | 132 ++++++++++++++++++
.../Jxl/Fields/JxlSetDefaultVisitor.cs | 49 +++++++
.../Formats/Jxl/Fields/JxlU32Coder.cs | 127 +++++++++++++++++
.../Formats/Jxl/Fields/JxlU64Coder.cs | 105 ++++++++++++++
.../Formats/Jxl/Fields/JxlVisitor.cs | 92 ++++++++++++
.../Formats/Jxl/Fields/JxlVisitorBase.cs | 74 ++++++++++
src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs | 23 ++-
.../Formats/Jxl/Metadata/JxlBitDepth.cs | 2 +-
.../Jxl/Metadata/JxlCustomTransformData.cs | 2 +-
16 files changed, 1046 insertions(+), 9 deletions(-)
create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlExtensionStates.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs
create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs
new file mode 100644
index 000000000..cfe143019
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+internal sealed class JxlAllDefaultVisitor : JxlVisitorBase
+{
+ public bool IsAllDefault { get; private set; } = true;
+
+ public override bool Bits(int bits, uint defaultValue, ref uint value)
+ {
+ this.IsAllDefault = value == defaultValue;
+ return true;
+ }
+
+ public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value)
+ {
+ this.IsAllDefault = value == defaultValue;
+ return true;
+ }
+
+ public override bool U64(ulong defaultValue, ref ulong value)
+ {
+ this.IsAllDefault = value == defaultValue;
+ return true;
+ }
+
+ public override bool F16(float defaultValue, ref float value)
+ {
+ this.IsAllDefault = MathF.Abs(value - defaultValue) < 1E-6f;
+ return true;
+ }
+
+ public override bool AllDefault(IJxlFields fields, ref bool allDefault) => false;
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs
new file mode 100644
index 000000000..398587baa
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Formats.Jxl.IO;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+///
+/// Raw bits coder
+///
+internal static class JxlBitsCoder
+{
+ ///
+ /// Maximum number of encodeable bits. Since this coder encodes
+ /// bits raw, this happens to be whatever is passed to it.
+ ///
+ // Looks like that's what the function does (fields.cc:406):
+ // it returns whatever is passed to it.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int MaxEncodedBits(int bits) => bits;
+
+ public static bool CanEncode(int bits, uint value, ref int encodedBits)
+ {
+ encodedBits = bits;
+ if (value >= (1 << bits))
+ {
+ DebugGuard.IsTrue(false, "Value is too large");
+
+ return false;
+ }
+
+ return true;
+ }
+
+ // NOTE: BitsCoder::Read (fields.cc:418) returns a uint32_t,
+ // suggesting the input bit size does not exceed 32 bits.
+ public static uint Read(uint bits, JxlBitReader reader) => reader.ReadBits32(bits);
+
+ public static uint Read(int bits, JxlBitReader reader) => reader.ReadBits32((uint)bits);
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs
new file mode 100644
index 000000000..e6e673f9c
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs
@@ -0,0 +1,89 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.IO;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+///
+/// An helper.
+///
+internal static class JxlBundle
+{
+ ///
+ /// Initializes the specified JXL fields.
+ ///
+ /// The JXL fields.
+ public static void Init(IJxlFields fields)
+ {
+ JxlInitVisitor initVisitor = new();
+
+ if (!initVisitor.Visit(fields))
+ {
+ DebugGuard.IsTrue(false, "Init should never fail");
+ }
+ }
+
+ ///
+ /// Sets all JXL fields provided by the input value to their defaults.
+ ///
+ /// The JXL fields.
+ public static void SetDefault(IJxlFields fields)
+ {
+ JxlSetDefaultVisitor visitor = new();
+
+ if (!visitor.Visit(fields))
+ {
+ DebugGuard.IsTrue(false, "SetDefault should never fail");
+ }
+ }
+
+ ///
+ /// Returns a value indicating whether every value provided by this
+ /// field is a default value. If at least one field isn't a default
+ /// value, the method returns false.
+ ///
+ /// The JXL fields.
+ /// A boolean indicating whether or not are all values initialized to their default values.
+ public static bool AllDefault(IJxlFields fields)
+ {
+ JxlAllDefaultVisitor allDefaultVisitor = new();
+
+ if (!allDefaultVisitor.Visit(fields))
+ {
+ DebugGuard.IsTrue(false, "AllDefault should never fail");
+ }
+
+ return allDefaultVisitor.IsAllDefault;
+ }
+
+ ///
+ /// Reads the fields from a bit-reader.
+ ///
+ /// The bit-reader.
+ /// The fields.
+ /// Status of the read operation.
+ public static bool Read(JxlBitReader reader, IJxlFields fields)
+ {
+ JxlReadVisitor visitor = new(reader);
+ if (!visitor.Visit(fields))
+ {
+ return false;
+ }
+
+ return visitor.OK;
+ }
+
+ ///
+ /// Tries to read the fields from a bit-reader.
+ ///
+ /// The bit-reader.
+ /// The fields.
+ /// Status of the read operation.
+ public static bool CanRead(JxlBitReader reader, IJxlFields fields)
+ {
+ JxlReadVisitor visitor = new(reader);
+ _ = visitor.Visit(fields);
+ return visitor.OK;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs
new file mode 100644
index 000000000..aae11aa12
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs
@@ -0,0 +1,118 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+internal sealed class JxlCanEncodeVisitor : JxlVisitorBase
+{
+ private long encodedBits;
+ private ulong extensions;
+ private long posAfterExt;
+
+ public bool OK { get; set; } = true;
+
+ public override bool Bits(int bits, uint defaultValue, ref uint value)
+ {
+ int enc = 0;
+ this.OK &= JxlBitsCoder.CanEncode(bits, value, ref enc);
+ this.encodedBits += enc;
+ return true;
+ }
+
+ public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value)
+ {
+ int encBits = 0;
+ this.OK &= JxlU32Coder.CanEncode(enc, value, ref encBits);
+ this.encodedBits += encBits;
+ return true;
+ }
+
+ public override bool U64(ulong defaultValue, ref ulong value)
+ {
+ int encBits = 0;
+ this.OK &= JxlU64Coder.CanEncode(value, ref encBits);
+ this.encodedBits += encBits;
+ return true;
+ }
+
+ public override bool F16(float defaultValue, ref float value)
+ {
+ int encBits = 0;
+ this.OK &= JxlF16Coder.CanEncode(value, ref encBits);
+ this.encodedBits += encBits;
+ return true;
+ }
+
+ public override bool AllDefault(IJxlFields fields, ref bool allDefault)
+ {
+ allDefault = JxlBundle.AllDefault(fields);
+ if (!this.Boolean(true, ref allDefault))
+ {
+ return false;
+ }
+
+ return allDefault;
+ }
+
+ public override bool BeginExtensions(ref ulong extensions)
+ {
+ if (!base.BeginExtensions(ref extensions))
+ {
+ return false;
+ }
+
+ this.extensions = extensions;
+
+ if (extensions != 0uL)
+ {
+ if (this.posAfterExt != 0)
+ {
+ return false;
+ }
+
+ this.posAfterExt = this.encodedBits;
+
+ if (this.posAfterExt == 0)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public bool GetSizes(ref int extensionBits, ref long totalBits)
+ {
+ if (!this.OK)
+ {
+ return false;
+ }
+
+ extensionBits = 0;
+ totalBits = this.encodedBits;
+
+ if (this.posAfterExt != 0)
+ {
+ if (this.encodedBits < this.posAfterExt)
+ {
+ return false;
+ }
+
+ extensionBits = (int)this.encodedBits - (int)this.posAfterExt;
+ int encodedBits = 0;
+ this.OK &= JxlU64Coder.CanEncode(extensionBits, ref encodedBits);
+ totalBits += encodedBits;
+
+ for (int i = 1; i < BitOperations.PopCount(this.extensions); i++)
+ {
+ encodedBits = 0;
+ this.OK &= JxlU64Coder.CanEncode(0, ref encodedBits);
+ totalBits += encodedBits;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlExtensionStates.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlExtensionStates.cs
new file mode 100644
index 000000000..2cc3c8258
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlExtensionStates.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+internal sealed class JxlExtensionStates
+{
+ private ulong begun;
+ private ulong ended;
+
+ public bool IsBegun => (this.begun & 1) != 0;
+
+ public bool IsEnded => (this.ended & 1) != 0;
+
+ public void Push()
+ {
+ this.begun <<= 1;
+ this.ended <<= 1;
+ }
+
+ public void Pop()
+ {
+ this.begun >>= 1;
+ this.ended >>= 1;
+ }
+
+ public void Begin()
+ {
+ DebugGuard.IsFalse(this.IsBegun, nameof(this.IsBegun), "This must be false.");
+ DebugGuard.IsFalse(this.IsEnded, nameof(this.IsEnded), "This must be false.");
+
+ this.begun++;
+ }
+
+ public void End()
+ {
+ DebugGuard.IsTrue(this.IsBegun, nameof(this.IsBegun), "This must be true.");
+ DebugGuard.IsFalse(this.IsEnded, nameof(this.IsEnded), "This must be false.");
+
+ this.ended++;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs
new file mode 100644
index 000000000..7bf83e185
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Formats.Jxl.IO;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+///
+/// Represents the Half-precision Floating-point number coder.
+///
+internal static class JxlF16Coder
+{
+ ///
+ /// Always returns 16, which is the maximum possible encoded bits.
+ /// The F16 coder always reads 16 bits from the bitstream.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int MaxEncodedBits() => 16;
+
+ ///
+ /// Returns a boolean indicating whether the input float
+ /// can be represented properly when encoded into a bit-stream.
+ /// Also stores the maximum encodeable bits into encodedBits (which is
+ /// always 16).
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool CanEncode(float value, ref int encodedBits)
+ {
+ encodedBits = MaxEncodedBits();
+ if (float.IsNaN(value) || float.IsInfinity(value))
+ {
+ return false; // NaN and Infinity are not valid
+ }
+
+ return MathF.Abs(value) <= 65504.0f;
+ }
+
+ public static bool Read(JxlBitReader reader, ref float value)
+ {
+ uint bits16 = reader.ReadBits32(16u);
+ uint sign = bits16 >> 15;
+ uint biasedExponent = (bits16 >> 10) & 0x1Fu;
+ uint mantissa = bits16 & 0x3FFu;
+
+ if (biasedExponent == 31u)
+ {
+ // NaN and Infinity are not valid
+ return false;
+ }
+
+ if (biasedExponent == 0u)
+ {
+ // Subnormal or zero.
+ value = (1.0f / 16384) * (mantissa * (1.0f / 1024));
+ if (sign != 0u)
+ {
+ value = -value;
+ }
+
+ return true;
+ }
+
+ uint biasedExp32 = biasedExponent + (127u - 15u);
+ uint mantissa32 = mantissa << (23 - 10);
+ uint bits32 = (sign << 31) | (biasedExp32 << 23) | mantissa32;
+
+ value = BitConverter.UInt32BitsToSingle(bits32);
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs
new file mode 100644
index 000000000..28c16c4e4
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+///
+/// This variant of sets values
+/// to be the default value.
+///
+internal sealed class JxlInitVisitor : JxlVisitorBase
+{
+ public override bool Bits(int bits, uint defaultValue, ref uint value)
+ {
+ value = defaultValue;
+ return true;
+ }
+
+ public override bool U32(JxlU32Distribution d0, JxlU32Distribution d1, JxlU32Distribution d2, JxlU32Distribution d3, uint defaultValue, ref uint value)
+ {
+ value = defaultValue;
+ return true;
+ }
+
+ public override bool U64(ulong defaultValue, ref ulong value)
+ {
+ value = defaultValue;
+ return true;
+ }
+
+ public override bool Boolean(bool defaultValue, ref bool value)
+ {
+ value = defaultValue;
+ return true;
+ }
+
+ public override bool F16(float defaultValue, ref float value)
+ {
+ value = defaultValue;
+ return true;
+ }
+
+ public override bool Conditional(bool condition) => true;
+
+ public override bool AllDefault(IJxlFields fields, ref bool allDefault)
+ {
+ _ = this.Boolean(true, ref allDefault);
+ return false;
+ }
+
+ public override bool VisitNested(IJxlFields fields) => true;
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs
new file mode 100644
index 000000000..b0396579c
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs
@@ -0,0 +1,132 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.IO;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+internal sealed class JxlReadVisitor(JxlBitReader reader) : JxlVisitorBase
+{
+ private ulong totalExtensionBits;
+ private bool notEnoughBytes;
+ private long posAfterExtSize;
+ private readonly ulong[] extensionBits = new ulong[JxlBundle.MaxExtensions];
+
+ public bool OK { get; private set; }
+
+ public override bool IsReading => true;
+
+ public override bool Bits(int bits, uint defaultValue, ref uint value)
+ {
+ value = JxlBitsCoder.Read(bits, reader);
+ return this.ThrowIfEndOfStreamOrReturnTrue();
+ }
+
+ public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value)
+ {
+ value = JxlU32Coder.Read(enc, reader);
+ return this.ThrowIfEndOfStreamOrReturnTrue();
+ }
+
+ public override bool U64(ulong defaultValue, ref ulong value)
+ {
+ value = JxlU64Coder.Read(reader);
+ return this.ThrowIfEndOfStreamOrReturnTrue();
+ }
+
+ public override bool F16(float defaultValue, ref float value)
+ {
+ this.OK &= JxlF16Coder.Read(reader, ref value);
+ return this.ThrowIfEndOfStreamOrReturnTrue();
+ }
+
+ public override void SetDefault(IJxlFields fields) => JxlBundle.SetDefault(fields);
+
+ public override bool BeginExtensions(ref ulong extensions)
+ {
+ if (!base.BeginExtensions(ref extensions))
+ {
+ return false;
+ }
+
+ if (extensions == 0)
+ {
+ return true;
+ }
+
+ for (ulong remainingExtensions = extensions; remainingExtensions != 0; remainingExtensions &= remainingExtensions - 1)
+ {
+ int idxExtension = Num0BitsBelowLS1BitNonzero(remainingExtensions);
+ if (!this.U64(0, ref this.extensionBits[idxExtension]))
+ {
+ return false;
+ }
+
+ if (!SafeAdd(this.totalExtensionBits, this.extensionBits[idxExtension], ref this.totalExtensionBits))
+ {
+ DebugGuard.IsTrue(false, "Extension bits overflow; the codestream is not valid");
+
+ return false;
+ }
+ }
+
+ this.posAfterExtSize = reader.TotalBitsConsumed;
+ return this.posAfterExtSize != 0;
+ }
+
+ public override bool EndExtensions()
+ {
+ if (!base.EndExtensions())
+ {
+ return false;
+ }
+
+ if (this.posAfterExtSize == 0)
+ {
+ return true;
+ }
+
+ if (this.notEnoughBytes)
+ {
+ return true;
+ }
+
+ long bitsRead = reader.TotalBitsConsumed;
+
+ long end = 0;
+ if (!SafeAdd(this.posAfterExtSize, this.totalExtensionBits, ref end))
+ {
+ DebugGuard.IsTrue(false, "Invalid extension size.");
+
+ return false;
+ }
+
+ if (bitsRead > end)
+ {
+ DebugGuard.IsTrue(false, "Read more extension bits than budgeted");
+
+ return false;
+ }
+
+ long remainingBits = end - bitsRead;
+
+ if (remainingBits != 0)
+ {
+ reader.SkipBits64((uint)remainingBits);
+ }
+
+ return this.ThrowIfEndOfStreamOrReturnTrue();
+ }
+
+ private bool ThrowIfEndOfStreamOrReturnTrue()
+ {
+ if (reader.IsEndOfStream)
+ {
+ DebugGuard.IsTrue(false, "Got an invalid end-of-stream");
+ this.notEnoughBytes = true;
+ return true;
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs
new file mode 100644
index 000000000..aab1bc90b
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+///
+/// This is similar to InitVisitor but also initializes
+/// nested fields.
+///
+internal sealed class JxlSetDefaultVisitor : JxlVisitorBase
+{
+ public override bool Bits(int bits, uint defaultValue, ref uint value)
+ {
+ value = defaultValue;
+ return true;
+ }
+
+ public override bool U32(JxlU32Distribution d0, JxlU32Distribution d1, JxlU32Distribution d2, JxlU32Distribution d3, uint defaultValue, ref uint value)
+ {
+ value = defaultValue;
+ return true;
+ }
+
+ public override bool U64(ulong defaultValue, ref ulong value)
+ {
+ value = defaultValue;
+ return true;
+ }
+
+ public override bool Boolean(bool defaultValue, ref bool value)
+ {
+ value = defaultValue;
+ return true;
+ }
+
+ public override bool F16(float defaultValue, ref float value)
+ {
+ value = defaultValue;
+ return true;
+ }
+
+ public override bool Conditional(bool condition) => true;
+
+ public override bool AllDefault(IJxlFields fields, ref bool allDefault)
+ {
+ _ = this.Boolean(true, ref allDefault);
+ return false;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs
new file mode 100644
index 000000000..e909f7f74
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs
@@ -0,0 +1,127 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Formats.Jxl.IO;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+///
+/// Unsigned 32-bit variable-length integer coder.
+///
+internal static class JxlU32Coder
+{
+ ///
+ /// Maximum number of writeable and/or readable bits in a variable-length integer.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int MaxEncodedBits(in JxlU32Enc enc)
+ {
+ int extraBits = 0;
+
+ for (int selector = 0; selector < 4; selector++)
+ {
+ JxlU32Distribution distr = enc.GetDistribution(selector);
+
+ if (distr.IsDirect)
+ {
+ continue;
+ }
+ else
+ {
+ extraBits = Math.Max(extraBits, (int)distr.ExtraBits);
+ }
+ }
+
+ return 2 + extraBits;
+ }
+
+ ///
+ /// Verifies that the value can be encoded.
+ ///
+ public static bool CanEncode(in JxlU32Enc enc, uint value, ref int encodedBits)
+ {
+ uint selector = 0;
+ int totalBits = 0;
+
+ bool isOk = ChooseSelector(in enc, value, ref selector, ref totalBits);
+
+ encodedBits = isOk ? totalBits : 0;
+
+ return isOk;
+ }
+
+ ///
+ /// Reads the U32 coded value.
+ ///
+ public static uint Read(in JxlU32Enc enc, JxlBitReader reader)
+ {
+ uint selector = reader.ReadBits32(2u);
+ JxlU32Distribution dist = enc.GetDistribution((int)selector);
+
+ if (dist.IsDirect)
+ {
+ return dist.Direct;
+ }
+ else
+ {
+ return reader.ReadBits32(dist.ExtraBits) + dist.Offset;
+ }
+ }
+
+ ///
+ /// Tries to find the best one of the four selectors based on the value.
+ ///
+ public static bool ChooseSelector(in JxlU32Enc enc, uint value, ref uint selector, ref int totalBits)
+ {
+ int bitsRequired = 32 - Num0BitsAboveMS1Bit(value);
+
+ if (bitsRequired > 32)
+ {
+ return false;
+ }
+
+ selector = 0;
+ totalBits = 64;
+
+ for (int s = 0; s < 4; s++)
+ {
+ JxlU32Distribution dist = enc.GetDistribution(s);
+
+ if (dist.IsDirect)
+ {
+ if (dist.Direct == value)
+ {
+ selector = (uint)s;
+ totalBits = 2;
+ return true;
+ }
+
+ continue;
+ }
+
+ uint extraBits = dist.ExtraBits;
+ uint offset = dist.Offset;
+
+ if (value < offset || value >= offset + (1u << (int)extraBits))
+ {
+ continue;
+ }
+
+ if (2 + extraBits < totalBits)
+ {
+ selector = (uint)s;
+ totalBits = 2 + (int)extraBits;
+ }
+ }
+
+ if (totalBits == 64)
+ {
+ DebugGuard.IsTrue(false, "No matching selector");
+
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs
new file mode 100644
index 000000000..6f5fb8b28
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs
@@ -0,0 +1,105 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Jxl.IO;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+///
+/// Unsigned 64-bit variable-length coding.
+///
+internal static class JxlU64Coder
+{
+ ///
+ /// Reads the variable-length, unsigned 64-bit integer.
+ ///
+ public static ulong Read(JxlBitReader reader)
+ {
+ uint selector = reader.ReadBits32(2u);
+
+ if (selector == 0u)
+ {
+ return 0u;
+ }
+ else if (selector == 1u)
+ {
+ return 1u + reader.ReadBits32(4u);
+ }
+ else if (selector == 2u)
+ {
+ return 17u + reader.ReadBits32(8u);
+ }
+
+ // Selector 3...
+ ulong result = reader.ReadBits32(12u);
+ int shift = 12;
+
+ while (reader.ReadBoolean())
+ {
+ if (shift == 60)
+ {
+ result |= (ulong)reader.ReadBits32(4u) << shift;
+ break;
+ }
+
+ result |= (ulong)reader.ReadBits32(8u) << shift;
+ shift += 8;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Returns a value indicating whether can the value be encoded,
+ /// as well as the number of encoded bits.
+ ///
+ public static bool CanEncode(ulong value, ref int encodedBits)
+ {
+ if (value == 0)
+ {
+ // 2 selector bits
+ encodedBits = 2;
+ }
+ else if (value <= 16)
+ {
+ // 2 selector bits + 4 payload bits
+ encodedBits = 2 + 4;
+ }
+ else if (value <= 272)
+ {
+ // 2 selector bits + 8 payload bits
+ encodedBits = 2 + 8;
+ }
+ else
+ {
+ // 2 selector bits + 12 payload bits
+ encodedBits = 2 + 12;
+ value >>= 12;
+ int shift = 12;
+ while (value > 0 && shift < 60)
+ {
+ // 1 continuation bit + 8 payload bits
+ encodedBits += 1 + 8;
+ value >>= 8;
+ shift += 8;
+ }
+ if (value > 0)
+ {
+ // 1 continuation bit + 4 payload bits
+ encodedBits += 1 + 4;
+ }
+ else
+ {
+ // 1 stop bit
+ encodedBits += 1;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Always returns 73.
+ ///
+ public static int MaxEncodedBits() => 73;
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs
new file mode 100644
index 000000000..7fa79f75d
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs
@@ -0,0 +1,92 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+///
+/// Base JPEG XL visitor that can visit all fields of a class.
+/// This is highly similar to the following Reflection code, but with
+/// lower overhead:
+///
+/// // Pseudocode
+/// void Visit(Type type)
+/// {
+/// foreach (PropertyInfo property in type.GetProperties())
+/// {
+/// /* visitor implementation */(property);
+/// }
+/// }
+///
+///
+internal class JxlVisitor
+{
+ public virtual bool IsReading => false;
+
+ public virtual bool Visit(IJxlFields fields) => false;
+
+ public virtual bool Boolean(bool defaultValue, ref bool value) => false;
+
+ public virtual bool U32(JxlU32Enc enc, uint defaultValue, ref uint value) => false;
+
+ public virtual bool U32(
+ JxlU32Distribution d0,
+ JxlU32Distribution d1,
+ JxlU32Distribution d2,
+ JxlU32Distribution d3,
+ uint defaultValue,
+ ref uint value)
+ => this.U32(new JxlU32Enc(d0, d1, d2, d3), value, ref defaultValue);
+
+ public virtual unsafe bool Enum(T defaultValue, ref T value)
+ where T : unmanaged
+ {
+ DebugGuard.IsTrue(sizeof(T) == 4, "We use unsafe bit casting so anything beside 4 bytes will break memory layout");
+
+ ref uint u32 = ref Unsafe.As(ref value);
+ if (!this.U32(
+ JxlFieldExpressions.Value(0),
+ JxlFieldExpressions.Value(1),
+ JxlFieldExpressions.BitsOffset(4, 2),
+ JxlFieldExpressions.BitsOffset(6, 18),
+ Unsafe.BitCast(defaultValue),
+ ref u32))
+ {
+ return false;
+ }
+
+ return System.Enum.IsDefined(typeof(T), value);
+ }
+
+ public virtual bool Bits(int bits, uint defaultValue, ref uint value) => false;
+
+ public virtual bool U64(ulong defaultValue, ref ulong value) => false;
+
+ public virtual bool F16(float defaultValue, ref float value) => false;
+
+ public virtual bool Conditional(bool condition) => condition;
+
+ public virtual bool AllDefault(IJxlFields fields, ref bool allDefault)
+ {
+ // Do not remove the fields parameter, derived classes
+ // use it.
+ if (!this.Boolean(true, ref allDefault))
+ {
+ return false;
+ }
+
+ return allDefault;
+ }
+
+ public virtual void SetDefault(IJxlFields fields)
+ {
+ // Used by derived methods.
+ }
+
+ public virtual bool VisitNested(IJxlFields fields) => this.Visit(fields);
+
+ public virtual bool BeginExtensions(ref ulong extensions) => false;
+
+ public virtual bool EndExtensions() => false;
+}
diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs
new file mode 100644
index 000000000..7d2dffffb
--- /dev/null
+++ b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs
@@ -0,0 +1,74 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics;
+
+#pragma warning disable SA1405 // Debug.Assert should provide message text
+
+namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
+
+internal class JxlVisitorBase : JxlVisitor
+{
+ private readonly JxlExtensionStates extensionStates = new();
+ private int depth;
+
+ public override bool Visit(IJxlFields fields)
+ {
+ if (this.depth >= JxlBundle.MaxExtensions)
+ {
+ return false;
+ }
+
+ this.depth++;
+ this.extensionStates.Push();
+
+ bool visited = fields.Visit(this);
+
+ if (visited)
+ {
+ // TODO: use DebugGuard
+ Debug.Assert(!this.extensionStates.IsBegun || this.extensionStates.IsEnded);
+ }
+
+ this.extensionStates.Pop();
+
+ // TODO: use DebugGuard
+ Debug.Assert(this.depth != 0);
+ this.depth--;
+
+ return visited;
+ }
+
+ public override bool Boolean(bool defaultValue, ref bool value)
+ {
+ uint bits = value ? 1u : 0u;
+ if (!this.Bits(1, defaultValue ? 1u : 0u, ref bits))
+ {
+ return false;
+ }
+
+ // TODO: use DebugGuard
+ Debug.Assert(bits <= 1u);
+
+ value = bits == 1u;
+
+ return true;
+ }
+
+ public override bool BeginExtensions(ref ulong extensions)
+ {
+ if (!this.U64(0uL, ref extensions))
+ {
+ return false;
+ }
+
+ this.extensionStates.Begin();
+ return true;
+ }
+
+ public override bool EndExtensions()
+ {
+ this.extensionStates.End();
+ return true;
+ }
+}
diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs
index 99b159991..50753321e 100644
--- a/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs
+++ b/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs
@@ -14,7 +14,16 @@ internal sealed class JxlBitReader(ReadOnlyMemory bytes)
private ulong buffer;
private uint bufferRemainingBits;
private int pointer;
- private bool endOfStream;
+
+ ///
+ /// Gets a value indicating whether this marks an end of stream.
+ ///
+ public bool IsEndOfStream { get; private set; }
+
+ ///
+ /// Gets the total number of bits consumed.
+ ///
+ public long TotalBitsConsumed => ((long)this.pointer * 8) + (64 - this.bufferRemainingBits);
///
/// Fetches a new buffer.
@@ -29,7 +38,7 @@ internal sealed class JxlBitReader(ReadOnlyMemory bytes)
// we don't have any more data... mark an end of stream
this.buffer = 0;
this.bufferRemainingBits = 0;
- this.endOfStream = true;
+ this.IsEndOfStream = true;
return;
}
@@ -66,7 +75,7 @@ internal sealed class JxlBitReader(ReadOnlyMemory bytes)
Debug.Assert(n <= 64, "Too many bits to pack into ulong");
this.MaybeRefill();
- if (this.endOfStream)
+ if (this.IsEndOfStream)
{
JxlThrowHelper.ThrowEndOfStream();
}
@@ -111,7 +120,7 @@ internal sealed class JxlBitReader(ReadOnlyMemory bytes)
Debug.Assert(n <= 32, "Too many bits to pack into uint");
this.MaybeRefill();
- if (this.endOfStream)
+ if (this.IsEndOfStream)
{
JxlThrowHelper.ThrowEndOfStream();
}
@@ -157,11 +166,11 @@ internal sealed class JxlBitReader(ReadOnlyMemory bytes)
public void SkipBits32(uint bits) => _ = this.ReadBits32(bits);
- public ulong ReadBits64(uint bits) => this.ReadBits64Core(bits, peek: false);
+ public ulong ReadBits64(ulong bits) => this.ReadBits64Core((uint)bits, peek: false);
- public ulong PeekBits64(uint bits) => this.ReadBits64Core(bits, peek: true);
+ public ulong PeekBits64(ulong bits) => this.ReadBits64Core((uint)bits, peek: true);
- public void SkipBits64(uint bits) => _ = this.ReadBits64(bits);
+ public void SkipBits64(ulong bits) => _ = this.ReadBits64(bits);
public bool ReadBoolean() => this.ReadBits32Core(1, peek: false) == 1;
}
diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs
index 0df657f44..a60ac0f13 100644
--- a/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs
+++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs
@@ -1,7 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using SixLabors.ImageSharp.Formats.Jxl.IO;
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
namespace SixLabors.ImageSharp.Formats.Jxl.Metadata;
diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs
index 78c0e9ba8..8b938b736 100644
--- a/src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs
+++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs
@@ -1,7 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using SixLabors.ImageSharp.Formats.Jxl.IO;
+using SixLabors.ImageSharp.Formats.Jxl.Fields;
namespace SixLabors.ImageSharp.Formats.Jxl.Metadata;