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 01/48] 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 02/48] 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 03/48] 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 04/48] 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 05/48] 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 06/48] 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 07/48] 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 08/48] 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 09/48] 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 10/48] 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 11/48] 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 12/48] 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 13/48] 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 14/48] 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 15/48] 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 16/48] 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 17/48] 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 18/48] 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 19/48] 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 20/48] 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 21/48] 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 22/48] 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 23/48] 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 24/48] 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 25/48] 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 26/48] 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 27/48] 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 28/48] 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 29/48] 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 30/48] 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 31/48] 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 32/48] 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 33/48] 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 34/48] 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 35/48] 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 36/48] 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 37/48] 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 38/48] 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 39/48] 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 40/48] 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 41/48] 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 42/48] 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 43/48] 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 44/48] 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 45/48] 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 46/48] 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 47/48] 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 48/48] 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 +}