diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs index a0029c0c27..2b304885d1 100644 --- a/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs @@ -21,4 +21,6 @@ internal static class JxlOpsinConstants public const float OpsinAbsorbanceBias0 = 0.0037930732552754493f; public const float OpsinAbsorbanceBias1 = OpsinAbsorbanceBias0; public const float OpsinAbsorbanceBias2 = OpsinAbsorbanceBias0; + + public static ReadOnlySpan OpsinAbsorbanceBias => [OpsinAbsorbanceBias0, OpsinAbsorbanceBias1, OpsinAbsorbanceBias2]; } diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs index 0b2640467b..3e4a2b6676 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs @@ -31,6 +31,16 @@ internal static class JxlBitsCoder return true; } + public static void Write(int bits, uint value, JxlBitWriter writer) + { + if (value >= (1u << bits)) + { + throw new InvalidOperationException("Value is too large to encode in " + bits + " bits"); + } + + writer.Write(bits, value); + } + // 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); diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs index 7202932539..94e5aa627c 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs @@ -1,7 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; @@ -74,6 +79,23 @@ internal static class JxlBundle return visitor.OK; } + public static bool CanEncode(IJxlFields fields, ref int extensionBits, ref long totalBits) + { + JxlCanEncodeVisitor canEncodeVisitor = new(); + + if (!canEncodeVisitor.Visit(fields)) + { + return false; + } + + if (!canEncodeVisitor.GetSizes(ref extensionBits, ref totalBits)) + { + return false; + } + + return true; + } + /// /// Tries to read the fields from a bit-reader. /// @@ -86,4 +108,66 @@ internal static class JxlBundle _ = visitor.Visit(fields); return visitor.OK; } + + public static bool Write(IJxlFields fields, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOutput) + { + int extensionBits = 0; + long totalBits = 0; + if (!CanEncode(fields, ref extensionBits, ref totalBits)) + { + return false; + } + + return writer.WithMaxBits(totalBits, layer, auxOutput, () => + { + JxlWriteVisitor visitor = new(extensionBits, writer); + + if (!visitor.Visit(fields)) + { + return false; + } + + return visitor.OK(); + }); + } + + public static bool WriteCodestreamHeaders(JxlCodecMetadata metadata, JxlBitWriter writer, JxlAuxiliaryOutput auxOut) + { + // Marker/signature + if (!writer.WithMaxBits(16, JxlLayerType.Header, auxOut, () => + { + writer.Write(8, 0xFF); + writer.Write(8, JxlShared.CodestreamMarker); + return true; + })) + { + return false; + } + + if (!WriteSizeHeader(metadata.Size!, writer, JxlLayerType.Header, auxOut)) + { + return false; + } + + if (!WriteImageMetadata(metadata.ImageMetadata!, writer, JxlLayerType.Header, auxOut)) + { + return false; + } + + metadata.CustomTransformData!.NonserializedXybEncoded = metadata.ImageMetadata!.XybEncoded; + + return Write(metadata.CustomTransformData!, writer, JxlLayerType.Header, auxOut); + } + + public static bool WriteFrameHeader(JxlFrameHeader frame, JxlBitWriter writer, JxlAuxiliaryOutput auxOut) + => Write(frame, writer, JxlLayerType.Header, auxOut); + + public static bool WriteImageMetadata(JxlImageMetadata metadata, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOut) + => Write(metadata, writer, layer, auxOut); + + public static bool WriteQuantizerParameters(JxlQuantizerParameters metadata, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOut) + => Write(metadata, writer, layer, auxOut); + + public static bool WriteSizeHeader(JxlSizeHeader header, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOut) + => Write(header, writer, layer, auxOut); } diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs index b930e5dc49..3da369a18e 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs @@ -66,4 +66,70 @@ internal static class JxlF16Coder return true; } + + public static bool Write(float value, JxlBitWriter writer) + { + uint bits32 = BitConverter.SingleToUInt32Bits(value); + + uint sign = bits32 >> 31; + uint biasedExp32 = (bits32 >> 23) & 0xFF; + uint mantissa32 = bits32 & 0x7FFFFF; + + int exp = (int)biasedExp32 - 127; + if (exp > 15) + { + throw new InvalidOperationException("Too big to encode, CanEncode should return false"); + } + + // Tiny or zero => zero. + if (exp < -24) + { + writer.Write(16, 0); + return true; + } + + uint biasedExp16 = 0; + uint mantissa16 = 0; + + if (exp < -14) + { + biasedExp16 = 0; + + uint subExp = unchecked((uint)(-14 - exp)); + + if (subExp is not (>= 1 and < 11)) + { + return false; + } + + mantissa16 = (1u << (int)(10 - subExp)) + (mantissa32 >> (int)(13 + subExp)); + } + else + { + // exp = [-14, 15] + biasedExp16 = unchecked((uint)(exp + 15)); + + if (biasedExp16 is not (>= 1 and < 31)) + { + return false; + } + + mantissa16 = mantissa32 >> 13; + } + + if (mantissa16 >= 1024) + { + return false; + } + + uint bits16 = (sign << 15) | (biasedExp16 << 10) | mantissa16; + + if (bits16 >= 0x10000) + { + return false; + } + + writer.Write(16, bits16); + return true; + } } diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs index bdfb456673..660b403887 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs @@ -73,7 +73,7 @@ internal static class JxlU32Coder /// public static bool ChooseSelector(in JxlU32Enc enc, uint value, ref uint selector, ref int totalBits) { - int bitsRequired = 32 - JxlMath.Num0BitsAboveMS1Bit(value); + uint bitsRequired = 32 - JxlMath.Num0BitsAboveMS1Bit(value); if (bitsRequired > 32) { @@ -116,11 +116,38 @@ internal static class JxlU32Coder if (totalBits == 64) { - DebugGuard.IsTrue(false, "No matching selector"); + throw new InvalidOperationException("No matching selector"); + } + return true; + } + + public static bool Write(in JxlU32Enc enc, uint value, JxlBitWriter writer) + { + uint selector = 0; + int totalBits = 0; + + if (!ChooseSelector(in enc, value, ref selector, ref totalBits)) + { return false; } + writer.Write(2, selector); + + JxlU32Distribution d = enc.GetDistribution((int)selector); + + if (!d.IsDirect) + { + uint offset = d.Offset; + + if (value < offset) + { + return false; + } + + writer.WriteBits(totalBits - 2, value - offset); + } + return true; } } diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs index 659b6f994d..8726c9a27b 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs @@ -99,6 +99,59 @@ internal static class JxlU64Coder return true; } + public static bool Write(ulong value, JxlBitWriter writer) + { + if (value == 0) + { + // Selector: use 0 bits, value 0 + writer.Write(2, 0); + } + else if (value <= 16) + { + // Selector: use 4 bits, value 1..16 + writer.Write(2, 1); + writer.Write(4, value - 1); + } + else if (value <= 272) + { + // Selector: use 8 bits, value 17..272 + writer.Write(2, 2); + writer.Write(8, value - 17); + } + else + { + // Selector: varint, first a 12-bit group, after that per 8-bit group. + writer.Write(2, 3); + writer.Write(12, value & 4095); + value >>= 12; + int shift = 12; + while (value > 0 && shift < 60) + { + // Indicate varint not done + writer.Write(1, 1); + writer.Write(8, value & 255); + value >>= 8; + shift += 8; + } + + if (value > 0) + { + // This only could happen if shift == N - 4. + writer.Write(1, 1); + writer.Write(4, value & 15); + + // Implicitly closed sequence, no extra stop bit is required. + } + else + { + // Indicate end of varint + writer.Write(1, 0); + } + } + + return true; + } + /// /// Always returns 73. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs index 7ddad4714e..a595406f36 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs @@ -4,7 +4,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsCode.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsCode.cs similarity index 96% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsCode.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsCode.cs index 56dbab4ed3..e5cd1f783b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsCode.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsCode.cs @@ -4,7 +4,7 @@ using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; internal sealed class JxlAnsCode { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs similarity index 99% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs index 180dbd3c1c..683ee27835 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsReader.cs @@ -5,7 +5,7 @@ using System.Buffers; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; internal static class JxlAnsReader { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsSymbolReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs similarity index 92% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsSymbolReader.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs index 8c4a83ca2f..115b4fad68 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsSymbolReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Ans/JxlAnsSymbolReader.cs @@ -3,7 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; internal sealed class JxlAnsSymbolReader { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs index 33b7307d30..0f199f27cc 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; /// /// Represents a bitstream reader. /// -internal sealed class JxlBitReader(Stream stream) +internal class JxlBitReader(Stream stream) { private ulong buffer; private uint bufferRemainingBits; @@ -154,17 +154,17 @@ internal sealed class JxlBitReader(Stream stream) } } - public uint ReadBits32(uint bits) => this.ReadBits32Core(bits, peek: false); + public virtual uint ReadBits32(uint bits) => this.ReadBits32Core(bits, peek: false); - public uint PeekBits32(uint bits) => this.ReadBits32Core(bits, peek: true); + public virtual uint PeekBits32(uint bits) => this.ReadBits32Core(bits, peek: true); - public void SkipBits32(uint bits) => _ = this.ReadBits32(bits); + public virtual void SkipBits32(uint bits) => _ = this.ReadBits32(bits); - public ulong ReadBits64(ulong bits) => this.ReadBits64Core((uint)bits, peek: false); + public virtual ulong ReadBits64(ulong bits) => this.ReadBits64Core((uint)bits, peek: false); - public ulong PeekBits64(ulong bits) => this.ReadBits64Core((uint)bits, peek: true); + public virtual ulong PeekBits64(ulong bits) => this.ReadBits64Core((uint)bits, peek: true); - public void SkipBits64(ulong bits) => _ = this.ReadBits64(bits); + public virtual void SkipBits64(ulong bits) => _ = this.ReadBits64(bits); - public bool ReadBoolean() => this.ReadBits32Core(1, peek: false) == 1; + public virtual bool ReadBoolean() => this.ReadBits32Core(1, peek: false) == 1; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlGroupBorderAssigner.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlGroupBorderAssigner.cs new file mode 100644 index 0000000000..3833c5ede2 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlGroupBorderAssigner.cs @@ -0,0 +1,330 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal sealed class JxlGroupBorderAssigner +{ + public const int MaxToFinalize = 3; + + private readonly JxlFrameDimensions frameDim; + private readonly Buffer2D counters; + + private const byte TopLeft = 0x01; + private const byte TopRight = 0x02; + private const byte BottomRight = 0x04; + private const byte BottomLeft = 0x08; + + public JxlGroupBorderAssigner(Configuration configuration, JxlFrameDimensions frameDim) + { + this.frameDim = frameDim; + + int xSize = frameDim.XSizeGroups; + int ySize = frameDim.YSizeGroups; + + int counterCount = (xSize + 1 + 7) / 8; + this.counters = configuration.MemoryAllocator.Allocate2D(ySize + 1, counterCount); + + void Set(int x, int y, uint corners) + { + int shift = 4 * (x & 7); + int bits = (int)(corners << shift); + this.counters[y, x / 8] |= bits; + } + + for (int x = 0; x < xSize + 1; x++) + { + Set(x, 0, TopLeft | TopRight); + Set(x, ySize, BottomLeft | BottomRight); + } + + for (int y = 0; y < ySize + 1; y++) + { + Set(0, y, TopLeft | BottomLeft); + Set(xSize, y, TopRight | BottomRight); + } + } + + public void ClearDone(int groupId) + { + void Clear(int x, int y, uint corners) + { + int shift = 4 * (x & 7); + int mask = ~(int)(corners << shift); + + ref int counter = ref this.counters[y, x / 8]; + _ = Interlocked.And(ref counter, mask); + } + + (int x, int y) = Math.DivRem(groupId, this.frameDim.XSizeGroups); + + Clear(x, y, BottomRight); + Clear(x + 1, y, BottomLeft); + Clear(x, y + 1, TopRight); + Clear(x + 1, y + 1, TopLeft); + } + + public void GroupDone(int groupId, int padx, int pady, Span rectsToFinalize, out int numToFinalize) + { + (int x, int y) = Math.DivRem(groupId, this.frameDim.XSizeGroups); + + Rectangle blockRect = RectangleUtils.CreateRectangle( + x * this.frameDim.GroupDimension / JxlFrameDimensions.BlockDimensions, + y * this.frameDim.GroupDimension / JxlFrameDimensions.BlockDimensions, + this.frameDim.GroupDimension / JxlFrameDimensions.BlockDimensions, + this.frameDim.GroupDimension / JxlFrameDimensions.BlockDimensions, + this.frameDim.XSizeBlocks, + this.frameDim.YSizeBlocks); + + int FetchStatus(int cx, int cy, uint bit) + { + int shift = 4 * (cx & 7); + int bits = (int)(bit << shift); + + ref int counter = ref this.counters[cy, cx / 8]; + + int status = Interlocked.Or(ref counter, bits); + status >>= shift; + + return (int)((bit | (uint)status) & 0xF); + } + + int topLeftStatus = FetchStatus(x, y, BottomRight); + int topRightStatus = FetchStatus(x + 1, y, BottomLeft); + int bottomRightStatus = FetchStatus(x + 1, y + 1, TopLeft); + int bottomLeftStatus = FetchStatus(x, y + 1, TopRight); + + int x1 = blockRect.X + blockRect.Width; + int y1 = blockRect.Y + blockRect.Height; + + bool isLastGroupX = this.frameDim.XSizeGroups == x + 1; + bool isLastGroupY = this.frameDim.YSizeGroups == y + 1; + + Span xpos = + [ + blockRect.X == 0 + ? 0 + : (blockRect.X * JxlFrameDimensions.BlockDimensions) - padx, + + blockRect.X == 0 + ? 0 + : Math.Min( + this.frameDim.XSize, + (blockRect.X * JxlFrameDimensions.BlockDimensions) + padx), + + isLastGroupX + ? this.frameDim.XSize + : (x1 * JxlFrameDimensions.BlockDimensions) - padx, + + Math.Min( + this.frameDim.XSize, + (x1 * JxlFrameDimensions.BlockDimensions) + padx) + ]; + + Span ypos = + [ + blockRect.Y == 0 + ? 0 + : (blockRect.Y * JxlFrameDimensions.BlockDimensions) - pady, + + blockRect.Y == 0 + ? 0 + : Math.Min( + this.frameDim.YSize, + (blockRect.Y * JxlFrameDimensions.BlockDimensions) + pady), + + isLastGroupY + ? this.frameDim.YSize + : (y1 * JxlFrameDimensions.BlockDimensions) - pady, + + Math.Min( + this.frameDim.YSize, + (y1 * JxlFrameDimensions.BlockDimensions) + pady) + ]; + + numToFinalize = 0; + + void AppendRect(int x0, int x1, int y0, int y1, ref int numToFinalize, Span rectsToFinalize, Span xpos, Span ypos) + { + Rectangle rect = new( + xpos[x0], + ypos[y0], + xpos[x1] - xpos[x0], + ypos[y1] - ypos[y0]); + + if (rect.Width == 0 || rect.Height == 0) + { + return; + } + + rectsToFinalize[numToFinalize++] = rect; + } + + bool[,] availablePartsMask = new bool[3, 3]; + + // Center + availablePartsMask[1, 1] = true; + + // Corners + if (topLeftStatus == 0xF) + { + availablePartsMask[0, 0] = true; + } + + if (topRightStatus == 0xF) + { + availablePartsMask[2, 0] = true; + } + + if (bottomRightStatus == 0xF) + { + availablePartsMask[2, 2] = true; + } + + if (bottomLeftStatus == 0xF) + { + availablePartsMask[0, 2] = true; + } + + // Other borders + if ((topLeftStatus & TopRight) != 0) + { + availablePartsMask[1, 0] = true; + } + + if ((topLeftStatus & BottomLeft) != 0) + { + availablePartsMask[0, 1] = true; + } + + if ((topRightStatus & BottomRight) != 0) + { + availablePartsMask[2, 1] = true; + } + + if ((bottomLeftStatus & BottomRight) != 0) + { + availablePartsMask[1, 2] = true; + } + + const int noSegment = 3; + + Span<(int First, int Last)> horizontalSegments = + [ + (noSegment, noSegment), + (noSegment, noSegment), + (noSegment, noSegment) + ]; + + for (int py = 0; py < 3; py++) + { + for (int px = 0; px < 3; px++) + { + if (!availablePartsMask[px, py]) + { + continue; + } + + if (horizontalSegments[py].First == noSegment) + { + horizontalSegments[py] = (px, horizontalSegments[py].Last); + } + + horizontalSegments[py] = (horizontalSegments[py].First, px + 1); + } + } + + if (horizontalSegments[0] == horizontalSegments[1] && + horizontalSegments[0] == horizontalSegments[2]) + { + AppendRect( + horizontalSegments[0].First, + horizontalSegments[0].Last, + 0, + 3, + ref numToFinalize, + rectsToFinalize, + xpos, + ypos); + } + else if (horizontalSegments[0] == horizontalSegments[1]) + { + AppendRect( + horizontalSegments[0].First, + horizontalSegments[0].Last, + 0, + 2, + ref numToFinalize, + rectsToFinalize, + xpos, + ypos); + + AppendRect( + horizontalSegments[2].First, + horizontalSegments[2].Last, + 2, + 3, + ref numToFinalize, + rectsToFinalize, + xpos, + ypos); + } + else if (horizontalSegments[1] == horizontalSegments[2]) + { + AppendRect( + horizontalSegments[0].First, + horizontalSegments[0].Last, + 0, + 1, + ref numToFinalize, + rectsToFinalize, + xpos, + ypos); + + AppendRect( + horizontalSegments[1].First, + horizontalSegments[1].Last, + 1, + 3, + ref numToFinalize, + rectsToFinalize, + xpos, + ypos); + } + else + { + AppendRect( + horizontalSegments[0].First, + horizontalSegments[0].Last, + 0, + 1, + ref numToFinalize, + rectsToFinalize, + xpos, + ypos); + + AppendRect( + horizontalSegments[1].First, + horizontalSegments[1].Last, + 1, + 2, + ref numToFinalize, + rectsToFinalize, + xpos, + ypos); + + AppendRect( + horizontalSegments[2].First, + horizontalSegments[2].Last, + 2, + 3, + ref numToFinalize, + rectsToFinalize, + xpos, + ypos); + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs index 1ddb429cba..47e685bdfc 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs @@ -1,11 +1,16 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Net.NetworkInformation; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using static System.Net.Mime.MediaTypeNames; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; @@ -61,4 +66,85 @@ internal static class JxlNoiseDecoder } } } + + public static void Random3Planes( + int visibleFrameIndex, + int nonVisibleFrameIndex, + int x0, + int y0, + (JxlImageF Image, Rectangle Bounds) plane0, + (JxlImageF Image, Rectangle Bounds) plane1, + (JxlImageF Image, Rectangle Bounds) plane2) + { + JxlXorShift rng = new((uint)visibleFrameIndex, (uint)nonVisibleFrameIndex, (uint)x0, (uint)y0); + + GenerateRandomImage(rng, plane0.Bounds, plane0.Image); + GenerateRandomImage(rng, plane1.Bounds, plane1.Image); + GenerateRandomImage(rng, plane2.Bounds, plane2.Image); + } + + private static void PrepareNoiseInput( + JxlPassesDecoderState decState, + JxlFrameDimensions frameDim, + JxlFrameHeader frameHeader, + int groupIndex, + int thread) + { + int groupDim = frameDim.GroupDimension; + + int gx = groupIndex % frameDim.XSizeGroups; + int gy = groupIndex / frameDim.XSizeGroups; + + JxlRenderPipelineInput input = decState.RenderPipeline.GetInputBuffers(groupIndex, thread); + int noiseCStart = 3 + frameHeader.Metadata!.ImageMetadata!.ExtraChannelCount; + + InlineArray3<(JxlImageF Image, Rectangle Rect)> rects = default; + + for (int iy = 0; iy < frameHeader.Upsampling; iy++) + { + for (int ix = 0; ix < frameHeader.Upsampling; ix++) + { + for (int c = 0; c < 3; c++) + { + var r = input.GetBuffer(noiseCStart + c); + + int x1 = r.Rect.X0 + r.Rect.XSize; + int y1 = r.Rect.Y0 + r.Rect.YSize; + + rects[c] = ( + r.Image, + RectangleUtils.CreateRectangle( + r.Rect.X0 + (ix * groupDim), + r.Rect.Y0 + (iy * groupDim), + groupDim, + groupDim, + x1, + y1)); + } + + Random3Planes( + decState.VisibleFrameIndex, + decState.NonvisibleFrameIndex, + (int)((gx * frameHeader.Upsampling) + ix) * groupDim, + (int)((gy * frameHeader.Upsampling) + iy) * groupDim, + rects[0], + rects[1], + rects[2]); + } + } + } + + public static float DecodeFloatParam(float precision, JxlBitReader br) + { + int absValQuant = (int)br.ReadBits32(10); + return absValQuant / precision; + } + + public static void DecodeNoise(JxlBitReader br, JxlNoiseParameters noiseParams) + { + for (int i = 0; i < noiseParams.Lookup.Length; i++) + { + noiseParams.Lookup[i] = DecodeFloatParam(JxlNoiseConstants.Precision, br); + } + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs index 3e32e94a48..adc70f36c3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs @@ -4,6 +4,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs new file mode 100644 index 0000000000..416a14b49a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Modular/JxlModularFrameDecoder.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Modular; + +internal sealed class JxlModularFrameDecoder +{ + private readonly Configuration configuration; + private readonly JxlModularImage fullImage; + private readonly List globalTransform; + private JxlFrameDimensions frameDimensions; + private bool doColor; + private bool haveSomething; + private bool useFullImage = true; + private bool allSameShift; + private JxlAnsCode code; + private readonly List contextMap; + private JxlGroupHeader groupHeader; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlendMode.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchBlendMode.cs similarity index 78% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlendMode.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchBlendMode.cs index 755cfe4eb9..89ddd2caae 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlendMode.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchBlendMode.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; internal enum JxlPatchBlendMode : byte { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlending.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchBlending.cs similarity index 74% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlending.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchBlending.cs index 7cedfc3053..ff7bda0d7b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlending.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchBlending.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; internal struct JxlPatchBlending { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs index d91c41e38e..096c0d3de2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchDictionary.cs @@ -1,10 +1,11 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; internal sealed class JxlPatchDictionary { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchPosition.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchPosition.cs similarity index 73% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchPosition.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchPosition.cs index 264d694e85..6cb90a3ab0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchPosition.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchPosition.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; internal struct JxlPatchPosition { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchReferencePosition.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchReferencePosition.cs similarity index 76% rename from src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchReferencePosition.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchReferencePosition.cs index 8e11eb55ad..1d604ffe90 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchReferencePosition.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/Patch/JxlPatchReferencePosition.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; internal struct JxlPatchReferencePosition { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlLinearAlgebra.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlLinearAlgebra.cs index 9b8e2f9fa6..be1f4a75b6 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlLinearAlgebra.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlLinearAlgebra.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; /// internal static class JxlLinearAlgebra { - public static void ConvertToDiagonal(Matrix2x2 a, Vector2 diag, Matrix2x2 u) + public static void ConvertToDiagonal(Matrix2x2 a, ref Vector2 diag, ref Matrix2x2 u) { DebugGuard.MustBeLessThan(Math.Abs(a[0][1] - a[1][0]), 1e-15, nameof(a)); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs new file mode 100644 index 0000000000..037eb46c1f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlXybEncoder.cs @@ -0,0 +1,466 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Numerics.Tensors; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +internal static class JxlXybEncoder +{ + private static void OpsinAbsorbance( + Vector r, + Vector g, + Vector b, + ReadOnlySpan premulAbsorb, + out Vector mixed0, + out Vector mixed1, + out Vector mixed2) + { + ReadOnlySpan bias = JxlOpsinConstants.OpsinAbsorbanceBias; + + mixed0 = Vector.FusedMultiplyAdd( + Vector.Create(premulAbsorb), + r, + Vector.FusedMultiplyAdd( + Vector.Create(premulAbsorb[8..]), + g, + Vector.FusedMultiplyAdd( + Vector.Create(premulAbsorb[16..]), + b, + Vector.Create(bias[0])))); + + mixed1 = Vector.FusedMultiplyAdd( + Vector.Create(premulAbsorb[24..]), + r, + Vector.FusedMultiplyAdd( + Vector.Create(premulAbsorb[32..]), + g, + Vector.FusedMultiplyAdd( + Vector.Create(premulAbsorb[40..]), + b, + Vector.Create(bias[1])))); + + mixed2 = Vector.FusedMultiplyAdd( + Vector.Create(premulAbsorb[48..]), + r, + Vector.FusedMultiplyAdd( + Vector.Create(premulAbsorb[56..]), + g, + Vector.FusedMultiplyAdd( + Vector.Create(premulAbsorb[64..]), + b, + Vector.Create(bias[2])))); + } + + private static void StoreXYB( + Vector r, + Vector g, + Vector b, + Span valX, + Span valY, + Span valZ) + { + Vector half = Vector.Create(0.5f); + + (half * (r - g)).CopyTo(valX); + (half * (r + g)).CopyTo(valY); + b.CopyTo(valZ); + } + + private static void LinearRgbToXyb( + Vector r, + Vector g, + Vector b, + ReadOnlySpan premulAbsorb, + Span valX, + Span valY, + Span valZ) + { + OpsinAbsorbance( + r, + g, + b, + premulAbsorb, + out Vector mixed0, + out Vector mixed1, + out Vector mixed2); + + mixed0 = ZeroIfNegative(mixed0); + mixed1 = ZeroIfNegative(mixed1); + mixed2 = ZeroIfNegative(mixed2); + + int n = Vector.Count; + + mixed0 = CubeRootAndAdd( + mixed0, + Vector.Create(premulAbsorb[(9 * n)..])); + + mixed1 = CubeRootAndAdd( + mixed1, + Vector.Create(premulAbsorb[(10 * n)..])); + + mixed2 = CubeRootAndAdd( + mixed2, + Vector.Create(premulAbsorb[(11 * n)..])); + + StoreXYB(mixed0, mixed1, mixed2, valX, valY, valZ); + } + + public static void LinearRgbRowToXyb( + Span row0, + Span row1, + Span row2, + ReadOnlySpan premulAbsorb, + int xSize) + { + int n = Vector.Count; + + for (int x = 0; x < xSize; x += n) + { + Vector r = Vector.Create(row0[x..]); + Vector g = Vector.Create(row1[x..]); + Vector b = Vector.Create(row2[x..]); + + LinearRgbToXyb( + r, + g, + b, + premulAbsorb, + row0[x..], + row1[x..], + row2[x..]); + } + } + + private static Vector CubeRootAndAdd(Vector v, Vector add) + { + // HACK: Vector doesn't support cuberoot + Span span = stackalloc float[Vector.Count]; + v.StoreUnsafe(ref MemoryMarshal.GetReference(span)); + TensorPrimitives.Cbrt(span, span); + return Vector.LoadUnsafe(ref MemoryMarshal.GetReference(span)) + add; + } + + private static Vector ZeroIfNegative(Vector v) => + Vector.ConditionalSelect( + Vector.LessThan(v, Vector.Zero), + Vector.Zero, + v); + + private static Vector LinearFromSRgb(Vector encoded) + => JxlSRgbTransferFunction.DisplayFromEncoded(encoded); + + private static void LinearSrgbToXyb( + Configuration configuration, + Memory premulAbsorb, + JxlImage3F image) + { + int xSize = image.Width; + + void ProcessRow(int y, ReadOnlySpan premulAbsorb) + { + Span row0 = image.PlaneRow(0, y); + Span row1 = image.PlaneRow(1, y); + Span row2 = image.PlaneRow(2, y); + + int lanes = Vector.Count; + + for (int x = 0; x < xSize; x += lanes) + { + Vector r = new(row0[x..]); + Vector g = new(row1[x..]); + Vector b = new(row2[x..]); + + LinearRgbToXyb( + r, + g, + b, + premulAbsorb, + row0[x..], + row1[x..], + row2[x..]); + } + } + + _ = Parallel.For(0, image.Height, configuration.GetParallelOptions(), x => + { + ProcessRow(x, premulAbsorb.Span); + }); + } + + private static void SRgbToXyb( + Configuration configuration, + Memory premulAbsorb, + JxlImage3F image) + { + int xSize = image.Width; + + void ProcessRow(int y, ReadOnlySpan premulAbsorb) + { + Span row0 = image.PlaneRow(0, y); + Span row1 = image.PlaneRow(1, y); + Span row2 = image.PlaneRow(2, y); + + int lanes = Vector.Count; + + for (int x = 0; x < xSize; x += lanes) + { + Vector r = LinearFromSRgb(new Vector(row0[x..])); + Vector g = LinearFromSRgb(new Vector(row1[x..])); + Vector b = LinearFromSRgb(new Vector(row2[x..])); + + LinearRgbToXyb( + r, + g, + b, + premulAbsorb, + row0[x..], + row1[x..], + row2[x..]); + } + } + + _ = Parallel.For(0, image.Height, configuration.GetParallelOptions(), x => + { + ProcessRow(x, premulAbsorb.Span); + }); + } + + private static void SrgbToXybAndLinear( + Configuration configuration, + Memory premulAbsorb, + JxlImage3F image, + JxlImage3F linear) + { + int xSize = image.Width; + + void ProcessRow(int y, ReadOnlySpan premulAbsorb) + { + Span rowImage0 = image.PlaneRow(0, y); + Span rowImage1 = image.PlaneRow(1, y); + Span rowImage2 = image.PlaneRow(2, y); + + Span rowLinear0 = linear.PlaneRow(0, y); + Span rowLinear1 = linear.PlaneRow(1, y); + Span rowLinear2 = linear.PlaneRow(2, y); + + int lanes = Vector.Count; + + for (int x = 0; x < xSize; x += lanes) + { + Vector r = LinearFromSRgb(new Vector(rowImage0[x..])); + Vector g = LinearFromSRgb(new Vector(rowImage1[x..])); + Vector b = LinearFromSRgb(new Vector(rowImage2[x..])); + + r.CopyTo(rowLinear0[x..]); + g.CopyTo(rowLinear1[x..]); + b.CopyTo(rowLinear2[x..]); + + LinearRgbToXyb( + r, + g, + b, + premulAbsorb, + rowImage0[x..], + rowImage1[x..], + rowImage2[x..]); + } + } + + _ = Parallel.For(0, image.Height, configuration.GetParallelOptions(), x => + { + ProcessRow(x, premulAbsorb.Span); + }); + } + + private static void ComputePremulAbsorb(float intensityTarget, Span premulAbsorb) + { + float mul = intensityTarget / 255.0f; + int lanes = Vector.Count; + + for (int j = 0; j < 3; j++) + { + for (int i = 0; i < 3; i++) + { + float absorb = JxlOpsinAbsorbanceMatrix[j][i] * mul; + + premulAbsorb + .Slice(((j * 3) + i) * lanes, lanes) + .Fill(absorb); + } + } + + for (int i = 0; i < 3; i++) + { + float negBiasCbrt = -MathF.Cbrt(JxlOpsinAbsorbanceBias[i]); + + premulAbsorb + .Slice((9 + i) * lanes, lanes) + .Fill(negBiasCbrt); + } + } + + public static bool ToXyb( + Configuration configuration, + JxlColorEncoding currentEncoding, + float intensityTarget, + JxlImageF? black, + JxlImage3F image, + JxlCmsInterface cms, + JxlImage3F? linear) + { + if (black is not null) + { + if (image.GetRectangle() != black.GetRectangle()) + { + return false; + } + } + + if (linear is not null) + { + if (image.GetRectangle() != linear.GetRectangle()) + { + return false; + } + } + + int lanes = Vector.Count; + + Memory premulAbsorb = new float[lanes * 12]; + ComputePremulAbsorb(intensityTarget, premulAbsorb.Span); + + bool wantLinear = linear is not null; + + JxlColorEncoding linearSrgb = JxlColorEncoding.LinearSRgb(currentEncoding.IsGray); + + if (linearSrgb.SameColorEncoding(currentEncoding)) + { + if (wantLinear) + { + JxlImageOperations.CopyImageTo(image, linear!); + } + + LinearSrgbToXyb(configuration, premulAbsorb, image); + return true; + } + + if (currentEncoding.IsSRgb) + { + if (wantLinear) + { + SrgbToXybAndLinear(configuration, premulAbsorb, image, linear!); + return true; + } + + SRgbToXyb(configuration, premulAbsorb, image); + return true; + } + + // TODO + ApplyColorTransform( + configuration, + currentEncoding, + intensityTarget, + image, + black, + image.GetRectangle(), + linearSrgb, + cms, + wantLinear ? linear! : image); + + if (wantLinear) + { + JxlImageOperations.CopyImageTo(linear!, image); + } + + LinearSrgbToXyb(configuration, premulAbsorb, image); + return true; + } + + public static void RgbToYcbcr( + Configuration configuration, + JxlImageF rPlane, + JxlImageF gPlane, + JxlImageF bPlane, + JxlImageF yPlane, + JxlImageF cbPlane, + JxlImageF crPlane) + { + int xSize = rPlane.Width; + int ySize = rPlane.Height; + + if (xSize == 0 || ySize == 0) + { + return; + } + + Vector k128 = new(128.0f / 255); + Vector kR = new(0.299f); + Vector kG = new(0.587f); + Vector kB = new(0.114f); + Vector kAmpR = new(0.701f); + Vector kAmpB = new(0.886f); + + Vector kDiffR = kAmpR + kR; + Vector kDiffB = kAmpB + kB; + + Vector kNormR = Vector.One / (kAmpR + kG + kB); + Vector kNormB = Vector.One / (kR + kG + kAmpB); + + int step = Vector.Count; + + int groupArea = JxlFrameDimensions.GroupDimensions * JxlFrameDimensions.GroupDimensions; + int linesPerGroup = JxlMath.DivCeil(groupArea, xSize); + int numStripes = JxlMath.DivCeil(ySize, linesPerGroup); + + void Transform(int index) + { + int y0 = index * linesPerGroup; + int y1 = Math.Min(y0 + linesPerGroup, ySize); + + for (int y = y0; y < y1; y++) + { + ReadOnlySpan rRow = rPlane.GetRow(y); + ReadOnlySpan gRow = gPlane.GetRow(y); + ReadOnlySpan bRow = bPlane.GetRow(y); + + Span yRow = yPlane.GetRow(y); + Span cbRow = cbPlane.GetRow(y); + Span crRow = crPlane.GetRow(y); + + for (int x = 0; x < xSize; x += step) + { + Vector r = new(rRow[x..]); + Vector g = new(gRow[x..]); + Vector b = new(bRow[x..]); + + Vector rBase = r * kR; + Vector rDiff = r * kDiffR; + Vector gBase = g * kG; + Vector bBase = b * kB; + Vector bDiff = b * kDiffB; + + Vector yBase = rBase + gBase + bBase; + Vector yVec = yBase - k128; + Vector cbVec = (bDiff - yBase) * kNormB; + Vector crVec = (rDiff - yBase) * kNormR; + + yVec.CopyTo(yRow[x..]); + cbVec.CopyTo(cbRow[x..]); + crVec.CopyTo(crRow[x..]); + } + } + } + + _ = Parallel.For(0, numStripes, configuration.GetParallelOptions(), Transform); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageFeatures.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageFeatures.cs index eb0e4581da..2bbfeb2fdf 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageFeatures.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageFeatures.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Image; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs index af665db380..89badfa498 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs @@ -364,6 +364,20 @@ internal static class JxlImageOperations } } + /// + /// Fills every value in every plane of the image with . + /// + /// The type of the image. + /// The value to fill every plane with. + /// The image to fill. + public static void FillImage(T value, JxlImage3 image) + where T : unmanaged + { + FillImage(value, image.Plane(0)); + FillImage(value, image.Plane(1)); + FillImage(value, image.Plane(2)); + } + /// /// Sets every value in the plane to 0. See also . /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs index 00c162be9a..c240746a3a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs index 2baad82d6d..233a98549d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs @@ -263,10 +263,10 @@ internal static class JxlConvolve } public static void SlowSymmetric3( - JxlImageF input, + JxlPlane input, Rectangle rect, JxlWeightsSymmetric3 weights, - JxlImageF output) + JxlPlane output) { int width = rect.Width; int height = rect.Height; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaDecoder.cs index bec5061305..cfe5be459e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlMaDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using Tree = System.Collections.Generic.List; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs index e1dabc6534..a63960f038 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/JxlModularEncoding.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs index 8f04197ee5..e5b45f7344 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs @@ -23,7 +23,7 @@ internal sealed class JxlXorShift } } - public void XorShift128Plus(uint seed1, uint seed2, uint seed3, uint seed4) + public JxlXorShift(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); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/RectangleUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/RectangleUtils.cs index 072b51983f..5226e7561a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/RectangleUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/RectangleUtils.cs @@ -5,13 +5,13 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; internal static class RectangleUtils { - public static int X0(in Rectangle rect) => rect.X; + public static int X0(this in Rectangle rect) => rect.X; - public static int Y0(in Rectangle rect) => rect.Y; + public static int Y0(this in Rectangle rect) => rect.Y; - public static int X1(in Rectangle rect) => rect.X + rect.Width; + public static int X1(this in Rectangle rect) => rect.X + rect.Width; - public static int Y1(in Rectangle rect) => rect.Y + rect.Height; + public static int Y1(this in Rectangle rect) => rect.Y + rect.Height; public static Rectangle Extend(Rectangle curr, int border, Rectangle parent) { @@ -22,4 +22,18 @@ internal static class RectangleUtils return new(newX0, newY0, newX1 - newX0, newY1 - newY0); } + + public static bool IsInside(this Rectangle a, Rectangle b) => + a.X0() >= b.X0() && + a.X1() <= b.X1() && + a.Y0() >= b.Y0() && + a.Y1() <= b.Y1(); + + public static Rectangle CreateRectangle(int xbegin, int ybegin, int xSizeMax, int ySizeMax, int xEnd, int yEnd) + => new(xbegin, ybegin, ClampedSize(xbegin, xSizeMax, xEnd), ClampedSize(ybegin, ySizeMax, yEnd)); + + private static int ClampedSize(int begin, int sizeMax, int end) + => begin + sizeMax <= end + ? sizeMax + : (end > begin ? end - begin : 0); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs index f2f2f0054f..18660007fc 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Patch; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs index 0a06b60da7..e87f08a09a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs index 52e83b4bb4..c40462b8c5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/AlphaBlendingTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/AlphaBlendingTests.cs new file mode 100644 index 0000000000..57e3a8a629 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/AlphaBlendingTests.cs @@ -0,0 +1,332 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; + +public class AlphaBlendingTests +{ + private static float[] RgbToArray(Rgb rgb) => [rgb.R, rgb.G, rgb.B]; + + private static readonly float[] Expected = [77.2f, 83.0f, 90.6f]; + + [Fact] + public void BlendingWithNonPremultiplied() + { + Rgb bgRgb = new(100, 110, 120); + float bgA = 180f / 255; + + Rgb fgRgb = new(25, 21, 23); + float fgA = 15420f / 65535; + float fgA2 = 2.0f; + + float[] outR = [0]; + float[] outG = [0]; + float[] outB = [0]; + float[] outAlpha = [0]; + + JxlAlphaBlendingInputLayer inputBg = new() + { + R = [bgRgb.R], + G = [bgRgb.G], + B = [bgRgb.B], + A = [bgA], + }; + + JxlAlphaBlendingInputLayer inputFg = new() + { + R = [fgRgb.R], + G = [fgRgb.G], + B = [fgRgb.B], + A = [fgA], + }; + + JxlAlphaBlendingOutput output = new() + { + R = outR, + G = outG, + B = outB, + A = outAlpha, + }; + + JxlAlphaHelper.PerformAlphaBlending( + inputBg, + inputFg, + output, + 1, + alphaIsPremultiplied: false, + clamp: false); + + Assert.Equal( + Expected, + [outR[0], outG[0], outB[0]], + new ApproximateFloatComparer(0.05f)); + + Assert.True(MathF.Abs(outAlpha[0] - (3174f / 4095)) <= 1e-5f); + + inputFg = new() + { + R = [fgRgb.R], + G = [fgRgb.G], + B = [fgRgb.B], + A = [fgA2], + }; + + JxlAlphaHelper.PerformAlphaBlending( + inputBg, + inputFg, + output, + 1, + alphaIsPremultiplied: false, + clamp: true); + + Assert.Equal( + RgbToArray(fgRgb), + [outR[0], outG[0], outB[0]], + new ApproximateFloatComparer(0.05f)); + + Assert.True(MathF.Abs(outAlpha[0] - 1.0f) <= 1e-5f); + } + + [Fact] + public void BlendingWithPremultiplied() + { + Rgb bgRgb = new(100, 110, 120); + float bgA = 180f / 255; + + Rgb fgRgb = new(25, 21, 23); + float fgA = 15420f / 65535; + float fgA2 = 2.0f; + + float[] outR = [0]; + float[] outG = [0]; + float[] outB = [0]; + float[] outAlpha = [0]; + + JxlAlphaBlendingInputLayer inputBg = new() + { + R = [bgRgb.R], + G = [bgRgb.G], + B = [bgRgb.B], + A = [bgA], + }; + + JxlAlphaBlendingInputLayer inputFg = new() + { + R = [fgRgb.R], + G = [fgRgb.G], + B = [fgRgb.B], + A = [fgA], + }; + + JxlAlphaBlendingOutput output = new() + { + R = outR, + G = outG, + B = outB, + A = outAlpha, + }; + + JxlAlphaHelper.PerformAlphaBlending( + inputBg, + inputFg, + output, + 1, + alphaIsPremultiplied: true, + clamp: false); + + Assert.Equal( + new float[] { 101.5f, 105.1f, 114.8f }, + [outR[0], outG[0], outB[0]], + new ApproximateFloatComparer(0.05f)); + + Assert.True(MathF.Abs(outAlpha[0] - (3174f / 4095)) <= 1e-5f); + + inputFg = new() + { + R = [fgRgb.R], + G = [fgRgb.G], + B = [fgRgb.B], + A = [fgA2], + }; + + JxlAlphaHelper.PerformAlphaBlending( + inputBg, + inputFg, + output, + 1, + alphaIsPremultiplied: true, + clamp: true); + + Assert.Equal( + RgbToArray(fgRgb), + [outR[0], outG[0], outB[0]], + new ApproximateFloatComparer(0.05f)); + + Assert.True(MathF.Abs(outAlpha[0] - 1.0f) <= 1e-5f); + } + + [Fact] + public void Mul() + { + Span bg = [100]; + Span fg = [25]; + Span output = [0]; + + JxlAlphaHelper.PerformMultiplyBlending( + bg, + fg, + output, + 1, + clamp: false); + + Assert.True(MathF.Abs(output[0] - (fg[0] * bg[0])) <= 0.05f); + + JxlAlphaHelper.PerformMultiplyBlending( + bg, + fg, + output, + 1, + clamp: true); + + Assert.True(MathF.Abs(output[0] - bg[0]) <= 0.05f); + } + + [Fact] + public void PremultiplyAndUnpremultiply() + { + float[] alpha = + { + 0f, + 63f / 255, + 127f / 255, + 1f + }; + + float[] r = { 120, 130, 140, 150 }; + float[] g = { 124, 134, 144, 154 }; + float[] b = { 127, 137, 147, 157 }; + + JxlAlphaHelper.PremultiplyAlpha(r, g, b, alpha, alpha.Length); + + Assert.Equal( + new[] + { + 0f, + 130 * 63f / 255, + 140 * 127f / 255, + 150f + }, + r, + new ApproximateFloatComparer(1e-5f)); + + Assert.Equal( + new[] + { + 0f, + 134 * 63f / 255, + 144 * 127f / 255, + 154f + }, + g, + new ApproximateFloatComparer(1e-5f)); + + Assert.Equal( + new[] + { + 0f, + 137 * 63f / 255, + 147 * 127f / 255, + 157f + }, + b, + new ApproximateFloatComparer(1e-5f)); + + JxlAlphaHelper.UnpremultiplyAlpha(r, g, b, alpha, alpha.Length); + + Assert.Equal( + new[] { 120f, 130f, 140f, 150f }, + r, + new ApproximateFloatComparer(1e-4f)); + + Assert.Equal( + new[] { 124f, 134f, 144f, 154f }, + g, + new ApproximateFloatComparer(1e-4f)); + + Assert.Equal( + new[] { 127f, 137f, 147f, 157f }, + b, + new ApproximateFloatComparer(1e-4f)); + } + + [Fact] + public void UnpremultiplyAndPremultiply() + { + float[] alpha = + { + 0f, + 63f / 255, + 127f / 255, + 1f + }; + + float[] r = { 50, 60, 70, 80 }; + float[] g = { 54, 64, 74, 84 }; + float[] b = { 57, 67, 77, 87 }; + + JxlAlphaHelper.UnpremultiplyAlpha(r, g, b, alpha, alpha.Length); + + Assert.Equal( + new[] + { + 50f * (1 << 26), + 60 * 255f / 63, + 70 * 255f / 127, + 80f + }, + r, + new ApproximateFloatComparer(1e-4f)); + + Assert.Equal( + new[] + { + 54f * (1 << 26), + 64 * 255f / 63, + 74 * 255f / 127, + 84f + }, + g, + new ApproximateFloatComparer(1e-4f)); + + Assert.Equal( + new[] + { + 57f * (1 << 26), + 67 * 255f / 63, + 77 * 255f / 127, + 87f + }, + b, + new ApproximateFloatComparer(1e-4f)); + + JxlAlphaHelper.PremultiplyAlpha(r, g, b, alpha, alpha.Length); + + Assert.Equal( + new[] { 50f, 60f, 70f, 80f }, + r, + new ApproximateFloatComparer(1e-4f)); + + Assert.Equal( + new[] { 54f, 64f, 74f, 84f }, + g, + new ApproximateFloatComparer(1e-4f)); + + Assert.Equal( + new[] { 57f, 67f, 77f, 87f }, + b, + new ApproximateFloatComparer(1e-4f)); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/LinearAlgebraTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/LinearAlgebraTests.cs new file mode 100644 index 0000000000..e131952743 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/LinearAlgebraTests.cs @@ -0,0 +1,169 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; +using Matrix2x2 = System.Runtime.CompilerServices.InlineArray2>; +using Vector2 = System.Runtime.CompilerServices.InlineArray2; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing.Encoder; + +public class LinearAlgebraTests +{ + private static Vector2 CreateVector2(double a, double b) + { + Vector2 v2 = default; + + v2[0] = a; + v2[1] = b; + + return v2; + } + + private static Matrix2x2 Diagonal(Vector2 d) + { + Matrix2x2 result = default; + + result[0][0] = d[0]; + result[0][1] = 0; + result[1][0] = 0; + result[1][1] = d[1]; + + return result; + } + + private static Matrix2x2 Identity() => Diagonal(CreateVector2(1.0f, 1.0f)); + + private static Matrix2x2 MatrixMultiply(Matrix2x2 a, Matrix2x2 b) + { + Matrix2x2 result = default; + + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < 2; x++) + { + result[y][x] = (a[0][x] * b[y][0]) + (a[1][x] * b[y][1]); + } + } + + return result; + } + + private static Matrix2x2 Transpose(Matrix2x2 a) + { + Matrix2x2 result = default; + + result[0] = CreateVector2(a[0][0], a[1][0]); + result[1] = CreateVector2(a[0][1], a[1][1]); + + return result; + } + + private static Matrix2x2 RandomSymmetricMatrix( + Rng rng, + float vmin, + float vmax) + { + Matrix2x2 result = default; + + result[0][0] = rng.UniformF(vmin, vmax); + result[0][1] = rng.UniformF(vmin, vmax); + result[1][0] = result[0][1]; + result[1][1] = rng.UniformF(vmin, vmax); + + return result; + } + + private static void VerifyMatrixEqual(Matrix2x2 a, Matrix2x2 b, float epsilon) + { + TolerantMath comparer = new(epsilon); + + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < 2; x++) + { + Assert.True( + comparer.AreEqual(a[y][x], b[y][x]), + $"Matrices differ at [{y}][{x}]: {a[y][x]} != {b[y][x]}"); + } + } + } + + private static void VerifyOrthogonal(Matrix2x2 a, float epsilon) => VerifyMatrixEqual( + Identity(), + MatrixMultiply(Transpose(a), a), + epsilon); + + [Fact] + public void ConvertToDiagonal() + { + { + Matrix2x2 i = Identity(); + Matrix2x2 u = default; + Vector2 d = default; + + JxlLinearAlgebra.ConvertToDiagonal(i, ref d, ref u); + + VerifyMatrixEqual(i, u, 1e-15f); + + for (int k = 0; k < 2; k++) + { + Assert.True( + Math.Abs(d[k] - 1.0f) <= 1e-15f, + $"d[{k}] = {d[k]}"); + } + } + + { + Matrix2x2 a = Identity(); + a[0][1] = 2.0f; + a[1][0] = 2.0f; + + Matrix2x2 u = default; + Vector2 d = default; + + JxlLinearAlgebra.ConvertToDiagonal(a, ref d, ref u); + + VerifyOrthogonal(u, 1e-12f); + VerifyMatrixEqual( + a, + MatrixMultiply(u, MatrixMultiply(Diagonal(d), Transpose(u))), + 1e-12f); + } + + { + Matrix2x2 a = default; + a[0] = CreateVector2(0.000208649f, 1.13687e-12f); + a[1] = CreateVector2(1.13687e-12f, 0.000208649f); + + Matrix2x2 u = default; + Vector2 d = default; + + JxlLinearAlgebra.ConvertToDiagonal(a, ref d, ref u); + + VerifyOrthogonal(u, 1e-12f); + VerifyMatrixEqual( + a, + MatrixMultiply(u, MatrixMultiply(Diagonal(d), Transpose(u))), + 1e-11f); + } + + { + Rng rng = new(0); + + for (int i = 0; i < 1_000_000; i++) + { + Matrix2x2 a = RandomSymmetricMatrix(rng, -1.0f, 1.0f); + Matrix2x2 u = default; + Vector2 d = default; + + JxlLinearAlgebra.ConvertToDiagonal(a, ref d, ref u); + + VerifyOrthogonal(u, 1e-12f); + VerifyMatrixEqual( + a, + MatrixMultiply(u, MatrixMultiply(Diagonal(d), Transpose(u))), + 5e-10f); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs new file mode 100644 index 0000000000..d3dce2c713 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/EntropyCoderTests.cs @@ -0,0 +1,105 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder.Ans; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; + +public class EntropyCoderTests +{ + [Fact] + public void PackUnpack() + { + for (int i = -31; i < 32; i++) + { + uint packed = JxlPackSigned.PackUnsigned(i); + + Assert.True(packed < 63u); + + int unpacked = JxlPackSigned.UnpackSigned(packed); + + Assert.Equal(i, unpacked); + } + } + + private sealed class MockBitReader : JxlBitReader + { + public ulong NBits { get; set; } + + public ulong Bits { get; set; } + + public MockBitReader(Stream stream) + : base(stream) + { + } + + public override uint PeekBits32(uint n) + { + Assert.Equal(this.NBits, n); + return (uint)this.Bits; + } + + public override uint ReadBits32(uint n) + { + Assert.Equal(this.NBits, n); + return (uint)this.Bits; + } + } + + private static void HybridUintRoundtrip( + JxlAnsHybridUIntConfiguration config, + uint limit = 1u << 24) + { + Rng rng = new(0); + + const int numIntegers = 1 << 20; + + uint[] integers = new uint[numIntegers]; + uint[] token = new uint[numIntegers]; + uint[] nbits = new uint[numIntegers]; + uint[] bits = new uint[numIntegers]; + + for (int i = 0; i < numIntegers; i++) + { + integers[i] = (uint)rng.UniformU(0, limit + 1); + config.Encode( + integers[i], + out token[i], + out nbits[i], + out bits[i]); + } + + for (int i = 0; i < numIntegers; i++) + { + MockBitReader br = new(Stream.Null) + { + NBits = nbits[i], + Bits = bits[i] + }; + + Assert.Equal( + integers[i], + JxlAnsSymbolReader.ReadHybridUintConfig( + config, + token[i], + ref br)); + } + } + + [Fact] + public void Test000() => HybridUintRoundtrip(new JxlAnsHybridUIntConfiguration(0, 0, 0)); + + [Fact] + public void Test411() => HybridUintRoundtrip(new JxlAnsHybridUIntConfiguration(4, 1, 1)); + + [Fact] + public void Test420() => HybridUintRoundtrip(new JxlAnsHybridUIntConfiguration(4, 2, 0)); + + [Fact] + public void Test421() => HybridUintRoundtrip( + new JxlAnsHybridUIntConfiguration(4, 2, 1), + 256); +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/GaborishTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/GaborishTests.cs new file mode 100644 index 0000000000..67461ddc89 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/GaborishTests.cs @@ -0,0 +1,113 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; + +public class GaborishTests +{ + private static JxlWeightsSymmetric3 GaborishKernel(float weight1, float weight2) + { + const float weight0 = 1.0f; + + // Normalize. + float mul = 1.0f / (weight0 + (4.0f * (weight1 + weight2))); + float w0 = weight0 * mul; + float w1 = weight1 * mul; + float w2 = weight2 * mul; + + JxlWeightsSymmetric3 wgt = new(); + wgt.SetC(Vector128.Create(w0)); + wgt.SetD(Vector128.Create(w1)); + wgt.SetR(Vector128.Create(w2)); + + return wgt; + } + + private static void ConvolveGaborish( + JxlPlane input, + float weight1, + float weight2, + JxlPlane output) + { + Assert.True(JxlImageOperations.SameSize(input, output)); + + // The test will fail if this throws. + JxlConvolve.SlowSymmetric3( + input, + input.GetRectangle(), + GaborishKernel(weight1, weight2), + output); + } + + private static void TestRoundTrip( + JxlImage3F input, + float maxL1) + { + JxlImage3F fwd = new(TestEnvironment.Configuration, input.XSize, input.YSize); + + ConvolveGaborish( + input.Plane(0), + 0, + 0, + fwd.Plane(0)); + + ConvolveGaborish( + input.Plane(1), + 0, + 0, + fwd.Plane(1)); + + ConvolveGaborish( + input.Plane(2), + 0, + 0, + fwd.Plane(2)); + + const float w = 0.92718927264540152f; + + InlineArray3 weights = default; + weights[0] = w; + weights[1] = w; + weights[2] = w; + + // The test will fail if this throws. + JxlGaborish.InverseGaborish( + TestEnvironment.Configuration, + fwd, + fwd.GetRectangle(), + weights); + + // TODO: VerifyRelativeError + Assert.True( + VerifyRelativeError( + input, + fwd, + maxL1, + 1e-4f)); + } + + [Fact] + public void TestZero() + { + JxlImage3F input = new(TestEnvironment.Configuration, 20, 20); + JxlImageOperations.ZeroFillImage(input); + TestRoundTrip(input, 0.0f); + } + + [Fact] + public void TestFlat() + { + JxlImage3F input = new(TestEnvironment.Configuration, 20, 20); + JxlImageOperations.FillImage(1.0f, input); + TestRoundTrip(input, 1e-5f); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ImageOperationsTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ImageOperationsTests.cs new file mode 100644 index 0000000000..a7751b2c49 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ImageOperationsTests.cs @@ -0,0 +1,146 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; + +public class ImageOperationsTests +{ + private static void TestFillImpl(JxlImage3 image, string layout) + where T : unmanaged + { + JxlImageOperations.FillImage((T)Convert.ChangeType(1, typeof(T), null), image); + + for (int y = 0; y < image.YSize; y++) + { + for (int c = 0; c < 3; c++) + { + Span row = image.PlaneRow(c, y); + + for (int x = 0; x < image.XSize; x++) + { + if (!EqualityComparer.Default.Equals( + row[x], + (T)Convert.ChangeType(1, typeof(T), null))) + { + Assert.Fail( + $"Not 1 at c={c} {x}, {y} " + + $"({image.XSize} x {image.YSize}) ({layout})"); + } + + row[x] = (T)Convert.ChangeType(2, typeof(T), null); + } + } + } + + JxlImageOperations.ZeroFillImage(image); + + for (int c = 0; c < 3; c++) + { + for (int y = 0; y < image.YSize; y++) + { + Span row = image.PlaneRow(c, y); + + for (int x = 0; x < image.XSize; x++) + { + if (!EqualityComparer.Default.Equals( + row[x], + default)) + { + Assert.Fail( + $"Not 0 at c={c} {x}, {y} " + + $"({image.XSize} x {image.YSize}) ({layout})"); + } + + row[x] = (T)Convert.ChangeType(3, typeof(T), null); + } + } + } + } + + private static void TestFillT() + where T : unmanaged + { + foreach (uint xSize in new uint[] { 0, 1, 15, 16, 31, 32 }) + { + foreach (uint ySize in new uint[] { 0, 1, 15, 16, 31, 32 }) + { + JxlImage3 image = JxlImage3.Create(TestEnvironment.Configuration, (int)xSize, (int)ySize); + + TestFillImpl(image, "size ctor"); + } + } + } + + [Fact] + public void TestFill() + { + TestFillT(); + TestFillT(); + TestFillT(); + } + + [Fact] + public void CopyImageToWithPaddingTest() + { + JxlPlane src = JxlPlane.Create(TestEnvironment.Configuration, 100, 61); + + for (int y = 0; y < src.YSize; y++) + { + Span row = src.GetRow(y); + + for (int x = 0; x < src.XSize; x++) + { + row[x] = (uint)((x * 1000) + y); + } + } + + Rectangle srcRect = new(10, 20, 30, 40); + Assert.True(srcRect.IsInside(src.GetRectangle())); + + JxlPlane dst = JxlPlane.Create(TestEnvironment.Configuration, 60, 50); + + JxlImageOperations.FillImage(0u, dst); + + Rectangle dstRect = new(20, 5, 30, 40); + Assert.True(dstRect.IsInside(dst.GetRectangle())); + + Assert.True( + JxlImageOperations.CopyImageToWithPadding( + srcRect, + src, + padding: 2, + dstRect, + dst)); + + Rectangle paddedDstRect = new( + 20 - 2, + 5 - 2, + 30 + 4, + 40 + 3); + + for (int y = 0; y < dst.YSize; y++) + { + Span row = dst.GetRow(y); + + for (int x = 0; x < dst.XSize; x++) + { + if (new Rectangle(x, y, 1, 1).IsInside(paddedDstRect)) + { + Assert.Equal( + (uint)( + ((x - dstRect.X0() + srcRect.X0()) * 1000) + + (y - dstRect.Y0() + srcRect.Y0())), + row[x]); + } + else + { + Assert.Equal(0u, row[x]); + } + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/TestUtils/TestDCT.cs b/tests/ImageSharp.Tests/Formats/Jxl/TestUtils/TestDCT.cs new file mode 100644 index 0000000000..244b4c9076 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/TestUtils/TestDCT.cs @@ -0,0 +1,108 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.TestUtils; + +/// +/// This is a naive Discrete Cosine Transform, without any optimization. +/// It is a reference to test against the optimized DCT. +/// +internal static class TestDCT +{ + private static double Alpha(int u) => u == 0 ? 0.7071067811865475 : 1.0; + + public static void Dct1D(ReadOnlySpan block, Span output, int n, int m) + { + Span matrix = stackalloc double[n * n]; + double scale = Math.Sqrt(2.0) / n; + + for (int y = 0; y < n; y++) + { + for (int u = 0; u < n; u++) + { + matrix[(n * u) + y] = + Alpha(u) * + Math.Cos((y + 0.5) * u * Math.PI / n) * + scale; + } + } + + for (int x = 0; x < m; x++) + { + for (int u = 0; u < n; u++) + { + output[(m * u) + x] = 0; + + for (int y = 0; y < n; y++) + { + output[(m * u) + x] += matrix[(n * u) + y] * block[(m * y) + x]; + } + } + } + } + + public static void Idct1D(ReadOnlySpan block, Span output, int n, int m) + { + Span matrix = stackalloc double[n * n]; + double scale = Math.Sqrt(2.0); + + for (int y = 0; y < n; y++) + { + for (int u = 0; u < n; u++) + { + matrix[(n * y) + u] = + Alpha(u) * + Math.Cos((y + 0.5) * u * Math.PI / n) * + scale; + } + } + + for (int x = 0; x < m; x++) + { + for (int u = 0; u < n; u++) + { + output[(m * u) + x] = 0; + + for (int y = 0; y < n; y++) + { + output[(m * u) + x] += matrix[(n * u) + y] * block[(m * y) + x]; + } + } + } + } + + public static void TransposeBlock(ReadOnlySpan input, Span output, int n, int m) + { + for (int x = 0; x < n; x++) + { + for (int y = 0; y < m; y++) + { + output[(y * n) + x] = input[(x * m) + y]; + } + } + } + + public static void DctSlow(Span block, int n) + { + int blockSize = n * n; + Span g = stackalloc double[blockSize]; + + Dct1D(block, g, n, n); + TransposeBlock(g, block, n, n); + + Dct1D(block, g, n, n); + TransposeBlock(g, block, n, n); + } + + public static void IdctSlow(Span block, int n) + { + int blockSize = n * n; + Span g = stackalloc double[blockSize]; + + Idct1D(block, g, n, n); + TransposeBlock(g, block, n, n); + + Idct1D(block, g, n, n); + TransposeBlock(g, block, n, n); + } +}