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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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 @@ internal sealed class JxlAcStrategyImage 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 @@ internal sealed class JxlAcStrategyImage #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 @@ internal sealed class JxlAcStrategyImage 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/31] 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/31] 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 @@ internal sealed class JxlImageMetadata : IJxlFields 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/31] 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/31] 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/31] 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 @@ using System.Runtime.CompilerServices; 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/31] 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 @@ using System.Runtime.CompilerServices; #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/31] 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 @@ internal sealed class 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 @@ internal sealed class 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/31] 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 @@ internal sealed class JxlQuantizedSpline : IDisposable 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/31] 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/31] 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/31] 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 @@ using System.Runtime.CompilerServices; 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 @@ internal struct JxlFrameDimensions 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/31] 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 @@ internal static class JxlAnsHelper 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/31] 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/31] 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/31] 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 @@ internal static class JxlAnsHelper 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/31] 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/31] 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 @@ internal static class JxlAnsHelper int value = distribution[sym]; sum += value; - if (value == AnsTableSize) + if (value == JxlAnsConstants.AnsTableSize) { if (singleSymbol != -1) { @@ -143,7 +142,7 @@ internal static class JxlAnsHelper 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/31] 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/31] 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 @@ internal static class JxlAnsHelper => 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 @@ internal sealed class JxlBitReader(ReadOnlyMemory bytes) 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/31] 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 @@ internal static class JxlAnsReader if (symbols[0] == symbols[1]) { Debug.Fail("Corrupt data"); - + counts.Dispose(); return null; } @@ -192,14 +192,14 @@ internal static class JxlAnsReader 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 @@ internal static class JxlAnsReader 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/31] 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/31] 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/31] 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/31] 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/31] 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; + } + } +}