diff --git a/src/ImageSharp/Formats/Jxl/Fields/IJxlFields.cs b/src/ImageSharp/Formats/Jxl/Fields/IJxlFields.cs new file mode 100644 index 0000000000..bdc09f5f41 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/IJxlFields.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +/// +/// Abstracts enumeration of all fields into a visitor. +/// +internal interface IJxlFields +{ + /// + /// Visits all fields into the specified JXL visitor. + /// + /// The visitor to use to visit all fields. + /// Status of the visit operation. + public bool Visit(JxlVisitor visitor); +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs new file mode 100644 index 0000000000..cfe1430193 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +internal sealed class JxlAllDefaultVisitor : JxlVisitorBase +{ + public bool IsAllDefault { get; private set; } = true; + + public override bool Bits(int bits, uint defaultValue, ref uint value) + { + this.IsAllDefault = value == defaultValue; + return true; + } + + public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value) + { + this.IsAllDefault = value == defaultValue; + return true; + } + + public override bool U64(ulong defaultValue, ref ulong value) + { + this.IsAllDefault = value == defaultValue; + return true; + } + + public override bool F16(float defaultValue, ref float value) + { + this.IsAllDefault = MathF.Abs(value - defaultValue) < 1E-6f; + return true; + } + + public override bool AllDefault(IJxlFields fields, ref bool allDefault) => false; +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs new file mode 100644 index 0000000000..398587baac --- /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 0000000000..e6e673f9c8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs @@ -0,0 +1,89 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +/// +/// An helper. +/// +internal static class JxlBundle +{ + /// + /// Initializes the specified JXL fields. + /// + /// The JXL fields. + public static void Init(IJxlFields fields) + { + JxlInitVisitor initVisitor = new(); + + if (!initVisitor.Visit(fields)) + { + DebugGuard.IsTrue(false, "Init should never fail"); + } + } + + /// + /// Sets all JXL fields provided by the input value to their defaults. + /// + /// The JXL fields. + public static void SetDefault(IJxlFields fields) + { + JxlSetDefaultVisitor visitor = new(); + + if (!visitor.Visit(fields)) + { + DebugGuard.IsTrue(false, "SetDefault should never fail"); + } + } + + /// + /// Returns a value indicating whether every value provided by this + /// field is a default value. If at least one field isn't a default + /// value, the method returns false. + /// + /// The JXL fields. + /// A boolean indicating whether or not are all values initialized to their default values. + public static bool AllDefault(IJxlFields fields) + { + JxlAllDefaultVisitor allDefaultVisitor = new(); + + if (!allDefaultVisitor.Visit(fields)) + { + DebugGuard.IsTrue(false, "AllDefault should never fail"); + } + + return allDefaultVisitor.IsAllDefault; + } + + /// + /// Reads the fields from a bit-reader. + /// + /// The bit-reader. + /// The fields. + /// Status of the read operation. + public static bool Read(JxlBitReader reader, IJxlFields fields) + { + JxlReadVisitor visitor = new(reader); + if (!visitor.Visit(fields)) + { + return false; + } + + return visitor.OK; + } + + /// + /// Tries to read the fields from a bit-reader. + /// + /// The bit-reader. + /// The fields. + /// Status of the read operation. + public static bool CanRead(JxlBitReader reader, IJxlFields fields) + { + JxlReadVisitor visitor = new(reader); + _ = visitor.Visit(fields); + return visitor.OK; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs new file mode 100644 index 0000000000..aae11aa120 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs @@ -0,0 +1,118 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +internal sealed class JxlCanEncodeVisitor : JxlVisitorBase +{ + private long encodedBits; + private ulong extensions; + private long posAfterExt; + + public bool OK { get; set; } = true; + + public override bool Bits(int bits, uint defaultValue, ref uint value) + { + int enc = 0; + this.OK &= JxlBitsCoder.CanEncode(bits, value, ref enc); + this.encodedBits += enc; + return true; + } + + public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value) + { + int encBits = 0; + this.OK &= JxlU32Coder.CanEncode(enc, value, ref encBits); + this.encodedBits += encBits; + return true; + } + + public override bool U64(ulong defaultValue, ref ulong value) + { + int encBits = 0; + this.OK &= JxlU64Coder.CanEncode(value, ref encBits); + this.encodedBits += encBits; + return true; + } + + public override bool F16(float defaultValue, ref float value) + { + int encBits = 0; + this.OK &= JxlF16Coder.CanEncode(value, ref encBits); + this.encodedBits += encBits; + return true; + } + + public override bool AllDefault(IJxlFields fields, ref bool allDefault) + { + allDefault = JxlBundle.AllDefault(fields); + if (!this.Boolean(true, ref allDefault)) + { + return false; + } + + return allDefault; + } + + public override bool BeginExtensions(ref ulong extensions) + { + if (!base.BeginExtensions(ref extensions)) + { + return false; + } + + this.extensions = extensions; + + if (extensions != 0uL) + { + if (this.posAfterExt != 0) + { + return false; + } + + this.posAfterExt = this.encodedBits; + + if (this.posAfterExt == 0) + { + return false; + } + } + + return true; + } + + public bool GetSizes(ref int extensionBits, ref long totalBits) + { + if (!this.OK) + { + return false; + } + + extensionBits = 0; + totalBits = this.encodedBits; + + if (this.posAfterExt != 0) + { + if (this.encodedBits < this.posAfterExt) + { + return false; + } + + extensionBits = (int)this.encodedBits - (int)this.posAfterExt; + int encodedBits = 0; + this.OK &= JxlU64Coder.CanEncode(extensionBits, ref encodedBits); + totalBits += encodedBits; + + for (int i = 1; i < BitOperations.PopCount(this.extensions); i++) + { + encodedBits = 0; + this.OK &= JxlU64Coder.CanEncode(0, ref encodedBits); + totalBits += encodedBits; + } + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlExtensionStates.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlExtensionStates.cs new file mode 100644 index 0000000000..2cc3c82581 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlExtensionStates.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +internal sealed class JxlExtensionStates +{ + private ulong begun; + private ulong ended; + + public bool IsBegun => (this.begun & 1) != 0; + + public bool IsEnded => (this.ended & 1) != 0; + + public void Push() + { + this.begun <<= 1; + this.ended <<= 1; + } + + public void Pop() + { + this.begun >>= 1; + this.ended >>= 1; + } + + public void Begin() + { + DebugGuard.IsFalse(this.IsBegun, nameof(this.IsBegun), "This must be false."); + DebugGuard.IsFalse(this.IsEnded, nameof(this.IsEnded), "This must be false."); + + this.begun++; + } + + public void End() + { + DebugGuard.IsTrue(this.IsBegun, nameof(this.IsBegun), "This must be true."); + DebugGuard.IsFalse(this.IsEnded, nameof(this.IsEnded), "This must be false."); + + this.ended++; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs new file mode 100644 index 0000000000..3c4126c4b8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.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/JxlFieldExpressions.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlFieldExpressions.cs new file mode 100644 index 0000000000..8d74c81bb8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlFieldExpressions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +internal static class JxlFieldExpressions +{ + public static JxlU32Distribution Value(uint value) + { + const uint directConstant = JxlU32Distribution.DirectConstant; + + return new(value | directConstant); + } + + public static JxlU32Distribution BitsOffset(uint bits, uint offset) + => new(((bits - 1u) & 0x1Fu) + ((offset & 0x3FFFFFFu) << 5)); + + public static JxlU32Distribution Bits(uint value) => BitsOffset(value, 0u); + + public static int MakeBit(int index) => 1 << index; +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs new file mode 100644 index 0000000000..28c16c4e46 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +/// +/// This variant of sets values +/// to be the default value. +/// +internal sealed class JxlInitVisitor : JxlVisitorBase +{ + public override bool Bits(int bits, uint defaultValue, ref uint value) + { + value = defaultValue; + return true; + } + + public override bool U32(JxlU32Distribution d0, JxlU32Distribution d1, JxlU32Distribution d2, JxlU32Distribution d3, uint defaultValue, ref uint value) + { + value = defaultValue; + return true; + } + + public override bool U64(ulong defaultValue, ref ulong value) + { + value = defaultValue; + return true; + } + + public override bool Boolean(bool defaultValue, ref bool value) + { + value = defaultValue; + return true; + } + + public override bool F16(float defaultValue, ref float value) + { + value = defaultValue; + return true; + } + + public override bool Conditional(bool condition) => true; + + public override bool AllDefault(IJxlFields fields, ref bool allDefault) + { + _ = this.Boolean(true, ref allDefault); + return false; + } + + public override bool VisitNested(IJxlFields fields) => true; +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs new file mode 100644 index 0000000000..b0396579cc --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs @@ -0,0 +1,132 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +internal sealed class JxlReadVisitor(JxlBitReader reader) : JxlVisitorBase +{ + private ulong totalExtensionBits; + private bool notEnoughBytes; + private long posAfterExtSize; + private readonly ulong[] extensionBits = new ulong[JxlBundle.MaxExtensions]; + + public bool OK { get; private set; } + + public override bool IsReading => true; + + public override bool Bits(int bits, uint defaultValue, ref uint value) + { + value = JxlBitsCoder.Read(bits, reader); + return this.ThrowIfEndOfStreamOrReturnTrue(); + } + + public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value) + { + value = JxlU32Coder.Read(enc, reader); + return this.ThrowIfEndOfStreamOrReturnTrue(); + } + + public override bool U64(ulong defaultValue, ref ulong value) + { + value = JxlU64Coder.Read(reader); + return this.ThrowIfEndOfStreamOrReturnTrue(); + } + + public override bool F16(float defaultValue, ref float value) + { + this.OK &= JxlF16Coder.Read(reader, ref value); + return this.ThrowIfEndOfStreamOrReturnTrue(); + } + + public override void SetDefault(IJxlFields fields) => JxlBundle.SetDefault(fields); + + public override bool BeginExtensions(ref ulong extensions) + { + if (!base.BeginExtensions(ref extensions)) + { + return false; + } + + if (extensions == 0) + { + return true; + } + + for (ulong remainingExtensions = extensions; remainingExtensions != 0; remainingExtensions &= remainingExtensions - 1) + { + int idxExtension = Num0BitsBelowLS1BitNonzero(remainingExtensions); + if (!this.U64(0, ref this.extensionBits[idxExtension])) + { + return false; + } + + if (!SafeAdd(this.totalExtensionBits, this.extensionBits[idxExtension], ref this.totalExtensionBits)) + { + DebugGuard.IsTrue(false, "Extension bits overflow; the codestream is not valid"); + + return false; + } + } + + this.posAfterExtSize = reader.TotalBitsConsumed; + return this.posAfterExtSize != 0; + } + + public override bool EndExtensions() + { + if (!base.EndExtensions()) + { + return false; + } + + if (this.posAfterExtSize == 0) + { + return true; + } + + if (this.notEnoughBytes) + { + return true; + } + + long bitsRead = reader.TotalBitsConsumed; + + long end = 0; + if (!SafeAdd(this.posAfterExtSize, this.totalExtensionBits, ref end)) + { + DebugGuard.IsTrue(false, "Invalid extension size."); + + return false; + } + + if (bitsRead > end) + { + DebugGuard.IsTrue(false, "Read more extension bits than budgeted"); + + return false; + } + + long remainingBits = end - bitsRead; + + if (remainingBits != 0) + { + reader.SkipBits64((uint)remainingBits); + } + + return this.ThrowIfEndOfStreamOrReturnTrue(); + } + + private bool ThrowIfEndOfStreamOrReturnTrue() + { + if (reader.IsEndOfStream) + { + DebugGuard.IsTrue(false, "Got an invalid end-of-stream"); + this.notEnoughBytes = true; + return true; + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs new file mode 100644 index 0000000000..aab1bc90ba --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +/// +/// This is similar to InitVisitor but also initializes +/// nested fields. +/// +internal sealed class JxlSetDefaultVisitor : JxlVisitorBase +{ + public override bool Bits(int bits, uint defaultValue, ref uint value) + { + value = defaultValue; + return true; + } + + public override bool U32(JxlU32Distribution d0, JxlU32Distribution d1, JxlU32Distribution d2, JxlU32Distribution d3, uint defaultValue, ref uint value) + { + value = defaultValue; + return true; + } + + public override bool U64(ulong defaultValue, ref ulong value) + { + value = defaultValue; + return true; + } + + public override bool Boolean(bool defaultValue, ref bool value) + { + value = defaultValue; + return true; + } + + public override bool F16(float defaultValue, ref float value) + { + value = defaultValue; + return true; + } + + public override bool Conditional(bool condition) => true; + + public override bool AllDefault(IJxlFields fields, ref bool allDefault) + { + _ = this.Boolean(true, ref allDefault); + return false; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs new file mode 100644 index 0000000000..e909f7f74f --- /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/JxlU32Distribution.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Distribution.cs new file mode 100644 index 0000000000..355b6f533f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Distribution.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +internal struct JxlU32Distribution(uint d) +{ + public const uint DirectConstant = 0x80000000u; + + public readonly bool IsDirect => (d & DirectConstant) != 0; + + public readonly uint Direct => d & (DirectConstant - 1u); + + public readonly uint ExtraBits => (d & 0x1Fu) + 1u; + + public readonly uint Offset => (d >> 5) & 0x3FFFFFF; +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs new file mode 100644 index 0000000000..5b6e10b847 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +internal readonly struct JxlU32Enc +{ + private readonly InlineArray4 d = default; + + public JxlU32Enc(JxlU32Distribution d0, JxlU32Distribution d1, JxlU32Distribution d2, JxlU32Distribution d3) + { + this.d[0] = d0; + this.d[1] = d1; + this.d[2] = d2; + this.d[3] = d3; + } + + public JxlU32Distribution GetDistribution(int selector) + { + // This stuff is internal, so if argument check + // fails it's not a user error. + DebugGuard.MustBeLessThan(selector, 4, nameof(selector)); + + return this.d[selector]; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs new file mode 100644 index 0000000000..ba121cafe2 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs @@ -0,0 +1,106 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.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 0000000000..7fa79f75d2 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs @@ -0,0 +1,92 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +/// +/// Base JPEG XL visitor that can visit all fields of a class. +/// This is highly similar to the following Reflection code, but with +/// lower overhead: +/// +/// // Pseudocode +/// void Visit(Type type) +/// { +/// foreach (PropertyInfo property in type.GetProperties()) +/// { +/// /* visitor implementation */(property); +/// } +/// } +/// +/// +internal class JxlVisitor +{ + public virtual bool IsReading => false; + + public virtual bool Visit(IJxlFields fields) => false; + + public virtual bool Boolean(bool defaultValue, ref bool value) => false; + + public virtual bool U32(JxlU32Enc enc, uint defaultValue, ref uint value) => false; + + public virtual bool U32( + JxlU32Distribution d0, + JxlU32Distribution d1, + JxlU32Distribution d2, + JxlU32Distribution d3, + uint defaultValue, + ref uint value) + => this.U32(new JxlU32Enc(d0, d1, d2, d3), value, ref defaultValue); + + public virtual unsafe bool Enum(T defaultValue, ref T value) + where T : unmanaged + { + DebugGuard.IsTrue(sizeof(T) == 4, "We use unsafe bit casting so anything beside 4 bytes will break memory layout"); + + ref uint u32 = ref Unsafe.As(ref value); + if (!this.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.Value(1), + JxlFieldExpressions.BitsOffset(4, 2), + JxlFieldExpressions.BitsOffset(6, 18), + Unsafe.BitCast(defaultValue), + ref u32)) + { + return false; + } + + return System.Enum.IsDefined(typeof(T), value); + } + + public virtual bool Bits(int bits, uint defaultValue, ref uint value) => false; + + public virtual bool U64(ulong defaultValue, ref ulong value) => false; + + public virtual bool F16(float defaultValue, ref float value) => false; + + public virtual bool Conditional(bool condition) => condition; + + public virtual bool AllDefault(IJxlFields fields, ref bool allDefault) + { + // Do not remove the fields parameter, derived classes + // use it. + if (!this.Boolean(true, ref allDefault)) + { + return false; + } + + return allDefault; + } + + public virtual void SetDefault(IJxlFields fields) + { + // Used by derived methods. + } + + public virtual bool VisitNested(IJxlFields fields) => this.Visit(fields); + + public virtual bool BeginExtensions(ref ulong extensions) => false; + + public virtual bool EndExtensions() => false; +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs new file mode 100644 index 0000000000..7d2dffffbf --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs @@ -0,0 +1,74 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +#pragma warning disable SA1405 // Debug.Assert should provide message text + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +internal class JxlVisitorBase : JxlVisitor +{ + private readonly JxlExtensionStates extensionStates = new(); + private int depth; + + public override bool Visit(IJxlFields fields) + { + if (this.depth >= JxlBundle.MaxExtensions) + { + return false; + } + + this.depth++; + this.extensionStates.Push(); + + bool visited = fields.Visit(this); + + if (visited) + { + // TODO: use DebugGuard + Debug.Assert(!this.extensionStates.IsBegun || this.extensionStates.IsEnded); + } + + this.extensionStates.Pop(); + + // TODO: use DebugGuard + Debug.Assert(this.depth != 0); + this.depth--; + + return visited; + } + + public override bool Boolean(bool defaultValue, ref bool value) + { + uint bits = value ? 1u : 0u; + if (!this.Bits(1, defaultValue ? 1u : 0u, ref bits)) + { + return false; + } + + // TODO: use DebugGuard + Debug.Assert(bits <= 1u); + + value = bits == 1u; + + return true; + } + + public override bool BeginExtensions(ref ulong extensions) + { + if (!this.U64(0uL, ref extensions)) + { + return false; + } + + this.extensionStates.Begin(); + return true; + } + + public override bool EndExtensions() + { + this.extensionStates.End(); + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs new file mode 100644 index 0000000000..1854b4ab67 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs @@ -0,0 +1,74 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Metadata; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; + +/// +/// Describes duration of frames that make up an animation. +/// +internal sealed class JxlAnimationFrame : IJxlFields +{ + /// + /// See . + /// + private uint duration; + + /// + /// See . + /// + private uint timecode; + + /// + /// Gets or sets the duration of the animation. + /// + public uint Duration + { + get => this.duration; + set => this.duration = value; + } + + /// + /// Gets or sets the timecode of the animation. The + /// format is 0xHHMMSSFF. + /// + public uint Timecode + { + get => this.timecode; + set => this.timecode = value; + } + + /// + /// Gets or sets the optional codec metadata. + /// + public JxlCodecMetadata? CodecMetadata { get; set; } + + public bool Visit(JxlVisitor visitor) + { + if (visitor.Conditional(this.CodecMetadata?.ImageMetadata?.HaveAnimation == true)) + { + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Bits(8), + JxlFieldExpressions.Bits(32), + 0, + ref this.duration)) + { + return false; + } + } + + if (visitor.Conditional(this.CodecMetadata?.ImageMetadata?.Animation?.ContainsTimecodes == true)) + { + if (!visitor.Bits(32, 0u, ref this.timecode)) + { + return false; + } + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendMode.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendMode.cs new file mode 100644 index 0000000000..035a48edd5 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendMode.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; + +/// +/// Represents the blending mode describing how to combine +/// current frame with previously saved frame. +/// +internal enum JxlBlendMode : byte +{ + /// + /// New values replace old ones. + /// + /// sample = new + /// + /// + Replace, + + /// + /// New values add to the old ones. + /// + /// sample = old + new + /// + /// + Add, + + /// + /// New values replace old ones if alpha>0: + /// + /// alpha = old + new * (1 - old) + /// + /// For other channels if !alpha_associated: + /// + /// sample = ((1 - newAlpha) * old * oldAlpha + newAlpha * new) / alpha + /// + /// For other channels if alpha_associated: + /// + /// sample = (1 - newAlpha) * old + new + /// + /// + Blend, + + /// + /// New values are added to the old ones if alpha>0: + /// For the alpha channel that is used as source: + /// + /// sample = old + new * (1 - old) + /// + /// Otherwise: + /// + /// sample = old + alpha * new + /// + /// + AlphaWeightedBlend, + + /// + /// New values are multiplied by old ones: + /// + /// sample = old * new + /// + /// + Multiply +} diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendingInfo.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendingInfo.cs new file mode 100644 index 0000000000..58c79d22db --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendingInfo.cs @@ -0,0 +1,146 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; + +/// +/// Provides options and instructions that tell the decoder the proper +/// way to blend the current and previous frame together. +/// +internal sealed class JxlBlendingInfo : IJxlFields +{ + /// + /// Initializes a new instance of the class. + /// + public JxlBlendingInfo() => JxlBundle.Init(this); + + /// + /// Gets or sets the blending mode. See . + /// + public JxlBlendMode BlendMode { get; set; } + + /// + /// Gets or sets the value that indicates which extra channel + /// to use as alpha channel for blending. + /// + public uint AlphaChannel { get; set; } + + /// + /// Gets or sets a value indicating whether the alpha or channel values + /// must be clamped* to the 0 through 1 range. + /// + /// + /// Clamped - must be limited to the specified range. + /// + public bool Clamp { get; set; } + + /// + /// Gets or sets the frame ID to copy from (0 through 3). + /// + /// + /// If is equal to , + /// the value of this property is ignored. + /// + public uint Source { get; set; } + + /// + /// Gets or sets the total number of extra channels. + /// + public int ExtraChannelCount { get; set; } + + /// + /// Gets or sets a value indicating whether the frame is partial. + /// + public bool IsPartialFrame { get; set; } + + public bool Visit(JxlVisitor visitor) + { + JxlBlendMode mode = this.BlendMode; + if (!VisitBlendMode(visitor, JxlBlendMode.Replace, ref mode)) + { + return false; + } + + this.BlendMode = mode; + + if (visitor.Conditional(this.ExtraChannelCount > 0 && mode is JxlBlendMode.Blend or JxlBlendMode.AlphaWeightedBlend)) + { + uint alphaChannel = this.AlphaChannel; + if (!visitor.U32( + JxlFieldExpressions.Value(0u), + JxlFieldExpressions.Value(1u), + JxlFieldExpressions.Value(2u), + JxlFieldExpressions.BitsOffset(3u, 3u), + 0, + ref alphaChannel)) + { + return false; + } + + this.AlphaChannel = alphaChannel; + + if (visitor.IsReading && alphaChannel >= this.ExtraChannelCount) + { + throw new InvalidOperationException("Invalid alpha channel for blending"); + } + } + + if (visitor.Conditional((this.ExtraChannelCount > 0 && mode is JxlBlendMode.Blend or JxlBlendMode.AlphaWeightedBlend) || mode == JxlBlendMode.Multiply)) + { + bool clamp = this.Clamp; + + if (!visitor.Boolean(false, ref clamp)) + { + return false; + } + + this.Clamp = clamp; + } + + if (visitor.Conditional(mode != JxlBlendMode.Replace || this.IsPartialFrame)) + { + uint source = this.Source; + + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(3), + 0, + ref source)) + { + return false; + } + + this.Source = source; + } + + return true; + } + + private static bool VisitBlendMode(JxlVisitor visitor, JxlBlendMode defaultValue, ref JxlBlendMode valueToEncode) + { + uint unsignedBackingValue = (uint)valueToEncode; + + if (!visitor.U32( + JxlFieldExpressions.Value((uint)JxlBlendMode.Replace), + JxlFieldExpressions.Value((uint)JxlBlendMode.Add), + JxlFieldExpressions.Value((uint)JxlBlendMode.Blend), + JxlFieldExpressions.BitsOffset(2u, 3u), + (uint)defaultValue, + ref unsignedBackingValue)) + { + return false; + } + + if (unsignedBackingValue > (uint)JxlBlendMode.Multiply) + { + throw new InvalidOperationException("Invalid blend mode"); + } + + valueToEncode = (JxlBlendMode)unsignedBackingValue; + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransform.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransform.cs new file mode 100644 index 0000000000..f874983486 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransform.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; + +/// +/// Represents the type of JPEG XL color transform. +/// +internal enum JxlColorTransform : byte +{ + /// + /// Use XYB encoding + /// + Xyb, + + /// + /// Encode according to the attached color profile. + /// + None, + + /// + /// Encode according to the attached color profile but + /// transformed into Y'Cb'Cr. + /// + YCbCr, +} diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs new file mode 100644 index 0000000000..de61664146 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; + +/// +/// Helper methods associated with JxlColorTransform. +/// +internal static class JxlColorTransformHelpers +{ + private static readonly int[][] JpegOrders = + [ + [0, 0, 0], // Grayscale + [1, 0, 2], // Y'Cb'Cr + [0, 1, 2], // None + [0, 1, 2] // Anything else + ]; + + public static ReadOnlySpan GetJpegOrder(JxlColorTransform transform, bool isGraysacle) + { + if (isGraysacle) + { + return JpegOrders[0]; + } + + if (transform == JxlColorTransform.YCbCr) + { + return JpegOrders[1]; + } + else if (transform == JxlColorTransform.None) + { + return JpegOrders[2]; + } + else + { + return JpegOrders[3]; + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameEncoding.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameEncoding.cs new file mode 100644 index 0000000000..77ae964ac9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameEncoding.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; + +/// +/// Represents the kind of frame encoding. +/// +internal enum JxlFrameEncoding : byte +{ + /// + /// Use VarDCT + /// + VarDct, + + /// + /// Use Modular encoding + /// + Modular +} diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs new file mode 100644 index 0000000000..a45c0e4710 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs @@ -0,0 +1,743 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// Disable IDE0032 for consistency with other fields. +// We have to avoid auto properties for most fields +// so we can use the ref keyword on them directly. +#pragma warning disable IDE0032 // Use auto property + +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; + +/// +/// Control information for a JPEG XL frame. +/// +internal sealed class JxlFrameHeader : IJxlFields +{ + // The following are backing fields for properties. + private JxlFrameEncoding encoding = JxlFrameEncoding.Modular; + private JxlFrameType frameType = JxlFrameType.RegularFrame; + private ulong flags; + private JxlColorTransform colorTransform = JxlColorTransform.Xyb; + private JxlYCbCrChromaSubsampling? chromaSubsampling; + private uint groupSizeShift; + private uint xQmScale; + private uint bQmScale; + private string? name; + private bool customSizeOrOrigin; + private Size frameSize; + private uint upsampling; + private List extraChannelUpsampling = []; + private Point frameOrigin; + private JxlBlendingInfo? blendingInfo; + private List extraChannelBlendingInfo = []; + private readonly JxlAnimationFrame? animationFrame; + private bool isLast; + private uint saveAsReference; + private bool saveBeforeColorTransform; + private uint dcLevel; + private JxlCodecMetadata? metadata; + private JxlLoopFilter? loopFilter; + private ulong extensions; + + private bool isPreviewFrame; // Non-serialized + + /// + /// Gets or sets the frame encoding method (e.g., Modular or VarDCT). + /// + public JxlFrameEncoding Encoding + { + get => this.encoding; + set => this.encoding = value; + } + + /// + /// Gets or sets the type of frame (e.g., RegularFrame). + /// + public JxlFrameType FrameType + { + get => this.frameType; + set => this.frameType = value; + } + + /// + /// Gets or sets the frame flags. + /// + public ulong Flags + { + get => this.flags; + set => this.flags = value; + } + + /// + /// Gets or sets the color transform used (e.g., XYB). + /// + public JxlColorTransform ColorTransform + { + get => this.colorTransform; + set => this.colorTransform = value; + } + + /// + /// Gets or sets the chroma subsampling information. + /// + public JxlYCbCrChromaSubsampling? ChromaSubsampling + { + get => this.chromaSubsampling; + set => this.chromaSubsampling = value; + } + + /// + /// Gets or sets the group size shift value. + /// + public uint GroupSizeShift + { + get => this.groupSizeShift; + set => this.groupSizeShift = value; + } + + /// + /// Gets or sets the X quantization matrix scale. + /// + public uint XQmScale + { + get => this.xQmScale; + set => this.xQmScale = value; + } + + /// + /// Gets or sets the B quantization matrix scale. + /// + public uint BQmScale + { + get => this.bQmScale; + set => this.bQmScale = value; + } + + /// + /// Gets or sets the frame name. + /// + public string? Name + { + get => this.name; + set => this.name = value; + } + + /// + /// Gets or sets a value indicating whether the frame has a custom size or origin. + /// + public bool CustomSizeOrOrigin + { + get => this.customSizeOrOrigin; + set => this.customSizeOrOrigin = value; + } + + /// + /// Gets or sets the frame size. + /// + public Size FrameSize + { + get => this.frameSize; + set => this.frameSize = value; + } + + /// + /// Gets or sets the upsampling factor. + /// + public uint Upsampling + { + get => this.upsampling; + set => this.upsampling = value; + } + + /// + /// Gets or sets the upsampling factors for extra channels. + /// + public List ExtraChannelUpsampling + { + get => this.extraChannelUpsampling; + set => this.extraChannelUpsampling = value; + } + + /// + /// Gets or sets the frame origin point. + /// + public Point FrameOrigin + { + get => this.frameOrigin; + set => this.frameOrigin = value; + } + + /// + /// Gets or sets the blending information for the frame. + /// + public JxlBlendingInfo? BlendingInfo + { + get => this.blendingInfo; + set => this.blendingInfo = value; + } + + /// + /// Gets or sets the blending information for extra channels. + /// + public List ExtraChannelBlendingInfo + { + get => this.extraChannelBlendingInfo; + set => this.extraChannelBlendingInfo = value; + } + + /// + /// Gets the associated animation frame, if any. + /// + public JxlAnimationFrame? AnimationFrame => this.animationFrame; + + /// + /// Gets or sets a value indicating whether this is the last frame. + /// + public bool IsLast + { + get => this.isLast; + set => this.isLast = value; + } + + /// + /// Gets or sets the reference frame index to save. + /// + public uint SaveAsReference + { + get => this.saveAsReference; + set => this.saveAsReference = value; + } + + /// + /// Gets or sets a value indicating whether to save before color transform. + /// + public bool SaveBeforeColorTransform + { + get => this.saveBeforeColorTransform; + set => this.saveBeforeColorTransform = value; + } + + /// + /// Gets or sets the DC level of the frame. + /// + public uint DcLevel + { + get => this.dcLevel; + set => this.dcLevel = value; + } + + /// + /// Gets or sets the codec metadata. + /// + public JxlCodecMetadata? Metadata + { + get => this.metadata; + set => this.metadata = value; + } + + /// + /// Gets or sets the loop filter applied to the frame. + /// + public JxlLoopFilter? LoopFilter + { + get => this.loopFilter; + set => this.loopFilter = value; + } + + /// + /// Gets or sets a value indicating whether this is a preview frame. Non-serialized. + /// + public bool IsPreviewFrame + { + get => this.isPreviewFrame; + set => this.isPreviewFrame = value; + } + + /// + /// Gets or sets the number of extensions. + /// + public ulong Extensions + { + get => this.extensions; + set => this.extensions = value; + } + + public int DefaultXSize + { + get + { + if (this.metadata == null) + { + return 0; + } + + if (this.isPreviewFrame) + { + return this.metadata.ImageMetadata?.PreviewSize?.XSize ?? 0; + } + + return this.metadata.XSize; + } + } + + public int DefaultYSize + { + get + { + if (this.metadata == null) + { + return 0; + } + + if (this.isPreviewFrame) + { + return this.metadata.ImageMetadata?.PreviewSize?.YSize ?? 0; + } + + return this.metadata.YSize; + } + } + + public JxlFrameDimensions FrameDimensions + { + get + { + int xsize = this.DefaultXSize; + int ysize = this.DefaultYSize; + + xsize = this.frameSize.Width != 0 ? this.frameSize.Width : xsize; + ysize = this.frameSize.Height != 0 ? this.frameSize.Height : ysize; + + if (this.dcLevel != 0) + { + xsize = JxlMath.DivCeil(xsize, 1 << (3 * (int)this.dcLevel)); + ysize = JxlMath.DivCeil(ysize, 1 << (3 * (int)this.dcLevel)); + } + + JxlFrameDimensions frameDim = new( + xsize, + ysize, + (int)this.groupSizeShift, + this.chromaSubsampling?.MaxHShift ?? 0, + this.chromaSubsampling?.MaxVShift ?? 0, + this.encoding == JxlFrameEncoding.Modular, + (int)this.upsampling); + + return frameDim; + } + } + + public bool NeedsColorTransform => !this.saveBeforeColorTransform || + this.frameType == JxlFrameType.RegularFrame || + this.frameType == JxlFrameType.SkipProgressive; + + /// + /// Gets a value indicating whether this frame is supposed to be saved for future usage by other frames. + /// + public bool CanBeReferenced => // DC frames cannot be referenced. The last frame cannot be referenced. + // A duration 0 frame makes little sense if it is not referenced. + // A non-duration 0 frame may or may not be referenced. + !this.isLast && + this.frameType != JxlFrameType.DcFrame && + (this.animationFrame?.Duration == 0 || this.saveAsReference != 0); + + private void UpdateFlag(bool condition, ulong flag) + { + if (condition) + { + this.flags |= flag; + } + else + { + this.flags &= ~flag; + } + } + + public bool Visit(JxlVisitor visitor) + { + bool allDefault = false; + if (visitor.AllDefault(this, ref allDefault)) + { + visitor.SetDefault(this); + return true; + } + + if (!VisitFrameType(visitor, JxlFrameType.RegularFrame, ref this.frameType)) + { + return false; + } + + if (visitor.IsReading && this.isPreviewFrame && this.frameType != JxlFrameType.RegularFrame) + { + throw new InvalidOperationException("Only regular frame could be a preview"); + } + + // FrameEncoding + bool isModular = this.encoding == JxlFrameEncoding.Modular; + if (!visitor.Boolean(false, ref isModular)) + { + return false; + } + + this.encoding = isModular + ? JxlFrameEncoding.Modular + : JxlFrameEncoding.VarDct; + + // Flags + if (!visitor.U64(0, ref this.flags)) + { + return false; + } + + // Color transform + bool xybEncoded = this.metadata?.ImageMetadata?.XybEncoded == true; + if (xybEncoded) + { + this.colorTransform = JxlColorTransform.Xyb; + } + else + { + bool alternate = this.colorTransform == JxlColorTransform.YCbCr; + if (!visitor.Boolean(false, ref alternate)) + { + return false; + } + + this.colorTransform = alternate + ? JxlColorTransform.YCbCr + : JxlColorTransform.None; + } + + // Chroma subsampling + if (visitor.Conditional(this.colorTransform == JxlColorTransform.YCbCr && + ((this.flags & (ulong)JxlFrameHeaderFlags.Dc) == 0))) + { + if (!visitor.VisitNested(this.chromaSubsampling!)) + { + return false; + } + } + + int numExtraChannels = this.metadata?.ImageMetadata?.ExtraChannelCount ?? 0; + + // Upsampling + if (visitor.Conditional((this.flags & (ulong)JxlFrameHeaderFlags.Dc) == 0)) + { + if (!visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(4), + JxlFieldExpressions.Value(8), + 1, + ref this.upsampling)) + { + return false; + } + + if (this.metadata != null && visitor.Conditional(numExtraChannels != 0)) + { + List extraChannels = this.metadata!.ImageMetadata?.ExtraChannels ?? []; + this.extraChannelUpsampling = new List(extraChannels.Count); + + for (int i = 0; i < extraChannels.Count; i++) + { + uint dimShift = (uint)extraChannels[i].DimensionShift; + uint ecUpsampling = 1; + ecUpsampling >>= (int)dimShift; + + if (!visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(4), + JxlFieldExpressions.Value(8), + 1, + ref ecUpsampling)) + { + return false; + } + + ecUpsampling <<= (int)dimShift; + + if (ecUpsampling < this.upsampling) + { + throw new InvalidOperationException("EC upsampling < color upsampling, invalid"); + } + + if (ecUpsampling > 8) + { + throw new InvalidOperationException("EC upsampling too large"); + } + + this.extraChannelUpsampling.Add(ecUpsampling); + } + } + else + { + this.extraChannelUpsampling.Clear(); + } + } + + // Modular / VarDCT specifics + if (visitor.Conditional(this.encoding == JxlFrameEncoding.Modular)) + { + if (!visitor.Bits(2, 1, ref this.groupSizeShift)) + { + return false; + } + } + + if (visitor.Conditional(this.encoding == JxlFrameEncoding.VarDct && + this.colorTransform == JxlColorTransform.Xyb)) + { + if (!visitor.Bits(3, 3, ref this.xQmScale)) + { + return false; + } + + if (!visitor.Bits(3, 2, ref this.bQmScale)) + { + return false; + } + } + else + { + this.xQmScale = this.bQmScale = 2; + } + + // Passes + if (visitor.Conditional(this.frameType != JxlFrameType.ReferenceOnly)) + { + if (!visitor.VisitNested(this.passes)) + { + return false; + } + } + + // DC frame + if (visitor.Conditional(this.frameType == JxlFrameType.DcFrame)) + { + if (!visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(3), + JxlFieldExpressions.Value(4), + 1, + ref this.dcLevel)) + { + return false; + } + } + else + { + this.dcLevel = 0; + } + + // Custom size/origin + bool isPartialFrame = false; + + if (visitor.Conditional(this.frameType != JxlFrameType.DcFrame)) + { + if (!visitor.Boolean(false, ref this.customSizeOrOrigin)) + { + return false; + } + + if (visitor.Conditional(this.customSizeOrOrigin)) + { + JxlU32Enc enc = new( + JxlFieldExpressions.Bits(8), + JxlFieldExpressions.BitsOffset(11, 256), + JxlFieldExpressions.BitsOffset(14, 2304), + JxlFieldExpressions.BitsOffset(30, 18688)); + + if (visitor.Conditional(this.frameType is JxlFrameType.RegularFrame or JxlFrameType.SkipProgressive)) + { + uint ux0 = JxlPackSigned.PackUnsigned(this.frameOrigin.X); + uint uy0 = JxlPackSigned.PackUnsigned(this.frameOrigin.Y); + + if (!visitor.U32(enc, 0, ref ux0)) + { + return false; + } + + if (!visitor.U32(enc, 0, ref uy0)) + { + return false; + } + + this.frameOrigin = new Point(JxlPackSigned.UnpackSigned(ux0), JxlPackSigned.UnpackSigned(uy0)); + } + + uint frameSizeWidth = (uint)this.frameSize.Width; + uint frameSizeHeight = (uint)this.frameSize.Height; + + if (!visitor.U32(enc, 0, ref frameSizeWidth)) + { + return false; + } + + if (!visitor.U32(enc, 0, ref frameSizeHeight)) + { + return false; + } + + if (this.customSizeOrOrigin && (this.frameSize.Width == 0 || this.frameSize.Height == 0)) + { + throw new InvalidOperationException("Invalid crop dimensions for frame"); + } + + int imageXSize = this.DefaultXSize; + int imageYSize = this.DefaultYSize; + + if (this.frameType is JxlFrameType.RegularFrame or JxlFrameType.SkipProgressive) + { + isPartialFrame |= this.frameOrigin.X > 0; + isPartialFrame |= this.frameOrigin.Y > 0; + isPartialFrame |= (this.frameSize.Width + this.frameOrigin.X) < imageXSize; + isPartialFrame |= (this.frameSize.Height + this.frameOrigin.Y) < imageYSize; + } + } + } + + // Blending, animation, last frame + if (visitor.Conditional(this.frameType is JxlFrameType.RegularFrame or JxlFrameType.SkipProgressive)) + { + this.blendingInfo!.ExtraChannelCount = numExtraChannels; + this.blendingInfo.IsPartialFrame = isPartialFrame; + + if (!visitor.VisitNested(this.blendingInfo)) + { + return false; + } + + bool replaceAll = this.blendingInfo.BlendMode == JxlBlendMode.Replace; + + this.extraChannelBlendingInfo = new List(numExtraChannels); + for (int i = 0; i < numExtraChannels; i++) + { + JxlBlendingInfo ecBlendingInfo = new() + { + IsPartialFrame = isPartialFrame, + ExtraChannelCount = numExtraChannels + }; + + if (!visitor.VisitNested(ecBlendingInfo)) + { + return false; + } + + this.extraChannelBlendingInfo.Add(ecBlendingInfo); + replaceAll &= ecBlendingInfo.BlendMode == JxlBlendMode.Replace; + } + + if (visitor.IsReading && this.isPreviewFrame) + { + if (!replaceAll || this.customSizeOrOrigin) + { + throw new InvalidOperationException("Preview is not compatible with blending"); + } + } + + if (visitor.Conditional(this.metadata?.ImageMetadata?.HaveAnimation == true)) + { + this.animationFrame!.CodecMetadata = this.metadata; + + if (!visitor.VisitNested(this.animationFrame!)) + { + return false; + } + } + + if (!visitor.Boolean(true, ref this.isLast)) + { + return false; + } + } + else + { + this.isLast = false; + } + + // SaveAsReference + if (visitor.Conditional(this.frameType != JxlFrameType.DcFrame && !this.isLast)) + { + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(3), + 0, + ref this.saveAsReference)) + { + return false; + } + } + + // SaveBeforeColorTransform logic + if (this.frameType != JxlFrameType.DcFrame) + { + if (visitor.Conditional( + this.CanBeReferenced && + this.blendingInfo?.BlendMode == JxlBlendMode.Replace && + !isPartialFrame && + (this.frameType == JxlFrameType.RegularFrame || + this.frameType == JxlFrameType.SkipProgressive))) + { + if (!visitor.Boolean(false, ref this.saveBeforeColorTransform)) + { + return false; + } + } + else if (visitor.Conditional(this.frameType == JxlFrameType.ReferenceOnly)) + { + if (!visitor.Boolean(true, ref this.saveBeforeColorTransform)) + { + return false; + } + + int xsize = this.customSizeOrOrigin + ? this.frameSize.Width + : this.metadata!.XSize; + + int ysize = this.customSizeOrOrigin + ? this.frameSize.Height + : this.metadata!.YSize; + + if (!this.saveBeforeColorTransform && + (xsize < this.metadata!.XSize || + ysize < this.metadata!.YSize || + this.frameOrigin.X != 0 || + this.frameOrigin.Y != 0)) + { + throw new InvalidOperationException("Non-patch reference frame with invalid crop"); + } + } + } + else + { + this.saveBeforeColorTransform = true; + } + + if (!VisitNameString(visitor, ref this.name)) + { + return false; + } + + this.loopFilter!.IsModular = isModular; + if (!visitor.VisitNested(this.loopFilter!)) + { + return false; + } + + if (!visitor.BeginExtensions(ref this.extensions)) + { + return false; + } + + return visitor.EndExtensions(); + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeaderFlags.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeaderFlags.cs new file mode 100644 index 0000000000..098bcb57fc --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeaderFlags.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; + +/// +/// Optional steps for postprocessing. These flags are the +/// source of truth. Override must set/clear them rather than +/// change their meaning. Values chosen such that typical flags +/// are 0, encoded in only two bits. +/// +[Flags] +internal enum JxlFrameHeaderFlags : byte +{ + /// + /// Noise is injected into decoded output. + /// + Noise = 1, + + /// + /// Overlay patches. + /// + Patches = 2, + + /// + /// Overlay splines. + /// + Splines = 16, + + /// + /// Implies skip adaptive DC smoothing. + /// + Dc = 32, + + SkipAdaptiveDcSmoothing = 128, +} diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameType.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameType.cs new file mode 100644 index 0000000000..6aa73eab26 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameType.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; + +/// +/// Defines the type of a JPEG XL frame. +/// +internal enum JxlFrameType : byte +{ + /// + /// A regular frame. It might be a crop, and it will be blended + /// on a previous frame (if any) and likely displayed or blended in + /// future frames. + /// + RegularFrame, + + /// + /// A DC frame. It is downsampled and only used as the DC + /// of a future and, possibly, preview frame. This cannot be cropped, + /// blended, or referenced by patches or blending modes. Frames using + /// DC cannot have non-default sizes. + /// + DcFrame, + + /// + /// A PatchesSource frame. Can only be used as source frame for + /// taking patches. It can be cropped but can't have a non-(0, 0) x0/y0. + /// + ReferenceOnly = 2, + + /// + /// Same as regular frame but not used for progressive rendering. + /// Implies no early display of DC. + /// + SkipProgressive, +} diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlPasses.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlPasses.cs new file mode 100644 index 0000000000..87277414ac --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlPasses.cs @@ -0,0 +1,220 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; + +/// +/// Used for decoding to lower resolutions. +/// +internal sealed class JxlPasses : IJxlFields +{ + /// + /// Defines the maximum amount of passes, which is 11. + /// + private const int MaxPasses = 11; + + private uint numPasses; + private uint numDownsample; + + /// + /// Gets or sets the number of passes. + /// + public uint NumberOfPasses + { + get => this.numPasses; + set => this.numPasses = value; + } + + /// + /// Gets or sets the number of downsamples. + /// + public uint NumberOfDownsamples + { + get => this.numDownsample; + set => this.numDownsample = value; + } + + /// + /// Gets the downsample values. + /// + public uint[] Downsample { get; } = new uint[MaxPasses]; + + /// + /// Gets the last pass values. + /// + public uint[] LastPass { get; } = new uint[MaxPasses]; + + /// + /// Gets the shift values. + /// + public uint[] Shift { get; } = new uint[MaxPasses]; + + public void GetDownsamplingBracket(int pass, out int minShift, out int maxShift) + { + maxShift = 2; + minShift = 3; + + for (int i = 0; ; i++) + { + for (int j = 0; j < this.numDownsample; ++j) + { + if (i == this.LastPass[j]) + { + uint ds = this.Downsample[j]; + + if (ds == 8) + { + minShift = 3; + } + + if (ds == 4) + { + minShift = 2; + } + + if (ds == 2) + { + minShift = 1; + } + + if (ds == 1) + { + minShift = 0; + } + } + } + + if (i == this.numPasses - 1) + { + minShift = 0; + } + + if (i == pass) + { + return; + } + + maxShift = minShift - 1; + } + } + + public uint GetDownsamplingTargetForCompletedPasses(int num) + { + if (num >= this.numPasses) + { + return 1; + } + + uint result = 0; + + for (int i = 0; i < this.numDownsample; i++) + { + if (num > this.LastPass[i]) + { + result = Math.Min(result, this.Downsample[i]); + } + } + + return result; + } + + public bool Visit(JxlVisitor visitor) + { + if (visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.BitsOffset(1, 3), + 0, + ref this.numPasses)) + { + return false; + } + + if (this.numPasses > MaxPasses) + { + return false; + } + + if (visitor.Conditional(this.numPasses != 1)) + { + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.BitsOffset(1, 3), + 0, + ref this.numDownsample)) + { + return false; + } + + if (this.numDownsample > 4) + { + return false; + } + + if (this.numDownsample > this.numPasses) + { + throw new InvalidOperationException("Number of downsaples is greater than number of passes"); + } + + for (int i = 0; i < this.numPasses - 1; i++) + { + if (!visitor.Bits(2, 0u, ref this.Shift[i])) + { + return false; + } + } + + this.Shift[this.numPasses - 1] = 0; + + for (int i = 0; i < this.numDownsample; i++) + { + if (!visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(4), + JxlFieldExpressions.Value(8), + 1, + ref this.Downsample[i])) + { + return false; + } + + if (i > 0 && this.Downsample[i] >= this.Downsample[i - 1]) + { + throw new InvalidOperationException("Downsample sequence should decrease"); + } + } + + for (int i = 0; i < this.numDownsample; i++) + { + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(3), + 0, + ref this.LastPass[i])) + { + return false; + } + + if (i > 0 && this.LastPass[i] <= this.LastPass[i - 1]) + { + throw new InvalidOperationException("Last pass sequence should increase"); + } + + if (this.LastPass[i] >= this.numPasses) + { + throw new InvalidOperationException("Last pass is greater than number of passes"); + } + } + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlYCbCrChromaSubsampling.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlYCbCrChromaSubsampling.cs new file mode 100644 index 0000000000..b10d8818aa --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlYCbCrChromaSubsampling.cs @@ -0,0 +1,147 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; + +/// +/// Gets the Y'Cb'Cr chroma subsampling information as part +/// of the JPEG XL Frame Header. +/// +internal sealed class JxlYCbCrChromaSubsampling : IJxlFields +{ + private readonly int[] channelMode = new int[3]; + + private static ReadOnlySpan HShiftData => [0, 1, 1, 0]; + + private static ReadOnlySpan VShiftData => [0, 1, 0, 1]; + + public byte MaxHShift { get; private set; } + + public byte MaxVShift { get; private set; } + + /// + /// Gets a value indicating whether this 4:4:4 chroma subsampling. + /// + public bool Is444 => + this.HShift(0) == 0 && this.VShift(0) == 0 && // Cb + this.HShift(2) == 0 && this.VShift(2) == 0 && // Cr + this.HShift(1) == 0 && this.VShift(1) == 0; // Y; + + /// + /// Gets a value indicating whether this 4:2:0 chroma subsampling. + /// + public bool Is420 => + this.HShift(0) == 1 && this.VShift(0) == 1 && // Cb + this.HShift(2) == 1 && this.VShift(2) == 1 && // Cr + this.HShift(1) == 0 && this.VShift(1) == 0; // Y + + /// + /// Gets a value indicating whether this 4:2:2 chroma subsampling. + /// + public bool Is422 => + this.HShift(0) == 1 && this.VShift(0) == 0 && // Cb + this.HShift(2) == 1 && this.VShift(2) == 0 && // Cr + this.HShift(1) == 0 && this.VShift(1) == 0; // Y + + /// + /// Gets a value indicating whether this 4:4:0 chroma subsampling. + /// + public bool Is440 => + this.HShift(0) == 0 && this.VShift(0) == 1 && // Cb + this.HShift(2) == 0 && this.VShift(2) == 1 && // Cr + this.HShift(1) == 0 && this.VShift(1) == 0; // Y + + public byte RawHShift(int c) => HShiftData[this.channelMode[c]]; + + public byte RawVShift(int c) => VShiftData[this.channelMode[c]]; + + public byte HShift(int c) => (byte)(this.MaxHShift - HShiftData[this.channelMode[c]]); + + public byte VShift(int c) => (byte)(this.MaxVShift - VShiftData[this.channelMode[c]]); + + private void Recompute() + { + this.MaxHShift = 0; + this.MaxVShift = 0; + + for (int i = 0; i < 3; i++) + { + int ch = this.channelMode[i]; + + this.MaxHShift = Math.Max(this.MaxHShift, HShiftData[ch]); + this.MaxVShift = Math.Max(this.MaxVShift, VShiftData[ch]); + } + } + + public bool Set(ReadOnlySpan hsample, ReadOnlySpan vsample) + { + for (int c = 0; c < 3; c++) + { + int cjpeg = c < 2 ? (c ^ 1) : c; + int i = 0; + + for (; i < 4; i++) + { + if (1 << HShiftData[i] == hsample[cjpeg] && 1 << VShiftData[i] == vsample[cjpeg]) + { + this.channelMode[c] = i; + break; + } + } + + if (i == 4) + { + return false; + } + } + + this.Recompute(); + return true; + } + + public override string ToString() + { + if (this.Is444) + { + return "4:4:4"; + } + else if (this.Is420) + { + return "4:2:0"; + } + else if (this.Is422) + { + return "4:2:2"; + } + else if (this.Is440) + { + return "4:4:0"; + } + else + { + return $"[Custom] {this.channelMode[0]}:{this.channelMode[1]}:{this.channelMode[2]}"; + } + } + + public bool Visit(JxlVisitor visitor) + { + for (int i = 0; i < 3; i++) + { + int channel = this.channelMode[i]; + + uint unsignedChannel = (uint)channel; + bool wroteSuccessfully = visitor.Bits(2, 0, ref unsignedChannel); + + if (!wroteSuccessfully) + { + return false; + } + + this.channelMode[i] = (int)unsignedChannel; + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsConstants.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsConstants.cs new file mode 100644 index 0000000000..dcd244ac7d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsConstants.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +internal static class JxlAnsConstants +{ + public const int AnsLogTableSize = 12; + public const int AnsTableSize = 1 << AnsLogTableSize; + public const int AnsTabMask = AnsTableSize - 1; + public const int PrefixMaxAlphabetSize = 4096; + public const int AnsMaxAlphabetSize = 256; + public const int PrefixMaxBits = 15; + public const int AnsSignature = 0x13; +} diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs new file mode 100644 index 0000000000..25a9d12609 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +[StructLayout(LayoutKind.Sequential)] +internal struct JxlAnsEntry +{ + public byte Cutoff; + public byte RightValue; + public ushort Frequency0; + public ushort Offsets1; + public ushort Frequency1XorFrequency0; +} diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs new file mode 100644 index 0000000000..35630eb103 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs @@ -0,0 +1,242 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +internal static class JxlAnsHelper +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetPopulationCountPrecision(int logCount, int shift) + => Math.Max(0, Math.Min(logCount, shift - ((JxlAnsConstants.AnsLogTableSize - logCount) >> 1))); + + // NOTE: The result may potentially be large, so prefer using a memory allocator + public static IMemoryOwner CreateFlatHistogram(Configuration configuration, int length, int totalCount) + { + Debug.Assert(length <= 0, "Length should be >= 0"); + Debug.Assert(length > totalCount, "Length should be <= totalCount"); + + int count = totalCount / length; + IMemoryOwner result = configuration.MemoryAllocator.Allocate(length); + Span resultSpan = result.Memory.Span; + uint unsignedCount = (uint)count; + + for (int i = 0; i < length; i++) + { + resultSpan[i] = unsignedCount; + } + + int remCounts = totalCount % length; + for (int i = 0; i < remCounts; i++) + { + resultSpan[i]++; + } + + return result; + } + + public static JxlAnsSymbol Lookup(ReadOnlySpan table, int value, int logEntrySize, int entrySizeMinus1) + { + int i = value >> logEntrySize; + int pos = value & entrySizeMinus1; + + JxlAnsEntry entry = table[i]; + + int cutoff = entry.Cutoff; + int rightValue = entry.RightValue; + int freq0 = entry.Frequency0; + + bool greater = pos >= cutoff; + + int offsets1or0 = greater ? entry.Offsets1 : 0; + int freq1xorfreq0or0 = greater ? entry.Frequency1XorFrequency0 : 0; + + JxlAnsSymbol symbol = new() + { + Value = greater ? rightValue : i, + Offset = offsets1or0 + pos, + Frequency = freq0 ^ freq1xorfreq0or0 + }; + + return symbol; + } + + public static bool InitAliasTable(Span preDistribution, uint logRange, int logAlphaSize, Span entries) + { + int range = 1 << (int)logRange; + int tableSize = 1 << logAlphaSize; + + Debug.Assert(tableSize <= range, "table_size must be <= range"); + + int distributionPointer = preDistribution.Length - 1; + + while (distributionPointer >= 0 && preDistribution[distributionPointer] == 0) + { + distributionPointer--; + } + + if (distributionPointer < 0) + { + preDistribution[0] = range; + distributionPointer = 0; + } + + Span distribution = preDistribution[..(distributionPointer + 1)]; + + if (distribution.Length > tableSize) + { + Debug.Fail("Too many items in the distribution"); + + return false; + } + + int entrySize = range >> logAlphaSize; + int singleSymbol = -1; + int sum = 0; + + for (int sym = 0; sym < distribution.Length; sym++) + { + int value = distribution[sym]; + sum += value; + + if (value == JxlAnsConstants.AnsTableSize) + { + if (singleSymbol != -1) + { + return false; + } + + singleSymbol = sym; + } + } + + if (sum != range) + { + return false; + } + + if (singleSymbol != -1) + { + byte sym = (byte)singleSymbol; + if (singleSymbol != sym) + { + return false; + } + + for (int i = 0; i < tableSize; i++) + { + ref JxlAnsEntry jxlEntry = ref entries[i]; + + jxlEntry.RightValue = sym; + jxlEntry.Cutoff = 0; + jxlEntry.Offsets1 = (ushort)(entrySize * i); + jxlEntry.Frequency0 = 0; + jxlEntry.Frequency1XorFrequency0 = JxlAnsConstants.AnsTableSize; + } + + return true; + } + + Span underfullPosn = stackalloc uint[distribution.Length]; + Span overfullPosn = stackalloc uint[distribution.Length]; + Span cutoffs = stackalloc uint[1 << logAlphaSize]; + + int underfullPointer = 0; + int overfullPointer = 0; + + for (int i = 0; i < distribution.Length; i++) + { + uint currentCutoff = (uint)distribution[i]; + + cutoffs[i] = currentCutoff; + + if (currentCutoff > entrySize) + { + overfullPosn[overfullPointer] = (uint)i; + overfullPointer++; + } + else if (currentCutoff < entrySize) + { + underfullPosn[underfullPointer] = (uint)i; + underfullPointer++; + } + } + + for (int i = distribution.Length; i < tableSize; i++) + { + cutoffs[i] = 0; + underfullPosn[underfullPointer] = (uint)i; + underfullPointer++; + } + + uint unsignedEntrySize = (uint)entrySize; + + while (overfullPointer >= 0) + { + uint overfullIndex = overfullPosn[overfullPointer]; + overfullPointer--; + + if (underfullPointer <= -1) + { + return false; + } + + uint underfullIndex = underfullPosn[underfullPointer]; + underfullPointer--; + + int signedOverfullIndex = (int)overfullIndex; + int signedUnderfullIndex = (int)underfullIndex; + + uint underfullBy = unsignedEntrySize - cutoffs[signedUnderfullIndex]; + cutoffs[signedOverfullIndex] -= underfullBy; + + ref JxlAnsEntry currentEntry = ref entries[signedUnderfullIndex]; + + currentEntry.RightValue = unchecked((byte)overfullIndex); + currentEntry.Offsets1 = unchecked((ushort)cutoffs[signedOverfullIndex]); + + uint currentCutoff = cutoffs[signedOverfullIndex]; + + if (currentCutoff < entrySize) + { + underfullPosn[underfullPointer] = overfullIndex; + underfullPointer++; + } + else if (currentCutoff > entrySize) + { + overfullPosn[overfullPointer] = overfullIndex; + overfullPointer++; + } + } + + for (uint i = 0; i < tableSize; i++) + { + uint currentCutoff = cutoffs[(int)i]; + ref JxlAnsEntry entry = ref entries[(int)i]; + + if (currentCutoff == entrySize) + { + entry.RightValue = (byte)i; + entry.Offsets1 = 0; + entry.Cutoff = 0; + } + else + { + entry.Offsets1 -= (ushort)currentCutoff; + entry.Cutoff = (byte)currentCutoff; + } + + int freq0 = i < distribution.Length ? distribution[(int)i] : 0; + int i1 = entry.RightValue; + int freq1 = i1 < distribution.Length ? distribution[i1] : 0; + + entry.Frequency0 = (ushort)freq0; + entry.Frequency1XorFrequency0 = (ushort)(freq1 ^ freq0); + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHybridUIntConfiguration.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHybridUIntConfiguration.cs new file mode 100644 index 0000000000..422eb2323f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHybridUIntConfiguration.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +internal sealed class JxlAnsHybridUIntConfiguration : IJxlFields +{ + public JxlAnsHybridUIntConfiguration(uint splitExponent = 4, uint msbInToken = 2, uint lsbInToken = 0) + { + this.SplitExponent = splitExponent; + this.SplitToken = 1u << (int)splitExponent; + this.MsbInToken = msbInToken; + this.LsbInToken = lsbInToken; + + Debug.Assert(splitExponent >= msbInToken + lsbInToken, "Split exponent should be < msbInToken + lsbInToken"); + } + + public uint SplitExponent { get; set; } + + public uint SplitToken { get; set; } + + public uint MsbInToken { get; set; } // Most significant bit + + public uint LsbInToken { get; set; } // Least significant bit + + public uint LsbMask => (1u << (int)this.LsbInToken) - 1; + + public void Encode(uint value, ref uint token, ref uint bitCount, ref uint bits) + { + if (value < this.SplitToken) + { + token = value; + bitCount = 0; + bits = 0; + } + else + { + uint n = FloorLog2Nonzero(value); + uint m = value - (1u << (int)n); + + unchecked + { + // The following expression is quite complex. + // See https://github.com/libjxl/libjxl/blob/main/lib/jxl/dec_ans.h#L83C16-L86C47. + token = this.SplitToken + + (uint)(((n - this.SplitExponent) << (int)(this.MsbInToken + this.LsbInToken)) + + ((m >> (int)(n - this.MsbInToken)) << (int)this.LsbInToken) + + (m & ((1 << (int)this.LsbInToken) - 1))); + + bitCount = n - this.MsbInToken - this.LsbInToken; + bits = (value >> (int)this.LsbInToken) & ((1u << (int)bitCount) - 1); + } + } + } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsLz77Parameters.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsLz77Parameters.cs new file mode 100644 index 0000000000..ecf5cdc3fa --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsLz77Parameters.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +internal sealed class JxlAnsLz77Parameters : IJxlFields +{ + public bool Enabled { get; set; } + + public uint MinimumSymbol { get; set; } + + public uint MinimumLength { get; set; } + + public JxlAnsHybridUIntConfiguration LengthUintConfig { get; set; } = new(0, 0, 0); + + public int NonserializedDistanceContext { get; set; } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs new file mode 100644 index 0000000000..d28e98c029 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs @@ -0,0 +1,256 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +internal static class JxlAnsReader +{ + // Prefer jagged arrays over multidimensional arrays + // for performance. Collection expressions help represent + // jagged arrays easily. + private static readonly byte[][] HuffmanLookup = + [ + [3, 10], [7, 12], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5], + [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2], + [3, 10], [5, 0], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5], + [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2], + [3, 10], [6, 11], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5], + [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2], + [3, 10], [5, 0], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5], + [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2], + [3, 10], [7, 13], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5], + [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2], + [3, 10], [5, 0], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5], + [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2], + [3, 10], [6, 11], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5], + [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2], + [3, 10], [5, 0], [3, 7], [4, 3], [3, 6], [3, 8], [3, 9], [4, 5], + [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2], + ]; + + public static uint DecodeVariableLengthUint8(JxlBitReader reader) + { + if (reader.ReadBoolean()) + { + uint bitCount = reader.ReadBits32(3u); + + return bitCount == 0 + ? 1u + : (reader.ReadBits32(bitCount) + (1u << (int)bitCount)); + } + + return 0u; + } + + public static uint DecodeVariableLengthUint16(JxlBitReader reader) + { + if (reader.ReadBoolean()) + { + uint bitCount = reader.ReadBits32(4u); + + return bitCount == 0 + ? 1u + : (reader.ReadBits32(bitCount) + (1u << (int)bitCount)); + } + + return 0u; + } + + // NOTE: this method returns null on failure. + // If the return value is a valid IMemoryOwner object, + // then it succeeded. + public static IMemoryOwner? ReadHistogram(Configuration configuration, int precisionBits, JxlBitReader reader) + { + int range = 1 << precisionBits; + bool isSimpleCode = reader.ReadBoolean(); + + IMemoryOwner counts; + + if (isSimpleCode) + { + Span symbols = stackalloc uint[2]; + symbols.Clear(); + + uint maxSymbol = 0u; + uint symCount = reader.ReadBits32(1u) + 1u; + for (uint i = 0; i < symCount; i++) + { + uint symbol = DecodeVariableLengthUint8(reader); + if (symbol > maxSymbol) + { + maxSymbol = symbol; + } + + symbols[(int)i] = symbol; + } + + // Up to 256 items + counts = configuration.MemoryAllocator.Allocate((int)maxSymbol + 1); + Span countsSpan = counts.Memory.Span; + + if (symCount == 1) + { + countsSpan[(int)symbols[0]] = (uint)range; + } + else + { + if (symbols[0] == symbols[1]) + { + Debug.Fail("Corrupt data"); + counts.Dispose(); + return null; + } + + countsSpan[(int)symbols[0]] = reader.ReadBits32((uint)precisionBits); + countsSpan[(int)symbols[1]] = (uint)range - countsSpan[(int)symbols[0]]; + } + } + else + { + bool isFlat = reader.ReadBoolean(); + + if (isFlat) + { + uint alphabetSize = DecodeVariableLengthUint8(reader) + 1u; + if (alphabetSize <= range) + { + return null; + } + + counts = JxlAnsHelper.CreateFlatHistogram(configuration, (int)alphabetSize, range); + return counts; + } + + int upperBoundLog = FloorLog2Nonzero(JxlAnsConstants.AnsLogTableSize + 1); + int log = 0; + + for (; log < upperBoundLog; log++) + { + bool logIncrementBit = reader.ReadBoolean(); + + if (!logIncrementBit) + { + break; + } + } + + uint shift = (reader.ReadBits32((uint)log) | (1u << log)) - 1u; + + if (shift > JxlAnsConstants.AnsLogTableSize + 1) + { + Debug.Fail("Invalid shift"); + + return null; + } + + uint length = DecodeVariableLengthUint8(reader) + 3u; + + counts = configuration.MemoryAllocator.Allocate((int)length); + Span countsSpan = counts.Memory.Span; + + uint totalCount = 0; + + // The length variable can represent up to 258 elements, + // so it'd be more beneficial to allocate on the stack + // than pool an array. + Span logCounts = stackalloc int[(int)length]; + + // stackalloc doesn't zero-init, so clear just in case. + logCounts.Clear(); + + int omitLog = -1; + int omitPos = -1; + + // See comments for logCounts definition + Span same = stackalloc int[(int)length]; + same.Clear(); + + for (int i = 0; i < length; i++) + { + uint index = reader.PeekBits32(7); + reader.SkipBits32(HuffmanLookup[index][0]); + logCounts[i] = HuffmanLookup[index][1] - 1; + + if (logCounts[i] == JxlAnsConstants.AnsLogTableSize) + { + uint rleLength = DecodeVariableLengthUint8(reader); + same[i] = (int)rleLength + 5; + i += (int)rleLength + 3; + continue; + } + + if (logCounts[i] > omitLog) + { + omitLog = logCounts[i]; + omitPos = i; + } + } + + if (omitPos < 0) + { + Debug.Fail("The histogram is corrupt or invalid."); + counts.Dispose(); + return null; + } + + if (omitPos + 1 < length && logCounts[omitPos + 1] == JxlAnsConstants.AnsLogTableSize) + { + Debug.Fail("The histogram is corrupt or invalid."); + counts.Dispose(); + return null; + } + + int previous = 0; + int sameCount = 0; + + for (int i = 0; i < length; i++) + { + if (same[i] > 0) + { + sameCount = same[i] - 1; + previous = i > 0 ? (int)countsSpan[i - 1] : 0; + } + + if (sameCount > 0) + { + countsSpan[i] = (uint)previous; + sameCount--; + } + else + { + int code = logCounts[i]; + + if (i == omitPos || code < 0) + { + continue; + } + else if (shift == 0 || code == 0) + { + countsSpan[i] = 1u << code; + } + else + { + int bitCount = JxlAnsHelper.GetPopulationCountPrecision(code, (int)shift); + countsSpan[i] = (1u << code) + (reader.ReadBits32((uint)bitCount) << (code - bitCount)); + } + } + + totalCount += countsSpan[i]; + } + + countsSpan[omitPos] = (uint)range - totalCount; + + if (countsSpan[omitPos] <= 0) + { + Debug.Fail("The histogram count is incorrect."); + counts.Dispose(); + return null; + } + } + + return counts; + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsSymbol.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsSymbol.cs new file mode 100644 index 0000000000..75afa94d9f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsSymbol.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +[StructLayout(LayoutKind.Sequential)] +internal struct JxlAnsSymbol(int value, int offset, int frequency) +{ + public int Value = value; + public int Offset = offset; + public int Frequency = frequency; +} diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs new file mode 100644 index 0000000000..50753321e7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs @@ -0,0 +1,176 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +/// +/// Represents a bitstream reader. +/// +internal sealed class JxlBitReader(ReadOnlyMemory bytes) +{ + private ulong buffer; + private uint bufferRemainingBits; + private int pointer; + + /// + /// 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. + /// + private void RefillCore() + { + ReadOnlySpan samplesSpan = bytes.Span; + + int remaining = samplesSpan.Length - this.pointer; + if (remaining <= 0) + { + // we don't have any more data... mark an end of stream + this.buffer = 0; + this.bufferRemainingBits = 0; + this.IsEndOfStream = true; + return; + } + + if (remaining >= 8) + { + this.buffer = BinaryPrimitives.ReadUInt64LittleEndian(samplesSpan[this.pointer..]); + this.bufferRemainingBits = 64u; + this.pointer += 8; + } + else + { + ulong value = 0; + for (int i = 0; i < remaining; i++) + { + value |= (ulong)samplesSpan[this.pointer + i] << (8 * i); + } + + this.buffer = value; + this.bufferRemainingBits = (uint)(remaining * 8); + this.pointer += remaining; + } + } + + private void MaybeRefill() + { + if (this.bufferRemainingBits <= 0) + { + this.RefillCore(); + } + } + + private ulong ReadBits64Core(uint n, bool peek = false) + { + Debug.Assert(n <= 64, "Too many bits to pack into ulong"); + this.MaybeRefill(); + + if (this.IsEndOfStream) + { + JxlThrowHelper.ThrowEndOfStream(); + } + + if (n <= this.bufferRemainingBits) + { + ulong result = this.buffer & ((1UL << (int)n) - 1); + + if (!peek) + { + this.buffer >>= (int)n; + this.bufferRemainingBits -= n; + } + + return result; + } + else + { + uint bitsFromCurrent = this.bufferRemainingBits; + ulong part = this.buffer & ((1UL << (int)bitsFromCurrent) - 1); + + this.buffer >>= (int)bitsFromCurrent; + this.bufferRemainingBits = 0; + + this.RefillCore(); + + uint bitsFromNext = n - bitsFromCurrent; + ulong nextPart = this.buffer & ((1UL << (int)bitsFromNext) - 1); + + if (!peek) + { + this.buffer >>= (int)bitsFromNext; + this.bufferRemainingBits -= bitsFromNext; + } + + return part | (nextPart << (int)bitsFromCurrent); + } + } + + private uint ReadBits32Core(uint n, bool peek = false) + { + Debug.Assert(n <= 32, "Too many bits to pack into uint"); + this.MaybeRefill(); + + if (this.IsEndOfStream) + { + JxlThrowHelper.ThrowEndOfStream(); + } + + if (n <= this.bufferRemainingBits) + { + uint result = (uint)(this.buffer & ((1UL << (int)n) - 1)); + + if (!peek) + { + this.buffer >>= (int)n; + this.bufferRemainingBits -= n; + } + + return result; + } + else + { + uint bitsFromCurrent = this.bufferRemainingBits; + uint part = (uint)(this.buffer & ((1UL << (int)bitsFromCurrent) - 1)); + + this.buffer >>= (int)bitsFromCurrent; + this.bufferRemainingBits = 0; + + this.RefillCore(); + + uint bitsFromNext = n - bitsFromCurrent; + uint nextPart = (uint)(this.buffer & ((1UL << (int)bitsFromNext) - 1)); + + if (!peek) + { + this.buffer >>= (int)bitsFromNext; + this.bufferRemainingBits -= bitsFromNext; + } + + return part | (nextPart << (int)bitsFromCurrent); + } + } + + public uint ReadBits32(uint bits) => this.ReadBits32Core(bits, peek: false); + + public uint PeekBits32(uint bits) => this.ReadBits32Core(bits, peek: true); + + public void SkipBits32(uint bits) => _ = this.ReadBits32(bits); + + public ulong ReadBits64(ulong bits) => this.ReadBits64Core((uint)bits, peek: false); + + public ulong PeekBits64(ulong bits) => this.ReadBits64Core((uint)bits, peek: true); + + public void SkipBits64(ulong bits) => _ = this.ReadBits64(bits); + + public bool ReadBoolean() => this.ReadBits32Core(1, peek: false) == 1; +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlAnimationHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlAnimationHeader.cs new file mode 100644 index 0000000000..647e8bbc7d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlAnimationHeader.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; + +internal sealed class JxlAnimationHeader : IJxlFields +{ + public int TpsNumerator { get; set; } + + public int TpsDenominator { get; set; } + + public int LoopCount { get; set; } + + public bool ContainsTimecodes { get; set; } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepth.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepth.cs new file mode 100644 index 0000000000..0a9db0f575 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepth.cs @@ -0,0 +1,129 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; + +/// +/// Represents the JPEG XL Bit Depth image metadata. +/// +internal sealed class JxlBitDepth : IJxlFields +{ + private uint bitsPerSample; + private uint exponentBitsPerSample; + + /// + /// Initializes a new instance of the class. + /// + public JxlBitDepth() => JxlBundle.Init(this); + + /// + /// Gets or sets a value indicating whether + /// the original (uncompressed) samples are floating point or + /// unsigned integer. + /// + public bool FloatingPointSample { get; set; } + + /// + /// Gets or sets the bit depth of the original (uncompressed) image samples. + /// Must be in the range [1, 32]. + /// + public uint BitsPerSample + { + get => this.bitsPerSample; + set => this.bitsPerSample = value; + } + + /// + /// + /// Gets or sets floating point exponent bits of the original (uncompressed) image samples, + /// only used if is . + /// + /// + /// If used, the samples are floating point with: + /// + /// 1 sign bit + /// exponent bits + /// ( - - 1) mantissa bits + /// + /// If used, must be in the range + /// [2, 8] and amount of mantissa bits must be in the range [2, 23]. + /// + /// + public uint ExponentBitsPerSample + { + get => this.exponentBitsPerSample; + set => this.exponentBitsPerSample = value; + } + + public bool Visit(JxlVisitor visitor) + { + if (!this.FloatingPointSample) + { + bool successful = visitor.U32( + JxlFieldExpressions.Value(8u), + JxlFieldExpressions.Value(10u), + JxlFieldExpressions.Value(12u), + JxlFieldExpressions.BitsOffset(6u, 1u), + 8u, + ref this.bitsPerSample); + + if (!successful) + { + return false; + } + + this.exponentBitsPerSample = 0; + } + else + { + if (!visitor.U32( + JxlFieldExpressions.Value(32u), + JxlFieldExpressions.Value(16u), + JxlFieldExpressions.Value(24u), + JxlFieldExpressions.BitsOffset(6u, 1u), + 32u, + ref this.bitsPerSample)) + { + return false; + } + + this.exponentBitsPerSample--; + + if (!visitor.Bits(4, 7, ref this.exponentBitsPerSample)) + { + return false; + } + + this.exponentBitsPerSample++; + } + + if (this.FloatingPointSample) + { + if (this.exponentBitsPerSample is < 2 or > 8) + { + DebugGuard.IsTrue(false, "Invalid exponent_bits_per_sample"); + + return false; + } + + int mantissaBits = (int)this.bitsPerSample - (int)this.exponentBitsPerSample - 1; + + if (mantissaBits is < 2 or > 23) + { + DebugGuard.IsTrue(false, "Invalid bits_per_sample"); + + return false; + } + } + else if (this.bitsPerSample > 31) + { + DebugGuard.IsTrue(false, "Invalid bits_per_sample"); + + return false; + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCodecMetadata.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCodecMetadata.cs new file mode 100644 index 0000000000..5a3aaeecb7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCodecMetadata.cs @@ -0,0 +1,57 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; + +internal sealed class JxlCodecMetadata +{ + public JxlImageMetadata? ImageMetadata { get; set; } + + public JxlSizeHeader? Size { get; set; } + + public JxlCustomTransformData? CustomTransformData { get; set; } + + public int XSize => this.Size?.XSize ?? 0; + + public int YSize => this.Size?.YSize ?? 0; + + public int GetOrientedPreviewXSize(bool keepOrientation) + { + if (this.ImageMetadata!.Orientation > 4 && !keepOrientation) + { + return this.ImageMetadata.PreviewSize.YSize; + } + + return this.ImageMetadata.PreviewSize.XSize; + } + + public int GetOrientedPreviewYSize(bool keepOrientation) + { + if (this.ImageMetadata!.Orientation > 4 && !keepOrientation) + { + return this.ImageMetadata.PreviewSize.XSize; + } + + return this.ImageMetadata.PreviewSize.YSize; + } + + public int GetOrientedXSize(bool keepOrientation) + { + if (this.ImageMetadata!.Orientation > 4 && !keepOrientation) + { + return this.YSize; + } + + return this.XSize; + } + + public int GetOrientedYSize(bool keepOrientation) + { + if (this.ImageMetadata!.Orientation > 4 && !keepOrientation) + { + return this.XSize; + } + + return this.YSize; + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs new file mode 100644 index 0000000000..3b3c6bc200 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; + +internal sealed class JxlCustomTransformData : IJxlFields +{ + public bool NonserializedXybEncoded { get; set; } + + public bool AllDefault { get; set; } + + public JxlOpsinInverseMatrix? OpsinInverseMatrix { get; set; } + + public int CustomWeightsMask { get; set; } + + public InlineArray15 Upsampling2Weights { get; set; } + + public InlineArray55 Upsampling4Weights { get; set; } + + public InlineArray210 Upsampling8Weights { get; set; } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExifOrientation.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExifOrientation.cs new file mode 100644 index 0000000000..406e7e9a1a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExifOrientation.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; + +internal enum JxlExifOrientation : byte +{ + Identity = 1, + FlipHorizontal = 2, + Rotate180 = 3, + FlipVertical = 4, + Transponse = 5, + Rotate90 = 6, + AntiTranspose = 7, + Rotate270 = 8 +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannel.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannel.cs new file mode 100644 index 0000000000..8e2b842a56 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannel.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; + +internal enum JxlExtraChannel : byte +{ + Alpha, + Depth, + SpotColor, + SelectionMask, + Black, + Cfa, + Thermal, + Reserved0, + Reserved1, + Reserved2, + Reserved3, + Reserved4, + Reserved5, + Reserved6, + Reserved7, + Unknown, + Optional +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs new file mode 100644 index 0000000000..5b492b1026 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; + +internal sealed class JxlExtraChannelInfo : IJxlFields +{ + public bool AllDefault { get; set; } + + public JxlExtraChannel Type { get; set; } + + public JxlBitDepth? BitDepth { get; set; } + + public int DimensionShift { get; set; } + + public string? Name { get; set; } + + public bool AlphaAssociated { get; set; } + + public InlineArray4 SpotColor { get; set; } + + public int CfaChannel { get; set; } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs new file mode 100644 index 0000000000..d9bc419e97 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs @@ -0,0 +1,128 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; + +internal sealed class JxlImageMetadata : IJxlFields +{ + public bool AllDefault { get; set; } + + public JxlBitDepth? BitDepth { get; set; } + + public bool Modular16BitBufferSufficient { get; set; } // Otherwise, 32 is + + public bool XybEncoded { get; set; } + + public JxlColorEncoding? ColorEncoding { get; set; } + + public int Orientation { get; set; } = 1; + + public bool HavePreview { get; set; } + + public bool HaveAnimation { get; set; } + + public bool HaveIntrinsicSize { get; set; } + + public JxlSizeHeader IntrinsicSize { get; set; } + + public JxlToneMapping? ToneMapping { get; set; } + + public int ExtraChannelCount { get; set; } + + public List ExtraChannels { get; set; } = []; + + public JxlPreviewHeader PreviewSize { get; set; } + + public JxlAnimationHeader Animation { get; set; } + + public long Extensions { get; set; } + + public bool NonserializedOnlyParseBasicInfos { get; set; } + + public float IntensityTarget + { + get + { + float intensityTarget = this.ToneMapping?.IntensityTarget ?? 0f; + + Debug.Assert(intensityTarget != 0f, "Intensity target should be present"); + + return intensityTarget; + } + + set + { + if (this.ToneMapping != null) + { + this.ToneMapping.IntensityTarget = value; + } + } + } + + public int AlphaBits + { + get + { + JxlExtraChannelInfo? ec = this.FindExtraChannel(JxlExtraChannel.Alpha); + + if (ec == null) + { + return 0; + } + + return ec.BitDepth?.BitsPerSample ?? 0; + } + + set + { + } + } + + public bool HasAlpha => this.AlphaBits != 0; + + public JxlExtraChannelInfo? FindExtraChannel(JxlExtraChannel type) + => this.ExtraChannels.FirstOrDefault(eci => eci.Type == type); + + public JxlExifOrientation GetExifOrientation() => (JxlExifOrientation)this.Orientation; + + public void SetFloat16Samples() + { + if (this.BitDepth != null) + { + this.BitDepth.BitsPerSample = 16; + this.BitDepth.ExponentBitsPerSample = 5; + this.BitDepth.FloatingPointSample = true; + } + + this.Modular16BitBufferSufficient = false; + } + + public void SetFloat32Samples() + { + if (this.BitDepth != null) + { + this.BitDepth.BitsPerSample = 32; + this.BitDepth.ExponentBitsPerSample = 8; + this.BitDepth.FloatingPointSample = true; + } + + this.Modular16BitBufferSufficient = false; + } + + public void SetUIntSamples(int bits) + { + if (this.BitDepth != null) + { + this.BitDepth.BitsPerSample = bits; + this.BitDepth.ExponentBitsPerSample = 0; + this.BitDepth.FloatingPointSample = false; + } + + this.Modular16BitBufferSufficient = bits <= 12; + } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs new file mode 100644 index 0000000000..357ce27c39 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; + +internal sealed class JxlOpsinInverseMatrix : IJxlFields +{ + public bool AllDefault { get; set; } + + public JxlMatrix3x3F InverseMatrix { get; set; } + + public InlineArray3 OpsinBiases { get; set; } + + public InlineArray4 QuantBiases { get; set; } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlPreviewHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlPreviewHeader.cs new file mode 100644 index 0000000000..e46763cfeb --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlPreviewHeader.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; + +internal sealed class JxlPreviewHeader : IJxlFields +{ + private bool div8; + private int ySizeDiv8; + private int ySize; + private int ratio; + private int xSizeDiv8; + private int xSize; + + public int YSize => this.div8 ? (this.ySizeDiv8 * 8) : this.ySize; + + public int XSize + { + get + { + if (this.ratio != 0) + { + SignedRational signedRational = JxlAspectRatioHelpers.FixedAspectRatios(this.ratio); + + return JxlAspectRatioHelpers.MultiplyTruncate(signedRational, this.YSize); + } + + return this.div8 ? (this.xSizeDiv8 * 8) : this.xSize; + } + } + + public void Set(int x, int y) + { + if (x == 0 || y == 0) + { + throw new ArgumentException("Empty preview"); + } + + this.div8 = ((x % JxlFrameDimensions.BlockDimensions) | (y % JxlFrameDimensions.BlockDimensions)) == 0; + + if (this.div8) + { + this.ySizeDiv8 = y / 8; + } + else + { + this.ySize = y; + } + + this.ratio = JxlAspectRatioHelpers.FindAspectRatio(x, y); + + if (this.ratio == 0) + { + if (this.div8) + { + this.xSizeDiv8 = x / 8; + } + else + { + this.xSize = x; + } + } + } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlSizeHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlSizeHeader.cs new file mode 100644 index 0000000000..5da30b8a0d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlSizeHeader.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; + +internal sealed class JxlSizeHeader : IJxlFields +{ + private bool isSmall; + private int ySizeDiv8Minus1; + private int ySize; + private int ratio; + private int xSizeDiv8Minus1; + private int xSize; + + public int YSize => this.isSmall ? ((this.ySizeDiv8Minus1 + 1) * 8) : this.ySize; + + public int XSize + { + get + { + if (this.ratio != 0) + { + SignedRational aspectRatio = JxlAspectRatioHelpers.FixedAspectRatios(this.ratio); + + return JxlAspectRatioHelpers.MultiplyTruncate(aspectRatio, this.YSize); + } + + return this.isSmall ? ((this.xSizeDiv8Minus1 + 1) * 8) : this.xSize; + } + } + + public void Set(int x, int y) + { + if (x > int.MaxValue || y > int.MaxValue) + { + throw new ArgumentException("Image too large"); + } + + if (x == 0 || y == 0) + { + throw new ArgumentException("Empty image"); + } + + this.ratio = JxlAspectRatioHelpers.FindAspectRatio(x, y); + this.isSmall = y < 256 && (y % JxlFrameDimensions.BlockDimensions) == 0 + && (this.ratio != 0 || (x <= 256 && (x % JxlFrameDimensions.BlockDimensions) == 0)); + + if (this.isSmall) + { + this.ySizeDiv8Minus1 = (y / 8) - 1; + } + else + { + this.ySize = y; + } + + if (this.ratio == 0) + { + if (this.isSmall) + { + this.xSizeDiv8Minus1 = (x / 8) - 1; + } + else + { + this.xSize = x; + } + } + } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs new file mode 100644 index 0000000000..9a399dc0b0 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; + +internal sealed class JxlToneMapping : IJxlFields +{ + public bool AllDefault { get; set; } + + public float IntensityTarget { get; set; } + + public float LowerBoundIntensityLevel { get; set; } + + public bool RelativeToMaxDisplay { get; set; } + + public float LinearBelow { get; set; } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/InlineArrays.cs b/src/ImageSharp/Formats/Jxl/InlineArrays.cs new file mode 100644 index 0000000000..9c19e9266c --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/InlineArrays.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +#pragma warning disable SA1649 // File name should match first type name + +namespace SixLabors.ImageSharp.Formats.Jxl; + +[InlineArray(3)] +internal struct InlineArray3 +{ + private T first; +} + +/// +/// Used by JxlCustomTransformData +/// +[InlineArray(15)] +internal struct InlineArray15 +{ + private T first; +} + +/// +/// Used by JxlCustomTransformData +/// +[InlineArray(55)] +internal struct InlineArray55 +{ + private T first; +} + +/// +/// Used by JxlCustomTransformData +/// +[InlineArray(210)] +internal struct InlineArray210 +{ + private T first; +} + +/// +/// Used by JxlWeightsSeparable5 +/// +[InlineArray(12)] +internal struct InlineArray12 +{ + private T first; +} diff --git a/src/ImageSharp/Formats/Jxl/JxlAspectRatioHelpers.cs b/src/ImageSharp/Formats/Jxl/JxlAspectRatioHelpers.cs new file mode 100644 index 0000000000..ee5d1c134b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/JxlAspectRatioHelpers.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl; + +internal static class JxlAspectRatioHelpers +{ + private static readonly SignedRational[] Ratios = + [ + new(1, 1), + new(12, 10), + new(4, 3), + new(3, 2), + new(16, 9), + new(5, 4), + new(2, 1) + ]; + + public static SignedRational FixedAspectRatios(int ratio) => Ratios[ratio - 1]; + + public static int FindAspectRatio(int x, int y) + { + for (int i = 0; i < 7; i++) + { + if (x == MultiplyTruncate(Ratios[i], y)) + { + return i; + } + } + + return 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int MultiplyTruncate(SignedRational rational, int multiplicand) => (multiplicand * rational.Numerator) / rational.Denominator; +} diff --git a/src/ImageSharp/Formats/Jxl/JxlFormat.cs b/src/ImageSharp/Formats/Jxl/JxlFormat.cs new file mode 100644 index 0000000000..00e7b66eb5 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/JxlFormat.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl; + +internal class JxlFormat : IImageFormat +{ + public string Name => "JPEG XL"; + + public string DefaultMimeType => "image/jxl"; + + IEnumerable IImageFormat.MimeTypes => new[] { "image/jxl" }; + + IEnumerable IImageFormat.FileExtensions => new[] { "jxl" }; +} diff --git a/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs b/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs new file mode 100644 index 0000000000..8a7416a573 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs @@ -0,0 +1,82 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl; + +internal sealed class JxlFrameDimensions +{ + public const int BlockDimensions = 8; + public const int DctBlockSize = BlockDimensions * BlockDimensions; + public const int GroupDimensions = 256; + public const int GroupDimensionsInBlocks = GroupDimensions / BlockDimensions; + + public JxlFrameDimensions(int xSizePixel, int ySizePixel, int groupSizeShift, int maxHorizontalShift, int maxVerticalShift, bool modularMode, int upsampling) + { + this.GroupDimension = (GroupDimensions >> 1) << groupSizeShift; + this.DcGroupDimension = this.GroupDimension * BlockDimensions; + this.XSizeUpsampled = xSizePixel; + this.YSizeUpsampled = ySizePixel; + this.XSize = DivCeil(xSizePixel, upsampling); + this.YSize = DivCeil(ySizePixel, upsampling); + this.XSizeBlocks = DivCeil(this.XSize, BlockDimensions << maxHorizontalShift) << maxHorizontalShift; + this.YSizeBlocks = DivCeil(this.YSize, BlockDimensions << maxVerticalShift) << maxVerticalShift; + this.XSizePadded = this.XSizeBlocks * BlockDimensions; + this.YSizePadded = this.YSizeBlocks * BlockDimensions; + + if (modularMode) + { + this.XSizePadded = this.XSize; + this.YSizePadded = this.YSize; + } + + this.XSizeUpsampledPadded = this.XSizePadded * upsampling; + this.YSizeUpsampledPadded = this.YSizePadded * upsampling; + this.XSizeGroups = DivCeil(this.XSize, GroupDimensions); + this.YSizeGroups = DivCeil(this.YSize, GroupDimensions); + this.XSizeDcGroups = DivCeil(this.XSizeBlocks, GroupDimensions); + this.YSizeDcGroups = DivCeil(this.YSizeBlocks, GroupDimensions); + this.NumGroups = this.XSizeGroups * this.YSizeGroups; + this.NumDcGroups = this.XSizeDcGroups * this.YSizeDcGroups; + } + + public int XSize { get; set; } + + public int YSize { get; set; } + + public int XSizeUpsampled { get; set; } + + public int YSizeUpsampled { get; set; } + + public int XSizeUpsampledPadded { get; set; } + + public int YSizeUpsampledPadded { get; set; } + + public int XSizePadded { get; set; } + + public int YSizePadded { get; set; } + + public int XSizeBlocks { get; set; } + + public int YSizeBlocks { get; set; } + + public int XSizeGroups { get; set; } + + public int YSizeGroups { get; set; } + + public int XSizeDcGroups { get; set; } + + public int YSizeDcGroups { get; set; } + + public int NumGroups { get; set; } + + public int NumDcGroups { get; set; } + + public int GroupDimension { get; set; } + + public int DcGroupDimension { get; set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int DivCeil(int x, int y) => x / y; +} diff --git a/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs b/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs new file mode 100644 index 0000000000..a39dfe707c --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats.Jxl; + +internal static class JxlThrowHelper +{ + [DoesNotReturn] + public static void ThrowEndOfStream() => throw new EndOfStreamException(); +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs new file mode 100644 index 0000000000..63a5c59c9f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +/// +/// Represents a three-plane, 2D raster image of type . +/// +internal sealed class JxlImage3B : JxlImage3 +{ + public JxlImage3B() + { + } +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs new file mode 100644 index 0000000000..b456dfd9a7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +/// +/// Represents a three-plane, 2D raster image of type . +/// +internal sealed class JxlImage3F : JxlImage3 +{ + public JxlImage3F() + { + } +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs new file mode 100644 index 0000000000..0d9f6c8202 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +/// +/// Represents a three-plane, 2D raster image of type . +/// +internal sealed class JxlImage3I : JxlImage3 +{ + public JxlImage3I() + { + } +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs new file mode 100644 index 0000000000..00615ff846 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +/// +/// Represents a three-plane, 2D raster image of type . +/// +internal sealed class JxlImage3S : JxlImage3 +{ + public JxlImage3S() + { + } +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs new file mode 100644 index 0000000000..1921e86d33 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +/// +/// Represents a three-plane, 2D raster image of type . +/// +internal sealed class JxlImage3U : JxlImage3 +{ + public JxlImage3U() + { + } +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs new file mode 100644 index 0000000000..0faba189ad --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +/// +/// Represents a single-plane, 2D raster image of type . +/// +internal sealed class JxlImageB : JxlPlane +{ + public JxlImageB() + { + } + + public JxlImageB(int width, int height) + : base(width, height) + { + } + + public JxlImageB(Configuration configuration, int xSize, int ySize, int prePadding = 0) + : base(xSize, ySize) + => this.Allocate(configuration, prePadding); + + public Memory GetRowBytesMemory(int y) + { + Debug.Assert(y < this.YSize, "Attempted to access out-of-bounds Y coordinate"); + + Memory row = this.Bytes[(y * this.BytesPerRow)..]; + + return row; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageF.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageF.cs new file mode 100644 index 0000000000..6810e87df9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageF.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +/// +/// Represents a single-plane, 2D raster image of type . +/// +internal sealed class JxlImageF : JxlPlane +{ + public JxlImageF() + { + } + + public JxlImageF(int width, int height) + : base(width, height) + { + } + + public JxlImageF(Configuration configuration, int xSize, int ySize, int prePadding = 0) + : base(xSize, ySize) + => this.Allocate(configuration, prePadding); +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageI.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageI.cs new file mode 100644 index 0000000000..0261ffd63c --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageI.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +/// +/// Represents a single-plane, 2D raster image of type . +/// +internal sealed class JxlImageI : JxlPlane +{ + public JxlImageI() + { + } + + public JxlImageI(int width, int height) + : base(width, height) + { + } + + public JxlImageI(Configuration configuration, int xSize, int ySize, int prePadding = 0) + : base(xSize, ySize) + => this.Allocate(configuration, prePadding); +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageS.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageS.cs new file mode 100644 index 0000000000..b53716f12f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageS.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +/// +/// Represents a single-plane, 2D raster image of type . +/// +internal sealed class JxlImageS : JxlPlane +{ + public JxlImageS() + { + } + + public JxlImageS(int width, int height) + : base(width, height) + { + } + + public JxlImageS(Configuration configuration, int xSize, int ySize, int prePadding = 0) + : base(xSize, ySize) + => this.Allocate(configuration, prePadding); +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageSB.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageSB.cs new file mode 100644 index 0000000000..355c50785c --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageSB.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +/// +/// Represents a single-plane, 2D raster image of type . +/// +internal sealed class JxlImageSB : JxlPlane +{ + public JxlImageSB() + { + } + + public JxlImageSB(int width, int height) + : base(width, height) + { + } + + public JxlImageSB(Configuration configuration, int xSize, int ySize, int prePadding = 0) + : base(xSize, ySize) + => this.Allocate(configuration, prePadding); +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageU.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageU.cs new file mode 100644 index 0000000000..d41669e4d1 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageU.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +/// +/// Represents a single-plane, 2D raster image of type . +/// +internal sealed class JxlImageU : JxlPlane +{ + public JxlImageU() + { + } + + public JxlImageU(int width, int height) + : base(width, height) + { + } + + public JxlImageU(Configuration configuration, int xSize, int ySize, int prePadding = 0) + : base(xSize, ySize) + => this.Allocate(configuration, prePadding); +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs new file mode 100644 index 0000000000..37707b7ba4 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs @@ -0,0 +1,95 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory; + +// NOTE: Do not seal this class. +internal class JxlImage3 : IDisposable + where T : unmanaged +{ + private const int PlaneCount = 3; + + private JxlPlane[] planes = new JxlPlane[3]; + + public JxlImage3() + { + } + + public JxlImage3(JxlImage3 other) + { + for (int i = 0; i < PlaneCount; i++) + { + this.planes[i] = other.planes[i]; + } + } + + public int XSize => this.planes[0].XSize; + + public int YSize => this.planes[0].YSize; + + public int BytesPerRow => this.planes[0].BytesPerRow; + + public int PixelsPerRow => this.planes[0].PixelsPerRow; + + public Span PlaneRow(int plane, int row) + { + this.PlaneRowBoundsCheck(plane, row); + + int rowOffset = row * this.planes[0].BytesPerRow; + Span rowSpan = MemoryMarshal.Cast(this.planes[plane].BytesSpan[rowOffset..]); + + return rowSpan; + } + + public JxlPlane Plane(int index) => this.planes[index]; + + public void Swap(JxlImage3 other) + { + for (int i = 0; i < PlaneCount; i++) + { + other.planes[i].Swap(this.planes[i]); + } + } + + public static JxlImage3 Create(Configuration configuration, int xSize, int ySize) + { + JxlPlane plane0 = JxlPlane.Create(configuration, xSize, ySize); + JxlPlane plane1 = JxlPlane.Create(configuration, xSize, ySize); + JxlPlane plane2 = JxlPlane.Create(configuration, xSize, ySize); + + return new JxlImage3() + { + planes = [plane0, plane1, plane2] + }; + } + + public bool ShrinkTo(int x, int y) + { + for (int i = 0; i < PlaneCount; i++) + { + if (!this.planes[i].ShrinkTo(x, y)) + { + return false; + } + } + + return true; + } + + [Conditional("DEBUG")] + private void PlaneRowBoundsCheck(int c, int y) => + Debug.Assert(c < PlaneCount && y < this.YSize, "The bounds check has failed"); + + public void Dispose() + { + foreach (JxlPlane plane in this.planes) + { + plane.Dispose(); + } + + GC.SuppressFinalize(this); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs new file mode 100644 index 0000000000..9948ae790f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs @@ -0,0 +1,115 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory; + +// NOTE: Do not seal this type. +internal class JxlPlaneBase : IDisposable +{ + private IMemoryOwner? bytes; + + public JxlPlaneBase(int xSize, int ySize, int sizeOfT) + { + this.XSize = xSize; + this.YSize = ySize; + this.OriginalXSize = xSize; + this.OriginalYSize = ySize; + this.BytesPerRow = 0; + this.Size = sizeOfT; + } + + public JxlPlaneBase() + : this(0, 0, 0) + { + } + + public int BytesPerRow { get; private set; } + + public int XSize { get; private set; } + + public int YSize { get; private set; } + + public Memory Bytes => +#if DEBUG + this.bytes?.Memory ?? throw new InvalidOperationException("Bytes are missing"); +#else + return this.bytes!.Memory; +#endif + + public Span BytesSpan => this.Bytes.Span; + + protected int Size { get; set; } + + protected int OriginalXSize { get; set; } + + protected int OriginalYSize { get; set; } + + public bool Allocate(Configuration configuration, int prePadding) + { + if (this.bytes != null || this.BytesPerRow != 0) + { + return false; + } + + if (this.XSize == 0 || this.YSize == 0) + { + return true; + } + + int totalBytes = unchecked(this.YSize * this.BytesPerRow); + + this.bytes = configuration.MemoryAllocator.Allocate(totalBytes + (prePadding * this.Size)); + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ShrinkTo(int x, int y) + { + if (x <= this.OriginalXSize || y <= this.OriginalYSize) + { + return false; + } + + Debug.Assert(x <= this.OriginalXSize, "ShrinkTo cannot expand memory"); + Debug.Assert(y <= this.OriginalYSize, "ShrinkTo cannot expand memory"); + + this.XSize = x; + this.YSize = y; + + return true; + } + + protected Span GetRowBase(int y) + where T : unmanaged + { + Debug.Assert(y < this.YSize, "Attempted to access out-of-bounds Y coordinate"); + + Span row = this.Bytes.Span[(y * this.BytesPerRow)..]; + + return MemoryMarshal.Cast(row); + } + + protected void SetBytes(IMemoryOwner bytes) => this.bytes = bytes; + + public void Swap(JxlPlaneBase other) + { + (this.XSize, other.XSize) = (other.XSize, this.XSize); + (this.YSize, other.YSize) = (other.YSize, this.YSize); + (this.OriginalXSize, other.OriginalXSize) = (other.OriginalXSize, this.OriginalXSize); + (this.OriginalYSize, other.OriginalYSize) = (other.OriginalYSize, this.OriginalYSize); + (this.BytesPerRow, other.BytesPerRow) = (other.BytesPerRow, this.BytesPerRow); + (this.bytes, other.bytes) = (other.bytes, this.bytes); + } + + public void Dispose() + { + this.bytes?.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs new file mode 100644 index 0000000000..6bb09c6002 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory; + +// NOTE: Do not seal this class. +internal class JxlPlane : JxlPlaneBase + where T : unmanaged +{ + public JxlPlane() + { + } + + public unsafe JxlPlane(int width, int height) + : base(width, height, sizeof(T)) + { + } + + public unsafe int PixelsPerRow => this.BytesPerRow / sizeof(T); + + public static JxlPlane Create(Configuration configuration, int xSize, int ySize, int prePadding = 0) + { + JxlPlane plane = new(xSize, ySize); + + bool allocated = plane.Allocate(configuration, prePadding); + + if (!allocated) + { + throw new InvalidOperationException("Failed to allocate a JPEG XL plane"); + } + + return plane; + } + + public Span GetRow(int y) => this.GetRowBase(y); +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlSpanHelper.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlSpanHelper.cs new file mode 100644 index 0000000000..fcf9b61aa5 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlSpanHelper.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory; + +internal static class JxlSpanHelper +{ + public static T NthElement(this Span span, int n) + where T : IComparable + { + int left = 0; + int right = span.Length - 1; + + while (true) + { + int pivotIndex = Partition(span, left, right); + if (pivotIndex == n) + { + return span[pivotIndex]; + } + else if (n < pivotIndex) + { + right = pivotIndex - 1; + } + else + { + left = pivotIndex + 1; + } + } + } + + private static int Partition(Span span, int left, int right) + where T : IComparable + { + T pivot = span[right]; + int storeIndex = left; + + for (int i = left; i < right; i++) + { + if (span[i].CompareTo(pivot) < 0) + { + (span[i], span[storeIndex]) = (span[storeIndex], span[i]); + storeIndex++; + } + } + + (span[storeIndex], span[right]) = (span[right], span[storeIndex]); + return storeIndex; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/IJxlDctAcImage.cs b/src/ImageSharp/Formats/Jxl/Processing/IJxlDctAcImage.cs new file mode 100644 index 0000000000..30969824cd --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/IJxlDctAcImage.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Base DCT AC coefficient image +/// +internal interface IJxlDctAcImage +{ + /// + /// Gets the bit width of AC coefficients + /// + public JxlDctAcType Type { get; } + + /// + /// Gets the number of pixels per row. + /// + public int PixelsPerRow { get; } + + /// + /// Gets a value indicating whether the image is empty and doesn't + /// have anything within. + /// + public bool IsEmpty { get; } + + /// + /// Returns a reference to the coefficients at the specified row. + /// + /// The plane index (which channel). + /// The row index within plane specified by . + /// The X offset at the specified row. + /// + /// A reference to the coefficients inside the channel + /// specified by index , at index of + /// the row specified by , with X offset + /// specified by . + /// + public JxlDctAcPointer GetPlaneRow(int channel, int y, int xBase = 0); + + /// + /// Returns a reference to the coefficients at the specified row. + /// + /// The plane index (which channel). + /// The row index within plane specified by . + /// The X offset at the specified row. + /// + /// A reference to the coefficients inside the channel + /// specified by index , at index of + /// the row specified by , with X offset + /// specified by . + /// + /// + /// This is a read-only kind of . + /// + public JxlDctReadOnlyAcPointer GetReadOnlyPlaneRow(int channel, int y, int xBase = 0); + + /// + /// Fills all planes with zero. + /// + public void Clear(); + + /// + /// Fills everything within the specified plane with zero. + /// + /// Desired index of the plane. + public void Clear(int plane = 0); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs new file mode 100644 index 0000000000..88c1e88f7b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +#pragma warning disable SA1405 // Debug.Assert should provide message text + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// AC context +/// +internal static class JxlAcContext +{ + public const int DctOrderContextStart = 0; + public const int NonZeroBuckets = 37; + public const int ZeroDensityContextCount = 458; + public const int ZeroDensityContextLimit = 474; + + public static ReadOnlySpan CoefficientFrequencyContext => + [ + 0xBAD, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, + 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, + 27, 27, 27, 27, 28, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30, + ]; + + public static ReadOnlySpan CoefficientNumNonzeroContext => + [ + 0xBAD, 0, 31, 62, 62, 93, 93, 93, 93, 123, 123, 123, 123, + 152, 152, 152, 152, 152, 152, 152, 152, 180, 180, 180, 180, 180, + 180, 180, 180, 180, 180, 180, 180, 206, 206, 206, 206, 206, 206, + 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, + 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, + ]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ZeroDensityContext(int nonZeroesLeft, int k, int coveredBlocks, int log2CoveredBlocks, int prev) + { + Debug.Assert((1 << log2CoveredBlocks) == coveredBlocks); + + nonZeroesLeft = (nonZeroesLeft + coveredBlocks - 1) >> log2CoveredBlocks; + k >>= log2CoveredBlocks; + + Debug.Assert(k > 0); + Debug.Assert(k < 64); + Debug.Assert(nonZeroesLeft > 0); + Debug.Assert(nonZeroesLeft < 64); + + return ((CoefficientNumNonzeroContext[nonZeroesLeft] + CoefficientFrequencyContext[k]) * 2) + prev; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs new file mode 100644 index 0000000000..84b2462591 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs @@ -0,0 +1,188 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using static SixLabors.ImageSharp.Formats.Jxl.JxlFrameDimensions; + +#pragma warning disable SA1405 // Debug.Assert should provide message text + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +[StructLayout(LayoutKind.Sequential, Pack = 8)] +internal struct JxlAcStrategy +{ + public const int MaximumCoefficientBlocks = 32; + public const int MaximumBlockDimension = BlockDimensions * MaximumCoefficientBlocks; + public const int MaximumCoefficientArea = MaximumBlockDimension * MaximumBlockDimension; + public const int NumberOfValidStrategies = 27; + + private static readonly int MultiblockBits = + GetTypeBit(JxlAcStrategyType.DCT16X16) | GetTypeBit(JxlAcStrategyType.DCT32X32) | + GetTypeBit(JxlAcStrategyType.DCT16X8) | GetTypeBit(JxlAcStrategyType.DCT8X16) | + GetTypeBit(JxlAcStrategyType.DCT32X8) | GetTypeBit(JxlAcStrategyType.DCT8X32) | + GetTypeBit(JxlAcStrategyType.DCT16X32) | GetTypeBit(JxlAcStrategyType.DCT32X16) | + GetTypeBit(JxlAcStrategyType.DCT32X64) | GetTypeBit(JxlAcStrategyType.DCT64X32) | + GetTypeBit(JxlAcStrategyType.DCT64X64) | GetTypeBit(JxlAcStrategyType.DCT64X128) | + GetTypeBit(JxlAcStrategyType.DCT128X64) | + GetTypeBit(JxlAcStrategyType.DCT128X128) | + GetTypeBit(JxlAcStrategyType.DCT128X256) | + GetTypeBit(JxlAcStrategyType.DCT256X128) | + GetTypeBit(JxlAcStrategyType.DCT256X256); + + private readonly bool isFirst; + + public JxlAcStrategy(JxlAcStrategyType strategy, bool isFirst) + { + this.Strategy = strategy; + this.isFirst = isFirst; + + Debug.Assert(this.IsMultiblock); + } + + public JxlAcStrategy(JxlAcStrategyType strategy) + : this(strategy, true) + { + } + + public JxlAcStrategy(int rawStrategy) + : this((JxlAcStrategyType)rawStrategy) + { + } + + private static ReadOnlySpan CoveredBlocksXLookup => + [ + 1, 1, 1, 1, 2, 4, 1, 2, 1, + 4, 2, 4, 1, 1, 1, 1, 1, 1, + 8, 4, 8, 16, 8, 16, 32, 16, 32 + ]; + + private static ReadOnlySpan CoveredBlocksYLookup => + [ + 1, 1, 1, 1, 2, 4, 2, 1, 4, + 1, 4, 2, 1, 1, 1, 1, 1, 1, + 8, 8, 4, 16, 16, 8, 32, 32, 16 + ]; + + private static ReadOnlySpan Log2CoveredBlocksLookup => + [ + 0, 0, 0, 0, 2, 4, 1, 1, 2, + 2, 3, 3, 0, 0, 0, 0, 0, 0, + 6, 5, 5, 8, 7, 7, 10, 9, 9 + ]; + + public readonly bool IsMultiblock => ((1 << (int)this.Strategy) & MultiblockBits) != 0; + + public readonly int RawStrategy => (int)this.Strategy; + + public readonly int CoveredBlocksX => CoveredBlocksXLookup[(int)this.Strategy]; + + public readonly int CoveredBlocksY => CoveredBlocksYLookup[(int)this.Strategy]; + + public readonly int Log2CoveredBlocks => Log2CoveredBlocksLookup[(int)this.Strategy]; + + public readonly bool IsFirstBlock => this.isFirst; + + public readonly JxlAcStrategyType Strategy { get; } + + public void ComputeNaturalCoefficientOrder(ref int order) => CoefficientOrderAndLookup(this, false, ref order); + + public void ComputeNaturalCoefficientOrderLookup(ref int lookup) => CoefficientOrderAndLookup(this, true, ref lookup); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetTypeBit(JxlAcStrategyType type) => 1 << (int)type; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsRawStrategyValid(int rawStrategy) => rawStrategy is < NumberOfValidStrategies and >= 0; + + private static void CoefficientOrderAndLookup(JxlAcStrategy strategy, bool isLookup, Span output) + { + // TODO: CoefficientLayout + // TODO: CeilLog2Nonzero + int cx = strategy.CoveredBlocksX; + int cy = strategy.CoveredBlocksY; + + CoefficientLayout(ref cx, ref cy); + + int xs = cx / cy; + int xsm = xs - 1; + int xss = CeilLog2Nonzero(xs); + int cur = cx * cy; + + for (int i = 0; i < cx * BlockDimensions; i++) + { + for (int j = 0; j <= i; j++) + { + int x = j; + int y = i - j; + + if ((i & 1) == 0) + { + // swap + (x, y) = (y, x); + } + + if ((y & xsm) != 0) + { + continue; + } + + y >>= xss; + int value = 0; + + if (x < cx && y < cy) + { + value = (y * cx) + x; + } + else + { + value = cur++; + } + + if (isLookup) + { + output[((y * cx) * BlockDimensions) + x] = value; + } + else + { + output[value] = ((y * cx) * BlockDimensions) + x; + } + } + } + + for (int ip = (cx * BlockDimensions) - 1; ip > 0; ip--) + { + int i = ip - 1; + + for (int j = 0; j <= i; j++) + { + int x = (cx * BlockDimensions) - 1 - (i - j); + int y = (cx * BlockDimensions) - 1 - j; + + if ((i & 1) != 0) + { + // swap + (x, y) = (y, x); + } + + if ((y & xsm) != 0) + { + continue; + } + + y >>= xss; + int value = cur++; + + if (isLookup) + { + output[((y * cx) * BlockDimensions) + x] = value; + } + else + { + output[value] = ((y * cx) * BlockDimensions) + x; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs new file mode 100644 index 0000000000..6070db54c9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs @@ -0,0 +1,119 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlAcStrategyImage : IDisposable +{ + private const byte Invalid = byte.MaxValue; // 255 + + private JxlImageB? layers; + private Memory row; + private int stride; + + public int XSize => this.layers!.XSize; + + public int YSize => this.layers!.YSize; + + public int PixelsPerRow => this.layers!.PixelsPerRow; + + public JxlAcStrategyRow GetRow(int y, int xPrefix = 0) + { + ReadOnlyMemory layerRow = this.layers!.GetRowBytesMemory(y); + ReadOnlyMemory row = layerRow[xPrefix..]; + + return new JxlAcStrategyRow(row); + } + + public static JxlAcStrategyImage Create(Configuration memoryManager, int xSize, int ySize) + { + JxlAcStrategyImage image = new() + { + layers = new JxlImageB(memoryManager, xSize, ySize) + }; + + image.row = image.layers.GetRowBytesMemory(0); + image.stride = image.layers.PixelsPerRow; + + return image; + } + + public int CountBlocks(JxlAcStrategyType type) + { + int value = 0; + int compare = ((int)type << 1) | 1; + + for (int y = 0; y < this.layers!.YSize; y++) + { + ReadOnlySpan row = this.layers!.GetRow(y); + + for (int x = 0; x < this.layers!.XSize; x++) + { + if (row[x] == compare) + { + value++; + } + } + } + + return value; + } + + public JxlAcStrategyRow GetRow(in Rectangle rect, int y) => this.GetRow(rect.Y + y, rect.X); + + public bool IsValid(int x, int y) => this.row.Span[(y * this.stride) + x] != Invalid; + + public bool SetNoBoundsChecks(int x, int y, JxlAcStrategyType type, bool check = true) + { + JxlAcStrategy strategy = new(type); + Span rowSpan = this.row.Span; + int rawType = (int)type; + int rawTypeTimes2 = rawType << 1; + + for (int iy = 0; iy < strategy.CoveredBlocksX; iy++) + { + for (int ix = 0; ix < strategy.CoveredBlocksX; ix++) + { + int pos = ((y + iy) * this.stride) + x + ix; + + if (check && rowSpan[pos] != Invalid) + { + Debug.Fail("Invalid AC strategy. Blocks overlap."); + + return false; + } + + rowSpan[pos] = (byte)(rawTypeTimes2 | ((iy | ix) == 0 ? 1 : 0)); + } + } + + return true; + } + + public bool Set(int x, int y, JxlAcStrategyType type) + { +#if DEBUG + JxlAcStrategy strategy = new(type); + + Debug.Assert(y + strategy.CoveredBlocksY <= this.layers!.YSize, "Invalid range"); + Debug.Assert(x + strategy.CoveredBlocksX <= this.layers.XSize, "Invalid range"); +#endif + + return this.SetNoBoundsChecks(x, y, type, check: false); + } + + public void FillDct8(in Rectangle rect) => this.FillPlane(((int)JxlAcStrategyType.DCT << 1) | 1, this.layers, in rect); + + public void FillDct8() => this.FillDct8(in this.layers.GetRectangle()); + + public void FillInvalid() => this.FillImage(Invalid, this.layers); + + public void Dispose() + { + this.layers?.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs new file mode 100644 index 0000000000..a99b3654ac --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlAcStrategyRow +{ + private readonly ReadOnlyMemory row; + + public JxlAcStrategyRow(ReadOnlyMemory row) => this.row = row; + + public JxlAcStrategy this[int x] + { + get + { + ReadOnlySpan span = this.row.Span; + + Debug.Assert(x * 8 < span.Length, "Too many bytes of memory were requested"); + + ref byte first = ref MemoryMarshal.GetReference(span); + JxlAcStrategyType strategy = (JxlAcStrategyType)(Unsafe.Add(ref Unsafe.As(ref first), x) >> 1); + bool isFirst = Unsafe.Add(ref first, x) != 0; + + return new JxlAcStrategy(strategy, isFirst); + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyType.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyType.cs new file mode 100644 index 0000000000..e1b935fa2b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyType.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal enum JxlAcStrategyType : ushort +{ + // Regular block size DCT + DCT = 0, + + // Encode pixels without transforming + IDENTITY = 1, + + // Use 2-by-2 DCT + DCT2X2 = 2, + + // Use 4-by-4 DCT + DCT4X4 = 3, + + // Use 16-by-16 DCT + DCT16X16 = 4, + + // Use 32-by-32 DCT + DCT32X32 = 5, + + // Use 16-by-8 DCT + DCT16X8 = 6, + + // Use 8-by-16 DCT + DCT8X16 = 7, + + // Use 32-by-8 DCT + DCT32X8 = 8, + + // Use 8-by-32 DCT + DCT8X32 = 9, + + // Use 32-by-16 DCT + DCT32X16 = 10, + + // Use 16-by-32 DCT + DCT16X32 = 11, + + // 4x8 and 8x4 DCT + DCT4X8 = 12, + DCT8X4 = 13, + + // Corner-DCT. + AFV0 = 14, + + AFV1 = 15, + AFV2 = 16, + AFV3 = 17, + + // Larger DCTs + DCT64X64 = 18, + DCT64X32 = 19, + DCT32X64 = 20, + + // No transforms smaller than 64x64 are allowed below. + DCT128X128 = 21, + DCT128X64 = 22, + DCT64X128 = 23, + DCT256X256 = 24, + DCT256X128 = 25, + DCT128X256 = 26 +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingInputLayer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingInputLayer.cs new file mode 100644 index 0000000000..4b49e32c11 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingInputLayer.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlAlphaBlendingInputLayer +{ + public ReadOnlyMemory R { get; set; } + + public ReadOnlyMemory G { get; set; } + + public ReadOnlyMemory B { get; set; } + + public ReadOnlyMemory A { get; set; } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs new file mode 100644 index 0000000000..c5db8cb8a7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlAlphaBlendingOutput +{ + public Memory R { get; set; } + + public Memory G { get; set; } + + public Memory B { get; set; } + + public Memory A { get; set; } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs new file mode 100644 index 0000000000..641205ab21 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs @@ -0,0 +1,192 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlAlphaHelper +{ + // TODO: SIMD support + private const float SmallAlpha = 1f / (1 << 26); + + // Force x to stay within the range of 0 through 1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float Clamp(float x) => Math.Clamp(x, 0f, 1f); + + public static void PerformAlphaBlending( + JxlAlphaBlendingInputLayer background, + JxlAlphaBlendingInputLayer foreground, + JxlAlphaBlendingOutput output, + int pixelCount, + bool alphaIsPremultiplied, + bool clamp) + { + // Store all channels from all parameters into spans, because + // creating a Span from Memory is expensive, especially + // in a loop. + ReadOnlySpan fgR = foreground.R.Span; + ReadOnlySpan fgG = foreground.G.Span; + ReadOnlySpan fgB = foreground.B.Span; + ReadOnlySpan fgA = foreground.A.Span; + + ReadOnlySpan bgR = background.R.Span; + ReadOnlySpan bgG = background.G.Span; + ReadOnlySpan bgB = background.B.Span; + ReadOnlySpan bgA = background.A.Span; + + Span outR = output.R.Span; + Span outG = output.G.Span; + Span outB = output.B.Span; + Span outA = output.A.Span; + + if (alphaIsPremultiplied) + { + for (int x = 0; x < pixelCount; x++) + { + float fga = clamp ? Clamp(fgA[x]) : fgA[x]; + outR[x] = fgR[x] + (bgR[x] * (1f - fga)); + outG[x] = fgG[x] + (bgG[x] * (1f - fga)); + outB[x] = fgB[x] + (bgB[x] * (1f - fga)); + outA[x] = 1f - ((1f - fga) * (1f - bgA[x])); + } + } + else + { + for (int x = 0; x < pixelCount; x++) + { + float fga = clamp ? Clamp(fgA[x]) : fgA[x]; + float newA = 1f - ((1f - fga) * (1f - bgA[x])); + float rnewA = newA > 0 ? 1f / newA : 0f; + outR[x] = ((fgR[x] * fga) + (bgR[x] * bgA[x] * (1f - fga))) * rnewA; + outG[x] = ((fgG[x] * fga) + (bgG[x] * bgA[x] * (1f - fga))) * rnewA; + outB[x] = ((fgB[x] * fga) + (bgB[x] * bgA[x] * (1f - fga))) * rnewA; + outA[x] = newA; + } + } + } + + public static void PerformAlphaBlending( + ReadOnlySpan bg, + ReadOnlySpan bga, + ReadOnlySpan fg, + ReadOnlySpan fga, + Span output, + int pixelCount, + bool alphaIsPremultiplied, + bool clamp) + { + if (bg == bga && fg == fga) + { + for (int x = 0; x < pixelCount; x++) + { + float fa = clamp ? Clamp(fga[x]) : fga[x]; + output[x] = 1f - ((1f - fa) * (1f - bga[x])); + } + } + else + { + if (alphaIsPremultiplied) + { + for (int x = 0; x < pixelCount; x++) + { + float fa = clamp ? Clamp(fga[x]) : fga[x]; + output[x] = fg[x] + (bg[x] * (1f - fa)); + } + } + else + { + for (int x = 0; x < pixelCount; x++) + { + float fa = clamp ? Clamp(fga[x]) : fga[x]; + float new_a = 1f - ((1f - fa) * (1f - bga[x])); + float rnew_a = new_a > 0 ? 1f / new_a : 0f; + output[x] = ((fg[x] * fa) + (bg[x] * bga[x] * (1f - fa))) * rnew_a; + } + } + } + } + + public static void PerformAlphaWeightedAdd( + ReadOnlySpan bg, + ReadOnlySpan fg, + ReadOnlySpan fga, + Span output, + int pixelCount, + bool clamp) + { + if (fg == fga) + { + bg[pixelCount..].CopyTo(output); + } + else if (clamp) + { + for (int x = 0; x < pixelCount; x++) + { + output[x] = bg[x] + (fg[x] * Clamp(fga[x])); + } + } + else + { + for (int x = 0; x < pixelCount; ++x) + { + output[x] = bg[x] + (fg[x] * fga[x]); + } + } + } + + public static void PerformMultiplyBlending( + ReadOnlySpan bg, + ReadOnlySpan fg, + Span output, + int pixelCount, + bool clamp) + { + if (clamp) + { + for (int x = 0; x < pixelCount; x++) + { + output[x] = bg[x] * Clamp(fg[x]); + } + } + else + { + for (int x = 0; x < pixelCount; x++) + { + output[x] = bg[x] * fg[x]; + } + } + } + + public static void PremultiplyAlpha( + Span r, + Span g, + Span b, + ReadOnlySpan a, + int pixelCount) + { + for (int x = 0; x < pixelCount; x++) + { + float multiplier = Math.Max(SmallAlpha, a[x]); + r[x] *= multiplier; + g[x] *= multiplier; + b[x] *= multiplier; + } + } + + public static void UnpremultiplyAlpha( + Span r, + Span g, + Span b, + ReadOnlySpan a, + int pixelCount) + { + for (int x = 0; x < pixelCount; x++) + { + float multiplier = 1f / Math.Max(SmallAlpha, a[x]); + r[x] *= multiplier; + g[x] *= multiplier; + b[x] *= multiplier; + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs new file mode 100644 index 0000000000..2642a2295f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlBlockContextMap +{ + public JxlBlockContextMap() + { + DefaultContextMap.CopyTo(this.ContextMap); + this.ContextCount = this.ContextMap.Max() + 1; + this.DcContextCount = 1; + } + + private static ReadOnlySpan DefaultContextMap => + [ + 0, 1, 2, 2, 3, 3, 4, 5, 6, 6, 6, 6, 6, + 7, 8, 9, 9, 10, 11, 12, 13, 14, 14, 14, 14, 14, + 7, 8, 9, 9, 10, 11, 12, 13, 14, 14, 14, 14, 14, + ]; + + public List[] DcThresholds { get; } = [[], [], []]; + + public List QfThresholds { get; } = []; + + public byte[] ContextMap { get; } = new byte[DefaultContextMap.Length]; + + public int ContextCount { get; set; } + + public int DcContextCount { get; set; } + + public int AcContextCount => (this.ContextCount * JxlAcContext.NonZeroBuckets) + JxlAcContext.ZeroDensityContextCount; + + public int Context(int dcIndex, uint qf, int ord, int c) + { + int qfIndex = 0; + for (int i = 0; i < this.QfThresholds.Count; i++) + { + uint t = this.QfThresholds[i]; + + if (qf > t) + { + qfIndex++; + } + } + + int idx = c < 2 ? c ^ 1 : 2; + idx = (idx * JxlForwardCoefficientOrder.OrderCount) + ord; + idx = (idx * (this.QfThresholds.Count + 1)) + qfIndex; + idx = (idx * this.DcContextCount) + dcIndex; + return this.ContextMap[idx]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ZeroDensityContextOffset(int blockContext) => + (this.ContextCount * JxlAcContext.NonZeroBuckets) + JxlAcContext.ZeroDensityContextCount + blockContext; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int NonZeroContext(int nonZeroes, int blockContext) + { + if (nonZeroes >= 64) + { + nonZeroes = 64; + } + + int ctx = nonZeroes < 8 ? nonZeroes : (4 + (nonZeroes / 2)); + return (ctx * this.ContextCount) + blockContext; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs new file mode 100644 index 0000000000..2052daee6e --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlDctAcImage : IJxlDctAcImage, IDisposable + where T : unmanaged +{ + private readonly JxlImage3 image; + + public unsafe JxlDctAcImage(Configuration configuration, int width, int height) + { + DebugGuard.IsTrue(sizeof(T) is 2 or 4, "The type must be 2 or 4 bytes"); + + this.image = JxlImage3.Create(configuration, width, height); + } + + public unsafe JxlDctAcType Type => sizeof(T) == 4 ? JxlDctAcType.Ac32 : JxlDctAcType.Ac16; + + public int PixelsPerRow => this.image.PixelsPerRow; + + public bool IsEmpty => this.image.XSize == 0 || this.image.YSize == 0; + + public void Clear() => JxlImageOperations.ClearImage(this.image); + + public void Clear(int plane = 0) => JxlImageOperations.ClearImage(this.image); + + public unsafe JxlDctAcPointer GetPlaneRow(int channel, int y, int xBase = 0) + { + if (sizeof(T) == 4) + { + Span span = (this.image as JxlImage3)!.PlaneRow(channel, y)[xBase..]; + return new JxlDctAcPointer() { Pointer32 = span }; + } + else + { + Span span = (this.image as JxlImage3)!.PlaneRow(channel, y)[xBase..]; + return new JxlDctAcPointer() { Pointer16 = span }; + } + } + + public unsafe JxlDctReadOnlyAcPointer GetReadOnlyPlaneRow(int channel, int y, int xBase = 0) + { + if (sizeof(T) == 4) + { + ReadOnlySpan span = (this.image as JxlImage3)!.PlaneRow(channel, y)[xBase..]; + return new JxlDctReadOnlyAcPointer(span); + } + else + { + ReadOnlySpan span = (this.image as JxlImage3)!.PlaneRow(channel, y)[xBase..]; + return new JxlDctReadOnlyAcPointer(span); + } + } + + public void Dispose() + { + this.image.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcPointer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcPointer.cs new file mode 100644 index 0000000000..9710712d77 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcPointer.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Pointer to DCT AC coefficients +/// +internal ref struct JxlDctAcPointer() +{ + /// + /// 16-bit pointer (if any) + /// + public Span Pointer16 = []; + + /// + /// 32-bit pointer (if any) + /// + public Span Pointer32 = []; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcType.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcType.cs new file mode 100644 index 0000000000..a7e4ede67f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcType.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Bit size for DCT AC coefficient +/// +internal enum JxlDctAcType : byte +{ + /// + /// 16-bit coefficient + /// + Ac16, + + /// + /// 32-bit coefficient + /// + Ac32 +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctReadOnlyAcPointer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctReadOnlyAcPointer.cs new file mode 100644 index 0000000000..c80d8eaa64 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctReadOnlyAcPointer.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Pointer to DCT AC coefficients. (Read-only) +/// +internal readonly ref struct JxlDctReadOnlyAcPointer +{ + /// + /// 16-bit pointer (if any) + /// + public readonly ReadOnlySpan Pointer16; + + /// + /// 32-bit pointer (if any) + /// + public readonly ReadOnlySpan Pointer32; + + internal JxlDctReadOnlyAcPointer(ReadOnlySpan pointer16) => this.Pointer16 = pointer16; + + internal JxlDctReadOnlyAcPointer(ReadOnlySpan pointer32) => this.Pointer32 = pointer32; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs new file mode 100644 index 0000000000..84e7819bc5 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs @@ -0,0 +1,361 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Read-only cosine lookups for the Discrete Cosine Transform (DCT), +/// a mathematical function used for quantization. +/// +internal static class JxlDctScales +{ + /// + /// Gets 8x1 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales8_1 => + [ + 1.000000000000000000f + ]; + + /// + /// Gets 16x2 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales16_2 => + [ + 1.000000000000000000f, + 0.901764195028874394f, + ]; + + /// + /// Gets 32x4 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales32_4 => + [ + 1.000000000000000000f, + 0.974886821136879522f, + 0.901764195028874394f, + 0.787054918159101335f, + ]; + + /// + /// Gets 64x8 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales64_8 => + [ + 1.0000000000000000f, + 0.9936866130906366f, + 0.9748868211368796f, + 0.9440180941651672f, + 0.9017641950288744f, + 0.8490574973847023f, + 0.7870549181591013f, + 0.7171081282466044f, + ]; + + /// + /// Gets 128x16 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales128_16 => + [ + 1.0f, + 0.9984194528776054f, + 0.9936866130906366f, + 0.9858278282666936f, + 0.9748868211368796f, + 0.9609244059440204f, + 0.9440180941651672f, + 0.9242615922757944f, + 0.9017641950288744f, + 0.8766500784429904f, + 0.8490574973847023f, + 0.8191378932865928f, + 0.7870549181591013f, + 0.7529833816270532f, + 0.7171081282466044f, + 0.6796228528314651f, + ]; + + /// + /// Gets 256x32 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales256_32 => + [ + 1.0f, + 0.9996047255830407f, + 0.9984194528776054f, + 0.9964458326264695f, + 0.9936866130906366f, + 0.9901456355893141f, + 0.9858278282666936f, + 0.9807391980963174f, + 0.9748868211368796f, + 0.9682788310563117f, + 0.9609244059440204f, + 0.9528337534340876f, + 0.9440180941651672f, + 0.9344896436056892f, + 0.9242615922757944f, + 0.913348084400198f, + 0.9017641950288744f, + 0.8895259056651056f, + 0.8766500784429904f, + 0.8631544288990163f, + 0.8490574973847023f, + 0.8343786191696513f, + 0.8191378932865928f, + 0.8033561501721485f, + 0.7870549181591013f, + 0.7702563888779096f, + 0.7529833816270532f, + 0.7352593067735488f, + 0.7171081282466044f, + 0.6985543251889097f, + 0.6796228528314651f, + 0.6603391026591464f, + ]; + + /// + /// Gets 1x8 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales1_8 => + [ + 1.000000000000000000f + ]; + + /// + /// Gets 2x16 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales2_16 => + [ + 1.000000000000000000f, + 1.108937353592731823f, + ]; + + /// + /// Gets 4x32 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales4_32 => + [ + 1.000000000000000000f, + 1.025760096781116015f, + 1.108937353592731823f, + 1.270559368765487251f, + ]; + + /// + /// Gets 8x64 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales8_64 => + [ + 1.0000000000000000f, + 1.0063534990068217f, + 1.0257600967811158f, + 1.0593017296817173f, + 1.1089373535927318f, + 1.1777765381970435f, + 1.2705593687654873f, + 1.3944898413647777f, + ]; + + /// + /// Gets 16x128 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales16_128 => + [ + 1.0f, + 1.0015830492062623f, + 1.0063534990068217f, + 1.0143759095928793f, + 1.0257600967811158f, + 1.0406645869480142f, + 1.0593017296817173f, + 1.0819447744633812f, + 1.1089373535927318f, + 1.1407059950032632f, + 1.1777765381970435f, + 1.2207956782315876f, + 1.2705593687654873f, + 1.3280505578213306f, + 1.3944898413647777f, + 1.4714043176061107f, + ]; + + /// + /// Gets 32x256 DCT resample scales. + /// + public static ReadOnlySpan ResampleScales32_256 => + [ + 1.0f, + 1.0003954307206069f, + 1.0015830492062623f, + 1.0035668445360069f, + 1.0063534990068217f, + 1.009952439375063f, + 1.0143759095928793f, + 1.0196390660647288f, + 1.0257600967811158f, + 1.0327603660498115f, + 1.0406645869480142f, + 1.049501024072585f, + 1.0593017296817173f, + 1.0701028169146336f, + 1.0819447744633812f, + 1.0948728278734026f, + 1.1089373535927318f, + 1.124194353004584f, + 1.1407059950032632f, + 1.158541237256391f, + 1.1777765381970435f, + 1.1984966740820495f, + 1.2207956782315876f, + 1.244777922949508f, + 1.2705593687654873f, + 1.2982690107339132f, + 1.3280505578213306f, + 1.3600643892400104f, + 1.3944898413647777f, + 1.4315278911623237f, + 1.4714043176061107f, + 1.5143734423314616f, + ]; + + /// + /// Gets the DCT multiplier constants + /// + public static ReadOnlySpan Multipliers4 => + [ + 0.541196100146197f, + 1.3065629648763764f, + ]; + + /// + /// Gets the DCT multiplier constants + /// + public static ReadOnlySpan Multipliers8 => + [ + 0.5097955791041592f, + 0.6013448869350453f, + 0.8999762231364156f, + 2.5629154477415055f, + ]; + + /// + /// Gets the DCT multiplier constants + /// + public static ReadOnlySpan Multipliers16 => + [ + 0.5024192861881557f, 0.5224986149396889f, 0.5669440348163577f, + 0.6468217833599901f, 0.7881546234512502f, 1.060677685990347f, + 1.7224470982383342f, 5.101148618689155f, + ]; + + /// + /// Gets the DCT multiplier constants + /// + public static ReadOnlySpan Multipliers32 => + [ + 0.5006029982351963f, 0.5054709598975436f, 0.5154473099226246f, + 0.5310425910897841f, 0.5531038960344445f, 0.5829349682061339f, + 0.6225041230356648f, 0.6748083414550057f, 0.7445362710022986f, + 0.8393496454155268f, 0.9725682378619608f, 1.1694399334328847f, + 1.4841646163141662f, 2.057781009953411f, 3.407608418468719f, + 10.190008123548033f, + ]; + + /// + /// Gets the DCT multiplier constants + /// + public static ReadOnlySpan Multipliers64 => + [ + 0.500150636020651f, 0.5013584524464084f, 0.5037887256810443f, + 0.5074711720725553f, 0.5124514794082247f, 0.5187927131053328f, + 0.52657731515427f, 0.535909816907992f, 0.5469204379855088f, + 0.5597698129470802f, 0.57465518403266f, 0.5918185358574165f, + 0.6115573478825099f, 0.6342389366884031f, 0.6603198078137061f, + 0.6903721282002123f, 0.7251205223771985f, 0.7654941649730891f, + 0.8127020908144905f, 0.8683447152233481f, 0.9345835970364075f, + 1.0144082649970547f, 1.1120716205797176f, 1.233832737976571f, + 1.3892939586328277f, 1.5939722833856311f, 1.8746759800084078f, + 2.282050068005162f, 2.924628428158216f, 4.084611078129248f, + 6.796750711673633f, 20.373878167231453f, + ]; + + /// + /// Gets the DCT multiplier constants + /// + public static ReadOnlySpan Multipliers128 => + [ + 0.5000376519155477f, 0.5003390374428216f, 0.5009427176380873f, + 0.5018505174842379f, 0.5030651913013697f, 0.5045904432216454f, + 0.5064309549285542f, 0.5085924210498143f, 0.5110815927066812f, + 0.5139063298475396f, 0.5170756631334912f, 0.5205998663018917f, + 0.524490540114724f, 0.5287607092074876f, 0.5334249333971333f, + 0.538499435291984f, 0.5440022463817783f, 0.549953374183236f, + 0.5563749934898856f, 0.5632916653417023f, 0.5707305880121454f, + 0.5787218851348208f, 0.5872989370937893f, 0.5964987630244563f, + 0.606362462272146f, 0.6169357260050706f, 0.6282694319707711f, + 0.6404203382416639f, 0.6534518953751283f, 0.6674352009263413f, + 0.6824501259764195f, 0.6985866506472291f, 0.7159464549705746f, + 0.7346448236478627f, 0.7548129391165311f, 0.776600658233963f, + 0.8001798956216941f, 0.8257487738627852f, 0.8535367510066064f, + 0.8838110045596234f, 0.9168844461846523f, 0.9531258743921193f, + 0.9929729612675466f, 1.036949040910389f, 1.0856850642580145f, + 1.1399486751015042f, 1.2006832557294167f, 1.2690611716991191f, + 1.346557628206286f, 1.4350550884414341f, 1.5369941008524954f, + 1.6555965242641195f, 1.7952052190778898f, 1.961817848571166f, + 2.163957818751979f, 2.4141600002500763f, 2.7316450287739396f, + 3.147462191781909f, 3.7152427383269746f, 4.5362909369693565f, + 5.827688377844654f, 8.153848602466814f, 13.58429025728446f, + 40.744688103351834f, + ]; + + /// + /// Gets the DCT multiplier constants + /// + public static ReadOnlySpan Multipliers256 => + [ + 0.5000094125358878f, 0.500084723455784f, 0.5002354020255269f, + 0.5004615618093246f, 0.5007633734146156f, 0.5011410648064231f, + 0.5015949217281668f, 0.502125288230386f, 0.5027325673091954f, + 0.5034172216566842f, 0.5041797745258774f, 0.5050208107132756f, + 0.5059409776624396f, 0.5069409866925212f, 0.5080216143561264f, + 0.509183703931388f, 0.5104281670536573f, 0.5117559854927805f, + 0.5131682130825206f, 0.5146659778093218f, 0.516250484068288f, + 0.5179230150949777f, 0.5196849355823947f, 0.5215376944933958f, + 0.5234828280796439f, 0.52552196311921f, 0.5276568203859896f, + 0.5298892183652453f, 0.5322210772308335f, 0.5346544231010253f, + 0.537191392591309f, 0.5398342376841637f, 0.5425853309375497f, + 0.545447171055775f, 0.5484223888484947f, 0.551513753605893f, + 0.554724179920619f, 0.5580567349898085f, 0.5615146464335654f, + 0.5651013106696203f, 0.5688203018875696f, 0.5726753816701664f, + 0.5766705093136241f, 0.5808098529038624f, 0.5850978012111273f, + 0.58953897647151f, 0.5941382481306648f, 0.5989007476325463f, + 0.6038318843443582f, 0.6089373627182432f, 0.614223200800649f, + 0.6196957502119484f, 0.6253617177319102f, 0.6312281886412079f, + 0.6373026519855411f, 0.6435930279473415f, 0.6501076975307724f, + 0.6568555347890955f, 0.6638459418498757f, 0.6710888870233562f, + 0.6785949463131795f, 0.6863753486870501f, 0.6944420255086364f, + 0.7028076645818034f, 0.7114857693151208f, 0.7204907235796304f, + 0.7298378629074134f, 0.7395435527641373f, 0.749625274727372f, + 0.7601017215162176f, 0.7709929019493761f, 0.7823202570613161f, + 0.7941067887834509f, 0.8063772028037925f, 0.8191580674598145f, + 0.83247799080191f, 0.8463678182968619f, 0.860860854031955f, + 0.8759931087426972f, 0.8918035785352535f, 0.9083345588266809f, + 0.9256319988042384f, 0.9437459026371479f, 0.962730784794803f, + 0.9826461881778968f, 1.0035572754078206f, 1.0255355056139732f, + 1.048659411496106f, 1.0730154944316674f, 1.0986992590905857f, + 1.1258164135986009f, 1.1544842669978943f, 1.184833362908442f, + 1.217009397314603f, 1.2511754798461228f, 1.287514812536712f, + 1.326233878832723f, 1.3675662599582539f, 1.411777227500661f, + 1.459169302866857f, 1.5100890297227016f, 1.5649352798258847f, + 1.6241695131835794f, 1.6883285509131505f, 1.7580406092704062f, + 1.8340456094306077f, 1.9172211551275689f, 2.0086161135167564f, + 2.1094945286246385f, 2.22139377701127f, 2.346202662531156f, + 2.486267909203593f, 2.644541877144861f, 2.824791402350551f, + 3.0318994541759925f, 3.2723115884254845f, 3.5547153325075804f, + 3.891107790700307f, 4.298537526449054f, 4.802076008665048f, + 5.440166215091329f, 6.274908408039339f, 7.413566756422303f, + 9.058751453879703f, 11.644627325175037f, 16.300023088031555f, + 27.163977662448232f, 81.48784219222516f, + ]; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlForwardCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlForwardCoefficientOrder.cs new file mode 100644 index 0000000000..26a210e6fd --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlForwardCoefficientOrder.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal static class JxlForwardCoefficientOrder +{ + public const byte OrderCount = 13; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CoefficientRows(int rows, int columns) => rows < columns ? rows : columns; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CoefficientColumns(int rows, int columns) => rows < columns ? columns : rows; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void CoefficientLayout(ref int rows, ref int columns) + { + rows = CoefficientRows(rows, columns); + columns = CoefficientColumns(rows, columns); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs new file mode 100644 index 0000000000..095e936d32 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs @@ -0,0 +1,106 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal static class JxlLehmerCode +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ValueOfLowest1Bit(int n) => n & -n; + + public static bool ComputeLehmerCode(ReadOnlySpan permutation, Span temp, int n, Span code) + { + temp[(n + 1)..].Clear(); + + for (int idx = 0; idx < n; idx++) + { + int s = permutation[idx]; + + uint penalty = 0u; + uint i = (uint)s + 1u; + + while (i != 0u) + { + penalty += temp[(int)i]; + i &= i - 1u; // Clear lowest bit + } + + if (s < penalty) + { + return false; + } + + code[idx] = (uint)s - penalty; + i = (uint)s + 1u; + + while (i < n + 1u) + { + temp[(int)i]++; + i += (uint)ValueOfLowest1Bit((int)i); + } + } + + return true; + } + + public static bool DecodeLehmerCode(ReadOnlySpan code, Span temp, int n, Span permutation) + { + if (n == 0) + { + return false; + } + + int log2n = CeilLog2Nonzero(n); + int paddedN = 1 << log2n; + + for (int i = 0; i < paddedN; i++) + { + int i1 = i + 1; + temp[i] = (uint)ValueOfLowest1Bit(i1); + } + + for (int i = 0; i < n; i++) + { + if (code[i] + i >= n) + { + return false; + } + + uint rank = code[i] + 1; + + int bit = paddedN; + int next = 0; + + for (int b = 0; b <= log2n; b++) + { + int cand = next + bit; + + if (cand < 1) + { + return false; + } + + bit >>= 1; + + if (temp[cand - 1] < rank) + { + next = cand; + rank -= temp[cand - 1]; + } + } + + permutation[i] = next; + + next++; + while (next <= paddedN) + { + temp[next - 1]--; + next += ValueOfLowest1Bit(next); + } + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs new file mode 100644 index 0000000000..312138db43 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs @@ -0,0 +1,263 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// JPEG XL loop filter. +/// +internal sealed class JxlLoopFilter : IJxlFields +{ + private const int SigmaBorder = 1; + private const int SigmaPadding = 2; + + /// + /// 4 * (sqrt(0.5)-1), so that Weight(sigma) = 0.5 + /// + private const float InverseSigmaNum = -1.1715728752538099024f; + + /// + /// kInvSigmaNum / 0.3 + /// + private const float MinSigma = -3.90524291751269967465540850526868f; + + /// + /// Gets the number of EPF (Edge-preserving filter) sharp entries. + /// + public const int EpfSharpEntries = 8; + + /// + /// Gets or sets a value indicating whether gaborish + /// convolution is preferred. + /// + public bool UseGaborishConvolution { get; set; } + + /// + /// Gets or sets a value indicating whether custom + /// gaborish weights are used. + /// + public bool GaborishCustom { get; set; } + + /// + /// Gets or sets the first custom X gaborish weight. + /// + public float GaborishXWeight1 { get; set; } + + /// + /// Gets or sets the second custom X gaborish weight. + /// + public float GaborishXWeight2 { get; set; } + + /// + /// Gets or sets the first custom Y gaborish weight. + /// + public float GaborishYWeight1 { get; set; } + + /// + /// Gets or sets the second custom Y gaborish weight. + /// + public float GaborishYWeight2 { get; set; } + + /// + /// Gets or sets the first custom B gaborish weight. + /// + public float GaborishBWeight1 { get; set; } + + /// + /// Gets or sets the second custom B gaborish weight. + /// + public float GaborishBWeight2 { get; set; } + + /// + /// Gets or sets the number of EPF (Edge-preserving filter) steps. + /// 0 means EPF is disabled, 1 applies only the first stage, + /// 2 applies both stages and 3 applies the first stage twice + /// and the second stage once. + /// + public int EpfIterations { get; set; } + + /// + /// Gets or sets a value indicating whether custom EPF sharpness + /// is used. + /// + public bool EpfSharpCustom { get; set; } + + /// + /// Gets or sets a value with 8 elements representing EPF sharpness lookup tables (LUTs). + /// + public float[] EpfSharpLookup { get; set; } = new float[8]; + + /// + /// Gets or sets a value indicating whether custom EPF weights are used. + /// + public bool EpfCustomWeights { get; set; } + + /// + /// Gets or sets the relative weight of each channel. + /// + public float[] EpfChannelScale { get; set; } = new float[3]; + + /// + /// Gets or sets the value that represents the minimum weight for first pass. + /// + public float EpfPass1ZeroFlush { get; set; } + + /// + /// Gets or sets the value that represents the minimum weight for second pass. + /// + public float EpfPass2ZeroFlush { get; set; } + + /// + /// Gets or sets a value indicating whether custom sigma parameters are used for EPF. + /// + public bool EpfCustomSigma { get; set; } + + /// + /// Gets or sets the quant multiplier. + /// + public float EpfQuantMultiplier { get; set; } + + /// + /// Gets or sets the multiplier for sigma in pass 0. + /// + public float EpfPass0SigmaScale { get; set; } + + /// + /// Gets or sets the multiplier for sigma in pass 2. + /// + public float EpfPass2SigmaScale { get; set; } + + /// + /// Gets or sets the inverse multiplier for sigma on borders. + /// + public float EpfBorderSadMul { get; set; } + + /// + /// Gets or sets the EPF sigma for modular. + /// + // NOTE: This value is not documented by libjxl. + public float EpfSigmaForModular { get; set; } + + /// + /// Gets or sets the number of extensions. + /// + public long Extensions { get; set; } + + /// + /// Mirror n floats starting at *span and store them before span. + /// + private static void LeftMirror(Span span, int n) + { + ref float p = ref MemoryMarshal.GetReference(span); + for (int i = 0; i < n; i++) + { + Unsafe.Add(ref p, -1 - i) = span[i]; + } + } + + /// + /// Mirror n floats starting at *(span - n) and store them at *span. + /// + private static void RightMirror(Span span, int n) + { + ref float p = ref MemoryMarshal.GetReference(span); + for (int i = 0; i < n; i++) + { + span[i] = Unsafe.Add(ref p, -1 - i); + } + } + + public bool ComputeSigma(Rectangle blockRect, JxlPassesDecoderState state) + { + if (this.EpfIterations <= 0) + { + return false; + } + + JxlAcStrategyImage acStrategy = state.Shared.AcStrategy; + float quantScale = state.Shared.Quantizer.Scale; + + int sigmaStride = state.Sigma.PixelsPerRow; + int sharpnessStride = state.Shared.EpfSharpness.PixelsPerRow; + + for (int by = 0; by < blockRect.Height; by++) + { + Span sigmaRow = state.Sigma.GetRowSpan(by); + Span sharpnessRow = state.Shared.EpfSharpness.GetRowSpan(by); + JxlAcStrategyRow acsRow = acStrategy.GetRow(in blockRect, by); + Span rowQuant = state.Shared.RawQuantField.GetRow(by); + + for (int bx = 0; bx < blockRect.Width; bx++) + { + JxlAcStrategy acs = acsRow[bx]; + int llfX = acs.CoveredBlocksX; + + if (!acs.IsFirstBlock) + { + continue; + } + + float sigmaQuant = this.EpfQuantMultiplier / (quantScale * rowQuant[bx] * InverseSigmaNum); + + for (int iy = 0; iy < acs.CoveredBlocksY; iy++) + { + for (int ix = 0; ix < acs.CoveredBlocksY; ix++) + { + float sigma = sigmaQuant * this.EpfSharpLookup[sharpnessRow[bx + ix + iy + sharpnessStride]]; + sigma = MathF.Min(-1e-4f, sigma); + sigmaRow[bx + ix + SigmaPadding + ((iy + SigmaPadding) * sigmaStride)] = 1.0f / sigma; + } + } + + if (bx + blockRect.X == 0) + { + for (int iy = 0; iy < acs.CoveredBlocksY; iy++) + { + LeftMirror(sigmaRow[(SigmaPadding + ((iy + SigmaPadding) * sigmaStride))..], SigmaBorder); + } + } + + if (bx + blockRect.X + llfX == state.Shared.FrameDimensions.XSizeBlocks) + { + for (int iy = 0; iy < acs.CoveredBlocksY; iy++) + { + RightMirror(sigmaRow[(SigmaPadding + bx + llfX + ((iy + SigmaPadding) * sigmaStride))..], SigmaBorder); + } + } + + int offsetBefore = bx + blockRect.X == 0 ? 1 : bx + SigmaPadding; + int offsetAfter = bx + blockRect.X + llfX == state.Shared.FrameDimensions.XSizeBlocks + ? SigmaPadding + llfX + bx + SigmaBorder + : SigmaPadding + llfX + bx; + + int num = offsetAfter - offsetBefore; + + if (by + blockRect.Y == 0) + { + for (int iy = 0; iy < SigmaBorder; iy++) + { + sigmaRow.Slice(offsetBefore + ((SigmaPadding - 1 - iy) * sigmaStride), num) + .CopyTo(sigmaRow[(offsetBefore + ((SigmaPadding + iy) * sigmaStride))..]); + } + } + + if (by + blockRect.Y + acs.CoveredBlocksX == state.Shared.FrameDimensions.YSizeBloks) + { + for (int iy = 0; iy < SigmaBorder; iy++) + { + sigmaRow[(offsetBefore + (sigmaStride * (acs.CoveredBlocksX + SigmaPadding + iy)))..] + .CopyTo(sigmaRow[(offsetBefore + (sigmaStride * (acs.CoveredBlocksY + SigmaPadding - 1 - iy)))..]); + } + } + } + } + + return true; + } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3.cs new file mode 100644 index 0000000000..a01e2a6216 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3.cs @@ -0,0 +1,142 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +#pragma warning disable IDE0044 // Add readonly modifier +#pragma warning disable IDE0051 // Remove unused private members + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal struct JxlMatrix3x3 +{ + /// + /// Represents the matrix element at 0,0. + /// + private double e00; + + /// + /// Represents the matrix element at 0,1. + /// + private double e01; + + /// + /// Represents the matrix element at 0,2. + /// + private double e02; + + /// + /// Represents the matrix element at 1,0. + /// + private double e10; + + /// + /// Represents the matrix element at 1,1. + /// + private double e11; + + /// + /// Represents the matrix element at 1,2. + /// + private double e12; + + /// + /// Represents the matrix element at 2,0. + /// + private double e20; + + /// + /// Represents the matrix element at 2,1. + /// + private double e21; + + /// + /// Represents the matrix element at 2,2. + /// + private double e22; + + /// + /// Wraps all these values into a Span. + /// + /// A Span with all matrix elements. + public Span AsSpan() => MemoryMarshal.CreateSpan(ref this.e00, 9); + + /// + /// Wraps all these values into a ReadOnlySpan. + /// + /// A ReadOnlySpan with all matrix elements. + public ReadOnlySpan AsReadOnlySpan() => MemoryMarshal.CreateReadOnlySpan(ref this.e00, 9); + + public static void Multiply(in JxlMatrix3x3 a, in JxlMatrix3x3 b, ref JxlMatrix3x3 c) + { + ReadOnlySpan spanA = a.AsReadOnlySpan(); + ReadOnlySpan spanB = b.AsReadOnlySpan(); + Span spanC = c.AsSpan(); + + for (int row = 0; row < 3; row++) + { + int row3 = row * 3; + for (int col = 0; col < 3; col++) + { + double sum = 0d; + for (int k = 0; k < 3; k++) + { + sum += spanA[row3 + k] * spanB[(k * 3) + col]; + } + + spanC[row3 + col] = sum; + } + } + } + + public static void Multiply(in JxlMatrix3x3 a, ReadOnlySpan b, Span c) + { + ReadOnlySpan spanA = a.AsReadOnlySpan(); + + for (int row = 0; row < 3; row++) + { + double sum = 0f; + int row3 = row * 3; + for (int col = 0; col < 3; col++) + { + sum += spanA[row3 + col] * b[col]; + } + + c[row] = sum; + } + } + + public static bool Invert(ref JxlMatrix3x3 matrix) + { + ReadOnlySpan m = matrix.AsReadOnlySpan(); + Span temp = + [ + ((double)m[4] * m[8]) - ((double)m[5] * m[7]), + ((double)m[2] * m[7]) - ((double)m[1] * m[8]), + ((double)m[1] * m[5]) - ((double)m[2] * m[4]), + ((double)m[5] * m[6]) - ((double)m[3] * m[8]), + ((double)m[0] * m[8]) - ((double)m[2] * m[6]), + ((double)m[2] * m[3]) - ((double)m[0] * m[5]), + ((double)m[3] * m[7]) - ((double)m[4] * m[6]), + ((double)m[1] * m[6]) - ((double)m[0] * m[7]), + ((double)m[0] * m[4]) - ((double)m[1] * m[3]), + ]; + + double det = (m[0] * temp[0]) + (m[1] * temp[3]) + (m[2] * temp[6]); + + if (Math.Abs(det) < 1e-10) + { + return false; + } + + double idet = 1.0 / det; + Span spanM = matrix.AsSpan(); + + for (int i = 0; i < 9; i++) + { + spanM[i] = (double)(temp[i] * idet); + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs new file mode 100644 index 0000000000..0a2d59a90f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs @@ -0,0 +1,155 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +#pragma warning disable IDE0044 // Add readonly modifier +#pragma warning disable IDE0051 // Remove unused private members + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal struct JxlMatrix3x3F +{ + /// + /// Represents the matrix element at 0,0. + /// + private float e00; + + /// + /// Represents the matrix element at 0,1. + /// + private float e01; + + /// + /// Represents the matrix element at 0,2. + /// + private float e02; + + /// + /// Represents the matrix element at 1,0. + /// + private float e10; + + /// + /// Represents the matrix element at 1,1. + /// + private float e11; + + /// + /// Represents the matrix element at 1,2. + /// + private float e12; + + /// + /// Represents the matrix element at 2,0. + /// + private float e20; + + /// + /// Represents the matrix element at 2,1. + /// + private float e21; + + /// + /// Represents the matrix element at 2,2. + /// + private float e22; + + internal JxlMatrix3x3F(float[][] array) + { + this.e00 = array[0][0]; + this.e01 = array[0][1]; + this.e02 = array[0][2]; + this.e10 = array[1][0]; + this.e11 = array[1][1]; + this.e12 = array[1][2]; + this.e20 = array[2][0]; + this.e21 = array[2][1]; + this.e22 = array[2][2]; + } + + /// + /// Wraps all these values into a Span. + /// + /// A Span with all matrix elements. + public Span AsSpan() => MemoryMarshal.CreateSpan(ref this.e00, 9); + + /// + /// Wraps all these values into a ReadOnlySpan. + /// + /// A ReadOnlySpan with all matrix elements. + public ReadOnlySpan AsReadOnlySpan() => MemoryMarshal.CreateReadOnlySpan(ref this.e00, 9); + + public static void Multiply(in JxlMatrix3x3F a, in JxlMatrix3x3F b, ref JxlMatrix3x3F c) + { + ReadOnlySpan spanA = a.AsReadOnlySpan(); + ReadOnlySpan spanB = b.AsReadOnlySpan(); + Span spanC = c.AsSpan(); + + for (int row = 0; row < 3; row++) + { + int row3 = row * 3; + for (int col = 0; col < 3; col++) + { + float sum = 0f; + for (int k = 0; k < 3; k++) + { + sum += spanA[row3 + k] * spanB[(k * 3) + col]; + } + + spanC[row3 + col] = sum; + } + } + } + + public static void Multiply(in JxlMatrix3x3F a, ReadOnlySpan b, Span c) + { + ReadOnlySpan spanA = a.AsReadOnlySpan(); + + for (int row = 0; row < 3; row++) + { + float sum = 0f; + int row3 = row * 3; + for (int col = 0; col < 3; col++) + { + sum += spanA[row3 + col] * b[col]; + } + + c[row] = sum; + } + } + + public static bool Invert(ref JxlMatrix3x3F matrix) + { + ReadOnlySpan m = matrix.AsReadOnlySpan(); + Span temp = + [ + ((double)m[4] * m[8]) - ((double)m[5] * m[7]), + ((double)m[2] * m[7]) - ((double)m[1] * m[8]), + ((double)m[1] * m[5]) - ((double)m[2] * m[4]), + ((double)m[5] * m[6]) - ((double)m[3] * m[8]), + ((double)m[0] * m[8]) - ((double)m[2] * m[6]), + ((double)m[2] * m[3]) - ((double)m[0] * m[5]), + ((double)m[3] * m[7]) - ((double)m[4] * m[6]), + ((double)m[1] * m[6]) - ((double)m[0] * m[7]), + ((double)m[0] * m[4]) - ((double)m[1] * m[3]), + ]; + + double det = (m[0] * temp[0]) + (m[1] * temp[3]) + (m[2] * temp[6]); + + if (Math.Abs(det) < 1e-10) + { + return false; + } + + double idet = 1.0 / det; + Span spanM = matrix.AsSpan(); + + for (int i = 0; i < 9; i++) + { + spanM[i] = (float)(temp[i] * idet); + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseHelper.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseHelper.cs new file mode 100644 index 0000000000..f6432e24db --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseHelper.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal static class JxlNoiseHelper +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static JxlNoiseIndexAndFraction IndexAndFraction(float x) + { + const int scaleNumerator = JxlNoiseParameters.NoisePoints - 2; + const float scale = scaleNumerator / 1.0f; + + float scaledX = MathF.Max(0f, x * scale); + float floorX = MathF.Floor(scaledX); + float fractionalX = scaledX - floorX; + + if (scaledX >= scaleNumerator + 1) + { + floorX = scaleNumerator; + fractionalX = 1f; + } + + return new((int)floorX, fractionalX); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseIndexAndFraction.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseIndexAndFraction.cs new file mode 100644 index 0000000000..c3ec1437c8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseIndexAndFraction.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +[StructLayout(LayoutKind.Sequential)] +internal struct JxlNoiseIndexAndFraction(int index, float fraction) +{ + public int Index = index; + + public float Fraction = fraction; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseLevel.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseLevel.cs new file mode 100644 index 0000000000..d5feebd52b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseLevel.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +[StructLayout(LayoutKind.Sequential)] +internal struct JxlNoiseLevel(float noiseLevel, float intensity) +{ + public float NoiseLevel = noiseLevel; + + public float Intensity = intensity; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseParameters.cs new file mode 100644 index 0000000000..3f51f85379 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseParameters.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlNoiseParameters +{ + public const int NoisePoints = 8; + + public float[] Lookup { get; set; } = new float[NoisePoints]; + + public bool ContainsAny => this.Lookup.Any(x => MathF.Abs(x) > 1e-3f); + + public void Clear() => Array.Fill(this.Lookup, 0f); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs new file mode 100644 index 0000000000..322bcd1c64 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal static class JxlOpsinInverseParameters +{ + private const float M02 = 0.078f; + private const float M00 = 0.30f; + private const float M01 = 1.0f - M02 - M00; + + private const float M12 = 0.078f; + private const float M10 = 0.23f; + private const float M11 = 1.0f - M12 - M10; + + private const float M20 = 0.24342268924547819f; + private const float M21 = 0.20476744424496821f; + private const float M22 = 1.0f - M20 - M21; + + private static readonly float[][] Matrix = + [ + [M00, M01, M02], + [M10, M11, M12], + [M20, M21, M22] + ]; + + public static JxlMatrix3x3F GetOpsinAbsorbanceInverseMatrix() + { + JxlMatrix3x3F matrix = new(Matrix); + _ = JxlMatrix3x3F.Invert(ref matrix); + return matrix; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPackSigned.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPackSigned.cs new file mode 100644 index 0000000000..f54b77f1d3 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPackSigned.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Provides PackSigned and UnpackSigned methods. +/// +internal static class JxlPackSigned +{ + /// + /// Encodes non-negative (X) into (2 * X), negative (-X) into (2 * X - 1) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint PackUnsigned(int x) + { + unchecked + { + uint value = (uint)x; + return (value << 1) ^ ((~value >> 31) - 1); + } + } + + /// + /// Reverse to PackSigned, i.e. UnpackSigned(PackSigned(X)) == X. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int UnpackSigned(uint x) + { + unchecked + { + return (int)((x >> 1) ^ (((~x) & 1) - 1)); + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs new file mode 100644 index 0000000000..fa4dd20518 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Quantization mode. +/// +internal enum JxlQuantMode : byte +{ + Library, + Id, + Dct2, + Dct4, + Dct4x8, + Afv, + Dct, + Raw +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantTable.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantTable.cs new file mode 100644 index 0000000000..5fc8b49a8e --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantTable.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Specifies which quantization table to use depending on +/// transform & block type. +/// +internal enum JxlQuantTable : byte +{ + DCT = 0, + IDENTITY, + DCT2X2, + DCT4X4, + DCT16X16, + DCT32X32, + + // DCT16X8 + DCT8X16, + + // DCT32X8 + DCT8X32, + + // DCT32X16 + DCT16X32, + DCT4X8, + + // DCT8X4 + AFV0, + + // AFV1 + // AFV2 + // AFV3 + DCT64X64, + + // DCT64X32, + DCT32X64, + DCT128X128, + + // DCT128X64, + DCT64X128, + DCT256X256, + + // DCT256X128, + DCT128X256 +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs new file mode 100644 index 0000000000..5a1405b350 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs @@ -0,0 +1,394 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// The quantizer for DCT DC/AC coefficients. +/// +/// +/// The quantizer's primary role is to lower the value +/// of coefficients. For example, during encoding, +/// coefficients may be divided by 3 and have to be +/// multiplied by 3 at decoding (which is lossy). Quantization is often +/// useful for variable-length coding where the amount +/// of bits depend on how large the number is. +/// +internal sealed class JxlQuantizer +{ + /// + /// Denominator for the global_scale value. + /// + private const int GlobalScaleDenominator = 1 << 16; + + /// + /// Numerator for the global_scale value. + /// + private const int GlobalScaleNumerator = 4096; + + /// + /// Numerator for biases. + /// + private const float BiasNumerator = 0.145f; + + /// + /// The default value of the quant. + /// + private const int DefaultQuant = 64; + + /// + /// The maximum value for a quant. Quant cannot be greater than this - + /// if attempted to, it will be limited to this value. + /// + private const int MaxQuant = 256; + + /// + /// Represents the multipliers for the DC coefficients. + /// + private readonly float[] mulDc = new float[4]; + + /// + /// Represents the inverse multipliers for the DC coefficients. + /// + private readonly float[] inverseMulDc = new float[4]; + + /// + /// Global scale + /// + private int globalScale; + + /// + /// Quantizer DC + /// + private int quantDc; + + /// + /// Inverse global scale + /// + private float inverseGlobalScale; + + /// + /// Reciprocal of inverseGlobalScale + /// + private float globalScaleSingle; + + /// + /// Inverse quantizer DC + /// + private float inverseQuantDc; + + /// + /// The zero bias. + /// + private readonly float[] zeroBias = new float[3]; + + /// + /// The dequant matrices. + /// + private readonly JxlDequantMatrices? dequant; + + /// + /// Initializes a new instance of the class using the default + /// DC quantizer & scale factors. + /// + /// The dequant. + public JxlQuantizer(JxlDequantMatrices dequant) + : this(dequant, DefaultQuant, GlobalScaleDenominator / DefaultQuant) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The dequant. + /// The DC quantizer. + /// The scale factor. + public JxlQuantizer(JxlDequantMatrices dequant, int quantDc, int globalScale) + { + this.dequant = dequant; + this.quantDc = quantDc; + this.globalScale = globalScale; + + this.RecomputeFromGlobalScale(); + this.inverseQuantDc = this.inverseGlobalScale / this.quantDc; + + ZeroBiasDefault.CopyTo(this.zeroBias); + } + + /// + /// Gets the scaling factor. + /// + public float Scale => this.globalScaleSingle; + + /// + /// Gets the inverse scaling factor. It is a reciprocal of . + /// + public float InverseGlobalScale => this.inverseGlobalScale; + + /// + /// Gets the inverse DC quantization base value. + /// + public float InverseQuantDc => this.inverseQuantDc; + + public ReadOnlySpan MulDc => this.mulDc; + + public ReadOnlySpan InverseMulDc => this.inverseMulDc; + + /// + /// Gets the zero-biases for quantizing channels X, Y, and B. + /// + private static ReadOnlySpan ZeroBiasDefault => [0.5f, 0.5f, 0.5f]; + + /// + /// Gets the default bias for quant. + /// + private static ReadOnlySpan DefaultQuantBias => + [ + 1.0f - 0.05465007330715401f, + 1.0f - 0.07005449891748593f, + 1.0f - 0.049935103337343655f, + 0.145f, + ]; + + /// + /// Clears the and values, + /// setting their contents to 1.0f. + /// + public void ClearDcMultipliers() + { + Array.Fill(this.mulDc, 1f); + Array.Fill(this.inverseMulDc, 1f); + } + + /// + /// Ensure that the input value stays within the range of 1..MaxQuant. + /// + /// Value to clamp + /// The input value clamped into the range of 1..MaxQuant. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Clamp(float value) => (int)MathF.Max(1.0f, MathF.Min(value, MaxQuant)); + + /// + /// Scales the global scale value. + /// + /// The new scale + /// The scale value, scaled by the global scale. + private float ScaleGlobalScale(float scale) + { + int newGlobalScale = (int)MathF.Round(this.globalScale * scale, MidpointRounding.AwayFromZero); + float scaleOut = newGlobalScale * 1.0f / this.globalScale; + this.globalScale = newGlobalScale; + + this.RecomputeFromGlobalScale(); + + return scaleOut; + } + + /// + /// Recomputes quant values from the scale. + /// + public void RecomputeFromGlobalScale() + { + this.globalScaleSingle = this.globalScale * (1.0f / GlobalScaleDenominator); + this.inverseGlobalScale = 1.0f * GlobalScaleDenominator / this.globalScale; + this.inverseQuantDc = this.inverseGlobalScale / this.quantDc; + + for (int c = 0; c < 3; c++) + { + this.mulDc[c] = this.GetDcStep(c); + this.inverseMulDc[c] = this.GetInverseDcStep(c); + } + } + + /// + /// Returns the dequant matrix. + /// + /// The quant kind + /// The quantization index + /// The dequant matrix. + public ReadOnlySpan DequantMatrix(JxlAcStrategyType strategy, int c) + => this.dequant.Matrix(strategy, c); + + /// + /// Returns the inverse dequant matrix. + /// + /// The quant kind + /// The quantization index + /// The inverse dequant matrix. + public ReadOnlySpan InverseDequantMatrix(JxlAcStrategyType strategy, int c) + => this.dequant.InverseMatrix(strategy, c); + + /// + /// Returns the DC quantization step. + /// + /// The quantization index + /// The DC quantization step + public float GetDcStep(int c) => this.inverseQuantDc * this.dequant.DcQuant(c); + + /// + /// Returns the inverse DC quantization step. + /// + /// The quantization index + /// The inverse DC quantization step + public float GetInverseDcStep(int c) => this.dequant.InverseDcQuant(c) * (this.globalScaleSingle * this.quantDc); + + /// + /// Creates JXL quantizer parameters with values reflecting those in this quantizer instance. + /// + /// The quantizer parameters. + public JxlQuantizerParameters GetParameters() => new() + { + QuantDc = (uint)this.quantDc, + GlobalScale = (uint)this.globalScale + }; + + /// + /// Reads the quantizer values from the bit-stream. + /// + /// The bit reader. + /// Thrown when it is not possible to parse the quantizer parameters. + public void Decode(JxlBitReader reader) + { + JxlQuantizerParameters qp = new(); + if (!JxlBundle.Read(reader, qp)) + { + throw new IOException("Could not read quantizer parameters"); + } + + this.globalScale = (int)qp.GlobalScale; + this.quantDc = (int)qp.QuantDc; + + this.RecomputeFromGlobalScale(); + } + + /// + /// Recomputes the scaling factors and quant. + /// + public void ComputeGlobalScaleAndQuant(float quantDc, float quantMedian, float quantMedianAbsd) + { + const int quantFieldTarget = 5; + float scale = GlobalScaleDenominator * (quantMedian - quantMedianAbsd) / quantFieldTarget; + + if (scale < 1) + { + scale = 1; + } + + if (scale > (1 << 15)) + { + scale = 1 << 15; + } + + int newGlobalScale = (int)scale; + int scaledQuantDc = (int)(quantDc * GlobalScaleNumerator * 1.6f); + + if (newGlobalScale > scaledQuantDc) + { + newGlobalScale = scaledQuantDc; + + if (newGlobalScale <= 0) + { + newGlobalScale = 1; + } + } + + this.globalScale = newGlobalScale; + + this.RecomputeFromGlobalScale(); + + float valueF = (quantDc * this.inverseGlobalScale) + 0.5f; + float clipValueF = MathF.Min(1 << 16, valueF); + int newQuant = (int)clipValueF; + this.quantDc = newQuant; + + this.RecomputeFromGlobalScale(); + } + + /// + /// Quantizes the specified rectangular selection. + /// + public void SetQuantFieldRect(JxlImageF qf, in Rectangle rect, JxlImageI rawQuantField) + { + for (int y = 0; y < rect.Height; y++) + { + ReadOnlySpan rowQf = qf.GetRow(in rect, y); + Span rowQi = rawQuantField.GetRow(in rect, y); + + for (int x = 0; x < rect.Width; x++) + { + int val = Clamp((rowQf[x] * this.inverseGlobalScale) + 0.5f); + + rowQi[x] = val; + } + } + } + + /// + /// Set the quant field. + /// + public bool SetQuantField(Configuration configuration, float quantDc, JxlImageF qf, JxlImageI? rawQuantField) + { + IMemoryOwner data = configuration.MemoryAllocator.Allocate(qf.XSize * qf.YSize); + Span dataSpan = data.Memory.Span; + + for (int y = 0; y < qf.YSize; y++) + { + ReadOnlySpan rowQf = qf.GetRow(y); + + for (int x = 0; x < qf.XSize; y++) + { + float quant = rowQf[x]; + + dataSpan[(qf.XSize * y) + x] = quant; + } + } + + dataSpan[dataSpan.Length / 2] = JxlSpanHelper.NthElement(dataSpan, dataSpan.Length / 2); + float quantMedian = dataSpan[dataSpan.Length / 2]; + + IMemoryOwner deviations = configuration.MemoryAllocator.Allocate(dataSpan.Length); + Span deviationsSpan = deviations.Memory.Span; + for (int i = 0; i < dataSpan.Length; i++) + { + deviationsSpan[i] = MathF.Abs(dataSpan[i] - quantMedian); + } + + deviationsSpan[deviationsSpan.Length / 2] = JxlSpanHelper.NthElement(deviationsSpan, deviationsSpan.Length / 2); + float quantMedianAbsd = deviationsSpan[deviationsSpan.Length / 2]; + + this.ComputeGlobalScaleAndQuant(quantDc, quantMedian, quantMedianAbsd); + + if (rawQuantField != null) + { + if (rawQuantField.GetSize() != qf.GetSize()) + { + data.Dispose(); + deviations.Dispose(); + + return false; + } + + this.SetQuantField(qf, qf.GetRectangle(), rawQuantField); + } + + data.Dispose(); + deviations.Dispose(); + + return true; + } + + public void SetQuant(float quantDc, float quantAc, JxlImageI rawQuantField) + { + this.ComputeGlobalScaleAndQuant(quantDc, quantAc, 0); + + int value = Clamp((quantAc * this.inverseGlobalScale) + 0.5f); + rawQuantField.Fill(value); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerParameters.cs new file mode 100644 index 0000000000..4faa558692 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerParameters.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Represents parameters for the JPEG XL quantizer. +/// +internal sealed class JxlQuantizerParameters : IJxlFields +{ + private uint globalScale; + private uint quantDc; + + /// + /// Initializes a new instance of the class. + /// + public JxlQuantizerParameters() => JxlBundle.Init(this); + + /// + /// Gets or sets the global scale value. + /// + public uint GlobalScale + { + get => this.globalScale; + set => this.globalScale = value; + } + + /// + /// Gets or sets the quant DC value. + /// + public uint QuantDc + { + get => this.quantDc; + set => this.quantDc = value; + } + + public bool Visit(JxlVisitor visitor) + { + if (!visitor.U32( + JxlFieldExpressions.BitsOffset(11, 1), + JxlFieldExpressions.BitsOffset(11, 2049), + JxlFieldExpressions.BitsOffset(12, 4097), + JxlFieldExpressions.BitsOffset(16, 8193), + 1, + ref this.globalScale)) + { + return false; + } + + return visitor.U32( + JxlFieldExpressions.Value(16), + JxlFieldExpressions.BitsOffset(5, 1), + JxlFieldExpressions.BitsOffset(8, 1), + JxlFieldExpressions.BitsOffset(16, 1), + 1, + ref this.quantDc); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs new file mode 100644 index 0000000000..249d1ec9a7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlWeightsSeparable5 +{ + public InlineArray12 Horizontal { get; set; } + + public InlineArray12 Vertical { get; set; } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric3.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric3.cs new file mode 100644 index 0000000000..4d350b3ab8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric3.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlWeightsSymmetric3 +{ + private InlineArray4 c; + + private InlineArray4 r; + + private InlineArray4 d; + + public Vector128 GetCVector() + { + ref float first = ref Unsafe.AsRef(in this.c[0]); + return Vector128.LoadUnsafe(ref first); + } + + public Vector128 GetRVector() + { + ref float first = ref Unsafe.AsRef(in this.r[0]); + return Vector128.LoadUnsafe(ref first); + } + + public Vector128 GetDVector() + { + ref float first = ref Unsafe.AsRef(in this.d[0]); + return Vector128.LoadUnsafe(ref first); + } + + public void SetC(Vector128 vec) + { + ref float first = ref Unsafe.AsRef(in this.c[0]); + vec.StoreUnsafe(ref first); + } + + public void SetD(Vector128 vec) + { + ref float first = ref Unsafe.AsRef(in this.d[0]); + vec.StoreUnsafe(ref first); + } + + public void SetR(Vector128 vec) + { + ref float first = ref Unsafe.AsRef(in this.r[0]); + vec.StoreUnsafe(ref first); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric5.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric5.cs new file mode 100644 index 0000000000..1e2481dbb4 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric5.cs @@ -0,0 +1,94 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlWeightsSymmetric5 +{ + private InlineArray4 c; + + private InlineArray4 r; + + private InlineArray4 r2; + + private InlineArray4 d; + + private InlineArray4 d2; + + private InlineArray4 l; + + public Vector128 GetCVector() + { + ref float first = ref Unsafe.AsRef(in this.c[0]); + return Vector128.LoadUnsafe(ref first); + } + + public Vector128 GetRVector() + { + ref float first = ref Unsafe.AsRef(in this.r[0]); + return Vector128.LoadUnsafe(ref first); + } + + public Vector128 GetR2Vector() + { + ref float first = ref Unsafe.AsRef(in this.r2[0]); + return Vector128.LoadUnsafe(ref first); + } + + public Vector128 GetDVector() + { + ref float first = ref Unsafe.AsRef(in this.d[0]); + return Vector128.LoadUnsafe(ref first); + } + + public Vector128 GetD2Vector() + { + ref float first = ref Unsafe.AsRef(in this.d2[0]); + return Vector128.LoadUnsafe(ref first); + } + + public Vector128 GetLVector() + { + ref float first = ref Unsafe.AsRef(in this.l[0]); + return Vector128.LoadUnsafe(ref first); + } + + public void SetC(Vector128 vec) + { + ref float first = ref Unsafe.AsRef(in this.c[0]); + vec.StoreUnsafe(ref first); + } + + public void SetD(Vector128 vec) + { + ref float first = ref Unsafe.AsRef(in this.d[0]); + vec.StoreUnsafe(ref first); + } + + public void SetD2(Vector128 vec) + { + ref float first = ref Unsafe.AsRef(in this.d2[0]); + vec.StoreUnsafe(ref first); + } + + public void SetR(Vector128 vec) + { + ref float first = ref Unsafe.AsRef(in this.r[0]); + vec.StoreUnsafe(ref first); + } + + public void SetR2(Vector128 vec) + { + ref float first = ref Unsafe.AsRef(in this.r2[0]); + vec.StoreUnsafe(ref first); + } + + public void SetL(Vector128 vec) + { + ref float first = ref Unsafe.AsRef(in this.l[0]); + vec.StoreUnsafe(ref first); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs new file mode 100644 index 0000000000..b3849d69f0 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlXorShift +{ + private readonly ulong[] s0 = new ulong[8]; + private readonly ulong[] s1 = new ulong[8]; + + public void XorShift128Plus(ulong seed) + { + this.s0[0] = SplitMix64(seed + 0x9E3779B97F4A7C15L); + this.s1[0] = SplitMix64(this.s0[0]); + for (int i = 1; i < 8; ++i) + { + this.s0[i] = SplitMix64(this.s1[i - 1]); + this.s1[i] = SplitMix64(this.s0[i]); + } + } + + public void XorShift128Plus(uint seed1, uint seed2, uint seed3, uint seed4) + { + this.s0[0] = SplitMix64((((ulong)seed1 << 32) + seed2) + 0x9E3779B97F4A7C15uL); + this.s1[0] = SplitMix64((((ulong)seed3 << 32) + seed4) + 0x9E3779B97F4A7C15uL); + for (int i = 1; i < 8; ++i) + { + this.s0[i] = SplitMix64(this.s0[i - 1]); + this.s1[i] = SplitMix64(this.s1[i - 1]); + } + } + + // TODO: SIMD + public void Fill(Span randomBits) + { + for (int i = 0; i < 8; ++i) + { + ulong s1 = this.s0[i]; + ulong s0 = this.s1[i]; + ulong bits = s1 + s0; + this.s0[i] = s0; + s1 ^= s1 << 23; + randomBits[i] = bits; + s1 ^= s0 ^ (s1 >> 18) ^ (s0 >> 5); + this.s1[i] = s1; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong SplitMix64(ulong z) + { + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9uL; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBuL; + return z ^ (z >> 31); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlControlPoint.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlControlPoint.cs new file mode 100644 index 0000000000..74f1a0c3a8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlControlPoint.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Splines; + +/// +/// A simple pair of first and second 32-bit signed integers +/// that represent a single control point. +/// +[StructLayout(LayoutKind.Sequential)] +internal struct JxlControlPoint(int first, int second) +{ + public int First = first; + public int Second = second; +} diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs new file mode 100644 index 0000000000..42aed25f8a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs @@ -0,0 +1,343 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Splines; + +internal sealed class JxlQuantizedSpline : IDisposable +{ + private IMemoryOwner? memoryOwner; + + public JxlQuantizedSpline() + { + for (int i = 0; i < 3; i++) + { + this.ColorDct[i] = new int[32]; + } + } + + private static ReadOnlySpan Loops => [1, 0, 2]; + + private static ReadOnlySpan ChannelWeight => [0.0042f, 0.075f, 0.07f, .3333f]; + + public Memory ControlPoints { get; set; } + + // NOTE: Do not use Configuration.MemoryAllocator.Allocate2D. This is + // a 3x32 array, and renting memory introduces too much overhead for + // 384 bytes of memory. + // Additionally, prefer jagged arrays instead of multidimensional arrays + // for performance. + public int[][] ColorDct { get; set; } = new int[3][]; + + public int[] SigmaDct { get; set; } = new int[32]; + + public void Dispose() + { + this.memoryOwner?.Dispose(); + this.ControlPoints = Memory.Empty; + GC.SuppressFinalize(this); + } + + public void ReserveControlPoints(Configuration configuration, int n) + { + this.memoryOwner = configuration.MemoryAllocator.Allocate(n); + + this.ControlPoints = this.memoryOwner.Memory; + } + + public static JxlQuantizedSpline Create(Configuration configuration, JxlSpline original, int quantizationAdjustment, float yToX, float yToB) + { + JxlQuantizedSpline spline = new(); + + spline.ReserveControlPoints(configuration, original.ControlPoints.Count - 1); + + PointF startingPoint = original.ControlPoints.First(); + int previousX = (int)MathF.Round(startingPoint.X); + int previousY = (int)MathF.Round(startingPoint.Y); + int previousDx = 0; // D stands for delta + int previousDy = 0; // D stands for delta + + int length = original.ControlPoints.Length; + IMemoryOwner newControls = configuration.MemoryAllocator.Allocate(length); + Span controlsSpan = newControls.Memory.Span; + + for (int i = 0; i < length; i++) + { + PointF controlPoint = original.ControlPoints[i]; + + int newX = (int)MathF.Round(controlPoint.X); + int newY = (int)MathF.Round(controlPoint.Y); + int newDx = newX - previousX; // D stands for delta + int newDy = newY - previousY; // D stands for delta + + controlsSpan[i] = new(newDx - previousDx, newDy - previousDy); + + previousDx = newDx; + previousDy = newDy; + previousX = newX; + previousY = newY; + } + + float quant = AdjustedQuant(quantizationAdjustment); + float inverseQuant = InverseAdjustedQuant(quantizationAdjustment); + + for (int j = 0; j < 3; j++) + { + int c = Loops[j]; + + float factor = (c == 0) ? yToX : (c == 1) ? 0 : yToB; + + // TODO: lower amount of branches by duplicating code + // for i=0 and adding a separate loop for i=1..31 + for (int i = 0; i < 32; i++) + { + float dctFactor = (i == 0) ? Sqrt2 : 1.0f; + float inverseDctFactor = (i == 0) ? Sqrt05 : 1.0f; + float restoredY = spline.ColorDct[1][i] * inverseDctFactor * ChannelWeight[1] * inverseQuant; + float decorrelated = spline.ColorDct[c][i] - (factor * restoredY); + spline.ColorDct[c][i] = ConvertToInteger(decorrelated * dctFactor * quant / ChannelWeight[c]); + } + } + + for (int i = 0; i < 32; i++) + { + float dctFactor = (i == 0) ? Sqrt2 : 1.0f; + spline.SigmaDct[i] = ConvertToInteger(original.SigmaDct[i] * dctFactor * quant / ChannelWeight[1]); + } + + return spline; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static int ConvertToInteger(float value) + { + const float max = int.MaxValue - 127f; + const float min = -max; + + return (int)MathF.Round(Math.Clamp(value, min, max)); + } + } + + public bool Dequantize( + Configuration configuration, + PointF startingPoint, + int quantizationAdjustment, + float yToX, + float yToB, + long imageSize, + ref long totalEstimatedAreaReached, + JxlSpline result) + { + long areaLimit = Math.Min(1024 * imageSize * (1L << 32), 1L << 42); + result.ClearControlPoints(); + result.ReserveControlPoints(configuration, this.ControlPoints.Length + 1); + + float px = MathF.Round(startingPoint.X); + float py = MathF.Round(startingPoint.Y); + + if (!this.ValidateSplinePointPos(px, py)) + { + Debug.Fail("Spline points out of range"); + + return false; + } + + int currentX = (int)px; + int currentY = (int)py; + + Span controlPoints = result.ControlPoints.Span; + Span thisControlPoints = this.ControlPoints.Span; + + controlPoints[0] = new(currentX, currentY); + + int currentDx = 0; // D stands for delta + int currentDy = 0; // D stands for delta + + long manhattanDistance = 0; + + int length = this.ControlPoints.Length; + + for (int i = 0; i < length; i++) + { + JxlControlPoint point = thisControlPoints[i]; + currentDx += point.First; + currentDy += point.Second; + manhattanDistance = Math.Abs(currentDx) + Math.Abs(currentDy); + if (manhattanDistance > areaLimit) + { + Debug.Fail("Manhattan distance is too large"); + + return false; + } + + if (!ValidateSplinePointPos(currentDx, currentDy)) + { + Debug.Fail("Delta points out of range"); + + return false; + } + + currentX += currentDx; + currentY += currentDy; + + if (!ValidateSplinePointPos(currentX, currentY)) + { + Debug.Fail("Current points out of range"); + + return false; + } + + controlPoints[i + 1] = new(currentX, currentY); + } + + float inverseQuant = InverseAdjustedQuant(quantizationAdjustment); + + for (int c = 0; c < 3; c++) + { + for (int i = 0; i < 32; i++) + { + float inverseDctFactor = (i == 0) ? Sqrt05 : 1.0f; + result.ColorDct[c][i] = this.ColorDct[c][i] * inverseDctFactor * ChannelWeight[c] * inverseQuant; + } + } + + for (int i = 0; i < 32; i++) + { + result.ColorDct[0][i] += yToX * result.ColorDct[1][i]; + result.ColorDct[2][i] += yToB * result.ColorDct[1][i]; + } + + long widthEstimate = 0; + Span color = stackalloc long[3]; + + for (int c = 0; c < 3; c++) + { + for (int i = 0; i < 32; i++) + { + color[c] += (long)MathF.Ceiling(inverseQuant * MathF.Abs(this.ColorDct[c][i])); + } + } + + color[0] += (long)MathF.Ceiling(MathF.Abs(yToX)) * color[1]; + color[2] += (long)MathF.Ceiling(MathF.Abs(yToB)) * color[1]; + + long maxColor = Math.Max(color[1], Math.Max(color[0], color[2])); + long logColor = Math.Max(1L, (long)CeilLog2Nonzero(1L + maxColor)); + float weightLimit = MathF.Ceiling(MathF.Sqrt((float)areaLimit / logColor) / MathF.Max(1, manhattanDistance)); + + for (int i = 0; i < 32; i++) + { + float inverseDctFactor = (i == 0) ? Sqrt05 : 1.0f; + result.SigmaDct[i] = this.SigmaDct[i] * inverseDctFactor * ChannelWeight[3] * inverseQuant; + float weightF = MathF.Ceiling(inverseQuant * MathF.Abs(this.SigmaDct[i])); + long weight = (long)Math.Min(weightLimit, Math.Max(1.0f, weightF)); + widthEstimate += weight * weight * logColor; + } + + totalEstimatedAreaReached = widthEstimate * manhattanDistance; + if (totalEstimatedAreaReached > areaLimit) + { + Debug.Fail("Total estimated area is too large"); + + return false; + } + + return true; + } + + public bool Decode( + Configuration configuration, + Span contextMap, + JxlAnsSymbolReader decoder, + JxlBitReader br, + int maxControlPoints, + ref int totalControlPoints) + { + int numControlPoints = decoder.ReadHybridUnsignedInteger(NumControlPointsContext, br, contextMap); + if (numControlPoints > maxControlPoints) + { + Debug.Fail("Too many control points"); + + return false; + } + + totalControlPoints += numControlPoints; + + if (totalControlPoints >= maxControlPoints) + { + Debug.Fail("Too many control points"); + + return false; + } + + this.ResizeControlPoints(configuration, numControlPoints); + + const long deltaLimit = 1L << 30; + Span controlPoints = this.ControlPoints.Span; + + int length = this.ControlPoints.Length; + + for (int i = 0; i < length; i++) + { + ref JxlControlPoint controlPoint = ref controlPoints[i]; + + controlPoint.First = UnpackSigned(decoder.ReadHybridUnsignedInteger(ControlPointsContext, br, contextMap)); + controlPoint.Second = UnpackSigned(decoder.ReadHybridUnsignedInteger(ControlPointsContext, br, contextMap)); + + if (controlPoint.First >= deltaLimit || controlPoint.First <= -deltaLimit || + controlPoint.Second >= deltaLimit || controlPoint.Second <= -deltaLimit) + { + Debug.Fail("Spline delta-delta is out of bounds"); + + return false; + } + } + + for (int i = 0; i < this.ColorDct.Length; i++) + { + if (!TryDecodeDct(contextMap, this.ColorDct[i])) + { + return false; + } + } + + if (!TryDecodeDct(contextMap, this.SigmaDct)) + { + return false; + } + + return true; + + bool TryDecodeDct(ReadOnlySpan contextMap, Span dct) + { + const int invalidCoefficient = int.MinValue; + + for (int i = 0; i < 32; i++) + { + dct[i] = UnpackSigned(decoder.ReadHybridUnsignedInteger(DctContext, br, contextMap)); + if (dct[i] == invalidCoefficient) + { + Debug.Fail("The DCT coefficient is invalid"); + + return false; + } + } + + return true; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float AdjustedQuant(int adjustment) + => (adjustment >= 0) + ? (1f + (.125f * adjustment)) + : 1f / (1f - (.125f * adjustment)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float InverseAdjustedQuant(int adjustment) + => (adjustment >= 0) + ? 1f / (1f + (.125f * adjustment)) + : (1f - (.125f * adjustment)); +} diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs new file mode 100644 index 0000000000..ef65c1d95e --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; + +namespace SixLabors.ImageSharp.Formats.Jxl.Splines; + +internal sealed class JxlSpline : IDisposable +{ + private IMemoryOwner? controlPoints; + + public Memory ControlPoints { get; private set; } + + public JxlDct32[] ColorDct { get; set; } = []; + + public JxlDct32 SigmaDct { get; set; } + + public void ClearControlPoints() + { + this.controlPoints?.Dispose(); + this.controlPoints = null; + + this.ControlPoints = Memory.Empty; + } + + public void ReserveControlPoints(Configuration configuration, int n) + { + this.ClearControlPoints(); + + this.controlPoints = configuration.MemoryAllocator.Allocate(n); + this.ControlPoints = this.controlPoints.Memory; + } + + public void Dispose() + { + this.ClearControlPoints(); + GC.SuppressFinalize(this); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineDataView.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineDataView.cs new file mode 100644 index 0000000000..1884b85c68 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineDataView.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Splines; + +internal sealed class JxlSplineDataView +{ + public List Splines { get; set; } = []; + + public List StartingPoints { get; set; } = []; + + public bool HasAny => this.Splines.Count > 0; +} diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs new file mode 100644 index 0000000000..adb619f5ff --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Splines; + +internal enum JxlSplineEntropyContext : byte +{ + QuantizationAdjustment, + StartingPosition, + NumSplines, + NumControlPoints, + ControlPoints, + Dct, + NumSplineContexts +} diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegment.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegment.cs new file mode 100644 index 0000000000..7e8bef1beb --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegment.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Splines; + +internal struct JxlSplineSegment +{ + public PointF Center { get; set; } + + public float MaximumDistance { get; set; } + + public float InverseSigma { get; set; } + + public float SigmaOver4TimesIntensity { get; set; } + + public InlineArray3 Color { get; set; } +} diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegmentSpan.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegmentSpan.cs new file mode 100644 index 0000000000..b4bd52af4f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegmentSpan.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Splines; + +internal struct JxlSplineSegmentSpan(int startInclusive, int endInclusive) +{ + public int StartInclusive { get; set; } = startInclusive; + + public int EndInclusive { get; set; } = endInclusive; +}