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;