From 85c851b137c26cb5bacd540480ef6cc132d5a1c8 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 14 Jul 2026 22:51:21 +0400 Subject: [PATCH 001/142] Add JPEG XL format --- src/ImageSharp/Formats/Jxl/JxlFormat.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/JxlFormat.cs diff --git a/src/ImageSharp/Formats/Jxl/JxlFormat.cs b/src/ImageSharp/Formats/Jxl/JxlFormat.cs new file mode 100644 index 0000000000..d5dfa802b1 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/JxlFormat.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; + +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" }; +} From 996e93dba2f3c6810b166c98b1e5e4faea0afaaf Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 14 Jul 2026 23:26:11 +0400 Subject: [PATCH 002/142] Implement ac_context.h --- src/ImageSharp/Formats/Jxl/Ac/JxlAcContext.cs | 53 ++++++++++++++ .../Formats/Jxl/Ac/JxlBlockContextMap.cs | 72 +++++++++++++++++++ .../JxlForwardCoefficientOrder.cs | 24 +++++++ src/ImageSharp/Formats/Jxl/JxlFormat.cs | 2 - 4 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Ac/JxlAcContext.cs create mode 100644 src/ImageSharp/Formats/Jxl/Ac/JxlBlockContextMap.cs create mode 100644 src/ImageSharp/Formats/Jxl/Coefficients/JxlForwardCoefficientOrder.cs diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcContext.cs b/src/ImageSharp/Formats/Jxl/Ac/JxlAcContext.cs new file mode 100644 index 0000000000..98f86534b2 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Ac/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.Ac; + +/// +/// 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/Ac/JxlBlockContextMap.cs b/src/ImageSharp/Formats/Jxl/Ac/JxlBlockContextMap.cs new file mode 100644 index 0000000000..3b7e75605f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Ac/JxlBlockContextMap.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.Coefficients; + +namespace SixLabors.ImageSharp.Formats.Jxl.Ac; + +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/Coefficients/JxlForwardCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Coefficients/JxlForwardCoefficientOrder.cs new file mode 100644 index 0000000000..04cacb176a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Coefficients/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.Coefficients; + +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/JxlFormat.cs b/src/ImageSharp/Formats/Jxl/JxlFormat.cs index d5dfa802b1..00e7b66eb5 100644 --- a/src/ImageSharp/Formats/Jxl/JxlFormat.cs +++ b/src/ImageSharp/Formats/Jxl/JxlFormat.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Collections.Generic; - namespace SixLabors.ImageSharp.Formats.Jxl; internal class JxlFormat : IImageFormat From d87178fd370387fac07bfd1faa033c2a719b1580 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 14 Jul 2026 23:40:34 +0400 Subject: [PATCH 003/142] Implemented frame_dimensions.h plus part of ac_strategy.h --- .../Formats/Jxl/Ac/JxlAcStrategyType.cs | 67 +++++++++++++++++++ .../Formats/Jxl/JxlFrameDimensions.cs | 65 ++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyType.cs create mode 100644 src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyType.cs b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyType.cs new file mode 100644 index 0000000000..3412d10f46 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyType.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Ac; + +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/JxlFrameDimensions.cs b/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs new file mode 100644 index 0000000000..accd7d68cb --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl; + +internal struct JxlFrameDimensions +{ + public const int BlockDimensions = 8; + public const int DctBlockSize = BlockDimensions * BlockDimensions; + public const int GroupDimensions = 256; + public const int GroupDimensionsInBlocks = GroupDimensions / BlockDimensions; + + public int XSize; + public int YSize; + public int XSizeUpsampled; + public int YSizeUpsampled; + public int XSizeUpsampledPadded; + public int YSizeUpsampledPadded; + public int XSizePadded; + public int YSizePadded; + public int XSizeBlocks; + public int YSizeBlocks; + public int XSizeGroups; + public int YSizeGroups; + public int XSizeDcGroups; + public int YSizeDcGroups; + public int NumGroups; + public int NumDcGroups; + public int GroupDimension; + public int DcGroupDimension; + + 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; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int DivCeil(int x, int y) => x / y; +} From 1ac61405358cc85f7571fee827860c6fccdf04ee Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 15 Jul 2026 14:44:57 +0400 Subject: [PATCH 004/142] Implement AC strategy Implementation of ac_strategy.h and ac_strategy.c --- .../Formats/Jxl/Ac/JxlAcStrategy.cs | 186 ++++++++++++++++++ .../Formats/Jxl/Ac/JxlAcStrategyImage.cs | 114 +++++++++++ .../Formats/Jxl/Ac/JxlAcStrategyRow.cs | 31 +++ 3 files changed, 331 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategy.cs create mode 100644 src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs create mode 100644 src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyRow.cs diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategy.cs b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategy.cs new file mode 100644 index 0000000000..7c704d8e91 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategy.cs @@ -0,0 +1,186 @@ +// 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.Ac; + +[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 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/Ac/JxlAcStrategyImage.cs b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs new file mode 100644 index 0000000000..0ffeabe32b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs @@ -0,0 +1,114 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Ac; + +internal sealed class JxlAcStrategyImage +{ + private const byte Invalid = byte.MaxValue; // 255 + + private JxlImageB? layers; + private readonly Memory row; + private readonly int stride; + + public JxlMemoryManager MemoryManager => this.layers.MemoryManager; + + 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.GetRow(y); + ReadOnlyMemory row = layerRow[xPrefix..]; + + return new JxlAcStrategyRow(row); + } + + public static JxlAcStrategyImage Create(JxlMemoryManager memoryManager, int xSize, int ySize) + { + JxlAcStrategyImage image = new() + { + layers = JxlImageB.Create(memoryManager, xSize, ySize) + }; + + image.row = image.layers.GetRow(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.GetRowSpan(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); +} diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyRow.cs b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyRow.cs new file mode 100644 index 0000000000..668876f711 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Ac/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.Ac; + +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); + } + } +} From 31b5505d8987a4beb21f14d47dd700ab763b1e8d Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 15 Jul 2026 14:51:15 +0400 Subject: [PATCH 005/142] Implement JxlMemoryManager For now JxlMemoryManager will be a wrapper around MemoryPool. --- .../Formats/Jxl/Ac/JxlAcStrategyImage.cs | 1 + .../Formats/Jxl/Memory/IJxlMemoryManager.cs | 12 ++++++++++++ .../Formats/Jxl/Memory/JxlMemoryManager.cs | 18 ++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Memory/IJxlMemoryManager.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/JxlMemoryManager.cs diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs index 0ffeabe32b..f41d186e19 100644 --- a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs +++ b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Diagnostics; +using SixLabors.ImageSharp.Formats.Jxl.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Ac; diff --git a/src/ImageSharp/Formats/Jxl/Memory/IJxlMemoryManager.cs b/src/ImageSharp/Formats/Jxl/Memory/IJxlMemoryManager.cs new file mode 100644 index 0000000000..c808388d28 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/IJxlMemoryManager.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory; + +internal interface IJxlMemoryManager +{ + IMemoryOwner Allocate(int size); + IMemoryOwner Allocate(int size); +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryManager.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryManager.cs new file mode 100644 index 0000000000..778ef59106 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryManager.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; + +namespace SixLabors.ImageSharp.Formats.Jxl.Memory; + +/// +/// Allows allocation and deallocation of managed memory. +/// +internal sealed class JxlMemoryManager : IJxlMemoryManager +{ + public static readonly JxlMemoryManager Instance = new(); + + public IMemoryOwner Allocate(int size) => MemoryPool.Shared.Rent(size); + + public IMemoryOwner Allocate(int size) => this.Allocate(size); +} From 21ff33f23c54224f9c8e3854adcf5b24550e0390 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:38:16 +0400 Subject: [PATCH 006/142] Implement image and memory buffers Implementation of image.h and image.c; AC strategy implementation was slightly adjusted to reduce errors. --- .../Formats/Jxl/Ac/JxlAcStrategyImage.cs | 38 +++--- .../Formats/Jxl/Memory/IJxlMemoryManager.cs | 12 -- .../Jxl/Memory/ImageTypes/JxlImage3B.cs | 14 +++ .../Jxl/Memory/ImageTypes/JxlImage3F.cs | 14 +++ .../Jxl/Memory/ImageTypes/JxlImage3I.cs | 14 +++ .../Jxl/Memory/ImageTypes/JxlImage3S.cs | 14 +++ .../Jxl/Memory/ImageTypes/JxlImage3U.cs | 14 +++ .../Jxl/Memory/ImageTypes/JxlImageB.cs | 34 ++++++ .../Jxl/Memory/ImageTypes/JxlImageF.cs | 23 ++++ .../Jxl/Memory/ImageTypes/JxlImageI.cs | 23 ++++ .../Jxl/Memory/ImageTypes/JxlImageS.cs | 23 ++++ .../Jxl/Memory/ImageTypes/JxlImageSB.cs | 23 ++++ .../Jxl/Memory/ImageTypes/JxlImageU.cs | 23 ++++ .../Formats/Jxl/Memory/JxlImage3{T}.cs | 85 +++++++++++++ .../Formats/Jxl/Memory/JxlMemoryManager.cs | 18 --- .../Formats/Jxl/Memory/JxlPlaneBase.cs | 115 ++++++++++++++++++ .../Formats/Jxl/Memory/JxlPlane{T}.cs | 36 ++++++ 17 files changed, 476 insertions(+), 47 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jxl/Memory/IJxlMemoryManager.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageF.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageI.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageS.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageSB.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageU.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Memory/JxlMemoryManager.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs create mode 100644 src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs index f41d186e19..0b2bdb74ae 100644 --- a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs +++ b/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs @@ -2,42 +2,40 @@ // Licensed under the Six Labors Split License. using System.Diagnostics; -using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; namespace SixLabors.ImageSharp.Formats.Jxl.Ac; -internal sealed class JxlAcStrategyImage +internal sealed class JxlAcStrategyImage : IDisposable { private const byte Invalid = byte.MaxValue; // 255 private JxlImageB? layers; - private readonly Memory row; - private readonly int stride; + private Memory row; + private int stride; - public JxlMemoryManager MemoryManager => this.layers.MemoryManager; + public int XSize => this.layers!.XSize; - public int XSize => this.layers.XSize; + public int YSize => this.layers!.YSize; - public int YSize => this.layers.YSize; - - public int PixelsPerRow => this.layers.PixelsPerRow; + public int PixelsPerRow => this.layers!.PixelsPerRow; public JxlAcStrategyRow GetRow(int y, int xPrefix = 0) { - ReadOnlyMemory layerRow = this.layers.GetRow(y); + ReadOnlyMemory layerRow = this.layers!.GetRowBytesMemory(y); ReadOnlyMemory row = layerRow[xPrefix..]; return new JxlAcStrategyRow(row); } - public static JxlAcStrategyImage Create(JxlMemoryManager memoryManager, int xSize, int ySize) + public static JxlAcStrategyImage Create(Configuration memoryManager, int xSize, int ySize) { JxlAcStrategyImage image = new() { - layers = JxlImageB.Create(memoryManager, xSize, ySize) + layers = new JxlImageB(memoryManager, xSize, ySize) }; - image.row = image.layers.GetRow(0); + image.row = image.layers.GetRowBytesMemory(0); image.stride = image.layers.PixelsPerRow; return image; @@ -48,11 +46,11 @@ public int CountBlocks(JxlAcStrategyType type) int value = 0; int compare = ((int)type << 1) | 1; - for (int y = 0; y < this.layers.YSize; y++) + for (int y = 0; y < this.layers!.YSize; y++) { - ReadOnlySpan row = this.layers.GetRowSpan(y); + ReadOnlySpan row = this.layers!.GetRow(y); - for (int x = 0; x < this.layers.XSize; x++) + for (int x = 0; x < this.layers!.XSize; x++) { if (row[x] == compare) { @@ -100,7 +98,7 @@ 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(y + strategy.CoveredBlocksY <= this.layers!.YSize, "Invalid range"); Debug.Assert(x + strategy.CoveredBlocksX <= this.layers.XSize, "Invalid range"); #endif @@ -112,4 +110,10 @@ public bool Set(int x, int y, JxlAcStrategyType type) 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/Memory/IJxlMemoryManager.cs b/src/ImageSharp/Formats/Jxl/Memory/IJxlMemoryManager.cs deleted file mode 100644 index c808388d28..0000000000 --- a/src/ImageSharp/Formats/Jxl/Memory/IJxlMemoryManager.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; - -namespace SixLabors.ImageSharp.Formats.Jxl.Memory; - -internal interface IJxlMemoryManager -{ - IMemoryOwner Allocate(int size); - IMemoryOwner Allocate(int size); -} 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..f46f6767b8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs @@ -0,0 +1,85 @@ +// 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 + 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"); +} diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryManager.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryManager.cs deleted file mode 100644 index 778ef59106..0000000000 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlMemoryManager.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; - -namespace SixLabors.ImageSharp.Formats.Jxl.Memory; - -/// -/// Allows allocation and deallocation of managed memory. -/// -internal sealed class JxlMemoryManager : IJxlMemoryManager -{ - public static readonly JxlMemoryManager Instance = new(); - - public IMemoryOwner Allocate(int size) => MemoryPool.Shared.Rent(size); - - public IMemoryOwner Allocate(int size) => this.Allocate(size); -} 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); +} From 6fea920e7f20db23b57f7e7b4378d0a6ec265f2c Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 15 Jul 2026 17:52:21 +0400 Subject: [PATCH 007/142] Add metadata models --- src/ImageSharp/Formats/Jxl/IO/IJxlFields.cs | 17 +++ .../Formats/Jxl/Metadata/InlineArrays.cs | 41 ++++++ .../Formats/Jxl/Metadata/JxlBitDepth.cs | 42 ++++++ .../Formats/Jxl/Metadata/JxlCodecMetadata.cs | 57 ++++++++ .../Jxl/Metadata/JxlCustomTransformData.cs | 25 ++++ .../Jxl/Metadata/JxlExifOrientation.cs | 16 +++ .../Formats/Jxl/Metadata/JxlExtraChannel.cs | 25 ++++ .../Jxl/Metadata/JxlExtraChannelInfo.cs | 27 ++++ .../Formats/Jxl/Metadata/JxlImageMetadata.cs | 126 ++++++++++++++++++ .../Jxl/Metadata/JxlOpsinInverseMatrix.cs | 19 +++ .../Formats/Jxl/Metadata/JxlToneMapping.cs | 21 +++ 11 files changed, 416 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/IO/IJxlFields.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/InlineArrays.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlCodecMetadata.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannelInfo.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlImageMetadata.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlToneMapping.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/IJxlFields.cs b/src/ImageSharp/Formats/Jxl/IO/IJxlFields.cs new file mode 100644 index 0000000000..191cc96a3e --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/IJxlFields.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +/// +/// 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/Metadata/InlineArrays.cs b/src/ImageSharp/Formats/Jxl/Metadata/InlineArrays.cs new file mode 100644 index 0000000000..f1f9d3037b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/InlineArrays.cs @@ -0,0 +1,41 @@ +// 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.Metadata; + +[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; +} diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs new file mode 100644 index 0000000000..0df657f44d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; + +internal sealed class JxlBitDepth : IJxlFields +{ + /// + /// 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 int BitsPerSample { get; set; } + + /// + /// + /// 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 int ExponentBitsPerSample { get; set; } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlCodecMetadata.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlCodecMetadata.cs new file mode 100644 index 0000000000..d26d79d9a5 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlCodecMetadata.cs @@ -0,0 +1,57 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; + +internal sealed class JxlCodecMetadata +{ + public JxlImageMetadata? ImageMetadata { get; set; } + + public SizeHeader Size { get; set; } + + public JxlCustomTransformData? CustomTransformData { get; set; } + + public int XSize => this.Size.XSize; + + public int YSize => this.Size.YSize; + + 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/Metadata/JxlCustomTransformData.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs new file mode 100644 index 0000000000..78c0e9ba81 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.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/Metadata/JxlExifOrientation.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs new file mode 100644 index 0000000000..9146c2bfa2 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; + +internal enum JxlExifOrientation +{ + Identity = 1, + FlipHorizontal = 2, + Rotate180 = 3, + FlipVertical = 4, + Transponse = 5, + Rotate90 = 6, + AntiTranspose = 7, + Rotate270 = 8 +} diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs new file mode 100644 index 0000000000..d8e62dc931 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; + +internal enum JxlExtraChannel +{ + Alpha, + Depth, + SpotColor, + SelectionMask, + Black, + Cfa, + Thermal, + Reserved0, + Reserved1, + Reserved2, + Reserved3, + Reserved4, + Reserved5, + Reserved6, + Reserved7, + Unknown, + Optional +} diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannelInfo.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannelInfo.cs new file mode 100644 index 0000000000..0279c2d7ab --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannelInfo.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.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/Metadata/JxlImageMetadata.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlImageMetadata.cs new file mode 100644 index 0000000000..633a01aed6 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlImageMetadata.cs @@ -0,0 +1,126 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.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; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs new file mode 100644 index 0000000000..ccf2b25303 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; + +internal sealed class JxlOpsinInverseMatrix : IJxlFields +{ + public bool AllDefault { get; set; } + + public JxlMatrix3x3 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/Metadata/JxlToneMapping.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlToneMapping.cs new file mode 100644 index 0000000000..1698b07016 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlToneMapping.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.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(); +} From 762edd1ec10448996665209062bd2e536f593028 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:16:33 +0400 Subject: [PATCH 008/142] Add headers & aspect ratio helpers --- .../Formats/Jxl/JxlAspectRatioHelpers.cs | 38 ++++++++++ .../Jxl/Metadata/JxlAnimationHeader.cs | 19 +++++ .../Formats/Jxl/Metadata/JxlImageMetadata.cs | 2 + .../Formats/Jxl/Metadata/JxlPreviewHeader.cs | 68 +++++++++++++++++ .../Formats/Jxl/Metadata/JxlSizeHeader.cs | 73 +++++++++++++++++++ 5 files changed, 200 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/JxlAspectRatioHelpers.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlAnimationHeader.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlPreviewHeader.cs create mode 100644 src/ImageSharp/Formats/Jxl/Metadata/JxlSizeHeader.cs 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/Metadata/JxlAnimationHeader.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlAnimationHeader.cs new file mode 100644 index 0000000000..3a11ff88f7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlAnimationHeader.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.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/Metadata/JxlImageMetadata.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlImageMetadata.cs index 633a01aed6..10855235b2 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlImageMetadata.cs +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlImageMetadata.cs @@ -123,4 +123,6 @@ public void SetUIntSamples(int bits) this.Modular16BitBufferSufficient = bits <= 12; } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); } diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlPreviewHeader.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlPreviewHeader.cs new file mode 100644 index 0000000000..0c2bbab2ed --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlPreviewHeader.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.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/Metadata/JxlSizeHeader.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlSizeHeader.cs new file mode 100644 index 0000000000..9c6e66dbef --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlSizeHeader.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.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(); +} From 487410e55f2e2de2de1a61fa378287d5625f7558 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:30:58 +0400 Subject: [PATCH 009/142] Implement field encodings, move IJxlFields This is an implementation of field_encodings.h. Note that I avoided implementing EnumValid() and Values() functions, as we have dedicated methods in .NET to do exactly that (Enum.IsDefined, Enum.GetValues) --- .../Formats/Jxl/{IO => Fields}/IJxlFields.cs | 2 +- .../Formats/Jxl/Fields/JxlFieldExpressions.cs | 21 +++++++++++++++ .../Formats/Jxl/Fields/JxlU32Distribution.cs | 17 ++++++++++++ .../Formats/Jxl/Fields/JxlU32Enc.cs | 26 +++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) rename src/ImageSharp/Formats/Jxl/{IO => Fields}/IJxlFields.cs (90%) create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlFieldExpressions.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlU32Distribution.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/IJxlFields.cs b/src/ImageSharp/Formats/Jxl/Fields/IJxlFields.cs similarity index 90% rename from src/ImageSharp/Formats/Jxl/IO/IJxlFields.cs rename to src/ImageSharp/Formats/Jxl/Fields/IJxlFields.cs index 191cc96a3e..bdc09f5f41 100644 --- a/src/ImageSharp/Formats/Jxl/IO/IJxlFields.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/IJxlFields.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.IO; +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; /// /// Abstracts enumeration of all fields into a visitor. 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/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..9583f57dac --- /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. + +using System.Diagnostics; + +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) + { + Debug.Assert(selector < 4, "Selector out of range"); + + return this.d[selector]; + } +} From ac1c866e5aaf26bb9123e31a858367d2425a587f Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 00:45:44 +0400 Subject: [PATCH 010/142] Add xorshift implemetation --- .../Formats/Jxl/Processing/JpegXorShift.cs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JpegXorShift.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/JpegXorShift.cs b/src/ImageSharp/Formats/Jxl/Processing/JpegXorShift.cs new file mode 100644 index 0000000000..430cfd6288 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JpegXorShift.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 JpegXorShift +{ + 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); + } +} From 85812226b64693e8be0f05e1dec2da9aea28ecd7 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 00:49:13 +0400 Subject: [PATCH 011/142] It's Jxl not Jpeg --- .../Formats/Jxl/Processing/{JpegXorShift.cs => JxlXorShift.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/ImageSharp/Formats/Jxl/Processing/{JpegXorShift.cs => JxlXorShift.cs} (97%) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JpegXorShift.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs similarity index 97% rename from src/ImageSharp/Formats/Jxl/Processing/JpegXorShift.cs rename to src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs index 430cfd6288..b3849d69f0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JpegXorShift.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs @@ -5,7 +5,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; -internal sealed class JpegXorShift +internal sealed class JxlXorShift { private readonly ulong[] s0 = new ulong[8]; private readonly ulong[] s1 = new ulong[8]; From 9b58f4b9d54fd688f4625675314366103d846df7 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 01:04:48 +0400 Subject: [PATCH 012/142] Implement spline abstractions Implementation of spline.h --- .../Jxl/{Metadata => }/InlineArrays.cs | 2 +- .../Formats/Jxl/Splines/JxlQuantizedSpline.cs | 26 +++++++++++++++++++ .../Formats/Jxl/Splines/JxlSpline.cs | 13 ++++++++++ .../Formats/Jxl/Splines/JxlSplineDataView.cs | 13 ++++++++++ .../Jxl/Splines/JxlSplineEntropyContext.cs | 15 +++++++++++ .../Formats/Jxl/Splines/JxlSplineSegment.cs | 17 ++++++++++++ .../Jxl/Splines/JxlSplineSegmentSpan.cs | 11 ++++++++ 7 files changed, 96 insertions(+), 1 deletion(-) rename src/ImageSharp/Formats/Jxl/{Metadata => }/InlineArrays.cs (92%) create mode 100644 src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs create mode 100644 src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs create mode 100644 src/ImageSharp/Formats/Jxl/Splines/JxlSplineDataView.cs create mode 100644 src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs create mode 100644 src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegment.cs create mode 100644 src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegmentSpan.cs diff --git a/src/ImageSharp/Formats/Jxl/Metadata/InlineArrays.cs b/src/ImageSharp/Formats/Jxl/InlineArrays.cs similarity index 92% rename from src/ImageSharp/Formats/Jxl/Metadata/InlineArrays.cs rename to src/ImageSharp/Formats/Jxl/InlineArrays.cs index f1f9d3037b..e89b924681 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/InlineArrays.cs +++ b/src/ImageSharp/Formats/Jxl/InlineArrays.cs @@ -5,7 +5,7 @@ #pragma warning disable SA1649 // File name should match first type name -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl; [InlineArray(3)] internal struct InlineArray3 diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs new file mode 100644 index 0000000000..c4e5bd2143 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Splines; + +internal sealed class JxlQuantizedSpline +{ + public JxlQuantizedSpline() + { + for (int i = 0; i < 3; i++) + { + this.ColorDct[i] = new int[32]; + } + } + + public Dictionary 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]; +} diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs new file mode 100644 index 0000000000..21dcf7dd3a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlSpline.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 JxlSpline +{ + public List ControlPoints { get; set; } = []; + + public JxlDct32[] ColorDct { get; set; } = []; + + public JxlDct32 SigmaDct { get; set; } +} 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..1daa61bf76 --- /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 +{ + 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; +} From 064376a9f202ad2662687f212c7877712181acfa Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:22:27 +0400 Subject: [PATCH 013/142] Add quantized spline implementations --- .../Formats/Jxl/Splines/JxlControlPoint.cs | 17 + .../Formats/Jxl/Splines/JxlQuantizedSpline.cs | 321 +++++++++++++++++- .../Formats/Jxl/Splines/JxlSpline.cs | 30 +- 3 files changed, 364 insertions(+), 4 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Splines/JxlControlPoint.cs 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 index c4e5bd2143..2d207812fc 100644 --- a/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs @@ -1,10 +1,16 @@ // 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 +internal sealed class JxlQuantizedSpline : IDisposable { + private IMemoryOwner? memoryOwner; + public JxlQuantizedSpline() { for (int i = 0; i < 3; i++) @@ -13,7 +19,11 @@ public JxlQuantizedSpline() } } - public Dictionary ControlPoints { get; set; } = []; + 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 @@ -23,4 +33,311 @@ public JxlQuantizedSpline() 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 invalidConstant = int.MinValue; + + for (int i = 0; i < 32; i++) + { + dct[i] = UnpackSigned(decoder.ReadHybridUnsignedInteger(DctContext, br, contextMap)); + if (dct[i] == invalidConstant) + { + Debug.Fail("The DCT constant 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 index 21dcf7dd3a..ef65c1d95e 100644 --- a/src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs @@ -1,13 +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 +internal sealed class JxlSpline : IDisposable { - public List ControlPoints { get; set; } = []; + 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); + } } From 90680b70c1ea53583fce7f22e77f7c1ed2b95c02 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:24:28 +0400 Subject: [PATCH 014/142] Prefer the term "coefficient" --- src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs index 2d207812fc..42aed25f8a 100644 --- a/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs @@ -312,14 +312,14 @@ public bool Decode( bool TryDecodeDct(ReadOnlySpan contextMap, Span dct) { - const int invalidConstant = int.MinValue; + const int invalidCoefficient = int.MinValue; for (int i = 0; i < 32; i++) { dct[i] = UnpackSigned(decoder.ReadHybridUnsignedInteger(DctContext, br, contextMap)); - if (dct[i] == invalidConstant) + if (dct[i] == invalidCoefficient) { - Debug.Fail("The DCT constant is invalid"); + Debug.Fail("The DCT coefficient is invalid"); return false; } From 1e40482cf5f594daf52467d26a50210d608c5740 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:27:03 +0400 Subject: [PATCH 015/142] Start work on ANS entropy Implemented ANS constants --- src/ImageSharp/Formats/Jxl/IO/JxlAnsConstants.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlAnsConstants.cs 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; +} From 396cd8d2cb3c7f325518075cb6d2101301ec1562 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 13:53:33 +0400 Subject: [PATCH 016/142] Implement JxlAnsHelper.GetPopulationCountPrecision See ans_common.h --- src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs new file mode 100644 index 0000000000..babbfe8fd5 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using static SixLabors.ImageSharp.Formats.Jxl.IO.JxlAnsConstants; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +internal static class JxlAnsHelper +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetPopulationCountPrecision(int logCount, int shift) + { + int r = Math.Min(logCount, shift - ((AnsLogTableSize - logCount) >> 1)); + + if (r < 0) + { + return 0; + } + + return r; + } +} From cb364fa4e94000bc2ce4604653d3290be2b52482 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 14:00:57 +0400 Subject: [PATCH 017/142] Turn JxlFrameDimensions into a sealed class It is too large for a struct. --- .../Formats/Jxl/JxlFrameDimensions.cs | 57 ++++++++++++------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs b/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs index accd7d68cb..8a7416a573 100644 --- a/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs +++ b/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs @@ -5,32 +5,13 @@ namespace SixLabors.ImageSharp.Formats.Jxl; -internal struct JxlFrameDimensions +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 int XSize; - public int YSize; - public int XSizeUpsampled; - public int YSizeUpsampled; - public int XSizeUpsampledPadded; - public int YSizeUpsampledPadded; - public int XSizePadded; - public int YSizePadded; - public int XSizeBlocks; - public int YSizeBlocks; - public int XSizeGroups; - public int YSizeGroups; - public int XSizeDcGroups; - public int YSizeDcGroups; - public int NumGroups; - public int NumDcGroups; - public int GroupDimension; - public int DcGroupDimension; - public JxlFrameDimensions(int xSizePixel, int ySizePixel, int groupSizeShift, int maxHorizontalShift, int maxVerticalShift, bool modularMode, int upsampling) { this.GroupDimension = (GroupDimensions >> 1) << groupSizeShift; @@ -60,6 +41,42 @@ public JxlFrameDimensions(int xSizePixel, int ySizePixel, int groupSizeShift, in 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; } From 528149fc1c63b1e146b0cfc1eacd59886e3b15bb Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 14:05:35 +0400 Subject: [PATCH 018/142] Implement CreateFlatHistogram See ans_common.h --- src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs index babbfe8fd5..33b15e6dc5 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs @@ -22,4 +22,28 @@ public static int GetPopulationCountPrecision(int logCount, int shift) return r; } + + // 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; + + for (int i = 0; i < length; i++) + { + resultSpan[i] = count; + } + + int remCounts = totalCount % length; + for (int i = 0; i < remCounts; i++) + { + resultSpan[i]++; + } + + return result; + } } From 490f02f0800bf51505de7e0c7b0bfba6c2c899a0 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 14:14:27 +0400 Subject: [PATCH 019/142] Add ANS entropy structs Add JxlAnsEntry and JxlAnsSymbol. See ans_common.h. These correspond to the Entry and Symbol structures within AliasTable. --- src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs | 31 +++++++++++++++++++ src/ImageSharp/Formats/Jxl/IO/JxlAnsSymbol.cs | 14 +++++++++ 2 files changed, 45 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlAnsSymbol.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs new file mode 100644 index 0000000000..c06cf04fed --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs @@ -0,0 +1,31 @@ +// 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 +{ + // Although the Entry struct looks like this: + // uint8_t cutoff; + // uint8_t right_value; + // uint16_t freq0; + // uint16_t offsets1; + // uint16_t freq1_xor_freq0; + // and clearly uses smaller types (e.g. byte or ushort), + // prefer using int here as otherwise we have to + // introduce many casts: some to assign values, others to + // convert from unsigned to signed kinds. + // + // This struct is 20 bytes which is more than the recommended + // maximum of 16 bytes, but I believe it justifies more due to + // reduced heap allocations that would be introduced if this + // struct would be turned into a class. + public int Entry; + public int RightValue; + public int Frequency0; + public int Offsets1; + public int Frequency1XorFrequency0; +} 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; +} From 73941c117bc357b2408e376b3d8f878b254595dc Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:17:26 +0400 Subject: [PATCH 020/142] Prefer byte for enums --- src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs | 2 +- src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs | 2 +- src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs index 9146c2bfa2..b5f52d67fe 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs @@ -3,7 +3,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; -internal enum JxlExifOrientation +internal enum JxlExifOrientation : byte { Identity = 1, FlipHorizontal = 2, diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs index d8e62dc931..1fb39d8604 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs @@ -3,7 +3,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; -internal enum JxlExtraChannel +internal enum JxlExtraChannel : byte { Alpha, Depth, diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs index 1daa61bf76..adb619f5ff 100644 --- a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs +++ b/src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs @@ -3,7 +3,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Splines; -internal enum JxlSplineEntropyContext +internal enum JxlSplineEntropyContext : byte { QuantizationAdjustment, StartingPosition, From 20c2dd31df5a1b2873417e941e9708e9c3aa9d4d Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:45:30 +0400 Subject: [PATCH 021/142] Implement ANS entropy helpers --- src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs | 25 +-- src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs | 202 ++++++++++++++++++ 2 files changed, 207 insertions(+), 20 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs index c06cf04fed..25a9d12609 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs @@ -8,24 +8,9 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO; [StructLayout(LayoutKind.Sequential)] internal struct JxlAnsEntry { - // Although the Entry struct looks like this: - // uint8_t cutoff; - // uint8_t right_value; - // uint16_t freq0; - // uint16_t offsets1; - // uint16_t freq1_xor_freq0; - // and clearly uses smaller types (e.g. byte or ushort), - // prefer using int here as otherwise we have to - // introduce many casts: some to assign values, others to - // convert from unsigned to signed kinds. - // - // This struct is 20 bytes which is more than the recommended - // maximum of 16 bytes, but I believe it justifies more due to - // reduced heap allocations that would be introduced if this - // struct would be turned into a class. - public int Entry; - public int RightValue; - public int Frequency0; - public int Offsets1; - public int Frequency1XorFrequency0; + 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 index 33b15e6dc5..2aaa85e4a2 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs @@ -46,4 +46,206 @@ public static IMemoryOwner CreateFlatHistogram(Configuration configuration, 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 == 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 = 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; + } } From fed0af3254d4c547f9002897f2ed732f0cd09e14 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:15:46 +0400 Subject: [PATCH 022/142] Add bit-stream reader --- src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs | 165 ++++++++++++++++++ src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs | 14 ++ 2 files changed, 179 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs create mode 100644 src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs new file mode 100644 index 0000000000..55e3ed3efd --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs @@ -0,0 +1,165 @@ +// 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; + private bool endOfStream; + + /// + /// 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.endOfStream = 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.endOfStream) + { + 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.endOfStream) + { + 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(uint bits) => this.ReadBits64Core(bits, peek: false); + + public ulong PeekBits64(uint bits) => this.ReadBits64Core(bits, peek: true); + + public void SkipBits64(uint bits) => _ = this.ReadBits64(bits); +} diff --git a/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs b/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs new file mode 100644 index 0000000000..239d36cd01 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats.Jxl; + +internal static class JxlThrowHelper +{ + private static readonly EndOfStreamException EndOfStream = new(); + + [DoesNotReturn] + public static void ThrowEndOfStream() => throw EndOfStream; +} From 25e41da0ab9bf2ec2eae60d5afd52bee3c8e05eb Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:16:52 +0400 Subject: [PATCH 023/142] Avoid 'using static' --- src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs index 2aaa85e4a2..59ac75da4e 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs @@ -4,7 +4,6 @@ using System.Buffers; using System.Diagnostics; using System.Runtime.CompilerServices; -using static SixLabors.ImageSharp.Formats.Jxl.IO.JxlAnsConstants; namespace SixLabors.ImageSharp.Formats.Jxl.IO; @@ -13,7 +12,7 @@ internal static class JxlAnsHelper [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetPopulationCountPrecision(int logCount, int shift) { - int r = Math.Min(logCount, shift - ((AnsLogTableSize - logCount) >> 1)); + int r = Math.Min(logCount, shift - ((JxlAnsConstants.AnsLogTableSize - logCount) >> 1)); if (r < 0) { @@ -111,7 +110,7 @@ public static bool InitAliasTable(Span preDistribution, uint logRange, int int value = distribution[sym]; sum += value; - if (value == AnsTableSize) + if (value == JxlAnsConstants.AnsTableSize) { if (singleSymbol != -1) { @@ -143,7 +142,7 @@ public static bool InitAliasTable(Span preDistribution, uint logRange, int jxlEntry.Cutoff = 0; jxlEntry.Offsets1 = (ushort)(entrySize * i); jxlEntry.Frequency0 = 0; - jxlEntry.Frequency1XorFrequency0 = AnsTableSize; + jxlEntry.Frequency1XorFrequency0 = JxlAnsConstants.AnsTableSize; } return true; From 4ec9b957a2b27cd7edadc1e02404eaa182a516d1 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:17:56 +0400 Subject: [PATCH 024/142] Simplify --- src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs index 59ac75da4e..e8b3080ac3 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs @@ -11,16 +11,7 @@ internal static class JxlAnsHelper { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetPopulationCountPrecision(int logCount, int shift) - { - int r = Math.Min(logCount, shift - ((JxlAnsConstants.AnsLogTableSize - logCount) >> 1)); - - if (r < 0) - { - return 0; - } - - return r; - } + => 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) From b716d4e5ff4608d2400e4817a2f33c3b9969df5d Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 23:12:57 +0400 Subject: [PATCH 025/142] Add ANS VarLen & histogram parser Currently, there's a VarLenUint8/VarLenUint16 as well as histogram parsing implementation. I will additionally have to implement parsing of ANS codes, uint config and LZ77 parameters. --- src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs | 9 +- src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs | 256 ++++++++++++++++++ src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs | 2 + 3 files changed, 263 insertions(+), 4 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs index e8b3080ac3..35630eb103 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs @@ -14,18 +14,19 @@ 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) + 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; + IMemoryOwner result = configuration.MemoryAllocator.Allocate(length); + Span resultSpan = result.Memory.Span; + uint unsignedCount = (uint)count; for (int i = 0; i < length; i++) { - resultSpan[i] = count; + resultSpan[i] = unsignedCount; } int remCounts = totalCount % length; diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs new file mode 100644 index 0000000000..8fd8bd7c8c --- /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"); + + 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."); + + return null; + } + + if (omitPos + 1 < length && logCounts[omitPos + 1] == JxlAnsConstants.AnsLogTableSize) + { + Debug.Fail("The histogram is corrupt or invalid."); + + 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."); + + return null; + } + } + + return counts; + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs index 55e3ed3efd..99b1599915 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs @@ -162,4 +162,6 @@ private uint ReadBits32Core(uint n, bool peek = false) public ulong PeekBits64(uint bits) => this.ReadBits64Core(bits, peek: true); public void SkipBits64(uint bits) => _ = this.ReadBits64(bits); + + public bool ReadBoolean() => this.ReadBits32Core(1, peek: false) == 1; } From 1381def54af7d0c40eb4855a15fc4e86c8db13cf Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 23:13:59 +0400 Subject: [PATCH 026/142] Fix memory leaks --- src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs index 8fd8bd7c8c..d28e98c029 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs @@ -100,7 +100,7 @@ public static uint DecodeVariableLengthUint16(JxlBitReader reader) if (symbols[0] == symbols[1]) { Debug.Fail("Corrupt data"); - + counts.Dispose(); return null; } @@ -192,14 +192,14 @@ public static uint DecodeVariableLengthUint16(JxlBitReader reader) 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; } @@ -246,7 +246,7 @@ public static uint DecodeVariableLengthUint16(JxlBitReader reader) if (countsSpan[omitPos] <= 0) { Debug.Fail("The histogram count is incorrect."); - + counts.Dispose(); return null; } } From 8ffb41a7a2f498fde3dbf785ca3e09cdf43f6fde Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 23:14:58 +0400 Subject: [PATCH 027/142] Don't use end of stream singleton --- src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs b/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs index 239d36cd01..a39dfe707c 100644 --- a/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs +++ b/src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs @@ -7,8 +7,6 @@ namespace SixLabors.ImageSharp.Formats.Jxl; internal static class JxlThrowHelper { - private static readonly EndOfStreamException EndOfStream = new(); - [DoesNotReturn] - public static void ThrowEndOfStream() => throw EndOfStream; + public static void ThrowEndOfStream() => throw new EndOfStreamException(); } From 3ed56c954f903d216793cd8f28df4fc168cd3fc4 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 16 Jul 2026 23:25:31 +0400 Subject: [PATCH 028/142] Add ANS hybrid uint configuration & LZ77 parameters model --- .../Jxl/IO/JxlAnsHybridUIntConfiguration.cs | 60 +++++++++++++++++++ .../Formats/Jxl/IO/JxlAnsLz77Parameters.cs | 21 +++++++ 2 files changed, 81 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlAnsHybridUIntConfiguration.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlAnsLz77Parameters.cs 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(); +} From 7f35b1a3db1bec4b90879f56c77980106fb4ee10 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 17 Jul 2026 00:39:36 +0400 Subject: [PATCH 029/142] Implement Lehmer codes --- .../Formats/Jxl/Processing/JxlLehmerCode.cs | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs 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; + } +} From 57eadbb3f94ced32b2500a1c2bfd45463b90d1d3 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 17 Jul 2026 00:51:42 +0400 Subject: [PATCH 030/142] Implement noise shared logic --- .../Formats/Jxl/Processing/JxlNoiseHelper.cs | 28 +++++++++++++++++++ .../Processing/JxlNoiseIndexAndFraction.cs | 14 ++++++++++ .../Formats/Jxl/Processing/JxlNoiseLevel.cs | 14 ++++++++++ .../Jxl/Processing/JxlNoiseParameters.cs | 15 ++++++++++ 4 files changed, 71 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlNoiseHelper.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlNoiseIndexAndFraction.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlNoiseLevel.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlNoiseParameters.cs 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); +} From 6de4a89413ee19673ae6c176eb4c678b552d2998 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 17 Jul 2026 01:11:30 +0400 Subject: [PATCH 031/142] Add alpha blending --- .../Processing/JxlAlphaBlendingInputLayer.cs | 15 ++ .../Jxl/Processing/JxlAlphaBlendingOutput.cs | 15 ++ .../Formats/Jxl/Processing/JxlAlphaHelper.cs | 192 ++++++++++++++++++ 3 files changed, 222 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingInputLayer.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs 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; + } + } +} From 289fecdb998f969e81470378b2259d627c6ffd37 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 17 Jul 2026 21:36:04 +0400 Subject: [PATCH 032/142] Move AC strategy & Coefficients stuff into Processing --- src/ImageSharp/Formats/Jxl/{Ac => Processing}/JxlAcContext.cs | 2 +- src/ImageSharp/Formats/Jxl/{Ac => Processing}/JxlAcStrategy.cs | 2 +- .../Formats/Jxl/{Ac => Processing}/JxlAcStrategyImage.cs | 2 +- .../Formats/Jxl/{Ac => Processing}/JxlAcStrategyRow.cs | 2 +- .../Formats/Jxl/{Ac => Processing}/JxlAcStrategyType.cs | 2 +- .../Formats/Jxl/{Ac => Processing}/JxlBlockContextMap.cs | 2 +- .../{Coefficients => Processing}/JxlForwardCoefficientOrder.cs | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) rename src/ImageSharp/Formats/Jxl/{Ac => Processing}/JxlAcContext.cs (97%) rename src/ImageSharp/Formats/Jxl/{Ac => Processing}/JxlAcStrategy.cs (99%) rename src/ImageSharp/Formats/Jxl/{Ac => Processing}/JxlAcStrategyImage.cs (98%) rename src/ImageSharp/Formats/Jxl/{Ac => Processing}/JxlAcStrategyRow.cs (94%) rename src/ImageSharp/Formats/Jxl/{Ac => Processing}/JxlAcStrategyType.cs (94%) rename src/ImageSharp/Formats/Jxl/{Ac => Processing}/JxlBlockContextMap.cs (97%) rename src/ImageSharp/Formats/Jxl/{Coefficients => Processing}/JxlForwardCoefficientOrder.cs (93%) diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcContext.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs similarity index 97% rename from src/ImageSharp/Formats/Jxl/Ac/JxlAcContext.cs rename to src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs index 98f86534b2..88c1e88f7b 100644 --- a/src/ImageSharp/Formats/Jxl/Ac/JxlAcContext.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs @@ -6,7 +6,7 @@ #pragma warning disable SA1405 // Debug.Assert should provide message text -namespace SixLabors.ImageSharp.Formats.Jxl.Ac; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// /// AC context diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategy.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs similarity index 99% rename from src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategy.cs rename to src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs index 7c704d8e91..26b087deea 100644 --- a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategy.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs @@ -8,7 +8,7 @@ #pragma warning disable SA1405 // Debug.Assert should provide message text -namespace SixLabors.ImageSharp.Formats.Jxl.Ac; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; [StructLayout(LayoutKind.Sequential, Pack = 8)] internal struct JxlAcStrategy diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs rename to src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs index 0b2bdb74ae..6070db54c9 100644 --- a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; -namespace SixLabors.ImageSharp.Formats.Jxl.Ac; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal sealed class JxlAcStrategyImage : IDisposable { diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyRow.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs similarity index 94% rename from src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyRow.cs rename to src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs index 668876f711..a99b3654ac 100644 --- a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyRow.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Ac; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal sealed class JxlAcStrategyRow { diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyType.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyType.cs similarity index 94% rename from src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyType.cs rename to src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyType.cs index 3412d10f46..e1b935fa2b 100644 --- a/src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyType.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Ac; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal enum JxlAcStrategyType : ushort { diff --git a/src/ImageSharp/Formats/Jxl/Ac/JxlBlockContextMap.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs similarity index 97% rename from src/ImageSharp/Formats/Jxl/Ac/JxlBlockContextMap.cs rename to src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs index 3b7e75605f..cf7008eb50 100644 --- a/src/ImageSharp/Formats/Jxl/Ac/JxlBlockContextMap.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs @@ -4,7 +4,7 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Coefficients; -namespace SixLabors.ImageSharp.Formats.Jxl.Ac; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal sealed class JxlBlockContextMap { diff --git a/src/ImageSharp/Formats/Jxl/Coefficients/JxlForwardCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlForwardCoefficientOrder.cs similarity index 93% rename from src/ImageSharp/Formats/Jxl/Coefficients/JxlForwardCoefficientOrder.cs rename to src/ImageSharp/Formats/Jxl/Processing/JxlForwardCoefficientOrder.cs index 04cacb176a..26a210e6fd 100644 --- a/src/ImageSharp/Formats/Jxl/Coefficients/JxlForwardCoefficientOrder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlForwardCoefficientOrder.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Coefficients; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal static class JxlForwardCoefficientOrder { From b15c190a8e9ab400543ecbd03b754cc84789cc2b Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:00:21 +0400 Subject: [PATCH 033/142] Add symmetric weights; fix broken using See convolve.h --- .../Jxl/Processing/JxlBlockContextMap.cs | 1 - .../Jxl/Processing/JxlWeightsSymmetric3.cs | 52 ++++++++++ .../Jxl/Processing/JxlWeightsSymmetric5.cs | 94 +++++++++++++++++++ 3 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric3.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric5.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs index cf7008eb50..2642a2295f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Formats.Jxl.Coefficients; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; 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); + } +} From b3023f29cfda20984a9b3fb8adb514c4331d22c9 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:04:20 +0400 Subject: [PATCH 034/142] Add separable weights See convolve.h --- src/ImageSharp/Formats/Jxl/InlineArrays.cs | 9 +++++++++ .../Formats/Jxl/Processing/JxlWeightsSeparable5.cs | 11 +++++++++++ 2 files changed, 20 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs diff --git a/src/ImageSharp/Formats/Jxl/InlineArrays.cs b/src/ImageSharp/Formats/Jxl/InlineArrays.cs index e89b924681..9c19e9266c 100644 --- a/src/ImageSharp/Formats/Jxl/InlineArrays.cs +++ b/src/ImageSharp/Formats/Jxl/InlineArrays.cs @@ -39,3 +39,12 @@ internal struct InlineArray210 { private T first; } + +/// +/// Used by JxlWeightsSeparable5 +/// +[InlineArray(12)] +internal struct InlineArray12 +{ + private T first; +} 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; } +} From 73c7cec5cdb98c72027578472de69265a35340c8 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:35:35 +0400 Subject: [PATCH 035/142] Add DCT scales See dct_scales.h and dct_scales.cc --- .../Formats/Jxl/Processing/JxlDctScales.cs | 361 ++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs 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, + ]; +} From 7223d98e66be7830b7fae570a28e71c60087eacd Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 17 Jul 2026 23:02:35 +0400 Subject: [PATCH 036/142] Add DCT memory, make JxlImage3 implement IDisposable --- .../Formats/Jxl/Memory/JxlImage3{T}.cs | 12 +++- .../Formats/Jxl/Processing/IJxlDctAcImage.cs | 68 +++++++++++++++++++ .../Jxl/Processing/JxlDctAcImage{T}.cs | 63 +++++++++++++++++ .../Formats/Jxl/Processing/JxlDctAcPointer.cs | 20 ++++++ .../Formats/Jxl/Processing/JxlDctAcType.cs | 20 ++++++ .../Jxl/Processing/JxlDctReadOnlyAcPointer.cs | 24 +++++++ 6 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/IJxlDctAcImage.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlDctAcPointer.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlDctAcType.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlDctReadOnlyAcPointer.cs diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs index f46f6767b8..37707b7ba4 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs @@ -7,7 +7,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Memory; // NOTE: Do not seal this class. -internal class JxlImage3 +internal class JxlImage3 : IDisposable where T : unmanaged { private const int PlaneCount = 3; @@ -82,4 +82,14 @@ public bool ShrinkTo(int x, int y) [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/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/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; +} From 51b77654155c021c1a96726d8808adc64f451f70 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 17 Jul 2026 23:09:19 +0400 Subject: [PATCH 037/142] Implement signed packing See pack_signed.h --- .../Formats/Jxl/Processing/JxlPackSigned.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlPackSigned.cs 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)); + } + } +} From 446e4a5e658d8cfaef46013989c98a27125e3d51 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 18 Jul 2026 07:15:06 +0400 Subject: [PATCH 038/142] Add loop filter See loop_filter.h, loop_filter.cc, epf.h and epf.cc --- .../Formats/Jxl/Processing/JxlAcStrategy.cs | 2 + .../Formats/Jxl/Processing/JxlLoopFilter.cs | 260 ++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs index 26b087deea..84b2462591 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs @@ -82,6 +82,8 @@ public JxlAcStrategy(int rawStrategy) 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); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs new file mode 100644 index 0000000000..01759a4d93 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs @@ -0,0 +1,260 @@ +// 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 +{ + /// + /// 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.Slice(SigmaPadding + (iy + SigmaPadding) * SigmaStride), sigmaBorder); + } + } + + if (bx + blockRect.X + llfX == state.Shared.FrameDimensions.XSizeBlocks) + { + for (int iy = 0; iy < acs.CoveredBlocksY; iy++) + { + RightMirror(sigmaRow.Slice(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.Slice(offsetBefore + ((SigmaPadding + iy) * sigmaStride))); + } + } + + if (by + blockRect.Y + acs.CoveredBlocksX == state.Shared.FrameDimensions.YSizeBloks) + { + for (int iy = 0; iy < SigmaBorder; iy++) + { + sigmaRow.Slice(offsetBefore + (sigmaStride * (acs.CoveredBlocksX + SigmaPadding + iy))) + .CopyTo(sigmaRow.Slice(offsetBefore + (sigmaStride * (acs.CoveredBlocksY + SigmaPadding - 1 - iy)))); + } + } + } + } + + return true; + } + + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} From decc0b232c33fa0f18d7db50f01e743af6410521 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:07:46 +0400 Subject: [PATCH 039/142] Fully implement JPEG XL fields See fields.h and fields.cc --- .../Jxl/Fields/JxlAllDefaultVisitor.cs | 35 +++++ .../Formats/Jxl/Fields/JxlBitsCoder.cs | 41 ++++++ .../Formats/Jxl/Fields/JxlBundle.cs | 89 ++++++++++++ .../Formats/Jxl/Fields/JxlCanEncodeVisitor.cs | 118 ++++++++++++++++ .../Formats/Jxl/Fields/JxlExtensionStates.cs | 42 ++++++ .../Formats/Jxl/Fields/JxlF16Coder.cs | 73 ++++++++++ .../Formats/Jxl/Fields/JxlInitVisitor.cs | 51 +++++++ .../Formats/Jxl/Fields/JxlReadVisitor.cs | 132 ++++++++++++++++++ .../Jxl/Fields/JxlSetDefaultVisitor.cs | 49 +++++++ .../Formats/Jxl/Fields/JxlU32Coder.cs | 127 +++++++++++++++++ .../Formats/Jxl/Fields/JxlU64Coder.cs | 105 ++++++++++++++ .../Formats/Jxl/Fields/JxlVisitor.cs | 92 ++++++++++++ .../Formats/Jxl/Fields/JxlVisitorBase.cs | 74 ++++++++++ src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs | 23 ++- .../Formats/Jxl/Metadata/JxlBitDepth.cs | 2 +- .../Jxl/Metadata/JxlCustomTransformData.cs | 2 +- 16 files changed, 1046 insertions(+), 9 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlExtensionStates.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs create mode 100644 src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs new file mode 100644 index 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..7bf83e1854 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +/// +/// Represents the Half-precision Floating-point number coder. +/// +internal static class JxlF16Coder +{ + /// + /// Always returns 16, which is the maximum possible encoded bits. + /// The F16 coder always reads 16 bits from the bitstream. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int MaxEncodedBits() => 16; + + /// + /// Returns a boolean indicating whether the input float + /// can be represented properly when encoded into a bit-stream. + /// Also stores the maximum encodeable bits into encodedBits (which is + /// always 16). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CanEncode(float value, ref int encodedBits) + { + encodedBits = MaxEncodedBits(); + if (float.IsNaN(value) || float.IsInfinity(value)) + { + return false; // NaN and Infinity are not valid + } + + return MathF.Abs(value) <= 65504.0f; + } + + public static bool Read(JxlBitReader reader, ref float value) + { + uint bits16 = reader.ReadBits32(16u); + uint sign = bits16 >> 15; + uint biasedExponent = (bits16 >> 10) & 0x1Fu; + uint mantissa = bits16 & 0x3FFu; + + if (biasedExponent == 31u) + { + // NaN and Infinity are not valid + return false; + } + + if (biasedExponent == 0u) + { + // Subnormal or zero. + value = (1.0f / 16384) * (mantissa * (1.0f / 1024)); + if (sign != 0u) + { + value = -value; + } + + return true; + } + + uint biasedExp32 = biasedExponent + (127u - 15u); + uint mantissa32 = mantissa << (23 - 10); + uint bits32 = (sign << 31) | (biasedExp32 << 23) | mantissa32; + + value = BitConverter.UInt32BitsToSingle(bits32); + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs new file mode 100644 index 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/JxlU64Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs new file mode 100644 index 0000000000..6f5fb8b281 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs @@ -0,0 +1,105 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Fields; + +/// +/// Unsigned 64-bit variable-length coding. +/// +internal static class JxlU64Coder +{ + /// + /// Reads the variable-length, unsigned 64-bit integer. + /// + public static ulong Read(JxlBitReader reader) + { + uint selector = reader.ReadBits32(2u); + + if (selector == 0u) + { + return 0u; + } + else if (selector == 1u) + { + return 1u + reader.ReadBits32(4u); + } + else if (selector == 2u) + { + return 17u + reader.ReadBits32(8u); + } + + // Selector 3... + ulong result = reader.ReadBits32(12u); + int shift = 12; + + while (reader.ReadBoolean()) + { + if (shift == 60) + { + result |= (ulong)reader.ReadBits32(4u) << shift; + break; + } + + result |= (ulong)reader.ReadBits32(8u) << shift; + shift += 8; + } + + return result; + } + + /// + /// Returns a value indicating whether can the value be encoded, + /// as well as the number of encoded bits. + /// + public static bool CanEncode(ulong value, ref int encodedBits) + { + if (value == 0) + { + // 2 selector bits + encodedBits = 2; + } + else if (value <= 16) + { + // 2 selector bits + 4 payload bits + encodedBits = 2 + 4; + } + else if (value <= 272) + { + // 2 selector bits + 8 payload bits + encodedBits = 2 + 8; + } + else + { + // 2 selector bits + 12 payload bits + encodedBits = 2 + 12; + value >>= 12; + int shift = 12; + while (value > 0 && shift < 60) + { + // 1 continuation bit + 8 payload bits + encodedBits += 1 + 8; + value >>= 8; + shift += 8; + } + if (value > 0) + { + // 1 continuation bit + 4 payload bits + encodedBits += 1 + 4; + } + else + { + // 1 stop bit + encodedBits += 1; + } + } + + return true; + } + + /// + /// Always returns 73. + /// + public static int MaxEncodedBits() => 73; +} diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs new file mode 100644 index 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/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs index 99b1599915..50753321e7 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs @@ -14,7 +14,16 @@ internal sealed class JxlBitReader(ReadOnlyMemory bytes) private ulong buffer; private uint bufferRemainingBits; private int pointer; - private bool endOfStream; + + /// + /// Gets a value indicating whether this marks an end of stream. + /// + public bool IsEndOfStream { get; private set; } + + /// + /// Gets the total number of bits consumed. + /// + public long TotalBitsConsumed => ((long)this.pointer * 8) + (64 - this.bufferRemainingBits); /// /// Fetches a new buffer. @@ -29,7 +38,7 @@ private void RefillCore() // we don't have any more data... mark an end of stream this.buffer = 0; this.bufferRemainingBits = 0; - this.endOfStream = true; + this.IsEndOfStream = true; return; } @@ -66,7 +75,7 @@ private ulong ReadBits64Core(uint n, bool peek = false) Debug.Assert(n <= 64, "Too many bits to pack into ulong"); this.MaybeRefill(); - if (this.endOfStream) + if (this.IsEndOfStream) { JxlThrowHelper.ThrowEndOfStream(); } @@ -111,7 +120,7 @@ private uint ReadBits32Core(uint n, bool peek = false) Debug.Assert(n <= 32, "Too many bits to pack into uint"); this.MaybeRefill(); - if (this.endOfStream) + if (this.IsEndOfStream) { JxlThrowHelper.ThrowEndOfStream(); } @@ -157,11 +166,11 @@ private uint ReadBits32Core(uint n, bool peek = false) public void SkipBits32(uint bits) => _ = this.ReadBits32(bits); - public ulong ReadBits64(uint bits) => this.ReadBits64Core(bits, peek: false); + public ulong ReadBits64(ulong bits) => this.ReadBits64Core((uint)bits, peek: false); - public ulong PeekBits64(uint bits) => this.ReadBits64Core(bits, peek: true); + public ulong PeekBits64(ulong bits) => this.ReadBits64Core((uint)bits, peek: true); - public void SkipBits64(uint bits) => _ = this.ReadBits64(bits); + public void SkipBits64(ulong bits) => _ = this.ReadBits64(bits); public bool ReadBoolean() => this.ReadBits32Core(1, peek: false) == 1; } diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs index 0df657f44d..a60ac0f137 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Fields; namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs index 78c0e9ba81..8b938b736b 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Fields; namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; From a6a0263836f469d4fd8e810ae354e91cbe65f9ce Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:21:25 +0400 Subject: [PATCH 040/142] Implement visitor for JxlBitDepth --- .../Formats/Jxl/Metadata/JxlBitDepth.cs | 93 ++++++++++++++++++- 1 file changed, 90 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs index a60ac0f137..fd842b3633 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs @@ -5,8 +5,19 @@ namespace SixLabors.ImageSharp.Formats.Jxl.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 @@ -18,7 +29,11 @@ internal sealed class JxlBitDepth : IJxlFields /// Gets or sets the bit depth of the original (uncompressed) image samples. /// Must be in the range [1, 32]. /// - public int BitsPerSample { get; set; } + public uint BitsPerSample + { + get => this.bitsPerSample; + set => this.bitsPerSample = value; + } /// /// @@ -36,7 +51,79 @@ internal sealed class JxlBitDepth : IJxlFields /// [2, 8] and amount of mantissa bits must be in the range [2, 23]. /// /// - public int ExponentBitsPerSample { get; set; } + 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; + } - public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); + return true; + } } From 6aa313f89589a04d3a5f7ed6665e3d27d5af2ef7 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:56:36 +0400 Subject: [PATCH 041/142] Add matrices --- .../Jxl/Metadata/JxlOpsinInverseMatrix.cs | 5 +- .../Formats/Jxl/Processing/JxlLoopFilter.cs | 2 +- .../Formats/Jxl/Processing/JxlMatrix3x3.cs | 142 ++++++++++++++++++ .../Formats/Jxl/Processing/JxlMatrix3x3F.cs | 142 ++++++++++++++++++ 4 files changed, 288 insertions(+), 3 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs b/src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs index ccf2b25303..d0a14ffb40 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs +++ b/src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs @@ -1,7 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing; namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; @@ -9,7 +10,7 @@ internal sealed class JxlOpsinInverseMatrix : IJxlFields { public bool AllDefault { get; set; } - public JxlMatrix3x3 InverseMatrix { get; set; } + public JxlMatrix3x3F InverseMatrix { get; set; } public InlineArray3 OpsinBiases { get; set; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs index 01759a4d93..1a98138a37 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs @@ -237,7 +237,7 @@ public bool ComputeSigma(Rectangle blockRect, JxlPassesDecoderState state) { for (int iy = 0; iy < SigmaBorder; iy++) { - sigmaRow.Slice(offsetBefore + (SigmaPadding - 1 - iy) * sigmaStride, num) + sigmaRow.Slice(offsetBefore + ((SigmaPadding - 1 - iy) * sigmaStride), num) .CopyTo(sigmaRow.Slice(offsetBefore + ((SigmaPadding + iy) * sigmaStride))); } } 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..fda6282e05 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.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 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; + + /// + /// 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; + } +} From d401b71313ff188473744d4f1516bcfd3afc8b03 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:01:25 +0400 Subject: [PATCH 042/142] Fix errors --- .../Formats/Jxl/Processing/JxlLoopFilter.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs index 1a98138a37..312138db43 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs @@ -12,6 +12,9 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// 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 /// @@ -206,7 +209,7 @@ public bool ComputeSigma(Rectangle blockRect, JxlPassesDecoderState state) { 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; + sigmaRow[bx + ix + SigmaPadding + ((iy + SigmaPadding) * sigmaStride)] = 1.0f / sigma; } } @@ -214,7 +217,7 @@ public bool ComputeSigma(Rectangle blockRect, JxlPassesDecoderState state) { for (int iy = 0; iy < acs.CoveredBlocksY; iy++) { - LeftMirror(sigmaRow.Slice(SigmaPadding + (iy + SigmaPadding) * SigmaStride), sigmaBorder); + LeftMirror(sigmaRow[(SigmaPadding + ((iy + SigmaPadding) * sigmaStride))..], SigmaBorder); } } @@ -222,7 +225,7 @@ public bool ComputeSigma(Rectangle blockRect, JxlPassesDecoderState state) { for (int iy = 0; iy < acs.CoveredBlocksY; iy++) { - RightMirror(sigmaRow.Slice(SigmaPadding + bx + llfX + (iy + SigmaPadding) * sigmaStride), SigmaBorder); + RightMirror(sigmaRow[(SigmaPadding + bx + llfX + ((iy + SigmaPadding) * sigmaStride))..], SigmaBorder); } } @@ -238,7 +241,7 @@ public bool ComputeSigma(Rectangle blockRect, JxlPassesDecoderState state) for (int iy = 0; iy < SigmaBorder; iy++) { sigmaRow.Slice(offsetBefore + ((SigmaPadding - 1 - iy) * sigmaStride), num) - .CopyTo(sigmaRow.Slice(offsetBefore + ((SigmaPadding + iy) * sigmaStride))); + .CopyTo(sigmaRow[(offsetBefore + ((SigmaPadding + iy) * sigmaStride))..]); } } @@ -246,8 +249,8 @@ public bool ComputeSigma(Rectangle blockRect, JxlPassesDecoderState state) { for (int iy = 0; iy < SigmaBorder; iy++) { - sigmaRow.Slice(offsetBefore + (sigmaStride * (acs.CoveredBlocksX + SigmaPadding + iy))) - .CopyTo(sigmaRow.Slice(offsetBefore + (sigmaStride * (acs.CoveredBlocksY + SigmaPadding - 1 - iy)))); + sigmaRow[(offsetBefore + (sigmaStride * (acs.CoveredBlocksX + SigmaPadding + iy)))..] + .CopyTo(sigmaRow[(offsetBefore + (sigmaStride * (acs.CoveredBlocksY + SigmaPadding - 1 - iy)))..]); } } } From a803f01ab603333bb324b1c14684c4be629630e1 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 18 Jul 2026 21:27:27 +0400 Subject: [PATCH 043/142] Add Y'Cb'Cr chroma subsampling as part of the frame header See frame_header.h --- .../Jxl/IO/JxlYCbCrChromaSubsampling.cs | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlYCbCrChromaSubsampling.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlYCbCrChromaSubsampling.cs b/src/ImageSharp/Formats/Jxl/IO/JxlYCbCrChromaSubsampling.cs new file mode 100644 index 0000000000..90d9cf6738 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/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; + +/// +/// 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; + } +} From 16c55f343ad805e916cb39e2e366e4ab8b92b890 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 20 Jul 2026 01:43:13 +0400 Subject: [PATCH 044/142] Add frame header --- .../Formats/Jxl/Fields/JxlF16Coder.cs | 1 - .../Formats/Jxl/Fields/JxlU32Enc.cs | 6 +- .../Formats/Jxl/Fields/JxlU64Coder.cs | 1 + .../Jxl/IO/FrameHeader/JxlAnimationFrame.cs | 74 ++ .../Jxl/IO/FrameHeader/JxlBlendMode.cs | 64 ++ .../Jxl/IO/FrameHeader/JxlBlendingInfo.cs | 146 ++++ .../Jxl/IO/FrameHeader/JxlColorTransform.cs | 26 + .../FrameHeader/JxlColorTransformHelpers.cs | 39 + .../Jxl/IO/FrameHeader/JxlFrameEncoding.cs | 20 + .../Jxl/IO/FrameHeader/JxlFrameHeader.cs | 743 ++++++++++++++++++ .../Jxl/IO/FrameHeader/JxlFrameHeaderFlags.cs | 36 + .../Jxl/IO/FrameHeader/JxlFrameType.cs | 37 + .../Formats/Jxl/IO/FrameHeader/JxlPasses.cs | 220 ++++++ .../JxlYCbCrChromaSubsampling.cs | 2 +- 14 files changed, 1410 insertions(+), 5 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendMode.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendingInfo.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransform.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameEncoding.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeaderFlags.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameType.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlPasses.cs rename src/ImageSharp/Formats/Jxl/IO/{ => FrameHeader}/JxlYCbCrChromaSubsampling.cs (98%) diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs index 7bf83e1854..3c4126c4b8 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.IO; diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs index 9583f57dac..5b6e10b847 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics; - namespace SixLabors.ImageSharp.Formats.Jxl.Fields; internal readonly struct JxlU32Enc @@ -19,7 +17,9 @@ public JxlU32Enc(JxlU32Distribution d0, JxlU32Distribution d1, JxlU32Distributio public JxlU32Distribution GetDistribution(int selector) { - Debug.Assert(selector < 4, "Selector out of range"); + // 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 index 6f5fb8b281..ba121cafe2 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs @@ -83,6 +83,7 @@ public static bool CanEncode(ulong value, ref int encodedBits) value >>= 8; shift += 8; } + if (value > 0) { // 1 continuation bit + 4 payload bits 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/JxlYCbCrChromaSubsampling.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlYCbCrChromaSubsampling.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/IO/JxlYCbCrChromaSubsampling.cs rename to src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlYCbCrChromaSubsampling.cs index 90d9cf6738..b10d8818aa 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlYCbCrChromaSubsampling.cs +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlYCbCrChromaSubsampling.cs @@ -3,7 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.IO; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; /// /// Gets the Y'Cb'Cr chroma subsampling information as part From ded8906f2931378de64456edebc848d648a18bd0 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 20 Jul 2026 03:25:03 +0400 Subject: [PATCH 045/142] Move Metadata folder to IO --- .../Formats/Jxl/{ => IO}/Metadata/JxlAnimationHeader.cs | 4 ++-- .../Formats/Jxl/{ => IO}/Metadata/JxlBitDepth.cs | 2 +- .../Formats/Jxl/{ => IO}/Metadata/JxlCodecMetadata.cs | 8 ++++---- .../Jxl/{ => IO}/Metadata/JxlCustomTransformData.cs | 2 +- .../Formats/Jxl/{ => IO}/Metadata/JxlExifOrientation.cs | 2 +- .../Formats/Jxl/{ => IO}/Metadata/JxlExtraChannel.cs | 2 +- .../Formats/Jxl/{ => IO}/Metadata/JxlExtraChannelInfo.cs | 4 ++-- .../Formats/Jxl/{ => IO}/Metadata/JxlImageMetadata.cs | 4 ++-- .../Jxl/{ => IO}/Metadata/JxlOpsinInverseMatrix.cs | 2 +- .../Formats/Jxl/{ => IO}/Metadata/JxlPreviewHeader.cs | 4 ++-- .../Formats/Jxl/{ => IO}/Metadata/JxlSizeHeader.cs | 4 ++-- .../Formats/Jxl/{ => IO}/Metadata/JxlToneMapping.cs | 6 +++--- 12 files changed, 22 insertions(+), 22 deletions(-) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlAnimationHeader.cs (79%) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlBitDepth.cs (98%) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlCodecMetadata.cs (86%) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlCustomTransformData.cs (92%) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlExifOrientation.cs (83%) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlExtraChannel.cs (86%) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlExtraChannelInfo.cs (85%) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlImageMetadata.cs (96%) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlOpsinInverseMatrix.cs (90%) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlPreviewHeader.cs (93%) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlSizeHeader.cs (94%) rename src/ImageSharp/Formats/Jxl/{ => IO}/Metadata/JxlToneMapping.cs (74%) diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlAnimationHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlAnimationHeader.cs similarity index 79% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlAnimationHeader.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlAnimationHeader.cs index 3a11ff88f7..647e8bbc7d 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlAnimationHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlAnimationHeader.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlAnimationHeader : IJxlFields { diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepth.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepth.cs index fd842b3633..0a9db0f575 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepth.cs @@ -3,7 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; /// /// Represents the JPEG XL Bit Depth image metadata. diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlCodecMetadata.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCodecMetadata.cs similarity index 86% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlCodecMetadata.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCodecMetadata.cs index d26d79d9a5..5a3aaeecb7 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlCodecMetadata.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCodecMetadata.cs @@ -1,19 +1,19 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlCodecMetadata { public JxlImageMetadata? ImageMetadata { get; set; } - public SizeHeader Size { get; set; } + public JxlSizeHeader? Size { get; set; } public JxlCustomTransformData? CustomTransformData { get; set; } - public int XSize => this.Size.XSize; + public int XSize => this.Size?.XSize ?? 0; - public int YSize => this.Size.YSize; + public int YSize => this.Size?.YSize ?? 0; public int GetOrientedPreviewXSize(bool keepOrientation) { diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs similarity index 92% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs index 8b938b736b..3b3c6bc200 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs @@ -3,7 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlCustomTransformData : IJxlFields { diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExifOrientation.cs similarity index 83% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExifOrientation.cs index b5f52d67fe..406e7e9a1a 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExifOrientation.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal enum JxlExifOrientation : byte { diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannel.cs similarity index 86% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannel.cs index 1fb39d8604..8e2b842a56 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannel.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal enum JxlExtraChannel : byte { diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannelInfo.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs similarity index 85% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannelInfo.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs index 0279c2d7ab..5b492b1026 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannelInfo.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlExtraChannelInfo : IJxlFields { diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlImageMetadata.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs similarity index 96% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlImageMetadata.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs index 10855235b2..d9bc419e97 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlImageMetadata.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs @@ -2,9 +2,9 @@ // Licensed under the Six Labors Split License. using System.Diagnostics; -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlImageMetadata : IJxlFields { diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs similarity index 90% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs index d0a14ffb40..357ce27c39 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs @@ -4,7 +4,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing; -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlOpsinInverseMatrix : IJxlFields { diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlPreviewHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlPreviewHeader.cs similarity index 93% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlPreviewHeader.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlPreviewHeader.cs index 0c2bbab2ed..e46763cfeb 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlPreviewHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlPreviewHeader.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlPreviewHeader : IJxlFields { diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlSizeHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlSizeHeader.cs similarity index 94% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlSizeHeader.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlSizeHeader.cs index 9c6e66dbef..5da30b8a0d 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlSizeHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlSizeHeader.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlSizeHeader : IJxlFields { diff --git a/src/ImageSharp/Formats/Jxl/Metadata/JxlToneMapping.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs similarity index 74% rename from src/ImageSharp/Formats/Jxl/Metadata/JxlToneMapping.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs index 1698b07016..9a399dc0b0 100644 --- a/src/ImageSharp/Formats/Jxl/Metadata/JxlToneMapping.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.Metadata; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlToneMapping : IJxlFields { @@ -15,7 +15,7 @@ internal sealed class JxlToneMapping : IJxlFields public bool RelativeToMaxDisplay { get; set; } - public float LinearBelow { get; set;} + public float LinearBelow { get; set; } public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); } From 674f82ad3ce50282cada599a6e4c460da3c5fe73 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 20 Jul 2026 05:41:45 +0400 Subject: [PATCH 046/142] Add quantizer --- .../Formats/Jxl/Memory/JxlSpanHelper.cs | 50 +++ .../Formats/Jxl/Processing/JxlQuantizer.cs | 388 ++++++++++++++++++ .../Jxl/Processing/JxlQuantizerParameters.cs | 60 +++ 3 files changed, 498 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Memory/JxlSpanHelper.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerParameters.cs 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/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs new file mode 100644 index 0000000000..7ee27b1fb8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs @@ -0,0 +1,388 @@ +// 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()) + { + return false; + } + + this.SetQuantField(qf, qf.GetRectangle(), rawQuantField); + } + + 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); + } +} From 07f6be0000d433846c53a9d34dbc8daca025aac1 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 20 Jul 2026 05:43:25 +0400 Subject: [PATCH 047/142] Fix memory leak --- src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs index 7ee27b1fb8..5a1405b350 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs @@ -369,12 +369,18 @@ public bool SetQuantField(Configuration configuration, float quantDc, JxlImageF { if (rawQuantField.GetSize() != qf.GetSize()) { + data.Dispose(); + deviations.Dispose(); + return false; } this.SetQuantField(qf, qf.GetRectangle(), rawQuantField); } + data.Dispose(); + deviations.Dispose(); + return true; } From b72019667a43ac05866785a4dac519552b90b6ca Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 20 Jul 2026 06:04:00 +0400 Subject: [PATCH 048/142] Add opsin inverse parameters, some quantizer weight work --- .../Formats/Jxl/Processing/JxlMatrix3x3F.cs | 13 +++++ .../Processing/JxlOpsinInverseParameters.cs | 33 +++++++++++++ .../Formats/Jxl/Processing/JxlQuantMode.cs | 19 ++++++++ .../Formats/Jxl/Processing/JxlQuantTable.cs | 47 +++++++++++++++++++ 4 files changed, 112 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlQuantTable.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs index fda6282e05..0a2d59a90f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs @@ -55,6 +55,19 @@ internal struct JxlMatrix3x3F /// 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. /// 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/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 +} From 2758454c1bb0f5a9cc477d5d8985e1ce2123b2cc Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:46:41 +0400 Subject: [PATCH 049/142] Move, add entropy coding common functions --- .../Formats/Jxl/Fields/JxlU32Coder.cs | 2 +- .../Jxl/IO/{ => Entropy}/JxlAnsConstants.cs | 2 +- .../Jxl/IO/{ => Entropy}/JxlAnsEntry.cs | 2 +- .../Jxl/IO/{ => Entropy}/JxlAnsHelper.cs | 2 +- .../JxlAnsHybridUIntConfiguration.cs | 2 +- .../IO/{ => Entropy}/JxlAnsLz77Parameters.cs | 2 +- .../Jxl/IO/{ => Entropy}/JxlAnsSymbol.cs | 2 +- .../Jxl/IO/FrameHeader/JxlFrameHeader.cs | 2 +- src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs | 9 ++ .../Formats/Jxl/IO/JxlHuffmanCode.cs | 20 +++ .../Decoder}/JxlAnsReader.cs | 3 +- .../Decoder}/JxlBitReader.cs | 2 +- .../{ => Processing}/JxlAspectRatioHelpers.cs | 2 +- .../Jxl/Processing/JxlBlockContextMap.cs | 4 +- .../Formats/Jxl/Processing/JxlEntropyCoder.cs | 120 ++++++++++++++++++ .../{ => Processing}/JxlFrameDimensions.cs | 2 +- .../Splines/JxlControlPoint.cs | 2 +- .../Splines/JxlQuantizedSpline.cs | 2 +- .../Jxl/{ => Processing}/Splines/JxlSpline.cs | 2 +- .../Splines/JxlSplineDataView.cs | 2 +- .../Splines/JxlSplineEntropyContext.cs | 2 +- .../Splines/JxlSplineSegment.cs | 2 +- .../Splines/JxlSplineSegmentSpan.cs | 2 +- 23 files changed, 171 insertions(+), 21 deletions(-) rename src/ImageSharp/Formats/Jxl/IO/{ => Entropy}/JxlAnsConstants.cs (89%) rename src/ImageSharp/Formats/Jxl/IO/{ => Entropy}/JxlAnsEntry.cs (86%) rename src/ImageSharp/Formats/Jxl/IO/{ => Entropy}/JxlAnsHelper.cs (99%) rename src/ImageSharp/Formats/Jxl/IO/{ => Entropy}/JxlAnsHybridUIntConfiguration.cs (97%) rename src/ImageSharp/Formats/Jxl/IO/{ => Entropy}/JxlAnsLz77Parameters.cs (90%) rename src/ImageSharp/Formats/Jxl/IO/{ => Entropy}/JxlAnsSymbol.cs (85%) create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlHuffmanCode.cs rename src/ImageSharp/Formats/Jxl/{IO => Processing/Decoder}/JxlAnsReader.cs (98%) rename src/ImageSharp/Formats/Jxl/{IO => Processing/Decoder}/JxlBitReader.cs (98%) rename src/ImageSharp/Formats/Jxl/{ => Processing}/JxlAspectRatioHelpers.cs (94%) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlEntropyCoder.cs rename src/ImageSharp/Formats/Jxl/{ => Processing}/JxlFrameDimensions.cs (98%) rename src/ImageSharp/Formats/Jxl/{ => Processing}/Splines/JxlControlPoint.cs (86%) rename src/ImageSharp/Formats/Jxl/{ => Processing}/Splines/JxlQuantizedSpline.cs (99%) rename src/ImageSharp/Formats/Jxl/{ => Processing}/Splines/JxlSpline.cs (93%) rename src/ImageSharp/Formats/Jxl/{ => Processing}/Splines/JxlSplineDataView.cs (82%) rename src/ImageSharp/Formats/Jxl/{ => Processing}/Splines/JxlSplineEntropyContext.cs (80%) rename src/ImageSharp/Formats/Jxl/{ => Processing}/Splines/JxlSplineSegment.cs (85%) rename src/ImageSharp/Formats/Jxl/{ => Processing}/Splines/JxlSplineSegmentSpan.cs (81%) diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs index e909f7f74f..b097311ab5 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsConstants.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsConstants.cs similarity index 89% rename from src/ImageSharp/Formats/Jxl/IO/JxlAnsConstants.cs rename to src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsConstants.cs index dcd244ac7d..d9b6d1150d 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsConstants.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsConstants.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.IO; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; internal static class JxlAnsConstants { diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsEntry.cs similarity index 86% rename from src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs rename to src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsEntry.cs index 25a9d12609..7ec8c7cd4c 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsEntry.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; -namespace SixLabors.ImageSharp.Formats.Jxl.IO; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; [StructLayout(LayoutKind.Sequential)] internal struct JxlAnsEntry diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs similarity index 99% rename from src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs rename to src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs index 35630eb103..8e775050e1 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs @@ -5,7 +5,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.IO; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; internal static class JxlAnsHelper { diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHybridUIntConfiguration.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs similarity index 97% rename from src/ImageSharp/Formats/Jxl/IO/JxlAnsHybridUIntConfiguration.cs rename to src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs index 422eb2323f..b8609f9163 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsHybridUIntConfiguration.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.IO; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; internal sealed class JxlAnsHybridUIntConfiguration : IJxlFields { diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsLz77Parameters.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsLz77Parameters.cs similarity index 90% rename from src/ImageSharp/Formats/Jxl/IO/JxlAnsLz77Parameters.cs rename to src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsLz77Parameters.cs index ecf5cdc3fa..cbf6d3a4e4 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsLz77Parameters.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsLz77Parameters.cs @@ -3,7 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.IO; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; internal sealed class JxlAnsLz77Parameters : IJxlFields { diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsSymbol.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsSymbol.cs similarity index 85% rename from src/ImageSharp/Formats/Jxl/IO/JxlAnsSymbol.cs rename to src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsSymbol.cs index 75afa94d9f..240b2463eb 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsSymbol.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsSymbol.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; -namespace SixLabors.ImageSharp.Formats.Jxl.IO; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; [StructLayout(LayoutKind.Sequential)] internal struct JxlAnsSymbol(int value, int offset, int frequency) diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs index a45c0e4710..43d7045041 100644 --- a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs @@ -7,7 +7,7 @@ #pragma warning disable IDE0032 // Use auto property using SixLabors.ImageSharp.Formats.Jxl.Fields; -using SixLabors.ImageSharp.Formats.Jxl.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Processing; namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs b/src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs new file mode 100644 index 0000000000..640f667401 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs @@ -0,0 +1,9 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +internal static class JxlHuffman +{ + +} diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlHuffmanCode.cs b/src/ImageSharp/Formats/Jxl/IO/JxlHuffmanCode.cs new file mode 100644 index 0000000000..30bca3446d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlHuffmanCode.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +/// +/// A single Huffman code. +/// +internal struct JxlHuffmanCode +{ + /// + /// Number of bits for this symbol. + /// + public byte Bits; + + /// + /// Symbol value/offset. + /// + public ushort Value; +} diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs index d28e98c029..5320f5f171 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs @@ -3,8 +3,9 @@ using System.Buffers; using System.Diagnostics; +using SixLabors.ImageSharp.Formats.Jxl.IO; -namespace SixLabors.ImageSharp.Formats.Jxl.IO; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; internal static class JxlAnsReader { diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs rename to src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs index 50753321e7..87c1408f8e 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs @@ -4,7 +4,7 @@ using System.Buffers.Binary; using System.Diagnostics; -namespace SixLabors.ImageSharp.Formats.Jxl.IO; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; /// /// Represents a bitstream reader. diff --git a/src/ImageSharp/Formats/Jxl/JxlAspectRatioHelpers.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAspectRatioHelpers.cs similarity index 94% rename from src/ImageSharp/Formats/Jxl/JxlAspectRatioHelpers.cs rename to src/ImageSharp/Formats/Jxl/Processing/JxlAspectRatioHelpers.cs index ee5d1c134b..7fcda36c9a 100644 --- a/src/ImageSharp/Formats/Jxl/JxlAspectRatioHelpers.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAspectRatioHelpers.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal static class JxlAspectRatioHelpers { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs index 2642a2295f..92dd8d94dd 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs @@ -23,9 +23,9 @@ public JxlBlockContextMap() public List[] DcThresholds { get; } = [[], [], []]; - public List QfThresholds { get; } = []; + public List QfThresholds { get; set; } = []; - public byte[] ContextMap { get; } = new byte[DefaultContextMap.Length]; + public byte[] ContextMap { get; set; } = new byte[DefaultContextMap.Length]; public int ContextCount { get; set; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlEntropyCoder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlEntropyCoder.cs new file mode 100644 index 0000000000..1b896638ca --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlEntropyCoder.cs @@ -0,0 +1,120 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Helper functions for use in entropy coding. +/// +internal static class JxlEntropyCoder +{ + /// + /// Global DC threshold distributions + /// + public static readonly JxlU32Enc DcThresholdDistributions = new( + JxlFieldExpressions.Bits(4), + JxlFieldExpressions.BitsOffset(8, 16), + JxlFieldExpressions.BitsOffset(16, 272), + JxlFieldExpressions.BitsOffset(32, 65808)); + + /// + /// Global QF threshold distributions + /// + public static readonly JxlU32Enc QfThresholdDistributions = new( + JxlFieldExpressions.Bits(2), + JxlFieldExpressions.BitsOffset(3, 4), + JxlFieldExpressions.BitsOffset(5, 12), + JxlFieldExpressions.BitsOffset(8, 44)); + + /// + /// Predicts the entropy symbol using top and left neighboring pixels. + /// + /// Above row; set to if missing + /// Current row + /// The x coordinate offset + /// Default value that's used if both current and above row cannot be used + /// The predicted coefficient using neighboring pixels. + public static int PredictFromTopAndLeft( + ReadOnlySpan rowTop, + ReadOnlySpan row, + int x, + int defaultValue) + { + if (x == 0) + { + return rowTop.Length == 0 ? defaultValue : rowTop[x]; + } + + if (rowTop.Length == 0) + { + return row[x - 1]; + } + + return (rowTop[x] + rowTop[x - 1] + 1) / 2; + } + + public static bool DecodeBlockContextMap(Configuration configuration, JxlBitReader reader, ref JxlBlockContextMap contextMap) + { + List[] dct = contextMap.DcThresholds; + byte[] ctxMap = contextMap.ContextMap; + + bool isDefaultContextMap = reader.ReadBoolean(); + + if (isDefaultContextMap) + { + contextMap = new(); + return true; + } + + contextMap.DcContextCount = 1; + + for (int j = 0; j <= 2; j++) + { + int dcThresholdCount = (int)reader.ReadBits32(4u); + + dct[j] = new List(dcThresholdCount); + + contextMap.DcContextCount = dcThresholdCount + 1; + + for (int i = 0; i < dcThresholdCount; i++) + { + dct[j][i] = JxlPackSigned.UnpackSigned(JxlU32Coder.Read(DcThresholdDistributions, reader)); + } + } + + int qfThresholdCount = (int)reader.ReadBits32(4u); + + List qft = new(qfThresholdCount); + + for (int i = 0; i < qfThresholdCount; i++) + { + qft[i] = JxlU32Coder.Read(QfThresholdDistributions, reader) + 1; + } + + contextMap.QfThresholds = qft; + + if (contextMap.DcContextCount * qft.Count > 64) + { + throw new InvalidOperationException("Invalid block context map. It is too large."); + } + + Array.Resize(ref ctxMap, 3 * JxlForwardCoefficientOrder.OrderCount * contextMap.DcContextCount * qft.Count); + + contextMap.ContextMap = ctxMap; + + if (!JxlDecoderCore.DecodeContextMap(configuration, ref ctxMap, contextMap.ContextCount, reader)) + { + return false; + } + + if (contextMap.ContextCount > 16) + { + throw new InvalidOperationException("Too many distinct contexts in block context map"); + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlFrameDimensions.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs rename to src/ImageSharp/Formats/Jxl/Processing/JxlFrameDimensions.cs index 8a7416a573..4ec265f523 100644 --- a/src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlFrameDimensions.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal sealed class JxlFrameDimensions { diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlControlPoint.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlControlPoint.cs similarity index 86% rename from src/ImageSharp/Formats/Jxl/Splines/JxlControlPoint.cs rename to src/ImageSharp/Formats/Jxl/Processing/Splines/JxlControlPoint.cs index 74f1a0c3a8..48d94b3455 100644 --- a/src/ImageSharp/Formats/Jxl/Splines/JxlControlPoint.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlControlPoint.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Splines; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; /// /// A simple pair of first and second 32-bit signed integers diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs similarity index 99% rename from src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs rename to src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs index 42aed25f8a..0e0135c175 100644 --- a/src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs @@ -5,7 +5,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Splines; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; internal sealed class JxlQuantizedSpline : IDisposable { diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSpline.cs similarity index 93% rename from src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs rename to src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSpline.cs index ef65c1d95e..f95886b268 100644 --- a/src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSpline.cs @@ -3,7 +3,7 @@ using System.Buffers; -namespace SixLabors.ImageSharp.Formats.Jxl.Splines; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; internal sealed class JxlSpline : IDisposable { diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineDataView.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineDataView.cs similarity index 82% rename from src/ImageSharp/Formats/Jxl/Splines/JxlSplineDataView.cs rename to src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineDataView.cs index 1884b85c68..7b3822d783 100644 --- a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineDataView.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineDataView.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Splines; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; internal sealed class JxlSplineDataView { diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineEntropyContext.cs similarity index 80% rename from src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs rename to src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineEntropyContext.cs index adb619f5ff..b6be2fe517 100644 --- a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineEntropyContext.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Splines; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; internal enum JxlSplineEntropyContext : byte { diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegment.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegment.cs similarity index 85% rename from src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegment.cs rename to src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegment.cs index 7e8bef1beb..65f2177927 100644 --- a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegment.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegment.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Splines; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; internal struct JxlSplineSegment { diff --git a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegmentSpan.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegmentSpan.cs similarity index 81% rename from src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegmentSpan.cs rename to src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegmentSpan.cs index b4bd52af4f..2c65b1695d 100644 --- a/src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegmentSpan.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegmentSpan.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Splines; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; internal struct JxlSplineSegmentSpan(int startInclusive, int endInclusive) { From 6eda7e2ad440a31a21fbbe776842b2ea49055ab1 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:49:11 +0400 Subject: [PATCH 050/142] Remove aggressive inlining attributes, fix usings --- src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs | 4 +--- src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs | 4 +--- src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs | 2 -- src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs | 2 +- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs index 398587baac..0b2640467b 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs @@ -1,8 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; @@ -17,7 +16,6 @@ internal static class JxlBitsCoder /// // 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) diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs index 3c4126c4b8..d45b0968af 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; @@ -15,7 +15,6 @@ 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; /// @@ -24,7 +23,6 @@ internal static class JxlF16Coder /// 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(); diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs index b097311ab5..8cd2fe7ea3 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; @@ -14,7 +13,6 @@ 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; diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs index ba121cafe2..659b6f994d 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; From f71bab20a6224973634d8fd376657ba106038547 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:52:56 +0400 Subject: [PATCH 051/142] Use distinct read-only spans --- .../FrameHeader/JxlColorTransformHelpers.cs | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs index de61664146..81bc941870 100644 --- a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs @@ -8,32 +8,24 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; /// 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 - ]; + private static ReadOnlySpan Grayscale => [0, 0, 0]; + + private static ReadOnlySpan YCbCr => [1, 0, 2]; + + private static ReadOnlySpan None => [0, 1, 2]; public static ReadOnlySpan GetJpegOrder(JxlColorTransform transform, bool isGraysacle) { if (isGraysacle) { - return JpegOrders[0]; + return Grayscale; } if (transform == JxlColorTransform.YCbCr) { - return JpegOrders[1]; - } - else if (transform == JxlColorTransform.None) - { - return JpegOrders[2]; - } - else - { - return JpegOrders[3]; + return YCbCr; } + + return None; } } From abafaf8c031b38d40bf7499332f80fd19d1c0383 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 21 Jul 2026 17:44:00 +0400 Subject: [PATCH 052/142] Add CFL See chroma_from_luma.cc and chroma_from_luma.h --- .../Formats/Jxl/Processing/JxlAcStrategy.cs | 2 +- .../Jxl/Processing/JxlChromaFromLuma.cs | 46 +++++++ .../Jxl/Processing/JxlColorCorrelation.cs | 113 ++++++++++++++++++ .../Jxl/Processing/JxlColorCorrelationMap.cs | 42 +++++++ 4 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlChromaFromLuma.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs index 84b2462591..32c7fe3e18 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using static SixLabors.ImageSharp.Formats.Jxl.JxlFrameDimensions; +using static SixLabors.ImageSharp.Formats.Jxl.Processing.JxlFrameDimensions; #pragma warning disable SA1405 // Debug.Assert should provide message text diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlChromaFromLuma.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlChromaFromLuma.cs new file mode 100644 index 0000000000..0121140472 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlChromaFromLuma.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using static SixLabors.ImageSharp.Formats.Jxl.Processing.JxlFrameDimensions; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Shared constants for CFL (Chroma From Luma) functions. +/// +internal static class JxlChromaFromLuma +{ + /// + /// Tiles are 64x64. + /// + public const int ColorTileDimension = 64; + + /// + /// Division of the color tile dimension by the block dimension. Therefore, + /// this is 8x8. + /// + public const int ColorTileDimensionInBlocks = ColorTileDimension / BlockDimensions; + + public const int DefaultColorFactor = 84; + + /// + /// Chroma From Luma fixed point precision, which is + /// 11 bits. + /// + public const int CflFixedPointPrecision = 11; + + /// + /// 524287 + /// + public const int CflFixedPointRatioMax = (256 << CflFixedPointPrecision) - 1; + + /// + /// Shared variable U32 distributions for the color factor. + /// + public static readonly JxlU32Enc ColorFactorDistribution = new( + JxlFieldExpressions.Value(DefaultColorFactor), + JxlFieldExpressions.Value(256), + JxlFieldExpressions.BitsOffset(8, 2), + JxlFieldExpressions.BitsOffset(16, 258)); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs new file mode 100644 index 0000000000..69e617ea90 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs @@ -0,0 +1,113 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlColorCorrelation +{ + private float baseCorrelationX; + private float baseCorrelationB = DefaultYToBRatio; + + private readonly float[] dcFactors = new float[4]; + private uint colorFactor = JxlChromaFromLuma.DefaultColorFactor; + private float colorScale = 1.0f / JxlChromaFromLuma.DefaultColorFactor; + + public int YToXDc { get; private set; } + + public int YToBDc { get; private set; } + + public float ColorFactor => this.colorFactor; + + public float BaseCorrelationX + { + get => this.baseCorrelationX; + set => this.baseCorrelationX = value; + } + + public float BaseCorrelationB + { + get => this.baseCorrelationB; + set => this.baseCorrelationB = value; + } + + public bool IsJpegCompatible => + this.BaseCorrelationX == 0 && + this.BaseCorrelationB == 0 && + this.YToBDc == 0 && + this.YToXDc == 0 && + this.ColorFactor == JxlChromaFromLuma.DefaultColorFactor; + + public ReadOnlySpan DcFactors => this.dcFactors; + + public float YToXRatio(int xFactor) => this.BaseCorrelationX + (xFactor * this.colorScale); + + public float YToBRatio(int bFactor) => this.BaseCorrelationB + (bFactor * this.colorScale); + + public void SetColorFactor(uint factor) + { + this.colorFactor = factor; + this.colorScale = 1f / factor; + this.RecomputeDcFactors(); + } + + public void SetYToBDc(int yToBDc) + { + this.YToBDc = yToBDc; + this.RecomputeDcFactors(); + } + + public void SetYToXDc(int yToXDc) + { + this.YToXDc = yToXDc; + this.RecomputeDcFactors(); + } + + public void RecomputeDcFactors() + { + this.dcFactors[0] = this.YToXRatio(this.YToXDc); + this.dcFactors[2] = this.YToBRatio(this.YToBDc); + } + + public bool DecodeDc(JxlBitReader reader) + { + bool allDefault = reader.ReadBoolean(); + + if (allDefault) + { + return true; + } + + this.SetColorFactor(JxlU32Coder.Read(JxlChromaFromLuma.ColorFactorDistribution, reader)); + + if (!JxlF16Coder.Read(reader, ref this.baseCorrelationX)) + { + return false; + } + + if (MathF.Abs(this.baseCorrelationX) > 4f) + { + throw new InvalidOperationException("Base X correlation is out of range"); + } + + if (!JxlF16Coder.Read(reader, ref this.baseCorrelationB)) + { + return false; + } + + if (MathF.Abs(this.baseCorrelationB) > 4f) + { + throw new InvalidOperationException("Base B correlation is out of range"); + } + + this.YToXDc = (int)reader.ReadBits32(8) + sbyte.MinValue; + this.YToBDc = (int)reader.ReadBits32(8) + sbyte.MinValue; + + this.RecomputeDcFactors(); + return true; + } + + public static int RatioJpeg(int factor) => factor * (1 << JxlChromaFromLuma.CflFixedPointPrecision) / JxlChromaFromLuma.DefaultColorFactor; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs new file mode 100644 index 0000000000..89ae3f7296 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// JPEG XL Color correlation map. +/// +internal sealed class JxlColorCorrelationMap +{ + public JxlColorCorrelation Base { get; set; } = new(); + + public JxlImageSB? YToXMap { get; set; } + + public JxlImageSB? YToBMap { get; set; } + + public bool DecodeDc(JxlBitReader reader) => this.Base.DecodeDc(reader); + + public static JxlColorCorrelationMap Create(Configuration configuration, int width, int height, bool xyb) + { + JxlColorCorrelationMap map = new(); + + (int xBlocks, int yBlocks) = (DivCeil(width, JxlChromaFromLuma.ColorTileDimension), DivCeil(height, JxlChromaFromLuma.ColorTileDimension)); + + map.YToXMap = new JxlImageSB(configuration, xBlocks, yBlocks); + map.YToBMap = new JxlImageSB(configuration, xBlocks, yBlocks); + + ZeroFillImage(map.YToXMap); + ZeroFillImage(map.YToBMap); + + if (!xyb) + { + map.Base.BaseCorrelationB = 0; + } + + map.Base.RecomputeDcFactors(); + return map; + } +} From 9c6b4efa249511d947eca6eb19c9de0c2d7a6f29 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:38:03 +0400 Subject: [PATCH 053/142] Add Huffman common methods --- src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs | 177 +++++++++++++++++++- 1 file changed, 176 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs b/src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs index 640f667401..678321bab8 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs @@ -1,9 +1,184 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; + namespace SixLabors.ImageSharp.Formats.Jxl.IO; +/// +/// Shared Huffman I/O utilities. +/// internal static class JxlHuffman { - + /// + /// Returns Reverse(Reverse(Key, Len) + 1, Len). The + /// Reverse(Key, Len) function performs bitwise reversal + /// of the len least significant bits of the key value. + /// + public static uint GetNextKey(uint key, int len) + { + uint step = 1u << (len - 1); + while ((key & step) != 0) + { + step >>= 1; + } + + return (key & (step - 1)) + step; + } + + /// + /// Replicates into every + /// times with the upper bound of . + /// + public static void ReplicateValue(Span table, int step, int end, JxlHuffmanCode code) + { + do + { + end -= step; + table[end] = code; + } + while (end > 0); + } + + /// + /// Returns the table width of the next 2nd level table. + /// + /// The histogram of bit lengths for remaining symbols + /// Code length of the next processed symbol + /// Amount of bits for the root symbol + /// Table width for the 2nd level table. + public static int NextTableBitSize(ReadOnlySpan count, int length, int rootBits) + { + uint left = 1u << (length - rootBits); + + while (length < JxlAnsConstants.PrefixMaxBits) + { + if (left <= count[length]) + { + break; + } + + left -= count[length]; + length++; + left <<= 1; + } + + return length - rootBits; + } + + public static uint BuildHuffmanTable( + Span rootTable, + int rootBits, + ReadOnlySpan codeLengths, + Span count) + { + if (codeLengths.Length > (1u << JxlAnsConstants.PrefixMaxBits)) + { + return 0u; + } + + Span offset = stackalloc ushort[JxlAnsConstants.PrefixMaxBits + 1]; + + Span sortedStorage = stackalloc ushort[codeLengths.Length]; + + int maxLength = 1; + ushort sum = 0; + int len, symbol; + for (len = 1; len <= JxlAnsConstants.PrefixMaxBits; len++) + { + offset[len] = sum; + + if (count[len] != 0) + { + sum = (ushort)(sum + count[len]); + maxLength = len; + } + } + + for (symbol = 0; symbol < codeLengths.Length; symbol++) + { + if (codeLengths[symbol] != 0) + { + sortedStorage[offset[codeLengths[symbol]]++] = (ushort)symbol; + } + } + + Span table = rootTable; + int tableBits = rootBits; + uint tableSize = 1u << tableBits; + uint totalSize = tableSize; + + JxlHuffmanCode code = default; + + if (offset[JxlAnsConstants.PrefixMaxBits] == 1) + { + code.Bits = 0; + code.Value = sortedStorage[0]; + + for (int i = 0; i < totalSize; i++) + { + table[i] = code; + } + } + + if (tableBits > maxLength) + { + tableBits = maxLength; + tableSize = 1u << tableBits; + } + + int key = 0; + code.Bits = 0; + int step = 2; + + do + { + for (; count[code.Bits] != 0; --count[code.Bits]) + { + code.Value = sortedStorage[symbol++]; + ReplicateValue(table[key..], step, (int)tableSize, code); + key = (int)GetNextKey((uint)key, code.Bits); + } + + step <<= 1; + } + while (++code.Bits <= tableBits); + + while (totalSize != tableSize) + { + table[..(int)tableSize].CopyTo(table[(int)tableSize..]); + tableSize <<= 1; + } + + uint mask = totalSize - 1u; + int low = -1; + + uint tableOffset = 0; + for (step = 2; len <= maxLength; len++, step <<= 1) + { + for (; count[len] != 0; --count[len]) + { + if ((key & mask) != low) + { + tableOffset += tableSize; + table = table[(int)tableSize..]; + tableBits = NextTableBitSize(count, len, rootBits); + tableSize = 1u << tableBits; + totalSize += tableSize; + low = key & (int)mask; + + rootTable[low].Bits = (byte)(tableBits + rootBits); + rootTable[low].Value = (ushort)(tableOffset - low); + } + + code.Bits = (byte)(len - rootBits); + code.Value = sortedStorage[symbol++]; + + ReplicateValue(table[(key >> rootBits)..], step, (int)tableSize, code); + key = (int)GetNextKey((uint)key, len); + } + } + + return totalSize; + } } From 359125039eda7cabaf0061b24946599fe148a82e Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:47:40 +0400 Subject: [PATCH 054/142] Start work on Butteraugli See butteraugli.h Added the parameters structure --- .../Butteraugli/ButteraugliParameters.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliParameters.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliParameters.cs new file mode 100644 index 0000000000..9b5fd57618 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliParameters.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; + +/// +/// Parameters for Butteraugli. +/// +internal struct ButteraugliParameters() +{ + /// + /// Multiplier for penalizing new HF artifacts more than + /// blurring away features. Value of 1.0 represents neutral. + /// + public float HfAsymmetry = 1f; + + /// + /// Multiplier for the psychovisual difference in the X channel. + /// + public float XMultiplier = 1f; + + /// + /// Number of nits that correspond to 1.0f input values. + /// + public float IntensityTarget = 80f; +} From 765c892df0b1443d4192963afdc883cd08fe209d Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 25 Jul 2026 00:03:38 +0400 Subject: [PATCH 055/142] Implement Butteraugli core functions --- .../Jxl/Processing/Butteraugli/Butteraugli.cs | 2333 +++++++++++++++++ .../Butteraugli/ButteraugliComparator.cs | 46 + .../Butteraugli/ButteraugliParameters.cs | 12 +- .../Formats/Jxl/Processing/JxlQuantizer.cs | 1 - .../Jxl/Processing/JxlWeightsSeparable5.cs | 8 +- 5 files changed, 2390 insertions(+), 10 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs new file mode 100644 index 0000000000..eb25583ecf --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -0,0 +1,2333 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; + +/// +/// Implementation of Google Butteraugli, an advanced +/// image comparison system. Unlike PSNR or SSIM, +/// Butteraugli compares images the way humans may +/// spot differences instead of pixelwise. +/// +internal static class Butteraugli +{ + // NOTE: The meaning of those constants + // is not perfectly understood. + public const float WMfMalta = 37.0819870399f; + public const float Norm1Mf = 130262059.556f; + public const float WMfMaltaX = 8246.75321353f; + public const float Norm1MfX = 1009002.70582f; + + public const float WHfMalta = 18.7237414387f; + public const float Norm1Hf = 4498534.45232f; + public const float WHfMaltaX = 6923.99476109f; + public const float Norm1HfX = 8051.15833247f; + + public const float WUhfMalta = 1.10039032555f; + public const float Norm1Uhf = 71.7800275169f; + public const float WUhfMaltaX = 173.5f; + public const float Norm1UhfX = 5.0f; + + private const float IntensityTargetNormalizationHack = 0.79079917404f; + + private static readonly float InternalGoodQualityThreshold = + 17.83f * IntensityTargetNormalizationHack; + + private static readonly float GlobalScale = + 1.0f / InternalGoodQualityThreshold; + + public static ReadOnlySpan Wmul => + [ + 400.0, 1.50815703118, 0, + 2150.0, 10.6195433239, 16.2176043152, + 29.2353797994, 0.844626970982, 0.703646627719, + ]; + + public static ReadOnlySpan ComputeKernel(float sigma) + { + const float m = 2.25f; // Accuracy increases when m is increased + float scaler = -1.0f / (2.0f * sigma * sigma); + int diff = Math.Max(1, (int)(m * MathF.Abs(sigma))); + + // Use new because there's only up to 3 elements + float[] kernel = new float[(2 * diff) + 1]; + + for (int i = -diff; i <= diff; i++) + { + kernel[i + diff] = MathF.Exp(scaler * i * i); + } + + return kernel; + } + + public static void ConvolveBorderColumn( + JxlImageF input, + ReadOnlySpan kernel, + int x, + Span rowOut) + { + int offset = kernel.Length / 2; + + int minX = x < offset ? 0 : x - offset; + int maxX = Math.Min(input.XSize - 1, x + offset); + + float weight = 0.0f; + for (int j = minX; j <= maxX; j++) + { + weight += kernel[j - x + offset]; + } + + float scale = 1.0f / weight; + + for (int y = 0; y < input.YSize; y++) + { + Span rowIn = input.GetRow(y); + + float sum = 0.0f; + + for (int j = minX; j <= maxX; j++) + { + sum += rowIn[j] * kernel[j - x + offset]; + } + + rowOut[y] = sum * scale; + } + } + + public static bool ConvolutionWithTranspose( + JxlImageF input, + ReadOnlySpan kernel, + JxlImageF output) + { + if (output.XSize != input.YSize) + { + return false; + } + + if (output.YSize != input.XSize) + { + return false; + } + + int len = kernel.Length; + int offset = len / 2; + + float weightNoBorder = 0.0f; + + for (int j = 0; j < len; j++) + { + weightNoBorder += kernel[j]; + } + + float scaleNoBorder = 1.0f / weightNoBorder; + + int border1 = Math.Min(input.XSize, offset); + int border2 = input.XSize > offset ? input.XSize - offset : 0; + + Span scaledKernel = stackalloc float[(len / 2) + 1]; + + for (int i = 0; i <= len / 2; i++) + { + scaledKernel[i] = kernel[i] * scaleNoBorder; + } + + // Middle + switch (len) + { + case 7: + { + float sk0 = scaledKernel[0]; + float sk1 = scaledKernel[1]; + float sk2 = scaledKernel[2]; + float sk3 = scaledKernel[3]; + + for (int y = 0; y < input.YSize; y++) + { + Span rowIn = input.GetRow(y); + + for (int x = border1; x < border2; x++) + { + int i = x - border1; + + float sum0 = (rowIn[i + 0] + rowIn[i + 6]) * sk0; + float sum1 = (rowIn[i + 1] + rowIn[i + 5]) * sk1; + float sum2 = (rowIn[i + 2] + rowIn[i + 4]) * sk2; + float sum = (rowIn[i + 3] * sk3) + sum0 + sum1 + sum2; + + output.GetRow(x)[y] = sum; + } + } + + break; + } + + case 13: + { + for (int y = 0; y < input.YSize; y++) + { + Span rowIn = input.GetRow(y); + + for (int x = border1; x < border2; x++) + { + int i = x - border1; + + float sum0 = (rowIn[i + 0] + rowIn[i + 12]) * scaledKernel[0]; + float sum1 = (rowIn[i + 1] + rowIn[i + 11]) * scaledKernel[1]; + float sum2 = (rowIn[i + 2] + rowIn[i + 10]) * scaledKernel[2]; + float sum3 = (rowIn[i + 3] + rowIn[i + 9]) * scaledKernel[3]; + + sum0 += (rowIn[i + 4] + rowIn[i + 8]) * scaledKernel[4]; + sum1 += (rowIn[i + 5] + rowIn[i + 7]) * scaledKernel[5]; + + float sum = rowIn[i + 6] * scaledKernel[6]; + + output.GetRow(x)[y] = sum + sum0 + sum1 + sum2 + sum3; + } + } + + break; + } + + case 15: + { + for (int y = 0; y < input.YSize; y++) + { + Span rowIn = input.GetRow(y); + + for (int x = border1; x < border2; x++) + { + int i = x - border1; + + float sum0 = (rowIn[i + 0] + rowIn[i + 14]) * scaledKernel[0]; + float sum1 = (rowIn[i + 1] + rowIn[i + 13]) * scaledKernel[1]; + float sum2 = (rowIn[i + 2] + rowIn[i + 12]) * scaledKernel[2]; + float sum3 = (rowIn[i + 3] + rowIn[i + 11]) * scaledKernel[3]; + + sum0 += (rowIn[i + 4] + rowIn[i + 10]) * scaledKernel[4]; + sum1 += (rowIn[i + 5] + rowIn[i + 9]) * scaledKernel[5]; + sum2 += (rowIn[i + 6] + rowIn[i + 8]) * scaledKernel[6]; + + float sum = rowIn[i + 7] * scaledKernel[7]; + + output.GetRow(x)[y] = sum + sum0 + sum1 + sum2 + sum3; + } + } + + break; + } + + case 33: + { + for (int y = 0; y < input.YSize; y++) + { + Span rowIn = input.GetRow(y); + + for (int x = border1; x < border2; x++) + { + int i = x - border1; + + float sum0 = (rowIn[i + 0] + rowIn[i + 32]) * scaledKernel[0]; + float sum1 = (rowIn[i + 1] + rowIn[i + 31]) * scaledKernel[1]; + float sum2 = (rowIn[i + 2] + rowIn[i + 30]) * scaledKernel[2]; + float sum3 = (rowIn[i + 3] + rowIn[i + 29]) * scaledKernel[3]; + + sum0 += (rowIn[i + 4] + rowIn[i + 28]) * scaledKernel[4]; + sum1 += (rowIn[i + 5] + rowIn[i + 27]) * scaledKernel[5]; + sum2 += (rowIn[i + 6] + rowIn[i + 26]) * scaledKernel[6]; + sum3 += (rowIn[i + 7] + rowIn[i + 25]) * scaledKernel[7]; + + sum0 += (rowIn[i + 8] + rowIn[i + 24]) * scaledKernel[8]; + sum1 += (rowIn[i + 9] + rowIn[i + 23]) * scaledKernel[9]; + sum2 += (rowIn[i + 10] + rowIn[i + 22]) * scaledKernel[10]; + sum3 += (rowIn[i + 11] + rowIn[i + 21]) * scaledKernel[11]; + + sum0 += (rowIn[i + 12] + rowIn[i + 20]) * scaledKernel[12]; + sum1 += (rowIn[i + 13] + rowIn[i + 19]) * scaledKernel[13]; + sum2 += (rowIn[i + 14] + rowIn[i + 18]) * scaledKernel[14]; + sum3 += (rowIn[i + 15] + rowIn[i + 17]) * scaledKernel[15]; + + float sum = rowIn[i + 16] * scaledKernel[16]; + + output.GetRow(x)[y] = sum + sum0 + sum1 + sum2 + sum3; + } + } + + break; + } + + default: + throw new NotSupportedException($"Kernel size {len} not implemented."); + } + + // Left border + for (int x = 0; x < border1; x++) + { + ConvolveBorderColumn(input, kernel, x, output.GetRow(x)); + } + + // Right border + for (int x = border2; x < input.XSize; x++) + { + ConvolveBorderColumn(input, kernel, x, output.GetRow(x)); + } + + return true; + } + + private static bool Blur( + JxlImageF input, + float sigma, + in ButteraugliParameters parameters, + ButteraugliBlurTemp temp, + JxlImageF output) + { + ReadOnlySpan kernel = ComputeKernel(sigma); + + // Separable5 does an in-place convolution, so this fast path is not safe + // if input aliases output. + if (kernel.Length == 5 && !ReferenceEquals(input, output)) + { + float sumWeights = 0.0f; + + foreach (float w in kernel) + { + sumWeights += w; + } + + float scale = 1.0f / sumWeights; + + float w0 = kernel[2] * scale; + float w1 = kernel[1] * scale; + float w2 = kernel[0] * scale; + + JxlWeightsSeparable5 weights = default; + FillRep4(ref weights.Horizontal, w0, w1, w2); + FillRep4(ref weights.Vertical, w0, w1, w2); + + if (!Separable5(input, input.GetRectangle(), weights, null, output)) + { + return false; + } + + return true; + } + + if (!temp.GetTransposed(input, out JxlImageF tempT)) + { + return false; + } + + if (!ConvolutionWithTranspose(input, kernel, tempT)) + { + return false; + } + + if (!ConvolutionWithTranspose(tempT, kernel, output)) + { + return false; + } + + return true; + } + + /// + /// Equivalent to HWY_REP4. + /// + private static void FillRep4(ref InlineArray12 values, float a, float b, float c) + { + for (int i = 0; i < 4; i++) + { + values[i] = a; + values[4 + i] = b; + values[8 + i] = c; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector MaximumClamp(Vector value, float maximum) + { + Vector multiplier = new(0.724216145665f); + Vector maximumValue = new(maximum); + Vector ifPositive = ((value - maximumValue) * multiplier) + maximumValue; + Vector ifNegative = ((value + maximumValue) * multiplier) - maximumValue; + Vector positiveOrValue = Vector.ConditionalSelect(Vector.GreaterThan(value, maximumValue), ifPositive, value); + Vector result = Vector.ConditionalSelect(Vector.LessThan(value, Vector.Negate(maximumValue)), ifNegative, positiveOrValue); + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector RemoveRangeAroundZero(Vector x, float width) + { + Vector w = new(width); + + return Vector.ConditionalSelect( + Vector.GreaterThan(x, w), + x - w, + Vector.ConditionalSelect( + Vector.LessThan(x, -w), + x + w, + Vector.Zero)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector AmplifyRangeAroundZero(Vector x, float width) + { + Vector w = new(width); + + return Vector.ConditionalSelect( + Vector.GreaterThan(x, w), + x + w, + Vector.ConditionalSelect( + Vector.LessThan(x, -w), + x - w, + x + x)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void XybLowFrequencyToValues( + Vector x, + Vector y, + Vector bArg, + out Vector valX, + out Vector valY, + out Vector valB) + { + Vector xMul = new(33.832837186260f); + Vector yMul = new(14.458268100570f); + Vector bMul = new(49.87984651440f); + Vector yToBMul = new(-0.362267051518f); + + Vector b = (yToBMul * y) + bArg; + + valB = b * bMul; + valX = x * xMul; + valY = y * yMul; + } + + public static void XybLowFrequencyToValues(JxlImage3F xybLf) + { + int lanes = Vector.Count; + + for (int y = 0; y < xybLf.YSize; y++) + { + Span rowX = xybLf.PlaneRow(0, y); + Span rowY = xybLf.PlaneRow(1, y); + Span rowB = xybLf.PlaneRow(2, y); + + for (int x = 0; x < xybLf.XSize; x += lanes) + { + Vector valX = new(rowX.Slice(x, lanes)); + Vector valY = new(rowY.Slice(x, lanes)); + Vector valB = new(rowB.Slice(x, lanes)); + + XybLowFrequencyToValues( + valX, + valY, + valB, + out valX, + out valY, + out valB); + + valX.CopyTo(rowX.Slice(x, lanes)); + valY.CopyTo(rowY.Slice(x, lanes)); + valB.CopyTo(rowB.Slice(x, lanes)); + } + } + } + + public static bool SuppressXByY(JxlImageF inY, JxlImageF inOutX) + { + if (!SameSize(inOutX, inY)) + { + return false; + } + + int xSize = inY.XSize; + int ySize = inY.YSize; + int lanes = Vector.Count; + + const float suppress = 46.0f; + const float s = 0.653020556257f; + + Vector sv = new(s); + Vector oneMinusS = new(1.0f - s); + Vector ywv = new(suppress); + + for (int y = 0; y < ySize; y++) + { + ReadOnlySpan rowY = inY.GetRow(y); + Span rowX = inOutX.GetRow(y); + + for (int x = 0; x < xSize; x += lanes) + { + Vector vx = new(rowX.Slice(x, lanes)); + Vector vy = new(rowY.Slice(x, lanes)); + + Vector scaler = + ((ywv / ((vy * vy) + ywv)) * oneMinusS) + sv; + + (scaler * vx).CopyTo(rowX.Slice(x, lanes)); + } + } + + return true; + } + + public static void Subtract(JxlPlane a, JxlPlane b, JxlPlane c) + { + int lanes = Vector.Count; + + for (int y = 0; y < a.YSize; y++) + { + ReadOnlySpan rowA = a.GetRow(y); + ReadOnlySpan rowB = b.GetRow(y); + Span rowC = c.GetRow(y); + + for (int x = 0; x < a.XSize; x += lanes) + { + Vector va = new(rowA.Slice(x, lanes)); + Vector vb = new(rowB.Slice(x, lanes)); + + (va - vb).CopyTo(rowC.Slice(x, lanes)); + } + } + } + + public static bool SeparateLFAndMF( + in ButteraugliParameters parameters, + JxlImage3F xyb, + JxlImage3F lf, + JxlImage3F mf, + ButteraugliBlurTemp blurTemp) + { + const float sigmaLf = 7.15593339443f; + + for (int i = 0; i < 3; i++) + { + if (!Blur( + xyb.Plane(i), + sigmaLf, + parameters, + blurTemp, + lf.Plane(i))) + { + return false; + } + + Subtract( + xyb.Plane(i), + lf.Plane(i), + mf.Plane(i)); + } + + XybLowFrequencyToValues(lf); + + return true; + } + + public static bool SeparateMfAndHf( + Configuration configuration, + in ButteraugliParameters parameters, + JxlImage3F mf, + JxlImageF[] hf, + BlurTemp blurTemp) + { + const float sigmaHf = 3.22489901262f; + + int xSize = mf.XSize; + int ySize = mf.YSize; + + hf[0] = new JxlImageF(configuration, xSize, ySize); + hf[1] = new JxlImageF(configuration, xSize, ySize); + + int lanes = Vector.Count; + + for (int i = 0; i < 3; i++) + { + if (i == 2) + { + if (!Blur(mf.Plane(i), sigmaHf, parameters, blurTemp, mf.Plane(i))) + { + return false; + } + + break; + } + + for (int y = 0; y < ySize; y++) + { + Span rowMf = mf.PlaneRow(i, y); + Span rowHf = hf[i].GetRow(y); + + for (int x = 0; x < xSize; x += lanes) + { + new Vector(rowMf.Slice(x, lanes)) + .CopyTo(rowHf.Slice(x, lanes)); + } + } + + if (!Blur(mf.Plane(i), sigmaHf, parameters, blurTemp, mf.Plane(i))) + { + return false; + } + + if (i == 0) + { + const float removeMfRange = 0.29f; + + for (int y = 0; y < ySize; y++) + { + Span rowMf = mf.PlaneRow(0, y); + Span rowHf = hf[0].GetRow(y); + + for (int x = 0; x < xSize; x += lanes) + { + Vector mfv = new(rowMf.Slice(x, lanes)); + Vector hfv = new Vector(rowHf.Slice(x, lanes)) - mfv; + + mfv = RemoveRangeAroundZero(mfv, removeMfRange); + + mfv.CopyTo(rowMf.Slice(x, lanes)); + hfv.CopyTo(rowHf.Slice(x, lanes)); + } + } + } + else + { + const float addMfRange = 0.1f; + + for (int y = 0; y < ySize; y++) + { + Span rowMf = mf.PlaneRow(1, y); + Span rowHf = hf[1].GetRow(y); + + for (int x = 0; x < xSize; x += lanes) + { + Vector mfv = new(rowMf.Slice(x, lanes)); + Vector hfv = new Vector(rowHf.Slice(x, lanes)) - mfv; + + mfv = AmplifyRangeAroundZero(mfv, addMfRange); + + mfv.CopyTo(rowMf.Slice(x, lanes)); + hfv.CopyTo(rowHf.Slice(x, lanes)); + } + } + } + } + + return SuppressXByY(hf[1], hf[0]); + } + + public static bool SeparateHFAndUHF( + in ButteraugliParameters parameters, + JxlImageF[] hf, + JxlImageF[] uhf, + JxlBlurTemp blurTemp) + { + const float sigmaUhf = 1.56416327805f; + + int xSize = hf[0].XSize; + int ySize = hf[0].YSize; + + uhf[0] = new JxlImageF(xSize, ySize); + uhf[1] = new JxlImageF(xSize, ySize); + + int lanes = Vector.Count; + + for (int i = 0; i < 2; i++) + { + for (int y = 0; y < ySize; y++) + { + Span rowUhf = uhf[i].GetRow(y); + Span rowHf = hf[i].GetRow(y); + + for (int x = 0; x < xSize; x += lanes) + { + new Vector(rowHf.Slice(x, lanes)).CopyTo(rowUhf.Slice(x, lanes)); + } + } + + if (!Blur(hf[i], sigmaUhf, parameters, blurTemp, hf[i])) + { + return false; + } + + if (i == 0) + { + const float removeHfRange = 1.5f; + const float removeUhfRange = 0.04f; + + for (int y = 0; y < ySize; y++) + { + Span rowUhf = uhf[0].GetRow(y); + Span rowHf = hf[0].GetRow(y); + + for (int x = 0; x < xSize; x += lanes) + { + Vector hfv = new(rowHf.Slice(x, lanes)); + Vector uhfv = new Vector(rowUhf.Slice(x, lanes)) - hfv; + + hfv = RemoveRangeAroundZero(hfv, removeHfRange); + uhfv = RemoveRangeAroundZero(uhfv, removeUhfRange); + + hfv.CopyTo(rowHf.Slice(x, lanes)); + uhfv.CopyTo(rowUhf.Slice(x, lanes)); + } + } + } + else + { + const float addHfRange = 0.132f; + const float maxClampHf = 28.4691806922f; + const float maxClampUhf = 5.19175294647f; + const float mulYHf = 2.155f; + const float mulYUhf = 2.69313763794f; + + Vector mulHf = new(mulYHf); + Vector mulUhf = new(mulYUhf); + + for (int y = 0; y < ySize; y++) + { + Span rowUhf = uhf[1].GetRow(y); + Span rowHf = hf[1].GetRow(y); + + for (int x = 0; x < xSize; x += lanes) + { + Vector hfv = new(rowHf.Slice(x, lanes)); + hfv = MaximumClamp(hfv, maxClampHf); + + Vector uhfv = new Vector(rowUhf.Slice(x, lanes)) - hfv; + + uhfv = MaximumClamp(uhfv, maxClampUhf); + uhfv *= mulUhf; + + uhfv.CopyTo(rowUhf.Slice(x, lanes)); + + hfv *= mulHf; + hfv = AmplifyRangeAroundZero(hfv, addHfRange); + + hfv.CopyTo(rowHf.Slice(x, lanes)); + } + } + } + } + + return true; + } + + public static void DeallocateHFAndUHF(JxlImageF[] hf, JxlImageF[] uhf) + { + for (int i = 0; i < 2; i++) + { + hf[i] = new JxlImageF(); + uhf[i] = new JxlImageF(); + } + } + + public static bool SeparateFrequencies( + Configuration configuration, + in ButteraugliParameters parameters, + BlurTemp blurTemp, + JxlImage3F xyb, + PsychoImage ps) + { + ps.Lf = JxlImage3F.Create( + configuration, + xyb.XSize, + xyb.YSize); + + ps.Mf = JxlImage3F.Create( + configuration, + xyb.XSize, + xyb.YSize); + + if (!SeparateLFAndMF( + parameters, + xyb, + ps.Lf, + ps.Mf, + blurTemp)) + { + return false; + } + + if (!SeparateMfAndHf( + parameters, + ps.Mf, + ps.Hf, + blurTemp)) + { + return false; + } + + if (!SeparateHFAndUHF( + parameters, + ps.Hf, + ps.Uhf, + blurTemp)) + { + return false; + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector Sum( + Vector a, + Vector b, + Vector c, + Vector d) + => (a + b) + (c + d); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector Sum( + Vector a, + Vector b, + Vector c, + Vector d, + Vector e) + => Sum(a, b, c, d + e); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector Sum( + Vector a, + Vector b, + Vector c, + Vector d, + Vector e, + Vector f, + Vector g) + => Sum(a, b, c, Sum(d, e, f, g)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector Sum( + Vector a, + Vector b, + Vector c, + Vector d, + Vector e, + Vector f, + Vector g, + Vector h, + Vector i) + => (Sum(a, b, c, d) + Sum(e, f, g, h)) + i; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector MaltaUnitLF( + ReadOnlySpan row, + int index, + int xs) + { + // helper + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static Vector Load(ReadOnlySpan row, int index) + => new(row.Slice(index, Vector.Count)); + + int xs3 = 3 * xs; + + Vector center = Load(row, index); + + Vector sumYConst = Sum( + Load(row, index - 4), + Load(row, index - 2), + center, + Load(row, index + 2), + Load(row, index + 4)); + + Vector retval = sumYConst * sumYConst; + + Vector sum; + + sum = Sum( + Load(row, index - xs3 - xs), + Load(row, index - xs - xs), + center, + Load(row, index + xs + xs), + Load(row, index + xs3 + xs)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 - 3), + Load(row, index - xs - xs - 2), + center, + Load(row, index + xs + xs + 2), + Load(row, index + xs3 + 3)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 + 3), + Load(row, index - xs - xs + 2), + center, + Load(row, index + xs + xs - 2), + Load(row, index + xs3 - 3)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 - xs + 1), + Load(row, index - xs - xs + 1), + center, + Load(row, index + xs + xs - 1), + Load(row, index + xs3 + xs - 1)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 - xs - 1), + Load(row, index - xs - xs - 1), + center, + Load(row, index + xs + xs + 1), + Load(row, index + xs3 + xs + 1)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - 4 - xs), + Load(row, index - 2 - xs), + center, + Load(row, index + 2 + xs), + Load(row, index + 4 + xs)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - 4 + xs), + Load(row, index - 2 + xs), + center, + Load(row, index + 2 - xs), + Load(row, index + 4 - xs)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 - 2), + Load(row, index - xs - xs - 1), + center, + Load(row, index + xs + xs + 1), + Load(row, index + xs3 + 2)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 + 2), + Load(row, index - xs - xs + 1), + center, + Load(row, index + xs + xs - 1), + Load(row, index + xs3 - 2)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs - xs - 3), + Load(row, index - xs - 2), + center, + Load(row, index + xs + 2), + Load(row, index + xs + xs + 3)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs - xs + 3), + Load(row, index - xs + 2), + center, + Load(row, index + xs - 2), + Load(row, index + xs + xs - 3)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index + xs + xs - 4), + Load(row, index + xs - 2), + center, + Load(row, index - xs + 2), + Load(row, index - xs - xs + 4)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs - xs - 4), + Load(row, index - xs - 2), + center, + Load(row, index + xs + 2), + Load(row, index + xs + xs + 4)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 - xs - 2), + Load(row, index - xs - xs - 1), + center, + Load(row, index + xs + xs + 1), + Load(row, index + xs3 + xs + 2)); + retval = (sum * sum) + retval; + + sum = Sum( + Load(row, index - xs3 - xs + 2), + Load(row, index - xs - xs + 1), + center, + Load(row, index + xs + xs - 1), + Load(row, index + xs3 + xs - 2)); + retval = (sum * sum) + retval; + + return retval; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector MaltaUnit( + ReadOnlySpan row, + int index, + int xs) + { + // helper + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static Vector Load(ReadOnlySpan row, int index) + => new(row.Slice(index, Vector.Count)); + + int xs3 = 3 * xs; + + Vector center = Load(row, index); + + Vector sumYConst = Sum( + Load(row, index - 4), + Load(row, index - 3), + Load(row, index - 2), + Load(row, index - 1), + center, + Load(row, index + 1), + Load(row, index + 2), + Load(row, index + 3), + Load(row, index + 4)); + + Vector retval = sumYConst * sumYConst; + + Vector sum; + + sum = Sum( + Load(row, index - xs3 - xs), + Load(row, index - xs3), + Load(row, index - xs - xs), + Load(row, index - xs), + center, + Load(row, index + xs), + Load(row, index + xs + xs), + Load(row, index + xs3), + Load(row, index + xs3 + xs)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 - 3), + Load(row, index - xs - xs - 2), + Load(row, index - xs - 1), + center, + Load(row, index + xs + 1), + Load(row, index + xs + xs + 2), + Load(row, index + xs3 + 3)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 + 3), + Load(row, index - xs - xs + 2), + Load(row, index - xs + 1), + center, + Load(row, index + xs - 1), + Load(row, index + xs + xs - 2), + Load(row, index + xs3 - 3)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 - xs + 1), + Load(row, index - xs3 + 1), + Load(row, index - xs - xs + 1), + Load(row, index - xs), + center, + Load(row, index + xs), + Load(row, index + xs + xs - 1), + Load(row, index + xs3 - 1), + Load(row, index + xs3 + xs - 1)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 - xs - 1), + Load(row, index - xs3 - 1), + Load(row, index - xs - xs - 1), + Load(row, index - xs), + center, + Load(row, index + xs), + Load(row, index + xs + xs + 1), + Load(row, index + xs3 + 1), + Load(row, index + xs3 + xs + 1)); + retval += sum * sum; + + sum = Sum( + Load(row, index - 4 - xs), + Load(row, index - 3 - xs), + Load(row, index - 2 - xs), + Load(row, index - 1), + center, + Load(row, index + 1), + Load(row, index + 2 + xs), + Load(row, index + 3 + xs), + Load(row, index + 4 + xs)); + retval += sum * sum; + + sum = Sum( + Load(row, index - 4 + xs), + Load(row, index - 3 + xs), + Load(row, index - 2 + xs), + Load(row, index - 1), + center, + Load(row, index + 1), + Load(row, index + 2 - xs), + Load(row, index + 3 - xs), + Load(row, index + 4 - xs)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 - 2), + Load(row, index - xs - xs - 1), + Load(row, index - xs - 1), + center, + Load(row, index + xs + 1), + Load(row, index + xs + xs + 1), + Load(row, index + xs3 + 2)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 + 2), + Load(row, index - xs - xs + 1), + Load(row, index - xs + 1), + center, + Load(row, index + xs - 1), + Load(row, index + xs + xs - 1), + Load(row, index + xs3 - 2)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs - xs - 3), + Load(row, index - xs - 2), + Load(row, index - xs - 1), + center, + Load(row, index + xs + 1), + Load(row, index + xs + 2), + Load(row, index + xs + xs + 3)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs - xs + 3), + Load(row, index - xs + 2), + Load(row, index - xs + 1), + center, + Load(row, index + xs - 1), + Load(row, index + xs - 2), + Load(row, index + xs + xs - 3)); + retval += sum * sum; + + sum = Sum( + Load(row, index + xs - 4), + Load(row, index + xs - 3), + Load(row, index + xs - 2), + Load(row, index - 1), + center, + Load(row, index + 1), + Load(row, index - xs + 2), + Load(row, index - xs + 3), + Load(row, index - xs + 4)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs - 4), + Load(row, index - xs - 3), + Load(row, index - xs - 2), + Load(row, index - 1), + center, + Load(row, index + 1), + Load(row, index + xs + 2), + Load(row, index + xs + 3), + Load(row, index + xs + 4)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 - xs - 1), + Load(row, index - xs3 - 1), + Load(row, index - xs - xs - 1), + Load(row, index - xs), + center, + Load(row, index + xs), + Load(row, index + xs + xs + 1), + Load(row, index + xs3 + 1), + Load(row, index + xs3 + xs + 1)); + retval += sum * sum; + + sum = Sum( + Load(row, index - xs3 - xs + 1), + Load(row, index - xs3 + 1), + Load(row, index - xs - xs + 1), + Load(row, index - xs), + center, + Load(row, index + xs), + Load(row, index + xs + xs - 1), + Load(row, index + xs3 - 1), + Load(row, index + xs3 + xs - 1)); + retval += sum * sum; + + return retval; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float PaddedMaltaUnit( + JxlImageF diffs, + int x0, + int y0, + bool isLF) + { + if (x0 >= 4 && + y0 >= 4 && + x0 < diffs.XSize - 4 && + y0 < diffs.YSize - 4) + { + return isLF + ? MaltaUnitLF( + diffs.GetRow(y0), + x0, + diffs.PixelsPerRow)[0] + : MaltaUnit( + diffs.GetRow(y0), + x0, + diffs.PixelsPerRow)[0]; + } + + Span borderImage = stackalloc float[12 * 9]; + + for (int dy = 0; dy < 9; dy++) + { + int y = y0 + dy - 4; + + if (y < 0 || y >= diffs.YSize) + { + borderImage.Slice(dy * 12, 12).Clear(); + continue; + } + + ReadOnlySpan rowDiffs = diffs.GetRow(y); + + for (int dx = 0; dx < 9; dx++) + { + int x = x0 + dx - 4; + + borderImage[(dy * 12) + dx] = + x < 0 || x >= diffs.XSize + ? 0.0f + : rowDiffs[x]; + } + + borderImage.Slice((dy * 12) + 9, 3).Clear(); + } + + return isLF + ? MaltaUnitLF( + diffs.GetRow(y0), + x0, + diffs.PixelsPerRow)[0] + : MaltaUnit( + borderImage, + (4 * 12) + 4, + 12)[0]; + } + + public static bool MaltaDiffMap( + bool isLf, + JxlImageF lum0, + JxlImageF lum1, + float w0Gt1, + float w0Lt1, + float norm1, + float len, + float mulli, + JxlImageF diffs, + JxlImageF blockDiffAc) + { + if (!SameSize(lum0, lum1) || !SameSize(lum0, diffs)) + { + return false; + } + + int width = lum0.XSize; + int height = lum0.YSize; + + const float weight0 = 0.5f; + const float weight1 = 0.33f; + + float norm2_0Gt1 = + (float)(mulli * MathF.Sqrt(weight0 * w0Gt1) / ((len * 2) + 1) * norm1); + + float norm2_0Lt1 = + (float)(mulli * MathF.Sqrt(weight1 * w0Lt1) / ((len * 2) + 1) * norm1); + + for (int y = 0; y < height; y++) + { + ReadOnlySpan row0 = lum0.GetRow(y); + ReadOnlySpan row1 = lum1.GetRow(y); + Span rowDiffs = diffs.GetRow(y); + + for (int x = 0; x < width; x++) + { + float absVal = 0.5f * + (MathF.Abs(row0[x]) + MathF.Abs(row1[x])); + + float diff = row0[x] - row1[x]; + + float scaler = norm2_0Gt1 / ((float)norm1 + absVal); + + rowDiffs[x] = scaler * diff; + + float scaler2 = norm2_0Lt1 / ((float)norm1 + absVal); + + float fabs0 = MathF.Abs(row0[x]); + + float tooSmall = 0.55f * fabs0; + float tooBig = 1.05f * fabs0; + + if (row0[x] < 0) + { + if (row1[x] > -tooSmall) + { + rowDiffs[x] -= scaler2 * (row1[x] + tooSmall); + } + else if (row1[x] < -tooBig) + { + rowDiffs[x] += scaler2 * (-row1[x] - tooBig); + } + } + else + { + if (row1[x] < tooSmall) + { + rowDiffs[x] += scaler2 * (tooSmall - row1[x]); + } + else if (row1[x] > tooBig) + { + rowDiffs[x] -= scaler2 * (row1[x] - tooBig); + } + } + } + } + + int y0 = 0; + + for (; y0 < 4; y0++) + { + Span row = blockDiffAc.GetRow(y0); + + for (int x = 0; x < width; x++) + { + row[x] += PaddedMaltaUnit(diffs, x, y0, isLf); + } + } + + int lanes = Vector.Count; + int alignedX = Math.Max(4, lanes); + + int stride = diffs.PixelsPerRow; + + for (; y0 < height - 4; y0++) + { + ReadOnlySpan input = diffs.GetRow(y0); + Span output = blockDiffAc.GetRow(y0); + ref float outputReference = ref MemoryMarshal.GetReference(output); + + int x = 0; + + for (; x < alignedX; x++) + { + output[x] += PaddedMaltaUnit(diffs, x, y0, isLf); + } + + for (; x + lanes + 4 <= width; x += lanes) + { + Vector value = Vector.LoadUnsafe(ref Unsafe.Add(ref outputReference, x)); + + Vector malta = isLf + ? MaltaUnitLF(input, x, stride) + : MaltaUnit(input, x, stride); + + (value + malta).CopyTo(output[x..]); + } + + for (; x < width; x++) + { + output[x] += PaddedMaltaUnit(diffs, x, y0, isLf); + } + } + + for (; y0 < height; y0++) + { + Span row = blockDiffAc.GetRow(y0); + + for (int x = 0; x < width; x++) + { + row[x] += PaddedMaltaUnit(diffs, x, y0, isLf); + } + } + + return true; + } + + public static bool MaltaDiffMap( + JxlImageF lum0, + JxlImageF lum1, + float w0Gt1, + float w0Lt1, + float norm1, + JxlImageF diffs, + JxlImageF blockDiffAc) + { + const float len = 3.75f; + const float mulli = 0.39905817637f; + + return MaltaDiffMap( + false, + lum0, + lum1, + w0Gt1, + w0Lt1, + norm1, + len, + mulli, + diffs, + blockDiffAc); + } + + public static bool MaltaDiffMapLf( + JxlImageF lum0, + JxlImageF lum1, + float w0Gt1, + float w0Lt1, + float norm1, + JxlImageF diffs, + JxlImageF blockDiffAc) + { + const float len = 3.75f; + const float mulli = 0.611612573796f; + + return MaltaDiffMap( + true, + lum0, + lum1, + w0Gt1, + w0Lt1, + norm1, + len, + mulli, + diffs, + blockDiffAc); + } + + public static void CombineChannelsForMasking( + JxlImageF[] hf, + JxlImageF[] uhf, + JxlImageF output) + { + // Only X and Y components are involved in masking. + ReadOnlySpan muls = + [ + 2.5f, + 0.4f, + 0.4f + ]; + + int width = hf[0].XSize; + int height = hf[0].YSize; + + for (int y = 0; y < height; y++) + { + ReadOnlySpan rowYHf = hf[1].GetRow(y); + ReadOnlySpan rowYUhf = uhf[1].GetRow(y); + ReadOnlySpan rowXHf = hf[0].GetRow(y); + ReadOnlySpan rowXUhf = uhf[0].GetRow(y); + + Span row = output.GetRow(y); + + for (int x = 0; x < width; x++) + { + float xDiff = (rowXUhf[x] + rowXHf[x]) * muls[0]; + float yDiff = (rowYUhf[x] * muls[1]) + (rowYHf[x] * muls[2]); + + row[x] = MathF.Sqrt((xDiff * xDiff) + (yDiff * yDiff)); + } + } + } + + public static void DiffPrecompute( + JxlImageF xyb, + float mul, + float biasArg, + JxlImageF output) + { + int width = xyb.XSize; + int height = xyb.YSize; + + float bias = mul * biasArg; + float sqrtBias = MathF.Sqrt(bias); + + for (int y = 0; y < height; y++) + { + ReadOnlySpan rowIn = xyb.GetRow(y); + Span rowOut = output.GetRow(y); + + for (int x = 0; x < width; x++) + { + rowOut[x] = + MathF.Sqrt( + (mul * MathF.Abs(rowIn[x])) + bias) + - sqrtBias; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreMin3( + float value, + ref float min0, + ref float min1, + ref float min2) + { + if (value < min2) + { + if (value < min0) + { + min2 = min1; + min1 = min0; + min0 = value; + } + else if (value < min1) + { + min2 = min1; + min1 = value; + } + else + { + min2 = value; + } + } + } + + public static void FuzzyErosion(JxlImageF from, JxlImageF to) + { + int width = from.XSize; + int height = from.YSize; + + const int step = 3; + + for (int y = 0; y < height; y++) + { + Span output = to.GetRow(y); + + for (int x = 0; x < width; x++) + { + float min0 = from.GetRow(y)[x]; + float min1 = 2 * min0; + float min2 = min1; + + if (x >= step) + { + StoreMin3( + from.GetRow(y)[x - step], + ref min0, + ref min1, + ref min2); + + if (y >= step) + { + StoreMin3( + from.GetRow(y - step)[x - step], + ref min0, + ref min1, + ref min2); + } + + if (y < height - step) + { + StoreMin3( + from.GetRow(y + step)[x - step], + ref min0, + ref min1, + ref min2); + } + } + + if (x < width - step) + { + StoreMin3( + from.GetRow(y)[x + step], + ref min0, + ref min1, + ref min2); + + if (y >= step) + { + StoreMin3( + from.GetRow(y - step)[x + step], + ref min0, + ref min1, + ref min2); + } + + if (y < height - step) + { + StoreMin3( + from.GetRow(y + step)[x + step], + ref min0, + ref min1, + ref min2); + } + } + + if (y >= step) + { + StoreMin3( + from.GetRow(y - step)[x], + ref min0, + ref min1, + ref min2); + } + + if (y < height - step) + { + StoreMin3( + from.GetRow(y + step)[x], + ref min0, + ref min1, + ref min2); + } + + output[x] = + (0.45f * min0) + + (0.3f * min1) + + (0.25f * min2); + } + } + } + + public static bool Mask( + Configuration configuration, + JxlImageF mask0, + JxlImageF mask1, + in ButteraugliParameters parameters, + JxlBlurTemp blurTemp, + JxlImageF? diffAc, + out JxlImageF mask) + { + int width = mask0.XSize; + int height = mask0.YSize; + + mask = new(configuration, width, height); + + const float mul = 6.19424080439f; + const float bias = 12.61050594197f; + const float radius = 2.7f; + + JxlImageF diff0 = new(configuration, width, height); + JxlImageF diff1 = new(configuration, width, height); + JxlImageF blurred0 = new(configuration, width, height); + JxlImageF blurred1 = new(configuration, width, height); + + DiffPrecompute(mask0, mul, bias, diff0); + DiffPrecompute(mask1, mul, bias, diff1); + + if (!Blur(diff0, radius, parameters, blurTemp, blurred0)) + { + return false; + } + + FuzzyErosion(blurred0, diff0); + + if (!Blur(diff1, radius, parameters, blurTemp, blurred1)) + { + return false; + } + + for (int y = 0; y < height; y++) + { + Span maskRow = mask.GetRow(y); + Span diffRow = diffAc is not null ? diffAc.GetRow(y) : []; + + ReadOnlySpan diff0Row = diff0.GetRow(y); + ReadOnlySpan blur0Row = blurred0.GetRow(y); + ReadOnlySpan blur1Row = blurred1.GetRow(y); + + for (int x = 0; x < width; x++) + { + maskRow[x] = diff0Row[x]; + + if (diffRow != null) + { + const float maskToErrorMul = 10.0f; + float diff = blur0Row[x] - blur1Row[x]; + diffRow[x] += maskToErrorMul * diff * diff; + } + } + } + + return true; + } + + public static bool MaskPsychoImage( + Configuration configuration, + ButteraugliPsychoImage pi0, + ButteraugliPsychoImage pi1, + int width, + int height, + in ButteraugliParameters parameters, + BlurTemp blurTemp, + JxlImageF mask, + JxlImageF? diffAc) + { + JxlImageF mask0 = new(configuration, width, height); + JxlImageF mask1 = new(configuration, width, height); + + CombineChannelsForMasking( + pi0.Hf, + pi0.Uhf, + mask0); + + CombineChannelsForMasking( + pi1.Hf, + pi1.Uhf, + mask1); + + return Mask( + mask0, + mask1, + parameters, + blurTemp, + mask, + diffAc); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MaskY(float delta) + { + const float offset = 0.829591754942f; + const float scaler = 0.451936922203f; + const float mul = 2.5485944793f; + + float c = mul / ((scaler * delta) + offset); + float result = GlobalScale * (1.0f + c); + + return result * result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MaskDcY(float delta) + { + const float offset = 0.20025578522f; + const float scaler = 3.87449418804f; + const float mul = 0.505054525019f; + + float c = mul / ((scaler * delta) + offset); + float result = GlobalScale * (1.0f + c); + + return result * result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MaskColor( + ReadOnlySpan color, + float mask) + => (color[0] * mask) + (color[1] * mask) + (color[2] * mask); + + public static bool CombineChannelsToDiffmap( + JxlImageF mask, + JxlImage3F blockDiffDc, + JxlImage3F blockDiffAc, + float xmul, + JxlImageF result) + { + if (!SameSize(mask, result)) + { + return false; + } + + Span diffDc = stackalloc float[3]; + Span diffAc = stackalloc float[3]; + + int xsize = mask.XSize; + int ysize = mask.YSize; + + for (int y = 0; y < ysize; ++y) + { + Span rowOut = result.GetRow(y); + + for (int x = 0; x < xsize; ++x) + { + float val = mask.GetRow(y)[x]; + + float maskVal = MaskY(val); + float dcMaskVal = MaskDcY(val); + + for (int i = 0; i < 3; ++i) + { + diffDc[i] = blockDiffDc.PlaneRow(i, y)[x]; + diffAc[i] = blockDiffAc.PlaneRow(i, y)[x]; + } + + diffAc[0] *= xmul; + diffDc[0] *= xmul; + + rowOut[x] = MathF.Sqrt( + MaskColor(diffDc, dcMaskVal) + + MaskColor(diffAc, maskVal)); + } + } + + return true; + } + + public static void L2Diff( + JxlImageF i0, + JxlImageF i1, + float w, + JxlImageF diffmap) + { + if (w == 0) + { + return; + } + + for (int y = 0; y < i0.YSize; ++y) + { + ReadOnlySpan row0 = i0.GetRow(y); + ReadOnlySpan row1 = i1.GetRow(y); + Span rowDiff = diffmap.GetRow(y); + + for (int x = 0; x < i0.XSize; ++x) + { + float diff = row0[x] - row1[x]; + rowDiff[x] += diff * diff * w; + } + } + } + + public static void SetL2Diff( + JxlImageF i0, + JxlImageF i1, + float w, + JxlImageF diffmap) + { + if (w == 0) + { + return; + } + + for (int y = 0; y < i0.YSize; ++y) + { + ReadOnlySpan row0 = i0.GetRow(y); + ReadOnlySpan row1 = i1.GetRow(y); + Span rowDiff = diffmap.GetRow(y); + + for (int x = 0; x < i0.XSize; ++x) + { + float diff = row0[x] - row1[x]; + rowDiff[x] = diff * diff * w; + } + } + } + + public static void L2DiffAsymmetric( + JxlImageF i0, + JxlImageF i1, + float w0gt1, + float w0lt1, + JxlImageF diffmap) + { + if (w0gt1 == 0 && w0lt1 == 0) + { + return; + } + + Vector vw0gt1 = new(w0gt1 * 0.8f); + Vector vw0lt1 = new(w0lt1 * 0.8f); + + int lanes = Vector.Count; + + for (int y = 0; y < i0.YSize; ++y) + { + ReadOnlySpan row0 = i0.GetRow(y); + ReadOnlySpan row1 = i1.GetRow(y); + Span rowDiff = diffmap.GetRow(y); + + for (int x = 0; x < i0.XSize; x += lanes) + { + Vector val0 = new(row0[x..]); + Vector val1 = new(row1[x..]); + + // Primary symmetric quadratic objective. + Vector diff = val0 - val1; + + Vector total = + (diff * diff * vw0gt1) + new Vector(rowDiff[x..]); + + Vector fabs0 = Vector.Abs(val0); + + Vector tooSmall = fabs0 * new Vector(0.4f); + + Vector tooBig = fabs0; + + Vector ifNeg = + Vector.ConditionalSelect( + Vector.GreaterThan(val1, -tooSmall), + val1 + tooSmall, + Vector.ConditionalSelect( + Vector.LessThan(val1, -tooBig), + -val1 - tooBig, + Vector.Zero)); + + Vector ifPos = + Vector.ConditionalSelect( + Vector.LessThan(val1, tooSmall), + tooSmall - val1, + Vector.ConditionalSelect( + Vector.GreaterThan(val1, tooBig), + val1 - tooBig, + Vector.Zero)); + + Vector v = + Vector.ConditionalSelect( + Vector.LessThan(val0, Vector.Zero), + ifNeg, + ifPos); + + total += vw0lt1 * v * v; + + total.CopyTo(rowDiff[x..]); + } + } + } + + public static void OpsinAbsorbance( + bool clamp, + Vector in0, + Vector in1, + Vector in2, + out Vector out0, + out Vector out1, + out Vector out2) + { + Vector mix0 = new(0.29956550340058319f); + Vector mix1 = new(0.63373087833825936f); + Vector mix2 = new(0.077705617820981968f); + Vector mix3 = new(1.7557483643287353f); + + Vector mix4 = new(0.22158691104574774f); + Vector mix5 = new(0.69391388044116142f); + Vector mix6 = new(0.0987313588422f); + Vector mix7 = new(1.7557483643287353f); + + Vector mix8 = new(0.02f); + Vector mix9 = new(0.02f); + Vector mix10 = new(0.20480129041026129f); + Vector mix11 = new(12.226454707163354f); + + out0 = (mix0 * in0) + ((mix1 * in1) + ((mix2 * in2) + mix3)); + out1 = (mix4 * in0) + ((mix5 * in1) + ((mix6 * in2) + mix7)); + out2 = (mix8 * in0) + ((mix9 * in1) + ((mix10 * in2) + mix11)); + + if (clamp) + { + out0 = Vector.Max(out0, mix3); + out1 = Vector.Max(out1, mix7); + out2 = Vector.Max(out2, mix11); + } + } + + public static bool OpsinDynamicsImage( + JxlImage3F rgb, + in ButteraugliParameters parameters, + JxlImage3F blurred, + BlurTemp blurTemp, + JxlImage3F xyb) + { + if (blurred == null) + { + return false; + } + + const double sigma = 1.2; + + if (!Blur(rgb.Plane(0), sigma, parameters, blurTemp, blurred.Plane(0))) + { + return false; + } + + if (!Blur(rgb.Plane(1), sigma, parameters, blurTemp, blurred.Plane(1))) + { + return false; + } + + if (!Blur(rgb.Plane(2), sigma, parameters, blurTemp, blurred.Plane(2))) + { + return false; + } + + Vector intensityMultiplier = new((float)parameters.IntensityTarget); + + int lanes = Vector.Count; + + Vector minValue = new(1e-4f); + + for (int y = 0; y < rgb.YSize; ++y) + { + ReadOnlySpan rowR = rgb.PlaneRow(0, y); + ReadOnlySpan rowG = rgb.PlaneRow(1, y); + ReadOnlySpan rowB = rgb.PlaneRow(2, y); + + ReadOnlySpan blurredR = blurred.PlaneRow(0, y); + ReadOnlySpan blurredG = blurred.PlaneRow(1, y); + ReadOnlySpan blurredB = blurred.PlaneRow(2, y); + + Span outX = xyb.PlaneRow(0, y); + Span outY = xyb.PlaneRow(1, y); + Span outB = xyb.PlaneRow(2, y); + + for (int x = 0; x < rgb.XSize; x += lanes) + { + Vector sensitivity0; + Vector sensitivity1; + Vector sensitivity2; + { + OpsinAbsorbance( + true, + new Vector(blurredR[x..]) * intensityMultiplier, + new Vector(blurredG[x..]) * intensityMultiplier, + new Vector(blurredB[x..]) * intensityMultiplier, + out Vector pre0, + out Vector pre1, + out Vector pre2); + + pre0 = Vector.Max(pre0, minValue); + pre1 = Vector.Max(pre1, minValue); + pre2 = Vector.Max(pre2, minValue); + + sensitivity0 = Gamma(pre0) / pre0; + sensitivity1 = Gamma(pre1) / pre1; + sensitivity2 = Gamma(pre2) / pre2; + + sensitivity0 = Vector.Max(sensitivity0, minValue); + sensitivity1 = Vector.Max(sensitivity1, minValue); + sensitivity2 = Vector.Max(sensitivity2, minValue); + } + + OpsinAbsorbance( + false, + new Vector(rowR[x..]) * intensityMultiplier, + new Vector(rowG[x..]) * intensityMultiplier, + new Vector(rowB[x..]) * intensityMultiplier, + out Vector cur0, + out Vector cur1, + out Vector cur2); + + cur0 *= sensitivity0; + cur1 *= sensitivity1; + cur2 *= sensitivity2; + + Vector min01 = new(1.7557483643287353f); + Vector min2 = new(12.226454707163354f); + + cur0 = Vector.Max(cur0, min01); + cur1 = Vector.Max(cur1, min01); + cur2 = Vector.Max(cur2, min2); + + (cur0 - cur1).CopyTo(outX[x..]); + (cur0 + cur1).CopyTo(outY[x..]); + cur2.CopyTo(outB[x..]); + } + } + + return true; + } + + public static bool ButteraugliDiffmapInPlace( + Configuration configuration, + JxlImage3F image0, + JxlImage3F image1, + in ButteraugliParameters parameters, + JxlImageF diffmap) + { + int xSize = image0.XSize; + int ySize = image0.YSize; + + using var blurTemp = new JxlBlurTemp(); + + using (JxlImage3F temp = new(configuration, xSize, ySize)) + { + if (!OpsinDynamicsImage(image0, parameters, temp, blurTemp, image0)) + { + return false; + } + + if (!OpsinDynamicsImage(image1, parameters, temp, blurTemp, image1)) + { + return false; + } + } + + using JxlPlane blockDiffDc = JxlImageF.Create(configuration, xSize, ySize); + blockDiffDc.ZeroFill(); + + // LF/DC + using (JxlImage3F lf0 = new(configuration, xSize, ySize)) + using (JxlImage3F lf1 = new(configuration, xSize, ySize)) + { + if (!SeparateLFAndMF(parameters, image0, lf0, image0, blurTemp)) + { + return false; + } + + if (!SeparateLFAndMF(parameters, image1, lf1, image1, blurTemp)) + { + return false; + } + + for (int c = 0; c < 3; c++) + { + L2Diff( + lf0.Plane(c), + lf1.Plane(c), + Wmul[6 + c], + blockDiffDc); + } + } + + JxlImageF[] hf0 = new JxlImageF[2]; + JxlImageF[] hf1 = new JxlImageF[2]; + + if (!SeparateMfAndHf(parameters, image0, hf0, blurTemp)) + { + return false; + } + + if (!SeparateMfAndHf(parameters, image1, hf1, blurTemp)) + { + return false; + } + + using JxlImageF blockDiffAc = new(configuration, xSize, ySize); + blockDiffAc.ZeroFill(); + + using (JxlImageF diffs = new(configuration, xSize, ySize)) + { + if (!MaltaDiffMap( + true, + image0.Plane(1), + image1.Plane(1), + WMfMalta, + WMfMalta, + Norm1Mf, + diffs, + blockDiffAc)) + { + return false; + } + + if (!MaltaDiffMap( + true, + image0.Plane(0), + image1.Plane(0), + WMfMaltaX, + WMfMaltaX, + Norm1MfX, + diffs, + blockDiffAc)) + { + return false; + } + } + + for (int c = 0; c < 3; c++) + { + L2Diff( + image0.Plane(c), + image1.Plane(c), + Wmul[3 + c], + blockDiffAc); + } + + // Free MF images + image0.Dispose(); + image1.Dispose(); + + JxlImageF[] uhf0 = new JxlImageF[2]; + JxlImageF[] uhf1 = new JxlImageF[2]; + + if (!SeparateHFAndUHF(parameters, hf0, uhf0, blurTemp)) + { + return false; + } + + if (!SeparateHFAndUHF(parameters, hf1, uhf1, blurTemp)) + { + return false; + } + + float hfAsymmetry = parameters.HfAsymmetry; + + using (JxlImageF diffs = new(configuration, xSize, ySize)) + { + MaltaDiffMap( + false, + uhf0[1], + uhf1[1], + WUhfMalta * hfAsymmetry, + WUhfMalta / hfAsymmetry, + Norm1Uhf, + diffs, + blockDiffAc); + + MaltaDiffMap( + false, + uhf0[0], + uhf1[0], + wUhfMaltaX * hfAsymmetry, + wUhfMaltaX / hfAsymmetry, + norm1UhfX, + diffs, + blockDiffAc); + + float sqrtAsym = MathF.Sqrt(hfAsymmetry); + + MaltaDiffMap( + true, + hf0[1], + hf1[1], + WHfMalta * sqrtAsym, + WHfMalta / sqrtAsym, + Norm1Hf, + diffs, + blockDiffAc); + + MaltaDiffMap( + true, + hf0[0], + hf1[0], + WHfMaltaX * sqrtAsym, + WHfMaltaX / sqrtAsym, + Norm1HfX, + diffs, + blockDiffAc); + } + + for (int c = 0; c < 2; c++) + { + L2DiffAsymmetric( + hf0[c], + hf1[c], + Wmul[c] * hfAsymmetry, + Wmul[c] / hfAsymmetry, + blockDiffAc); + } + + // Mask + using JxlImageF mask = new(configuration, xSize, ySize); + using JxlImageF mask0 = new(configuration, xSize, ySize); + using JxlImageF mask1 = new(configuration, xSize, ySize); + + CombineChannelsForMasking(hf0, uhf0, mask0); + CombineChannelsForMasking(hf1, uhf1, mask1); + + DeallocateHFAndUHF(hf0, uhf0); + DeallocateHFAndUHF(hf1, uhf1); + + if (!Mask(mask0, mask1, parameters, blurTemp, mask, blockDiffAc)) + { + return false; + } + + for (int y = 0; y < ySize; y++) + { + ReadOnlySpan dc = blockDiffDc.GetRow(y); + ReadOnlySpan ac = blockDiffAc.GetRow(y); + Span output = diffmap.GetRow(y); + ReadOnlySpan maskRow = mask.GetRow(y); + + for (int x = 0; x < xSize; x++) + { + float m = maskRow[x]; + + output[x] = + MathF.Sqrt( + (dc[x] * (float)MaskDcY(m)) + + (ac[x] * (float)MaskY(m))); + } + } + + return true; + } + + // Calculate a 2x2 subsampled image for purposes of recursive butteraugli at + // multiresolution. + public static JxlImage3F SubSample2x(Configuration configuration, JxlImage3F input) + { + int xs = (input.XSize + 1) / 2; + int ys = (input.YSize + 1) / 2; + + JxlImage3F retval = new(Configuration, xs, ys); + + for (int c = 0; c < 3; ++c) + { + for (int y = 0; y < ys; ++y) + { + for (int x = 0; x < xs; ++x) + { + retval.PlaneRow(c, y)[x] = 0.0f; + } + } + } + + for (int c = 0; c < 3; ++c) + { + for (int y = 0; y < input.YSize; ++y) + { + ReadOnlySpan srcRow = input.PlaneRow(c, y); + + for (int x = 0; x < input.XSize; ++x) + { + retval.PlaneRow(c, y / 2)[x / 2] += + 0.25f * srcRow[x]; + } + } + + if ((input.XSize & 1) != 0) + { + for (int y = 0; y < retval.YSize; ++y) + { + int lastColumn = retval.XSize - 1; + retval.PlaneRow(c, y)[lastColumn] *= 2.0f; + } + } + + if ((input.YSize & 1) != 0) + { + for (int x = 0; x < retval.XSize; ++x) + { + int lastRow = retval.YSize - 1; + retval.PlaneRow(c, lastRow)[x] *= 2.0f; + } + } + } + + return retval; + } + + public static void AddSupersampled2x(JxlImageF src, float w, JxlImageF dest) + { + const float heuristicMixingValue = 0.3f; + + for (int y = 0; y < dest.YSize; ++y) + { + Span destRow = dest.GetRow(y); + + for (int x = 0; x < dest.XSize; ++x) + { + destRow[x] *= 1.0f - (heuristicMixingValue * w); + destRow[x] += w * src.GetRow(y / 2)[x / 2]; + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs new file mode 100644 index 0000000000..be5837534b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; + +internal sealed class ButteraugliComparator +{ + private int xSize; + private int ySize; + private ButteraugliParameters parameters; + private readonly ButteraugliPsychoImage pi0; + private readonly JxlImage3F temp; + private bool tempInUse; + private readonly ButteraugliBlurTemp blurTemp; + + /// + /// Computes the butteraugli map between the original image given in the constructor and the distorted image given here. + /// + public bool Diffmap(JxlImage3F rgb1, JxlImageF result) + { + throw new NotImplementedException(); + } + + /// + /// Same as Diffmap but OpsinDynamicsImage() was already applied. + /// + public bool DiffmapOpsinDynamicsImage(JxlImage3F xyb1, JxlImageF result) + { + throw new NotImplementedException(); + } + + /// + /// Same as above but the frequency decomposition was already applied. + /// + public bool DiffmapPsychoImage(ButteraugliPsychoImage pi1, JxlImageF diffmap) + { + throw new NotImplementedException(); + } + + public bool Mask(JxlImageF mask) + { + throw new NotImplementedException(); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliParameters.cs index 9b5fd57618..7af303c8ba 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliParameters.cs @@ -9,18 +9,18 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; internal struct ButteraugliParameters() { /// - /// Multiplier for penalizing new HF artifacts more than + /// Gets or sets the multiplier for penalizing new HF artifacts more than /// blurring away features. Value of 1.0 represents neutral. /// - public float HfAsymmetry = 1f; + public float HfAsymmetry { get; set; } = 1f; /// - /// Multiplier for the psychovisual difference in the X channel. + /// Gets or sets the multiplier for the psychovisual difference in the X channel. /// - public float XMultiplier = 1f; + public float XMultiplier { get; set; } = 1f; /// - /// Number of nits that correspond to 1.0f input values. + /// Gets or sets the number of nits that correspond to 1.0f input values. /// - public float IntensityTarget = 80f; + public float IntensityTarget { get; set; } = 80f; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs index 5a1405b350..4230952860 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs @@ -4,7 +4,6 @@ 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; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs index 249d1ec9a7..7fdec43b03 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs @@ -3,9 +3,11 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; -internal sealed class JxlWeightsSeparable5 +internal struct JxlWeightsSeparable5 { - public InlineArray12 Horizontal { get; set; } + // Don't make these a property so we can ref into them. - public InlineArray12 Vertical { get; set; } + public InlineArray12 Horizontal; + + public InlineArray12 Vertical; } From 0d159b9477c3bc4b563ad5f02774dd38f6e96fda Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 1 Aug 2026 23:44:39 +0400 Subject: [PATCH 056/142] Add CMS* prototype, XYB decoding, TOC, convolution and coefficient ordering * CMS - Color Management System --- .../Formats/Jxl/Cms/JxlCieXyPrimaries.cs | 27 ++ .../Formats/Jxl/Cms/JxlColorEncoding.cs | 77 ++++ .../Formats/Jxl/Cms/JxlColorSpace.cs | 31 ++ .../Jxl/Cms/JxlCustomTransferFunction.cs | 109 +++++ src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs | 53 +++ .../Formats/Jxl/Cms/JxlPrimaries.cs | 27 ++ .../Formats/Jxl/Cms/JxlRenderingIntent.cs | 13 + .../Formats/Jxl/Cms/JxlTransferFunction.cs | 45 ++ .../Formats/Jxl/Cms/JxlWhitePoint.cs | 33 ++ src/ImageSharp/Formats/Jxl/Cms/README.md | 4 + .../Jxl/IO/Metadata/JxlImageMetadata.cs | 22 + .../Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs | 8 +- src/ImageSharp/Formats/Jxl/InlineArrays.cs | 9 + .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 157 +++++++ .../Jxl/Processing/Decoder/JxlNoiseDecoder.cs | 63 +++ .../Processing/Decoder/JxlOpsinParameters.cs | 18 + .../Decoder/JxlOutputEncodingInfo.cs | 78 ++++ .../Jxl/Processing/Decoder/JxlXybDecoder.cs | 180 ++++++++ .../Jxl/Processing/JxlCoefficientOrder.cs | 91 ++++ .../Formats/Jxl/Processing/JxlConvolve.cs | 424 ++++++++++++++++++ .../Formats/Jxl/Processing/JxlDctScales.cs | 2 +- .../Formats/Jxl/Processing/JxlToc.cs | 191 ++++++++ .../Formats/Jxl/Processing/JxlXorShift.cs | 2 + 23 files changed, 1661 insertions(+), 3 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlColorSpace.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlPrimaries.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlTransferFunction.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlWhitePoint.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/README.md create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs new file mode 100644 index 0000000000..7e5a4367e1 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles; + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +/// +/// RGB primaries for CIEXY +/// +internal struct JxlCieXyPrimaries +{ + /// + /// Gets or sets the R component + /// + public CieXyChromaticityCoordinates R { get; set; } + + /// + /// Gets or sets the G component + /// + public CieXyChromaticityCoordinates G { get; set; } + + /// + /// Gets or sets the B component + /// + public CieXyChromaticityCoordinates B { get; set; } +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs new file mode 100644 index 0000000000..9a87dccc2d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs @@ -0,0 +1,77 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +internal sealed class JxlColorEncoding +{ + public JxlWhitePoint WhitePoint { get; set; } = JxlWhitePoint.D65; + + public JxlPrimaries Primaries { get; set; } = JxlPrimaries.SRgb; + + public JxlRenderingIntent RenderingIntent { get; set; } = JxlRenderingIntent.Relative; + + public bool HaveFields { get; set; } = true; + + public JxlIccBytes? Icc { get; set; } + + public JxlColorSpace ColorSpace { get; set; } = JxlColorSpace.Rgb; + + public bool Cmyk { get; set; } + + public JxlCustomTransferFunction TransferFunction { get; set; } + + public JxlCustomXy White { get; set; } + + public JxlCustomXy Red { get; set; } + + public JxlCustomXy Green { get; set; } + + public JxlCustomXy Blue { get; set; } + + public bool HasPrimaries => this.ColorSpace is not (JxlColorSpace.Gray or JxlColorSpace.Xyb); + + public int Channels => (this.ColorSpace == JxlColorSpace.Gray) ? 1 : 3; + + public bool TryGetPrimaries(out JxlCieXyPrimaries xy) + { + xy = default; + + if (!this.HasPrimaries || !this.HasPrimaries) + { + return false; + } + + switch (this.Primaries) + { + case JxlPrimaries.Custom: + xy.R = this.Red.GetValue(); + xy.G = this.Green.GetValue(); + xy.B = this.Blue.GetValue(); + break; + + case JxlPrimaries.SRgb: + xy.R = new(0.639998686f, 0.330010138f); + xy.G = new(0.300003784f, 0.600003357f); + xy.B = new(0.150002046f, 0.059997204f); + break; + + case JxlPrimaries.Bt2020: + xy.R = new(0.708f, 0.292f); + xy.G = new(0.170f, 0.797f); + xy.B = new(0.131f, 0.046f); + break; + + case JxlPrimaries.P3: + xy.R = new(0.680f, 0.320f); + xy.G = new(0.265f, 0.690f); + xy.B = new(0.150f, 0.060f); + break; + + default: + throw new InvalidOperationException("Invalid primaries: " + this.Primaries); + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlColorSpace.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlColorSpace.cs new file mode 100644 index 0000000000..b65cbbef98 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlColorSpace.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +/// +/// Supported, JPEG XL-specific color space types. +/// +internal enum JxlColorSpace : byte +{ + /// + /// Trichromatic color data. This also includes CMYK if Black + /// ExtraChannelInfo is present. + /// + Rgb, + + /// + /// Single-channel data. + /// + Gray, + + /// + /// Like Rgb but fixed values for primaries. + /// + Xyb, + + /// + /// Unknown color space + /// + Unknown +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs new file mode 100644 index 0000000000..acf049d4a7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs @@ -0,0 +1,109 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +internal struct JxlCustomTransferFunction +{ + private const uint MaxGamma = 8192; + private const uint GammaMultiplier = 10000000; + + public JxlCustomTransferFunction() + { + } + + public bool HaveGamma { get; set; } + + public uint Gamma { get; set; } + + public JxlTransferFunction TransferFunction { get; set; } = JxlTransferFunction.SRgb; + + public readonly bool IsUnknown => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Unknown; + + public readonly bool IsSrgb => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.SRgb; + + public readonly bool IsLinear => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Linear; + + public readonly bool IsPq => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Pq; + + public readonly bool IsHlg => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Hlg; + + public readonly bool Is709 => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Bt709; + + public readonly bool IsDci => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Dci; + + public readonly JxlTransferFunction GetTransferFunction() + { + if (this.HaveGamma) + { + return JxlTransferFunction.Unknown; + } + + return this.TransferFunction; + } + + public void SetTransferFunction(JxlTransferFunction tf) + { + this.HaveGamma = false; + this.TransferFunction = tf; + } + + public readonly float GetGamma() + { + if (!this.HaveGamma) + { + return 0.0f; + } + + return this.Gamma * (1.0f / GammaMultiplier); + } + + public void SetGamma(float newGamma) + { + if (newGamma is < 1.0f / MaxGamma or > 1.0f) + { + throw new InvalidOperationException($"Invalid gamma {newGamma}"); + } + + this.HaveGamma = false; + + if (IsAlmostEqual(newGamma, 1.0f)) + { + this.TransferFunction = JxlTransferFunction.Linear; + return; + } + + if (IsAlmostEqual(newGamma, 1.0f / 2.6f)) + { + this.TransferFunction = JxlTransferFunction.Dci; + return; + } + + // Don't translate 0.45.. to kSRGB nor k709 - that might change pixel + // values because those curves also have a linear part. + this.HaveGamma = true; + this.Gamma = (uint)MathF.Round((float)(newGamma * GammaMultiplier)); + this.TransferFunction = JxlTransferFunction.Unknown; + } + + public readonly bool IsSame(JxlCustomTransferFunction other) + { + if (this.HaveGamma != other.HaveGamma) + { + return false; + } + + if (this.HaveGamma) + { + return this.Gamma == other.Gamma; + } + + return this.TransferFunction == other.TransferFunction; + } + + private static bool IsAlmostEqual(float a, float b) + { + const float dist = 1e-3f; + return MathF.Abs(a - b) < dist; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs new file mode 100644 index 0000000000..95652cdaf4 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles; + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +/// +/// A serializable form of CieXyChromaticityCoordinates +/// +internal struct JxlCustomXy +{ + private const uint Multiplier = 1000000; + private const float RoughLimit = 4.0f; + private const int Min = -0x200000; + private const int Max = 0x1FFFFF; + + public int X { get; set; } + + public int Y { get; set; } + + public readonly CieXyChromaticityCoordinates GetValue() => new( + x: this.X * (1.0f / Multiplier), + y: this.Y * (1.0f / Multiplier)); + + public bool SetValue(CieXyChromaticityCoordinates xy) + { + bool ok = (Math.Abs(xy.X) < RoughLimit) && (Math.Abs(xy.Y) < RoughLimit); + + if (!ok) + { + throw new InvalidOperationException("X or Y is out of bounds"); + } + + this.X = (int)MathF.Round((float)(xy.X * Multiplier)); + + if (this.X is < Min or > Max) + { + throw new InvalidOperationException("X is out of bounds"); + } + + this.Y = (int)MathF.Round((float)(xy.Y * Multiplier)); + + if (this.Y is < Min or > Max) + { + throw new InvalidOperationException("Y is out of bounds"); + } + + return true; + } + + public readonly bool IsSame(CieXyChromaticityCoordinates other) => this.X == other.X && this.Y == other.Y; +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlPrimaries.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlPrimaries.cs new file mode 100644 index 0000000000..48d4766fa6 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlPrimaries.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +/// +/// JPEG XL primaries +/// +internal enum JxlPrimaries : byte +{ + /// + /// Same as ITU-R BT.709 + /// + SRgb = 1, + + /// + /// Values encoded in separate fields + /// + Custom = 2, + + /// + /// ITU-R BT.2020 + /// + Bt2020 = 9, + + P3 = 11, +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs new file mode 100644 index 0000000000..739dddcccd --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +internal enum JxlRenderingIntent : byte +{ + // Values match ICC sRGB encodings + Perceptual, // Good for photos, requires a profile with LUT + Relative, // Good for logos + Saturation, // Perhaps useful for CG with fully saturated colors + Absolute, // Leaves white point unchanged; good for proofing +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlTransferFunction.cs new file mode 100644 index 0000000000..cfee681765 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlTransferFunction.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +/// +/// JPEG XL transfer function type +/// +internal enum JxlTransferFunction : byte +{ + /// + /// ITU-R BT.709 + /// + Bt709 = 1, + + /// + /// Unknown transfer function + /// + Unknown = 2, + + /// + /// Linear transfer function + /// + Linear = 8, + + /// + /// sRGB + /// + SRgb = 13, + + /// + /// From ITU-R BT.2100 + /// + Pq = 16, + + /// + /// From SMPTE RP 431-2 reference projector + /// + Dci = 17, + + /// + /// From ITU-R BT.2100 + /// + Hlg = 18, +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlWhitePoint.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlWhitePoint.cs new file mode 100644 index 0000000000..c6c7e019f5 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlWhitePoint.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +/// +/// White point from CICP Color Primaries. +/// +// Note that we define a separate enum instead of using the ColorPrimaries +// enum from CICP code because JPEG XL doesn't support all color primaries defined +// by CICP. +internal enum JxlWhitePoint : byte +{ + /// + /// sRGB/ITU-R BT.709/Display P3/ITU-R BT.2020 + /// + D65 = 1, + + /// + /// Actual values encoded in separate fields + /// + Custom = 2, + + /// + /// XYZ + /// + E = 10, + + /// + /// DCI-P3 + /// + Dci = 11, +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/README.md b/src/ImageSharp/Formats/Jxl/Cms/README.md new file mode 100644 index 0000000000..557a4c8b91 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/README.md @@ -0,0 +1,4 @@ +# CMS +This is the JPEG XL Color Management System component. + +Not to be confused with Content Management System. diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs index d9bc419e97..09400f9a83 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Diagnostics; +using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.Fields; namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; @@ -124,5 +125,26 @@ public void SetUIntSamples(int bits) this.Modular16BitBufferSufficient = bits <= 12; } + public void SetIntensityTarget() + { + JxlCustomTransferFunction? tf = this.ColorEncoding?.TransferFunction; + + if (tf is not null) + { + if (tf.Value.IsPq) + { + this.SetIntensityTarget(10000); + } + else if (tf.Value.IsHlg) + { + this.SetIntensityTarget(1000); + } + else + { + this.SetIntensityTarget(DefaultIntensityTarget); + } + } + } + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); } diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs index 357ce27c39..dc44594d9a 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs @@ -12,9 +12,13 @@ internal sealed class JxlOpsinInverseMatrix : IJxlFields public JxlMatrix3x3F InverseMatrix { get; set; } - public InlineArray3 OpsinBiases { get; set; } + // Prefer arrays so we can set values like this: + // JxlOpsinInverseMatrix m = ...; + // m.OpsinBiases[0] = 1f; + // An InlineArray can't do that. + public float[] OpsinBiases { get; set; } = new float[3]; - public InlineArray4 QuantBiases { get; set; } + public float[] QuantBiases { get; set; } = new float[4]; public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); } diff --git a/src/ImageSharp/Formats/Jxl/InlineArrays.cs b/src/ImageSharp/Formats/Jxl/InlineArrays.cs index 9c19e9266c..f006d2fd09 100644 --- a/src/ImageSharp/Formats/Jxl/InlineArrays.cs +++ b/src/ImageSharp/Formats/Jxl/InlineArrays.cs @@ -13,6 +13,15 @@ internal struct InlineArray3 private T first; } +/// +/// Used by JxlOpsinParameters +/// +[InlineArray(36)] +internal struct InlineArray36 +{ + private T first; +} + /// /// Used by JxlCustomTransformData /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs new file mode 100644 index 0000000000..45c7463248 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -0,0 +1,157 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal sealed class JxlDecoderCore : ImageDecoderCore +{ + /// + /// Identifies the signature of the JPEG XL file. + /// + private enum JxlSignature : byte + { + /// + /// Error status indicating not enough bytes to detect the signature. + /// + NotEnoughBytes, + + /// + /// A JPEG XL code stream. + /// + CodeStream, + + /// + /// The signature is invalid. + /// + Invalid, + + /// + /// Container format. + /// + Container + } + + /// + /// Represents a data type. + /// + private enum JxlDataType : byte + { + /// + /// + /// + UInt8, + + /// + /// + /// + UInt16, + + /// + /// + /// + Float, + + /// + /// + /// + Float16 + } + + public JxlDecoderCore(DecoderOptions options) + : base(options) + { + } + + /// + /// Ensures that the coordinates are not out of bounds. + /// + /// First coordinate + /// Second coordinate + /// Image width + /// Boolean indicating whether the coordinates are out of bounds + private static bool IsOutOfBounds(int a, int b, int size) + { + int position = a + b; + + return position > size || position < a; + } + + private static int InitialBasicInfoSizeHint() + { + const int containerHeaderSize = 48; + const int maxCodestreamBasicInfoSize = 50; + return containerHeaderSize + maxCodestreamBasicInfoSize; + } + + private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length, ref int position) + { + if (position >= length) + { + return JxlSignature.NotEnoughBytes; + } + + buffer = buffer[position..]; + length -= position; + + // 0xFF 0x0A represents a codestream + if (length >= 1 && buffer[0] == 0xFF) + { + if (length < 2) + { + // We need at least two bytes for a valid codestream signature + return JxlSignature.NotEnoughBytes; + } + else if (buffer[1] == CodestreamMarker) + { + position += 2; + return JxlSignature.CodeStream; + } + else + { + return JxlSignature.Invalid; + } + } + + // Container? + if (length >= 1 && buffer[0] == 0) + { + if (length < SignatureBox.Length) + { + return JxlSignature.NotEnoughBytes; + } + else if (buffer[SignatureBox.Length..].SequenceEqual(SignatureBox)) + { + position += SignatureBox.Length; + return JxlSignature.Container; + } + else + { + return JxlSignature.Invalid; + } + } + + // Signature is invalid + return JxlSignature.Invalid; + } + + private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length) + { + int position = 0; + return DetectSignature(buffer, length, ref position); + } + + private static int BitsPerChannel(JxlDataType dataType) + => dataType switch + { + JxlDataType.UInt8 => 8, + JxlDataType.UInt16 or JxlDataType.Float16 => 16, + JxlDataType.Float => 32, + _ => 0 + }; + + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) => throw new NotImplementedException(); + + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs new file mode 100644 index 0000000000..819517551a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal static class JxlNoiseDecoder +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void BitsToFloatingPoint(ReadOnlySpan randomBits, Span floats) + { + Vector bits = new(randomBits); + Vector rand12 = ((bits >> 9) | new Vector(0x3F800000u)).As(); + rand12.StoreUnsafe(ref MemoryMarshal.GetReference(floats)); + } + + public static void GenerateRandomImage(JxlXorShift rng, Rectangle rectangle, JxlImageF noise) + { + const int floatsPerBatch = JxlXorShift.Generators * sizeof(ulong) / sizeof(float); + + int xSize = rectangle.Width; + int ySize = rectangle.Height; + + Span batch64 = stackalloc ulong[JxlXorShift.Generators]; + Span batch32 = stackalloc uint[JxlXorShift.Generators * 2]; + + // stackalloc doesn't zero-initialize, so clear values + batch64.Clear(); + batch32.Clear(); + + int n = Vector.Count; + + for (int y = 0; y < ySize; y++) + { + Span row = noise.GetRow(rectangle, y); + int x = 0; + for (; x + floatsPerBatch < xSize; x += floatsPerBatch) + { + rng.Fill(batch64); + MemoryMarshal.Cast(batch32).CopyTo(batch64); + for (int i = 0; i < floatsPerBatch; i += n) + { + BitsToFloatingPoint(batch32[i..], row[(x + i)..]); + } + } + + rng.Fill(batch64); + MemoryMarshal.Cast(batch32).CopyTo(batch64); + + int batchPos = 0; + + for (; x < xSize; x += n) + { + BitsToFloatingPoint(batch32[batchPos..], row[x..]); + batchPos += n; + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs new file mode 100644 index 0000000000..c18a18ef0b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal sealed class JxlOpsinParameters +{ + // Use arrays instead of InlineArrays because, with inline arrays we can't do: + // JxlOpsinParameters parameters = ...; + // parameters.OpsinBiasesCbrt[0] /* <-- error */ = 1.25f; + public float[] InverseOpsinMatrix { get; set; } = new float[36]; + + public float[] OpsinBiases { get; set; } = new float[4]; + + public float[] OpsinBiasesCbrt { get; set; } = new float[4]; + + public float[] QuantBiases { get; set; } = new float[4]; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs new file mode 100644 index 0000000000..bdec666432 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs @@ -0,0 +1,78 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +// Prefer class instead of struct because it's too large for a struct +// Note that this struct wasn't well documented, so it's not that easy to add +// XML documentation here. +internal sealed class JxlOutputEncodingInfo +{ + public JxlColorEncoding? OriginalColorEncoding { get; set; } + + public float OriginalIntensityTarget { get; set; } + + public JxlMatrix3x3F OriginalInverseMatrix { get; set; } + + public bool DefaultTransform { get; set; } + + public bool XybEncoded { get; set; } + + /// + /// Gets or sets the requested color encoding. + /// + public JxlColorEncoding ColorEncoding { get; set; } = new(); + + public JxlColorEncoding LinearColorEncoding { get; set; } = new(); + + public bool ColorEncodingIsOriginal { get; set; } + + public JxlOpsinParameters OpsinParameters { get; set; } = new(); + + public bool AllDefaultOpsin { get; set; } + + public float InverseGamma { get; set; } + + public Vector3 Luminances { get; set; } + + public float DesiredIntensityTarget { get; set; } + + public bool CmsSet { get; set; } + + public JxlCmsInterface Cms { get; set; } + + public void SetFromMetadata(JxlCodecMetadata metadata) + { + JxlImageMetadata imageMetadata = metadata.ImageMetadata ?? throw new InvalidOperationException("Missing image metadata"); + + this.OriginalColorEncoding = imageMetadata.ColorEncoding; + this.OriginalIntensityTarget = imageMetadata.IntensityTarget; + this.DesiredIntensityTarget = this.OriginalIntensityTarget; + + JxlOpsinInverseMatrix inverseMatrix = metadata.CustomTransformData?.OpsinInverseMatrix ?? throw new InvalidOperationException("Missing Opsin inverse matrix or transform data"); + this.OriginalInverseMatrix = inverseMatrix.InverseMatrix; + this.DefaultTransform = inverseMatrix.AllDefault; + this.XybEncoded = imageMetadata.XybEncoded; + + JxlOpsinParameters parameters = this.OpsinParameters; + + imageMetadata.OpsinBiases.CopyTo(parameters.OpsinBiases); + parameters.OpsinBiasesCbrt[0] = MathF.Cbrt(parameters.OpsinBiases[0]); + parameters.OpsinBiasesCbrt[1] = MathF.Cbrt(parameters.OpsinBiases[1]); + parameters.OpsinBiasesCbrt[2] = MathF.Cbrt(parameters.OpsinBiases[2]); + + parameters.OpsinBiasesCbrt[3] = 1; + parameters.OpsinBiases[3] = 1; + + inverseMatrix.QuantBiases.AsSpan().CopyTo(parameters.QuantBiases); + + bool origOK = JxlXybDecoder.CanOutputToColorEncoding(this.OriginalColorEncoding ?? throw new InvalidCastException("Missing color encoding")); + bool origGrey = this.OriginalColorEncoding.IsGray; + + return this.SetColorEncoding(!this.XybEncoded || origOK ? this.OriginalColorEncoding : JxlColorEncoding.LinearSrgb(origGrey)); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs new file mode 100644 index 0000000000..940b01170a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs @@ -0,0 +1,180 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Decodes the XYB color format (which JPEG XL uses) into RGB. +/// +internal static class JxlXybDecoder +{ + /// + /// Converts XYB to RGB using SIMD, one vector at a time. + /// + /// X channel + /// Y channel + /// B channel + /// Opsin parameters & configuration + /// Output R + /// Output G + /// Output B + public static void ConvertXybToRgb( + Vector opsinX, + Vector opsinY, + Vector opsinB, + JxlOpsinParameters opsinParameters, + ref Vector linearR, + ref Vector linearG, + ref Vector linearB) + { + Vector negBiasR = new(opsinParameters.OpsinBiaases[0]); + Vector negBiasG = new(opsinParameters.OpsinBiaases[1]); + Vector negBiasB = new(opsinParameters.OpsinBiaases[2]); + + Vector gammaR = opsinX + opsinY; + Vector gammaG = opsinY - opsinX; + Vector gammaB = opsinB; + + Vector gammaR2 = gammaR * gammaR; + Vector gammaG2 = gammaG * gammaG; + Vector gammaB2 = gammaB * gammaB; + + Vector mixedR = (gammaR2 * gammaR) + negBiasR; + Vector mixedG = (gammaG2 * gammaG) + negBiasG; + Vector mixedB = (gammaB2 * gammaB) + negBiasB; + + Span inverseMatrix = opsinParameters.GetInverseOpsinMatrixSpan(); + + linearR = LoadDuplicate128(ref inverseMatrix[0 * 4]) * mixedR; + linearG = LoadDuplicate128(ref inverseMatrix[3 * 4]) * mixedR; + linearB = LoadDuplicate128(ref inverseMatrix[6 * 4]) * mixedR; + + linearR = (LoadDuplicate128(ref inverseMatrix[1 * 4]) * mixedG) + linearR; + linearG = (LoadDuplicate128(ref inverseMatrix[4 * 4]) * mixedG) + linearG; + linearB = (LoadDuplicate128(ref inverseMatrix[7 * 4]) * mixedG) + linearB; + + linearR = (LoadDuplicate128(ref inverseMatrix[2 * 4]) * mixedB) + linearR; + linearG = (LoadDuplicate128(ref inverseMatrix[5 * 4]) * mixedB) + linearG; + linearB = (LoadDuplicate128(ref inverseMatrix[8 * 4]) * mixedB) + linearB; + } + + public static bool OpsinToLinear(JxlImage3F opsin, Rectangle rect, JxlImage3F linear, JxlOpsinParameters opsinParameters) + { + if (!SameSize(rect, linear)) + { + return false; + } + + if (Vector.Count < 4) + { + // TODO: support 64bit vectors or no SIMD? + throw new PlatformNotSupportedException("XYB to RGB conversion requires at least 128-bit SIMD"); + } + + // Reuse variables instead of creating them over + // and over again + Unsafe.SkipInit(out Vector linearR); + Unsafe.SkipInit(out Vector linearG); + Unsafe.SkipInit(out Vector linearB); + + for (int y = 0; y < rect.Height; y++) + { + ReadOnlySpan rowOpsin0 = opsin.PlaneRow(rect, 0, y); + ReadOnlySpan rowOpsin1 = opsin.PlaneRow(rect, 1, y); + ReadOnlySpan rowOpsin2 = opsin.PlaneRow(rect, 2, y); + + ref float rowOpsin0Reference = ref MemoryMarshal.GetReference(rowOpsin0); + ref float rowOpsin1Reference = ref MemoryMarshal.GetReference(rowOpsin1); + ref float rowOpsin2Reference = ref MemoryMarshal.GetReference(rowOpsin2); + + Span rowLinear0 = linear.PlaneRow(0, y); + Span rowLinear1 = linear.PlaneRow(1, y); + Span rowLinear2 = linear.PlaneRow(2, y); + + ref float rowLinear0Reference = ref MemoryMarshal.GetReference(rowLinear0); + ref float rowLinear1Reference = ref MemoryMarshal.GetReference(rowLinear1); + ref float rowLinear2Reference = ref MemoryMarshal.GetReference(rowLinear2); + + for (int x = 0; x < rect.Height; x += Vector.Count) + { + Vector inOpsinX = Vector.LoadUnsafe(ref Unsafe.Add(ref rowOpsin0Reference, x)); + Vector inOpsinY = Vector.LoadUnsafe(ref Unsafe.Add(ref rowOpsin1Reference, x)); + Vector inOpsinB = Vector.LoadUnsafe(ref Unsafe.Add(ref rowOpsin2Reference, x)); + + ConvertXybToRgb(inOpsinX, inOpsinY, inOpsinB, opsinParameters, ref linearR, ref linearG, ref linearB); + + linearR.StoreUnsafe(ref Unsafe.Add(ref rowLinear0Reference, x)); + linearG.StoreUnsafe(ref Unsafe.Add(ref rowLinear1Reference, x)); + linearB.StoreUnsafe(ref Unsafe.Add(ref rowLinear2Reference, x)); + } + } + + return true; + } + + /// + /// A SIMD utility method which reads next 128 bits + /// (which in this case happens to be next 4 floats), + /// and duplicates them to fit in the CPU vector size. + /// For example, + /// + /// 128 bit vectors: A B C D (as-is) + /// 256 bit vectors: A B C D A B C D (duplicate once) + /// 512 bit vectors: A B C D A B C D A B C D A B C D (duplicate three times) + /// + /// Vector<T> has support for arbitrarily large + /// vector sizes. For example, some ARM CPUs support 2048-bit + /// vectors through Vector<T>. In that specific case, this + /// method can be used for future-proofing. + /// + /// Note that this method, albeit future-proof, may be considered + /// slow for smaller vector sizes (think CPUs with 128bit or 256bit vectors). + /// + /// Reference to first element to load & duplicate. + /// Vector with first 128 bits duplicated across the vector width. + private static Vector LoadDuplicate128(ref float reference) + { + Span value = stackalloc float[Vector.Count]; + Span values128 = [ + reference, + Unsafe.Add(ref reference, 1), + Unsafe.Add(ref reference, 2), + Unsafe.Add(ref reference, 3) + ]; + + for (int i = 0; i < Vector.Count; i += 4) + { + values128[i..].CopyTo(value[i..]); + } + + return new(value); + } + + public static bool CanOutputToColorEncoding(JxlColorEncoding colorEncoding) + { + if (!colorEncoding.HaveFields) + { + return false; + } + + JxlCustomTransferFunction tf = colorEncoding.TransferFunction; + + if (!tf.IsPq && !tf.IsSrgb && !tf.HaveGamma && !tf.IsLinear && !tf.IsHlg && !tf.IsDci && !tf.Is709) + { + return false; + } + + if (colorEncoding.IsGray && colorEncoding.WhitePoint != JxlWhitePoint.D65) + { + return false; + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs new file mode 100644 index 0000000000..ccea59b2c8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs @@ -0,0 +1,91 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Security.Principal; +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Helps with reordering coefficients. +/// +internal static class JxlCoefficientOrder +{ + public const int Limit = 6156; + + public const int CoefficientOrderMaxSize = Limit * JxlFrameDimensions.DctBlockSize; + + public const int PermutationContexts = 8; + + /// + /// Gets the pattern which coefficients must follow to compute offsets. + /// + public static ReadOnlySpan CoefficientOrderOffsets => + [ + 0, 1, 2, 3, 4, 5, 6, 10, 14, 18, + 34, 50, 66, 68, 70, 72, 76, 80, 84, 92, + 100, 108, 172, 236, 300, 332, 364, 396, 652, 908, + 1164, 1292, 1420, 1548, 2572, 3596, 4620, 5132, 5644, Limit + ]; + + /// + /// Gets the pattern which coefficients must follow to compute offsets. + /// + public static ReadOnlySpan StrategyOrder => + [ + 0, 1, 1, 1, 2, 3, 4, 4, 5, 5, 6, 6, 1, 1, + 1, 1, 1, 1, 7, 8, 8, 9, 10, 10, 11, 12, 12, + ]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int CoeffOrderOffset(int o, int c) => CoefficientOrderOffsets[(3 * o) + c] * JxlFrameDimensions.DctBlockSize; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint CoeffOrderContext(uint value) + { + uint token = 0; + uint nbits = 0; + uint bits = 0; + + new JxlAnsHybridUIntConfiguration(0, 0, 0).Encode(value, ref token, ref nbits, ref bits); + + return Math.Min(token, PermutationContexts - 1u); + } + + public static bool ReadPermutation(int skip, int size, Span order, JxlBitReader bitReader, JxlAnsSymbolReader reader, Span contextMap) + { + Span lehmer = stackalloc uint[size]; + lehmer.Clear(); + + Span temp = stackalloc uint[size * 2]; + temp.Clear(); + + uint end = reader.ReadHybridUnsignedInteger(CoeffOrderContext((int)size), bitReader, contextMap) + skip; + + if (end > size) + { + throw new InvalidOperationException("Invalid permutation size"); + } + + uint last = 0; + + for (int i = skip; i < end; i++) + { + lehmer[i] = reader.ReadHybridUnsignedInteger(CoeffOrderContext(last), bitReader, contextMap); + last = lehmer[i]; + if (lehmer[i] >= size - i) + { + throw new InvalidOperationException("Invalid lehmer code"); + } + } + + if (order.IsEmpty) + { + return true; + } + + return JxlLehmerCode.DecodeLehmerCode(lehmer, temp, size, order); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs new file mode 100644 index 0000000000..9bf6a7dcb0 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs @@ -0,0 +1,424 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Convolution filters +/// +internal static class JxlConvolve +{ + /// + /// Weighted sum of 1x5 pixels around ix, iy with [wx2, wx1, wx0, wx1, wx2]. + /// + public static float WeightedSumBorder( + JxlImageF input, + Func wrapY, + long ix, + long iy, + int width, + int height, + float wx0, + float wx1, + float wx2) + { + ReadOnlySpan row = input.GetRow(wrapY(iy, height)); + + float inM2 = row[WrapMirror(ix - 2, width)]; + float inP2 = row[WrapMirror(ix + 2, width)]; + float inM1 = row[WrapMirror(ix - 1, width)]; + float inP1 = row[WrapMirror(ix + 1, width)]; + float in00 = row[(int)ix]; + + float sum2 = wx2 * (inM2 + inP2); + float sum1 = wx1 * (inM1 + inP1); + float sum0 = wx0 * in00; + + return sum2 + (sum1 + sum0); + } + + public static Vector WeightedSum( + JxlImageF input, + Func wrapY, + int ix, + long iy, + int height, + Vector wx0, + Vector wx1, + Vector wx2) + { + ReadOnlySpan center = input.GetRow(wrapY(iy, height))[ix..]; + ref float centerRef = ref MemoryMarshal.GetReference(center); + + Vector inM2 = Vector.LoadUnsafe(ref Unsafe.Subtract(ref centerRef, 2)); + Vector inP2 = Vector.LoadUnsafe(ref Unsafe.Add(ref centerRef, 2)); + Vector inM1 = Vector.LoadUnsafe(ref Unsafe.Subtract(ref centerRef, 1)); + Vector inP1 = Vector.LoadUnsafe(ref Unsafe.Add(ref centerRef, 1)); + Vector in00 = Vector.LoadUnsafe(ref centerRef); + + Vector sum2 = wx2 * (inM2 + inP2); + Vector sum1 = wx1 * (inM1 + inP1); + Vector sum0 = wx0 * in00; + + return sum2 + (sum1 + sum0); + } + + public static float Symmetric5Border(JxlImageF input, Func wrapY, long ix, long iy, JxlWeightsSymmetric5 weights) + { + float w0 = weights.GetCVector()[0]; + float w1 = weights.GetRVector()[0]; + float w2 = weights.GetR2Vector()[0]; + float w4 = weights.GetDVector()[0]; + float w5 = weights.GetCVector()[0]; + float w8 = weights.GetD2Vector()[0]; + + int width = input.XSize; + int height = input.YSize; + + float sum0 = WeightedSumBorder(input, wrapY, ix, iy, width, height, w0, w1, w2) + + WeightedSumBorder(input, wrapY, ix, iy - 2, width, height, w2, w5, w8); + + float sum1 = WeightedSumBorder(input, wrapY, ix, iy + 2, width, height, w2, w5, w8); + + sum0 += WeightedSumBorder(input, wrapY, ix, iy + 1, width, height, w1, w4, w5); + sum1 += WeightedSumBorder(input, wrapY, ix, iy - 1, width, height, w1, w4, w5); + + return sum0 + sum1; + } + + public static void Symmetric5Interior( + JxlImageF image, + int ix, + Func wrapY, + int rix, + long iy, + JxlWeightsSymmetric5 weights, + Span rowOut) + { + Vector w0 = LoadDuplicate128(weights.GetCVector()); // c + Vector w1 = LoadDuplicate128(weights.GetRVector()); // r + Vector w2 = LoadDuplicate128(weights.GetR2Vector()); // R + Vector w4 = LoadDuplicate128(weights.GetDVector()); // d + Vector w5 = LoadDuplicate128(weights.GetLVector()); // L + Vector w8 = LoadDuplicate128(weights.GetD2Vector()); // D + + int height = image.YSize; + Vector sum0 = WeightedSum(image, wrapY, ix, iy, height, w0, w1, w2) + + WeightedSum(image, wrapY, ix, iy - 2, height, w2, w5, w8); + + Vector sum1 = WeightedSum(image, wrapY, ix, iy + 2, height, w2, w5, w8); + + sum0 += WeightedSum(image, wrapY, ix, iy - 1, height, w1, w4, w5); + sum1 += WeightedSum(image, wrapY, ix, iy + 1, height, w1, w4, w5); + + (sum0 + sum1).StoreUnsafe(ref Unsafe.Add(ref MemoryMarshal.GetReference(rowOut), rix)); + } + + public static void Symmetric5Row( + JxlImageF image, + Func wrapY, + in Rectangle rect, + long iy, + JxlWeightsSymmetric5 weights, + Span rowOut) + { + const int radius = 2; + int xEnd = rect.Right; + + int rix = 0; + int ix = rect.X; + + int n = Vector.Count; + int alignedX = RoundUpTo(radius, n); + + for (; ix < Math.Min(alignedX, xEnd); ix++, rix++) + { + rowOut[rix] = Symmetric5Border(image, wrapY, ix, iy, weights); + } + + for (; ix + n + radius <= xEnd; ix += n, rix += n) + { + Symmetric5Interior(image, ix, wrapY, rix, iy, weights, rowOut); + } + + for (; ix < xEnd; ix++, rix++) + { + rowOut[rix] = Symmetric5Border(image, wrapY, ix, iy, weights); + } + } + + public static bool Symmetric5( + JxlImageF input, + in Rectangle rectangle, + JxlWeightsSymmetric5 weights, + JxlImageF output, + Rectangle outputRect) + { + if (rectangle.Width != outputRect.Width || rectangle.Height != outputRect.Height) + { + return false; + } + + int height = rectangle.Height; + + for (int riy = 0; riy < height; riy++) + { + int iy = rectangle.Y + riy; + + if (iy < 2 || iy >= rectangle.Height - 2) + { + Symmetric5Row(input, WrapMirror, in rectangle, iy, weights, output.GetRow(outputRect, riy)); + } + else + { + Symmetric5Row(input, in rectangle, iy, weights, output.GetRow(outputRect, riy)); + } + } + + return true; + } + + public static float SlowSymmetric3Pixel( + JxlImageF image, + int x, + int y, + int width, + int height, + JxlWeightsSymmetric3 weights, + Func wrapX, + Func wrapY) + { + float sum = 0.0f; + + float c0 = weights.GetCVector()[0]; + float r0 = weights.GetRVector()[0]; + float d0 = weights.GetDVector()[0]; + + for (int ky = -1; ky <= 1; ky++) + { + int yy = wrapY(y + ky, height); + ReadOnlySpan row = image.GetRow(yy); + + float wc = (ky == 0) ? c0 : r0; + float wlr = (ky == 0) ? r0 : d0; + + int xm1 = wrapX(x - 1, width); + int xp1 = wrapX(x + 1, width); + + sum += (row[x] * wc) + ((row[xm1] + row[xp1]) * wlr); + } + + return sum; + } + + public static void SlowSymmetric3Row( + JxlImageF image, + int y, + int width, + int height, + JxlWeightsSymmetric3 weights, + Span outputRow, + Func wrapY) + { + outputRow[0] = SlowSymmetric3Pixel( + image, + 0, + y, + width, + height, + weights, + WrapMirror, + wrapY); + + for (int x = 1; x < width - 1; x++) + { + outputRow[x] = SlowSymmetric3Pixel( + image, + x, + y, + width, + height, + weights, + WrapUnchanged, + wrapY); + } + + outputRow[width - 1] = SlowSymmetric3Pixel( + image, + width - 1, + y, + width, + height, + weights, + WrapMirror, + wrapY); + } + + public static void SlowSymmetric3( + JxlImageF input, + Rectangle rect, + JxlWeightsSymmetric3 weights, + JxlImageF output) + { + int width = rect.Width; + int height = rect.Height; + + const int radius = 1; + + for (int y = 0; y < height; y++) + { + Span rowOut = output.GetRow(y); + + if (y < radius || y >= height - radius) + { + SlowSymmetric3Row( + input, + y, + width, + height, + weights, + rowOut, + WrapMirror); + } + else + { + SlowSymmetric3Row( + input, + y, + width, + height, + weights, + rowOut, + WrapUnchanged); + } + } + } + + public static float SlowSeparablePixel( + JxlImageF image, + Rectangle rect, + int x, + int y, + int radius, + ReadOnlySpan horzWeights, + ReadOnlySpan vertWeights) + { + int width = image.XSize; + int height = image.YSize; + + float sum = 0; + + for (int dy = -radius; dy <= radius; dy++) + { + float wy = vertWeights[Math.Abs(dy) * 4]; + int sy = WrapMirror(rect.Y + y + dy, height); + ReadOnlySpan row = image.GetRow(sy); + + for (int dx = -radius; dx <= radius; dx++) + { + float wx = horzWeights[Math.Abs(dx) * 4]; + int sx = WrapMirror(rect.X + x + dx, width); + sum += row[sx] * wx * wy; + } + } + + return sum; + } + + public static void SlowSeparable( + JxlImageF input, + Rectangle inputRect, + JxlWeightsSeparable5 weights, + JxlImageF output, + Rectangle outputRect, + int radius) + { + ReadOnlySpan horz = weights.Horizontal; + ReadOnlySpan vert = weights.Vertical; + + for (int y = 0; y < inputRect.Height; y++) + { + Span rowOut = output.GetRow(outputRect, y); + + for (int x = 0; x < inputRect.Width; x++) + { + rowOut[x] = SlowSeparablePixel( + input, + inputRect, + x, + y, + radius, + horz, + vert); + } + } + } + + public static void SlowSeparable5( + JxlImageF input, + Rectangle inputRect, + JxlWeightsSeparable5 weights, + JxlImageF output, + Rectangle outputRect) + => SlowSeparable(input, inputRect, weights, output, outputRect, 2); + + public static void FirstL1(ReadOnlySpan c, Span dst) + { + dst[0] = c[0]; + for (int i = 1; i < dst.Length; i++) + { + dst[i] = c[i - 1]; + } + } + + public static void FirstL2(ReadOnlySpan c, Span dst) + { + dst[0] = c[1]; + dst[1] = c[0]; + + for (int i = 2; i < dst.Length; i++) + { + dst[i] = c[i - 2]; + } + } + + /// + /// A SIMD utility method which takes in the 128 bit vector + /// and duplicates its values to fit in the CPU vector size. + /// For example, + /// + /// 128 bit vectors: A B C D (as-is) + /// 256 bit vectors: A B C D A B C D (duplicate once) + /// 512 bit vectors: A B C D A B C D A B C D A B C D (duplicate three times) + /// + /// Vector<T> has support for arbitrarily large + /// vector sizes. For example, some ARM CPUs support 2048-bit + /// vectors through Vector<T>. In that specific case, this + /// method can be used for future-proofing. + /// + /// Note that this method, albeit future-proof, may be considered + /// slow for smaller vector sizes (think CPUs with 256bit vectors). + /// + /// Vector to duplicate. + /// New vector that is duplicated across the width. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector LoadDuplicate128(Vector128 vec) + { + Span value = stackalloc float[Vector.Count]; + for (int i = 0; i < Vector.Count; i += 4) + { + vec.CopyTo(value[i..]); + } + + return new(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int RoundUpTo(int value, int multiple) => ((value + multiple - 1) / multiple) * multiple; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs index 84e7819bc5..26bc7ee8e3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs @@ -5,7 +5,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// /// Read-only cosine lookups for the Discrete Cosine Transform (DCT), -/// a mathematical function used for quantization. +/// a mathematical function used for quantization and coefficient reordering. /// internal static class JxlDctScales { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs new file mode 100644 index 0000000000..21fa5d23b2 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs @@ -0,0 +1,191 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Table of Contents encoding +/// +internal static class JxlToc +{ + private static readonly JxlU32Enc TocDistribution = new( + JxlFieldExpressions.Bits(10), + JxlFieldExpressions.BitsOffset(14, 2024), + JxlFieldExpressions.BitsOffset(22, 17408), + JxlFieldExpressions.BitsOffset(30, 4211712)); + + public static int AcGroupIndex(int pass, int group, int numGroups, int numDcGroups) + => 2 + numDcGroups + (pass * numGroups) + group; + + public static int NumberOfTocEntries(int numGroups, int numDcGroups, int numPasses) + { + if (numGroups == 1 && numPasses == 1) + { + return 1; + } + + return AcGroupIndex(0, 0, numGroups, numDcGroups) + (numGroups * numPasses); + } + + private const int BitsPerByte = 8; + private const int MaxTocEntries = 65536; + + public static bool ReadToc( + Configuration configuration, + int tocEntries, + JxlBitReader reader, + List sizes, + List permutation) + { + if (tocEntries > MaxTocEntries) + { + return false; // too many TOC entries + } + + sizes.Clear(); + sizes.Capacity = tocEntries; + + for (int i = 0; i < tocEntries; i++) + { + sizes.Add(0); + } + + if (reader.TotalBitsConsumed >= reader.TotalBytes * BitsPerByte) + { + return false; // not enough bytes + } + + bool CheckBitBudget(int numEntries) + { + long minimalBitCost = numEntries * (2 + 10); + long bitBudget = reader.TotalBytes * BitsPerByte; + long expenses = reader.TotalBitsConsumed; + + return expenses <= bitBudget && + minimalBitCost <= bitBudget - expenses; + } + + if (tocEntries <= 0) + { + return false; + } + + if (reader.ReadBits32(1) == 1) + { + if (!CheckBitBudget(tocEntries)) + { + return false; + } + + permutation.Clear(); + + for (int i = 0; i < tocEntries; i++) + { + permutation.Add(default); + } + + if (!DecodePermutation( + configuration, + 0, + tocEntries, + permutation, + reader)) + { + return false; + } + } + + if (!reader.JumpToByteBoundary()) + { + return false; + } + + if (!CheckBitBudget(tocEntries)) + { + return false; + } + + for (int i = 0; i < tocEntries; i++) + { + sizes[i] = JxlU32Coder.Read(TocDistribution, reader); + } + + if (!reader.JumpToByteBoundary()) + { + return false; + } + + return CheckBitBudget(0); + } + + public static bool ReadGroupOffsets( + Configuration configuration, + int tocEntries, + JxlBitReader reader, + List offsets, + List sizes, + out ulong totalSize) + { + totalSize = 0; + + List permutation = []; + + if (!ReadToc( + configuration, + tocEntries, + reader, + sizes, + permutation)) + { + return false; + } + + offsets.Clear(); + offsets.Capacity = tocEntries; + + for (int i = 0; i < tocEntries; i++) + { + offsets.Add(0); + } + + ulong offset = 0; + + for (int i = 0; i < tocEntries; i++) + { + ulong size = sizes[i]; + + if (offset + size < offset) + { + return false; + } + + offsets[i] = offset; + offset += size; + } + + totalSize = offset; + + if (permutation.Count != 0) + { + List permutedOffsets = new(tocEntries); + List permutedSizes = new(tocEntries); + + foreach (byte index in permutation) + { + permutedOffsets.Add(offsets[index]); + permutedSizes.Add(sizes[index]); + } + + offsets.Clear(); + offsets.AddRange(permutedOffsets); + + sizes.Clear(); + sizes.AddRange(permutedSizes); + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs index b3849d69f0..78d8900a52 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs @@ -7,6 +7,8 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal sealed class JxlXorShift { + public const int Generators = 8; + private readonly ulong[] s0 = new ulong[8]; private readonly ulong[] s1 = new ulong[8]; From 083385bde44688700d2332fa1201fb205b22b88d Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 1 Aug 2026 23:46:24 +0400 Subject: [PATCH 057/142] Use inline arrays in quantizer --- src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs index 4230952860..01723479bb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs @@ -51,12 +51,12 @@ internal sealed class JxlQuantizer /// /// Represents the multipliers for the DC coefficients. /// - private readonly float[] mulDc = new float[4]; + private readonly InlineArray4 mulDc; /// /// Represents the inverse multipliers for the DC coefficients. /// - private readonly float[] inverseMulDc = new float[4]; + private readonly InlineArray4 inverseMulDc; /// /// Global scale @@ -86,7 +86,7 @@ internal sealed class JxlQuantizer /// /// The zero bias. /// - private readonly float[] zeroBias = new float[3]; + private readonly InlineArray3 zeroBias; /// /// The dequant matrices. From a1cae71e4d8e1fe6182236b1004901aed798ce7d Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 1 Aug 2026 23:52:40 +0400 Subject: [PATCH 058/142] Prefer const --- .../Formats/Jxl/Processing/Butteraugli/Butteraugli.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index eb25583ecf..87ac5855a8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -36,10 +36,10 @@ internal static class Butteraugli private const float IntensityTargetNormalizationHack = 0.79079917404f; - private static readonly float InternalGoodQualityThreshold = + private const float InternalGoodQualityThreshold = 17.83f * IntensityTargetNormalizationHack; - private static readonly float GlobalScale = + private const float GlobalScale = 1.0f / InternalGoodQualityThreshold; public static ReadOnlySpan Wmul => From eb80187037968dd04393235d0df4fc6a0acf7bf9 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sun, 2 Aug 2026 00:32:55 +0400 Subject: [PATCH 059/142] Don't use System.Diagnostics.Debug --- .../Formats/Jxl/Fields/JxlF16Coder.cs | 1 - .../Formats/Jxl/Fields/JxlReadVisitor.cs | 2 +- .../Formats/Jxl/Fields/JxlVisitorBase.cs | 22 ++++++----- .../Formats/Jxl/IO/Entropy/JxlAnsHelper.cs | 13 +++---- .../Entropy/JxlAnsHybridUIntConfiguration.cs | 6 ++- .../Jxl/IO/Metadata/JxlImageMetadata.cs | 6 ++- .../Jxl/Memory/ImageTypes/JxlImageB.cs | 4 +- .../Formats/Jxl/Memory/JxlImage3{T}.cs | 9 +++-- .../Formats/Jxl/Memory/JxlPlaneBase.cs | 8 ++-- .../Jxl/Processing/Decoder/JxlAnsReader.cs | 19 +++------- .../Jxl/Processing/Decoder/JxlBitReader.cs | 5 +-- .../Formats/Jxl/Processing/JxlAcContext.cs | 18 +++++---- .../Formats/Jxl/Processing/JxlAcStrategy.cs | 5 --- .../Jxl/Processing/JxlAcStrategyImage.cs | 14 +++---- .../Jxl/Processing/JxlAcStrategyRow.cs | 3 +- .../Formats/Jxl/Processing/JxlMatrix3x3F.cs | 2 +- .../Processing/Splines/JxlQuantizedSpline.cs | 38 +++++-------------- 17 files changed, 71 insertions(+), 104 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs index d45b0968af..b930e5dc49 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs index b0396579cc..35be5d15e0 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs index 7d2dffffbf..ff3ee5a3d5 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs @@ -1,10 +1,6 @@ // 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 @@ -26,14 +22,18 @@ public override bool Visit(IJxlFields fields) if (visited) { - // TODO: use DebugGuard - Debug.Assert(!this.extensionStates.IsBegun || this.extensionStates.IsEnded); + if (!(!this.extensionStates.IsBegun || this.extensionStates.IsEnded)) + { + throw new InvalidOperationException("Invalid extension state"); + } } this.extensionStates.Pop(); - // TODO: use DebugGuard - Debug.Assert(this.depth != 0); + if (this.depth == 0) + { + throw new InvalidOperationException("Depth must not be 0"); + } this.depth--; return visited; @@ -47,8 +47,10 @@ public override bool Boolean(bool defaultValue, ref bool value) return false; } - // TODO: use DebugGuard - Debug.Assert(bits <= 1u); + if (bits > 1u) + { + throw new InvalidOperationException("Invalid bits"); + } value = bits == 1u; diff --git a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs index 8e775050e1..7feda2848d 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Diagnostics; using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; @@ -16,8 +15,8 @@ public static int GetPopulationCountPrecision(int logCount, int shift) // 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"); + DebugGuard.MustBeLessThanOrEqualTo(length, 0, nameof(length)); + DebugGuard.MustBeGreaterThan(length, totalCount, nameof(length)); int count = totalCount / length; IMemoryOwner result = configuration.MemoryAllocator.Allocate(length); @@ -66,11 +65,11 @@ public static JxlAnsSymbol Lookup(ReadOnlySpan table, int value, in public static bool InitAliasTable(Span preDistribution, uint logRange, int logAlphaSize, Span entries) { + DebugGuard.MustBeLessThan(logAlphaSize, (int)logRange, nameof(logAlphaSize)); + 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) @@ -88,9 +87,7 @@ public static bool InitAliasTable(Span preDistribution, uint logRange, int if (distribution.Length > tableSize) { - Debug.Fail("Too many items in the distribution"); - - return false; + throw new InvalidOperationException("Too many items in the distribution"); } int entrySize = range >> logAlphaSize; diff --git a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs index b8609f9163..6c2d8cc2ba 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs @@ -1,7 +1,6 @@ // 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.Entropy; @@ -15,7 +14,10 @@ public JxlAnsHybridUIntConfiguration(uint splitExponent = 4, uint msbInToken = 2 this.MsbInToken = msbInToken; this.LsbInToken = lsbInToken; - Debug.Assert(splitExponent >= msbInToken + lsbInToken, "Split exponent should be < msbInToken + lsbInToken"); + if (splitExponent < msbInToken + lsbInToken) + { + throw new InvalidOperationException("Split exponent should be < msbInToken + lsbInToken"); + } } public uint SplitExponent { get; set; } diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs index 09400f9a83..1fcf00b6fe 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics; using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.Fields; @@ -49,7 +48,10 @@ public float IntensityTarget { float intensityTarget = this.ToneMapping?.IntensityTarget ?? 0f; - Debug.Assert(intensityTarget != 0f, "Intensity target should be present"); + if (intensityTarget == 0f) + { + throw new InvalidOperationException("Intensity target should be present"); + } return intensityTarget; } diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs index 0faba189ad..2d36f5b668 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics; - namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; /// @@ -25,7 +23,7 @@ public JxlImageB(Configuration configuration, int xSize, int ySize, int prePaddi public Memory GetRowBytesMemory(int y) { - Debug.Assert(y < this.YSize, "Attempted to access out-of-bounds Y coordinate"); + DebugGuard.MustBeLessThan(y, this.YSize, nameof(y)); Memory row = this.Bytes[(y * this.BytesPerRow)..]; diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs index 37707b7ba4..ab637308f9 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics; using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Jxl.Memory; @@ -79,9 +78,11 @@ public bool ShrinkTo(int x, int y) return true; } - [Conditional("DEBUG")] - private void PlaneRowBoundsCheck(int c, int y) => - Debug.Assert(c < PlaneCount && y < this.YSize, "The bounds check has failed"); + private void PlaneRowBoundsCheck(int c, int y) + { + DebugGuard.MustBeLessThan(c, PlaneCount, nameof(c)); + DebugGuard.MustBeLessThan(y, this.YSize, nameof(y)); + } public void Dispose() { diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs index 9948ae790f..367b3fc300 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -76,8 +75,8 @@ public bool ShrinkTo(int x, int y) return false; } - Debug.Assert(x <= this.OriginalXSize, "ShrinkTo cannot expand memory"); - Debug.Assert(y <= this.OriginalYSize, "ShrinkTo cannot expand memory"); + DebugGuard.MustBeLessThanOrEqualTo(x, this.OriginalXSize, nameof(x)); + DebugGuard.MustBeLessThanOrEqualTo(y, this.OriginalYSize, nameof(y)); this.XSize = x; this.YSize = y; @@ -88,10 +87,9 @@ public bool ShrinkTo(int x, int y) protected Span GetRowBase(int y) where T : unmanaged { - Debug.Assert(y < this.YSize, "Attempted to access out-of-bounds Y coordinate"); + DebugGuard.MustBeLessThan(y, this.YSize, nameof(y)); Span row = this.Bytes.Span[(y * this.BytesPerRow)..]; - return MemoryMarshal.Cast(row); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs index 5320f5f171..3795f092b8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs @@ -2,8 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Diagnostics; -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; @@ -100,9 +99,8 @@ public static uint DecodeVariableLengthUint16(JxlBitReader reader) { if (symbols[0] == symbols[1]) { - Debug.Fail("Corrupt data"); counts.Dispose(); - return null; + throw new InvalidOperationException("The data is corrupt"); } countsSpan[(int)symbols[0]] = reader.ReadBits32((uint)precisionBits); @@ -142,9 +140,7 @@ public static uint DecodeVariableLengthUint16(JxlBitReader reader) if (shift > JxlAnsConstants.AnsLogTableSize + 1) { - Debug.Fail("Invalid shift"); - - return null; + throw new InvalidOperationException("Invalid shift"); } uint length = DecodeVariableLengthUint8(reader) + 3u; @@ -192,16 +188,14 @@ public static uint DecodeVariableLengthUint16(JxlBitReader reader) if (omitPos < 0) { - Debug.Fail("The histogram is corrupt or invalid."); counts.Dispose(); - return null; + throw new InvalidOperationException("The histogram is corrupt or invalid."); } if (omitPos + 1 < length && logCounts[omitPos + 1] == JxlAnsConstants.AnsLogTableSize) { - Debug.Fail("The histogram is corrupt or invalid."); counts.Dispose(); - return null; + throw new InvalidOperationException("The histogram is corrupt or invalid."); } int previous = 0; @@ -246,9 +240,8 @@ public static uint DecodeVariableLengthUint16(JxlBitReader reader) if (countsSpan[omitPos] <= 0) { - Debug.Fail("The histogram count is incorrect."); counts.Dispose(); - return null; + throw new InvalidOperationException("The histogram count is incorrect."); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs index 87c1408f8e..cdc69838df 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers.Binary; -using System.Diagnostics; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; @@ -72,7 +71,7 @@ private void MaybeRefill() private ulong ReadBits64Core(uint n, bool peek = false) { - Debug.Assert(n <= 64, "Too many bits to pack into ulong"); + DebugGuard.MustBeLessThanOrEqualTo(n, 64u, nameof(n)); this.MaybeRefill(); if (this.IsEndOfStream) @@ -117,7 +116,7 @@ private ulong ReadBits64Core(uint n, bool peek = false) private uint ReadBits32Core(uint n, bool peek = false) { - Debug.Assert(n <= 32, "Too many bits to pack into uint"); + DebugGuard.MustBeLessThanOrEqualTo(n, 32u, nameof(n)); this.MaybeRefill(); if (this.IsEndOfStream) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs index 88c1e88f7b..58e5c8b4f4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs @@ -1,11 +1,8 @@ // 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; /// @@ -38,15 +35,20 @@ internal static class JxlAcContext [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int ZeroDensityContext(int nonZeroesLeft, int k, int coveredBlocks, int log2CoveredBlocks, int prev) { - Debug.Assert((1 << log2CoveredBlocks) == coveredBlocks); + DebugGuard.IsTrue((1 << log2CoveredBlocks) == coveredBlocks, "log2CoveredBlocks must be equal to Log2(coveredBlocks)"); nonZeroesLeft = (nonZeroesLeft + coveredBlocks - 1) >> log2CoveredBlocks; k >>= log2CoveredBlocks; - Debug.Assert(k > 0); - Debug.Assert(k < 64); - Debug.Assert(nonZeroesLeft > 0); - Debug.Assert(nonZeroesLeft < 64); + if (k is < 0 or >= 64) + { + throw new InvalidOperationException("k must be within range of 0..63 inclusive"); + } + + if (nonZeroesLeft is <= 0 or >= 64) + { + throw new InvalidOperationException("nonZeroesLeft must be within range of 1..63 inclusive"); + } 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 index 32c7fe3e18..e7a518224c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs @@ -1,13 +1,10 @@ // 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.Processing.JxlFrameDimensions; -#pragma warning disable SA1405 // Debug.Assert should provide message text - namespace SixLabors.ImageSharp.Formats.Jxl.Processing; [StructLayout(LayoutKind.Sequential, Pack = 8)] @@ -37,8 +34,6 @@ public JxlAcStrategy(JxlAcStrategyType strategy, bool isFirst) { this.Strategy = strategy; this.isFirst = isFirst; - - Debug.Assert(this.IsMultiblock); } public JxlAcStrategy(JxlAcStrategyType strategy) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs index 6070db54c9..a33485df5d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs @@ -1,7 +1,6 @@ // 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; @@ -81,9 +80,7 @@ public bool SetNoBoundsChecks(int x, int y, JxlAcStrategyType type, bool check = if (check && rowSpan[pos] != Invalid) { - Debug.Fail("Invalid AC strategy. Blocks overlap."); - - return false; + throw new InvalidOperationException("Invalid AC strategy. Blocks overlap."); } rowSpan[pos] = (byte)(rawTypeTimes2 | ((iy | ix) == 0 ? 1 : 0)); @@ -95,12 +92,13 @@ public bool SetNoBoundsChecks(int x, int y, JxlAcStrategyType type, bool check = 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 + if (y + strategy.CoveredBlocksX > this.layers!.YSize || + x + strategy.CoveredBlocksX > this.layers.XSize) + { + throw new InvalidOperationException("Invalid range"); + } return this.SetNoBoundsChecks(x, y, type, check: false); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs index a99b3654ac..b94c565e7b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -19,7 +18,7 @@ public JxlAcStrategy this[int x] { ReadOnlySpan span = this.row.Span; - Debug.Assert(x * 8 < span.Length, "Too many bytes of memory were requested"); + DebugGuard.MustBeLessThan(x * 8, span.Length, "x overflows"); ref byte first = ref MemoryMarshal.GetReference(span); JxlAcStrategyType strategy = (JxlAcStrategyType)(Unsafe.Add(ref Unsafe.As(ref first), x) >> 1); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs index 0a2d59a90f..b8620c2dd4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs @@ -4,7 +4,7 @@ using System.Runtime.InteropServices; #pragma warning disable IDE0044 // Add readonly modifier -#pragma warning disable IDE0051 // Remove unused private members +#pragma warning disable IDE0052 // Remove unread private members namespace SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs index 0e0135c175..76b3e24b4a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Diagnostics; using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; @@ -139,9 +138,7 @@ public bool Dequantize( if (!this.ValidateSplinePointPos(px, py)) { - Debug.Fail("Spline points out of range"); - - return false; + throw new InvalidOperationException("Spline points out of range"); } int currentX = (int)px; @@ -165,18 +162,15 @@ public bool Dequantize( 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; + throw new InvalidOperationException("Manhattan distance is too large"); } if (!ValidateSplinePointPos(currentDx, currentDy)) { - Debug.Fail("Delta points out of range"); - - return false; + throw new InvalidOperationException("Delta points out of range"); } currentX += currentDx; @@ -184,9 +178,7 @@ public bool Dequantize( if (!ValidateSplinePointPos(currentX, currentY)) { - Debug.Fail("Current points out of range"); - - return false; + throw new InvalidOperationException("Current points out of range"); } controlPoints[i + 1] = new(currentX, currentY); @@ -239,9 +231,7 @@ public bool Dequantize( totalEstimatedAreaReached = widthEstimate * manhattanDistance; if (totalEstimatedAreaReached > areaLimit) { - Debug.Fail("Total estimated area is too large"); - - return false; + throw new InvalidOperationException("Total estimated area is too large"); } return true; @@ -258,18 +248,14 @@ public bool Decode( int numControlPoints = decoder.ReadHybridUnsignedInteger(NumControlPointsContext, br, contextMap); if (numControlPoints > maxControlPoints) { - Debug.Fail("Too many control points"); - - return false; + throw new InvalidOperationException("Too many control points"); } totalControlPoints += numControlPoints; if (totalControlPoints >= maxControlPoints) { - Debug.Fail("Too many control points"); - - return false; + throw new InvalidOperationException("Too many control points"); } this.ResizeControlPoints(configuration, numControlPoints); @@ -289,9 +275,7 @@ public bool Decode( if (controlPoint.First >= deltaLimit || controlPoint.First <= -deltaLimit || controlPoint.Second >= deltaLimit || controlPoint.Second <= -deltaLimit) { - Debug.Fail("Spline delta-delta is out of bounds"); - - return false; + throw new InvalidOperationException("Spline delta-delta is out of bounds"); } } @@ -319,9 +303,7 @@ bool TryDecodeDct(ReadOnlySpan contextMap, Span dct) dct[i] = UnpackSigned(decoder.ReadHybridUnsignedInteger(DctContext, br, contextMap)); if (dct[i] == invalidCoefficient) { - Debug.Fail("The DCT coefficient is invalid"); - - return false; + throw new InvalidOperationException("The DCT coefficient is invalid"); } } From 79e8ce7b1367e2c8ae3cc2c3ed1cdddfafc2be2b Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:15:07 +0400 Subject: [PATCH 060/142] Add Move to Front transform See inverse_mtf-inl.h --- .../Formats/Jxl/Processing/JxlInverseMtf.cs | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlInverseMtf.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlInverseMtf.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlInverseMtf.cs new file mode 100644 index 0000000000..6638f8600a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlInverseMtf.cs @@ -0,0 +1,91 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Inverse Move to Front implementation +/// +internal static class JxlInverseMtf +{ + // NOTE: here we use Vector512 to store 64 bytes in a + // more efficient manner. However, it doesn't necessarily + // require 512-bit CPU vector support. + // If the user's CPU has 256-bit vectors, the JIT will emit + // such instructions for each half. Likewise, if the user's + // CPU only goes up to 128-bit vectors, the JIT will emit + // 128-bit vector code for each quarter. And if the CPU + // doesn't support SIMD at all, the JIT will emit scalar + // instructions. + public static void MoveToFront(Span v, byte index) + { + byte value = v[index]; + byte i = index; + + ref byte vR = ref MemoryMarshal.GetReference(v); + + if (i < 4) + { + for (; i != 0; --i) + { + v[i] = v[i - 1]; + } + } + else + { + int tail = i & 63; + + if (tail != 0) + { + i -= (byte)tail; + Vector512 vec = Vector512.LoadUnsafe(ref Unsafe.Add(ref vR, i)); + Vector512 prev = Vector512.LoadUnsafe(ref Unsafe.Add(ref vR, i + 1)); + + // TODO: optimize this? + Span maskBytes = stackalloc byte[64]; + + for (int j = 0; j < 64; j++) + { + maskBytes[j] = (byte)(j < tail ? 0xFF : 0); + } + + Vector512 mask = Vector512.Create(maskBytes); + Vector512 filter = Vector512.ConditionalSelect(mask, vec, prev); + filter.StoreUnsafe(ref Unsafe.Add(ref vR, i + 1)); + } + + while (i != 0) + { + i -= 64; + Vector512 vec = Vector512.LoadUnsafe(ref Unsafe.Add(ref vR, i)); + vec.StoreUnsafe(ref Unsafe.Add(ref vR, i + 1)); + } + } + + v[0] = value; + } + + public static void InverseMoveToFrontTransform(Span v, int vLength) + { + Span mtf = stackalloc byte[256 + 64]; + for (int i = 0; i < 256; i++) + { + mtf[i] = (byte)i; + } + + for (int i = 0; i < vLength; i++) + { + byte index = v[i]; + v[i] = mtf[index]; + + if (index != 0) + { + MoveToFront(mtf, index); + } + } + } +} From d1f66463bc299dba93c1ce66cc5f961668421f2e Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:18:39 +0400 Subject: [PATCH 061/142] Prefer inline arrays --- .../Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs | 14 ++++++-------- .../Jxl/Processing/Decoder/JxlOpsinParameters.cs | 13 ++++++------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs index dc44594d9a..ef74d80122 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +#pragma warning disable SA1401 // Fields should be private + using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing; @@ -8,17 +10,13 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; internal sealed class JxlOpsinInverseMatrix : IJxlFields { - public bool AllDefault { get; set; } + public InlineArray3 OpsinBiases; - public JxlMatrix3x3F InverseMatrix { get; set; } + public InlineArray3 QuantBiases; - // Prefer arrays so we can set values like this: - // JxlOpsinInverseMatrix m = ...; - // m.OpsinBiases[0] = 1f; - // An InlineArray can't do that. - public float[] OpsinBiases { get; set; } = new float[3]; + public bool AllDefault { get; set; } - public float[] QuantBiases { get; set; } = new float[4]; + public JxlMatrix3x3F InverseMatrix { get; set; } public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs index c18a18ef0b..6fdfa28f9f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOpsinParameters.cs @@ -1,18 +1,17 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +#pragma warning disable SA1401 // Fields should be private + namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; internal sealed class JxlOpsinParameters { - // Use arrays instead of InlineArrays because, with inline arrays we can't do: - // JxlOpsinParameters parameters = ...; - // parameters.OpsinBiasesCbrt[0] /* <-- error */ = 1.25f; - public float[] InverseOpsinMatrix { get; set; } = new float[36]; + public InlineArray36 InverseOpsinMatrix; - public float[] OpsinBiases { get; set; } = new float[4]; + public InlineArray4 OpsinBiases; - public float[] OpsinBiasesCbrt { get; set; } = new float[4]; + public InlineArray4 OpsinBiasesCbrt; - public float[] QuantBiases { get; set; } = new float[4]; + public InlineArray4 QuantBiases; } From fe41799b9f4c9169e30a8c92410ff545500a60f6 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:21:07 +0400 Subject: [PATCH 062/142] Prevent potential overflow --- src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 45c7463248..fbb7a22d02 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -73,7 +73,7 @@ public JxlDecoderCore(DecoderOptions options) /// Boolean indicating whether the coordinates are out of bounds private static bool IsOutOfBounds(int a, int b, int size) { - int position = a + b; + long position = a + b; return position > size || position < a; } From 8209aa1f197ea059d7c5f17759f1a6bc61daba22 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:22:37 +0400 Subject: [PATCH 063/142] Refactor --- src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs index ccea59b2c8..2bded64ac0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using System.Security.Principal; using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; @@ -62,7 +61,7 @@ public static bool ReadPermutation(int skip, int size, Span order, JxlBitRe Span temp = stackalloc uint[size * 2]; temp.Clear(); - uint end = reader.ReadHybridUnsignedInteger(CoeffOrderContext((int)size), bitReader, contextMap) + skip; + uint end = reader.ReadHybridUnsignedInteger(CoeffOrderContext((uint)size), bitReader, contextMap) + skip; if (end > size) { From 074ff1b231576b622c89eca82a339ad36f01ec80 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:37:16 +0400 Subject: [PATCH 064/142] Validate the size parameter in JxlCoefficientOrder.ReadPermutation --- src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs index 2bded64ac0..4ee1a99587 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs @@ -55,6 +55,8 @@ public static uint CoeffOrderContext(uint value) public static bool ReadPermutation(int skip, int size, Span order, JxlBitReader bitReader, JxlAnsSymbolReader reader, Span contextMap) { + DebugGuard.MustBeLessThanOrEqualTo(size, 65536, nameof(size)); + Span lehmer = stackalloc uint[size]; lehmer.Clear(); From f19c8797ec804b48602b6188e29a7478b61a1f37 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 8 Aug 2026 18:17:35 +0400 Subject: [PATCH 065/142] Add more implementations and prototypes - Add decoding of Huffman Codes (see dec_huffman.cc and dec_huffman.h) - Add constructors to JxlImage3* classes - Make JxlColorCorrelationMap.Create 'xyb' parameter use true as a default value - Prototype of DCT quant weight parameters - Add passes shared state (see passes_state.cc and passes_state.h) - Add prototype for image operations (see image_ops.cc and image_ops.h) - Simplify inverse MTF (Move to Front) transform - Add patch context (see patch_dictionary_internal.h) - Add prototype of quantizer weights - Add 2nd prototype of ANS entropy decoding (see dec_ans.cc and dec_ans.h) - Add prototype of patch dictionary decoding (see dec_patch_dictionary.cc and dec_patch_dictionary.h) --- .../Jxl/IO/Entropy/JxlAnsLz77Parameters.cs | 70 +++- .../Formats/Jxl/IO/JxlHuffmanCode.cs | 6 +- .../Jxl/Memory/ImageTypes/JxlImage3B.cs | 5 + .../Jxl/Memory/ImageTypes/JxlImage3F.cs | 5 + .../Jxl/Memory/ImageTypes/JxlImage3I.cs | 5 + .../Jxl/Memory/ImageTypes/JxlImage3S.cs | 5 + .../Jxl/Memory/ImageTypes/JxlImage3U.cs | 5 + .../Formats/Jxl/Memory/JxlImage3{T}.cs | 11 +- .../Jxl/Processing/Decoder/JxlAnsCode.cs | 50 +++ .../Jxl/Processing/Decoder/JxlAnsReader.cs | 32 ++ .../Processing/Decoder/JxlAnsSymbolReader.cs | 30 ++ .../Processing/Decoder/JxlHuffmanDecoder.cs | 331 ++++++++++++++++++ .../Processing/Decoder/JxlPatchBlendMode.cs | 16 + .../Processing/Decoder/JxlPatchBlending.cs | 11 + .../Processing/Decoder/JxlPatchDictionary.cs | 256 ++++++++++++++ .../Processing/Decoder/JxlPatchPosition.cs | 11 + .../Decoder/JxlPatchReferencePosition.cs | 13 + .../Jxl/Processing/JxlColorCorrelationMap.cs | 2 +- .../Processing/JxlDctQuantWeightParameters.cs | 28 ++ .../Jxl/Processing/JxlImageFeatures.cs | 27 ++ .../Jxl/Processing/JxlImageOperations.cs | 51 +++ .../Formats/Jxl/Processing/JxlInverseMtf.cs | 82 +---- .../Jxl/Processing/JxlPassesSharedState.cs | 111 ++++++ .../Formats/Jxl/Processing/JxlPatchContext.cs | 32 ++ .../Formats/Jxl/Processing/JxlQuantWeights.cs | 15 + 25 files changed, 1132 insertions(+), 78 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsCode.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsSymbolReader.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlendMode.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlending.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchPosition.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchReferencePosition.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlDctQuantWeightParameters.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlImageFeatures.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlPatchContext.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlQuantWeights.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsLz77Parameters.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsLz77Parameters.cs index cbf6d3a4e4..01016f4dd6 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsLz77Parameters.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsLz77Parameters.cs @@ -7,15 +7,75 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; internal sealed class JxlAnsLz77Parameters : IJxlFields { - public bool Enabled { get; set; } + private bool enabled; + private uint minimumSymbol; + private uint minimumLength; + private JxlAnsHybridUIntConfiguration lengthUintConfig = new(0, 0, 0); - public uint MinimumSymbol { get; set; } + public JxlAnsLz77Parameters() => JxlBundle.Init(this); - public uint MinimumLength { get; set; } + public bool Enabled + { + get => this.enabled; + set => this.enabled = value; + } - public JxlAnsHybridUIntConfiguration LengthUintConfig { get; set; } = new(0, 0, 0); + public uint MinimumSymbol + { + get => this.minimumSymbol; + set => this.minimumSymbol = value; + } + + public uint MinimumLength + { + get => this.minimumLength; + set => this.minimumLength = value; + } + + public JxlAnsHybridUIntConfiguration LengthUintConfig + { + get => this.lengthUintConfig; + set => this.lengthUintConfig = value; + } public int NonserializedDistanceContext { get; set; } - public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); + public ref JxlAnsHybridUIntConfiguration GetLengthUIntConfigReference() => ref this.lengthUintConfig; + + public bool Visit(JxlVisitor visitor) + { + if (!visitor.Boolean(false, ref this.enabled)) + { + return false; + } + + if (!visitor.Conditional(this.enabled)) + { + return true; + } + + if (!visitor.U32( + JxlFieldExpressions.Value(224u), + JxlFieldExpressions.Value(512u), + JxlFieldExpressions.Value(4096u), + JxlFieldExpressions.BitsOffset(15u, 8u), + 224u, + ref this.minimumSymbol)) + { + return false; + } + + if (!visitor.U32( + JxlFieldExpressions.Value(3u), + JxlFieldExpressions.Value(4u), + JxlFieldExpressions.BitsOffset(2u, 5u), + JxlFieldExpressions.BitsOffset(8u, 9u), + 3u, + ref this.minimumLength)) + { + return false; + } + + return true; + } } diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlHuffmanCode.cs b/src/ImageSharp/Formats/Jxl/IO/JxlHuffmanCode.cs index 30bca3446d..2f95f7226c 100644 --- a/src/ImageSharp/Formats/Jxl/IO/JxlHuffmanCode.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlHuffmanCode.cs @@ -6,15 +6,15 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO; /// /// A single Huffman code. /// -internal struct JxlHuffmanCode +internal struct JxlHuffmanCode(byte bits, ushort value) { /// /// Number of bits for this symbol. /// - public byte Bits; + public byte Bits = bits; /// /// Symbol value/offset. /// - public ushort Value; + public ushort Value = value; } diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs index 63a5c59c9f..8c3b2ae36a 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs @@ -11,4 +11,9 @@ internal sealed class JxlImage3B : JxlImage3 public JxlImage3B() { } + + public JxlImage3B(Configuration configuration, int xSize, int ySize) + : base(configuration, xSize, ySize) + { + } } diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs index b456dfd9a7..8387f63194 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs @@ -11,4 +11,9 @@ internal sealed class JxlImage3F : JxlImage3 public JxlImage3F() { } + + public JxlImage3F(Configuration configuration, int xSize, int ySize) + : base(configuration, xSize, ySize) + { + } } diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs index 0d9f6c8202..f3486b95a7 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs @@ -11,4 +11,9 @@ internal sealed class JxlImage3I : JxlImage3 public JxlImage3I() { } + + public JxlImage3I(Configuration configuration, int xSize, int ySize) + : base(configuration, xSize, ySize) + { + } } diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs index 00615ff846..94b88056a1 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs @@ -11,4 +11,9 @@ internal sealed class JxlImage3S : JxlImage3 public JxlImage3S() { } + + public JxlImage3S(Configuration configuration, int xSize, int ySize) + : base(configuration, xSize, ySize) + { + } } diff --git a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs index 1921e86d33..8d118c048c 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs @@ -11,4 +11,9 @@ internal sealed class JxlImage3U : JxlImage3 public JxlImage3U() { } + + public JxlImage3U(Configuration configuration, int xSize, int ySize) + : base(configuration, xSize, ySize) + { + } } diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs index ab637308f9..9e9b10c099 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs @@ -17,6 +17,9 @@ public JxlImage3() { } + public JxlImage3(Configuration configuration, int xSize, int ySize) + => this.Allocate(configuration, xSize, ySize); + public JxlImage3(JxlImage3 other) { for (int i = 0; i < PlaneCount; i++) @@ -54,15 +57,15 @@ public void Swap(JxlImage3 other) } public static JxlImage3 Create(Configuration configuration, int xSize, int ySize) + => new(configuration, xSize, ySize); + + public void Allocate(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] - }; + this.planes = [plane0, plane1, plane2]; } public bool ShrinkTo(int x, int y) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsCode.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsCode.cs new file mode 100644 index 0000000000..56dbab4ed3 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsCode.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal sealed class JxlAnsCode +{ + public List HuffmanData { get; set; } = []; + + public List UIntConfig { get; set; } = []; + + public List DegenerateSymbols { get; set; } = []; + + public bool UsePrefixCode { get; set; } + + public byte LogAlphaSize { get; set; } + + public JxlAnsLz77Parameters Lz77 { get; set; } = new(); + + public int MaxNumBits { get; set; } + + public void UpdateMaxNumBits(int ctx, int symbol) + { + Span configs = CollectionsMarshal.AsSpan(this.UIntConfig); + ref JxlAnsHybridUIntConfiguration cfg = ref configs[ctx]; + if (this.Lz77.Enabled && this.Lz77.NonserializedDistanceContext != ctx && symbol >= this.Lz77.MinimumSymbol) + { + symbol -= (int)this.Lz77.MinimumSymbol; + cfg = ref this.Lz77.GetLengthUIntConfigReference(); + } + + uint splitToken = cfg.SplitToken; + uint msbInToken = cfg.MsbInToken; + uint lsbInToken = cfg.LsbInToken; + uint splitExponent = cfg.SplitExponent; + + if (symbol < splitToken) + { + this.MaxNumBits = Math.Max(this.MaxNumBits, (int)splitExponent); + return; + } + + uint nExtra = splitExponent - (msbInToken + lsbInToken) + (((uint)symbol - splitToken) >> (int)(msbInToken + lsbInToken)); + uint total = msbInToken + lsbInToken + nExtra + 1; + this.MaxNumBits = Math.Max(this.MaxNumBits, (int)total); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs index 3795f092b8..51e014d80d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs @@ -2,12 +2,17 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; internal static class JxlAnsReader { + private const int WindowSize = 1 << 20; + + private const int NumSpecialDistances = 120; + // Prefer jagged arrays over multidimensional arrays // for performance. Collection expressions help represent // jagged arrays easily. @@ -31,6 +36,33 @@ internal static class JxlAnsReader [3, 10], [4, 4], [3, 7], [4, 1], [3, 6], [3, 8], [3, 9], [4, 2], ]; + private static readonly sbyte[][] SpecialDistances = + [ + [0, 1], [1, 0], [1, 1], [-1, 1], [0, 2], [2, 0], [1, 2], [-1, 2], + [2, 1], [-2, 1], [2, 2], [-2, 2], [0, 3], [3, 0], [1, 3], [-1, 3], + [3, 1], [-3, 1], [2, 3], [-2, 3], [3, 2], [-3, 2], [0, 4], [4, 0], + [1, 4], [-1, 4], [4, 1], [-4, 1], [3, 3], [-3, 3], [2, 4], [-2, 4], + [4, 2], [-4, 2], [0, 5], [3, 4], [-3, 4], [4, 3], [-4, 3], [5, 0], + [1, 5], [-1, 5], [5, 1], [-5, 1], [2, 5], [-2, 5], [5, 2], [-5, 2], + [4, 4], [-4, 4], [3, 5], [-3, 5], [5, 3], [-5, 3], [0, 6], [6, 0], + [1, 6], [-1, 6], [6, 1], [-6, 1], [2, 6], [-2, 6], [6, 2], [-6, 2], + [4, 5], [-4, 5], [5, 4], [-5, 4], [3, 6], [-3, 6], [6, 3], [-6, 3], + [0, 7], [7, 0], [1, 7], [-1, 7], [5, 5], [-5, 5], [7, 1], [-7, 1], + [4, 6], [-4, 6], [6, 4], [-6, 4], [2, 7], [-2, 7], [7, 2], [-7, 2], + [3, 7], [-3, 7], [7, 3], [-7, 3], [5, 6], [-5, 6], [6, 5], [-6, 5], + [8, 0], [4, 7], [-4, 7], [7, 4], [-7, 4], [8, 1], [8, 2], [6, 6], + [-6, 6], [8, 3], [5, 7], [-5, 7], [7, 5], [-7, 5], [8, 4], [6, 7], + [-6, 7], [7, 6], [-7, 6], [8, 5], [7, 7], [-7, 7], [8, 6], [8, 7] + ]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int SpecialDistance(int index, int multiplier) + { + Span indexDistance = SpecialDistances[index]; + int dist = indexDistance[0] + (multiplier * indexDistance[1]); + return dist > 1 ? dist : 1; + } + public static uint DecodeVariableLengthUint8(JxlBitReader reader) { if (reader.ReadBoolean()) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsSymbolReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsSymbolReader.cs new file mode 100644 index 0000000000..8c4a83ca2f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsSymbolReader.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal sealed class JxlAnsSymbolReader +{ + private const int MaxCheckpointInterval = 512; + + // Use class because the Lz77Window property uses 2KB memory + private sealed class Checkpoint + { + public uint State { get; set; } + + public uint NumToCopy { get; set; } + + public uint CopyPos { get; set; } + + public uint NumDecoded { get; set; } + + public uint[] Lz77Window { get; set; } = new uint[MaxCheckpointInterval]; + } + + private readonly JxlAnsEntry[] aliasTables = []; + private JxlHuffmanDecodingData huffmanData; + private bool usePrefixCode; + private uint state = AnsSignature << 16u; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs new file mode 100644 index 0000000000..991d9c6159 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs @@ -0,0 +1,331 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Decodes Huffman codes. +/// +internal sealed class JxlHuffmanDecoder +{ + /// + /// Number of bits that a huffman table uses. + /// + private const int HuffmanTableBits = 8; + + private const int GoalSize = 1 << HuffmanTableBits; + + public const int CodeLengthCodes = 18; + + public const int DefaultCodeLength = 8; + + public const int CodeLengthRepeatCode = 16; + + /// + /// Static Huffman codes for code length code lengths. + /// + private static readonly JxlHuffmanCode[] CodeLengthCodeLengthsCodes = + [ + new(2, 0), new(2, 4), new(2, 3), new(3, 2), new(2, 0), new(2, 4), new(2, 3), new(4, 1), + new(2, 0), new(2, 4), new(2, 3), new(3, 2), new(2, 0), new(2, 4), new(2, 3), new(4, 5), + ]; + + private static ReadOnlySpan CodeLengthCodeOrder => + [ + 1, 2, 3, 4, 0, 5, 17, 6, 16, 7, 8, 9, 10, 11, 12, 13, 14, 15, + ]; + + /// + /// Gets or sets the list of huffman codes. + /// + public JxlHuffmanCode[] Table { get; set; } = []; + + public static bool ReadHuffmanCodeLengths(Span codeLengthCodeLengths, int numSymbols, Span codeLengths, JxlBitReader br) + { + int symbol = 0; + int prevCodeLen = DefaultCodeLength; + int repeat = 0; + int repeatCodeLen = 0; + int space = 32768; + + Span table = stackalloc JxlHuffmanCode[32]; + Span counts = stackalloc ushort[16]; + table.Clear(); + counts.Clear(); + + for (int i = 0; i < CodeLengthCodes; i++) + { + counts[codeLengthCodeLengths[i]]++; + } + + if (JxlHuffman.BuildHuffmanTable(table, 5, codeLengthCodeLengths, counts) == 0) + { + return false; + } + + while (symbol < numSymbols && space > 0) + { + JxlHuffmanCode code = table[(int)br.PeekBits32(5u)]; + br.SkipBits32(code.Bits); + byte codeLength = (byte)code.Value; // It is indeed converted from ushort to byte + + if (codeLength < CodeLengthRepeatCode) + { + repeat = 0; + codeLengths[symbol++] = codeLength; + if (codeLength != 0) + { + prevCodeLen = codeLength; + space -= 32768 >> codeLength; + } + } + else + { + int extraBits = codeLength - 14; + byte newLength = 0; + if (codeLength == CodeLengthRepeatCode) + { + newLength = (byte)prevCodeLen; + } + + if (repeatCodeLen != newLength) + { + repeat = 0; + repeatCodeLen = newLength; + } + + int oldRepeat = repeat; + + if (repeat > 0) + { + repeat -= 2; + repeat <<= extraBits; + } + + repeat += (int)br.ReadBits32((uint)extraBits) + 3; + int repeatDelta = repeat - oldRepeat; + + if (symbol + repeatDelta > numSymbols) + { + return false; + } + + codeLengths.Slice(symbol, repeatDelta).Fill((byte)repeatCodeLen); + symbol += repeatDelta; + if (repeatCodeLen != 0) + { + space -= repeatDelta << (15 - repeatCodeLen); + } + } + } + + if (space != 0) + { + return false; + } + + codeLengths[symbol..].Clear(); + return true; + } + + /// + /// Reads a simple Huffman code. + /// + /// Alphabet size (256 at most) + /// Bit-stream reader + /// Output table (must have at most 8 items) + /// Status of the operation + public static bool ReadSimpleCode(int alphabetSize, JxlBitReader br, Span table) + { + int maxBits = (alphabetSize > 1) ? FloorLog2Nonzero(alphabetSize - 1) + 1 : 0; + uint symbolCount = br.ReadBits32(2u) + 1u; + + Span symbols = stackalloc ushort[4]; + symbols.Clear(); // Clearing is necessary. Not every value will be initialized. + + for (int i = 0; i < symbolCount; i++) + { + uint symbol = br.ReadBits32((uint)maxBits); + if (symbol >= alphabetSize) + { + return false; + } + + symbols[i] = (ushort)symbol; + } + + for (int i = 0; i < symbolCount - 1; i++) + { + for (int j = i + 1; j < symbolCount; j++) + { + if (symbols[i] == symbols[j]) + { + return false; + } + } + } + + if (symbolCount == 4) + { + symbolCount += br.ReadBits32(1u); + } + + int tableSize = 1; + switch (symbolCount) + { + case 1: + table[0] = new(0, symbols[0]); + break; + + case 2: + if (symbols[0] > symbols[1]) + { + SwapSymbols(0, 1, symbols); + } + + table[0] = new(1, symbols[0]); + table[1] = new(1, symbols[1]); + tableSize = 2; + break; + + case 3: + if (symbols[1] > symbols[2]) + { + SwapSymbols(1, 2, symbols); + } + + table[0] = new(1, symbols[0]); + table[2] = new(1, symbols[0]); + table[1] = new(2, symbols[1]); + table[3] = new(2, symbols[2]); + tableSize = 4; + break; + + case 4: + for (int i = 0; i < 3; i++) + { + for (int j = i + 1; j < 4; j++) + { + if (symbols[i] > symbols[j]) + { + SwapSymbols(i, j, symbols); + } + } + } + + table[0] = new(2, symbols[0]); + table[2] = new(2, symbols[1]); + table[1] = new(2, symbols[2]); + table[3] = new(2, symbols[3]); + tableSize = 4; + break; + + case 5: + if (symbols[2] > symbols[3]) + { + SwapSymbols(2, 3, symbols); + } + + table[0] = new(1, symbols[0]); + table[1] = new(2, symbols[1]); + table[2] = new(1, symbols[0]); + table[3] = new(3, symbols[2]); + table[4] = new(1, symbols[0]); + table[5] = new(2, symbols[1]); + table[6] = new(1, symbols[0]); + table[7] = new(3, symbols[3]); + tableSize = 8; + break; + + default: + // This should be unreachable. + return false; + } + + while (tableSize != GoalSize) + { + table[tableSize..].CopyTo(table); + tableSize <<= 1; + } + + return true; + } + + public bool ReadFromBitStream(int alphabetSize, JxlBitReader br) + { + if (alphabetSize > (1 << JxlAnsConstants.PrefixMaxBits)) + { + return false; + } + + uint simpleCodeOrSkip = br.ReadBits32(2u); + if (simpleCodeOrSkip == 1u) + { + this.Table = new JxlHuffmanCode[GoalSize]; + return ReadSimpleCode(alphabetSize, br, this.Table); + } + + // The alphabet size is at most 256 + Span codeLengths = stackalloc byte[alphabetSize]; + codeLengths.Clear(); // Zero-initialized in reference software + + Span codeLengthCodeLengths = stackalloc byte[CodeLengthCodes]; + codeLengthCodeLengths.Clear(); // Zero-initialized in reference software + + int space = 32; + int numCodes = 0; + + for (uint i = simpleCodeOrSkip; i < CodeLengthCodes && space > 0; i++) + { + int codeLengthIndex = CodeLengthCodeOrder[(int)i]; + JxlHuffmanCode huff = CodeLengthCodeLengthsCodes[(int)br.PeekBits32(4u)]; + br.SkipBits32(huff.Bits); + byte value = (byte)huff.Value; // It's indeed converted from ushort to byte + codeLengthCodeLengths[codeLengthIndex] = value; + + if (value != 0) + { + space -= 32 >> value; + numCodes++; + } + } + + bool ok = (numCodes == 1 || space == 0) && ReadHuffmanCodeLengths(codeLengthCodeLengths, alphabetSize, codeLengths, br); + + if (!ok) + { + return false; + } + + Span counts = stackalloc ushort[16]; + counts.Clear(); // Zero-initialized + + this.Table = new JxlHuffmanCode[alphabetSize + 376]; + uint tableSize = JxlHuffman.BuildHuffmanTable(this.Table, HuffmanTableBits, codeLengths, counts); + + this.Table = this.Table[..(int)tableSize]; + + return tableSize > 0; + } + + public ushort ReadSymbol(JxlBitReader br) + { + Span table = this.Table.AsSpan()[(int)br.PeekBits32(HuffmanTableBits)..]; + int bitCount = table[0].Bits; + if (bitCount > HuffmanTableBits) + { + br.SkipBits32(HuffmanTableBits); + bitCount -= HuffmanTableBits; + table = table[(int)(table[0].Value + br.PeekBits32((uint)bitCount))..]; + } + + br.SkipBits32(table[0].Bits); + return table[0].Value; + } + + private static void SwapSymbols(int i, int j, Span symbols) => RuntimeUtility.Swap(ref symbols[i], ref symbols[j]); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlendMode.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlendMode.cs new file mode 100644 index 0000000000..755cfe4eb9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlendMode.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal enum JxlPatchBlendMode : byte +{ + None, + Replace, + Add, + Multiply, + BlendAbove, + BlendBelow, + AlphaWeightedAddAbove, + AlphaWeightedAddBelow +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlending.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlending.cs new file mode 100644 index 0000000000..7cedfc3053 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchBlending.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal struct JxlPatchBlending +{ + public JxlPatchBlendMode Mode; + public int AlphaChannel; + public bool Clamp; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs new file mode 100644 index 0000000000..1b72325667 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs @@ -0,0 +1,256 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal sealed class JxlPatchDictionary +{ + private struct PatchTreeNode + { + public long LeftChild; + public long RightChild; + public int YCenter; + public int Start; + public int Count; + } + + private struct SortedPatch + { + public int First; + public int Second; + } + + private readonly JxlReferenceFrame[] referenceFrames = new JxlReferenceFrame[4]; + private readonly List positions = []; + private readonly List referencePositions = []; + private readonly List blendings = []; + private int blendingsStride; + private readonly List patchTree = []; + private readonly List numPatches = []; + private readonly List sortedPatchesY0 = []; + private readonly List sortedPatchesY1 = []; + + public bool HasAny => this.positions.Count > 0; + + public void Clear() + { + this.positions.Clear(); + ComputePatchTree(); + } + + public void Decode( + JxlMemoryManager memoryManager, + JxlBitReader br, + ulong xsize, + ulong ysize, + ulong numExtraChannels, + ref bool usesExtraChannels) + { + this.positions.Clear(); + this.blendingsStride = (int)(numExtraChannels + 1); + + List contextMap = []; + var code = new JxlAnsCode(); + + var status = DecodeHistograms( + memoryManager, + br, + PatchDictionaryContexts, + code, + contextMap); + + JxlAnsSymbolReader decoder = JxlAnsSymbolReader.Create(code, br); + + ulong ReadNum(int context) + => decoder.ReadHybridUint(context, br, contextMap); + + ulong numRefPatch = ReadNum(kNumRefPatchContext); + + ulong numPixels = xsize * ysize; + ulong maxRefPatches = 1024 + (numPixels / 4); + ulong maxPatches = maxRefPatches * 4; + ulong maxBlendingInfos = maxPatches * 4; + + if (numRefPatch > maxRefPatches) + { + throw new InvalidOperationException("Too many patches in dictionary"); + } + + ulong totalPatches = 0; + ulong nextSize = 1; + + for (ulong id = 0; id < numRefPatch; id++) + { + JxlPatchReferencePosition refPos = new() + { + Ref = ReadNum(kReferenceFrameContext) + }; + + if (refPos.Ref >= kMaxNumReferenceFrames || this.referenceFrames[(int)refPos.Ref].Frame.XSize == 0) + { + throw new InvalidOperationException("Invalid reference frame ID"); + } + + if (!this.referenceFrames[refPos.Ref].IsInXYB) + { + throw new InvalidOperationException("Patches cannot use frames saved post color transforms"); + } + + JxlImageBundle ib = this.referenceFrames[refPos.Ref].Frame; + + refPos.X0 = ReadNum(kPatchReferencePositionContext); + refPos.Y0 = ReadNum(kPatchReferencePositionContext); + refPos.XSize = ReadNum(kPatchSizeContext) + 1; + refPos.YSize = ReadNum(kPatchSizeContext) + 1; + + if (refPos.X0 + refPos.XSize > ib.XSize) + { + throw new InvalidOperationException("Invalid position specified in reference frame"); + } + + if (refPos.Y0 + refPos.YSize > ib.YSize) + { + throw new InvalidOperationException("Invalid position specified in reference frame"); + } + + ulong idCount = ReadNum(kPatchCountContext); + + if (idCount > maxPatches) + { + throw new InvalidOperationException("Too many patches in dictionary"); + } + + idCount++; + + totalPatches += idCount; + + if (totalPatches > maxPatches) + { + throw new InvalidOperationException("Too many patches in dictionary"); + } + + if (nextSize < totalPatches) + { + nextSize *= 2; + nextSize = Math.Min(nextSize, maxPatches); + } + + if (nextSize * (ulong)this.blendingsStride > maxBlendingInfos) + { + throw new InvalidOperationException("Too many patches in dictionary"); + } + + _ = this.blendings.EnsureCapacity((int)nextSize); + _ = this.blendings.EnsureCapacity((int)(nextSize * (ulong)this.blendingsStride)); + + bool chooseAlpha = numExtraChannels > 1; + + for (ulong i = 0; i < idCount; i++) + { + JxlPatchPosition pos = new() + { + ReferencePositionIndex = this.referencePositions.Count + }; + + if (i == 0) + { + pos.X = ReadNum(kPatchPositionContext); + pos.Y = ReadNum(kPatchPositionContext); + } + else + { + long deltaX = JxlPackSigned.UnpackSigned(ReadNum(kPatchOffsetContext)); + + if (deltaX < 0 && (int)(-deltaX) > this.positions[^1].X) + { + throw new InvalidOperationException($"Invalid patch: negative x coordinate ({this.positions[^1].X}, delta {deltaX})"); + } + + pos.X = (int)(this.positions[^1].X + deltaX); + + long deltaY = JxlPackSigned.UnpackSigned(ReadNum(kPatchOffsetContext)); + + if (deltaY < 0 && (int)(-deltaY) > this.positions[^1].Y) + { + throw new InvalidOperationException($"Invalid patch: negative y coordinate ({this.positions[^1].Y}, delta {deltaY})"); + } + + pos.Y = (int)(this.positions[^1].Y + deltaY); + } + + if (pos.X + refPos.XSize > (int)xsize) + { + throw new InvalidOperationException($"Invalid patch x: {pos.X} + {refPos.XSize} > {xsize}"); + } + + if (pos.Y + refPos.YSize > (int)ysize) + { + throw new InvalidOperationException($"Invalid patch y: {pos.Y} + {refPos.YSize} > {ysize}"); + } + + for (int j = 0; j < this.blendingsStride; j++) + { + uint blendMode = (uint)ReadNum(kPatchBlendModeContext); + + if (blendMode >= kNumPatchBlendModes) + { + throw new InvalidOperationException($"Invalid patch blend mode: {blendMode}"); + } + + JxlPatchBlending info = new() + { + Mode = (JxlPatchBlendMode)blendMode + }; + + if (UsesAlpha(info.Mode)) + { + usesExtraChannels = true; + } + + if (info.Mode != JxlPatchBlendMode.None && j > 0) + { + usesExtraChannels = true; + } + + if (UsesAlpha(info.Mode) && chooseAlpha) + { + info.AlphaChannel = (uint)ReadNum(kPatchAlphaChannelContext); + + if (info.AlphaChannel >= (int)numExtraChannels) + { + throw new InvalidOperationException($"Invalid alpha channel for blending: {info.AlphaChannel} out of {numExtraChannels}"); + } + } + else + { + info.AlphaChannel = 0; + } + + if (UsesClamp(info.Mode)) + { + info.Clamp = ReadNum(kPatchClampContext) != 0; + } + else + { + info.Clamp = false; + } + + this.blendings.Add(info); + } + + this.positions.Add(pos); + } + + this.positions.Add(refPos); + } + + this.positions.TrimExcess(); + + if (!decoder.CheckAnsFinalState()) + { + throw new InvalidOperationException("ANS checksum failure."); + } + + this.ComputePatchTree(); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchPosition.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchPosition.cs new file mode 100644 index 0000000000..264d694e85 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchPosition.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal struct JxlPatchPosition +{ + public int X; + public int Y; + public int ReferencePositionIndex; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchReferencePosition.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchReferencePosition.cs new file mode 100644 index 0000000000..8e11eb55ad --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchReferencePosition.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal struct JxlPatchReferencePosition +{ + public int Ref; + public int X0; + public int Y0; + public int XSize; + public int YSize; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs index 89ae3f7296..b2e2581f72 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs @@ -19,7 +19,7 @@ internal sealed class JxlColorCorrelationMap public bool DecodeDc(JxlBitReader reader) => this.Base.DecodeDc(reader); - public static JxlColorCorrelationMap Create(Configuration configuration, int width, int height, bool xyb) + public static JxlColorCorrelationMap Create(Configuration configuration, int width, int height, bool xyb = true) { JxlColorCorrelationMap map = new(); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctQuantWeightParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctQuantWeightParameters.cs new file mode 100644 index 0000000000..288d129956 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctQuantWeightParameters.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal sealed class JxlDctQuantWeightParameters +{ + private const int Log2MaxDistanceBands = 4; + private const int MaxDistanceBands = 1 + (1 << Log2MaxDistanceBands); + + private int numDistanceBands; + private readonly float[][] distanceBands; + + public JxlDctQuantWeightParameters() + { + this.distanceBands = new float[3][]; + for (int i = 0; i < 3; i++) + { + this.distanceBands[i] = new float[MaxDistanceBands]; + } + } + + public JxlDctQuantWeightParameters(float[][] distanceBands, int numDistanceBands) + { + this.numDistanceBands = numDistanceBands; + this.distanceBands = distanceBands; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageFeatures.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlImageFeatures.cs new file mode 100644 index 0000000000..142edd0710 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlImageFeatures.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Image features for the JPEG XL passes decoder +/// +internal sealed class JxlImageFeatures +{ + /// + /// Gets or sets noise parameters for the passes decoder + /// + public JxlNoiseParameters NoiseParameters { get; set; } = new(); + + /// + /// Gets or sets patch dictionary for the passes decoder + /// + public JxlPatchDictionary PatchDictionary { get; set; } = new(); + + /// + /// Gets or sets splines for the passes decoder + /// + public JxlSplines Splines { get; set; } = new(); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs new file mode 100644 index 0000000000..f1b50596f7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal static class JxlImageOperations +{ + /// + /// Returns true if first image has same width and height as the second image. + /// + /// First image + /// Second image + /// True if width and height is equal. + public static bool SameSize(JxlPlaneBase a, JxlPlaneBase b) => a.XSize == b.XSize && a.YSize == b.YSize; + + public static bool CopyImage(JxlPlane from, JxlPlane to) + where T : unmanaged + { + if (!SameSize(from, to)) + { + return false; + } + + if (from.XSize == 0 || from.YSize == 0) + { + return true; + } + + for (int y = 0; y < from.YSize; y++) + { + Span rowFrom = from.GetRow(y); + Span rowTo = to.GetRow(y); + rowFrom.CopyTo(rowTo); + } + + return true; + } + + public static bool CopyImageTo(Rectangle rectFrom, JxlPlane from, Rectangle rectTo, JxlPlane to) + where T : unmanaged + { + if (rectFrom != rectTo) + { + return false; + } + + + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlInverseMtf.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlInverseMtf.cs index 6638f8600a..8016ec51c0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlInverseMtf.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlInverseMtf.cs @@ -1,10 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; - namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// @@ -12,79 +8,35 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// internal static class JxlInverseMtf { - // NOTE: here we use Vector512 to store 64 bytes in a - // more efficient manner. However, it doesn't necessarily - // require 512-bit CPU vector support. - // If the user's CPU has 256-bit vectors, the JIT will emit - // such instructions for each half. Likewise, if the user's - // CPU only goes up to 128-bit vectors, the JIT will emit - // 128-bit vector code for each quarter. And if the CPU - // doesn't support SIMD at all, the JIT will emit scalar - // instructions. - public static void MoveToFront(Span v, byte index) + public static void MoveToFront(Span values, byte index) { - byte value = v[index]; - byte i = index; - - ref byte vR = ref MemoryMarshal.GetReference(v); - - if (i < 4) - { - for (; i != 0; --i) - { - v[i] = v[i - 1]; - } - } - else - { - int tail = i & 63; - - if (tail != 0) - { - i -= (byte)tail; - Vector512 vec = Vector512.LoadUnsafe(ref Unsafe.Add(ref vR, i)); - Vector512 prev = Vector512.LoadUnsafe(ref Unsafe.Add(ref vR, i + 1)); + byte value = values[index]; - // TODO: optimize this? - Span maskBytes = stackalloc byte[64]; - - for (int j = 0; j < 64; j++) - { - maskBytes[j] = (byte)(j < tail ? 0xFF : 0); - } - - Vector512 mask = Vector512.Create(maskBytes); - Vector512 filter = Vector512.ConditionalSelect(mask, vec, prev); - filter.StoreUnsafe(ref Unsafe.Add(ref vR, i + 1)); - } - - while (i != 0) - { - i -= 64; - Vector512 vec = Vector512.LoadUnsafe(ref Unsafe.Add(ref vR, i)); - vec.StoreUnsafe(ref Unsafe.Add(ref vR, i + 1)); - } - } - - v[0] = value; + // CopyTo supports overlapping source and destination regions. + values[..index].CopyTo(values[1..]); + values[0] = value; } - public static void InverseMoveToFrontTransform(Span v, int vLength) + public static void InverseMoveToFrontTransform(Span values) { - Span mtf = stackalloc byte[256 + 64]; - for (int i = 0; i < 256; i++) + Span table = stackalloc byte[256]; + + for (int i = 0; i < table.Length; i++) { - mtf[i] = (byte)i; + table[i] = (byte)i; } - for (int i = 0; i < vLength; i++) + for (int i = 0; i < values.Length; i++) { - byte index = v[i]; - v[i] = mtf[index]; + byte index = values[i]; + byte value = table[index]; + values[i] = value; if (index != 0) { - MoveToFront(mtf, index); + // CopyTo handles the overlap and shifts the preceding entries. + table[..index].CopyTo(table[1..]); + table[0] = value; } } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs new file mode 100644 index 0000000000..806c302f51 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs @@ -0,0 +1,111 @@ +// 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.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal class JxlPassesSharedState +{ + public JxlCodecMetadata CodecMetadata { get; set; } = new(); + + public JxlFrameDimensions FrameDimensions { get; set; } + + public JxlAcStrategyImage AcStrategy { get; set; } + + public JxlDequantMatrices Matrices { get; set; } = new(); + + public JxlQuantizer Quantizer { get; set; } + + public JxlImageI RawQuantField { get; set; } + + public JxlImageB EpfSharpness { get; set; } + + public JxlColorCorrelationMap ColorMap { get; set; } + + public JxlImageFeatures ImageFeatures { get; set; } = new(); + + public int CoeffOrderSize { get; set; } + + public List CoeffOrders { get; set; } = []; + + public JxlImageB QuantDc { get; set; } + + public JxlImage3F DcStorage { get; set; } + + public JxlImage3F Dc { get; set; } + + public JxlBlockContextMap BlockContextMap { get; set; } = new(); + + public JxlImage3F[] DcFrames { get; set; } = new JxlImage3F[4]; + + public JxlReferenceFrame[] ReferenceFrames { get; set; } = new JxlReferenceFrame[4]; + + public int NumHistograms { get; set; } + + public JxlPassesSharedState(Configuration configuration, JxlFrameHeader frameHeader, bool encoder) + { + if (frameHeader.Metadata is null) + { + throw new InvalidOperationException("The frame header metadata is missing"); + } + + this.CodecMetadata = frameHeader.Metadata; + this.FrameDimensions = frameHeader.FrameDimensions; + this.ImageFeatures.PatchDictionary.SetShared(this.ImageFeatures.ReferenceFrames); + + JxlFrameDimensions dimensions = frameHeader.FrameDimensions; + + this.AcStrategy = JxlAcStrategyImage.Create(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + this.RawQuantField = new JxlImageI(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + this.EpfSharpness = new JxlImageB(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + this.ColorMap = JxlColorCorrelationMap.Create(configuration, dimensions.XSize, dimensions.YSize); + + this.CoeffOrderSize = JxlCoefficientOrder.CoefficientOrderMaxSize; + + if (encoder && + this.CoeffOrders.Count < (frameHeader.Passes.NumPasses & JxlCoefficientOrder.CoefficientOrderMaxSize) && + frameHeader.Encoding == JxlFrameEncoding.VarDct) + { + // we add the padding to CoeffOrders so its length is equal to the variable upperBound + int upperBound = frameHeader.Passes.NumPasses & JxlCoefficientOrder.CoefficientOrderMaxSize; + int length = this.CoeffOrders.Count; + int delta = upperBound - length; + + for (int i = 0; i < delta; i++) + { + this.CoeffOrders.Add(0); // default constant + } + } + + this.QuantDc = new JxlImageB(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + + bool useDcFrame = (frameHeader.Flags & (ulong)JxlFrameHeaderFlags.Dc) != 0; + if (!encoder && useDcFrame) + { + if (frameHeader.DcLevel == 4) + { + throw new InvalidOperationException("DC level for DC frames cannot be equal to 4"); + } + + this.DcStorage = new JxlImage3F(); + this.Dc = this.DcFrames[(int)frameHeader.DcLevel]; + + if (this.Dc.XSize == 0) + { + throw new InvalidOperationException("DC frame was specified for DC Level = " + frameHeader.DcLevel + ", but frame wasn't decoded with level " + frameHeader.DcLevel + 1); + } + + this.QuantDc.Clear(); + } + else + { + this.DcStorage = new JxlImage3F(configuration, dimensions.XSizeBlocks, dimensions.YSizeBlocks); + this.Dc = this.DcStorage; + } + + this.Quantizer = new(this.Matrices); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPatchContext.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPatchContext.cs new file mode 100644 index 0000000000..a79a4720ef --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPatchContext.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Context numbers for patch decoding +/// +internal enum JxlPatchContext : byte +{ + NumRefPatch = 0, + + ReferenceFrame = 1, + + PatchSize = 2, + + PatchReferencePosition = 3, + + PatchPosition = 4, + + PatchBlendMode = 5, + + PatchOffset = 6, + + PatchCount = 7, + + PatchAlphaChannel = 8, + + PatchClamp = 9, + + NumPatchDictionaryContexts +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantWeights.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantWeights.cs new file mode 100644 index 0000000000..2c50d92afd --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantWeights.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal static class JxlQuantWeights +{ + public const int MaxQuantTableSize = JxlAcStrategy.MaximumCoefficientArea; + + public const int NumPredefinedTables = 1; + + public const int CeilLog2NumPredefinedTables = 0; + + public const int Log2NumQuantModes = 3; +} From ff98ffd0f74778f0abe7b44851e1586490449589 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 8 Aug 2026 18:26:07 +0400 Subject: [PATCH 066/142] Add image copy operations --- .../Jxl/Processing/JxlImageOperations.cs | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs index f1b50596f7..c62f6a4030 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs @@ -46,6 +46,47 @@ public static bool CopyImageTo(Rectangle rectFrom, JxlPlane from, Rectangl return false; } - + if (!from.IsRectangleInside(rectFrom)) + { + return false; + } + + if (!to.IsRectangleInside(rectTo)) + { + return false; + } + + if (rectFrom.Width == 0) + { + return true; + } + + for (int y = 0; y < rectFrom.Height; y++) + { + Span rowFrom = from.GetRow(rectFrom, y); + Span rowTo = to.GetRow(rectTo, y); + rowFrom.CopyTo(rowTo); + } + + return true; + } + + public static bool CopyImageTo(Rectangle rectFrom, JxlImage3 from, Rectangle rectTo, JxlImage3 to) + where T : unmanaged + { + if (rectFrom != rectTo) + { + return false; + } + + for (int plane = 0; plane < 3; plane++) + { + if (!CopyImageTo(rectFrom, from.Plane(plane), rectTo, to.Plane(plane))) + { + return false; + } + } + + return true; } } From e718a4ad31782c10a6f0d5b9c5e92536a009f327 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 8 Aug 2026 19:53:58 +0400 Subject: [PATCH 067/142] Implement image operations --- .../Formats/Jxl/Memory/JxlPlane{T}.cs | 7 + .../Jxl/Processing/JxlImageOperations.cs | 457 ++++++++++++++++++ 2 files changed, 464 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs index 6bb09c6002..7326768f09 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Processing; + namespace SixLabors.ImageSharp.Formats.Jxl.Memory; // NOTE: Do not seal this class. @@ -33,4 +35,9 @@ public static JxlPlane Create(Configuration configuration, int xSize, int ySi } public Span GetRow(int y) => this.GetRowBase(y); + + /// + /// Fills everything in this image with 0. + /// + public void Clear() => JxlImageOperations.ZeroFillImage(this); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs index c62f6a4030..b6c8a3eb93 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs @@ -1,10 +1,17 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +/// +/// Provides methods for processing 2D views of memory used by the +/// JPEG XL codec. +/// internal static class JxlImageOperations { /// @@ -15,6 +22,15 @@ internal static class JxlImageOperations /// True if width and height is equal. public static bool SameSize(JxlPlaneBase a, JxlPlaneBase b) => a.XSize == b.XSize && a.YSize == b.YSize; + /// + /// Copies everything from one plane to another. + /// + /// The type of planes to copy. + /// Source plane (read-only) + /// Destination plane (write-only) + /// + /// Status of the copy operation + /// public static bool CopyImage(JxlPlane from, JxlPlane to) where T : unmanaged { @@ -38,6 +54,20 @@ public static bool CopyImage(JxlPlane from, JxlPlane to) return true; } + /// + /// Within bounds specified by input rectangles, copies everything from one plane to another. + /// + /// The type of planes to copy. + /// Area in the source plane. + /// Source plane to copy (read-only) + /// Area in the destination plane. + /// Destination plane to copy (write-only) + /// + /// Status of the copy operation. + /// + /// + /// Rectangles MUST be within plane bounds and both should be same. + /// public static bool CopyImageTo(Rectangle rectFrom, JxlPlane from, Rectangle rectTo, JxlPlane to) where T : unmanaged { @@ -71,6 +101,21 @@ public static bool CopyImageTo(Rectangle rectFrom, JxlPlane from, Rectangl return true; } + /// + /// Within bounds specified by input rectangles, copies everything within every plane + /// from one image to another. + /// + /// The type of image to copy. + /// Area in the source image. + /// Source image to copy (read-only) + /// Area in the destination image. + /// Destination image to copy (write-only) + /// + /// Status of the copy operation. + /// + /// + /// Rectangles MUST be within image bounds and both should be same. + /// public static bool CopyImageTo(Rectangle rectFrom, JxlImage3 from, Rectangle rectTo, JxlImage3 to) where T : unmanaged { @@ -89,4 +134,416 @@ public static bool CopyImageTo(Rectangle rectFrom, JxlImage3 from, Rectang return true; } + + /// + /// Converts a plane from one type to another within specified bounds and ensures to clamp values if the + /// minimum and maximum limits of the result type are lower than the input type (e.g., input is + /// and destination is ). + /// + /// Type of the source plane + /// Type of the destination plane + /// The area of the source plane + /// The source plane (read-only) + /// The area of the destination plane + /// The destination plane (write-only) + /// + /// Status of the copy operation. + /// + public static bool ConvertPlaneAndClamp(Rectangle rectFrom, JxlPlane from, Rectangle rectTo, JxlPlane to) + where TFrom : unmanaged, INumber + where TTo : unmanaged, INumber + { + if (rectFrom != rectTo) + { + return false; + } + + for (int y = 0; y < rectTo.Height; y++) + { + Span rowFrom = from.GetRow(rectFrom, y); + Span rowTo = to.GetRow(rectTo, y); + + for (int x = 0; x < rectTo.Width; x++) + { + rowTo[x] = TTo.CreateSaturating(rowFrom[x]); + } + } + + return true; + } + + /// + /// + /// Copies an image region from to , + /// including up to pixels of surrounding source data + /// on each side of the region. + /// + /// + /// Padding is taken from the neighboring pixels in the source plane and is + /// limited by the source plane boundaries. The destination rectangle is + /// expanded by the same amount to preserve the relative pixel positions. + /// Returns if the destination does not have enough + /// space for the required left or top padding. + /// + /// + /// The source plane area + /// The source plane (read-only) + /// The maximum number of pixels to include around the source region. + /// The destination plane area. + /// The destination place (write-only) + /// + /// if the region was copied successfully; + /// otherwise, if the destination cannot accommodate + /// the required padding. + /// + public static bool CopyImageToWithPadding(Rectangle fromRect, JxlPlane from, int padding, Rectangle toRect, JxlPlane to) + where T : unmanaged + { + int xExtra0 = Math.Min(padding, fromRect.X); + int xExtra1 = Math.Min( + padding, + from.XSize - fromRect.X - fromRect.Width); + + int yExtra0 = Math.Min(padding, fromRect.Y); + int yExtra1 = Math.Min( + padding, + from.YSize - fromRect.Y - fromRect.Height); + + if (toRect.X < xExtra0 || toRect.Y < yExtra0) + { + return false; + } + + return CopyImageTo( + new Rectangle( + fromRect.X - xExtra0, + fromRect.Y - yExtra0, + fromRect.Width + xExtra0 + xExtra1, + fromRect.Height + yExtra0 + yExtra1), + from, + new Rectangle( + toRect.X - xExtra0, + toRect.Y - yExtra0, + toRect.Width + xExtra0 + xExtra1, + toRect.Height + yExtra0 + yExtra1), + to); + } + + /// + /// Performs linear combination of two grayscale images, and allocates & returns the + /// image with the linear combination. The returned image can later be disposed. + /// + /// The type of the plane. This type should be numeric as linear combination involves multiplication. + /// The configuration which includes a memory allocator for the return value. + /// The lambda for image 1. + /// The first image. + /// The lambda for image 2. + /// The second image. + /// A new image with linear combination, or null if it failed. + public static JxlPlane? LinComb(Configuration configuration, T lambda1, JxlPlane image1, T lambda2, JxlPlane image2) + where T : unmanaged, INumber + { + int xSize = image1.XSize; + int ySize = image1.YSize; + + if (xSize != image2.XSize || ySize != image2.YSize) + { + return null; + } + + JxlPlane result = JxlPlane.Create(configuration, xSize, ySize); + + for (int y = 0; y < ySize; y++) + { + Span row1 = image1.GetRow(y); + Span row2 = image2.GetRow(y); + Span rowOut = result.GetRow(y); + + for (int x = 0; x < xSize; x++) + { + rowOut[x] = (lambda1 * row1[x]) + (lambda2 * row2[x]); + } + } + + return result; + } + + /// + /// Multiplies all image values by the lambda in-place. + /// + /// The type of the plane. Should be numeric. + /// The lambda for multiplication. + /// The image to multiply. + public static void ScaleImage(T lambda, JxlPlane image) + where T : unmanaged, INumber + { + for (int y = 0; y < image.YSize; y++) + { + // TODO: SIMD + Span row = image.GetRow(y); + for (int x = 0; x < image.XSize; x++) + { + row[x] = lambda * row[x]; + } + } + } + + /// + /// Multiplies all image values within all planes by the lambda in-place. + /// + /// The type of the image. Should be numeric. + /// The lambda for multiplication. + /// The image to multiply. + public static void ScaleImage(T lambda, JxlImage3 image) + where T : unmanaged, INumber + { + for (int plane = 0; plane < 3; plane++) + { + ScaleImage(lambda, image.Plane(plane)); + } + } + + /// + /// Fills every value in the plane with . + /// + /// The type of the plane. + /// The value to fill everything with. + /// The plane to fill. + public static void FillImage(T value, JxlPlane image) + where T : unmanaged + { + for (int y = 0; y < image.YSize; y++) + { + Span row = image.GetRow(y); + row.Fill(value); + } + } + + /// + /// Sets every value in the plane to 0. See also . + /// + /// The type of the plane. + /// The image to clear. + public static void ZeroFillImage(JxlPlane image) + where T : unmanaged + { + if (image.XSize == 0) + { + return; + } + + for (int y = 0; y < image.YSize; y++) + { + image.GetRow(y).Clear(); + } + } + + /// + /// Core method for the WrapMirror function. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Mirror(long x, long xSize) + { + DebugGuard.MustBeGreaterThan(xSize, 0, nameof(xSize)); + + while (x < 0 || x >= xSize) + { + if (x < 0) + { + x = -x - 1; + } + else + { + x = (2 * xSize) - 1 - x; + } + } + + return (int)x; + } + + /// + /// Searches the entire plane to find the smallest and largest value. + /// + /// The type of the plane. + /// The plane where to search for the minimum & maximum values. + /// Lowest value found in the plane. + /// Largest value found in the plane. + public static void ImageMinMax(JxlPlane image, out T min, out T max) + where T : unmanaged, INumber, IMinMaxValue + { + min = T.MaxValue; // Start with opposite + max = T.MinValue; // Start with opposite + + for (int y = 0; y < image.YSize; y++) + { + Span row = image.GetRow(y); + + for (int x = 0; x < image.XSize; x++) + { + min = T.Min(min, row[x]); + max = T.Max(max, row[x]); + } + } + } + + /// + /// Within the bounds specified by the rectangle , sets every + /// value within the area to be . + /// + /// The type of the plane. + /// The value to fill the area with. + /// The input plane. + /// The area of the plane to fill everything with. + public static void FillPlane(T value, JxlPlane image, Rectangle rect) + where T : unmanaged + { + for (int y = 0; y < rect.Height; y++) + { + Span row = image.GetRow(rect, y); + row.Fill(value); + } + } + + /// + /// Clears every plane within the image. + /// + /// The type of the image + /// The image whose all planes will be cleared. + public static void ZeroFillImage(JxlImage3 image) + where T : unmanaged + { + for (int plane = 0; plane < 3; plane++) + { + ZeroFillImage(image.Plane(plane)); + } + } + + private static bool DownsampleImageCore(JxlPlane input, int factor, JxlPlane output) + { + if (factor == 1) + { + return false; + } + + if (!output.ShrinkTo(JxlMath.DivCeil(input.XSize, factor), JxlMath.DivCeil(input.YSize, factor))) + { + return false; + } + + int inStride = input.PixelsPerRow; + for (int y = 0; y < output.YSize; y++) + { + Span rowOut = output.GetRow(y); + Span rowIn = input.GetRow(factor * y); + for (int x = 0; x < output.XSize; x++) + { + int count = 0; + float sum = 0; + for (int iy = 0; iy < factor && iy + (factor * y) < input.YSize; iy++) + { + for (int ix = 0; ix < factor && ix + (factor * x) < input.XSize; ix++) + { + sum += rowIn[(iy * inStride) + (x * factor) + ix]; + count++; + } + } + + rowOut[x] = sum / count; + } + } + + return true; + } + + /// + /// Downsamples the image. The resulting image can later be disposed. + /// + /// Configuration which has a memory allocator which is used to allocate the result image. + /// The image to downsample. + /// Downsampling factor. + /// A new downsampled image. It can later be disposed. + public static JxlImageF? DownsampleImage(Configuration configuration, JxlImageF image, int factor) + { + JxlImageF downsampled = JxlImageF.Create( + configuration, + JxlMath.DivCeil(image.XSize, factor) + JxlFrameDimensions.BlockDimensions, + JxlMath.DivCeil(image.YSize, factor) + JxlFrameDimensions.BlockDimensions); + + if (!DownsampleImageCore(image, factor, downsampled)) + { + downsampled.Dispose(); + return null; + } + + return downsampled; + } + + /// + /// Downsamples all planes within the image. The resulting image can later be disposed. + /// + /// Configuration which has a memory allocator which is used to allocate the result image. + /// The image to downsample. + /// Downsampling factor. + /// A new downsampled image. It can later be disposed. + public static JxlImage3F? DownsampleImage(Configuration configuration, JxlImage3F opsin, int factor) + { + if (factor == 1) + { + return null; + } + + JxlImage3F downsampled = JxlImage3F.Create( + configuration, + JxlMath.DivCeil(opsin.XSize, factor) + JxlFrameDimensions.BlockDimensions, + JxlMath.DivCeil(opsin.YSize, factor) + JxlFrameDimensions.BlockDimensions); + + if (!downsampled.ShrinkTo( + downsampled.XSize - JxlFrameDimensions.BlockDimensions, + downsampled.YSize - JxlFrameDimensions.BlockDimensions)) + { + return null; + } + + for (int plane = 0; plane < 3; plane++) + { + if (!DownsampleImageCore(opsin.Plane(plane), factor, downsampled.Plane(plane))) + { + downsampled.Dispose(); + return null; + } + } + + return downsampled; + } + + public static bool PadImageToBlockMultipleInPlace(JxlImage3 input, int blockDimensions) + { + int xSizeOriginal = input.XSize; + int ySizeOriginal = input.YSize; + + int xSize = JxlMath.RoundUpTo(xSizeOriginal, blockDimensions); + int ySize = JxlMath.RoundUpTo(ySizeOriginal, blockDimensions); + + if (!input.ShrinkTo(xSize, ySize)) + { + return false; + } + + for (int plane = 0; plane < 3; plane++) + { + for (int y = 0; y < ySizeOriginal; y++) + { + Span row = input.PlaneRow(plane, y); + row[xSizeOriginal..].Fill(row[xSizeOriginal - 1]); + } + + Span sourceRow = input.PlaneRow(plane, ySizeOriginal - 1); + for (int y = ySizeOriginal; y < ySize; y++) + { + sourceRow.CopyTo(input.PlaneRow(plane, y)); + } + } + + return true; + } } From 048945c5d83ab2ccfd1369f13897e09e296fb439 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 8 Aug 2026 19:57:50 +0400 Subject: [PATCH 068/142] Reduce number of errors --- .../Formats/Jxl/Processing/Butteraugli/Butteraugli.cs | 4 ++-- .../Formats/Jxl/Processing/JxlColorCorrelationMap.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index 87ac5855a8..c0322e91c1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -2069,7 +2069,7 @@ public static bool ButteraugliDiffmapInPlace( } using JxlPlane blockDiffDc = JxlImageF.Create(configuration, xSize, ySize); - blockDiffDc.ZeroFill(); + blockDiffDc.Clear(); // LF/DC using (JxlImage3F lf0 = new(configuration, xSize, ySize)) @@ -2109,7 +2109,7 @@ public static bool ButteraugliDiffmapInPlace( } using JxlImageF blockDiffAc = new(configuration, xSize, ySize); - blockDiffAc.ZeroFill(); + blockDiffAc.Clear(); using (JxlImageF diffs = new(configuration, xSize, ySize)) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs index b2e2581f72..03dd8af454 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs @@ -28,8 +28,8 @@ public static JxlColorCorrelationMap Create(Configuration configuration, int wid map.YToXMap = new JxlImageSB(configuration, xBlocks, yBlocks); map.YToBMap = new JxlImageSB(configuration, xBlocks, yBlocks); - ZeroFillImage(map.YToXMap); - ZeroFillImage(map.YToBMap); + map.YToXMap.Clear(); + map.YToBMap.Clear(); if (!xyb) { From 4f71c526a0d0bba8291e38c60628e8bac9e3f7ed Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 8 Aug 2026 21:11:24 +0400 Subject: [PATCH 069/142] Implement JPEG XL numerics functions & reduce errors --- .../Formats/Jxl/Fields/JxlReadVisitor.cs | 7 +- .../Formats/Jxl/Fields/JxlU32Coder.cs | 3 +- .../Entropy/JxlAnsHybridUIntConfiguration.cs | 3 +- .../Jxl/Processing/Decoder/JxlAnsReader.cs | 2 +- .../Processing/Decoder/JxlHuffmanDecoder.cs | 4 +- .../Formats/Jxl/Processing/JxlAcStrategy.cs | 3 +- .../Jxl/Processing/JxlColorCorrelationMap.cs | 2 +- .../Jxl/Processing/JxlFrameDimensions.cs | 21 +- .../Jxl/Processing/JxlImageOperations.cs | 4 +- .../Formats/Jxl/Processing/JxlLehmerCode.cs | 2 +- .../Formats/Jxl/Processing/JxlMath.cs | 682 ++++++++++++++++++ .../Processing/Splines/JxlQuantizedSpline.cs | 2 +- 12 files changed, 707 insertions(+), 28 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs index 35be5d15e0..c60e5985ee 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Processing; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; @@ -56,13 +57,13 @@ public override bool BeginExtensions(ref ulong extensions) for (ulong remainingExtensions = extensions; remainingExtensions != 0; remainingExtensions &= remainingExtensions - 1) { - int idxExtension = Num0BitsBelowLS1BitNonzero(remainingExtensions); + ulong idxExtension = JxlMath.Num0BitsBelowLS1Bit_Nonzero(remainingExtensions); if (!this.U64(0, ref this.extensionBits[idxExtension])) { return false; } - if (!SafeAdd(this.totalExtensionBits, this.extensionBits[idxExtension], ref this.totalExtensionBits)) + if (!JxlMath.SafeAdd(this.totalExtensionBits, this.extensionBits[idxExtension], ref this.totalExtensionBits)) { DebugGuard.IsTrue(false, "Extension bits overflow; the codestream is not valid"); @@ -94,7 +95,7 @@ public override bool EndExtensions() long bitsRead = reader.TotalBitsConsumed; long end = 0; - if (!SafeAdd(this.posAfterExtSize, this.totalExtensionBits, ref end)) + if (!JxlMath.SafeAdd(this.posAfterExtSize, this.totalExtensionBits, ref end)) { DebugGuard.IsTrue(false, "Invalid extension size."); diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs index 8cd2fe7ea3..bdfb456673 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Processing; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; @@ -72,7 +73,7 @@ public static uint Read(in JxlU32Enc enc, JxlBitReader reader) /// public static bool ChooseSelector(in JxlU32Enc enc, uint value, ref uint selector, ref int totalBits) { - int bitsRequired = 32 - Num0BitsAboveMS1Bit(value); + int bitsRequired = 32 - JxlMath.Num0BitsAboveMS1Bit(value); if (bitsRequired > 32) { diff --git a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs index 6c2d8cc2ba..c1d6409076 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs @@ -2,6 +2,7 @@ // 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.Entropy; @@ -40,7 +41,7 @@ public void Encode(uint value, ref uint token, ref uint bitCount, ref uint bits) } else { - uint n = FloorLog2Nonzero(value); + uint n = JxlMath.FloorLog2Nonzero(value); uint m = value - (1u << (int)n); unchecked diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs index 51e014d80d..78707b7027 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs @@ -155,7 +155,7 @@ public static uint DecodeVariableLengthUint16(JxlBitReader reader) return counts; } - int upperBoundLog = FloorLog2Nonzero(JxlAnsConstants.AnsLogTableSize + 1); + int upperBoundLog = JxlMath.FloorLog2Nonzero(JxlAnsConstants.AnsLogTableSize + 1); int log = 0; for (; log < upperBoundLog; log++) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs index 991d9c6159..216d5e7795 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs @@ -141,10 +141,10 @@ public static bool ReadHuffmanCodeLengths(Span codeLengthCodeLengths, int /// Status of the operation public static bool ReadSimpleCode(int alphabetSize, JxlBitReader br, Span table) { - int maxBits = (alphabetSize > 1) ? FloorLog2Nonzero(alphabetSize - 1) + 1 : 0; + int maxBits = (alphabetSize > 1) ? JxlMath.FloorLog2Nonzero(alphabetSize - 1) + 1 : 0; uint symbolCount = br.ReadBits32(2u) + 1u; - Span symbols = stackalloc ushort[4]; + scoped Span symbols = stackalloc ushort[4]; symbols.Clear(); // Clearing is necessary. Not every value will be initialized. for (int i = 0; i < symbolCount; i++) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs index e7a518224c..c6ecc8f16f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs @@ -94,7 +94,6 @@ public JxlAcStrategy(int rawStrategy) private static void CoefficientOrderAndLookup(JxlAcStrategy strategy, bool isLookup, Span output) { // TODO: CoefficientLayout - // TODO: CeilLog2Nonzero int cx = strategy.CoveredBlocksX; int cy = strategy.CoveredBlocksY; @@ -102,7 +101,7 @@ private static void CoefficientOrderAndLookup(JxlAcStrategy strategy, bool isLoo int xs = cx / cy; int xsm = xs - 1; - int xss = CeilLog2Nonzero(xs); + int xss = JxlMath.CeilLog2Nonzero(xs); int cur = cx * cy; for (int i = 0; i < cx * BlockDimensions; i++) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs index 03dd8af454..9ce8e2e86a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelationMap.cs @@ -23,7 +23,7 @@ public static JxlColorCorrelationMap Create(Configuration configuration, int wid { JxlColorCorrelationMap map = new(); - (int xBlocks, int yBlocks) = (DivCeil(width, JxlChromaFromLuma.ColorTileDimension), DivCeil(height, JxlChromaFromLuma.ColorTileDimension)); + (int xBlocks, int yBlocks) = (JxlMath.DivCeil(width, JxlChromaFromLuma.ColorTileDimension), JxlMath.DivCeil(height, JxlChromaFromLuma.ColorTileDimension)); map.YToXMap = new JxlImageSB(configuration, xBlocks, yBlocks); map.YToBMap = new JxlImageSB(configuration, xBlocks, yBlocks); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlFrameDimensions.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlFrameDimensions.cs index 4ec265f523..70a3bff8bb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlFrameDimensions.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlFrameDimensions.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; - namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal sealed class JxlFrameDimensions @@ -18,10 +16,10 @@ public JxlFrameDimensions(int xSizePixel, int ySizePixel, int groupSizeShift, in 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.XSize = JxlMath.DivCeil(xSizePixel, upsampling); + this.YSize = JxlMath.DivCeil(ySizePixel, upsampling); + this.XSizeBlocks = JxlMath.DivCeil(this.XSize, BlockDimensions << maxHorizontalShift) << maxHorizontalShift; + this.YSizeBlocks = JxlMath.DivCeil(this.YSize, BlockDimensions << maxVerticalShift) << maxVerticalShift; this.XSizePadded = this.XSizeBlocks * BlockDimensions; this.YSizePadded = this.YSizeBlocks * BlockDimensions; @@ -33,10 +31,10 @@ public JxlFrameDimensions(int xSizePixel, int ySizePixel, int groupSizeShift, in 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.XSizeGroups = JxlMath.DivCeil(this.XSize, GroupDimensions); + this.YSizeGroups = JxlMath.DivCeil(this.YSize, GroupDimensions); + this.XSizeDcGroups = JxlMath.DivCeil(this.XSizeBlocks, GroupDimensions); + this.YSizeDcGroups = JxlMath.DivCeil(this.YSizeBlocks, GroupDimensions); this.NumGroups = this.XSizeGroups * this.YSizeGroups; this.NumDcGroups = this.XSizeDcGroups * this.YSizeDcGroups; } @@ -76,7 +74,4 @@ public JxlFrameDimensions(int xSizePixel, int ySizePixel, int groupSizeShift, in 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/Processing/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs index b6c8a3eb93..b356b9aab1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs @@ -464,7 +464,7 @@ private static bool DownsampleImageCore(JxlPlane input, int factor, JxlPl /// A new downsampled image. It can later be disposed. public static JxlImageF? DownsampleImage(Configuration configuration, JxlImageF image, int factor) { - JxlImageF downsampled = JxlImageF.Create( + JxlImageF downsampled = new( configuration, JxlMath.DivCeil(image.XSize, factor) + JxlFrameDimensions.BlockDimensions, JxlMath.DivCeil(image.YSize, factor) + JxlFrameDimensions.BlockDimensions); @@ -492,7 +492,7 @@ private static bool DownsampleImageCore(JxlPlane input, int factor, JxlPl return null; } - JxlImage3F downsampled = JxlImage3F.Create( + JxlImage3F downsampled = new( configuration, JxlMath.DivCeil(opsin.XSize, factor) + JxlFrameDimensions.BlockDimensions, JxlMath.DivCeil(opsin.YSize, factor) + JxlFrameDimensions.BlockDimensions); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs index 095e936d32..4afbcb34fd 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs @@ -52,7 +52,7 @@ public static bool DecodeLehmerCode(ReadOnlySpan code, Span temp, in return false; } - int log2n = CeilLog2Nonzero(n); + int log2n = JxlMath.CeilLog2Nonzero(n); int paddedN = 1 << log2n; for (int i = 0; i < paddedN; i++) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs new file mode 100644 index 0000000000..79eee30226 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs @@ -0,0 +1,682 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Common math functions used by the JPEG XL codec. +/// +internal static class JxlMath +{ + /// + /// Number of bits in a single byte. + /// + public const int BitsPerByte = 8; // This makes it more clear than just typing the number 8 + + /// + /// Default intensity target constant. + /// + public const float DefaultIntensityTarget = 255f; + + /// + /// Multiplier for conversion of log2(x) result to ln(x). The + /// value is derived by 1.0f / MathF.Log2(MathF.E). + /// + public const float InverseLog2E = 0.6931471805599453f; + + /// + /// Integer division by default is truncated toward zero. This + /// function performs division and ceiling without any floating-point + /// operations. + /// + /// Dividend + /// Divisor + /// + /// Result of the division with ceiling. It is equivalent to + /// MathF.Ceiling(a / b) without any floating-point usage. + /// + /// + /// This function only works for positive values. Division will be + /// incorrect for negative values. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int DivCeil(int a, int b) + { + unchecked + { + // This is a wrapper for Numerics.DivideCeil but for + // int input values. + return (int)Numerics.DivideCeil((uint)a, (uint)b); + } + } + + /// + /// Ensures that rounded to the multiple of . + /// + /// The value to round. + /// The allowed step size. + /// + /// , transformed to ensure that it stays within the step size of . + /// + /// + /// + /// If value=4, align=6, the result is 6. If value=7 and align=3 the result is 9. + /// + /// + /// This will not work correctly for negative values. + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int RoundUpTo(int value, int align) => DivCeil(value, align) * align; + + /// + /// Ensures that rounded to the multiple of . + /// + /// The value to round. + /// The allowed step size. + /// + /// , transformed to ensure that it stays within the step size of . + /// + /// + /// + /// If value=4, align=6, the result is 6. If value=7 and align=3 the result is 9. + /// + /// + /// This will not work correctly for negative values. + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint RoundUpTo(uint value, uint align) => Numerics.DivideCeil(value, align) * align; + + /// + /// Performs subtraction & returns a boolean indicating whether or not did the subtraction + /// result in an overflow. + /// + /// The minuend. + /// The subtrahend. + /// Subtracted value. + /// True if the subtraction led to an overflow. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool SubOverflow(uint a, uint b, out uint c) + { + c = a - b; + return (((a ^ b) & (a ^ c)) >> 31) != 0; + } + + /// + /// Performs subtraction & returns a boolean indicating whether or not did the subtraction + /// result in an overflow. + /// + /// The minuend. + /// The subtrahend. + /// Subtracted value. + /// True if the subtraction led to an overflow. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool SubOverflow(int a, int b, out int c) + { + c = 0; + unchecked + { + return SubOverflow((uint)a, (uint)b, out Unsafe.As(ref c)); + } + } + + /// + /// A slow, but safe multiplication method which returns false on an overflow. + /// + /// The multiplier + /// The multiplicand + /// The resulting value (or 0 on error) + /// + /// If the multiplication leads to an overflow returns false, + /// otherwise returns true and places the output in . + /// + /// + /// This method is meant for unsigned multiplication only. Negative values + /// won't work properly. + /// + public static bool SafeMultiply(int a, int b, out int product) + { + product = 0; + + if (a == 0 || b == 0) + { + return true; + } + + if (b > (int.MaxValue / a)) + { + return false; + } + + product = a * b; + + return true; + } + + /// + /// A slow, but safe multiplication method which returns false on an overflow. + /// + /// The multiplier + /// The multiplicand + /// The resulting value (or 0 on error) + /// + /// If the multiplication leads to an overflow returns false, + /// otherwise returns true and places the output in . + /// + /// + /// This method is meant for unsigned multiplication only. Negative values + /// won't work properly. + /// + public static bool SafeMultiply(uint a, uint b, out uint product) + { + product = 0; + + if (a == 0 || b == 0) + { + return true; + } + + if (b > (uint.MaxValue / a)) + { + return false; + } + + product = a * b; + + return true; + } + + /// + /// Performs addition and returns a boolean indicating whether the addition led to + /// an overflow. + /// + /// The augend + /// The addend + /// The resulting sum of addition. + /// A boolean indicating whether the addition led to an overflow. + /// + /// This method is meant for unsigned addition only. Negative values + /// won't work properly. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool SafeAdd(int a, int b, out int sum) + { + unchecked + { + sum = a + b; + return sum >= a; + } + } + + /// + /// Performs addition and returns a boolean indicating whether the addition led to + /// an overflow. + /// + /// The augend + /// The addend + /// The resulting sum of addition. + /// A boolean indicating whether the addition led to an overflow. + /// + /// This method is meant for unsigned addition only. Negative values + /// won't work properly. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool SafeAdd(long a, long b, out long sum) + { + unchecked + { + sum = a + b; + return sum >= a; + } + } + + /// + /// Performs addition and returns a boolean indicating whether the addition led to + /// an overflow. + /// + /// The augend + /// The addend + /// The resulting sum of addition. + /// A boolean indicating whether the addition led to an overflow. + /// + /// This method is meant for unsigned addition only. Negative values + /// won't work properly. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool SafeAdd(uint a, uint b, out uint sum) + { + unchecked + { + sum = a + b; + return sum >= a; + } + } + + /// + /// Performs addition and returns a boolean indicating whether the addition led to + /// an overflow. + /// + /// The augend + /// The addend + /// The resulting sum of addition. + /// A boolean indicating whether the addition led to an overflow. + /// + /// This method is meant for unsigned addition only. Negative values + /// won't work properly. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool SafeAdd(ulong a, ulong b, out ulong sum) + { + unchecked + { + sum = a + b; + return sum >= a; + } + } + + /// + /// Ensures that the value stays within the specified range. + /// + /// The input value + /// The lower bound + /// The upper bound + /// + /// if value is lower than . + /// if value is higher than . Otherwise, if it's within + /// the range. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Clamp1(int value, int low, int high) => Numerics.Clamp(value, low, high); + + /// + /// Rounds the dimensions up to block dimensions. + /// + /// The dimensions to round up to block dimensions. + /// The rounded dimensions. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int RoundUpToBlockDimensions(int dim) + { + unchecked + { + return (dim + 7) & ~7; + } + } + + /// + /// Rounds the dimensions up to block dimensions. + /// + /// The dimensions to round up to block dimensions. + /// The rounded dimensions. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint RoundUpToBlockDimensions(uint dim) + { + unchecked + { + return (dim + 7u) & ~7u; + } + } + + /// + /// Rounds the specified number of bits to a multiple of bytes. + /// + /// The input bits. + /// Bits rounded to the multiples of bytes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int RoundUpBitsToByteMultiple(int bits) + { + unchecked + { + return (bits + 7) & ~7; + } + } + + /// + /// Rounds the specified number of bits to a multiple of bytes. + /// + /// The input bits. + /// Bits rounded to the multiples of bytes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint RoundUpBitsToByteMultiple(uint bits) + { + unchecked + { + return (bits + 7u) & ~7u; + } + } + + /// + /// Multiplies by π. + /// + /// The value to multiply. + /// + /// multiplier * π + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Pi(float multiplier) => multiplier * MathF.PI; + + /// + /// Multiplies by π. + /// + /// The value to multiply. + /// + /// multiplier * π + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Pi(double multiplier) => multiplier * Math.PI; + + /// + /// Returns the amount of zero bits before the highest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits before the highest 1 bit. This is the equivalent + /// of leading zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Num0BitsAboveMS1Bit_Nonzero(uint x) + { + DebugGuard.MustBeGreaterThan(x, 0u, nameof(x)); + + return BitOperations.LeadingZeroCount(x); + } + + /// + /// Returns the amount of zero bits before the highest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits before the highest 1 bit. This is the equivalent + /// of leading zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Num0BitsAboveMS1Bit_Nonzero(int x) + { + unchecked + { + return Num0BitsAboveMS1Bit_Nonzero((uint)x); + } + } + + /// + /// Returns the amount of zero bits before the highest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits before the highest 1 bit. This is the equivalent + /// of leading zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Num0BitsAboveMS1Bit_Nonzero(ulong x) + { + DebugGuard.MustBeGreaterThan(x, 0uL, nameof(x)); + + return BitOperations.LeadingZeroCount(x); + } + + /// + /// Returns the amount of zero bits before the highest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits before the highest 1 bit. This is the equivalent + /// of leading zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Num0BitsAboveMS1Bit_Nonzero(long x) + { + unchecked + { + return Num0BitsAboveMS1Bit_Nonzero((ulong)x); + } + } + + /// + /// Returns the number of 0 bits to the right of the lowest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits after the lowest 1 bit. This is the equivalent + /// of trailing zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Num0BitsBelowLS1Bit_Nonzero(uint x) + { + DebugGuard.MustBeGreaterThan(x, 0u, nameof(x)); + + return (uint)BitOperations.TrailingZeroCount(x); + } + + /// + /// Returns the number of 0 bits to the right of the lowest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits after the lowest 1 bit. This is the equivalent + /// of trailing zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Num0BitsBelowLS1Bit_Nonzero(ulong x) + { + DebugGuard.MustBeGreaterThan(x, 0uL, nameof(x)); + + return (uint)BitOperations.TrailingZeroCount(x); + } + + /// + /// Returns the number of 0 bits to the right of the lowest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits after the lowest 1 bit. This is the equivalent + /// of trailing zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Num0BitsBelowLS1Bit_Nonzero(int x) => Num0BitsBelowLS1Bit_Nonzero((uint)x); + + /// + /// Returns the number of 0 bits to the right of the lowest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits after the lowest 1 bit. This is the equivalent + /// of trailing zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Num0BitsBelowLS1Bit_Nonzero(long x) => Num0BitsBelowLS1Bit_Nonzero((ulong)x); + + /// + /// Returns the number of 0 bits to the right of the lowest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits after the lowest 1 bit. This is the equivalent + /// of trailing zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Num0BitsBelowLS1Bit(uint x) => x == 0 ? 32u : Num0BitsBelowLS1Bit_Nonzero(x); + + /// + /// Returns the number of 0 bits to the right of the lowest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits after the lowest 1 bit. This is the equivalent + /// of trailing zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Num0BitsBelowLS1Bit(int x) => Num0BitsBelowLS1Bit_Nonzero((uint)x); + + /// + /// Returns the number of 0 bits to the right of the lowest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits after the lowest 1 bit. This is the equivalent + /// of trailing zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Num0BitsBelowLS1Bit(ulong x) => x == 0 ? 64u : Num0BitsBelowLS1Bit_Nonzero(x); + + /// + /// Returns the number of 0 bits to the right of the lowest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits after the lowest 1 bit. This is the equivalent + /// of trailing zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Num0BitsBelowLS1Bit(long x) => Num0BitsBelowLS1Bit_Nonzero((ulong)x); + + /// + /// Returns the amount of zero bits before the highest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits before the highest 1 bit. This is the equivalent + /// of leading zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Num0BitsAboveMS1Bit(int x) => x == 0 ? sizeof(int) * 8 : Num0BitsAboveMS1Bit_Nonzero(x); + + /// + /// Returns the amount of zero bits before the highest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits before the highest 1 bit. This is the equivalent + /// of leading zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Num0BitsAboveMS1Bit(uint x) => x == 0 ? sizeof(uint) * 8u : unchecked((uint)Num0BitsAboveMS1Bit_Nonzero((int)x)); + + /// + /// Returns the amount of zero bits before the highest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits before the highest 1 bit. This is the equivalent + /// of leading zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long Num0BitsAboveMS1Bit(long x) => x == 0 ? sizeof(long) * 8L : Num0BitsAboveMS1Bit_Nonzero(x); + + /// + /// Returns the amount of zero bits before the highest 1 bit. + /// + /// The input value. + /// + /// Number of zero bits before the highest 1 bit. This is the equivalent + /// of leading zero count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Num0BitsAboveMS1Bit(ulong x) => x == 0 ? sizeof(ulong) * 8uL : unchecked((ulong)Num0BitsAboveMS1Bit_Nonzero((long)x)); + + /// + /// Integer equivalent of MathF.Floor(MathF.Log2(x)). + /// + /// The input value. + /// Floor(Log2(x)) in integer form. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint FloorLog2Nonzero(uint x) => (uint)(((sizeof(uint) * 8) - 1) ^ Num0BitsAboveMS1Bit_Nonzero(x)); + + /// + /// Integer equivalent of MathF.Floor(MathF.Log2(x)). + /// + /// The input value. + /// Floor(Log2(x)) in integer form. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int FloorLog2Nonzero(int x) => ((sizeof(int) * 8) - 1) ^ Num0BitsAboveMS1Bit_Nonzero(x); + + /// + /// Integer equivalent of MathF.Floor(MathF.Log2(x)). + /// + /// The input value. + /// Floor(Log2(x)) in integer form. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long FloorLog2Nonzero(long x) => ((sizeof(long) * 8) - 1) ^ Num0BitsAboveMS1Bit_Nonzero(x); + + /// + /// Integer equivalent of MathF.Floor(MathF.Log2(x)). + /// + /// The input value. + /// Floor(Log2(x)) in integer form. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong FloorLog2Nonzero(ulong x) => (ulong)(((sizeof(ulong) * 8) - 1) ^ Num0BitsAboveMS1Bit_Nonzero(x)); + + /// + /// Integer equivalent of MathF.Ceiling(MathF.Log2(x)). + /// + /// The input value. + /// Ceil(Log2(x)) in integer form. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CeilLog2Nonzero(int x) + { + int floorLog2 = FloorLog2Nonzero(x); + + if ((x & (x - 1)) == 0) + { + return floorLog2; + } + + return floorLog2 + 1; + } + + /// + /// Integer equivalent of MathF.Ceiling(MathF.Log2(x)). + /// + /// The input value. + /// Ceil(Log2(x)) in integer form. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint CeilLog2Nonzero(uint x) + { + uint floorLog2 = FloorLog2Nonzero(x); + + if ((x & (x - 1)) == 0) + { + return floorLog2; + } + + return floorLog2 + 1; + } + + /// + /// Integer equivalent of MathF.Ceiling(MathF.Log2(x)). + /// + /// The input value. + /// Ceil(Log2(x)) in integer form. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long CeilLog2Nonzero(long x) + { + long floorLog2 = FloorLog2Nonzero(x); + + if ((x & (x - 1)) == 0) + { + return floorLog2; + } + + return floorLog2 + 1; + } + + /// + /// Integer equivalent of MathF.Ceiling(MathF.Log2(x)). + /// + /// The input value. + /// Ceil(Log2(x)) in integer form. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong CeilLog2Nonzero(ulong x) + { + ulong floorLog2 = FloorLog2Nonzero(x); + + if ((x & (x - 1)) == 0) + { + return floorLog2; + } + + return floorLog2 + 1; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs index 76b3e24b4a..ed5729067c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs @@ -216,7 +216,7 @@ public bool Dequantize( 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)); + long logColor = Math.Max(1L, JxlMath.CeilLog2Nonzero(1L + maxColor)); float weightLimit = MathF.Ceiling(MathF.Sqrt((float)areaLimit / logColor) / MathF.Max(1, manhattanDistance)); for (int i = 0; i < 32; i++) From 4d0a46863902cd3968214bd30de37e0631947c88 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 8 Aug 2026 21:22:58 +0400 Subject: [PATCH 070/142] Add rectangle support to images This massively reduces number of syntax errors --- src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs | 7 +++++++ src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs index 9e9b10c099..91aed2e97a 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs @@ -46,6 +46,13 @@ public Span PlaneRow(int plane, int row) return rowSpan; } + public Span PlaneRow(Rectangle rectangle, int c, int y) + { + DebugGuard.MustBeGreaterThanOrEqualTo(y + rectangle.Top, 0, nameof(y)); + + return this.PlaneRow(c, y + rectangle.Top)[rectangle.Left..]; + } + public JxlPlane Plane(int index) => this.planes[index]; public void Swap(JxlImage3 other) diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs index 7326768f09..e61381b194 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs @@ -36,6 +36,17 @@ public static JxlPlane Create(Configuration configuration, int xSize, int ySi public Span GetRow(int y) => this.GetRowBase(y); + public Span GetRow(Rectangle rectangle, int y) + { + DebugGuard.MustBeGreaterThanOrEqualTo(y + rectangle.Top, 0, nameof(y)); + + return this.GetRow(y + rectangle.Top)[rectangle.Left..]; + } + + public bool IsRectangleInside(Rectangle rectangle) => rectangle.Contains(this.GetRectangle()); + + public Rectangle GetRectangle() => new(0, 0, this.XSize, this.YSize); + /// /// Fills everything in this image with 0. /// From 2c517a2fbcf2864c58ed36c33b22c5cda40bd7a5 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 8 Aug 2026 21:36:39 +0400 Subject: [PATCH 071/142] Document JxlPlane and JxlPlaneBase --- .../Formats/Jxl/Memory/JxlPlaneBase.cs | 75 ++++++++++++++++++- .../Formats/Jxl/Memory/JxlPlane{T}.cs | 45 ++++++++++- 2 files changed, 116 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs index 367b3fc300..1b63eb43f2 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs @@ -7,11 +7,22 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Memory; -// NOTE: Do not seal this type. +/// +/// Base class for a single-plane image. +/// internal class JxlPlaneBase : IDisposable { + /// + /// Underlying bytes + /// private IMemoryOwner? bytes; + /// + /// Initializes a new instance of the class. + /// + /// Plane width + /// Plane height + /// The size of each pixel in bytes. public JxlPlaneBase(int xSize, int ySize, int sizeOfT) { this.XSize = xSize; @@ -22,17 +33,32 @@ public JxlPlaneBase(int xSize, int ySize, int sizeOfT) this.Size = sizeOfT; } + /// + /// Initializes a new instance of the class with empty values. + /// public JxlPlaneBase() : this(0, 0, 0) { } + /// + /// Gets the number of bytes per row. + /// public int BytesPerRow { get; private set; } + /// + /// Gets the width of the image. + /// public int XSize { get; private set; } + /// + /// Gets the height of the image. + /// public int YSize { get; private set; } + /// + /// Gets the underlying bytes of this image as a Memory<T>. + /// public Memory Bytes => #if DEBUG this.bytes?.Memory ?? throw new InvalidOperationException("Bytes are missing"); @@ -40,14 +66,31 @@ public JxlPlaneBase() return this.bytes!.Memory; #endif + /// + /// Gets the underlying bytes of this image as a Span<T>. + /// public Span BytesSpan => this.Bytes.Span; protected int Size { get; set; } + /// + /// Gets or sets the width that was initially assigned. For example, if the image gets shrinked, + /// the XSize YSize properties get changed while this property will stay same. + /// protected int OriginalXSize { get; set; } + /// + /// Gets or sets the height that was initially assigned. For example, if the image gets shrinked, + /// the XSize YSize properties get changed while this property will stay same. + /// protected int OriginalYSize { get; set; } + /// + /// Allocates the underlying memory for the plane. + /// + /// The configuration which has a memory allocator used to allocate memory. + /// Padding + /// Status of allocation. public bool Allocate(Configuration configuration, int prePadding) { if (this.bytes != null || this.BytesPerRow != 0) @@ -67,6 +110,21 @@ public bool Allocate(Configuration configuration, int prePadding) return true; } + /// + /// Shrinks the image so its width is equal to and its height is + /// equal to . + /// + /// The output width + /// The output height + /// Status of the shrinking operation. + /// + /// + /// This method can only shrink memory. It cannot expand it. + /// + /// + /// When shrinking, the underlying memory does not get resized. + /// + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool ShrinkTo(int x, int y) { @@ -84,6 +142,12 @@ public bool ShrinkTo(int x, int y) return true; } + /// + /// Base function to return the span for a specified row as a generic <T>. + /// + /// The type of the row. + /// The index of the row to get the span for. + /// A span which covers the row memory. protected Span GetRowBase(int y) where T : unmanaged { @@ -93,8 +157,10 @@ protected Span GetRowBase(int y) return MemoryMarshal.Cast(row); } - protected void SetBytes(IMemoryOwner bytes) => this.bytes = bytes; - + /// + /// Swaps properties & data of this image with the specified image. + /// + /// The other image to swap with. public void Swap(JxlPlaneBase other) { (this.XSize, other.XSize) = (other.XSize, this.XSize); @@ -105,6 +171,9 @@ public void Swap(JxlPlaneBase other) (this.bytes, other.bytes) = (other.bytes, this.bytes); } + /// + /// Releases all underlying memory used by this plane. + /// public void Dispose() { this.bytes?.Dispose(); diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs index e61381b194..2d96ab636f 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs @@ -5,21 +5,44 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Memory; -// NOTE: Do not seal this class. +/// +/// A generic version of a 2D single-plane JPEG XL image. +/// +/// The type of each pixel. internal class JxlPlane : JxlPlaneBase where T : unmanaged { + /// + /// Initializes a new instance of the class. + /// public JxlPlane() { } + /// + /// Initializes a new instance of the class with the specified width and height. + /// + /// Plane width. + /// Plane height public unsafe JxlPlane(int width, int height) : base(width, height, sizeof(T)) { } + /// + /// Gets the number of pixels per row. + /// public unsafe int PixelsPerRow => this.BytesPerRow / sizeof(T); + /// + /// Allocates a new plane. + /// + /// The configuration which contains a memory allocator. + /// Plane width + /// Plane height + /// Padding + /// A new allocated plane + /// Thrown when allocation fails. public static JxlPlane Create(Configuration configuration, int xSize, int ySize, int prePadding = 0) { JxlPlane plane = new(xSize, ySize); @@ -34,8 +57,19 @@ public static JxlPlane Create(Configuration configuration, int xSize, int ySi return plane; } + /// + /// Returns a span for the specified row. + /// + /// The row index. + /// A span which covers memory for the specified row. public Span GetRow(int y) => this.GetRowBase(y); + /// + /// Returns a span for the specified row within the specified rectangle bounds. + /// + /// The bounds. + /// The row index. + /// A span which covers memory for the specified row with the rectangle offsets. public Span GetRow(Rectangle rectangle, int y) { DebugGuard.MustBeGreaterThanOrEqualTo(y + rectangle.Top, 0, nameof(y)); @@ -43,8 +77,17 @@ public Span GetRow(Rectangle rectangle, int y) return this.GetRow(y + rectangle.Top)[rectangle.Left..]; } + /// + /// Checks if the specified rectangle is within the bounds image. + /// + /// The input rectangle. + /// Boolean indicating whether the rectangle is inside. public bool IsRectangleInside(Rectangle rectangle) => rectangle.Contains(this.GetRectangle()); + /// + /// Returns the rectangle for this image bounds. + /// + /// A rectangle with x,y=0,0 width,height=XSize,YSize. public Rectangle GetRectangle() => new(0, 0, this.XSize, this.YSize); /// From 50813c1cbbde2cb901d941ff90b5bc76395e85eb Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 8 Aug 2026 22:12:01 +0400 Subject: [PATCH 072/142] Add passes decoder state prototype & reduce errors in JxlQuantizedSpline --- .../Decoder/JxlGroupDecoderCache.cs | 12 +++ .../Decoder/JxlPassesDecoderState.cs | 77 +++++++++++++++++++ .../Processing/Splines/JxlQuantizedSpline.cs | 7 +- 3 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlGroupDecoderCache.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlGroupDecoderCache.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlGroupDecoderCache.cs new file mode 100644 index 0000000000..f82e332d8d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlGroupDecoderCache.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Cache for the JPEG XL Group Decoder. +/// +internal sealed class JxlGroupDecoderCache +{ + +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs new file mode 100644 index 0000000000..5b534ef773 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs @@ -0,0 +1,77 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal sealed class JxlPassesDecoderState +{ + public JxlPassesDecoderState(JxlFrameHeader header, Configuration configuration) + { + this.XDmMultiplier = MathF.Pow(1 / 1.25f, header.XQmScale - 2f); + this.BDmMultiplier = MathF.Pow(1 / 1.25f, header.BQmScale - 2f); + + this.MainOutput.Callback = PixelCallback; + this.MainOutput.Buffer = null; + + this.UndoOrientation = JxlOrientation.Identity; + this.Upsampler8x = GetUpsamplingImage(configuration, this.Shared.CodecMetadata.CustomTransformData, 0, 3); + + if (header.LoopFilter?.EpfIterations > 0) + { + this.Sigma = new JxlImageF( + configuration, + (this.Shared.FrameDimensions.XSizeBlocks + 2) * SigmaPadding, + (this.Shared.FrameDimensions.YSizeBlocks + 2) * SigmaPadding); + } + + this.SharedStorage = new(configuration, header, false); + this.Shared = this.SharedStorage; + } + + public JxlPassesSharedState SharedStorage { get; set; } + + public JxlPassesSharedState Shared { get; set; } + + public JxlRenderPipelineStage[] Upsampler8x { get; set; } = []; + + public List Code { get; set; } = []; + + public List> ContextMap { get; set; } = []; + + public float XDmMultiplier { get; set; } + + public float BDmMultiplier { get; set; } + + public JxlImageF? Sigma { get; set; } + + public int Width { get; set; } + + public int Height { get; set; } + + public JxlImageOutput MainOutput { get; set; } + + public List ExtraOutput { get; set; } = []; + + public bool FastXybSRgb8Conversion { get; set; } + + public bool UnpremultiplyAlpha { get; set; } + + public JxlOrientation UndoOrientation { get; set; } + + public int VisibleFrameIndex { get; set; } + + public int NonvisibleFrameIndex { get; set; } + + public int UsedAcs { get; set; } + + public JxlDctAcImage Coefficients { get; set; } = []; + + public JxlRenderPipeline RenderPipeline { get; set; } + + public JxlImageBundle FrameStorageForReferencing { get; set; } + + public JxlOutputEncodingInfo OutputEncodingInfo { get; set; } = new(); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs index ed5729067c..40dff40e2b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; @@ -269,8 +270,8 @@ public bool Decode( { ref JxlControlPoint controlPoint = ref controlPoints[i]; - controlPoint.First = UnpackSigned(decoder.ReadHybridUnsignedInteger(ControlPointsContext, br, contextMap)); - controlPoint.Second = UnpackSigned(decoder.ReadHybridUnsignedInteger(ControlPointsContext, br, contextMap)); + controlPoint.First = JxlPackSigned.UnpackSigned(decoder.ReadHybridUnsignedInteger(ControlPointsContext, br, contextMap)); + controlPoint.Second = JxlPackSigned.UnpackSigned(decoder.ReadHybridUnsignedInteger(ControlPointsContext, br, contextMap)); if (controlPoint.First >= deltaLimit || controlPoint.First <= -deltaLimit || controlPoint.Second >= deltaLimit || controlPoint.Second <= -deltaLimit) @@ -300,7 +301,7 @@ bool TryDecodeDct(ReadOnlySpan contextMap, Span dct) for (int i = 0; i < 32; i++) { - dct[i] = UnpackSigned(decoder.ReadHybridUnsignedInteger(DctContext, br, contextMap)); + dct[i] = JxlPackSigned.UnpackSigned(decoder.ReadHybridUnsignedInteger(DctContext, br, contextMap)); if (dct[i] == invalidCoefficient) { throw new InvalidOperationException("The DCT coefficient is invalid"); From 27a848cd256ed439752b92d1ff3ed7280e034d34 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:06:41 +0400 Subject: [PATCH 073/142] Add DCT, transpose, and performance improvements - Add a slice & assert to JxlHuffmanDecoder alphabetSize to allocate at most 256 items - Use [0, 0] instead of stackalloc[2] followed by Clear() in JxlAnsReader - Add assert to Butteraugli ComputeKernel method & use float for Butteraugli Wmul & use InlineArray - Add transpose. - Note: transpose is scalar, it doesn't support SIMD yet - Add shared constants & file signature - Improve while loop in JxlImageOperations.Mirror - Floating-point Discrete Cosine Transform (1D and 2D) - Add an inline array of 2 items Source files implemented from libjxl with this commit: - dct-inl.h - dct_block-inl.h - transpose-inl.h --- src/ImageSharp/Formats/Jxl/InlineArrays.cs | 9 + .../Jxl/Processing/Butteraugli/Butteraugli.cs | 20 +- .../Jxl/Processing/Decoder/JxlAnsReader.cs | 3 +- .../Processing/Decoder/JxlHuffmanDecoder.cs | 5 +- .../Formats/Jxl/Processing/JxlDct.cs | 305 ++++++++++++++++++ .../Formats/Jxl/Processing/JxlDctOutput.cs | 52 +++ .../Formats/Jxl/Processing/JxlDctScales.cs | 27 ++ .../Formats/Jxl/Processing/JxlDctSource.cs | 55 ++++ .../Jxl/Processing/JxlImageOperations.cs | 8 +- .../Formats/Jxl/Processing/JxlShared.cs | 30 ++ .../Formats/Jxl/Processing/JxlTranspose.cs | 22 ++ 11 files changed, 522 insertions(+), 14 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlDct.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlDctOutput.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlDctSource.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlShared.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs diff --git a/src/ImageSharp/Formats/Jxl/InlineArrays.cs b/src/ImageSharp/Formats/Jxl/InlineArrays.cs index f006d2fd09..6276051700 100644 --- a/src/ImageSharp/Formats/Jxl/InlineArrays.cs +++ b/src/ImageSharp/Formats/Jxl/InlineArrays.cs @@ -7,6 +7,15 @@ namespace SixLabors.ImageSharp.Formats.Jxl; +/// +/// Used by Butteraugli +/// +[InlineArray(2)] +internal struct InlineArray2 +{ + private T first; +} + [InlineArray(3)] internal struct InlineArray3 { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index c0322e91c1..73420e86d7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -42,11 +42,11 @@ internal static class Butteraugli private const float GlobalScale = 1.0f / InternalGoodQualityThreshold; - public static ReadOnlySpan Wmul => + public static ReadOnlySpan Wmul => [ - 400.0, 1.50815703118, 0, - 2150.0, 10.6195433239, 16.2176043152, - 29.2353797994, 0.844626970982, 0.703646627719, + 400.0f, 1.50815703118f, 0f, + 2150.0f, 10.6195433239f, 16.2176043152f, + 29.2353797994f, 0.844626970982f, 0.703646627719f, ]; public static ReadOnlySpan ComputeKernel(float sigma) @@ -55,7 +55,11 @@ public static ReadOnlySpan ComputeKernel(float sigma) float scaler = -1.0f / (2.0f * sigma * sigma); int diff = Math.Max(1, (int)(m * MathF.Abs(sigma))); - // Use new because there's only up to 3 elements + // If sigma is very large we should not return a 'new float[]' allocation. + // This guard is temporary so we can verify the range of the number of elements. + // TODO: remove guard if the value doesn't exceed the limit for many JXL files + DebugGuard.MustBeLessThanOrEqualTo(sigma, 32f, nameof(sigma)); + float[] kernel = new float[(2 * diff) + 1]; for (int i = -diff; i <= diff; i++) @@ -536,7 +540,7 @@ public static bool SeparateMfAndHf( Configuration configuration, in ButteraugliParameters parameters, JxlImage3F mf, - JxlImageF[] hf, + ref InlineArray2 hf, BlurTemp blurTemp) { const float sigmaHf = 3.22489901262f; @@ -2095,8 +2099,8 @@ public static bool ButteraugliDiffmapInPlace( } } - JxlImageF[] hf0 = new JxlImageF[2]; - JxlImageF[] hf1 = new JxlImageF[2]; + InlineArray2 hf0 = default; + InlineArray2 hf1 = default; if (!SeparateMfAndHf(parameters, image0, hf0, blurTemp)) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs index 78707b7027..180dbd3c1c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlAnsReader.cs @@ -103,8 +103,7 @@ public static uint DecodeVariableLengthUint16(JxlBitReader reader) if (isSimpleCode) { - Span symbols = stackalloc uint[2]; - symbols.Clear(); + Span symbols = [0, 0]; uint maxSymbol = 0u; uint symCount = reader.ReadBits32(1u) + 1u; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs index 216d5e7795..7201a511aa 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlHuffmanDecoder.cs @@ -269,8 +269,9 @@ public bool ReadFromBitStream(int alphabetSize, JxlBitReader br) return ReadSimpleCode(alphabetSize, br, this.Table); } - // The alphabet size is at most 256 - Span codeLengths = stackalloc byte[alphabetSize]; + DebugGuard.MustBeLessThanOrEqualTo(alphabetSize, 256, nameof(alphabetSize)); + + Span codeLengths = stackalloc byte[256].Slice(0, alphabetSize); codeLengths.Clear(); // Zero-initialized in reference software Span codeLengthCodeLengths = stackalloc byte[CodeLengthCodes]; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDct.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDct.cs new file mode 100644 index 0000000000..5e3e76355f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDct.cs @@ -0,0 +1,305 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Discrete Cosine Transform with SIMD support. +/// +internal static class JxlDct +{ + /// + /// Creates a new coefficient bundle. + /// + /// Number of items. + /// Coefficient size. + /// A new coefficient bundle. + public static CoefficientBundle CoeffBundle(int n, int sz) => new(n, sz); + + public static void Dct1DCore(int n, int sz, Span mem, Span tmp) + { + if (n == 2) + { + Vector in1 = new(mem); + Vector in2 = new(mem[sz..]); + (in1 + in2).CopyTo(mem); + (in1 - in2).CopyTo(mem[sz..]); + } + else + { + CoefficientBundle cb = CoeffBundle(n / 2, sz); + + cb.AddReverse(mem, mem[(n / 2 * sz)..], tmp); + Dct1DCore(n / 2, sz, tmp, tmp[(n * sz)..]); + + cb.SubReverse(mem, mem[(n / 2 * sz)..], tmp[(n / 2 * sz)..]); + cb.Multiply(tmp); + + Dct1DCore(n / 2, sz, tmp[(n / 2 * sz)..], tmp[(n * sz)..]); + cb.B(tmp[(n / 2 * sz)..]); + + CoeffBundle(n, sz).InverseEvenOdd(tmp, mem); + } + } + + public static void InverseDct1DCore(int n, int sz, Span from, int fromStride, Span to, int toStride, Span tmp) + { + if (n == 1) + { + from.CopyTo(to); + } + else if (n == 2) + { + Vector in1 = new(from); + Vector in2 = new(from[fromStride..]); + (in1 + in2).CopyTo(to); + (in1 + in2).CopyTo(to[toStride..]); + } + else + { + CoefficientBundle cbDiv2 = CoeffBundle(n / 2, sz); + CoefficientBundle cb = CoeffBundle(n, sz); + + cb.ForwardEvenOdd(from, fromStride, tmp); + InverseDct1DCore(n / 2, sz, tmp, sz, tmp, sz, tmp[(n * sz)..]); + + cbDiv2.BTranspose(tmp[((n / 2) * sz)..]); + InverseDct1DCore(n / 2, sz, tmp[((n / 2) * sz)..], sz, tmp[((n / 2) * sz)..], sz, tmp[(n * sz)..]); + + cb.MultiplyAndAdd(tmp, to, toStride); + } + } + + public static void Dct1DWrapper(int n, int m, bool fit, JxlDctSource from, JxlDctOutput to, int mp, Span tmp) + { + CoefficientBundle cb = CoeffBundle(n, m); + + for (int i = 0; i < mp; i += m) + { + cb.LoadFromBlock(from, i, tmp); + Dct1DCore(n, m, tmp, tmp[(n * m)..]); + cb.StoreToBlockAndScale(tmp, ref to, i); + + if (fit) + { + return; + } + } + } + + public static void InverseDct1DWrapper(int n, int m, bool fit, JxlDctSource from, JxlDctOutput to, int mp, Span tmp) + { + for (int i = 0; i < mp; i += m) + { + InverseDct1DCore(n, m, from.Address(0, i), from.Stride, to.Address(0, i), to.Stride, tmp); + + if (fit) + { + return; + } + } + } + + public static void Dct1DCapped(int n, int m, int l, JxlDctSource from, JxlDctOutput to, Span tmp) + { + bool fit = m <= l; + Dct1DWrapper(n, m, fit, from, to, m, tmp); + } + + public static void InverseDct1DCapped(int n, int m, int l, JxlDctSource from, JxlDctOutput to, Span tmp) + { + bool fit = m <= l; + InverseDct1DWrapper(n, m, fit, from, to, m, tmp); + } + + public static void Dct1D(int n, int m, JxlDctSource from, JxlDctOutput to, Span tmp) + { + int lanes = Vector.Count; + Dct1DCapped(n, m, lanes, from, to, tmp); + } + + public static void InverseDct1D(int n, int m, JxlDctSource source, JxlDctOutput output, Span tmp) + { + int lanes = Vector.Count; + InverseDct1DCapped(n, m, lanes, source, output, tmp); + } + + public static void ComputeScaledDct(int rows, int columns, JxlDctSource from, Span to, Span scratchSpace) + { + Span block = scratchSpace; + Span tmp = scratchSpace[(rows * columns)..]; + + if (rows < columns) + { + Dct1D(rows, columns, from, new JxlDctOutput(block, columns), tmp); + JxlTranspose.Transpose(rows, columns, new JxlDctSource(block, columns), new JxlDctOutput(to, rows)); + Dct1D(columns, rows, new JxlDctSource(to, rows), new JxlDctOutput(block, rows), tmp); + JxlTranspose.Transpose(columns, rows, new JxlDctSource(block, rows), new JxlDctOutput(to, columns)); + } + else + { + Dct1D(rows, columns, from, new JxlDctOutput(to, columns), tmp); + JxlTranspose.Transpose(rows, columns, new JxlDctSource(to, columns), new JxlDctOutput(block, rows)); + Dct1D(columns, rows, new JxlDctSource(block, rows), new JxlDctOutput(to, rows), tmp); + } + } + + public static void ComputeScaledInverseDct(int rows, int columns, Span from, JxlDctOutput to, Span scratchSpace) + { + Span block = scratchSpace; + Span tmp = scratchSpace[(rows * columns)..]; + + if (rows < columns) + { + JxlTranspose.Transpose(rows, columns, new JxlDctSource(from, columns), new JxlDctOutput(block, rows)); + InverseDct1D(columns, rows, new JxlDctSource(block, rows), new JxlDctOutput(from, rows), tmp); + JxlTranspose.Transpose(columns, rows, new JxlDctSource(from, rows), new JxlDctOutput(block, columns)); + InverseDct1D(rows, columns, new JxlDctSource(block, columns), to, tmp); + } + else + { + InverseDct1D(columns, rows, new JxlDctSource(from, rows), new JxlDctOutput(block, rows), tmp); + JxlTranspose.Transpose(columns, rows, new JxlDctSource(block, rows), new JxlDctOutput(from, columns)); + InverseDct1D(rows, columns, new JxlDctSource(from, columns), to, tmp); + } + } + + /// + /// Core methods for the Discrete Cosine Transform (DCT). + /// + public readonly struct CoefficientBundle(int n, int sz) + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddReverse(Span aIn1, Span aIn2, Span aOut) + { + for (int i = 0; i < n; i++) + { + Vector in1 = new(aIn1[(i * sz)..]); + Vector in2 = new(aIn2[((n - i - 1) * sz)..]); + (in1 + in2).CopyTo(aOut[(i * sz)..]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SubReverse(Span aIn1, Span aIn2, Span aOut) + { + for (int i = 0; i < n; i++) + { + Vector in1 = new(aIn1[(i * sz)..]); + Vector in2 = new(aIn2[((n - i - 1) * sz)..]); + (in1 - in2).CopyTo(aOut[(i * sz)..]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void B(Span coeff) + { + Vector sqrt2 = new(JxlDctScales.Sqrt2); + Vector in10 = new(coeff); + Vector in20 = new(coeff[sz..]); + ((in10 * sqrt2) + in20).CopyTo(coeff); + + for (int i = 1; i + 1 < n; i++) + { + Vector in1 = new(coeff[(i * sz)..]); + Vector in2 = new(coeff[((i + 1) * sz)..]); + (in1 + in2).CopyTo(coeff[(i * sz)..]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void BTranspose(Span coeff) + { + for (int i = n - 1; i > 0; i--) + { + Vector in1 = new(coeff[(i * sz)..]); + Vector in2 = new(coeff[((i - 1) * sz)..]); + (in1 + in2).CopyTo(coeff[(i * sz)..]); + } + + Vector sqrt2 = new(JxlDctScales.Sqrt2); + Vector in1x = new(coeff); + (in1x * sqrt2).CopyTo(coeff); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void InverseEvenOdd(Span aIn, Span aOut) + { + for (int i = 0; i < n / 2; i++) + { + new Vector(aIn[(i * sz)..]).CopyTo(aOut[((2 * i) * sz)..]); + } + + for (int i = n / 2; i < n; i++) + { + new Vector(aIn[(i * sz)..]).CopyTo(aOut[(((2 * (i - (n / 2))) + 1) * sz)..]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ForwardEvenOdd(Span aIn, int aInStride, Span aOut) + { + for (int i = 0; i < n / 2; i++) + { + new Vector(aIn[(2 * i * aInStride)..]).CopyTo(aOut[(i * sz)..]); + } + + for (int i = n / 2; i < n; i++) + { + new Vector(aIn[(((2 * (i - (n / 2))) + 1) * aInStride)..]).CopyTo(aOut[(i * sz)..]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Multiply(Span coeff) + { + ReadOnlySpan multipliers = JxlDctScales.GetMultipliers(n); + + for (int i = 0; i < n / 2; i++) + { + Vector in1 = new(coeff[(((n / 2) + i) * sz)..]); + Vector mul = new(multipliers[i]); + (in1 * mul).CopyTo(coeff[((n / (2 + i)) * sz)..]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void MultiplyAndAdd(Span coeff, Span output, int outStride) + { + ReadOnlySpan multipliers = JxlDctScales.GetMultipliers(n); + + for (int i = 0; i < n / 2; i++) + { + Vector mul = new(multipliers[i]); + Vector in1 = new(coeff[(i * sz)..]); + Vector in2 = new(coeff[((n / (2 + i)) * sz)..]); + Vector out1 = (mul * in2) * in1; + Vector out2 = -(mul * in2) + in1; + out1.CopyTo(output[(i * outStride)..]); + out2.CopyTo(output[((n - i - 1) * outStride)..]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void LoadFromBlock(in JxlDctSource input, int offset, Span coeff) + { + for (int i = 0; i < n; i++) + { + input.LoadPart(i, offset).CopyTo(coeff[(i * sz)..]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void StoreToBlockAndScale(Span coeff, ref JxlDctOutput output, int offset) + { + Vector mul = new(1.0f / n); + for (int i = 0; i < n; i++) + { + output.StorePart(mul * new Vector(coeff[(i * sz)..]), i, offset); + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctOutput.cs new file mode 100644 index 0000000000..1be3b9adfe --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctOutput.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Output DCT block. +/// +internal ref struct JxlDctOutput(Span data, int stride) +{ + /// + /// Raw block data. + /// + public Span Data = data; + + /// + /// Stride size. + /// + public readonly int Stride = stride; + + /// + /// Returns the span to the start of a row and offset. + /// + /// The row index. + /// The offset. + /// + /// Span for that row & offset. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Span Address(int row, int i) => this.Data[((row * this.Stride) + i)..]; + + /// + /// Writes a single value to the block at the row and offset. + /// + /// The value to write. + /// The row index. + /// The offset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(float value, int row, int i) => this.Data[(row * this.Stride) + i] = value; + + /// + /// Stores the vector into the data at the specified row and offset. + /// + /// The vector to write. + /// The row index. + /// The offset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void StorePart(Vector value, int row, int index) => value.CopyTo(this.Address(row, index)); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs index 26bc7ee8e3..0ebcb7a1fe 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs @@ -9,6 +9,16 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// internal static class JxlDctScales { + /// + /// Square root of 2. + /// + public const float Sqrt2 = 1.41421356237f; + + /// + /// Square root of 0.5. + /// + public const float Sqrt05 = 0.70710678118f; + /// /// Gets 8x1 DCT resample scales. /// @@ -358,4 +368,21 @@ internal static class JxlDctScales 9.058751453879703f, 11.644627325175037f, 16.300023088031555f, 27.163977662448232f, 81.48784219222516f, ]; + + /// + /// Returns DCT multipliers for size . + /// + /// The multiplier size + /// DCT multipliers for the given size. + public static ReadOnlySpan GetMultipliers(int n) => n switch + { + 4 => Multipliers4, + 8 => Multipliers8, + 16 => Multipliers16, + 32 => Multipliers32, + 64 => Multipliers64, + 128 => Multipliers128, + 256 => Multipliers256, + _ => [] + }; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctSource.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctSource.cs new file mode 100644 index 0000000000..08dafc11e7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctSource.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Source DCT block. +/// +internal readonly ref struct JxlDctSource(Span data, int stride) +{ + /// + /// Raw block data. + /// + public readonly Span Data = data; + + /// + /// Stride size. + /// + public readonly int Stride = stride; + + /// + /// Returns the span to the start of a row and offset. + /// + /// The row index. + /// The offset. + /// + /// Span for that row & offset. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span Address(int row, int i) => this.Data[((row * this.Stride) + i)..]; + + /// + /// Returns the coefficient at the row and offset. + /// + /// The row index. + /// The offset. + /// + /// Coefficient at that row and offset. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Read(int row, int i) => this.Data[(row * this.Stride) + i]; + + /// + /// Loads a vector at the specified row and offset. + /// + /// The row index. + /// The offset. + /// + /// Vector at that row and offset. + /// + public Vector LoadPart(int row, int i) => new(this.Address(row, i)); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs index b356b9aab1..e5ddf834f7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs @@ -346,16 +346,20 @@ private static int Mirror(long x, long xSize) { DebugGuard.MustBeGreaterThan(xSize, 0, nameof(xSize)); - while (x < 0 || x >= xSize) + while (true) { if (x < 0) { x = -x - 1; } - else + else if (x >= xSize) { x = (2 * xSize) - 1 - x; } + else + { + break; + } } return (int)x; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlShared.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlShared.cs new file mode 100644 index 0000000000..711dbc27ea --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlShared.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Shared JPEG XL constants +/// +internal static class JxlShared +{ + /// + /// Maximum number of passes in an image. + /// + public const int MaximumNumberOfPasses = 11; + + /// + /// Maximum number of reference frames. + /// + public const int MaximumNumberOfReferenceFrames = 4; + + /// + /// Gets the 12-byte signature (a.k.a. magic) for JPEG XL files. + /// + public static ReadOnlySpan SignatureBox => + [ + 0x00, 0x00, 0x00, 0x0C, + (byte)'J', (byte)'X', (byte)'L', (byte)' ', + 0x0D, 0x0A, 0x87, 0x0A + ]; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs new file mode 100644 index 0000000000..4b05150a77 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Performs transpose on JPEG XL DCT blocks. +/// +internal static class JxlTranspose +{ + // TODO: SIMD + public static void Transpose(int r, int c, JxlDctSource from, JxlDctOutput to) + { + for (int n = 0; n < r; n++) + { + for (int m = 0; m < c; m++) + { + to.Write(from.Read(n, m), m, n); + } + } + } +} From 0d9aea4f127a0f599fe1d3d74638373c543f2d06 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:08:39 +0400 Subject: [PATCH 074/142] Remove GC.SuppressFinalize(this) --- .../Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs index 40dff40e2b..7e46f49b05 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs @@ -38,7 +38,6 @@ public void Dispose() { this.memoryOwner?.Dispose(); this.ControlPoints = Memory.Empty; - GC.SuppressFinalize(this); } public void ReserveControlPoints(Configuration configuration, int n) From 20e3a3452a4add118c29c79f368164cbcacdcdfb Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:35:38 +0400 Subject: [PATCH 075/142] Add image bundle --- .../Formats/Jxl/Processing/JxlImageBundle.cs | 506 ++++++++++++++++++ 1 file changed, 506 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs new file mode 100644 index 0000000000..48f84175b3 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs @@ -0,0 +1,506 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// An image bundle. +/// +internal sealed class JxlImageBundle +{ + /// + /// Image data for additional channels. + /// + private List extraChannels = []; + + /// + /// Initializes a new instance of the class. + /// + public JxlImageBundle() + { + } + + /// + /// Initializes a new instance of the class with the specified image metadata. + /// + /// Initial image metadata. + public JxlImageBundle(JxlImageMetadata? metadata) => this.Metadata = metadata; + + /// + /// Gets or sets the optional name of the bundle. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the blend mode. Default is Blend. + /// + public JxlBlendMode BlendMode { get; set; } = JxlBlendMode.Blend; + + /// + /// Gets or sets a value indicating whether blending should be done. (Default: false) + /// + public bool Blend { get; set; } + + /// + /// Gets or sets a value indicating whether this a reference frame. + /// + public bool UseForNextFrame { get; set; } + + /// + /// Gets or sets the duration for animation. + /// + public uint Duration { get; set; } + + /// + /// Gets or sets the timecode for animation. + /// + public uint Timecode { get; set; } + + /// + /// Gets or sets the frame origin. + /// + public Point Origin { get; set; } + + /// + /// Gets or sets the chroma subsampling for Y'Cb'Cr images. + /// + public JxlYCbCrChromaSubsampling? ChromaSubsampling { get; set; } + + /// + /// Gets or sets the color transform mode for this image, the default is None. + /// + public JxlColorTransform ColorTransform { get; set; } = JxlColorTransform.None; + + /// + /// Gets or sets the JPEG data if the input image was converted to JPEG XL from a JPEG. + /// + public JxlJpegData? JpegData { get; set; } + + /// + /// Gets a value indicating whether returns the image does or will represent quantized DCT-8 coefficients + /// stored in the 8x8 pixel regions. + /// + public bool IsJpeg => this.JpegData is not null; + + /// + /// Gets or sets the number of bytes that were actually read. + /// + public long DecodedBytes { get; set; } + + /// + /// Gets a value indicating whether the black extra channel is present. + /// + public bool ContainsBlack => this.Metadata?.FindExtraChannel(JxlExtraChannel.Black) is not null; + + /// + /// Gets a value indicating whether the alpha extra channel is present. + /// + public bool ContainsAlpha => this.Metadata?.FindExtraChannel(JxlExtraChannel.Alpha) is not null; + + /// + /// Gets a value indicating whether the alpha channel is premultiplied. + /// + public bool IsAlphaPremultiplied => this.Metadata?.FindExtraChannel(JxlExtraChannel.Alpha)?.AlphaAssociated == true; + + /// + /// Gets a value indicating whether the color encoding specifies Gray. + /// + public bool IsGray => this.CurrentColorEncoding?.IsGray == true; + + /// + /// Gets a value indicating whether the color encoding specifies sRGB. + /// + public bool IsSrgb => this.CurrentColorEncoding?.IsSrgb == true; + + /// + /// Gets a value indicating whether the color encoding specifies linear sRGB. + /// + public bool IsLinearSrgb => this.CurrentColorEncoding?.IsLinearSrgb == true; + + /// + /// Gets the current color encoding for this image. + /// + public JxlColorEncoding? CurrentColorEncoding { get; private set; } + + /// + /// Gets the image metadata for this image bundle. + /// + public JxlImageMetadata? Metadata { get; } + + /// + /// Gets the color data. + /// + public JxlImage3F? Color { get; private set; } + + /// + /// Gets a value indicating whether the color data is present and usable. + /// + public bool HasColor => this.Color?.XSize != 0; + + /// + /// Gets the width. + /// + public int XSize + { + get + { + if (this.IsJpeg) + { + return this.JpegData!.Width; + } + + if (this.Color?.XSize != 0) + { + return this.Color!.XSize; + } + + return this.extraChannels?.Count > 0 ? 0 : this.extraChannels![0].XSize; + } + } + + /// + /// Gets the height. + /// + public int YSize + { + get + { + if (this.IsJpeg) + { + return this.JpegData!.Height; + } + + if (this.Color?.YSize != 0) + { + return this.Color!.YSize; + } + + return this.extraChannels?.Count > 0 ? 0 : this.extraChannels![0].YSize; + } + } + + /// + /// Gets the black extra channel. + /// + public JxlImageF? Black + { + get + { + if (!this.ContainsBlack || this.Metadata is null) + { + return null; + } + + int ec = this.Metadata!.FindExtraChannel(JxlExtraChannel.Black) - this.Metadata.ExtraChannelInfo.Data; + return this.extraChannels[ec]; + } + } + + /// + /// Gets the alpha extra channel. + /// + public JxlImageF? Alpha + { + get + { + if (!this.ContainsAlpha || this.Metadata is null) + { + return null; + } + + int ec = this.Metadata!.FindExtraChannel(JxlExtraChannel.Alpha) - this.Metadata.ExtraChannelInfo.Data; + return this.extraChannels[ec]; + } + } + + /// + /// Gets the oriented X size. + /// + public int OrientedXSize => this.Metadata?.Orientation > 4 ? this.YSize : this.XSize; + + /// + /// Gets the oriented Y size. + /// + public int OrientedYSize => this.Metadata?.Orientation > 4 ? this.XSize : this.YSize; + + /// + /// Gets the real bit depth. + /// + public uint RealBitDepth => this.Metadata!.BitDepth!.BitsPerSample; + + /// + /// Returns false if the width or height is 0 and any extra channel + /// does not match this image bundle's width or height; returns true if + /// the sizes are otherwise correct. + /// + /// + /// True if the sizes are correct. False if they aren't. + /// + public bool VerifySizes() + { + if (this.ContainsExtraChannels()) + { + int xs = this.XSize; + int ys = this.YSize; + + if (xs == 0 || ys == 0) + { + return false; + } + + foreach (JxlImageF ec in this.extraChannels) + { + if (ec.XSize != xs || ec.YSize != ys) + { + return false; + } + } + } + + return true; + } + + /// + /// Overrides the color encoding for this image bundle. + /// + /// The new color encoding. + public void OverrideProfile(JxlColorEncoding encoding) => this.CurrentColorEncoding = encoding; + + /// + /// If the color data is present, assigns it to and returns true, + /// otherwise returns false and assigns null. + /// + /// Output color data. + /// True if color isn't null. + public bool TryGetColor(out JxlImage3F? color) + { + color = null; + if (this.Color is not null) + { + color = this.Color; + } + + return color is not null; + } + + /// + /// Removes the color data, replacing it with a new Image3F with 0 as width and height. + /// + public void RemoveColor() => this.Color = new JxlImage3F(); + + /// + /// Removes all extra channels, if any. + /// + public void ClearExtraChannels() => this.extraChannels.Clear(); + + /// + /// Returns true if there is at least 1 extra channel. + /// + /// Boolean indicating if extra channels are present. + public bool ContainsExtraChannels() => this.extraChannels.Count > 0; + + /// + /// Returns an enumerable for extra channels. + /// + /// Extra channels enumerable. + public IEnumerable EnumerateExtraChannels() => this.extraChannels; + + /// + /// Sets the extra channels. + /// + /// The extra channels. + /// + /// True if each plane had width and height greater than 0 and sizes are correct + /// after changing the extra channels; false otherwise. + /// + public bool TrySetExtraChannels(List extraChannels) + { + foreach (JxlImageF plane in extraChannels) + { + if (plane.XSize == 0 || plane.YSize == 0) + { + return false; + } + } + + this.extraChannels = extraChannels; + + return this.VerifySizes(); + } + + /// + /// Attempts to set the alpha channel. + /// + /// The alpha channel to set. + /// True if it was set successfully; false otherwise. + /// Thrown if the corresponding extra channel has incorrect info. + public bool TrySetAlpha(JxlImageF alpha) + { + if (this.Metadata is null) + { + return false; + } + + JxlExtraChannelInfo? eci = this.Metadata!.FindExtraChannel(JxlExtraChannel.Alpha); + + if (eci is null) + { + return false; + } + + if (alpha.XSize == 0 || alpha.YSize == 0) + { + return false; + } + + int eciIndex = this.Metadata.ExtraChannelInfo.Data; + + if (eciIndex != this.extraChannels.Count) + { + throw new InvalidOperationException("The SetAlpha parameter is incorrect"); + } + + this.extraChannels.Add(alpha); + + return this.VerifySizes(); + } + + /// + /// Ensures that the metadata of this image is valid. + /// + /// True if metadata is correct. False if it isn't. + /// Rare. + public bool VerifyMetadata() + { + if (this.CurrentColorEncoding?.Icc?.IsEmpty == true) + { + return false; + } + + if (this.Metadata?.ColorEncoding?.IsGray != this.IsGray) + { + return false; + } + + if (this.Metadata?.HasAlpha == true) + { + JxlImageF? img = this.Alpha; + if (img?.XSize == 0) + { + throw new InvalidOperationException("Alpha should not have width equal to 0"); + } + } + + int alphaBits = this.Metadata?.AlphaBits ?? 0; + + if (alphaBits > 32) + { + return false; + } + + return true; + } + + /// + /// Updates the bundle from the sepcified image. + /// + /// Color data. + /// Current color encoding. + /// True if setting the image succeeded; otherwise false. + public bool SetFromImage(JxlImage3F color, JxlColorEncoding current) + { + if (color.XSize == 0 || color.YSize == 0) + { + return false; + } + + if (this.Metadata?.ColorEncoding?.IsGray == this.IsGray) + { + return false; + } + + this.Color = color; + this.CurrentColorEncoding = current; + + return this.VerifySizes(); + } + + /// + /// Shrinks this image and all of its extra channels to the specified + /// width and height. + /// + /// The desired width. + /// The desired height. + /// + /// If this bundle color data or any of the extra channels + /// happens to have a smaller width or height than the specified + /// width or height, that is considered expanding, which will immediately + /// return false. If this method returns true, all colors and + /// extra channels have successfully been shrunk. + /// + public bool ShrinkTo(int width, int height) + { + if (this.HasColor) + { + if (this.Color?.ShrinkTo(width, height) != true) + { + return false; + } + } + + foreach (JxlImageF extraChannel in this.extraChannels) + { + if (!extraChannel.ShrinkTo(width, height)) + { + return false; + } + } + + return true; + } + + /// + /// Copies this image bundle to a new bundle. + /// + /// + /// A configuration with a memory allocator. + /// + /// + /// A new copy of this image bundle. + /// + /// + /// Thrown if some extra channels cannot be copied. + /// + public JxlImageBundle Copy(Configuration configuration) + { + JxlImageBundle copy = new(this.Metadata); + + if (this.Color is not null) + { + copy.Color = new JxlImage3F(configuration, this.Color.XSize, this.Color.YSize); + } + + copy.CurrentColorEncoding = this.CurrentColorEncoding; + copy.JpegData = this.JpegData; + copy.ColorTransform = this.ColorTransform; + copy.ChromaSubsampling = this.ChromaSubsampling; + + foreach (JxlImageF plane in this.extraChannels) + { + JxlImageF ec = new(configuration, plane.XSize, plane.YSize); + if (!JxlImageOperations.CopyImage(plane, ec)) + { + throw new InvalidOperationException("Cannot copy extra channel"); + } + + copy.extraChannels.Add(ec); + } + + return copy; + } +} From 6e432ca50bc411fa9c896300080cf3fced437b34 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:45:39 +0400 Subject: [PATCH 076/142] Use inline arrays in Butteraugli --- .../Formats/Jxl/Processing/Butteraugli/Butteraugli.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index 73420e86d7..2011c186b3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -726,7 +726,7 @@ public static bool SeparateHFAndUHF( return true; } - public static void DeallocateHFAndUHF(JxlImageF[] hf, JxlImageF[] uhf) + public static void DeallocateHFAndUHF(InlineArray2 hf, InlineArray2 uhf) { for (int i = 0; i < 2; i++) { @@ -1424,8 +1424,8 @@ public static bool MaltaDiffMapLf( } public static void CombineChannelsForMasking( - JxlImageF[] hf, - JxlImageF[] uhf, + InlineArray2 hf, + InlineArray2 uhf, JxlImageF output) { // Only X and Y components are involved in masking. @@ -2157,8 +2157,8 @@ public static bool ButteraugliDiffmapInPlace( image0.Dispose(); image1.Dispose(); - JxlImageF[] uhf0 = new JxlImageF[2]; - JxlImageF[] uhf1 = new JxlImageF[2]; + InlineArray2 uhf0 = default; + InlineArray2 uhf1 = default; if (!SeparateHFAndUHF(parameters, hf0, uhf0, blurTemp)) { From 8b4ff41b9ae24206032e158866eb041f02f81afe Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 15 Aug 2026 15:46:33 +0400 Subject: [PATCH 077/142] First prototype of JxlDecoderCore - Implemented decode.cc - Added prototype of JXL image info - Made JxlBitReader use ReadOnlyMemory - Made changes to ICC codecs, changing accessibility of a few methods from private to internal and exposing IccDataReader's index - Added JxlMemoryWriter as a MemoryAllocator alternative to MemoryStream --- .../Formats/Jxl/Fields/JxlBundle.cs | 2 +- .../Formats/Jxl/IO/JxlMemoryWriter.cs | 82 + src/ImageSharp/Formats/Jxl/JxlImageInfo.cs | 22 + .../Jxl/Processing/Decoder/JxlBitReader.cs | 14 +- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 2938 ++++++++++++++++- .../Jxl/Processing/JxlBoxCodingMode.cs | 20 + .../Profiles/ICC/DataReader/IccDataReader.cs | 7 + .../Metadata/Profiles/ICC/IccReader.cs | 4 +- 8 files changed, 2988 insertions(+), 101 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/IO/JxlMemoryWriter.cs create mode 100644 src/ImageSharp/Formats/Jxl/JxlImageInfo.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlBoxCodingMode.cs diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs index e6e673f9c8..7202932539 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Fields; diff --git a/src/ImageSharp/Formats/Jxl/IO/JxlMemoryWriter.cs b/src/ImageSharp/Formats/Jxl/IO/JxlMemoryWriter.cs new file mode 100644 index 0000000000..77058802b3 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/JxlMemoryWriter.cs @@ -0,0 +1,82 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +/// +/// A disposable writer for bytes in memory. It is highly similar to +/// , but its buffer relies on +/// . +/// +internal sealed class JxlMemoryWriter(MemoryAllocator allocator) : IDisposable +{ + /// + /// The initial capacity in bytes. + /// + private const int InitialCapacity = 1024; + + /// + /// Core buffer. + /// + private IMemoryOwner buffer = allocator.Allocate(InitialCapacity); + + /// + /// Gets the length of the written data in bytes. + /// + public int Length { get; private set; } + + /// + /// Gets the capacity of the buffer in bytes. + /// + public int Capacity => this.buffer.Memory.Length; + + /// + /// Releases the underlying buffer. + /// + public void Dispose() => this.buffer.Dispose(); + + /// + /// Writes the specified bytes into the writer. + /// + /// The bytes to write. + public void Write(ReadOnlySpan bytes) + { + int requiredCapacity = checked(this.Length + bytes.Length); + this.EnsureCapacity(requiredCapacity); + + bytes.CopyTo(this.buffer.Memory.Span[this.Length..]); + this.Length = requiredCapacity; + } + + /// + /// Returns a span containing the bytes written to the writer. + /// + /// A span containing the written bytes. + public Span AsSpan() => this.buffer.Memory.Span[..this.Length]; + + /// + /// Returns memory containing the bytes written to the writer. + /// + /// Memory containing the written bytes. + public Memory AsMemory() => this.buffer.Memory[..this.Length]; + + private void EnsureCapacity(int requiredCapacity) + { + if (requiredCapacity <= this.Capacity) + { + return; + } + + int newCapacity = Math.Max(requiredCapacity, checked(this.Capacity * 2)); + + IMemoryOwner previousBuffer = this.buffer; + this.buffer = allocator.Allocate(newCapacity); + + previousBuffer.Memory.Span[..this.Length].CopyTo(this.buffer.Memory.Span); + + previousBuffer.Dispose(); + } +} diff --git a/src/ImageSharp/Formats/Jxl/JxlImageInfo.cs b/src/ImageSharp/Formats/Jxl/JxlImageInfo.cs new file mode 100644 index 0000000000..47f5e4e954 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/JxlImageInfo.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Metadata; + +namespace SixLabors.ImageSharp.Formats.Jxl; + +/// +/// Image information specific to the JPEG XL format. +/// +public class JxlImageInfo : ImageInfo +{ + /// + /// Initializes a new instance of the class. + /// + /// Image size + /// Image metadata + public JxlImageInfo(Size size, ImageMetadata metadata) + : base(size, metadata) + { + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs index cdc69838df..9dbc090aec 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs @@ -8,8 +8,10 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; /// /// Represents a bitstream reader. /// -internal sealed class JxlBitReader(ReadOnlyMemory bytes) +internal ref struct JxlBitReader(ReadOnlySpan bytes) { + private readonly ReadOnlySpan data = bytes; + private ulong buffer; private uint bufferRemainingBits; private int pointer; @@ -22,16 +24,14 @@ internal sealed class JxlBitReader(ReadOnlyMemory bytes) /// /// Gets the total number of bits consumed. /// - public long TotalBitsConsumed => ((long)this.pointer * 8) + (64 - this.bufferRemainingBits); + public readonly 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; + int remaining = this.data.Length - this.pointer; if (remaining <= 0) { // we don't have any more data... mark an end of stream @@ -43,7 +43,7 @@ private void RefillCore() if (remaining >= 8) { - this.buffer = BinaryPrimitives.ReadUInt64LittleEndian(samplesSpan[this.pointer..]); + this.buffer = BinaryPrimitives.ReadUInt64LittleEndian(this.data[this.pointer..]); this.bufferRemainingBits = 64u; this.pointer += 8; } @@ -52,7 +52,7 @@ private void RefillCore() ulong value = 0; for (int i = 0; i < remaining; i++) { - value |= (ulong)samplesSpan[this.pointer + i] << (8 * i); + value |= (ulong)this.data[this.pointer + i] << (8 * i); } this.buffer = value; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index fbb7a22d02..e984b36b93 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -1,155 +1,2911 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using System.Buffers.Binary; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; -internal sealed class JxlDecoderCore : ImageDecoderCore +/// +/// Internal decoder for JPEG XL. +/// +internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable { + private const long NumBuffersLimit = 1 << 20; + + /// + /// Current stage of the decoding pipeline. + /// + private JxlDecoderStage decoderStage; + + /// + /// Status of whether or not has the signature been parsed. + /// + private bool gotSignature; + + /// + /// Did we parse the final code stream? + /// + private bool lastCodestreamSeen; + + /// + /// Did we parse the signature of the code stream? + /// + private bool gotCodestreamSignature; + + /// + /// Did we parse basic JXL information? + /// + private bool gotBasicInfo; + + /// + /// Did we parse the transform data? + /// + private bool gotTransformData; + + /// + /// Did we parse all the codestream metadata headers? + /// + private bool gotAllHeaders; + + /// + /// Are we decoding pixels now? + /// + private bool postHeaders; + + /// + /// ICC profile for JPEG XL metadata, if present. + /// + private IccProfile? iccProfile; + + /// + /// The frame index box, if present. + /// + private JxlDecoderFrameIndexBox? frameIndexBox; + + /// + /// Did we get the preview image, or determined we cannot get it or there isn't any? + /// + private bool gotPreviewImage; + + /// + /// Is this a preview frame? + /// + private bool previewFrame; + + private long filePosition; + + /// + /// Offset where box contents start. + /// + private long boxContentsBegin; + + /// + /// Offset where box contents end. + /// + private long boxContentsEnd; + + /// + /// boxContentsEnd - boxContentsBegin + /// + private long boxContentsSize; + + /// + /// Total size of the box in bytes. + /// + private long boxSize; + + /// + /// Size of the headers in bytes. + /// + private long headerSize; + + /// + /// Are box contents unbounded? + /// + private bool boxContentsUnbounded; + + /// + /// Type of the box currently being decoded. + /// + private JxlBoxType boxType; + + /// + /// Underlying type for brob boxes. + /// + private JxlBoxType boxDecodedType; + + private bool boxEvent; + + /// + /// Should box contents be decompressed (using Brotli)? + /// + private bool decompressBoxes; + + /// + /// Should the output buffer for the box be set? + /// + private bool boxOutBufferSet; + + /// + /// Should the output buffer for the current box be set? + /// + private bool boxOutBufferSetCurrentBox; + + /// + /// Output buffer for the current box. + /// + private IMemoryOwner? boxOutputBuffer; + + /// + /// Size of the output buffer. + /// + private long boxOutBufferSize; + + /// + /// Offset of start of box output buffer. + /// + private long boxOutBufferBegin; + + /// + /// Current offset of start of box output buffer. + /// + private long boxOutBufferPos; + + /// + /// Should orientation be preserved? + /// + private bool keepOrientation; + + /// + /// Should alpha channel be unpremultiplied? + /// + private bool unpremultiplyAlpha; + + private bool renderSpotcolors; + + private bool coalescing; + + /// + /// Custom intensity target. + /// + private float desiredIntensityTarget; + + private int eventsWanted; + + private int originalEventsWanted; + + private long basicInfoSizeHint; + + /// + /// Is container format present? + /// + private bool haveContainer; + + /// + /// Total number of boxes. + /// + private long boxCount; + + /// + /// The level of progressive detail in frame coding. + /// + private JxlProgressiveDetail progressiveDetail = JxlProgressiveDetail.Dc; + + /// + /// Progressive detail of current frame. + /// + private JxlProgressiveDetail frameProgressiveDetail; + + /// + /// The intended downsampling ratio for the current progression step. + /// + private long downsamplingTarget; + + /// + /// True if the image output buffer or callback was set. + /// + private bool imageOutBufferSet; + + /// + /// Size of the image output buffer. + /// + private long imageOutputSize; + + /// + /// Output data for extra channels. + /// + private List extraChannelOutputs = []; + + /// + /// Codec metadata if present. + /// + private JxlCodecMetadata? metadata; + + /// + /// Image metadata if present. + /// + private JxlImageMetadata? imageMetadata; + + /// + /// The image bundle. + /// + private JxlImageBundle? imageBundle; + + /// + /// State for passes decoder. + /// + private JxlPassesDecoderState? passesState; + + /// + /// State for frame decoder. + /// + private JxlFrameDecoder? frameDecoder; + + /// + /// The next section. + /// + private long nextSection; + + private List sectionProcessed = []; + + /// + /// The frame header, if present. + /// + private JxlFrameHeader? frameHeader; + + /// + /// Remaining frame size. + /// + private long remainingFrameSize; + + /// + /// Stage of the decoding pipeline. + /// + private JxlFrameStage frameStage; + + /// + /// Has progression for DC frames been completed? + /// + private bool dcFrameProgressionDone; + + private bool isLastOfStill; + + /// + /// Is the currently processed frame the last of the codestream? + /// + private bool isLastTotal; + + /// + /// How many frames should be skipped? + /// + private int skipFrames; + + /// + /// Is active frame being skipped? + /// + private bool skippingFrame; + + private int internalFrames; + + private int externalFrames; + + /// + /// All frame reference.s + /// + private List frameReferences = []; + + private List frameExternalToInternal = []; + + private List frameRequired = []; + + /// + /// Codestream input data is temporarily copied here. + /// + private JxlMemoryWriter? codestreamCopy; + + private long codestreamUnconsumed; + + /// + /// Position in the codestreamCopy vector. + /// + private long codestreamPos; + + /// + /// Number of remaining bits in the codestream copy. + /// + private long codestreamBitsAhead; + + /// + /// Stage of the box parsing pipeline. + /// + private JxlBoxStage boxStage; + + /// + /// FTYP minor-version. + /// + /// 0 - jxlp must be in order + /// 1 - OOO jxlp allowed + /// + /// + private int jxlFileFormatVersion; + + /// + /// Counter of next expected jxlp box. + /// + private int nextJxlpIndex; + + /// + /// OOO jxlp payloads keyed by counter. Keys are: codestream bytes without + /// 4byte header, and is_last. + /// + private Dictionary jxlpOooBuffer = []; + + private long jxlpOooBufferTotal; + + private int bufferingJxlpIndex; + + private bool bufferingJxlpIsLast; + + /// + /// Decompresses box contents. + /// + private JxlBoxContentDecoder? boxContentDecoder; + + /// + /// Decodes JPEG XL to JPEG. + /// + private JxlToJpegDecoder? jpegDecoder; + + private JxlBoxContentDecoder? metadataDecoder; + + /// + /// Raw bytes for EXIF metadata. + /// + private IMemoryOwner? exifMetadata; + + /// + /// Raw bytes for XMP metadata. + /// + private IMemoryOwner? xmpMetadata; + + /// + /// State of EXIF storage. 0 - not stored, + /// 1 - currently stored, 2 - finished. + /// + private int storeExif; + + /// + /// State of XMP storage. 0 - not stored, + /// 1 - currently stored, 2 - finished. + /// + private int storeXmp; + + /// + /// Position in the output buffer for JPEG + /// reconstruction. + /// + private long reconstructionOutputBufferPos; + + /// + /// EXIF size for JPEG reconstruction. + /// + private long reconstructionExifSize; + + /// + /// XMP size for JPEG reconstruction. + /// + private long reconstructionXmpSize; + + /// + /// Stage of reconstruction pipeline. + /// + private JpegReconstructionStage reconstructionOutputJpeg; + + /// + /// Next input data. + /// + private IMemoryOwner? nextInput; + + private long availableInput; + + private bool inputClosed; + + /// + /// Output image buffer. + /// + private Stream? imageOutBuffer; + + /// + /// Callback to initialize image output. + /// + private JxlImageOutputInitializerCallback? imageOutputInitCallback; + + /// + /// Callback to run image output. + /// + private JxlImageOutputRunCallback? imageOutputRunCallback; + + /// + /// Callback to dispose image output. + /// + private JxlImageOutputDestroyCallback? imageOutputDestroyCallback; + + /// + /// Bit depth for image output. + /// + private JxlBitDepth imageOutputBitDepth = new(); + + public JxlDecoderCore(DecoderOptions options) + : base(options) + => this.Reset(); + + public long SizeHintBasicInfo => this.gotBasicInfo ? 0 : this.basicInfoSizeHint; + + /// + /// Gets or sets a value indicating whether orientation should be kept. + /// + public bool KeepOrientation + { + get => this.keepOrientation; + set + { + this.BeforeUpdateState(nameof(this.KeepOrientation)); + this.keepOrientation = value; + } + } + + /// + /// Gets or sets a value indicating whether to unpremultiply RGB values + /// by the alpha channel. + /// + public bool UnpremultiplyAlpha + { + get => this.unpremultiplyAlpha; + set + { + this.BeforeUpdateState(nameof(this.UnpremultiplyAlpha)); + this.unpremultiplyAlpha = value; + } + } + + /// + /// Gets or sets a value indicating whether spotcolors (special inks used in printing) + /// are rendered in the output. + /// + public bool RenderSpotcolors + { + get => this.renderSpotcolors; + set + { + this.BeforeUpdateState(nameof(this.RenderSpotcolors)); + this.renderSpotcolors = value; + } + } + + /// + /// Gets or sets a value indicating whether multiple frames (especially zero-duration frames) + /// have to be merged into a single image. + /// + public bool Coalescing + { + get => this.coalescing; + set + { + this.BeforeUpdateState(nameof(this.Coalescing)); + this.coalescing = value; + } + } + + /// + /// Gets the dimensions of the current image buffer. + /// + public Size CurrentDimensions + { + get + { + int width; + int height; + + if (this.frameHeader?.IsPreviewFrame == true) + { + width = this.metadata!.GetOrientedPreviewXSize(this.keepOrientation); + height = this.metadata!.GetOrientedPreviewYSize(this.keepOrientation); + } + else + { + width = this.metadata!.GetOrientedXSize(this.keepOrientation); + height = this.metadata!.GetOrientedYSize(this.keepOrientation); + + if (!this.coalescing) + { + JxlFrameDimensions dim = this.frameHeader!.FrameDimensions; + + width = dim.XSizeUpsampled; + height = dim.YSizeUpsampled; + + if (!this.keepOrientation && this.metadata.ImageMetadata!.Orientation > 4) + { + RuntimeUtility.Swap(ref width, ref height); + } + } + } + + return new Size(width, height); + } + } + + /// + /// Stage of the decoder pipeline. + /// + private enum JxlDecoderStage : byte + { + /// + /// Initialized but hasn't decoded yet. + /// + Initialized, + + /// + /// Decoding right now. + /// + Started, + + /// + /// Code stream done, but other boxes could still occur. + /// + CodeStreamFinished, + + /// + /// Decoding failed and the decoder is no longer usable. + /// + Error + } + + /// + /// Identifies the signature of the JPEG XL file. + /// + private enum JxlSignature : byte + { + /// + /// Error status indicating not enough bytes to detect the signature. + /// + NotEnoughBytes, + + /// + /// A JPEG XL code stream. + /// + CodeStream, + + /// + /// The signature is invalid. + /// + Invalid, + + /// + /// Container format. + /// + Container + } + + /// + /// Represents a data type. + /// + private enum JxlDataType : byte + { + /// + /// + /// + UInt8, + + /// + /// + /// + UInt16, + + /// + /// + /// + Float, + + /// + /// + /// + Float16 + } + + /// + /// Frame stage for this decoder. + /// + private enum JxlFrameStage : byte + { + /// + /// Frame header should be parsed. + /// + Header, + + /// + /// TOC should be parsed. + /// + Toc, + + /// + /// Full pixels should be parsed. + /// + Full + } + + /// + /// Stage of the box parsing pipeline. + /// + private enum JxlBoxStage : byte + { + /// + /// Box header of the next box. + /// + Header, + + /// + /// File type box. + /// + Ftyp, + + /// + /// Box with skipped contents. + /// + Skip, + + /// + /// Code stream boxes. + /// + CodeStream, + + /// + /// Extra header of partial code stream box. + /// + PartialCodeStream, + + /// + /// Out-of-order jxlp box payload. + /// + BufferingJxlp, + + /// + /// Jpeg reconstruction box. + /// + JpegReconstruction + } + + /// + /// Reconstruction stage for JPEG images. + /// + private enum JpegReconstructionStage : byte + { + /// + /// Don't output anything. + /// + None, + + /// + /// Set metadata to the JPEG data. + /// + SetMetadata, + + /// + /// Outputting the JPEG bytes. + /// + Output + } + + /// + /// A single frame index box entry. See . + /// + private struct JxlDecoderFrameIndexBoxEntry + { + /// + /// Offset of start byte of this frame compared to start + /// byte of previous frame. + /// + public long Offset; + + /// + /// Duration in ticks between the start of this frame and the start of the next frame. + /// + public int DurationInTicks; + + /// + /// Amount of frames. + /// + public int AmountOfFrames; + } + + // This is a class not a struct. This is so we can + // assign its values from an array access. Like this: + // this.frameReferences[(int)internalIndex].Reference = ... + // where this.frameReferences = JxlFrameReference[]. + private sealed class JxlFrameReference(int reference, int savedAs) + { + public int Reference = reference; + public int SavedAs = savedAs; + } + + /// + /// A frame index box. + /// + private sealed class JxlDecoderFrameIndexBox + { + /// + /// Gets or sets all entries within this frame index box. + /// + public List Entries { get; set; } = []; + + /// + /// Gets the number of entries. + /// + public int Count => this.Entries.Count; + + /// + /// Gets or sets the numerator. (Default: 1) + /// + public int Numerator { get; set; } = 1; + + /// + /// Gets or sets the denominator. (Default: 1000) + /// + public int Denominator { get; set; } = 1000; + + /// + /// Adds a new frame. + /// + /// Offset to first byte. + /// Duration in ticks. + /// Amount of frames. + public void AddFrame(long offset, int ticks, int frames) => this.Entries.Add(new JxlDecoderFrameIndexBoxEntry() + { + Offset = offset, + AmountOfFrames = frames, + DurationInTicks = ticks + }); + } + + private sealed record JxlExtraChannelOutput(JxlPixelFormat Format, object? Buffer, long BufferSize); + + private sealed record JxlOooEntry(byte[] CodestreamBytes, bool IsLast); + + public void Dispose() + { + // RewindDecodingState resets everything, + // including disposal of streams. + this.RewindDecodingState(); + + // Streams like input and output streams may + // be unmanaged. + GC.SuppressFinalize(this); + } + + /// + /// Ensures that the coordinates are not out of bounds. + /// + /// First coordinate + /// Second coordinate + /// Image width + /// Boolean indicating whether the coordinates are out of bounds + private static bool IsOutOfBounds(int a, int b, int size) + { + long position = a + b; + + return position > size || position < a; + } + + private static int InitialBasicInfoSizeHint() + { + const int containerHeaderSize = 48; + const int maxCodestreamBasicInfoSize = 50; + return containerHeaderSize + maxCodestreamBasicInfoSize; + } + + private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length, ref int position) + { + if (position >= length) + { + return JxlSignature.NotEnoughBytes; + } + + buffer = buffer[position..]; + length -= position; + + // 0xFF 0x0A represents a codestream + if (length >= 1 && buffer[0] == 0xFF) + { + if (length < 2) + { + // We need at least two bytes for a valid codestream signature + return JxlSignature.NotEnoughBytes; + } + else if (buffer[1] == CodestreamMarker) + { + position += 2; + return JxlSignature.CodeStream; + } + else + { + return JxlSignature.Invalid; + } + } + + // Container? + if (length >= 1 && buffer[0] == 0) + { + if (length < SignatureBox.Length) + { + return JxlSignature.NotEnoughBytes; + } + else if (buffer[SignatureBox.Length..].SequenceEqual(SignatureBox)) + { + position += SignatureBox.Length; + return JxlSignature.Container; + } + else + { + return JxlSignature.Invalid; + } + } + + // Signature is invalid + return JxlSignature.Invalid; + } + + private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length) + { + int position = 0; + return DetectSignature(buffer, length, ref position); + } + + private static int BitsPerChannel(JxlDataType dataType) + => dataType switch + { + JxlDataType.UInt8 => 8, + JxlDataType.UInt16 or JxlDataType.Float16 => 16, + JxlDataType.Float => 32, + _ => 0 + }; + + private static uint GetBitDepth(JxlBitDepth bitDepth, JxlImageMetadata metadata, JxlPixelFormat pixelFormat) + { + if (bitDepth.Type == JxlBitDepthType.FromPixelFormat) + { + return BitsPerChannel(pixelFormat.DataType); + } + else if (bitDepth.Type == JxlBitDepthType.FromCodeStream) + { + return metadata.BitDepth!.BitsPerSample; + } + else if (bitDepth.Type == JxlBitDepthType.Custom) + { + return bitDepth.BitsPerSample; + } + + return 0; + } + + private List GetFrameDependencies(int index, Span references) + { + DebugGuard.MustBeLessThan(index, references.Length, nameof(index)); + + const int storageNum = 8; + + List result = []; + int invalid = references.Length; + List[] storage = new List[storageNum]; + + for (int s = 0; s < storageNum; s++) + { + storage[s] = new List(references.Length); + int mask = 1 << s; + int id = invalid; + + for (int i = 0; i < references.Length; i++) + { + if ((references[i].SavedAs & mask) != 0) + { + id = i; + } + + storage[s][i] = id; + } + } + + Span seen = stackalloc byte[index + 1]; + seen.Clear(); // All values are explicitly cleared in reference source + + Stack stack = []; + stack.Push(index); + seen[index] = 1; + + for (int s = 0; s < storageNum; s++) + { + int frameRef = storage[s][index]; + + if (frameRef == invalid) + { + continue; + } + + if (seen[frameRef] != 0) + { + continue; + } + + stack.Push(frameRef); + seen[frameRef] = 1; + result.Add(frameRef); + } + + while (stack.Count > 0) + { + int frameIndex = stack.Pop(); + if (frameIndex == 0) + { + continue; + } + + for (int s = 0; s < storageNum; s++) + { + int mask = 1 << s; + + if ((references[frameIndex].Reference & mask) == 0) + { + continue; + } + + int frameRef = storage[s][frameIndex - 1]; + if (frameRef == invalid) + { + continue; + } + + if (seen[frameRef] != 0) + { + continue; + } + + stack.Push(frameRef); + seen[frameRef] = 1; + result.Add(frameRef); + } + } + + return result; + } + + /// + /// Checks if the buffer of specified length can be added. + /// + /// Length of the desired buffer. + /// + /// True if buffers with such lengths can be added; false if they + /// exceed the size limit. + /// + public bool CanAddBuffer(long length) + { + const long bufferLimit = 1 << 48; + return length < bufferLimit && + (length + this.jxlpOooBufferTotal + (this.codestreamCopy?.Memory.Length ?? 0)) < bufferLimit; + } + + public bool TryInjectNextBufferedJxlpBox() + { + if (!this.jxlpOooBuffer.TryGetValue(this.nextJxlpIndex, out JxlOooEntry? value)) + { + return false; + } + + if (value == this.jxlpOooBuffer.Last().Value) + { + return true; + } + + value.Deconstruct(out byte[] data, out bool isLast); + int length = data.Length; + + this.codestreamCopy!.Write(data); + + if (isLast) + { + this.lastCodestreamSeen = true; + } + + _ = this.jxlpOooBuffer.Remove(this.nextJxlpIndex++); + + this.jxlpOooBufferTotal -= length; + + return true; + } + + /// + /// Returns true if the jbrd box needs exif or xmp. + /// + /// JBRD needs more boxes - true, otherwise false. + public bool JbrdNeedsMoreBoxes() => + (this.storeExif < 2 && this.reconstructionExifSize > 0) + || (this.storeXmp < 2 && this.reconstructionXmpSize > 0); + + /// + /// Moves the input data forward by size bytes. + /// + /// Number of bytes to advance. + /// Thrown if advancing out of bounds. + public void AdvanceInput(long size) + { + if (this.availableInput < size) + { + throw new InvalidOperationException("Attempting to advance out of bounds"); + } + + this.nextInput += size; + this.filePosition += size; + this.availableInput -= size; + } + + /// + /// Returns number of available bytes in the code stream. + /// + /// + /// Number of available code stream bytes. + /// + public long AvailableCodeStream() + { + long avail = this.availableInput; + + if (!this.boxContentsUnbounded) + { + avail = Math.Min(avail, this.boxContentsEnd - this.filePosition); + } + + return avail; + } + + /// + /// Ensures that the copy of the code stream is present. + /// + /// + /// Thrown if the copy is missing or null. + /// + private void EnsureCodeStreamCopy() + { + if (this.codestreamCopy is null) + { + throw new InvalidOperationException("Copy of the code stream is missing"); + } + } + + /// + /// Moves forward by 'size' bytes in the code stream. + /// + /// Number of bytes to advance. + public void AdvanceCodeStream(long size) + { + this.EnsureCodeStreamCopy(); + long avail = this.AvailableCodeStream(); + + if (this.codestreamCopy!.Length == 0) + { + if (size <= avail) + { + // We have >= size bytes available, so + // advancing won't be out of bounds. + this.AdvanceInput(size); + } + else + { + // We have a limited amount of bytes for the + // code stream, and advancing by size would be + // out of bounds. So limit the value. + this.codestreamPos = size - avail; + this.AdvanceInput(avail); + } + } + else + { + this.codestreamPos += size; + if (this.codestreamPos + this.codestreamUnconsumed >= this.codestreamCopy.Length) + { + long advance = Math.Min( + this.codestreamUnconsumed, + this.codestreamUnconsumed + this.codestreamPos - this.codestreamCopy.Length); + + this.AdvanceInput(advance); + + this.codestreamPos -= Math.Min(this.codestreamPos, this.codestreamCopy.Length); + this.codestreamUnconsumed = 0; + + // Now we want to clear the code stream copy... + this.codestreamCopy.Dispose(); + this.codestreamCopy = new(this.Options.Configuration.MemoryAllocator); + } + } + } + + /// + /// Attempts to expand the buffer. + /// + /// Status of requesting more input. + public bool TryRequestMoreInput() + { + this.EnsureCodeStreamCopy(); + + if (this.codestreamCopy!.Length > 0) + { + long avail = this.AvailableCodeStream(); + + if (!this.CanAddBuffer(avail)) + { + return false; + } + + this.codestreamCopy.Write(this.nextInput!.Memory.Span[..(int)avail]); + + this.AdvanceInput(avail); + } + else + { + this.AdvanceInput(this.codestreamUnconsumed); + this.codestreamUnconsumed = 0; + } + + return true; + } + + public Memory? TryGetCodestreamInput() + { + if (this.codestreamCopy is null) + { + return null; + } + + if (this.codestreamCopy.Length == 0 && this.codestreamPos > 0) + { + long avail = this.AvailableCodeStream(); + long skip = Math.Min(this.codestreamPos, avail); + this.AdvanceInput(skip); + this.codestreamPos -= skip; + + if (this.codestreamPos > 0) + { + _ = this.TryRequestMoreInput(); + return null; + } + } + + if (this.codestreamPos > this.codestreamCopy.Length) + { + throw new InvalidOperationException("Codestream position > length of codestream copy"); + } + + if (this.codestreamUnconsumed > this.codestreamCopy.Length) + { + throw new InvalidOperationException("Codestream unconsumed > length of codestream copy"); + } + + long availCodestream = this.AvailableCodeStream(); + + if (this.codestreamCopy.Length == 0) + { + if (availCodestream == 0) + { + _ = this.TryRequestMoreInput(); + return null; + } + + return this.nextInput!.Memory[..(int)availCodestream]; + } + else + { + if (!this.CanAddBuffer(availCodestream)) + { + return null; + } + + this.codestreamCopy.Write(this.nextInput!.Memory.Span.Slice((int)this.codestreamUnconsumed, (int)(availCodestream - this.codestreamUnconsumed))); + + this.codestreamUnconsumed = availCodestream; + + return this.codestreamCopy.AsMemory(); + } + } + + /// + /// Returns true if the decoder can continue using code stream input. + /// + /// True if the decoder can use code stream input. Otherwise false. + public bool CanUseMoreCodestreamInput() => this.decoderStage != JxlDecoderStage.CodeStreamFinished; + + /// + /// Checks if width * height can be represented safely as a + /// positive integer after rounding the width up to the next + /// multiple of 32. + /// + /// Input width. + /// Input height. + /// + /// Boolean indicating whether the padded image dimensions fit + /// within a signed 32-bit integer when calculating the total + /// pixel count. + /// + /// + /// Negative values aren't rejected, but will produce incorrect + /// results. This method is meant to be used with positive values only. + /// + public static bool CheckSizeLimit(int width, int height) + { + if (width == 0 || height == 0) + { + return true; + } + + int paddedWidth = JxlMath.DivCeil(width, 32) * 32; + + if (paddedWidth < width) + { + // Overflow + return false; + } + + int pixelCount = paddedWidth * height; + + if (pixelCount / paddedWidth != height) + { + // Overflow + return false; + } + + return true; + } + + /// + /// Resets the decoder state to its default values, and, + /// additionally, releases memory used by buffers and replaces + /// them with new fresh copies. + /// + public void RewindDecodingState() + { + this.decoderStage = JxlDecoderStage.Initialized; + + this.gotSignature = false; + this.lastCodestreamSeen = false; + this.gotCodestreamSignature = false; + this.gotBasicInfo = false; + this.gotTransformData = false; + this.gotAllHeaders = false; + this.postHeaders = false; + + this.iccProfile = null; + + this.gotPreviewImage = false; + this.previewFrame = false; + this.filePosition = 0; + + this.boxContentsBegin = 0; + this.boxContentsEnd = 0; + this.boxContentsSize = 0; + this.boxSize = 0; + this.headerSize = 0; + this.boxContentsUnbounded = false; + + this.boxType = null; + this.boxDecodedType = null; + + this.boxEvent = false; + this.boxStage = JxlBoxStage.Header; + + this.jxlFileFormatVersion = 0; + this.nextJxlpIndex = 0; + this.jxlpOooBuffer.Clear(); + this.jxlpOooBufferTotal = 0; + this.bufferingJxlpIndex = 0; + this.bufferingJxlpIsLast = false; + + this.boxOutBufferSet = false; + this.boxOutBufferSetCurrentBox = false; + this.boxOutputBuffer?.Dispose(); + this.boxOutputBuffer = null; + this.boxOutBufferSize = 0; + this.boxOutBufferBegin = 0; + this.boxOutBufferPos = 0; + + this.exifMetadata?.Dispose(); + this.exifMetadata = null; + this.xmpMetadata?.Dispose(); + this.xmpMetadata = null; + this.storeExif = 0; + this.storeXmp = 0; + + this.reconstructionOutputBufferPos = 0; + this.reconstructionExifSize = 0; + this.reconstructionXmpSize = 0; + this.reconstructionOutputJpeg = JpegReconstructionStage.None; + + this.eventsWanted = this.originalEventsWanted; + this.basicInfoSizeHint = InitialBasicInfoSizeHint(); + this.haveContainer = false; + this.boxCount = 0; + this.downsamplingTarget = 8; + + this.imageOutBufferSet = false; + this.imageOutBuffer?.Dispose(); + this.imageOutBuffer = null; + this.imageOutputInitCallback = null; + this.imageOutputRunCallback = null; + this.imageOutputDestroyCallback = null; + this.imageOutputSize = 0; + + this.imageOutputBitDepth = new() + { + Type = JxlBitDepthType.FromPixelFormat + }; + + this.extraChannelOutputs.Clear(); + + this.nextInput?.Dispose(); + this.nextInput = null; + + this.availableInput = 0; + this.inputClosed = false; + + this.passesState?.Reset(); + this.frameDecoder?.Reset(); + this.nextSection = 0; + this.sectionProcessed.Clear(); + + this.imageBundle.Reset(); + this.metadata = new JxlCodecMetadata(); + this.imageMetadata = this.metadata.ImageMetadata; + + this.frameHeader = new() + { + Metadata = this.metadata + }; + + this.codestreamCopy?.Dispose(); + this.codestreamCopy = new(this.Options.Configuration.MemoryAllocator); + this.codestreamUnconsumed = 0; + this.codestreamPos = 0; + this.codestreamBitsAhead = 0; + + this.frameStage = JxlFrameStage.Header; + this.remainingFrameSize = 0; + this.isLastOfStill = false; + this.isLastTotal = false; + this.skipFrames = 0; + this.skippingFrame = false; + this.internalFrames = 0; + this.externalFrames = 0; + } + + /// + /// Resets the decoder to its default values. + /// + public void Reset() + { + this.RewindDecodingState(); + + this.keepOrientation = false; + this.unpremultiplyAlpha = false; + this.renderSpotcolors = true; + this.coalescing = true; + this.desiredIntensityTarget = 0f; + this.originalEventsWanted = 0; + this.eventsWanted = 0; + + this.frameReferences.Clear(); + this.frameExternalToInternal.Clear(); + this.frameRequired.Clear(); + + this.decompressBoxes = false; + } + /// - /// Identifies the signature of the JPEG XL file. + /// Returns the code stream as a Span. /// - private enum JxlSignature : byte + /// A Span representing the code stream. + /// Thrown if the code stream cannot be retrieved. + private Span GetCodeStreamSpan() { - /// - /// Error status indicating not enough bytes to detect the signature. - /// - NotEnoughBytes, + Memory codestreamInput = this.TryGetCodestreamInput() + ?? throw new InvalidOperationException("Cannot retrieve codestream input"); - /// - /// A JPEG XL code stream. - /// - CodeStream, + Span span = codestreamInput.Span; - /// - /// The signature is invalid. - /// - Invalid, + return span; + } + + /// + /// Skips frames without decoding them. + /// + /// Number of frames to skip. + public void SkipFrames(int amount) + { + this.skipFrames += amount; + this.frameRequired.Clear(); + + int nextFrame = this.externalFrames + this.skipFrames; + + if (nextFrame < this.frameExternalToInternal.Count) + { + int internalIndex = this.frameExternalToInternal[nextFrame]; + if (internalIndex < this.frameReferences.Count) + { + List deps = this.GetFrameDependencies(internalIndex, CollectionsMarshal.AsSpan(this.frameReferences)); + this.ResizeFrameRequired(internalIndex + 1); + + foreach (int index in deps) + { + if (index < this.frameRequired.Count) + { + this.frameRequired[index] = 1; + } + } + } + } + } + + /// + /// Ensures that frameRequired's count reaches . + /// + /// Max. number of items that frameRequired must have. + private void ResizeFrameRequired(int upperBound) + { + while (this.frameRequired.Count < upperBound) + { + this.frameRequired.Add(0); + } + } + + /// + /// Skips the current frame without having to decode it. + /// + /// + /// Thrown if the frame cannot be skipped. + /// + public void SkipCurrentFrame() + { + if (this.frameStage == JxlFrameStage.Full) + { + throw new InvalidOperationException("The decoder is ready to parse the frame, so the frame cannot be skipped"); + } + + this.frameStage = JxlFrameStage.Header; + this.AdvanceCodeStream(this.remainingFrameSize); + + if (this.isLastOfStill) + { + this.imageOutBufferSet = false; + } + } + + /// + /// Ensures that the decoder state doesn't change while + /// it's busy decoding an image. + /// + /// Name of the parameter that was changed/ + /// Thrown if the state doesn't match initialization. + private void BeforeUpdateState(string propertyName) + { + if (this.decoderStage != JxlDecoderStage.Initialized) + { + throw new InvalidOperationException("The decoder is already processing the image, so " + propertyName + " cannot be changed"); + } + } + + /// + /// Reads a single bundle into . + /// + /// Type of the bundle to read. + /// Bundle binary data. + /// Bit reader to continue from. + /// The bundle to parse. + /// Status of parsing the bundle. + private bool ReadBundle(Span data, JxlBitReader br, T bundle) + where T : IJxlFields + { + JxlBitReader reader = new(data); + reader.SkipBits64((ulong)br.TotalBitsConsumed); + + bool canRead = JxlBundle.CanRead(reader, bundle); + + if (!canRead) + { + return this.TryRequestMoreInput(); + } + + if (!JxlBundle.Read(reader, bundle)) + { + return false; + } + + return true; + } + + /// + /// Reads all basic metadata and headers. + /// + /// Status of the parsing. + /// Thrown if the data is incorrect. + /// Thrown if the data is malformed. + public bool ReadBasicInfo() + { + if (!this.gotCodestreamSignature) + { + Span span = this.GetCodeStreamSpan(); + + if (span.Length < 2) + { + return this.TryRequestMoreInput(); + } + + if (span[0] != 0xFF || span[1] != CodestreamMarker) + { + throw new InvalidOperationException("The file signature is invalid"); + } + + this.gotCodestreamSignature = true; + this.AdvanceCodeStream(2); + } + + Span sp = this.GetCodeStreamSpan(); + + JxlBitReader bitReader = new(sp); + + if (!this.ReadBundle(sp, bitReader, this.metadata!.Size!)) + { + throw new InvalidDataException("Could not parse the size header"); + } + + if (!this.ReadBundle(sp, bitReader, this.metadata!.ImageMetadata!)) + { + throw new InvalidDataException("Could not parse the image metadata"); + } + + long totalBits = bitReader.TotalBitsConsumed; + + this.AdvanceCodeStream(totalBits / JxlMath.BitsPerByte); + + this.codestreamBitsAhead = totalBits % JxlMath.BitsPerByte; + this.gotBasicInfo = true; + this.basicInfoSizeHint = 0; + this.imageMetadata = this.metadata.ImageMetadata; + + if (!CheckSizeLimit(this.metadata.Size!.XSize, this.metadata.Size.YSize)) + { + throw new InvalidOperationException("The image is too large"); + } + + return true; + } + + /// + /// Parses all necessary headers. + /// + /// Status of the parsing. + /// Thrown if data is incorrect. + public bool ReadAllHeaders() + { + if (!this.gotTransformData) + { + Span span = this.GetCodeStreamSpan(); + + JxlBitReader reader = new(span); + reader.SkipBits64((ulong)this.codestreamBitsAhead); + + this.metadata!.CustomTransformData!.NonserializedXybEncoded = this.metadata.ImageMetadata!.XybEncoded; + + if (!this.ReadBundle(span, reader, this.metadata.CustomTransformData)) + { + throw new InvalidOperationException("Cannot read custom transform data bundle"); + } + + long totalBits = reader.TotalBitsConsumed; + this.AdvanceCodeStream(totalBits / JxlMath.BitsPerByte); + this.codestreamBitsAhead = totalBits % JxlMath.BitsPerByte; + this.gotTransformData = true; + } + + Span sp = this.GetCodeStreamSpan(); + + JxlBitReader bitReader = new(sp); + bitReader.SkipBits64((ulong)this.codestreamBitsAhead); + + if (this.metadata!.ImageMetadata!.ColorEncoding!.NeedsIcc) + { + // TODO: optimize this? ImageSharp ICC doesn't support spans + // so we need to allocate an array. + IccDataReader reader = new(sp.ToArray()); + IccProfileHeader header = IccReader.ReadHeader(reader); + IccTagDataEntry[] tagData = IccReader.ReadTagData(reader); + + IccProfile icc = new(header, tagData); + this.iccProfile = icc; + sp = sp[reader.Index..]; + + byte[] iccRawData = icc.ToByteArray(); + this.metadata.ImageMetadata.ColorEncoding.SetIccRaw(iccRawData); + } + + this.gotAllHeaders = true; + bitReader.JumpToByteBoundary(); + + this.AdvanceCodeStream(bitReader.TotalBitsConsumed / JxlMath.BitsPerByte); + this.codestreamBitsAhead = 0; + + this.passesState ??= new(this.frameHeader!, this.Options.Configuration); + this.passesState.OutputEncodingInfo.SetFromMetadata(this.metadata); + + if (this.desiredIntensityTarget > 0f) + { + this.passesState.OutputEncodingInfo.DesiredIntensityTarget = this.desiredIntensityTarget; + } + + this.imageMetadata = this.metadata.ImageMetadata; + + return true; + } + + /// + /// Processes all sections in this JPEG XL images and invokes + /// the frame decoder. + /// + /// Thrown if the data is invalid or malformed. + public void ProcessSections() + { + Span span = this.GetCodeStreamSpan(); + + var toc = this.frameDecoder!.Toc; + + long pos = 0; + List sectionInfo = []; + List sectionStatus = []; + + for (long i = this.nextSection; i < toc.Size; i++) + { + if (this.sectionProcessed[(int)i] != 0) + { + pos += toc[i].Size; + continue; + } + + long id = toc[i].Id; + long size = toc[i].Size; + + if (IsOutOfBounds((int)pos, (int)size, span.Length)) + { + break; + } + + JxlBitReader br = new(span.Slice((int)pos, (int)size)); + sectionInfo.Add(new(br, id, i)); + sectionStatus.Add(default); + pos += size; + } + + this.frameDecoder.ProcessSections(sectionInfo, sectionStatus); + + bool outOfBounds = false; + + foreach (JxlFrameDecoder.SectionInfo info in sectionInfo) + { + if (!info.BitReader.AllReadsWithinBounds) + { + outOfBounds = true; + break; + } + } + + if (outOfBounds) + { + throw new InvalidOperationException("Frame out of bounds"); + } + + for (int i = 0; i < sectionStatus.Count; i++) + { + JxlFrameDecoder.SectionStatus ss = sectionStatus[i]; + + if (ss == JxlFrameDecoder.Done) + { + this.sectionProcessed[sectionInfo[i].Index] = 1; + } + else if (ss != JxlFrameDecoder.Skipped) + { + throw new InvalidOperationException("Unexpected section status"); + } + } + + long completedPrefixBytes = 0; + + while (this.nextSection < this.sectionProcessed.Count && this.sectionProcessed[(int)this.nextSection] == 1) + { + completedPrefixBytes += toc[(int)this.nextSection].Size; + this.nextSection++; + } + + this.remainingFrameSize -= completedPrefixBytes; + this.AdvanceCodeStream(completedPrefixBytes); + } + + /// + /// Processes all codestream contents. + /// + /// Status of codestream processing. + /// Thrown when data is corrupt, malformed, or incorrect. + public int ProcessCodestream() + { + if (!this.gotBasicInfo) + { + bool status = this.ReadBasicInfo(); + + if (!status) + { + throw new InvalidOperationException("Could not parse basic info"); + } + } + + if ((this.eventsWanted & BasicInfo) != 0) + { + this.eventsWanted &= ~BasicInfo; + return JxlCodestreamType.BasicInfo; + } + + if (this.eventsWanted == 0) + { + this.decoderStage = JxlDecoderStage.CodeStreamFinished; + return JxlCodestreamType.Success; + } + + if (!this.gotAllHeaders) + { + bool status = this.ReadAllHeaders(); + + if (!status) + { + throw new InvalidOperationException("Could not parse headers"); + } + } + + if ((this.eventsWanted & ColorEncoding) != 0) + { + this.eventsWanted &= ~ColorEncoding; + return JxlCodestreamType.ColorEncoding; + } + + if (this.eventsWanted == 0) + { + this.decoderStage = JxlDecoderStage.CodeStreamFinished; + return JxlCodestreamType.Success; + } + + this.postHeaders = true; + + if (!this.gotPreviewImage && this.metadata!.ImageMetadata!.HavePreview) + { + this.previewFrame = true; + } + + while (true) + { + bool parseFrames = (this.eventsWanted & (PreviewImage | DecodedFrame | FullImage)) != 0; + if (!parseFrames) + { + break; + } + + if (this.frameStage == JxlFrameStage.Header && this.isLastTotal) + { + break; + } + + if (this.frameStage == JxlFrameStage.Header) + { + if (this.reconstructionOutputJpeg is JpegReconstructionStage.SetMetadata or JpegReconstructionStage.Output) + { + throw new InvalidOperationException("Cannot decode frames following a JPEG reconstruction frame"); + } + + this.imageBundle ??= new(this.imageMetadata!); + + if (!this.jpegDecoder.SetImageBundleJpegData(this.imageBundle!)) + { + throw new InvalidOperationException("Cannot set JXL->JPEG decoder image bundle"); + } + + this.frameDecoder = new(this.passesState!, this.metadata!, useSlowRenderingPipeline: false); + this.frameHeader = new() + { + Metadata = this.metadata + }; + + Span span = this.GetCodeStreamSpan(); + JxlBitReader reader = new(span); + + this.frameDecoder.InitializeFrame(reader, this.imageBundle!, this.previewFrame); + + if (!reader.AllReadsWithinBounds) + { + return this.TryRequestMoreInput() ? 1 : 0; + } + + this.AdvanceCodeStream(reader.TotalBitsConsumed / JxlMath.BitsPerByte); + this.frameHeader = this.frameDecoder.GetFrameHeader(); + + JxlFrameDimensions dim = this.frameHeader.FrameDimensions; + + if (!CheckSizeLimit(dim.XSizeUpsampledPadded, dim.YSizeUpsampledPadded)) + { + throw new InvalidOperationException("Frame is too large"); + } + + int outputType = this.previewFrame ? PreviewImage : FullImage; + bool outputNeeded = (this.eventsWanted & outputType) != 0; + + if (outputNeeded) + { + this.frameDecoder.InitializeFrameOutput(); + } + + this.remainingFrameSize = this.frameDecoder.SumSectionSizes(); + + this.frameStage = JxlFrameStage.Toc; + if (this.previewFrame) + { + if ((this.eventsWanted & PreviewImage) == 0) + { + this.frameStage = JxlFrameStage.Header; + this.AdvanceCodeStream(this.remainingFrameSize); + this.gotPreviewImage = true; + this.previewFrame = false; + } + + continue; + } + + int savedAs = JxlFrameDecoder.SavedAs(this.frameHeader); + this.isLastTotal = this.frameHeader.IsLast; + this.isLastOfStill = this.isLastTotal || this.frameHeader.AnimationFrame!.Duration > 0; + this.isLastOfStill |= !this.coalescing && this.frameHeader.FrameType == JxlFrameType.RegularFrame; + + int internalFrameIndex = this.internalFrames; + int externalFrameIndex = this.externalFrames; + + if (this.isLastOfStill) + { + this.externalFrames++; + } + + this.internalFrames++; + + if (this.skipFrames > 0) + { + this.skippingFrame = true; + + if (this.isLastOfStill) + { + this.skipFrames--; + } + } + else + { + this.skippingFrame = false; + } + + if (externalFrameIndex >= this.frameExternalToInternal.Count) + { + this.frameExternalToInternal.Add(internalFrameIndex); + + if (this.frameExternalToInternal.Count != externalFrameIndex + 1) + { + throw new InvalidOperationException("Internal error"); + } + } + + if (internalFrameIndex >= this.frameReferences.Count) + { + this.frameReferences.Add(new JxlFrameReference(0xFF, savedAs)); + + if (this.frameReferences.Count != internalFrameIndex + 1) + { + throw new InvalidOperationException("Internal error"); + } + } + + if (this.skippingFrame) + { + bool referenceable = this.frameHeader.CanBeReferenced + || this.frameHeader.FrameType == JxlFrameType.DcFrame; + + if (internalFrameIndex < this.frameRequired.Count && this.frameRequired[internalFrameIndex] == 0) + { + referenceable = false; + } + + if (!referenceable) + { + this.frameStage = JxlFrameStage.Header; + this.AdvanceCodeStream(this.remainingFrameSize); + continue; + } + } + + if ((this.eventsWanted & Frame) != 0 && this.isLastOfStill) + { + if (!this.skippingFrame) + { + return Frame; + } + } + + if (this.frameStage == JxlFrameStage.Toc) + { + this.frameDecoder.SetRenderSpotcolors(this.renderSpotcolors); + this.frameDecoder.SetCoalescing(this.coalescing); + + if (!this.previewFrame && + (this.eventsWanted & FrameProgression) != 0) + { + this.frameProgressiveDetail = this.frameDecoder.SetPauseAtProgressive(this.progressiveDetail); + } + else + { + this.frameProgressiveDetail = JxlProgressiveDetail.Frames; + } + + this.dcFrameProgressionDone = false; + this.nextSection = 0; + this.sectionProcessed.Clear(); + ResizeSectionProcessed(this.frameDecoder.Toc.Size); + + if (this.previewFrame || (this.eventsWanted & FullImage) != 0) + { + this.frameStage = JxlFrameStage.Full; + } + else if (!this.isLastTotal) + { + this.frameStage = JxlFrameStage.Header; + this.AdvanceCodeStream(this.remainingFrameSize); + continue; + } + else + { + break; + } + } + + if (this.frameStage == JxlFrameStage.Full) + { + if (!this.imageOutBufferSet) + { + if (this.previewFrame) + { + return NeedPreviewOutBuffer; + } + + if ((!this.jpegDecoder.IsOutputSet || this.imageBundle!.JpegData is null) + && this.isLastOfStill + && !this.skippingFrame) + { + return NeedImageOutputBuffer; + } + } + + if (this.imageOutBufferSet) + { + Size dimensions = this.CurrentDimensions; + int bitsPerSample = GetBitDepth(this.imageOutputBitDepth, this.metadata!.ImageMetadata!, this.imageOutputFormat); + + this.frameDecoder.SetImageOutput( + new PixelCallback( + this.imageOutputInitCallback, + this.imageOutputRunCallback, + this.imageOutputDestroyCallback, + this.imageOutputInitOpaque), + this.imageOutBuffer, + this.imageOutputSize, + dimensions.Width, + dimensions.Height, + this.imageOutputFormat, + bitsPerSample, + this.unpremultiplyAlpha, + !this.keepOrientation); + + for (int i = 0; i < this.extraChannelOutputs.Count; i++) + { + JxlExtraChannelOutput extra = this.extraChannelOutputs[i]; + int ecBitsPerSample = GetBitDepth(this.imageOutputBitDepth, this.metadata.ImageMetadata!.ExtraChannels[i], extra.Format); + + this.frameDecoder.AddExtraChannelOutput( + extra.Buffer, + extra.BufferSize, + dimensions.Width, + extra.Format, + ecBitsPerSample); + } + } + + long nextNumPassesToPause = this.frameDecoder.NextNumPassesToPause; + + this.ProcessSections(); + + bool allSectionsDone = this.frameDecoder.DecodedAll; + bool gotDcOnly = !allSectionsDone && this.frameDecoder.HasDecodedDc; + + if (this.frameProgressiveDetail >= JxlProgressiveDetail.Dc && + !this.dcFrameProgressionDone && + gotDcOnly) + { + this.dcFrameProgressionDone = true; + this.downsamplingTarget = 8; + return Progression; + } + + bool newProgressionStepDone = this.frameDecoder.NumCompletePasses >= nextNumPassesToPause; + + if (!allSectionsDone && + this.frameProgressiveDetail >= JxlProgressiveDetail.LastPasses && + newProgressionStepDone) + { + this.downsamplingTarget = this.frameHeader.Passes.GetDownsamplingTargetForCompletedPasses(this.frameDecoder.NumCompletePasses); + return Progression; + } + + if (!allSectionsDone) + { + return this.TryRequestMoreInput() ? 1 : 0; + } + + if (!this.previewFrame) + { + long internalIndex = this.internalFrames - 1; + if (this.frameReferences.Count <= internalIndex) + { + throw new InvalidOperationException("Internal error"); + } + + this.frameReferences[(int)internalIndex].Reference = this.frameDecoder.References; + } + + this.frameDecoder.FinalizeFrame(); + + if (this.jpegDecoder.IsOutputSet && this.imageBundle!.JpegData is not null) + { + this.frameStage = JxlFrameStage.Header; + this.reconstructionOutputJpeg = JpegReconstructionStage.SetMetadata; + + return FullImage; + } - /// - /// Container format. - /// - Container + if (this.previewFrame || this.isLastOfStill) + { + this.imageOutBufferSet = false; + this.extraChannelOutputs.Clear(); + } + } + + this.frameStage = JxlFrameStage.Header; + this.imageBundle.Reset(); + + if (this.previewFrame) + { + this.gotPreviewImage = true; + this.previewFrame = false; + this.eventsWanted &= ~PreviewImage; + return PreviewImage; + } + else if (this.isLastOfStill && (this.eventsWanted & FullImage) != 0 && !this.skippingFrame) + { + return FullImage; + } + } + } + + this.decoderStage = JxlDecoderStage.CodeStreamFinished; + return 1; } /// - /// Represents a data type. + /// Sets the input JPEG XL data to . /// - private enum JxlDataType : byte + /// The input data to parse JPEG XL. + /// Thrown if the input data cannot be changed at this moment. + public void SetInput(IMemoryOwner data) { - /// - /// - /// - UInt8, - - /// - /// - /// - UInt16, + if (this.nextInput is not null) + { + throw new InvalidOperationException("Input is already present. Use DisposeInput first"); + } - /// - /// - /// - Float, + if (this.inputClosed) + { + throw new InvalidOperationException("Input is closed"); + } - /// - /// - /// - Float16 + this.nextInput = data; + this.availableInput = data.Memory.Length; } - public JxlDecoderCore(DecoderOptions options) - : base(options) + /// + /// Disposes the input data. + /// + /// + /// Number of available bytes left in the input data before disposal. + /// + public long DisposeInput() { + long previousAvailableBytes = this.availableInput; + + this.nextInput?.Dispose(); + this.nextInput = null; + this.availableInput = 0; + + return previousAvailableBytes; } /// - /// Ensures that the coordinates are not out of bounds. + /// Closes the input stream so it can't be read anymore. /// - /// First coordinate - /// Second coordinate - /// Image width - /// Boolean indicating whether the coordinates are out of bounds - private static bool IsOutOfBounds(int a, int b, int size) + public void CloseInput() => this.inputClosed = true; + + /// + /// Sets the output buffer for JPEG reconstruction. + /// + /// Buffer for JPEG reconstruction. + /// Thrown when the buffer can't be set. + public void SetJpegBuffer(Memory data) { - long position = a + b; + if (this.internalFrames > 1) + { + throw new InvalidOperationException("JPEG reconstruction only works for first frames"); + } - return position > size || position < a; - } + if (this.jpegDecoder.IsOutputSet) + { + throw new InvalidOperationException("Already set JPEG buffer"); + } - private static int InitialBasicInfoSizeHint() - { - const int containerHeaderSize = 48; - const int maxCodestreamBasicInfoSize = 50; - return containerHeaderSize + maxCodestreamBasicInfoSize; + this.jpegDecoder.SetOutputBuffer(data); } - private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length, ref int position) + /// + /// Parses the start of a box. + /// + /// Input bytes to parse from. + /// Size of remaining input bytes. + /// Offset of input bytes. + /// File offset. + /// Type of the parsed box. + /// Output box size. + /// Output header size. + /// + /// True if the parsing went fine. False if the parsing requests + /// more input bytes. + /// + /// + /// Thrown when data is invalid. + /// + private static bool ParseBoxHeader(Span input, long size, long pos, long filePos, JxlBoxType type, out long boxSize, out long headerSize) { - if (position >= length) + boxSize = 0; + headerSize = 0; + + if (IsOutOfBounds((int)pos, 8, (int)size)) { - return JxlSignature.NotEnoughBytes; + headerSize = 8; + return false; } - buffer = buffer[position..]; - length -= position; + long boxStart = pos; + boxSize = BinaryPrimitives.ReadInt32BigEndian(input[(int)pos..]); + pos += 4; + type = (JxlBoxType)BitConverter.ToInt32(input.Slice((int)pos, 4)); + pos += 4; - // 0xFF 0x0A represents a codestream - if (length >= 1 && buffer[0] == 0xFF) + if (boxSize == 1) { - if (length < 2) + headerSize = 16; + + if (IsOutOfBounds((int)pos, 8, (int)size)) { - // We need at least two bytes for a valid codestream signature - return JxlSignature.NotEnoughBytes; + return false; } - else if (buffer[1] == CodestreamMarker) + + long boxSize64 = BinaryPrimitives.ReadInt64BigEndian(input[(int)pos..]); + pos += 8; + boxSize = boxSize64; + } + + headerSize = pos - boxStart; + + if (boxSize > 0 && boxSize < headerSize) + { + throw new InvalidOperationException("Invalid box size"); + } + + if (filePos + boxSize < filePos) + { + throw new InvalidOperationException("Box size overflow"); + } + + return true; + } + + /// + /// Processes all boxes and their contents if this is a container format. + /// + /// Status of processing. + /// Thrown when data is invalid. + public int ProcessBoxes() + { + // We have a box handling loop here. + while (true) + { + if (this.boxStage != JxlBoxStage.Header) { - position += 2; - return JxlSignature.CodeStream; + this.AdvanceInput(this.headerSize); + this.headerSize = 0; + + if ((this.eventsWanted & Box) != 0 && this.boxEvent && !this.boxOutBufferSetCurrentBox) + { + this.boxEvent = false; + } + + if ((this.eventsWanted & Box) != 0 && this.boxOutBufferSetCurrentBox) + { + Memory nextOut = this.boxOutputBuffer!.Memory[(int)this.boxOutBufferPos..]; + long availOut = this.boxOutBufferSize - this.boxOutBufferPos; + + Span bufferSpan = this.boxOutputBuffer.Memory.Span; + Span startSlice = bufferSpan[(int)this.boxOutBufferPos..]; + + int status = this.boxContentDecoder!.Process( + this.nextInput, + this.availableInput, + this.filePosition - this.boxContentsBegin, + nextOut, + ref availOut); + + long produced = startSlice.Length - availOut; + this.boxOutBufferPos += produced; + + if (status == Complete && (this.eventsWanted & Complete) == 0) + { + status = Success; + } + + if (status is not (Success or NeedMoreInput)) + { + return status; + } + } + + if (this.storeExif == 1 || this.storeXmp == 1) + { + IMemoryOwner metadata = (this.storeExif == 1 ? this.exifMetadata : this.xmpMetadata) ?? throw new InvalidOperationException("Metadata is missing, but should be present"); + + // Boxes should not contain more than 64MiB data. + const long blockSizeLimit = 64L << 20; + + // Use array version of metadata so we + // can resize the array. + byte[] md = metadata.Memory.ToArray(); + + while (true) + { + if (md.Length == 0) + { + Array.Resize(ref md, 64); + } + + Span originalNextOutput = md.AsSpan()[(int)this.reconstructionOutputBufferPos..]; + Span nextOutput = originalNextOutput; + long availableOutput = md.Length - this.reconstructionOutputBufferPos; + + int boxResult = this.metadataDecoder.Decode( + this.nextInput, + this.availableInput, + this.filePosition - this.boxContentsBegin, + ref nextOutput, + ref availableOutput); + + long produced = originalNextOutput.Length - nextOutput.Length; + this.reconstructionOutputBufferPos += produced; + + if (boxResult == NeedMoreOutput) + { + if (md.Length >= blockSizeLimit) + { + throw new InvalidOperationException("Box with EXIF or XMP metadata is too large"); + } + + Array.Resize(ref md, md.Length * 2); + } + else if (boxResult == NeedMoreInput) + { + break; + } + else if (boxResult == Complete) + { + long neededSize = this.storeExif == 1 ? this.reconstructionExifSize : this.reconstructionXmpSize; + + if (this.boxContentsUnbounded && this.reconstructionOutputBufferPos < neededSize) + { + break; + } + else + { + Array.Resize(ref md, (int)this.reconstructionOutputBufferPos); + + if (this.storeExif == 1) + { + this.storeExif = 2; + } + + if (this.storeXmp == 1) + { + this.storeXmp = 2; + } + + break; + } + } + else + { + // Error + return boxResult; + } + } + } + } + + if (this.reconstructionOutputJpeg == JpegReconstructionStage.SetMetadata && this.JbrdNeedsMoreBoxes()) + { + JxlJpegData jpegData = this.imageBundle!.JpegData.GetData(); + + if (this.reconstructionExifSize > 0) + { + int status = JxlToJpegDecoder.SetExif(this.exifMetadata!.Memory, jpegData); + if (status != Success) + { + return status; + } + } + + if (this.reconstructionXmpSize > 0) + { + int status = JxlToJpegDecoder.SetXmp(this.xmpMetadata!.Memory, jpegData); + if (status != Success) + { + return status; + } + } + + this.reconstructionOutputJpeg = JpegReconstructionStage.Output; + } + + if (this.reconstructionOutputJpeg == JpegReconstructionStage.Output && !this.JbrdNeedsMoreBoxes()) + { + int status = this.jpegDecoder!.WriteOutput(this.imageBundle!.JpegData); + if (status != Success) + { + return status; + } + + this.reconstructionOutputJpeg = JpegReconstructionStage.None; + this.imageBundle.Reset(); + + if ((this.eventsWanted & FullImage) != 0) + { + return FullImage; + } + } + + if (this.boxStage == JxlBoxStage.Header) + { + if (!this.haveContainer) + { + if (this.decoderStage == JxlDecoderStage.CodeStreamFinished) + { + return Success; + } + + this.boxStage = JxlBoxStage.CodeStream; + this.boxContentsUnbounded = true; + + continue; + } + + if (this.availableInput == 0) + { + if (this.decoderStage != JxlDecoderStage.CodeStreamFinished) + { + return NeedMoreInput; + } + + if (this.JbrdNeedsMoreBoxes()) + { + return NeedMoreInput; + } + + if (this.inputClosed) + { + return Success; + } + + if ((this.eventsWanted & Box) != 0) + { + return Success; + } + + return NeedMoreInput; + } + + bool boxedCodestreamDone = ((this.eventsWanted & Box) != 0) + && this.decoderStage == JxlDecoderStage.CodeStreamFinished + && !this.JbrdNeedsMoreBoxes() + && this.lastCodestreamSeen; + + if (boxedCodestreamDone && + this.availableInput >= 2 && + this.nextInput!.Memory.Span[0] == 0xFF && + this.nextInput.Memory.Span[1] == CodestreamMarker) + { + return Success; + } + + int status = ParseBoxHeader(this.nextInput, this.availableInput, 0, this.filePosition, this.boxType, out long boxSize, out long headerSize); + + if (this.boxType == JxlBoxTypes.Brob) + { + if (this.availableInput < headerSize + 4) + { + return NeedMoreInput; + } + + this.boxDecodedType = BitConverter.ToInt32(this.nextInput!.Memory.Span[(int)headerSize..]); + } + else + { + this.boxDecodedType = this.boxType; + } + + this.boxCount++; + + if (boxedCodestreamDone && this.boxType == JxlBoxTypes.Jxl) + { + return Success; + } + + if (this.boxCount == 2 && this.boxType != JxlBoxType.FileType) + { + throw new InvalidOperationException("The second box must be a ftyp (File Type) box"); + } + + if (this.boxType == JxlBoxTypes.FileType && this.boxCount != 2) + { + throw new InvalidOperationException("The ftyp (File Type) box must be a second box"); + } + + this.boxContentsUnbounded = boxSize == 0; + this.boxContentsBegin = this.filePosition + headerSize; + this.boxContentsEnd = this.boxContentsUnbounded ? 0 : (this.filePosition + boxSize); + this.boxContentsSize = this.boxContentsUnbounded ? 0 : (boxSize - headerSize); + this.boxSize = boxSize; + this.headerSize = headerSize; + + if ((this.originalEventsWanted & JpegReconstruction) != 0) + { + if (this.storeExif == 0 && this.boxDecodedType == JxlBoxTypes.Exif) + { + this.storeExif = 1; + this.reconstructionOutputBufferPos = 0; + } + + if (this.storeXmp == 0 && this.boxDecodedType == JxlBoxTypes.Xml) + { + this.storeXmp = 1; + this.reconstructionOutputBufferPos = 0; + } + } + + if ((this.eventsWanted & Box) != 0) + { + bool decompress = this.decompressBoxes && this.boxType == JxlBoxTypes.Brob; + this.boxContentDecoder.StartBox(decompress, this.boxContentsUnbounded, this.boxContentsSize); + } + + if (this.storeExif == 1 || this.storeXmp == 1) + { + bool brob = this.boxType == JxlBoxTypes.Brob; + this.metadataDecoder.StartBox(brob, this.boxContentsUnbounded, this.boxContentsSize); + } + + if (this.boxType == JxlBoxTypes.FileType) + { + this.boxStage = JxlBoxStage.Ftyp; + } + else if (this.boxType == JxlBoxTypes.JxlCodeStream) + { + if (this.lastCodestreamSeen) + { + throw new InvalidOperationException("Only one jxlc (JPEG XL codestream) box can be present"); + } + + this.lastCodestreamSeen = true; + this.boxStage = JxlBoxStage.CodeStream; + } + else if (this.boxType == JxlBoxTypes.JxlPartialCodeStream) + { + this.boxStage = JxlBoxStage.PartialCodeStream; + } + else if ((this.originalEventsWanted & JpegReconstruction) != 0 && this.boxType == JxlBoxTypes.JpegReconstructionData) + { + if ((this.eventsWanted & JpegReconstruction) == 0) + { + throw new InvalidOperationException("Multiple JPEG reconstruction boxes detected"); + } + + this.boxStage = JxlBoxStage.JpegReconstruction; + } + else + { + this.boxStage = JxlBoxStage.Skip; + } + + if ((this.eventsWanted & Box) != 0) + { + this.boxEvent = true; + this.boxOutBufferSetCurrentBox = false; + return Box; + } + } + else if (this.boxStage == JxlBoxStage.Ftyp) + { + if (this.boxContentsSize < 12) + { + throw new InvalidOperationException("The file type box is too small"); + } + + if (this.availableInput < 8) + { + return NeedMoreInput; + } + + Span nextSpan = this.nextInput!.Memory.Span; + if (!(nextSpan[0] == 'j' && nextSpan[1] == 'x' && nextSpan[2] == 'l' && nextSpan[3] == ' ')) + { + throw new InvalidOperationException("File type box major brand must be \"jxl \""); + } + + uint version = BinaryPrimitives.ReadUInt32BigEndian(nextSpan[4..]); + if (version > 1) + { + throw new InvalidOperationException("Unknown JXL file format version " + version + ", known versions are 0 and 1"); + } + + this.jxlFileFormatVersion = (int)version; + this.AdvanceInput(8); + this.boxStage = JxlBoxStage.Skip; + } + else if (this.boxStage == JxlBoxStage.PartialCodeStream) + { + if (this.lastCodestreamSeen) + { + throw new InvalidOperationException("Cannot have jxlp box after last jxlp box"); + } + + if (this.availableInput < 4) + { + return NeedMoreInput; + } + + if (!this.boxContentsUnbounded && this.boxContentsSize < 4) + { + throw new InvalidOperationException("jxlp box is too small to contain an index"); + } + + uint jxlpIndex = BinaryPrimitives.ReadUInt32BigEndian(this.nextInput!.Memory.Span); + uint counter = jxlpIndex & 0x7FFFFFFFu; + bool isLast = (jxlpIndex & 0x80000000u) != 0; + + if (counter < this.nextJxlpIndex) + { + throw new InvalidOperationException("jxlp box index " + counter + " is a duplicate (already processed)"); + } + + this.AdvanceInput(4); + + if (counter == this.nextJxlpIndex) + { + this.nextJxlpIndex++; + + if (isLast) + { + this.lastCodestreamSeen = true; + } + + this.boxStage = JxlBoxStage.CodeStream; + } + else if (this.jxlFileFormatVersion >= 1) + { + if (this.jxlpOooBuffer.Count >= NumBuffersLimit) + { + return Error; + } + + // When creating a new OOO (Out-of-order) entry, + // the data is initially empty. + byte[] buffer = []; + JxlOooEntry entry = new(buffer, isLast); + this.jxlpOooBuffer.Add((int)counter, entry); + + this.bufferingJxlpIndex = (int)counter; + this.bufferingJxlpIsLast = isLast; + this.boxStage = JxlBoxStage.BufferingJxlp; + } + else + { + throw new InvalidOperationException("JXLP box with index " + counter + " is out of order (index " + this.nextJxlpIndex + " was expected). Out-of-order jxlp boxes require file format version 1 in the file type (ftyp) box."); + } + } + else if (this.boxStage == JxlBoxStage.CodeStream) + { + int status = this.ProcessCodestream(); + + if (status == FullImage) + { + if (this.reconstructionOutputJpeg != JpegReconstructionStage.None) + { + continue; + } + } + + if (status == NeedMoreInput) + { + if (this.filePosition == this.boxContentsEnd && !this.boxContentsUnbounded) + { + bool hasMoreData = this.TryInjectNextBufferedJxlpBox(); + + if (hasMoreData) + { + continue; + } + + this.boxStage = JxlBoxStage.Header; + continue; + } + } + + if (status == Success) + { + if (this.JbrdNeedsMoreBoxes()) + { + this.boxStage = JxlBoxStage.Skip; + continue; + } + + if (this.boxContentsUnbounded) + { + break; + } + + if ((this.eventsWanted & Box) != 0) + { + this.boxStage = JxlBoxStage.Skip; + continue; + } + } + + return status; + } + else if (this.boxStage == JxlBoxStage.BufferingJxlp) + { + long remaining = this.boxContentsUnbounded + ? this.availableInput + : Math.Min(this.availableInput, this.boxContentsEnd - this.filePosition); + + if (!this.CanAddBuffer(remaining) || !this.jxlpOooBuffer.TryGetValue(this.bufferingJxlpIndex, out JxlOooEntry? entry)) + { + return Error; + } + + entry!.CodestreamBytes.Write(this.nextInput!.Memory.Span[..(int)remaining]); + this.jxlpOooBufferTotal += remaining; + this.AdvanceInput(remaining); + + bool boxDone = !this.boxContentsUnbounded && this.filePosition >= this.boxContentsEnd; + + if (!boxDone) + { + return NeedMoreInput; + } + + this.boxStage = JxlBoxStage.Header; + } + else if (this.boxStage == JxlBoxStage.JpegReconstruction) + { + if (!this.jpegDecoder.IsParsingBox) + { + this.jpegDecoder.StartBox(this.boxContentsUnbounded, this.boxContentsSize); + } + + Span nextInput = this.nextInput!.Memory.Span; + long availableInput = this.availableInput; + + int reconstructionResult = this.jpegDecoder.Process(ref nextInput, ref availableInput); + + long consumed = this.nextInput.Memory.Length - nextInput.Length; + this.AdvanceInput(consumed); + + if (reconstructionResult == JpegReconstruction) + { + JxlJpegData jpegData = this.jpegDecoder!.GetJpegData(); + long numExif = JxlToJpegDecoder.NumExifMarkers(jpegData); + long numXmp = JxlToJpegDecoder.NumXmpMarkers(jpegData); + + if (numExif > 0) + { + if (numExif > 1) + { + throw new InvalidOperationException("Only one EXIF marker for JPEG reconstruction can be present"); + } + + if (JxlToJpegDecoder.ExifBoxContentSize(jpegData, ref this.reconstructionExifSize) != Success) + { + throw new InvalidOperationException("Invalid jbrd EXIF size"); + } + } + + if (numXmp > 0) + { + if (numXmp > 1) + { + throw new InvalidOperationException("Only one XMP marker for JPEG reconstruction can be present"); + } + + if (JxlToJpegDecoder.XmlBoxContentSize(jpegData, ref this.reconstructionXmpSize) != Success) + { + throw new InvalidOperationException("Invalid jbrd XMP size"); + } + } + + this.boxStage = JxlBoxStage.Header; + + if ((this.eventsWanted & JpegReconstruction) != 0) + { + this.eventsWanted &= ~JpegReconstruction; + return JpegReconstruction; + } + } + else + { + return reconstructionResult; + } + } + else if (this.boxStage == JxlBoxStage.Skip) + { + if (this.boxContentsUnbounded) + { + if (this.inputClosed) + { + return Success; + } + + if (!this.boxOutBufferSet) + { + return Success; + } + + this.AdvanceInput(this.availableInput); + return NeedMoreInput; + } + + long remaining = this.boxContentsEnd - this.filePosition; + if (this.availableInput < remaining) + { + this.basicInfoSizeHint = InitialBasicInfoSizeHint() + this.boxContentsEnd - this.filePosition; + this.AdvanceInput(this.availableInput); + return NeedMoreInput; + } + else + { + this.AdvanceInput(remaining); + this.boxStage = JxlBoxStage.Header; + } } else { - return JxlSignature.Invalid; + throw new InvalidOperationException("Unreachable"); } } - // Container? - if (length >= 1 && buffer[0] == 0) + return Success; + } + + /// + /// Releases memory used by the JPEG output buffer. + /// + public void DisposeJpegBuffer() => this.jpegDecoder.DisposeOutputBuffer(); + + /// + /// Main core decoding routine. + /// + /// Thrown when data or input parameters are invalid. + public void DecodeInput() + { + if (this.decoderStage == JxlDecoderStage.Initialized) { - if (length < SignatureBox.Length) + this.decoderStage = JxlDecoderStage.Started; + } + + if (this.decoderStage == JxlDecoderStage.Error) + { + // Should NEVER occur! If it does make sure to always reset the decoder + // in the Decode method. + throw new InvalidOperationException("The core decoder cannot be used because it contains an error. A reset must be made."); + } + + if (!this.gotSignature) + { + JxlSignatureCheck status = CheckSignature(this.nextInput, this.availableInput); + if (status == JxlSignatureCheck.InvalidSignature) { - return JxlSignature.NotEnoughBytes; + throw new InvalidOperationException("The signature is invalid."); } - else if (buffer[SignatureBox.Length..].SequenceEqual(SignatureBox)) + + if (status == JxlSignatureCheck.NotEnoughBytes) { - position += SignatureBox.Length; - return JxlSignature.Container; + if (this.inputClosed) + { + throw new InvalidOperationException("The input is closed"); + } + + ThrowNotEnoughData(); + } + + this.gotSignature = true; + + if (status == JxlSignatureCheck.Container) + { + this.haveContainer = true; } else { - return JxlSignature.Invalid; + this.lastCodestreamSeen = true; } } - // Signature is invalid - return JxlSignature.Invalid; - } + int status = this.ProcessBoxes(); - private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length) - { - int position = 0; - return DetectSignature(buffer, length, ref position); - } + if (status == NeedMoreInput && this.inputClosed) + { + ThrowNotEnoughData(); + } - private static int BitsPerChannel(JxlDataType dataType) - => dataType switch + if (status == Success) { - JxlDataType.UInt8 => 8, - JxlDataType.UInt16 or JxlDataType.Float16 => 16, - JxlDataType.Float => 32, - _ => 0 - }; + if (this.CanUseMoreCodestreamInput()) + { + throw new InvalidOperationException("The code stream did not finish"); + } + + if (this.JbrdNeedsMoreBoxes()) + { + throw new InvalidOperationException("Missing metadata boxes for JPEG reconstruction"); + } + } + } protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) => throw new NotImplementedException(); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlBoxCodingMode.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlBoxCodingMode.cs new file mode 100644 index 0000000000..89f5af5e3f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlBoxCodingMode.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Specifies the type of box content. +/// +internal enum JxlBoxCodingMode : byte +{ + /// + /// Compress using Brotli codec. + /// + Brotli, + + /// + /// No compression (raw contents). + /// + Uncompressed +} diff --git a/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.cs b/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.cs index c5464c8d72..faa94dea40 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.cs @@ -3,6 +3,8 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Icc; +#pragma warning disable IDE0032 // Use auto property + /// /// Provides methods to read ICC data types /// @@ -25,6 +27,11 @@ internal sealed partial class IccDataReader public IccDataReader(byte[] data) => this.data = data ?? throw new ArgumentNullException(nameof(data)); + /// + /// Gets the reading position. + /// + public int Index => this.currentIndex; + /// /// Gets the length in bytes of the raw data /// diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs index 084ec388d6..d46a7332ba 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs @@ -53,7 +53,7 @@ public static IccTagDataEntry[] ReadTagData(byte[] data) return ReadTagData(reader); } - private static IccProfileHeader ReadHeader(IccDataReader reader) + internal static IccProfileHeader ReadHeader(IccDataReader reader) { reader.SetIndex(0); @@ -79,7 +79,7 @@ private static IccProfileHeader ReadHeader(IccDataReader reader) }; } - private static IccTagDataEntry[] ReadTagData(IccDataReader reader) + internal static IccTagDataEntry[] ReadTagData(IccDataReader reader) { IccTagTableEntry[] tagTable = ReadTagTable(reader); List entries = new(tagTable.Length); From e03a581fc73652ce503b5275f1e17211ec4d1df9 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 17 Aug 2026 18:31:55 +0400 Subject: [PATCH 078/142] Add JPEG XL container prototype & file type box JxlBoxHeader contains a header for boxes in the JPEG XL container. JxlFileTypeBox represents the ftyp box. BinaryUtils contains helper methods to read/write primitives from/to Stream in custom endianness --- .../Formats/Jxl/IO/BinaryUtils.Generated.cs | 321 ++++++++++++++++++ src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt | 86 +++++ .../Formats/Jxl/IO/Container/JxlBoxHeader.cs | 132 +++++++ .../Jxl/IO/Container/JxlFileTypeBox.cs | 58 ++++ 4 files changed, 597 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt create mode 100644 src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Container/JxlFileTypeBox.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs new file mode 100644 index 0000000000..055538df7a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs @@ -0,0 +1,321 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + + +/// +/// Reads primitives from streams with correct endianness. +/// +// TODO: move this class into the IO or Common folder? +internal static class BinaryUtils +{ + /// + /// Reads a + /// from the specified stream in little-endian order. + /// + /// The stream where the will be read from. + /// + public static Int16 ReadInt16LittleEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(Int16)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadInt16LittleEndian(data); + } + + /// + /// Reads a + /// from the specified stream in big-endian order. + /// + /// The stream where the will be read from. + /// + public static Int16 ReadInt16BigEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(Int16)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadInt16BigEndian(data); + } + + /// + /// Writes a + /// into the specified stream in little-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteInt16LittleEndian(Stream stream, Int16 value) + { + Span data = stackalloc byte[sizeof(Int16)]; + BinaryPrimitives.WriteInt16LittleEndian(data, value); + stream.Write(data); + } + + /// + /// Writes a + /// into the specified stream in big-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteInt16BigEndian(Stream stream, Int16 value) + { + Span data = stackalloc byte[sizeof(Int16)]; + BinaryPrimitives.WriteInt16BigEndian(data, value); + stream.Write(data); + } + /// + /// Reads a + /// from the specified stream in little-endian order. + /// + /// The stream where the will be read from. + /// + public static UInt16 ReadUInt16LittleEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(UInt16)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadUInt16LittleEndian(data); + } + + /// + /// Reads a + /// from the specified stream in big-endian order. + /// + /// The stream where the will be read from. + /// + public static UInt16 ReadUInt16BigEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(UInt16)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadUInt16BigEndian(data); + } + + /// + /// Writes a + /// into the specified stream in little-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteUInt16LittleEndian(Stream stream, UInt16 value) + { + Span data = stackalloc byte[sizeof(UInt16)]; + BinaryPrimitives.WriteUInt16LittleEndian(data, value); + stream.Write(data); + } + + /// + /// Writes a + /// into the specified stream in big-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteUInt16BigEndian(Stream stream, UInt16 value) + { + Span data = stackalloc byte[sizeof(UInt16)]; + BinaryPrimitives.WriteUInt16BigEndian(data, value); + stream.Write(data); + } + /// + /// Reads a + /// from the specified stream in little-endian order. + /// + /// The stream where the will be read from. + /// + public static Int32 ReadInt32LittleEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(Int32)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadInt32LittleEndian(data); + } + + /// + /// Reads a + /// from the specified stream in big-endian order. + /// + /// The stream where the will be read from. + /// + public static Int32 ReadInt32BigEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(Int32)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadInt32BigEndian(data); + } + + /// + /// Writes a + /// into the specified stream in little-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteInt32LittleEndian(Stream stream, Int32 value) + { + Span data = stackalloc byte[sizeof(Int32)]; + BinaryPrimitives.WriteInt32LittleEndian(data, value); + stream.Write(data); + } + + /// + /// Writes a + /// into the specified stream in big-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteInt32BigEndian(Stream stream, Int32 value) + { + Span data = stackalloc byte[sizeof(Int32)]; + BinaryPrimitives.WriteInt32BigEndian(data, value); + stream.Write(data); + } + /// + /// Reads a + /// from the specified stream in little-endian order. + /// + /// The stream where the will be read from. + /// + public static UInt32 ReadUInt32LittleEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(UInt32)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadUInt32LittleEndian(data); + } + + /// + /// Reads a + /// from the specified stream in big-endian order. + /// + /// The stream where the will be read from. + /// + public static UInt32 ReadUInt32BigEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(UInt32)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadUInt32BigEndian(data); + } + + /// + /// Writes a + /// into the specified stream in little-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteUInt32LittleEndian(Stream stream, UInt32 value) + { + Span data = stackalloc byte[sizeof(UInt32)]; + BinaryPrimitives.WriteUInt32LittleEndian(data, value); + stream.Write(data); + } + + /// + /// Writes a + /// into the specified stream in big-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteUInt32BigEndian(Stream stream, UInt32 value) + { + Span data = stackalloc byte[sizeof(UInt32)]; + BinaryPrimitives.WriteUInt32BigEndian(data, value); + stream.Write(data); + } + /// + /// Reads a + /// from the specified stream in little-endian order. + /// + /// The stream where the will be read from. + /// + public static Int64 ReadInt64LittleEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(Int64)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadInt64LittleEndian(data); + } + + /// + /// Reads a + /// from the specified stream in big-endian order. + /// + /// The stream where the will be read from. + /// + public static Int64 ReadInt64BigEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(Int64)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadInt64BigEndian(data); + } + + /// + /// Writes a + /// into the specified stream in little-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteInt64LittleEndian(Stream stream, Int64 value) + { + Span data = stackalloc byte[sizeof(Int64)]; + BinaryPrimitives.WriteInt64LittleEndian(data, value); + stream.Write(data); + } + + /// + /// Writes a + /// into the specified stream in big-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteInt64BigEndian(Stream stream, Int64 value) + { + Span data = stackalloc byte[sizeof(Int64)]; + BinaryPrimitives.WriteInt64BigEndian(data, value); + stream.Write(data); + } + /// + /// Reads a + /// from the specified stream in little-endian order. + /// + /// The stream where the will be read from. + /// + public static UInt64 ReadUInt64LittleEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(UInt64)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadUInt64LittleEndian(data); + } + + /// + /// Reads a + /// from the specified stream in big-endian order. + /// + /// The stream where the will be read from. + /// + public static UInt64 ReadUInt64BigEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(UInt64)]; + stream.ReadExactly(data); + return BinaryPrimitives.ReadUInt64BigEndian(data); + } + + /// + /// Writes a + /// into the specified stream in little-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteUInt64LittleEndian(Stream stream, UInt64 value) + { + Span data = stackalloc byte[sizeof(UInt64)]; + BinaryPrimitives.WriteUInt64LittleEndian(data, value); + stream.Write(data); + } + + /// + /// Writes a + /// into the specified stream in big-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void WriteUInt64BigEndian(Stream stream, UInt64 value) + { + Span data = stackalloc byte[sizeof(UInt64)]; + BinaryPrimitives.WriteUInt64BigEndian(data, value); + stream.Write(data); + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt new file mode 100644 index 0000000000..c83e9e4c4b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt @@ -0,0 +1,86 @@ +<#@ template language="C#" #> +<#@ import namespace="System" #> +<#@ import namespace="System.IO" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ output extension=".Generated.cs" #> +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO; + +<# + List types = [ + typeof(short), + typeof(ushort), + typeof(int), + typeof(uint), + typeof(long), + typeof(ulong) + ]; +#> + +/// +/// Reads primitives from streams with correct endianness. +/// +// TODO: move this class into the IO or Common folder? +internal static class BinaryUtils +{ +<# + foreach (Type type in types) + { +#> + /// + /// Reads a + /// from the specified stream in little-endian order. + /// + /// The stream where the will be read from. + /// + public static <#= type.Name #> Read<#= type.Name#>LittleEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(<#= type.Name #>)]; + stream.ReadExactly(data); + return BinaryPrimitives.Read<#= type.Name #>LittleEndian(data); + } + + /// + /// Reads a + /// from the specified stream in big-endian order. + /// + /// The stream where the will be read from. + /// + public static <#= type.Name #> Read<#= type.Name#>BigEndian(Stream stream) + { + Span data = stackalloc byte[sizeof(<#= type.Name #>)]; + stream.ReadExactly(data); + return BinaryPrimitives.Read<#= type.Name #>BigEndian(data); + } + + /// + /// Writes a + /// into the specified stream in little-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void Write<#= type.Name #>LittleEndian(Stream stream, <#= type.Name #> value) + { + Span data = stackalloc byte[sizeof(<#= type.Name #>)]; + BinaryPrimitives.Write<#= type.Name #>LittleEndian(data, value); + stream.Write(data); + } + + /// + /// Writes a + /// into the specified stream in big-endian order. + /// + /// The stream where the will be written to. + /// Value which will be written to the stream. + public static void Write<#= type.Name #>BigEndian(Stream stream, <#= type.Name #> value) + { + Span data = stackalloc byte[sizeof(<#= type.Name #>)]; + BinaryPrimitives.Write<#= type.Name #>BigEndian(data, value); + stream.Write(data); + } +<# } #> +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs new file mode 100644 index 0000000000..d39f3c7e75 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs @@ -0,0 +1,132 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using System.Runtime.InteropServices; +using System.Text; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Container; + +/// +/// Header for JPEG XL container format. +/// +[StructLayout(LayoutKind.Sequential, Size = 16)] +internal struct JxlBoxHeader +{ + /// + /// Box size in bytes. + /// + public ulong Size; + + /// + /// Type of the box. + /// + public uint Type; + + /// + /// True if the size field extends until the end of the file. + /// + public bool SizeExtendsTillEnd; + + /// + /// Initializes a new instance of the struct. + /// + /// The size of the box. + /// The type of the box. + /// Does the box size extend till the end of the file? + public JxlBoxHeader(ulong size, uint type, bool sizeExtendsTillEnd) + { + this.Size = size; + this.Type = type; + this.SizeExtendsTillEnd = sizeExtendsTillEnd; + } + + /// + /// Converts a 4-character ASCII string (e.g. "jxlc") into a uint type code. + /// + /// Input type string to convert + /// Unsigned integer representation of the type string + public static uint TypeFromString(string typeString) + { + if (typeString.Length != 4) + { + throw new ArgumentException("Box type must be exactly 4 characters", nameof(typeString)); + } + + Span buffer = stackalloc byte[4]; + _ = Encoding.ASCII.GetBytes(typeString, buffer); + + return BinaryPrimitives.ReadUInt32BigEndian(buffer); + } + + /// + /// Converts a uint type code back into a 4-character ASCII string. + /// + /// Unsigned integer representation of the type string + /// The string representing the type code. + public static string TypeToString(uint typeCode) + { + Span buffer = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32BigEndian(buffer, typeCode); + + return Encoding.ASCII.GetString(buffer); + } + + /// + /// Parses the JPEG XL box header. + /// + /// A stream to parse the header from. + /// The box header. + /// Thrown when the header is invalid. + public static JxlBoxHeader ReadHeader(Stream stream) + { + ulong size = BinaryUtils.ReadUInt32BigEndian(stream); + bool haveSize64 = false; + + if (size == 1) + { + // When the size value is equal to 1, a new 64-bit + // size field follows. + haveSize64 = true; + size = BinaryUtils.ReadUInt64BigEndian(stream); + } + + // Read the 4-byte type field. + uint type = BinaryUtils.ReadUInt32BigEndian(stream); + + if (haveSize64) + { + // When the 64-bit largesize was read, + // the size cannot proceed till the end of the file. + if (size is 0 or 1) + { + throw new InvalidOperationException("Large size cannot have another large size or extend till the end of the file"); + } + + return new JxlBoxHeader(size, type, sizeExtendsTillEnd: false); + } + else + { + return new JxlBoxHeader(size, type, sizeExtendsTillEnd: size == 0); + } + } + + /// + /// Writes the box header to the specified stream. + /// + /// The stream to write the box header to. + public readonly void WriteHeader(Stream writer) + { + if (this.Size is > uint.MaxValue or 1) + { + BinaryUtils.WriteUInt32BigEndian(writer, 1); // Indicates a large size is present + BinaryUtils.WriteUInt64BigEndian(writer, this.Size); + } + else + { + BinaryUtils.WriteUInt32BigEndian(writer, (uint)this.Size); + } + + BinaryUtils.WriteUInt32BigEndian(writer, this.Type); + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Container/JxlFileTypeBox.cs b/src/ImageSharp/Formats/Jxl/IO/Container/JxlFileTypeBox.cs new file mode 100644 index 0000000000..ca2adb1ca4 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Container/JxlFileTypeBox.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Container; + +/// +/// A ftyp box payload. +/// +internal sealed class JxlFileTypeBox(string majorBrand, uint minorVersion) +{ + /// + /// Gets or sets the primary format. Has to be "jxl " for JPEG XL. + /// + public string MajorBrand { get; set; } = majorBrand; + + /// + /// Gets or sets the revision of the major brand. + /// + public uint MinorVersion { get; set; } = minorVersion; + + /// + /// Gets or sets the list of other brands the file is compatible with. + /// + public List CompatibleBrands { get; set; } = []; + + public int GetPayloadSize() => 8 + (this.CompatibleBrands.Count * 4); + + public static JxlFileTypeBox Parse(Stream stream, ulong boxSize) + { + string majorBrand = JxlBoxHeader.TypeToString(BinaryUtils.ReadUInt32BigEndian(stream)); + uint minorVersion = BinaryUtils.ReadUInt32BigEndian(stream); + boxSize -= 8; + + List compatibleBrands = []; + for (ulong i = 0; i < boxSize; i += 4) + { + compatibleBrands.Add(JxlBoxHeader.TypeToString(BinaryUtils.ReadUInt32BigEndian(stream))); + } + + JxlFileTypeBox ftyp = new(majorBrand, minorVersion) + { + CompatibleBrands = compatibleBrands + }; + + return ftyp; + } + + public void WritePayload(Stream stream) + { + BinaryUtils.WriteUInt32BigEndian(stream, JxlBoxHeader.TypeFromString(this.MajorBrand)); + BinaryUtils.WriteUInt32BigEndian(stream, this.MinorVersion); + + foreach (string compatibleBrand in this.CompatibleBrands) + { + BinaryUtils.WriteUInt32BigEndian(stream, JxlBoxHeader.TypeFromString(compatibleBrand)); + } + } +} From 2d12bc6b4aec1f3edb55634daa39e0299e27bb72 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 18 Aug 2026 20:31:34 +0400 Subject: [PATCH 079/142] Zero-copy input access (prototype 1) --- .../Formats/Jxl/IO/Container/JxlBoxHeader.cs | 13 ++- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 102 +++++------------- 2 files changed, 34 insertions(+), 81 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs index d39f3c7e75..dc5eb13513 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs @@ -28,17 +28,24 @@ internal struct JxlBoxHeader /// public bool SizeExtendsTillEnd; + /// + /// True if the size is 64-bit. + /// + public bool ContainsLargeSize; + /// /// Initializes a new instance of the struct. /// /// The size of the box. /// The type of the box. /// Does the box size extend till the end of the file? - public JxlBoxHeader(ulong size, uint type, bool sizeExtendsTillEnd) + /// Is there a 64-bit size field? + public JxlBoxHeader(ulong size, uint type, bool sizeExtendsTillEnd, bool containsLargeSize) { this.Size = size; this.Type = type; this.SizeExtendsTillEnd = sizeExtendsTillEnd; + this.ContainsLargeSize = containsLargeSize; } /// @@ -103,11 +110,11 @@ public static JxlBoxHeader ReadHeader(Stream stream) throw new InvalidOperationException("Large size cannot have another large size or extend till the end of the file"); } - return new JxlBoxHeader(size, type, sizeExtendsTillEnd: false); + return new JxlBoxHeader(size, type, sizeExtendsTillEnd: false, containsLargeSize: true); } else { - return new JxlBoxHeader(size, type, sizeExtendsTillEnd: size == 0); + return new JxlBoxHeader(size, type, sizeExtendsTillEnd: size == 0, containsLargeSize: false); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index e984b36b93..be5f1d335a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.IO; +using SixLabors.ImageSharp.Formats.Jxl.IO.Container; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.IO; @@ -1539,36 +1540,29 @@ private bool ReadBundle(Span data, JxlBitReader br, T bundle) /// Status of the parsing. /// Thrown if the data is incorrect. /// Thrown if the data is malformed. - public bool ReadBasicInfo() + public bool ReadBasicInfo(Stream stream) { if (!this.gotCodestreamSignature) { - Span span = this.GetCodeStreamSpan(); - - if (span.Length < 2) - { - return this.TryRequestMoreInput(); - } + Span fileSignature = stackalloc byte[2]; + stream.ReadExactly(fileSignature); - if (span[0] != 0xFF || span[1] != CodestreamMarker) + if (fileSignature[0] != 0xFF || fileSignature[1] != CodestreamMarker) { throw new InvalidOperationException("The file signature is invalid"); } this.gotCodestreamSignature = true; - this.AdvanceCodeStream(2); } - Span sp = this.GetCodeStreamSpan(); - - JxlBitReader bitReader = new(sp); + JxlBitReader bitReader = new(stream); - if (!this.ReadBundle(sp, bitReader, this.metadata!.Size!)) + if (!this.ReadBundle(stream, bitReader, this.metadata!.Size!)) { throw new InvalidDataException("Could not parse the size header"); } - if (!this.ReadBundle(sp, bitReader, this.metadata!.ImageMetadata!)) + if (!this.ReadBundle(stream, bitReader, this.metadata!.ImageMetadata!)) { throw new InvalidDataException("Could not parse the image metadata"); } @@ -1924,8 +1918,7 @@ public int ProcessCodestream() if (this.skippingFrame) { - bool referenceable = this.frameHeader.CanBeReferenced - || this.frameHeader.FrameType == JxlFrameType.DcFrame; + bool referenceable = this.frameHeader.CanBeReferenced || this.frameHeader.FrameType == JxlFrameType.DcFrame; if (internalFrameIndex < this.frameRequired.Count && this.frameRequired[internalFrameIndex] == 0) { @@ -2182,63 +2175,18 @@ public void SetJpegBuffer(Memory data) /// Parses the start of a box. /// /// Input bytes to parse from. - /// Size of remaining input bytes. - /// Offset of input bytes. - /// File offset. - /// Type of the parsed box. + /// Type of the box /// Output box size. /// Output header size. - /// - /// True if the parsing went fine. False if the parsing requests - /// more input bytes. - /// /// /// Thrown when data is invalid. /// - private static bool ParseBoxHeader(Span input, long size, long pos, long filePos, JxlBoxType type, out long boxSize, out long headerSize) + private static void ParseBoxHeader(Stream input, out JxlBoxType type, out long boxSize, out long headerSize) { - boxSize = 0; - headerSize = 0; - - if (IsOutOfBounds((int)pos, 8, (int)size)) - { - headerSize = 8; - return false; - } - - long boxStart = pos; - boxSize = BinaryPrimitives.ReadInt32BigEndian(input[(int)pos..]); - pos += 4; - type = (JxlBoxType)BitConverter.ToInt32(input.Slice((int)pos, 4)); - pos += 4; - - if (boxSize == 1) - { - headerSize = 16; - - if (IsOutOfBounds((int)pos, 8, (int)size)) - { - return false; - } - - long boxSize64 = BinaryPrimitives.ReadInt64BigEndian(input[(int)pos..]); - pos += 8; - boxSize = boxSize64; - } - - headerSize = pos - boxStart; - - if (boxSize > 0 && boxSize < headerSize) - { - throw new InvalidOperationException("Invalid box size"); - } - - if (filePos + boxSize < filePos) - { - throw new InvalidOperationException("Box size overflow"); - } - - return true; + JxlBoxHeader header = JxlBoxHeader.ReadHeader(input); + boxSize = (long)header.Size; + headerSize = (header.ContainsLargeSize ? 12 : 4) + 4; + type = (JxlBoxType)header.Type; } /// @@ -2246,14 +2194,14 @@ private static bool ParseBoxHeader(Span input, long size, long pos, long f /// /// Status of processing. /// Thrown when data is invalid. - public int ProcessBoxes() + public int ProcessBoxes(Stream stream) { // We have a box handling loop here. while (true) { if (this.boxStage != JxlBoxStage.Header) { - this.AdvanceInput(this.headerSize); + // this.AdvanceInput(this.headerSize); this.headerSize = 0; if ((this.eventsWanted & Box) != 0 && this.boxEvent && !this.boxOutBufferSetCurrentBox) @@ -2582,20 +2530,18 @@ public int ProcessBoxes() return NeedMoreInput; } - Span nextSpan = this.nextInput!.Memory.Span; - if (!(nextSpan[0] == 'j' && nextSpan[1] == 'x' && nextSpan[2] == 'l' && nextSpan[3] == ' ')) + if (BinaryUtils.ReadInt32BigEndian(stream) != 0x6A786C20) // Bytes "jxl " in Big Endian { throw new InvalidOperationException("File type box major brand must be \"jxl \""); } - uint version = BinaryPrimitives.ReadUInt32BigEndian(nextSpan[4..]); + uint version = BinaryUtils.ReadUInt32BigEndian(stream); if (version > 1) { throw new InvalidOperationException("Unknown JXL file format version " + version + ", known versions are 0 and 1"); } this.jxlFileFormatVersion = (int)version; - this.AdvanceInput(8); this.boxStage = JxlBoxStage.Skip; } else if (this.boxStage == JxlBoxStage.PartialCodeStream) @@ -2615,7 +2561,7 @@ public int ProcessBoxes() throw new InvalidOperationException("jxlp box is too small to contain an index"); } - uint jxlpIndex = BinaryPrimitives.ReadUInt32BigEndian(this.nextInput!.Memory.Span); + uint jxlpIndex = BinaryUtils.ReadUInt32BigEndian(stream); uint counter = jxlpIndex & 0x7FFFFFFFu; bool isLast = (jxlpIndex & 0x80000000u) != 0; @@ -2624,8 +2570,6 @@ public int ProcessBoxes() throw new InvalidOperationException("jxlp box index " + counter + " is a duplicate (already processed)"); } - this.AdvanceInput(4); - if (counter == this.nextJxlpIndex) { this.nextJxlpIndex++; @@ -2720,9 +2664,11 @@ public int ProcessBoxes() return Error; } - entry!.CodestreamBytes.Write(this.nextInput!.Memory.Span[..(int)remaining]); + // Now we want to write the 'remaining' number of bytes + // from input into the codestream. + using IMemoryOwner buffer = this.Options.Configuration.MemoryAllocator.Allocate((int)remaining); + entry!.CodestreamBytes.Write(buffer.Memory.Span); this.jxlpOooBufferTotal += remaining; - this.AdvanceInput(remaining); bool boxDone = !this.boxContentsUnbounded && this.filePosition >= this.boxContentsEnd; From 08c13fc028e406d4cef26979aba616204186990b Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 18 Aug 2026 20:51:03 +0400 Subject: [PATCH 080/142] Performance improvements --- .../Formats/Jxl/IO/BinaryUtils.Generated.cs | 1 - src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt | 1 - .../Formats/Jxl/IO/Container/JxlBoxHeader.cs | 28 ++++++++++++++++--- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs index 055538df7a..a5b07c45f6 100644 --- a/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs +++ b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs @@ -5,7 +5,6 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO; - /// /// Reads primitives from streams with correct endianness. /// diff --git a/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt index c83e9e4c4b..ba98aca4ad 100644 --- a/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt +++ b/src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt @@ -20,7 +20,6 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO; typeof(ulong) ]; #> - /// /// Reads primitives from streams with correct endianness. /// diff --git a/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs index dc5eb13513..93c0e992bc 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers.Binary; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -13,6 +14,18 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Container; [StructLayout(LayoutKind.Sequential, Size = 16)] internal struct JxlBoxHeader { + private static readonly Dictionary KnownTypeCodes = new() + { + { 0x6A786C20, "jxl " }, + { 0x6A786C70, "jxlp" }, + { 0x6A786C63, "jxlc" }, + { 0x66747970, "ftyp" }, + { 0x6A627264, "jbrd" }, + { 0x45786966, "Exif" }, + { 0x786D6C20, "xml " }, + { 0x6A756D62, "jumb" } + }; + /// /// Box size in bytes. /// @@ -53,6 +66,7 @@ public JxlBoxHeader(ulong size, uint type, bool sizeExtendsTillEnd, bool contain /// /// Input type string to convert /// Unsigned integer representation of the type string + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint TypeFromString(string typeString) { if (typeString.Length != 4) @@ -60,10 +74,10 @@ public static uint TypeFromString(string typeString) throw new ArgumentException("Box type must be exactly 4 characters", nameof(typeString)); } - Span buffer = stackalloc byte[4]; - _ = Encoding.ASCII.GetBytes(typeString, buffer); - - return BinaryPrimitives.ReadUInt32BigEndian(buffer); + return ((uint)typeString[0] << 24) | + ((uint)typeString[1] << 16) | + ((uint)typeString[2] << 8) | + typeString[3]; } /// @@ -73,6 +87,12 @@ public static uint TypeFromString(string typeString) /// The string representing the type code. public static string TypeToString(uint typeCode) { + if (KnownTypeCodes.TryGetValue(typeCode, out string? str)) + { + return str; + } + + // The box type is not known Span buffer = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(buffer, typeCode); From 6ba0d37002506726f10c816c1c81add9439be242 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 18 Aug 2026 20:56:11 +0400 Subject: [PATCH 081/142] Zero-copy input access (prototype 2) --- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index be5f1d335a..6466c82264 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Buffers.Binary; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Fields; @@ -733,8 +732,9 @@ private struct JxlDecoderFrameIndexBoxEntry // where this.frameReferences = JxlFrameReference[]. private sealed class JxlFrameReference(int reference, int savedAs) { - public int Reference = reference; - public int SavedAs = savedAs; + public int Reference { get; set; } = reference; + + public int SavedAs { get; set; } = savedAs; } /// @@ -812,27 +812,24 @@ private static int InitialBasicInfoSizeHint() return containerHeaderSize + maxCodestreamBasicInfoSize; } - private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length, ref int position) + private static JxlSignature DetectSignature(Stream buffer) { - if (position >= length) + int firstByte = buffer.ReadByte(); + if (firstByte == -1) { - return JxlSignature.NotEnoughBytes; + throw new EndOfStreamException(); } - buffer = buffer[position..]; - length -= position; - - // 0xFF 0x0A represents a codestream - if (length >= 1 && buffer[0] == 0xFF) + if (firstByte == 0xFF) { - if (length < 2) + int secondByte = buffer.ReadByte(); + if (secondByte == -1) { - // We need at least two bytes for a valid codestream signature - return JxlSignature.NotEnoughBytes; + throw new EndOfStreamException(); } - else if (buffer[1] == CodestreamMarker) + + if (secondByte == CodestreamMarker) { - position += 2; return JxlSignature.CodeStream; } else @@ -842,15 +839,13 @@ private static JxlSignature DetectSignature(ReadOnlySpan buffer, int lengt } // Container? - if (length >= 1 && buffer[0] == 0) + if (firstByte == 0) { - if (length < SignatureBox.Length) - { - return JxlSignature.NotEnoughBytes; - } - else if (buffer[SignatureBox.Length..].SequenceEqual(SignatureBox)) + Span signatureBox = stackalloc byte[JxlShared.SignatureBox.Length]; + buffer.ReadExactly(signatureBox); + + if (signatureBox.SequenceEqual(JxlShared.SignatureBox)) { - position += SignatureBox.Length; return JxlSignature.Container; } else From c53589347240c2ed797d8da3656686560a7b57e6 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:34:14 +0400 Subject: [PATCH 082/142] Zero-copy input access (prototype 3) --- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 6466c82264..d1edd36b9d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -858,12 +858,6 @@ private static JxlSignature DetectSignature(Stream buffer) return JxlSignature.Invalid; } - private static JxlSignature DetectSignature(ReadOnlySpan buffer, int length) - { - int position = 0; - return DetectSignature(buffer, length, ref position); - } - private static int BitsPerChannel(JxlDataType dataType) => dataType switch { @@ -1146,9 +1140,8 @@ public bool TryRequestMoreInput() return false; } - this.codestreamCopy.Write(this.nextInput!.Memory.Span[..(int)avail]); - - this.AdvanceInput(avail); + using IMemoryOwner codestreamPending = this.Options.Configuration.MemoryAllocator.Allocate((int)avail); + this.codestreamCopy.Write(codestreamPending.Memory.Span); } else { @@ -1170,7 +1163,7 @@ public bool TryRequestMoreInput() { long avail = this.AvailableCodeStream(); long skip = Math.Min(this.codestreamPos, avail); - this.AdvanceInput(skip); + this.Skip(skip); this.codestreamPos -= skip; if (this.codestreamPos > 0) @@ -2235,7 +2228,8 @@ public int ProcessBoxes(Stream stream) if (this.storeExif == 1 || this.storeXmp == 1) { - IMemoryOwner metadata = (this.storeExif == 1 ? this.exifMetadata : this.xmpMetadata) ?? throw new InvalidOperationException("Metadata is missing, but should be present"); + IMemoryOwner metadata = (this.storeExif == 1 ? this.exifMetadata : this.xmpMetadata) + ?? throw new InvalidOperationException("Metadata is missing, but should be present"); // Boxes should not contain more than 64MiB data. const long blockSizeLimit = 64L << 20; From 75f676d3ad84b902863ed1579b64384272add68e Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 24 Aug 2026 07:16:13 +0400 Subject: [PATCH 083/142] Add prototype of modular - Added Reversible Color Transform (RCT) - Added encoder ANS histograms - Added incomplete context predictor - Added speed tier - Added image format detector - Added missing types - Simplified JxlBoxHeader --- .../Formats/Jxl/IO/Container/JxlBoxHeader.cs | 39 ++-- src/ImageSharp/Formats/Jxl/JxlFormat.cs | 9 +- .../Formats/Jxl/JxlImageFormatDetector.cs | 49 +++++ .../Jxl/Processing/Decoder/JxlFrameDecoder.cs | 109 ++++++++++ .../Decoder/JxlProgressiveDetail.cs | 47 +++++ .../Jxl/Processing/Decoder/JxlSectionInfo.cs | 13 ++ .../Processing/Decoder/JxlSectionStatus.cs | 30 +++ .../Jxl/Processing/Decoder/JxlTocEntry.cs | 11 + .../Encoder/Ans/JxlAnsHistogramStrategy.cs | 25 +++ .../Encoder/Ans/JxlClusteringType.cs | 25 +++ .../Processing/Encoder/Ans/JxlHistogram.cs | 111 ++++++++++ .../Encoder/Ans/JxlHistogramParameters.cs | 113 +++++++++++ .../Encoder/Ans/JxlHybridUIntMethod.cs | 35 ++++ .../Processing/Encoder/Ans/JxlLz77Method.cs | 90 ++++++++ .../Jxl/Processing/JxlAcStrategyImage.cs | 2 +- .../Formats/Jxl/Processing/JxlOverride.cs | 26 +++ .../Jxl/Processing/JxlOverrideHelpers.cs | 43 ++++ .../Jxl/Processing/JxlPassesSharedState.cs | 7 +- .../Formats/Jxl/Processing/JxlQuantizer.cs | 40 ++-- .../Formats/Jxl/Processing/JxlSpeedTier.cs | 76 +++++++ .../ContextPrediction/JxlContextPrediction.cs | 97 +++++++++ .../ContextPrediction/JxlFlatDecisionNode.cs | 19 ++ .../ContextPrediction/JxlMaTreeLookup.cs | 29 +++ .../JxlMaTreeLookupResult.cs | 9 + .../ContextPrediction/JxlModularHeader.cs | 140 +++++++++++++ .../JxlModularMultiplierInfo.cs | 17 ++ .../ContextPrediction/JxlModularState.cs | 192 ++++++++++++++++++ .../ContextPrediction/JxlPredictor.cs | 34 ++++ .../ContextPrediction/JxlPredictorFacts.cs | 25 +++ .../Encoding/ContextPrediction/JxlTreeKind.cs | 20 ++ .../Encoding/ContextPrediction/JxlTreeMode.cs | 30 +++ .../Processing/Modular/JxlModularChannel.cs | 77 +++++++ .../Jxl/Processing/Modular/JxlModularImage.cs | 11 + .../Processing/Modular/Transforms/JxlRct.cs | 115 +++++++++++ .../Transforms/JxlSqueezeParameters.cs | 74 +++++++ .../Modular/Transforms/JxlTransform.cs | 11 + .../Modular/Transforms/JxlTransformType.cs | 30 +++ 37 files changed, 1779 insertions(+), 51 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlProgressiveDetail.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionInfo.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionStatus.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTocEntry.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsHistogramStrategy.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlClusteringType.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogramParameters.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHybridUIntMethod.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlLz77Method.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlOverride.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlOverrideHelpers.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlSpeedTier.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlFlatDecisionNode.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlMaTreeLookup.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlMaTreeLookupResult.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularMultiplierInfo.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictor.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictorFacts.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlTreeKind.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlTreeMode.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueezeParameters.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransformType.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs b/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs index 93c0e992bc..8e3600bb5e 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs @@ -14,18 +14,6 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Container; [StructLayout(LayoutKind.Sequential, Size = 16)] internal struct JxlBoxHeader { - private static readonly Dictionary KnownTypeCodes = new() - { - { 0x6A786C20, "jxl " }, - { 0x6A786C70, "jxlp" }, - { 0x6A786C63, "jxlc" }, - { 0x66747970, "ftyp" }, - { 0x6A627264, "jbrd" }, - { 0x45786966, "Exif" }, - { 0x786D6C20, "xml " }, - { 0x6A756D62, "jumb" } - }; - /// /// Box size in bytes. /// @@ -87,16 +75,27 @@ public static uint TypeFromString(string typeString) /// The string representing the type code. public static string TypeToString(uint typeCode) { - if (KnownTypeCodes.TryGetValue(typeCode, out string? str)) + return typeCode switch { - return str; - } - - // The box type is not known - Span buffer = stackalloc byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(buffer, typeCode); + 0x6A786C20 => "jxl ", + 0x6A786C70 => "jxlp", + 0x6A786C63 => "jxlc", + 0x66747970 => "ftyp", + 0x6A627264 => "jbrd", + 0x45786966 => "Exif", + 0x786D6C20 => "xml ", + 0x6A756D62 => "jumb", + _ => Fallback(typeCode) + }; + + static string Fallback(uint typeCode) + { + // The box type is not known + Span buffer = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32BigEndian(buffer, typeCode); - return Encoding.ASCII.GetString(buffer); + return Encoding.ASCII.GetString(buffer); + } } /// diff --git a/src/ImageSharp/Formats/Jxl/JxlFormat.cs b/src/ImageSharp/Formats/Jxl/JxlFormat.cs index 00e7b66eb5..f5c23ad685 100644 --- a/src/ImageSharp/Formats/Jxl/JxlFormat.cs +++ b/src/ImageSharp/Formats/Jxl/JxlFormat.cs @@ -3,13 +3,20 @@ namespace SixLabors.ImageSharp.Formats.Jxl; -internal class JxlFormat : IImageFormat +/// +/// JPEG XL format +/// +public sealed 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/JxlImageFormatDetector.cs b/src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs new file mode 100644 index 0000000000..9b968bd1e3 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats.Jxl; + +/// +/// Checks if the first few bytes of a file represent +/// JPEG XL. +/// +public sealed class JxlImageFormatDetector : IImageFormatDetector +{ + /// + /// Gets file signature bytes which represent a container-based + /// JPEG XL file. + /// + private static ReadOnlySpan ContainerStart => + [ + 0x00, 0x00, 0x00, 0x0C, + 0x4A, 0x58, 0x4C, 0x20, + 0x0D, 0x0A, 0x87, 0x0A, + ]; + + /// + public int HeaderSize => 12; + + /// + public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) + { + if (header[0] == 0xFF && header[1] == 0x0A) + { + // Just codestream. + format = new JxlFormat(); + return true; + } + else if (header.SequenceEqual(ContainerStart)) + { + // Container format. + format = new JxlFormat(); + return true; + } + else + { + format = null; + return false; + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs new file mode 100644 index 0000000000..b17aacd5cb --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs @@ -0,0 +1,109 @@ +// 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; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal sealed class JxlFrameDecoder +{ + private JxlPassesDecoderState decoderState; + private List toc = []; + private ulong sectionSizesSum; + private JxlFrameHeader frameHeader; + private JxlFrameDimensions frameDimensions; + private JxlImageBundle decoded; + private JxlModularFrameDecoder modularFrameDecoder; + private bool renderSpotcolors = true; + private bool coalescing = true; + private List processedSection = []; + private List decodedPassesPerAcGroup = []; + private List decodedDcGroups = []; + private bool decodedDcGlobal; + private bool decodedAcGlobal; + private bool finalizedDc = true; + private long numSectionsDone; + private bool isFinalized = true; + private bool allocated; + private List groupDecoderCaches = []; + private bool useTaskId; + private bool useSlowRenderingPipeline; + private JxlProgressiveDetail progressiveDetail = JxlProgressiveDetail.Frames; + private List passesToPause = []; + + public static void DecodeGlobalDcInfo(Configuration configuration, JxlBitReader reader, bool isJpeg, JxlPassesDecoderState state) + { + state.SharedStorage.Quantizer.Decode(reader); + + if (!JxlEntropyCoder.DecodeBlockContextMap(configuration, reader, ref state.SharedStorage.BlockContextMap)) + { + throw new InvalidOperationException("Could not decode block context map"); + } + + if (!state.SharedStorage.ColorMap.DecodeDc(reader)) + { + throw new InvalidOperationException("Could not decode DC color correlation map"); + } + + if (isJpeg) + { + state.SharedStorage.Quantizer.ClearDcMultipliers(); + } + + state.SharedStorage.AcStrategy.FillInvalid(); + } + + public static void DecodeFrame(JxlPassesDecoderState decoderState, Stream stream, ref JxlFrameHeader header, JxlImageBundle decoded, JxlCodecMetadata metadata, bool useSlowRenderingPipeline) + { + JxlFrameDecoder frameDecoder = new(decoderState, metadata, useSlowRenderingPipeline); + JxlBitReader reader = new(stream); + + if (!frameDecoder.InitializeFrame(reader, decoded, isPreview: false)) + { + throw new InvalidOperationException("Frame initialization failed"); + } + + if (!frameDecoder.InitializeFrameOutput()) + { + throw new InvalidOperationException("Could not initialize frame output"); + } + + if (header is not null) + { + header = frameDecoder.frameHeader; + } + + bool closeOk = true; + List sectionReaders = []; + List sectionClosers = []; + List sectionInfos = []; + List sectionStatuses = []; + + int index = 0; + + foreach (JxlTocEntry toc in frameDecoder.toc) + { + JxlBitReader br = new(stream); + sectionInfos.Add(new JxlSectionInfo(br, toc.Id, index++)); + sectionClosers.Add(new JxlBitReaderScopedCloser(reader, closeOk)); + sectionReaders.Add(br); + } + + frameDecoder.ProcessSections(sectionInfos, sectionStatuses); + for (int i = 0; i < sectionStatuses.Count; i++) + { + if (sectionStatuses[i] != JxlSectionStatus.Done) + { + throw new InvalidOperationException("Section incomplete"); + } + } + + if (!closeOk) + { + throw new InvalidDataException("Stream cannot be closed"); + } + + frameDecoder.FinalizeFrame(); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlProgressiveDetail.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlProgressiveDetail.cs new file mode 100644 index 0000000000..70c3a4679a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlProgressiveDetail.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Types of progressive detail. +/// +internal enum JxlProgressiveDetail : byte +{ + /// + /// After completed regular frames + /// + Frames, + + /// + /// After completed DC. + /// + Dc, + + /// + /// After completed AC passes that are the last pass for their + /// resolution target. + /// + LastPasses, + + /// + /// After completed AC passes that are not the last pass for their + /// resolution target. + /// + Passes, + + /// + /// During DC frame when lower resolution are completed. + /// + DcProgressive, + + /// + /// After completed groups. + /// + DcGroups, + + /// + /// After completed groups. + /// + Groups, +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionInfo.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionInfo.cs new file mode 100644 index 0000000000..4637bacbda --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionInfo.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal sealed class JxlSectionInfo(JxlBitReader reader, int id, int index) +{ + public JxlBitReader BitReader { get; set; } = reader; + + public int Id { get; set; } = id; + + public int Index { get; set; } = index; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionStatus.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionStatus.cs new file mode 100644 index 0000000000..88c4f1068f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionStatus.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Status of processing a section. +/// +internal enum JxlSectionStatus +{ + /// + /// Processed normally. + /// + Done, + + /// + /// Skipped because other required sections were not yet processed. + /// + Skipped, + + /// + /// Skipped because the section was already processed. + /// + Duplicate, + + /// + /// Only partially decoded. Section will be processed again. + /// + Partial +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTocEntry.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTocEntry.cs new file mode 100644 index 0000000000..4e0cfac6bc --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlTocEntry.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal struct JxlTocEntry +{ + public int Size; + + public int Id; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsHistogramStrategy.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsHistogramStrategy.cs new file mode 100644 index 0000000000..1928788791 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsHistogramStrategy.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; + +/// +/// ANS histogram strategy during encoding +/// +internal enum JxlAnsHistogramStrategy : byte +{ + /// + /// Only try a few methods, early exit. + /// + Fast, + + /// + /// Only try a few methods. + /// + Approximate, + + /// + /// Try all methods. + /// + Precise +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlClusteringType.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlClusteringType.cs new file mode 100644 index 0000000000..4435c5d199 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlClusteringType.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; + +/// +/// JPEG XL ANS encoder clustering type +/// +internal enum JxlClusteringType : byte +{ + /// + /// Fastest clustering type, with only 4 clusters + /// + Fastest, + + /// + /// A fast clustering type. + /// + Fast, + + /// + /// Slower clustering type. + /// + Best, +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs new file mode 100644 index 0000000000..70a9321071 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs @@ -0,0 +1,111 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; + +/// +/// ANS histogram. +/// +internal sealed class JxlHistogram(int length) +{ + /// + /// Rounding constant + /// + private const int Rounding = 8; + + public List Counts { get; set; } = new(length); + + public int TotalCount { get; set; } + + public float Entropy { get; set; } + + /// + /// Resets all values to their defaults. + /// + public void Clear() + { + this.Counts.Clear(); + this.TotalCount = 0; + this.Entropy = 0f; + } + + /// + /// Adds a new symbol. + /// + /// + /// Index of the symbol to be added or, if it already + /// exists, incremented. + /// + public void Add(int symbol) + { + // Just to be careful here. If the symbol is too large, + // this can allocate a lot of memory. + DebugGuard.MustBeLessThan(symbol, 1_000_000, nameof(symbol)); + + _ = this.Counts.EnsureCapacity(symbol); + this.Counts[symbol]++; + this.TotalCount++; + } + + /// + /// Increments the specified symbol. This is equivalent to + /// but without any checks, like ensuring capacity. + /// + /// Index of the symbol. + public void FastAdd(int symbol) => this.Counts[symbol]++; + + /// + /// Adds the counts of the specified histogram to the current histogram. + /// + /// A specified histogram to add to this histogram. + public void AddHistogram(JxlHistogram other) + { + _ = this.Counts.EnsureCapacity(other.Counts.Count); + + for (int i = 0; i < other.Counts.Count; i++) + { + this.Counts[i] += other.Counts[i]; + } + + this.TotalCount += other.TotalCount; + } + + /// + /// Calculates the alphabet size. + /// + /// The alphabet size. + public int GetAlphabetSize() + { + for (int i = this.Counts.Count - 1; i >= 0; i--) + { + if (this.Counts[i] > 0) + { + return i + 1; + } + } + + return 0; + } + + /// + /// Finds the largest symbol. + /// + /// Largest symbol in the histogram. + public int GetMaxSymbol() + { + if (this.TotalCount == 0) + { + return 0; + } + + for (int i = this.Counts.Count - 1; i > 0; i--) + { + if (this.Counts[i] != 0) + { + return i; + } + } + + return 0; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogramParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogramParameters.cs new file mode 100644 index 0000000000..4f2047424c --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogramParameters.cs @@ -0,0 +1,113 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; + +/// +/// ANS histogram parameters +/// +internal sealed class JxlHistogramParameters +{ + /// + /// Initializes a new instance of the class with default values. + /// + public JxlHistogramParameters() + { + } + + /// + /// Initializes a new instance of the class using the specified + /// speed tier that prioritizes performance over compression and vice versa. + /// + public JxlHistogramParameters(JxlSpeedTier tier) + { + if (tier > JxlSpeedTier.Falcon) + { + // Fast modes + this.Clustering = JxlClusteringType.Fastest; + this.Lz77Method = JxlLz77Method.None; + } + else if (tier > JxlSpeedTier.Tortoise) + { + // Normal modes + this.Clustering = JxlClusteringType.Fast; + } + else + { + // Slow modes + this.Clustering = JxlClusteringType.Best; + } + + if (tier > JxlSpeedTier.Tortoise) + { + this.UIntMethod = JxlHybridUIntMethod.None; + } + + if (tier >= JxlSpeedTier.Squirrel) + { + this.AnsHistogramStrategy = JxlAnsHistogramStrategy.Approximate; + } + } + + /// + /// Gets or sets the clustering type. Default is Best. + /// + public JxlClusteringType Clustering { get; set; } = JxlClusteringType.Best; + + /// + /// Gets or sets the hybrid uint method. Default is Best. + /// + public JxlHybridUIntMethod UIntMethod { get; set; } = JxlHybridUIntMethod.Best; + + /// + /// Gets or sets the LZ77 method. Default is Rle. + /// + public JxlLz77Method Lz77Method { get; set; } = JxlLz77Method.Rle; + + /// + /// Gets or sets the ANS histogram strategy. Default is Precise. + /// + public JxlAnsHistogramStrategy AnsHistogramStrategy { get; set; } = JxlAnsHistogramStrategy.Precise; + + /// + /// Gets or sets image widths. + /// + public List ImageWidths { get; set; } = []; + + /// + /// Gets or sets the max number of histograms. + /// + public uint MaxHistograms { get; set; } = ~0u; + + /// + /// Gets or sets a value indicating whether to prefer Huffman coding. + /// + public bool ForceHuffman { get; set; } + + /// + /// Gets or sets a value indicating whether global state should be initialized. + /// (True by default) + /// + public bool InitializeGlobalState { get; set; } = true; + + /// + /// Gets or sets a value indicating whether streaming mode is enabled. + /// + public bool StreamingMode { get; set; } + + public bool AddMissingSymbols { get; set; } + + public bool AddFixedHistograms { get; set; } + + /// + /// Gets the uint configuration for histogram parameters. + /// + public JxlAnsHybridUIntConfiguration UIntConfig => this.UIntMethod switch + { + JxlHybridUIntMethod.ContextMap => new(2, 0, 1), + JxlHybridUIntMethod.Method000 => new(0, 0, 0), + _ => new(), + }; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHybridUIntMethod.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHybridUIntMethod.cs new file mode 100644 index 0000000000..a5150f7a74 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHybridUIntMethod.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; + +/// +/// Hybrid uint method. +/// +internal enum JxlHybridUIntMethod : byte +{ + /// + /// Simply use HybridUint420Configuration + /// + None, + + /// + /// Force the fastest option. + /// + Method000, + + /// + /// Try a couple of options. + /// + Fast, + + /// + /// Fast choice for context maps. + /// + ContextMap, + + /// + /// Slowest. + /// + Best, +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlLz77Method.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlLz77Method.cs new file mode 100644 index 0000000000..41cb9da764 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlLz77Method.cs @@ -0,0 +1,90 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; + +/// +/// Method for LZ77 compression. +/// +internal enum JxlLz77Method : byte +{ + /// + /// Do not use LZ77. + /// + None, + + /// + /// Use Run Length Encoding + /// + Rle, + + /// + /// LZ77 fast without runtime cost comparison + /// + Lz77b1w3f, + + /// + /// LZ77 + /// + Lz77b3w3f, + + /// + /// LZ77 + /// + Lz77b7w3f, + + /// + /// LZ77 + /// + Lz77b15w3f, + + /// + /// Slow LZ77 + /// + Lz77b31w3f, + + /// + /// Fast LZ77, but with runtime cost comparison. Almost always worse. + /// + Lz77b1w3t, + + /// + /// LZ77 + /// + Lz77b3w3t, + + /// + /// LZ77 + /// + Lz77b7w3t, + + /// + /// LZ77 + /// + Lz77b15w3t, + + /// + /// Slow LZ77 + /// + Lz77b31w3t, + + /// + /// Optimal-matching LZ77 (fast). + /// + Optc1, + + /// + /// Optional-matching LZ77. + /// + Optc3, + + /// + /// Optional-matching LZ77. + /// + Optc8, + + /// + /// Optional-matching LZ77 parsing big chain length. + /// + Optc256, +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs index a33485df5d..b4469baebc 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs @@ -105,7 +105,7 @@ public bool Set(int x, int y, JxlAcStrategyType type) 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 FillDct8() => this.FillDct8(this.layers!.GetRectangle()); public void FillInvalid() => this.FillImage(Invalid, this.layers); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlOverride.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlOverride.cs new file mode 100644 index 0000000000..ebd84b7b68 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlOverride.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Represents a boolean which can be overriden to be a default +/// value. +/// +internal enum JxlOverride : sbyte +{ + /// + /// Specifies a true value. + /// + On = 1, + + /// + /// Specifies a false value. + /// + Off = 0, + + /// + /// Specifies a default value. + /// + Default = -1 +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlOverrideHelpers.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlOverrideHelpers.cs new file mode 100644 index 0000000000..85464a16aa --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlOverrideHelpers.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Override utilities. +/// +internal static class JxlOverrideHelpers +{ + /// + /// Converts a boolean to an override. + /// + /// Input boolean. + /// + /// if true. Otherwise . + /// + public static JxlOverride FromBoolean(bool flag) => flag ? JxlOverride.On : JxlOverride.Off; + + /// + /// Converts an override to a boolean. + /// + /// The override. + /// Default value. + /// + /// If override is returns . + /// Otherwise returns true if , false if . + /// + public static bool ToBoolean(JxlOverride @override, bool defaultValue) + { + if (@override == JxlOverride.On) + { + return true; + } + + if (@override == JxlOverride.Off) + { + return false; + } + + return defaultValue; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs index 806c302f51..880c9e32d2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +#pragma warning disable SA1401 // Fields should be private + using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; @@ -9,6 +11,9 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal class JxlPassesSharedState { + // Make this a field so we can get a ref to it + public JxlBlockContextMap BlockContextMap; + public JxlCodecMetadata CodecMetadata { get; set; } = new(); public JxlFrameDimensions FrameDimensions { get; set; } @@ -37,8 +42,6 @@ internal class JxlPassesSharedState public JxlImage3F Dc { get; set; } - public JxlBlockContextMap BlockContextMap { get; set; } = new(); - public JxlImage3F[] DcFrames { get; set; } = new JxlImage3F[4]; public JxlReferenceFrame[] ReferenceFrames { get; set; } = new JxlReferenceFrame[4]; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs index 01723479bb..ef9e360bb1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; @@ -68,21 +69,6 @@ internal sealed class JxlQuantizer /// private int quantDc; - /// - /// Inverse global scale - /// - private float inverseGlobalScale; - - /// - /// Reciprocal of inverseGlobalScale - /// - private float globalScaleSingle; - - /// - /// Inverse quantizer DC - /// - private float inverseQuantDc; - /// /// The zero bias. /// @@ -116,7 +102,7 @@ public JxlQuantizer(JxlDequantMatrices dequant, int quantDc, int globalScale) this.globalScale = globalScale; this.RecomputeFromGlobalScale(); - this.inverseQuantDc = this.inverseGlobalScale / this.quantDc; + this.InverseQuantDc = this.InverseGlobalScale / this.quantDc; ZeroBiasDefault.CopyTo(this.zeroBias); } @@ -124,17 +110,17 @@ public JxlQuantizer(JxlDequantMatrices dequant, int quantDc, int globalScale) /// /// Gets the scaling factor. /// - public float Scale => this.globalScaleSingle; + public float Scale { get; private set; } /// /// Gets the inverse scaling factor. It is a reciprocal of . /// - public float InverseGlobalScale => this.inverseGlobalScale; + public float InverseGlobalScale { get; private set; } /// /// Gets the inverse DC quantization base value. /// - public float InverseQuantDc => this.inverseQuantDc; + public float InverseQuantDc { get; private set; } public ReadOnlySpan MulDc => this.mulDc; @@ -195,9 +181,9 @@ private float ScaleGlobalScale(float scale) /// public void RecomputeFromGlobalScale() { - this.globalScaleSingle = this.globalScale * (1.0f / GlobalScaleDenominator); - this.inverseGlobalScale = 1.0f * GlobalScaleDenominator / this.globalScale; - this.inverseQuantDc = this.inverseGlobalScale / this.quantDc; + this.Scale = 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++) { @@ -229,14 +215,14 @@ public ReadOnlySpan InverseDequantMatrix(JxlAcStrategyType strategy, int /// /// The quantization index /// The DC quantization step - public float GetDcStep(int c) => this.inverseQuantDc * this.dequant.DcQuant(c); + 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); + public float GetInverseDcStep(int c) => this.dequant.InverseDcQuant(c) * (this.Scale * this.quantDc); /// /// Creates JXL quantizer parameters with values reflecting those in this quantizer instance. @@ -302,7 +288,7 @@ public void ComputeGlobalScaleAndQuant(float quantDc, float quantMedian, float q this.RecomputeFromGlobalScale(); - float valueF = (quantDc * this.inverseGlobalScale) + 0.5f; + float valueF = (quantDc * this.InverseGlobalScale) + 0.5f; float clipValueF = MathF.Min(1 << 16, valueF); int newQuant = (int)clipValueF; this.quantDc = newQuant; @@ -322,7 +308,7 @@ public void SetQuantFieldRect(JxlImageF qf, in Rectangle rect, JxlImageI rawQuan for (int x = 0; x < rect.Width; x++) { - int val = Clamp((rowQf[x] * this.inverseGlobalScale) + 0.5f); + int val = Clamp((rowQf[x] * this.InverseGlobalScale) + 0.5f); rowQi[x] = val; } @@ -387,7 +373,7 @@ public void SetQuant(float quantDc, float quantAc, JxlImageI rawQuantField) { this.ComputeGlobalScaleAndQuant(quantDc, quantAc, 0); - int value = Clamp((quantAc * this.inverseGlobalScale) + 0.5f); + int value = Clamp((quantAc * this.InverseGlobalScale) + 0.5f); rawQuantField.Fill(value); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSpeedTier.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSpeedTier.cs new file mode 100644 index 0000000000..363b4a5e4b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSpeedTier.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Defines how quickly or slowly to encode an image. Slower +/// modes may take longer to encode the image but provide +/// better compression, while faster modes skip many algorithms, +/// therefore making compression faster, but at the same time, +/// less efficient. +/// +internal enum JxlSpeedTier : sbyte +{ + /// + /// 🌍 Try multiple combinations of Glacier + /// flags for modular mode. Otherwise like Glacier. + /// + TectonicPlate = -1, + + /// + /// 🧊 Learn a global tree in Modular mode. + /// + Glacier, + + /// + /// 🐢 Turns on FindBestQuantizationHQ loop. + /// + Tortoise, + + /// + /// 🐈 Turns on FindBestQuantization butteraugli loop. + /// + Kitten, + + /// + /// 🐿️ Turns on dots, patches, and spline detection, as well as + /// context clustering. This is the default mode. + /// + Squirrel, + + /// + /// 🐻 Turns on error diffusion and full AC strategy heuristics. This is the + /// equivalent of fast mode. + /// + Wombat, + + /// + /// 🐰 Turns on simple heuristics for AC strategy, quant field, + /// gaborish by default, non-default color map, initial quant field, + /// and non-default Chroma from Luma. + /// + Hare, + + /// + /// 🐆 Turns on clustering and enables coefficient reordering. + /// + Cheetah, + + /// + /// 🦅 Turns off most encoder encoder features. Does context clustering. + /// For modular, uses fixed tree with Weighted predictor. + /// + Falcon, + + /// + /// ⚡ Fastest possible setting for VarDCT. For Modular, uses fixed tree with + /// Gradient predictor. + /// + Thunder, + + /// + /// ⚡ For VarDCT, same as Thunder. For Modular, no tree, Gradient predictor, fast histograms. + /// + Lightning +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs new file mode 100644 index 0000000000..98c1d2dc54 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs @@ -0,0 +1,97 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +/// +/// Context prediction helper methods. +/// +internal static class JxlContextPrediction +{ + public static void SetPredictorMode(int i, JxlModularHeader header) + { + ref uint wr = ref header.GetWReference(); + Span w = MemoryMarshal.CreateSpan(ref wr, 4); + + switch (i) + { + case 0: + // ~ lossless16 predictor + w[0] = 0xd; + w[1] = 0xc; + w[2] = 0xc; + w[3] = 0xc; + header.P1C = 16; + header.P2C = 10; + header.P3Ca = 7; + header.P3Cb = 7; + header.P3Cc = 7; + header.P3Cd = 0; + header.P3Ce = 0; + break; + + case 1: + // ~ default lossless8 predictor + w[0] = 0xd; + w[1] = 0xc; + w[2] = 0xc; + w[3] = 0xb; + header.P1C = 8; + header.P2C = 8; + header.P3Ca = 4; + header.P3Cb = 0; + header.P3Cc = 3; + header.P3Cd = 23; + header.P3Ce = 2; + break; + + case 2: + // ~ west lossless8 predictor + w[0] = 0xd; + w[1] = 0xc; + w[2] = 0xd; + w[3] = 0xc; + header.P1C = 10; + header.P2C = 9; + header.P3Ca = 7; + header.P3Cb = 0; + header.P3Cc = 0; + header.P3Cd = 16; + header.P3Ce = 9; + break; + + case 3: + // ~ north lossless8 predictor + w[0] = 0xd; + w[1] = 0xd; + w[2] = 0xc; + w[3] = 0xc; + header.P1C = 16; + header.P2C = 8; + header.P3Ca = 0; + header.P3Cb = 16; + header.P3Cc = 0; + header.P3Cd = 23; + header.P3Ce = 0; + break; + + case 4: + default: + // something else, because why not + w[0] = 0xd; + w[1] = 0xc; + w[2] = 0xc; + w[3] = 0xc; + header.P1C = 10; + header.P2C = 10; + header.P3Ca = 5; + header.P3Cb = 5; + header.P3Cc = 5; + header.P3Cd = 12; + header.P3Ce = 4; + break; + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlFlatDecisionNode.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlFlatDecisionNode.cs new file mode 100644 index 0000000000..122f01befa --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlFlatDecisionNode.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +/// +/// Stores a node and its two children at the same time. +/// +internal struct JxlFlatDecisionNode +{ + public int Property0; + public int SplitValue0; + public JxlPredictor Predictor; + public InlineArray2 SplitValues; + public int Multiplier; + public uint ChildID; + public InlineArray2 Properties; + public int PredictorOffset; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlMaTreeLookup.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlMaTreeLookup.cs new file mode 100644 index 0000000000..ef42bf8c76 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlMaTreeLookup.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +internal sealed class JxlMaTreeLookup(JxlFlatDecisionNode[] nodes) +{ + public JxlMaTreeLookupResult Lookup(Span properties) + { + uint pos = 0; + while (true) + { + for (int i = 0; i < 2; i++) + { + JxlFlatDecisionNode node = nodes[pos]; + if (node.Property0 < 0) + { + return new(node.ChildID, node.Predictor, node.PredictorOffset, node.Multiplier); + } + + bool p0 = properties[node.Property0] <= node.SplitValue0; + uint off0 = properties[node.Properties[0]] <= node.SplitValues[0] ? 1u : 0u; + uint off1 = 2u | (properties[node.Properties[1]] <= node.SplitValues[1] ? 1u : 0u); + + pos = node.ChildID + (p0 ? off1 : off0); + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlMaTreeLookupResult.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlMaTreeLookupResult.cs new file mode 100644 index 0000000000..2609011449 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlMaTreeLookupResult.cs @@ -0,0 +1,9 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +/// +/// MA tree lookup result +/// +internal record struct JxlMaTreeLookupResult(uint Context, JxlPredictor Predictor, int Offset, int Multiplier); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs new file mode 100644 index 0000000000..4db1a00dc9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs @@ -0,0 +1,140 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +/// +/// Context prediction header +/// +internal sealed class JxlModularHeader : IJxlFields +{ + // Backing fields for properties so we can get + // a ref to them. + private bool allDefault; + private int p1C; + private int p2C; + private int p3Ca; + private int p3Cb; + private int p3Cc; + private int p3Cd; + private int p3Ce; + private InlineArray4 w; + + /// + /// Initializes a new instance of the class. + /// + public JxlModularHeader() => JxlBundle.Init(this); + + /// + /// Gets or sets a value indicating whether all values are default. + /// + public bool AllDefault + { + get => this.allDefault; + set => this.allDefault = value; + } + + /// + /// Gets or sets the p1C coefficient. + /// + public int P1C + { + get => this.p1C; + set => this.p1C = value; + } + + /// + /// Gets or sets the p2C coefficient. + /// + public int P2C + { + get => this.p2C; + set => this.p2C = value; + } + + /// + /// Gets or sets the p3Ca coefficient. + /// + public int P3Ca + { + get => this.p3Ca; + set => this.p3Ca = value; + } + + /// + /// Gets or sets the p3Cb coefficient. + /// + public int P3Cb + { + get => this.p3Cb; + set => this.p3Cb = value; + } + + /// + /// Gets or sets the p3Cc coefficient. + /// + public int P3Cc + { + get => this.p3Cc; + set => this.p3Cc = value; + } + + /// + /// Gets or sets the p3Cd coefficient. + /// + public int P3Cd + { + get => this.p3Cd; + set => this.p3Cd = value; + } + + /// + /// Gets or sets the p3Ce coefficient. + /// + public int P3Ce + { + get => this.p3Ce; + set => this.p3Ce = value; + } + + /// + /// Returns a reference to the first w item. + /// + /// Reference to w[0] + public ref uint GetWReference() => ref this.w[0]; + + public bool Visit(JxlVisitor v) + { + if (v.AllDefault(this, ref this.allDefault)) + { + v.SetDefault(this); + return true; + } + + if (!VisitP(16, ref this.p1C) || + !VisitP(10, ref this.p2C) || + !VisitP(7, ref this.p3Ca) || + !VisitP(7, ref this.p3Cb) || + !VisitP(7, ref this.p3Cc) || + !VisitP(0, ref this.p3Cd) || + !VisitP(0, ref this.p3Ce) || + !v.Bits(4, 0xD, ref this.w[0]) || + !v.Bits(4, 0xC, ref this.w[1]) || + !v.Bits(4, 0xC, ref this.w[2]) || + !v.Bits(4, 0xC, ref this.w[3])) + { + return false; + } + + return true; + + bool VisitP(int value, ref int p) + { + ref uint unsignedP = ref Unsafe.As(ref p); + return v.Bits(5, (uint)value, ref unsignedP); + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularMultiplierInfo.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularMultiplierInfo.cs new file mode 100644 index 0000000000..2b0b35511f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularMultiplierInfo.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +/// +/// Information about a multiplier for Modular. +/// +/// +/// A static property range, with each item containing channel and group ID. +/// +/// +/// The multiplier. +/// +internal record struct JxlModularMultiplierInfo( + InlineArray2> Range, + uint Multiplier); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs new file mode 100644 index 0000000000..e58897b4fa --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.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.Modular.Encoding.ContextPrediction; + +/// +/// State for context prediction +/// +internal sealed class JxlModularState +{ + /// + /// Rounding constant for predictions. + /// + private const long PredictionRound = ((1 << 3) >> 1) - 1; + + private InlineArray4 prediction; + private long pred; + private readonly uint[][] predErrors = new uint[4][]; + private readonly int[] error; + private readonly JxlModularHeader header; + + public JxlModularState(JxlModularHeader header, int width, int height) + { + this.header = header; + + for (int i = 0; i < 4; i++) + { + this.predErrors[i] = new uint[(width + 2) * 2]; + } + + this.error = new int[(width + 2) * 2]; + } + + /// + /// Gets a table for approximating division by a number from + /// 1 to 64. It is defined as follows. + /// + /// for (int i = 0; i < 64; i++) + /// { + /// DivisionLookup[i] = (1u << 24) / (i + 1); + /// } + /// + /// + private static ReadOnlySpan DivisionLookup => + [ + 16777216, 8388608, 5592405, 4194304, 3355443, 2796202, 2396745, 2097152, + 1864135, 1677721, 1525201, 1398101, 1290555, 1198372, 1118481, 1048576, + 986895, 932067, 883011, 838860, 798915, 762600, 729444, 699050, + 671088, 645277, 621378, 599186, 578524, 559240, 541200, 524288, + 508400, 493447, 479349, 466033, 453438, 441505, 430185, 419430, + 409200, 399457, 390167, 381300, 372827, 364722, 356962, 349525, + 342392, 335544, 328965, 322638, 316551, 310689, 305040, 299593, + 294337, 289262, 284359, 279620, 275036, 270600, 266305, 262144 + ]; + + /// + /// Adds extra bits to the prediction. + /// + /// The prediction + /// 3 extra bits added to the prediction + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static long AddBits(long x) => x << 3; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ErrorWeight(int x, uint maxWeight) + { + int shift = Math.Max(0, JxlMath.FloorLog2Nonzero(x + 1) - 5); // Math.Max call ensures the value isn't negative + return 4 + ((maxWeight * DivisionLookup[x >> shift]) >> shift); + } + + public static long WeightedAverage(Span p, Span w) + { + uint weightSum = w[0] + w[1] + w[2] + w[3]; + + if (weightSum <= 15) + { + throw new InvalidOperationException("Weight sum is too low"); + } + + int logWeight = (int)JxlMath.FloorLog2Nonzero(weightSum); + weightSum = 0; + + for (int i = 0; i < 4; i++) + { + w[i] >>= logWeight - 4; + weightSum += w[i]; + } + + long sum = (weightSum >> 1) - 1; + for (int i = 0; i < 4; i++) + { + // Dot product + sum += p[i] * w[i]; + } + + return (sum * DivisionLookup[(int)weightSum - 1]) >> 24; + } + + public long Predict(bool computeProperties, int x, int y, int width, long n, long w, long ne, long nw, long nn, Span properties, int offset) + { + bool yIsOdd = (y & 1) != 0; + + int cur_row = yIsOdd ? 0 : (width + 2); + int prev_row = yIsOdd ? (width + 2) : 0; + int pos_N = prev_row + x; + int pos_NE = x < width - 1 ? pos_N + 1 : pos_N; + int pos_NW = x > 0 ? pos_N - 1 : pos_N; + + Span weights = stackalloc uint[4]; + ref uint headerW = ref this.header.GetWReference(); + + for (int i = 0; i < 4; i++) + { + Span error = this.predErrors[i].AsSpan(); + weights[i] = error[pos_N] + error[pos_NE] + error[pos_NW]; + weights[i] = ErrorWeight((int)weights[i], Unsafe.Add(ref headerW, i)); + } + + n = AddBits(n); + w = AddBits(w); + ne = AddBits(ne); + nw = AddBits(nw); + nn = AddBits(nn); + + long teW = x == 0 ? 0 : this.error[cur_row + x - 1]; + long teN = this.error[pos_N]; + long teNW = this.error[pos_NW]; + long sumWN = teN + teW; + long teNE = this.error[pos_NE]; + + if (computeProperties) + { + long p = teW; + long absP = Math.Abs(p); + + if (Math.Abs(teN) > absP) + { + p = teN; + } + + if (Math.Abs(teNW) > absP) + { + p = teNW; + } + + if (Math.Abs(teNE) > absP) + { + p = teNE; + } + + properties[offset++] = (int)p; + } + + this.prediction[0] = w + ne - n; + this.prediction[1] = n - (((sumWN + teNE) * this.header.P1C) >> 5); + this.prediction[2] = w - (((sumWN + teNW) * this.header.P2C) >> 5); + this.prediction[3] = + n - (((teNW * this.header.P3Ca) + (teN * this.header.P3Cb) + (teNE * this.header.P3Cc) + + ((nn - n) * this.header.P3Cd) + ((nw - w) * this.header.P3Ce)) >> + 5); + + this.pred = WeightedAverage(this.prediction, weights); + + if (((teN ^ teW) | (teN ^ teNW)) > 0) + { + return (this.pred + PredictionRound) >> 3; + } + + long mx = Math.Max(w, Math.Max(ne, n)); + long mn = Math.Min(w, Math.Min(ne, n)); + this.pred = Math.Max(mn, Math.Min(mx, this.pred)); + return (this.pred + PredictionRound) >> 3; + } + + public void UpdatePredictionErrors(long value, int x, int y, int width) + { + bool yIsOdd = (y & 1) != 0; + + long curRow = yIsOdd ? 0 : (width + 2); + long prevRow = yIsOdd ? (width + 2) : 0; + value = AddBits(value); + this.error[curRow + x] = (int)(this.pred - value); + for (int i = 0; i < 4; i++) + { + long err = (Math.Abs(this.prediction[i] - value) + PredictionRound) >> 3; + this.predErrors[i][curRow + x] = (uint)err; + this.predErrors[i][prevRow + x + 1] += (uint)err; + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictor.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictor.cs new file mode 100644 index 0000000000..6b68b40ea5 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictor.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +/// +/// Defines the type of the predictor using neighboring +/// pixels, similar to intra prediction used in codecs like +/// VP8, AV1 and H.264. +/// +internal enum JxlPredictor : uint +{ + Zero = 0, + Left = 1, + Top = 2, + Average0 = 3, + Select = 4, + Gradient = 5, + Weighted = 6, + TopRight = 7, + TopLeft = 8, + LeftLeft = 9, + Average1 = 10, + Average2 = 11, + Average3 = 12, + Average4 = 13, + Best = 14, + Variable = 15, + + /// + /// Undefined predictor. + /// + Undefined = ~0u +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictorFacts.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictorFacts.cs new file mode 100644 index 0000000000..5a360af13c --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictorFacts.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +/// +/// Tools and constants for predictor types. +/// +internal static class JxlPredictorFacts +{ + /// + /// Number of modular predictors. + /// + public const int ModularPredictors = (int)JxlPredictor.Average4 + 1; + + /// + /// Number of modular encoder predictors. + /// + public const int ModularEncoderPredictors = (int)JxlPredictor.Variable + 1; + + /// + /// Number of static properties. + /// + public const int StaticProperties = 2; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlTreeKind.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlTreeKind.cs new file mode 100644 index 0000000000..1906d7ec03 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlTreeKind.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +/// +/// Kind of tree to use. +/// +// TODO: this enum wasn't documented +// https://github.com/libjxl/libjxl/blob/main/lib/jxl/modular/options.h#L100-L111 +internal enum JxlTreeKind : byte +{ + TrivialTreeNoPredictor, + Learn, + JpegTranscodeAcMeta, + FalconAcMeta, + AcMeta, + WpFixedDc, + GradientFixedDc +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlTreeMode.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlTreeMode.cs new file mode 100644 index 0000000000..93eaa16f8d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlTreeMode.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +/// +/// Decides which kinds of trees should be allowed. +/// +internal enum JxlTreeMode : byte +{ + /// + /// Gradient tree only + /// + GradientOnly, + + /// + /// Weighted predictor only + /// + WpOnly, + + /// + /// Disable weighted predictor + /// + NoWp, + + /// + /// Default trees + /// + Default +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs new file mode 100644 index 0000000000..d5fd89881d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs @@ -0,0 +1,77 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; + +/// +/// A wrapper over for modular operations. +/// +internal sealed class JxlModularChannel +{ + /// + /// Underlying plane buffer. + /// + private JxlPlane plane; + + public JxlModularChannel(Configuration configuration, int width, int height, int horizShift, int vertShift) + { + this.HorizontalShift = horizShift; + this.VerticalShift = vertShift; + this.Width = width; + this.Height = height; + this.plane = JxlPlane.Create(configuration, width, height); + } + + /// + /// Gets or sets the image width. + /// + public int Width { get; set; } + + /// + /// Gets or sets the image height. + /// + public int Height { get; set; } + + /// + /// Gets or sets the image horizontal shift. + /// + /// + /// width ~= width >> HorizontalShift + /// + public int HorizontalShift { get; set; } + + /// + /// Gets or sets the image vertical shift. + /// + /// + /// height ~= height >> VerticalShift + /// + public int VerticalShift { get; set; } + + /// + /// Gets or sets the index of the component. + /// + public int Component { get; set; } = -1; + + public void Shrink(Configuration configuration) + { + if (this.plane.XSize == this.Width && this.plane.YSize == this.Height) + { + return; + } + + this.plane.Dispose(); + this.plane = JxlPlane.Create(configuration, this.Width, this.Height); + } + + public void Shrink(Configuration configuration, int newWidth, int newHeight) + { + this.Width = newWidth; + this.Height = newHeight; + this.Shrink(configuration); + } + + public Span GetRow(int y) => this.plane.GetRow(y); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs new file mode 100644 index 0000000000..488e198c4d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; + +internal sealed class JxlModularImage +{ + public List Channels { get; set; } = []; + + +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs new file mode 100644 index 0000000000..35b8656615 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs @@ -0,0 +1,115 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; + +/// +/// Reversible Color Transform (RCT) +/// +internal static class JxlRct +{ + public static void InverseRctRow(int transformType, Span in0, Span in1, Span in2, Span out0, Span out1, Span out2, int width) + { + DebugGuard.MustBeBetweenOrEqualTo(transformType, 0, 6, nameof(transformType)); + + int second = transformType >> 1; + int third = transformType & 1; + + int n = Vector.Count; + + if (transformType == 6) + { + // SIMD-aligned loop + int x; + for (x = 0; x + n - 1 < width; x += n) + { + Vector y = new(in0[x..]); + Vector co = new(in1[x..]); + Vector cg = new(in2[x..]); + y -= cg >> 1; + Vector g = cg + y; + y -= co >> 1; + Vector r = y + co; + r.CopyTo(out0[x..]); + g.CopyTo(out1[x..]); + y.CopyTo(out2[x..]); + } + + // Remainder (SIMD-unaligned) + for (; x < width; x++) + { + int y = in0[x]; + int co = in1[x]; + int cg = in2[x]; + int tmp = y + -(cg >> 1); + int g = cg + tmp; + int b = tmp + -(co >> 1); + int r = b + co; + out0[x] = r; + out1[x] = g; + out2[x] = b; + } + } + else + { + // SIMD-aligned loop + int x; + for (x = 0; x + n - 1 < width; x += n) + { + // Add a Vec suffix because the variables + // 'second' and 'third' are already defined. + // Though 'first' isn't, it's still suffixed + // for consistency. + Vector firstVec = new(in0[x..]); + Vector secondVec = new(in1[x..]); + Vector thirdVec = new(in2[x..]); + + if (third > 0) + { + thirdVec += firstVec; + } + + if (second == 1) + { + secondVec += firstVec; + } + else if (second == 2) + { + secondVec += (firstVec + thirdVec) >> 1; + } + + firstVec.CopyTo(out0[x..]); + secondVec.CopyTo(out1[x..]); + thirdVec.CopyTo(out2[x..]); + } + + // Remainder (SIMD-unaligned) + for (; x < width; x++) + { + int firstCoeff = in0[x]; + int secondCoeff = in1[x]; + int thirdCoeff = in2[x]; + + if (third > 0) + { + thirdCoeff += firstCoeff; + } + + if (second == 1) + { + secondCoeff += firstCoeff; + } + else if (second == 2) + { + secondCoeff += (firstCoeff + thirdCoeff) >> 1; + } + + out0[x] = firstCoeff; + out1[x] = secondCoeff; + out2[x] = thirdCoeff; + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueezeParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueezeParameters.cs new file mode 100644 index 0000000000..7bb52fe5e5 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueezeParameters.cs @@ -0,0 +1,74 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; + +/// +/// Parameters for the squeeze transform. +/// +internal sealed class JxlSqueezeParameters : IJxlFields +{ + private bool horizontal; + private bool inPlace; + private uint beginC; + private uint numC; + + public JxlSqueezeParameters() => JxlBundle.Init(this); + + /// + /// Gets or sets a value indicating whether the transform is horizontal. + /// + public bool Horizontal + { + get => this.horizontal; + set => this.horizontal = value; + } + + /// + /// Gets or sets a value indicating whether the transform is in-place. + /// + public bool InPlace + { + get => this.inPlace; + set => this.inPlace = value; + } + + public uint BeginC + { + get => this.beginC; + set => this.beginC = value; + } + + public uint NumC + { + get => this.numC; + set => this.numC = value; + } + + public bool Visit(JxlVisitor visitor) + { + if (!visitor.Boolean(false, ref this.horizontal) || + !visitor.Boolean(false, ref this.inPlace) || + !visitor.U32( + JxlFieldExpressions.Bits(3), + JxlFieldExpressions.BitsOffset(6, 8), + JxlFieldExpressions.BitsOffset(10, 72), + JxlFieldExpressions.BitsOffset(13, 1096), + 0, + ref this.beginC) || + !visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(3), + JxlFieldExpressions.BitsOffset(4, 4), + 2, + ref this.numC)) + { + return false; + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs new file mode 100644 index 0000000000..7803010619 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Fields; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; + +internal sealed class JxlTransform : IJxlFields +{ + public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransformType.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransformType.cs new file mode 100644 index 0000000000..0919425db1 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransformType.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; + +/// +/// Represents the type of a transform. +/// +internal enum JxlTransformType : byte +{ + /// + /// Reversible Color Transform + /// + Rct, + + /// + /// Palette/indexed coding + /// + Palette, + + /// + /// Haar-style squeezing + /// + Squeeze, + + /// + /// Invalid/unknown + /// + Invalid +} From dfd99a0d8a64bdebf22f1e9beb96e6dfeb829b03 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 24 Aug 2026 16:11:54 +0400 Subject: [PATCH 084/142] Reduce errors in AC strategy code --- src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs | 6 +++--- src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs index c6ecc8f16f..6da2926640 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs @@ -81,9 +81,9 @@ public JxlAcStrategy(int rawStrategy) public readonly JxlAcStrategyType Strategy { get; } - public void ComputeNaturalCoefficientOrder(ref int order) => CoefficientOrderAndLookup(this, false, ref order); + public readonly void ComputeNaturalCoefficientOrder(Span order) => CoefficientOrderAndLookup(this, false, order); - public void ComputeNaturalCoefficientOrderLookup(ref int lookup) => CoefficientOrderAndLookup(this, true, ref lookup); + public readonly void ComputeNaturalCoefficientOrderLookup(Span lookup) => CoefficientOrderAndLookup(this, true, lookup); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetTypeBit(JxlAcStrategyType type) => 1 << (int)type; @@ -97,7 +97,7 @@ private static void CoefficientOrderAndLookup(JxlAcStrategy strategy, bool isLoo int cx = strategy.CoveredBlocksX; int cy = strategy.CoveredBlocksY; - CoefficientLayout(ref cx, ref cy); + JxlForwardCoefficientOrder.CoefficientLayout(ref cx, ref cy); int xs = cx / cy; int xsm = xs - 1; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs index b4469baebc..236d518140 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs @@ -103,11 +103,11 @@ public bool Set(int x, int y, JxlAcStrategyType type) 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(in Rectangle rect) => JxlImageOperations.FillPlane((byte)(((int)JxlAcStrategyType.DCT << 1) | 1), this.layers ?? throw new InvalidOperationException("Image is missing"), rect); public void FillDct8() => this.FillDct8(this.layers!.GetRectangle()); - public void FillInvalid() => this.FillImage(Invalid, this.layers); + public void FillInvalid() => JxlImageOperations.FillImage(Invalid, this.layers ?? throw new InvalidOperationException("Image is missing")); public void Dispose() { From a2f8fe6f2ca5b11ba1a0f0c7aed00946ff39eb9b Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 24 Aug 2026 16:33:14 +0400 Subject: [PATCH 085/142] Reduce errors --- .../Formats/Jxl/Cms/JxlOpsinConstants.cs | 24 +++++++++++++++++++ .../Decoder/JxlContextMapDecoder.cs | 8 +++++++ .../Jxl/Processing/Decoder/JxlXybDecoder.cs | 10 ++++---- .../Processing/Encoder/Ans/JxlHistogram.cs | 5 ---- .../Jxl/Processing/JxlColorCorrelation.cs | 3 ++- .../Jxl/Processing/JxlDctAcImage{T}.cs | 4 ++-- .../Formats/Jxl/Processing/JxlLoopFilter.cs | 9 +++---- .../Jxl/Processing/JxlWeightsSeparable5.cs | 1 - .../ContextPrediction/JxlModularState.cs | 16 ++++++------- .../Processing/Splines/JxlQuantizedSpline.cs | 15 ++++++------ 10 files changed, 62 insertions(+), 33 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlContextMapDecoder.cs diff --git a/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs b/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs new file mode 100644 index 0000000000..a0029c0c27 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms; + +/// +/// Opsin constants used by the color management system +/// +internal static class JxlOpsinConstants +{ + public const float BScale = 1f; + + // The following constants are used for XYB. + // They can be adjusted to change how Y<->B ratio + // works. For example, YToBRatio works better + // with 0.50017729543783418. + public const float YToBRatio = 1f; + public const float BToYRatio = 1f / YToBRatio; + + // Adjusting these constants influences the opsin absorbance. + public const float OpsinAbsorbanceBias0 = 0.0037930732552754493f; + public const float OpsinAbsorbanceBias1 = OpsinAbsorbanceBias0; + public const float OpsinAbsorbanceBias2 = OpsinAbsorbanceBias0; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlContextMapDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlContextMapDecoder.cs new file mode 100644 index 0000000000..78b989910a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlContextMapDecoder.cs @@ -0,0 +1,8 @@ +using System; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +public class JxlContextMapDecoder +{ + +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs index 940b01170a..7fd7d54c6e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs @@ -33,9 +33,9 @@ public static void ConvertXybToRgb( ref Vector linearG, ref Vector linearB) { - Vector negBiasR = new(opsinParameters.OpsinBiaases[0]); - Vector negBiasG = new(opsinParameters.OpsinBiaases[1]); - Vector negBiasB = new(opsinParameters.OpsinBiaases[2]); + Vector negBiasR = new(opsinParameters.OpsinBiases[0]); + Vector negBiasG = new(opsinParameters.OpsinBiases[1]); + Vector negBiasB = new(opsinParameters.OpsinBiases[2]); Vector gammaR = opsinX + opsinY; Vector gammaG = opsinY - opsinX; @@ -49,7 +49,7 @@ public static void ConvertXybToRgb( Vector mixedG = (gammaG2 * gammaG) + negBiasG; Vector mixedB = (gammaB2 * gammaB) + negBiasB; - Span inverseMatrix = opsinParameters.GetInverseOpsinMatrixSpan(); + Span inverseMatrix = opsinParameters.InverseOpsinMatrix; linearR = LoadDuplicate128(ref inverseMatrix[0 * 4]) * mixedR; linearG = LoadDuplicate128(ref inverseMatrix[3 * 4]) * mixedR; @@ -66,7 +66,7 @@ public static void ConvertXybToRgb( public static bool OpsinToLinear(JxlImage3F opsin, Rectangle rect, JxlImage3F linear, JxlOpsinParameters opsinParameters) { - if (!SameSize(rect, linear)) + if (!JxlImageOperations.SameSize(rect, linear)) { return false; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs index 70a9321071..52637c9898 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogram.cs @@ -8,11 +8,6 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; /// internal sealed class JxlHistogram(int length) { - /// - /// Rounding constant - /// - private const int Rounding = 8; - public List Counts { get; set; } = new(length); public int TotalCount { get; set; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs index 69e617ea90..7047c2c7cb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; @@ -9,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal sealed class JxlColorCorrelation { private float baseCorrelationX; - private float baseCorrelationB = DefaultYToBRatio; + private float baseCorrelationB = JxlOpsinConstants.YToBRatio; private readonly float[] dcFactors = new float[4]; private uint colorFactor = JxlChromaFromLuma.DefaultColorFactor; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs index 2052daee6e..8e74c26b72 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs @@ -23,9 +23,9 @@ public unsafe JxlDctAcImage(Configuration configuration, int width, int height) public bool IsEmpty => this.image.XSize == 0 || this.image.YSize == 0; - public void Clear() => JxlImageOperations.ClearImage(this.image); + public void Clear() => JxlImageOperations.ZeroFillImage(this.image); - public void Clear(int plane = 0) => JxlImageOperations.ClearImage(this.image); + public void Clear(int plane = 0) => JxlImageOperations.ZeroFillImage(this.image); public unsafe JxlDctAcPointer GetPlaneRow(int channel, int y, int xBase = 0) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs index 312138db43..2df002fba6 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; @@ -181,13 +182,13 @@ public bool ComputeSigma(Rectangle blockRect, JxlPassesDecoderState state) JxlAcStrategyImage acStrategy = state.Shared.AcStrategy; float quantScale = state.Shared.Quantizer.Scale; - int sigmaStride = state.Sigma.PixelsPerRow; + 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); + Span sigmaRow = state.Sigma.GetRow(by); + Span sharpnessRow = state.Shared.EpfSharpness.GetRow(by); JxlAcStrategyRow acsRow = acStrategy.GetRow(in blockRect, by); Span rowQuant = state.Shared.RawQuantField.GetRow(by); @@ -245,7 +246,7 @@ public bool ComputeSigma(Rectangle blockRect, JxlPassesDecoderState state) } } - if (by + blockRect.Y + acs.CoveredBlocksX == state.Shared.FrameDimensions.YSizeBloks) + if (by + blockRect.Y + acs.CoveredBlocksX == state.Shared.FrameDimensions.YSizeBlocks) { for (int iy = 0; iy < SigmaBorder; iy++) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs index 7fdec43b03..b2a0c0c55f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs @@ -6,7 +6,6 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal struct JxlWeightsSeparable5 { // Don't make these a property so we can ref into them. - public InlineArray12 Horizontal; public InlineArray12 Vertical; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs index e58897b4fa..f731dc9677 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs @@ -21,7 +21,7 @@ internal sealed class JxlModularState private readonly int[] error; private readonly JxlModularHeader header; - public JxlModularState(JxlModularHeader header, int width, int height) + public JxlModularState(JxlModularHeader header, int width) { this.header = header; @@ -104,9 +104,9 @@ public long Predict(bool computeProperties, int x, int y, int width, long n, lon int cur_row = yIsOdd ? 0 : (width + 2); int prev_row = yIsOdd ? (width + 2) : 0; - int pos_N = prev_row + x; - int pos_NE = x < width - 1 ? pos_N + 1 : pos_N; - int pos_NW = x > 0 ? pos_N - 1 : pos_N; + int posN = prev_row + x; + int posNE = x < width - 1 ? posN + 1 : posN; + int posNW = x > 0 ? posN - 1 : posN; Span weights = stackalloc uint[4]; ref uint headerW = ref this.header.GetWReference(); @@ -114,7 +114,7 @@ public long Predict(bool computeProperties, int x, int y, int width, long n, lon for (int i = 0; i < 4; i++) { Span error = this.predErrors[i].AsSpan(); - weights[i] = error[pos_N] + error[pos_NE] + error[pos_NW]; + weights[i] = error[posN] + error[posNE] + error[posNW]; weights[i] = ErrorWeight((int)weights[i], Unsafe.Add(ref headerW, i)); } @@ -125,10 +125,10 @@ public long Predict(bool computeProperties, int x, int y, int width, long n, lon nn = AddBits(nn); long teW = x == 0 ? 0 : this.error[cur_row + x - 1]; - long teN = this.error[pos_N]; - long teNW = this.error[pos_NW]; + long teN = this.error[posN]; + long teNW = this.error[posNW]; long sumWN = teN + teW; - long teNE = this.error[pos_NE]; + long teNE = this.error[posNE]; if (computeProperties) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs index 7e46f49b05..731c22835b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs @@ -50,10 +50,11 @@ public void ReserveControlPoints(Configuration configuration, int n) public static JxlQuantizedSpline Create(Configuration configuration, JxlSpline original, int quantizationAdjustment, float yToX, float yToB) { JxlQuantizedSpline spline = new(); + Span controlPoints = original.ControlPoints.Span; - spline.ReserveControlPoints(configuration, original.ControlPoints.Count - 1); + spline.ReserveControlPoints(configuration, controlPoints.Length - 1); - PointF startingPoint = original.ControlPoints.First(); + PointF startingPoint = controlPoints[0]; int previousX = (int)MathF.Round(startingPoint.X); int previousY = (int)MathF.Round(startingPoint.Y); int previousDx = 0; // D stands for delta @@ -65,7 +66,7 @@ public static JxlQuantizedSpline Create(Configuration configuration, JxlSpline o for (int i = 0; i < length; i++) { - PointF controlPoint = original.ControlPoints[i]; + PointF controlPoint = controlPoints[i]; int newX = (int)MathF.Round(controlPoint.X); int newY = (int)MathF.Round(controlPoint.Y); @@ -93,8 +94,8 @@ public static JxlQuantizedSpline Create(Configuration configuration, JxlSpline o // 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 dctFactor = (i == 0) ? JxlDctScales.Sqrt2 : 1.0f; + float inverseDctFactor = (i == 0) ? JxlDctScales.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]); @@ -103,7 +104,7 @@ public static JxlQuantizedSpline Create(Configuration configuration, JxlSpline o for (int i = 0; i < 32; i++) { - float dctFactor = (i == 0) ? Sqrt2 : 1.0f; + float dctFactor = (i == 0) ? JxlDctScales.Sqrt2 : 1.0f; spline.SigmaDct[i] = ConvertToInteger(original.SigmaDct[i] * dctFactor * quant / ChannelWeight[1]); } @@ -190,7 +191,7 @@ public bool Dequantize( { for (int i = 0; i < 32; i++) { - float inverseDctFactor = (i == 0) ? Sqrt05 : 1.0f; + float inverseDctFactor = (i == 0) ? JxlDctScales.Sqrt05 : 1.0f; result.ColorDct[c][i] = this.ColorDct[c][i] * inverseDctFactor * ChannelWeight[c] * inverseQuant; } } From 352b14df9a2d97ec5e077787de572a2f45e0c2fa Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 24 Aug 2026 21:17:12 +0400 Subject: [PATCH 086/142] Reduce errors, remove unused constant --- .../Formats/Jxl/Processing/JxlLoopFilter.cs | 5 ----- .../Formats/Jxl/Processing/JxlQuantizer.cs | 18 +++++++++--------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs index 2df002fba6..825c81cbbc 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs @@ -21,11 +21,6 @@ internal sealed class JxlLoopFilter : IJxlFields /// 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. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs index ef9e360bb1..9551b97e09 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs @@ -52,12 +52,12 @@ internal sealed class JxlQuantizer /// /// Represents the multipliers for the DC coefficients. /// - private readonly InlineArray4 mulDc; + private InlineArray4 mulDc; /// /// Represents the inverse multipliers for the DC coefficients. /// - private readonly InlineArray4 inverseMulDc; + private InlineArray4 inverseMulDc; /// /// Global scale @@ -148,8 +148,8 @@ public JxlQuantizer(JxlDequantMatrices dequant, int quantDc, int globalScale) /// public void ClearDcMultipliers() { - Array.Fill(this.mulDc, 1f); - Array.Fill(this.inverseMulDc, 1f); + ((Span)this.mulDc).Fill(1f); + ((Span)this.inverseMulDc).Fill(1f); } /// @@ -303,8 +303,8 @@ public void SetQuantFieldRect(JxlImageF qf, in Rectangle rect, JxlImageI rawQuan { for (int y = 0; y < rect.Height; y++) { - ReadOnlySpan rowQf = qf.GetRow(in rect, y); - Span rowQi = rawQuantField.GetRow(in rect, y); + ReadOnlySpan rowQf = qf.GetRow(rect, y); + Span rowQi = rawQuantField.GetRow(rect, y); for (int x = 0; x < rect.Width; x++) { @@ -352,7 +352,7 @@ public bool SetQuantField(Configuration configuration, float quantDc, JxlImageF if (rawQuantField != null) { - if (rawQuantField.GetSize() != qf.GetSize()) + if (rawQuantField.GetRectangle() != qf.GetRectangle()) { data.Dispose(); deviations.Dispose(); @@ -360,7 +360,7 @@ public bool SetQuantField(Configuration configuration, float quantDc, JxlImageF return false; } - this.SetQuantField(qf, qf.GetRectangle(), rawQuantField); + this.SetQuantFieldRect(qf, qf.GetRectangle(), rawQuantField); } data.Dispose(); @@ -374,6 +374,6 @@ 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); + JxlImageOperations.FillImage(value, rawQuantField); } } From 093d84612c2901eb95a1043b700341281d8bed51 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 24 Aug 2026 21:40:59 +0400 Subject: [PATCH 087/142] Finish inverse RCT --- .../Processing/Modular/Transforms/JxlRct.cs | 83 ++++++++++++++++++- .../Modular/Transforms/JxlTransform.cs | 28 +++++++ 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs index 35b8656615..ab55b12974 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs @@ -10,10 +10,22 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; /// internal static class JxlRct { - public static void InverseRctRow(int transformType, Span in0, Span in1, Span in2, Span out0, Span out1, Span out2, int width) + /// + /// Performs Inverse Reversible Color Transform (RCT) on one row. + /// + /// The kind of RCT. + /// Input Y + /// Input Co + /// Input Cg + /// Output R + /// Output G + /// Output B + private static void InverseRctRow(int transformType, Span in0, Span in1, Span in2, Span out0, Span out1, Span out2) { DebugGuard.MustBeBetweenOrEqualTo(transformType, 0, 6, nameof(transformType)); + int width = in0.Length; // All input & output channels have equal widths + int second = transformType >> 1; int third = transformType & 1; @@ -112,4 +124,73 @@ public static void InverseRctRow(int transformType, Span in0, Span in1 } } } + + /// + /// Performs Inverse Reversible Color Transform (RCT) on an entire + /// image. + /// + /// + /// The configuration is used to access maximum degree of parallelism. + /// + /// + /// Image to compute inverse RCT. + /// + /// + /// Offset of the color channel for Y, Co, Cg. + /// + /// + /// Type of Reversible Color Transform + /// + /// + /// Invoked when RCT/permutation is invalid. + /// + public static void InverseRct(Configuration configuration, JxlModularImage img, int beginC, int rctType) + { + JxlTransform.CheckEqualChannels(img, beginC, beginC + 2); + + int m = beginC; + JxlModularChannel c0 = img.Channels[m + 0]; + int w = c0.Width; + int h = c0.Height; + + if (rctType == 0) + { + // No-op + return; + } + + int permutation = rctType / 7; + + if (permutation >= 7) + { + throw new InvalidOperationException("Permutation must be <= 6"); + } + + int custom = rctType % 7; + + if (custom == 0) + { + // Permute-only. + JxlModularChannel ch0 = img.Channels[m]; + JxlModularChannel ch1 = img.Channels[m + 1]; + JxlModularChannel ch2 = img.Channels[m + 2]; + img.Channels[m + (permutation % 3)] = ch0; + img.Channels[m + ((permutation + 1 + (permutation / 3)) % 3)] = ch1; + img.Channels[m + ((permutation + 2 - (permutation / 3)) % 3)] = ch2; + return; + } + + _ = Parallel.For(0, configuration.MaxDegreeOfParallelism, y => + { + Span in0 = img.Channels[m].GetRow(y); + Span in1 = img.Channels[m + 1].GetRow(y); + Span in2 = img.Channels[m + 2].GetRow(y); + + Span out0 = img.Channels[m + (permutation % 3)].GetRow(y); + Span out1 = img.Channels[m + ((permutation + 1 + (permutation / 3)) % 3)].GetRow(y); + Span out2 = img.Channels[m + ((permutation + 2 - (permutation / 3)) % 3)].GetRow(y); + + InverseRctRow(custom, in0, in1, in2, out0, out1, out2); + }); + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs index 7803010619..860adb4692 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs @@ -8,4 +8,32 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; internal sealed class JxlTransform : IJxlFields { public bool Visit(JxlVisitor visitor) => throw new NotImplementedException(); + + public static void CheckEqualChannels(JxlModularImage image, int c1, int c2) + { + int channelsCount = image.Channels.Count; + + if (c1 > channelsCount || c2 >= channelsCount || c2 < c1) + { + throw new InvalidOperationException($"Invalid channel range: {c1}..{c2} (there are only {channelsCount} channels)"); + } + + if (c1 < image.NbMetaChannels && c2 >= image.NbMetaChannels) + { + throw new InvalidOperationException("Invalid: transforming mix of meta and nonmeta"); + } + + JxlModularChannel ch1 = image.Channels[c1]; + for (int c = c1 + 1; c <= c2; c++) + { + JxlModularChannel ch2 = image.Channels[c]; + if (ch1.Width != ch2.Width || + ch1.Height != ch2.Height || + ch1.HorizontalShift != ch2.HorizontalShift || + ch1.VerticalShift != ch2.VerticalShift) + { + throw new InvalidOperationException($"Channel {c} is not equal"); + } + } + } } From c5a7bb876256c5971387a15a80d0bae077cc3b6b Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 24 Aug 2026 22:37:05 +0400 Subject: [PATCH 088/142] Complete RCT --- .../Processing/Modular/Transforms/JxlRct.cs | 210 +++++++++++++++++- 1 file changed, 208 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs index ab55b12974..0f32e47aee 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs @@ -13,7 +13,9 @@ internal static class JxlRct /// /// Performs Inverse Reversible Color Transform (RCT) on one row. /// - /// The kind of RCT. + /// + /// The kind of RCT. + /// /// Input Y /// Input Co /// Input Cg @@ -180,7 +182,7 @@ public static void InverseRct(Configuration configuration, JxlModularImage img, return; } - _ = Parallel.For(0, configuration.MaxDegreeOfParallelism, y => + _ = Parallel.For(0, h, configuration.GetParallelOptions(), y => { Span in0 = img.Channels[m].GetRow(y); Span in1 = img.Channels[m + 1].GetRow(y); @@ -193,4 +195,208 @@ public static void InverseRct(Configuration configuration, JxlModularImage img, InverseRctRow(custom, in0, in1, in2, out0, out1, out2); }); } + + /// + /// Performs Reversible Color Transform (RCT) on one row. + /// + /// + /// The type of RCT transform method. + /// + /// R + /// G + /// B + /// Y + /// Co + /// Cg + private static void ForwardRctRow(int transform, Span in0, Span in1, Span in2, Span out0, Span out1, Span out2) + { + DebugGuard.MustBeLessThanOrEqualTo(transform, 6, nameof(transform)); + + int second = transform >> 1; + int third = transform & 1; + + int width = in0.Length; // All input & output channels have equal widths + + if (!Vector.IsHardwareAccelerated || Vector.Count == 1) + { + // No SIMD support. Use scalar. + if (transform == 6) + { + for (int x = 0; x < width; x++) + { + int r = in0[x]; + int g = in1[x]; + int b = in2[x]; + int o1 = r - b; + int tmp = b + (o1 >> 1); + int o2 = g - tmp; + out0[x] = tmp + (o2 >> 1); + out1[x] = o1; + out2[x] = o2; + } + } + else + { + for (int x = 0; x < width; x++) + { + int firstCoeff = in0[x]; + int secondCoeff = in1[x]; + int thirdCoeff = in2[x]; + + if (second == 1) + { + secondCoeff -= firstCoeff; + } + else if (second == 2) + { + secondCoeff -= (firstCoeff + thirdCoeff) >> 1; + } + + if (third != 0) + { + thirdCoeff -= firstCoeff; + } + + out0[x] = firstCoeff; + out1[x] = secondCoeff; + out2[x] = thirdCoeff; + } + } + } + else + { + // Have SIMD support + int lanes = Vector.Count; + + if (transform == 6) + { + for (int x = 0; x < width; x += lanes) + { + Vector r = new(in0[x..]); + Vector g = new(in1[x..]); + Vector b = new(in2[x..]); + Vector o1 = r - b; + Vector tmp = b + (o1 >> 1); + Vector o2 = g - tmp; + Vector o0 = tmp + (o2 >> 1); + o0.CopyTo(out0[x..]); + o1.CopyTo(out1[x..]); + o2.CopyTo(out2[x..]); + } + } + else + { + for (int x = 0; x < width; x += lanes) + { + Vector i0 = new(in0[x..]); + Vector i1 = new(in1[x..]); + Vector i2 = new(in2[x..]); + Vector o1 = i1; + + // TODO: duplicate loops for second == 1, second == 2 + // and otherwise? We should reduce the number of branches in + // loops. + if (second == 1) + { + o1 -= i0; + } + else if (second == 2) + { + o1 -= (i0 + i2) >> 1; + } + + Vector o2 = i2; + + if (third != 0) + { + o2 -= i0; + } + + i0.CopyTo(out0[x..]); + o1.CopyTo(out1[x..]); + o2.CopyTo(out2[x..]); + } + } + } + } + + private static void RctPermute(InlineArray3 input, int permutation, ref InlineArray3 output) + { + output[0] = input[permutation % 3]; + output[1] = input[(permutation + 1 + (permutation / 3)) % 3]; + output[2] = input[(permutation + 2 - (permutation / 3)) % 3]; + } + + /// + /// Performs Forward Reversible Color Transform. (Internal method) + /// + /// + /// Configuration for parallelism. + /// + /// + /// Input channels. + /// + /// + /// Output channels. + /// + /// + /// Kind of RCT. + /// + private static void ForwardRctCore(Configuration configuration, InlineArray3 input, InlineArray3 output, int rctType) + { + int permutation = rctType / 7; + int transform = rctType % 7; + + InlineArray3 inp = default; + RctPermute(input, permutation, ref inp); + + int width = output[0].Width; + int height = output[0].Height; + + _ = Parallel.For(0, height, configuration.GetParallelOptions(), y => + { + Span i0 = inp[0].GetRow(y); + Span i1 = inp[1].GetRow(y); + Span i2 = inp[2].GetRow(y); + + Span o0 = output[0].GetRow(y); + Span o1 = output[1].GetRow(y); + Span o2 = output[2].GetRow(y); + + ForwardRctRow(transform, i0, i1, i2, o0, o1, o2); + }); + } + + /// + /// Performs forward Reversible Color Transform (RCT). + /// + /// + /// Configuration for parallelism. + /// + /// + /// Image to perform Reversible Color Transform. + /// + /// + /// Offset of the color channel. + /// + /// + /// Kind of RCT. + /// + public static void ForwardRct(Configuration configuration, JxlModularImage image, int beginC, int rctType) + { + JxlTransform.CheckEqualChannels(image, beginC, beginC + 2); + + if (rctType == 0) + { + // No-op + return; + } + + InlineArray3 channels = default; + channels[0] = image.Channels[beginC]; + channels[1] = image.Channels[beginC + 1]; + channels[2] = image.Channels[beginC + 2]; + + return ForwardRct(configuration, new JxlModularImage(channels[0], channels[1], channels[2]), beginC, rctType); + } } From c8b0a5d9cbf00ae20c8c2ba704150453599134c8 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 24 Aug 2026 23:31:30 +0400 Subject: [PATCH 089/142] Simplify decoder core & add palette shared methods --- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 92 +++-------- .../Modular/Transforms/JxlPalette.cs | 152 ++++++++++++++++++ 2 files changed, 173 insertions(+), 71 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index d1edd36b9d..30dc630ff2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -1525,10 +1525,9 @@ private bool ReadBundle(Span data, JxlBitReader br, T bundle) /// /// Reads all basic metadata and headers. /// - /// Status of the parsing. /// Thrown if the data is incorrect. /// Thrown if the data is malformed. - public bool ReadBasicInfo(Stream stream) + public void ReadBasicInfo(Stream stream) { if (!this.gotCodestreamSignature) { @@ -1568,16 +1567,13 @@ public bool ReadBasicInfo(Stream stream) { throw new InvalidOperationException("The image is too large"); } - - return true; } /// /// Parses all necessary headers. /// - /// Status of the parsing. /// Thrown if data is incorrect. - public bool ReadAllHeaders() + public void ReadAllHeaders() { if (!this.gotTransformData) { @@ -1635,8 +1631,6 @@ public bool ReadAllHeaders() } this.imageMetadata = this.metadata.ImageMetadata; - - return true; } /// @@ -1651,8 +1645,8 @@ public void ProcessSections() var toc = this.frameDecoder!.Toc; long pos = 0; - List sectionInfo = []; - List sectionStatus = []; + List sectionInfo = []; + List sectionStatus = []; for (long i = this.nextSection; i < toc.Size; i++) { @@ -1671,7 +1665,7 @@ public void ProcessSections() } JxlBitReader br = new(span.Slice((int)pos, (int)size)); - sectionInfo.Add(new(br, id, i)); + sectionInfo.Add(new(br, (int)id, (int)i)); sectionStatus.Add(default); pos += size; } @@ -1680,7 +1674,7 @@ public void ProcessSections() bool outOfBounds = false; - foreach (JxlFrameDecoder.SectionInfo info in sectionInfo) + foreach (JxlSectionInfo info in sectionInfo) { if (!info.BitReader.AllReadsWithinBounds) { @@ -1696,13 +1690,13 @@ public void ProcessSections() for (int i = 0; i < sectionStatus.Count; i++) { - JxlFrameDecoder.SectionStatus ss = sectionStatus[i]; + JxlSectionStatus ss = sectionStatus[i]; - if (ss == JxlFrameDecoder.Done) + if (ss == JxlSectionStatus.Done) { this.sectionProcessed[sectionInfo[i].Index] = 1; } - else if (ss != JxlFrameDecoder.Skipped) + else if (ss != JxlSectionStatus.Skipped) { throw new InvalidOperationException("Unexpected section status"); } @@ -1729,12 +1723,7 @@ public int ProcessCodestream() { if (!this.gotBasicInfo) { - bool status = this.ReadBasicInfo(); - - if (!status) - { - throw new InvalidOperationException("Could not parse basic info"); - } + this.ReadBasicInfo(); } if ((this.eventsWanted & BasicInfo) != 0) @@ -1751,12 +1740,7 @@ public int ProcessCodestream() if (!this.gotAllHeaders) { - bool status = this.ReadAllHeaders(); - - if (!status) - { - throw new InvalidOperationException("Could not parse headers"); - } + this.ReadAllHeaders(); } if ((this.eventsWanted & ColorEncoding) != 0) @@ -1781,12 +1765,7 @@ public int ProcessCodestream() while (true) { bool parseFrames = (this.eventsWanted & (PreviewImage | DecodedFrame | FullImage)) != 0; - if (!parseFrames) - { - break; - } - - if (this.frameStage == JxlFrameStage.Header && this.isLastTotal) + if (!parseFrames || (this.frameStage == JxlFrameStage.Header && this.isLastTotal)) { break; } @@ -1934,8 +1913,7 @@ public int ProcessCodestream() this.frameDecoder.SetRenderSpotcolors(this.renderSpotcolors); this.frameDecoder.SetCoalescing(this.coalescing); - if (!this.previewFrame && - (this.eventsWanted & FrameProgression) != 0) + if (!this.previewFrame && (this.eventsWanted & FrameProgression) != 0) { this.frameProgressiveDetail = this.frameDecoder.SetPauseAtProgressive(this.progressiveDetail); } @@ -2312,20 +2290,12 @@ public int ProcessBoxes(Stream stream) if (this.reconstructionExifSize > 0) { - int status = JxlToJpegDecoder.SetExif(this.exifMetadata!.Memory, jpegData); - if (status != Success) - { - return status; - } + JxlToJpegDecoder.SetExif(this.exifMetadata!.Memory, jpegData); } if (this.reconstructionXmpSize > 0) { - int status = JxlToJpegDecoder.SetXmp(this.xmpMetadata!.Memory, jpegData); - if (status != Success) - { - return status; - } + JxlToJpegDecoder.SetXmp(this.xmpMetadata!.Memory, jpegData); } this.reconstructionOutputJpeg = JpegReconstructionStage.Output; @@ -2333,11 +2303,7 @@ public int ProcessBoxes(Stream stream) if (this.reconstructionOutputJpeg == JpegReconstructionStage.Output && !this.JbrdNeedsMoreBoxes()) { - int status = this.jpegDecoder!.WriteOutput(this.imageBundle!.JpegData); - if (status != Success) - { - return status; - } + this.jpegDecoder!.WriteOutput(this.imageBundle!.JpegData); this.reconstructionOutputJpeg = JpegReconstructionStage.None; this.imageBundle.Reset(); @@ -2365,22 +2331,12 @@ public int ProcessBoxes(Stream stream) if (this.availableInput == 0) { - if (this.decoderStage != JxlDecoderStage.CodeStreamFinished) - { - return NeedMoreInput; - } - - if (this.JbrdNeedsMoreBoxes()) + if (this.decoderStage != JxlDecoderStage.CodeStreamFinished || this.JbrdNeedsMoreBoxes()) { return NeedMoreInput; } - if (this.inputClosed) - { - return Success; - } - - if ((this.eventsWanted & Box) != 0) + if (this.inputClosed || (this.eventsWanted & Box) != 0) { return Success; } @@ -2610,12 +2566,11 @@ public int ProcessBoxes(Stream stream) { bool hasMoreData = this.TryInjectNextBufferedJxlpBox(); - if (hasMoreData) + if (!hasMoreData) { - continue; + this.boxStage = JxlBoxStage.Header; } - this.boxStage = JxlBoxStage.Header; continue; } } @@ -2732,12 +2687,7 @@ public int ProcessBoxes(Stream stream) { if (this.boxContentsUnbounded) { - if (this.inputClosed) - { - return Success; - } - - if (!this.boxOutBufferSet) + if (this.inputClosed || !this.boxOutBufferSet) { return Success; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs new file mode 100644 index 0000000000..4babb79f32 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs @@ -0,0 +1,152 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; + +/// +/// Palette/indexed coding +/// +internal static class JxlPalette +{ + /// + /// Represents maximum number of colors in a palette. + /// + private const int MaxPaletteLookupTableSize = 1 << 16; + + /// + /// Represents number of channels for RGB. This is 3 because RGB + /// has three channels: R (Red), G (Green), B (Blue). + /// + private const int RgbChannels = 3; + + /// + /// 5x5x5 color cube for the larger cube. + /// + private const int LargeCube = 5; + + /// + /// Smaller interleaved color cube to fill the holes of the larger cube. + /// + private const int SmallCube = 4; + + /// + /// Number of bits required to represent a small cube. + /// + private const int SmallCubeBits = 2; // 2 bits gives us 0..3 inclusive, so it's perfect to represent a small cube + + /// + /// Cube of SmallCube + /// + private const int LargeCubeOffset = SmallCube * SmallCube * SmallCube; + + private const int ImplicitPaletteSize = LargeCubeOffset + (LargeCube * LargeCube * LargeCube); + + /// + /// Static delta palette used by GetPaletteValue. + /// + private static readonly int[][] DeltaPalette = + [ + [0, 0, 0], [4, 4, 4], [11, 0, 0], + [0, 0, -13], [0, -12, 0], [-10, -10, -10], + [-18, -18, -18], [-27, -27, -27], [-18, -18, 0], + [0, 0, -32], [-32, 0, 0], [-37, -37, -37], + [0, -32, -32], [24, 24, 45], [50, 50, 50], + [-45, -24, -24], [-24, -45, -45], [0, -24, -24], + [-34, -34, 0], [-24, 0, -24], [-45, -45, -24], + [64, 64, 64], [-32, 0, -32], [0, -32, 0], + [-32, 0, 32], [-24, -45, -24], [45, 24, 45], + [24, -24, -45], [-45, -24, 24], [80, 80, 80], + [64, 0, 0], [0, 0, -64], [0, -64, -64], + [-24, -24, 45], [96, 96, 96], [64, 64, 0], + [45, -24, -24], [34, -34, 0], [112, 112, 112], + [24, -45, -45], [45, 45, -24], [0, -32, 32], + [24, -24, 45], [0, 96, 96], [45, -24, 24], + [24, -45, -24], [-24, -45, 24], [0, -64, 0], + [96, 0, 0], [128, 128, 128], [64, 0, 64], + [144, 144, 144], [96, 96, 0], [-36, -36, 36], + [45, -24, -45], [45, -45, -24], [0, 0, -96], + [0, 128, 128], [0, 96, 0], [45, 24, -45], + [-128, 0, 0], [24, -45, 24], [-45, 24, -45], + [64, 0, -64], [64, -64, -64], [96, 0, 96], + [45, -45, 24], [24, 45, -45], [64, 64, -64], + [128, 128, 0], [0, 0, -128], [-24, 45, -45] + ]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Scale(int denominator, int value, int bitDepth) + { + DebugGuard.IsTrue(denominator == 4, "Denominator is defined as 4"); + + return (value * ((1 << bitDepth) - 1)) >> 2; + } + + public static int GetPaletteValue(Span palette, int index, int c, int oneRow, int bitDepth) + { + if (index < 0) + { + if (c >= RgbChannels) + { + return 0; + } + + index = -(index + 1); + index %= 1 + (2 * (DeltaPalette.Length - 1)); + + // JPEG XL reference uses: + // static constexpr int kMultiplier[] = {-1, 1}; + // kMultiplier[index & 1] + // + // we use: + // ((index & 1) != 0 ? 1 : -1) + // + // The latter avoids multiplication and can make use + // of CPU registers instead of memory access (if the JIT allows it). + int result = DeltaPalette[(index + 1) >> 1][c] * ((index & 1) != 0 ? 1 : -1); + + if (bitDepth > 8) + { + result *= 1 << (bitDepth - 8); + } + + return result; + } + else if (palette.Length <= index && index < palette.Length + LargeCubeOffset) + { + if (c >= RgbChannels) + { + return 0; + } + + index -= palette.Length; + index >>= c * SmallCubeBits; + return Scale(SmallCube, index % SmallCube, bitDepth) + (1 << Math.Max(0, bitDepth - 3)); + } + else if (palette.Length + LargeCubeOffset <= index) + { + if (c >= RgbChannels) + { + return 0; + } + + switch (c) + { + case 1: + index /= LargeCube; + break; + + case 2: + index /= LargeCube * LargeCube; + break; + + default: + break; + } + + return Scale(LargeCube - 1, index % LargeCube, bitDepth); + } + + return palette[(c * oneRow) + index]; + } +} From c079cf9165a30b24764ea6a6e64218236b6074a7 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 24 Aug 2026 23:49:27 +0400 Subject: [PATCH 090/142] Add missing types, rename JxlBitDepth metadata to JxlBitDepthMetadata, reduce errors in JxlDecoderCore --- ...{JxlBitDepth.cs => JxlBitDepthMetadata.cs} | 6 ++-- .../Jxl/IO/Metadata/JxlExtraChannelInfo.cs | 2 +- .../Jxl/IO/Metadata/JxlImageMetadata.cs | 2 +- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 36 +++---------------- .../Formats/Jxl/Processing/JxlBitDepth.cs | 27 ++++++++++++++ .../Formats/Jxl/Processing/JxlBitDepthType.cs | 35 ++++++++++++++++++ .../Formats/Jxl/Processing/JxlDataType.cs | 31 ++++++++++++++++ .../Formats/Jxl/Processing/JxlEndianness.cs | 25 +++++++++++++ .../Formats/Jxl/Processing/JxlPixelFormat.cs | 34 ++++++++++++++++++ 9 files changed, 162 insertions(+), 36 deletions(-) rename src/ImageSharp/Formats/Jxl/IO/Metadata/{JxlBitDepth.cs => JxlBitDepthMetadata.cs} (96%) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlBitDepth.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlBitDepthType.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlDataType.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlEndianness.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlPixelFormat.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepth.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepthMetadata.cs similarity index 96% rename from src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepth.cs rename to src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepthMetadata.cs index 0a9db0f575..a6c9f72482 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepth.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepthMetadata.cs @@ -8,15 +8,15 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; /// /// Represents the JPEG XL Bit Depth image metadata. /// -internal sealed class JxlBitDepth : IJxlFields +internal sealed class JxlBitDepthMetadata : IJxlFields { private uint bitsPerSample; private uint exponentBitsPerSample; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public JxlBitDepth() => JxlBundle.Init(this); + public JxlBitDepthMetadata() => JxlBundle.Init(this); /// /// Gets or sets a value indicating whether diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs index 5b492b1026..16e149d1d4 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs @@ -11,7 +11,7 @@ internal sealed class JxlExtraChannelInfo : IJxlFields public JxlExtraChannel Type { get; set; } - public JxlBitDepth? BitDepth { get; set; } + public JxlBitDepthMetadata? BitDepth { get; set; } public int DimensionShift { get; set; } diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs index 1fcf00b6fe..e05bb09512 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs @@ -10,7 +10,7 @@ internal sealed class JxlImageMetadata : IJxlFields { public bool AllDefault { get; set; } - public JxlBitDepth? BitDepth { get; set; } + public JxlBitDepthMetadata? BitDepth { get; set; } public bool Modular16BitBufferSufficient { get; set; } // Otherwise, 32 is diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 30dc630ff2..35c52cc460 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -442,7 +442,7 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable /// /// Bit depth for image output. /// - private JxlBitDepth imageOutputBitDepth = new(); + private JxlBitDepthMetadata imageOutputBitDepth = new(); public JxlDecoderCore(DecoderOptions options) : base(options) @@ -595,32 +595,6 @@ private enum JxlSignature : byte Container } - /// - /// Represents a data type. - /// - private enum JxlDataType : byte - { - /// - /// - /// - UInt8, - - /// - /// - /// - UInt16, - - /// - /// - /// - Float, - - /// - /// - /// - Float16 - } - /// /// Frame stage for this decoder. /// @@ -858,12 +832,12 @@ private static JxlSignature DetectSignature(Stream buffer) return JxlSignature.Invalid; } - private static int BitsPerChannel(JxlDataType dataType) + private static uint BitsPerChannel(JxlDataType dataType) => dataType switch { - JxlDataType.UInt8 => 8, - JxlDataType.UInt16 or JxlDataType.Float16 => 16, - JxlDataType.Float => 32, + JxlDataType.Byte => 8, + JxlDataType.UInt16 or JxlDataType.Half => 16, + JxlDataType.Single => 32, _ => 0 }; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlBitDepth.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlBitDepth.cs new file mode 100644 index 0000000000..52b384bb94 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlBitDepth.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Describes the interpretation of the input and output +/// buffers. +/// +internal struct JxlBitDepth +{ + /// + /// Gets or sets the kind of bit depth. + /// + public JxlBitDepthType Type { get; set; } + + /// + /// Gets or sets the number of bits per sample when the + /// bit depth type is custom. + /// + public uint BitsPerSample { get; set; } + + /// + /// Gets or sets the custom exponent bits per sample. + /// + public uint ExponentBitsPerSample { get; set; } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlBitDepthType.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlBitDepthType.cs new file mode 100644 index 0000000000..e942d57a47 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlBitDepthType.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Specifies the kind of bit depth. +/// +internal enum JxlBitDepthType : byte +{ + /// + /// Default setting, where the encoder expects the + /// input pixels to use the full range of the pixel format + /// data type (e.g. for ushort, the input range is 0..65535 + /// and the value 65535 is mapped to 1.0 when converting + /// to float), and the decoder uses the full range to output + /// pixels. + /// + FromPixelFormat, + + /// + /// When selected, the encoder expects the input pixels + /// to be in the range defined by the bits per sample value of the + /// basic info (e.g., for 12-bit images using ushort data types + /// the range is 0..4095 and the 4095 value is mapped to 1.0 when + /// converting to float), and the decoder outputs pixels in + /// this range. + /// + FromCodeStream, + + /// + /// Specifies custom ranges for pixel outputs. + /// + Custom = 2, +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDataType.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDataType.cs new file mode 100644 index 0000000000..a9c8057e6f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDataType.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Specifies which data type to use for sample values +/// per channel per pixel. +/// +internal enum JxlDataType : byte +{ + /// + /// Use float + /// + Single = 0, + + /// + /// Use byte. May clip wide color gamut data. + /// + Byte = 2, + + /// + /// Use ushort. May clip wide color gamut data. + /// + UInt16 = 3, + + /// + /// Use 16-bit IEEE 754 half-precision floating-point values. + /// + Half = 5 +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlEndianness.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlEndianness.cs new file mode 100644 index 0000000000..a7c9dbc1a2 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlEndianness.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Specifies the ordering of multi-byte data. +/// +internal enum JxlEndianness : byte +{ + /// + /// Use endianness of the CPU/system. + /// + Native, + + /// + /// Force little endian. + /// + Little, + + /// + /// Force big endian. + /// + Big +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPixelFormat.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPixelFormat.cs new file mode 100644 index 0000000000..15aa587159 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPixelFormat.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Data type for the sample values per channel per pixel +/// for the output buffer for pixels. +/// +internal struct JxlPixelFormat +{ + /// + /// Gets or sets the amount of channels available in a pixel buffer. + /// + public int Channels { get; set; } + + /// + /// Gets or sets the data type of each channel. + /// + public JxlDataType DataType { get; set; } + + /// + /// Gets or sets a value that denotes whether multi-byte data types are represented in + /// big-endian or little-endian format. Applies to ushort + /// and float data types. + /// + public JxlEndianness Endianness { get; set; } + + /// + /// Gets or sets the alignment of scanlines to a multiple of + /// align bytes. + /// + public int Align { get; set; } +} From 7e236700ad16fc30b89582a23d6e7cac1eabbf32 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 24 Aug 2026 23:57:39 +0400 Subject: [PATCH 091/142] Add CodestreamMarker, don't manually check for out-of-bounds Bit Reader should automatically throw if it reads out of bounds anyway --- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 36 ++++++------------- .../Formats/Jxl/Processing/JxlShared.cs | 7 ++++ 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 35c52cc460..0287760e43 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -223,7 +223,7 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable /// /// Output data for extra channels. /// - private List extraChannelOutputs = []; + private readonly List extraChannelOutputs = []; /// /// Codec metadata if present. @@ -255,7 +255,7 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable /// private long nextSection; - private List sectionProcessed = []; + private readonly List sectionProcessed = []; /// /// The frame header, if present. @@ -301,11 +301,11 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable /// /// All frame reference.s /// - private List frameReferences = []; + private readonly List frameReferences = []; - private List frameExternalToInternal = []; + private readonly List frameExternalToInternal = []; - private List frameRequired = []; + private readonly List frameRequired = []; /// /// Codestream input data is temporarily copied here. @@ -802,7 +802,7 @@ private static JxlSignature DetectSignature(Stream buffer) throw new EndOfStreamException(); } - if (secondByte == CodestreamMarker) + if (secondByte == JxlShared.CodestreamMarker) { return JxlSignature.CodeStream; } @@ -961,7 +961,7 @@ public bool CanAddBuffer(long length) { const long bufferLimit = 1 << 48; return length < bufferLimit && - (length + this.jxlpOooBufferTotal + (this.codestreamCopy?.Memory.Length ?? 0)) < bufferLimit; + (length + this.jxlpOooBufferTotal + (this.codestreamCopy?.AsMemory().Length ?? 0)) < bufferLimit; } public bool TryInjectNextBufferedJxlpBox() @@ -1471,14 +1471,14 @@ private void BeforeUpdateState(string propertyName) /// Reads a single bundle into . /// /// Type of the bundle to read. - /// Bundle binary data. + /// Stream to read data from. /// Bit reader to continue from. /// The bundle to parse. /// Status of parsing the bundle. - private bool ReadBundle(Span data, JxlBitReader br, T bundle) + private bool ReadBundle(Stream stream, JxlBitReader br, T bundle) where T : IJxlFields { - JxlBitReader reader = new(data); + JxlBitReader reader = new(stream); reader.SkipBits64((ulong)br.TotalBitsConsumed); bool canRead = JxlBundle.CanRead(reader, bundle); @@ -1646,22 +1646,6 @@ public void ProcessSections() this.frameDecoder.ProcessSections(sectionInfo, sectionStatus); - bool outOfBounds = false; - - foreach (JxlSectionInfo info in sectionInfo) - { - if (!info.BitReader.AllReadsWithinBounds) - { - outOfBounds = true; - break; - } - } - - if (outOfBounds) - { - throw new InvalidOperationException("Frame out of bounds"); - } - for (int i = 0; i < sectionStatus.Count; i++) { JxlSectionStatus ss = sectionStatus[i]; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlShared.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlShared.cs index 711dbc27ea..e8126bc4dc 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlShared.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlShared.cs @@ -18,6 +18,13 @@ internal static class JxlShared /// public const int MaximumNumberOfReferenceFrames = 4; + /// + /// Reserved by ISO/IEC 10918-1. LF causes files opened in text mode + /// to be rejected because the marker changes to 0x0D instead. The + /// 0xFF prefix also ensures there were no 7-bit transmission limitations. + /// + public const byte CodestreamMarker = 0x0A; + /// /// Gets the 12-byte signature (a.k.a. magic) for JPEG XL files. /// From b8f4094a77ff077b8bcf616e8fadd25693abe689 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 25 Aug 2026 00:12:11 +0400 Subject: [PATCH 092/142] Reduce errors --- .../Formats/Jxl/Fields/JxlReadVisitor.cs | 12 ---- .../Jxl/Processing/Decoder/JxlBitReader.cs | 65 +++++++++---------- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 15 ++--- 3 files changed, 36 insertions(+), 56 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs index c60e5985ee..d54dba6706 100644 --- a/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs +++ b/src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs @@ -116,18 +116,6 @@ public override bool EndExtensions() 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/Processing/Decoder/JxlBitReader.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs index 9dbc090aec..33b7307d30 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBitReader.cs @@ -8,56 +8,61 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; /// /// Represents a bitstream reader. /// -internal ref struct JxlBitReader(ReadOnlySpan bytes) +internal sealed class JxlBitReader(Stream stream) { - private readonly ReadOnlySpan data = 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 readonly long TotalBitsConsumed => ((long)this.pointer * 8) + (64 - this.bufferRemainingBits); + public long TotalBitsConsumed => ((long)this.pointer * 8) + (64 - this.bufferRemainingBits); /// /// Fetches a new buffer. /// private void RefillCore() { - int remaining = this.data.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) + Span temp = stackalloc byte[8]; + int bytesRead = stream.Read(temp); + if (bytesRead == 8) { - this.buffer = BinaryPrimitives.ReadUInt64LittleEndian(this.data[this.pointer..]); + this.buffer = BinaryPrimitives.ReadUInt64LittleEndian(temp); this.bufferRemainingBits = 64u; this.pointer += 8; } else { + if (bytesRead == 0) + { + throw new EndOfStreamException(); + } + ulong value = 0; - for (int i = 0; i < remaining; i++) + for (int i = 0; i < bytesRead; i++) { - value |= (ulong)this.data[this.pointer + i] << (8 * i); + value |= (ulong)temp[i] << (8 * i); } this.buffer = value; - this.bufferRemainingBits = (uint)(remaining * 8); - this.pointer += remaining; + this.bufferRemainingBits = (uint)(bytesRead * 8); + this.pointer += bytesRead; + } + } + + public void JumpToByteBoundary() + { + uint remainder = (uint)(this.TotalBitsConsumed % 8); + + if (remainder == 0) + { + return; + } + + if (this.ReadBits32(8u - remainder) != 0) + { + throw new InvalidDataException("Non-zero padding bits"); } } @@ -74,11 +79,6 @@ private ulong ReadBits64Core(uint n, bool peek = false) DebugGuard.MustBeLessThanOrEqualTo(n, 64u, nameof(n)); this.MaybeRefill(); - if (this.IsEndOfStream) - { - JxlThrowHelper.ThrowEndOfStream(); - } - if (n <= this.bufferRemainingBits) { ulong result = this.buffer & ((1UL << (int)n) - 1); @@ -119,11 +119,6 @@ private uint ReadBits32Core(uint n, bool peek = false) DebugGuard.MustBeLessThanOrEqualTo(n, 32u, nameof(n)); this.MaybeRefill(); - if (this.IsEndOfStream) - { - JxlThrowHelper.ThrowEndOfStream(); - } - if (n <= this.bufferRemainingBits) { uint result = (uint)(this.buffer & ((1UL << (int)n) - 1)); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 0287760e43..0fe20686a5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -1753,11 +1753,6 @@ public int ProcessCodestream() this.frameDecoder.InitializeFrame(reader, this.imageBundle!, this.previewFrame); - if (!reader.AllReadsWithinBounds) - { - return this.TryRequestMoreInput() ? 1 : 0; - } - this.AdvanceCodeStream(reader.TotalBitsConsumed / JxlMath.BitsPerByte); this.frameHeader = this.frameDecoder.GetFrameHeader(); @@ -2701,13 +2696,13 @@ public void DecodeInput() if (!this.gotSignature) { - JxlSignatureCheck status = CheckSignature(this.nextInput, this.availableInput); - if (status == JxlSignatureCheck.InvalidSignature) + JxlSignature signature = DetectSignature(this.nextInput, this.availableInput); + if (signature == JxlSignature.Invalid) { throw new InvalidOperationException("The signature is invalid."); } - if (status == JxlSignatureCheck.NotEnoughBytes) + if (signature == JxlSignature.NotEnoughBytes) { if (this.inputClosed) { @@ -2719,7 +2714,7 @@ public void DecodeInput() this.gotSignature = true; - if (status == JxlSignatureCheck.Container) + if (signature == JxlSignature.Container) { this.haveContainer = true; } @@ -2750,6 +2745,8 @@ public void DecodeInput() } } + private static void ThrowNotEnoughData() => throw new InvalidOperationException("Not enough data"); + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) => throw new NotImplementedException(); protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) => throw new NotImplementedException(); From 2b43fd6d14e5e280005b2fe64d23f642448fdfab Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 25 Aug 2026 00:17:28 +0400 Subject: [PATCH 093/142] Simplify --- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 0fe20686a5..016269e4da 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -2314,11 +2314,6 @@ public int ProcessBoxes(Stream stream) if (this.boxType == JxlBoxTypes.Brob) { - if (this.availableInput < headerSize + 4) - { - return NeedMoreInput; - } - this.boxDecodedType = BitConverter.ToInt32(this.nextInput!.Memory.Span[(int)headerSize..]); } else @@ -2423,11 +2418,6 @@ public int ProcessBoxes(Stream stream) throw new InvalidOperationException("The file type box is too small"); } - if (this.availableInput < 8) - { - return NeedMoreInput; - } - if (BinaryUtils.ReadInt32BigEndian(stream) != 0x6A786C20) // Bytes "jxl " in Big Endian { throw new InvalidOperationException("File type box major brand must be \"jxl \""); @@ -2449,11 +2439,6 @@ public int ProcessBoxes(Stream stream) throw new InvalidOperationException("Cannot have jxlp box after last jxlp box"); } - if (this.availableInput < 4) - { - return NeedMoreInput; - } - if (!this.boxContentsUnbounded && this.boxContentsSize < 4) { throw new InvalidOperationException("jxlp box is too small to contain an index"); From 9f8eb4a434c7386bdb6bebdc2d1bcc49e84ac30e Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 25 Aug 2026 19:34:40 +0400 Subject: [PATCH 094/142] Add box content decoder with Brotli compression & reduce errors --- .../Decoder/JxlBoxContentDecoder.cs | 108 ++++++++++++++++++ .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 10 +- 2 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs new file mode 100644 index 0000000000..7621102ef9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs @@ -0,0 +1,108 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.IO.Compression; +using SixLabors.ImageSharp.Formats.Jxl.IO; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Allows decoding and decompressing box data. +/// +internal sealed class JxlBoxContentDecoder +{ + /// + /// Specifies how many bytes to read to fetch box data. This is ignored + /// if the box extends till EOF. + /// + private ulong boxSize; + + /// + /// When true the box size is ignored and is unbounded - that is, keeps going + /// till the end of the file or stream. + /// + private bool boxExtendsTillEnd; + + /// + /// This contains flags that determine whether the box is Brotli-compressed or + /// not. + /// + private JxlBoxCodingMode codingMode; + + /// + /// Prepares parsing the box. + /// + /// Specifies box compression. + /// Specifies whether or not the box size keeps going till the end of stream. + /// Specifies the fixed size of the box when it is not unbounded. + public void Initialize(JxlBoxCodingMode codingMode, bool isUnbounded, ulong size) + { + this.boxSize = size; + this.codingMode = codingMode; + this.boxExtendsTillEnd = isUnbounded; + } + + /// + /// Prepares parsing the box. + /// + /// True if the box is compressed with Brotli. If uncompressed - false. + /// Specifies whether or not the box size keeps going till the end of stream. + /// Specifies the fixed size of the box when it is not unbounded. + public void Initialize(bool isBrotliCompressed, bool isUnbounded, ulong size) + => this.Initialize( + isBrotliCompressed ? JxlBoxCodingMode.Brotli : JxlBoxCodingMode.Uncompressed, + isUnbounded, + size); + + public void Process(Stream stream, JxlMemoryWriter writer) + { + byte[] cache = ArrayPool.Shared.Rent(16384); + + try + { + if (this.codingMode == JxlBoxCodingMode.Brotli) + { + using BrotliStream brotli = new(stream, CompressionMode.Decompress, leaveOpen: true); + + int bytesRead; + while ((bytesRead = brotli.Read(cache, 0, cache.Length)) > 0) + { + writer.Write(cache.AsSpan(0, bytesRead)); + } + } + else + { + if (this.boxExtendsTillEnd) + { + int bytesRead; + while ((bytesRead = stream.Read(cache, 0, cache.Length)) > 0) + { + writer.Write(cache.AsSpan(0, bytesRead)); + } + } + else + { + ulong bytesLeft = this.boxSize; + while (bytesLeft > 0) + { + int toRead = (int)Math.Min((ulong)cache.Length, bytesLeft); + int bytesRead = stream.Read(cache, 0, toRead); + + if (bytesRead == 0) + { + throw new EndOfStreamException("Unexpected EOF while reading box content"); + } + + writer.Write(cache.AsSpan(0, bytesRead)); + bytesLeft -= (ulong)bytesRead; + } + } + } + } + finally + { + ArrayPool.Shared.Return(cache); + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 016269e4da..7ca3ace0e9 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -1508,7 +1508,7 @@ public void ReadBasicInfo(Stream stream) Span fileSignature = stackalloc byte[2]; stream.ReadExactly(fileSignature); - if (fileSignature[0] != 0xFF || fileSignature[1] != CodestreamMarker) + if (fileSignature[0] != 0xFF || fileSignature[1] != JxlShared.CodestreamMarker) { throw new InvalidOperationException("The file signature is invalid"); } @@ -1553,7 +1553,7 @@ public void ReadAllHeaders() { Span span = this.GetCodeStreamSpan(); - JxlBitReader reader = new(span); + JxlBitReader reader = new(this.stream); reader.SkipBits64((ulong)this.codestreamBitsAhead); this.metadata!.CustomTransformData!.NonserializedXybEncoded = this.metadata.ImageMetadata!.XybEncoded; @@ -2286,7 +2286,7 @@ public int ProcessBoxes(Stream stream) { if (this.decoderStage != JxlDecoderStage.CodeStreamFinished || this.JbrdNeedsMoreBoxes()) { - return NeedMoreInput; + ThrowNotEnoughData(); } if (this.inputClosed || (this.eventsWanted & Box) != 0) @@ -2294,7 +2294,7 @@ public int ProcessBoxes(Stream stream) return Success; } - return NeedMoreInput; + ThrowNotEnoughData(); } bool boxedCodestreamDone = ((this.eventsWanted & Box) != 0) @@ -2556,7 +2556,7 @@ public int ProcessBoxes(Stream stream) if (!boxDone) { - return NeedMoreInput; + ThrowNotEnoughData(); } this.boxStage = JxlBoxStage.Header; From 315ea0fb9e4fe00b7ba059e15b8b520782db00a1 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 25 Aug 2026 19:45:52 +0400 Subject: [PATCH 095/142] Reduce errors & refine documentation --- .../Jxl/Processing/Decoder/JxlBoxContentDecoder.cs | 3 ++- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs index 7621102ef9..5d1beddf3c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs @@ -8,7 +8,8 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; /// -/// Allows decoding and decompressing box data. +/// Allows decoding and decompressing box data in JPEG XL +/// container format. /// internal sealed class JxlBoxContentDecoder { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 7ca3ace0e9..92c0962c8b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -347,7 +347,7 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable /// OOO jxlp payloads keyed by counter. Keys are: codestream bytes without /// 4byte header, and is_last. /// - private Dictionary jxlpOooBuffer = []; + private readonly Dictionary jxlpOooBuffer = []; private long jxlpOooBufferTotal; @@ -358,14 +358,14 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable /// /// Decompresses box contents. /// - private JxlBoxContentDecoder? boxContentDecoder; + private readonly JxlBoxContentDecoder? boxContentDecoder; /// /// Decodes JPEG XL to JPEG. /// private JxlToJpegDecoder? jpegDecoder; - private JxlBoxContentDecoder? metadataDecoder; + private readonly JxlBoxContentDecoder? metadataDecoder; /// /// Raw bytes for EXIF metadata. @@ -2363,13 +2363,13 @@ public int ProcessBoxes(Stream stream) if ((this.eventsWanted & Box) != 0) { bool decompress = this.decompressBoxes && this.boxType == JxlBoxTypes.Brob; - this.boxContentDecoder.StartBox(decompress, this.boxContentsUnbounded, this.boxContentsSize); + this.boxContentDecoder!.Initialize(decompress, this.boxContentsUnbounded, (ulong)this.boxContentsSize); } if (this.storeExif == 1 || this.storeXmp == 1) { bool brob = this.boxType == JxlBoxTypes.Brob; - this.metadataDecoder.StartBox(brob, this.boxContentsUnbounded, this.boxContentsSize); + this.metadataDecoder!.Initialize(brob, this.boxContentsUnbounded, (ulong)this.boxContentsSize); } if (this.boxType == JxlBoxTypes.FileType) @@ -2631,7 +2631,7 @@ public int ProcessBoxes(Stream stream) } this.AdvanceInput(this.availableInput); - return NeedMoreInput; + ThrowNotEnoughData(); } long remaining = this.boxContentsEnd - this.filePosition; @@ -2639,7 +2639,7 @@ public int ProcessBoxes(Stream stream) { this.basicInfoSizeHint = InitialBasicInfoSizeHint() + this.boxContentsEnd - this.filePosition; this.AdvanceInput(this.availableInput); - return NeedMoreInput; + ThrowNotEnoughData(); } else { From 57958f3b1e82fc1d8493f4e850f08d8ee3dc3fe4 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 25 Aug 2026 19:54:15 +0400 Subject: [PATCH 096/142] Reduce errors & remove unnecessary if statements --- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 92c0962c8b..51c05d1f3a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -2111,6 +2111,7 @@ private static void ParseBoxHeader(Stream input, out JxlBoxType type, out long b /// /// Processes all boxes and their contents if this is a container format. /// + /// Stream to decode from. /// Status of processing. /// Thrown when data is invalid. public int ProcessBoxes(Stream stream) @@ -2664,24 +2665,16 @@ public int ProcessBoxes(Stream stream) /// /// Main core decoding routine. /// + /// Stream to decode from. /// Thrown when data or input parameters are invalid. - public void DecodeInput() + public void DecodeInput(Stream stream) { - if (this.decoderStage == JxlDecoderStage.Initialized) - { - this.decoderStage = JxlDecoderStage.Started; - } - - if (this.decoderStage == JxlDecoderStage.Error) - { - // Should NEVER occur! If it does make sure to always reset the decoder - // in the Decode method. - throw new InvalidOperationException("The core decoder cannot be used because it contains an error. A reset must be made."); - } + this.Reset(); + this.decoderStage = JxlDecoderStage.Started; if (!this.gotSignature) { - JxlSignature signature = DetectSignature(this.nextInput, this.availableInput); + JxlSignature signature = DetectSignature(stream); if (signature == JxlSignature.Invalid) { throw new InvalidOperationException("The signature is invalid."); @@ -2709,12 +2702,7 @@ public void DecodeInput() } } - int status = this.ProcessBoxes(); - - if (status == NeedMoreInput && this.inputClosed) - { - ThrowNotEnoughData(); - } + int status = this.ProcessBoxes(stream); if (status == Success) { From 0678f35b83baf349cdea9502881ac60cd8366480 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 25 Aug 2026 20:31:58 +0400 Subject: [PATCH 097/142] Add missing types, reduce errors --- .../Decoder/JxlBoxTypes.Generated.cs | 47 +++++++ .../Jxl/Processing/Decoder/JxlBoxTypes.tt | 47 +++++++ .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 124 +++++++++--------- .../Processing/Decoder/JxlDecoderStatus.cs | 87 ++++++++++++ 4 files changed, 243 insertions(+), 62 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxTypes.Generated.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxTypes.tt create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderStatus.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxTypes.Generated.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxTypes.Generated.cs new file mode 100644 index 0000000000..66bb65a4df --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxTypes.Generated.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Contains constants which represent JPEG XL container format +/// box types as an integer. +/// +internal static class JxlBoxTypes +{ + /// + /// ftyp + /// + public const int FileType = 0x66747970; + + /// + /// jxlc + /// + public const int JxlCodeStream = 0x6A786C63; + + /// + /// jxlp + /// + public const int JxlPartialCodeStream = 0x6A786C70; + + /// + /// brob + /// + public const int Brob = 0x62726F62; + + /// + /// xml + /// + public const int Xml = 0x786D6C20; + + /// + /// Exif + /// + public const int Exif = 0x45786966; + + /// + /// jxl + /// + public const int Jxl = 0x6A786C20; + +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxTypes.tt b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxTypes.tt new file mode 100644 index 0000000000..92be8c00c1 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxTypes.tt @@ -0,0 +1,47 @@ +<#@ template language="C#" #> +<#@ import namespace="System" #> +<#@ import namespace="System.IO" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ output extension=".Generated.cs" #> +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +<# + string[] boxTypes = new[] + { + "FileType=ftyp", + "JxlCodeStream=jxlc", + "JxlPartialCodeStream=jxlp", + "Brob=brob", + "Xml=xml ", + "Exif=Exif", + "Jxl=jxl " + }; +#> +/// +/// Contains constants which represent JPEG XL container format +/// box types as an integer. +/// +internal static class JxlBoxTypes +{ +<# + foreach (string boxType in boxTypes) + { + string[] split = boxType.Split('='); + string name = split[0]; + string fourCC = split[1]; + uint boxNum = ((uint)fourCC[0] << 24) | + ((uint)fourCC[1] << 16) | + ((uint)fourCC[2] << 8) | + fourCC[3]; + string hex = $"0x{boxNum:X8}"; +#> + /// + /// <#= fourCC #> + /// + public const int <#= name #> = <#= hex #>; + +<# } #> +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 51c05d1f3a..818f1ec76f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -1684,16 +1684,16 @@ public int ProcessCodestream() this.ReadBasicInfo(); } - if ((this.eventsWanted & BasicInfo) != 0) + if ((this.eventsWanted & JxlDecoderStatus.BasicInfo) != 0) { - this.eventsWanted &= ~BasicInfo; - return JxlCodestreamType.BasicInfo; + this.eventsWanted &= ~JxlDecoderStatus.BasicInfo; + return JxlDecoderStatus.BasicInfo; } if (this.eventsWanted == 0) { this.decoderStage = JxlDecoderStage.CodeStreamFinished; - return JxlCodestreamType.Success; + return JxlDecoderStatus.Success; } if (!this.gotAllHeaders) @@ -1701,16 +1701,16 @@ public int ProcessCodestream() this.ReadAllHeaders(); } - if ((this.eventsWanted & ColorEncoding) != 0) + if ((this.eventsWanted & JxlDecoderStatus.ColorEncoding) != 0) { - this.eventsWanted &= ~ColorEncoding; - return JxlCodestreamType.ColorEncoding; + this.eventsWanted &= ~JxlDecoderStatus.ColorEncoding; + return JxlDecoderStatus.ColorEncoding; } if (this.eventsWanted == 0) { this.decoderStage = JxlDecoderStage.CodeStreamFinished; - return JxlCodestreamType.Success; + return JxlDecoderStatus.Success; } this.postHeaders = true; @@ -1722,7 +1722,7 @@ public int ProcessCodestream() while (true) { - bool parseFrames = (this.eventsWanted & (PreviewImage | DecodedFrame | FullImage)) != 0; + bool parseFrames = (this.eventsWanted & (JxlDecoderStatus.PreviewImage | JxlDecoderStatus.Frame | JxlDecoderStatus.FullImage)) != 0; if (!parseFrames || (this.frameStage == JxlFrameStage.Header && this.isLastTotal)) { break; @@ -1763,7 +1763,7 @@ public int ProcessCodestream() throw new InvalidOperationException("Frame is too large"); } - int outputType = this.previewFrame ? PreviewImage : FullImage; + int outputType = this.previewFrame ? JxlDecoderStatus.PreviewImage : JxlDecoderStatus.FullImage; bool outputNeeded = (this.eventsWanted & outputType) != 0; if (outputNeeded) @@ -1776,7 +1776,7 @@ public int ProcessCodestream() this.frameStage = JxlFrameStage.Toc; if (this.previewFrame) { - if ((this.eventsWanted & PreviewImage) == 0) + if ((this.eventsWanted & JxlDecoderStatus.PreviewImage) == 0) { this.frameStage = JxlFrameStage.Header; this.AdvanceCodeStream(this.remainingFrameSize); @@ -1853,11 +1853,11 @@ public int ProcessCodestream() } } - if ((this.eventsWanted & Frame) != 0 && this.isLastOfStill) + if ((this.eventsWanted & JxlDecoderStatus.Frame) != 0 && this.isLastOfStill) { if (!this.skippingFrame) { - return Frame; + return JxlDecoderStatus.Frame; } } @@ -1866,7 +1866,7 @@ public int ProcessCodestream() this.frameDecoder.SetRenderSpotcolors(this.renderSpotcolors); this.frameDecoder.SetCoalescing(this.coalescing); - if (!this.previewFrame && (this.eventsWanted & FrameProgression) != 0) + if (!this.previewFrame && (this.eventsWanted & JxlDecoderStatus.Progression) != 0) { this.frameProgressiveDetail = this.frameDecoder.SetPauseAtProgressive(this.progressiveDetail); } @@ -1880,7 +1880,7 @@ public int ProcessCodestream() this.sectionProcessed.Clear(); ResizeSectionProcessed(this.frameDecoder.Toc.Size); - if (this.previewFrame || (this.eventsWanted & FullImage) != 0) + if (this.previewFrame || (this.eventsWanted & JxlDecoderStatus.FullImage) != 0) { this.frameStage = JxlFrameStage.Full; } @@ -1902,14 +1902,14 @@ public int ProcessCodestream() { if (this.previewFrame) { - return NeedPreviewOutBuffer; + return JxlDecoderStatus.NeedPreviewOutBuffer; } if ((!this.jpegDecoder.IsOutputSet || this.imageBundle!.JpegData is null) && this.isLastOfStill && !this.skippingFrame) { - return NeedImageOutputBuffer; + throw new InvalidOperationException("Image output buffer is too small"); } } @@ -1960,7 +1960,7 @@ public int ProcessCodestream() { this.dcFrameProgressionDone = true; this.downsamplingTarget = 8; - return Progression; + return JxlDecoderStatus.Progression; } bool newProgressionStepDone = this.frameDecoder.NumCompletePasses >= nextNumPassesToPause; @@ -1970,7 +1970,7 @@ public int ProcessCodestream() newProgressionStepDone) { this.downsamplingTarget = this.frameHeader.Passes.GetDownsamplingTargetForCompletedPasses(this.frameDecoder.NumCompletePasses); - return Progression; + return JxlDecoderStatus.Progression; } if (!allSectionsDone) @@ -1996,7 +1996,7 @@ public int ProcessCodestream() this.frameStage = JxlFrameStage.Header; this.reconstructionOutputJpeg = JpegReconstructionStage.SetMetadata; - return FullImage; + return JxlDecoderStatus.FullImage; } if (this.previewFrame || this.isLastOfStill) @@ -2013,12 +2013,12 @@ public int ProcessCodestream() { this.gotPreviewImage = true; this.previewFrame = false; - this.eventsWanted &= ~PreviewImage; - return PreviewImage; + this.eventsWanted &= ~JxlDecoderStatus.PreviewImage; + return JxlDecoderStatus.PreviewImage; } - else if (this.isLastOfStill && (this.eventsWanted & FullImage) != 0 && !this.skippingFrame) + else if (this.isLastOfStill && (this.eventsWanted & JxlDecoderStatus.FullImage) != 0 && !this.skippingFrame) { - return FullImage; + return JxlDecoderStatus.FullImage; } } } @@ -2100,12 +2100,12 @@ public void SetJpegBuffer(Memory data) /// /// Thrown when data is invalid. /// - private static void ParseBoxHeader(Stream input, out JxlBoxType type, out long boxSize, out long headerSize) + private static void ParseBoxHeader(Stream input, out int type, out long boxSize, out long headerSize) { JxlBoxHeader header = JxlBoxHeader.ReadHeader(input); boxSize = (long)header.Size; headerSize = (header.ContainsLargeSize ? 12 : 4) + 4; - type = (JxlBoxType)header.Type; + type = (int)header.Type; } /// @@ -2124,12 +2124,12 @@ public int ProcessBoxes(Stream stream) // this.AdvanceInput(this.headerSize); this.headerSize = 0; - if ((this.eventsWanted & Box) != 0 && this.boxEvent && !this.boxOutBufferSetCurrentBox) + if ((this.eventsWanted & JxlDecoderStatus.Box) != 0 && this.boxEvent && !this.boxOutBufferSetCurrentBox) { this.boxEvent = false; } - if ((this.eventsWanted & Box) != 0 && this.boxOutBufferSetCurrentBox) + if ((this.eventsWanted & JxlDecoderStatus.Box) != 0 && this.boxOutBufferSetCurrentBox) { Memory nextOut = this.boxOutputBuffer!.Memory[(int)this.boxOutBufferPos..]; long availOut = this.boxOutBufferSize - this.boxOutBufferPos; @@ -2147,12 +2147,12 @@ public int ProcessBoxes(Stream stream) long produced = startSlice.Length - availOut; this.boxOutBufferPos += produced; - if (status == Complete && (this.eventsWanted & Complete) == 0) + if (status == JxlDecoderStatus.Complete && (this.eventsWanted & JxlDecoderStatus.Complete) == 0) { - status = Success; + status = JxlDecoderStatus.Success; } - if (status is not (Success or NeedMoreInput)) + if (status is not (JxlDecoderStatus.Success or JxlDecoderStatus.NeedMoreInput)) { return status; } @@ -2191,7 +2191,7 @@ public int ProcessBoxes(Stream stream) long produced = originalNextOutput.Length - nextOutput.Length; this.reconstructionOutputBufferPos += produced; - if (boxResult == NeedMoreOutput) + if (boxResult == JxlDecoderStatus.NeedMoreOutput) { if (md.Length >= blockSizeLimit) { @@ -2200,11 +2200,11 @@ public int ProcessBoxes(Stream stream) Array.Resize(ref md, md.Length * 2); } - else if (boxResult == NeedMoreInput) + else if (boxResult == JxlDecoderStatus.NeedMoreInput) { break; } - else if (boxResult == Complete) + else if (boxResult == JxlDecoderStatus.Complete) { long neededSize = this.storeExif == 1 ? this.reconstructionExifSize : this.reconstructionXmpSize; @@ -2262,9 +2262,9 @@ public int ProcessBoxes(Stream stream) this.reconstructionOutputJpeg = JpegReconstructionStage.None; this.imageBundle.Reset(); - if ((this.eventsWanted & FullImage) != 0) + if ((this.eventsWanted & JxlDecoderStatus.FullImage) != 0) { - return FullImage; + return JxlDecoderStatus.FullImage; } } @@ -2274,7 +2274,7 @@ public int ProcessBoxes(Stream stream) { if (this.decoderStage == JxlDecoderStage.CodeStreamFinished) { - return Success; + return JxlDecoderStatus.Success; } this.boxStage = JxlBoxStage.CodeStream; @@ -2290,15 +2290,15 @@ public int ProcessBoxes(Stream stream) ThrowNotEnoughData(); } - if (this.inputClosed || (this.eventsWanted & Box) != 0) + if (this.inputClosed || (this.eventsWanted & JxlDecoderStatus.) != 0) { - return Success; + return JxlDecoderStatus.Success; } ThrowNotEnoughData(); } - bool boxedCodestreamDone = ((this.eventsWanted & Box) != 0) + bool boxedCodestreamDone = ((this.eventsWanted & JxlDecoderStatus.Box) != 0) && this.decoderStage == JxlDecoderStage.CodeStreamFinished && !this.JbrdNeedsMoreBoxes() && this.lastCodestreamSeen; @@ -2306,9 +2306,9 @@ public int ProcessBoxes(Stream stream) if (boxedCodestreamDone && this.availableInput >= 2 && this.nextInput!.Memory.Span[0] == 0xFF && - this.nextInput.Memory.Span[1] == CodestreamMarker) + this.nextInput.Memory.Span[1] == JxlShared.CodestreamMarker) { - return Success; + return JxlDecoderStatus.Success; } int status = ParseBoxHeader(this.nextInput, this.availableInput, 0, this.filePosition, this.boxType, out long boxSize, out long headerSize); @@ -2326,10 +2326,10 @@ public int ProcessBoxes(Stream stream) if (boxedCodestreamDone && this.boxType == JxlBoxTypes.Jxl) { - return Success; + return JxlDecoderStatus.Success; } - if (this.boxCount == 2 && this.boxType != JxlBoxType.FileType) + if (this.boxCount == 2 && this.boxType != JxlBoxTypes.FileType) { throw new InvalidOperationException("The second box must be a ftyp (File Type) box"); } @@ -2346,7 +2346,7 @@ public int ProcessBoxes(Stream stream) this.boxSize = boxSize; this.headerSize = headerSize; - if ((this.originalEventsWanted & JpegReconstruction) != 0) + if ((this.originalEventsWanted & JxlDecoderStatus.JpegReconstruction) != 0) { if (this.storeExif == 0 && this.boxDecodedType == JxlBoxTypes.Exif) { @@ -2361,7 +2361,7 @@ public int ProcessBoxes(Stream stream) } } - if ((this.eventsWanted & Box) != 0) + if ((this.eventsWanted & JxlDecoderStatus.Box) != 0) { bool decompress = this.decompressBoxes && this.boxType == JxlBoxTypes.Brob; this.boxContentDecoder!.Initialize(decompress, this.boxContentsUnbounded, (ulong)this.boxContentsSize); @@ -2391,9 +2391,9 @@ public int ProcessBoxes(Stream stream) { this.boxStage = JxlBoxStage.PartialCodeStream; } - else if ((this.originalEventsWanted & JpegReconstruction) != 0 && this.boxType == JxlBoxTypes.JpegReconstructionData) + else if ((this.originalEventsWanted & JxlDecoderStatus.JpegReconstruction) != 0 && this.boxType == JxlBoxTypes.JpegReconstructionData) { - if ((this.eventsWanted & JpegReconstruction) == 0) + if ((this.eventsWanted & JxlDecoderStatus.JpegReconstruction) == 0) { throw new InvalidOperationException("Multiple JPEG reconstruction boxes detected"); } @@ -2405,11 +2405,11 @@ public int ProcessBoxes(Stream stream) this.boxStage = JxlBoxStage.Skip; } - if ((this.eventsWanted & Box) != 0) + if ((this.eventsWanted & JxlDecoderStatus.Box) != 0) { this.boxEvent = true; this.boxOutBufferSetCurrentBox = false; - return Box; + return JxlDecoderStatus.Box; } } else if (this.boxStage == JxlBoxStage.Ftyp) @@ -2469,7 +2469,7 @@ public int ProcessBoxes(Stream stream) { if (this.jxlpOooBuffer.Count >= NumBuffersLimit) { - return Error; + return JxlDecoderStatus.Error; } // When creating a new OOO (Out-of-order) entry, @@ -2491,7 +2491,7 @@ public int ProcessBoxes(Stream stream) { int status = this.ProcessCodestream(); - if (status == FullImage) + if (status == JxlDecoderStatus.FullImage) { if (this.reconstructionOutputJpeg != JpegReconstructionStage.None) { @@ -2499,7 +2499,7 @@ public int ProcessBoxes(Stream stream) } } - if (status == NeedMoreInput) + if (status == JxlDecoderStatus.NeedMoreInput) { if (this.filePosition == this.boxContentsEnd && !this.boxContentsUnbounded) { @@ -2514,7 +2514,7 @@ public int ProcessBoxes(Stream stream) } } - if (status == Success) + if (status == JxlDecoderStatus.Success) { if (this.JbrdNeedsMoreBoxes()) { @@ -2527,7 +2527,7 @@ public int ProcessBoxes(Stream stream) break; } - if ((this.eventsWanted & Box) != 0) + if ((this.eventsWanted & JxlDecoderStatus.Box) != 0) { this.boxStage = JxlBoxStage.Skip; continue; @@ -2544,7 +2544,7 @@ public int ProcessBoxes(Stream stream) if (!this.CanAddBuffer(remaining) || !this.jxlpOooBuffer.TryGetValue(this.bufferingJxlpIndex, out JxlOooEntry? entry)) { - return Error; + return JxlDecoderStatus.Error; } // Now we want to write the 'remaining' number of bytes @@ -2577,7 +2577,7 @@ public int ProcessBoxes(Stream stream) long consumed = this.nextInput.Memory.Length - nextInput.Length; this.AdvanceInput(consumed); - if (reconstructionResult == JpegReconstruction) + if (reconstructionResult == JxlDecoderStatus.JpegReconstruction) { JxlJpegData jpegData = this.jpegDecoder!.GetJpegData(); long numExif = JxlToJpegDecoder.NumExifMarkers(jpegData); @@ -2611,10 +2611,10 @@ public int ProcessBoxes(Stream stream) this.boxStage = JxlBoxStage.Header; - if ((this.eventsWanted & JpegReconstruction) != 0) + if ((this.eventsWanted & JxlDecoderStatus.JpegReconstruction) != 0) { - this.eventsWanted &= ~JpegReconstruction; - return JpegReconstruction; + this.eventsWanted &= ~JxlDecoderStatus.JpegReconstruction; + return JxlDecoderStatus.JpegReconstruction; } } else @@ -2628,7 +2628,7 @@ public int ProcessBoxes(Stream stream) { if (this.inputClosed || !this.boxOutBufferSet) { - return Success; + return JxlDecoderStatus.Success; } this.AdvanceInput(this.availableInput); @@ -2654,7 +2654,7 @@ public int ProcessBoxes(Stream stream) } } - return Success; + return JxlDecoderStatus.Success; } /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderStatus.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderStatus.cs new file mode 100644 index 0000000000..93a4e0b365 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderStatus.cs @@ -0,0 +1,87 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Pending event masks and procedure statuses used by the JPEG XL decoder. +/// +internal static class JxlDecoderStatus +{ + /// + /// Everything went smoothly. + /// + public const int Success = 0; + + /// + /// An error occurred. + /// + public const int Error = 1; + + /// + /// Not enough bytes left to continue. + /// + public const int NeedMoreInput = 2; + + /// + /// The decoder can decode a preview image and requests setting + /// a preview output buffer. + /// + public const int NeedPreviewOutBuffer = 3; + + /// + /// Not enough memory allocated for the output image buffer. + /// + public const int NeedMoreOutput = 6; + + /// + /// Specifies an event mask for parsing the JXL basic info. + /// + public const int BasicInfo = 0x40; + + /// + /// Specifies an event mask for parsing and decoding the ICC color profile. + /// + public const int ColorEncoding = 0x100; + + /// + /// Specifies decoding a preview image or a small frame. + /// + public const int PreviewImage = 0x200; + + /// + /// Specifies an event mask for decoding a single frame. This + /// event is called once for a still image and multiple times for + /// animated JPEG XLs. + /// + public const int Frame = 0x400; + + /// + /// Specifies an event mask for decoding a full frame (or layer in case coalescing + /// is disabled). + /// + public const int FullImage = 0x1000; + + /// + /// Specifies pending JXL->JPEG decoding. + /// + public const int JpegReconstruction = 0x2000; + + /// + /// Specifies pending decompression of box data. + /// See . + /// + public const int Box = 0x4000; + + /// + /// Specifies an event mask for a progressive step in decoding + /// the frame. + /// + public const int Progression = 0x8000; + + /// + /// Specifies an event mask that specifies a box being decoded is + /// now complete. + /// + public const int Complete = 0x10000; +} From 4118c4a191e013ac4e6248c6f27bea1042e2953a Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:33:41 +0400 Subject: [PATCH 098/142] Quantizer improvements, reduce errors - Use Stream for Box Content Decoder - Reduce errors in JxlDecoderCore - Add GetStride method to JxlFrameDecoder - Add JxlDctQuantWeightParameters and JxlQuantizerEncoding to implement more quant_weights.h components, and add proper documentation to each member of JxlQuantMode. - Remove InlineArray2 (there's already one built into System.Runtime.CompilerServices, so prefer to use that) --- src/ImageSharp/Formats/Jxl/InlineArrays.cs | 9 - .../Decoder/JxlBoxContentDecoder.cs | 2 +- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 11 +- .../Jxl/Processing/Decoder/JxlFrameDecoder.cs | 25 +++ .../Processing/JxlDctQuantWeightParameters.cs | 15 +- .../Formats/Jxl/Processing/JxlQuantMode.cs | 34 +++- .../Jxl/Processing/JxlQuantizerEncoding.cs | 180 ++++++++++++++++++ 7 files changed, 251 insertions(+), 25 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs diff --git a/src/ImageSharp/Formats/Jxl/InlineArrays.cs b/src/ImageSharp/Formats/Jxl/InlineArrays.cs index 6276051700..f006d2fd09 100644 --- a/src/ImageSharp/Formats/Jxl/InlineArrays.cs +++ b/src/ImageSharp/Formats/Jxl/InlineArrays.cs @@ -7,15 +7,6 @@ namespace SixLabors.ImageSharp.Formats.Jxl; -/// -/// Used by Butteraugli -/// -[InlineArray(2)] -internal struct InlineArray2 -{ - private T first; -} - [InlineArray(3)] internal struct InlineArray3 { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs index 5d1beddf3c..d339e69087 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs @@ -56,7 +56,7 @@ public void Initialize(bool isBrotliCompressed, bool isUnbounded, ulong size) isUnbounded, size); - public void Process(Stream stream, JxlMemoryWriter writer) + public void Process(Stream stream, Stream writer) { byte[] cache = ArrayPool.Shared.Rent(16384); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 818f1ec76f..5578f47534 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -2138,11 +2138,8 @@ public int ProcessBoxes(Stream stream) Span startSlice = bufferSpan[(int)this.boxOutBufferPos..]; int status = this.boxContentDecoder!.Process( - this.nextInput, - this.availableInput, - this.filePosition - this.boxContentsBegin, - nextOut, - ref availOut); + stream, + this.boxOutputBuffer); long produced = startSlice.Length - availOut; this.boxOutBufferPos += produced; @@ -2290,7 +2287,7 @@ public int ProcessBoxes(Stream stream) ThrowNotEnoughData(); } - if (this.inputClosed || (this.eventsWanted & JxlDecoderStatus.) != 0) + if (this.inputClosed || (this.eventsWanted & JxlDecoderStatus.Box) != 0) { return JxlDecoderStatus.Success; } @@ -2704,7 +2701,7 @@ public void DecodeInput(Stream stream) int status = this.ProcessBoxes(stream); - if (status == Success) + if (status == JxlDecoderStatus.Success) { if (this.CanUseMoreCodestreamInput()) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs index b17aacd5cb..e127354a7b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs @@ -32,6 +32,31 @@ internal sealed class JxlFrameDecoder private JxlProgressiveDetail progressiveDetail = JxlProgressiveDetail.Frames; private List passesToPause = []; + /// + /// Gets a value indicating whether there are any DC groups left to decode. + /// + private bool ContainsDcGroupToDecode => this.decodedDcGroups.Any(x => x == 0); + + private static int GetStride(int width, JxlPixelFormat format) + { + if (!JxlMath.SafeMultiply(BytesPerChannel(format.DataType), format.Channels, out int xStride)) + { + throw new InvalidOperationException("Image too large"); + } + + if (!JxlMath.SafeMultiply(xStride, width, out int yStride)) + { + throw new InvalidOperationException("Image too large"); + } + + if (!JxlMath.SafeRoundUpTo(yStride, format.Align, yStride)) + { + throw new InvalidOperationException("Image too large"); + } + + return yStride; + } + public static void DecodeGlobalDcInfo(Configuration configuration, JxlBitReader reader, bool isJpeg, JxlPassesDecoderState state) { state.SharedStorage.Quantizer.Decode(reader); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctQuantWeightParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctQuantWeightParameters.cs index 288d129956..8cd8a16abd 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctQuantWeightParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctQuantWeightParameters.cs @@ -8,21 +8,22 @@ internal sealed class JxlDctQuantWeightParameters private const int Log2MaxDistanceBands = 4; private const int MaxDistanceBands = 1 + (1 << Log2MaxDistanceBands); - private int numDistanceBands; - private readonly float[][] distanceBands; - public JxlDctQuantWeightParameters() { - this.distanceBands = new float[3][]; + this.DistanceBands = new float[3][]; for (int i = 0; i < 3; i++) { - this.distanceBands[i] = new float[MaxDistanceBands]; + this.DistanceBands[i] = new float[MaxDistanceBands]; } } public JxlDctQuantWeightParameters(float[][] distanceBands, int numDistanceBands) { - this.numDistanceBands = numDistanceBands; - this.distanceBands = distanceBands; + this.NumDistanceBands = numDistanceBands; + this.DistanceBands = distanceBands; } + + public int NumDistanceBands { get; set; } + + public float[][] DistanceBands { get; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs index fa4dd20518..1b974343a4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs @@ -4,16 +4,48 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// -/// Quantization mode. +/// Specifies which algorithm should be used to quantize coefficients. /// internal enum JxlQuantMode : byte { + /// + /// The quantizer relies on predefined tables for quantization. + /// The idea is similar to Huffman coding. + /// Library, + + /// + /// The quantizer uses an Identity transform. + /// Id, + + /// + /// The quantizer uses a 2x2 Discrete Cosine Transform. + /// Dct2, + + /// + /// The quantizer uses a 4x4 Discrete Cosine Transform. + /// Dct4, + + /// + /// The quantizer uses a 4x8 Discrete Cosine Transform. + /// Dct4x8, + + /// + /// The quantizer uses the AFV transform. + /// Afv, + + /// + /// The quantizer uses a Discrete Cosine Transform with custom block size. + /// Dct, + + /// + /// No quantization is performed. Input data becomes the output as-is. + /// Raw } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs new file mode 100644 index 0000000000..a50b7ad25b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs @@ -0,0 +1,180 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Specifies weights and quantizer modes. +/// +internal sealed class JxlQuantizerEncoding +{ + /// + /// Gets or sets the kind of transform used for this quantizer encoding. + /// + public JxlQuantMode Mode { get; set; } + + /// + /// Gets or sets the weights for DCT4+ tables. + /// + public JxlDctQuantWeightParameters? DctParameters { get; set; } + + /// + /// Gets or sets the weights for the 4x4 sub-block in AFV. + /// + public JxlDctQuantWeightParameters? DctParametersAfv4x4 { get; set; } + + /// + /// Gets or sets the weights for the identity transform. + /// + public InlineArray3> IdWeights { get; set; } + + /// + /// Gets or sets the weights for the DCT2 transform. + /// + public InlineArray3> Dct2Weights { get; set; } + + /// + /// Gets or sets the multipliers for the DCT4 transform. + /// + public InlineArray3> Dct4Multipliers { get; set; } + + /// + /// Gets or sets the weights for the AFV transform. + /// + public InlineArray3> AfvWeights { get; set; } + + /// + /// Gets or sets the multipliers for the 4x8 DCT block-based transform. + /// + public InlineArray3 Dct4x8Multipliers { get; set; } + + /// + /// Gets or sets the explicit quantization table (like in JPEG). + /// + /// + /// Only used when == . + /// + public int[]? QuantizationTable { get; set; } + + /// + /// Gets or sets the denominator for each item in the explicit quantization table. + /// + /// + /// Only used when == . + /// + public float QuantizationTableDenominator { get; set; } = 1f / (8 * 255); + + /// + /// Gets or sets a value indicating which predefined table to use. The value is + /// only used when == . + /// + public byte Predefined { get; set; } + + /// + /// Creates a new quantizer encoding with the Library quantizer mode + /// and the specified library index. + /// + /// The library index (aka predefined table). + /// A new Library quantizer encoding. + public static JxlQuantizerEncoding Library(int libraryIndex) + { + DebugGuard.MustBeLessThan(libraryIndex, JxlQuantWeights.NumPredefinedTables, nameof(libraryIndex)); + + return new() + { + Mode = JxlQuantMode.Library, + Predefined = (byte)libraryIndex + }; + } + + /// + /// Creates a new quantizer encoding with the Identity quantizer mode + /// and the specified XYB/identity weights. + /// + /// Weights for the identity transform. + /// A new Identity quantizer encoding. + public static JxlQuantizerEncoding Identity(in InlineArray3> xybWeights) + => new() + { + Mode = JxlQuantMode.Id, + IdWeights = xybWeights + }; + + /// + /// Creates a new quantizer encoding with the DCT2x2 quantizer mode + /// and the specified XYB/DCT2x2 weights. + /// + /// Weights for the DCT2x2 transform. + /// A new DCT2x2 quantizer encoding. + public static JxlQuantizerEncoding Dct2(in InlineArray3> xybWeights) + => new() + { + Mode = JxlQuantMode.Dct2, + Dct2Weights = xybWeights + }; + + /// + /// Creates a new quantizer encoding with the DCT4x4 quantizer mode, + /// the specified XYB/DCT4x4 multipliers, and quantizer weight parameters. + /// + /// Quantizer weights for the DCT4x4 transform. + /// XYB multipliers for the DCT4x4 transform. + /// A new DCT4x4 quantizer encoding. + public static JxlQuantizerEncoding Dct4(JxlDctQuantWeightParameters parameters, in InlineArray3> xybMul) + => new() + { + Mode = JxlQuantMode.Dct4, + DctParameters = parameters, + Dct4Multipliers = xybMul + }; + + /// + /// Creates a new quantizer encoding with the DCT4x8 quantizer mode, + /// the specified XYB/DCT4x8 multipliers, and quantizer weight parameters. + /// + /// Quantizer weights for the DCT4x8 transform. + /// XYB multipliers for the DCT4x8 transform. + /// A new DCT4x8 quantizer encoding. + public static JxlQuantizerEncoding Dct4x8(JxlDctQuantWeightParameters parameters, in InlineArray3 xybMul) + => new() + { + Mode = JxlQuantMode.Dct4x8, + DctParameters = parameters, + Dct4x8Multipliers = xybMul + }; + + /// + /// Creates a new quantizer encoding with the DCT quantizer mode + /// and quantizer weight parameters. + /// + /// Quantizer weights for the DCT transform. + /// A new DCT quantizer encoding. + public static JxlQuantizerEncoding Dct(JxlDctQuantWeightParameters parameters) + => new() + { + Mode = JxlQuantMode.Dct, + DctParameters = parameters, + }; + + /// + /// Creates a new quantizer encoding with the AFV quantizer mode, + /// quantizer weight parameters for 4x8/4x4 blocks, and weights. + /// + /// Quantizer weights for the 4x8 sub-block for the AFV transform. + /// Quantizer weights for the 4x4 sub-block for the AFV transform. + /// Quantizer weights. + /// A new DCT quantizer encoding. + public static JxlQuantizerEncoding Afv( + JxlDctQuantWeightParameters params4x8, + JxlDctQuantWeightParameters params4x4, + in InlineArray3> weights) + => new() + { + Mode = JxlQuantMode.Afv, + DctParameters = params4x8, + AfvWeights = weights, + DctParametersAfv4x4 = params4x4 + }; +} From 874ec5bcfb67d5a214b839481f5f03704e6c01b5 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:46:03 +0400 Subject: [PATCH 099/142] Complete quantizer encoding --- .../Jxl/Processing/JxlQuantizerEncoding.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs index a50b7ad25b..fead5354cd 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs @@ -10,6 +10,34 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// internal sealed class JxlQuantizerEncoding { + public JxlQuantizerEncoding() + { + } + + public JxlQuantizerEncoding(JxlQuantizerEncoding other) + { + // Simple shallow copy + this.AfvWeights = other.AfvWeights; + this.Dct2Weights = other.Dct2Weights; + this.Dct4Multipliers = other.Dct4Multipliers; + this.Dct4x8Multipliers = other.Dct4x8Multipliers; + this.DctParameters = other.DctParameters; + this.DctParametersAfv4x4 = other.DctParametersAfv4x4; + this.IdWeights = other.IdWeights; + this.Mode = other.Mode; + this.Predefined = other.Predefined; + this.QuantizationTable = other.QuantizationTable; + this.QuantizationTableDenominator = other.QuantizationTableDenominator; + + if (other.QuantizationTable is not null) + { + // Do a deep clone for the quantization table. + // Using AsSpan() should be way faster than a normal array copy... + this.QuantizationTable = GC.AllocateUninitializedArray(other.QuantizationTable.Length); + other.QuantizationTable.AsSpan().CopyTo(this.QuantizationTable); + } + } + /// /// Gets or sets the kind of transform used for this quantizer encoding. /// @@ -177,4 +205,23 @@ public static JxlQuantizerEncoding Afv( AfvWeights = weights, DctParametersAfv4x4 = params4x4 }; + + /// + /// Creates a new raw quantizer encoding. + /// + /// The quantization table for raw quantization. + /// The shift value for the denominator. + /// A raw quantizer encoding. + public static JxlQuantizerEncoding Raw(Span quantizationTable, int shift = 0) + { + JxlQuantizerEncoding encoding = new() + { + Mode = JxlQuantMode.Raw, + QuantizationTableDenominator = (1 << shift) * (1f / (8 * 255)), + QuantizationTable = GC.AllocateUninitializedArray(quantizationTable.Length) + }; + + quantizationTable.CopyTo(encoding.QuantizationTable); + return encoding; + } } From acc474ea354527c2a186c272c2394e342940b389 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:25:32 +0400 Subject: [PATCH 100/142] Complete modular transforms, context prediction Common/Helpers - Add InterleaveLower and InterleaveUpper to Vector128_ and Vector256_ - Add unit test for InterleaveLower and InterleaveUpper (specifically for Vector256_) - Add Average to Numerics.cs Common - Add 32 and 33 to the InlineArray.tt text template Formats/Jxl/IO/Metadata - Remove unnecessary System.Runtime.CompilerServices using directive from JxlCustomTransformData and JxlOpsinInvreseMatrix Formats/Jxl/Processing/Decoder - Remove unncessary using SixLabors.ImageSharp.Formats.Jxl.IO Formats/Jxl/Processing/Encoder - Add partial Fast Lossless Encoder work (+enc_fast_lossless.cc; largest file in libjxl source) - Add linear algebra (+enc_linalg.cc, +enc_linalg.h) Formats/Jxl/Processing/Jpeg - Work that would later become JXL<->JPEG lossless coding mode Formats/Jxl/Processing/Modular/Encoding/ContextPrediction - Finish context prediction (+context_predict.h) Formats/Jxl/Processing/Modular/Transforms - Finish Reversible Color Transform (+rct.cc, +rct.h, +enc_rct.cc, +enc_rct.h) - Finish Palette/Indexed coding (+palette.cc, +palette.h, +enc_palette.cc, enc_palette.h) - Finish Squeeze transform (+squeeze.cc, +squeeze.h, +enc_squeeze.cc, +enc_squeeze.h) Formats/Jxl/Processing/RenderPipeline - Incomplete render pipeline abstractions with EPF (Edge Preserving Filter) 0 stage (+render_pipeline_stage.cc, +render_pipeline_stage.h, +stage_epf.cc, +stage_epf.h) Formats/Jxl/Processing/Splines - Remove unnecessary System.Runtime.CompilerServices using directive Formats/Jxl/Processing - Add dequantizer matrices - Remove JxlEndianness (prefer ByteOrder from ImageSharp/Common) - Add missing constant to JxlLoopFilter - Remove unnecessary using SixLabors.ImageSharp.Common.Helpers from JxlMath - Replace JxlPixelFormat to use ByteOrder - Update quantizers to use dequantizer matrices and quantizer weights - Add quantizer encoding and constants - Add SIMD utilities - Remove System.Runtime.CompilerServices using from JxlWeightsSeparable5 - Remove InlineArray3, InlineArray36 and InlineArray15 from InlineArrays (3 and 15 already exist in System.Runtime.CompilerServices; 36 already exists in InlineArray.tt from ImageSharp/Common) NEXT STEPS The current focus would be applying refactors and optimizations from reviews, followed by completing the JPEG XL modular. --- src/ImageSharp/Common/Helpers/Numerics.cs | 9 + .../Common/Helpers/Vector128Utilities.cs | 40 + .../Common/Helpers/Vector256Utilities.cs | 166 ++ src/ImageSharp/Common/InlineArray.cs | 22 +- src/ImageSharp/Common/InlineArray.tt | 2 +- .../Jxl/IO/Metadata/JxlCustomTransformData.cs | 1 + .../Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs | 1 + src/ImageSharp/Formats/Jxl/InlineArrays.cs | 33 - .../Decoder/JxlBoxContentDecoder.cs | 1 - .../Encoder/JxlFastLosslessEncoder.cs | 1365 +++++++++++++++++ .../Processing/Encoder/JxlLinearAlgebra.cs | 52 + .../Jxl/Processing/Jpeg/JpegAppMarkerType.cs | 30 + .../Jxl/Processing/JxlDequantMatrices.cs | 265 ++++ .../Formats/Jxl/Processing/JxlEndianness.cs | 25 - .../Formats/Jxl/Processing/JxlLoopFilter.cs | 5 + .../Formats/Jxl/Processing/JxlMath.cs | 53 + .../Formats/Jxl/Processing/JxlPixelFormat.cs | 2 +- .../Formats/Jxl/Processing/JxlQuantWeights.cs | 512 +++++++ .../Formats/Jxl/Processing/JxlQuantizer.cs | 12 +- .../Jxl/Processing/JxlQuantizerConstants.cs | 46 + .../Jxl/Processing/JxlQuantizerEncoding.cs | 22 +- ...JxlSimdUtils.StoreInterleaved.Generated.cs | 133 ++ .../JxlSimdUtils.StoreInterleaved.tt | 44 + .../Formats/Jxl/Processing/JxlSimdUtils.cs | 104 ++ .../Jxl/Processing/JxlWeightsSeparable5.cs | 2 + .../ContextPrediction/JxlContextPrediction.cs | 390 +++++ .../ContextPrediction/JxlPredictionResult.cs | 13 + .../ContextPrediction/JxlPredictorMode.cs | 35 + .../Processing/Modular/JxlModularChannel.cs | 24 +- .../Jxl/Processing/Modular/JxlModularImage.cs | 10 +- .../Modular/Transforms/JxlPalette.cs | 1227 ++++++++++++++- .../Processing/Modular/Transforms/JxlRct.cs | 1 + .../Modular/Transforms/JxlSqueeze.cs | 793 ++++++++++ .../Transforms/JxlSqueezeParameters.cs | 18 +- .../Modular/Transforms/JxlTransform.cs | 25 + .../Processing/RenderPipeline/Epf0Stage.cs | 109 ++ .../Processing/RenderPipeline/EpfStageType.cs | 14 + .../Jxl/Processing/RenderPipeline/EpfUtils.cs | 22 + .../RenderPipelineChannelMode.cs | 30 + .../RenderPipeline/RenderPipelineStageBase.cs | 92 ++ .../RenderPipelineStageConfiguration.cs | 21 + .../Processing/Splines/JxlSplineSegment.cs | 2 + src/ImageSharp/ImageSharp.csproj | 14 + .../Common/Vector256UtilitiesTests.cs | 48 + 44 files changed, 5731 insertions(+), 104 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlLinearAlgebra.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Jpeg/JpegAppMarkerType.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlDequantMatrices.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlEndianness.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerConstants.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt create mode 100644 src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictionResult.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictorMode.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/EpfStageType.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/EpfUtils.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineChannelMode.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageConfiguration.cs create mode 100644 tests/ImageSharp.Tests/Common/Vector256UtilitiesTests.cs diff --git a/src/ImageSharp/Common/Helpers/Numerics.cs b/src/ImageSharp/Common/Helpers/Numerics.cs index e5a6b45493..a5a5571795 100644 --- a/src/ImageSharp/Common/Helpers/Numerics.cs +++ b/src/ImageSharp/Common/Helpers/Numerics.cs @@ -1033,4 +1033,13 @@ public static nuint Vector512Count(this ReadOnlySpan span) public static nuint Vector512Count(int length) where TVector : struct => (uint)length / (uint)Vector512.Count; + + /// + /// Computes the average of two integers. + /// + /// First integer + /// Second integer + /// The average of x, y. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Average(int x, int y) => (x + y + ((x > y) ? 1 : 0)) >> 1; } diff --git a/src/ImageSharp/Common/Helpers/Vector128Utilities.cs b/src/ImageSharp/Common/Helpers/Vector128Utilities.cs index 6bb1f59ef8..6b4c6ad63c 100644 --- a/src/ImageSharp/Common/Helpers/Vector128Utilities.cs +++ b/src/ImageSharp/Common/Helpers/Vector128Utilities.cs @@ -859,4 +859,44 @@ public static Vector128 UnpackLow(Vector128 left, Vector128 Vector128 unpacked = Vector128.Create(left.GetLower(), right.GetLower()); return Vector128.ShuffleNative(unpacked, Vector128.Create(0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15)); } + + /// + /// Interleaves the lower half of the vector. + /// + /// First vector + /// Second vector + /// + /// { a[0], b[0], a[1], b[1] } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 InterleaveLower(Vector128 a, Vector128 b) + { + Vector128 shuffledA = Vector128.Shuffle(a, Vector128.Create(0, 0, 1, 1)); + Vector128 shuffledB = Vector128.Shuffle(b, Vector128.Create(0, 0, 1, 1)); + + Vector128 maskA = Vector128.Create(-1, 0, -1, 0); + Vector128 maskB = Vector128.Create(0, -1, 0, -1); + + return (shuffledA & maskA) | (shuffledB & maskB); + } + + /// + /// Interleaves the upper half of the vector. + /// + /// First vector + /// Second vector + /// + /// { a[2], b[2], a[3], b[3] } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 InterleaveUpper(Vector128 a, Vector128 b) + { + Vector128 shuffledA = Vector128.Shuffle(a, Vector128.Create(2, 2, 3, 3)); + Vector128 shuffledB = Vector128.Shuffle(b, Vector128.Create(2, 2, 3, 3)); + + Vector128 maskA = Vector128.Create(-1, 0, -1, 0); + Vector128 maskB = Vector128.Create(0, -1, 0, -1); + + return (shuffledA & maskA) | (shuffledB & maskB); + } } diff --git a/src/ImageSharp/Common/Helpers/Vector256Utilities.cs b/src/ImageSharp/Common/Helpers/Vector256Utilities.cs index 1dd7122713..4bd78b88fd 100644 --- a/src/ImageSharp/Common/Helpers/Vector256Utilities.cs +++ b/src/ImageSharp/Common/Helpers/Vector256Utilities.cs @@ -397,4 +397,170 @@ public static Vector256 UnpackLow(Vector256 left, Vector256 ri return Vector256.Create(lo, hi); } + + /// + /// Multiplies only the even indices of the two 256-bit vectors, + /// producing half as many elements of twice the element width. + /// + /// Left vector to multiply. + /// Right vector to multiply + /// + /// + /// { + /// A[0] * B[0], + /// A[2] * B[2], + /// A[4] * B[4], + /// A[6] * B[6] + /// } + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyEven(Vector256 left, Vector256 right) + => Vector256.Create( + left[0] * right[0], + left[2] * right[2], + left[4] * right[4], + left[6] * right[6]); + + /// + /// Multiplies only the odd indices of the two 256-bit vectors, + /// producing half as many elements of twice the element width. + /// + /// Left vector to multiply. + /// Right vector to multiply + /// + /// + /// { + /// A[1] * B[1], + /// A[3] * B[3], + /// A[5] * B[5], + /// A[7] * B[7] + /// } + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyOdd(Vector256 left, Vector256 right) + => Vector256.Create( + left[1] * right[1], + left[3] * right[3], + left[5] * right[5], + left[7] * right[7]); + + /// + /// Produces a vector by interleaving the even-indexed elements + /// of the left and right vectors. + /// + /// Left vector to interleave. + /// Right vector to interleave. + /// + /// + /// { + /// A[0], B[0], + /// A[2], B[2], + /// A[4], B[4], + /// A[6], B[6] + /// } + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 InterleaveEven( + Vector256 left, + Vector256 right) + => Vector256.Create( + left[0], + right[0], + left[2], + right[2], + left[4], + right[4], + left[6], + right[6]); + + /// + /// Produces a vector by interleaving the odd-indexed elements + /// of the left and right vectors. + /// + /// Left vector to interleave. + /// Right vector to interleave. + /// + /// + /// { + /// A[1], B[1], + /// A[3], B[3], + /// A[5], B[5], + /// A[7], B[7] + /// } + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 InterleaveOdd( + Vector256 left, + Vector256 right) + => Vector256.Create( + left[1], + right[1], + left[3], + right[3], + left[5], + right[5], + left[7], + right[7]); + + /// + /// Produces a vector with masks where 0xFFFFFFFF specifies + /// that the left value does not equal to the right value and + /// 0x00000000 specifies that the value equals to the + /// right value. + /// + /// Left vector to compare for inequality. + /// Right vector to compare for inequality. + /// + /// 0xFFFFFFFF for values that aren't equal, 0x00000000 for + /// values that are equal. This is essentially the inverse of + /// . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 NotEqual( + Vector256 left, + Vector256 right) => ~Vector256.Equals(left, right); + + /// + /// Interleaves the lower half of the vector. + /// + /// First vector + /// Second vector + /// + /// { a[0], b[0], a[1], b[1], a[2], b[2], a[3], b[3] } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 InterleaveLower(Vector256 a, Vector256 b) + { + Vector256 shuffledA = Vector256.Shuffle(a, Vector256.Create(0, 0, 1, 1, 2, 2, 3, 3)); + Vector256 shuffledB = Vector256.Shuffle(b, Vector256.Create(0, 0, 1, 1, 2, 2, 3, 3)); + + Vector256 maskA = Vector256.Create(-1, 0, -1, 0, -1, 0, -1, 0); + Vector256 maskB = Vector256.Create(0, -1, 0, -1, 0, -1, 0, -1); + + return (shuffledA & maskA) | (shuffledB & maskB); + } + + /// + /// Interleaves the upper half of the vector. + /// + /// First vector + /// Second vector + /// + /// { a[4], b[4], a[5], b[5], a[6], b[6], a[7], b[7] } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 InterleaveUpper(Vector256 a, Vector256 b) + { + Vector256 shuffledA = Vector256.Shuffle(a, Vector256.Create(4, 4, 5, 5, 6, 6, 7, 7)); + Vector256 shuffledB = Vector256.Shuffle(b, Vector256.Create(4, 4, 5, 5, 6, 6, 7, 7)); + + Vector256 maskA = Vector256.Create(-1, 0, -1, 0, -1, 0, -1, 0); + Vector256 maskB = Vector256.Create(0, -1, 0, -1, 0, -1, 0, -1); + + return (shuffledA & maskA) | (shuffledB & maskB); + } } diff --git a/src/ImageSharp/Common/InlineArray.cs b/src/ImageSharp/Common/InlineArray.cs index 700551a8f3..d4cde5f256 100644 --- a/src/ImageSharp/Common/InlineArray.cs +++ b/src/ImageSharp/Common/InlineArray.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. // @@ -71,6 +71,24 @@ internal struct InlineArray26 private T t; } +/// +/// Represents a safe, fixed sized buffer of 32 elements. +/// +[InlineArray(32)] +internal struct InlineArray32 +{ + private T t; +} + +/// +/// Represents a safe, fixed sized buffer of 33 elements. +/// +[InlineArray(33)] +internal struct InlineArray33 +{ + private T t; +} + /// /// Represents a safe, fixed sized buffer of 36 elements. /// @@ -88,3 +106,5 @@ internal struct InlineArray256 { private T t; } + + diff --git a/src/ImageSharp/Common/InlineArray.tt b/src/ImageSharp/Common/InlineArray.tt index d689b0469a..998f8ae105 100644 --- a/src/ImageSharp/Common/InlineArray.tt +++ b/src/ImageSharp/Common/InlineArray.tt @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp; <#GenerateInlineArrays();#> <#+ -private static int[] Lengths = [4, 8, 14, 16, 18, 19, 26, 36, 256]; +private static int[] Lengths = [4, 8, 14, 16, 18, 19, 26, 32, 33, 36, 256]; void GenerateInlineArrays() { diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs index 3b3c6bc200..7cc5b9fe56 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs index ef74d80122..a6df675acf 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs @@ -3,6 +3,7 @@ #pragma warning disable SA1401 // Fields should be private +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/InlineArrays.cs b/src/ImageSharp/Formats/Jxl/InlineArrays.cs index f006d2fd09..acb9c3d413 100644 --- a/src/ImageSharp/Formats/Jxl/InlineArrays.cs +++ b/src/ImageSharp/Formats/Jxl/InlineArrays.cs @@ -7,30 +7,6 @@ namespace SixLabors.ImageSharp.Formats.Jxl; -[InlineArray(3)] -internal struct InlineArray3 -{ - private T first; -} - -/// -/// Used by JxlOpsinParameters -/// -[InlineArray(36)] -internal struct InlineArray36 -{ - private T first; -} - -/// -/// Used by JxlCustomTransformData -/// -[InlineArray(15)] -internal struct InlineArray15 -{ - private T first; -} - /// /// Used by JxlCustomTransformData /// @@ -48,12 +24,3 @@ internal struct InlineArray210 { private T first; } - -/// -/// Used by JxlWeightsSeparable5 -/// -[InlineArray(12)] -internal struct InlineArray12 -{ - private T first; -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs index d339e69087..8a37b7dbaa 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs @@ -3,7 +3,6 @@ using System.Buffers; using System.IO.Compression; -using SixLabors.ImageSharp.Formats.Jxl.IO; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs new file mode 100644 index 0000000000..316f1ed12f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs @@ -0,0 +1,1365 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// Suppress IDE0057. This is so we can stack-allocate +// a powers of 2 and then slice it to the appropriate +// length (which produces better code). +// +// Without this suppression, the analyzer produces a warning, +// recommending changing this: +// stackalloc ulong[32].Slice(0, 18) +// to: +// (stackalloc ulong[32])[..18] +// +// But then the analyzer produces a new warning, recommending +// to remove the paranthesis, changing this: +// (stackalloc ulong[32])[..18] +// to: +// stackalloc ulong[32][..18] +// +// which is invalid C# syntax. +#pragma warning disable IDE0057 // Use range operator + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +/// +/// Extreme performance JPEG XL encoder which provides minimal lossless compression. +/// It also uses minimal dependencies. +/// +internal sealed class JxlFastLosslessEncoder +{ + /// + /// Specifies maximum number of bytes a frame header may use. + /// + private const int MaxFrameHeaderSize = 5; + + private const int NumRawSymbols = 19; + + private const int NumLz77 = 33; + + /// + /// Cache/dictionary size for LZ77 + /// + private const int Lz77CacheSize = 32; + + private const int Lz77Offset = 224; + + private const int Lz77MinLength = 7; + + /// + /// Input frame data is stored here. + /// + private readonly IFjxlFrameInputSource input; + + /// + /// Image width of the input image. + /// + private readonly int width; + + /// + /// Image height of the input image. + /// + private readonly int height; + + /// + /// Image width in groups. + /// + private readonly int numGroupsX; + + /// + /// Image height in groups. + /// + private readonly int numGroupsY; + + /// + /// Image width in groups (DC). + /// + private readonly int numDcGroupsX; + + /// + /// Image height in groups (DC). + /// + private readonly int numDcGroupsY; + + /// + /// Number of channels. (f.e. RGBA is 4, YUV is 3) + /// + private readonly int channels; + + /// + /// Number of bits represented per pixel. (f.e. 8 means pixels + /// have a 0-255 range) + /// + /// + /// Higher bit depths can represent more colors. + /// + private readonly int bitDepth; + + /// + /// Should the output image be stored in big-endian order? + /// + private readonly bool isBigEndian; + + private readonly int effort; + + private readonly bool collided; + + /// + /// Prefix codes for LZ77. + /// + private InlineArray4 hcode; + + private readonly List lookup = []; + + /// + /// Bit writer to write the JPEG XL headers. + /// + private readonly BitWriter header; + + /// + /// Bit writers for writing JPEG XL groups. + /// + private readonly List> groupData = []; + + /// + /// Sizes for each group. + /// + private readonly List groupSizes = []; + + private int acGroupDataOffset; + + private int minDcGlobalSize; + + private int currentBitWriter; + + private int bitWriterBytePos; + + private int bitsInBuffer; + + private long bitBuffer; + + private bool processDone; + + /// + /// Abstracts access to a raster frame data required for encoding. + /// + internal interface IFjxlFrameInputSource : IDisposable + { + /// + /// Returns a span that wraps over channel color data at the + /// specified rectangular position. + /// + /// Target type of the color data. + /// Left offset + /// Right offset + /// Selection width + /// Selection height + /// The actual offset of the row in row-major order is stored here. + /// + /// A wrapper over the color data of the channel at the specified + /// position. + /// + public Span GetColorChannelData(int x, int y, int width, int height, out long rowOffset) + where T : unmanaged; + } + + /// + /// Gets minimum raw lengths for prefix coding. + /// + private static ReadOnlySpan MinimumRawLength => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + + /// + /// Gets maximum raw lengths for prefix coding. + /// + private static ReadOnlySpan MaximumRawLength => [7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 10]; + + /// + /// Gets a lookup used by the method + /// to translate a bucket into a base group size. + /// + private static ReadOnlySpan GroupSizeOffset => + [ + 0, + 1024, + 17408, + 4211712 + ]; + + /// + /// Gets a lookup to determine how many bits a TOC bucket uses. + /// + private static ReadOnlySpan TocBits => [12, 16, 24, 32]; + + /// + /// Approximates Floor(Log2(v)) using integers. + /// + /// Value to retrieve Floor(Log2(v)) of. + /// Floor of second logarithm of v, or 31 if v is equal to 0. + /// This method may use CPU intrinsics provided by the .NET Runtime (e.g. BMI1 on x86). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint FloorLog2(uint v) => v == 0 ? 0 : 31u - (uint)BitOperations.LeadingZeroCount(v); + + /// + /// Approximates count trailing zeros of v using integers. + /// + /// Value to retrieve number of 0 bits after last 1 bit of. + /// After the least significant 1 bit, returns the number of 0 bits. E.g. 1000 1000 00 -> 5. + /// This method may use CPU intrinsics provided by the .NET Runtime (e.g. BMI1 on x86). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint CtzNonZero(ulong v) => (uint)BitOperations.TrailingZeroCount(v); + + /// + /// Returns a TOC bucket based on the group size. + /// + /// Specified group size. + /// TOC bucket matching the appropriate group size. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int TocBucket(int groupSize) + { + int bucket = 0; + + while (bucket < 3 && groupSize >= GroupSizeOffset[bucket + 1]) + { + bucket++; + } + + return bucket; + } + + /// + /// Returns the total number of bits required to represent + /// all given group sizes in the TOC. + /// + /// Group sizes to calculate bit sizes of. + /// Accumulated number of bits required to represent each group size. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int TocSize(Span groupSizes) + { + int tocBits = 0; + + ref int unsafeRef = ref MemoryMarshal.GetReference(groupSizes); + + for (int i = 0; i < groupSizes.Length; i++) + { + // TODO: we can try using AVX2 gather intrinsics, + // especially because TocBits can absolutely fit + // in the L1 cache + int groupSize = Unsafe.Add(ref unsafeRef, i); + int bucketForGroupSize = TocBucket(groupSize); + int bitsUsedByBucket = TocBits[bucketForGroupSize]; + + tocBits += bitsUsedByBucket; + } + + return tocBits; + } + + /// + /// Returns the number of bytes for the frame header. + /// + /// Indicates presence of the alpha channel. + /// Indicates whether this is the final frame. + /// Frame header size in bytes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int FrameHeaderSize(bool containsAlpha, bool isLast) + { + // Original code (from libjxl): + // + // size_t nbits = 28 + (have_alpha ? 4 : 0) + (is_last ? 0 : 2); + // return (nbits + 7) / 8; + // + // In this implementation we just use constants to shave a few CPU cycles. + // The total amount of branches is reduced by one (for the !containsAlpha case), + // but we remove the arithmetic/shifting instructions. + unchecked + { + if (containsAlpha) + { + if (isLast) + { + return 5; // (34 + 7) / 8 + } + else + { + return 4; // (32 + 7) / 8 + } + } + else + { + return 4; // (30 + 7) / 8 AND (28 + 7) / 8 yield the same result + } + } + } + + private static long GetSectionSize(InlineArray4 groupData) + { + long size = 0; + + for (int j = 0; j < 4; j++) + { + BitWriter writer = groupData[j]; + + size += (writer.BytesWritten * 8) + writer.BitsInBuffer; + } + + return (size + 7) / 8; + } + + /// + /// Approximates number of bytes needed for the output image buffer. + /// + /// Bytes for the frame buffer. + private long GetOutputSize() + { + long totalSizeGroups = 0; + + Span> groups = CollectionsMarshal.AsSpan(this.groupData); + + for (int i = 0; i < groups.Length; i++) + { + InlineArray4 section = groups[i]; + + totalSizeGroups += GetSectionSize(section); + } + + return this.header.BytesWritten + totalSizeGroups; + } + + /// + /// Returns the maximum amount of bytes potentially required for the image buffer. + /// + /// Upper bound of bytes for frame buffer. + private long GetMaxRequiredOutput() => this.GetOutputSize() + 32; + + private void WriteHeader(bool addImageHeader, bool isLast) + { + BitWriter output = this.header; + bool haveAlpha = this.channels is 2 or 4; + + if (addImageHeader) + { + // File signature. This signature specifies + // a raw codestream. No container format here. + output.Write(16, 0x0AFF); + + // Handcrafted size header. + output.Write(1, 0); // Not small + + WriteSize(this.height); + output.Write(3, 0b000); // No special ratio + WriteSize(this.width); + + // Handcrafted image metadata + output.Write(1, 0); // all_default = 0 (don't assume values to be set to their defaults) + output.Write(1, 0); // extra_fields = 0 (extra fields are disabled and therefore not present) + output.Write(1, 0); // bit_depth.floating_point_sample = 0 (samples are integers) + + if (this.bitDepth == 8) + { + output.Write(2, 0b00); // bit_depth.bits_per_sample = 8 (predefined bit depth of 8 bits) + } + else if (this.bitDepth == 10) + { + output.Write(2, 0b01); // bit_depth.bits_per_sample = 10 (predefined bit depth of 10 bits) + } + else if (this.bitDepth == 12) + { + output.Write(2, 0b10); // bit_depth.bits_per_sample = 12 (predefined bit depth of 12 bits) + } + else + { + output.Write(2, 0b11); // Custom bit depth + output.Write(6, (ulong)this.bitDepth - 1); // bit depth minus 1 (so 0 becomes 1, 9 becomes 10, etc) + } + + if (this.bitDepth <= 14) + { + output.Write(1, 1); // 16-bit-buffer is sufficient + } + else + { + output.Write(1, 0); // 16-bit-buffer is NOT sufficient + } + + if (haveAlpha) + { + output.Write(2, 0b01); // Emit one extra channel (the alpha channel) + + if (this.bitDepth == 8) + { + output.Write(1, 1); // all_default = 1 (8-bit alpha is the default) + } + else + { + output.Write(1, 0); // all_default = 0 + output.Write(2, 0); // type = alpha + output.Write(1, 0); // samples are not floating point + + if (this.bitDepth == 10) + { + output.Write(2, 0b01); // bit_depth.bits_per_sample = 10 (predefined bit depth of 10 bits) + } + else if (this.bitDepth == 12) + { + output.Write(2, 0b10); // bit_depth.bits_per_sample = 12 (predefined bit depth of 12 bits) + } + else + { + output.Write(2, 0b11); // Custom bit depth + output.Write(6, (ulong)this.bitDepth - 1); // bit depth minus 1 (so 0 becomes 1, 9 becomes 10, etc) + } + + output.Write(2, 0); // dim_shift = 0 + output.Write(2, 0); // name_len = 0 + output.Write(1, 0); // alpha_associated = 0 + } + } + else + { + output.Write(2, 0b00); // 0 extra channels + } + + output.Write(1, 0); // not XYB + + if (this.channels > 2) + { + output.Write(1, 1); // color_encoding.all_default = 1 (sRGB) + } + else + { + output.Write(1, 0); // color_encoding.all_default = 0 + output.Write(1, 0); // color_encoding.want_icc = 0 + output.Write(2, 0b01); // Grayscale + output.Write(2, 0b01); // D65 + output.Write(1, 0); // No gamma transfer function + output.Write(2, 0b10); // transfer function: 2 + u(4) + output.Write(4, 11); // transfer function (specifies sRGB) + output.Write(2, 1); // relative rendering intent + } + + output.Write(2, 0b00); // No extensions + output.Write(1, 1); // all_default transform data + output.ZeroPadToByte(); // No ICC and no preview. Frame should start at byte boundary. + } + + // Handcrafted frame header + output.Write(1, 0); // all_default = 0 (non-default values) + output.Write(2, 0b00); // regular frame + output.Write(1, 1); // modular + output.Write(2, 0b00); // default flags + output.Write(1, 0); // not Y'Cb'Cr + output.Write(2, 0b00); // no upsampling + + if (haveAlpha) + { + output.Write(2, 0b00); // no alpha upsampling + } + + output.Write(2, 0b01); // default group size + output.Write(2, 0b00); // exactly one pass + output.Write(1, 0); // no custom size or origin + output.Write(2, 0b00); // Replace blending mode + + if (haveAlpha) + { + output.Write(2, 0b00); // Replace blending mode for alpha channel + } + + output.Write(2, 0b00); // a frame has no name + output.Write(1, 0); // loop filter is not all_default + output.Write(1, 0); // no Gaborish transform + output.Write(2, 0b00); // 0 EPF filters + output.Write(2, 0b00); // no LF extensions + output.Write(2, 0b00); // no FH extensions + + output.Write(1, 0); // no TOC permutation + output.ZeroPadToByte(); // TOC is byte aligned + + Span groupSizes = CollectionsMarshal.AsSpan(this.groupSizes); + + for (int i = 0; i < groupSizes.Length; i++) + { + int groupSize = groupSizes[i]; + + int bucket = TocBucket(groupSize); + output.Write(2, (ulong)bucket); + output.Write(TocBits[bucket] - 2, (ulong)(groupSize - GroupSizeOffset[bucket])); + } + + output.ZeroPadToByte(); // Groups are byte-aligned + + // Sizes are coded using a special variable-length + // kind of coding. This method does that here. + // + // It has a prefix of 2 bits, followed by the suffix of N + // bits which depend on the prefix: + // + // prefix 0b00: 9 consecutive bits + // prefix 0b01: 13 consecutive bits + // prefix 0b10: 18 consecutive bits + // prefix 0b11: 30 consecutive bits + void WriteSize(int size) + { + ulong sizeMinus1 = (ulong)size - 1uL; + + if (sizeMinus1 < (1 << 9)) + { + output.Write(2, 0b00); // 9 bits + output.Write(9, sizeMinus1); + } + else if (sizeMinus1 < (1 << 13)) + { + output.Write(2, 0b01); // 13 bits + output.Write(13, sizeMinus1); + } + else if (sizeMinus1 < (1 << 18)) + { + output.Write(2, 0b10); // 18 bits + output.Write(18, sizeMinus1); + } + else + { + output.Write(2, 0b11); // 30 bits + output.Write(30, sizeMinus1); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ComputeDcGlobalPadding(Span groupSizes, int acGroupDataOffset, int minDcGlobalSize, bool containsAlpha, bool isLast) + { + // Libjxl reference implements this method like this: + /* + size_t ComputeDcGlobalPadding(const std::vector& group_sizes, + size_t ac_group_data_offset, + size_t min_dc_global_size, bool have_alpha, + bool is_last) { + std::vector new_group_sizes = group_sizes; + new_group_sizes[0] = min_dc_global_size; + size_t toc_size = TOCSize(new_group_sizes); + size_t actual_offset = + FrameHeaderSize(have_alpha, is_last) + toc_size + group_sizes[0]; + return ac_group_data_offset - actual_offset; + } + */ + // The reference implementation copies the entire vector so that + // element 0 can be modified without affecting the original. + // Since TocSize() does not throw, temporarily modify element 0 + // instead, avoiding the allocation and copy. + int firstItem = groupSizes[0]; + groupSizes[0] = minDcGlobalSize; + int tocSize = TocSize(groupSizes); + int actualOffset = FrameHeaderSize(containsAlpha, isLast) + tocSize + firstItem; + groupSizes[0] = firstItem; + return acGroupDataOffset - actualOffset; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void EncodeHybridUintLz77(int value, out int token, out int nBits, out int bits) + { + unchecked + { + int n = (int)FloorLog2((uint)value); + + if (value < 16) + { + token = value; + nBits = 0; + bits = 0; + } + else + { + token = 16 + n - 4; + nBits = n; + bits = value - (1 << n); + } + } + } + + /// + /// SIMD Mask32 + /// + private struct Mask32 + { + /// + /// Actual mask. + /// + public ushort Mask; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly uint CountPrefix() => CtzNonZero(~(uint)this.Mask); + } + + /// + /// Wrapper over a 32-bit integer vector. + /// + /// Underlying vector. + private struct SimdVec32(Vector vector) + { + /// + /// The actual vector for this simd vector. + /// + public Vector Vec = vector; + + /// + /// Adds both vectors. + /// + /// First vector + /// Second vector + /// a + b + public static SimdVec32 operator +(SimdVec32 a, SimdVec32 b) => new(a.Vec + b.Vec); + + /// + /// Subtracts both vectors. + /// + /// First vector + /// Second vector + /// a - b + public static SimdVec32 operator -(SimdVec32 a, SimdVec32 b) => new(a.Vec - b.Vec); + + /// + /// XORs both vectors. + /// + /// First vector + /// Second vector + /// a ^ b + public static SimdVec32 operator ^(SimdVec32 a, SimdVec32 b) => new(a.Vec ^ b.Vec); + + /// + /// Sets bits to all 1 if vector items are equal, otherwise to all 0. + /// For example, if vector a is {5, 2, 4, 1} and vector b is {7, 3, 4, 5} + /// the result is {0, 0, 0xFFFFFFFF, 0}. + /// + /// First vector + /// Second vector + /// a == b + public static SimdVec32 operator ==(SimdVec32 a, SimdVec32 b) => new(Vector.Equals(a.Vec, b.Vec)); + + // We don't use this. It's to remove an error where == requires !=. + public static SimdVec32 operator !=(SimdVec32 a, SimdVec32 b) => new(Vector.Equals(a.Vec, b.Vec)); + + /// + /// Sets bits to all 1 if vector items are larger, otherwise to all 0. + /// For example, if vector a is {5, 2, 4, 1} and vector b is {3, 3, 4, 5} + /// the result is {0xFFFFFFFF, 0, 0, 0}. + /// + /// First vector + /// Second vector + /// a > b + public static SimdVec32 operator >(SimdVec32 a, SimdVec32 b) => new(Vector.GreaterThan(a.Vec, b.Vec)); + + /// + /// Sets bits to all 1 if vector items are lower, otherwise to all 0. + /// For example, if vector a is {5, 2, 4, 1} and vector b is {3, 3, 4, 5} + /// the result is {0, 0xFFFFFFFF, 0, 0xFFFFFFFF}. + /// + /// First vector + /// Second vector + /// a < b + public static SimdVec32 operator <(SimdVec32 a, SimdVec32 b) => new(Vector.LessThan(a.Vec, b.Vec)); + + /// + /// Converts this vector to a mask. + /// + /// + /// Mask where bits are 1 if the item + /// at the index is set to all 1, otherwise 0. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Mask32 ToMask() + { + int mask = 0; + + for (int i = 0; i < 16 && i < Vector.Count; i++) + { + if (this.Vec[i] == uint.MaxValue) + { + mask |= 1 << i; + } + } + + return new() { Mask = (ushort)mask }; + } + + public static SimdVec32 Load(Span data) => new(new Vector(data)); + + public static SimdVec32 Value(uint value) => new(new Vector(value)); + + public readonly SimdVec32 ValueToToken() => new(new Vector(32u) - GetLzcnt(this.Vec)); + + public readonly SimdVec32 SaturateSubtract(SimdVec32 toSubtract) => new(Vector.Max(this.Vec, toSubtract.Vec) - toSubtract.Vec); + + public readonly SimdVec32 Pow2() => new(Vector.ShiftLeft(Vector.One, unchecked((int)this.Vec[0]))); + + public readonly void Store(Span data) => this.Vec.CopyTo(data); + + // We don't use this. + public override readonly bool Equals(object? obj) => false; + + // We don't use this. + public override readonly int GetHashCode() => this.Vec.GetHashCode(); + } + + /// + /// SIMD Mask16 + /// + private struct Mask16 + { + /// + /// Actual mask. + /// + public uint Mask; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly uint CountPrefix() => CtzNonZero(~this.Mask); + } + + /// + /// Wrapper over a 16-bit integer vector. + /// + /// Underlying vector. + private struct SimdVec16(Vector vector) + { + /// + /// The actual vector for this simd vector. + /// + public Vector Vec = vector; + + /// + /// Adds both vectors. + /// + /// First vector + /// Second vector + /// a + b + public static SimdVec16 operator +(SimdVec16 a, SimdVec16 b) => new(a.Vec + b.Vec); + + /// + /// Subtracts both vectors. + /// + /// First vector + /// Second vector + /// a - b + public static SimdVec16 operator -(SimdVec16 a, SimdVec16 b) => new(a.Vec - b.Vec); + + /// + /// XORs both vectors. + /// + /// First vector + /// Second vector + /// a ^ b + public static SimdVec16 operator ^(SimdVec16 a, SimdVec16 b) => new(a.Vec ^ b.Vec); + + /// + /// Sets bits to all 1 if vector items are equal, otherwise to all 0. + /// For example, if vector a is {5, 2, 4, 1} and vector b is {7, 3, 4, 5} + /// the result is {0, 0, 0xFFFF, 0}. + /// + /// First vector + /// Second vector + /// a == b + public static SimdVec16 operator ==(SimdVec16 a, SimdVec16 b) => new(Vector.Equals(a.Vec, b.Vec)); + + // We don't use this. It's to remove an error where == requires !=. + public static SimdVec16 operator !=(SimdVec16 a, SimdVec16 b) => new(Vector.Equals(a.Vec, b.Vec)); + + /// + /// Sets bits to all 1 if vector items are larger, otherwise to all 0. + /// For example, if vector a is {5, 2, 4, 1} and vector b is {3, 3, 4, 5} + /// the result is {0xFFFF, 0, 0, 0}. + /// + /// First vector + /// Second vector + /// a > b + public static SimdVec16 operator >(SimdVec16 a, SimdVec16 b) => new(Vector.GreaterThan(a.Vec, b.Vec)); + + /// + /// Sets bits to all 1 if vector items are lower, otherwise to all 0. + /// For example, if vector a is {5, 2, 4, 1} and vector b is {3, 3, 4, 5} + /// the result is {0, 0xFFFF, 0, 0xFFFF}. + /// + /// First vector + /// Second vector + /// a < b + public static SimdVec16 operator <(SimdVec16 a, SimdVec16 b) => new(Vector.LessThan(a.Vec, b.Vec)); + + /// + /// Converts this vector to a mask. + /// + /// + /// Mask where bits are 1 if the item + /// at the index is set to all 1, otherwise 0. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Mask16 ToMask() + { + uint mask = 0; + + for (int i = 0; i < 32 && i < Vector.Count; i++) + { + if (this.Vec[i] == ushort.MaxValue) + { + mask |= 1u << i; + } + } + + return new() { Mask = mask }; + } + + public static SimdVec16 FromTwo32(SimdVec32 lo, SimdVec32 hi) + { + Vector narrow = Vector.Narrow(lo.Vec, hi.Vec); + return new(narrow); + } + + public static SimdVec16 Load(Span data) => new(new Vector(data)); + + public static SimdVec16 Value(ushort value) => new(new Vector(value)); + + public readonly SimdVec16 ValueToToken() => new(new Vector(16) - GetLzcnt(this.Vec)); + + public readonly SimdVec16 SaturateSubtract(SimdVec16 toSubtract) => new(Vector.Max(this.Vec, toSubtract.Vec) - toSubtract.Vec); + + public readonly SimdVec16 Pow2() => new(Vector.ShiftLeft(Vector.One, unchecked(this.Vec[0]))); + + public readonly void Store(Span data) => this.Vec.CopyTo(data); + + // We don't use this. + public override readonly bool Equals(object? obj) => false; + + // We don't use this. + public override readonly int GetHashCode() => this.Vec.GetHashCode(); + } + + /// + /// Pair of two vectors. + /// + /// Type of the vector. + /// Low vector + /// High vector + private struct VectorPair(T lo, T hi) + where T : unmanaged + { + public T Low = lo; + public T High = hi; + } + + /// + /// The prefix code is used for encoding LZ77-compressed coefficients. + /// + private sealed class PrefixCode + { +#pragma warning disable SA1401 // Fields should be private + + /// + /// Maximum number of raw symbols for prefix coding. + /// + private const int MaxNumSymbols = NumRawSymbols + 1 < NumLz77 ? NumLz77 : NumRawSymbols + 1; + + /// + /// Gets or sets the Huffman raw bit lengths. + /// + public InlineArray19 RawLengths; + + /// + /// Gets or sets the Huffman raw code values. + /// + public InlineArray19 RawCodes; + + /// + /// Gets or sets the Huffman LZ77 bit lengths. + /// + public InlineArray33 Lz77Lengths; + + /// + /// Gets or sets the Huffman LZ77 code values. + /// + public InlineArray33 Lz77Codes; + + /// + /// Gets or sets the Huffman LZ77 cache code values. + /// + public InlineArray32 Lz77CacheBits; + + /// + /// Gets or sets the Huffman LZ77 cache bit lengths. + /// + public InlineArray32 Lz77CacheLengths; + + public PrefixCode(Span rawCounts, Span lz77Counts) + { + Span level1Counts = stackalloc ulong[NumRawSymbols + 1]; + rawCounts[..NumRawSymbols].CopyTo(level1Counts); + + this.RawCount = NumRawSymbols; + + while (this.RawCount > 0 && level1Counts[this.RawCount - 1] == 0) + { + this.RawCount--; + } + + level1Counts[this.RawCount] = 0; + + for (int i = 0; i < NumLz77; i++) + { + level1Counts[this.RawCount] += lz77Counts[i]; + } + + Span level1Lengths = stackalloc byte[NumRawSymbols + 1]; + level1Lengths.Clear(); + + ComputeCodeLengths(level1Counts, this.RawCount + 1, MinimumRawLength, MaximumRawLength, level1Lengths); + + Span level2Lengths = stackalloc byte[NumLz77]; + Span minLengths = stackalloc byte[NumLz77]; + + level2Lengths.Clear(); + minLengths.Clear(); + + int l = 15 - level1Lengths[this.RawCount]; + Span maxLengths = stackalloc byte[NumLz77]; + maxLengths.Fill((byte)l); + + int numLz77 = NumLz77; + while (numLz77 > 0 && lz77Counts[numLz77 - 1] == 0) + { + numLz77--; + } + + ComputeCodeLengths(lz77Counts, numLz77, minLengths, maxLengths, level2Lengths); + + level1Lengths[..this.RawCount].CopyTo(this.RawLengths); + + for (int i = 0; i < numLz77; i++) + { + this.Lz77Lengths[i] = (byte)(level2Lengths[i] != 0 ? level1Lengths[this.RawCount] + level2Lengths[i] : 0); + } + + ComputeCanonicalCode(this.RawLengths, this.RawCodes, this.Lz77Lengths, this.Lz77Codes); + + // Prepare the LZ77 cache + for (int count = 0; count < Lz77CacheSize; count++) + { + EncodeHybridUintLz77(count, out int token, out int nbits, out int bits); + this.Lz77CacheLengths[count] = (byte)(this.Lz77Lengths[token] + nbits + this.RawLengths[0]); + this.Lz77CacheBits[count] = + (ulong)((((bits << this.Lz77Lengths[token]) | this.Lz77Codes[token]) << this.RawLengths[0]) | + this.RawLengths[0]); + } + } + + /// + /// Gets a lookup used to reverse integers bit-wise. + /// + private static ReadOnlySpan ReverseNibbleLookup => + [ + 0b0000, 0b1000, 0b0100, 0b1100, 0b0010, 0b1010, 0b0110, 0b1110, + 0b0001, 0b1001, 0b0101, 0b1101, 0b0011, 0b1011, 0b0111, 0b1111, + ]; + +#pragma warning restore SA1401 // Fields should be private + + /// + /// Gets or sets the number of raw codes. + /// + public int RawCount { get; set; } + + /// + /// Reverses the integer bit-wise. + /// + /// Number of bits for the integer. + /// Actual bits to reverse. + /// + /// Input integer but reversed. F.e. 10010 becomes 01001. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ushort BitReverse(int nbits, ushort bits) + { + unchecked + { + ushort rev16 = (ushort)((ReverseNibbleLookup[bits & 0xF] << 12) | + (ReverseNibbleLookup[(bits >> 4) & 0xF] << 8) | + (ReverseNibbleLookup[(bits >> 8) & 0xF] << 4) | + ReverseNibbleLookup[bits >> 12]); + return (ushort)(rev16 >> (16 - nbits)); + } + } + + private static void ComputeCanonicalCode(Span firstChunkLengths, Span firstChunkCodes, Span secondChunkLengths, Span secondChunkCodes) + { + const int maxCodeLength = 15; + + Span codeLengthCounts = stackalloc byte[maxCodeLength + 1]; + codeLengthCounts.Clear(); + + for (int i = 0; i < firstChunkCodes.Length; i++) + { + codeLengthCounts[firstChunkLengths[i]]++; + + if (firstChunkLengths[i] > 8) + { + throw new InvalidOperationException("First chunk length is too large"); + } + + if (firstChunkLengths[i] <= 0) + { + throw new InvalidOperationException("First chunk length cannot be <= 0"); + } + } + + for (int i = 0; i < secondChunkCodes.Length; i++) + { + codeLengthCounts[secondChunkLengths[i]]++; + + if (secondChunkLengths[i] > maxCodeLength) + { + throw new InvalidOperationException("Second chunk length is too large"); + } + } + + Span nextCode = stackalloc ushort[maxCodeLength + 1]; + nextCode.Clear(); + + ushort code = 0; + + for (int i = 1; i < maxCodeLength + 1; i++) + { + code = unchecked((ushort)((code + codeLengthCounts[i - 1]) << 1)); + nextCode[i] = code; + } + + unchecked + { + for (int i = 0; i < firstChunkCodes.Length; i++) + { + firstChunkCodes[i] = (byte)BitReverse(firstChunkLengths[i], nextCode[firstChunkLengths[i]]++); + } + + for (int i = 0; i < secondChunkCodes.Length; i++) + { + secondChunkCodes[i] = (byte)BitReverse(secondChunkLengths[i], nextCode[secondChunkLengths[i]]++); + } + } + } + + private static void ComputeCodeLengthsNonZeroImpl( + Span freqs, + int n, + int precision, + T infty, + Span minLimit, + Span maxLimit, + Span nbits) + where T : unmanaged, INumber + { + DebugGuard.MustBeLessThan(precision, 15, nameof(precision)); + DebugGuard.MustBeLessThanOrEqualTo(n, MaxNumSymbols, nameof(n)); + + int scale = 1 << precision; + int width = scale + 1; + + Span dynp = stackalloc T[width * (n + 1)]; + dynp.Fill(infty); + dynp[0] = T.Zero; + + for (int sym = 0; sym < n; sym++) + { + for (int bits = minLimit[sym]; bits <= maxLimit[sym]; bits++) + { + int offsetDelta = 1 << (precision - bits); + T cost = T.CreateChecked(freqs[sym]) * T.CreateChecked(bits); + + for (int off = 0; off + offsetDelta <= scale; off++) + { + int current = (sym * width) + off; + int next = ((sym + 1) * width) + off + offsetDelta; + + dynp[next] = T.Min(dynp[current] + cost, dynp[next]); + } + } + } + + int offFinal = scale; + + for (int sym = n - 1; sym >= 0; sym--) + { + if (offFinal <= 0) + { + throw new InvalidOperationException("Offset should be greater than zero"); + } + + for (int bits = minLimit[sym]; bits <= maxLimit[sym]; bits++) + { + int offsetDelta = 1 << (precision - bits); + + if (offsetDelta <= offFinal) + { + int current = (sym * width) + offFinal; + int previous = (sym * width) + offFinal - offsetDelta; + + T cost = T.CreateChecked(freqs[sym]) * T.CreateChecked(bits); + + if (dynp[current] == dynp[previous] + cost) + { + offFinal -= offsetDelta; + nbits[sym] = (byte)bits; + break; + } + } + } + } + } + + private static void ComputeCodeLengthsNonZero(Span freqs, int n, Span minLimit, Span maxLimit, Span nbits) + { + int precision = 0; + int shortestLength = 255; + ulong frequencySum = 0; + + for (int i = 0; i < n; i++) + { + frequencySum += freqs[i]; + + if (minLimit[i] < 1) + { + minLimit[i] = 1; + } + + precision = Math.Max(maxLimit[i], precision); + shortestLength = Math.Min(minLimit[i], shortestLength); + } + + precision -= shortestLength - 1; + ulong infinity = frequencySum * (ulong)precision; + + if (infinity < uint.MaxValue / 2) + { + ComputeCodeLengthsNonZeroImpl(freqs, n, precision, (uint)infinity, minLimit, maxLimit, nbits); + } + else + { + ComputeCodeLengthsNonZeroImpl(freqs, n, precision, infinity, minLimit, maxLimit, nbits); + } + } + + private static void ComputeCodeLengths(Span freqs, int n, ReadOnlySpan minLimitIn, ReadOnlySpan maxLimitIn, Span nbits) + { + DebugGuard.MustBeLessThanOrEqualTo(n, MaxNumSymbols, nameof(n)); + + Span compactFreqs = stackalloc ulong[MaxNumSymbols]; + Span minLimit = stackalloc byte[MaxNumSymbols]; + Span maxLimit = stackalloc byte[MaxNumSymbols]; + + int ni = 0; + for (int i = 0; i < n; i++) + { + if (freqs[i] != 0) + { + compactFreqs[ni] = freqs[i]; + minLimit[ni] = minLimitIn[i]; + maxLimit[ni] = maxLimitIn[i]; + ni++; + } + } + + compactFreqs[ni..].Clear(); + minLimit[ni..].Clear(); + maxLimit[ni..].Clear(); + + Span numBits = stackalloc byte[MaxNumSymbols]; + numBits.Clear(); + + ComputeCodeLengthsNonZero(compactFreqs, ni, minLimit, maxLimit, numBits); + + ni = 0; + + for (int i = 0; i < n; i++) + { + nbits[i] = 0; + if (freqs[i] != 0) + { + nbits[i] = numBits[ni++]; + } + } + } + + /// + /// Writes this LZ77 prefix code into the bit-stream. + /// + /// The bit-stream to write the prefix code into. + public void Write(BitWriter writer) + { + Span codeLengthCounts = stackalloc ulong[32].Slice(0, 18); + codeLengthCounts.Clear(); + codeLengthCounts[17] = 3 + (2 * (NumLz77 - 1)); + + for (int i = 0; i < 19; i++) + { + byte rawLength = this.RawLengths[i]; + + codeLengthCounts[rawLength]++; + } + + for (int i = 0; i < 33; i++) + { + byte lz77Length = this.Lz77Lengths[i]; + + codeLengthCounts[lz77Length]++; + } + + // Lengths for representing the code length + Span codeLengthLengths = stackalloc byte[32].Slice(0, 18); + Span codeLengthLengthsMinimum = stackalloc byte[32].Slice(0, 18); + Span codeLengthLengthsMaximum = stackalloc byte[32].Slice(0, 18); + + codeLengthLengths.Clear(); + codeLengthLengthsMinimum.Clear(); + codeLengthLengthsMaximum.Fill(5); + + ComputeCodeLengths(codeLengthCounts, 18, codeLengthLengthsMinimum, codeLengthLengthsMaximum, codeLengthLengths); + + writer.Write(2, 0b00); // HSKIP = 0 (Don't skip code lengths) + + // As per Brotli RFC + Span codeLengthOrder = [1, 2, 3, 4, 0, 5, 17, 6, 16, + 7, 8, 9, 10, 11, 12, 13, 14, 15]; + + // Lengths & codes for representing lengths of code lengths + Span codeLengthLengthLengths = [2, 4, 3, 2, 2, 4]; + Span codeLengthLengthCodes = [0, 7, 3, 2, 1, 15]; + + // Maximum number of code lengths + int numCodeLengths = 18; + while (codeLengthLengths[codeLengthOrder[numCodeLengths - 1]] == 0) + { + numCodeLengths--; + } + + // Max bits written in this loop: 18 * 4 = 72 + for (int i = 0; i < numCodeLengths; i++) + { + int symbol = codeLengthLengths[codeLengthOrder[i]]; + writer.Write(codeLengthLengthLengths[symbol], codeLengthLengthCodes[symbol]); + } + + Span codeLengthBits = stackalloc ushort[32].Slice(0, 18); + codeLengthBits.Clear(); + ComputeCanonicalCode([], [], codeLengthLengths, codeLengthBits); + + for (int i = 0; i < 19; i++) + { + byte rawLength = this.RawLengths[i]; + + writer.Write(codeLengthLengths[rawLength], codeLengthBits[rawLength]); + } + + int numLz77 = NumLz77; + while (this.Lz77Lengths[numLz77 - 1] == 0) + { + numLz77--; + } + + // Max bits in this block: 24 + writer.Write(codeLengthLengths[17], codeLengthBits[17]); + writer.Write(3, 0b010); // 5 + writer.Write(codeLengthLengths[17], codeLengthBits[17]); + writer.Write(3, 0b000); // (5 - 2) * 8 + 3 = 27 + writer.Write(codeLengthLengths[17], codeLengthBits[17]); + writer.Write(3, 0b010); // (27 - 2) * 8 + 5 = 205 + + // Encode LZ77 symbols with values 224 + i. + // Max. bits in this loop: 33 * 5 = 165 + for (int i = 0; i < numLz77; i++) + { + writer.Write(codeLengthLengths[this.Lz77Lengths[i]], codeLengthBits[this.Lz77Lengths[i]]); + } + } + } + + /// + /// Simple MSB-first bit-stream writer implementation built on top + /// of a stream. + /// + /// Output bytes are written here. + private sealed class BitWriter(Stream stream) : IDisposable + { + /// + /// Temporary cache used to store pending written bits + /// before they're written to the output stream. + /// + private ulong buffer; + + /// + /// Gets the total number of bytes written to the output buffer so far. + /// + public long BytesWritten { get; private set; } + + /// + /// Gets the number of bits actively in the bit cache. + /// This is used to track how many bits were written into + /// the cache prior to sending the cache to the stream. + /// + public int BitsInBuffer { get; private set; } + + /// + /// Writes the specified bits in the Most Significant Byte (MSB) + /// order. + /// + /// Represents the number of bits to write to the bit-stream. + /// Represents the value to write to the bit-stream. + public void Write(int count, ulong bits) + { + DebugGuard.MustBeBetweenOrEqualTo(count, 0, 56, nameof(count)); + + if (count < 64) + { + bits &= (1UL << count) - 1; + } + + this.buffer |= bits << this.BitsInBuffer; + this.BitsInBuffer += count; + + this.FlushBytes(); + } + + /// + /// Internal method used to flush bytes from the cache + /// () into the output stream. + /// + private void FlushBytes() + { + int bytes = this.BitsInBuffer / 8; + + for (int i = 0; i < bytes; i++) + { + stream.WriteByte((byte)this.buffer); + this.BytesWritten++; + this.buffer >>= 8; + } + + this.BitsInBuffer -= bytes * 8; + } + + /// + /// Used by the dispose method to flush the remaining bits + /// that are not byte-aligned. F.e. if we dispose this reader + /// and we have 5 bits left, those final 5 bits are set to all 0 + /// and the byte is written to the stream. + /// + public void ZeroPadToByte() + { + if (this.BitsInBuffer != 0) + { + this.Write(8 - this.BitsInBuffer, 0); + } + } + + /// + /// Flushes out the final bytes. + /// + public void Dispose() => this.ZeroPadToByte(); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlLinearAlgebra.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlLinearAlgebra.cs new file mode 100644 index 0000000000..9b8e2f9fa6 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlLinearAlgebra.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Matrix2x2 = System.Runtime.CompilerServices.InlineArray2>; +using Vector2 = System.Runtime.CompilerServices.InlineArray2; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +/// +/// Handles linear algebra for encoding. +/// +internal static class JxlLinearAlgebra +{ + public static void ConvertToDiagonal(Matrix2x2 a, Vector2 diag, Matrix2x2 u) + { + DebugGuard.MustBeLessThan(Math.Abs(a[0][1] - a[1][0]), 1e-15, nameof(a)); + + double b = -(a[0][0] + a[1][1]); + double c = (a[0][0] * a[1][1]) - (a[0][1] * a[0][1]); + double d = (b * b) - (4.0 * c); + + if (Math.Abs(a[0][1]) < 1e-10 || d < 0) + { + // Already diagonal. + diag[0] = a[0][0]; + diag[1] = a[1][1]; + u[0][0] = u[1][1] = 1.0; + u[0][1] = u[1][0] = 0.0; + return; + } + + double sqd = Math.Sqrt(d); + double l1 = (-b - sqd) * 0.5; + double l2 = (-b + sqd) * 0.5; + + Vector2 v1 = default; + v1[0] = a[0][0] - l1; + v1[1] = a[1][0]; + + double v1n = 1.0 / JxlMath.Hypot(v1[0], v1[1]); + v1[0] = v1[0] * v1n; + v1[1] = v1[1] * v1n; + + diag[0] = l1; + diag[1] = l2; + + u[0][0] = v1[1]; + u[0][1] = -v1[0]; + u[1][0] = v1[0]; + u[1][1] = v1[1]; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/JpegAppMarkerType.cs b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/JpegAppMarkerType.cs new file mode 100644 index 0000000000..2b05317ab4 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/JpegAppMarkerType.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Jpeg; + +/// +/// Identifies the kind of APP marker in a JPEG file. +/// +internal enum JpegAppMarkerType : byte +{ + /// + /// Unknown APP marker + /// + Unknown, + + /// + /// Contains ICC profile metadata + /// + Icc, + + /// + /// Contains EXIF profile metadata + /// + Exif, + + /// + /// Contains XMP profile metadata + /// + Xmp +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDequantMatrices.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDequantMatrices.cs new file mode 100644 index 0000000000..15b0935415 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDequantMatrices.cs @@ -0,0 +1,265 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Contains matrices used to inverse quantize coefficients. +/// +internal sealed class JxlDequantMatrices +{ + /// + /// Sum(DotProduct(RequiredSizeX, RequiredSizeY)). + /// + private const int SumRequiredXY = 2056; + + private const int TotalTableSize = SumRequiredXY * JxlFrameDimensions.DctBlockSize * 3; + + /// + /// Contains weights & multipliers for transforms used by the codec (e.g. DCT, identity, AFV). + /// + public static readonly JxlQuantizerEncoding[] Library = GetLibrary(); + + private uint computedMask; + + /// + /// Storage for quantization. + /// + private readonly Memory tableStorage; + + /// + /// Contains matrices for forward quantization. + /// + private readonly Memory table; + + /// + /// Contains matrices for inverse quantization. + /// + private readonly Memory inverseTable; + + /// + /// Quantization table for DC + /// + private InlineArray3 dcQuant; + + /// + /// Inverse quantization table for DC + /// + private InlineArray3 inverseDcQuant; + + /// + /// Table offsets. + /// + private readonly int[] tableOffsets = new int[JxlAcStrategy.NumberOfValidStrategies * 3]; + + /// + /// Quantizer encodings. Multiple may be used depending on the kind of transform. + /// + private JxlQuantizerEncoding[] encodings = []; + + /// + /// Initializes a new instance of the class. + /// + public JxlDequantMatrices() + { + // float dc_quant_[3] = {kDCQuant[0], kDCQuant[1], kDCQuant[2]}; + // float inv_dc_quant_[3] = {kInvDCQuant[0], kInvDCQuant[1], kInvDCQuant[2]}; + this.dcQuant[0] = JxlQuantizerConstants.DcQuant[0]; + this.dcQuant[1] = JxlQuantizerConstants.DcQuant[1]; + this.dcQuant[2] = JxlQuantizerConstants.DcQuant[2]; + + this.inverseDcQuant[0] = JxlQuantizerConstants.InverseDcQuant[0]; + this.inverseDcQuant[1] = JxlQuantizerConstants.InverseDcQuant[1]; + this.inverseDcQuant[2] = JxlQuantizerConstants.InverseDcQuant[2]; + + this.encodings = new JxlQuantizerEncoding[JxlQuantizerConstants.NumberOfQuantizerTables]; + for (int i = 0; i < this.encodings.Length; i++) + { + this.encodings[i] = JxlQuantizerEncoding.Library(0); + } + + int pos = 0; + Span offsets = stackalloc int[JxlQuantizerConstants.NumberOfQuantizerTables * 3]; + + for (int i = 0; i < JxlQuantizerConstants.NumberOfQuantizerTables; i++) + { + int numBlocks = RequiredSizeX[i] * RequiredSizeY[i]; + int num = numBlocks * JxlFrameDimensions.DctBlockSize; + int i3 = 3 * i; + + for (int c = 0; c < 3; c++) + { + offsets[i3 + c] = pos + (c * num); + } + + pos += 3 * num; + } + + for (int i = 0; i < JxlAcStrategy.NumberOfValidStrategies; i++) + { + for (int c = 0; c < 3; c++) + { + this.tableOffsets[(i * 3) + c] = offsets[((int)JxlQuantizerConstants.AcStrategyToQuantTableMap[i] * 3) + c]; + } + } + } + + /// + /// Gets a lookup which represents required widths for each quantizer. + /// + private static ReadOnlySpan RequiredSizeX => [1, 1, 1, 1, 2, 4, 1, 1, 2, 1, 1, 8, 4, 16, 8, 32, 16]; + + /// + /// Gets a lookup which represents required heights for each quantizer. + /// + private static ReadOnlySpan RequiredSizeY => [1, 1, 1, 1, 2, 4, 2, 4, 4, 1, 1, 8, 8, 16, 16, 32, 32]; + + /// + /// Returns the default library with quantizer encodings for all transforms + /// used by the JPEG XL codec. + /// + /// Encodings for all kinds of transforms. + /// Used when quantization constants were partially updated. + public static JxlQuantizerEncoding[] GetLibrary() + { + if (JxlQuantizerConstants.NumberOfQuantizerTables != 17) + { + throw new InvalidOperationException("This function should be updated when adding new quantization types"); + } + + if (JxlQuantWeights.NumPredefinedTables != 1) + { + throw new InvalidOperationException("This function should be updated when adding new quantization matrices to the library"); + } + + Verify(0, JxlQuantTable.DCT); + Verify(1, JxlQuantTable.IDENTITY); + Verify(2, JxlQuantTable.DCT2X2); + Verify(3, JxlQuantTable.DCT4X4); + Verify(4, JxlQuantTable.DCT16X16); + Verify(5, JxlQuantTable.DCT32X32); + Verify(6, JxlQuantTable.DCT8X16); + Verify(7, JxlQuantTable.DCT8X32); + Verify(8, JxlQuantTable.DCT16X32); + Verify(9, JxlQuantTable.DCT4X8); + Verify(10, JxlQuantTable.AFV0); + Verify(11, JxlQuantTable.DCT64X64); + Verify(12, JxlQuantTable.DCT32X64); + Verify(13, JxlQuantTable.DCT128X128); + Verify(14, JxlQuantTable.DCT64X128); + Verify(15, JxlQuantTable.DCT256X256); + Verify(16, JxlQuantTable.DCT128X256); + + return + [ + JxlQuantWeights.Dct, + JxlQuantWeights.Identity, + JxlQuantWeights.Dct2x2, + JxlQuantWeights.Dct4x4, + JxlQuantWeights.Dct16x16, + JxlQuantWeights.Dct32x32, + JxlQuantWeights.Dct8x16, + JxlQuantWeights.Dct8x32, + JxlQuantWeights.Dct16x32, + JxlQuantWeights.Dct4x8, + JxlQuantWeights.Afv, + JxlQuantWeights.Dct64x64, + JxlQuantWeights.Dct32x32, + JxlQuantWeights.Dct128x128, + JxlQuantWeights.Dct64x128, + JxlQuantWeights.Dct256x256, + JxlQuantWeights.Dct128x256 + ]; + + [Conditional("DEBUG")] + static void Verify(int expected, JxlQuantTable actual) + { + if (expected != (byte)actual) + { + throw new InvalidOperationException("Quantizer modes were partially updated; this method needs to be updated too"); + } + } + } + + /// + /// Returns a matrix for the specified kind of quantizer and index. + /// + /// Quantizer kind + /// Index + /// Matrix + public Span GetMatrix(JxlAcStrategyType quantKind, int c) + { + DebugGuard.MustBeGreaterThan((1 << (int)quantKind) & this.computedMask, 0, nameof(quantKind)); + return this.table.Span[this.tableOffsets[((int)quantKind * 3) + c]..]; + } + + /// + /// Returns an inverse matrix for the specified kind of quantizer and index. + /// + /// Quantizer kind + /// Index + /// Inverse matrix + public Span GetInverseMatrix(JxlAcStrategyType quantKind, int c) + { + DebugGuard.MustBeGreaterThan((1 << (int)quantKind) & this.computedMask, 0, nameof(quantKind)); + return this.inverseTable.Span[this.tableOffsets[((int)quantKind * 3) + c]..]; + } + + /// + /// Returns a DC quant for index c. + /// + /// The DC quantizer index. + /// DC quant for index . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float GetDcQuant(int c) => this.dcQuant[c]; + + /// + /// Returns all DC quantizers. See also . + /// + /// Span that covers all DC quantizers. + public Span GetDcQuants() => this.dcQuant; + + /// + /// Returns an inverse DC quant for index c. + /// + /// The inverse DC quantizer index. + /// Inverse DC quant for index . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float GetInverseDcQuant(int c) => this.inverseDcQuant[c]; + + /// + /// Applies the specified DC quantizer. + /// + /// DC quantizer to apply to the dequantization matrices. + public void SetDcQuant(InlineArray3 dc) + { + for (int c = 0; c < 3; c++) + { + this.dcQuant[c] = 1f / dc[c]; + this.inverseDcQuant[c] = dc[c]; + } + } + + /// + /// Sets custom quantizer encodings for transform functions. + /// + /// The encodings to identify required transform functions. + public void SetEncodings(JxlQuantizerEncoding[] encodings) + { + this.encodings = encodings; + this.computedMask = 0; + } + + /// + /// Returns quantizer encodings for this dequant matrices instance. + /// + /// + /// Encodings set by the method. + /// By default (when the aforementioned method wasn't invoked), the result + /// is simply an empty span. + /// + public Span GetEncodings() => this.encodings; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlEndianness.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlEndianness.cs deleted file mode 100644 index a7c9dbc1a2..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlEndianness.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; - -/// -/// Specifies the ordering of multi-byte data. -/// -internal enum JxlEndianness : byte -{ - /// - /// Use endianness of the CPU/system. - /// - Native, - - /// - /// Force little endian. - /// - Little, - - /// - /// Force big endian. - /// - Big -} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs index 825c81cbbc..2edfeca0c1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs @@ -21,6 +21,11 @@ internal sealed class JxlLoopFilter : IJxlFields /// private const float InverseSigmaNum = -1.1715728752538099024f; + /// + /// / 3 + /// + public const float MinimumSigma = -3.90524291751269967465540850526868f; + /// /// Gets the number of EPF (Edge-preserving filter) sharp entries. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs index 79eee30226..0157dba4e1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Common.Helpers; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; @@ -679,4 +680,56 @@ public static ulong CeilLog2Nonzero(ulong x) return floorLog2 + 1; } + + /// + /// Computes the hypotenuse of x and y. + /// + /// X + /// Y + /// Hypotenuse of x and y + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Hypot(double x, double y) + { + x = Math.Abs(x); + y = Math.Abs(y); + + if (x < y) + { + RuntimeUtility.Swap(ref x, ref y); + } + + if (x == 0.0) + { + return 0.0; + } + + double ratio = y / x; + return x * Math.Sqrt(1 + (ratio * ratio)); + } + + /// + /// Computes the hypotenuse of x and y. + /// + /// X + /// Y + /// Hypotenuse of x and y + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Hypot(float x, float y) + { + x = MathF.Abs(x); + y = MathF.Abs(y); + + if (x < y) + { + RuntimeUtility.Swap(ref x, ref y); + } + + if (x == 0.0f) + { + return 0.0f; + } + + float ratio = y / x; + return x * MathF.Sqrt(1 + (ratio * ratio)); + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPixelFormat.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPixelFormat.cs index 15aa587159..fadb8709fb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlPixelFormat.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPixelFormat.cs @@ -24,7 +24,7 @@ internal struct JxlPixelFormat /// big-endian or little-endian format. Applies to ushort /// and float data types. /// - public JxlEndianness Endianness { get; set; } + public ByteOrder Endianness { get; set; } /// /// Gets or sets the alignment of scanlines to a multiple of diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantWeights.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantWeights.cs index 2c50d92afd..dfe12b8f30 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantWeights.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantWeights.cs @@ -12,4 +12,516 @@ internal static class JxlQuantWeights public const int CeilLog2NumPredefinedTables = 0; public const int Log2NumQuantModes = 3; + + /// + /// DCT quantizer encoding. (6 distance bands) + /// + public static readonly JxlQuantizerEncoding Dct = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [3150f, 0f, -0.4f, -0.4f, -0.4f, -2f], + [560f, 0f, -0.3f, -0.3f, -0.3f, -0.3f], + [512f, -2f, -1f, 0f, -1f, -2f] + ], + 6)); + + /// + /// Identity quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Identity = JxlQuantizerEncoding.Identity( + [ + [280f, 3160f, 3160f], + [60f, 864f, 864f], + [18f, 200f, 200f], + ]); + + /// + /// DCT2X2 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct2x2 = JxlQuantizerEncoding.Dct2( + [ + [3840f, 2560f, 1280f, 640f, 480f, 300f], + [960f, 640f, 320f, 180f, 140f, 120f], + [640f, 320f, 128f, 64f, 32f, 16f], + ]); + + /// + /// DCT4X4 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct4x4 = JxlQuantizerEncoding.Dct4( + new JxlDctQuantWeightParameters( + [ + [2200, 0, 0, 0], + [392, 0, 0, 0], + [112, -0.25f, -0.25f, -0.5f] + ], + 4), + [ + [1, 1], + [1, 1], + [1, 1] + ]); + + /// + /// DCT16x16 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct16x16 = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [ + 8996.8725711814115328f, + -1.3000777393353804f, + -0.49424529824571225f, + -0.439093774457103443f, + -0.6350101832695744f, + -0.90177264050827612f, + -1.6162099239887414f, + ], + [ + 3191.48366296844234752f, + -0.67424582104194355f, + -0.80745813428471001f, + -0.44925837484843441f, + -0.35865440981033403f, + -0.31322389111877305f, + -0.37615025315725483f, + ], + [ + 1157.50408145487200256f, + -2.0531423165804414f, + -1.4f, + -0.50687130033378396f, + -0.42708730624733904f, + -1.4856834539296244f, + -4.9209142884401604f, + ] + ], + 7)); + + /// + /// DCT32x32 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct32x32 = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [ + 15718.40830982518931456f, + -1.025f, + -0.98f, + -0.9012f, + -0.4f, + -0.48819395464f, + -0.421064f, + -0.27f, + ], + [ + 7305.7636810695983104f, + -0.8041958212306401f, + -0.7633036457487539f, + -0.55660379990111464f, + -0.49785304658857626f, + -0.43699592683512467f, + -0.40180866526242109f, + -0.27321683125358037f, + ], + [ + 3803.53173721215041536f, + -3.060733579805728f, + -2.0413270132490346f, + -2.0235650159727417f, + -0.5495389509954993f, + -0.4f, + -0.4f, + -0.3f, + ] + ], + 7)); + + /// + /// DCT8x16 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct8x16 = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [ + 7240.7734393502f, + -0.7f, + -0.7f, + -0.2f, + -0.2f, + -0.2f, + -0.5f, + ], + [ + 1448.15468787004f, + -0.5f, + -0.5f, + -0.5f, + -0.2f, + -0.2f, + -0.2f, + ], + [ + 506.854140754517f, + -1.4f, + -0.2f, + -0.5f, + -0.5f, + -1.5f, + -3.6f, + ] + ], + 7)); + + /// + /// DCT8x32 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct8x32 = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [ + 16283.2494710648897f, + -1.7812845336559429f, + -1.6309059012653515f, + -1.0382179034313539f, + -0.85f, + -0.7f, + -0.9f, + -1.2360638576849587f, + ], + [ + 5089.15750884921511936f, + -0.320049391452786891f, + -0.35362849922161446f, + -0.30340000000000003f, + -0.61f, + -0.5f, + -0.5f, + -0.6f, + ], + [ + 3397.77603275308720128f, + -0.321327362693153371f, + -0.34507619223117997f, + -0.70340000000000003f, + -0.9f, + -1.0f, + -1.0f, + -1.1754605576265209f, + ] + ], + 8)); + + /// + /// DCT16x32 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct16x32 = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [ + 13844.97076442300573f, + -0.97113799999999995f, + -0.658f, + -0.42026f, + -0.22712f, + -0.2206f, + -0.226f, + -0.6f, + ], + [ + 4798.964084220744293f, + -0.61125308982767057f, + -0.83770786552491361f, + -0.79014862079498627f, + -0.2692727459704829f, + -0.38272769465388551f, + -0.22924222653091453f, + -0.20719098826199578f, + ], + [ + 1807.236946760964614f, + -1.2f, + -1.2f, + -0.7f, + -0.7f, + -0.7f, + -0.4f, + -0.5f, + ] + ], + 8)); + + /// + /// DCT4x8 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct4x8 = JxlQuantizerEncoding.Dct4x8( + new JxlDctQuantWeightParameters( + [ + [ + 2198.050556016380522f, + -0.96269623020744692f, + -0.76194253026666783f, + -0.6551140670773547f + ], + [ + 764.3655248643528689f, + -0.92630200888366945f, + -0.9675229603596517f, + -0.27845290869168118f + ], + [ + 527.107573587542228f, + -1.4594385811273854f, + -1.450082094097871593f, + -1.5843722511996204f + ] + ], + 4), + [1, 1, 1]); + + /// + /// AFV quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Afv = JxlQuantizerEncoding.Afv( + Dct4x8.DctParameters!, + Dct4x4.DctParameters!, + [ + [3072, 3072, 256, 256, 256, 414, 0, 0, 0], + [1024, 1024, 50, 50, 50, 58, 0, 0, 0], + [384, 384, 12, 12, 12, 22, -0.25f, -0.25f, -0.25f] + ]); + + /// + /// DCT64x64 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct64x64 = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [ + 0.9f * 26629.073922049845f, + -1.025f, + -0.78f, + -0.65012f, + -0.19041574084286472f, + -0.20819395464f, + -0.421064f, + -0.32733845535848671f, + ], + [ + 0.9f * 9311.3238710010046f, + -0.3041958212306401f, + -0.3633036457487539f, + -0.35660379990111464f, + -0.3443074455424403f, + -0.33699592683512467f, + -0.30180866526242109f, + -0.27321683125358037f, + ], + [ + 0.9f * 4992.2486445538634f, + -1.2f, + -1.2f, + -0.8f, + -0.7f, + -0.7f, + -0.4f, + -0.5f, + ] + ], + 8)); + + /// + /// DCT32x64 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct32x64 = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [ + 0.65f * 23629.073922049845f, + -1.025f, + -0.78f, + -0.65012f, + -0.19041574084286472f, + -0.20819395464f, + -0.421064f, + -0.32733845535848671f, + ], + [ + 0.65f * 8611.3238710010046f, + -0.3041958212306401f, + -0.3633036457487539f, + -0.35660379990111464f, + -0.3443074455424403f, + -0.33699592683512467f, + -0.30180866526242109f, + -0.27321683125358037f, + ], + [ + 0.65f * 4492.2486445538634f, + -1.2f, + -1.2f, + -0.8f, + -0.7f, + -0.7f, + -0.4f, + -0.5f, + ] + ], + 8)); + + /// + /// DCT128x128 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct128x128 = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [ + 1.8f * 26629.073922049845f, + -1.025f, + -0.78f, + -0.65012f, + -0.19041574084286472f, + -0.20819395464f, + -0.421064f, + -0.32733845535848671f, + ], + [ + 1.8f * 9311.3238710010046f, + -0.3041958212306401f, + -0.3633036457487539f, + -0.35660379990111464f, + -0.3443074455424403f, + -0.33699592683512467f, + -0.30180866526242109f, + -0.27321683125358037f, + ], + [ + 1.8f * 4992.2486445538634f, + -1.2f, + -1.2f, + -0.8f, + -0.7f, + -0.7f, + -0.4f, + -0.5f, + ] + ], + 8)); + + /// + /// DCT64x128 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct64x128 = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [ + 1.3f * 23629.073922049845f, + -1.025f, + -0.78f, + -0.65012f, + -0.19041574084286472f, + -0.20819395464f, + -0.421064f, + -0.32733845535848671f, + ], + [ + 1.3f * 8611.3238710010046f, + -0.3041958212306401f, + -0.3633036457487539f, + -0.35660379990111464f, + -0.3443074455424403f, + -0.33699592683512467f, + -0.30180866526242109f, + -0.27321683125358037f, + ], + [ + 1.3f * 4492.2486445538634f, + -1.2f, + -1.2f, + -0.8f, + -0.7f, + -0.7f, + -0.4f, + -0.5f, + ] + ], + 8)); + + /// + /// DCT256x256 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct256x256 = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [ + 3.6f * 26629.073922049845f, + -1.025f, + -0.78f, + -0.65012f, + -0.19041574084286472f, + -0.20819395464f, + -0.421064f, + -0.32733845535848671f, + ], + [ + 3.6f * 9311.3238710010046f, + -0.3041958212306401f, + -0.3633036457487539f, + -0.35660379990111464f, + -0.3443074455424403f, + -0.33699592683512467f, + -0.30180866526242109f, + -0.27321683125358037f, + ], + [ + 3.6f * 4992.2486445538634f, + -1.2f, + -1.2f, + -0.8f, + -0.7f, + -0.7f, + -0.4f, + -0.5f, + ] + ], + 8)); + + /// + /// DCT128x256 quantizer encoding. + /// + public static readonly JxlQuantizerEncoding Dct128x256 = JxlQuantizerEncoding.Dct( + new JxlDctQuantWeightParameters( + [ + [ + 2.6f * 23629.073922049845f, + -1.025f, + -0.78f, + -0.65012f, + -0.19041574084286472f, + -0.20819395464f, + -0.421064f, + -0.32733845535848671f, + ], + [ + 2.6f * 8611.3238710010046f, + -0.3041958212306401f, + -0.3633036457487539f, + -0.35660379990111464f, + -0.3443074455424403f, + -0.33699592683512467f, + -0.30180866526242109f, + -0.27321683125358037f, + ], + [ + 2.6f * 4492.2486445538634f, + -1.2f, + -1.2f, + -0.8f, + -0.7f, + -0.7f, + -0.4f, + -0.5f, + ] + ], + 8)); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs index 9551b97e09..54a41f99ab 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs @@ -134,7 +134,7 @@ public JxlQuantizer(JxlDequantMatrices dequant, int quantDc, int globalScale) /// /// Gets the default bias for quant. /// - private static ReadOnlySpan DefaultQuantBias => + public static ReadOnlySpan DefaultQuantBias => [ 1.0f - 0.05465007330715401f, 1.0f - 0.07005449891748593f, @@ -165,7 +165,7 @@ public void ClearDcMultipliers() /// /// The new scale /// The scale value, scaled by the global scale. - private float ScaleGlobalScale(float scale) + public float ScaleGlobalScale(float scale) { int newGlobalScale = (int)MathF.Round(this.globalScale * scale, MidpointRounding.AwayFromZero); float scaleOut = newGlobalScale * 1.0f / this.globalScale; @@ -199,7 +199,7 @@ public void RecomputeFromGlobalScale() /// The quantization index /// The dequant matrix. public ReadOnlySpan DequantMatrix(JxlAcStrategyType strategy, int c) - => this.dequant.Matrix(strategy, c); + => this.dequant!.GetMatrix(strategy, c); /// /// Returns the inverse dequant matrix. @@ -208,21 +208,21 @@ public ReadOnlySpan DequantMatrix(JxlAcStrategyType strategy, int c) /// The quantization index /// The inverse dequant matrix. public ReadOnlySpan InverseDequantMatrix(JxlAcStrategyType strategy, int c) - => this.dequant.InverseMatrix(strategy, c); + => this.dequant!.GetInverseMatrix(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); + public float GetDcStep(int c) => this.InverseQuantDc * this.dequant!.GetDcQuant(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.Scale * this.quantDc); + public float GetInverseDcStep(int c) => this.dequant!.GetInverseDcQuant(c) * (this.Scale * this.quantDc); /// /// Creates JXL quantizer parameters with values reflecting those in this quantizer instance. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerConstants.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerConstants.cs new file mode 100644 index 0000000000..daf28db696 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerConstants.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Shared constants used by the quantizer. +/// +internal static class JxlQuantizerConstants +{ + /// + /// Total number of quantization tables. + /// + public const byte NumberOfQuantizerTables = (byte)(JxlQuantTable.DCT128X256 + 1); + + /// + /// Gets the inverse DC quantization table. + /// + public static ReadOnlySpan InverseDcQuant => [4096f, 512f, 256f]; + + /// + /// Gets the forward DC quantization table. + /// + public static ReadOnlySpan DcQuant => [ + 1f / 4096f, + 1f / 512f, + 1f / 256f]; + + /// + /// Gets a translation table for converting AC strategies to quant tables. + /// Simply pass the index of the AC strategy enum and you'll get back the + /// matching quant table. + /// + public static ReadOnlySpan AcStrategyToQuantTableMap => + [ + JxlQuantTable.DCT, JxlQuantTable.IDENTITY, JxlQuantTable.DCT2X2, + JxlQuantTable.DCT4X4, JxlQuantTable.DCT16X16, JxlQuantTable.DCT32X32, + JxlQuantTable.DCT8X16, JxlQuantTable.DCT8X16, JxlQuantTable.DCT8X32, + JxlQuantTable.DCT8X32, JxlQuantTable.DCT16X32, JxlQuantTable.DCT16X32, + JxlQuantTable.DCT4X8, JxlQuantTable.DCT4X8, JxlQuantTable.AFV0, + JxlQuantTable.AFV0, JxlQuantTable.AFV0, JxlQuantTable.AFV0, + JxlQuantTable.DCT64X64, JxlQuantTable.DCT32X64, JxlQuantTable.DCT32X64, + JxlQuantTable.DCT128X128, JxlQuantTable.DCT64X128, JxlQuantTable.DCT64X128, + JxlQuantTable.DCT256X256, JxlQuantTable.DCT128X256, JxlQuantTable.DCT128X256 + ]; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs index fead5354cd..8a12e6dbf5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; - namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// @@ -56,27 +54,27 @@ public JxlQuantizerEncoding(JxlQuantizerEncoding other) /// /// Gets or sets the weights for the identity transform. /// - public InlineArray3> IdWeights { get; set; } + public float[][]? IdWeights { get; set; } /// /// Gets or sets the weights for the DCT2 transform. /// - public InlineArray3> Dct2Weights { get; set; } + public float[][]? Dct2Weights { get; set; } /// /// Gets or sets the multipliers for the DCT4 transform. /// - public InlineArray3> Dct4Multipliers { get; set; } + public float[][]? Dct4Multipliers { get; set; } /// /// Gets or sets the weights for the AFV transform. /// - public InlineArray3> AfvWeights { get; set; } + public float[][]? AfvWeights { get; set; } /// /// Gets or sets the multipliers for the 4x8 DCT block-based transform. /// - public InlineArray3 Dct4x8Multipliers { get; set; } + public float[]? Dct4x8Multipliers { get; set; } /// /// Gets or sets the explicit quantization table (like in JPEG). @@ -123,7 +121,7 @@ public static JxlQuantizerEncoding Library(int libraryIndex) /// /// Weights for the identity transform. /// A new Identity quantizer encoding. - public static JxlQuantizerEncoding Identity(in InlineArray3> xybWeights) + public static JxlQuantizerEncoding Identity(float[][] xybWeights) => new() { Mode = JxlQuantMode.Id, @@ -136,7 +134,7 @@ public static JxlQuantizerEncoding Identity(in InlineArray3> /// /// Weights for the DCT2x2 transform. /// A new DCT2x2 quantizer encoding. - public static JxlQuantizerEncoding Dct2(in InlineArray3> xybWeights) + public static JxlQuantizerEncoding Dct2(float[][] xybWeights) => new() { Mode = JxlQuantMode.Dct2, @@ -150,7 +148,7 @@ public static JxlQuantizerEncoding Dct2(in InlineArray3> xyb /// Quantizer weights for the DCT4x4 transform. /// XYB multipliers for the DCT4x4 transform. /// A new DCT4x4 quantizer encoding. - public static JxlQuantizerEncoding Dct4(JxlDctQuantWeightParameters parameters, in InlineArray3> xybMul) + public static JxlQuantizerEncoding Dct4(JxlDctQuantWeightParameters parameters, float[][] xybMul) => new() { Mode = JxlQuantMode.Dct4, @@ -165,7 +163,7 @@ public static JxlQuantizerEncoding Dct4(JxlDctQuantWeightParameters parameters, /// Quantizer weights for the DCT4x8 transform. /// XYB multipliers for the DCT4x8 transform. /// A new DCT4x8 quantizer encoding. - public static JxlQuantizerEncoding Dct4x8(JxlDctQuantWeightParameters parameters, in InlineArray3 xybMul) + public static JxlQuantizerEncoding Dct4x8(JxlDctQuantWeightParameters parameters, float[] xybMul) => new() { Mode = JxlQuantMode.Dct4x8, @@ -197,7 +195,7 @@ public static JxlQuantizerEncoding Dct(JxlDctQuantWeightParameters parameters) public static JxlQuantizerEncoding Afv( JxlDctQuantWeightParameters params4x8, JxlDctQuantWeightParameters params4x4, - in InlineArray3> weights) + float[][] weights) => new() { Mode = JxlQuantMode.Afv, diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs new file mode 100644 index 0000000000..b03c77eced --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs @@ -0,0 +1,133 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal static partial class JxlSimdUtils +{ + public static void StoreInterleaved(Vector v1, Vector v2, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + } + + public static void StoreInterleaved(Vector v1, Vector v2, Vector v3, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + } + + public static void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + } + + public static void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, Vector v5, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); + } + + public static void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, Vector v5, Vector v6, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); + v6.StoreUnsafe(ref Unsafe.Add(ref memory, 5)); + } + + public static void StoreInterleaved(Vector128 v1, Vector128 v2, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + } + + public static void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + } + + public static void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + } + + public static void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, Vector128 v5, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); + } + + public static void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, Vector128 v5, Vector128 v6, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); + v6.StoreUnsafe(ref Unsafe.Add(ref memory, 5)); + } + + public static void StoreInterleaved(Vector256 v1, Vector256 v2, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + } + + public static void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + } + + public static void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + } + + public static void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, Vector256 v5, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); + } + + public static void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, Vector256 v5, Vector256 v6, ref T memory) + { + v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); + v6.StoreUnsafe(ref Unsafe.Add(ref memory, 5)); + } + +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt new file mode 100644 index 0000000000..41a5b9c90d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt @@ -0,0 +1,44 @@ +<#@ template debug="false" hostspecific="false" language="C#" #> +<#@ assembly name="System.Core" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ output extension=".Generated.cs" #> +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +internal static partial class JxlSimdUtils +{ +<# + string[] vectorTypes = [ + "Vector", + "Vector128", + "Vector256" + ]; + + const int maxVectorSize = 6; + + foreach (string vect in vectorTypes) { + for (int i = 2; i <= maxVectorSize; i++) { + List vectorParameters = []; + for (int j = 0; j < i; j++) { + vectorParameters.Add($"{vect} v{j + 1}"); + } + string inlineParameters = string.Join(", ", vectorParameters) + ", "; +#> + public static void StoreInterleaved(<#= inlineParameters #>ref T memory) + { +<# for (int j = 0; j < i; j++) { #> + v<#= j + 1 #>.StoreUnsafe(ref Unsafe.Add(ref memory, <#= j #>)); +<# } #> + } + +<# } } #> +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs new file mode 100644 index 0000000000..dd8e7314ee --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs @@ -0,0 +1,104 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing; + +/// +/// Shared SIMD-accelerated utilities. +/// +internal static partial class JxlSimdUtils +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 ConcatLowerLower(Vector256 a, Vector256 b) => Vector256.Create(a.GetLower(), b.GetLower()); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 ConcatUpperUpper(Vector256 a, Vector256 b) => Vector256.Create(a.GetUpper(), b.GetUpper()); + + public static void Transpose8x8Block(Span fromSpan, Span toSpan, int stride) + { + ref int from = ref MemoryMarshal.GetReference(fromSpan); + ref int to = ref MemoryMarshal.GetReference(toSpan); + + if (Vector256.IsHardwareAccelerated) + { + Vector256 i0 = Vector256.LoadUnsafe(ref from); + Vector256 i1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref from, stride)); + Vector256 i2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref from, 2 * stride)); + Vector256 i3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref from, 3 * stride)); + Vector256 i4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref from, 4 * stride)); + Vector256 i5 = Vector256.LoadUnsafe(ref Unsafe.Add(ref from, 5 * stride)); + Vector256 i6 = Vector256.LoadUnsafe(ref Unsafe.Add(ref from, 6 * stride)); + Vector256 i7 = Vector256.LoadUnsafe(ref Unsafe.Add(ref from, 7 * stride)); + + Vector256 q0 = Vector256_.InterleaveLower(i0, i2); + Vector256 q1 = Vector256_.InterleaveLower(i1, i3); + Vector256 q2 = Vector256_.InterleaveUpper(i0, i2); + Vector256 q3 = Vector256_.InterleaveUpper(i1, i3); + Vector256 q4 = Vector256_.InterleaveLower(i4, i6); + Vector256 q5 = Vector256_.InterleaveLower(i5, i7); + Vector256 q6 = Vector256_.InterleaveUpper(i4, i6); + Vector256 q7 = Vector256_.InterleaveUpper(i5, i7); + + Vector256 r0 = Vector256_.InterleaveLower(q0, q1); + Vector256 r1 = Vector256_.InterleaveUpper(q0, q1); + Vector256 r2 = Vector256_.InterleaveLower(q2, q3); + Vector256 r3 = Vector256_.InterleaveUpper(q2, q3); + Vector256 r4 = Vector256_.InterleaveLower(q4, q5); + Vector256 r5 = Vector256_.InterleaveUpper(q4, q5); + Vector256 r6 = Vector256_.InterleaveLower(q6, q7); + Vector256 r7 = Vector256_.InterleaveUpper(q6, q7); + + i0 = ConcatLowerLower(r4, r0); + i1 = ConcatLowerLower(r5, r1); + i2 = ConcatLowerLower(r6, r2); + i3 = ConcatLowerLower(r7, r3); + i4 = ConcatUpperUpper(r4, r0); + i5 = ConcatUpperUpper(r5, r1); + i6 = ConcatUpperUpper(r6, r2); + i7 = ConcatUpperUpper(r7, r3); + + i0.StoreUnsafe(ref to); + i1.StoreUnsafe(ref Unsafe.Add(ref to, 8)); + i2.StoreUnsafe(ref Unsafe.Add(ref to, 16)); + i3.StoreUnsafe(ref Unsafe.Add(ref to, 24)); + i4.StoreUnsafe(ref Unsafe.Add(ref to, 32)); + i5.StoreUnsafe(ref Unsafe.Add(ref to, 40)); + i6.StoreUnsafe(ref Unsafe.Add(ref to, 48)); + i7.StoreUnsafe(ref Unsafe.Add(ref to, 56)); + } + else + { + // Vector128 fallback + for (int n = 0; n < 8; n += 4) + { + for (int m = 0; m < 8; m += 4) + { + Vector128 p0 = Vector128.LoadUnsafe(ref Unsafe.Add(ref from, (n * stride) + m)); + Vector128 p1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref from, ((n + 1) * stride) + m)); + Vector128 p2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref from, ((n + 2) * stride) + m)); + Vector128 p3 = Vector128.LoadUnsafe(ref Unsafe.Add(ref from, ((n + 3) * stride) + m)); + + Vector128 q0 = Vector128_.InterleaveLower(p0, p2); + Vector128 q1 = Vector128_.InterleaveLower(p1, p3); + Vector128 q2 = Vector128_.InterleaveUpper(p0, p2); + Vector128 q3 = Vector128_.InterleaveUpper(p1, p3); + + Vector128 r0 = Vector128_.InterleaveLower(q0, q1); + Vector128 r1 = Vector128_.InterleaveUpper(q0, q1); + Vector128 r2 = Vector128_.InterleaveLower(q2, q3); + Vector128 r3 = Vector128_.InterleaveUpper(q2, q3); + + r0.StoreUnsafe(ref Unsafe.Add(ref to, (m * 8) + n)); + r1.StoreUnsafe(ref Unsafe.Add(ref to, ((m + 1) * 8) + n)); + r2.StoreUnsafe(ref Unsafe.Add(ref to, ((m + 2) * 8) + n)); + r3.StoreUnsafe(ref Unsafe.Add(ref to, ((m + 3) * 8) + n)); + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs index b2a0c0c55f..91258b8529 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal struct JxlWeightsSeparable5 diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs index 98c1d2dc54..fcbef7afd7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; @@ -10,6 +11,10 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPr /// internal static class JxlContextPrediction { + private const int ExtraPropertiesPerChannel = 4; + + private const int NumberOfProperties = 1; + public static void SetPredictorMode(int i, JxlModularHeader header) { ref uint wr = ref header.GetWReference(); @@ -94,4 +99,389 @@ public static void SetPredictorMode(int i, JxlModularHeader header) break; } } + + /// + /// Returns true if the (meta)predictor makes use of the weighted predictor. + /// + /// The input predictor. + /// Value indicating whether the predictor uses weighted prediction. + public static bool IsWeightedPredictor(JxlPredictor predictor) => predictor switch + { + JxlPredictor.Zero or + JxlPredictor.Left or + JxlPredictor.Top or + JxlPredictor.Average0 or + JxlPredictor.Select or + JxlPredictor.Gradient => false, + + JxlPredictor.Weighted => true, + + JxlPredictor.TopRight or + JxlPredictor.TopLeft or + JxlPredictor.LeftLeft or + JxlPredictor.Average1 or + JxlPredictor.Average2 or + JxlPredictor.Average3 or + JxlPredictor.Average4 => false, + + JxlPredictor.Best or + JxlPredictor.Variable => true, + + _ => false, + }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ClampedGradient(int n, int w, int l) + { + int min = Math.Min(n, w); + int max = Math.Max(n, w); + + int gradient = n + w - l; + + int clamp = l < min ? max : gradient; + return l > max ? min : clamp; + } + + // This is actually a simple Paeth predictor, we'd often see + // this in PNG files + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Select(int a, int b, int c) + { + int p = a + b - c; + int pa = Numerics.Abs(p - a); + int pb = Numerics.Abs(p - b); + return pa < pb ? a : b; + } + + public static void PrecomputeReferences(JxlModularChannel channel, int y, JxlModularImage image, int i, JxlModularChannel references) + { + references.Plane.Clear(); + int offset = 0; + int numExtraProps = references.Width; + int oneRow = references.Plane.PixelsPerRow; + JxlModularChannel channelI = image.Channels[i]; + + for (int j = i - 1; i >= 0 && offset < numExtraProps; j--) + { + JxlModularChannel channelJ = image.Channels[j]; + + if (channelJ.Width != channelI.Width || channelJ.Height != channelI.Height) + { + continue; + } + + if (channelJ.HorizontalShift != channelI.HorizontalShift || + channelJ.VerticalShift != channelI.VerticalShift) + { + continue; + } + + Span rp = references.GetRow(0)[offset..]; + Span rpp = channelJ.GetRow(y); + Span rpprev = channelJ.GetRow(y > 0 ? y - 1 : 0); + + for (int x = 0; x < channel.Width; x++, rp = rp[oneRow..]) + { + int v = rpp[x]; + rp[0] = Numerics.Abs(v); + rp[1] = v; + + // Neighboring variables + int vleft = x > 0 ? rpp[x - 1] : 0; + int vtop = y > 0 ? rpprev[x] : vleft; + int vtopleft = x > 0 && y > 0 ? rpprev[x - 1] : vleft; + + // Prediction + int vpredicted = ClampedGradient(vleft, vtop, vtopleft); + rp[2] = Numerics.Abs(v - vpredicted); + rp[3] = v - vpredicted; + } + + offset += ExtraPropertiesPerChannel; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void InitializePropertiesForRow(Span p, InlineArray2 staticProperties, int y) + { + p[0] = staticProperties[0]; + p[1] = staticProperties[1]; + p[2] = y; + p[9] = 0; // Local gradient + } + + // Prediction for one pixel using neighbors + [MethodImpl(InliningOptions.HotPath)] // This method is called frequently + public static int PredictOne( + JxlPredictor p, + int left, + int top, + int toptop, + int topleft, + int topright, + int leftleft, + int toprightright, + int wpPred) => p switch + { + JxlPredictor.Zero => 0, + JxlPredictor.Left => left, + JxlPredictor.Top => top, + JxlPredictor.Select => Select(left, top, topleft), + JxlPredictor.Weighted => wpPred, + JxlPredictor.Gradient => ClampedGradient(left, top, topleft), + JxlPredictor.TopLeft => topleft, + JxlPredictor.TopRight => topright, + JxlPredictor.LeftLeft => leftleft, + JxlPredictor.Average0 => (left + top) / 2, + JxlPredictor.Average1 => (left + topleft) / 2, + JxlPredictor.Average2 => (topleft + top) / 2, + JxlPredictor.Average3 => (top + topright) / 2, + JxlPredictor.Average4 => ((6 * top) - (2 * toptop) + (7 * left) + (1 * leftleft) + + (1 * toprightright) + (3 * topright) + 8) / + 16, + _ => 0, + }; + + public static JxlPredictionResult Predict( + JxlPredictorMode mode, + Span p, // contains properties + int w, // block width + ref int pp, // This is a reference to the output pixel stored in row-major order. Negative offsets are accessed to reference other pixels in the image, specifically neighboring pixles. + int oneRow, // Number of pixels on one row + int x, + int y, + JxlPredictor predictor, + JxlMaTreeLookup? lookup, + JxlModularChannel? references, + JxlModularState? wpState, + Span predictions) + { + int offset = 3; // Start at position 3 because of 2 static properties + y + + // Status flags + // computeProperties = should the p (properties) variable be updated? + // nec = are there no edge cases? + bool computeProperties = (mode & JxlPredictorMode.UseTree) != 0 || (mode & JxlPredictorMode.ForceComputeProperties) != 0; + bool nec = (mode & JxlPredictorMode.NoEdgeCases) != 0; + + // The following variables are neighboring pixels relative to the pixel to predict. + // Pixels may be unavailable and therefore replaced with default values. For example, + // at Y=0, the top pixel may not be available because we're already at the very top + // of the image, there's no "above" of that. + int left = nec || x > 0 ? Unsafe.Subtract(ref pp, 1) : (y > 0 ? Unsafe.Subtract(ref pp, oneRow) : 0); // ⬅️ (or 0 if unavailable) + int top = nec || y > 0 ? Unsafe.Subtract(ref pp, oneRow) : left; // ⬆️ (or ⬅️ if unavailable) + int topleft = nec || (x > 0 && y > 0) ? Unsafe.Add(ref pp, -1 - oneRow) : left; // ↗️ (or ⬅️ if unavailable) + int topright = nec || (x + 1 < w && y > 0) ? Unsafe.Add(ref pp, 1 - oneRow) : top; // ↖️ (or ⬆️ if unavailable) + int leftleft = nec || x > 1 ? Unsafe.Subtract(ref pp, 2) : left; // ⬅️⬅️ (or ⬅️ if unavailable) + int toptop = nec || y > 1 ? Unsafe.Add(ref pp, -oneRow - oneRow) : top; // ⬆️⬆️ (or ⬆️ if unavailable) + int toprightright = nec || (x + 2 < w && y > 0) ? Unsafe.Add(ref pp, 2 - oneRow) : topright; // ↗️➡️ (or ↗️ if unavailable) + + if (computeProperties) + { + p[offset++] = x; + p[offset++] = top > 0 ? top : -top; + p[offset++] = left > 0 ? left : -left; + p[offset++] = top; + p[offset++] = left; + + // Local gradient + p[offset] = left - p[offset + 1]; + offset++; + + // Local gradient + p[offset++] = left + top - topleft; + + // FFV1 context properties + p[offset++] = left - topleft; + p[offset++] = topleft - top; + p[offset++] = top - topright; + p[offset++] = top - toptop; + p[offset++] = left - leftleft; + } + + // Predicted weighted prediction value + int wpPred = 0; + + if ((mode & JxlPredictorMode.UseWeightedPrediction) != 0) + { + if (wpState is null) + { + throw new InvalidOperationException("Weighted prediction state is missing"); + } + + wpPred = unchecked((int)wpState.Predict(computeProperties, x, y, w, top, left, topright, topleft, toptop, p, offset)); + } + + if (!nec && computeProperties) + { + if (references is null) + { + throw new InvalidOperationException("References are missing"); + } + + offset += NumberOfProperties; + + // Extra properties + Span rp = references.GetRow(x); + for (int i = 0; i < references.Width; i++) + { + p[offset++] = rp[i]; + } + } + + JxlPredictionResult predResult = default; + + if ((mode & JxlPredictorMode.UseTree) != 0) + { + if (lookup is null) + { + throw new InvalidOperationException("Lookup is missing"); + } + + JxlMaTreeLookupResult result = lookup.Lookup(p); + predictor = result.Predictor; + predResult = new((int)result.Context, result.Offset, default, result.Multiplier); + } + + if ((mode & JxlPredictorMode.AllPredictions) != 0) + { + for (int i = 0; i < JxlPredictorFacts.ModularPredictors; i++) + { + predictions[i] = PredictOne((JxlPredictor)i, left, top, toptop, topleft, topright, leftleft, toprightright, wpPred); + } + } + + predResult = new( + predResult.Context, + predResult.Guess + PredictOne(predictor, left, top, toptop, topleft, topright, leftleft, toprightright, wpPred), + predictor, + predResult.Multiplier); + + return predResult; + } + + // The following methods are just wrappers over the Predict + // method. + // See https://github.com/libjxl/libjxl/blob/main/lib/jxl/modular/encoding/context_predict.h#L593-L709 + public static JxlPredictionResult PredictNoTreeNoWeightedPrediction( + int w, + ref int pp, + int oneRow, + int x, + int y, + JxlPredictor predictor) + => Predict(0, [], w, ref pp, oneRow, x, y, predictor, null, null, null, []); + + public static JxlPredictionResult PredictNoTreeWeightedPrediction( + int w, + ref int pp, + int oneRow, + int x, + int y, + JxlPredictor predictor, + JxlModularState wpState) + => Predict(JxlPredictorMode.UseTree, [], w, ref pp, oneRow, x, y, predictor, null, null, wpState, []); + + public static JxlPredictionResult PredictTreeNoWeightedPrediction( + Span p, + int w, + ref int pp, + int oneRow, + int x, + int y, + JxlMaTreeLookup treeLookup, + JxlModularChannel references) + => Predict(JxlPredictorMode.UseTree, p, w, ref pp, oneRow, x, y, JxlPredictor.Zero, treeLookup, references, null, []); + + public static JxlPredictionResult PredictTreeNoWeightedPredictionNoEdgeCases( + Span p, + int w, + ref int pp, + int oneRow, + int x, + int y, + JxlMaTreeLookup treeLookup, + JxlModularChannel references) + => Predict(JxlPredictorMode.UseTree | JxlPredictorMode.NoEdgeCases, p, w, ref pp, oneRow, x, y, JxlPredictor.Zero, treeLookup, references, null, []); + + public static JxlPredictionResult PredictTreeWeightedPrediction( + Span p, + int w, + ref int pp, + int oneRow, + int x, + int y, + JxlMaTreeLookup treeLookup, + JxlModularChannel references, + JxlModularState wpState) + => Predict(JxlPredictorMode.UseTree | JxlPredictorMode.UseWeightedPrediction, p, w, ref pp, oneRow, x, y, JxlPredictor.Zero, treeLookup, references, wpState, []); + + public static JxlPredictionResult PredictTreeWeightedPredictionNoEdgeCases( + Span p, + int w, + ref int pp, + int oneRow, + int x, + int y, + JxlMaTreeLookup treeLookup, + JxlModularChannel references, + JxlModularState wpState) + => Predict(JxlPredictorMode.UseTree | JxlPredictorMode.UseWeightedPrediction | JxlPredictorMode.NoEdgeCases, p, w, ref pp, oneRow, x, y, JxlPredictor.Zero, treeLookup, references, wpState, []); + + public static JxlPredictionResult PredictLearn( + Span p, + int w, + ref int pp, + int oneRow, + int x, + int y, + JxlPredictor predictor, + JxlModularChannel references, + JxlModularState wpState) + => Predict(JxlPredictorMode.ForceComputeProperties | JxlPredictorMode.UseWeightedPrediction, p, w, ref pp, oneRow, x, y, predictor, null, references, wpState, []); + + public static JxlPredictionResult PredictLearnAll( + Span p, + int w, + ref int pp, + int oneRow, + int x, + int y, + JxlModularChannel references, + JxlModularState wpState, + Span predictions) + => Predict(JxlPredictorMode.ForceComputeProperties | JxlPredictorMode.UseWeightedPrediction | JxlPredictorMode.AllPredictions, p, w, ref pp, oneRow, x, y, JxlPredictor.Zero, null, references, wpState, predictions); + + public static JxlPredictionResult PredictLearnNoEdgeCases( + Span p, + int w, + ref int pp, + int oneRow, + int x, + int y, + JxlPredictor predictor, + JxlModularChannel references, + JxlModularState wpState) + => Predict(JxlPredictorMode.ForceComputeProperties | JxlPredictorMode.UseWeightedPrediction | JxlPredictorMode.NoEdgeCases, p, w, ref pp, oneRow, x, y, predictor, null, references, wpState, []); + + public static JxlPredictionResult PredictLearnAllNoEdgeCases( + Span p, + int w, + ref int pp, + int oneRow, + int x, + int y, + JxlModularChannel references, + JxlModularState wpState, + Span predictions) + => Predict(JxlPredictorMode.ForceComputeProperties | JxlPredictorMode.UseWeightedPrediction | JxlPredictorMode.AllPredictions | JxlPredictorMode.NoEdgeCases, p, w, ref pp, oneRow, x, y, JxlPredictor.Zero, null, references, wpState, predictions); + + public static JxlPredictionResult PredictAllNoWeightedPrediction( + int w, + ref int pp, + int oneRow, + int x, + int y, + Span predictions) + => Predict(JxlPredictorMode.AllPredictions, [], w, ref pp, oneRow, x, y, JxlPredictor.Zero, null, null, null, predictions); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictionResult.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictionResult.cs new file mode 100644 index 0000000000..f1822a2a07 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictionResult.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +/// +/// The result of context prediction. +/// +/// Context used in MA lookup. +/// Predicted coefficient. +/// Kind of predictor mode used. +/// Multiplier used in MA lookup. +internal record struct JxlPredictionResult(int Context, int Guess, JxlPredictor Predictor, int Multiplier); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictorMode.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictorMode.cs new file mode 100644 index 0000000000..5144f14cb6 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlPredictorMode.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; + +/// +/// Flags for context prediction. +/// +[Flags] +internal enum JxlPredictorMode : byte +{ + /// + /// Should tree-based prediction be used? + /// + UseTree = 1, + + /// + /// Should the weighted predictor be used? + /// + UseWeightedPrediction = 2, + + /// + /// Should properties be computed? (When this bit is 0, + /// the properties are not set and therefore have their + /// default values) + /// + ForceComputeProperties = 4, + + /// + /// Try all predictors? + /// + AllPredictions = 8, + + NoEdgeCases = 16 +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs index d5fd89881d..acf3b9532c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularChannel.cs @@ -1,27 +1,22 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular; /// -/// A wrapper over for modular operations. +/// A wrapper over for modular operations. /// internal sealed class JxlModularChannel { - /// - /// Underlying plane buffer. - /// - private JxlPlane plane; - public JxlModularChannel(Configuration configuration, int width, int height, int horizShift, int vertShift) { this.HorizontalShift = horizShift; this.VerticalShift = vertShift; this.Width = width; this.Height = height; - this.plane = JxlPlane.Create(configuration, width, height); + this.Plane = new JxlImageI(configuration, width, height); } /// @@ -55,15 +50,20 @@ public JxlModularChannel(Configuration configuration, int width, int height, int /// public int Component { get; set; } = -1; + /// + /// Gets or sets the backing plane buffer. + /// + public JxlImageI Plane { get; set; } + public void Shrink(Configuration configuration) { - if (this.plane.XSize == this.Width && this.plane.YSize == this.Height) + if (this.Plane.XSize == this.Width && this.Plane.YSize == this.Height) { return; } - this.plane.Dispose(); - this.plane = JxlPlane.Create(configuration, this.Width, this.Height); + this.Plane.Dispose(); + this.Plane = new JxlImageI(configuration, this.Width, this.Height); } public void Shrink(Configuration configuration, int newWidth, int newHeight) @@ -73,5 +73,5 @@ public void Shrink(Configuration configuration, int newWidth, int newHeight) this.Shrink(configuration); } - public Span GetRow(int y) => this.plane.GetRow(y); + public Span GetRow(int y) => this.Plane.GetRow(y); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs index 488e198c4d..ebcdcdcdc8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/JxlModularImage.cs @@ -7,5 +7,13 @@ internal sealed class JxlModularImage { public List Channels { get; set; } = []; - + /// + /// Gets or sets the total number of metachannels in this image. + /// + public int MetaChannels { get; set; } + + /// + /// Gets or sets the bit depth used in this image. + /// + public int BitDepth { get; set; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs index 4babb79f32..bfcd683c5b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs @@ -2,11 +2,14 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; /// -/// Palette/indexed coding +/// Palette/indexed decoder and encoder. /// internal static class JxlPalette { @@ -43,8 +46,39 @@ internal static class JxlPalette private const int ImplicitPaletteSize = LargeCubeOffset + (LargeCube * LargeCube * LargeCube); + private const bool EncodeToHighQualityImplicitPalette = true; + + /// + /// Minimum index required to use the implicit palette. + /// + private const int MinimumImplicitPaletteIndex = -((2 * 72) - 1); + + /// + /// Backing array used to construct data for the matrix. + /// + private static readonly int[,] DefaultOffsetsData = + { + { 1, 2 }, + { 0, 3 }, + { 0, 4 }, + { 1, 1 }, + { 1, 3 }, + { 2, 2 }, + { 1, 0 }, + { 1, 4 }, + { 2, 1 }, + { 2, 3 }, + { 2, 0 }, + { 2, 4 } + }; + + /// + /// Used by the palette encoder. + /// + private static readonly DenseMatrix DefaultOffsets = new(DefaultOffsetsData); + /// - /// Static delta palette used by GetPaletteValue. + /// Static delta palette used by . /// private static readonly int[][] DeltaPalette = [ @@ -149,4 +183,1193 @@ public static int GetPaletteValue(Span palette, int index, int c, int oneRo return palette[(c * oneRow) + index]; } + + public static void MetaPalette(Configuration configuration, JxlModularImage input, int beginC, int endC, int numberOfColors, int numberOfDeltas) + { + JxlTransform.CheckEqualChannels(input, beginC, endC); + int nb = endC - beginC + 1; + if (beginC >= input.MetaChannels) + { + // Palette was done on normal channels + input.MetaChannels++; + } + else + { + // Palette was done on metachannels + if (endC >= input.MetaChannels) + { + throw new InvalidOperationException("End channel offset is out of bounds"); + } + + input.MetaChannels += 2 - nb; + } + + input.Channels.RemoveRange(beginC + 1, endC - beginC); + JxlModularChannel ch = new(configuration, numberOfColors + numberOfDeltas, nb, -1, -1); + input.Channels.Insert(0, ch); + } + + /// + /// Decodes palette/indexed images. + /// + /// Configuration for parallelism. + /// Input & out images. + /// Offset of output channel. + /// Number of colors. + /// Number of deltas. + /// Kind of predictor mode to use. + /// For weighted prediction. + /// Thrown when the input for prediction is invalid. + /// Thrown when there are too many channels. + public static void InversePalette(Configuration configuration, JxlModularImage input, int beginC, int nbColors, int nbDeltas, JxlPredictor predictor, JxlModularHeader weightedHeader) + { + if (input.MetaChannels < 1) + { + throw new InvalidOperationException("A palette transform was invoked without a palette"); + } + + int nb = input.Channels[0].Height; + int c0 = beginC + 1; + + if (c0 >= input.Channels.Count) + { + throw new InvalidOperationException("Channel is out of range"); + } + + JxlModularChannel channel = input.Channels[c0]; + int w = channel.Width; + int h = channel.Height; + + if (nb < 1) + { + throw new InvalidOperationException("Transforms are corrupted"); + } + + for (int i = 1; i < nb; i++) + { + JxlModularChannel newChannel = new(configuration, w, h, channel.HorizontalShift, channel.VerticalShift); + input.Channels.Insert(c0 + 1, newChannel); + } + + JxlModularChannel palette = input.Channels[0]; + + int oneRow = palette.Plane.PixelsPerRow; + int oneRowImage = channel.Plane.PixelsPerRow; + int bitDepth = Math.Min(input.BitDepth, 24); + + if (w == 0) + { + // Channel is empty. Don't do anything. + } + else if (nbDeltas == 0 && predictor == JxlPredictor.Zero) + { + if (nb == 1) + { + _ = Parallel.For(0, h, configuration.GetParallelOptions(), y => + { + Span p = channel.GetRow(y); + Span paletteData = palette.GetRow(0); + + for (int x = 0; x < w; x++) + { + int index = Math.Clamp(p[x], 0, palette.Width - 1); + + p[x] = GetPaletteValue(paletteData, index, 0, oneRow, bitDepth); + } + }); + } + else if (nb == 2) + { + _ = Parallel.For(0, h, configuration.GetParallelOptions(), y => + { + Span paletteData = palette.GetRow(0); + Span p0 = channel.GetRow(y); + Span p1 = input.Channels[c0 + 1].GetRow(y); + + for (int x = 0; x < w; x++) + { + int index0 = Math.Clamp(p0[x], 0, palette.Width - 1); + int index1 = Math.Clamp(p1[x], 0, palette.Width - 1); + + p0[x] = GetPaletteValue(paletteData, index0, 0, oneRow, bitDepth); + p1[x] = GetPaletteValue(paletteData, index1, 0, oneRow, bitDepth); + } + }); + } + else if (nb == 3) + { + _ = Parallel.For(0, h, configuration.GetParallelOptions(), y => + { + Span paletteData = palette.GetRow(0); + Span p0 = channel.GetRow(y); + Span p1 = input.Channels[c0 + 1].GetRow(y); + Span p2 = input.Channels[c0 + 2].GetRow(y); + + for (int x = 0; x < w; x++) + { + int index0 = Math.Clamp(p0[x], 0, palette.Width - 1); + int index1 = Math.Clamp(p1[x], 0, palette.Width - 1); + int index2 = Math.Clamp(p2[x], 0, palette.Width - 1); + + p0[x] = GetPaletteValue(paletteData, index0, 0, oneRow, bitDepth); + p1[x] = GetPaletteValue(paletteData, index1, 0, oneRow, bitDepth); + p2[x] = GetPaletteValue(paletteData, index2, 0, oneRow, bitDepth); + } + }); + } + else if (nb == 4) + { + _ = Parallel.For(0, h, configuration.GetParallelOptions(), y => + { + Span paletteData = palette.GetRow(0); + Span p0 = channel.GetRow(y); + Span p1 = input.Channels[c0 + 1].GetRow(y); + Span p2 = input.Channels[c0 + 2].GetRow(y); + Span p3 = input.Channels[c0 + 3].GetRow(y); + + for (int x = 0; x < w; x++) + { + int index0 = Math.Clamp(p0[x], 0, palette.Width - 1); + int index1 = Math.Clamp(p1[x], 0, palette.Width - 1); + int index2 = Math.Clamp(p2[x], 0, palette.Width - 1); + int index3 = Math.Clamp(p3[x], 0, palette.Width - 1); + + p0[x] = GetPaletteValue(paletteData, index0, 0, oneRow, bitDepth); + p1[x] = GetPaletteValue(paletteData, index1, 0, oneRow, bitDepth); + p2[x] = GetPaletteValue(paletteData, index2, 0, oneRow, bitDepth); + p3[x] = GetPaletteValue(paletteData, index3, 0, oneRow, bitDepth); + } + }); + } + else if (nb == 5) + { + _ = Parallel.For(0, h, configuration.GetParallelOptions(), y => + { + Span paletteData = palette.GetRow(0); + Span p0 = channel.GetRow(y); + Span p1 = input.Channels[c0 + 1].GetRow(y); + Span p2 = input.Channels[c0 + 2].GetRow(y); + Span p3 = input.Channels[c0 + 3].GetRow(y); + Span p4 = input.Channels[c0 + 4].GetRow(y); + + for (int x = 0; x < w; x++) + { + int index0 = Math.Clamp(p0[x], 0, palette.Width - 1); + int index1 = Math.Clamp(p1[x], 0, palette.Width - 1); + int index2 = Math.Clamp(p2[x], 0, palette.Width - 1); + int index3 = Math.Clamp(p3[x], 0, palette.Width - 1); + int index4 = Math.Clamp(p4[x], 0, palette.Width - 1); + + p0[x] = GetPaletteValue(paletteData, index0, 0, oneRow, bitDepth); + p1[x] = GetPaletteValue(paletteData, index1, 0, oneRow, bitDepth); + p2[x] = GetPaletteValue(paletteData, index2, 0, oneRow, bitDepth); + p3[x] = GetPaletteValue(paletteData, index3, 0, oneRow, bitDepth); + p4[x] = GetPaletteValue(paletteData, index4, 0, oneRow, bitDepth); + } + }); + } + else + { + throw new NotImplementedException($"Too many channels for palette compressed images: {nb}"); + } + } + else + { + JxlImageI plane = input.Channels[c0].Plane; + JxlImageI indices = new(configuration, plane.XSize, plane.YSize); + input.Channels[c0].Plane = indices; + + if (predictor == JxlPredictor.Weighted) + { + _ = Parallel.For(0, nb, configuration.GetParallelOptions(), c => + { + JxlModularChannel channel = input.Channels[c0 + c]; + JxlModularState wpState = new(weightedHeader, channel.Width); + Span paletteData = palette.GetRow(0); + + for (int y = 0; y < channel.Height; y++) + { + Span p = channel.GetRow(y); + Span idx = indices.GetRow(y); + + for (int x = 0; x < channel.Width; x++) + { + int index = idx[x]; + int value = 0; + int paletteEntry = GetPaletteValue(paletteData, index, c, oneRow, bitDepth); + JxlPredictionResult pred = JxlContextPrediction.PredictTreeNoWeightedPrediction(channel.Width, p[x..], oneRowImage, x, y, predictor, wpState); + + if (index < nbDeltas) + { + value = pred.Guess + paletteEntry; + } + else + { + value = paletteEntry; + } + + p[x] = value; + wpState.UpdatePredictionErrors(p[x], x, y, channel.Width); + } + } + }); + } + else + { + _ = Parallel.For(0, nb, configuration.GetParallelOptions(), c => + { + JxlModularChannel channel = input.Channels[c0 + c]; + Span paletteData = palette.GetRow(0); + + for (int y = 0; y < channel.Height; y++) + { + Span p = channel.GetRow(y); + Span idx = indices.GetRow(y); + + for (int x = 0; x < channel.Width; x++) + { + int index = idx[x]; + int value = 0; + int paletteEntry = GetPaletteValue(paletteData, index, c, oneRow, bitDepth); + + if (index < nbDeltas) + { + JxlPredictionResult pred = JxlContextPrediction.PredictNoTreeNoWeightedPrediction(channel.Width, p[x..], oneRowImage, x, y, predictor); + value = pred.Guess + paletteEntry; + } + else + { + value = paletteEntry; + } + + p[x] = value; + } + } + }); + } + } + + if (c0 >= input.MetaChannels) + { + input.MetaChannels--; + } + else + { + if (input.MetaChannels >= 2 - nb) + { + throw new InvalidOperationException("Too many meta channels"); + } + + input.MetaChannels -= 2 - nb; + + if (beginC + nb - 1 < input.MetaChannels) + { + throw new InvalidOperationException("Too many meta channels"); + } + + input.Channels.RemoveAt(0); + } + } + + private static float ColorDistance(Span a, Span b) + { + InlineArray3 array = default; + array[0] = b[0]; + array[1] = b[1]; + array[2] = b[2]; + return ColorDistance(a, array); + } + + private static float ColorDistance(Span a, InlineArray3 b) + { + if (a.Length != 3) + { + throw new InvalidOperationException("Length mismatch"); + } + + float distance = 0; + float ave3 = 0; + + if (a.Length >= 3) + { + ave3 = (a[0] + b[0] + a[1] + b[1] + a[2] + b[2]) * (1.21f / 3.0f); + } + + float sumA = 0; + float sumB = 0; + + for (int c = 0; c < a.Length; c++) + { + float diff = a[c] - b[c]; + float weight = c == 0 ? 3f : c == 1 ? 5f : 2f; + + if (c < 3 && (a[c] + b[c] >= ave3)) + { + weight += c == 2 ? 1.12f : 1.15f; + + if (c == 2 && ((a[2] + b[2]) < 1.22f * ave3)) + { + weight -= 0.5f; + } + } + + distance += diff * diff * weight * weight; + int sumWeight = c == 0 ? 3 : c == 1 ? 5 : 1; + + sumA += a[c] * sumWeight; + sumB += b[c] * sumWeight; + } + + distance *= 4; + float sumDiff = sumA - sumB; + distance += sumDiff * sumDiff; + return distance; + } + + private static int QuantizeColorToImplicitPaletteIndex(Span color, int paletteSize, int bitDepth, bool highQuality) + { + int index = 1; + int quant = (1 << bitDepth) - 1; + int half = bitDepth > 1 ? (1 << (bitDepth - 1)) : 0; + + if (highQuality) + { + int multiplier = 1; + + for (int i = 0; i < color.Length; i++) + { + int value = color[i]; + int quantized = (((LargeCube - 1) * value) + half) / quant; + index += quantized * multiplier; + multiplier *= LargeCube; + } + + return index + (paletteSize * LargeCubeOffset); + } + else + { + int multiplier = 1; + int bdMinus3 = 1 << Math.Max(0, bitDepth - 3); + + for (int i = 0; i < color.Length; i++) + { + int value = color[i]; + value -= bdMinus3; + value = Math.Max(0, value); + + int quantized = (((LargeCube - 1) * value) + half) / quant; + quantized = Math.Min(quantized, SmallCube - 1); // cannot be > SmallCube - 1 + + index += quantized * multiplier; + multiplier *= SmallCube; + } + + return index + paletteSize; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int RoundInteger(int value, int div) + { + if (value < 0) + { + return (-value + (div / 2)) / div; + } + else + { + return (value + (div / 2)) / div; + } + } + + /// + /// Encodes an image into palette/indexed coding mode. + /// + /// Configuration for memory allocation/management. + /// Input image. + /// Offset of starting channel. + /// Offset of final channel. + /// Number of colors. + /// Number of deltas. + /// Should the output palette produced by this method be sorted? + /// Should the palette be quantized in lossy mode? (May discard subtle pixel values) + /// The kind of predictor that was used. + /// Header for weighted prediction. + public static void ForwardPalette( + Configuration configuration, + JxlModularImage input, + int beginC, + int endC, + ref int numberOfColors, + ref int numberOfDeltas, + bool ordered, + bool lossy, + ref JxlPredictor predictor, + JxlModularHeader wpHeader) + { + PaletteIterationData paletteIterationData = new(); + int originalNumberOfColors = numberOfColors; + int originalNumberOfDeltas = numberOfDeltas; + + if (lossy && input.BitDepth >= 8) + { + ForwardPaletteIteration( + configuration, + input, + beginC, + endC, + ref originalNumberOfColors, + ref originalNumberOfDeltas, + ordered, + lossy, + ref predictor, + wpHeader, + paletteIterationData); + } + + paletteIterationData.IsFinalRun = false; + ForwardPaletteIteration( + configuration, + input, + beginC, + endC, + ref numberOfColors, + ref numberOfDeltas, + ordered, + lossy, + ref predictor, + wpHeader, + paletteIterationData); + } + + private static void ForwardPaletteIteration( + Configuration configuration, + JxlModularImage input, + int beginC, + int endC, + ref int numberOfColors, + ref int numberOfDeltas, + bool ordered, + bool lossy, + ref JxlPredictor predictor, + JxlModularHeader wpHeader, + PaletteIterationData paletteIterationData) + { + JxlTransform.CheckEqualChannels(input, beginC, endC); + DebugGuard.MustBeGreaterThanOrEqualTo(beginC, input.MetaChannels, nameof(beginC)); + int nb = endC - beginC + 1; // inclusive number of channels + + JxlModularChannel beginCChannel = input.Channels[beginC]; + int w = beginCChannel.Width; + int h = beginCChannel.Height; + + if (input.BitDepth >= 32) + { + throw new InvalidOperationException("Bit depth is too large"); + } + + if (!lossy && numberOfColors < 2) + { + throw new InvalidOperationException("Lossless palette transform needs at least 3 channels"); + } + + int idx = 0; + + if (!lossy && nb == 1) + { + if (numberOfColors == 0) + { + throw new InvalidOperationException("No colors"); + } + + JxlTransform.ComputeMinMax(beginCChannel, out int minValue, out int maxValue); + int lookupTableSize = maxValue - minValue + 1; + + if (lookupTableSize < MaxPaletteLookupTableSize) + { + HashSet chPalette = []; + + for (int y = 0; y < h; y++) + { + Span p = beginCChannel.GetRow(y); + + for (int x = 0; x < w; x++) + { + bool newColor = chPalette.Add(p[x]); + + if (newColor) + { + idx++; + + if (idx > numberOfColors) + { + throw new InvalidOperationException("Index out of bounds"); + } + } + } + } + + // Don't dispose. The channel is stored into the input. + JxlModularChannel modularChannel = new(configuration, idx, 1, -1, -1); + + numberOfColors = idx; + idx = 0; + + Span ppalette = modularChannel.GetRow(0); + + foreach (int p in chPalette) + { + ppalette[idx++] = p; + } + + for (int y = 0; y < h; y++) + { + Span p = beginCChannel.GetRow(y); + + for (int x = 0; x < w; x++) + { + for (idx = 0; p[x] != ppalette[idx] && idx < numberOfColors; idx++) + { + // nop; this is to find the value of idx + } + + p[x] = idx; + } + } + + predictor = JxlPredictor.Zero; + input.MetaChannels++; + input.Channels.Insert(0, modularChannel); + + return; + } + + Span lookup = stackalloc int[lookupTableSize]; + idx = 0; + + for (int y = 0; y < h; y++) + { + Span p = beginCChannel.GetRow(y); + for (int x = 0; x < w; x++) + { + if (lookup[p[x] - minValue] == 0) + { + lookup[p[x] - minValue] = 1; + idx++; + + if (idx > numberOfColors) + { + throw new InvalidOperationException("Index out of bounds"); + } + } + } + } + + // Don't dispose. The channel is stored into the input. + JxlModularChannel channel = new(configuration, idx, 1, -1, -1); + numberOfColors = idx; + idx = 0; + Span pPalette = channel.GetRow(0); + + for (int i = 0; i < lookupTableSize; i++) + { + if (lookup[i] != 0) + { + pPalette[idx] = i + minValue; + lookup[i] = idx; + idx++; + } + } + + for (int y = 0; y < h; y++) + { + Span p = beginCChannel.GetRow(y); + + for (int x = 0; x < w; x++) + { + p[x] = lookup[p[x] - minValue]; + } + } + + predictor = JxlPredictor.Zero; + input.MetaChannels++; + input.Channels.Insert(0, channel); + + return; + } + + JxlModularImage quantizedInput = new(configuration, 1, 1, -1, -1); + + if (lossy) + { + quantizedInput.Dispose(); + quantizedInput = new(configuration, w, h, input.BitDepth, nb); + + for (int c = 0; c < nb; c++) + { + if (!JxlImageOperations.CopyImage(input.Channels[beginC + c].Plane, quantizedInput.Channels[c].Plane)) + { + throw new InvalidOperationException("Copying failed"); + } + } + } + + numberOfDeltas = 0; + bool deltaUsed = false; + List candidatePalette = []; + List candidatePaletteImageOrder = []; + Dictionary inversePalette = []; + + // Don't use stackalloc for color so we can store it as a + // dictionary member in colorFrequencyMap (see below) + int[] color = new int[nb]; + Span colorSpan = color.AsSpan(); + Span colorWithError = stackalloc float[nb]; + + if (lossy) + { + paletteIterationData.FindFrequentColorDeltas(w * h, input.BitDepth); + numberOfDeltas = paletteIterationData.FrequentDeltas[0].Count; + Dictionary colorFrequencyMap = []; + + DenseMatrix offsets = new(4, 2); + + for (int y = 1; y + 1 < h; y++) + { + for (int x = 1; x + 1 < w; x++) + { + for (int c = 0; c < nb; c++) + { + colorSpan[c] = input.Channels[beginC + c].GetRow(y)[x]; + } + + // Defaults + offsets[0, 0] = 1; + offsets[0, 0] = 0; + offsets[1, 0] = -1; + offsets[1, 1] = 0; + offsets[2, 0] = 0; + offsets[2, 1] = 1; + offsets[3, 0] = 0; + offsets[3, 1] = -1; + + bool makesCross = true; + + for (int i = 0; i < 4 && makesCross; ++i) + { + int dx = offsets[i, 0]; + int dy = offsets[i, 1]; + + for (int c = 0; c < nb && makesCross; c++) + { + if (input.Channels[beginC + c].GetRow(y + dy)[x + dx] != colorSpan[c]) + { + makesCross = false; + } + } + } + + if (makesCross) + { + colorFrequencyMap[color]++; + } + } + } + + const float imageFraction = 0.01f; + int colorFrequencyLowerBound = 5 + (int)(input.Height * input.Width * imageFraction); + + foreach (KeyValuePair colorFreq in colorFrequencyMap) + { + if (colorFreq.Value > colorFrequencyLowerBound) + { + candidatePalette.Insert(0, colorFreq.Key); + candidatePaletteImageOrder.Add(colorFreq.Key); + } + } + } + + Dictionary implicitColor = []; + int[][] implicitColors = new int[ImplicitPaletteSize][]; + + for (int k = 0; k < ImplicitPaletteSize; k++) + { + for (int i = 0; i < nb; i++) + { + color[i] = GetPaletteValue([], k, i, 0, input.BitDepth); + } + + implicitColor[color] = true; + implicitColors[k] = color; + } + + int implicitColorsUsed = 0; + Dictionary colorFreqMap = []; + for (int y = 0; y < h; y++) + { + for (int x = 0; x < w; x++) + { + if (lossy && candidatePalette.Count >= numberOfColors) + { + break; + } + + for (int c = 0; c < nb; c++) + { + colorSpan[c] = input.Channels[beginC + c].GetRow(y)[x]; + } + + const bool new_color = candidatePalette.Add(color).second; + if (new_color) + { + if (implicitColor[color]) + { + implicitColorsUsed++; + } + else + { + candidatePaletteImageOrder.Add(color); + if (candidatePaletteImageOrder.Count > numberOfColors) + { + throw new InvalidOperationException("Too many colors for palette/indexed"); + } + } + } + + colorFreqMap[color]++; + } + } + + numberOfColors = numberOfDeltas + candidatePaletteImageOrder.Count; + if (!lossy && numberOfColors + implicitColorsUsed == 1) + { + // It's not useful to have a single-color palette. + throw new InvalidOperationException("Palette only has one color"); + } + + for (int k = 0; k < ImplicitPaletteSize; k++) + { + color = implicitColors[k]; + if (colorFreqMap[color] > 10) + { + numberOfColors++; + candidatePaletteImageOrder.Add(color); + } + } + + for (int k = 0; k < ImplicitPaletteSize; k++) + { + color = implicitColors[k]; + inversePalette[color] = numberOfColors + k; + } + + // Don't dispose this. + JxlModularChannel newChannel = new(configuration, numberOfColors, nb, -1, -1); + Span palette = newChannel.GetRow(0); + int oneRow = newChannel.Plane.PixelsPerRow; + int oneRowImage = beginCChannel.Plane.PixelsPerRow; + int bitDepth = Math.Min(input.BitDepth, 24); // max. 24 bits, cannot be greater + + if (lossy) + { + for (int i = 0; i < numberOfDeltas; i++) + { + for (int c = 0; c < 3; c++) + { + palette[(c * oneRow) + i] = paletteIterationData.FrequentDeltas[c][i]; + } + } + } + + float frequencyThreshold = 4f; + int clr = 0; + + if (ordered && nb >= 3) + { + candidatePaletteImageOrder.Sort((ap, bp) => + { + float ay = (0.299f * ap[0]) + (0.587f * ap[1]) + (0.114f * ap[2]) + 0.1f; + + if (ap.Length > 3) + { + ay *= 1f + ap[3]; + } + + float by = (0.299f * bp[0]) + (0.587f * bp[1]) + (0.114f * bp[2]) + 0.1f; + + if (bp.Length > 3) + { + by *= 1f + bp[3]; + } + + ay = colorFreqMap[ap] > frequencyThreshold ? -ay : ay; + by = colorFreqMap[bp] > frequencyThreshold ? -by : by; + + return ay.CompareTo(by); + }); + } + + foreach (int[] pcol in candidatePaletteImageOrder) + { + Span pcolSpan = pcol.AsSpan(); + + for (int i = 0; i < nb; i++) + { + palette[numberOfDeltas + (i * oneRow) + clr] = pcolSpan[i]; + } + + inversePalette[pcol] = clr++; + } + + List wpStates = []; + + for (int c = 0; c < nb; c++) + { + wpStates.Add(new JxlModularState(wpHeader, w)); + } + + InlineArray3> errorRow = default; + + if (lossy) + { + errorRow[0] = new(nb, w + 4); + errorRow[1] = new(nb, w + 4); + errorRow[2] = new(nb, w + 4); + } + + Span bestValue = stackalloc int[nb]; + Span idealResidual = stackalloc int[nb]; + Span quantizedValue = stackalloc int[nb]; + Span predictions = stackalloc int[nb]; + + // This is a temporary buffer, values are copied here. + // It is so we can swap spans. Since spans are just a view + // of memory, using CopyTo as a swap means we need a + // separate buffer like this for the swapping value. + Span tempBuffer = stackalloc float[w + 4]; + + for (int y = 0; y < h; y++) + { + for (int c = 0; c < nb; c++) + { + p_in[c] = input.channel[begin_c + c].Row(y); + if (lossy) + p_quant[c] = quantized_input.channel[c].Row(y); + } + + Span p = beginCChannel.GetRow(y); + + for (int x = 0; x < w; x++) + { + int index; + if (!lossy) + { + for (int c = 0; c < nb; c++) + { + color[c] = p_in[c][x]; + } + + index = inversePalette[color]; + } + else + { + int best_index = 0; + bool best_is_delta = false; + float best_distance = float.PositiveInfinity; + + bestValue.Clear(); + idealResidual.Clear(); + quantizedValue.Clear(); + predictions.Clear(); + + foreach (double diffusion_multiplier in (Span)[0.55, 0.75]) + { + for (int c = 0; c < nb; c++) + { + colorWithError[c] = + p_in[c][x] + ((paletteIterationData.IsFinalRun ? 1 : 0) * + diffusion_multiplier * errorRow[0][c, x + 2]); + color[c] = (int)Math.Clamp(MathF.Round(colorWithError[c]), 0, (1 << input.BitDepth) - 1); + } + + for (int c = 0; c < nb; c++) + { + predictions[c] = PredictTreeNoWeightedPrediction(w, p_quant[c] + x, oneRowImage, x, y, predictor, wpStates[c]).Guess; + } + + void TryIndex(int index, Span predictions, Span idealResidual, Span colorWithError, ref Span bestValue, ref Span quantizedValue, Span palette, ref int numberOfColors, ref int numberOfDeltas) + { + for (int c = 0; c < nb; c++) + { + quantizedValue[c] = GetPaletteValue(palette, index, c, oneRow, bitDepth); + if (index < numberOfDeltas) + { + quantizedValue[c] += predictions[c]; + } + } + + float color_distance = 32.0f / (1 << Math.Max(0, 2 * (bitDepth - 8))) * ColorDistance(colorWithError, quantizedValue); + + float indexPenalty = 0; + if (index == -1) + { + indexPenalty = -124; + } + else if (index < 0) + { + indexPenalty = -2 * index; + } + else if (index < numberOfDeltas) + { + indexPenalty = 250; + } + else if (index < numberOfColors) + { + indexPenalty = 150; + } + else if (index < numberOfColors + LargeCubeOffset) + { + indexPenalty = 70; + } + else + { + indexPenalty = 256; + } + + float distance = color_distance + indexPenalty; + if (distance < best_distance) + { + best_distance = distance; + best_index = index; + best_is_delta = index < numberOfDeltas; + + RuntimeUtility.Swap(ref bestValue, ref quantizedValue); + + for (int c = 0; c < nb; c++) + { + idealResidual[c] = (int)(colorWithError[c] - predictions[c]); + } + } + } + + for (index = MinimumImplicitPaletteIndex; index < numberOfColors; index++) + { + TryIndex(index); + } + + TryIndex(QuantizeColorToImplicitPaletteIndex(color, numberOfColors, bitDepth, false)); + + if (EncodeToHighQualityImplicitPalette) + { + TryIndex(QuantizeColorToImplicitPaletteIndex(color, numberOfColors, bitDepth, true)); + } + } + + index = best_index; + deltaUsed |= best_is_delta; + + if (!paletteIterationData.IsFinalRun) + { + for (int c = 0; c < 3; c++) + { + paletteIterationData.Deltas[c].Add(idealResidual[c]); + } + + paletteIterationData.DeltaDistances.Add(best_distance); + } + + for (int c = 0; c < nb; c++) + { + wpStates[c].UpdatePredictionErrors(bestValue[c], x, y, w); + p_quant[c][x] = bestValue[c]; + } + + float len_error = 0; + for (int c = 0; c < nb; c++) + { + float local_error = colorWithError[c] - bestValue[c]; + len_error += local_error * local_error; + } + + len_error = MathF.Sqrt(len_error); + float modulate = 1f; + long len_limit = 38 << Math.Max(0, bitDepth - 8); + if (len_error > len_limit) + { + modulate *= len_limit / len_error; + } + + DenseMatrix offsets = new(12, 2); + + for (int c = 0; c < nb; c++) + { + float total_error = colorWithError[c] - bestValue[c]; + + DefaultOffsets.Data.AsSpan().CopyTo(offsets.Data); + + float total_available = 0; + for (int i = 0; i < 11; i++) + { + int row = offsets[i, 0]; + int col = offsets[i, 1]; + + if (Math.Sign(errorRow[row][c, x + col]) != Math.Sign(total_error)) + { + total_available += errorRow[row][c, x + col]; + } + } + + float weight = MathF.Abs(total_error) / (MathF.Abs(total_available) + 1e-3f); + weight = MathF.Min(weight, 1.0f); + + for (int i = 0; i < 11; ++i) + { + int row = offsets[i, 0]; + int col = offsets[i, 1]; + + if (Math.Sign(errorRow[row][c, x + col]) != Math.Sign(total_error)) + { + total_error += weight * errorRow[row][c, x + col]; + errorRow[row][c, x + col] *= 1 - weight; + } + } + + total_error *= modulate; + float remaining_error = (1.0f / 14f) * total_error; + errorRow[0][c, x + 3] += 2 * remaining_error; + errorRow[0][c, x + 4] += remaining_error; + errorRow[1][c, x + 0] += remaining_error; + + for (int i = 0; i < 5; ++i) + { + errorRow[1][c, x + i] += remaining_error; + errorRow[2][c, x + i] += remaining_error; + } + } + } + + if (paletteIterationData.IsFinalRun) + { + p[x] = index; + } + } + + if (lossy) + { + for (int c = 0; c < nb; c++) + { + // Variables for swapping + Span pos0 = errorRow[0].Data.AsSpan(c, w + 4); + Span pos1 = errorRow[1].Data.AsSpan(c, w + 4); + Span pos2 = errorRow[2].Data.AsSpan(c, w + 4); + + // we need to swap: + // error_row[0][c].swap(error_row[1][c]); + pos0.CopyTo(tempBuffer); + pos1.CopyTo(pos0); + tempBuffer.CopyTo(pos1); + + // swap old1, old2 + pos1.CopyTo(tempBuffer); + pos2.CopyTo(pos1); + + pos2.Clear(); + } + } + } + + if (!deltaUsed) + { + predictor = JxlPredictor.Zero; + } + + if (paletteIterationData.IsFinalRun) + { + input.MetaChannels++; + input.Channels.RemoveRange(beginC + 1, endC - beginC); + input.Channels.Insert(0, newChannel); + } + + numberOfColors -= numberOfDeltas; + } + + /// + /// For palette encoding. + /// + internal sealed class PaletteIterationData + { + /// + /// Maximum number of deltas. + /// + private const int MaxDeltas = 128; + + private InlineArray3> deltas; + + public bool IsFinalRun { get; set; } + + public InlineArray3> Deltas + { + get => this.deltas; + set => this.deltas = value; + } + + public List DeltaDistances { get; set; } = []; + + public List[] FrequentDeltas { get; set; } = new List[3]; + + public void FindFrequentColorDeltas(int numPixels, int bitDepth) + { + Dictionary, double> deltaFrequencyMap = []; + int bucketSize = 3 << Math.Max(0, bitDepth - 3); + for (int i = 0; i < this.Deltas[0].Count; i++) + { + InlineArray3 delta = default; + delta[0] = RoundInteger(this.Deltas[0][i], bucketSize); + delta[1] = RoundInteger(this.Deltas[1][i], bucketSize); + delta[2] = RoundInteger(this.Deltas[2][i], bucketSize); + + // Condition equivalent to delta[0] == 0 && delta[1] == 0 && delta[2] == 0 + if ((delta[0] | delta[1] | delta[2]) == 0) + { + continue; + } + + deltaFrequencyMap[delta] += Math.Sqrt(Math.Sqrt(this.DeltaDistances[i])); + } + + float deltaDistanceMultiplier = 1f / numPixels; + Span allZero = [0, 0, 0]; + + foreach (KeyValuePair, double> deltaFrequency in deltaFrequencyMap) + { + float deltaDistance = MathF.Sqrt(ColorDistance(allZero, deltaFrequency.Key)) + 1f; + double second = deltaFrequency.Value * deltaDistance * deltaDistanceMultiplier; + deltaFrequencyMap[deltaFrequency.Key] = second; + } + + Dictionary, double> sorted = deltaFrequencyMap.ToDictionary( + entry => entry.Key, + entry => entry.Value); + + IOrderedEnumerable, double>> sortedEnumerator = sorted.OrderBy( + x => x.Value); + + foreach (KeyValuePair, double> deltaFrequency in sortedEnumerator) + { + if (this.FrequentDeltas[0].Count >= MaxDeltas) + { + break; + } + + if (deltaFrequency.Value < 17) + { + break; + } + + for (int c = 0; c < 3; c++) + { + this.FrequentDeltas[c].Add(deltaFrequency.Key[c] * bucketSize); + } + } + } + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs index 0f32e47aee..315fcf2b4e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlRct.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs new file mode 100644 index 0000000000..d21d0438c7 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs @@ -0,0 +1,793 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +#pragma warning disable IDE0057 // Use range operator + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; + +/// +/// Implements the squeeze transform. +/// +/// +/// The squeeze transform in JXL is a reversible +/// wavelet-like decomposition used in the modular mode +/// to reduce redundancy and improve compression, +/// especially for structured or synthetic images. +/// It works by hierarchically splitting channesl +/// into lower-resolution representations plus +/// residuals, giving us multi-resolution coding while +/// remaining lossless. +/// +internal static class JxlSqueeze +{ + private const int MaxFirstPreviewSize = 8; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int SmoothTendency(int b, int a, int n) + { + int diff = 0; + if (b >= a && a >= n) + { + diff = ((4 * b) - (3 * n) - a + 6) / 12; + + if (diff - (diff & 1) > 2 * (b - a)) + { + diff = (2 * (b - a)) + 1; + } + + if (diff + (diff & 1) > 2 * (a - n)) + { + diff = 2 * (a - n); + } + } + else if (b <= a && a <= n) + { + diff = ((4 * b) - (3 * n) - a - 6) / 12; + + if (diff + (diff & 1) < 2 * (b - a)) + { + diff = (2 * (b - a)) - 1; + } + + if (diff - (diff & 1) < 2 * (a - n)) + { + diff = 2 * (a - n); + } + } + + return diff; + } + + // The function operates on 256-bit fixed size vectors, + // 8 elements at a time. It should still work even on CPUs + // without 256-bit vector support (the JIT will translate + // these into 128-bit halves, or scalar without SIMD support). + // + // The FastUnsqueeze method CAN operate on vectors below + // 256-bit, but not above. It's better to simply use Vector256 + // rather than duplicate everything. Vector may be a problem + // as its number of elements can be greater than 8 which is too + // much for this method. + [MethodImpl(InliningOptions.HotPath)] // Called on an entire image + private static void FastUnsqueeze(Span pResidual, Span pAvg, Span pNAvg, Span pPout, Span pOut, Span pNOut) + { + Vector256 oneThird = Vector256.Create(0x55555556); + + ref int pAvgRef = ref MemoryMarshal.GetReference(pAvg); + ref int pNAvgRef = ref MemoryMarshal.GetReference(pNAvg); + ref int pPoutReference = ref MemoryMarshal.GetReference(pPout); + ref int pResidualRef = ref MemoryMarshal.GetReference(pResidual); + ref int pOutRef = ref MemoryMarshal.GetReference(pOut); + ref int pNOutRef = ref MemoryMarshal.GetReference(pNOut); + + Vector256 avg = Vector256.LoadUnsafe(ref pAvgRef); + Vector256 nextAvg = Vector256.LoadUnsafe(ref pNAvgRef); + Vector256 top = Vector256.LoadUnsafe(ref pPoutReference); + + Vector256 ba = top - avg; + Vector256 an = avg - nextAvg; + Vector256 nonmono = ba ^ an; + Vector256 absba = Vector256.Abs(ba); + Vector256 absan = Vector256.Abs(an); + Vector256 absbn = Vector256.Abs(top - nextAvg); + + Vector256 a3eh = Vector256_.MultiplyEven(absba, oneThird); + Vector256 a3oh = Vector256_.MultiplyOdd(absba, oneThird); + + Vector256 a3 = BitConverter.IsLittleEndian + ? Vector256_.InterleaveOdd(a3eh.AsInt32(), a3oh.AsInt32()) + : Vector256_.InterleaveEven(a3eh.AsInt32(), a3oh.AsInt32()); + + a3 += absbn + Vector256.Create(2); + + Vector256 absdiff = a3 >> 2; + + Vector256 skipdiff = Vector256_.NotEqual(ba, Vector256.Zero); + skipdiff &= Vector256_.NotEqual(an, Vector256.Zero); + skipdiff &= Vector256.LessThan(nonmono, Vector256.Zero); + + Vector256 absBa2 = (absba << 1) + (absdiff & Vector256.One); + + absdiff = Vector256.ConditionalSelect( + Vector256.GreaterThan(absdiff, absBa2), + (absba << 1) + Vector256.One, + absdiff); + + Vector256 absan2 = absan << 1; + absdiff = Vector256.ConditionalSelect( + Vector256.GreaterThan(absdiff + (absdiff & Vector256.One), absan2), + absan2, + absdiff); + + Vector256 diff1 = Vector256.ConditionalSelect( + Vector256.LessThan(top, nextAvg), + -absdiff, + absdiff); + + Vector256 tendency = diff1 & ~skipdiff; + Vector256 diffMinusTendency = Vector256.LoadUnsafe(ref pResidualRef); + Vector256 diff = diffMinusTendency + tendency; + Vector256 output = avg + (diff + (diff << 31)); + + output.StoreUnsafe(ref pOutRef); + (output - diff).StoreUnsafe(ref pNOutRef); + } + + public static void InverseHorizontalSqueeze(Configuration configuration, JxlModularImage input, int c, int rc) + { + // Channel offsets should not overflow. + DebugGuard.MustBeLessThan(c, input.Channels.Count, nameof(c)); + DebugGuard.MustBeLessThan(rc, input.Channels.Count, nameof(c)); + + JxlModularChannel inputChannel = input.Channels[c]; + JxlModularChannel inputResidualChannel = input.Channels[rc]; + + if (inputChannel.Width != JxlMath.DivCeil(inputChannel.Width + inputResidualChannel.Width, 2)) + { + throw new InvalidOperationException("Invalid width"); + } + + if (inputChannel.Height != inputResidualChannel.Height) + { + throw new InvalidOperationException("Height of the input channel must be equal to the height of the residual channel"); + } + + if (inputResidualChannel.Width == 0) + { + input.Channels[c].HorizontalShift--; + return; + } + + // Do not dispose. + JxlModularChannel outputChannel = new( + configuration, + inputChannel.Width + inputResidualChannel.Width, + inputChannel.Height, + inputChannel.HorizontalShift - 1, + inputChannel.VerticalShift); + + if (inputResidualChannel.Height == 0) + { + input.Channels[c] = outputChannel; + return; + } + + // The number of rows a single parallel iteration computes + // is stored here. + const int rowsPerThread = 8; + + // rowsPerThread * 9, aligned to the power of 2. + const int rowsPerThreadMul9Alignment = 128; + + // rowsPerThread * 8, aligned to the power of 2. + const int rowsPerThreadMul8Alignment = 64; + + _ = Parallel.For(0, JxlMath.DivCeil(inputChannel.Height, rowsPerThread), configuration.GetParallelOptions(), idx => + { + int y0 = idx * rowsPerThread; + int rows = Math.Min(rowsPerThread, inputChannel.Height - y0); + int x = 0; + + int onerow_in = inputChannel.Plane.PixelsPerRow; + int onerow_inr = inputResidualChannel.Plane.PixelsPerRow; + int onerow_out = outputChannel.Plane.PixelsPerRow; + Span pResidual = inputResidualChannel.GetRow(y0); + Span pAverage = inputChannel.GetRow(y0); + Span pOut = outputChannel.GetRow(y0); + ref int pOutRef = ref MemoryMarshal.GetReference(pOut); + + Span bpAvg = stackalloc int[rowsPerThreadMul9Alignment].Slice(0, rowsPerThread * 9); + Span bpResidual = stackalloc int[rowsPerThreadMul8Alignment].Slice(0, rowsPerThread * 8); + Span bpOutEven = stackalloc int[rowsPerThreadMul8Alignment].Slice(0, rowsPerThread * 8); + Span bpOutOdd = stackalloc int[rowsPerThreadMul8Alignment].Slice(0, rowsPerThread * 8); + Span bpOutEvenT = stackalloc int[rowsPerThreadMul8Alignment].Slice(0, rowsPerThread * 8); + Span bpOutOddT = stackalloc int[rowsPerThreadMul8Alignment].Slice(0, rowsPerThread * 8); + + ref int bpOutEvenTRef = ref MemoryMarshal.GetReference(bpOutEvenT); + ref int bpOutOddTRef = ref MemoryMarshal.GetReference(bpOutOddT); + + int n = Vector256.Count; + + if (inputResidualChannel.Width > 16 && rows == rowsPerThread) + { + for (; x < inputResidualChannel.Width - 9; x += 8) + { + JxlSimdUtils.Transpose8x8Block(pResidual[x..], bpResidual, onerow_inr); + JxlSimdUtils.Transpose8x8Block(pAverage[x..], bpAvg, onerow_in); + + for (int y = 0; y < rowsPerThread; y++) + { + bpAvg[64 + y] = pAverage[x + 8 + (onerow_in * y)]; + } + + for (int i = 0; i < 8; i++) + { + // i * 8 + int i8 = i << 3; + + FastUnsqueeze( + bpResidual[i8..], + bpAvg[i8..], + bpAvg[(8 * (i + 1))..], + (x + i > 0) ? bpOutOdd[(8 * ((x + i - 1) & 7))..] : bpAvg[i8..], + bpOutEven[i8..], + bpOutOdd[i8..]); + } + + JxlSimdUtils.Transpose8x8Block(bpOutEven, bpOutEvenT, 8); + JxlSimdUtils.Transpose8x8Block(bpOutOdd, bpOutOddT, 8); + + for (int y = 0; y < rowsPerThread; y++) + { + // y * 8 + int y8 = y << 3; + + for (int i = 0; i < rowsPerThread; i += n) + { + int offset = y8 + i; + + Vector256 even = Vector256.LoadUnsafe(ref Unsafe.Add(ref bpOutEvenTRef, offset)); + Vector256 odd = Vector256.LoadUnsafe(ref Unsafe.Add(ref bpOutOddTRef, offset)); + + JxlSimdUtils.StoreInterleaved( + even, + odd, + ref Unsafe.Add(ref pOutRef, ((x + i) << 1) + (onerow_out * y))); + } + } + } + } + + for (int y = 0; y < rows; y++) + { + UnsqueezeRow(y0 + y, x); + } + }); + + input.Channels[c] = outputChannel; + + void UnsqueezeRow(int y, int x0) + { + Span residual = inputResidualChannel.GetRow(y); + Span average = inputChannel.GetRow(y); + Span output = outputChannel.GetRow(y); + int inputChannelWidth = inputChannel.Width; + int outputChannelWidth = outputChannel.Width; + + for (int x = x0; x < inputResidualChannel.Width; x++) + { + int xLsh1 = x << 1; // Prevents left shifting three times. Saves on CPU cycles. + + int diffMinusTendency = residual[x]; + int avg = average[x]; + int nextAverage = x + 1 < inputChannelWidth ? average[x + 1] : avg; + + int left = x > 0 ? output[xLsh1 - 1] : avg; + int tendency = SmoothTendency(left, avg, nextAverage); + int diff = diffMinusTendency + tendency; + + int a = avg + (diff / 2); + output[xLsh1] = a; + + int b = a - diff; + output[xLsh1 + 1] = b; + } + + if ((outputChannelWidth & 1) > 0) + { + output[outputChannelWidth - 1] = average[inputChannelWidth - 1]; + } + } + } + + public static void InverseVerticalSqueeze(Configuration configuration, JxlModularImage input, int c, int rc) + { + // Channel offsets should not overflow. + DebugGuard.MustBeLessThan(c, input.Channels.Count, nameof(c)); + DebugGuard.MustBeLessThan(rc, input.Channels.Count, nameof(c)); + + JxlModularChannel inputChannel = input.Channels[c]; + JxlModularChannel inputResidualChannel = input.Channels[rc]; + + if (inputChannel.Height != JxlMath.DivCeil(inputChannel.Height + inputResidualChannel.Height, 2)) + { + throw new InvalidOperationException("Invalid height"); + } + + if (inputChannel.Width != inputResidualChannel.Width) + { + throw new InvalidOperationException("Width of the input channel must be equal to the width of the residual channel"); + } + + if (inputResidualChannel.Height == 0) + { + input.Channels[c].VerticalShift--; + return; + } + + // Do not dispose. + JxlModularChannel outputChannel = new( + configuration, + inputChannel.Width, + inputChannel.Height + inputResidualChannel.Height, + inputChannel.HorizontalShift, + inputChannel.VerticalShift - 1); + + if (inputResidualChannel.Width == 0) + { + input.Channels[c] = outputChannel; + return; + } + + // The number of columns a single parallel iteration computes + // is stored here. + const int colsPerThread = 8; + + _ = Parallel.For(0, JxlMath.DivCeil(inputChannel.Width, colsPerThread), configuration.GetParallelOptions(), idx => + { + int x0 = idx * colsPerThread; + int x1 = Math.Min((idx + 1) * colsPerThread, inputChannel.Width); + int w = x1 - x0; + + for (int y = 0; y < inputResidualChannel.Height; y++) + { + int yLsh1 = y << 1; + + Span pResidual = inputResidualChannel.GetRow(y)[x0..]; + Span pAverage = inputChannel.GetRow(y)[x0..]; + Span pNAvg = inputChannel.GetRow(y + 1 < inputChannel.Height ? y + 1 : y)[x0..]; + Span pOut = outputChannel.GetRow(yLsh1)[x0..]; + Span pNOut = outputChannel.GetRow(yLsh1 + 1)[x0..]; + Span pPOut = y > 0 ? outputChannel.GetRow(yLsh1 - 1)[x0..] : pNAvg; + int x = 0; + + for (; x + 7 < w; x += 8) + { + FastUnsqueeze( + pResidual[x..], + pAverage[x..], + pNAvg[x..], + pPOut[x..], + pOut[x..], + pNOut[x..]); + } + + // Remainder + for (; x < w; x++) + { + int avg = pNAvg[x]; + int nextAvg = pNAvg[x]; + int top = pPOut[x]; + int tendency = SmoothTendency(top, avg, nextAvg); + int diffMinusTendency = pResidual[x]; + int diff = diffMinusTendency + tendency; + int output = avg + (diff >> 1); + pOut[x] = output; + pNOut[x] = output - diff; + } + } + }); + + if ((outputChannel.Height & 1) > 0) + { + int y = inputChannel.Height - 1; + + Span pAverage = inputChannel.GetRow(y); + Span pOutput = outputChannel.GetRow(y << 1); + + for (int x = 0; x < inputChannel.Width; x++) + { + pOutput[x] = pAverage[x]; + } + } + + input.Channels[c] = outputChannel; + } + + public static void InverseSqueeze(Configuration configuration, JxlModularImage input, Span parameters) + { + int totalNumberOfChannels = input.Channels.Count; + + for (int i = parameters.Length - 1; i >= 0; i--) + { + ref JxlSqueezeParameters parameter = ref parameters[i]; + + CheckMetaSqueezeParameters(parameter, totalNumberOfChannels); + + bool horizontal = parameter.Horizontal; + bool inPlace = parameter.InPlace; + int beginC = parameter.BeginC; + int endC = parameter.BeginC + parameter.NumC - 1; + + int offset = inPlace + ? endC + 1 + : totalNumberOfChannels + beginC + endC - 1; + + if (beginC < input.MetaChannels) + { + if (input.MetaChannels <= parameter.NumC) + { + throw new InvalidOperationException("Not enough meta channels"); + } + + input.MetaChannels -= parameter.NumC; + } + + for (int c = beginC; c <= endC; c++) + { + int rc = offset + c - beginC; + + if (rc >= totalNumberOfChannels) + { + throw new InvalidOperationException("Residual channel offset out of bounds"); + } + + JxlModularChannel channelC = input.Channels[c]; // Input channel + JxlModularChannel channelRC = input.Channels[rc]; // Residual channel + + if (channelC.Width < channelRC.Width || channelC.Height < channelRC.Height) + { + throw new InvalidOperationException("Input channel width or height does not match residual channel width/height"); + } + + if (horizontal) + { + InverseHorizontalSqueeze(configuration, input, c, rc); + } + else + { + InverseVerticalSqueeze(configuration, input, c, rc); + } + } + } + } + + public static void DefaultSqueezeParameters(List squeezeParameters, JxlModularImage image) + { + int numberOfChannels = image.Channels.Count - image.MetaChannels; + squeezeParameters.Clear(); + + JxlModularChannel numMetaChannelsChannel = image.Channels[image.MetaChannels]; + int w = numMetaChannelsChannel.Width; + int h = numMetaChannelsChannel.Height; + bool wide = w > h; + + JxlModularChannel nextNumMetaChannelsChannel = image.Channels[image.MetaChannels + 1]; + + if (numberOfChannels > 2 && nextNumMetaChannelsChannel.Width == w && nextNumMetaChannelsChannel.Height == h) + { + JxlSqueezeParameters parameters = new() + { + Horizontal = true, + InPlace = false, + BeginC = image.MetaChannels + 1, + NumC = 2 + }; + + squeezeParameters.Add(parameters); + parameters.Horizontal = false; + squeezeParameters.Add(parameters); + } + + JxlSqueezeParameters newParameters = new() + { + BeginC = image.MetaChannels, + NumC = numberOfChannels, + InPlace = true + }; + + if (!wide) + { + if (h > MaxFirstPreviewSize) + { + newParameters.Horizontal = false; + squeezeParameters.Add(newParameters); + h = (h + 1) >> 1; + } + } + + while (w > MaxFirstPreviewSize || h > MaxFirstPreviewSize) + { + if (w > MaxFirstPreviewSize) + { + newParameters.Horizontal = true; + squeezeParameters.Add(newParameters); + w = (w + 1) >> 1; + } + + if (w > MaxFirstPreviewSize) + { + newParameters.Horizontal = false; + squeezeParameters.Add(newParameters); + h = (h + 1) >> 1; + } + } + } + + private static void CheckMetaSqueezeParameters(in JxlSqueezeParameters parameter, int numChannels) + { + int c1 = parameter.BeginC; + int c2 = parameter.BeginC + parameter.NumC - 1; + + if (c1 < 0 || + c1 >= numChannels || + c2 < 0 || + c2 >= numChannels || + c2 < c1) + { + throw new InvalidOperationException("Invalid channel range"); + } + } + + public static void MetaSqueeze(Configuration configuration, JxlModularImage image, List parameters) + { + if (parameters.Count == 0) + { + DefaultSqueezeParameters(parameters, image); + } + + foreach (JxlSqueezeParameters parameter in parameters) + { + CheckMetaSqueezeParameters(parameter, image.Channels.Count); + + bool horizontal = parameter.Horizontal; + bool inPlace = parameter.InPlace; + int beginC = parameter.BeginC; + int endC = parameter.BeginC + parameter.NumC - 1; + + if (beginC < image.MetaChannels) + { + if (endC >= image.MetaChannels) + { + throw new InvalidOperationException("Invalid squeeze: mix of meta and nonmeta channels"); + } + + if (!inPlace) + { + throw new InvalidOperationException("Invalid squeeze: meta channels require in-place residuals"); + } + + image.MetaChannels += parameter.NumC; + } + + int offset = inPlace + ? endC + 1 + : image.Channels.Count; + + for (int c = beginC; c <= endC; c++) + { + JxlModularChannel channel = image.Channels[c]; + + if (channel.Height > 30 || channel.VerticalShift > 30) + { + throw new InvalidOperationException("Too many squeezes: shift > 30"); + } + + int w = channel.Width; + int h = channel.Height; + + if ((w & h) == 0) // either w, or h, is 0 + { + throw new InvalidOperationException("Squeezing empty channel"); + } + + if (horizontal) + { + channel.Width = (w + 1) >> 1; + + if (channel.HorizontalShift >= 0) + { + channel.HorizontalShift++; + } + + w -= (w + 1) >> 1; + } + else + { + channel.HorizontalShift = (h + 1) >> 1; + + if (channel.VerticalShift >= 0) + { + channel.VerticalShift++; + } + + h -= (h + 1) >> 1; + } + + channel.Shrink(configuration); + + JxlModularChannel placeholder = new(configuration, w, h, channel.HorizontalShift, channel.VerticalShift) + { + Component = channel.Component + }; + + image.Channels.Insert(offset + (c - beginC), placeholder); + } + } + } + + public static void ForwardHorizontalSqueeze(Configuration configuration, JxlModularImage input, int c, int rc) + { + JxlModularChannel inputChannel = input.Channels[c]; + + // Do not dispose these. + JxlModularChannel outputChannel = new(configuration, (inputChannel.Width + 1) >> 1, inputChannel.Height, inputChannel.HorizontalShift + 1, inputChannel.VerticalShift); + JxlModularChannel outputChannelResidual = new(configuration, inputChannel.Width - outputChannel.Width, outputChannel.Height, inputChannel.HorizontalShift + 1, inputChannel.VerticalShift); + + outputChannel.Component = inputChannel.Component; + outputChannelResidual.Component = inputChannel.Component; + + for (int y = 0; y < outputChannel.Height; y++) + { + Span pIn = inputChannel.GetRow(y); + Span pOut = outputChannel.GetRow(y); + Span pRes = outputChannelResidual.GetRow(y); + + for (int x = 0; x < outputChannelResidual.Width; x++) + { + int x2 = x << 1; // x * 2 + + int a = pIn[x2]; + int b = pIn[x2 + 1]; + int avg = Numerics.Average(a, b); + pOut[x] = avg; + int diff = a - b; + int nextAvg = avg; + + if (x + 1 < outputChannelResidual.Width) + { + int c2 = pIn[x2 + 2]; // actually C, but 1. variable 'c' already defined 2. names should be camelCase + int d = pIn[x2 + 3]; + + nextAvg = Numerics.Average(c2, d); + } + else if ((inputChannel.Width & 1) != 0) + { + nextAvg = pIn[x2 + 2]; + } + + int left = x > 0 ? pIn[x2 - 1] : avg; + int tendency = SmoothTendency(left, avg, nextAvg); + + pRes[x] = diff - tendency; + } + + if ((inputChannel.Width & 1) != 0) + { + int x = outputChannel.Width - 1; + pOut[x] = pIn[x * 2]; + } + } + + input.Channels[c] = outputChannel; + input.Channels.Insert(rc, outputChannelResidual); + } + + public static void ForwardVerticalSqueeze(Configuration configuration, JxlModularImage input, int c, int rc) + { + JxlModularChannel inputChannel = input.Channels[c]; + + // Do not dispose these. + JxlModularChannel outputChannel = new(configuration, inputChannel.Width, (inputChannel.Height + 1) >> 1, inputChannel.HorizontalShift, inputChannel.VerticalShift + 1); + JxlModularChannel outputResidualChannel = new(configuration, inputChannel.Width, inputChannel.Height - outputChannel.Height, inputChannel.HorizontalShift, inputChannel.VerticalShift + 1); + + outputChannel.Component = inputChannel.Component; + outputResidualChannel.Component = inputChannel.Component; + + int oneRowInput = inputChannel.Plane.PixelsPerRow; + + for (int y = 0; y < outputChannel.Height; y++) + { + Span pIn = inputChannel.GetRow(y * 2); + Span pOut = outputChannel.GetRow(y); + Span pResidual = outputResidualChannel.GetRow(y); + + for (int x = 0; x < outputChannel.Width; x++) + { + int a = pIn[x]; + int b = pIn[x + oneRowInput]; + int avg = Numerics.Average(a, b); + pOut[x] = avg; + int diff = a - b; + int nextAvg = avg; + + if (y + 1 < outputResidualChannel.Height) + { + int c2 = pIn[x + (2 * oneRowInput)]; // actually C, but 1. variable 'c' already defined 2. names should be camelCase + int d = pIn[x + (3 * oneRowInput)]; + nextAvg = Numerics.Average(c2, d); + } + else if ((inputChannel.Height & 1) != 0) + { + nextAvg = pIn[x + (2 * oneRowInput)]; + } + + int top = y > 0 ? pIn[x - oneRowInput] : avg; + int tendency = SmoothTendency(top, avg, nextAvg); + + pResidual[x] = diff - tendency; + } + } + + if ((inputChannel.Height & 1) != 0) + { + int y = outputChannel.Height - 1; + + Span pIn = inputChannel.GetRow(y * 2); + Span pOut = outputChannel.GetRow(y); + + for (int x = 0; x < outputChannel.Width; x++) + { + pOut[x] = pIn[x]; + } + } + + input.Channels[c] = outputChannel; + input.Channels.Insert(rc, outputResidualChannel); + } + + public static void ForwardSqueeze(Configuration configuration, JxlModularImage input, List parameters) + { + if (parameters.Count == 0) + { + DefaultSqueezeParameters(parameters, input); + + if (parameters.Count == 0) + { + // If there's nothing to do, don't squeeze. + return; + } + } + + foreach (JxlSqueezeParameters parameter in parameters) + { + CheckMetaSqueezeParameters(parameter, input.Channels.Count); + + bool horizontal = parameter.Horizontal; + bool inPlace = parameter.InPlace; + int beginC = parameter.BeginC; + int endC = parameter.BeginC + parameter.NumC - 1; + + int offset = inPlace + ? endC + 1 + : input.Channels.Count; + + for (int c = beginC; c <= endC; c++) + { + if (horizontal) + { + ForwardHorizontalSqueeze(configuration, input, c, offset + c - beginC); + } + else + { + ForwardVerticalSqueeze(configuration, input, c, offset + c - beginC); + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueezeParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueezeParameters.cs index 7bb52fe5e5..1d4c332770 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueezeParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueezeParameters.cs @@ -8,12 +8,12 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; /// /// Parameters for the squeeze transform. /// -internal sealed class JxlSqueezeParameters : IJxlFields +internal struct JxlSqueezeParameters : IJxlFields { private bool horizontal; private bool inPlace; - private uint beginC; - private uint numC; + private int beginC; + private int numC; public JxlSqueezeParameters() => JxlBundle.Init(this); @@ -22,7 +22,7 @@ internal sealed class JxlSqueezeParameters : IJxlFields /// public bool Horizontal { - get => this.horizontal; + readonly get => this.horizontal; set => this.horizontal = value; } @@ -31,19 +31,19 @@ public bool Horizontal /// public bool InPlace { - get => this.inPlace; + readonly get => this.inPlace; set => this.inPlace = value; } - public uint BeginC + public int BeginC { - get => this.beginC; + readonly get => this.beginC; set => this.beginC = value; } - public uint NumC + public int NumC { - get => this.numC; + readonly get => this.numC; set => this.numC = value; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs index 860adb4692..75ba986086 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs @@ -36,4 +36,29 @@ public static void CheckEqualChannels(JxlModularImage image, int c1, int c2) } } } + + public static void ComputeMinMax(JxlModularChannel channel, out int min, out int max) + { + // Start with opposite bounds so the first iteration + // guarantees to set these values + min = int.MaxValue; + max = int.MinValue; + + for (int y = 0; y < channel.Height; y++) + { + Span p = channel.GetRow(y); + for (int x = 0; x < channel.Width; x++) + { + if (p[x] < min) + { + min = p[x]; + } + + if (p[x] > max) + { + max = p[x]; + } + } + } + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs new file mode 100644 index 0000000000..56f52fab8a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs @@ -0,0 +1,109 @@ +// 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.ImageTypes; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +/// +/// Edge Preserving Filter (type 0) stage +/// +internal sealed class Epf0Stage : RenderPipelineStageBase +{ + private static readonly int[][] SadOffsets = + [ + [-2, 0], [-1, -1], [-1, 0], [-1, 1], [0, -2], [0, -1], + [0, 1], [0, 2], [1, -1], [1, 0], [1, 1], [2, 0] + ]; + + private readonly JxlLoopFilter loopFilter; + private readonly JxlImageF sigma; + + public Epf0Stage(JxlLoopFilter loopFilter, JxlImageF sigma, Configuration configuration) : base(configuration) + { + this.loopFilter = loopFilter; + this.sigma = sigma; + this.Settings = RenderPipelineStageConfiguration.CreateSymmetricBorderOnly(3); + } + + public override string Name => "EPF0"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void AddPixel( + int row, + InlineArray7>> rows, + int x, + Vector256 sad, + Vector256 inverseSigma, + ref Vector256 xOut, + ref Vector256 yOut, + ref Vector256 bOut, + ref Vector256 wOut) + { + int rowPlus3 = row + 3; + Vector256 cx = Vector256.Create(rows[0][rowPlus3].Span[x..]); + Vector256 cy = Vector256.Create(rows[1][rowPlus3].Span[x..]); + Vector256 cb = Vector256.Create(rows[2][rowPlus3].Span[x..]); + Vector256 weight = EpfUtils.Weight(sad, inverseSigma); + wOut += weight; + xOut += (weight * cx) + xOut; + yOut += (weight * cy) + yOut; + bOut += (weight * cb) + bOut; + } + + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + Span> sads = stackalloc Vector256[16].Slice(0, 12); + sads.Clear(); + + int xStart = -JxlMath.RoundUpTo(xExtraLeft, Vector256.Count); + int xEnd = width + xExtraRight; + Span rowSigma = this.sigma.GetRow((yPos / JxlFrameDimensions.BlockDimensions) + JxlDecoderCache.SigmaPadding); + + float sm = this.loopFilter.EpfPass0SigmaScale * 1.65f; + float bsm = sm * this.loopFilter.EpfBorderSadMul; + + Span sadMulCenter = [bsm, sm, sm, sm, sm, sm, sm, bsm]; + Span sadMulBorder = [bsm, bsm, bsm, bsm, bsm, bsm, bsm, bsm]; + + int yPosModBlockDim = yPos % JxlFrameDimensions.BlockDimensions; + Span sadMul = yPosModBlockDim is 0 or JxlFrameDimensions.BlockDimensions - 1 + ? sadMulBorder + : sadMulCenter; + + InlineArray3>> rows = default; + for (int c = 0; c < 3; c++) + { + for (int i = 0; i < 7; i++) + { + rows[c][i] = this.GetInputRowMemory(inputRows, c, i - 3); + } + } + + for (int x = xStart; x < xEnd; x += Vector256.Count) + { + int xPlusXpos = x + xPos; + + int bx = (xPlusXpos + (JxlDecoderCache.SigmaPadding * JxlFrameDimensions.BlockDimensions)) / JxlFrameDimensions.BlockDimensions; + int ix = xPlusXpos % JxlFrameDimensions.BlockDimensions; + + if (rowSigma[bx] < JxlLoopFilter.MinimumSigma) + { + for (int c = 0; c < 3; c++) + { + Vector256 px = Vector256.Create(rows[c][3].Span[x..]); + px.CopyTo(GetOutputRow(outputRows, c, 0)[x..]); + } + + continue; + } + + Vector256 vsm = Vector256.Create(sadMul[ix..]); + Vector256 inverseSigma = Vector256.Create(rowSigma[bx]) * vsm; + + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/EpfStageType.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/EpfStageType.cs new file mode 100644 index 0000000000..8a85367158 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/EpfStageType.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +/// +/// Used by the EPF render pipeline stage. +/// +internal enum EpfStageType : byte +{ + Zero, + One, + Two +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/EpfUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/EpfUtils.cs new file mode 100644 index 0000000000..52150ae27f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/EpfUtils.cs @@ -0,0 +1,22 @@ +// 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.RenderPipeline; + +/// +/// Utilities for EPF stages. +/// +internal static class EpfUtils +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Weight(Vector256 sad, Vector256 inverseSigma) + { + Vector256 v = (sad * inverseSigma) + Vector256.One; + Vector256 whereNegative = Vector256.LessThan(v, Vector256.Zero); + Vector256 zeroIfNegative = Vector256.ConditionalSelect(whereNegative, Vector256.Zero, whereNegative); + return zeroIfNegative; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineChannelMode.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineChannelMode.cs new file mode 100644 index 0000000000..4d936af8f2 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineChannelMode.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +/// +/// Specifies how does a render pipeline stage apply to channels. +/// +internal enum RenderPipelineChannelMode : byte +{ + /// + /// Channel is not modified. + /// + Ignored, + + /// + /// Channel is in-place. + /// + InPlace, + + /// + /// Channel is modified and written to a new buffer. + /// + InOut, + + /// + /// Read-only channel. + /// + Input +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs new file mode 100644 index 0000000000..b02b62db7a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs @@ -0,0 +1,92 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +/// +/// Base class for a render pipeline stage. +/// +[DebuggerDisplay($"{{{nameof(Name)}}}")] +internal abstract class RenderPipelineStageBase(Configuration configuration) : IDisposable +{ + private const int RenderPipelineXOffset = 32; + + /// + /// Gets or sets the configuration for this render pipeline stage. + /// + public RenderPipelineStageConfiguration Settings { get; set; } + + /// + /// Gets a value indicating whether this stage is initialized and is therefore + /// ready to use. + /// + public virtual bool IsInitialized => true; + + /// + /// Gets a value indicating whether, from this stage on, the pipeline will operate + /// on an image rather than the frame-sized buffer. Only one stage in the pipeline + /// should return true, and it should implement . + /// + public virtual bool SwitchToImageDimensions => false; + + /// + /// Gets a friendly name representing this stage. + /// + public virtual string Name => "(invalid pipeline stage)"; + + /// + /// If any unmanaged or pooled memory is present by the derived stage, releases + /// memory used by that. + /// + public virtual void Dispose() + { + } + + public virtual void ProcessRow( + Buffer2D> inputRows, + Buffer2D> outputRows, + int xExtraLeft, + int xExtraRight, + int width, + int xPos, + int yPos) + { + } + + /// + /// Represents how each channel will be processed. + /// + /// Desired channel. + /// Mode specifying how the specified channel will be processed. + public virtual RenderPipelineChannelMode GetChannelMode(int channel) + => RenderPipelineChannelMode.Ignored; + + public virtual void SetInputSizes(Span inputSizes) + { + } + + public Span GetInputRow(Buffer2D> inputRows, int c, int offset) + => inputRows[c, this.Settings.BorderY + offset].Span[RenderPipelineXOffset..]; + + public Memory GetInputRowMemory(Buffer2D> inputRows, int c, int offset) + => inputRows[c, this.Settings.BorderY + offset][RenderPipelineXOffset..]; + + public static Span GetOutputRow(Buffer2D> outputRows, int c, int offset) + => outputRows[c, offset].Span[RenderPipelineXOffset..]; + + public virtual void GetImageDimensions(out int width, out int height, out Point frameOrigin) + { + width = 0; + height = 0; + frameOrigin = default; + } + + public virtual void ProcessPaddingRow(Buffer2D> outputRows, int width, int xPos, int yPos) + { + } + + protected Configuration GetConfiguration() => configuration; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageConfiguration.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageConfiguration.cs new file mode 100644 index 0000000000..acd7b8b765 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageConfiguration.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal record struct RenderPipelineStageConfiguration(int BorderX, int BorderY, int ShiftX, int ShiftY) +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RenderPipelineStageConfiguration CreateShiftX(int shift, int border) => new(border, 0, shift, 0); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RenderPipelineStageConfiguration CreateShiftY(int shift, int border) => new(0, border, 0, shift); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RenderPipelineStageConfiguration CreateSymmetric(int shift, int border) => new(border, border, shift, shift); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RenderPipelineStageConfiguration CreateSymmetricBorderOnly(int border) => CreateSymmetric(shift: 0, border); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegment.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegment.cs index 65f2177927..fd9a068edd 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegment.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineSegment.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; internal struct JxlSplineSegment diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 971d73b849..b3925b7a7c 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -44,6 +44,11 @@ + + True + True + JxlSimdUtils.StoreInterleaved.tt + @@ -57,6 +62,11 @@ True InlineArray.tt + + True + True + JxlSimdUtils.StoreInterleaved.tt + True True @@ -164,6 +174,10 @@ TextTemplatingFileGenerator InlineArray.cs + + TextTemplatingFileGenerator + JxlSimdUtils.StoreInterleaved.Generated.cs + ImageMetadataExtensions.cs TextTemplatingFileGenerator diff --git a/tests/ImageSharp.Tests/Common/Vector256UtilitiesTests.cs b/tests/ImageSharp.Tests/Common/Vector256UtilitiesTests.cs new file mode 100644 index 0000000000..1c294afa44 --- /dev/null +++ b/tests/ImageSharp.Tests/Common/Vector256UtilitiesTests.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Tests.Common; + +public class Vector256UtilitiesTests +{ + [Theory] + [InlineData(new int[] { 4, 5, 6, 7, 8, 9, 10, 11 }, new int[] { 1, 2, 3, 4, 0, -1, -2, -3 }, new int[] { 4, 1, 5, 2, 6, 3, 7, 4 })] + public void TestVector256InterleaveLower(int[] a, int[] b, int[] expected) + { + Vector256 v256a = Vector256.Create(a); + Vector256 v256b = Vector256.Create(b); + + Vector256 v256 = Vector256_.InterleaveLower(v256a, v256b); + + int[] result = new int[Vector256.Count]; + v256.CopyTo(result); + + bool isEqual = expected.SequenceEqual(result); + if (!isEqual) + { + Assert.Fail($"Lower shuffle failed.\n\nExpected: [{string.Join(", ", expected)}]\nActual: [{string.Join(", ", result)}]"); + } + } + + [Theory] + [InlineData(new int[] { 4, 5, 6, 7, 8, 9, 10, 11 }, new int[] { 1, 2, 3, 4, 0, -1, -2, -3 }, new int[] { 8, 0, 9, -1, 10, -2, 11, -3 })] + public void TestVector256InterleaveUpper(int[] a, int[] b, int[] expected) + { + Vector256 v256a = Vector256.Create(a); + Vector256 v256b = Vector256.Create(b); + + Vector256 v256 = Vector256_.InterleaveUpper(v256a, v256b); + + int[] result = new int[Vector256.Count]; + v256.CopyTo(result); + + bool isEqual = expected.SequenceEqual(result); + if (!isEqual) + { + Assert.Fail($"Lower shuffle failed.\n\nExpected: [{string.Join(", ", expected)}]\nActual: [{string.Join(", ", result)}]"); + } + } +} From 7b8749d8dce01f002da88671cc04208ef229774e Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:31:47 +0400 Subject: [PATCH 101/142] Don't use stackalloc for two bytes This avoids the stack cookie --- src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 5578f47534..a057a270ff 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -1505,7 +1505,7 @@ public void ReadBasicInfo(Stream stream) { if (!this.gotCodestreamSignature) { - Span fileSignature = stackalloc byte[2]; + Span fileSignature = [0, 0]; stream.ReadExactly(fileSignature); if (fileSignature[0] != 0xFF || fileSignature[1] != JxlShared.CodestreamMarker) From 15c32fe1a25cbad160a62bf2df8b9194d30ab8e5 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:33:11 +0400 Subject: [PATCH 102/142] Prefer Math.DivRem --- .../Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index a057a270ff..7fb1775e08 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -1530,9 +1530,10 @@ public void ReadBasicInfo(Stream stream) long totalBits = bitReader.TotalBitsConsumed; - this.AdvanceCodeStream(totalBits / JxlMath.BitsPerByte); + (long div, long rem) = Math.DivRem(totalBits, JxlMath.BitsPerByte); + this.AdvanceCodeStream(div); - this.codestreamBitsAhead = totalBits % JxlMath.BitsPerByte; + this.codestreamBitsAhead = rem; this.gotBasicInfo = true; this.basicInfoSizeHint = 0; this.imageMetadata = this.metadata.ImageMetadata; From de8c6443354540adbc98c73588a2a97447a89cfc Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:35:19 +0400 Subject: [PATCH 103/142] Use Math.DivRem --- .../Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 7fb1775e08..8dcd47711b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -1565,8 +1565,10 @@ public void ReadAllHeaders() } long totalBits = reader.TotalBitsConsumed; - this.AdvanceCodeStream(totalBits / JxlMath.BitsPerByte); - this.codestreamBitsAhead = totalBits % JxlMath.BitsPerByte; + (long div, long rem) = Math.DivRem(totalBits, JxlMath.BitsPerByte); + + this.AdvanceCodeStream(div); + this.codestreamBitsAhead = rem; this.gotTransformData = true; } From a5d5ab6c8ba493ebd9b6bf2144f681f40999490d Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:35:51 +0400 Subject: [PATCH 104/142] Prefer byte --- .../Formats/Jxl/Processing/Decoder/JxlSectionStatus.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionStatus.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionStatus.cs index 88c4f1068f..eefcf9ad90 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionStatus.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlSectionStatus.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; /// /// Status of processing a section. /// -internal enum JxlSectionStatus +internal enum JxlSectionStatus : byte { /// /// Processed normally. From 1b6e824f6d8e77b0bfad5aef1450aeed0d0a9864 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:39:47 +0400 Subject: [PATCH 105/142] Don't use GetWReference (it should return span) --- .../Encoding/ContextPrediction/JxlContextPrediction.cs | 4 +--- .../Modular/Encoding/ContextPrediction/JxlModularHeader.cs | 6 +++--- .../Modular/Encoding/ContextPrediction/JxlModularState.cs | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs index fcbef7afd7..f856ef0e39 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlContextPrediction.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; @@ -17,8 +16,7 @@ internal static class JxlContextPrediction public static void SetPredictorMode(int i, JxlModularHeader header) { - ref uint wr = ref header.GetWReference(); - Span w = MemoryMarshal.CreateSpan(ref wr, 4); + Span w = header.GetW(); switch (i) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs index 4db1a00dc9..04a50fe270 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularHeader.cs @@ -101,10 +101,10 @@ public int P3Ce } /// - /// Returns a reference to the first w item. + /// Returns a span to the w array. /// - /// Reference to w[0] - public ref uint GetWReference() => ref this.w[0]; + /// Reference to w + public Span GetW() => this.w; public bool Visit(JxlVisitor v) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs index f731dc9677..7e52a62e57 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs @@ -109,13 +109,13 @@ public long Predict(bool computeProperties, int x, int y, int width, long n, lon int posNW = x > 0 ? posN - 1 : posN; Span weights = stackalloc uint[4]; - ref uint headerW = ref this.header.GetWReference(); + Span headerW = this.header.GetW(); for (int i = 0; i < 4; i++) { Span error = this.predErrors[i].AsSpan(); weights[i] = error[posN] + error[posNE] + error[posNW]; - weights[i] = ErrorWeight((int)weights[i], Unsafe.Add(ref headerW, i)); + weights[i] = ErrorWeight((int)weights[i], headerW[i]); } n = AddBits(n); From c6c69428222540d90ff0b521bd581d7489565c7b Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:41:13 +0400 Subject: [PATCH 106/142] Parenthesize --- .../Modular/Encoding/ContextPrediction/JxlModularState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs index 7e52a62e57..76aceefbcd 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs @@ -72,7 +72,7 @@ public static uint ErrorWeight(int x, uint maxWeight) public static long WeightedAverage(Span p, Span w) { - uint weightSum = w[0] + w[1] + w[2] + w[3]; + uint weightSum = (w[0] + w[1]) + (w[2] + w[3]); if (weightSum <= 15) { From 79b0113c280f053f09e5a7dd5e4d9a0d1ad004b7 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:42:09 +0400 Subject: [PATCH 107/142] Don't use stackalloc --- .../Modular/Encoding/ContextPrediction/JxlModularState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs index 76aceefbcd..2b112ff3cc 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Encoding/ContextPrediction/JxlModularState.cs @@ -108,7 +108,7 @@ public long Predict(bool computeProperties, int x, int y, int width, long n, lon int posNE = x < width - 1 ? posN + 1 : posN; int posNW = x > 0 ? posN - 1 : posN; - Span weights = stackalloc uint[4]; + Span weights = [0, 0, 0, 0]; Span headerW = this.header.GetW(); for (int i = 0; i < 4; i++) From e7d3608b3dbe1cb65fc6ae4f7af53e2c7548472e Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:45:33 +0400 Subject: [PATCH 108/142] Remove SuppressFinalize from sealed class --- src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs index 8e74c26b72..4f311f8982 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs @@ -55,9 +55,5 @@ public unsafe JxlDctReadOnlyAcPointer GetReadOnlyPlaneRow(int channel, int y, in } } - public void Dispose() - { - this.image.Dispose(); - GC.SuppressFinalize(this); - } + public void Dispose() => this.image.Dispose(); } From e6ed4baefd76de2e4ea479914dde49c09992a709 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:46:24 +0400 Subject: [PATCH 109/142] Use Math.Clamp --- src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs index 54a41f99ab..3b7500fd05 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs @@ -261,15 +261,7 @@ public void ComputeGlobalScaleAndQuant(float quantDc, float quantMedian, float q const int quantFieldTarget = 5; float scale = GlobalScaleDenominator * (quantMedian - quantMedianAbsd) / quantFieldTarget; - if (scale < 1) - { - scale = 1; - } - - if (scale > (1 << 15)) - { - scale = 1 << 15; - } + scale = Math.Clamp(scale, 1, 1 << 15); int newGlobalScale = (int)scale; int scaledQuantDc = (int)(quantDc * GlobalScaleNumerator * 1.6f); From 4858ef3783f943b718aafcef59d2794e3e4cafab Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:47:10 +0400 Subject: [PATCH 110/142] Use StartsWith --- src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs b/src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs index 9b968bd1e3..17484114ce 100644 --- a/src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs +++ b/src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs @@ -28,7 +28,7 @@ public sealed class JxlImageFormatDetector : IImageFormatDetector /// public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) { - if (header[0] == 0xFF && header[1] == 0x0A) + if (header.StartsWith([(byte)0xFF, (byte)0x0A])) { // Just codestream. format = new JxlFormat(); From 2835528a72d6d40a2da9b9c16f291501a4573bfa Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:48:41 +0400 Subject: [PATCH 111/142] Remove try/finally --- .../Decoder/JxlBoxContentDecoder.cs | 53 +++++++++---------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs index 8a37b7dbaa..93fe598304 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs @@ -59,50 +59,45 @@ public void Process(Stream stream, Stream writer) { byte[] cache = ArrayPool.Shared.Rent(16384); - try + if (this.codingMode == JxlBoxCodingMode.Brotli) { - if (this.codingMode == JxlBoxCodingMode.Brotli) - { - using BrotliStream brotli = new(stream, CompressionMode.Decompress, leaveOpen: true); + using BrotliStream brotli = new(stream, CompressionMode.Decompress, leaveOpen: true); + int bytesRead; + while ((bytesRead = brotli.Read(cache, 0, cache.Length)) > 0) + { + writer.Write(cache.AsSpan(0, bytesRead)); + } + } + else + { + if (this.boxExtendsTillEnd) + { int bytesRead; - while ((bytesRead = brotli.Read(cache, 0, cache.Length)) > 0) + while ((bytesRead = stream.Read(cache, 0, cache.Length)) > 0) { writer.Write(cache.AsSpan(0, bytesRead)); } } else { - if (this.boxExtendsTillEnd) + ulong bytesLeft = this.boxSize; + while (bytesLeft > 0) { - int bytesRead; - while ((bytesRead = stream.Read(cache, 0, cache.Length)) > 0) + int toRead = (int)Math.Min((ulong)cache.Length, bytesLeft); + int bytesRead = stream.Read(cache, 0, toRead); + + if (bytesRead == 0) { - writer.Write(cache.AsSpan(0, bytesRead)); + throw new EndOfStreamException("Unexpected EOF while reading box content"); } - } - else - { - ulong bytesLeft = this.boxSize; - while (bytesLeft > 0) - { - int toRead = (int)Math.Min((ulong)cache.Length, bytesLeft); - int bytesRead = stream.Read(cache, 0, toRead); - if (bytesRead == 0) - { - throw new EndOfStreamException("Unexpected EOF while reading box content"); - } - - writer.Write(cache.AsSpan(0, bytesRead)); - bytesLeft -= (ulong)bytesRead; - } + writer.Write(cache.AsSpan(0, bytesRead)); + bytesLeft -= (ulong)bytesRead; } } } - finally - { - ArrayPool.Shared.Return(cache); - } + + ArrayPool.Shared.Return(cache); } } From 90ddb9de23f897eb66e0ab7ef27235d19497216e Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:02:05 +0400 Subject: [PATCH 112/142] Add JPEG parsed data --- .../Jpeg/{ => Data}/JpegAppMarkerType.cs | 2 +- .../Processing/Jpeg/Data/JpegDataConstants.cs | 20 +++++++++++++++++++ .../Formats/Jxl/Processing/Jpeg/README.md | 9 +++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) rename src/ImageSharp/Formats/Jxl/Processing/Jpeg/{ => Data}/JpegAppMarkerType.cs (89%) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegDataConstants.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Jpeg/README.md diff --git a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/JpegAppMarkerType.cs b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegAppMarkerType.cs similarity index 89% rename from src/ImageSharp/Formats/Jxl/Processing/Jpeg/JpegAppMarkerType.cs rename to src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegAppMarkerType.cs index 2b05317ab4..ba121c80f2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/JpegAppMarkerType.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegAppMarkerType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Jpeg; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Jpeg.Data; /// /// Identifies the kind of APP marker in a JPEG file. diff --git a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegDataConstants.cs b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegDataConstants.cs new file mode 100644 index 0000000000..525d3f4c0a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegDataConstants.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Jpeg.Data; + +/// +/// Constants used in parsed JPEG data. +/// +internal static class JpegDataConstants +{ + /// + /// Maximum number of components. + /// + public const int MaxComponents = 4; + + /// + /// Maximum number of quantizer tables. + /// + public const int MaximumQuantizationTables = 4; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/README.md b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/README.md new file mode 100644 index 0000000000..4b98e0aff0 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/README.md @@ -0,0 +1,9 @@ +# Jxl/Processing/Jpeg +This folder contains logic to: + +- Parse and represent JPEG data +- Write JPEG markers +- JPEG to JXL +- JXL to JPEG + +This does not contain a JPEG codec. From 30599106625324ded636cf0fad48e4fc3b3d8020 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:03:23 +0400 Subject: [PATCH 113/142] Use InlineArray for color correlation --- src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs index 7047c2c7cb..4ec5258801 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlColorCorrelation.cs @@ -12,7 +12,7 @@ internal sealed class JxlColorCorrelation private float baseCorrelationX; private float baseCorrelationB = JxlOpsinConstants.YToBRatio; - private readonly float[] dcFactors = new float[4]; + private InlineArray4 dcFactors; private uint colorFactor = JxlChromaFromLuma.DefaultColorFactor; private float colorScale = 1.0f / JxlChromaFromLuma.DefaultColorFactor; From a95594607d120d211822464b91be0c4864936fbf Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:11:16 +0400 Subject: [PATCH 114/142] Add JPEG data constants --- .../Processing/Jpeg/Data/JpegDataConstants.cs | 85 +++++++++++++++++++ .../Formats/Jxl/Processing/Jpeg/README.md | 2 + 2 files changed, 87 insertions(+) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegDataConstants.cs b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegDataConstants.cs index 525d3f4c0a..7dd73a50c1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegDataConstants.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegDataConstants.cs @@ -17,4 +17,89 @@ internal static class JpegDataConstants /// Maximum number of quantizer tables. /// public const int MaximumQuantizationTables = 4; + + /// + /// Maximum number of Huffman code tables. + /// + public const int MaxHuffmanTables = 4; + + /// + /// Maximum number of bits for a Huffman code. + /// + public const int JpegHuffmanMaxBitLength = 16; + + /// + /// Alphabet size for Huffman tables used in the JPEG format. + /// + public const int JpegHuffmanAlphabetSize = 256; + + /// + /// Alphabet size for Huffman tables used in the JPEG format. + /// + /// + /// This is specific to the DC coefficients of the Discrete + /// Cosine Transform. + /// + public const int JpegDcAlphabetSize = 12; + + /// + /// Maximum number of DHT "Define Huffman Tables" markers. + /// + public const int MaxDhtMarkers = 512; + + /// + /// Largest value for width OR height. + /// + public const int MaxDimPixels = 65535; + + /// + /// Marker that specifies APP1. + /// + public const int App1 = 0xE1; + + /// + /// Marker that specifies APP2. + /// + public const int App2 = 0xE2; + + /// + /// Gets the tag bytes specifying the ICC profile. + /// + public static ReadOnlySpan IccProfileTag => "ICC_PROFILE\0"u8; + + /// + /// Gets the tag bytes specifying the EXIF profile. + /// + public static ReadOnlySpan ExifTag => "Exif\0\0"u8; + + /// + /// Gets the tag bytes specifying the XMP profile. + /// + public static ReadOnlySpan XmpTag => "http://ns.adobe.com/xap/1.0/\0"u8; + + public static ReadOnlySpan JpegNaturalOrder => + [ + 0, 1, 8, 16, 9, 2, 3, 10, + 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, + 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, + 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, + 53, 60, 61, 54, 47, 55, 62, 63, + 63, 63, 63, 63, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63 + ]; + + public static ReadOnlySpan JpegZigZagOrder => + [ + 0, 1, 5, 6, 14, 15, 27, 28, + 2, 4, 7, 13, 16, 26, 29, 42, + 3, 8, 12, 17, 25, 30, 41, 43, + 9, 11, 18, 24, 31, 40, 44, 53, + 10, 19, 23, 32, 39, 45, 52, 54, + 20, 22, 33, 38, 46, 51, 55, 60, + 21, 34, 37, 47, 50, 56, 59, 61, + 35, 36, 48, 49, 57, 58, 62, 63 + ]; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/README.md b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/README.md index 4b98e0aff0..bde5de389e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/README.md +++ b/src/ImageSharp/Formats/Jxl/Processing/Jpeg/README.md @@ -7,3 +7,5 @@ This folder contains logic to: - JXL to JPEG This does not contain a JPEG codec. + +Logic in this folder is used for JPEG<->JXL lossless compression. From 497c27c0d71c8f764a7f04d36fa8dcc2eb084e0e Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 1 Sep 2026 16:46:46 +0400 Subject: [PATCH 115/142] Update folder structure --- .../Jxl/IO/FrameHeader/JxlFrameHeader.cs | 1 + .../Jpeg/Data/JpegAppMarkerType.cs | 2 +- .../Jpeg/Data/JpegDataConstants.cs | 2 +- .../Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs | 29 ++++++++ .../Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs | 38 ++++++++++ .../Jxl/{Processing => IO}/Jpeg/README.md | 0 .../Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs | 2 +- src/ImageSharp/Formats/Jxl/InlineArrays.cs | 9 +++ .../{ => AcStrategy}/IJxlDctAcImage.cs | 4 +- .../{ => AcStrategy}/JxlAcContext.cs | 2 +- .../{ => AcStrategy}/JxlAcStrategy.cs | 2 +- .../{ => AcStrategy}/JxlAcStrategyImage.cs | 2 +- .../{ => AcStrategy}/JxlAcStrategyRow.cs | 2 +- .../{ => AcStrategy}/JxlAcStrategyType.cs | 2 +- .../JxlAlphaBlendingInputLayer.cs | 2 +- .../{ => Blending}/JxlAlphaBlendingOutput.cs | 2 +- .../{ => Blending}/JxlAlphaHelper.cs | 2 +- .../Jxl/Processing/Butteraugli/Butteraugli.cs | 1 + .../Jxl/Processing/{ => Dct}/JxlDct.cs | 2 +- .../Processing/{ => Dct}/JxlDctAcImage{T}.cs | 3 +- .../Processing/{ => Dct}/JxlDctAcPointer.cs | 2 +- .../Jxl/Processing/{ => Dct}/JxlDctAcType.cs | 2 +- .../Jxl/Processing/{ => Dct}/JxlDctOutput.cs | 2 +- .../{ => Dct}/JxlDctQuantWeightParameters.cs | 2 +- .../{ => Dct}/JxlDctReadOnlyAcPointer.cs | 2 +- .../Jxl/Processing/{ => Dct}/JxlDctScales.cs | 2 +- .../Jxl/Processing/{ => Dct}/JxlDctSource.cs | 2 +- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 1 + .../Jxl/Processing/Decoder/JxlFrameDecoder.cs | 1 + .../Jxl/Processing/Decoder/JxlNoiseDecoder.cs | 1 + .../Decoder/JxlOutputEncodingInfo.cs | 1 + .../Decoder/JxlPassesDecoderState.cs | 1 + .../Processing/Decoder/JxlPatchDictionary.cs | 2 + .../Encoder/Ans/JxlHistogramParameters.cs | 1 + .../AuxiliaryOutput/JxlAuxiliaryOutput.cs | 72 +++++++++++++++++++ .../JxlAuxiliaryOutputConstants.cs | 9 +++ .../Encoder/AuxiliaryOutput/JxlLayerTotals.cs | 22 ++++++ .../Encoder/AuxiliaryOutput/JxlLayerType.cs | 23 ++++++ .../Jxl/Processing/JxlBlockContextMap.cs | 1 + .../Jxl/Processing/JxlCoefficientOrder.cs | 1 + .../Formats/Jxl/Processing/JxlConvolve.cs | 1 + .../Formats/Jxl/Processing/JxlEntropyCoder.cs | 1 + .../Jxl/Processing/JxlImageFeatures.cs | 1 + .../Formats/Jxl/Processing/JxlLoopFilter.cs | 1 + .../Processing/JxlOpsinInverseParameters.cs | 2 + .../Jxl/Processing/JxlPassesSharedState.cs | 2 + .../Formats/Jxl/Processing/JxlTranspose.cs | 2 + .../Processing/{ => Noise}/JxlNoiseHelper.cs | 2 +- .../{ => Noise}/JxlNoiseIndexAndFraction.cs | 2 +- .../Processing/{ => Noise}/JxlNoiseLevel.cs | 2 +- .../{ => Noise}/JxlNoiseParameters.cs | 2 +- .../{ => Primitives}/JxlAspectRatioHelpers.cs | 2 +- .../{ => Primitives}/JxlBitDepth.cs | 2 +- .../{ => Primitives}/JxlBitDepthType.cs | 2 +- .../{ => Primitives}/JxlDataType.cs | 2 +- .../{ => Primitives}/JxlInverseMtf.cs | 2 +- .../{ => Primitives}/JxlLehmerCode.cs | 2 +- .../{ => Primitives}/JxlMatrix3x3.cs | 2 +- .../{ => Primitives}/JxlMatrix3x3F.cs | 2 +- .../{ => Primitives}/JxlOverride.cs | 2 +- .../{ => Primitives}/JxlOverrideHelpers.cs | 2 +- .../{ => Primitives}/JxlPackSigned.cs | 2 +- .../{ => Primitives}/JxlPixelFormat.cs | 2 +- .../{ => Primitives}/JxlSpeedTier.cs | 2 +- .../{ => Primitives}/JxlWeightsSeparable5.cs | 2 +- .../{ => Primitives}/JxlWeightsSymmetric3.cs | 2 +- .../{ => Primitives}/JxlWeightsSymmetric5.cs | 2 +- .../{ => Primitives}/JxlXorShift.cs | 2 +- .../{ => Quantization}/JxlDequantMatrices.cs | 3 +- .../{ => Quantization}/JxlQuantMode.cs | 2 +- .../{ => Quantization}/JxlQuantTable.cs | 2 +- .../{ => Quantization}/JxlQuantWeights.cs | 4 +- .../{ => Quantization}/JxlQuantizer.cs | 3 +- .../JxlQuantizerConstants.cs | 2 +- .../JxlQuantizerEncoding.cs | 4 +- .../JxlQuantizerParameters.cs | 2 +- .../Processing/Splines/JxlQuantizedSpline.cs | 2 + 77 files changed, 285 insertions(+), 50 deletions(-) rename src/ImageSharp/Formats/Jxl/{Processing => IO}/Jpeg/Data/JpegAppMarkerType.cs (89%) rename src/ImageSharp/Formats/Jxl/{Processing => IO}/Jpeg/Data/JpegDataConstants.cs (97%) create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs rename src/ImageSharp/Formats/Jxl/{Processing => IO}/Jpeg/README.md (100%) rename src/ImageSharp/Formats/Jxl/Processing/{ => AcStrategy}/IJxlDctAcImage.cs (95%) rename src/ImageSharp/Formats/Jxl/Processing/{ => AcStrategy}/JxlAcContext.cs (96%) rename src/ImageSharp/Formats/Jxl/Processing/{ => AcStrategy}/JxlAcStrategy.cs (98%) rename src/ImageSharp/Formats/Jxl/Processing/{ => AcStrategy}/JxlAcStrategyImage.cs (98%) rename src/ImageSharp/Formats/Jxl/Processing/{ => AcStrategy}/JxlAcStrategyRow.cs (92%) rename src/ImageSharp/Formats/Jxl/Processing/{ => AcStrategy}/JxlAcStrategyType.cs (94%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Blending}/JxlAlphaBlendingInputLayer.cs (83%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Blending}/JxlAlphaBlendingOutput.cs (82%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Blending}/JxlAlphaHelper.cs (98%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Dct}/JxlDct.cs (99%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Dct}/JxlDctAcImage{T}.cs (94%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Dct}/JxlDctAcPointer.cs (87%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Dct}/JxlDctAcType.cs (84%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Dct}/JxlDctOutput.cs (96%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Dct}/JxlDctQuantWeightParameters.cs (92%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Dct}/JxlDctReadOnlyAcPointer.cs (91%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Dct}/JxlDctScales.cs (99%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Dct}/JxlDctSource.cs (96%) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutput.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutputConstants.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlLayerTotals.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlLayerType.cs rename src/ImageSharp/Formats/Jxl/Processing/{ => Noise}/JxlNoiseHelper.cs (92%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Noise}/JxlNoiseIndexAndFraction.cs (82%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Noise}/JxlNoiseLevel.cs (83%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Noise}/JxlNoiseParameters.cs (85%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlAspectRatioHelpers.cs (93%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlBitDepth.cs (90%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlBitDepthType.cs (94%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlDataType.cs (90%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlInverseMtf.cs (94%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlLehmerCode.cs (97%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlMatrix3x3.cs (98%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlMatrix3x3F.cs (98%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlOverride.cs (87%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlOverrideHelpers.cs (94%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlPackSigned.cs (93%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlPixelFormat.cs (93%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlSpeedTier.cs (96%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlWeightsSeparable5.cs (82%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlWeightsSymmetric3.cs (94%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlWeightsSymmetric5.cs (97%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlXorShift.cs (96%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Quantization}/JxlDequantMatrices.cs (98%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Quantization}/JxlQuantMode.cs (94%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Quantization}/JxlQuantTable.cs (90%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Quantization}/JxlQuantWeights.cs (99%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Quantization}/JxlQuantizer.cs (98%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Quantization}/JxlQuantizerConstants.cs (96%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Quantization}/JxlQuantizerEncoding.cs (98%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Quantization}/JxlQuantizerParameters.cs (95%) diff --git a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs index 43d7045041..c1cf766b0e 100644 --- a/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs +++ b/src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs @@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegAppMarkerType.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegAppMarkerType.cs similarity index 89% rename from src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegAppMarkerType.cs rename to src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegAppMarkerType.cs index ba121c80f2..15438464ee 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegAppMarkerType.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegAppMarkerType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Jpeg.Data; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; /// /// Identifies the kind of APP marker in a JPEG file. diff --git a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegDataConstants.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegDataConstants.cs similarity index 97% rename from src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegDataConstants.cs rename to src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegDataConstants.cs index 7dd73a50c1..e2c36e92ba 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/Data/JpegDataConstants.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegDataConstants.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Jpeg.Data; +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; /// /// Constants used in parsed JPEG data. diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs new file mode 100644 index 0000000000..7386bd5a3f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; + +/// +/// Representation of quantization values for an 8x8 pixel block. +/// +internal struct JpegQuantizationTable() +{ + /// + /// Quantization values + /// + public InlineArray64 Values; + + public int Precision { get; set; } + + /// + /// Gets or sets the index of the quantization table + /// as it was parsed from the input JPEG. + /// + public int Index { get; set; } + + /// + /// Gets or sets a value indicating whether this table + /// is the last one within its marker segment. + /// + public bool IsLast { get; set; } = true; +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs new file mode 100644 index 0000000000..5cff485be9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg; + +/// +/// Decodes Huffman codes in a JPEG file for JPEG to JPEG XL encoder. +/// +internal static class JpegHuffmanDecoder +{ + private const int RootTableBits = 8; + private const int LookupSize = 8; + + private static int NextTableBitSize(Span count, int length) + { + int left = 1 << (length - RootTableBits); + while (length < MaxBitLength) + { + left -= count[length]; + + if (left <= 0) + { + break; + } + + length++; + left <<= 1; + } + + return length - RootTableBits; + } + + public struct HuffmanTableEntry() + { + public byte Bits = 0; + public ushort Value = 0xFFFF; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Jpeg/README.md b/src/ImageSharp/Formats/Jxl/IO/Jpeg/README.md similarity index 100% rename from src/ImageSharp/Formats/Jxl/Processing/Jpeg/README.md rename to src/ImageSharp/Formats/Jxl/IO/Jpeg/README.md diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs index a6df675acf..346e374148 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; -using SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; diff --git a/src/ImageSharp/Formats/Jxl/InlineArrays.cs b/src/ImageSharp/Formats/Jxl/InlineArrays.cs index acb9c3d413..f61f7490ea 100644 --- a/src/ImageSharp/Formats/Jxl/InlineArrays.cs +++ b/src/ImageSharp/Formats/Jxl/InlineArrays.cs @@ -16,6 +16,15 @@ internal struct InlineArray55 private T first; } +/// +/// Used by JpegQuantizationTable +/// +[InlineArray(64)] +internal struct InlineArray64 +{ + private T first; +} + /// /// Used by JxlCustomTransformData /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/IJxlDctAcImage.cs b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/IJxlDctAcImage.cs similarity index 95% rename from src/ImageSharp/Formats/Jxl/Processing/IJxlDctAcImage.cs rename to src/ImageSharp/Formats/Jxl/Processing/AcStrategy/IJxlDctAcImage.cs index 30969824cd..6d773d7cd3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/IJxlDctAcImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/IJxlDctAcImage.cs @@ -1,7 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; /// /// Base DCT AC coefficient image diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcContext.cs similarity index 96% rename from src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs rename to src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcContext.cs index 58e5c8b4f4..61893c621b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcContext.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcContext.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; /// /// AC context diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategy.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs rename to src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategy.cs index 6da2926640..36f0b71c5b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategy.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategy.cs @@ -5,7 +5,7 @@ using System.Runtime.InteropServices; using static SixLabors.ImageSharp.Formats.Jxl.Processing.JxlFrameDimensions; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; [StructLayout(LayoutKind.Sequential, Pack = 8)] internal struct JxlAcStrategy diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyImage.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs rename to src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyImage.cs index 236d518140..875cae762f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyImage.cs @@ -3,7 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; internal sealed class JxlAcStrategyImage : IDisposable { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyRow.cs similarity index 92% rename from src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs rename to src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyRow.cs index b94c565e7b..7a9b1eeafb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyRow.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyRow.cs @@ -4,7 +4,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; internal sealed class JxlAcStrategyRow { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyType.cs b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyType.cs similarity index 94% rename from src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyType.cs rename to src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyType.cs index e1b935fa2b..317f71ccb3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAcStrategyType.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; internal enum JxlAcStrategyType : ushort { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingInputLayer.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingInputLayer.cs similarity index 83% rename from src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingInputLayer.cs rename to src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingInputLayer.cs index 4b49e32c11..9a73f659fb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingInputLayer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingInputLayer.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; internal sealed class JxlAlphaBlendingInputLayer { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingOutput.cs similarity index 82% rename from src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs rename to src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingOutput.cs index c5db8cb8a7..62296b20e7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingOutput.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; internal sealed class JxlAlphaBlendingOutput { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs rename to src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs index 641205ab21..b3c890f9a2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; internal sealed class JxlAlphaHelper { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index 2011c186b3..99f1cf2d88 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDct.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.cs similarity index 99% rename from src/ImageSharp/Formats/Jxl/Processing/JxlDct.cs rename to src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.cs index 5e3e76355f..c498c3a6d7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDct.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDct.cs @@ -4,7 +4,7 @@ using System.Numerics; using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; /// /// Discrete Cosine Transform with SIMD support. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcImage{T}.cs similarity index 94% rename from src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs rename to src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcImage{T}.cs index 4f311f8982..6d132c4128 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcImage{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcImage{T}.cs @@ -2,8 +2,9 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; internal sealed class JxlDctAcImage : IJxlDctAcImage, IDisposable where T : unmanaged diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcPointer.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcPointer.cs similarity index 87% rename from src/ImageSharp/Formats/Jxl/Processing/JxlDctAcPointer.cs rename to src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcPointer.cs index 9710712d77..732e8f4a04 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcPointer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcPointer.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; /// /// Pointer to DCT AC coefficients diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcType.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcType.cs similarity index 84% rename from src/ImageSharp/Formats/Jxl/Processing/JxlDctAcType.cs rename to src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcType.cs index a7e4ede67f..35801b9192 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctAcType.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; /// /// Bit size for DCT AC coefficient diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctOutput.cs similarity index 96% rename from src/ImageSharp/Formats/Jxl/Processing/JxlDctOutput.cs rename to src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctOutput.cs index 1be3b9adfe..5ef6218908 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctOutput.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctOutput.cs @@ -4,7 +4,7 @@ using System.Numerics; using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; /// /// Output DCT block. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctQuantWeightParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctQuantWeightParameters.cs similarity index 92% rename from src/ImageSharp/Formats/Jxl/Processing/JxlDctQuantWeightParameters.cs rename to src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctQuantWeightParameters.cs index 8cd8a16abd..2c0573668c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctQuantWeightParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctQuantWeightParameters.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; internal sealed class JxlDctQuantWeightParameters { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctReadOnlyAcPointer.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctReadOnlyAcPointer.cs similarity index 91% rename from src/ImageSharp/Formats/Jxl/Processing/JxlDctReadOnlyAcPointer.cs rename to src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctReadOnlyAcPointer.cs index c80d8eaa64..cbedf64fe8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctReadOnlyAcPointer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctReadOnlyAcPointer.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; /// /// Pointer to DCT AC coefficients. (Read-only) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctScales.cs similarity index 99% rename from src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs rename to src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctScales.cs index 0ebcb7a1fe..0785afe128 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctScales.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctScales.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; /// /// Read-only cosine lookups for the Discrete Cosine Transform (DCT), diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDctSource.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctSource.cs similarity index 96% rename from src/ImageSharp/Formats/Jxl/Processing/JxlDctSource.cs rename to src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctSource.cs index 08dafc11e7..0fe17e92ad 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDctSource.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctSource.cs @@ -4,7 +4,7 @@ using System.Numerics; using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; /// /// Source DCT block. diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 8dcd47711b..8947b0f142 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.Container; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Metadata.Profiles.Icc; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs index e127354a7b..75b39cbeb0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs index 819517551a..1ddb429cba 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlNoiseDecoder.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs index bdec666432..616d7ceeb8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlOutputEncodingInfo.cs @@ -4,6 +4,7 @@ using System.Numerics; using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs index 5b534ef773..9c5b910c2d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs index 1b72325667..d9bc0e88f8 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; internal sealed class JxlPatchDictionary diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogramParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogramParameters.cs index 4f2047424c..c6d7526f72 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogramParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlHistogramParameters.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutput.cs new file mode 100644 index 0000000000..6a34aabdc4 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutput.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; + +/// +/// Provides statistics gathered during compression or decompression. +/// +internal sealed class JxlAuxiliaryOutput +{ + public JxlLayerTotals[] Layers { get; set; } = new JxlLayerTotals[JxlAuxiliaryOutputConstants.NumberOfImageLayers]; + + public long NumberOfBlocks { get; set; } + + public long NumberOfSmallBlocks { get; set; } + + public long NumberOfDct4x8Blocks { get; set; } + + public long NumberOfAfvBlocks { get; set; } + + public long NumberOfDct8Blocks { get; set; } + + public long NumberOfDct8x16Blocks { get; set; } + + public long NumberOfDct8x32Blocks { get; set; } + + public long NumberOfDct16Blocks { get; set; } + + public long NumberOfDct16x32Blocks { get; set; } + + public long NumberOfDct32Blocks { get; set; } + + public long NumberOfDct32x64Blocks { get; set; } + + public long NumberOfDct64Blocks { get; set; } + + public long NumberOfButteraugliIterations { get; set; } + + public long TotalBits => this.Layers.Sum(x => x.TotalBits); + + public static string GetLayerName(JxlLayerType layer) => layer switch + { + JxlLayerType.Header => "Headers", + JxlLayerType.Toc => "TOC", + JxlLayerType.Dictionary => "Patches", + JxlLayerType.Splines => "Splines", + JxlLayerType.Noise => "Noise", + JxlLayerType.Quant => "Quantizer", + JxlLayerType.ModularTree => "ModularTree", + JxlLayerType.ModularGlobal => "ModularGlobal", + JxlLayerType.Dc => "DC", + JxlLayerType.ModularDcGroup => "ModularDcGroup", + JxlLayerType.ControlFields => "ControlFields", + JxlLayerType.Order => "CoeffOrder", + JxlLayerType.Ac => "ACHistograms", + JxlLayerType.AcTokens => "ACTokens", + JxlLayerType.ModularAcGroup => "ModularAcGroup", + _ => "Invalid", + }; + + public void Assimilate(JxlAuxiliaryOutput victim) + { + for (int i = 0; i < JxlAuxiliaryOutputConstants.NumberOfImageLayers; i++) + { + this.Layers[i].Assimilate(victim.Layers[i]); + } + + this.NumberOfBlocks += victim.NumberOfBlocks; + this.NumberOfSmallBlocks += victim.NumberOfSmallBlocks; + + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutputConstants.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutputConstants.cs new file mode 100644 index 0000000000..1c1b4edd5c --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutputConstants.cs @@ -0,0 +1,9 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; + +internal class JxlAuxiliaryOutputConstants +{ + public const int NumberOfImageLayers = (int)JxlLayerType.ModularAcGroup + 1; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlLayerTotals.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlLayerTotals.cs new file mode 100644 index 0000000000..a62785cbd9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlLayerTotals.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; + +internal struct JxlLayerTotals +{ + public int NumberOfClusteredHistograms; + public int ExtraBits; + public int HistogramBits; + public int TotalBits; + public double ClusteredEntropy; + + public void Assimilate(in JxlLayerTotals victim) + { + this.NumberOfClusteredHistograms += victim.NumberOfClusteredHistograms; + this.HistogramBits += victim.HistogramBits; + this.ExtraBits += victim.ExtraBits; + this.TotalBits += victim.TotalBits; + this.ClusteredEntropy += victim.ClusteredEntropy; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlLayerType.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlLayerType.cs new file mode 100644 index 0000000000..5852bd654d --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlLayerType.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; + +internal enum JxlLayerType : byte +{ + Header = 0, + Toc, + Dictionary, + Splines, + Noise, + Quant, + ModularTree, + ModularGlobal, + Dc, + ModularDcGroup, + ControlFields, + Order, + Ac, + AcTokens, + ModularAcGroup, +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs index 92dd8d94dd..b47a949871 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlBlockContextMap.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs index 4ee1a99587..deb5045f2a 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.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 9bf6a7dcb0..ebe7f5195d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlEntropyCoder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlEntropyCoder.cs index 1b896638ca..b36517e30b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlEntropyCoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlEntropyCoder.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageFeatures.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlImageFeatures.cs index 142edd0710..63d4025eb1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlImageFeatures.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlImageFeatures.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.Noise; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs index 2edfeca0c1..c7ea5b8365 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlLoopFilter.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs index 322bcd1c64..820b11a5ba 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal static class JxlOpsinInverseParameters diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs index 880c9e32d2..dcc699503b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs @@ -6,6 +6,8 @@ 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.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs index 4b05150a77..99a3a8c984 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseHelper.cs b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseHelper.cs similarity index 92% rename from src/ImageSharp/Formats/Jxl/Processing/JxlNoiseHelper.cs rename to src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseHelper.cs index f6432e24db..1b6dfa2480 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseHelper.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseHelper.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; internal static class JxlNoiseHelper { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseIndexAndFraction.cs b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseIndexAndFraction.cs similarity index 82% rename from src/ImageSharp/Formats/Jxl/Processing/JxlNoiseIndexAndFraction.cs rename to src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseIndexAndFraction.cs index c3ec1437c8..fe18886a04 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseIndexAndFraction.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseIndexAndFraction.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; [StructLayout(LayoutKind.Sequential)] internal struct JxlNoiseIndexAndFraction(int index, float fraction) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseLevel.cs b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseLevel.cs similarity index 83% rename from src/ImageSharp/Formats/Jxl/Processing/JxlNoiseLevel.cs rename to src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseLevel.cs index d5feebd52b..836a0e3026 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseLevel.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseLevel.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; [StructLayout(LayoutKind.Sequential)] internal struct JxlNoiseLevel(float noiseLevel, float intensity) diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseParameters.cs similarity index 85% rename from src/ImageSharp/Formats/Jxl/Processing/JxlNoiseParameters.cs rename to src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseParameters.cs index 3f51f85379..de8072c3ee 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlNoiseParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseParameters.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; internal sealed class JxlNoiseParameters { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlAspectRatioHelpers.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlAspectRatioHelpers.cs similarity index 93% rename from src/ImageSharp/Formats/Jxl/Processing/JxlAspectRatioHelpers.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlAspectRatioHelpers.cs index 7fcda36c9a..6c69a8fff7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlAspectRatioHelpers.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlAspectRatioHelpers.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; internal static class JxlAspectRatioHelpers { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlBitDepth.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlBitDepth.cs similarity index 90% rename from src/ImageSharp/Formats/Jxl/Processing/JxlBitDepth.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlBitDepth.cs index 52b384bb94..a0f6765224 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlBitDepth.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlBitDepth.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; /// /// Describes the interpretation of the input and output diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlBitDepthType.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlBitDepthType.cs similarity index 94% rename from src/ImageSharp/Formats/Jxl/Processing/JxlBitDepthType.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlBitDepthType.cs index e942d57a47..754ac41010 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlBitDepthType.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlBitDepthType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; /// /// Specifies the kind of bit depth. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDataType.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlDataType.cs similarity index 90% rename from src/ImageSharp/Formats/Jxl/Processing/JxlDataType.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlDataType.cs index a9c8057e6f..8bc0bf6911 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDataType.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlDataType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; /// /// Specifies which data type to use for sample values diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlInverseMtf.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlInverseMtf.cs similarity index 94% rename from src/ImageSharp/Formats/Jxl/Processing/JxlInverseMtf.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlInverseMtf.cs index 8016ec51c0..18f9a55bdb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlInverseMtf.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlInverseMtf.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; /// /// Inverse Move to Front implementation diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlLehmerCode.cs similarity index 97% rename from src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlLehmerCode.cs index 4afbcb34fd..2cbd4ea7ab 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlLehmerCode.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; internal static class JxlLehmerCode { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlMatrix3x3.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlMatrix3x3.cs index a01e2a6216..88454e1f69 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlMatrix3x3.cs @@ -6,7 +6,7 @@ #pragma warning disable IDE0044 // Add readonly modifier #pragma warning disable IDE0051 // Remove unused private members -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; internal struct JxlMatrix3x3 { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlMatrix3x3F.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlMatrix3x3F.cs index b8620c2dd4..2d66df2729 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlMatrix3x3F.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlMatrix3x3F.cs @@ -6,7 +6,7 @@ #pragma warning disable IDE0044 // Add readonly modifier #pragma warning disable IDE0052 // Remove unread private members -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; internal struct JxlMatrix3x3F { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlOverride.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlOverride.cs similarity index 87% rename from src/ImageSharp/Formats/Jxl/Processing/JxlOverride.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlOverride.cs index ebd84b7b68..c25f354046 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlOverride.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlOverride.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; /// /// Represents a boolean which can be overriden to be a default diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlOverrideHelpers.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlOverrideHelpers.cs similarity index 94% rename from src/ImageSharp/Formats/Jxl/Processing/JxlOverrideHelpers.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlOverrideHelpers.cs index 85464a16aa..a4e06edc17 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlOverrideHelpers.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlOverrideHelpers.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; /// /// Override utilities. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPackSigned.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlPackSigned.cs similarity index 93% rename from src/ImageSharp/Formats/Jxl/Processing/JxlPackSigned.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlPackSigned.cs index f54b77f1d3..a4c2fec411 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlPackSigned.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlPackSigned.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; /// /// Provides PackSigned and UnpackSigned methods. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPixelFormat.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlPixelFormat.cs similarity index 93% rename from src/ImageSharp/Formats/Jxl/Processing/JxlPixelFormat.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlPixelFormat.cs index fadb8709fb..be00bd3e76 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlPixelFormat.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlPixelFormat.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; /// /// Data type for the sample values per channel per pixel diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSpeedTier.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlSpeedTier.cs similarity index 96% rename from src/ImageSharp/Formats/Jxl/Processing/JxlSpeedTier.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlSpeedTier.cs index 363b4a5e4b..8cbc589a1b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSpeedTier.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlSpeedTier.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; /// /// Defines how quickly or slowly to encode an image. Slower diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSeparable5.cs similarity index 82% rename from src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSeparable5.cs index 91258b8529..03c16de089 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSeparable5.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSeparable5.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; internal struct JxlWeightsSeparable5 { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric3.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSymmetric3.cs similarity index 94% rename from src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric3.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSymmetric3.cs index 4d350b3ab8..69cdc0395a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric3.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSymmetric3.cs @@ -4,7 +4,7 @@ using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; internal sealed class JxlWeightsSymmetric3 { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric5.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSymmetric5.cs similarity index 97% rename from src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric5.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSymmetric5.cs index 1e2481dbb4..df5fa66b7b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlWeightsSymmetric5.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSymmetric5.cs @@ -4,7 +4,7 @@ using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; internal sealed class JxlWeightsSymmetric5 { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs similarity index 96% rename from src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs index 78d8900a52..68edb75d5e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; internal sealed class JxlXorShift { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlDequantMatrices.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Processing/JxlDequantMatrices.cs rename to src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs index 15b0935415..9649f44203 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlDequantMatrices.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs @@ -3,8 +3,9 @@ using System.Diagnostics; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; /// /// Contains matrices used to inverse quantize coefficients. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantMode.cs similarity index 94% rename from src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs rename to src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantMode.cs index 1b974343a4..412dd245d3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantMode.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantMode.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; /// /// Specifies which algorithm should be used to quantize coefficients. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantTable.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantTable.cs similarity index 90% rename from src/ImageSharp/Formats/Jxl/Processing/JxlQuantTable.cs rename to src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantTable.cs index 5fc8b49a8e..dca689b88f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantTable.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantTable.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; /// /// Specifies which quantization table to use depending on diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantWeights.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs similarity index 99% rename from src/ImageSharp/Formats/Jxl/Processing/JxlQuantWeights.cs rename to src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs index dfe12b8f30..4986ee7e39 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantWeights.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantWeights.cs @@ -1,7 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; internal static class JxlQuantWeights { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs rename to src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs index 3b7500fd05..1ea7cfcbd1 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs @@ -6,9 +6,10 @@ using SixLabors.ImageSharp.Formats.Jxl.Fields; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; /// /// The quantizer for DCT DC/AC coefficients. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerConstants.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerConstants.cs similarity index 96% rename from src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerConstants.cs rename to src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerConstants.cs index daf28db696..ff6e9c3754 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerConstants.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerConstants.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; /// /// Shared constants used by the quantizer. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerEncoding.cs similarity index 98% rename from src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs rename to src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerEncoding.cs index 8a12e6dbf5..283892392a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerEncoding.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerEncoding.cs @@ -1,7 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; /// /// Specifies weights and quantizer modes. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerParameters.cs similarity index 95% rename from src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerParameters.cs rename to src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerParameters.cs index 4faa558692..2963d4c329 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlQuantizerParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerParameters.cs @@ -3,7 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Fields; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; /// /// Represents parameters for the JPEG XL quantizer. diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs index 731c22835b..a3901bf408 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs @@ -3,7 +3,9 @@ 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.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; From 4b9a74d969434ff68cea549dab041575b306351d Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:12:57 +0400 Subject: [PATCH 116/142] Add JPEG parser for JXL<->JPEG conversion This isn't a decoder or encoder. It's just a parser/writer so the codec can take some quantization parameters from a JPEG file, as JPEG and JPEG XL are very similar. --- src/ImageSharp/Common/InlineArray.cs | 9 + src/ImageSharp/Common/InlineArray.tt | 2 +- .../Formats/Jxl/IO/Jpeg/Data/JpegComponent.cs | 53 + .../Jxl/IO/Jpeg/Data/JpegComponentScanInfo.cs | 16 + .../Jxl/IO/Jpeg/Data/JpegComponentType.cs | 15 + .../Formats/Jxl/IO/Jpeg/Data/JpegData.cs | 1009 +++++++++++++++++ .../Jxl/IO/Jpeg/Data/JpegExtraZeroRunInfo.cs | 11 + .../Jxl/IO/Jpeg/Data/JpegHuffmanCode.cs | 30 + .../Formats/Jxl/IO/Jpeg/Data/JpegInfo.cs | 17 + .../Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs | 8 +- .../Formats/Jxl/IO/Jpeg/Data/JpegScanInfo.cs | 43 + .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 5 +- .../Formats/Jxl/Processing/JxlImageBundle.cs | 3 +- 13 files changed, 1214 insertions(+), 7 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponent.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponentScanInfo.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponentType.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegExtraZeroRunInfo.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegHuffmanCode.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegInfo.cs create mode 100644 src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegScanInfo.cs diff --git a/src/ImageSharp/Common/InlineArray.cs b/src/ImageSharp/Common/InlineArray.cs index d4cde5f256..a72e3f5394 100644 --- a/src/ImageSharp/Common/InlineArray.cs +++ b/src/ImageSharp/Common/InlineArray.cs @@ -44,6 +44,15 @@ internal struct InlineArray16 private T t; } +/// +/// Represents a safe, fixed sized buffer of 17 elements. +/// +[InlineArray(17)] +internal struct InlineArray17 +{ + private T t; +} + /// /// Represents a safe, fixed sized buffer of 18 elements. /// diff --git a/src/ImageSharp/Common/InlineArray.tt b/src/ImageSharp/Common/InlineArray.tt index 998f8ae105..d15650592a 100644 --- a/src/ImageSharp/Common/InlineArray.tt +++ b/src/ImageSharp/Common/InlineArray.tt @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp; <#GenerateInlineArrays();#> <#+ -private static int[] Lengths = [4, 8, 14, 16, 18, 19, 26, 32, 33, 36, 256]; +private static int[] Lengths = [4, 8, 14, 16, 17, 18, 19, 26, 32, 33, 36, 256]; void GenerateInlineArrays() { diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponent.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponent.cs new file mode 100644 index 0000000000..297acacc0a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponent.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; + +// We may need to get a ref to fields, so don't +// make these properties. +#pragma warning disable SA1401 // Fields should be private + +/// +/// Represents one component of a jpeg file. +/// +internal sealed class JpegComponent +{ + /// + /// One-byte id of the component + /// + public int Id; + + /// + /// In interleaved mode, each minimal coded unit (MCU) + /// has horizontal x vertical sample factor DCT blocks + /// from this component. This is the horizontal factor. + /// + public int HorizontalSampleFactor = 1; + + /// + /// In interleaved mode, each minimal coded unit (MCU) + /// has horizontal x vertical sample factor DCT blocks + /// from this component. This is the vertical factor. + /// + public int VerticalSampleFactor = 1; + + /// + /// Index of quantization table used for this component. + /// + public int QuantIndex; + + /// + /// Width measured in 8x8 blocks + /// + public int WidthInBlocks; + + /// + /// Width measured in 8x8 blocks + /// + public int HeightInBlocks; + + /// + /// Gets or sets DCT coefficients. + /// + public List Coefficients { get; set; } = []; +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponentScanInfo.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponentScanInfo.cs new file mode 100644 index 0000000000..1a3ba3707b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponentScanInfo.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; + +/// +/// Huffman table indexes used for one component of one scan. +/// +// We may need to get a ref to fields, so don't +// make these properties. +internal struct JpegComponentScanInfo +{ + public int ComponentIndex; + public int DcTableIndex; + public int AcTableIndex; +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponentType.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponentType.cs new file mode 100644 index 0000000000..adf5940c6e --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponentType.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; + +internal enum JpegComponentType : byte +{ + Gray, + + YCbCr, + + Rgb, + + Custom +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs new file mode 100644 index 0000000000..9ab957be93 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs @@ -0,0 +1,1009 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Fields; +using SixLabors.ImageSharp.Formats.Jxl.Processing; + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; + +internal sealed class JpegData : IJxlFields +{ + private int restartInterval; + private bool hasZeroPaddingBit; + + public int Width { get; set; } + + public int Height { get; set; } + + public int RestartInterval + { + get => this.restartInterval; + set => this.restartInterval = value; + } + + public bool HasZeroPaddingBit + { + get => this.hasZeroPaddingBit; + set => this.hasZeroPaddingBit = value; + } + + /// + /// Gets or sets raw bytes of APP markers. Types + /// of APP markers are specified by . + /// + public List> AppData { get; set; } = []; + + /// + /// Gets or sets kinds of APP markers for each marker data + /// within . + /// + public List AppMarkerTypes { get; set; } = []; + + /// + /// Gets or sets raw bytes of COM markers. + /// + public List> ComData { get; set; } = []; + + /// + /// Gets or sets quantization tables. + /// + public List Quant { get; set; } = []; + + /// + /// Gets or sets definitions of Huffman codes. + /// + public List HuffmanCodes { get; set; } = []; + + /// + /// Gets or sets JPEG components. + /// + public List Components { get; set; } = []; + + /// + /// Gets or sets scan infos. + /// + public List ScanInfos { get; set; } = []; + + public List MarkerOrder { get; set; } = []; + + public List> InterMarkerData { get; set; } = []; + + public List TailData { get; set; } = []; + + public List PaddingBits { get; set; } = []; + + public void CalculateMcuSize(JpegScanInfo scan, out int mcusPerRow, out int mcuRows) + { + bool isInterleaved = scan.NumComponents > 1; + JpegComponent baseComponent = this.Components[scan.Components[0].ComponentIndex]; + + int horizontalGroup = isInterleaved ? 1 : baseComponent.HorizontalSampleFactor; + int verticalGroup = isInterleaved ? 1 : baseComponent.VerticalSampleFactor; + + int maxHSampFactor = 1; + int maxVSampFactor = 1; + + foreach (JpegComponent component in this.Components) + { + maxHSampFactor = Math.Max(component.HorizontalSampleFactor, maxHSampFactor); + maxVSampFactor = Math.Max(component.VerticalSampleFactor, maxVSampFactor); + } + + mcusPerRow = JxlMath.DivCeil(this.Width * horizontalGroup, 8 * maxHSampFactor); + mcuRows = JxlMath.DivCeil(this.Height * verticalGroup, 8 * maxVSampFactor); + } + + public static void SetJpegDataFromIcc(Span icc, JpegData jpegData) + { + int iccPos = 0; + + for (int i = 0; i < jpegData.AppData.Count; i++) + { + if (jpegData.AppMarkerTypes[i] != JpegAppMarkerType.Icc) + { + continue; + } + + if (jpegData.AppData[i].Count < 17) + { + throw new InvalidOperationException("ICC APP marker too small: " + jpegData.AppData[i].Count); + } + + int len = jpegData.AppData[i].Count - 17; + if (iccPos + len > icc.Length) + { + throw new InvalidOperationException("ICC length is less than APP markers: requested " + len + " more bytes, " + (icc.Length - iccPos) + " available"); + } + + icc.Slice(iccPos, len).CopyTo(CollectionsMarshal.AsSpan(jpegData.AppData[i])[17..]); + iccPos += len; + } + + if (iccPos != icc.Length && iccPos != 0) + { + throw new InvalidOperationException("ICC length > APP markers"); + } + } + + private static bool VisitMarker(ref byte marker, JxlVisitor visitor, ref JpegInfo info) + { + uint marker32 = marker - 0xC0u; + + if (!visitor.Bits(6, 0x00, ref marker32)) + { + return false; + } + + marker = (byte)(marker32 + 0xC0u); + + if ((marker & 0xf0) == 0xe0) + { + info.NumberOfAppMarkers++; + } + + if (marker == 0xfe) + { + info.NumberOfComMarkers++; + } + + if (marker == 0xda) + { + info.NumberOfScans++; + } + + if (marker == 0xff) + { + info.NumberOfIntermarkers++; + } + + if (marker == 0xdd) + { + info.HasDri = true; + } + + return true; + } + + /// + public bool Visit(JxlVisitor visitor) + { + // The following is just JPEG parsing/writing code. + // Nothing different. + bool isGray = this.Components.Count == 1; + + if (!visitor.Boolean(false, ref isGray)) + { + return false; + } + + if (visitor.IsReading) + { + this.Components = new List(isGray ? 1 : 3); + } + + JpegInfo info = default; + + if (visitor.IsReading) + { + byte marker = 0xC0; + + do + { + if (!VisitMarker(ref marker, visitor, ref info)) + { + return false; + } + + this.MarkerOrder.Add(marker); + + if (this.MarkerOrder.Count > 16384) + { + throw new InvalidOperationException("Too many markers: " + this.MarkerOrder.Count); + } + } + while (marker != 0xD9); + } + else + { + if (this.MarkerOrder.Count > 16384) + { + throw new InvalidOperationException("Too many markers: " + this.MarkerOrder.Count); + } + + Span markerData = CollectionsMarshal.AsSpan(this.MarkerOrder); + + for (int i = 0; i < markerData.Length; i++) + { + ref byte marker = ref markerData[i]; + + if (!VisitMarker(ref marker, visitor, ref info)) + { + return false; + } + } + + if (this.MarkerOrder.Count > 0) + { + if (this.MarkerOrder[^1] != 0xD9) + { + throw new InvalidOperationException("Last marker should always be EOI (0xD9) marker"); + } + } + } + + if (info.NumberOfScans == 0) + { + throw new InvalidOperationException("No JPEG scans"); + } + + if (visitor.IsReading) + { + this.AppData = new List>(info.NumberOfAppMarkers); + this.AppMarkerTypes = new List(info.NumberOfAppMarkers); + this.ComData = new List>(info.NumberOfAppMarkers); + this.ScanInfos = new List(info.NumberOfAppMarkers); + } + + if (this.AppData.Count != info.NumberOfAppMarkers || + this.AppMarkerTypes.Count != info.NumberOfAppMarkers || + this.ComData.Count != info.NumberOfComMarkers || + this.ScanInfos.Count != info.NumberOfScans) + { + throw new InvalidOperationException("Mismatch between number of APP markers and the actual APP marker count"); + } + + for (int i = 0; i < this.AppData.Count; i++) + { + uint uMarkerType = (uint)this.AppMarkerTypes[i]; + + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.Value(1), + JxlFieldExpressions.BitsOffset(1, 2), + JxlFieldExpressions.BitsOffset(2, 4), + 0, + ref uMarkerType)) + { + return false; + } + + this.AppMarkerTypes[i] = (JpegAppMarkerType)uMarkerType; + + if (this.AppMarkerTypes[i] is not JpegAppMarkerType.Unknown and + not JpegAppMarkerType.Icc and + not JpegAppMarkerType.Exif and + not JpegAppMarkerType.Xmp) + { + throw new InvalidOperationException("Unknown APP marker type: " + (uint)this.AppMarkerTypes[i]); + } + + uint len = (uint)this.AppMarkerTypes.Count - 1; + if (!visitor.Bits(16, 0, ref len)) + { + return false; + } + + if (visitor.IsReading) + { + this.AppData[i] = new List((int)len + 1); + if (len + 1 < 3) + { + throw new InvalidOperationException("Marker size is invalid"); + } + } + + if (len + 1 < 3) + { + throw new InvalidOperationException("Marker size is invalid"); + } + } + + for (int i = 0; i < this.ComData.Count; i++) + { + List com = this.ComData[i]; + + uint len = (uint)com.Count - 1; + + if (!visitor.Bits(16, 0, ref len)) + { + return false; + } + + if (len + 1 < 3) + { + throw new InvalidOperationException("Marker size is invalid"); + } + + if (visitor.IsReading) + { + this.ComData[i] = new List((int)len + 1); + + if (len + 1 < 3) + { + throw new InvalidOperationException("Marker size is invalid"); + } + } + + if (len + 1 < 3) + { + throw new InvalidOperationException("Marker size is invalid"); + } + } + + uint numQuantTables = (uint)this.Quant.Count; + + if (!visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(3), + JxlFieldExpressions.Value(4), + 2, + ref numQuantTables)) + { + return false; + } + + if (numQuantTables == 4) + { + throw new InvalidOperationException("Invalid number of quant tables"); + } + + if (visitor.IsReading) + { + this.Quant = new List((int)numQuantTables); + } + + Span quantSpan = CollectionsMarshal.AsSpan(this.Quant); + + for (int i = 0; i < numQuantTables; i++) + { + ref JpegQuantizationTable quant = ref quantSpan[i]; + + if (quant.Precision > 1) + { + throw new InvalidOperationException("Quant tables with more than 16 bits are not supported"); + } + + if (!visitor.Bits(1, 0, ref Unsafe.As(ref quant.Precision))) + { + return false; + } + + if (!visitor.Bits(2, (uint)i, ref Unsafe.As(ref quant.Index))) + { + return false; + } + + if (!visitor.Boolean(true, ref quant.IsLast)) + { + return false; + } + } + + Span components = CollectionsMarshal.AsSpan(this.Components); + + JpegComponentType componentType = + components.Length == 1 && components[0].Id == 1 ? JpegComponentType.Gray + : components.Length == 3 && components[0].Id == 1 && + components[1].Id == 2 && components[2].Id == 3 + ? JpegComponentType.YCbCr + : components.Length == 3 && components[0].Id == 'R' && + components[1].Id == 'G' && components[2].Id == 'B' + ? JpegComponentType.Rgb + : JpegComponentType.Custom; + + if (visitor.Bits(2, (uint)JpegComponentType.YCbCr, ref Unsafe.As(ref componentType))) + { + return false; + } + + uint numberOfComponents; + + if (componentType == JpegComponentType.Gray) + { + numberOfComponents = 1; + } + else if (componentType != JpegComponentType.Custom) + { + numberOfComponents = 3; + } + else + { + numberOfComponents = (uint)components.Length; + + if (!visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(3), + JxlFieldExpressions.Value(4), + 3, + ref numberOfComponents)) + { + return false; + } + + if (numberOfComponents is not 1 and not 3) + { + throw new InvalidOperationException("Invalid number of components: " + numberOfComponents); + } + } + + if (visitor.IsReading) + { + this.Components = new List((int)numberOfComponents); + + // It's unsafe to assign a new List (or add/remove items to it) + // while keeping a Span to it. + components = CollectionsMarshal.AsSpan(this.Components); + } + + if (componentType == JpegComponentType.Custom) + { + foreach (JpegComponent component in components) + { + if (!visitor.Bits(8, 0, ref Unsafe.As(ref component.Id))) + { + return false; + } + } + } + else if (componentType == JpegComponentType.Gray) + { + components[0].Id = 1; + } + else if (componentType == JpegComponentType.Rgb) + { + components[0].Id = 'R'; + components[1].Id = 'G'; + components[2].Id = 'B'; + } + else + { + components[0].Id = 1; + components[1].Id = 2; + components[2].Id = 3; + } + + uint usedTables = 0; + + for (int i = 0; i < components.Length; i++) + { + if (!visitor.Bits(2, 0, ref Unsafe.As(ref components[i].QuantIndex))) + { + return false; + } + + if (components[i].QuantIndex >= this.Quant.Count) + { + throw new InvalidOperationException("Invalid quant table for component " + components[i].QuantIndex); + } + + usedTables |= 1u << components[i].QuantIndex; + } + + for (int i = 0; i < this.Quant.Count; i++) + { + if ((usedTables & (1 << i)) != 0) + { + continue; + } + + if (i == 0) + { + throw new InvalidOperationException("First quant table unused"); + } + + for (int j = 0; j < 64; j++) + { + if (this.Quant[i].Values[j] != this.Quant[i - 1].Values[j]) + { + throw new InvalidOperationException("Non-trivial unused quant table"); + } + } + } + + uint numHuff = (uint)this.HuffmanCodes.Count; + + if (!visitor.U32( + JxlFieldExpressions.Value(4), + JxlFieldExpressions.BitsOffset(3, 2), + JxlFieldExpressions.BitsOffset(4, 10), + JxlFieldExpressions.BitsOffset(6, 26), + 4, + ref numHuff)) + { + return false; + } + + if (visitor.IsReading) + { + this.HuffmanCodes = new List((int)numHuff); + } + + Span huffs = CollectionsMarshal.AsSpan(this.HuffmanCodes); + + for (int i = 0; i < huffs.Length; i++) + { + ref JpegHuffmanCode hc = ref huffs[i]; + + bool isAc = (hc.SlotId >> 4) != 0; + uint id = (uint)hc.SlotId & 0x0Fu; + + if (!visitor.Boolean(false, ref isAc)) + { + return false; + } + + if (!visitor.Bits(2, 0, ref id)) + { + return false; + } + + hc.SlotId = ((isAc ? 1 : 0) << 4) | (int)id; + + if (!visitor.Boolean(true, ref hc.IsLast)) + { + return false; + } + + int numSymbols = 0; + + for (int j = 0; j <= 16; j++) + { + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.Value(1), + JxlFieldExpressions.BitsOffset(3, 2), + JxlFieldExpressions.Bits(8), + 0, + ref Unsafe.As(ref hc.Counts[j]))) + { + return false; + } + + numSymbols += hc.Counts[j]; + } + + if (numSymbols == 0) + { + // At least 2 symbols are required, since one of them is EOI. + // This case is used to represent an empty DHT marker. + continue; + } + + if (numSymbols > 17) + { + throw new InvalidOperationException("Huffman code too large (" + numSymbols + ")"); + } + + InlineArray5 valueSlots = default; + + for (int j = 0; j < numSymbols; j++) + { + // Goes up to 256, included. Might have the same symbol appear twice. + if (!visitor.U32( + JxlFieldExpressions.Bits(2), + JxlFieldExpressions.BitsOffset(2, 4), + JxlFieldExpressions.BitsOffset(4, 8), + JxlFieldExpressions.BitsOffset(8, 1), + 0, + ref Unsafe.As(ref hc.Values[j]))) + { + return false; + } + + valueSlots[hc.Values[j] >> 6] |= 1L << (hc.Values[j] & 0x3F); + } + + if (hc.Values[numSymbols - 1] != JpegDataConstants.JpegHuffmanAlphabetSize) + { + throw new InvalidOperationException("Missing EOI symbol"); + } + + if (valueSlots[4] != 1) + { + return false; + } + + int numValues = 1; + + for (int j = 0; j < 4; j++) + { + numValues += BitOperations.PopCount((uint)valueSlots[i]); + } + + if (numValues != numSymbols) + { + throw new InvalidOperationException("Duplicate Huffman symbols"); + } + + if (!isAc) + { + bool onlyDC = ((valueSlots[0] >> JpegDataConstants.JpegDcAlphabetSize) | valueSlots[1] | valueSlots[2] | valueSlots[3]) == 0; + + if (!onlyDC) + { + throw new InvalidOperationException("Huffman symbols out of DC range"); + } + } + } + + foreach (JpegScanInfo scan in this.ScanInfos) + { + if (!visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.Value(3), + JxlFieldExpressions.Value(4), + 1, + ref Unsafe.As(ref scan.NumComponents))) + { + return false; + } + + if (scan.NumComponents >= 4) + { + throw new InvalidOperationException("Invalid number of components in SOS marker"); + } + + if (!visitor.Bits(6, 0, ref Unsafe.As(ref scan.Ss))) + { + return false; + } + + if (!visitor.Bits(6, 63, ref Unsafe.As(ref scan.Se))) + { + return false; + } + + if (!visitor.Bits(4, 0, ref Unsafe.As(ref scan.Al))) + { + return false; + } + + if (!visitor.Bits(4, 0, ref Unsafe.As(ref scan.Ah))) + { + return false; + } + + for (int i = 0; i < scan.NumComponents; i++) + { + if (!visitor.Bits(2, 0, ref Unsafe.As(ref scan.Components[i].ComponentIndex))) + { + return false; + } + + if (scan.Components[i].ComponentIndex >= components.Length) + { + throw new InvalidOperationException("Invalid component idx in SOS marker"); + } + + if (!visitor.Bits(2, 0, ref Unsafe.As(ref scan.Components[i].AcTableIndex))) + { + return false; + } + + if (!visitor.Bits(2, 0, ref Unsafe.As(ref scan.Components[i].DcTableIndex))) + { + return false; + } + } + + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.Value(1), + JxlFieldExpressions.Value(2), + JxlFieldExpressions.BitsOffset(3, 3), + JxlShared.MaximumNumberOfPasses - 1, + ref Unsafe.As(ref scan.LastNeededPass))) + { + return false; + } + } + + if (info.HasDri) + { + if (!visitor.Bits(16, 0, ref Unsafe.As(ref this.restartInterval))) + { + return false; + } + } + + foreach (JpegScanInfo scan in this.ScanInfos) + { + int numResetPoints = scan.ResetPoints.Count; + + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.BitsOffset(2, 1), + JxlFieldExpressions.BitsOffset(4, 4), + JxlFieldExpressions.BitsOffset(16, 20), + 0, + ref Unsafe.As(ref numResetPoints))) + { + return false; + } + + if (visitor.IsReading) + { + scan.ResetPoints = new List(numResetPoints); + } + + int lastBlockIdx = -1; + foreach (int blk in scan.ResetPoints) + { + int blockIdx = blk - (lastBlockIdx + 1); + + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.BitsOffset(3, 1), + JxlFieldExpressions.BitsOffset(5, 9), + JxlFieldExpressions.BitsOffset(28, 41), + 0, + ref Unsafe.As(ref blockIdx))) + { + return false; + } + + blockIdx += lastBlockIdx + 1; + if (blockIdx >= (3u << 26)) + { + // At most 8K x 8K x num_channels blocks are possible in a JPEG. + // So valid block indices are below 3 * 2^26. + throw new InvalidOperationException("Invalid block ID: " + blockIdx); + } + + lastBlockIdx = blockIdx; + } + + int numExtraZeroRuns = scan.ExtraZeroRuns.Count; + + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.BitsOffset(2, 1), + JxlFieldExpressions.BitsOffset(4, 4), + JxlFieldExpressions.BitsOffset(16, 20), + 0, + ref Unsafe.As(ref numExtraZeroRuns))) + { + return false; + } + + if (visitor.IsReading) + { + scan.ExtraZeroRuns = new List(numExtraZeroRuns); + } + + lastBlockIdx = -1; + + Span extraZeroes = CollectionsMarshal.AsSpan(scan.ExtraZeroRuns); + + for (int i = 0; i < extraZeroes.Length; i++) + { + ref JpegExtraZeroRunInfo extraZeroRun = ref extraZeroes[i]; + + ref int block_idx = ref extraZeroRun.BlockIndex; + ref int extra_zero_runs = ref extraZeroRun.NumExtraZeroRuns; + + if (!visitor.U32( + JxlFieldExpressions.Value(1), + JxlFieldExpressions.BitsOffset(2, 2), + JxlFieldExpressions.BitsOffset(4, 5), + JxlFieldExpressions.BitsOffset(8, 20), + 1, + ref Unsafe.As(ref extra_zero_runs))) + { + return false; + } + + block_idx -= lastBlockIdx + 1; + + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.BitsOffset(3, 1), + JxlFieldExpressions.BitsOffset(5, 9), + JxlFieldExpressions.BitsOffset(28, 41), + 0, + ref Unsafe.As(ref block_idx))) + { + return false; + } + + block_idx += lastBlockIdx + 1; + + if (extra_zero_runs > 4) + { + throw new InvalidOperationException("Invalid number of extra zero runs: " + extra_zero_runs); + } + + if (block_idx > (3u << 26)) + { + throw new InvalidOperationException("Invalid block ID: " + block_idx); + } + + lastBlockIdx = block_idx; + } + } + + List interMarkerDataSizes = new(info.NumberOfIntermarkers); + + for (int i = 0; i < info.NumberOfIntermarkers; ++i) + { + int len = visitor.IsReading ? 0 : this.InterMarkerData[i].Count; + + if (!visitor.Bits(16, 0, ref Unsafe.As(ref len))) + { + return false; + } + + if (visitor.IsReading) + { + interMarkerDataSizes.Add(len); + } + } + + int tail_data_len = this.TailData.Count; + + if (visitor.IsReading && tail_data_len > 4260096) + { + throw new InvalidOperationException("Tail data too large (max size = 4260096, size = " + tail_data_len + ")"); + } + + if (!visitor.U32( + JxlFieldExpressions.Value(0), + JxlFieldExpressions.BitsOffset(8, 1), + JxlFieldExpressions.BitsOffset(16, 257), + JxlFieldExpressions.BitsOffset(22, 65793), + 0, + ref Unsafe.As(ref tail_data_len))) + { + return false; + } + + if (!visitor.Boolean(false, ref this.hasZeroPaddingBit)) + { + return false; + } + + if (this.hasZeroPaddingBit) + { + uint nbit = (uint)this.PaddingBits.Count; + + if (!visitor.Bits(24, 0, ref nbit)) + { + return false; + } + + if (visitor.IsReading) + { + this.PaddingBits = new List((int)Math.Min(1024u, nbit)); + + for (int i = 0; i < nbit; i++) + { + bool bbit = false; + + if (!visitor.Boolean(false, ref bbit)) + { + return false; + } + + this.PaddingBits.Add(bbit ? (byte)1 : (byte)0); + } + } + else + { + Span bits = CollectionsMarshal.AsSpan(this.PaddingBits); + + for (int i = 0; i < bits.Length; i++) + { + ref byte bit = ref bits[i]; + bool bbit = bit != 0; + + if (!visitor.Boolean(false, ref bbit)) + { + return false; + } + + bit = bbit ? (byte)1 : (byte)0; + } + } + } + + int dhtIndex = 0; // index of the Define Huffman Table + int scanIndex = 0; + bool isProgressive = false; + + InlineArray4 acOk = default; + InlineArray4 dcOk = default; + + // All values of acOk, dcOk by default are false. + acOk[0] = acOk[1] = acOk[2] = acOk[3] = false; + dcOk[0] = dcOk[1] = dcOk[2] = dcOk[3] = false; + + Span markerOrderSpan = CollectionsMarshal.AsSpan(this.MarkerOrder); + + for (int i = 0; i < markerOrderSpan.Length; i++) + { + byte marker = markerOrderSpan[i]; + + if (marker == 0xC2) + { + isProgressive = true; + } + else if (marker == 0xC4) + { + Span huffmanCode = CollectionsMarshal.AsSpan(this.HuffmanCodes); + + for (; dhtIndex < huffmanCode.Length;) + { + ref JpegHuffmanCode huff = ref huffmanCode[dhtIndex++]; + int index = huff.SlotId; + if ((index & 0x10) != 0) + { + index -= 0x10; + acOk[index] = true; + } + else + { + dcOk[index] = true; + } + + if (huff.IsLast) + { + break; + } + } + } + else if (marker == 0xDA) + { + Span scanInfo = CollectionsMarshal.AsSpan(this.ScanInfos); + JpegScanInfo si = scanInfo[scanIndex++]; + + for (int j = 0; j < si.NumComponents; ++j) + { + ref JpegComponentScanInfo csi = ref si.Components[j]; + + int dcTableIndex = csi.DcTableIndex; + int acTableIndex = csi.AcTableIndex; + + bool wantDc = !isProgressive || (si.Ss == 0); + + if (wantDc && !dcOk[dcTableIndex]) + { + throw new InvalidOperationException("DC Huffman table used before defined"); + } + + bool wantAc = !isProgressive || (si.Ss != 0) || (si.Se != 0); + + if (wantAc && !acOk[acTableIndex]) + { + throw new InvalidOperationException("AC Huffman table used before defined"); + } + } + } + } + + // Apply postponed actions + if (visitor.IsReading) + { + this.TailData = new List(tail_data_len); + + if (interMarkerDataSizes.Count != info.NumberOfIntermarkers) + { + return false; + } + + this.InterMarkerData = new List>(info.NumberOfIntermarkers); + + for (int i = 0; i < info.NumberOfIntermarkers; ++i) + { + this.InterMarkerData.Add(new List(interMarkerDataSizes[i])); + } + } + + return true; + } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegExtraZeroRunInfo.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegExtraZeroRunInfo.cs new file mode 100644 index 0000000000..ed2d41eb03 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegExtraZeroRunInfo.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; + +internal struct JpegExtraZeroRunInfo +{ + public int BlockIndex; + + public int NumExtraZeroRuns; +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegHuffmanCode.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegHuffmanCode.cs new file mode 100644 index 0000000000..324ff5f306 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegHuffmanCode.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; + +// We may need to get a ref to fields, so don't +// make these properties. +internal struct JpegHuffmanCode() +{ + /// + /// Bit length histogram + /// + public InlineArray17 Counts; + + /// + /// Symbol values stored by increasing bit lengths. + /// + public InlineArray17 Values; + + /// + /// The index of the code in the current set of Huffman codes. + /// For AC component Huffman codes, 0x10 is added to the index. + /// + public int SlotId; + + /// + /// True if the code is last within its marker segment. + /// + public bool IsLast = true; +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegInfo.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegInfo.cs new file mode 100644 index 0000000000..2454f6bbe9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegInfo.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; + +internal struct JpegInfo +{ + public int NumberOfAppMarkers { get; set; } + + public int NumberOfComMarkers { get; set; } + + public int NumberOfScans { get; set; } + + public int NumberOfIntermarkers { get; set; } + + public bool HasDri { get; set; } +} diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs index 7386bd5a3f..051835048d 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs @@ -6,6 +6,8 @@ namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; /// /// Representation of quantization values for an 8x8 pixel block. /// +// We may need to get a ref to fields, so don't +// make these properties. internal struct JpegQuantizationTable() { /// @@ -13,17 +15,17 @@ internal struct JpegQuantizationTable() /// public InlineArray64 Values; - public int Precision { get; set; } + public int Precision; /// /// Gets or sets the index of the quantization table /// as it was parsed from the input JPEG. /// - public int Index { get; set; } + public int Index; /// /// Gets or sets a value indicating whether this table /// is the last one within its marker segment. /// - public bool IsLast { get; set; } = true; + public bool IsLast = true; } diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegScanInfo.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegScanInfo.cs new file mode 100644 index 0000000000..617f0c7357 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegScanInfo.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; + +// We may need to get a ref to fields, so don't +// make these properties. +#pragma warning disable SA1401 // Fields should be private + +internal sealed class JpegScanInfo +{ + // Variables copied from ITU-T T.81 spec + + /// + /// Start of spectral band in zigzag sequence + /// + public int Ss; + + /// + /// End of spectral band in zigzag sequence + /// + public int Se; + + /// + /// Successive approximation bit position. (High) + /// + public int Ah; + + /// + /// Successive approximation bit position. (Low) + /// + public int Al; + + public int NumComponents; + + public InlineArray4 Components; + + public int LastNeededPass; + + public List ResetPoints { get; set; } = []; + + public List ExtraZeroRuns { get; set; } = []; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 8947b0f142..5b58a19128 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO; using SixLabors.ImageSharp.Formats.Jxl.IO.Container; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.IO; @@ -2241,7 +2242,7 @@ public int ProcessBoxes(Stream stream) if (this.reconstructionOutputJpeg == JpegReconstructionStage.SetMetadata && this.JbrdNeedsMoreBoxes()) { - JxlJpegData jpegData = this.imageBundle!.JpegData.GetData(); + JpegData jpegData = this.imageBundle!.JpegData.GetData(); if (this.reconstructionExifSize > 0) { @@ -2580,7 +2581,7 @@ public int ProcessBoxes(Stream stream) if (reconstructionResult == JxlDecoderStatus.JpegReconstruction) { - JxlJpegData jpegData = this.jpegDecoder!.GetJpegData(); + JpegData jpegData = this.jpegDecoder!.GetJpegData(); long numExif = JxlToJpegDecoder.NumExifMarkers(jpegData); long numXmp = JxlToJpegDecoder.NumXmpMarkers(jpegData); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs index 48f84175b3..184feb6e74 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; +using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; @@ -79,7 +80,7 @@ public JxlImageBundle() /// /// Gets or sets the JPEG data if the input image was converted to JPEG XL from a JPEG. /// - public JxlJpegData? JpegData { get; set; } + public JpegData? JpegData { get; set; } /// /// Gets a value indicating whether returns the image does or will represent quantized DCT-8 coefficients From bc7e30e32da7a27ac90d9e13f494e2caedb98a24 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:27:46 +0400 Subject: [PATCH 117/142] Refactor & optimize --- src/ImageSharp/Common/Helpers/Numerics.cs | 9 ----- .../AuxiliaryOutput/JxlAuxiliaryOutput.cs | 12 +++++- .../Encoder/JxlFastLosslessEncoder.cs | 2 +- .../Formats/Jxl/Processing/JxlMath.cs | 40 ++++++------------- .../Modular/Transforms/JxlPalette.cs | 2 +- .../Modular/Transforms/JxlSqueeze.cs | 23 +++++++---- .../Processing/RenderPipeline/Epf0Stage.cs | 1 - .../RenderPipelineStageConfiguration.cs | 2 +- 8 files changed, 41 insertions(+), 50 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/Numerics.cs b/src/ImageSharp/Common/Helpers/Numerics.cs index a5a5571795..e5a6b45493 100644 --- a/src/ImageSharp/Common/Helpers/Numerics.cs +++ b/src/ImageSharp/Common/Helpers/Numerics.cs @@ -1033,13 +1033,4 @@ public static nuint Vector512Count(this ReadOnlySpan span) public static nuint Vector512Count(int length) where TVector : struct => (uint)length / (uint)Vector512.Count; - - /// - /// Computes the average of two integers. - /// - /// First integer - /// Second integer - /// The average of x, y. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int Average(int x, int y) => (x + y + ((x > y) ? 1 : 0)) >> 1; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutput.cs index 6a34aabdc4..d39b707bc2 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutput.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/AuxiliaryOutput/JxlAuxiliaryOutput.cs @@ -67,6 +67,16 @@ public void Assimilate(JxlAuxiliaryOutput victim) this.NumberOfBlocks += victim.NumberOfBlocks; this.NumberOfSmallBlocks += victim.NumberOfSmallBlocks; - + this.NumberOfDct4x8Blocks += victim.NumberOfDct4x8Blocks; + this.NumberOfAfvBlocks += victim.NumberOfAfvBlocks; + this.NumberOfDct8Blocks += victim.NumberOfDct8Blocks; + this.NumberOfDct8x16Blocks += victim.NumberOfDct8x16Blocks; + this.NumberOfDct8x32Blocks += victim.NumberOfDct8x32Blocks; + this.NumberOfDct16Blocks += victim.NumberOfDct16Blocks; + this.NumberOfDct16x32Blocks += victim.NumberOfDct16x32Blocks; + this.NumberOfDct32Blocks += victim.NumberOfDct32Blocks; + this.NumberOfDct32x64Blocks += victim.NumberOfDct32x64Blocks; + this.NumberOfDct64Blocks += victim.NumberOfDct64Blocks; + this.NumberOfButteraugliIterations += victim.NumberOfButteraugliIterations; } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs index 316f1ed12f..3feb60c72a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs @@ -163,7 +163,7 @@ internal interface IFjxlFrameInputSource : IDisposable /// A wrapper over the color data of the channel at the specified /// position. /// - public Span GetColorChannelData(int x, int y, int width, int height, out long rowOffset) + Span GetColorChannelData(int x, int y, int width, int height, out long rowOffset) where T : unmanaged; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs index 0157dba4e1..a0042c9c50 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs @@ -3,7 +3,6 @@ using System.Numerics; using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Common.Helpers; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; @@ -687,25 +686,7 @@ public static ulong CeilLog2Nonzero(ulong x) /// X /// Y /// Hypotenuse of x and y - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double Hypot(double x, double y) - { - x = Math.Abs(x); - y = Math.Abs(y); - - if (x < y) - { - RuntimeUtility.Swap(ref x, ref y); - } - - if (x == 0.0) - { - return 0.0; - } - - double ratio = y / x; - return x * Math.Sqrt(1 + (ratio * ratio)); - } + public static float Hypot(float x, float y) => Hypot(x, y); /// /// Computes the hypotenuse of x and y. @@ -713,23 +694,26 @@ public static double Hypot(double x, double y) /// X /// Y /// Hypotenuse of x and y + public static double Hypot(double x, double y) => Hypot(x, y); + [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float Hypot(float x, float y) + private static T Hypot(T x, T y) + where T : INumber, IRootFunctions { - x = MathF.Abs(x); - y = MathF.Abs(y); + x = T.Abs(x); + y = T.Abs(y); if (x < y) { - RuntimeUtility.Swap(ref x, ref y); + (y, x) = (x, y); } - if (x == 0.0f) + if (x == T.Zero) { - return 0.0f; + return T.Zero; } - float ratio = y / x; - return x * MathF.Sqrt(1 + (ratio * ratio)); + T ratio = y / x; + return x * T.Sqrt(T.One + (ratio * ratio)); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs index bfcd683c5b..4a41bc34c7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs @@ -492,7 +492,7 @@ private static float ColorDistance(Span a, InlineArray3 b) if (a.Length >= 3) { - ave3 = (a[0] + b[0] + a[1] + b[1] + a[2] + b[2]) * (1.21f / 3.0f); + ave3 = ((a[0] + b[0]) + (a[1] + b[1]) + (a[2] + b[2])) * (1.21f / 3.0f); } float sumA = 0; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs index d21d0438c7..6888015825 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs @@ -27,6 +27,15 @@ internal static class JxlSqueeze { private const int MaxFirstPreviewSize = 8; + /// + /// Computes the average of two integers. + /// + /// First integer + /// Second integer + /// The average of x, y. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Average(int x, int y) => (x + y + ((x > y) ? 1 : 0)) >> 1; + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int SmoothTendency(int b, int a, int n) { @@ -534,10 +543,8 @@ private static void CheckMetaSqueezeParameters(in JxlSqueezeParameters parameter int c1 = parameter.BeginC; int c2 = parameter.BeginC + parameter.NumC - 1; - if (c1 < 0 || - c1 >= numChannels || - c2 < 0 || - c2 >= numChannels || + if ((uint)c1 >= numChannels || + (uint)c2 >= numChannels || c2 < c1) { throw new InvalidOperationException("Invalid channel range"); @@ -654,7 +661,7 @@ public static void ForwardHorizontalSqueeze(Configuration configuration, JxlModu int a = pIn[x2]; int b = pIn[x2 + 1]; - int avg = Numerics.Average(a, b); + int avg = Average(a, b); pOut[x] = avg; int diff = a - b; int nextAvg = avg; @@ -664,7 +671,7 @@ public static void ForwardHorizontalSqueeze(Configuration configuration, JxlModu int c2 = pIn[x2 + 2]; // actually C, but 1. variable 'c' already defined 2. names should be camelCase int d = pIn[x2 + 3]; - nextAvg = Numerics.Average(c2, d); + nextAvg = Average(c2, d); } else if ((inputChannel.Width & 1) != 0) { @@ -711,7 +718,7 @@ public static void ForwardVerticalSqueeze(Configuration configuration, JxlModula { int a = pIn[x]; int b = pIn[x + oneRowInput]; - int avg = Numerics.Average(a, b); + int avg = Average(a, b); pOut[x] = avg; int diff = a - b; int nextAvg = avg; @@ -720,7 +727,7 @@ public static void ForwardVerticalSqueeze(Configuration configuration, JxlModula { int c2 = pIn[x + (2 * oneRowInput)]; // actually C, but 1. variable 'c' already defined 2. names should be camelCase int d = pIn[x + (3 * oneRowInput)]; - nextAvg = Numerics.Average(c2, d); + nextAvg = Average(c2, d); } else if ((inputChannel.Height & 1) != 0) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs index 56f52fab8a..5df7525ea9 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs @@ -103,7 +103,6 @@ public override void ProcessRow(Buffer2D> inputRows, Buffer2D vsm = Vector256.Create(sadMul[ix..]); Vector256 inverseSigma = Vector256.Create(rowSigma[bx]) * vsm; - } } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageConfiguration.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageConfiguration.cs index acd7b8b765..282876b6bc 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageConfiguration.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageConfiguration.cs @@ -5,7 +5,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; -internal record struct RenderPipelineStageConfiguration(int BorderX, int BorderY, int ShiftX, int ShiftY) +internal readonly record struct RenderPipelineStageConfiguration(int BorderX, int BorderY, int ShiftX, int ShiftY) { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static RenderPipelineStageConfiguration CreateShiftX(int shift, int border) => new(border, 0, shift, 0); From 081384d8136588fe769e0b4b8bb0e6f3f7004411 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:48:53 +0400 Subject: [PATCH 118/142] Optimize --- .../Modular/Transforms/JxlPalette.cs | 21 +++++++++++-------- .../Modular/Transforms/JxlTransform.cs | 19 +++++++---------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs index 4a41bc34c7..63d0c0b081 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs @@ -658,6 +658,11 @@ private static void ForwardPaletteIteration( DebugGuard.MustBeGreaterThanOrEqualTo(beginC, input.MetaChannels, nameof(beginC)); int nb = endC - beginC + 1; // inclusive number of channels + // TODO: if this assert below triggers, increase nb to 6 and update + // the stack allocation for the 'tmp' variable below + // so the size is big enough. + DebugGuard.MustBeLessThanOrEqualTo(nb, 5, nameof(nb)); + JxlModularChannel beginCChannel = input.Channels[beginC]; int w = beginCChannel.Width; int h = beginCChannel.Height; @@ -1036,10 +1041,11 @@ private static void ForwardPaletteIteration( errorRow[2] = new(nb, w + 4); } - Span bestValue = stackalloc int[nb]; - Span idealResidual = stackalloc int[nb]; - Span quantizedValue = stackalloc int[nb]; - Span predictions = stackalloc int[nb]; + Span tmp = stackalloc int[32]; // Power of 2 + Span bestValue = tmp.Slice(0 * nb, nb); + Span idealResidual = tmp.Slice(1 * nb, nb); + Span quantizedValue = tmp.Slice(2 * nb, nb); + Span predictions = tmp.Slice(30 * nb, nb); // This is a temporary buffer, values are copied here. // It is so we can swap spans. Since spans are just a view @@ -1051,7 +1057,7 @@ private static void ForwardPaletteIteration( { for (int c = 0; c < nb; c++) { - p_in[c] = input.channel[begin_c + c].Row(y); + p_in[c] = input.channel[beginC + c].Row(y); if (lossy) p_quant[c] = quantized_input.channel[c].Row(y); } @@ -1076,10 +1082,7 @@ private static void ForwardPaletteIteration( bool best_is_delta = false; float best_distance = float.PositiveInfinity; - bestValue.Clear(); - idealResidual.Clear(); - quantizedValue.Clear(); - predictions.Clear(); + tmp.Clear(); foreach (double diffusion_multiplier in (Span)[0.55, 0.75]) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs index 75ba986086..cf3c2156f7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlTransform.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics.Tensors; using SixLabors.ImageSharp.Formats.Jxl.Fields; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; @@ -46,19 +47,13 @@ public static void ComputeMinMax(JxlModularChannel channel, out int min, out int for (int y = 0; y < channel.Height; y++) { - Span p = channel.GetRow(y); - for (int x = 0; x < channel.Width; x++) - { - if (p[x] < min) - { - min = p[x]; - } + ReadOnlySpan p = channel.GetRow(y); - if (p[x] > max) - { - max = p[x]; - } - } + int minRow = TensorPrimitives.Min(p); + int maxRow = TensorPrimitives.Max(p); + + min = Math.Min(minRow, min); + max = Math.Max(maxRow, max); } } } From 80f107591e29d3eb63f5869c8415820178b6ca34 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:50:50 +0400 Subject: [PATCH 119/142] Make interface an abstract class --- .../Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs index 3feb60c72a..91263989ef 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs @@ -147,8 +147,11 @@ internal sealed class JxlFastLosslessEncoder /// /// Abstracts access to a raster frame data required for encoding. /// - internal interface IFjxlFrameInputSource : IDisposable + internal abstract class IFjxlFrameInputSource : IDisposable { + /// + public abstract void Dispose(); + /// /// Returns a span that wraps over channel color data at the /// specified rectangular position. @@ -163,7 +166,7 @@ internal interface IFjxlFrameInputSource : IDisposable /// A wrapper over the color data of the channel at the specified /// position. /// - Span GetColorChannelData(int x, int y, int width, int height, out long rowOffset) + public abstract Span GetColorChannelData(int x, int y, int width, int height, out long rowOffset) where T : unmanaged; } From fad73d8cfdaa84dded8f2c0501642be08820c15d Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:37:00 +0400 Subject: [PATCH 120/142] Add encoder ANS SIMD bit cost estimation --- .../Entropy/JxlAnsHybridUIntConfiguration.cs | 2 +- .../Jxl/Processing/Encoder/Ans/JxlAnsSimd.cs | 275 ++++++++++++++++++ .../Jxl/Processing/JxlCoefficientOrder.cs | 6 +- 3 files changed, 277 insertions(+), 6 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsSimd.cs diff --git a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs index c1d6409076..9ff4306522 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs @@ -31,7 +31,7 @@ public JxlAnsHybridUIntConfiguration(uint splitExponent = 4, uint msbInToken = 2 public uint LsbMask => (1u << (int)this.LsbInToken) - 1; - public void Encode(uint value, ref uint token, ref uint bitCount, ref uint bits) + public void Encode(uint value, out uint token, out uint bitCount, out uint bits) { if (value < this.SplitToken) { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsSimd.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsSimd.cs new file mode 100644 index 0000000000..e23bc27009 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlAnsSimd.cs @@ -0,0 +1,275 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; + +/// +/// SIMD utilities for ANS entropy encoder +/// +internal static class JxlAnsSimd +{ + private static readonly Vector IotaOffsets = CreateIotaOffsets(); + + private static Vector CreateIotaOffsets() + { + Span values = stackalloc uint[Vector.Count]; + + for (int i = 0; i < values.Length; i++) + { + values[i] = (uint)i; + } + + return new Vector(values); + } + + /// + /// Adds continuously incrementing numbers to the vector. + /// + /// The input vector. + /// vec + [ 1, 2, 3, 4, 5, ... ] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector Iota(uint vec) => Vector.Create(vec) + IotaOffsets; + + private static uint EstimateTokenCostImpl(uint e, uint m, uint l, ref uint values, int len, ref uint output) + { + Vector split = Vector.Create(1u << (int)e); + Vector expOffset = Vector.Create(127u); + Vector ebOffset = Vector.Create(127u + m + l); + Vector @base = Vector.Create((1u << (int)e) - (e << (int)(m + l))); + Vector mulN = Vector.Create(1u << (int)(m + l)); + Vector maskL = Vector.Create((1u << (int)l) - 1); + Vector maskM = Vector.Create(((1u << (int)m) - 1) << (int)l); + Vector largeThreshold = Vector.Create((1u << 2) - 1); + const uint largeShiftVal = 10; + Vector largeShift = Vector.Create(largeShiftVal); + + Vector extraBits = Vector.Zero; + int lastFull = Vector.Count * (len / Vector.Count); + + for (int i = 0; i < lastFull; i += Vector.Count) + { + Vector val = Vector.LoadUnsafe(ref Unsafe.Add(ref values, i)); + Vector isLarge = Vector.GreaterThan(val, largeThreshold); + Vector valShifted = Vector.ShiftRightLogical(val, (int)largeShiftVal); + Vector notLiteral = Vector.GreaterThanOrEqual(val, split); + Vector valFixed = Vector.ConditionalSelect(isLarge, valShifted, val); + Vector L = val & maskL; + Vector exp = Vector.ShiftRightLogical(valFixed, 23); + Vector expFixed = Vector.ConditionalSelect(isLarge, exp + largeShift, exp); + Vector n = expFixed - expOffset; + Vector eb = expFixed - ebOffset; + Vector M = Vector.ShiftRightLogical(valFixed, (int)(23 - m - l)); + Vector a = @base + (n * mulN); + Vector d = M & maskM; + Vector ebFixed = Vector.ConditionalSelect(notLiteral, eb, Vector.Zero); + Vector c = a | L; + extraBits += ebFixed; + Vector t = c | d; + Vector tFixed = Vector.ConditionalSelect(notLiteral, t, val); + tFixed.StoreUnsafe(ref Unsafe.Add(ref output, i)); + } + + if (lastFull < len) + { + Vector stop = Vector.Create((uint)len); + Vector fence = Iota((uint)lastFull); + Vector take = Vector.LessThan(fence, stop); + Vector val = Vector.LoadUnsafe(ref Unsafe.Add(ref values, lastFull)); + Vector isLarge = Vector.GreaterThan(val, largeThreshold); + Vector valShifted = Vector.ShiftRightLogical(val, (int)largeShiftVal); + Vector notLiteral = Vector.GreaterThanOrEqual(val, split); + Vector valFixed = Vector.ConditionalSelect(isLarge, valShifted, val); + Vector L = val | maskL; + Vector exp = Vector.ShiftRightLogical(valFixed, 23); + Vector exp_fixed = Vector.ConditionalSelect(isLarge, exp + largeShift, exp); + Vector n = exp_fixed - expOffset; + Vector eb = exp_fixed - ebOffset; + Vector M = Vector.ShiftRightLogical(valFixed, 23); + Vector a = @base + (n * mulN); + Vector d = M & maskM; + Vector ebFixed = Vector.ConditionalSelect(notLiteral, eb, Vector.Zero); + Vector ebMasked = Vector.ConditionalSelect(take, ebFixed, Vector.Zero); + Vector c = a | L; + extraBits += ebMasked; + Vector t = c | d; + Vector tFixed = Vector.ConditionalSelect(notLiteral, t, val); + tFixed.StoreUnsafe(ref Unsafe.Add(ref output, lastFull)); + } + + return Vector.Sum(extraBits); + } + + public static uint EstimateTokenCost(ref uint values, int len, JxlAnsHybridUIntConfiguration cfg, ref uint tokens) + { + if (!Vector.IsHardwareAccelerated) + { + // No SIMD support + uint extraBits = 0; + + for (int i = 0; i < len; i++) + { + uint v = Unsafe.Add(ref values, i); + cfg.Encode(v, out uint tok, out uint nbits, out _); // Last parameter is bits + extraBits += nbits; + Unsafe.Add(ref tokens, i) = tok; + } + + return extraBits; + } + else + { + // Have SIMD support + if (cfg.SplitExponent == 0) + { + return EstimateTokenCostImpl(0, 0, 0, ref values, len, ref tokens); + } + else if (cfg.SplitExponent == 2) + { + return EstimateTokenCostImpl(2, 0, 1, ref values, len, ref tokens); + } + else if (cfg.SplitExponent == 3) + { + if (cfg.MsbInToken == 1) + { + if (cfg.LsbInToken == 0) + { + return EstimateTokenCostImpl(3, 1, 0, ref values, len, ref tokens); + } + else + { + return EstimateTokenCostImpl(3, 1, 2, ref values, len, ref tokens); + } + } + else + { + if (cfg.LsbInToken == 0) + { + return EstimateTokenCostImpl(3, 2, 0, ref values, len, ref tokens); + } + else + { + return EstimateTokenCostImpl(3, 2, 1, ref values, len, ref tokens); + } + } + } + else if (cfg.SplitExponent == 4) + { + if (cfg.MsbInToken == 1) + { + if (cfg.LsbInToken == 0) + { + return EstimateTokenCostImpl(4, 1, 0, ref values, len, ref tokens); + } + else if (cfg.LsbInToken == 2) + { + return EstimateTokenCostImpl(4, 1, 2, ref values, len, ref tokens); + } + else + { + return EstimateTokenCostImpl(4, 1, 3, ref values, len, ref tokens); + } + } + else + { + if (cfg.LsbInToken == 0) + { + return EstimateTokenCostImpl(4, 2, 0, ref values, len, ref tokens); + } + else if (cfg.LsbInToken == 1) + { + return EstimateTokenCostImpl(4, 2, 1, ref values, len, ref tokens); + } + else + { + return EstimateTokenCostImpl(4, 2, 2, ref values, len, ref tokens); + } + } + } + else if (cfg.SplitExponent == 5) + { + if (cfg.MsbInToken == 1) + { + if (cfg.LsbInToken == 0) + { + return EstimateTokenCostImpl(5, 1, 0, ref values, len, ref tokens); + } + else if (cfg.LsbInToken == 2) + { + return EstimateTokenCostImpl(5, 1, 2, ref values, len, ref tokens); + } + else + { + return EstimateTokenCostImpl(5, 1, 4, ref values, len, ref tokens); + } + } + else + { + if (cfg.LsbInToken == 0) + { + return EstimateTokenCostImpl(5, 2, 0, ref values, len, ref tokens); + } + else if (cfg.LsbInToken == 1) + { + return EstimateTokenCostImpl(5, 2, 1, ref values, len, ref tokens); + } + else if (cfg.LsbInToken == 2) + { + return EstimateTokenCostImpl(5, 2, 2, ref values, len, ref tokens); + } + else + { + return EstimateTokenCostImpl(5, 2, 3, ref values, len, ref tokens); + } + } + } + else if (cfg.SplitExponent == 6) + { + if (cfg.MsbInToken == 0) + { + return EstimateTokenCostImpl(6, 0, 0, ref values, len, ref tokens); + } + else if (cfg.MsbInToken == 1) + { + return EstimateTokenCostImpl(6, 1, 5, ref values, len, ref tokens); + } + else + { + return EstimateTokenCostImpl(6, 2, 4, ref values, len, ref tokens); + } + } + else if (cfg.SplitExponent is >= 7 and <= 12) + { + if (cfg.SplitExponent == 7) + { + return EstimateTokenCostImpl(7, 0, 0, ref values, len, ref tokens); + } + else if (cfg.SplitExponent == 8) + { + return EstimateTokenCostImpl(8, 0, 0, ref values, len, ref tokens); + } + else if (cfg.SplitExponent == 9) + { + return EstimateTokenCostImpl(9, 0, 0, ref values, len, ref tokens); + } + else if (cfg.SplitExponent == 10) + { + return EstimateTokenCostImpl(10, 0, 0, ref values, len, ref tokens); + } + else if (cfg.SplitExponent == 11) + { + return EstimateTokenCostImpl(11, 0, 0, ref values, len, ref tokens); + } + else + { + return EstimateTokenCostImpl(12, 0, 0, ref values, len, ref tokens); + } + } + + return ~0u; + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs index deb5045f2a..4cace39d73 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs @@ -45,11 +45,7 @@ internal static class JxlCoefficientOrder [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint CoeffOrderContext(uint value) { - uint token = 0; - uint nbits = 0; - uint bits = 0; - - new JxlAnsHybridUIntConfiguration(0, 0, 0).Encode(value, ref token, ref nbits, ref bits); + new JxlAnsHybridUIntConfiguration(0, 0, 0).Encode(value, out uint token, out uint nbits, out uint bits); return Math.Min(token, PermutationContexts - 1u); } From 64bade82be47caca243d0a794734b2562eb788a1 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:18:09 +0400 Subject: [PATCH 121/142] Add huffman encoder, inverse Gaborish transform, gamma correction, noise encoder tools, and complete render pipeline EPF 0 stage Files implemented: - enc_gaborish.cc - enc_gaborish.h - enc_gamma_correct.h - enc_huffman.cc - enc_huffman.h - enc_huffman_tree.cc - enc_huffman_tree.h - enc_noise.cc - enc_noise.h - render_pipeline/stage_epf.cc - render_pipeline/stage_epf.h --- .../Formats/Jxl/Memory/JxlImage3{T}.cs | 6 + .../Encoder/Huffman/JxlHuffmanEncoder.cs | 226 ++++++++++ .../Encoder/Huffman/JxlHuffmanTree.cs | 411 ++++++++++++++++++ .../Jxl/Processing/Encoder/JxlGaborish.cs | 69 +++ .../Jxl/Processing/Encoder/JxlGammaCorrect.cs | 51 +++ .../Encoder/Noise/JxlLossFunction.cs | 63 +++ .../Encoder/Noise/JxlNoiseEncoder.cs | 66 +++ .../Encoder/Noise/JxlNoiseHistogram.cs | 87 ++++ .../Formats/Jxl/Processing/JxlConvolve.cs | 19 +- .../Primitives/JxlWeightsSymmetric5.cs | 71 +-- .../Processing/Primitives/RectangleUtils.cs | 25 ++ .../Processing/RenderPipeline/Epf0Stage.cs | 63 ++- 12 files changed, 1116 insertions(+), 41 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Huffman/JxlHuffmanEncoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Huffman/JxlHuffmanTree.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGammaCorrect.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlLossFunction.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Primitives/RectangleUtils.cs diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs index 91aed2e97a..4442eb6495 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs @@ -94,6 +94,12 @@ private void PlaneRowBoundsCheck(int c, int y) DebugGuard.MustBeLessThan(y, this.YSize, nameof(y)); } + /// + /// Returns the rectangle for this image bounds. + /// + /// A rectangle with x,y=0,0 width,height=XSize,YSize. + public Rectangle GetRectangle() => new(0, 0, this.XSize, this.YSize); + public void Dispose() { foreach (JxlPlane plane in this.planes) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Huffman/JxlHuffmanEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Huffman/JxlHuffmanEncoder.cs new file mode 100644 index 0000000000..7caf854655 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Huffman/JxlHuffmanEncoder.cs @@ -0,0 +1,226 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Huffman; + +/// +/// Derives & writes Huffman codes. +/// +internal static class JxlHuffmanEncoder +{ + private const int CodeLengthCodes = 18; + + private static ReadOnlySpan StorageOrder => [1, 2, 3, 4, 0, 5, 17, 6, 16, 7, 8, 9, 10, 11, 12, 13, 14, 15]; + + private static ReadOnlySpan HuffmanBitLengthHuffmanCodeSymbols => [0, 7, 3, 2, 1, 15]; + + private static ReadOnlySpan HuffmanBitLengthHuffmanCodeBitLengths => [2, 4, 3, 2, 2, 4]; + + public static void StoreHuffmanTreeOfHuffmanTreeToBitMask(int numCodes, Span codeLengthBitDepth, JxlBitWriter writer) + { + int codesToStore = CodeLengthCodes; + if (numCodes > 1) + { + for (; codesToStore > 0; codesToStore--) + { + if (codeLengthBitDepth[StorageOrder[codesToStore - 1]] != 0) + { + break; + } + } + } + + int skipSome = 0; + if (codeLengthBitDepth[StorageOrder[0]] == 0 && codeLengthBitDepth[StorageOrder[1]] == 0) + { + skipSome = 2; // skips two + if (codeLengthBitDepth[StorageOrder[2]] == 0) + { + skipSome = 3; // skips three + } + } + + writer.Write(2, skipSome); + + for (int i = skipSome; i < codesToStore; ++i) + { + int l = codeLengthBitDepth[StorageOrder[i]]; + writer.Write(HuffmanBitLengthHuffmanCodeBitLengths[l], HuffmanBitLengthHuffmanCodeSymbols[l]); + } + } + + public static void StoreHuffmanTreeToBitMask(int huffmanTreeSize, Span huffmanTree, Span huffmanTreeExtraBits, Span codeLengthBitDepth, Span codeLengthBitDepthSymbols, JxlBitWriter writer) + { + for (int i = 0; i < huffmanTreeSize; ++i) + { + int ix = huffmanTree[i]; + writer.Write(codeLengthBitDepth[ix], codeLengthBitDepthSymbols[ix]); + DebugGuard.MustBeLessThan(ix, 17, nameof(ix)); + + // Extra bits + // + // Micro optimization: + // Original: + // switch (ix) + // { + // case 16: + // writer->Write(2, huffman_tree_extra_bits[i]); + // break; + // case 17: + // writer->Write(3, huffman_tree_extra_bits[i]); + // break; + // default: + // // no-op + // break; + // } + if ((ix & 16) != 0) + { + writer.Write(2 + (ix & 1), huffmanTreeExtraBits[i]); + } + } + } + + public static void StoreSimpleHuffmanTree(Span depths, InlineArray4 symbols, int numSymbols, int maxBits, JxlBitWriter writer) + { + writer.Write(2, 1); + writer.Write(2, numSymbols - 1); + + for (int i = 0; i < numSymbols; i++) + { + for (int j = i + 1; j < numSymbols; j++) + { + if (depths[symbols[j]] < depths[symbols[i]]) + { + RuntimeUtility.Swap(ref symbols[j], ref symbols[i]); + } + } + } + + if (numSymbols == 2) + { + writer.Write(maxBits, symbols[0]); + writer.Write(maxBits, symbols[1]); + } + else if (numSymbols == 3) + { + writer.Write(maxBits, symbols[0]); + writer.Write(maxBits, symbols[1]); + writer.Write(maxBits, symbols[2]); + } + else + { + writer.Write(maxBits, symbols[0]); + writer.Write(maxBits, symbols[1]); + writer.Write(maxBits, symbols[2]); + writer.Write(maxBits, symbols[3]); + writer.Write(1, depths[symbols[0]] == 1 ? 1 : 0); + } + } + + public static void StoreHuffmanTree(Span depths, int num, JxlBitWriter writer) + { + Span arena = stackalloc byte[2 * num]; + Span huffmanTree = arena; + Span huffmanTreeExtraBits = arena[num..]; + int huffmanTreeSize = 0; + JxlHuffmanTree.WriteHuffmanTree(depths, num, ref huffmanTreeSize, huffmanTree, huffmanTreeExtraBits); + + Span huffmanTreeHistogram = stackalloc int[CodeLengthCodes]; + huffmanTreeHistogram.Clear(); + + for (int i = 0; i < huffmanTreeSize; ++i) + { + huffmanTreeHistogram[huffmanTree[i]]++; + } + + int numCodes = 0; + int code = 0; + + for (int i = 0; i < CodeLengthCodes; ++i) + { + if (huffmanTreeHistogram[i] != 0) + { + if (numCodes == 0) + { + code = i; + numCodes = 1; + } + else if (numCodes == 1) + { + numCodes = 2; + break; + } + } + } + + Span codeLengthBitDepth = stackalloc byte[CodeLengthCodes]; + Span codeLengthBitDepthSymbols = stackalloc short[CodeLengthCodes]; + codeLengthBitDepth.Clear(); + codeLengthBitDepthSymbols.Clear(); + + JxlHuffmanTree.CreateHuffmanTree(huffmanTreeHistogram, CodeLengthCodes, 5, codeLengthBitDepth); + JxlHuffmanTree.ConvertBitDepthsToSymbols(codeLengthBitDepth, CodeLengthCodes, codeLengthBitDepthSymbols); + + StoreHuffmanTreeOfHuffmanTreeToBitMask(numCodes, codeLengthBitDepth, writer); + + if (numCodes == 1) + { + codeLengthBitDepth[code] = 0; + } + + StoreHuffmanTreeToBitMask(huffmanTreeSize, huffmanTree, huffmanTreeExtraBits, codeLengthBitDepth, codeLengthBitDepthSymbols, writer); + } + + public static void BuildAndStoreHuffmanTree(Span histogram, int length, Span depth, Span bits, JxlBitWriter writer) + { + int count = 0; + InlineArray4 s4 = default; + + for (int i = 0; i < length; i++) + { + if (histogram[i] != 0) + { + if (count < 4) + { + s4[count] = i; + } + else if (count > 4) + { + break; + } + + count++; + } + } + + int maxBitsCounter = length - 1; + int maxBits = 0; + + while (maxBitsCounter != 0) + { + maxBitsCounter >>= 1; + ++maxBits; + } + + if (count <= 1) + { + writer.Write(4, 1); + writer.Write(maxBits, s4[0]); + return; + } + + JxlHuffmanTree.CreateHuffmanTree(histogram, length, 15, depth); + JxlHuffmanTree.ConvertBitDepthsToSymbols(depth, length, bits); + + if (count <= 4) + { + StoreSimpleHuffmanTree(depth, s4, count, maxBits, writer); + } + else + { + StoreHuffmanTree(depth, length, writer); + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Huffman/JxlHuffmanTree.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Huffman/JxlHuffmanTree.cs new file mode 100644 index 0000000000..0e3faa65aa --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Huffman/JxlHuffmanTree.cs @@ -0,0 +1,411 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Numerics.Tensors; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Huffman; + +/// +/// Node of a Huffman tree. +/// +internal struct JxlHuffmanTree(int count, short left, short right) +{ + public int TotalCount = count; + + /// + /// Index of the left node of the tree. + /// + public short IndexLeft = left; + + /// + /// Index of the right node of the tree. If it's missing + /// then this is the value of the node. + /// + public short IndexRightOrValue = right; + + /// + /// Gets a lookup table with pre-reversed 4-bit values. + /// This lookup is used by . + /// + private static ReadOnlySpan ReverseLookup => + [ + 0x0, 0x8, 0x4, 0xc, 0x2, 0xa, 0x6, 0xe, + 0x1, 0x9, 0x5, 0xd, 0x3, 0xb, 0x7, 0xf + ]; + + public static void SetDepth(ref JxlHuffmanTree p, Span pool, Span depth, byte level) + { + if (p.IndexLeft >= 0) + { + level++; + SetDepth(ref pool[p.IndexLeft], pool, depth, level); + SetDepth(ref pool[p.IndexRightOrValue], pool, depth, level); + } + else + { + depth[p.IndexRightOrValue] = level; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Compare(JxlHuffmanTree v0, JxlHuffmanTree v1) + { + if (v0.TotalCount != v1.TotalCount) + { + return v0.TotalCount.CompareTo(v1.TotalCount); + } + + return v0.IndexRightOrValue.CompareTo(v1.IndexRightOrValue); + } + + public static void CreateHuffmanTree(Span data, int length, int treeLimit, Span depth) + { + JxlHuffmanTree[]? pool = null; + + // This is basically the equivalent of List but is fixed-size. + // We don't need an entire collection on the heap. + int desiredTreeItems = (2 * length) + 1; + Span tree = + desiredTreeItems <= 256 + ? stackalloc JxlHuffmanTree[256].Slice(0, desiredTreeItems) + : pool = ArrayPool.Shared.Rent(desiredTreeItems); + + // Number of items in our "fixed List". So in other to + // add to the tree we do 'tree[treeRef++] = ...'. + int treeRef = 0; + + for (int countLimit = 1; ; countLimit *= 2) + { + tree.Clear(); // Always clear on every iteration + + int i = length; + for (; i != 0;) + { + --i; + if (data[i] != 0) + { + int count = Math.Max(data[i], countLimit - 1); + tree[treeRef++] = new JxlHuffmanTree(count, -1, (short)i); + } + } + + if (treeRef == 1) + { + // Fake value; will be fixed on upper level. + depth[tree[0].IndexRightOrValue] = 1; + break; + } + + tree.Sort(Compare); + + JxlHuffmanTree sentinel = new(int.MaxValue, -1, -1); + tree[treeRef++] = sentinel; + tree[treeRef++] = sentinel; // We do this twice, yes + + i = 0; + int j = treeRef + 1; + + for (int k = treeRef - 1; k != 0; --k) + { + int left; + int right; + + if (tree[i].TotalCount <= tree[j].TotalCount) + { + left = i; + i++; + } + else + { + left = j; + j++; + } + + if (tree[i].TotalCount <= tree[j].TotalCount) + { + right = i; + i++; + } + else + { + right = j; + j++; + } + + int j_end = treeRef - 1; + ref JxlHuffmanTree currTree = ref tree[j_end]; + currTree.TotalCount = tree[left].TotalCount + tree[right].TotalCount; + currTree.IndexLeft = (short)left; + currTree.IndexRightOrValue = (short)right; + + tree[treeRef++] = sentinel; + } + + SetDepth(ref tree[(2 * treeRef) - 1], tree, depth, 0); + + if (TensorPrimitives.Max((ReadOnlySpan)depth[..length]) <= treeLimit) + { + break; + } + } + + // Don't forget to return the pooled array + if (pool is not null) + { + ArrayPool.Shared.Return(pool); + } + } + + public static void Reverse(Span v, int start, int end) + { + end--; + while (start < end) + { + RuntimeUtility.Swap(ref v[end], ref v[start]); + start++; + end++; + } + } + + public static void WriteHuffmanTreeRepetitions(byte previousValue, byte value, int repetitions, ref int treeSize, Span tree, Span extraBitsData) + { + DebugGuard.MustBeGreaterThan(repetitions, 0, nameof(repetitions)); + + if (previousValue != value) + { + tree[treeSize] = value; + extraBitsData[treeSize] = 0; + treeSize++; + repetitions--; + } + + if (repetitions == 7) + { + tree[treeSize] = value; + extraBitsData[treeSize] = 0; + treeSize++; + } + + if (repetitions < 3) + { + for (int i = 0; i < repetitions; ++i) + { + tree[treeSize] = value; + extraBitsData[treeSize] = 0; + treeSize++; + } + } + else + { + repetitions -= 3; + int start = treeSize; + while (true) + { + tree[treeSize] = 16; + extraBitsData[treeSize] = (byte)(repetitions & 0x3); + treeSize++; + repetitions >>= 2; + + if (repetitions == 0) + { + break; + } + + repetitions--; + } + + Reverse(tree, start, treeSize); + Reverse(extraBitsData, start, treeSize); + } + } + + public static void WriteHuffmanTreeRepetitionsZeros(int repetitions, ref int treeSize, Span tree, Span extraBitsData) + { + if (repetitions == 11) + { + tree[treeSize] = 0; + extraBitsData[treeSize] = 0; + treeSize++; + repetitions--; + } + + if (repetitions < 3) + { + for (int i = 0; i < repetitions; ++i) + { + tree[treeSize] = 0; + extraBitsData[treeSize] = 0; + treeSize++; + } + } + else + { + repetitions -= 3; + int start = treeSize; + + while (true) + { + tree[treeSize] = 17; + extraBitsData[treeSize] = (byte)(repetitions & 0x7); + treeSize++; + repetitions >>= 3; + + if (repetitions == 0) + { + break; + } + + repetitions--; + } + + Reverse(tree, start, treeSize); + Reverse(extraBitsData, start, treeSize); + } + } + + // Decides whether or not to use Run Length Encoding (RLE). + // Basically that's where, for example, when we have a + // string of repetitive letters "aaaaaa", instead of encoding + // them all separately, it encodes "a times 6". + public static void DecideOverRleUse(Span depth, int length, ref bool useRleForNonZero, ref bool useRleForZero) + { + int totalRepsZero = 0; + int totalRepsNonZero = 0; + int countRepsZero = 1; + int countRepsNonZero = 1; + + for (int i = 0; i < length;) + { + byte value = depth[i]; + int reps = 1; + + for (int k = i + 1; k < length && depth[k] == value; k++) + { + reps++; + } + + if (reps >= 3 && value == 0) + { + totalRepsZero += reps; + countRepsZero++; + } + + if (reps >= 4 && value != 0) + { + totalRepsNonZero += reps; + countRepsNonZero++; + } + + i += reps; + } + + useRleForNonZero = totalRepsNonZero > countRepsNonZero * 2; + useRleForZero = totalRepsZero > countRepsZero * 2; + } + + public static void WriteHuffmanTree(Span depth, int length, ref int treeSize, Span tree, Span extraBitsData) + { + byte previousValue = 8; + int newLength = length; + + for (int i = 0; i < length; i++) + { + if (depth[length - i - 1] == 0) + { + newLength--; + } + else + { + break; + } + } + + bool useRleForNonZeroes = false; + bool useRleForZero = false; + + if (length > 50) + { + DecideOverRleUse(depth, newLength, ref useRleForNonZeroes, ref useRleForZero); + } + + for (int i = 0; i < newLength;) + { + byte value = depth[i]; + int reps = 1; + + if ((value != 0 && useRleForNonZeroes) || (value == 0 && useRleForZero)) + { + for (int k = i + 1; k < newLength && depth[k] == value; k++) + { + reps++; + } + } + + if (value == 0) + { + WriteHuffmanTreeRepetitionsZeros(reps, ref treeSize, tree, extraBitsData); + } + else + { + WriteHuffmanTreeRepetitions(previousValue, value, reps, ref treeSize, tree, extraBitsData); + previousValue = value; + } + + i += reps; + } + } + + public static short ReverseBits(int numBits, short bits) + { + int result = ReverseLookup[bits & 0xf]; + + for (int i = 4; i < numBits; i += 4) + { + result <<= 4; + bits = (short)(bits >> 4); + result |= ReverseLookup[bits & 0xf]; + } + + result >>= -numBits & 0x3; + + return (short)result; + } + + public static void ConvertBitDepthsToSymbols(Span depth, int len, Span bits) + { + // In Brotli, all bit depths are [1..15] + // 0 bit depth means that the symbol does not exist. + const int maxBits = 16; // 0..15 are values for bits + + Span blCount = stackalloc short[maxBits]; + blCount.Clear(); // explicitly cleared from reference + + for (int i = 0; i < len; i++) + { + blCount[depth[i]]++; + } + + blCount[0] = 0; + + Span nextCode = stackalloc short[maxBits]; // not cleared in reference + nextCode[0] = 0; + + int code = 0; + for (int i = 1; i < maxBits; ++i) + { + code = (code + blCount[i - 1]) << 1; + nextCode[i] = (short)code; + } + + for (int i = 0; i < len; ++i) + { + if (depth[i] != 0) + { + bits[i] = ReverseBits(depth[i], nextCode[depth[i]]++); + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs new file mode 100644 index 0000000000..191e2c5a4a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs @@ -0,0 +1,69 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +/// +/// Gaborish transform +/// +internal static class JxlGaborish +{ + private static ReadOnlySpan GaborishLookup => [ + -0.09495815671340026f, -0.041031725066768575f, 0.013710004822696948f, + 0.006510206083837737f, -0.0014789063378272242f]; + + public static void InverseGaborish(Configuration configuration, JxlImage3F inOut, Rectangle rect, InlineArray3 mul) + { + InlineArray3 weights = default; + + for (int i = 0; i < 3; ++i) + { + double sum = 1.0 + (mul[i] * 4 * ((GaborishLookup[0] + GaborishLookup[1]) + (GaborishLookup[2] + GaborishLookup[4]) + (2 * GaborishLookup[3]))); + sum = Math.Max(sum, 1e-5); // if (sum < 1e-5) sum = 1e-5 + + float normalize = (float)(1.0f / sum); + float normalizeMul = mul[i] * normalize; + + weights[i] = new JxlWeightsSymmetric5() + { + C = JxlWeightsSymmetric5.CreateVector4(normalize), + R = JxlWeightsSymmetric5.CreateVector4(normalizeMul * GaborishLookup[0]), + R2 = JxlWeightsSymmetric5.CreateVector4(normalizeMul * GaborishLookup[2]), + D = JxlWeightsSymmetric5.CreateVector4(normalizeMul * GaborishLookup[1]), + D2 = JxlWeightsSymmetric5.CreateVector4(normalizeMul * GaborishLookup[4]), + L = JxlWeightsSymmetric5.CreateVector4(normalizeMul * GaborishLookup[3]) + }; + } + + using JxlImageF temp = new(configuration, inOut.Plane(2).XSize, inOut.Plane(2).YSize); + + if (!JxlImageOperations.CopyImage(inOut.Plane(2), temp)) + { + throw new InvalidOperationException("Image copying failed"); + } + + Rectangle xRect = RectangleUtils.Extend(rect, 3, inOut.GetRectangle()); + + if (!JxlConvolve.Symmetric5(inOut.Plane(0), xRect, ref weights[0], inOut.Plane(2), xRect)) + { + throw new InvalidOperationException("Symmetric5 convolution failed"); + } + + if (!JxlConvolve.Symmetric5(inOut.Plane(1), xRect, ref weights[1], inOut.Plane(0), xRect)) + { + throw new InvalidOperationException("Symmetric5 convolution failed"); + } + + if (!JxlConvolve.Symmetric5(temp, xRect, ref weights[2], inOut.Plane(1), xRect)) + { + throw new InvalidOperationException("Symmetric5 convolution failed"); + } + + inOut.Plane(0).Swap(inOut.Plane(1)); + inOut.Plane(0).Swap(inOut.Plane(2)); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGammaCorrect.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGammaCorrect.cs new file mode 100644 index 0000000000..1b60ed38c8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGammaCorrect.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +internal static class JxlGammaCorrect +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double SRgb8ToLinearDirect(double srgb) + { + if (srgb <= 0.0) + { + return 0.0; + } + + if (srgb <= 0.04045) + { + return srgb / 12.92; + } + + if (srgb >= 1.0) + { + return 1.0; + } + + return Math.Pow((srgb + 0.055) / 1.055, 2.4); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double LinearToSRgb8Direct(double linear) + { + if (linear <= 0.0) + { + return 0.0; + } + + if (linear >= 1.0) + { + return 1.0; + } + + if (linear <= 0.0031308) + { + return linear * 12.92; + } + + return (Math.Pow(linear, 1.0 / 2.4) * 1.055) - 0.055; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlLossFunction.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlLossFunction.cs new file mode 100644 index 0000000000..e338862bb0 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlLossFunction.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Noise; + +internal sealed class JxlLossFunction(ReadOnlyMemory noiseLevels) +{ + public double Compute(Span w, Span df, bool skipRegularization = false) + { + const double reg = 0.005; + const double asym = 1.1; + + double lossFunction = 0; + + w.Clear(); + + ReadOnlySpan levels = noiseLevels.Span; + + for (int i = 0; i < levels.Length; i++) + { + JxlNoiseLevel nl = levels[i]; + + JxlNoiseIndexAndFraction pos = JxlNoiseHelper.IndexAndFraction(nl.Intensity); + + double low = w[pos.Index]; + double hi = w[pos.Index + 1]; + double val = (low * (1.0f - pos.Fraction)) + (hi * pos.Fraction); + double dist = val - nl.NoiseLevel; + + if (dist > 0) + { + lossFunction += asym * dist * dist; + df[pos.Index] -= asym * (1.0f - pos.Fraction) * dist; + df[pos.Index + 1] -= asym * pos.Fraction * dist; + } + else + { + lossFunction += dist * dist; + df[pos.Index] -= (1.0f - pos.Fraction) * dist; + df[pos.Index + 1] -= pos.Fraction * dist; + } + } + + if (skipRegularization) + { + return lossFunction; + } + + int levelsSize = levels.Length; + + for (int i = 0; i + 1 < w.Length; i++) + { + double diff = w[i] - w[i + 1]; + lossFunction += reg * levelsSize * diff * diff; + df[i] -= reg * diff * levelsSize; + df[i + 1] += reg * diff * levelsSize; + } + + return lossFunction; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs new file mode 100644 index 0000000000..026ee73f2f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseEncoder.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Numerics.Tensors; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Noise; + +/// +/// Noise functions for encoder. +/// +internal static class JxlNoiseEncoder +{ + public static float GetScoreSumsOfAbsoluteDifferences(JxlImage3F opsin, int x, int y, int blockSize) + { + const int smallBlockSizeX = 3; + const int smallBlockSizeY = 4; + + int numSAD = (blockSize - smallBlockSizeX) * (blockSize - smallBlockSizeY); + int counter = 0; + const int offset = 2; + + float[]? pooled = null; + + Span sad = numSAD <= 128 + ? stackalloc float[128].Slice(0, numSAD) + : pooled = ArrayPool.Shared.Rent(numSAD); + + for (int yBl = 0; yBl + smallBlockSizeY < blockSize; ++yBl) + { + for (int xBl = 0; xBl + smallBlockSizeX < blockSize; ++xBl) + { + float sadSum = 0; + + for (int cy = 0; cy < smallBlockSizeY; ++cy) + { + for (int cx = 0; cx < smallBlockSizeX; ++cx) + { + float wnd = 0.5f * (opsin.PlaneRow(1, y + yBl + cy)[x + xBl + cx] + opsin.PlaneRow(0, y + yBl + cy)[x + xBl + cx]); + float center = 0.5f * (opsin.PlaneRow(1, y + offset + cy)[x + offset + cx] + opsin.PlaneRow(0, y + offset + cy)[x + offset + cx]); + sadSum += MathF.Abs(center - wnd); + } + } + + sad[counter++] = sadSum; + } + } + + int samples = numSAD / 2; + + // As with ROAD (rank order absolute distance), we keep the smallest half of + // the values in SAD (we use here the more robust patch SAD instead of + // absolute single-pixel differences). + sad.Sort(); + + float totalSadSum = TensorPrimitives.Sum(sad); + + if (pooled is not null) + { + ArrayPool.Shared.Return(pooled); + } + + return totalSadSum / samples; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs new file mode 100644 index 0000000000..5cacaa5d02 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs @@ -0,0 +1,87 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Numerics.Tensors; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Noise; + +internal sealed class JxlNoiseHistogram +{ + private const int Bins = 256; + + private readonly uint[] bins = new uint[Bins]; + + public int Mode + { + get + { + int maxIdx = 0; + + for (int i = 0; i < Bins; i++) + { + if (this.bins[i] > this.bins[maxIdx]) + { + maxIdx = i; + } + } + + return maxIdx; + } + } + + /// + /// Gets the Inter-quartile range. + /// + public double Iqr => this.Quantile(0.75) - this.Quantile(0.25); + + public void Increment(float x) => this.bins[Index(x)]++; + + public uint Get(float x) => this.bins[Index(x)]; + + public uint Bin(int bin) => this.bins[bin]; + + public double Quantile(double q01) + { + long total = 1 + TensorPrimitives.Sum((ReadOnlySpan)this.bins.AsSpan()); + long target = (long)q01 * total; + long sum = 0; + int i = 0; + + for (; i < Bins; i++) + { + sum += this.bins[i]; + + if (sum == target) + { + return i + 0.5; + } + + if (sum > target) + { + break; + } + } + + int next = i + 1; + + while (next < Bins && this.bins[next] == 0) + { + next++; + } + + double excess = target - sum; + double weightNext = this.bins[Index(next)] / excess; + + return ClampX((next * weightNext) + (i * (1.0 - weightNext))); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T ClampX(T x) + where T : unmanaged, INumber + => T.Clamp(x, T.Zero, T.CreateSaturating(Bins - 1)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Index(float x) => ClampX((int)x); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs index ebe7f5195d..2baad82d6d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlConvolve.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; @@ -70,7 +71,7 @@ public static Vector WeightedSum( return sum2 + (sum1 + sum0); } - public static float Symmetric5Border(JxlImageF input, Func wrapY, long ix, long iy, JxlWeightsSymmetric5 weights) + public static float Symmetric5Border(JxlImageF input, Func wrapY, long ix, long iy, ref JxlWeightsSymmetric5 weights) { float w0 = weights.GetCVector()[0]; float w1 = weights.GetRVector()[0]; @@ -99,7 +100,7 @@ public static void Symmetric5Interior( Func wrapY, int rix, long iy, - JxlWeightsSymmetric5 weights, + ref JxlWeightsSymmetric5 weights, Span rowOut) { Vector w0 = LoadDuplicate128(weights.GetCVector()); // c @@ -126,7 +127,7 @@ public static void Symmetric5Row( Func wrapY, in Rectangle rect, long iy, - JxlWeightsSymmetric5 weights, + ref JxlWeightsSymmetric5 weights, Span rowOut) { const int radius = 2; @@ -140,25 +141,25 @@ public static void Symmetric5Row( for (; ix < Math.Min(alignedX, xEnd); ix++, rix++) { - rowOut[rix] = Symmetric5Border(image, wrapY, ix, iy, weights); + rowOut[rix] = Symmetric5Border(image, wrapY, ix, iy, ref weights); } for (; ix + n + radius <= xEnd; ix += n, rix += n) { - Symmetric5Interior(image, ix, wrapY, rix, iy, weights, rowOut); + Symmetric5Interior(image, ix, wrapY, rix, iy, ref weights, rowOut); } for (; ix < xEnd; ix++, rix++) { - rowOut[rix] = Symmetric5Border(image, wrapY, ix, iy, weights); + rowOut[rix] = Symmetric5Border(image, wrapY, ix, iy, ref weights); } } public static bool Symmetric5( - JxlImageF input, + JxlPlane input, in Rectangle rectangle, - JxlWeightsSymmetric5 weights, - JxlImageF output, + ref JxlWeightsSymmetric5 weights, + JxlPlane output, Rectangle outputRect) { if (rectangle.Width != outputRect.Width || rectangle.Height != outputRect.Height) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSymmetric5.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSymmetric5.cs index df5fa66b7b..7fb8977a50 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSymmetric5.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlWeightsSymmetric5.cs @@ -6,89 +6,98 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; -internal sealed class JxlWeightsSymmetric5 +internal struct JxlWeightsSymmetric5 { - private InlineArray4 c; + public InlineArray4 C; - private InlineArray4 r; + public InlineArray4 R; - private InlineArray4 r2; + public InlineArray4 R2; - private InlineArray4 d; + public InlineArray4 D; - private InlineArray4 d2; + public InlineArray4 D2; - private InlineArray4 l; + public InlineArray4 L; - public Vector128 GetCVector() + public static InlineArray4 CreateVector4(float x) { - ref float first = ref Unsafe.AsRef(in this.c[0]); + InlineArray4 array = default; + + array[0] = array[1] = array[2] = array[3] = x; + + return array; + } + + public readonly Vector128 GetCVector() + { + ref float first = ref Unsafe.AsRef(in this.C[0]); return Vector128.LoadUnsafe(ref first); } - public Vector128 GetRVector() + public readonly Vector128 GetRVector() { - ref float first = ref Unsafe.AsRef(in this.r[0]); + ref float first = ref Unsafe.AsRef(in this.R[0]); return Vector128.LoadUnsafe(ref first); } - public Vector128 GetR2Vector() + public readonly Vector128 GetR2Vector() { - ref float first = ref Unsafe.AsRef(in this.r2[0]); + ref float first = ref Unsafe.AsRef(in this.R2[0]); return Vector128.LoadUnsafe(ref first); } - public Vector128 GetDVector() + public readonly Vector128 GetDVector() { - ref float first = ref Unsafe.AsRef(in this.d[0]); + ref float first = ref Unsafe.AsRef(in this.D[0]); return Vector128.LoadUnsafe(ref first); } - public Vector128 GetD2Vector() + public readonly Vector128 GetD2Vector() { - ref float first = ref Unsafe.AsRef(in this.d2[0]); + ref float first = ref Unsafe.AsRef(in this.D2[0]); return Vector128.LoadUnsafe(ref first); } - public Vector128 GetLVector() + public readonly Vector128 GetLVector() { - ref float first = ref Unsafe.AsRef(in this.l[0]); + ref float first = ref Unsafe.AsRef(in this.L[0]); return Vector128.LoadUnsafe(ref first); } - public void SetC(Vector128 vec) + public readonly void SetC(Vector128 vec) { - ref float first = ref Unsafe.AsRef(in this.c[0]); + ref float first = ref Unsafe.AsRef(in this.C[0]); vec.StoreUnsafe(ref first); } - public void SetD(Vector128 vec) + public readonly void SetD(Vector128 vec) { - ref float first = ref Unsafe.AsRef(in this.d[0]); + ref float first = ref Unsafe.AsRef(in this.D[0]); vec.StoreUnsafe(ref first); } - public void SetD2(Vector128 vec) + public readonly void SetD2(Vector128 vec) { - ref float first = ref Unsafe.AsRef(in this.d2[0]); + ref float first = ref Unsafe.AsRef(in this.D2[0]); vec.StoreUnsafe(ref first); } - public void SetR(Vector128 vec) + public readonly void SetR(Vector128 vec) { - ref float first = ref Unsafe.AsRef(in this.r[0]); + ref float first = ref Unsafe.AsRef(in this.R[0]); vec.StoreUnsafe(ref first); } - public void SetR2(Vector128 vec) + public readonly void SetR2(Vector128 vec) { - ref float first = ref Unsafe.AsRef(in this.r2[0]); + ref float first = ref Unsafe.AsRef(in this.R2[0]); vec.StoreUnsafe(ref first); } - public void SetL(Vector128 vec) + public readonly void SetL(Vector128 vec) { - ref float first = ref Unsafe.AsRef(in this.l[0]); + ref float first = ref Unsafe.AsRef(in this.L[0]); vec.StoreUnsafe(ref first); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/RectangleUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/RectangleUtils.cs new file mode 100644 index 0000000000..072b51983f --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/RectangleUtils.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +internal static class RectangleUtils +{ + public static int X0(in Rectangle rect) => rect.X; + + public static int Y0(in Rectangle rect) => rect.Y; + + public static int X1(in Rectangle rect) => rect.X + rect.Width; + + public static int Y1(in Rectangle rect) => rect.Y + rect.Height; + + public static Rectangle Extend(Rectangle curr, int border, Rectangle parent) + { + int newX0 = X0(in curr) > X0(in parent) + border ? X0(in curr) - border : X0(in parent); + int newY0 = Y0(in curr) > Y0(in parent) + border ? Y0(in curr) - border : Y0(in parent); + int newX1 = X1(in curr) + border > X1(in parent) ? X1(in parent) : X1(in curr) + border; + int newY1 = Y1(in curr) + border > Y1(in parent) ? Y1(in parent) : Y1(in curr) + border; + + return new(newX0, newY0, newX1 - newX0, newY1 - newY0); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs index 5df7525ea9..28f3dcb34d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs @@ -19,6 +19,11 @@ internal sealed class Epf0Stage : RenderPipelineStageBase [0, 1], [0, 2], [1, -1], [1, 0], [1, 1], [2, 0] ]; + private static readonly int[][] PlusOffsets = + [ + [0, 0], [-1, 0], [0, -1], [1, 0], [0, 1] + ]; + private readonly JxlLoopFilter loopFilter; private readonly JxlImageF sigma; @@ -29,12 +34,13 @@ public Epf0Stage(JxlLoopFilter loopFilter, JxlImageF sigma, Configuration config this.Settings = RenderPipelineStageConfiguration.CreateSymmetricBorderOnly(3); } + /// public override string Name => "EPF0"; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void AddPixel( int row, - InlineArray7>> rows, + InlineArray3>> rows, int x, Vector256 sad, Vector256 inverseSigma, @@ -54,6 +60,7 @@ public static void AddPixel( bOut += (weight * cb) + bOut; } + /// public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) { Span> sads = stackalloc Vector256[16].Slice(0, 12); @@ -103,6 +110,60 @@ public override void ProcessRow(Buffer2D> inputRows, Buffer2D vsm = Vector256.Create(sadMul[ix..]); Vector256 inverseSigma = Vector256.Create(rowSigma[bx]) * vsm; + + sads.Clear(); + + for (int c = 0; c < 3; c++) + { + Vector256 scale = Vector256.Create(this.loopFilter.EpfChannelScale[c]); + + for (int i = 0; i < 12; i++) + { + Vector256 sad = Vector256.Zero; + + foreach (Span offset in PlusOffsets) + { + Vector256 r11 = Vector256.Create((ReadOnlySpan)rows[c][3 + offset[0]][(x + offset[1])..].Span); + Vector256 c11 = Vector256.Create((ReadOnlySpan)rows[c][3 + SadOffsets[i][0] + offset[0]][(x + SadOffsets[i][1] + offset[1])..].Span); + sad += Vector256.Abs(r11 - c11); + } + + sads[i] = (sad * scale) + sads[i]; + } + } + + Vector256 xCC = Vector256.Create((ReadOnlySpan)rows[0][3 + 0][x..].Span); + Vector256 yCC = Vector256.Create((ReadOnlySpan)rows[1][3 + 0][x..].Span); + Vector256 bCC = Vector256.Create((ReadOnlySpan)rows[2][3 + 0][x..].Span); + + Vector256 w = Vector256.One; + Vector256 X = xCC; + Vector256 Y = yCC; + Vector256 B = bCC; + + for (int i = 0; i < 12; i++) + { + AddPixel(SadOffsets[i][0], rows, x + SadOffsets[i][1], sads[i], inverseSigma, ref X, ref Y, ref B, ref w); + } + + Vector256 inverseW = Vector256.One / w; + + (X * inverseW).CopyTo(GetOutputRow(outputRows, 0, 0)[x..]); + (Y * inverseW).CopyTo(GetOutputRow(outputRows, 1, 0)[x..]); + (B * inverseW).CopyTo(GetOutputRow(outputRows, 2, 0)[x..]); + } + } + + /// + public override RenderPipelineChannelMode GetChannelMode(int channel) + { + if (channel < 3) + { + return RenderPipelineChannelMode.InOut; + } + else + { + return RenderPipelineChannelMode.Ignored; } } } From 83456232133ac14d3314f43994c20e37f7698c00 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:38:31 +0400 Subject: [PATCH 122/142] Refactor, optimize 1. Reorder the overflow check in JpegData 2. Prefer switch in JpegData instead of multiple if statements 3. Don't explicitly false initialize acOk and dcOk in JpegData (they're already initialized to false). 4. IFjxlFrameInputSource -> FjxlFrameInputSource (it's an abstract class, I prefix is for interfaces) 5. Document that the JxlSqueeze.Average method is specific to the squeeze transform. 6. Prefer RuntimeUtility.Swap over tuple-based swap (micro-optimization) --- .../Formats/Jxl/IO/Jpeg/Data/JpegData.cs | 33 ++++++++----------- .../Encoder/JxlFastLosslessEncoder.cs | 4 +-- .../Formats/Jxl/Processing/JxlMath.cs | 3 +- .../Modular/Transforms/JxlSqueeze.cs | 4 +++ 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs index 9ab957be93..4555442bcb 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs @@ -113,7 +113,7 @@ public static void SetJpegDataFromIcc(Span icc, JpegData jpegData) } int len = jpegData.AppData[i].Count - 17; - if (iccPos + len > icc.Length) + if (iccPos > icc.Length - len) { throw new InvalidOperationException("ICC length is less than APP markers: requested " + len + " more bytes, " + (icc.Length - iccPos) + " available"); } @@ -144,24 +144,23 @@ private static bool VisitMarker(ref byte marker, JxlVisitor visitor, ref JpegInf info.NumberOfAppMarkers++; } - if (marker == 0xfe) + switch (marker) { - info.NumberOfComMarkers++; - } + case 0xFE: + info.NumberOfComMarkers++; + break; - if (marker == 0xda) - { - info.NumberOfScans++; - } + case 0xDA: + info.NumberOfScans++; + break; - if (marker == 0xff) - { - info.NumberOfIntermarkers++; - } + case 0xFF: + info.NumberOfIntermarkers++; + break; - if (marker == 0xdd) - { - info.HasDri = true; + case 0xDD: + info.HasDri = true; + break; } return true; @@ -919,10 +918,6 @@ ref Unsafe.As(ref tail_data_len))) InlineArray4 acOk = default; InlineArray4 dcOk = default; - // All values of acOk, dcOk by default are false. - acOk[0] = acOk[1] = acOk[2] = acOk[3] = false; - dcOk[0] = dcOk[1] = dcOk[2] = dcOk[3] = false; - Span markerOrderSpan = CollectionsMarshal.AsSpan(this.MarkerOrder); for (int i = 0; i < markerOrderSpan.Length; i++) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs index 91263989ef..bea22c76a3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlFastLosslessEncoder.cs @@ -53,7 +53,7 @@ internal sealed class JxlFastLosslessEncoder /// /// Input frame data is stored here. /// - private readonly IFjxlFrameInputSource input; + private readonly FjxlFrameInputSource input; /// /// Image width of the input image. @@ -147,7 +147,7 @@ internal sealed class JxlFastLosslessEncoder /// /// Abstracts access to a raster frame data required for encoding. /// - internal abstract class IFjxlFrameInputSource : IDisposable + internal abstract class FjxlFrameInputSource : IDisposable { /// public abstract void Dispose(); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs index a0042c9c50..f0ab04104e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlMath.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Common.Helpers; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; @@ -705,7 +706,7 @@ private static T Hypot(T x, T y) if (x < y) { - (y, x) = (x, y); + RuntimeUtility.Swap(ref x, ref y); } if (x == T.Zero) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs index 6888015825..11752a92c9 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlSqueeze.cs @@ -30,6 +30,10 @@ internal static class JxlSqueeze /// /// Computes the average of two integers. /// + /// + /// This method is specific to the Squeeze transform. + /// It is not a generic average method. + /// /// First integer /// Second integer /// The average of x, y. From 4116e9c60906e1f1d19a4d88689859780ddbb8d4 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 19:45:20 +0400 Subject: [PATCH 123/142] Complete Edge Preserving Filter render pipeline stages --- .../Processing/RenderPipeline/Epf1Stage.cs | 204 ++++++++++++++++++ .../Processing/RenderPipeline/Epf2Stage.cs | 144 +++++++++++++ 2 files changed, 348 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs new file mode 100644 index 0000000000..9ad9b3dec8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs @@ -0,0 +1,204 @@ +// 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.ImageTypes; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +/// +/// Edge Preserving Filter (type 1) stage. +/// +internal class Epf1Stage : RenderPipelineStageBase +{ + private readonly JxlLoopFilter loopFilter; + private readonly JxlImageF sigma; + + public Epf1Stage(Configuration configuration, JxlLoopFilter loopFilter, JxlImageF sigma) + : base(configuration) + { + this.loopFilter = loopFilter; + this.sigma = sigma; + this.Settings = RenderPipelineStageConfiguration.CreateSymmetricBorderOnly(2); + } + + /// + public override string Name => "EPF1"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void AddPixel( + int row, + InlineArray3>> rows, + int x, + Vector256 sad, + Vector256 inverseSigma, + ref Vector256 xOut, + ref Vector256 yOut, + ref Vector256 bOut, + ref Vector256 wOut) + { + Vector256 cx = Vector256.Create((ReadOnlySpan)rows[0][2 + row][x..].Span); + Vector256 cy = Vector256.Create((ReadOnlySpan)rows[1][2 + row][x..].Span); + Vector256 cb = Vector256.Create((ReadOnlySpan)rows[2][2 + row][x..].Span); + + Vector256 weight = EpfUtils.Weight(sad, inverseSigma); + wOut += weight; + xOut = (weight + cx) * xOut; + yOut = (weight + cy) * yOut; + bOut = (weight + cb) * bOut; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 AbsoluteDifference(Vector256 x, Vector256 y) => Vector256.Abs(x - y); + + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + int xStart = -JxlMath.RoundUpTo(xExtraLeft, Vector256.Count); + int xEnd = width + xExtraRight; + + Span rowSigma = this.sigma.GetRow((yPos / JxlFrameDimensions.BlockDimensions) + JxlDecoderCache.SigmaPadding); + float sm = 1.65f; + float bsm = sm * this.loopFilter.EpfBorderSadMul; + + Span sadMulCenter = [bsm, sm, sm, sm, sm, sm, sm, bsm]; + Span sadMulBorder = [bsm, bsm, bsm, bsm, bsm, bsm, bsm, bsm]; + + InlineArray3>> rows = default; + for (int c = 0; c < 3; c++) + { + for (int i = 0; i < 5; i++) + { + rows[c][i] = this.GetInputRowMemory(inputRows, c, i - 2); + } + } + + Span sadMul = (yPos % JxlFrameDimensions.BlockDimensions is 0 or JxlFrameDimensions.BlockDimensions - 1) + ? sadMulBorder + : sadMulCenter; + + for (int x = xStart; x < xEnd; x += Vector256.Count) + { + int bx = (x + xPos + (JxlDecoderCache.SigmaPadding * JxlFrameDimensions.BlockDimensions)) / JxlFrameDimensions.BlockDimensions; + int ix = (x + xPos) % JxlFrameDimensions.BlockDimensions; + + if (rowSigma[bx] < JxlLoopFilter.MinimumSigma) + { + for (int c = 0; c < 3; c++) + { + Vector256 px = Vector256.Create((ReadOnlySpan)rows[c][2][x..].Span); + px.CopyTo(GetOutputRow(outputRows, c, 0)[x..]); + } + + continue; + } + + Vector256 vsm = Vector256.Create((ReadOnlySpan)sadMul[ix..]); + Vector256 inverseSigma = Vector256.Create(rowSigma[bx]) * vsm; + Vector256 sad0 = Vector256.Zero; + Vector256 sad1 = Vector256.Zero; + Vector256 sad2 = Vector256.Zero; + Vector256 sad3 = Vector256.Zero; + + // Compute sum of absolute differences (SAD) + for (int c = 0; c < 3; c++) + { + // center px = 22, px above = 21 + Vector256 t; + + Vector256 p20 = Vector256.Create((ReadOnlySpan)rows[c][2 + -2][x..].Span); + Vector256 p21 = Vector256.Create((ReadOnlySpan)rows[c][2 + -1][x..].Span); + Vector256 sad0c = AbsoluteDifference(p20, p21); // SAD 2, 1 + + Vector256 p11 = Vector256.Create((ReadOnlySpan)rows[c][2 + -1][(x - 1)..].Span); + Vector256 sad1c = AbsoluteDifference(p11, p21); // SAD 1, 2 + + Vector256 p31 = Vector256.Create((ReadOnlySpan)rows[c][2 + -1][(x + 1)..].Span); + Vector256 sad2c = AbsoluteDifference(p31, p21); // SAD 3, 2 + + Vector256 p02 = Vector256.Create((ReadOnlySpan)rows[c][2][(x - 2)..].Span); + Vector256 p12 = Vector256.Create((ReadOnlySpan)rows[c][2][(x - 1)..].Span); + sad1c += AbsoluteDifference(p02, p12); // SAD 1, 2 + sad0c += AbsoluteDifference(p11, p12); // SAD 2, 1 + + // TODO(eustas): why unaligned? + Vector256 p22 = Vector256.Create((ReadOnlySpan)rows[c][2][x..].Span); + t = AbsoluteDifference(p12, p22); + sad1c += t; // SAD 1, 2 + sad2c += t; // SAD 3, 2 + t = AbsoluteDifference(p22, p21); + Vector256 sad3c = t; // SAD 2, 3 + sad0c += t; // SAD 2, 1 + + Vector256 p32 = Vector256.Create((ReadOnlySpan)rows[c][2][(x + 1)..].Span); + sad0c += AbsoluteDifference(p31, p32); // SAD 2, 1 + t = AbsoluteDifference(p22, p32); + sad1c += t; // SAD 1, 2 + sad2c += t; // SAD 3, 2 + + Vector256 p42 = Vector256.Create((ReadOnlySpan)rows[c][2][(x + 2)..].Span); + sad2c += AbsoluteDifference(p42, p32); // SAD 3, 2 + + Vector256 p13 = Vector256.Create((ReadOnlySpan)rows[c][2 + 1][(x - 1)..].Span); + sad3c += AbsoluteDifference(p13, p12); // SAD 2, 3 + + Vector256 p23 = Vector256.Create((ReadOnlySpan)rows[c][2 + 1][x..].Span); + t = AbsoluteDifference(p22, p23); + sad0c += t; // SAD 2, 1 + sad3c += t; // SAD 2, 3 + sad1c += AbsoluteDifference(p13, p23); // SAD 1, 2 + + Vector256 p33 = Vector256.Create((ReadOnlySpan)rows[c][2 + 1][(x + 1)..].Span); + sad2c += AbsoluteDifference(p33, p23); // SAD 3, 2 + sad3c += AbsoluteDifference(p33, p32); // SAD 2, 3 + + Vector256 p24 = Vector256.Create((ReadOnlySpan)rows[c][2 + 2][x..].Span); + sad3c += AbsoluteDifference(p24, p23); // SAD 2, 3 + + Vector256 scale = Vector256.Create(this.loopFilter.EpfChannelScale[c]); + sad0 = (sad0c * scale) + sad0; + sad1 = (sad1c * scale) + sad1; + sad2 = (sad2c * scale) + sad2; + sad3 = (sad3c * scale) + sad3; + } + + Vector256 xCC = Vector256.Create((ReadOnlySpan)rows[0][2 + 0][x..].Span); + Vector256 yCC = Vector256.Create((ReadOnlySpan)rows[1][2 + 0][x..].Span); + Vector256 bCC = Vector256.Create((ReadOnlySpan)rows[2][2 + 0][x..].Span); + + Vector256 w = Vector256.One; + Vector256 X = xCC; + Vector256 Y = yCC; + Vector256 B = bCC; + + // Top row + AddPixel(-1, rows, x, sad0, inverseSigma, ref X, ref Y, ref B, ref w); + + // Center + AddPixel(0, rows, x - 1, sad1, inverseSigma, ref X, ref Y, ref B, ref w); + AddPixel(0, rows, x + 1, sad2, inverseSigma, ref X, ref Y, ref B, ref w); + + // Bottom + AddPixel(1, rows, x + 1, sad3, inverseSigma, ref X, ref Y, ref B, ref w); + + Vector256 inverseW = Vector256.One / w; + (X * inverseW).CopyTo(GetOutputRow(outputRows, 0, 0)[x..]); + (Y * inverseW).CopyTo(GetOutputRow(outputRows, 1, 0)[x..]); + (B * inverseW).CopyTo(GetOutputRow(outputRows, 2, 0)[x..]); + } + } + + /// + public override RenderPipelineChannelMode GetChannelMode(int channel) + { + if (channel < 3) + { + return RenderPipelineChannelMode.InOut; + } + else + { + return RenderPipelineChannelMode.Ignored; + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs new file mode 100644 index 0000000000..ff6198e7b8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs @@ -0,0 +1,144 @@ +// 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.ImageTypes; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +/// +/// Edge Preserving Filter (type 2) stage +/// +internal sealed class Epf2Stage : RenderPipelineStageBase +{ + private readonly JxlLoopFilter loopFilter; + private readonly JxlImageF sigma; + + public Epf2Stage(JxlLoopFilter loopFilter, JxlImageF sigma, Configuration configuration) + : base(configuration) + { + this.loopFilter = loopFilter; + this.sigma = sigma; + this.Settings = RenderPipelineStageConfiguration.CreateSymmetricBorderOnly(2); + } + + /// + public override string Name => "EPF2"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 AbsoluteDifference(Vector256 x, Vector256 y) => Vector256.Abs(x - y); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddPixel( + int row, + InlineArray3>> rows, + int x, + Vector256 rx, + Vector256 ry, + Vector256 rb, + Vector256 inverseSigma, + ref Vector256 X, + ref Vector256 Y, + ref Vector256 B, + ref Vector256 w) + { + Vector256 cx = Vector256.Create((ReadOnlySpan)rows[0][1 + row][x..].Span); + Vector256 cy = Vector256.Create((ReadOnlySpan)rows[1][1 + row][x..].Span); + Vector256 cb = Vector256.Create((ReadOnlySpan)rows[2][1 + row][x..].Span); + + Vector256 sad = AbsoluteDifference(cx, rx) * Vector256.Create(this.loopFilter.EpfChannelScale[0]); + sad = (AbsoluteDifference(cy, ry) * Vector256.Create(this.loopFilter.EpfChannelScale[1])) + sad; + sad = (AbsoluteDifference(cb, rb) * Vector256.Create(this.loopFilter.EpfChannelScale[2])) + sad; + + Vector256 weight = EpfUtils.Weight(sad, inverseSigma); + w += weight; + X = (weight * cx) + X; + Y = (weight * cy) + Y; + B = (weight * cb) + B; + } + + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + int xStart = -JxlMath.RoundUpTo(xExtraLeft, Vector256.Count); + int xEnd = width + xExtraRight; + + Span rowSigma = this.sigma.GetRow((yPos / JxlFrameDimensions.BlockDimensions) + JxlDecoderCache.SigmaPadding); + float sm = 1.65f; + float bsm = sm * this.loopFilter.EpfBorderSadMul; + + Span sadMulCenter = [bsm, sm, sm, sm, sm, sm, sm, bsm]; + Span sadMulBorder = [bsm, bsm, bsm, bsm, bsm, bsm, bsm, bsm]; + + InlineArray3>> rows = default; + for (int c = 0; c < 3; c++) + { + for (int i = 0; i < 3; i++) + { + rows[c][i] = this.GetInputRowMemory(inputRows, c, i - 1); + } + } + + Span sadMul = (yPos % JxlFrameDimensions.BlockDimensions is 0 or JxlFrameDimensions.BlockDimensions - 1) + ? sadMulBorder + : sadMulCenter; + + for (int x = xStart; x < xEnd; x += Vector256.Count) + { + int bx = (x + xPos + (JxlDecoderCache.SigmaPadding * JxlFrameDimensions.BlockDimensions)) / JxlFrameDimensions.BlockDimensions; + int ix = (x + xPos) % JxlFrameDimensions.BlockDimensions; + + if (rowSigma[bx] < JxlLoopFilter.MinimumSigma) + { + for (int c = 0; c < 3; c++) + { + Vector256 px = Vector256.Create((ReadOnlySpan)rows[c][1][x..].Span); + px.CopyTo(GetOutputRow(outputRows, c, 0)[x..]); + } + + continue; + } + + Vector256 vsm = Vector256.Create((ReadOnlySpan)sadMul[ix..]); + Vector256 inverseSigma = Vector256.Create(rowSigma[bx]) * vsm; + + Vector256 xCC = Vector256.Create((ReadOnlySpan)rows[0][1 + 0][x..].Span); + Vector256 yCC = Vector256.Create((ReadOnlySpan)rows[1][1 + 0][x..].Span); + Vector256 bCC = Vector256.Create((ReadOnlySpan)rows[2][1 + 0][x..].Span); + + Vector256 w = Vector256.One; + Vector256 X = xCC; + Vector256 Y = yCC; + Vector256 B = bCC; + + // Top row + this.AddPixel(-1, rows, x, xCC, yCC, bCC, inverseSigma, ref X, ref Y, ref B, ref w); + + // Center + this.AddPixel(0, rows, x - 1, xCC, yCC, bCC, inverseSigma, ref X, ref Y, ref B, ref w); + this.AddPixel(0, rows, x + 1, xCC, yCC, bCC, inverseSigma, ref X, ref Y, ref B, ref w); + + // Bottom + this.AddPixel(1, rows, x, xCC, yCC, bCC, inverseSigma, ref X, ref Y, ref B, ref w); + + Vector256 inverseW = Vector256.One / w; + (X * inverseW).CopyTo(GetOutputRow(outputRows, 0, 0)[x..]); + (Y * inverseW).CopyTo(GetOutputRow(outputRows, 1, 0)[x..]); + (B * inverseW).CopyTo(GetOutputRow(outputRows, 2, 0)[x..]); + } + } + + /// + public override RenderPipelineChannelMode GetChannelMode(int channel) + { + if (channel < 3) + { + return RenderPipelineChannelMode.InOut; + } + else + { + return RenderPipelineChannelMode.Ignored; + } + } +} From 5874c4c9fdbb341b66afcc6c7f326695be564a37 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 19:47:46 +0400 Subject: [PATCH 124/142] Optimize JxlNoiseHistogram - Use double for Clamp - Avoid bounds check in while loop --- .../Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs index 5cacaa5d02..396021a55d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlNoiseHistogram.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; using System.Numerics.Tensors; using System.Runtime.CompilerServices; @@ -66,7 +65,7 @@ public double Quantile(double q01) int next = i + 1; - while (next < Bins && this.bins[next] == 0) + while ((uint)next < this.bins.Length && this.bins[next] == 0) { next++; } @@ -78,10 +77,8 @@ public double Quantile(double q01) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static T ClampX(T x) - where T : unmanaged, INumber - => T.Clamp(x, T.Zero, T.CreateSaturating(Bins - 1)); + private static double ClampX(double x) => Math.Clamp(x, 0, Bins - 1); [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int Index(float x) => ClampX((int)x); + private static int Index(float x) => (int)ClampX((int)x); } From 5f99a6530f7807fd89ba5945c5e783abf8b13bec Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:16:57 +0400 Subject: [PATCH 125/142] Further update folder structure, start working on write to output stage (incomplete) --- .../{Processing => IO}/JxlBoxCodingMode.cs | 2 +- .../Primitives => IO}/JxlDataType.cs | 2 +- .../Formats/Jxl/Memory/JxlPlane{T}.cs | 2 +- .../Processing/AcStrategy/JxlAcStrategy.cs | 2 +- .../AcStrategy/JxlAcStrategyImage.cs | 1 + .../Jxl/Processing/Dct/JxlDctAcImage{T}.cs | 1 + .../Decoder/JxlBoxContentDecoder.cs | 1 + .../Jxl/Processing/Decoder/JxlDecoderCache.cs | 14 ++ .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 1 + .../Jxl/Processing/Decoder/JxlFrameDecoder.cs | 1 + .../Decoder/JxlPassesDecoderState.cs | 1 + .../Processing/Decoder/JxlPatchDictionary.cs | 1 + .../Jxl/Processing/Decoder/JxlXybDecoder.cs | 1 + .../Jxl/Processing/Encoder/JxlGaborish.cs | 1 + .../Processing/{ => Image}/JxlImageBundle.cs | 2 +- .../{ => Image}/JxlImageFeatures.cs | 2 +- .../{ => Image}/JxlImageOperations.cs | 3 +- .../Jxl/Processing/JxlChromaFromLuma.cs | 2 +- .../Jxl/Processing/JxlPassesSharedState.cs | 2 + .../Modular/Transforms/JxlPalette.cs | 1 + .../Jxl/Processing/Primitives/Delegates.cs | 25 +++ .../{ => Primitives}/JxlFrameDimensions.cs | 2 +- .../JxlOpsinInverseParameters.cs | 4 +- .../Processing/Primitives/JxlPixelFormat.cs | 2 + .../Quantization/JxlDequantMatrices.cs | 1 + .../Processing/Quantization/JxlQuantizer.cs | 1 + .../Processing/RenderPipeline/Epf0Stage.cs | 2 + .../Processing/RenderPipeline/Epf1Stage.cs | 2 + .../Processing/RenderPipeline/Epf2Stage.cs | 2 + .../RenderPipeline/WriteToOutputStage.cs | 212 ++++++++++++++++++ 30 files changed, 284 insertions(+), 12 deletions(-) rename src/ImageSharp/Formats/Jxl/{Processing => IO}/JxlBoxCodingMode.cs (87%) rename src/ImageSharp/Formats/Jxl/{Processing/Primitives => IO}/JxlDataType.cs (90%) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCache.cs rename src/ImageSharp/Formats/Jxl/Processing/{ => Image}/JxlImageBundle.cs (99%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Image}/JxlImageFeatures.cs (92%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Image}/JxlImageOperations.cs (99%) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Primitives/Delegates.cs rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlFrameDimensions.cs (97%) rename src/ImageSharp/Formats/Jxl/Processing/{ => Primitives}/JxlOpsinInverseParameters.cs (87%) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlBoxCodingMode.cs b/src/ImageSharp/Formats/Jxl/IO/JxlBoxCodingMode.cs similarity index 87% rename from src/ImageSharp/Formats/Jxl/Processing/JxlBoxCodingMode.cs rename to src/ImageSharp/Formats/Jxl/IO/JxlBoxCodingMode.cs index 89f5af5e3f..412d18a8aa 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlBoxCodingMode.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlBoxCodingMode.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.IO; /// /// Specifies the type of box content. diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlDataType.cs b/src/ImageSharp/Formats/Jxl/IO/JxlDataType.cs similarity index 90% rename from src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlDataType.cs rename to src/ImageSharp/Formats/Jxl/IO/JxlDataType.cs index 8bc0bf6911..7370f1b25b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlDataType.cs +++ b/src/ImageSharp/Formats/Jxl/IO/JxlDataType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +namespace SixLabors.ImageSharp.Formats.Jxl.IO; /// /// Specifies which data type to use for sample values diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs index 2d96ab636f..f8602a1897 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Memory; diff --git a/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategy.cs b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategy.cs index 36f0b71c5b..78e97ed16c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategy.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategy.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using static SixLabors.ImageSharp.Formats.Jxl.Processing.JxlFrameDimensions; +using static SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives.JxlFrameDimensions; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; diff --git a/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyImage.cs b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyImage.cs index 875cae762f..4c751e290c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyImage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyImage.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcImage{T}.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcImage{T}.cs index 6d132c4128..002342ae57 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcImage{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctAcImage{T}.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs index 93fe598304..9d35ea85a9 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlBoxContentDecoder.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.IO.Compression; +using SixLabors.ImageSharp.Formats.Jxl.IO; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCache.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCache.cs new file mode 100644 index 0000000000..8e50433369 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCache.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +/// +/// Constants used by decoder cache. +/// +internal static class JxlDecoderCache +{ + public const int SigmaBorder = 1; + + public const int SigmaPadding = 2; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 5b58a19128..6a850d8288 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Metadata.Profiles.Icc; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs index 75b39cbeb0..9b8e9b0f28 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPassesDecoderState.cs index 9c5b910c2d..3e32e94a48 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.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs index d9bc0e88f8..c990288589 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs index 7fd7d54c6e..87f96e97eb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlXybDecoder.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Cms; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs index 191e2c5a4a..4fddd68873 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlGaborish.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; 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; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs similarity index 99% rename from src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs rename to src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs index 184feb6e74..b4ca42d788 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlImageBundle.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageBundle.cs @@ -7,7 +7,7 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Image; /// /// An image bundle. diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageFeatures.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageFeatures.cs similarity index 92% rename from src/ImageSharp/Formats/Jxl/Processing/JxlImageFeatures.cs rename to src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageFeatures.cs index 63d4025eb1..eb0e4581da 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlImageFeatures.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageFeatures.cs @@ -4,7 +4,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Image; /// /// Image features for the JPEG XL passes decoder diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs similarity index 99% rename from src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs rename to src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs index e5ddf834f7..0a4ff8f5d7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlImageOperations.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs @@ -5,8 +5,9 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Image; /// /// Provides methods for processing 2D views of memory used by the diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlChromaFromLuma.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlChromaFromLuma.cs index 0121140472..49ed970b84 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlChromaFromLuma.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlChromaFromLuma.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Jxl.Fields; -using static SixLabors.ImageSharp.Formats.Jxl.Processing.JxlFrameDimensions; +using static SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives.JxlFrameDimensions; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs index dcc699503b..f56689382a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlPassesSharedState.cs @@ -7,6 +7,8 @@ using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs index 63d0c0b081..4e306c1f40 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Modular/Transforms/JxlPalette.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; using SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Encoding.ContextPrediction; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Modular.Transforms; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/Delegates.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/Delegates.cs new file mode 100644 index 0000000000..e0f7c78a33 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/Delegates.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +internal delegate void JxlImageOutputCallback( + Span data, + int x, + int y, + int numPixels, + Span pixels); + +internal delegate void JxlImageOutputInitializeCallback( + Span data, + int numThreads, + int numPixelsPerThread); + +internal delegate void JxlImageOutputRunCallback( + Span data, + int x, + int y, + int numPixels, + Span pixels); + +internal delegate void JxlImageOutputDestroyCallback(Span data); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlFrameDimensions.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlFrameDimensions.cs similarity index 97% rename from src/ImageSharp/Formats/Jxl/Processing/JxlFrameDimensions.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlFrameDimensions.cs index 70a3bff8bb..7bd67326a3 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlFrameDimensions.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlFrameDimensions.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; internal sealed class JxlFrameDimensions { diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlOpsinInverseParameters.cs similarity index 87% rename from src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs rename to src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlOpsinInverseParameters.cs index 820b11a5ba..4e51220296 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlOpsinInverseParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlOpsinInverseParameters.cs @@ -1,9 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing; +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; internal static class JxlOpsinInverseParameters { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlPixelFormat.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlPixelFormat.cs index be00bd3e76..5e0d3ef22b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlPixelFormat.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlPixelFormat.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.IO; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs index 9649f44203..e7084a87cd 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlDequantMatrices.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs index 1ea7cfcbd1..5788160141 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizer.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs index 28f3dcb34d..a7bcafcc3d 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf0Stage.cs @@ -4,6 +4,8 @@ using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs index 9ad9b3dec8..b42513898c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs @@ -4,6 +4,8 @@ using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs index ff6198e7b8..7100df484c 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf2Stage.cs @@ -4,6 +4,8 @@ using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs new file mode 100644 index 0000000000..09426afb2b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs @@ -0,0 +1,212 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal sealed class WriteToOutputStage +{ + private const int ChunkSize = 1024; + + /// + /// Gets the 32x32 blue noise dithering pattern lookup + /// table (from ), + /// scaled to have an average of 0 and be fully contained in (0.49219 + /// to -0.49219). Rows are padded to 48 (32 + 16) to allow SIMD to wrap around + /// horizontally. + /// + private static ReadOnlySpan DitheringPattern => + [ + -0.26057f, 0.32619f, 0.21039f, -0.03281f, -0.10616f, 0.16792f, 0.43042f, -0.48061f, + -0.00965f, -0.31075f, 0.24899f, -0.35322f, -0.02509f, -0.25285f, 0.02895f, 0.10230f, + -0.28373f, -0.00193f, 0.23355f, 0.43428f, -0.23741f, 0.18336f, -0.31847f, -0.11002f, + -0.36094f, 0.26057f, -0.19108f, -0.29531f, 0.40726f, -0.09458f, 0.11002f, -0.48833f, + -0.26057f, 0.32619f, 0.21039f, -0.03281f, -0.10616f, 0.16792f, 0.43042f, -0.48061f, + -0.00965f, -0.31075f, 0.24899f, -0.35322f, -0.02509f, -0.25285f, 0.02895f, 0.10230f, + 0.16020f, -0.35708f, -0.18336f, 0.36094f, -0.28373f, -0.34550f, -0.20267f, 0.07914f, + 0.35708f, -0.41498f, 0.47675f, -0.21811f, -0.12546f, 0.44200f, -0.41884f, -0.17178f, + 0.39954f, 0.33778f, -0.33778f, 0.04053f, -0.46517f, 0.27215f, -0.16792f, 0.39182f, + 0.20653f, -0.43814f, -0.02895f, 0.17950f, -0.41498f, 0.01737f, 0.24899f, 0.49219f, + 0.16020f, -0.35708f, -0.18336f, 0.36094f, -0.28373f, -0.34550f, -0.20267f, 0.07914f, + 0.35708f, -0.41498f, 0.47675f, -0.21811f, -0.12546f, 0.44200f, -0.41884f, -0.17178f, + -0.00965f, 0.08300f, 0.41112f, -0.46903f, 0.04053f, 0.47289f, 0.26057f, -0.05983f, + -0.13704f, 0.14862f, 0.03281f, 0.29531f, -0.45744f, 0.22583f, 0.14862f, -0.09072f, + -0.37638f, 0.19881f, -0.14476f, 0.14476f, -0.09072f, 0.48447f, -0.39954f, 0.06369f, + -0.05983f, -0.26829f, 0.43428f, -0.12546f, 0.28759f, -0.22969f, -0.32619f, -0.15248f, + -0.00965f, 0.08300f, 0.41112f, -0.46903f, 0.04053f, 0.47289f, 0.26057f, -0.05983f, + -0.13704f, 0.14862f, 0.03281f, 0.29531f, -0.45744f, 0.22583f, 0.14862f, -0.09072f, + -0.42270f, 0.23741f, -0.23355f, -0.11774f, 0.18722f, 0.11388f, -0.43814f, -0.24899f, + 0.41884f, 0.21039f, -0.28373f, -0.06756f, 0.07914f, 0.36480f, -0.31075f, 0.30303f, + -0.03281f, 0.07142f, -0.42656f, 0.38024f, -0.27987f, 0.00579f, 0.12546f, -0.22197f, + 0.29917f, 0.36866f, 0.13704f, -0.47289f, 0.09072f, 0.35708f, -0.04825f, 0.38796f, + -0.42270f, 0.23741f, -0.23355f, -0.11774f, 0.18722f, 0.11388f, -0.43814f, -0.24899f, + 0.41884f, 0.21039f, -0.28373f, -0.06756f, 0.07914f, 0.36480f, -0.31075f, 0.30303f, + -0.28759f, -0.07142f, 0.44200f, 0.27601f, -0.38024f, -0.16020f, -0.01737f, 0.30303f, + -0.33006f, -0.40340f, -0.16792f, 0.40726f, -0.36480f, -0.00579f, -0.19108f, 0.41498f, + -0.26443f, 0.46903f, -0.21811f, 0.28759f, -0.04053f, 0.22197f, 0.34550f, -0.44972f, + -0.14476f, -0.34164f, 0.04053f, -0.19494f, 0.45358f, -0.37252f, 0.21425f, 0.05597f, + -0.28759f, -0.07142f, 0.44200f, 0.27601f, -0.38024f, -0.16020f, -0.01737f, 0.30303f, + -0.33006f, -0.40340f, -0.16792f, 0.40726f, -0.36480f, -0.00579f, -0.19108f, 0.41498f, + 0.31075f, 0.14090f, -0.33778f, 0.00579f, 0.34550f, -0.29917f, 0.38796f, 0.13704f, + 0.05983f, -0.10230f, 0.34164f, 0.10616f, -0.23741f, 0.19494f, -0.47675f, 0.04439f, + -0.39568f, 0.24127f, 0.10616f, -0.49219f, -0.17950f, -0.36094f, -0.30303f, 0.45744f, + -0.01351f, 0.24513f, -0.39182f, -0.07528f, 0.18722f, -0.26057f, -0.11002f, -0.45358f, + 0.31075f, 0.14090f, -0.33778f, 0.00579f, 0.34550f, -0.29917f, 0.38796f, 0.13704f, + 0.05983f, -0.10230f, 0.34164f, 0.10616f, -0.23741f, 0.19494f, -0.47675f, 0.04439f, + 0.46903f, -0.17178f, -0.41112f, 0.07528f, -0.09458f, 0.21811f, -0.20267f, -0.48833f, + 0.44972f, 0.00965f, 0.24127f, -0.42656f, 0.48447f, -0.11774f, 0.26443f, 0.14090f, + -0.15634f, -0.07142f, -0.32233f, 0.36094f, 0.42270f, 0.19108f, 0.07142f, -0.11002f, + 0.15634f, 0.38024f, -0.28759f, 0.27987f, -0.00193f, 0.33006f, 0.11388f, -0.21039f, + 0.46903f, -0.17178f, -0.41112f, 0.07528f, -0.09458f, 0.21811f, -0.20267f, -0.48833f, + 0.44972f, 0.00965f, 0.24127f, -0.42656f, 0.48447f, -0.11774f, 0.26443f, 0.14090f, + 0.02123f, 0.17950f, 0.38024f, -0.24127f, -0.44586f, 0.48833f, -0.03667f, 0.26829f, + -0.36866f, -0.22583f, 0.17178f, -0.30689f, 0.29145f, -0.04825f, -0.35322f, 0.43042f, + 0.34936f, 0.00193f, 0.16792f, -0.12932f, 0.03667f, -0.06756f, 0.31847f, -0.40726f, + -0.24513f, 0.09458f, -0.17564f, 0.47675f, -0.43042f, -0.32233f, 0.40340f, 0.26057f, + 0.02123f, 0.17950f, 0.38024f, -0.24127f, -0.44586f, 0.48833f, -0.03667f, 0.26829f, + -0.36866f, -0.22583f, 0.17178f, -0.30689f, 0.29145f, -0.04825f, -0.35322f, 0.43042f, + -0.47675f, -0.12160f, -0.04825f, 0.28759f, 0.10230f, 0.15634f, -0.14862f, -0.27601f, + 0.36094f, -0.12932f, -0.05983f, -0.45358f, -0.17950f, 0.01737f, 0.09458f, -0.29145f, + -0.22969f, -0.43428f, 0.45744f, -0.38796f, -0.27601f, -0.21039f, -0.46131f, 0.22969f, + 0.41112f, -0.05211f, -0.48061f, 0.16406f, 0.05211f, -0.14862f, -0.03281f, -0.36866f, + -0.47675f, -0.12160f, -0.04825f, 0.28759f, 0.10230f, 0.15634f, -0.14862f, -0.27601f, + 0.36094f, -0.12932f, -0.05983f, -0.45358f, -0.17950f, 0.01737f, 0.09458f, -0.29145f, + -0.27215f, 0.34164f, -0.31075f, 0.42656f, -0.38410f, -0.32619f, 0.02895f, 0.19881f, + 0.08300f, 0.42270f, 0.31461f, 0.13318f, 0.45744f, 0.37638f, -0.40726f, 0.31847f, + -0.08686f, 0.21425f, 0.29917f, 0.07914f, 0.26829f, 0.13704f, 0.48447f, -0.15248f, + 0.02509f, -0.34936f, 0.34936f, -0.10230f, 0.42656f, -0.23741f, 0.22583f, 0.09072f, + -0.27215f, 0.34164f, -0.31075f, 0.42656f, -0.38410f, -0.32619f, 0.02895f, 0.19881f, + 0.08300f, 0.42270f, 0.31461f, 0.13318f, 0.45744f, 0.37638f, -0.40726f, 0.31847f, + 0.44972f, 0.20267f, 0.04825f, -0.21425f, 0.24513f, -0.07142f, 0.39954f, -0.46131f, + -0.39568f, -0.01351f, -0.33392f, 0.05597f, -0.26443f, 0.22197f, -0.20653f, 0.15248f, + 0.04439f, -0.46517f, -0.16406f, -0.04439f, -0.34936f, 0.37252f, -0.01351f, -0.30689f, + 0.29917f, 0.20653f, -0.26829f, 0.26443f, 0.13318f, -0.39954f, 0.30303f, -0.08686f, + 0.44972f, 0.20267f, 0.04825f, -0.21425f, 0.24513f, -0.07142f, 0.39954f, -0.46131f, + -0.39568f, -0.01351f, -0.33392f, 0.05597f, -0.26443f, 0.22197f, -0.20653f, 0.15248f, + -0.42656f, 0.12932f, -0.14476f, -0.46903f, -0.00579f, 0.34936f, -0.18722f, 0.28373f, + -0.23741f, 0.22969f, -0.16020f, -0.38024f, -0.08300f, -0.48447f, -0.02123f, -0.14862f, + 0.48061f, -0.31847f, 0.39568f, -0.24899f, 0.18722f, -0.41884f, 0.10230f, -0.08300f, + -0.38796f, 0.06369f, -0.19881f, -0.44972f, 0.00579f, -0.33392f, 0.37252f, -0.19108f, + -0.42656f, 0.12932f, -0.14476f, -0.46903f, -0.00579f, 0.34936f, -0.18722f, 0.28373f, + -0.23741f, 0.22969f, -0.16020f, -0.38024f, -0.08300f, -0.48447f, -0.02123f, -0.14862f, + -0.02509f, -0.35708f, 0.32619f, 0.46517f, 0.17178f, -0.28373f, 0.10616f, 0.47675f, + -0.09458f, 0.15248f, 0.43428f, 0.35322f, 0.17564f, 0.27215f, 0.41112f, -0.36480f, + 0.24899f, 0.11774f, 0.01351f, 0.33006f, -0.11388f, -0.18336f, 0.41884f, -0.23355f, + 0.16406f, 0.46131f, 0.38410f, -0.04825f, -0.15634f, 0.49219f, 0.17564f, 0.03667f, + -0.02509f, -0.35708f, 0.32619f, 0.46517f, 0.17178f, -0.28373f, 0.10616f, 0.47675f, + -0.09458f, 0.15248f, 0.43428f, 0.35322f, 0.17564f, 0.27215f, 0.41112f, -0.36480f, + 0.40726f, 0.23355f, -0.25285f, -0.08300f, -0.41112f, -0.12160f, -0.35708f, 0.05211f, + -0.41884f, -0.29531f, 0.02123f, -0.21425f, 0.09844f, -0.30689f, -0.11388f, 0.34550f, + -0.26443f, -0.07142f, -0.39954f, 0.44586f, 0.05983f, -0.48833f, 0.24127f, 0.34936f, + -0.44200f, -0.12546f, 0.12160f, -0.30303f, 0.27215f, 0.07528f, -0.48447f, -0.29145f, + 0.40726f, 0.23355f, -0.25285f, -0.08300f, -0.41112f, -0.12160f, -0.35708f, 0.05211f, + -0.41884f, -0.29531f, 0.02123f, -0.21425f, 0.09844f, -0.30689f, -0.11388f, 0.34550f, + 0.28373f, -0.17564f, 0.09458f, 0.02123f, 0.30689f, 0.41884f, 0.20653f, -0.03667f, + 0.32233f, 0.25671f, -0.45744f, -0.05597f, 0.46517f, -0.41498f, 0.00965f, 0.07142f, + -0.44586f, 0.16406f, -0.20653f, 0.21811f, -0.29917f, 0.28759f, -0.05597f, 0.03281f, + -0.32619f, -0.00965f, 0.31847f, -0.37252f, 0.18722f, -0.11002f, -0.22969f, -0.06369f, + 0.28373f, -0.17564f, 0.09458f, 0.02123f, 0.30689f, 0.41884f, 0.20653f, -0.03667f, + 0.32233f, 0.25671f, -0.45744f, -0.05597f, 0.46517f, -0.41498f, 0.00965f, 0.07142f, + -0.39568f, 0.36866f, -0.45744f, -0.31847f, 0.14476f, -0.22583f, -0.49219f, 0.37638f, + -0.19494f, -0.13318f, 0.39182f, -0.35322f, 0.29531f, -0.24127f, 0.21039f, -0.18722f, + 0.45358f, 0.31461f, -0.13318f, -0.01737f, -0.36094f, 0.12932f, -0.25671f, 0.43814f, + -0.16792f, 0.23355f, -0.22197f, 0.44972f, -0.42270f, 0.33392f, 0.42656f, 0.11774f, + -0.39568f, 0.36866f, -0.45744f, -0.31847f, 0.14476f, -0.22583f, -0.49219f, 0.37638f, + -0.19494f, -0.13318f, 0.39182f, -0.35322f, 0.29531f, -0.24127f, 0.21039f, -0.18722f, + -0.13318f, 0.19494f, -0.03667f, 0.44972f, 0.24513f, -0.15248f, 0.08300f, -0.33006f, + 0.00579f, 0.12546f, 0.19494f, 0.05983f, -0.15634f, 0.14476f, 0.36480f, -0.04053f, + -0.33006f, 0.25671f, -0.46903f, 0.37252f, 0.48833f, -0.09458f, -0.41112f, 0.19108f, + 0.08686f, -0.46903f, -0.07528f, 0.04053f, -0.26829f, -0.02895f, 0.22197f, -0.34164f, + -0.13318f, 0.19494f, -0.03667f, 0.44972f, 0.24513f, -0.15248f, 0.08300f, -0.33006f, + 0.00579f, 0.12546f, 0.19494f, 0.05983f, -0.15634f, 0.14476f, 0.36480f, -0.04053f, + 0.47289f, -0.21811f, 0.06756f, -0.38410f, -0.27987f, -0.06369f, 0.27987f, 0.43814f, + -0.25671f, -0.39182f, 0.49219f, -0.27601f, -0.07914f, -0.48061f, 0.42656f, -0.38410f, + 0.11002f, 0.03667f, -0.27215f, 0.15634f, 0.07528f, -0.22197f, 0.33006f, 0.38410f, + -0.34936f, 0.27987f, 0.15248f, 0.40340f, 0.09844f, -0.16406f, -0.46131f, 0.03281f, + 0.47289f, -0.21811f, 0.06756f, -0.38410f, -0.27987f, -0.06369f, 0.27987f, 0.43814f, + -0.25671f, -0.39182f, 0.49219f, -0.27601f, -0.07914f, -0.48061f, 0.42656f, -0.38410f, + -0.29531f, 0.31461f, -0.10616f, 0.39954f, 0.01351f, 0.33778f, -0.43814f, 0.17178f, + -0.08686f, 0.23741f, -0.44586f, 0.33778f, -0.00193f, -0.31461f, 0.23741f, -0.12932f, + -0.22583f, -0.06756f, 0.40340f, -0.16792f, -0.43428f, 0.01351f, -0.14476f, -0.04053f, + -0.29145f, 0.46517f, -0.13704f, -0.39182f, -0.32233f, 0.29531f, 0.38410f, 0.16020f, + -0.29531f, 0.31461f, -0.10616f, 0.39954f, 0.01351f, 0.33778f, -0.43814f, 0.17178f, + -0.08686f, 0.23741f, -0.44586f, 0.33778f, -0.00193f, -0.31461f, 0.23741f, -0.12932f, + -0.44200f, 0.26443f, 0.12546f, -0.42270f, 0.21425f, -0.19881f, -0.35708f, 0.04825f, + 0.36480f, -0.02895f, -0.21425f, 0.09072f, 0.41498f, 0.18336f, 0.04439f, 0.29917f, + 0.47675f, -0.40340f, 0.27601f, -0.31461f, 0.31075f, 0.17564f, 0.24899f, -0.45744f, + 0.05597f, -0.19494f, 0.00193f, 0.36094f, 0.24127f, -0.09844f, -0.24513f, -0.00965f, + -0.44200f, 0.26443f, 0.12546f, -0.42270f, 0.21425f, -0.19881f, -0.35708f, 0.04825f, + 0.36480f, -0.02895f, -0.21425f, 0.09072f, 0.41498f, 0.18336f, 0.04439f, 0.29917f, + -0.17564f, -0.05597f, -0.34550f, -0.24899f, 0.48061f, 0.15248f, -0.11388f, 0.45358f, + -0.16406f, -0.32233f, 0.31461f, -0.11774f, -0.36866f, -0.18722f, -0.25671f, -0.44200f, + 0.13318f, -0.02123f, 0.19881f, -0.10616f, 0.43042f, -0.36866f, -0.24899f, 0.41112f, + 0.11002f, 0.21425f, -0.25671f, -0.47675f, -0.04439f, 0.13704f, -0.37252f, 0.43814f, + -0.17564f, -0.05597f, -0.34550f, -0.24899f, 0.48061f, 0.15248f, -0.11388f, 0.45358f, + -0.16406f, -0.32233f, 0.31461f, -0.11774f, -0.36866f, -0.18722f, -0.25671f, -0.44200f, + 0.19108f, 0.03667f, 0.35708f, -0.14090f, 0.08300f, -0.02123f, -0.30303f, -0.48061f, + 0.11774f, 0.20267f, -0.43042f, 0.25285f, 0.14090f, -0.04439f, 0.38796f, 0.34550f, + -0.34164f, -0.19494f, 0.05983f, -0.48447f, 0.09844f, -0.00579f, -0.07914f, 0.33778f, + -0.41498f, -0.10230f, 0.30689f, 0.17178f, 0.48833f, -0.20267f, 0.07914f, 0.33392f, + 0.19108f, 0.03667f, 0.35708f, -0.14090f, 0.08300f, -0.02123f, -0.30303f, -0.48061f, + 0.11774f, 0.20267f, -0.43042f, 0.25285f, 0.14090f, -0.04439f, 0.38796f, 0.34550f, + -0.48833f, -0.30689f, 0.41498f, 0.22969f, -0.44586f, 0.32233f, 0.25285f, 0.39182f, + -0.23355f, 0.01737f, 0.42270f, -0.27987f, 0.46903f, -0.47289f, 0.02123f, -0.09072f, + 0.21811f, 0.44586f, -0.25285f, 0.36480f, -0.29145f, 0.47289f, -0.18722f, 0.14476f, + -0.31461f, 0.43814f, -0.36094f, 0.04439f, -0.29917f, -0.41884f, 0.25285f, -0.11774f, + -0.48833f, -0.30689f, 0.41498f, 0.22969f, -0.44586f, 0.32233f, 0.25285f, 0.39182f, + -0.23355f, 0.01737f, 0.42270f, -0.27987f, 0.46903f, -0.47289f, 0.02123f, -0.09072f, + 0.46131f, 0.11388f, -0.21039f, -0.07528f, -0.38024f, -0.26057f, 0.06369f, -0.05983f, + 0.29145f, -0.40340f, -0.09072f, 0.06756f, -0.16020f, 0.27601f, -0.31075f, 0.10616f, + -0.14090f, -0.43042f, 0.25671f, -0.05211f, -0.13318f, 0.23355f, -0.44972f, 0.02895f, + 0.26829f, -0.02895f, -0.17950f, 0.37252f, -0.13704f, 0.40726f, 0.01351f, -0.26443f, + 0.46131f, 0.11388f, -0.21039f, -0.07528f, -0.38024f, -0.26057f, 0.06369f, -0.05983f, + 0.29145f, -0.40340f, -0.09072f, 0.06756f, -0.16020f, 0.27601f, -0.31075f, 0.10616f, + -0.03281f, -0.40340f, 0.27987f, 0.17564f, 0.02509f, 0.44200f, -0.15248f, -0.34550f, + 0.14862f, -0.19881f, -0.01351f, 0.36866f, -0.38796f, 0.19494f, -0.22197f, 0.32619f, + -0.37638f, 0.00193f, 0.30689f, 0.12160f, -0.39182f, 0.16792f, -0.34550f, 0.39954f, + -0.23355f, 0.09072f, -0.43428f, 0.22969f, -0.06369f, 0.12546f, -0.35322f, 0.30689f, + -0.03281f, -0.40340f, 0.27987f, 0.17564f, 0.02509f, 0.44200f, -0.15248f, -0.34550f, + 0.14862f, -0.19881f, -0.01351f, 0.36866f, -0.38796f, 0.19494f, -0.22197f, 0.32619f, + -0.09844f, 0.06756f, 0.38410f, -0.33392f, -0.18336f, 0.35322f, 0.21039f, -0.42270f, + 0.48833f, 0.33006f, 0.21811f, -0.33392f, 0.12932f, -0.05211f, 0.39568f, 0.04825f, + 0.48061f, 0.17950f, -0.31847f, -0.21811f, 0.38024f, 0.05211f, 0.32233f, -0.06756f, + -0.12546f, 0.46131f, 0.16020f, -0.25285f, 0.29531f, -0.44972f, 0.17950f, -0.16406f, + -0.09844f, 0.06756f, 0.38410f, -0.33392f, -0.18336f, 0.35322f, 0.21039f, -0.42270f, + 0.48833f, 0.33006f, 0.21811f, -0.33392f, 0.12932f, -0.05211f, 0.39568f, 0.04825f, + 0.22583f, -0.46131f, -0.27601f, -0.00579f, 0.12932f, -0.47289f, -0.09844f, 0.10230f, + -0.28759f, -0.12160f, -0.49219f, -0.24127f, 0.44586f, -0.11388f, -0.45358f, -0.27215f, + -0.17178f, -0.07528f, -0.47675f, 0.43042f, -0.02509f, -0.27215f, -0.19108f, 0.19881f, + -0.49219f, -0.37252f, 0.33392f, -0.00193f, -0.33006f, -0.20267f, 0.48061f, 0.34164f, + 0.22583f, -0.46131f, -0.27601f, -0.00579f, 0.12932f, -0.47289f, -0.09844f, 0.10230f, + -0.28759f, -0.12160f, -0.49219f, -0.24127f, 0.44586f, -0.11388f, -0.45358f, -0.27215f, + -0.22969f, 0.42270f, -0.12160f, 0.31075f, 0.46903f, -0.22583f, 0.27215f, -0.02509f, + 0.03281f, 0.40340f, 0.25671f, 0.08686f, 0.00965f, 0.29145f, -0.41112f, 0.14090f, + 0.24513f, 0.34164f, 0.08686f, -0.14862f, 0.27601f, -0.42656f, 0.48447f, 0.09844f, + 0.26443f, -0.27987f, 0.05597f, -0.10230f, 0.43428f, 0.08686f, 0.02895f, -0.38024f, + -0.22969f, 0.42270f, -0.12160f, 0.31075f, 0.46903f, -0.22583f, 0.27215f, -0.02509f, + 0.03281f, 0.40340f, 0.25671f, 0.08686f, 0.00965f, 0.29145f, -0.41112f, 0.14090f, + 0.15634f, 0.09458f, -0.36480f, 0.18336f, -0.05211f, -0.40726f, 0.36866f, -0.33778f, + -0.19881f, 0.16020f, -0.37638f, -0.16020f, -0.29917f, 0.20267f, 0.41884f, -0.01737f, + -0.34936f, -0.24127f, 0.02509f, 0.20653f, -0.36480f, -0.08686f, 0.01737f, -0.33778f, + 0.41498f, -0.03667f, 0.37638f, -0.17178f, -0.47289f, 0.26829f, -0.28759f, -0.05597f, + 0.15634f, 0.09458f, -0.36480f, 0.18336f, -0.05211f, -0.40726f, 0.36866f, -0.33778f, + -0.19881f, 0.16020f, -0.37638f, -0.16020f, -0.29917f, 0.20267f, 0.41884f, -0.01737f, + 0.35708f, 0.00193f, 0.25285f, -0.15634f, -0.30303f, 0.06369f, 0.22197f, 0.45358f, + -0.43814f, 0.30303f, -0.04053f, 0.46517f, 0.35322f, -0.21039f, 0.06756f, -0.14090f, + 0.37638f, -0.43042f, 0.45744f, -0.29531f, 0.39568f, 0.14862f, 0.23741f, -0.13704f, + -0.21425f, 0.16406f, -0.40726f, 0.22583f, 0.13318f, 0.38796f, -0.12932f, -0.43428f, + 0.35708f, 0.00193f, 0.25285f, -0.15634f, -0.30303f, 0.06369f, 0.22197f, 0.45358f, + -0.43814f, 0.30303f, -0.04053f, 0.46517f, 0.35322f, -0.21039f, 0.06756f, -0.14090f, + -0.31461f, -0.20653f, 0.46131f, -0.45358f, 0.39568f, -0.24513f, -0.14090f, 0.11002f, + -0.08300f, -0.26829f, 0.05211f, -0.46517f, -0.09844f, -0.39568f, -0.32619f, -0.06369f, + 0.16792f, 0.28373f, 0.11388f, -0.04439f, -0.18336f, -0.44200f, 0.35322f, -0.26057f, + -0.46517f, 0.31075f, -0.07914f, -0.34164f, -0.24513f, -0.02123f, 0.19108f, 0.44200f, + -0.31461f, -0.20653f, 0.46131f, -0.45358f, 0.39568f, -0.24513f, -0.14090f, 0.11002f, + -0.08300f, -0.26829f, 0.05211f, -0.46517f, -0.09844f, -0.39568f, -0.32619f, -0.06369f, + 0.04825f, -0.07914f, -0.39954f, 0.12160f, 0.29145f, 0.00965f, -0.37638f, 0.32233f, + 0.20267f, -0.17564f, 0.39182f, 0.12160f, 0.18336f, 0.32619f, 0.26057f, 0.49219f, + -0.48447f, -0.20653f, -0.10616f, -0.38796f, 0.31847f, 0.07528f, -0.01737f, 0.44586f, + 0.11774f, 0.02509f, 0.47289f, 0.07142f, 0.33392f, -0.38410f, -0.17950f, 0.28373f, + 0.04825f, -0.07914f, -0.39954f, 0.12160f, 0.29145f, 0.00965f, -0.37638f, 0.32233f, + 0.20267f, -0.17564f, 0.39182f, 0.12160f, 0.18336f, 0.32619f, 0.26057f, 0.49219f + ]; +} From 9ad02108f331abab74d824c16285d31c93d7fdb0 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:25:37 +0400 Subject: [PATCH 126/142] Add IJxlImageOutput --- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 3 +- .../Jxl/Processing/Image/IJxlImageOutput.cs | 54 +++++++++++++++++++ .../Jxl/Processing/Primitives/Delegates.cs | 25 --------- 3 files changed, 55 insertions(+), 27 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Image/IJxlImageOutput.cs delete mode 100644 src/ImageSharp/Formats/Jxl/Processing/Primitives/Delegates.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 6a850d8288..4606b6d338 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -1928,8 +1928,7 @@ public int ProcessCodestream() new PixelCallback( this.imageOutputInitCallback, this.imageOutputRunCallback, - this.imageOutputDestroyCallback, - this.imageOutputInitOpaque), + this.imageOutputDestroyCallback), this.imageOutBuffer, this.imageOutputSize, dimensions.Width, diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/IJxlImageOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/IJxlImageOutput.cs new file mode 100644 index 0000000000..72d631ff3b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/IJxlImageOutput.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Image; + +/// +/// Abstracts a way for the codec to output images during decoding. +/// +internal interface IJxlImageOutput : IDisposable +{ + /// + /// Gets or sets the pixel format for the output pixels. + /// + public JxlPixelFormat PixelFormat { get; set; } + + /// + /// Gets or sets the output bit depth for unsigned data types. + /// + public int BitsPerSample { get; set; } + + /// + /// Gets or sets the pixel buffer for image output. + /// + public Memory Buffer { get; set; } + + /// + /// Gets or sets length of a row of image buffer in bytes. + /// + public int Stride { get; set; } + + /// + /// Outputs the image. + /// + /// Data where the image should be output. + /// X offset + /// Y offset + /// Pixels to output. + public void Output( + Span data, + int x, + int y, + Span pixels); + + /// + /// Initializes image data. + /// + /// Output image data. + /// Number of pixels a thread processes. + public void Initialize( + Span data, + int numPixelsPerThread); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/Delegates.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/Delegates.cs deleted file mode 100644 index e0f7c78a33..0000000000 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/Delegates.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; - -internal delegate void JxlImageOutputCallback( - Span data, - int x, - int y, - int numPixels, - Span pixels); - -internal delegate void JxlImageOutputInitializeCallback( - Span data, - int numThreads, - int numPixelsPerThread); - -internal delegate void JxlImageOutputRunCallback( - Span data, - int x, - int y, - int numPixels, - Span pixels); - -internal delegate void JxlImageOutputDestroyCallback(Span data); From eead4a93615fc6bd71bfa6563c31a47afe701c7f Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:31:31 +0400 Subject: [PATCH 127/142] Add ShouldFlip methods, fix typo --- .../Jxl/IO/Metadata/JxlExifOrientation.cs | 2 +- .../RenderPipeline/WriteToOutputStage.cs | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExifOrientation.cs b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExifOrientation.cs index 406e7e9a1a..fb7d8d7bba 100644 --- a/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExifOrientation.cs +++ b/src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExifOrientation.cs @@ -9,7 +9,7 @@ internal enum JxlExifOrientation : byte FlipHorizontal = 2, Rotate180 = 3, FlipVertical = 4, - Transponse = 5, + Transpose = 5, Rotate90 = 6, AntiTranspose = 7, Rotate270 = 8 diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs index 09426afb2b..e07be6ec06 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs @@ -1,6 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; + namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; internal sealed class WriteToOutputStage @@ -209,4 +212,22 @@ internal sealed class WriteToOutputStage 0.04825f, -0.07914f, -0.39954f, 0.12160f, 0.29145f, 0.00965f, -0.37638f, 0.32233f, 0.20267f, -0.17564f, 0.39182f, 0.12160f, 0.18336f, 0.32619f, 0.26057f, 0.49219f ]; + + private static bool ShouldFlipX(JxlExifOrientation orientation) => + orientation is JxlExifOrientation.FlipHorizontal or + JxlExifOrientation.Rotate180 or + JxlExifOrientation.Rotate270 or + JxlExifOrientation.AntiTranspose; + + private static bool ShouldFlipY(JxlExifOrientation orientation) => + orientation is JxlExifOrientation.FlipVertical or + JxlExifOrientation.Rotate180 or + JxlExifOrientation.Rotate90 or + JxlExifOrientation.AntiTranspose; + + private static bool ShouldTranspose(JxlExifOrientation orientation) => + orientation is JxlExifOrientation.Transpose or + JxlExifOrientation.Rotate90 or + JxlExifOrientation.Rotate270 or + JxlExifOrientation.AntiTranspose; } From a0feb216aecc78cfcdee578380bac221a43accff Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:50:24 +0400 Subject: [PATCH 128/142] Add Y'Cb'Cr stage, make Epf1Stage class sealed --- .../Processing/RenderPipeline/Epf1Stage.cs | 2 +- .../RenderPipeline/WriteToOutputStage.cs | 2 + .../Processing/RenderPipeline/YCbCrStage.cs | 67 +++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs index b42513898c..5dc19b6626 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/Epf1Stage.cs @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; /// /// Edge Preserving Filter (type 1) stage. /// -internal class Epf1Stage : RenderPipelineStageBase +internal sealed class Epf1Stage : RenderPipelineStageBase { private readonly JxlLoopFilter loopFilter; private readonly JxlImageF sigma; diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs index e07be6ec06..ad790ed10a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs @@ -1,8 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs new file mode 100644 index 0000000000..6de34c61bf --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +/// +/// SIMD-based conversion from Y'Cb'Cr to RGB pixel buffers. +/// +internal sealed class YCbCrStage : RenderPipelineStageBase +{ + public YCbCrStage(Configuration configuration) + : base(configuration) + { + } + + /// + public override string Name => "YCbCr"; + + /// + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + // Vectors for conversion, defined by the ITU + Vector c128 = Vector.Create(128.0f / 255); + Vector crcr = Vector.Create(1.402f); + Vector cgcb = Vector.Create(-0.114f * 1.772f / 0.587f); + Vector cgcr = Vector.Create(-0.299f * 1.402f / 0.587f); + Vector cbcb = Vector.Create(1.772f); + + Span row0 = this.GetInputRow(inputRows, 0, 0); + Span row1 = this.GetInputRow(inputRows, 1, 0); + Span row2 = this.GetInputRow(inputRows, 2, 0); + + // Using refs for better performance + ref float row0Ref = ref MemoryMarshal.GetReference(row0); + ref float row1Ref = ref MemoryMarshal.GetReference(row1); + ref float row2Ref = ref MemoryMarshal.GetReference(row2); + + for (int x = 0; x < width; x += Vector.Count) + { + // Y'Cb'Cr input vectors + Vector yVec = Vector.LoadUnsafe(ref Unsafe.Add(ref row1Ref, x)) + c128; + Vector cbVec = Vector.LoadUnsafe(ref Unsafe.Add(ref row0Ref, x)); + Vector crVec = Vector.LoadUnsafe(ref Unsafe.Add(ref row2Ref, x)); + + // RGB output vectors + Vector rVec = (crcr * crVec) + yVec; + Vector gVec = (cgcr * crVec) + ((cgcb * cbVec) + yVec); + Vector bVec = (cbcb * cbVec) + yVec; + + // Copying to the output... + rVec.StoreUnsafe(ref Unsafe.Add(ref row0Ref, x)); + gVec.StoreUnsafe(ref Unsafe.Add(ref row1Ref, x)); + bVec.StoreUnsafe(ref Unsafe.Add(ref row2Ref, x)); + } + } + + /// + public override RenderPipelineChannelMode GetChannelMode(int channel) => + channel < 3 + ? RenderPipelineChannelMode.InPlace + : RenderPipelineChannelMode.Ignored; +} From 9f3ccfada9e8a33cb18f32cb515499a7452bde8e Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:51:53 +0400 Subject: [PATCH 129/142] Use primary constructor --- .../Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs index 6de34c61bf..7c6ad0362b 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/YCbCrStage.cs @@ -11,13 +11,8 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; /// /// SIMD-based conversion from Y'Cb'Cr to RGB pixel buffers. /// -internal sealed class YCbCrStage : RenderPipelineStageBase +internal sealed class YCbCrStage(Configuration configuration) : RenderPipelineStageBase(configuration) { - public YCbCrStage(Configuration configuration) - : base(configuration) - { - } - /// public override string Name => "YCbCr"; From 76ed571b621bfd0f553de107b97cebae5ed481d0 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:06:58 +0400 Subject: [PATCH 130/142] Add Gaborish stage --- .../RenderPipeline/GaborishStage.cs | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/GaborishStage.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/GaborishStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/GaborishStage.cs new file mode 100644 index 0000000000..fbc83ab214 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/GaborishStage.cs @@ -0,0 +1,100 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal sealed class GaborishStage : RenderPipelineStageBase +{ + private InlineArray9 weights; + + public GaborishStage(Configuration configuration, JxlLoopFilter lf) + : base(configuration) + { + this.Settings = RenderPipelineStageConfiguration.CreateSymmetricBorderOnly(1); + + this.weights[0] = 1; + this.weights[1] = lf.GaborishXWeight1; + this.weights[2] = lf.GaborishXWeight2; + this.weights[3] = 1; + this.weights[4] = lf.GaborishYWeight1; + this.weights[5] = lf.GaborishYWeight2; + this.weights[6] = 1; + this.weights[7] = lf.GaborishBWeight1; + this.weights[8] = lf.GaborishBWeight2; + + // Normalization + for (int c = 0; c < 3; c++) + { + int c3 = c * 3; // prevent repeated multiplication + + float div = this.weights[c3] + (4 * (this.weights[c3 + 1] + this.weights[c3 + 2])); + float mul = 1.0f / div; + + this.weights[c3] *= mul; + this.weights[c3 + 1] *= mul; + this.weights[c3 + 2] *= mul; + } + } + + /// + public override string Name => "Gab"; + + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + int xStart = -JxlMath.RoundUpTo(xExtraLeft, Vector.Count); + int xEnd = width + xExtraRight; + + for (int c = 0; c < 3; c++) + { + int c3 = c * 3; // prevent repeated multiplication + + Span rowT = this.GetInputRow(inputRows, c, -1); + Span rowM = this.GetInputRow(inputRows, c, 0); + Span rowB = this.GetInputRow(inputRows, c, 1); + Span rowOut = GetOutputRow(outputRows, c, 0); + + Vector w0 = Vector.Create(this.weights[c3]); + Vector w1 = Vector.Create(this.weights[c3 + 1]); + Vector w2 = Vector.Create(this.weights[c3 + 2]); + + // Ref for performance + ref float refRowT = ref MemoryMarshal.GetReference(rowT); + ref float refRowM = ref MemoryMarshal.GetReference(rowM); + ref float refRowB = ref MemoryMarshal.GetReference(rowB); + ref float refRowOut = ref MemoryMarshal.GetReference(rowOut); + + for (int x = xStart; x < xEnd; x += Vector.Count) + { + Vector t = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowT, x)); + Vector tl = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowT, x - 1)); + Vector tr = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowT, x + 1)); + + Vector m = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowM, x)); + Vector l = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowM, x - 1)); + Vector r = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowM, x + 1)); + + Vector b = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowB, x)); + Vector bl = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowB, x - 1)); + Vector br = Vector.LoadUnsafe(ref Unsafe.Add(ref refRowB, x + 1)); + + Vector sum0 = m; + Vector sum1 = (l + r) + (t + b); + Vector sum2 = (tl + tr) + (bl + br); + + Vector pixels = (sum2 * w2) + ((sum1 * w1) + (sum0 * w0)); + pixels.StoreUnsafe(ref Unsafe.Add(ref refRowOut, x)); + } + } + } + + /// + public override RenderPipelineChannelMode GetChannelMode(int channel) => + channel < 3 + ? RenderPipelineChannelMode.InPlace + : RenderPipelineChannelMode.Ignored; +} From 64edab5721df90ee4adf86b4740444d7b41de2ad Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:20:52 +0400 Subject: [PATCH 131/142] Add patch dictionary stage --- .../Processing/Decoder/JxlPatchDictionary.cs | 2 +- .../RenderPipeline/PatchDictionaryStage.cs | 40 +++++++++++++++++++ .../RenderPipeline/RenderPipelineStageBase.cs | 6 +++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs index c990288589..d91c41e38e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlPatchDictionary.cs @@ -53,7 +53,7 @@ public void Decode( this.blendingsStride = (int)(numExtraChannels + 1); List contextMap = []; - var code = new JxlAnsCode(); + JxlAnsCode code = new(); var status = DecodeHistograms( memoryManager, diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs new file mode 100644 index 0000000000..0e6bf82745 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Drawing; +using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal sealed class PatchDictionaryStage(Configuration configuration, JxlPatchDictionary patches, List extraChannelInfos) + : RenderPipelineStageBase(configuration) +{ + /// + public override string Name => "Patches"; + + /// + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + int channels = 3 + extraChannelInfos.Count; + + Span> rowPtrs = new Memory[channels]; + + for (int i = 0; i < channels; i++) + { + rowPtrs[i] = this.GetInputRowMemory(inputRows, i, 0, xExtraLeft); + } + + return patches.AddOneRow(rowPtrs, yPos, xPos - xExtraLeft, width + xExtraLeft + xExtraRight, extraChannelInfos); + } + + /// + public override RenderPipelineChannelMode GetChannelMode(int channel) + { + int numChannels = 3 + extraChannelInfos.Count; + return channel < numChannels + ? RenderPipelineChannelMode.InPlace + : RenderPipelineChannelMode.Ignored; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs index b02b62db7a..07cb0f59db 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/RenderPipelineStageBase.cs @@ -71,9 +71,15 @@ public virtual void SetInputSizes(Span inputSizes) public Span GetInputRow(Buffer2D> inputRows, int c, int offset) => inputRows[c, this.Settings.BorderY + offset].Span[RenderPipelineXOffset..]; + public Span GetInputRow(Buffer2D> inputRows, int c, int offset, int xExtraLeft) + => inputRows[c, this.Settings.BorderY + offset].Span[(RenderPipelineXOffset - xExtraLeft)..]; + public Memory GetInputRowMemory(Buffer2D> inputRows, int c, int offset) => inputRows[c, this.Settings.BorderY + offset][RenderPipelineXOffset..]; + public Memory GetInputRowMemory(Buffer2D> inputRows, int c, int offset, int xExtraLeft) + => inputRows[c, this.Settings.BorderY + offset][(RenderPipelineXOffset - xExtraLeft)..]; + public static Span GetOutputRow(Buffer2D> outputRows, int c, int offset) => outputRows[c, offset].Span[RenderPipelineXOffset..]; From e0305d78c5dc0986a860034e3c5d859c1a406528 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:58:14 +0400 Subject: [PATCH 132/142] Partial adaptive quantizer encoder --- .../Encoder/JxlAdaptiveQuantization.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs new file mode 100644 index 0000000000..00150b3dd3 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlAdaptiveQuantization.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +/// +/// Adaptive quantization encoder +/// +internal static class JxlAdaptiveQuantization +{ + // Scaling differences between JPEG XL and Butteraugli + private const float SGMul = 226.77216153508914f; + private const float SGMul2 = 1f / 73.377132366608819f; + + // Includes correlation factor for std::log -> log2 + private const float SGRetMul = SGMul2 * 18.6580932135f * JxlMath.InverseLog2E; + private const float SGVOffset = 7.7825991679894591f; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float ComputeMaskForAcStrategyUse(float outputValue) + { + const float multiplier = 1f; + const float offset = 0.001f; + + return multiplier / (outputValue + offset); + } + + public static float RatioOfDerivativesOfCubicRootToSimpleGamma(float v, bool invert = false) + { + float epsilon = 1e-2f; + v = Math.Max(0, v); // cannot be < 0 + const float numMul = SGRetMul * 3 * SGMul; + float voffset = (SGVOffset * JxlMath.InverseLog2E) + epsilon; + const float denMul = JxlMath.InverseLog2E * SGMul; + + float v2 = v * v; + float num = (numMul * v2) + epsilon; + float den = ((denMul * v) * v2) + voffset; + + return invert ? num / den : den / num; + } +} From 5de2701e1c1061be684d95bf4044899539b135cb Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 22:23:34 +0400 Subject: [PATCH 133/142] Add JxlToJpegDecoder static methods --- .../Jxl/Processing/Decoder/JxlDecoderCore.cs | 13 +- .../Processing/Decoder/JxlToJpegDecoder.cs | 150 ++++++++++++++++++ 2 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs index 4606b6d338..9fefcde018 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlDecoderCore.cs @@ -363,11 +363,6 @@ internal sealed class JxlDecoderCore : ImageDecoderCore, IDisposable /// private readonly JxlBoxContentDecoder? boxContentDecoder; - /// - /// Decodes JPEG XL to JPEG. - /// - private JxlToJpegDecoder? jpegDecoder; - private readonly JxlBoxContentDecoder? metadataDecoder; /// @@ -2246,12 +2241,12 @@ public int ProcessBoxes(Stream stream) if (this.reconstructionExifSize > 0) { - JxlToJpegDecoder.SetExif(this.exifMetadata!.Memory, jpegData); + JxlToJpegDecoder.TrySetExif(this.exifMetadata!.Memory.Span, jpegData); } if (this.reconstructionXmpSize > 0) { - JxlToJpegDecoder.SetXmp(this.xmpMetadata!.Memory, jpegData); + JxlToJpegDecoder.TrySetXmp(this.xmpMetadata!.Memory.Span, jpegData); } this.reconstructionOutputJpeg = JpegReconstructionStage.Output; @@ -2592,7 +2587,7 @@ public int ProcessBoxes(Stream stream) throw new InvalidOperationException("Only one EXIF marker for JPEG reconstruction can be present"); } - if (JxlToJpegDecoder.ExifBoxContentSize(jpegData, ref this.reconstructionExifSize) != Success) + if (!JxlToJpegDecoder.ExifBoxContentSize(jpegData, ref this.reconstructionExifSize)) { throw new InvalidOperationException("Invalid jbrd EXIF size"); } @@ -2605,7 +2600,7 @@ public int ProcessBoxes(Stream stream) throw new InvalidOperationException("Only one XMP marker for JPEG reconstruction can be present"); } - if (JxlToJpegDecoder.XmlBoxContentSize(jpegData, ref this.reconstructionXmpSize) != Success) + if (!JxlToJpegDecoder.XmpBoxContentSize(jpegData, ref this.reconstructionXmpSize)) { throw new InvalidOperationException("Invalid jbrd XMP size"); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs new file mode 100644 index 0000000000..995b74852a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlToJpegDecoder.cs @@ -0,0 +1,150 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; + +internal class JxlToJpegDecoder +{ + /// + /// Returns the number of EXIF markers in the JPEG file. + /// + /// Input parsed JPEG data + /// Number of EXIFs in the JPEG + public static int NumExifMarkers(JpegData jpegData) => jpegData.AppMarkerTypes.Count(x => x == JpegAppMarkerType.Exif); + + /// + /// Returns the number of XMP markers in the JPEG file. + /// + /// Input parsed JPEG data + /// Number of XMPs in the JPEG + public static int NumXmpMarkers(JpegData jpegData) => jpegData.AppMarkerTypes.Count(x => x == JpegAppMarkerType.Xmp); + + /// + /// Attempts to set EXIF data in the JPEG file. + /// + /// EXIF data + /// JPEG file for EXIF data + /// If EXIF data was set, true; returns false if no EXIF marker is present, or is present but not enough data + public static bool TrySetExif(Span data, JpegData jpegData) + { + int size = data.Length; + ReadOnlySpan exifTag = JpegDataConstants.ExifTag; + int exifTagSize = exifTag.Length; + + for (int i = 0; i < jpegData.AppData.Count; ++i) + { + if (jpegData.AppMarkerTypes[i] == JpegAppMarkerType.Exif) + { + Span dataSpan = CollectionsMarshal.AsSpan(jpegData.AppData[i]); + + if (dataSpan.Length != size + 3 + exifTagSize - 4) + { + return false; + } + + // The first 9 bytes are used for JPEG marker header. + dataSpan[0] = 0xE1; + + // The second and third byte are already filled in correctly + exifTag.CopyTo(dataSpan[3..]); + + // The first 4 bytes are the TIFF header from the box contents, and are + // not included in the JPEG + data[4..].CopyTo(dataSpan[(3 + exifTagSize)..]); + + return true; + } + } + + return false; + } + + /// + /// Attempts to set XMP data in the JPEG file. + /// + /// XMP data + /// JPEG file for XMP data + /// If XMP data was set, true; returns false if no XMP marker is present, or is present but not enough data + public static bool TrySetXmp(Span data, JpegData jpegData) + { + int size = data.Length; + ReadOnlySpan xmpTag = JpegDataConstants.XmpTag; + int xmpTagSize = xmpTag.Length; + + for (int i = 0; i < jpegData.AppData.Count; ++i) + { + if (jpegData.AppMarkerTypes[i] == JpegAppMarkerType.Xmp) + { + Span dataSpan = CollectionsMarshal.AsSpan(jpegData.AppData[i]); + + if (dataSpan.Length != size + 3 + xmpTagSize) + { + return false; + } + + // The first 9 bytes are used for JPEG marker header. + dataSpan[0] = 0xE1; + + // The second and third byte are already filled in correctly + xmpTag.CopyTo(dataSpan[3..]); + + data.CopyTo(dataSpan[(3 + xmpTagSize)..]); + + return true; + } + } + + return false; + } + + public static bool ExifBoxContentSize(JpegData jpegData, ref long size) + { + size = 0; + int exifTagLength = JpegDataConstants.ExifTag.Length; + + for (int i = 0; i < jpegData.AppData.Count; ++i) + { + if (jpegData.AppMarkerTypes[i] == JpegAppMarkerType.Exif) + { + if (jpegData.AppData[i].Count < 3 + exifTagLength) + { + // too small for app marker header + return false; + } + + // The first 4 bytes are the TIFF header from the box contents, and are + // not included in the JPEG + size = jpegData.AppData[i].Count + 4 - 3 - exifTagLength; + return true; + } + } + + return false; + } + + public static bool XmpBoxContentSize(JpegData jpegData, ref long size) + { + size = 0; + int xmpTagLength = JpegDataConstants.XmpTag.Length; + + for (int i = 0; i < jpegData.AppData.Count; ++i) + { + if (jpegData.AppMarkerTypes[i] == JpegAppMarkerType.Xmp) + { + if (jpegData.AppData[i].Count < 3 + xmpTagLength) + { + // too small for app marker header + return false; + } + + size = jpegData.AppData[i].Count - 3 - xmpTagLength; + return true; + } + } + + return false; + } +} From 4476abd507ccecee159ad1a5f5e510c11b4bf2d2 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:40:18 +0400 Subject: [PATCH 134/142] Add 3 missing methods to JxlFrameDecoder, add spline and spotcolor render pipeline stages --- .../Jxl/Processing/Decoder/JxlFrameDecoder.cs | 25 +++++++++++++ .../Processing/RenderPipeline/SplineStage.cs | 21 +++++++++++ .../RenderPipeline/SpotColorStage.cs | 37 +++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/SplineStage.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/SpotColorStage.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs index 9b8e9b0f28..5e0eefc9ef 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Decoder/JxlFrameDecoder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.IO; using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; @@ -133,4 +134,28 @@ public static void DecodeFrame(JxlPassesDecoderState decoderState, Stream stream frameDecoder.FinalizeFrame(); } + + private static int BytesPerChannel(JxlDataType dataType) => + dataType == JxlDataType.Byte ? 1 + : dataType == JxlDataType.Single + ? 4 + : 2; + + private int GetStorageLocation(int thread, int task) => this.useTaskId ? task : thread; + + private void PrepareStorage(int numThreads, int numTasks) + { + int storageSize = Math.Min(numThreads, numTasks); + if (storageSize > this.groupDecoderCaches.Count) + { + this.groupDecoderCaches = [.. this.groupDecoderCaches.Take(storageSize)]; + } + + this.useTaskId = numThreads > numTasks; + bool useNoise = (this.frameHeader.Flags & (int)JxlFrameHeaderFlags.Noise) != 0; + bool useGroupIds = this.modularFrameDecoder.UsesFullImage && (this.frameHeader.Encoding == JxlFrameEncoding.VarDct || useNoise); + + this.decoderState.RenderPipeline?.PrepareForThreads(storageSize, useGroupIds); + this.decoderState.Upsampler8x.PrepareForThreads(numThreads); + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/SplineStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/SplineStage.cs new file mode 100644 index 0000000000..39864265d0 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/SplineStage.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal sealed class SplineStage(Configuration configuration, JxlSplines splines) : RenderPipelineStageBase(configuration) +{ + public override string Name => "Splines"; + + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + Span rowX = this.GetInputRow(inputRows, 0, 0, xExtraLeft); + Span rowY = this.GetInputRow(inputRows, 1, 0, xExtraLeft); + Span rowB = this.GetInputRow(inputRows, 2, 0, xExtraLeft); + splines.AddToRow(rowX, rowY, rowB, yPos, xPos - xExtraLeft, xPos + width + xExtraRight); + } + + public override RenderPipelineChannelMode GetChannelMode(int channel) => channel < 3 ? RenderPipelineChannelMode.InPlace : RenderPipelineChannelMode.Ignored; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/SpotColorStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/SpotColorStage.cs new file mode 100644 index 0000000000..6b0fc97ed8 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/SpotColorStage.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; + +internal sealed class SpotColorStage(Configuration configuration, int spotColorOffset, Memory spotColor) + : RenderPipelineStageBase(configuration) +{ + private readonly int spotC = 3 + spotColorOffset; + + public override string Name => "Spot"; + + public override void ProcessRow(Buffer2D> inputRows, Buffer2D> outputRows, int xExtraLeft, int xExtraRight, int width, int xPos, int yPos) + { + Span spotColors = spotColor.Span; + + float scale = 0; + for (int c = 0; c < 3; c++) + { + Span p = this.GetInputRow(inputRows, c, 0); + Span s = this.GetInputRow(inputRows, this.spotC, 0); + + for (int x = 0; x < width; x++) + { + float mix = scale * s[x]; + p[x] = (mix * spotColors[c]) + ((1.0f - mix) * p[x]); + } + } + } + + public override RenderPipelineChannelMode GetChannelMode(int channel) + => channel < 3 ? RenderPipelineChannelMode.InPlace + : channel == this.spotC ? RenderPipelineChannelMode.Input + : RenderPipelineChannelMode.Ignored; +} From 57392184374eb043c0342415380f3ec322795935 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 3 Sep 2026 21:23:22 +0400 Subject: [PATCH 135/142] Add a few CMS transfer functions (incomplete) --- .../JxlBt709TransferFunction.cs | 20 +++ .../JxlHybridLogGammaTransferFunction.cs | 53 ++++++++ .../JxlHybridLogGammaTransferFunctionBase.cs | 114 ++++++++++++++++++ ...ceptualQuantizationTransferFunctionBase.cs | 54 +++++++++ .../Formats/Jxl/Processing/JxlSimdUtils.cs | 8 ++ .../RenderPipeline/PatchDictionaryStage.cs | 1 - .../RenderPipeline/WriteToOutputStage.cs | 3 - 7 files changed, 249 insertions(+), 4 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlBt709TransferFunction.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHybridLogGammaTransferFunction.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHybridLogGammaTransferFunctionBase.cs create mode 100644 src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPerceptualQuantizationTransferFunctionBase.cs diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlBt709TransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlBt709TransferFunction.cs new file mode 100644 index 0000000000..99b6548b60 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlBt709TransferFunction.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; + +/// +/// ITU-R BT.709 transfer function +/// +internal static class JxlBt709TransferFunction +{ + public static double EncodedFromDisplay(double d) + { + if (d < Threshold) + { + return MulLow * d; + } + + return (MulHi * Math.Pow(d, PowHi)) + Sub; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHybridLogGammaTransferFunction.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHybridLogGammaTransferFunction.cs new file mode 100644 index 0000000000..3957c3ac7b --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHybridLogGammaTransferFunction.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Formats.Jxl.Processing; + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; + +internal sealed class JxlHybridLogGammaTransferFunction : JxlHybridLogGammaTransferFunctionBase +{ + private const float HiAdd = B * Inverse12; + private const float HiMul = 0.003639807079052639f; // MathF.Exp(-C * RA) * Inverse12 + private const float HiPow = 8.067285659607931f; // RA * JxlMath.InverseLog2E + + /// + /// Initializes a new instance of the class. + /// + /// + /// Use static methods. Don't instantiate this class. + /// + private JxlHybridLogGammaTransferFunction() + { + } + + public static Vector EncodedFromDisplay(Vector x) + { + Vector sign = Vector.Create(0x80000000u).As(); + Vector originalSign = x & sign; + x = Vector.AndNot(sign, x); + Vector belowInverse12 = Vector.LessThan(x, Vector.Create(Inverse12)); + + Vector lo = Vector.SquareRoot(Vector.Create(3.0f) * x); + Vector hi = (Vector.Create(A * JxlMath.InverseLog2E) * Vector.Log2((Vector.Create(12f) * x) + Vector.Create(-B))) + Vector.Create(C); + Vector magnitude = Vector.ConditionalSelect(belowInverse12, lo, hi); + return Vector.AndNot(sign, magnitude) | originalSign; + } + + public static Vector DisplayFromEncoded(Vector x) + { + Vector sign = Vector.Create(0x80000000u).As(); + Vector originalSign = x & sign; + x = Vector.AndNot(sign, x); + Vector below05 = Vector.LessThan(x, Vector.Create(0.5f)); + + Vector lo = x * (x * Vector.Create(1f / 3f)); + Vector hi = (Pow2(x * Vector.Create(HiPow)) * Vector.Create(HiMul)) + Vector.Create(HiAdd); + Vector magnitude = Vector.ConditionalSelect(below05, lo, hi); + + return Vector.AndNot(sign, magnitude) | originalSign; + } + + private static Vector Pow2(Vector x) => x * x; +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHybridLogGammaTransferFunctionBase.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHybridLogGammaTransferFunctionBase.cs new file mode 100644 index 0000000000..795db106c3 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHybridLogGammaTransferFunctionBase.cs @@ -0,0 +1,114 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; + +/// +/// Base class for HLG transfer function. +/// +internal abstract class JxlHybridLogGammaTransferFunctionBase +{ + // Shared constants used by transfer functions + protected const float A = 0.17883277f; + protected const float RA = 1.0f / A; + protected const float B = 1 - (4 * A); + protected const float C = 0.5599107295f; + protected const float Inverse12 = 1.0f / 12.0f; + + /// + /// Converts encoded signal to display signal. + /// + /// The encoded signal + /// The display signal + protected static double DisplayFromEncoded(double encoded) => Ootf(InverseOotf(encoded)); + + /// + /// Converts display signal to encoded signal. + /// + /// The display signal + /// The encoded signal + protected static double EncodedFromDisplay(double display) => Oetf(InverseOetf(display)); + + /// + /// Opto-Electronic Transfer Function - converts + /// real-world scene light (s) into a digital video + /// signal inside a camera. + /// + /// + /// + /// + /// Scene light + /// Digital video signal + private static double Oetf(double s) + { + if (s == 0) + { + return 0; + } + + double originalSign = s; + + s = Math.Abs(s); + + if (s <= Inverse12) + { + return Math.CopySign(Math.Sqrt(3.0 * s), originalSign); + } + + double e = (A * Math.Log((12 * s) - B)) + C; + DebugGuard.MustBeGreaterThan(e, 0.0, nameof(e)); + + return Math.CopySign(e, originalSign); + } + + /// + /// Inverse Opto-Electronic Transfer Function - converts + /// digital video signal into a real-world scene light. + /// + /// + /// + /// + /// Digital video signal + /// Scene light + private static double InverseOetf(double e) + { + if (e == 0) + { + return 0; + } + + double originalSign = e; + + e = Math.Abs(e); + + if (e <= 0.5) + { + return Math.CopySign(e * e * (1.0 / 3), originalSign); + } + + double s = (Math.Exp((e - C) * RA) + B) * Inverse12; + DebugGuard.MustBeGreaterThan(s, 0.0, nameof(s)); + + return Math.CopySign(s, originalSign); + } + + /// + /// Opto-Optical Transfer Function - as-is. + /// + /// + /// + /// + /// Input signal + /// Digital video signal + private static double Ootf(double s) => s; + + /// + /// Inverse Opto-Optical Transfer Function - as-is. + /// + /// + /// + /// + /// Digital video signal + /// Scene light + private static double InverseOotf(double s) => s; +} diff --git a/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPerceptualQuantizationTransferFunctionBase.cs b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPerceptualQuantizationTransferFunctionBase.cs new file mode 100644 index 0000000000..eea7b06560 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPerceptualQuantizationTransferFunctionBase.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions; + +/// +/// Base class for PQ transfer function. +/// +internal abstract class JxlPerceptualQuantizationTransferFunctionBase +{ + private const double M1 = 2610.0 / 16384; + private const double M2 = (2523.0 / 4096) * 128; + private const double C1 = 3424.0 / 4096; + private const double C2 = (2413.0 / 4096) * 32; + private const double C3 = (2392.0 / 4096) * 32; + + protected static double DisplayFromEncoded(float displayIntensityTarget, double e) + { + if (e == 0.0) + { + return 0.0; + } + + double originalSign = e; + + e = Math.Abs(e); + + double xp = Math.Pow(e, 1.0 / M2); + double num = Math.Max(xp - C1, 0.0); + double den = C2 - (C3 * xp); + double d = Math.Pow(num / den, 1.0 / M1); + + return Math.CopySign(d * (10000.0 / displayIntensityTarget), originalSign); + } + + protected static double EncodedFromDisplay(float displayIntensityTarget, double d) + { + if (d == 0.0) + { + return 0.0; + } + + double originalSign = d; + + d = Math.Abs(d); + + double xp = Math.Pow(d * (displayIntensityTarget * (1 / 10000)), M1); + double num = C1 + (xp * C2); + double den = 1.0 + (xp * C3); + double e = Math.Pow(num / den, M2); + + return Math.CopySign(e, originalSign); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs index dd8e7314ee..8da98bbf4f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; @@ -101,4 +102,11 @@ public static void Transpose8x8Block(Span fromSpan, Span toSpan, int s } } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector Pow(Vector @base, Vector exponent) + { + Vector vec = Vector.Log2(@base) * exponent; + return vec * vec; + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs index 0e6bf82745..f2f2f0054f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/PatchDictionaryStage.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Drawing; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; using SixLabors.ImageSharp.Memory; diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs index ad790ed10a..d442faeac6 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs @@ -1,10 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; -using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; -using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; From 7880fb290e94f14fb1734f773c748487fa2a0cf4 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 3 Sep 2026 22:07:56 +0400 Subject: [PATCH 136/142] Add SIMD float->Half conversion This is needed for WriteToOutputStage which uses demote from float to Half --- .../Jxl/Processing/Primitives/JxlHalfUtils.cs | 178 ++++++++++++++++++ .../RenderPipeline/WriteToOutputStage.cs | 5 + 2 files changed, 183 insertions(+) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlHalfUtils.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlHalfUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlHalfUtils.cs new file mode 100644 index 0000000000..49faab821a --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlHalfUtils.cs @@ -0,0 +1,178 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +internal static class JxlHalfUtils +{ + /* + * SIMD version of the following bit-twiddling implementation + * of float to Half conversion, because Vector doesn't + * have a method like this (only reinterpretation, we want + * conversion). + public static ushort ConvertSingleToHalf(float f) + { + uint x = BitConverter.SingleToUInt32Bits(f); + + uint sign = (x >> 16) & 0x8000; // sign bit + int exp = (int)((x >> 23) & 0xFF) - 127 + 15; // exponent rebias + uint mantissa = x & 0x007FFFFF; // mantissa bits + + if (exp <= 0) + { + // Subnormal or zero + if (exp < -10) + { + return (ushort)sign; + } + + mantissa = (mantissa | 0x00800000) >> (1 - exp); + return (ushort)(sign | (mantissa >> 13)); + } + else if (exp >= 31) + { + // Inf or NaN + return (ushort)(sign | 0x7C00 | (mantissa >> 13)); + } + else + { + // Normalized + return (ushort)(sign | ((uint)exp << 10) | (mantissa >> 13)); + } + } + */ + public static Vector ConvertSingleToHalf(Vector f) + { + Vector x = f.As(); + + Vector sign = (x >> 16) & Vector.Create(0x8000u); + Vector exponent = ((x >> 23) & Vector.Create(0xFFu)).As() - Vector.Create(127) + Vector.Create(15); + Vector mantissa = x & Vector.Create(0x007FFFFFu); + + Vector results = Vector.Zero; + + Vector lessThanMinus10 = Vector.LessThan(exponent, Vector.Create(-10)).As(); + Vector gte31 = Vector.GreaterThanOrEqual(exponent, Vector.Create(31)).As(); + Vector notLtMinus10OrGte31 = ~(lessThanMinus10 | gte31); + + // Subnormal or zero (exp < -10) = sign + results = Vector.ConditionalSelect(lessThanMinus10, sign, results); + + // exp <= 0 = (ushort)(sign | (((mantissa | 0x00800000) >> (1 - exp)) >> 13)); + results = Vector.ConditionalSelect( + Vector.LessThanOrEqual(exponent, Vector.Zero).As(), + sign | (ShiftRightAll(mantissa.As() | Vector.Create(0x00800000), Vector.One - exponent) >> 13).As(), + results); + + // exp >= 31 = (ushort)(sign | 0x7C00 | (mantissa >> 13)) + results = Vector.ConditionalSelect( + gte31, + sign | Vector.Create(0x7C00u) | (mantissa >> 13), + results); + + // anything else - normalized + results = Vector.ConditionalSelect( + notLtMinus10OrGte31, + sign | (exponent.As() << 10) | (mantissa >> 13), + results); + + return VectorUInt32ToUInt16(results); + } + + private static Vector VectorUInt32ToUInt16(Vector uint32) + { + Vector clipped = uint32 & Vector.Create(0xFFFFu); + return clipped.As(); + } + + /// + /// Vectors don't support shifting right using shift value as vector. + /// This is a hack to do this. + /// + /// Kind of vector + /// Input vector + /// Vectors with shift value for each corresponding item. + /// Shifted vectors + private static Vector ShiftRightAll(Vector vec, Vector shiftVec) + where T : unmanaged, IShiftOperators + { + Span firstVec = stackalloc T[Vector.Count]; + Span secondVec = stackalloc T[Vector.Count]; + + vec.CopyTo(firstVec); + shiftVec.CopyTo(secondVec); + + ref T firstRef = ref MemoryMarshal.GetReference(firstVec); + ref T secondRef = ref MemoryMarshal.GetReference(secondVec); + + int count = Vector.Count; + + if (count == 1) + { + return Vector.Create(firstRef >>> secondRef); + } + else if (count == 2) + { + firstRef >>>= secondRef; + Unsafe.Add(ref firstRef, 1) >>>= Unsafe.Add(ref secondRef, 1); + return Vector.Create(firstVec); + } + else if (count == 4) + { + firstRef >>>= secondRef; + Unsafe.Add(ref firstRef, 1) >>>= Unsafe.Add(ref secondRef, 1); + Unsafe.Add(ref firstRef, 2) >>>= Unsafe.Add(ref secondRef, 2); + Unsafe.Add(ref firstRef, 3) >>>= Unsafe.Add(ref secondRef, 3); + return Vector.Create(firstVec); + } + else if (count == 8) + { + firstRef >>>= secondRef; + Unsafe.Add(ref firstRef, 1) >>>= Unsafe.Add(ref secondRef, 1); + Unsafe.Add(ref firstRef, 2) >>>= Unsafe.Add(ref secondRef, 2); + Unsafe.Add(ref firstRef, 3) >>>= Unsafe.Add(ref secondRef, 3); + Unsafe.Add(ref firstRef, 4) >>>= Unsafe.Add(ref secondRef, 4); + Unsafe.Add(ref firstRef, 5) >>>= Unsafe.Add(ref secondRef, 5); + Unsafe.Add(ref firstRef, 6) >>>= Unsafe.Add(ref secondRef, 6); + Unsafe.Add(ref firstRef, 7) >>>= Unsafe.Add(ref secondRef, 7); + return Vector.Create(firstVec); + } + else if (count == 16) + { + firstRef >>>= secondRef; + Unsafe.Add(ref firstRef, 1) >>>= Unsafe.Add(ref secondRef, 1); + Unsafe.Add(ref firstRef, 2) >>>= Unsafe.Add(ref secondRef, 2); + Unsafe.Add(ref firstRef, 3) >>>= Unsafe.Add(ref secondRef, 3); + Unsafe.Add(ref firstRef, 4) >>>= Unsafe.Add(ref secondRef, 4); + Unsafe.Add(ref firstRef, 5) >>>= Unsafe.Add(ref secondRef, 5); + Unsafe.Add(ref firstRef, 6) >>>= Unsafe.Add(ref secondRef, 6); + Unsafe.Add(ref firstRef, 7) >>>= Unsafe.Add(ref secondRef, 7); + Unsafe.Add(ref firstRef, 8) >>>= Unsafe.Add(ref secondRef, 8); + Unsafe.Add(ref firstRef, 9) >>>= Unsafe.Add(ref secondRef, 9); + Unsafe.Add(ref firstRef, 10) >>>= Unsafe.Add(ref secondRef, 10); + Unsafe.Add(ref firstRef, 11) >>>= Unsafe.Add(ref secondRef, 11); + Unsafe.Add(ref firstRef, 12) >>>= Unsafe.Add(ref secondRef, 12); + Unsafe.Add(ref firstRef, 13) >>>= Unsafe.Add(ref secondRef, 13); + Unsafe.Add(ref firstRef, 14) >>>= Unsafe.Add(ref secondRef, 14); + Unsafe.Add(ref firstRef, 15) >>>= Unsafe.Add(ref secondRef, 15); + return Vector.Create(firstVec); + } + else + { + // Vector too large, use a (slightly) slower loop + // instead of duplicating too much. + firstRef >>>= secondRef; + + for (int i = 1; i < count; i++) + { + Unsafe.Add(ref firstRef, i) >>>= Unsafe.Add(ref secondRef, i); + } + + return Vector.Create(firstVec); + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs index d442faeac6..2d1b989c9a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/RenderPipeline/WriteToOutputStage.cs @@ -1,7 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.RenderPipeline; From 5fe70bac5b609eefd693f9cf1829005304152349 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Thu, 3 Sep 2026 22:32:28 +0400 Subject: [PATCH 137/142] Add photon noise encoder --- .../Encoder/Noise/JxlPhotonNoise.cs | 60 +++++++++++++++++++ .../Jxl/Processing/Noise/JxlNoiseConstants.cs | 13 ++++ .../Processing/Noise/JxlNoiseParameters.cs | 2 +- 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlPhotonNoise.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseConstants.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlPhotonNoise.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlPhotonNoise.cs new file mode 100644 index 0000000000..ce48a39e32 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Noise/JxlPhotonNoise.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Cms; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Noise; + +internal static class JxlPhotonNoise +{ + /// + /// Assumes a daylight-like spectrum. + /// + private const float PhotonsPerLxSPerUm2 = 11260; + + /// + /// Order of magnitude for cameras in the 2010-2020 decade, + /// taking the CFA into account. + /// + private const float EffectiveQuantumEfficiency = 0.20f; + + private const float PhotoResponseNonUniformity = 0.005f; + + private const float InputReferredReadNoise = 3; + + private const float SensorAreaUm2 = 36000f * 24000; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float Square(float x) => x * x; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float Cube(float x) => x * x * x; + + public static JxlNoiseParameters SimulatePhotonNoise(int xSize, int ySize, float iso) + { + float opsinAbsorbanceBiasCbrt = MathF.Cbrt(JxlOpsinConstants.OpsinAbsorbanceBias1); + float h18 = 10f / iso; + float pixelAreaUm2 = SensorAreaUm2 / (xSize * ySize); + float electronsPerPixel18 = EffectiveQuantumEfficiency * PhotonsPerLxSPerUm2 * h18 * pixelAreaUm2; + JxlNoiseParameters parameters = new(); + Span lookup = parameters.Lookup; // Faster than float[] + + for (int i = 0; i < JxlNoiseParameters.NoisePoints; ++i) + { + float scaledIndex = i / (JxlNoiseParameters.NoisePoints - 2f); + float y = 2 * scaledIndex; + float linear = MathF.Max(0f, Cube(y - opsinAbsorbanceBiasCbrt) + JxlOpsinConstants.OpsinAbsorbanceBias1); + float electronsPerPixel = electronsPerPixel18 * (linear / 0.18f); + float noise = MathF.Sqrt(Square(InputReferredReadNoise) + electronsPerPixel + Square(PhotoResponseNonUniformity * electronsPerPixel)); + float linearNoise = noise * (0.18f / electronsPerPixel18); + float opsinDerivative = (1f / 3) / Square(MathF.Sqrt(linear - JxlOpsinConstants.OpsinAbsorbanceBias1)); + float opsinNoise = linearNoise * opsinDerivative; + + lookup[i] = Math.Clamp(opsinNoise / (0.22f * MathF.Sqrt(2f) * 1.13f), 0f, JxlNoiseConstants.NoiseLutMax); + } + + return parameters; + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseConstants.cs b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseConstants.cs new file mode 100644 index 0000000000..87027aad94 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseConstants.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Noise; + +/// +/// Constants used by noise processing. +/// +internal static class JxlNoiseConstants +{ + public const float Precision = 1024f; + public const float NoiseLutMax = 1023.4999f / Precision; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseParameters.cs b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseParameters.cs index de8072c3ee..9fef114a25 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseParameters.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Noise/JxlNoiseParameters.cs @@ -11,5 +11,5 @@ internal sealed class JxlNoiseParameters public bool ContainsAny => this.Lookup.Any(x => MathF.Abs(x) > 1e-3f); - public void Clear() => Array.Fill(this.Lookup, 0f); + public void Clear() => this.Lookup.AsSpan().Clear(); } From bedf8eccac278da36e1defee3e6d794d3a0fe4e1 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 4 Sep 2026 21:57:14 +0400 Subject: [PATCH 138/142] Add tests, complete splines, SIMD-accelerate transpose, add a few SIMD methods, add SIMD-based quantization, XorShift constructor, heuristics (incomplete), spline encoder, v256/v128 support to DCT output/source, JxlBlending (blending.cc, blending.h) --- src/ImageSharp/Common/Helpers/SimdUtils.cs | 39 +- .../Common/Helpers/Vector128Utilities.cs | 40 ++ .../Common/Helpers/Vector256Utilities.cs | 40 ++ .../Formats/Jxl/Memory/JxlImage3{T}.cs | 33 ++ .../Processing/AcStrategy/JxlAcStrategyRow.cs | 10 +- .../Blending/JxlAlphaBlendingInputLayer.cs | 10 +- .../Blending/JxlAlphaBlendingOutput.cs | 10 +- .../Jxl/Processing/Blending/JxlBlending.cs | 368 ++++++++++++++++ .../Jxl/Processing/Dct/JxlDctOutput.cs | 19 + .../Jxl/Processing/Dct/JxlDctSource.cs | 21 + .../Jxl/Processing/Encoder/Ans/JxlToken.cs | 13 + .../Jxl/Processing/Encoder/JxlHeuristics.cs | 170 ++++++++ .../Processing/Encoder/JxlSplineEncoder.cs | 83 ++++ .../Jxl/Processing/JxlCoefficientOrder.cs | 2 +- ...JxlSimdUtils.StoreInterleaved.Generated.cs | 165 ++++---- .../JxlSimdUtils.StoreInterleaved.tt | 5 +- .../Formats/Jxl/Processing/JxlSimdUtils.cs | 332 ++++++++++++++- .../Formats/Jxl/Processing/JxlTranspose.cs | 113 ++++- .../Processing/Primitives/JxlLehmerCode.cs | 14 +- .../Jxl/Processing/Primitives/JxlXorShift.cs | 2 +- .../Quantization/JxlQuantizerSimd.cs | 28 ++ .../Formats/Jxl/Processing/Splines/Dct32.cs | 15 + .../Processing/Splines/JxlQuantizedSpline.cs | 4 +- .../Jxl/Processing/Splines/JxlSpline.cs | 4 +- .../Jxl/Processing/Splines/JxlSplineUtils.cs | 394 +++++++++++++++++ .../Jxl/Processing/Splines/JxlSplines.cs | 289 +++++++++++++ .../Formats/Jxl/Processing/AnsCommonTests.cs | 48 +++ .../Encoder/GammaCorrectionTests.cs | 37 ++ .../Encoder/Noise/PhotonNoiseTests.cs | 43 ++ .../Processing/Primitives/LehmerCodeTests.cs | 88 ++++ .../Processing/Primitives/XorShiftTests.cs | 396 ++++++++++++++++++ tests/ImageSharp.Tests/Formats/Jxl/README.md | 5 + tests/ImageSharp.Tests/Formats/Jxl/Rng.cs | 87 ++++ 33 files changed, 2816 insertions(+), 111 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlToken.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlHeuristics.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlSplineEncoder.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerSimd.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Splines/Dct32.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineUtils.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/GammaCorrectionTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/Noise/PhotonNoiseTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/Primitives/LehmerCodeTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/Primitives/XorShiftTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/README.md create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Rng.cs diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.cs b/src/ImageSharp/Common/Helpers/SimdUtils.cs index 2b8f58b086..3e0abcd6ef 100644 --- a/src/ImageSharp/Common/Helpers/SimdUtils.cs +++ b/src/ImageSharp/Common/Helpers/SimdUtils.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Diagnostics; @@ -71,6 +71,43 @@ internal static Vector FastRound(this Vector v) return val_2p23_f32 | sign; } + /// + /// Estimates the reciprocal of this vector. + /// + /// The vector to get reciprocal estimate of. + /// An estimated reciprocal of each element in the vector. + internal static Vector ReciprocalEstimate(this Vector v) + { + // TODO: System.Runtime.Intrinsics.Arm has Sve and Sve2 + // support but is for evaluation purposes only; add SVE/SVE2 + // support when possible + if (Avx512F.IsSupported && Vector.Count == 16) + { + // x86 + return Avx512F.Reciprocal14(v.AsVector512()).AsVector(); + } + else if (Avx.IsSupported && Vector.Count == 8) + { + // x86 + return Avx.Reciprocal(v.AsVector256()).AsVector(); + } + else if (AdvSimd.IsSupported && Vector.Count == 4) + { + // ARM + return AdvSimd.ReciprocalEstimate(v.AsVector128()).AsVector(); + } + else if (Sse.IsSupported && Vector.Count == 4) + { + // x86 + return Sse.Reciprocal(v.AsVector128()).AsVector(); + } + else + { + // Exact reciprocal fallback (slower) + return Vector.One / v; + } + } + [Conditional("DEBUG")] private static void DebugVerifySpanInput(ReadOnlySpan source, ReadOnlySpan dest, int shouldBeDivisibleBy) { diff --git a/src/ImageSharp/Common/Helpers/Vector128Utilities.cs b/src/ImageSharp/Common/Helpers/Vector128Utilities.cs index 6b4c6ad63c..0e9a40dd1b 100644 --- a/src/ImageSharp/Common/Helpers/Vector128Utilities.cs +++ b/src/ImageSharp/Common/Helpers/Vector128Utilities.cs @@ -899,4 +899,44 @@ public static Vector128 InterleaveUpper(Vector128 a, Vector128 b) return (shuffledA & maskA) | (shuffledB & maskB); } + + /// + /// Interleaves the lower half of the vector. + /// + /// First vector + /// Second vector + /// + /// { a[0], b[0], a[1], b[1] } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 InterleaveLower(Vector128 a, Vector128 b) + { + Vector128 shuffledA = Vector128.Shuffle(a, Vector128.Create(0, 0, 1, 1)); + Vector128 shuffledB = Vector128.Shuffle(b, Vector128.Create(0, 0, 1, 1)); + + Vector128 maskA = Vector128.Create(-1, 0, -1, 0).AsSingle(); + Vector128 maskB = Vector128.Create(0, -1, 0, -1).AsSingle(); + + return (shuffledA & maskA) | (shuffledB & maskB); + } + + /// + /// Interleaves the upper half of the vector. + /// + /// First vector + /// Second vector + /// + /// { a[2], b[2], a[3], b[3] } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 InterleaveUpper(Vector128 a, Vector128 b) + { + Vector128 shuffledA = Vector128.Shuffle(a, Vector128.Create(2, 2, 3, 3)); + Vector128 shuffledB = Vector128.Shuffle(b, Vector128.Create(2, 2, 3, 3)); + + Vector128 maskA = Vector128.Create(-1, 0, -1, 0).AsSingle(); + Vector128 maskB = Vector128.Create(0, -1, 0, -1).AsSingle(); + + return (shuffledA & maskA) | (shuffledB & maskB); + } } diff --git a/src/ImageSharp/Common/Helpers/Vector256Utilities.cs b/src/ImageSharp/Common/Helpers/Vector256Utilities.cs index 4bd78b88fd..bb11108869 100644 --- a/src/ImageSharp/Common/Helpers/Vector256Utilities.cs +++ b/src/ImageSharp/Common/Helpers/Vector256Utilities.cs @@ -563,4 +563,44 @@ public static Vector256 InterleaveUpper(Vector256 a, Vector256 b) return (shuffledA & maskA) | (shuffledB & maskB); } + + /// + /// Interleaves the lower half of the vector. + /// + /// First vector + /// Second vector + /// + /// { a[0], b[0], a[1], b[1], a[2], b[2], a[3], b[3] } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 InterleaveLower(Vector256 a, Vector256 b) + { + Vector256 shuffledA = Vector256.Shuffle(a, Vector256.Create(0, 0, 1, 1, 2, 2, 3, 3)); + Vector256 shuffledB = Vector256.Shuffle(b, Vector256.Create(0, 0, 1, 1, 2, 2, 3, 3)); + + Vector256 maskA = Vector256.Create(-1, 0, -1, 0, -1, 0, -1, 0).AsSingle(); + Vector256 maskB = Vector256.Create(0, -1, 0, -1, 0, -1, 0, -1).AsSingle(); + + return (shuffledA & maskA) | (shuffledB & maskB); + } + + /// + /// Interleaves the upper half of the vector. + /// + /// First vector + /// Second vector + /// + /// { a[4], b[4], a[5], b[5], a[6], b[6], a[7], b[7] } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 InterleaveUpper(Vector256 a, Vector256 b) + { + Vector256 shuffledA = Vector256.Shuffle(a, Vector256.Create(4, 4, 5, 5, 6, 6, 7, 7)); + Vector256 shuffledB = Vector256.Shuffle(b, Vector256.Create(4, 4, 5, 5, 6, 6, 7, 7)); + + Vector256 maskA = Vector256.Create(-1, 0, -1, 0, -1, 0, -1, 0).AsSingle(); + Vector256 maskB = Vector256.Create(0, -1, 0, -1, 0, -1, 0, -1).AsSingle(); + + return (shuffledA & maskA) | (shuffledB & maskB); + } } diff --git a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs index 4442eb6495..882c1098bb 100644 --- a/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs +++ b/src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Jxl.Memory; @@ -9,6 +11,21 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Memory; internal class JxlImage3 : IDisposable where T : unmanaged { + private sealed class TypeChangingMemoryManager(Memory memory) : MemoryManager + where TTarget : unmanaged + { + public override Span GetSpan() => MemoryMarshal.Cast(memory.Span); + + // we don't use these + public override MemoryHandle Pin(int elementIndex = 0) => throw new NotImplementedException(); + + public override void Unpin() => throw new NotImplementedException(); + + protected override void Dispose(bool disposing) + { + } + } + private const int PlaneCount = 3; private JxlPlane[] planes = new JxlPlane[3]; @@ -46,6 +63,22 @@ public Span PlaneRow(int plane, int row) return rowSpan; } + // This method performs minor allocations! + public Memory PlaneRowMemory(int plane, int row) + { + this.PlaneRowBoundsCheck(plane, row); + + int rowOffset = row * this.planes[0].BytesPerRow; + Memory rowMemoryBytes = this.planes[plane].Bytes[rowOffset..]; + + // we have to allocate a utility class so we can reinterpret + // a Memory. + // Unsafe.As is truly unsafe because, f.e. what if there are + // 400 bytes but T is 4 bytes? the length will remain as 400. + TypeChangingMemoryManager reinterpreter = new(rowMemoryBytes); + return reinterpreter.Memory; + } + public Span PlaneRow(Rectangle rectangle, int c, int y) { DebugGuard.MustBeGreaterThanOrEqualTo(y + rectangle.Top, 0, nameof(y)); diff --git a/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyRow.cs b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyRow.cs index 7a9b1eeafb..60900dc578 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyRow.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/AcStrategy/JxlAcStrategyRow.cs @@ -6,17 +6,13 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; -internal sealed class JxlAcStrategyRow +internal readonly struct JxlAcStrategyRow(ReadOnlyMemory row) { - private readonly ReadOnlyMemory row; - - public JxlAcStrategyRow(ReadOnlyMemory row) => this.row = row; - - public JxlAcStrategy this[int x] + public readonly JxlAcStrategy this[int x] { get { - ReadOnlySpan span = this.row.Span; + ReadOnlySpan span = row.Span; DebugGuard.MustBeLessThan(x * 8, span.Length, "x overflows"); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingInputLayer.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingInputLayer.cs index 9a73f659fb..b749a5ba26 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingInputLayer.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingInputLayer.cs @@ -3,13 +3,13 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; -internal sealed class JxlAlphaBlendingInputLayer +internal ref struct JxlAlphaBlendingInputLayer(ReadOnlySpan singleSpan) { - public ReadOnlyMemory R { get; set; } + public ReadOnlySpan R = singleSpan; - public ReadOnlyMemory G { get; set; } + public ReadOnlySpan G = singleSpan; - public ReadOnlyMemory B { get; set; } + public ReadOnlySpan B = singleSpan; - public ReadOnlyMemory A { get; set; } + public ReadOnlySpan A = singleSpan; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingOutput.cs index 62296b20e7..76a10d2158 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingOutput.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaBlendingOutput.cs @@ -3,13 +3,13 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; -internal sealed class JxlAlphaBlendingOutput +internal ref struct JxlAlphaBlendingOutput(Span singleSpan) { - public Memory R { get; set; } + public Span R = singleSpan; - public Memory G { get; set; } + public Span G = singleSpan; - public Memory B { get; set; } + public Span B = singleSpan; - public Memory A { get; set; } + public Span A = singleSpan; } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs new file mode 100644 index 0000000000..7ddad4714e --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlBlending.cs @@ -0,0 +1,368 @@ +// 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.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; + +internal static class JxlBlending +{ + public static bool NeedsBlending(JxlFrameHeader header) + { + if (header.FrameType is not JxlFrameType.RegularFrame and not JxlFrameType.SkipProgressive) + { + return false; + } + + JxlBlendingInfo? blendingInfo = header.BlendingInfo; + if (blendingInfo is null) + { + return false; + } + + bool replaceAll = blendingInfo.BlendMode == JxlBlendMode.Replace; + + foreach (JxlBlendingInfo info in header.ExtraChannelBlendingInfo) + { + if (info.BlendMode != JxlBlendMode.Replace) + { + replaceAll = false; + } + } + + if (!header.CustomSizeOrOrigin && replaceAll) + { + return false; + } + + return true; + } + + public static void PerformBlending( + Configuration configuration, + Buffer2D bg, + Buffer2D fg, + Buffer2D output, + int x0, + int xsize, + JxlPatchBlending colorBlending, + Span ecBlending, + List extraChannelInfo) + { + bool hasAlpha = extraChannelInfo.Any(x => x.Type == JxlExtraChannel.Alpha); + + int numEc = extraChannelInfo.Count; + using JxlImageF tmp = new(configuration, xsize, 3 + numEc); + + for (int i = 0; i < numEc; i++) + { + int i3 = 3 + i; + + switch (ecBlending[i].Mode) + { + case JxlPatchBlendMode.Add: + { + Span row = tmp.GetRow(i3); + for (int x = 0; x < xsize; x++) + { + row[x] = bg[i3, x + x0] + fg[i3, x + x0]; + } + + continue; + } + + case JxlPatchBlendMode.BlendAbove: + { + int alpha = ecBlending[i].AlphaChannel; + bool isPremultiplied = extraChannelInfo[alpha].AlphaAssociated; + + Span bgSpan3 = bg.DangerousGetRowSpan(i3)[x0..]; + Span bgSpan3Alpha = bg.DangerousGetRowSpan(3 + alpha)[x0..]; + Span fgSpan3 = fg.DangerousGetRowSpan(i3)[x0..]; + Span fgSpan3Alpha = fg.DangerousGetRowSpan(3 + alpha)[x0..]; + + JxlAlphaHelper.PerformAlphaBlending( + bgSpan3, + bgSpan3Alpha, + fgSpan3, + fgSpan3Alpha, + tmp.GetRow(i3), + xsize, + isPremultiplied, + ecBlending[i].Clamp); + + continue; + } + + case JxlPatchBlendMode.BlendBelow: + { + int alpha = ecBlending[i].AlphaChannel; + bool isPremultiplied = extraChannelInfo[alpha].AlphaAssociated; + + Span bgSpan3 = bg.DangerousGetRowSpan(i3)[x0..]; + Span bgSpan3Alpha = bg.DangerousGetRowSpan(3 + alpha)[x0..]; + Span fgSpan3 = fg.DangerousGetRowSpan(i3)[x0..]; + Span fgSpan3Alpha = fg.DangerousGetRowSpan(3 + alpha)[x0..]; + + JxlAlphaHelper.PerformAlphaBlending( + bgSpan3, + bgSpan3Alpha, + fgSpan3, + fgSpan3Alpha, + tmp.GetRow(3 + i), + xsize, + isPremultiplied, + ecBlending[i].Clamp); + + continue; + } + + case JxlPatchBlendMode.AlphaWeightedAddAbove: + { + int alpha = ecBlending[i].AlphaChannel; + + Span bgSpan3 = bg.DangerousGetRowSpan(i3)[x0..]; + Span bgSpan3Alpha = bg.DangerousGetRowSpan(3 + alpha)[x0..]; + Span fgSpan3 = fg.DangerousGetRowSpan(i3)[x0..]; + + JxlAlphaHelper.PerformAlphaWeightedAdd( + bgSpan3, + fgSpan3, + bgSpan3Alpha, + tmp.GetRow(3 + i), + xsize, + ecBlending[i].Clamp); + + continue; + } + + case JxlPatchBlendMode.AlphaWeightedAddBelow: + { + int alpha = ecBlending[i].AlphaChannel; + + Span bgSpan3 = bg.DangerousGetRowSpan(i3)[x0..]; + Span bgSpan3Alpha = bg.DangerousGetRowSpan(3 + alpha)[x0..]; + Span fgSpan3 = fg.DangerousGetRowSpan(i3)[x0..]; + + JxlAlphaHelper.PerformAlphaWeightedAdd( + fgSpan3, + bgSpan3, + bgSpan3Alpha, + tmp.GetRow(3 + i), + xsize, + ecBlending[i].Clamp); + + continue; + } + + case JxlPatchBlendMode.Multiply: + { + Span bgSpan3 = bg.DangerousGetRowSpan(i3)[x0..]; + Span fgSpan3 = fg.DangerousGetRowSpan(i3)[x0..]; + + JxlAlphaHelper.PerformMultiplyBlending( + bgSpan3, + fgSpan3, + tmp.GetRow(i3), + xsize, + ecBlending[i].Clamp); + + continue; + } + + case JxlPatchBlendMode.Replace: + if (xsize > 0) + { + Span fgSpan3 = fg.DangerousGetRowSpan(i3)[x0..]; + fgSpan3.Slice(0, xsize).CopyTo(tmp.GetRow(i3)); + } + + continue; + + case JxlPatchBlendMode.None: + if (xsize > 0) + { + Span bgSpan3 = bg.DangerousGetRowSpan(i3)[x0..]; + bgSpan3.Slice(0, xsize).CopyTo(tmp.GetRow(i3)); + } + + continue; + } + } + + int colorBlendingAlpha = colorBlending.AlphaChannel; + + void Add() + { + for (int p = 0; p < 3; p++) + { + Span output = tmp.GetRow(p); + Span bgSpan = bg.DangerousGetRowSpan(p); + Span fgSpan = fg.DangerousGetRowSpan(p); + + for (int x = 0; x < xsize; x++) + { + int xPlusX0 = x + x0; + + output[x] = bgSpan[xPlusX0] + fgSpan[xPlusX0]; + } + } + } + + void BlendWeighted(Span bottom, Span top) + { + bool isPremultiplied = extraChannelInfo[colorBlendingAlpha].AlphaAssociated; + + JxlAlphaHelper.PerformAlphaBlending( + new JxlAlphaBlendingInputLayer() + { + R = bottom[x0..], + G = bottom[(x0 + 1)..], + B = bottom[(2 + x0)..], + A = bottom[(3 + colorBlendingAlpha + x0)..] + }, + new JxlAlphaBlendingInputLayer() + { + R = top[x0..], + G = top[(x0 + 1)..], + B = top[(x0 + 2)..], + A = top[(3 + colorBlendingAlpha + x0)..] + }, + new JxlAlphaBlendingOutput() + { + R = tmp.GetRow(0), + G = tmp.GetRow(1), + B = tmp.GetRow(2), + A = tmp.GetRow(3) + }, + xsize, + isPremultiplied, + colorBlending.Clamp); + } + + void AddWeighted(Span bottom, Span top) + { + for (int c = 0; c < 3; c++) + { + JxlAlphaHelper.PerformAlphaWeightedAdd(bottom[(c + x0)..], top[(c + x0)..], top[(3 + colorBlendingAlpha + x0)..], tmp.GetRow(c), xsize, colorBlending.Clamp); + } + } + + void Copy(Span src) + { + for (int p = 0; p < 3; p++) + { + src.Slice(p + x0, xsize).CopyTo(tmp.GetRow(p)); + } + } + + switch (colorBlending.Mode) + { + case JxlPatchBlendMode.Add: + { + Add(); + break; + } + + case JxlPatchBlendMode.AlphaWeightedAddAbove: + { + if (hasAlpha) + { + AddWeighted(bg.DangerousGetSingleSpan(), fg.DangerousGetSingleSpan()); + } + else + { + Add(); + } + + break; + } + + case JxlPatchBlendMode.AlphaWeightedAddBelow: + { + if (hasAlpha) + { + AddWeighted(fg.DangerousGetSingleSpan(), bg.DangerousGetSingleSpan()); + } + else + { + Add(); + } + + break; + } + + case JxlPatchBlendMode.BlendAbove: + { + if (hasAlpha) + { + BlendWeighted(bg.DangerousGetSingleSpan(), fg.DangerousGetSingleSpan()); + } + else + { + Copy(fg.DangerousGetSingleSpan()); + } + + break; + } + + case JxlPatchBlendMode.BlendBelow: + { + if (hasAlpha) + { + BlendWeighted(fg.DangerousGetSingleSpan(), bg.DangerousGetSingleSpan()); + } + else + { + Copy(fg.DangerousGetSingleSpan()); + } + + break; + } + + case JxlPatchBlendMode.Multiply: + { + Span bgSpan = bg.DangerousGetSingleSpan(); + Span fgSpan = fg.DangerousGetSingleSpan(); + + for (int p = 0; p < 3; p++) + { + JxlAlphaHelper.PerformMultiplyBlending( + bgSpan[(p + x0)..], + fgSpan[(p + x0)..], + tmp.GetRow(p), + xsize, + colorBlending.Clamp); + } + + break; + } + + case JxlPatchBlendMode.Replace: + { + Copy(fg.DangerousGetSingleSpan()); + break; + } + + case JxlPatchBlendMode.None: + { + Copy(bg.DangerousGetSingleSpan()); + break; + } + } + + if (xsize != 0) + { + Span outputSpan = output.DangerousGetSingleSpan(); + + for (int i = 0; i < 3; i++) + { + tmp.GetRow(i).Slice(0, xsize).CopyTo(outputSpan[(i + x0)..]); + } + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctOutput.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctOutput.cs index 5ef6218908..5a1d5c9581 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctOutput.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctOutput.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; @@ -49,4 +50,22 @@ internal ref struct JxlDctOutput(Span data, int stride) /// The offset. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly void StorePart(Vector value, int row, int index) => value.CopyTo(this.Address(row, index)); + + /// + /// Stores the vector into the data at the specified row and offset. + /// + /// The vector to write. + /// The row index. + /// The offset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void StorePart256(Vector256 value, int row, int index) => value.CopyTo(this.Address(row, index)); + + /// + /// Stores the vector into the data at the specified row and offset. + /// + /// The vector to write. + /// The row index. + /// The offset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void StorePart128(Vector128 value, int row, int index) => value.CopyTo(this.Address(row, index)); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctSource.cs b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctSource.cs index 0fe17e92ad..ba99dd5f67 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctSource.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Dct/JxlDctSource.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; @@ -52,4 +53,24 @@ internal readonly ref struct JxlDctSource(Span data, int stride) /// Vector at that row and offset. /// public Vector LoadPart(int row, int i) => new(this.Address(row, i)); + + /// + /// Loads a vector at the specified row and offset. + /// + /// The row index. + /// The offset. + /// + /// Vector at that row and offset. + /// + public Vector256 LoadPart256(int row, int i) => Vector256.Create(this.Address(row, i)); + + /// + /// Loads a vector at the specified row and offset. + /// + /// The row index. + /// The offset. + /// + /// Vector at that row and offset. + /// + public Vector128 LoadPart128(int row, int i) => Vector128.Create(this.Address(row, i)); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlToken.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlToken.cs new file mode 100644 index 0000000000..2b07fb14d9 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/Ans/JxlToken.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; + +internal struct JxlToken(JxlSplineEntropyContext c, uint value) +{ + public bool IsLz77Length; + public JxlSplineEntropyContext Context = c; + public uint Value = value; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlHeuristics.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlHeuristics.cs new file mode 100644 index 0000000000..846f791f69 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlHeuristics.cs @@ -0,0 +1,170 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.AcStrategy; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +internal static class JxlHeuristics +{ + private static ReadOnlySpan SimpleContextMap => + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + ]; + + public static void FindBestBlockEntropyModel(JxlCompressParameters cparameters, JxlImageI rqf, JxlAcStrategyImage acStrategy, JxlBlockContextMap blockCtxMap) + { + if (cparameters.DecodingSpeedTier >= 1) + { + SimpleContextMap.CopyTo(blockCtxMap.ContextMap.AsSpan()); + blockCtxMap.ContextCount = 2; + blockCtxMap.DcContextCount = 1; + return; + } + + if (cparameters.SpeedTier >= JxlSpeedTier.Falcon) + { + return; + } + + int total = rqf.XSize * rqf.YSize; + int sizeForContextModel = (1 << 10) * cparameters.ButteraugliDistance; + + if (total < sizeForContextModel) + { + return; + } + + OccCounters counters = new(rqf, acStrategy); + int sizeForQfSplit = (1 << 13) * cparameters.ButteraugliDistance; + int numQfSegments = total < sizeForQfSplit ? 1 : 2; + List qft = blockCtxMap.QfThresholds; + qft.Clear(); + int cumulativeSum = 0; + int next = 1; + int lastCut = 256; + int cut = total * next / numQfSegments; + + for (int j = 0; j < 256; j++) + { + cumulativeSum += counters.QfCounts[j]; + + if (cumulativeSum > cut) + { + if (j != 0) + { + qft.Add((uint)j); + } + + lastCut = j; + + while (cumulativeSum > cut) + { + next++; + cut = total * next / numQfSegments; + } + } + else if (next > qft.Count + 1) + { + if (j - 1 == lastCut && j != 0) + { + qft.Add((uint)j); + } + } + } + + int[]? pooledCounts = null; + int[]? pooledRemap = null; + int[]? pooledClusters = null; + int countsLength = JxlForwardCoefficientOrder.OrderCount * (qft.Count + 1); + + Span counts = + countsLength <= 128 + ? stackalloc int[128].Slice(0, countsLength) + : pooledCounts = ArrayPool.Shared.Rent(countsLength); + + Span remap = + countsLength <= 128 + ? stackalloc int[128].Slice(0, countsLength) + : pooledRemap = ArrayPool.Shared.Rent(countsLength); + + Span clusters = + countsLength <= 128 + ? stackalloc int[128].Slice(0, countsLength) + : pooledClusters = ArrayPool.Shared.Rent(countsLength); + + int qftPos = 0; + + for (int j = 0; j < 256; j++) + { + if (qftPos < qft.Count && j == qft[qftPos]) + { + qftPos++; + } + + for (int i = 0; i < JxlForwardCoefficientOrder.OrderCount; i++) + { + counts[qftPos + (i * (qft.Count + 1))] += counters.QfOrdCounts[i, j]; + } + } + + JxlSimdUtils.Iota(remap, 0); + remap.CopyTo(clusters); + + int numClusters = Math.Clamp(total / sizeForContextModel / 2, 2, 9); + int numClustersChroma = Math.Clamp(total / sizeForContextModel / 3, 1, 5); + + // TODO: method incomplete + // do not forget to ArrayPool.Shared.Return pooledCounts, pooledRemap, pooledClusters if needed + } + + private sealed class OccCounters : IDisposable + { + private readonly int[] qfCounts; + private readonly int[] dataForQfOrdCounts; + private readonly int[] ordCounts; + + public OccCounters(JxlImageI rqf, JxlAcStrategyImage acStrategy) + { + this.qfCounts = ArrayPool.Shared.Rent(256); + this.dataForQfOrdCounts = ArrayPool.Shared.Rent(256 * JxlForwardCoefficientOrder.OrderCount); + this.ordCounts = ArrayPool.Shared.Rent(JxlForwardCoefficientOrder.OrderCount); + + this.QfOrdCounts = new(JxlForwardCoefficientOrder.OrderCount, 256, this.dataForQfOrdCounts); + + for (int y = 0; y < rqf.YSize; y++) + { + Span qfRow = rqf.GetRow(y); + JxlAcStrategyRow acsRow = acStrategy.GetRow(y); + + for (int x = 0; x < rqf.XSize; x++) + { + int ord = JxlCoefficientOrder.StrategyOrder[acsRow[x].RawStrategy]; + int qf = qfRow[x] - 1; + this.qfCounts[qf]++; + this.QfOrdCounts[ord, qf]++; + this.ordCounts[ord]++; + } + } + } + + public Span QfCounts => this.qfCounts.AsSpan(); + + public DenseMatrix QfOrdCounts { get; } + + public Span OrdCounts => this.ordCounts.AsSpan(); + + public void Dispose() + { + ArrayPool.Shared.Return(this.qfCounts); + ArrayPool.Shared.Return(this.dataForQfOrdCounts); + ArrayPool.Shared.Return(this.ordCounts); + } + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlSplineEncoder.cs b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlSplineEncoder.cs new file mode 100644 index 0000000000..949b43870e --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Encoder/JxlSplineEncoder.cs @@ -0,0 +1,83 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Ans; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +internal sealed class JxlSplineEncoder +{ + private static void Tokenize(JxlQuantizedSpline spline, List tokens) + { + tokens.Add(new(JxlSplineEntropyContext.NumControlPoints, (uint)spline.ControlPoints.Length)); + + foreach (JxlControlPoint point in spline.ControlPoints.Span) + { + tokens.Add(new(JxlSplineEntropyContext.ControlPoints, JxlPackSigned.PackUnsigned(point.First))); + tokens.Add(new(JxlSplineEntropyContext.ControlPoints, JxlPackSigned.PackUnsigned(point.Second))); + } + + void EncodeDCT(Span dct) + { + for (int i = 0; i < 32; i++) + { + tokens.Add(new(JxlSplineEntropyContext.Dct, JxlPackSigned.PackUnsigned(dct[i]))); + } + } + + foreach (Span dct in spline.ColorDct) + { + EncodeDCT(dct); + } + + EncodeDCT(spline.SigmaDct); + } + + public static void EncodeAllStartingPoints(Span points, List tokens) + { + long lastX = 0; + long lastY = 0; + + for (int i = 0; i < points.Length; i++) + { + long x = (long)MathF.Round(points[i].X, MidpointRounding.AwayFromZero); + long y = (long)MathF.Round(points[i].Y, MidpointRounding.AwayFromZero); + + if (i == 0) + { + tokens.Add(new(JxlSplineEntropyContext.StartingPosition, (uint)x)); + tokens.Add(new(JxlSplineEntropyContext.StartingPosition, (uint)y)); + } + else + { + tokens.Add(new(JxlSplineEntropyContext.StartingPosition, JxlPackSigned.PackUnsigned((int)(x - lastX)))); + tokens.Add(new(JxlSplineEntropyContext.StartingPosition, JxlPackSigned.PackUnsigned((int)(y - lastY)))); + } + + lastX = x; + lastY = y; + } + } + + public static void EncodeSplines(JxlSplines splines, JxlBitWriter writer, JxlLayerType layer, JxlHistogramParameters histogramParameters, JxlAuxiliaryOutput auxOut) + { + Span quantizedSplines = splines.QuantizedSplines; + List> tokens = [[]]; + tokens[0].Add(new(JxlSplineEntropyContext.NumSplineContexts, (uint)(quantizedSplines.Length - 1))); + + EncodeAllStartingPoints(splines.StartingPoints, tokens[0]); + + tokens[0].Add(new(JxlSplineEntropyContext.QuantizationAdjustment, JxlPackSigned.PackUnsigned(splines.QuantizationAdjustment))); + + foreach (JxlQuantizedSpline spline in quantizedSplines) + { + Tokenize(spline, tokens[0]); + } + + _ = BuildAndEncodeHistograms(writer, histogramParameters, JxlSplineEntropyContext.NumSplineContexts, tokens, out JxlEntropyEncodingData codes, writer, layer, auxOut); + WriteTokens(tokens[0], codes, 0, writer, layer, auxOut); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs index 4cace39d73..00c162be9a 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlCoefficientOrder.cs @@ -50,7 +50,7 @@ public static uint CoeffOrderContext(uint value) return Math.Min(token, PermutationContexts - 1u); } - public static bool ReadPermutation(int skip, int size, Span order, JxlBitReader bitReader, JxlAnsSymbolReader reader, Span contextMap) + public static bool ReadPermutation(int skip, int size, Span order, JxlBitReader bitReader, JxlAnsSymbolReader reader, Span contextMap) { DebugGuard.MustBeLessThanOrEqualTo(size, 65536, nameof(size)); diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs index b03c77eced..8e4495e0f0 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.Generated.cs @@ -10,124 +10,139 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal static partial class JxlSimdUtils { - public static void StoreInterleaved(Vector v1, Vector v2, ref T memory) + public static unsafe void StoreInterleaved(Vector v1, Vector v2, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); } - public static void StoreInterleaved(Vector v1, Vector v2, Vector v3, ref T memory) + public static unsafe void StoreInterleaved(Vector v1, Vector v2, Vector v3, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); } - public static void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, ref T memory) + public static unsafe void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); } - public static void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, Vector v5, ref T memory) + public static unsafe void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, Vector v5, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); } - public static void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, Vector v5, Vector v6, ref T memory) + public static unsafe void StoreInterleaved(Vector v1, Vector v2, Vector v3, Vector v4, Vector v5, Vector v6, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); - v6.StoreUnsafe(ref Unsafe.Add(ref memory, 5)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); + v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); } - public static void StoreInterleaved(Vector128 v1, Vector128 v2, ref T memory) + public static unsafe void StoreInterleaved(Vector128 v1, Vector128 v2, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); } - public static void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, ref T memory) + public static unsafe void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); } - public static void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, ref T memory) + public static unsafe void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); } - public static void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, Vector128 v5, ref T memory) + public static unsafe void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, Vector128 v5, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); } - public static void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, Vector128 v5, Vector128 v6, ref T memory) + public static unsafe void StoreInterleaved(Vector128 v1, Vector128 v2, Vector128 v3, Vector128 v4, Vector128 v5, Vector128 v6, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); - v6.StoreUnsafe(ref Unsafe.Add(ref memory, 5)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); + v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); } - public static void StoreInterleaved(Vector256 v1, Vector256 v2, ref T memory) + public static unsafe void StoreInterleaved(Vector256 v1, Vector256 v2, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); } - public static void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, ref T memory) + public static unsafe void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); } - public static void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, ref T memory) + public static unsafe void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); } - public static void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, Vector256 v5, ref T memory) + public static unsafe void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, Vector256 v5, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); } - public static void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, Vector256 v5, Vector256 v6, ref T memory) + public static unsafe void StoreInterleaved(Vector256 v1, Vector256 v2, Vector256 v3, Vector256 v4, Vector256 v5, Vector256 v6, ref T memory) + where T : unmanaged { - v1.StoreUnsafe(ref Unsafe.Add(ref memory, 0)); - v2.StoreUnsafe(ref Unsafe.Add(ref memory, 1)); - v3.StoreUnsafe(ref Unsafe.Add(ref memory, 2)); - v4.StoreUnsafe(ref Unsafe.Add(ref memory, 3)); - v5.StoreUnsafe(ref Unsafe.Add(ref memory, 4)); - v6.StoreUnsafe(ref Unsafe.Add(ref memory, 5)); + v1.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 0)); + v2.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 1)); + v3.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 2)); + v4.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 3)); + v5.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 4)); + v6.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * 5)); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt index 41a5b9c90d..d4dff7ee41 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.StoreInterleaved.tt @@ -33,10 +33,11 @@ internal static partial class JxlSimdUtils } string inlineParameters = string.Join(", ", vectorParameters) + ", "; #> - public static void StoreInterleaved(<#= inlineParameters #>ref T memory) + public static unsafe void StoreInterleaved(<#= inlineParameters #>ref T memory) + where T : unmanaged { <# for (int j = 0; j < i; j++) { #> - v<#= j + 1 #>.StoreUnsafe(ref Unsafe.Add(ref memory, <#= j #>)); + v<#= j + 1 #>.StoreUnsafe(ref Unsafe.Add(ref memory, Vector.Count * <#= j #>)); <# } #> } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs index 8da98bbf4f..fdcc932c67 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlSimdUtils.cs @@ -15,10 +15,12 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; internal static partial class JxlSimdUtils { [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 ConcatLowerLower(Vector256 a, Vector256 b) => Vector256.Create(a.GetLower(), b.GetLower()); + public static Vector256 ConcatLowerLower(Vector256 a, Vector256 b) + where T : unmanaged => Vector256.Create(a.GetLower(), b.GetLower()); [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 ConcatUpperUpper(Vector256 a, Vector256 b) => Vector256.Create(a.GetUpper(), b.GetUpper()); + public static Vector256 ConcatUpperUpper(Vector256 a, Vector256 b) + where T : unmanaged => Vector256.Create(a.GetUpper(), b.GetUpper()); public static void Transpose8x8Block(Span fromSpan, Span toSpan, int stride) { @@ -109,4 +111,330 @@ public static Vector Pow(Vector @base, Vector exponent) Vector vec = Vector.Log2(@base) * exponent; return vec * vec; } + + /// + /// + /// Fills the span so its first value is equal to + /// and subsequent values increment by one. For example, with start=5, + /// the span's values will be: + /// + /// { start, start+1, start+2, start+3, start+4, ... to the end of the span } + /// + /// + /// + /// or, more precisely: + /// + /// { 5, 6, 7, 8, 9, 10, 11, ... to the end of the span } + /// + /// + /// + /// + /// The span where the values are filled. + /// Initial value. + public static void Iota(Span span, int start) + { + ref int spanRef = ref MemoryMarshal.GetReference(span); + + // Using fixed-size vectors so we can construct an + // incrementMask more easily. + if (Vector512.IsHardwareAccelerated) + { + Vector512 incrementMask = Vector512.Create(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16); + Vector512 v = Vector512.Create(start) + (incrementMask - Vector512.One); + + if ((span.Length % Vector512.Count) == 0) + { + // Aligned length + for (int i = 0; i < span.Length; i += Vector512.Count) + { + v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); + v += incrementMask; + } + } + else + { + // We will need a scalar remainder + int vectorLength = span.Length - (span.Length % Vector512.Count); + + int i; + for (i = 0; i < vectorLength; i += Vector512.Count) + { + v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); + v += incrementMask; + } + + int val = v.ToScalar(); + for (; i < span.Length; i++) + { + Unsafe.Add(ref spanRef, i) = val++; + } + } + } + else if (Vector256.IsHardwareAccelerated) + { + Vector256 incrementMask = Vector256.Create(1, 2, 3, 4, 5, 6, 7, 8); + Vector256 v = Vector256.Create(start) + (incrementMask - Vector256.One); + + if ((span.Length % Vector256.Count) == 0) + { + // Aligned length + for (int i = 0; i < span.Length; i += Vector256.Count) + { + v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); + v += incrementMask; + } + } + else + { + // We will need a scalar remainder + int vectorLength = span.Length - (span.Length % Vector256.Count); + + int i; + for (i = 0; i < vectorLength; i += Vector256.Count) + { + v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); + v += incrementMask; + } + + int val = v.ToScalar(); + for (; i < span.Length; i++) + { + Unsafe.Add(ref spanRef, i) = val++; + } + } + } + else if (Vector128.IsHardwareAccelerated) + { + Vector128 incrementMask = Vector128.Create(1, 2, 3, 4); + Vector128 v = Vector128.Create(start) + (incrementMask - Vector128.One); + + if ((span.Length % Vector128.Count) == 0) + { + // Aligned length + for (int i = 0; i < span.Length; i += Vector128.Count) + { + v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); + v += incrementMask; + } + } + else + { + // We will need a scalar remainder + int vectorLength = span.Length - (span.Length % Vector128.Count); + + int i; + for (i = 0; i < vectorLength; i += Vector128.Count) + { + v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); + v += incrementMask; + } + + int val = v.ToScalar(); + for (; i < span.Length; i++) + { + Unsafe.Add(ref spanRef, i) = val++; + } + } + } + else if (Vector64.IsHardwareAccelerated) + { + Vector64 incrementMask = Vector64.Create(1, 2); + Vector64 v = Vector64.Create(start) + (incrementMask - Vector64.One); + + if ((span.Length % Vector64.Count) == 0) + { + // Aligned length + for (int i = 0; i < span.Length; i += Vector64.Count) + { + v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); + v += incrementMask; + } + } + else + { + // We will need a scalar remainder + int vectorLength = span.Length - (span.Length % Vector64.Count); + + int i; + for (i = 0; i < vectorLength; i += Vector64.Count) + { + v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); + v += incrementMask; + } + + int val = v.ToScalar(); + for (; i < span.Length; i++) + { + Unsafe.Add(ref spanRef, i) = val++; + } + } + } + else + { + // No SIMD + int value = start; + spanRef = value; + value++; + for (int i = 1; i < span.Length; i++) + { + Unsafe.Add(ref spanRef, i) = value++; + } + } + } + + /// + /// + /// Fills the span so its first value is equal to + /// and subsequent values increment by one. For example, with start=5, + /// the span's values will be: + /// + /// { start, start+1, start+2, start+3, start+4, ... to the end of the span } + /// + /// + /// + /// or, more precisely: + /// + /// { 5, 6, 7, 8, 9, 10, 11, ... to the end of the span } + /// + /// + /// + /// + /// The span where the values are filled. + /// Initial value. + public static void Iota(Span span, T start) + where T : unmanaged, INumber + { + // Slightly slower than the int variant + ref T spanRef = ref MemoryMarshal.GetReference(span); + + if (Vector.IsSupported && Vector.IsHardwareAccelerated) + { + Vector incrementMask = IotaMask.IncrementMask; + Vector v = Vector.Create(start) + (incrementMask - Vector.One); + + if ((span.Length % Vector.Count) == 0) + { + // Aligned length + for (int i = 0; i < span.Length; i += Vector.Count) + { + v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); + v += incrementMask; + } + } + else + { + // Remainder needed + int vectorLength = span.Length - (span.Length % Vector.Count); + + int i; + for (i = 0; i < vectorLength; i += Vector.Count) + { + v.StoreUnsafe(ref Unsafe.Add(ref spanRef, i)); + v += incrementMask; + } + + T val = v.ToScalar(); + for (; i < span.Length; i++) + { + Unsafe.Add(ref spanRef, i) = val++; + } + } + } + else + { + // Scalar (slow) + T value = start; + spanRef = value; + value++; + for (int i = 1; i < span.Length; i++) + { + Unsafe.Add(ref spanRef, i) = value++; + } + } + } + + public static Vector Iota(T start) + where T : unmanaged, INumber + => IotaMask.IncrementMask + Vector.Create(start); + + /// + /// Vectorized floating-point error function (precise approximate). + /// + /// Vector to compute error of. + /// Vector whose each item is an error (similar to std::erf). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector FastErff(Vector x) + { + Vector zero = Vector.Zero; + Vector one = Vector.One; + + Vector xle0 = Vector.LessThanOrEqual(x, zero); + Vector absx = Vector.Abs(x); + + Vector denom1 = (absx * new Vector(0.0777394369f)) + new Vector(0.000205260015f); + Vector denom2 = (denom1 * absx) + new Vector(0.232120216f); + Vector denom3 = (denom2 * absx) + new Vector(0.277820801f); + Vector denom4 = (denom3 * absx) + one; + Vector denom5 = denom4 * denom4; + Vector invDenom5 = one / denom5; + Vector result = one - Vector.Multiply(invDenom5, invDenom5); + + // Change sign if x <= 0. + return Vector.ConditionalSelect(xle0, -result, result); + } + + /// + /// Scalar floating-point error function (precise approximate). + /// + /// Value to compute error of. + /// A scalar error value (similar to std::erf). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float FastErff(float x) + { + float zero = 0.0f; + float one = 1.0f; + + bool xle0 = x <= zero; + float absx = MathF.Abs(x); + + float denom1 = (absx * 0.0777394369f) + 0.000205260015f; + float denom2 = (denom1 * absx) + 0.232120216f; + float denom3 = (denom2 * absx) + 0.277820801f; + float denom4 = (denom3 * absx) + one; + float denom5 = denom4 * denom4; + float invDenom5 = one / denom5; + float result = one - (invDenom5 * invDenom5); + + // Change sign if x <= 0. + return xle0 ? -result : result; + } + + /// + /// Incrementing values to compute the Iota function. + /// + /// + /// Creating a Vector<T> incrementing values would be + /// slow as Vector<T> is not a fixed-size vector, leaving + /// no other option but a slow loop. This class caches these + /// vectors for significantly better performance, though still + /// not as fast as an int variant. + /// + /// Type of the vector. + private static class IotaMask + where T : unmanaged, INumber + { + public static readonly Vector IncrementMask; + + static IotaMask() + { + Span values = stackalloc T[Vector.Count]; + + for (int i = 0; i < Vector.Count; i++) + { + values[i] = T.CreateSaturating(i + 1); + } + + IncrementMask = Vector.Create(values); + } + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs index 99a3a8c984..5797553f54 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlTranspose.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; namespace SixLabors.ImageSharp.Formats.Jxl.Processing; @@ -10,9 +12,29 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing; /// internal static class JxlTranspose { - // TODO: SIMD public static void Transpose(int r, int c, JxlDctSource from, JxlDctOutput to) { + if (Vector256.IsHardwareAccelerated) + { + if (((r | c) & 7) == 0) // equivalent to (r % 8 == 0 && c % 8 == 0); micro-optimization, reduces one branch + { + // we can use SIMD + TransposeSimd256(r, c, from, to, r, c); + return; + } + } + else if (Vector128.IsHardwareAccelerated) + { + if (((r | c) & 3) == 0) // equivalent to (r % 4 == 0 && c % 4 == 0); micro-optimization, reduces one branch + { + // we can use SIMD + TransposeSimd128(r, c, from, to, r, c); + return; + } + } + + // fallback: can't use SIMD (block size isn't aligned or + // there's no v128/v256 support) for (int n = 0; n < r; n++) { for (int m = 0; m < c; m++) @@ -21,4 +43,93 @@ public static void Transpose(int r, int c, JxlDctSource from, JxlDctOutput to) } } } + + private static void TransposeSimd256(int rowsOr0, int colsOr0, JxlDctSource from, JxlDctOutput to, int rowsP, int colsP) + { + int rows = rowsOr0 == 0 ? rowsP : rowsOr0; + int cols = colsOr0 == 0 ? colsP : colsOr0; + + for (int n = 0; n < rows; n += 8) + { + for (int m = 0; m < cols; m += 8) + { + Vector256 i0 = from.LoadPart256(n, m); + Vector256 i1 = from.LoadPart256(n + 1, m); + Vector256 i2 = from.LoadPart256(n + 2, m); + Vector256 i3 = from.LoadPart256(n + 3, m); + Vector256 i4 = from.LoadPart256(n + 4, m); + Vector256 i5 = from.LoadPart256(n + 5, m); + Vector256 i6 = from.LoadPart256(n + 6, m); + Vector256 i7 = from.LoadPart256(n + 7, m); + + Vector256 q0 = Vector256_.InterleaveLower(i0, i2); + Vector256 q1 = Vector256_.InterleaveLower(i1, i3); + Vector256 q2 = Vector256_.InterleaveUpper(i0, i2); + Vector256 q3 = Vector256_.InterleaveUpper(i1, i3); + Vector256 q4 = Vector256_.InterleaveLower(i4, i6); + Vector256 q5 = Vector256_.InterleaveLower(i5, i7); + Vector256 q6 = Vector256_.InterleaveUpper(i4, i6); + Vector256 q7 = Vector256_.InterleaveUpper(i5, i7); + + Vector256 r0 = Vector256_.InterleaveLower(q0, q1); + Vector256 r1 = Vector256_.InterleaveUpper(q0, q1); + Vector256 r2 = Vector256_.InterleaveLower(q2, q3); + Vector256 r3 = Vector256_.InterleaveUpper(q2, q3); + Vector256 r4 = Vector256_.InterleaveLower(q4, q5); + Vector256 r5 = Vector256_.InterleaveUpper(q4, q5); + Vector256 r6 = Vector256_.InterleaveLower(q6, q7); + Vector256 r7 = Vector256_.InterleaveUpper(q6, q7); + + i0 = JxlSimdUtils.ConcatLowerLower(r4, r0); + i1 = JxlSimdUtils.ConcatLowerLower(r5, r1); + i2 = JxlSimdUtils.ConcatLowerLower(r6, r2); + i3 = JxlSimdUtils.ConcatLowerLower(r7, r3); + i4 = JxlSimdUtils.ConcatUpperUpper(r4, r0); + i5 = JxlSimdUtils.ConcatUpperUpper(r5, r1); + i6 = JxlSimdUtils.ConcatUpperUpper(r6, r2); + i7 = JxlSimdUtils.ConcatUpperUpper(r7, r3); + + to.StorePart256(i0, m, n); + to.StorePart256(i1, m + 1, n); + to.StorePart256(i2, m + 2, n); + to.StorePart256(i3, m + 3, n); + to.StorePart256(i4, m + 4, n); + to.StorePart256(i5, m + 5, n); + to.StorePart256(i6, m + 6, n); + to.StorePart256(i7, m + 7, n); + } + } + } + + private static void TransposeSimd128(int rowsOr0, int colsOr0, JxlDctSource from, JxlDctOutput to, int rowsP, int colsP) + { + int rows = rowsOr0 == 0 ? rowsP : rowsOr0; + int cols = colsOr0 == 0 ? colsP : colsOr0; + + for (int n = 0; n < rows; n += 4) + { + for (int m = 0; m < cols; m += 4) + { + Vector128 p0 = from.LoadPart128(n, m); + Vector128 p1 = from.LoadPart128(n + 1, m); + Vector128 p2 = from.LoadPart128(n + 2, m); + Vector128 p3 = from.LoadPart128(n + 3, m); + + Vector128 q0 = Vector128_.InterleaveLower(p0, p2); + Vector128 q1 = Vector128_.InterleaveLower(p1, p3); + Vector128 q2 = Vector128_.InterleaveUpper(p0, p2); + Vector128 q3 = Vector128_.InterleaveUpper(p1, p3); + + Vector128 r0 = Vector128_.InterleaveLower(q0, q1); + Vector128 r1 = Vector128_.InterleaveUpper(q0, q1); + Vector128 r2 = Vector128_.InterleaveLower(q2, q3); + Vector128 r3 = Vector128_.InterleaveUpper(q2, q3); + + to.StorePart128(r0, m, n); + to.StorePart128(r1, m + 1, n); + to.StorePart128(r2, m + 2, n); + to.StorePart128(r3, m + 3, n); + } + } + } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlLehmerCode.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlLehmerCode.cs index 2cbd4ea7ab..83e591fa9e 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlLehmerCode.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlLehmerCode.cs @@ -10,16 +10,16 @@ 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) + 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 s = permutation[idx]; uint penalty = 0u; - uint i = (uint)s + 1u; + uint i = s + 1u; while (i != 0u) { @@ -32,8 +32,8 @@ public static bool ComputeLehmerCode(ReadOnlySpan permutation, Span t return false; } - code[idx] = (uint)s - penalty; - i = (uint)s + 1u; + code[idx] = s - penalty; + i = s + 1u; while (i < n + 1u) { @@ -45,7 +45,7 @@ public static bool ComputeLehmerCode(ReadOnlySpan permutation, Span t return true; } - public static bool DecodeLehmerCode(ReadOnlySpan code, Span temp, int n, Span permutation) + public static bool DecodeLehmerCode(ReadOnlySpan code, Span temp, int n, Span permutation) { if (n == 0) { @@ -91,7 +91,7 @@ public static bool DecodeLehmerCode(ReadOnlySpan code, Span temp, in } } - permutation[i] = next; + permutation[i] = unchecked((uint)next); next++; while (next <= paddedN) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs index 68edb75d5e..8f04197ee5 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Primitives/JxlXorShift.cs @@ -12,7 +12,7 @@ internal sealed class JxlXorShift private readonly ulong[] s0 = new ulong[8]; private readonly ulong[] s1 = new ulong[8]; - public void XorShift128Plus(ulong seed) + public JxlXorShift(ulong seed) { this.s0[0] = SplitMix64(seed + 0x9E3779B97F4A7C15L); this.s1[0] = SplitMix64(this.s0[0]); diff --git a/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerSimd.cs b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerSimd.cs new file mode 100644 index 0000000000..66d92467cb --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Quantization/JxlQuantizerSimd.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization; + +/// +/// SIMD utilities used by the quantizer. +/// +internal static class JxlQuantizerSimd +{ + public static Vector AdjustQuantBias(int c, Vector quantI, Span biases) + { + Vector quant = quantI.As(); + Vector constSign = Vector.Create(int.MinValue).As(); + Vector sign = quant & constSign; + Vector absoluteQuant = Vector.AndNot(constSign, quant); + + Vector is01 = Vector.LessThan(absoluteQuant, Vector.Create(1.125f)); + Vector not0 = Vector.GreaterThan(absoluteQuant, Vector.One); + + Vector oneBias = Vector.ConditionalSelect(not0, Vector.Create(biases[c]) ^ sign, Vector.Zero); + Vector bias = -(Vector.Create(biases[3]) * quant.ReciprocalEstimate()) + quant; + + return Vector.ConditionalSelect(is01, oneBias, bias); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/Dct32.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/Dct32.cs new file mode 100644 index 0000000000..e7509e8c05 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/Dct32.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; + +/// +/// Storage for 32 DCT coefficients (floating-point). +/// +[InlineArray(32)] +internal struct Dct32 +{ + private float first; +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs index a3901bf408..0a06b60da7 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlQuantizedSpline.cs @@ -224,7 +224,7 @@ public bool Dequantize( for (int i = 0; i < 32; i++) { - float inverseDctFactor = (i == 0) ? Sqrt05 : 1.0f; + float inverseDctFactor = (i == 0) ? JxlDctScales.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)); @@ -240,7 +240,7 @@ public bool Dequantize( return true; } - public bool Decode( + public bool TryDecode( Configuration configuration, Span contextMap, JxlAnsSymbolReader decoder, diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSpline.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSpline.cs index f95886b268..e3c35d5e09 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSpline.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSpline.cs @@ -11,9 +11,9 @@ internal sealed class JxlSpline : IDisposable public Memory ControlPoints { get; private set; } - public JxlDct32[] ColorDct { get; set; } = []; + public Dct32[] ColorDct { get; set; } = []; - public JxlDct32 SigmaDct { get; set; } + public Dct32 SigmaDct { get; set; } public void ClearControlPoints() { diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineUtils.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineUtils.cs new file mode 100644 index 0000000000..89b446e1f0 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplineUtils.cs @@ -0,0 +1,394 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Dct; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; + +internal static class JxlSplineUtils +{ + public const float DesiredRenderingDistance = 1f; + + private static ReadOnlySpan ContinuousIDCTMultipliers => + [ + MathF.PI / 32 * 0, MathF.PI / 32 * 1, MathF.PI / 32 * 2, MathF.PI / 32 * 3, MathF.PI / 32 * 4, + MathF.PI / 32 * 5, MathF.PI / 32 * 6, MathF.PI / 32 * 7, MathF.PI / 32 * 8, MathF.PI / 32 * 9, + MathF.PI / 32 * 10, MathF.PI / 32 * 11, MathF.PI / 32 * 12, MathF.PI / 32 * 13, MathF.PI / 32 * 14, + MathF.PI / 32 * 15, MathF.PI / 32 * 16, MathF.PI / 32 * 17, MathF.PI / 32 * 18, MathF.PI / 32 * 19, + MathF.PI / 32 * 20, MathF.PI / 32 * 21, MathF.PI / 32 * 22, MathF.PI / 32 * 23, MathF.PI / 32 * 24, + MathF.PI / 32 * 25, MathF.PI / 32 * 26, MathF.PI / 32 * 27, MathF.PI / 32 * 28, MathF.PI / 32 * 29, + MathF.PI / 32 * 30, MathF.PI / 32 * 31, + ]; + + public static float ContinuousInverseDCT(in Dct32 dct, float t) + { + ref float multipliers = ref MemoryMarshal.GetReference(ContinuousIDCTMultipliers); + ReadOnlySpan dctData = dct; + ref float dctDataRef = ref MemoryMarshal.GetReference(dctData); + + if (Vector.Count <= 32 && Vector.IsHardwareAccelerated) + { + Vector result = Vector.Zero; + Vector tandhalf = Vector.Create(t + 0.5f); + + for (int i = 0; i < 32; i += Vector.Count) + { + Vector cosArg = Vector.LoadUnsafe(ref Unsafe.Add(ref multipliers, i)) * tandhalf; + Vector cos = Vector.Cos(cosArg); + Vector localRes = Vector.LoadUnsafe(ref Unsafe.Add(ref dctDataRef, i)) * cos; + result = (Vector.Create(JxlDctScales.Sqrt2) * localRes) + result; + } + + return Vector.Sum(result); + } + + // Might have SIMD support but Vector > 32 (e.g. on some CPUs), + // so let's try different fixed-size vectors first. + else if (Vector512.IsHardwareAccelerated) + { + Vector512 result = Vector512.Zero; + Vector512 tandhalf = Vector512.Create(t + 0.5f); + + for (int i = 0; i < 32; i += Vector512.Count) + { + Vector512 cosArg = Vector512.LoadUnsafe(ref Unsafe.Add(ref multipliers, i)) * tandhalf; + Vector512 cos = Vector512.Cos(cosArg); + Vector512 localRes = Vector512.LoadUnsafe(ref Unsafe.Add(ref dctDataRef, i)) * cos; + result = (Vector512.Create(JxlDctScales.Sqrt2) * localRes) + result; + } + + return Vector512.Sum(result); + } + else if (Vector256.IsHardwareAccelerated) + { + Vector256 result = Vector256.Zero; + Vector256 tandhalf = Vector256.Create(t + 0.5f); + + for (int i = 0; i < 32; i += Vector256.Count) + { + Vector256 cosArg = Vector256.LoadUnsafe(ref Unsafe.Add(ref multipliers, i)) * tandhalf; + Vector256 cos = Vector256.Cos(cosArg); + Vector256 localRes = Vector256.LoadUnsafe(ref Unsafe.Add(ref dctDataRef, i)) * cos; + result = (Vector256.Create(JxlDctScales.Sqrt2) * localRes) + result; + } + + return Vector256.Sum(result); + } + else if (Vector128.IsHardwareAccelerated) + { + Vector128 result = Vector128.Zero; + Vector128 tandhalf = Vector128.Create(t + 0.5f); + + for (int i = 0; i < 32; i += Vector128.Count) + { + Vector128 cosArg = Vector128.LoadUnsafe(ref Unsafe.Add(ref multipliers, i)) * tandhalf; + Vector128 cos = Vector128.Cos(cosArg); + Vector128 localRes = Vector128.LoadUnsafe(ref Unsafe.Add(ref dctDataRef, i)) * cos; + result = (Vector128.Create(JxlDctScales.Sqrt2) * localRes) + result; + } + + return Vector128.Sum(result); + } + else if (Vector64.IsHardwareAccelerated) + { + Vector64 result = Vector64.Zero; + Vector64 tandhalf = Vector64.Create(t + 0.5f); + + for (int i = 0; i < 32; i += Vector64.Count) + { + Vector64 cosArg = Vector64.LoadUnsafe(ref Unsafe.Add(ref multipliers, i)) * tandhalf; + Vector64 cos = Vector64.Cos(cosArg); + Vector64 localRes = Vector64.LoadUnsafe(ref Unsafe.Add(ref dctDataRef, i)) * cos; + result = (Vector64.Create(JxlDctScales.Sqrt2) * localRes) + result; + } + + return Vector64.Sum(result); + } + else + { + // Scalar fallback. + float result = 0f; + float tandhalf = t + 0.5f; + + for (int i = 0; i < 32; i++) + { + float cosArg = Unsafe.Add(ref multipliers, i) * tandhalf; + float cos = MathF.Cos(cosArg); + float localRes = Unsafe.Add(ref dctDataRef, i) * cos; + result = (JxlDctScales.Sqrt2 * localRes) + result; + } + + return result; + } + } + + // SIMD version + private static void DrawSegmentPacked(ref JxlSplineSegment segment, bool add, int y, int x, int x0, InlineArray3> rows) + { + Vector inverseSigma = Vector.Create(segment.InverseSigma); + Vector half = Vector.Create(0.5f); + Vector oneOver2s2 = Vector.Create(0.353553391f); + Vector sigmaOver4TimesIntensity = Vector.Create(segment.SigmaOver4TimesIntensity); + + Vector dx = JxlSimdUtils.Iota(x + x0).As() - Vector.Create(segment.Center.X); + Vector dy = Vector.Create(y - segment.Center.Y); + + Vector sqd = (dx * dx) + (dy * dy); + Vector distance = Vector.SquareRoot(sqd); + + Vector oneDimensionalFactor = + JxlSimdUtils.FastErff(((distance * half) + oneOver2s2) * inverseSigma) + - JxlSimdUtils.FastErff(((distance * half) - oneOver2s2) * inverseSigma); + + Vector localIntensity = sigmaOver4TimesIntensity * (oneDimensionalFactor * oneDimensionalFactor); + + for (int c = 0; c < 3; c++) + { + Span currRow = rows[c].Span; + ref float currRowRef = ref MemoryMarshal.GetReference(currRow); + + // TODO: move the add branch outside the loop and duplicate the + // loops twice? this removes the branch + Vector cm = Vector.Create(add ? segment.Color[c] : -segment.Color[c]); + + Vector @in = Vector.LoadUnsafe(ref Unsafe.Add(ref currRowRef, x)); + ((cm * localIntensity) + @in).StoreUnsafe(ref Unsafe.Add(ref currRowRef, x)); + } + } + + // Scalar version (for remaining items left to process) + private static void DrawSegmentScalar(ref JxlSplineSegment segment, bool add, int y, int x, int x0, InlineArray3> rows) + { + float inverseSigma = segment.InverseSigma; + float half = 0.5f; + float oneOver2s2 = 0.353553391f; + float sigmaOver4TimesIntensity = segment.SigmaOver4TimesIntensity; + + float dx = (x + x0) - segment.Center.X; + float dy = y - segment.Center.Y; + + float sqd = (dx * dx) + (dy * dy); + float distance = MathF.Sqrt(sqd); + + float oneDimensionalFactor = + JxlSimdUtils.FastErff(((distance * half) + oneOver2s2) * inverseSigma) + - JxlSimdUtils.FastErff(((distance * half) - oneOver2s2) * inverseSigma); + + float localIntensity = sigmaOver4TimesIntensity * (oneDimensionalFactor * oneDimensionalFactor); + + for (int c = 0; c < 3; c++) + { + Span currRow = rows[c].Span; + float cm = add ? segment.Color[c] : -segment.Color[c]; + currRow[x] = (cm * localIntensity) + currRow[x]; + } + } + + public static void DrawSegment(ref JxlSplineSegment segment, bool add, int y, int x0, int x1, InlineArray3> rows) + { + int start = (int)MathF.Round(segment.Center.X - segment.MaximumDistance, MidpointRounding.AwayFromZero); + int end = (int)MathF.Round(segment.Center.X + segment.MaximumDistance, MidpointRounding.AwayFromZero); + + if (end < x0 || start >= x1) + { + return; // span does not intersect scan + } + + int spanX0 = Math.Max(x0, start) - x0; + int spanX1 = Math.Min(x1, end + 1) - x0; + + int x = spanX0; + for (; x + Vector.Count <= spanX1; x += Vector.Count) + { + DrawSegmentPacked(ref segment, add, y, x, x0, rows); + } + + for (; x < spanX1; ++x) + { + DrawSegmentScalar(ref segment, add, y, x, x0, rows); + } + } + + public static void ComputeSegments(int imageYSize, PointF center, float intensity, InlineArray3 color, float sigma, List segments, List segmentSpans) + { + if (!(float.IsFinite(sigma) && sigma != 0.0f && float.IsFinite(1.0f / sigma) && float.IsFinite(intensity))) + { + return; + } + + // This is about 30% faster, but for higher precision + // one can change this to 5 instead. + const float distanceExp = 3f; + + float maxColor = MathF.Max(0.01f, MathF.Abs(color[0] * intensity)); + maxColor = MathF.Max(maxColor, MathF.Abs(color[1] * intensity)); + maxColor = MathF.Max(maxColor, MathF.Abs(color[2] * intensity)); + + float maximumDistance = MathF.Sqrt(-2.0f * sigma * sigma * ((MathF.Log(0.1f) * distanceExp) - MathF.Log(maxColor))); + + int y0 = (int)MathF.Round(center.Y - maximumDistance, MidpointRounding.AwayFromZero); + y0 = Math.Max(y0, 0); + + int y1 = (int)MathF.Round(center.Y + maximumDistance, MidpointRounding.AwayFromZero) + 1; + y1 = Math.Min(y1, imageYSize); + + if (y1 <= y0) + { + return; + } + + JxlSplineSegment segment = new() + { + Center = center, + InverseSigma = 1.0f / sigma, + SigmaOver4TimesIntensity = 0.25f * sigma * intensity, + MaximumDistance = maximumDistance, + Color = color + }; + + segments.Add(segment); + segmentSpans.Add(new JxlSplineSegmentSpan(y0, y1)); + } + + public static void DrawSegments(Memory rowX, Memory rowY, Memory rowB, int y, int x0, int x1, bool add, Span segments, Span segmentIndices, Span segmentYStart) + { + InlineArray3> rows = default; + rows[0] = rowX; + rows[1] = rowY; + rows[2] = rowB; + + for (int i = segmentYStart[y]; i < segmentYStart[y + 1]; i++) + { + DrawSegment(ref segments[segmentIndices[i]], add, y, x0, x1, rows); + } + } + + public static void SegmentsFromPoints(int imageYSize, JxlSpline spline, List<(PointF Point, float Multiplier)> pointsToDraw, float arcLength, List segments, List segmentsSpans) + { + float inverseArcLength = 1.0f / arcLength; + int k = 0; + + foreach ((PointF point, float multiplier) in pointsToDraw) + { + float progressAlongArc = MathF.Min(1.0f, (k++ * DesiredRenderingDistance) * inverseArcLength); + + InlineArray3 color = default; + color[0] = ContinuousInverseDCT(spline.ColorDct[0], (32 - 1) * progressAlongArc); + color[1] = ContinuousInverseDCT(spline.ColorDct[1], (32 - 1) * progressAlongArc); + color[2] = ContinuousInverseDCT(spline.ColorDct[2], (32 - 1) * progressAlongArc); + + float sigma = ContinuousInverseDCT(spline.SigmaDct, (32 - 1) * progressAlongArc); + ComputeSegments(imageYSize, point, multiplier, color, sigma, segments, segmentsSpans); + } + } + + public static void DrawCentripetalCatmullRomSpline(Span points, List result) + { + if (points.Length == 0) + { + return; + } + + if (points.Length == 1) + { + result.Add(points[0]); + return; + } + + List pointsCopy = []; + for (int i = 0; i < points.Length; i++) + { + pointsCopy.Add(points[i]); + } + + const int numPoints = 16; + pointsCopy.Insert(0, pointsCopy[0] + (pointsCopy[0] - pointsCopy[1])); + pointsCopy.Add(pointsCopy[^1] + (pointsCopy[^1] - pointsCopy[^2])); + + for (int start = 0; start < pointsCopy.Count - 3; start++) + { + Span p = CollectionsMarshal.AsSpan(pointsCopy)[start..]; + result.Add(p[1]); + + InlineArray3 d = default; + InlineArray4 t = default; + + for (int k = 0; k < 3; ++k) + { + d[k] = MathF.Sqrt(JxlMath.Hypot(p[k + 1].X - p[k].X, p[k + 1].Y - p[k].Y)); + t[k + 1] = t[k] + d[k]; + } + + for (int i = 1; i < numPoints; ++i) + { + float tt = d[0] + (((float)i / numPoints) * d[1]); + InlineArray3 a = default; + + for (int k = 0; k < 3; ++k) + { + a[k] = p[k] + (((tt - t[k]) / d[k]) * (p[k + 1] - p[k])); + } + + InlineArray3 b = default; + + for (int k = 0; k < 2; ++k) + { + b[k] = a[k] + (((tt - t[k]) / (d[k] + d[k + 1])) * (a[k + 1] - a[k])); + } + + result.Add(b[0] + (((tt - t[1]) / d[1]) * (b[1] - b[0]))); + } + } + + result.Add(pointsCopy[^2]); + } + + public static void ForEachEquallySpacedPoint(Span points, Action functor) + { + PointF current = points[0]; + functor(current, DesiredRenderingDistance); + + ref PointF next = ref points[0]; + ref PointF end = ref points[^1]; // last + + while (!Unsafe.AreSame(ref next, ref end)) + { + ref PointF previous = ref current; + float arcLengthFromPrevious = 0f; + + while (true) + { + if (next == end) + { + functor(previous, arcLengthFromPrevious); + return; + } + + float arcLengthToNext = MathF.Sqrt(SquaredNorm(next - previous)); + + if (arcLengthFromPrevious + arcLengthToNext >= DesiredRenderingDistance) + { + current = previous + (((DesiredRenderingDistance - arcLengthFromPrevious) / arcLengthToNext) * (next - previous)); + functor(current, DesiredRenderingDistance); + break; + } + + arcLengthFromPrevious += arcLengthToNext; + previous = ref next; + next = ref Unsafe.Add(ref next, 1); + } + } + } + + private static float SquaredNorm(PointF pointF) + { + float x = pointF.X; + float y = pointF.Y; + + return (x * x) + (y * y); + } +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs new file mode 100644 index 0000000000..52e83b4bb4 --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Splines/JxlSplines.cs @@ -0,0 +1,289 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +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.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Splines; + +internal sealed class JxlSplines +{ + private List splinesStorage = []; + private readonly List startingPointsStorage = []; + private JxlSplineDataView data = new(); + private readonly List segments = []; + private IMemoryOwner segmentIndices = EmptyMemoryOwner.Instance; + private IMemoryOwner segmentYStart = EmptyMemoryOwner.Instance; + + public bool HasAny => this.data.HasAny; + + public Span QuantizedSplines => CollectionsMarshal.AsSpan(this.data.Splines); + + public Span StartingPoints => CollectionsMarshal.AsSpan(this.data.StartingPoints); + + public int QuantizationAdjustment { get; private set; } + + public void SetData(JxlSplineDataView data) + { + this.Clear(); + this.data = data; + } + + public void Clear() + { + this.QuantizationAdjustment = 0; + this.splinesStorage.Clear(); + this.startingPointsStorage.Clear(); + this.data = new(); + this.segments.Clear(); + this.segmentIndices = EmptyMemoryOwner.Instance; + this.segmentYStart = EmptyMemoryOwner.Instance; + } + + public void Decode(Configuration configuration, JxlBitReader reader, int numPixels) + { + List contextMap = []; + + JxlAnsReader.DecodeHistograms(reader, NumSplineContexts, out JxlAnsCode code, contextMap); + JxlAnsSymbolReader decoder = new(code, reader); + + int numSplines = decoder.ReadHybridUint(NumSplinesContext, reader, contextMap); + int maxControlPoints = Math.Min(MaxNumControlPoints, numPixels / MaxNumControlPointsPerPixelRatio); + + if (numSplines > maxControlPoints || numSplines + 1 > maxControlPoints) + { + throw new InvalidOperationException("Too many splines: " + numSplines); + } + + numSplines++; + + DecodeAllStartingPoints(this.startingPointsStorage, reader, decoder, contextMap, numSplines); + + this.QuantizationAdjustment = JxlPackSigned.UnpackSigned(decoder.ReadHybridUint(QuantizationAdjustmentContext, reader, contextMap)); + this.splinesStorage = new List(numSplines); + + int numControlPoints = numSplines; + + for (int i = 0; i < numSplines; ++i) + { + JxlQuantizedSpline spline = new(); + if (!spline.TryDecode( + configuration, + CollectionsMarshal.AsSpan(contextMap), + decoder, + reader, + maxControlPoints, + ref numControlPoints)) + { + throw new InvalidOperationException("Could not decode quantized spline. Index of the quantized spline: " + i); + } + + this.splinesStorage.Add(spline); + } + + if (!decoder.CheckAnsFinalState()) + { + throw new InvalidOperationException("Not ANS final state"); + } + + this.data = new JxlSplineDataView() + { + Splines = this.splinesStorage, + StartingPoints = this.startingPointsStorage + }; + + if (!this.HasAny) + { + throw new InvalidOperationException("Decoded splines but got none"); + } + } + + public void InitializeDrawCache(Configuration configuration, int imageXSize, int imageYSize, JxlColorCorrelation colorCorrelation) + { + this.segments.Clear(); + this.segmentIndices = EmptyMemoryOwner.Instance; + this.segmentYStart = EmptyMemoryOwner.Instance; + + List segmentsSpans = []; + List intermediatePoints = []; + List splines = []; + long totalEstimatedAreaReached = 0; + + for (int i = 0; i < this.data.Splines.Count; i++) + { + JxlSpline spline = new(); + + if (!this.data.Splines[i].Dequantize( + configuration, + this.data.StartingPoints[i], + this.QuantizationAdjustment, + colorCorrelation.YToXRatio(0), + colorCorrelation.YToBRatio(0), + imageXSize * imageYSize, + ref totalEstimatedAreaReached, + spline)) + { + throw new InvalidOperationException("Could not dequantize a quantized spline"); + } + + if (AdjacentFind(spline.ControlPoints.Span) != spline.ControlPoints.Length - 1) + { + throw new InvalidOperationException("Identical successive control points in spline " + i); + } + + splines.Add(spline); + } + +#if JPEG_XL_THROW_ON_LARGE_SPLINE_AREA + if (totalEstimatedAreaReached > Math.Min((8 * imageXSize * imageYSize) + (1 << 25), 1 << 30)) + { + throw new InvalidOperationException("Total spline area is too large"); + } +#endif + + foreach (JxlSpline spline in splines) + { + List<(PointF Point, float Multiplier)> pointsToDraw = []; + + void AddPoint(PointF point, float multiplier) => pointsToDraw.Add((point, multiplier)); + + intermediatePoints.Clear(); + + JxlSplineUtils.DrawCentripetalCatmullRomSpline(spline.ControlPoints.Span, intermediatePoints); + JxlSplineUtils.ForEachEquallySpacedPoint(CollectionsMarshal.AsSpan(intermediatePoints), AddPoint); + + float arcLength = ((pointsToDraw.Count - 2) * JxlSplineUtils.DesiredRenderingDistance) + pointsToDraw[^1].Multiplier; + if (arcLength <= 0f) + { + // This spline wouldn't have any effect. + continue; + } + + JxlSplineUtils.SegmentsFromPoints(imageYSize, spline, pointsToDraw, arcLength, this.segments, segmentsSpans); + } + + int segmentYStartNumBytes = (imageYSize + 2) * 4; + this.segmentYStart = configuration.MemoryAllocator.Allocate(segmentYStartNumBytes); + + Span segmentYStart = this.segmentYStart.Memory.Span; + segmentYStart.Clear(); + + Span population = segmentYStart[1..]; + + foreach (JxlSplineSegmentSpan segmentSpan in segmentsSpans) + { + population[segmentSpan.StartInclusive]++; + population[segmentSpan.EndInclusive]--; + } + + int total = 0; + int coverage = 0; + + for (int y = 0; y < imageYSize; y++) + { + if (population[y] < 0) + { + if (coverage < -population[y]) + { + throw new InvalidOperationException("Coverage is invalid"); + } + } + + coverage += population[y]; + population[y] = (byte)total; + total += coverage; + } + + this.segmentIndices = configuration.MemoryAllocator.Allocate(total * 4); + Span segmentIndices = MemoryMarshal.Cast(this.segmentIndices.Memory.Span); + + for (int i = 0; i < this.segments.Count; i++) + { + JxlSplineSegmentSpan segmentSpan = segmentsSpans[i]; + + for (int y = segmentSpan.StartInclusive; y < segmentSpan.EndInclusive; y++) + { + segmentIndices[population[y]++] = i; + } + } + } + + private static int AdjacentFind(Span span) + where T : IEquatable + { + for (int i = 0; i < span.Length - 1; i++) + { + if (span[i].Equals(span[i + 1])) + { + return i; + } + } + + return span.Length; + } + + public void AddTo(JxlImage3F opsin, Rectangle opsinRect) => this.Apply(add: true, opsin, opsinRect); + + public void AddToRow(Memory rowX, Memory rowY, Memory rowB, int y, int x0, int x1) + => this.ApplyToRow(add: true, rowX, rowY, rowB, y, x0, x1); + + public void SubtractFrom(JxlImage3F opsin) => this.Apply(add: false, opsin, opsin.GetRectangle()); + + private void ApplyToRow(bool add, Memory rowX, Memory rowY, Memory rowB, int y, int x0, int x1) + { + if (this.segments.Count == 0) + { + return; + } + + JxlSplineUtils.DrawSegments( + rowX, + rowY, + rowB, + y, + x0, + x1, + add, + CollectionsMarshal.AsSpan(this.segments), + MemoryMarshal.Cast(this.segmentIndices.Memory.Span), + MemoryMarshal.Cast(this.segmentYStart.Memory.Span)); + } + + private void Apply(bool add, JxlImage3F opsin, Rectangle opsinRect) + { + if (this.segments.Count == 0) + { + return; + } + + int y0 = RectangleUtils.Y0(in opsinRect); + int x0 = RectangleUtils.X0(in opsinRect); + int x1 = RectangleUtils.X1(in opsinRect); + + for (int y = 0; y < opsinRect.Height; y++) + { + this.ApplyToRow( + add, + opsin.PlaneRowMemory(0, y0 + y)[x0..], + opsin.PlaneRowMemory(1, y0 + y)[x0..], + opsin.PlaneRowMemory(2, y0 + y)[x0..], + y0 + y, + x0, + x1); + } + } + + private sealed class EmptyMemoryOwner : IMemoryOwner + { + public static readonly EmptyMemoryOwner Instance = new(); + + public Memory Memory => Memory.Empty; + + public void Dispose() + { + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs new file mode 100644 index 0000000000..87b2fc6153 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/AnsCommonTests.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; + +public class AnsCommonTests +{ + private static void VerifyAliasDistribution(Span distribution, uint logRange) + { + const int logAlphaSize = 8; + + Span table = stackalloc JxlAnsEntry[1 << logAlphaSize]; + bool success = JxlAnsHelper.InitAliasTable(distribution, logRange, logAlphaSize, table); + Assert.True(success); + + uint range = 1u << (int)logRange; + List[] offsets = new List[distribution.Length]; + + for (int i = 0; i < range; i++) + { + JxlAnsSymbol s = JxlAnsHelper.Lookup(table, i, JxlAnsConstants.AnsLogTableSize - 8, (1 << (JxlAnsConstants.AnsLogTableSize - 8)) - 1); + + offsets[s.Value] ??= []; + offsets[s.Value].Add(s.Offset); + } + + for (int i = 0; i < distribution.Length; i++) + { + Assert.Equal(distribution[i], offsets[i].Count); + offsets[i].Sort(); + + for (int j = 0; j < offsets[i].Count; j++) + { + Assert.Equal(offsets[i][j], j); + } + } + } + + [Fact] + public void AliasDistributionSmoke() + { + VerifyAliasDistribution([JxlAnsConstants.AnsTableSize / 2, JxlAnsConstants.AnsTableSize / 2], JxlAnsConstants.AnsLogTableSize); + VerifyAliasDistribution([JxlAnsConstants.AnsTableSize], JxlAnsConstants.AnsLogTableSize); + VerifyAliasDistribution([0, 0, 0, JxlAnsConstants.AnsTableSize, 0], JxlAnsConstants.AnsLogTableSize); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/GammaCorrectionTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/GammaCorrectionTests.cs new file mode 100644 index 0000000000..edb9751cae --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/GammaCorrectionTests.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing.Encoder; + +public class GammaCorrectionTests +{ + [Fact] + public void TestLinearToSRgbEdgeCases() + { + Assert.Equal(0, JxlGammaCorrect.LinearToSRgb8Direct(0.0)); + + Assert.True(new TolerantMath(2E-5).AreEqual(0, JxlGammaCorrect.LinearToSRgb8Direct(1E-6))); + + Assert.Equal(0, JxlGammaCorrect.LinearToSRgb8Direct(-1E-6)); + Assert.Equal(0, JxlGammaCorrect.LinearToSRgb8Direct(-1E6)); + + Assert.True(new TolerantMath(1E-5).AreEqual(1, JxlGammaCorrect.LinearToSRgb8Direct(1 - 1E-6))); + + Assert.Equal(1, JxlGammaCorrect.LinearToSRgb8Direct(1 + 1E-6)); + Assert.Equal(1, JxlGammaCorrect.LinearToSRgb8Direct(1E6)); + } + + [Fact] + public void TestRoundTrip() + { + for (double linear = 0.0; linear <= 1.0; linear += 1E-7) + { + double srgb = JxlGammaCorrect.LinearToSRgb8Direct(linear); + double linear2 = JxlGammaCorrect.SRgb8ToLinearDirect(srgb); + + Assert.True(Math.Abs(linear - linear2) < 2E-13, $"Linear = {linear}, Linear2 = {linear2}"); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/Noise/PhotonNoiseTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/Noise/PhotonNoiseTests.cs new file mode 100644 index 0000000000..4f1eb5e5a9 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Encoder/Noise/PhotonNoiseTests.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.Noise; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing.Encoder.Noise; + +/// +/// Tests for the simulation of photon noise in the JPEG XL encoder routines. +/// +public class PhotonNoiseTests +{ + [Fact] + public void TestPhotonNoiseEncoder() + { + ApproximateFloatComparer comparer = new(1e-6f); + + Assert.Equal( + JxlPhotonNoise.SimulatePhotonNoise(xSize: 6000, ySize: 4000, iso: 100).Lookup, + [0.00259652f, 0.0139648f, 0.00681551f, 0.00632582f, + 0.00694917f, 0.00803922f, 0.00934574f, 0.0107607f], + comparer); + + Assert.Equal( + JxlPhotonNoise.SimulatePhotonNoise(xSize: 6000, ySize: 4000, iso: 800).Lookup, + [0.02077220f, 0.0420923f, 0.01820690f, 0.01439020f, + 0.01293670f, 0.01254030f, 0.01277390f, 0.0134161f], + comparer); + + Assert.Equal( + JxlPhotonNoise.SimulatePhotonNoise(xSize: 6000, ySize: 4000, iso: 6400).Lookup, + [0.1661770f, 0.1691120f, 0.05309080f, 0.03963960f, + 0.03357410f, 0.03001650f, 0.02776740f, 0.0263478f], + comparer); + + Assert.Equal( + JxlPhotonNoise.SimulatePhotonNoise(xSize: 4000, ySize: 3000, iso: 6400).Lookup, + [0.0830886f, 0.1008720f, 0.0367748f, 0.0280305f, 0.0240236f, + 0.0218040f, 0.0205771f, 0.0200058f], + comparer); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Primitives/LehmerCodeTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Primitives/LehmerCodeTests.cs new file mode 100644 index 0000000000..66bd1f4a53 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Primitives/LehmerCodeTests.cs @@ -0,0 +1,88 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Processing; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing.Primitives; + +public class LehmerCodeTests +{ + private sealed class WorkingSet(int maxN) + { + public int PaddedN { get; } = maxN << JxlMath.CeilLog2Nonzero(maxN + 1); + + public uint[] Permutation { get; } = new uint[maxN]; + + public uint[] Temporary { get; } = new uint[maxN]; + + public uint[] LehmerCodes { get; } = new uint[maxN]; + + public uint[] Decoded { get; } = new uint[maxN]; + } + + private static void RoundTrip(int n, WorkingSet ws) + { + Assert.NotEqual(0, n); + int paddedN = 1 << JxlMath.CeilLog2Nonzero(n); + + Rng rng = new(((ulong)n * 65537) + 13); + Assert.True(n < 1 << (sizeof(uint) * 8)); + + Span permutationsSpan = ws.Permutation.AsSpan(); + JxlSimdUtils.Iota(permutationsSpan[..n], 0u); + + for (int rep = 0; rep < 3; rep++) + { + rng.Shuffle(permutationsSpan[..n]); + + Assert.True( + JxlLehmerCode.ComputeLehmerCode(permutationsSpan, ws.Temporary, n, ws.LehmerCodes), + "Could not compute Lehmer code"); + + ws.Temporary.AsSpan()[..(paddedN * 4)].Clear(); + + Assert.True( + JxlLehmerCode.DecodeLehmerCode(ws.LehmerCodes.AsSpan(), ws.Temporary.AsSpan(), n, ws.Decoded.AsSpan()), + "Could not decode Lehmer code"); + + for (int i = 0; i < n; ++i) + { + Assert.Equal(permutationsSpan[i], ws.Decoded[i]); + } + } + } + + private static void RoundTripSizeRange(int begin, int end) + { + Assert.NotEqual(0, begin); + List workingSets = []; + + int numThreads = Environment.ProcessorCount; + + // initialization + for (int i = 0; i < numThreads; i++) + { + workingSets.Add(new WorkingSet(end - 1)); + } + + // loop + Parallel.For( + begin, + end, + () => new WorkingSet(end - 1), + (n, _, workingSet) => + { + RoundTrip(n, workingSet); + return workingSet; + }, + _ => { }); + } + + [Fact] + public void TestLehmerCodes() + { + RoundTripSizeRange(1, 1026); + RoundTripSizeRange(65536, 65540); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/Primitives/XorShiftTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Primitives/XorShiftTests.cs new file mode 100644 index 0000000000..756ed5668e --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/Primitives/XorShiftTests.cs @@ -0,0 +1,396 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives; +using static SixLabors.ImageSharp.Tests.TestImages; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing.Primitives; + +public class XorShiftTests +{ + private const int Vectors = 64; + + private static readonly ulong[][] ExpectedVectors = + [ + [0x6E901576D477CBB1uL, 0xE9E53789195DA2A2uL, 0xB681F6DDA5E0AE99uL, + 0x8EFD18CE21FD6896uL, 0xA898A80DF75CF532uL, 0x50CEB2C9E2DE7E32uL, + 0x3CA7C2FEB25C0DD0uL, 0xA4D0866B80B4D836uL], + [0x8CD6A1E6233D3A26uL, 0x3D4603ADE98B112DuL, 0xDC427AF674019E36uL, + 0xE28B4D230705AC53uL, 0x7297E9BBA88783DDuL, 0x34D3D23CFCD9B41AuL, + 0x5A223615ADBE96B8uL, 0xE5EB529027CFBD01uL], + [ + 0xC1894CF00DFAC6A2uL, 0x18EDF8AE9085E404uL, 0x8E936625296B4CCDuL, + 0x31971EF3A14A899BuL, 0xBE87535FCE0BF26AuL, 0x576F7A752BC6649FuL, + 0xA44CBADCE0C6B937uL, 0x3DBA819BB17A353AuL], + [ + 0x27CE38DFCC1C5EB6uL, 0x920BEB5606340256uL, 0x3986CBC40C9AFC2CuL, + 0xE22BCB3EEB1E191EuL, 0x6E1FCDD3602A8FBAuL, 0x052CB044E5415A29uL, + 0x46266646EFB9ECD7uL, 0x8F44914618D29335uL], + [ + 0xDD30AEDF72A362C5uL, 0xBC1D824E16BB98F4uL, 0x9EA6009C2AA3D2F1uL, + 0xF65C0FBBE17AF081uL, 0x22424D06A8738991uL, 0x8A62763F2B7611D2uL, + 0x2F3E89F722637939uL, 0x84D338BEF50AFD50uL], + [ + 0x00F46494898E2B0BuL, 0x81239DC4FB8E8003uL, 0x414AD93EC5773FE7uL, + 0x791473C450E4110FuL, 0x87F127BF68C959ACuL, 0x6429282D695EF67BuL, + 0x661082E11546CBA8uL, 0x5815D53FA5436BFDuL], + [ + 0xB3DEADAB9BE6E0F9uL, 0xAA1B7B8F7CED0202uL, 0x4C5ED437699D279EuL, + 0xA4471727F1CB39D3uL, 0xE439DA193F802F70uL, 0xF89401BB04FA6493uL, + 0x3B08045A4FE898BAuL, 0x32137BFE98227950uL], + [ + 0xFBAE4A092897FEF3uL, 0x0639F6CE56E71C8EuL, 0xF0AD6465C07F0C1EuL, + 0xFF8E28563361DCE5uL, 0xC2013DB7F86BC6B9uL, 0x8EFCC0503330102FuL, + 0x3F6B767EA5C4DA40uL, 0xB9864B950B2232E1uL], + [ + 0x76EB58DE8E5EC22AuL, 0x9BBBF49A18B32F4FuL, 0xC8405F02B2B2FAB9uL, + 0xC3E122A5F146BC34uL, 0xC90BB046660F5765uL, 0xB933981310DBECCFuL, + 0x5A2A7BFC9126FD1CuL, 0x8BB388C94DF87901uL], + [ + 0x753EB89AD63EF3C3uL, 0xF24AAF40C89D65ADuL, 0x23F68931C1A6AA6DuL, + 0xF47E79BF702C6DD0uL, 0xA3AD113244EE7EAEuL, 0xD42CBEA28F793DC3uL, + 0xD896FCF1820F497CuL, 0x042B86D2818948C1uL], + [ + 0x8F2A4FC5A4265763uL, 0xEC499E6F95EAA10CuL, 0xE3786D4ECCD0DEB5uL, + 0xC725C53D3AC4CC43uL, 0x065A4ACBBF83610EuL, 0x35C61C9FEF167129uL, + 0x7B720AEAA7D70048uL, 0x14206B841377D039uL], + [ + 0xAD27D78BF96055F6uL, 0x5F43B20FF47ADCD4uL, 0xE184C2401E2BF71EuL, + 0x30B263D78990045DuL, 0xC22F00EBFF9BA201uL, 0xAE7F86522B53A562uL, + 0x2853312BC039F0A4uL, 0x868D619E6549C3C8uL], + [ + 0xFD5493D8AE9A8371uL, 0x773D5E224DF61B3BuL, 0x5377C54FBB1A8280uL, + 0xCAD4DE3B8265CAFAuL, 0xCDF3F19C91EBD5F6uL, 0xC8EA0F182D73BD78uL, + 0x220502D593433FF1uL, 0xB81205E612DC31B1uL], + [ + 0x8F32A39EAEDA4C70uL, 0x1D4B0914AA4DAC7FuL, 0x56EF1570F3A8B405uL, + 0x29812CB17404A592uL, 0x97A2AAF69CAE90F2uL, 0x12BF5E02778BBFE5uL, + 0x9D4B55AD42A05FD2uL, 0x06C2BAB5E6086620uL], + [ + 0x8DB4B9648302B253uL, 0xD756AD9E3AEA12C7uL, 0x68709B7F11D4B188uL, + 0x7CC299DDCD707A4BuL, 0x97B860C370A7661DuL, 0xCECD314FC20E64F5uL, + 0x55F412CDFB4C7EC3uL, 0x55EE97591193B525uL], + [ + 0xCF70F3ACA96E6254uL, 0x022FEDECA2E09F46uL, 0x686823DB60AE1ECFuL, + 0xFD36190D3739830EuL, 0x74E1C09027F68120uL, 0xB5883A835C093842uL, + 0x93E1EFB927E9E4E3uL, 0xB2721E249D7E5EBEuL], + [ + 0x69B6E21C44188CB8uL, 0x5D6CFB853655A7AAuL, 0x3E001A0B425A66DCuL, + 0x8C57451103A5138FuL, 0x7BF8B4BE18EAB402uL, 0x494102EB8761A365uL, + 0xB33796A9F6A81F0EuL, 0x10005AB3BCCFD960uL], + [ + 0xB2CF25740AE965DCuL, 0x6F7C1DF7EF53D670uL, 0x648DD6087AC2251EuL, + 0x040955D9851D487DuL, 0xBD550FC7E21A7F66uL, 0x57408F484DEB3AB5uL, + 0x481E24C150B506C1uL, 0x72C0C3EAF91A40D6uL], + [ + 0x1997A481858A5D39uL, 0x539718F4BEF50DC1uL, 0x2EC4DC4787E7E368uL, + 0xFF1CE78879419845uL, 0xE219A93DD6F6DD30uL, 0x85328618D02FEC1AuL, + 0xC86E02D969181B20uL, 0xEBEC8CD8BBA34E6EuL], + [ + 0x28B55088A16CE947uL, 0xDD25AC11E6350195uL, 0xBD1F176694257B1CuL, + 0x09459CCF9FCC9402uL, 0xF8047341E386C4E4uL, 0x7E8E9A9AD984C6C0uL, + 0xA4661E95062AA092uL, 0x70A9947005ED1152uL], + [ + 0x4C01CF75DBE98CCDuL, 0x0BA076CDFC7373B9uL, 0x6C5E7A004B57FB59uL, + 0x336B82297FD3BC56uL, 0x7990C0BE74E8D60FuL, 0xF0275CC00EC5C8C8uL, + 0x6CF29E682DFAD2E9uL, 0xFA4361524BD95D72uL], + [ + 0x631D2A19FF62F018uL, 0x41C43863B985B3FAuL, 0xE052B2267038EFD9uL, + 0xE2A535FAC575F430uL, 0xE004EEA90B1FF5B8uL, 0x42DFE2CA692A1F26uL, + 0x90FB0BFC9A189ECCuL, 0x4484102BD3536BD0uL], + [ + 0xD027134E9ACCA5A5uL, 0xBBAB4F966D476A9BuL, 0x713794A96E03D693uL, + 0x9F6335E6B94CD44AuL, 0xC5090C80E7471617uL, 0x6D9C1B0C87B58E33uL, + 0x1969CE82E31185A5uL, 0x2099B97E87754EBEuL], + [ + 0x60EBAF4ED934350FuL, 0xC26FBF0BA5E6ECFFuL, 0x9E54150F0312EC57uL, + 0x0973B48364ED0041uL, 0x800A523241426CFCuL, 0x03AB5EC055F75989uL, + 0x8CF315935DEEB40AuL, 0x83D3FC0190BD1409uL], + [ + 0x26D35394CF720A51uL, 0xCE9EAA15243CBAFEuL, 0xE2B45FBAF21B29E0uL, + 0xDB92E98EDE73F9E0uL, 0x79B16F5101C26387uL, 0x1AC15959DE88C86FuL, + 0x387633AEC6D6A580uL, 0xA6FC05807BFC5EB8uL], + [ + 0x2D26C8E47C6BADA9uL, 0x820E6EC832D52D73uL, 0xB8432C3E0ED0EE5BuL, + 0x0F84B3C4063AAA87uL, 0xF393E4366854F651uL, 0x749E1B4D2366A567uL, + 0x805EACA43480D004uL, 0x244EBF3AA54400A5uL], + [ + 0xBFDC3763AA79F75AuL, 0x9E3A74CC751F41DBuL, 0xF401302A149DBC55uL, + 0x6B25F7973D7BF7BCuL, 0x13371D34FDBC3DAEuL, 0xC5E1998C8F484DCDuL, + 0x7031B8AE5C364464uL, 0x3847F0C4F3DA2C25uL], + [ + 0x24C6387D2C0F1225uL, 0x77CCE960255C67A4uL, 0x21A0947E497B10EBuL, + 0xBB5DB73A825A9D7EuL, 0x26294A41999E553DuL, 0x3953E0089F87D925uL, + 0x3DAE6E5D4E5EAAFEuL, 0x74B545460341A7AAuL], + [ + 0x710E5EB08A7DB820uL, 0x7E43C4E77CAEA025uL, 0xD4C91529C8B060C1uL, + 0x09AE26D8A7B0CA29uL, 0xAB9F356BB360A772uL, 0xB68834A25F19F6E9uL, + 0x79B8D9894C5734E2uL, 0xC6847E7C8FFD265FuL], + [ + 0x10C4BCB06A5111E6uL, 0x57CB50955B6A2516uL, 0xEF53C87798B6995FuL, + 0xAB38E15BBD8D0197uL, 0xA51C6106EFF73C93uL, 0x83D7F0E2270A7134uL, + 0x0923FD330397FCE5uL, 0xF9DE54EDFE58FB45uL], + [ + 0x07D44833ACCD1A94uL, 0xAAD3C9E945E2F9F3uL, 0xABF4C879B876AA37uL, + 0xF29C69A21B301619uL, 0x2DDCE959111C788BuL, 0x7CEDB48F8AC1729BuL, + 0x93F3BA9A02B659BEuL, 0xF20A87FF17933CBEuL], + [ + 0x8E96EBE93180CFE6uL, 0x94CAA12873937079uL, 0x05F613D9380D4189uL, + 0xBCAB40C1DC79F38AuL, 0x0AD8907B7C61D19EuL, 0x88534E189D103910uL, + 0x2DB2FAABA160AB8FuL, 0xA070E7506B06F15CuL], + [ + 0x6FB1FCDAFFEF87A9uL, 0xE735CF25337A090DuL, 0x172C6EDCEFEF1825uL, + 0x76957EA49EF0542DuL, 0x819BF4CD250F7C49uL, 0xD6FF23E4AD00C4D4uL, + 0xE79673C1EC358FF0uL, 0xAC9C048144337938uL], + [ + 0x4C5387FF258B3AF4uL, 0xEDB68FAEC2CB1AA3uL, 0x02A624E67B4E1DA4uL, + 0x5C44797A38E08AF2uL, 0x36546A70E9411B4BuL, 0x47C17B24D2FD9675uL, + 0x101957AAA020CA26uL, 0x47A1619D4779F122uL], + [ + 0xF84B8BCDC92D9A3CuL, 0x951D7D2C74B3066BuL, 0x7AC287C06EDDD9B2uL, + 0x4C38FC476608D38FuL, 0x224D793B19CB4BCDuL, 0x835A255899BF1A41uL, + 0x4AD250E9F62DB4ABuL, 0xD9B44F4B58781096uL], + [ + 0xABBAF99A8EB5C6B8uL, 0xFB568E900D3A9F56uL, 0x11EDF63D23C5DF11uL, + 0xA9C3011D3FA7C5A8uL, 0xAEDD3CF11AFFF725uL, 0xABCA472B5F1EDD6BuL, + 0x0600B6BB5D879804uL, 0xDB4DE007F22191A0uL], + [ + 0xD76CC9EFF0CE9392uL, 0xF5E0A772B59BA49AuL, 0x7D1AE1ED0C1261B5uL, + 0x79224A33B5EA4F4AuL, 0x6DD825D80C40EA60uL, 0x47FC8E747E51C953uL, + 0x695C05F72888BF98uL, 0x1A012428440B9015uL], + [ + 0xD754DD61F9B772BFuL, 0xC4A2FCF4C0F9D4EBuL, 0x461167CDF67A24A2uL, + 0x434748490EBCB9D4uL, 0x274DD9CDCA5781DEuL, 0x36BAC63BA9A85209uL, + 0x30324DAFDA36B70FuL, 0x337570DB4FE6DAB3uL], + [ + 0xF46CBDD57C551546uL, 0x8E02507E676DA3E3uL, 0xD826245A8C15406DuL, + 0xDFB38A5B71113B72uL, 0x5EA38454C95B16B5uL, 0x28C054FB87ABF3E1uL, + 0xAA2724C0BA1A8096uL, 0xECA83EC980304F2FuL], + [ + 0x6AA76EC294EB3303uL, 0x42D4CDB2A8032E3BuL, 0x7999EDF75DCD8735uL, + 0xB422BFFE696CCDCCuL, 0x8F721461FD7CCDFEuL, 0x148E1A5814FDE253uL, + 0x4DC941F4375EF8FFuL, 0x27B2A9E0EB5B49CFuL], + [ + 0xCEA592EF9343EBE1uL, 0xF7D38B5FA7698903uL, 0x6CCBF352203FEAB6uL, + 0x830F3095FCCDA9C5uL, 0xDBEEF4B81B81C8F4uL, 0x6D7EB9BCEECA5CF9uL, + 0xC58ABB0FBE436C69uL, 0xE4B97E6DB2041A4BuL], + [ + 0x7E40FC772978AF14uL, 0xCDDA4BBAE28354A1uL, 0xE4F993B832C32613uL, + 0xD3608093C68A4B35uL, 0x9A3B60E01BEE3699uL, 0x03BEF248F3288713uL, + 0x70B9294318F3E9B4uL, 0x8D2ABB913B8610DEuL], + [ + 0x37F209128E7D8B2CuL, 0x81D2AB375BD874BCuL, 0xA716A1B7373F7408uL, + 0x0CEE97BEC4706540uL, 0xA40C5FD9CDBC1512uL, 0x73CAF6C8918409E7uL, + 0x45E11BCEDF0BBAA1uL, 0x612C612BFF6E6605uL], + [ + 0xF8ECB14A12D0F649uL, 0xDA683CD7C01BA1ACuL, 0xA2203F7510E124C1uL, + 0x7F83E52E162F3C78uL, 0x77D2BB73456ACADBuL, 0x37FC34FC840BBA6FuL, + 0x3076BC7D4C6EBC1FuL, 0x4F514123632B5FA9uL], + [ + 0x44D789DED935E884uL, 0xF8291591E09FEC9FuL, 0xD9CED2CF32A2E4B7uL, + 0x95F70E1EB604904AuL, 0xDE438FE43C14F6ABuL, 0x4C8D23E4FAFCF8D8uL, + 0xC716910A3067EB86uL, 0x3D6B7915315095D3uL], + [ + 0x3170FDBADAB92095uL, 0x8F1963933FC5650BuL, 0x72F94F00ABECFEABuL, + 0x6E3AE826C6AAB4CEuL, 0xA677A2BF31068258uL, 0x9660CDC4F363AF10uL, + 0xD81A15A152379EF1uL, 0x5D7D285E1080A3F9uL], + [ + 0xDAD5DDFF9A2249B3uL, 0x6F9721D926103FAEuL, 0x1418CBB83FFA349AuL, + 0xE71A30AD48C012B2uL, 0xBE76376C63751132uL, 0x3496467ACA713AE6uL, + 0x8D7EC01369F991A3uL, 0xD8C73A88B96B154EuL], + [ + 0x8B5D9C74AEB4833AuL, 0xF914FB3F867B912FuL, 0xB894EA034936B1DCuL, + 0x8A16D21BE51C4F5BuL, 0x31FF048ED582D98EuL, 0xB95AB2F4DC65B820uL, + 0x04082B9170561AF7uL, 0xA215610A5DC836FAuL], + [ + 0xB2ADE592C092FAACuL, 0x7A1E683BCBF13294uL, 0xC7A4DBF86858C096uL, + 0x3A49940F97BFF316uL, 0xCAE5C06B82C46703uL, 0xC7F413A0F951E2BDuL, + 0x6665E7BB10EB5916uL, 0x86F84A5A94EDE319uL], + [ + 0x4EA199D8FAA79CA3uL, 0xDFA26E5BF1981704uL, 0x0F5E081D37FA4E01uL, + 0x9CB632F89CD675CDuL, 0x4A09DB89D48C0304uL, 0x88142742EA3C7672uL, + 0xAC4F149E6D2E9BDBuL, 0x6D9E1C23F8B1C6C6uL], + [ + 0xD58BE47B92DEC0E9uL, 0x8E57573645E34328uL, 0x4CC094CCB5FB5126uL, + 0x5F1D66AF6FB40E3CuL, 0x2BA15509132D3B00uL, 0x0D6545646120E567uL, + 0x3CF680C45C223666uL, 0x96B28E32930179DAuL], + [ + 0x5900C45853AC7990uL, 0x61881E3E8B7FF169uL, 0x4DE5F835DF2230FFuL, + 0x4427A9E7932F73FFuL, 0x9B641BAD379A8C8DuL, 0xDF271E5BF98F4E5CuL, + 0xDFDA16DB830FF5EEuL, 0x371C7E7CFB89C0E9uL], + [ + 0x4410A8576247A250uL, 0x6AD2DA12B45AC0D9uL, 0x18DFC72AAC85EECCuL, + 0x06FC8BB2A0EF25C8uL, 0xEB287619C85E6118uL, 0x19553ECA67F25A2CuL, + 0x3B9557F1DCEC5BAAuL, 0x7BAD9E8B710D1079uL], + [ + 0x34F365D66BD22B28uL, 0xE6E124B9F10F835DuL, 0x0573C38ABF2B24DCuL, + 0xD32E6AF10A0125AEuL, 0x383590ACEA979519uL, 0x8376ED7A39E28205uL, + 0xF0B7F184DCBDA435uL, 0x062A203390E31794uL], + [ + 0xA2AFFD7E41918760uL, 0x7F90FC1BD0819C86uL, 0x5033C08E5A969533uL, + 0x2707AF5C6D039590uL, 0x57BBD5980F17DF9CuL, 0xD3FE6E61D763268AuL, + 0x9E0A0AE40F335A3BuL, 0x43CF4EB0A99613C5uL], + [ + 0xD4D2A397CE1A7C2EuL, 0x3DF7CE7CC3212DADuL, 0x0880F0D5D356C75AuL, + 0xA8AFC44DD03B1346uL, 0x79263B46C13A29E0uL, 0x11071B3C0ED58E7AuL, + 0xED46DC9F538406BFuL, 0x2C94974F2B94843DuL], + [ + 0xE246E13C39AB5D5EuL, 0xAC1018489D955B20uL, 0x8601B558771852B8uL, + 0x110BD4C06DB40173uL, 0x738FC8A18CCA0EBBuL, 0x6673E09BE0EA76E5uL, + 0x024BC7A0C7527877uL, 0x45E6B4652E2EC34EuL], + [ + 0xD1ED26A1A375CDC8uL, 0xAABC4E896A617CB8uL, 0x0A9C9E8E57D753C6uL, + 0xA3774A75FEB4C30EuL, 0x30B816C01C93E49EuL, 0xF405BABC06D2408CuL, + 0xCC0CE6B4CE788ABCuL, 0x75E7922D0447956CuL], + [ + 0xD07C1676A698BC95uL, 0x5F9AEA4840E2D860uL, 0xD5FC10D58BDF6F02uL, + 0xF190A2AD4BC2EEA7uL, 0x0C24D11F51726931uL, 0xDB646899A16B6512uL, + 0x7BC10670047B1DD8uL, 0x2413A5ABCD45F092uL], + [ + 0x4E66892190CFD923uL, 0xF10162440365EC8EuL, 0x158ACA5A6A2280AEuL, + 0x0D60ED11C0224166uL, 0x7CD2E9A71B9D7488uL, 0x450D7289706AB2A3uL, + 0x88FAE34EC9A0D7DCuL, 0x96FF9103575A97DAuL], + [ + 0x77990FAC6046C446uL, 0xB174B5FB30C76676uL, 0xE352CE3EB56CF82AuL, + 0xC6039B6873A9A082uL, 0xE3F80F3AE333148AuL, 0xB853BA24BA3539B9uL, + 0xE8863E52ECCB0C74uL, 0x309B4CC1092CC245uL], + [ + 0xBC2B70BEE8388D9FuL, 0xE48D92AE22216DCEuL, 0xF15F3BF3E2C15D8FuL, + 0x1DD964D4812D8B24uL, 0xD56AF02FB4665E4CuL, 0x98002200595BD9A3uL, + 0x049246D50BB8FA12uL, 0x1B542DF485B579B9uL], + [ + 0x2347409ADFA8E497uL, 0x36015C2211D62498uL, 0xE9F141F32EB82690uL, + 0x1F839912D0449FB9uL, 0x4E4DCFFF2D02D97CuL, 0xF8A03AB4C0F625C9uL, + 0x0605F575795DAC5CuL, 0x4746C9BEA0DDA6B1uL], + [ + 0xCA5BB519ECE7481BuL, 0xFD496155E55CA945uL, 0xF753B9DBB1515F81uL, + 0x50549E8BAC0F70E7uL, 0x8614FB0271E21C60uL, 0x60C72947EB0F0070uL, + 0xA6511C10AEE742B6uL, 0x48FB48F2CACCB43EuL] + ]; + + [Fact] + public void TestGolden() + { + JxlXorShift rng = new(12345); + Span lanes = stackalloc ulong[JxlXorShift.Generators]; + + for (ulong vector = 0; vector < Vectors; vector++) + { + rng.Fill(lanes); + + for (int i = 0; i < JxlXorShift.Generators; i++) + { + Assert.Equal(ExpectedVectors[(int)vector][i], lanes[i]); + } + } + } + + [Fact] + public void TestSeedChanges() + { + Span lanes = stackalloc ulong[JxlXorShift.Generators]; + const int numberOfSeeds = 16384; + + List first = new(numberOfSeeds); + for (int seed = 0; seed < numberOfSeeds; seed++) + { + JxlXorShift xs128Plus = new((ulong)seed); + xs128Plus.Fill(lanes); + first.Add(lanes[0]); + } + + Assert.Equal(numberOfSeeds, first.Count); + first.Sort(); + first = [.. first.Distinct()]; + Assert.Equal(numberOfSeeds, first.Count); + } + + [Fact] + public void TestFloat() + { +#if ALLOW_JPEGXL_SLOW_TESTS + const int seedMax = 4096; +#else + const int seedMax = 256; +#endif + + Parallel.For(0, seedMax, seed => + { + const int VecCap = 16; + + JxlXorShift rng = new((ulong)seed); + + Span batch64 = stackalloc ulong[JxlXorShift.Generators]; + Span batch32 = stackalloc uint[2 * JxlXorShift.Generators]; + + Span lanes = stackalloc float[Vector.Count]; + + int count = 0; + const int reps = 32000; + double sum = 0.0; + while (count < reps) + { + rng.Fill(batch64); + + MemoryMarshal.Cast(batch64).CopyTo(batch32); + + for (int i = 0; i < VecCap; i += Vector.Count) + { + Vector bits = new(batch32.Slice(i, Vector.Count)); + + // (bits >> 9) | 0x3F800000 + Vector shifted = bits >> 9; + Vector mantissa = shifted | new Vector(0x3F800000); + + Vector rand12 = Vector.AsVectorSingle(mantissa); + + rand12.CopyTo(lanes); + + for (int j = 0; j < Vector.Count; j++) + { + float lane = lanes[j]; + sum += lane; + count++; + + Assert.True(lane < 2.0f); + Assert.True(lane >= 1.0f); + } + } + } + }); + } + + [Fact] + public void TestNotZero() + { +#if ALLOW_JPEGXL_SLOW_TESTS + const int seedMax = 2000; +#else + const int seedMax = 500; +#endif + + Parallel.For(0, seedMax, task => + { + Span lanes = stackalloc ulong[JxlXorShift.Generators]; + JxlXorShift rng = new((ulong)task); + int numZero = 0; + + for (int vectors = 0; vectors < 10000; vectors++) + { + rng.Fill(lanes); + for (int i = 0; i < lanes.Length; i++) + { + if (lanes[i] == 0) + { + numZero++; + } + } + } + + Assert.True(numZero < 1, "There should not be any 0 values produced by the RNG"); + }); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jxl/README.md b/tests/ImageSharp.Tests/Formats/Jxl/README.md new file mode 100644 index 0000000000..4eba637344 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/README.md @@ -0,0 +1,5 @@ +# JPEG XL Tests + +### Preprocessor directives +Enable a ALLOW_JPEGXL_SLOW_TESTS preprocessor directive to allow +slower tests that test the library more extensively. diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Rng.cs b/tests/ImageSharp.Tests/Formats/Jxl/Rng.cs new file mode 100644 index 0000000000..b862171520 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Rng.cs @@ -0,0 +1,87 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl; + +/// +/// Deterministic random number generator used for compatibility +/// with JPEG XL tests. +/// +internal struct Rng +{ + private ulong s0; + private ulong s1; + + public Rng(ulong seed) + { + this.s0 = 0x94D049BB133111EBUL; + this.s1 = 0xBF58476D1CE4E5B9UL + seed; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong Next() + { + ulong s1 = this.s0; + ulong s0 = this.s1; + ulong bits = s1 + s0; + this.s0 = s0; + + s1 ^= s1 << 23; + s1 ^= s0 ^ (s1 >> 18) ^ (s0 >> 5); + + this.s1 = s1; + + return bits; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long UniformI(long begin, long end) => (long)(this.Next() % (ulong)(end - begin)) + begin; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong UniformU(ulong begin, ulong end) => (this.Next() % (end - begin)) + begin; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float UniformF(float begin, float end) + { + uint u = (uint)(this.Next() >> (64 - 23)) | 0x3F800000u; + float f = BitConverter.UInt32BitsToSingle(u); + + return ((end - begin) * (f - 1.0f)) + begin; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Bernoulli(float p) => this.UniformF(0, 1) < p; + + internal readonly struct GeometricDistribution + { + public readonly float Value { get; } + + public GeometricDistribution(float value) => this.Value = value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GeometricDistribution Make(float p) => new(1.0f / MathF.Log(1.0f - p)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint Geometric(in GeometricDistribution dist) + { + float f = this.UniformF(0, 1); + float invLog1mp = dist.Value; + + float log = MathF.Log(1.0f - f) * invLog1mp; + + return (uint)log; + } + + public void Shuffle(Span span) + { + for (nuint i = 0; i + 1 < (nuint)span.Length; i++) + { + nuint a = (nuint)this.UniformU(i, (nuint)span.Length); + RuntimeUtility.Swap(ref span[(int)a], ref span[(int)i]); + } + } +} From eacb94302dbd15ce49076a3dc2badbd56ad5acc8 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 4 Sep 2026 22:02:46 +0400 Subject: [PATCH 139/142] Reduce errors --- .../Jxl/Processing/Blending/JxlAlphaHelper.cs | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs index b3c890f9a2..c9a5482fbb 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs @@ -22,23 +22,20 @@ public static void PerformAlphaBlending( 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 fgR = foreground.R; + ReadOnlySpan fgG = foreground.G; + ReadOnlySpan fgB = foreground.B; + ReadOnlySpan fgA = foreground.A; - ReadOnlySpan bgR = background.R.Span; - ReadOnlySpan bgG = background.G.Span; - ReadOnlySpan bgB = background.B.Span; - ReadOnlySpan bgA = background.A.Span; + ReadOnlySpan bgR = background.R; + ReadOnlySpan bgG = background.G; + ReadOnlySpan bgB = background.B; + ReadOnlySpan bgA = background.A; - Span outR = output.R.Span; - Span outG = output.G.Span; - Span outB = output.B.Span; - Span outA = output.A.Span; + Span outR = output.R; + Span outG = output.G; + Span outB = output.B; + Span outA = output.A; if (alphaIsPremultiplied) { From 4e929f3ca877b3adcf1fb93431163d8f5ca8fce8 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Fri, 4 Sep 2026 22:03:10 +0400 Subject: [PATCH 140/142] Move comment --- .../Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs index c9a5482fbb..1393e5cfa6 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Blending/JxlAlphaHelper.cs @@ -5,9 +5,9 @@ namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Blending; +// TODO: SIMD support 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 From 58c803edcd9e5f8e104666737e71f6e10134733b Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 5 Sep 2026 00:07:40 +0400 Subject: [PATCH 141/142] Complete Butteraugli Just the tests for Butteraugli are remaining --- .../Jxl/Processing/Butteraugli/Butteraugli.cs | 558 ++++++++++++++---- .../Butteraugli/ButteraugliBlurTemp.cs | 24 + .../Butteraugli/ButteraugliComparator.cs | 284 ++++++++- .../Butteraugli/ButteraugliPsychoImage.cs | 23 + .../Processing/Image/JxlImageOperations.cs | 9 + .../Formats/Jxl/Processing/JxlToc.cs | 11 +- 6 files changed, 776 insertions(+), 133 deletions(-) create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliBlurTemp.cs create mode 100644 src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs index 99f1cf2d88..f65a66df92 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/Butteraugli.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Jxl.Memory; 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.Butteraugli; @@ -43,6 +44,26 @@ internal static class Butteraugli private const float GlobalScale = 1.0f / InternalGoodQualityThreshold; +#pragma warning disable // Just indentation warnings + /// + /// Underlying data for . + /// + private static readonly float[,] HeatmapData = + { + {0, 0, 0}, {0, 0, 1}, + {0, 1, 1}, {0, 1, 0}, // Good level + {1, 1, 0}, {1, 0, 0}, // Bad level + {1, 0, 1}, {0.5f, 0.5f, 1.0f}, + {1.0f, 0.5f, 0.5f}, // Pastel colors for the very bad quality range. + {1.0f, 1.0f, 0.5f}, {1, 1, 1}, + {1, 1, 1}, // Last color repeated to have a solid range of white. + }; +#pragma warning restore + + private static readonly DenseMatrix Heatmap; + + static Butteraugli() => Heatmap = new(HeatmapData); + public static ReadOnlySpan Wmul => [ 400.0f, 1.50815703118f, 0f, @@ -72,7 +93,7 @@ public static ReadOnlySpan ComputeKernel(float sigma) } public static void ConvolveBorderColumn( - JxlImageF input, + JxlPlane input, ReadOnlySpan kernel, int x, Span rowOut) @@ -83,6 +104,7 @@ public static void ConvolveBorderColumn( int maxX = Math.Min(input.XSize - 1, x + offset); float weight = 0.0f; + for (int j = minX; j <= maxX; j++) { weight += kernel[j - x + offset]; @@ -106,9 +128,9 @@ public static void ConvolveBorderColumn( } public static bool ConvolutionWithTranspose( - JxlImageF input, + JxlPlane input, ReadOnlySpan kernel, - JxlImageF output) + JxlPlane output) { if (output.XSize != input.YSize) { @@ -286,11 +308,11 @@ public static bool ConvolutionWithTranspose( } private static bool Blur( - JxlImageF input, + Configuration configuration, + JxlPlane input, float sigma, - in ButteraugliParameters parameters, ButteraugliBlurTemp temp, - JxlImageF output) + JxlPlane output) { ReadOnlySpan kernel = ComputeKernel(sigma); @@ -323,10 +345,7 @@ private static bool Blur( return true; } - if (!temp.GetTransposed(input, out JxlImageF tempT)) - { - return false; - } + JxlPlane tempT = temp.GetTransposed(configuration, input); if (!ConvolutionWithTranspose(input, kernel, tempT)) { @@ -447,9 +466,9 @@ public static void XybLowFrequencyToValues(JxlImage3F xybLf) } } - public static bool SuppressXByY(JxlImageF inY, JxlImageF inOutX) + public static bool SuppressXByY(JxlPlane inY, JxlPlane inOutX) { - if (!SameSize(inOutX, inY)) + if (!JxlImageOperations.SameSize(inOutX, inY)) { return false; } @@ -506,7 +525,7 @@ public static void Subtract(JxlPlane a, JxlPlane b, JxlPlane hf, - BlurTemp blurTemp) + ref InlineArray2> hf, + ButteraugliBlurTemp blurTemp) { const float sigmaHf = 3.22489901262f; int xSize = mf.XSize; int ySize = mf.YSize; - hf[0] = new JxlImageF(configuration, xSize, ySize); - hf[1] = new JxlImageF(configuration, xSize, ySize); + hf[0] = JxlPlane.Create(configuration, xSize, ySize); + hf[1] = JxlPlane.Create(configuration, xSize, ySize); int lanes = Vector.Count; @@ -558,7 +576,7 @@ public static bool SeparateMfAndHf( { if (i == 2) { - if (!Blur(mf.Plane(i), sigmaHf, parameters, blurTemp, mf.Plane(i))) + if (!Blur(configuration, mf.Plane(i), sigmaHf, blurTemp, mf.Plane(i))) { return false; } @@ -578,7 +596,7 @@ public static bool SeparateMfAndHf( } } - if (!Blur(mf.Plane(i), sigmaHf, parameters, blurTemp, mf.Plane(i))) + if (!Blur(configuration, mf.Plane(i), sigmaHf, blurTemp, mf.Plane(i))) { return false; } @@ -631,18 +649,18 @@ public static bool SeparateMfAndHf( } public static bool SeparateHFAndUHF( - in ButteraugliParameters parameters, - JxlImageF[] hf, - JxlImageF[] uhf, - JxlBlurTemp blurTemp) + Configuration configuration, + InlineArray2> hf, + InlineArray2> uhf, + ButteraugliBlurTemp blurTemp) { const float sigmaUhf = 1.56416327805f; int xSize = hf[0].XSize; int ySize = hf[0].YSize; - uhf[0] = new JxlImageF(xSize, ySize); - uhf[1] = new JxlImageF(xSize, ySize); + uhf[0] = JxlPlane.Create(configuration, xSize, ySize); + uhf[1] = JxlPlane.Create(configuration, xSize, ySize); int lanes = Vector.Count; @@ -659,7 +677,7 @@ public static bool SeparateHFAndUHF( } } - if (!Blur(hf[i], sigmaUhf, parameters, blurTemp, hf[i])) + if (!Blur(configuration, hf[i], sigmaUhf, blurTemp, hf[i])) { return false; } @@ -727,34 +745,36 @@ public static bool SeparateHFAndUHF( return true; } - public static void DeallocateHFAndUHF(InlineArray2 hf, InlineArray2 uhf) + public static void DeallocateHFAndUHF(InlineArray2> hf, InlineArray2> uhf) { for (int i = 0; i < 2; i++) { - hf[i] = new JxlImageF(); - uhf[i] = new JxlImageF(); + hf[i].Dispose(); + uhf[i].Dispose(); + + hf[i] = new JxlPlane(); + uhf[i] = new JxlPlane(); } } public static bool SeparateFrequencies( Configuration configuration, - in ButteraugliParameters parameters, - BlurTemp blurTemp, + ButteraugliBlurTemp blurTemp, JxlImage3F xyb, - PsychoImage ps) + ButteraugliPsychoImage ps) { - ps.Lf = JxlImage3F.Create( + ps.Lf = new JxlImage3F( configuration, xyb.XSize, xyb.YSize); - ps.Mf = JxlImage3F.Create( + ps.Mf = new JxlImage3F( configuration, xyb.XSize, xyb.YSize); if (!SeparateLFAndMF( - parameters, + configuration, xyb, ps.Lf, ps.Mf, @@ -764,16 +784,16 @@ public static bool SeparateFrequencies( } if (!SeparateMfAndHf( - parameters, + configuration, ps.Mf, - ps.Hf, + ref ps.Hf, blurTemp)) { return false; } if (!SeparateHFAndUHF( - parameters, + configuration, ps.Hf, ps.Uhf, blurTemp)) @@ -1177,7 +1197,7 @@ static Vector Load(ReadOnlySpan row, int index) [MethodImpl(MethodImplOptions.AggressiveInlining)] private static float PaddedMaltaUnit( - JxlImageF diffs, + JxlPlane diffs, int x0, int y0, bool isLF) @@ -1238,17 +1258,43 @@ private static float PaddedMaltaUnit( public static bool MaltaDiffMap( bool isLf, - JxlImageF lum0, - JxlImageF lum1, + JxlPlane lum0, + JxlPlane lum1, + float w0Gt1, + float w0Lt1, + float norm1, + JxlPlane diffs, + JxlPlane blockDiffAc) + { + if (isLf) + { + const float len = 3.75f; + const float mulli = 0.39905817637f; + + return MaltaDiffMap(true, lum0, lum1, w0Gt1, w0Lt1, norm1, len, mulli, diffs, blockDiffAc); + } + else + { + const float len = 3.75f; + const float mulli = 0.611612573796f; + + return MaltaDiffMap(false, lum0, lum1, w0Gt1, w0Lt1, norm1, len, mulli, diffs, blockDiffAc); + } + } + + public static bool MaltaDiffMap( + bool isLf, + JxlPlane lum0, + JxlPlane lum1, float w0Gt1, float w0Lt1, float norm1, float len, float mulli, - JxlImageF diffs, - JxlImageF blockDiffAc) + JxlPlane diffs, + JxlPlane blockDiffAc) { - if (!SameSize(lum0, lum1) || !SameSize(lum0, diffs)) + if (!JxlImageOperations.SameSize(lum0, lum1) || !JxlImageOperations.SameSize(lum0, diffs)) { return false; } @@ -1400,13 +1446,13 @@ public static bool MaltaDiffMap( } public static bool MaltaDiffMapLf( - JxlImageF lum0, - JxlImageF lum1, + JxlPlane lum0, + JxlPlane lum1, float w0Gt1, float w0Lt1, float norm1, - JxlImageF diffs, - JxlImageF blockDiffAc) + JxlPlane diffs, + JxlPlane blockDiffAc) { const float len = 3.75f; const float mulli = 0.611612573796f; @@ -1425,9 +1471,9 @@ public static bool MaltaDiffMapLf( } public static void CombineChannelsForMasking( - InlineArray2 hf, - InlineArray2 uhf, - JxlImageF output) + InlineArray2> hf, + InlineArray2> uhf, + JxlPlane output) { // Only X and Y components are involved in masking. ReadOnlySpan muls = @@ -1614,8 +1660,7 @@ public static bool Mask( Configuration configuration, JxlImageF mask0, JxlImageF mask1, - in ButteraugliParameters parameters, - JxlBlurTemp blurTemp, + ButteraugliBlurTemp blurTemp, JxlImageF? diffAc, out JxlImageF mask) { @@ -1636,14 +1681,14 @@ public static bool Mask( DiffPrecompute(mask0, mul, bias, diff0); DiffPrecompute(mask1, mul, bias, diff1); - if (!Blur(diff0, radius, parameters, blurTemp, blurred0)) + if (!Blur(configuration, diff0, radius, blurTemp, blurred0)) { return false; } FuzzyErosion(blurred0, diff0); - if (!Blur(diff1, radius, parameters, blurTemp, blurred1)) + if (!Blur(configuration, diff1, radius, blurTemp, blurred1)) { return false; } @@ -1661,7 +1706,7 @@ public static bool Mask( { maskRow[x] = diff0Row[x]; - if (diffRow != null) + if (diffRow.Length > 0) { const float maskToErrorMul = 10.0f; float diff = blur0Row[x] - blur1Row[x]; @@ -1673,16 +1718,15 @@ public static bool Mask( return true; } - public static bool MaskPsychoImage( + public static bool MaskButteraugliPsychoImage( Configuration configuration, ButteraugliPsychoImage pi0, ButteraugliPsychoImage pi1, int width, int height, - in ButteraugliParameters parameters, - BlurTemp blurTemp, + ButteraugliBlurTemp blurTemp, JxlImageF mask, - JxlImageF? diffAc) + out JxlImageF? diffAc) { JxlImageF mask0 = new(configuration, width, height); JxlImageF mask1 = new(configuration, width, height); @@ -1698,12 +1742,12 @@ public static bool MaskPsychoImage( mask1); return Mask( + configuration, mask0, mask1, - parameters, blurTemp, mask, - diffAc); + out diffAc); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -1741,11 +1785,11 @@ public static float MaskColor( public static bool CombineChannelsToDiffmap( JxlImageF mask, JxlImage3F blockDiffDc, - JxlImage3F blockDiffAc, + JxlImage3 blockDiffAc, float xmul, JxlImageF result) { - if (!SameSize(mask, result)) + if (!JxlImageOperations.SameSize(mask, result)) { return false; } @@ -1786,10 +1830,10 @@ public static bool CombineChannelsToDiffmap( } public static void L2Diff( - JxlImageF i0, - JxlImageF i1, + JxlPlane i0, + JxlPlane i1, float w, - JxlImageF diffmap) + JxlPlane diffmap) { if (w == 0) { @@ -1811,10 +1855,10 @@ public static void L2Diff( } public static void SetL2Diff( - JxlImageF i0, - JxlImageF i1, + JxlPlane i0, + JxlPlane i1, float w, - JxlImageF diffmap) + JxlPlane diffmap) { if (w == 0) { @@ -1836,11 +1880,11 @@ public static void SetL2Diff( } public static void L2DiffAsymmetric( - JxlImageF i0, - JxlImageF i1, + JxlPlane i0, + JxlPlane i1, float w0gt1, float w0lt1, - JxlImageF diffmap) + JxlPlane diffmap) { if (w0gt1 == 0 && w0lt1 == 0) { @@ -1942,11 +1986,27 @@ public static void OpsinAbsorbance( } } + // A simple HDR-compatible gamma function + private static Vector Gamma(Vector v) + { + Vector kRetMul = Vector.Create(19.245013259874995f * JxlMath.InverseLog2E); + Vector kRetAdd = Vector.Create(-23.16046239805755f); + + // if (value < 0) value = 0; + v = Vector.ConditionalSelect(Vector.LessThan(v, Vector.Zero), Vector.Zero, v); + + Vector biased = v + Vector.Create(9.9710635769299145f); + Vector log = Vector.Log2(biased); + + return (kRetMul * log) + kRetAdd; + } + public static bool OpsinDynamicsImage( + Configuration configuration, JxlImage3F rgb, in ButteraugliParameters parameters, JxlImage3F blurred, - BlurTemp blurTemp, + ButteraugliBlurTemp blurTemp, JxlImage3F xyb) { if (blurred == null) @@ -1954,19 +2014,19 @@ public static bool OpsinDynamicsImage( return false; } - const double sigma = 1.2; + const float sigma = 1.2f; - if (!Blur(rgb.Plane(0), sigma, parameters, blurTemp, blurred.Plane(0))) + if (!Blur(configuration, rgb.Plane(0), sigma, blurTemp, blurred.Plane(0))) { return false; } - if (!Blur(rgb.Plane(1), sigma, parameters, blurTemp, blurred.Plane(1))) + if (!Blur(configuration, rgb.Plane(1), sigma, blurTemp, blurred.Plane(1))) { return false; } - if (!Blur(rgb.Plane(2), sigma, parameters, blurTemp, blurred.Plane(2))) + if (!Blur(configuration, rgb.Plane(2), sigma, blurTemp, blurred.Plane(2))) { return false; } @@ -2058,16 +2118,16 @@ public static bool ButteraugliDiffmapInPlace( int xSize = image0.XSize; int ySize = image0.YSize; - using var blurTemp = new JxlBlurTemp(); + using ButteraugliBlurTemp blurTemp = new(); using (JxlImage3F temp = new(configuration, xSize, ySize)) { - if (!OpsinDynamicsImage(image0, parameters, temp, blurTemp, image0)) + if (!OpsinDynamicsImage(configuration, image0, parameters, temp, blurTemp, image0)) { return false; } - if (!OpsinDynamicsImage(image1, parameters, temp, blurTemp, image1)) + if (!OpsinDynamicsImage(configuration, image1, parameters, temp, blurTemp, image1)) { return false; } @@ -2080,12 +2140,12 @@ public static bool ButteraugliDiffmapInPlace( using (JxlImage3F lf0 = new(configuration, xSize, ySize)) using (JxlImage3F lf1 = new(configuration, xSize, ySize)) { - if (!SeparateLFAndMF(parameters, image0, lf0, image0, blurTemp)) + if (!SeparateLFAndMF(configuration, image0, lf0, image0, blurTemp)) { return false; } - if (!SeparateLFAndMF(parameters, image1, lf1, image1, blurTemp)) + if (!SeparateLFAndMF(configuration, image1, lf1, image1, blurTemp)) { return false; } @@ -2100,15 +2160,15 @@ public static bool ButteraugliDiffmapInPlace( } } - InlineArray2 hf0 = default; - InlineArray2 hf1 = default; + InlineArray2> hf0 = default; + InlineArray2> hf1 = default; - if (!SeparateMfAndHf(parameters, image0, hf0, blurTemp)) + if (!SeparateMfAndHf(configuration, image0, ref hf0, blurTemp)) { return false; } - if (!SeparateMfAndHf(parameters, image1, hf1, blurTemp)) + if (!SeparateMfAndHf(configuration, image1, ref hf1, blurTemp)) { return false; } @@ -2158,15 +2218,15 @@ public static bool ButteraugliDiffmapInPlace( image0.Dispose(); image1.Dispose(); - InlineArray2 uhf0 = default; - InlineArray2 uhf1 = default; + InlineArray2> uhf0 = default; + InlineArray2> uhf1 = default; - if (!SeparateHFAndUHF(parameters, hf0, uhf0, blurTemp)) + if (!SeparateHFAndUHF(configuration, hf0, uhf0, blurTemp)) { return false; } - if (!SeparateHFAndUHF(parameters, hf1, uhf1, blurTemp)) + if (!SeparateHFAndUHF(configuration, hf1, uhf1, blurTemp)) { return false; } @@ -2175,7 +2235,7 @@ public static bool ButteraugliDiffmapInPlace( using (JxlImageF diffs = new(configuration, xSize, ySize)) { - MaltaDiffMap( + _ = MaltaDiffMap( false, uhf0[1], uhf1[1], @@ -2185,19 +2245,19 @@ public static bool ButteraugliDiffmapInPlace( diffs, blockDiffAc); - MaltaDiffMap( + _ = MaltaDiffMap( false, uhf0[0], uhf1[0], - wUhfMaltaX * hfAsymmetry, - wUhfMaltaX / hfAsymmetry, - norm1UhfX, + WUhfMaltaX * hfAsymmetry, + WUhfMaltaX / hfAsymmetry, + Norm1UhfX, diffs, blockDiffAc); float sqrtAsym = MathF.Sqrt(hfAsymmetry); - MaltaDiffMap( + _ = MaltaDiffMap( true, hf0[1], hf1[1], @@ -2207,7 +2267,7 @@ public static bool ButteraugliDiffmapInPlace( diffs, blockDiffAc); - MaltaDiffMap( + _ = MaltaDiffMap( true, hf0[0], hf1[0], @@ -2239,15 +2299,15 @@ public static bool ButteraugliDiffmapInPlace( DeallocateHFAndUHF(hf0, uhf0); DeallocateHFAndUHF(hf1, uhf1); - if (!Mask(mask0, mask1, parameters, blurTemp, mask, blockDiffAc)) + if (!Mask(configuration, mask0, mask1, blurTemp, mask, out JxlImageF newBlockDiffAc)) { return false; } for (int y = 0; y < ySize; y++) { - ReadOnlySpan dc = blockDiffDc.GetRow(y); - ReadOnlySpan ac = blockDiffAc.GetRow(y); + ReadOnlySpan dc = newBlockDiffAc.GetRow(y); + ReadOnlySpan ac = newBlockDiffAc.GetRow(y); Span output = diffmap.GetRow(y); ReadOnlySpan maskRow = mask.GetRow(y); @@ -2255,10 +2315,7 @@ public static bool ButteraugliDiffmapInPlace( { float m = maskRow[x]; - output[x] = - MathF.Sqrt( - (dc[x] * (float)MaskDcY(m)) + - (ac[x] * (float)MaskY(m))); + output[x] = MathF.Sqrt((dc[x] * (float)MaskDcY(m)) + (ac[x] * (float)MaskY(m))); } } @@ -2272,7 +2329,7 @@ public static JxlImage3F SubSample2x(Configuration configuration, JxlImage3F inp int xs = (input.XSize + 1) / 2; int ys = (input.YSize + 1) / 2; - JxlImage3F retval = new(Configuration, xs, ys); + JxlImage3F retval = new(configuration, xs, ys); for (int c = 0; c < 3; ++c) { @@ -2293,8 +2350,7 @@ public static JxlImage3F SubSample2x(Configuration configuration, JxlImage3F inp for (int x = 0; x < input.XSize; ++x) { - retval.PlaneRow(c, y / 2)[x / 2] += - 0.25f * srcRow[x]; + retval.PlaneRow(c, y / 2)[x / 2] += 0.25f * srcRow[x]; } } @@ -2335,4 +2391,286 @@ public static void AddSupersampled2x(JxlImageF src, float w, JxlImageF dest) } } } + + private static void ScoreToRgb(float score, float goodThreshold, float badThreshold, ref InlineArray3 rgb) + { + if (score < goodThreshold) + { + score = (score / goodThreshold) * 0.3f; + } + else if (score < badThreshold) + { + score = 0.3f + ((score - goodThreshold) / (badThreshold - goodThreshold) * 0.15f); + } + else + { + score = 0.45f + ((score - badThreshold) / (badThreshold * 12) * 0.5f); + } + + int tableSize = HeatmapData.GetLength(0); + score = Math.Clamp(score * (tableSize - 1), 0f, tableSize - 2); + + int ix = (int)score; + ix = Math.Clamp(ix, 0, tableSize - 2); // handle NaN + float mix = score - ix; + + for (int i = 0; i < 3; ++i) + { + float v = (mix * Heatmap[ix + 1, i]) + ((1 - mix) * Heatmap[ix, i]); + rgb[i] = MathF.Pow(v, 0.5f); + } + } + + public static JxlImage3F CreateHeatMapImage(Configuration configuration, JxlImageF distmap, float goodThreshold, float badThreshold) + { + // Do not dispose (this is the return value; it is caller's responsibility to dispose this + // or keep it) + JxlImage3F heatmap = new(configuration, distmap.XSize, distmap.YSize); + + for (int y = 0; y < distmap.YSize; y++) + { + Span row_distmap = distmap.GetRow(y); + Span row_h0 = heatmap.PlaneRow(0, y); + Span row_h1 = heatmap.PlaneRow(1, y); + Span row_h2 = heatmap.PlaneRow(2, y); + + for (int x = 0; x < distmap.XSize; ++x) + { + float d = row_distmap[x]; + InlineArray3 rgb = default; + ScoreToRgb(d, goodThreshold, badThreshold, ref rgb); + row_h0[x] = rgb[0]; + row_h1[x] = rgb[1]; + row_h2[x] = rgb[2]; + } + } + + return heatmap; + } + + public static float ButteraugliFuzzyInverse(float seek) + { + float pos = 0f; + + for (float range = 1.0f; range >= 1e-10f; range *= 0.5f) + { + float cur = ButteraugliFuzzyClass(pos); + + if (cur < seek) + { + pos -= range; + } + else + { + pos += range; + } + } + + // Normalization (pos) can be printed if seek == 1.0, for example + // when debugging. + return pos; + } + + public static float ButteraugliFuzzyClass(float score) + { + const float fuzzyWidthUp = 4.8f; + const float fuzzyWidthDown = 4.8f; + const float m0 = 2.0f; + const float scaler = 0.7777f; + + float val; + + if (score < 1.0) + { + val = m0 / (1.0f + MathF.Exp((score - 1.0f) * fuzzyWidthDown)); + val -= 1.0f; + val *= 2.0f - scaler; + val += scaler; + } + else + { + val = m0 / (1.0f + MathF.Exp((score - 1.0f) * fuzzyWidthUp)); + val *= scaler; + } + + return val; + } + + public static bool ButteraugliInterfaceInPlace(Configuration configuration, JxlImage3F rgb0, JxlImage3F rgb1, ButteraugliParameters parameters, JxlImageF diffmap, out double diffvalue) + { + diffvalue = 0; + + int xsize = rgb0.XSize; + int ysize = rgb0.YSize; + + if ((xsize & ysize) == 0) // equivalent to xsize == 0 || ysize == 0, minus one branch + { + throw new InvalidOperationException("Zero-sized image"); + } + + if (!JxlImageOperations.SameSize(rgb0, rgb1)) + { + throw new InvalidOperationException("Size mismatch"); + } + + const int max = 8; + + if (xsize < max || ysize < max) + { + bool ok = ButteraugliDiffmapSmall(configuration, max, rgb0, rgb1, parameters, out diffmap); + diffvalue = ButteraugliScoreFromDiffmap(diffmap, parameters); + return ok; + } + + JxlImageF subDiffmap = new(); + + if (xsize >= 15 && ysize >= 15) + { + using JxlImage3F rgb0Sub = SubSample2x(configuration, rgb0); + using JxlImage3F rgb1Sub = SubSample2x(configuration, rgb1); + + if (!ButteraugliDiffmapInPlace(configuration, rgb0Sub, rgb1Sub, parameters, diffmap)) + { + return false; + } + } + + if (!ButteraugliDiffmapInPlace(configuration, rgb0, rgb1, parameters, diffmap)) + { + return false; + } + + if (xsize >= 15 && ysize >= 15) + { + AddSupersampled2x(subDiffmap, 0.5f, diffmap); + } + + diffvalue = ButteraugliScoreFromDiffmap(diffmap, parameters); + return true; + } + + public static bool ButteraugliInterface(Configuration configuration, JxlImage3F rgb0, JxlImage3F rgb1, ButteraugliParameters parameters, JxlImageF diffmap, out double diffValue) + { + diffValue = 0; + + if (!ButteraugliDiffmap(configuration, rgb0, rgb1, parameters, out diffmap)) + { + return false; + } + + diffValue = ButteraugliScoreFromDiffmap(diffmap, parameters); + return true; + } + + public static bool ButteraugliInterface(Configuration configuration, JxlImage3F rgb0, JxlImage3F rgb1, float hfAsymmetry, float xmul, JxlImageF diffmap, out double diffValue) + { + ButteraugliParameters parameters = new() + { + HfAsymmetry = hfAsymmetry, + XMultiplier = xmul + }; + + return ButteraugliInterface(configuration, rgb0, rgb1, parameters, diffmap, out diffValue); + } + + public static bool ButteraugliDiffmap(Configuration configuration, JxlImage3F rgb0, JxlImage3F rgb1, ButteraugliParameters parameters, out JxlImageF diffmap) + { + diffmap = new(); + + int xsize = rgb0.XSize; + int ysize = rgb0.YSize; + + if ((xsize & ysize) == 0) // equivalent to xsize == 0 || ysize == 0, minus one branch + { + throw new InvalidOperationException("Zero-sized image"); + } + + if (!JxlImageOperations.SameSize(rgb0, rgb1)) + { + throw new InvalidOperationException("Size mismatch"); + } + + const int max = 8; + + if (xsize < max || ysize < max) + { + if (!ButteraugliDiffmapSmall(configuration, max, rgb0, rgb1, parameters, out diffmap)) + { + return false; + } + } + + ButteraugliComparator butteraugli = ButteraugliComparator.Make(configuration, rgb0, parameters); + + return butteraugli.Diffmap(configuration, rgb1, diffmap); + } + + public static bool ButteraugliDiffmapSmall(Configuration configuration, int max, JxlImage3F rgb0, JxlImage3F rgb1, ButteraugliParameters parameters, out JxlImageF diffmap) + { + int xsize = rgb0.XSize; + int ysize = rgb0.YSize; + + int xborder = xsize < max ? (max - xsize) / 2 : 0; + int yborder = ysize < max ? (max - ysize) / 2 : 0; + int xscaled = Math.Max(max, xsize); + int yscaled = Math.Max(max, ysize); + + using JxlImage3F scaled0 = new(configuration, xscaled, yscaled); + using JxlImage3F scaled1 = new(configuration, xscaled, yscaled); + + for (int i = 0; i < 3; ++i) + { + for (int y = 0; y < yscaled; y++) + { + for (int x = 0; x < xscaled; x++) + { + int x2 = Math.Min(xsize - 1, x > xborder ? x - xborder : 0); + int y2 = Math.Min(ysize - 1, y > yborder ? y - yborder : 0); + + scaled0.PlaneRow(i, y)[x] = rgb0.PlaneRow(i, y2)[x2]; + scaled1.PlaneRow(i, y)[x] = rgb1.PlaneRow(i, y2)[x2]; + } + } + } + + JxlImageF diffmapScaled = new(); + bool ok = ButteraugliDiffmap(configuration, scaled0, scaled1, parameters, out diffmapScaled); + + diffmap = new(configuration, xsize, ysize); + + for (int y = 0; y < ysize; y++) + { + Span diffmapRow = diffmap.GetRow(y); + Span diffmapScaledRow = diffmapScaled.GetRow(y + yborder); + + for (int x = 0; x < xsize; x++) + { + diffmapRow[x] = diffmapScaledRow[x + xborder]; + } + } + + return ok; + } + + public static float ButteraugliScoreFromDiffmap(JxlImageF diffmap, ButteraugliParameters parameters) + { + float retval = 0.0f; + + for (int y = 0; y < diffmap.YSize; ++y) + { + Span row = diffmap.GetRow(y); + for (int x = 0; x < diffmap.XSize; ++x) + { + retval = Math.Max(retval, row[x]); + } + } + + return retval; + } + + public static bool MaltaDiffMap(JxlPlane lum0, JxlPlane lum1, float w0Gt1, float w0Lt1, float norm1, JxlPlane diffs, JxlImage3 blockDiffAc, int c) + => MaltaDiffMap(isLf: false, lum0, lum1, w0Gt1, w0Lt1, norm1, diffs, blockDiffAc.Plane(c)); + + public static bool MaltaDiffMapLf(JxlPlane lum0, JxlPlane lum1, float w0Gt1, float w0Lt1, float norm1, JxlPlane diffs, JxlImage3 blockDiffAc, int c) + => MaltaDiffMap(isLf: true, lum0, lum1, w0Gt1, w0Lt1, norm1, diffs, blockDiffAc.Plane(c)); } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliBlurTemp.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliBlurTemp.cs new file mode 100644 index 0000000000..26aa8372ce --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliBlurTemp.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jxl.Memory; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; + +internal sealed class ButteraugliBlurTemp : IDisposable +{ + public JxlPlane TransposedTemp { get; set; } = new(); + + public JxlPlane GetTransposed(Configuration configuration, JxlPlane input) + { + if (this.TransposedTemp.XSize == 0) + { + // Yes, YSize and XSize are swapped + this.TransposedTemp = JxlPlane.Create(configuration, input.YSize, input.XSize); + } + + return this.TransposedTemp; + } + + public void Dispose() => this.TransposedTemp.Dispose(); +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs index be5837534b..d290268da6 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliComparator.cs @@ -1,46 +1,300 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Jxl.Memory; using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; -internal sealed class ButteraugliComparator +internal class ButteraugliComparator : IDisposable { - private int xSize; - private int ySize; + private readonly int xSize; + private readonly int ySize; private ButteraugliParameters parameters; - private readonly ButteraugliPsychoImage pi0; - private readonly JxlImage3F temp; - private bool tempInUse; - private readonly ButteraugliBlurTemp blurTemp; + private readonly ButteraugliPsychoImage pi0 = new(); + private JxlImage3F? temp; + private readonly ButteraugliBlurTemp blurTemp = new(); + private ButteraugliComparator? sub; + + public ButteraugliComparator(int xsize, int ysize, ButteraugliParameters parameters) + { + this.xSize = xsize; + this.ySize = ysize; + this.parameters = parameters; + } + + public JxlImage3F Temp => this.temp ??= new(); + + public static ButteraugliComparator Make(Configuration configuration, JxlImage3F rgb0, ButteraugliParameters parameters) + { + int xSize = rgb0.XSize; + int ySize = rgb0.YSize; + + ButteraugliComparator result = new(xSize, ySize, parameters) + { + temp = new JxlImage3F(configuration, xSize, ySize) + }; + + if (xSize < 8 || ySize < 8) + { + return result; + } + + JxlImage3F xyb0 = new(configuration, xSize, ySize); + + if (!Butteraugli.OpsinDynamicsImage(configuration, rgb0, parameters, result.Temp, result.blurTemp, xyb0)) + { + throw new InvalidOperationException("OpsinDynamicsImage failed"); + } + + result.ReleaseTemp(); + + if (!Butteraugli.SeparateFrequencies(configuration, result.blurTemp, xyb0, result.pi0)) + { + throw new InvalidOperationException("Could not separate frequencies"); + } + + JxlImage3F subsampledRgb0 = Butteraugli.SubSample2x(configuration, rgb0); + result.sub = Make(configuration, subsampledRgb0, parameters); + + return result; + } + + public void ReleaseTemp() + { + this.temp?.Dispose(); + this.temp = null; + } /// /// Computes the butteraugli map between the original image given in the constructor and the distorted image given here. /// - public bool Diffmap(JxlImage3F rgb1, JxlImageF result) + public virtual bool Diffmap(Configuration configuration, JxlImage3F rgb1, JxlImageF result) { - throw new NotImplementedException(); + if (this.xSize < 8 || this.ySize < 8) + { + result.Clear(); + return true; + } + + JxlImage3F xyb1 = new(configuration, this.xSize, this.ySize); + + if (!Butteraugli.OpsinDynamicsImage(configuration, rgb1, this.parameters, this.Temp, this.blurTemp, xyb1)) + { + return false; + } + + this.ReleaseTemp(); + + if (!this.DiffmapOpsinDynamicsImage(configuration, xyb1, out result)) + { + return false; + } + + if (this.sub is not null) + { + if (this.sub.xSize < 8 || this.sub.ySize < 8) + { + return true; + } + + JxlImage3F subXyb = new(configuration, this.sub.xSize, this.sub.ySize); + JxlImage3F subsampledRgb1 = Butteraugli.SubSample2x(configuration, rgb1); + + if (!Butteraugli.OpsinDynamicsImage(configuration, subsampledRgb1, this.parameters, this.sub.Temp, this.sub.blurTemp, subXyb)) + { + return false; + } + + this.sub.ReleaseTemp(); + + if (!this.DiffmapOpsinDynamicsImage(configuration, subXyb, out JxlImageF subResult)) + { + return false; + } + + Butteraugli.AddSupersampled2x(subResult, 0.5f, result); + } + + return true; } /// /// Same as Diffmap but OpsinDynamicsImage() was already applied. /// - public bool DiffmapOpsinDynamicsImage(JxlImage3F xyb1, JxlImageF result) + public bool DiffmapOpsinDynamicsImage(Configuration configuration, JxlImage3F xyb1, out JxlImageF result) { - throw new NotImplementedException(); + result = new(); + + if (this.xSize < 8 || this.ySize < 8) + { + result.Clear(); + return true; + } + + ButteraugliPsychoImage pi1 = new(); + + if (!Butteraugli.SeparateFrequencies(configuration, this.blurTemp, xyb1, pi1)) + { + return false; + } + + result = new(configuration, this.xSize, this.ySize); + return this.DiffmapPsychoImage(configuration, pi1, result); } /// /// Same as above but the frequency decomposition was already applied. /// - public bool DiffmapPsychoImage(ButteraugliPsychoImage pi1, JxlImageF diffmap) + public bool DiffmapPsychoImage(Configuration configuration, ButteraugliPsychoImage pi1, JxlImageF diffmap) { - throw new NotImplementedException(); + if (this.xSize < 8 || this.ySize < 8) + { + diffmap.Clear(); + return true; + } + + float hfAsymmetry = this.parameters.HfAsymmetry; + float xmul = this.parameters.XMultiplier; + + JxlImageF diffs = new(configuration, this.xSize, this.ySize); + JxlImage3 blockDiffAc = new(configuration, this.xSize, this.ySize); + + JxlImageOperations.ZeroFillImage(blockDiffAc); + + if (!Butteraugli.MaltaDiffMap( + this.pi0.Uhf[1], + pi1.Uhf[1], + Butteraugli.WUhfMalta * hfAsymmetry, + Butteraugli.WUhfMalta / hfAsymmetry, + Butteraugli.Norm1Uhf, + diffs, + blockDiffAc, + 1)) + { + return false; + } + + if (!Butteraugli.MaltaDiffMap( + this.pi0.Uhf[0], + pi1.Uhf[0], + Butteraugli.WUhfMaltaX * hfAsymmetry, + Butteraugli.WUhfMaltaX / hfAsymmetry, + Butteraugli.Norm1UhfX, + diffs, + blockDiffAc, + 0)) + { + return false; + } + + if (!Butteraugli.MaltaDiffMapLf( + this.pi0.Hf[1], + pi1.Hf[1], + Butteraugli.WHfMalta * MathF.Sqrt(hfAsymmetry), + Butteraugli.WHfMalta / MathF.Sqrt(hfAsymmetry), + Butteraugli.Norm1Hf, + diffs, + blockDiffAc, + 1)) + { + return false; + } + + if (!Butteraugli.MaltaDiffMapLf( + this.pi0.Hf[0], + pi1.Hf[0], + Butteraugli.WHfMaltaX * MathF.Sqrt(hfAsymmetry), + Butteraugli.WHfMaltaX / MathF.Sqrt(hfAsymmetry), + Butteraugli.Norm1HfX, + diffs, + blockDiffAc, + 0)) + { + return false; + } + + if (!Butteraugli.MaltaDiffMapLf( + this.pi0.Mf!.Plane(1), + pi1.Mf!.Plane(1), + Butteraugli.WHfMalta, + Butteraugli.WMfMalta, + Butteraugli.Norm1Mf, + diffs, + blockDiffAc, + 1)) + { + return false; + } + + if (!Butteraugli.MaltaDiffMapLf( + this.pi0.Mf!.Plane(0), + pi1.Mf!.Plane(0), + Butteraugli.WHfMaltaX, + Butteraugli.WMfMaltaX, + Butteraugli.Norm1MfX, + diffs, + blockDiffAc, + 0)) + { + return false; + } + + JxlImage3F blockDiffDc = new(configuration, this.xSize, this.ySize); + + for (int c = 0; c < 3; c++) + { + if (c < 2) + { + Butteraugli.L2DiffAsymmetric( + this.pi0.Hf[c], + pi1.Hf[c], + Butteraugli.Wmul[c] * hfAsymmetry, + Butteraugli.Wmul[c] / hfAsymmetry, + blockDiffAc.Plane(c)); + } + + Butteraugli.L2Diff( + this.pi0.Mf.Plane(c), + pi1.Mf.Plane(c), + Butteraugli.Wmul[3 + c], + blockDiffAc.Plane(c)); + + Butteraugli.SetL2Diff( + this.pi0.Lf!.Plane(c), + pi1.Lf!.Plane(c), + Butteraugli.Wmul[6 + c], + blockDiffDc.Plane(c)); + } + + JxlImageF mask = new(); + + if (!Butteraugli.MaskButteraugliPsychoImage( + configuration, + this.pi0, + pi1, + this.xSize, + this.ySize, + this.blurTemp, + mask, + out JxlImageF? diffAc)) + { + return false; + } + + diffAc?.BytesSpan.CopyTo(blockDiffAc.Plane(1).BytesSpan); + + return Butteraugli.CombineChannelsToDiffmap(mask, blockDiffDc, blockDiffAc, xmul, diffmap); } - public bool Mask(JxlImageF mask) + public virtual bool Mask(Configuration configuration, JxlImageF mask) + => Butteraugli.MaskButteraugliPsychoImage(configuration, this.pi0, this.pi0, this.xSize, this.ySize, this.blurTemp, mask, out _); + + public void Dispose() { - throw new NotImplementedException(); + this.temp?.Dispose(); + this.temp = null; + this.blurTemp.Dispose(); } } diff --git a/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs new file mode 100644 index 0000000000..de1166cf8e --- /dev/null +++ b/src/ImageSharp/Formats/Jxl/Processing/Butteraugli/ButteraugliPsychoImage.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Jxl.Memory; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; + +namespace SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; + +// Public fields because InlineArray2 values +// cannot be mutated if it's a property +#pragma warning disable SA1401 // Fields should be private + +internal sealed class ButteraugliPsychoImage +{ + public InlineArray2> Uhf; // XY + + public InlineArray2> Hf; // XY + + public JxlImage3F? Mf { get; set; } // XYB + + public JxlImage3F? Lf { get; set; } // XYB +} diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs index 0a4ff8f5d7..b4e98c5ca4 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs @@ -23,6 +23,15 @@ internal static class JxlImageOperations /// True if width and height is equal. public static bool SameSize(JxlPlaneBase a, JxlPlaneBase b) => a.XSize == b.XSize && a.YSize == b.YSize; + /// + /// Returns true if first image has same width and height as the second image. + /// + /// First image + /// Second image + /// True if width and height is equal. + public static bool SameSize(JxlImage3 a, JxlImage3 b) + where T : unmanaged => a.XSize == b.XSize && a.YSize == b.YSize; + /// /// Copies everything from one plane to another. /// diff --git a/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs b/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs index 21fa5d23b2..b6dfd4e39f 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/JxlToc.cs @@ -31,6 +31,7 @@ public static int NumberOfTocEntries(int numGroups, int numDcGroups, int numPass } private const int BitsPerByte = 8; + private const int MaxTocEntries = 65536; public static bool ReadToc( @@ -98,10 +99,7 @@ bool CheckBitBudget(int numEntries) } } - if (!reader.JumpToByteBoundary()) - { - return false; - } + reader.JumpToByteBoundary(); if (!CheckBitBudget(tocEntries)) { @@ -113,10 +111,7 @@ bool CheckBitBudget(int numEntries) sizes[i] = JxlU32Coder.Read(TocDistribution, reader); } - if (!reader.JumpToByteBoundary()) - { - return false; - } + reader.JumpToByteBoundary(); return CheckBitBudget(0); } From b0f0d34a2eb14ba3b0fa2a4fda8c270fe9ea3e19 Mon Sep 17 00:00:00 2001 From: winscripter <142818255+winscripter@users.noreply.github.com> Date: Sat, 5 Sep 2026 00:35:43 +0400 Subject: [PATCH 142/142] Complete Butteraugli and its tests --- .../Processing/Image/JxlImageOperations.cs | 35 +++++ .../Jxl/Processing/ButteraugliTests.cs | 139 ++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs diff --git a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs index b4e98c5ca4..af665db380 100644 --- a/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs +++ b/src/ImageSharp/Formats/Jxl/Processing/Image/JxlImageOperations.cs @@ -64,6 +64,41 @@ public static bool CopyImage(JxlPlane from, JxlPlane to) return true; } + /// + /// Copies everything from one image to another. + /// + /// The type of image to copy. + /// Source image (read-only) + /// Destination image (write-only) + /// + /// Status of the copy operation + /// + public static bool CopyImage(JxlImage3 from, JxlImage3 to) + where T : unmanaged + { + if (!SameSize(from, to)) + { + return false; + } + + if (from.XSize == 0 || from.YSize == 0) + { + return true; + } + + for (int c = 0; c < 3; c++) + { + for (int y = 0; y < from.YSize; y++) + { + Span rowFrom = from.PlaneRow(c, y); + Span rowTo = to.PlaneRow(c, y); + rowFrom.CopyTo(rowTo); + } + } + + return true; + } + /// /// Within bounds specified by input rectangles, copies everything from one plane to another. /// diff --git a/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs new file mode 100644 index 0000000000..8adabd83e7 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jxl/Processing/ButteraugliTests.cs @@ -0,0 +1,139 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Microsoft.Diagnostics.Runtime.Interop; +using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Butteraugli; +using SixLabors.ImageSharp.Formats.Jxl.Processing.Image; + +namespace SixLabors.ImageSharp.Tests.Formats.Jxl.Processing; + +/// +/// Tests for Google Butteraugli (C# implementation), a tool +/// used for comparing images in a more sophisticated way +/// (the way humans may notice differences). For example, +/// if every pixel was off by 1, PSNR, which operates on a +/// pixel-by-pixel basis, would report a large difference, +/// while Butteraugli would report barely any differences, +/// as us humans wouldn't notice anything if every pixel +/// was just off by one. +/// +public class ButteraugliTests +{ + private static JxlImage3F SinglePixelImage(float r, float g, float b) + { + JxlImage3F img = new(TestEnvironment.Configuration, 1, 1); + img.PlaneRow(0, 0)[0] = r; + img.PlaneRow(1, 0)[0] = r; + img.PlaneRow(2, 0)[0] = r; + return img; + } + + private static void AddUniformNoise(JxlImage3F img, float d, ulong seed) + { + Rng generator = new(seed); + + for (int y = 0; y < img.YSize; ++y) + { + for (int c = 0; c < 3; ++c) + { + Span planeRow = img.PlaneRow(c, y); + + for (int x = 0; x < img.XSize; ++x) + { + planeRow[x] += generator.UniformF(-d, d); + } + } + } + } + + private static void AddEdge(JxlImage3F image, float d, int x0, int y0) + { + int h = Math.Min(image.YSize - y0, 100); + int w = Math.Min(image.XSize - x0, 5); + + for (int dy = 0; dy < h; ++dy) + { + Span planeRow = image.PlaneRow(1, y0 + dy); + + for (int dx = 0; dx < w; ++dx) + { + planeRow[x0 + dx] += d; + } + } + } + + [Fact] + public void TestSinglePixel() + { + JxlImage3F rgb0 = SinglePixelImage(0.5f, 0.5f, 0.5f); + JxlImage3F rgb1 = SinglePixelImage(0.5f, 0.49f, 0.5f); + + ButteraugliParameters butteraugliParameters = new(); + JxlImageF diffmap = new(); + + Assert.True( + Butteraugli.ButteraugliInterface(TestEnvironment.Configuration, rgb0, rgb1, butteraugliParameters, diffmap, out double diffval), + "Butteraugli initialization failed"); + + Assert.True(new TolerantMath(0.5).AreEqual(diffval, 2.5), $"Diff value isn't even close to 2.5 (it's {diffval})"); + + JxlImageF diffmap2 = new(); + Assert.True(Butteraugli.ButteraugliInterfaceInPlace( + TestEnvironment.Configuration, + rgb0, + rgb1, + butteraugliParameters, + diffmap2, + out double diffval2)); + + Assert.True(new TolerantMath(1e-10).AreEqual(diffval, diffval2), $"Diff value isn't even close to diffval2 (diffval={diffval}, diffval2={diffval2})"); + } + + // TODO: we need to port test image stuff from libjxl + [Fact] + public void TestLargeImage() + { + const int xSize = 1024; + const int ySize = 1024; + + JxlTestImage img = new(); + img.SetDimensions(xSize, ySize); + + JxlTestFrame frame = img.AddFrame(); + frame.RandomFill(777); + + JxlImage3F rgb0 = GetColorImage(img.Ppf); + JxlImage3F rgb1 = new(TestEnvironment.Configuration, xSize, ySize); + JxlImageOperations.CopyImage(rgb0, rgb1); + + AddUniformNoise(rgb1, 0.02f, 7777uL); + AddEdge(rgb1, 0.1f, xSize / 2, xSize / 2); + + ButteraugliParameters butteraugliParameters = new(); + JxlImageF diffmap = new(); + Assert.True( + Butteraugli.ButteraugliInterface(TestEnvironment.Configuration,, rgb0, rgb1, butteraugliParameters, diffmap, out double diffval), + "Couldn't initialize Butteraugli"); + + double distp = Butteraugli.ComputeDistanceP(diffmap, butteraugliParameters, 3.0); + Assert.True(new TolerantMath(0.5).AreEqual(diffval, 4.0), $"Diff isn't even close to 4.0 (diffval={diffval})"); + Assert.True(new TolerantMath(0.5).AreEqual(distp, 1.5), $"Distance isn't even close to 4.0 (distp={distp})"); + + JxlImageF diffmap2 = new(); + Assert.True( + Butteraugli.ButteraugliInterfaceInPlace( + TestEnvironment.Configuration, + rgb0, + rgb1, + butteraugliParameters, + diffmap2, + out double diffval2), + "Butteraugli in-place interface initialization failed"); + + double distp2 = Butteraugli.ComputeDistanceP(diffmap2, butteraugliParameters, 3.0); + + Assert.True(new TolerantMath(5e-7).AreEqual(diffval, diffval2), $"Diffval != diffval2 (diffval={diffval}, diffval2={diffval2})"); + Assert.True(new TolerantMath(1e-7).AreEqual(distp, distp2), $"Distp != distp2 (distp={distp}, distp2={distp2})"); + } +}