Browse Source

Merge 6de4a89413 into 5a0a74fecc

pull/3153/merge
winscripter 23 hours ago
committed by GitHub
parent
commit
3e296d5a21
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 53
      src/ImageSharp/Formats/Jxl/Ac/JxlAcContext.cs
  2. 186
      src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategy.cs
  3. 119
      src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs
  4. 31
      src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyRow.cs
  5. 67
      src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyType.cs
  6. 72
      src/ImageSharp/Formats/Jxl/Ac/JxlBlockContextMap.cs
  7. 24
      src/ImageSharp/Formats/Jxl/Coefficients/JxlForwardCoefficientOrder.cs
  8. 17
      src/ImageSharp/Formats/Jxl/Fields/IJxlFields.cs
  9. 21
      src/ImageSharp/Formats/Jxl/Fields/JxlFieldExpressions.cs
  10. 17
      src/ImageSharp/Formats/Jxl/Fields/JxlU32Distribution.cs
  11. 26
      src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs
  12. 15
      src/ImageSharp/Formats/Jxl/IO/JxlAnsConstants.cs
  13. 16
      src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs
  14. 242
      src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs
  15. 60
      src/ImageSharp/Formats/Jxl/IO/JxlAnsHybridUIntConfiguration.cs
  16. 21
      src/ImageSharp/Formats/Jxl/IO/JxlAnsLz77Parameters.cs
  17. 256
      src/ImageSharp/Formats/Jxl/IO/JxlAnsReader.cs
  18. 14
      src/ImageSharp/Formats/Jxl/IO/JxlAnsSymbol.cs
  19. 167
      src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs
  20. 41
      src/ImageSharp/Formats/Jxl/InlineArrays.cs
  21. 38
      src/ImageSharp/Formats/Jxl/JxlAspectRatioHelpers.cs
  22. 15
      src/ImageSharp/Formats/Jxl/JxlFormat.cs
  23. 82
      src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs
  24. 12
      src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs
  25. 14
      src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs
  26. 14
      src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs
  27. 14
      src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs
  28. 14
      src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs
  29. 14
      src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs
  30. 34
      src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs
  31. 23
      src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageF.cs
  32. 23
      src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageI.cs
  33. 23
      src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageS.cs
  34. 23
      src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageSB.cs
  35. 23
      src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageU.cs
  36. 85
      src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs
  37. 115
      src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs
  38. 36
      src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs
  39. 19
      src/ImageSharp/Formats/Jxl/Metadata/JxlAnimationHeader.cs
  40. 42
      src/ImageSharp/Formats/Jxl/Metadata/JxlBitDepth.cs
  41. 57
      src/ImageSharp/Formats/Jxl/Metadata/JxlCodecMetadata.cs
  42. 25
      src/ImageSharp/Formats/Jxl/Metadata/JxlCustomTransformData.cs
  43. 16
      src/ImageSharp/Formats/Jxl/Metadata/JxlExifOrientation.cs
  44. 25
      src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannel.cs
  45. 27
      src/ImageSharp/Formats/Jxl/Metadata/JxlExtraChannelInfo.cs
  46. 128
      src/ImageSharp/Formats/Jxl/Metadata/JxlImageMetadata.cs
  47. 19
      src/ImageSharp/Formats/Jxl/Metadata/JxlOpsinInverseMatrix.cs
  48. 68
      src/ImageSharp/Formats/Jxl/Metadata/JxlPreviewHeader.cs
  49. 73
      src/ImageSharp/Formats/Jxl/Metadata/JxlSizeHeader.cs
  50. 21
      src/ImageSharp/Formats/Jxl/Metadata/JxlToneMapping.cs
  51. 15
      src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingInputLayer.cs
  52. 15
      src/ImageSharp/Formats/Jxl/Processing/JxlAlphaBlendingOutput.cs
  53. 192
      src/ImageSharp/Formats/Jxl/Processing/JxlAlphaHelper.cs
  54. 106
      src/ImageSharp/Formats/Jxl/Processing/JxlLehmerCode.cs
  55. 28
      src/ImageSharp/Formats/Jxl/Processing/JxlNoiseHelper.cs
  56. 14
      src/ImageSharp/Formats/Jxl/Processing/JxlNoiseIndexAndFraction.cs
  57. 14
      src/ImageSharp/Formats/Jxl/Processing/JxlNoiseLevel.cs
  58. 15
      src/ImageSharp/Formats/Jxl/Processing/JxlNoiseParameters.cs
  59. 58
      src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs
  60. 17
      src/ImageSharp/Formats/Jxl/Splines/JxlControlPoint.cs
  61. 343
      src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs
  62. 39
      src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs
  63. 13
      src/ImageSharp/Formats/Jxl/Splines/JxlSplineDataView.cs
  64. 15
      src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs
  65. 17
      src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegment.cs
  66. 11
      src/ImageSharp/Formats/Jxl/Splines/JxlSplineSegmentSpan.cs

53
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;
/// <summary>
/// AC context
/// </summary>
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<int> 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<int> 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;
}
}

186
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<byte> 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<byte> 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<byte> 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<int> 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;
}
}
}
}
}

119
src/ImageSharp/Formats/Jxl/Ac/JxlAcStrategyImage.cs

@ -0,0 +1,119 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics;
using SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
namespace SixLabors.ImageSharp.Formats.Jxl.Ac;
internal sealed class JxlAcStrategyImage : IDisposable
{
private const byte Invalid = byte.MaxValue; // 255
private JxlImageB? layers;
private Memory<byte> row;
private int stride;
public int XSize => this.layers!.XSize;
public int YSize => this.layers!.YSize;
public int PixelsPerRow => this.layers!.PixelsPerRow;
public JxlAcStrategyRow GetRow(int y, int xPrefix = 0)
{
ReadOnlyMemory<byte> layerRow = this.layers!.GetRowBytesMemory(y);
ReadOnlyMemory<byte> row = layerRow[xPrefix..];
return new JxlAcStrategyRow(row);
}
public static JxlAcStrategyImage Create(Configuration memoryManager, int xSize, int ySize)
{
JxlAcStrategyImage image = new()
{
layers = new JxlImageB(memoryManager, xSize, ySize)
};
image.row = image.layers.GetRowBytesMemory(0);
image.stride = image.layers.PixelsPerRow;
return image;
}
public int CountBlocks(JxlAcStrategyType type)
{
int value = 0;
int compare = ((int)type << 1) | 1;
for (int y = 0; y < this.layers!.YSize; y++)
{
ReadOnlySpan<byte> row = this.layers!.GetRow(y);
for (int x = 0; x < this.layers!.XSize; x++)
{
if (row[x] == compare)
{
value++;
}
}
}
return value;
}
public JxlAcStrategyRow GetRow(in Rectangle rect, int y) => this.GetRow(rect.Y + y, rect.X);
public bool IsValid(int x, int y) => this.row.Span[(y * this.stride) + x] != Invalid;
public bool SetNoBoundsChecks(int x, int y, JxlAcStrategyType type, bool check = true)
{
JxlAcStrategy strategy = new(type);
Span<byte> rowSpan = this.row.Span;
int rawType = (int)type;
int rawTypeTimes2 = rawType << 1;
for (int iy = 0; iy < strategy.CoveredBlocksX; iy++)
{
for (int ix = 0; ix < strategy.CoveredBlocksX; ix++)
{
int pos = ((y + iy) * this.stride) + x + ix;
if (check && rowSpan[pos] != Invalid)
{
Debug.Fail("Invalid AC strategy. Blocks overlap.");
return false;
}
rowSpan[pos] = (byte)(rawTypeTimes2 | ((iy | ix) == 0 ? 1 : 0));
}
}
return true;
}
public bool Set(int x, int y, JxlAcStrategyType type)
{
#if DEBUG
JxlAcStrategy strategy = new(type);
Debug.Assert(y + strategy.CoveredBlocksY <= this.layers!.YSize, "Invalid range");
Debug.Assert(x + strategy.CoveredBlocksX <= this.layers.XSize, "Invalid range");
#endif
return this.SetNoBoundsChecks(x, y, type, check: false);
}
public void FillDct8(in Rectangle rect) => this.FillPlane(((int)JxlAcStrategyType.DCT << 1) | 1, this.layers, in rect);
public void FillDct8() => this.FillDct8(in this.layers.GetRectangle());
public void FillInvalid() => this.FillImage(Invalid, this.layers);
public void Dispose()
{
this.layers?.Dispose();
GC.SuppressFinalize(this);
}
}

31
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<byte> row;
public JxlAcStrategyRow(ReadOnlyMemory<byte> row) => this.row = row;
public JxlAcStrategy this[int x]
{
get
{
ReadOnlySpan<byte> 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<byte, int>(ref first), x) >> 1);
bool isFirst = Unsafe.Add(ref first, x) != 0;
return new JxlAcStrategy(strategy, isFirst);
}
}
}

67
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
}

72
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<byte> 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<int>[] DcThresholds { get; } = [[], [], []];
public List<uint> 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;
}
}

24
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);
}
}

17
src/ImageSharp/Formats/Jxl/Fields/IJxlFields.cs

@ -0,0 +1,17 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
/// <summary>
/// Abstracts enumeration of all fields into a visitor.
/// </summary>
internal interface IJxlFields
{
/// <summary>
/// Visits all fields into the specified JXL visitor.
/// </summary>
/// <param name="visitor">The visitor to use to visit all fields.</param>
/// <returns>Status of the visit operation.</returns>
public bool Visit(JxlVisitor visitor);
}

21
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;
}

17
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;
}

26
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<JxlU32Distribution> 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];
}
}

15
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;
}

16
src/ImageSharp/Formats/Jxl/IO/JxlAnsEntry.cs

@ -0,0 +1,16 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Jxl.IO;
[StructLayout(LayoutKind.Sequential)]
internal struct JxlAnsEntry
{
public byte Cutoff;
public byte RightValue;
public ushort Frequency0;
public ushort Offsets1;
public ushort Frequency1XorFrequency0;
}

242
src/ImageSharp/Formats/Jxl/IO/JxlAnsHelper.cs

@ -0,0 +1,242 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Jxl.IO;
internal static class JxlAnsHelper
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetPopulationCountPrecision(int logCount, int shift)
=> Math.Max(0, Math.Min(logCount, shift - ((JxlAnsConstants.AnsLogTableSize - logCount) >> 1)));
// NOTE: The result may potentially be large, so prefer using a memory allocator
public static IMemoryOwner<uint> 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<uint> result = configuration.MemoryAllocator.Allocate<uint>(length);
Span<uint> resultSpan = result.Memory.Span;
uint unsignedCount = (uint)count;
for (int i = 0; i < length; i++)
{
resultSpan[i] = unsignedCount;
}
int remCounts = totalCount % length;
for (int i = 0; i < remCounts; i++)
{
resultSpan[i]++;
}
return result;
}
public static JxlAnsSymbol Lookup(ReadOnlySpan<JxlAnsEntry> 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<int> preDistribution, uint logRange, int logAlphaSize, Span<JxlAnsEntry> 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<int> distribution = preDistribution[..(distributionPointer + 1)];
if (distribution.Length > tableSize)
{
Debug.Fail("Too many items in the distribution");
return false;
}
int entrySize = range >> logAlphaSize;
int singleSymbol = -1;
int sum = 0;
for (int sym = 0; sym < distribution.Length; sym++)
{
int value = distribution[sym];
sum += value;
if (value == JxlAnsConstants.AnsTableSize)
{
if (singleSymbol != -1)
{
return false;
}
singleSymbol = sym;
}
}
if (sum != range)
{
return false;
}
if (singleSymbol != -1)
{
byte sym = (byte)singleSymbol;
if (singleSymbol != sym)
{
return false;
}
for (int i = 0; i < tableSize; i++)
{
ref JxlAnsEntry jxlEntry = ref entries[i];
jxlEntry.RightValue = sym;
jxlEntry.Cutoff = 0;
jxlEntry.Offsets1 = (ushort)(entrySize * i);
jxlEntry.Frequency0 = 0;
jxlEntry.Frequency1XorFrequency0 = JxlAnsConstants.AnsTableSize;
}
return true;
}
Span<uint> underfullPosn = stackalloc uint[distribution.Length];
Span<uint> overfullPosn = stackalloc uint[distribution.Length];
Span<uint> 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;
}
}

60
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();
}

21
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();
}

256
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<uint>? ReadHistogram(Configuration configuration, int precisionBits, JxlBitReader reader)
{
int range = 1 << precisionBits;
bool isSimpleCode = reader.ReadBoolean();
IMemoryOwner<uint> counts;
if (isSimpleCode)
{
Span<uint> 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<uint>((int)maxSymbol + 1);
Span<uint> countsSpan = counts.Memory.Span;
if (symCount == 1)
{
countsSpan[(int)symbols[0]] = (uint)range;
}
else
{
if (symbols[0] == symbols[1])
{
Debug.Fail("Corrupt data");
counts.Dispose();
return null;
}
countsSpan[(int)symbols[0]] = reader.ReadBits32((uint)precisionBits);
countsSpan[(int)symbols[1]] = (uint)range - countsSpan[(int)symbols[0]];
}
}
else
{
bool isFlat = reader.ReadBoolean();
if (isFlat)
{
uint alphabetSize = DecodeVariableLengthUint8(reader) + 1u;
if (alphabetSize <= range)
{
return null;
}
counts = JxlAnsHelper.CreateFlatHistogram(configuration, (int)alphabetSize, range);
return counts;
}
int upperBoundLog = FloorLog2Nonzero(JxlAnsConstants.AnsLogTableSize + 1);
int log = 0;
for (; log < upperBoundLog; log++)
{
bool logIncrementBit = reader.ReadBoolean();
if (!logIncrementBit)
{
break;
}
}
uint shift = (reader.ReadBits32((uint)log) | (1u << log)) - 1u;
if (shift > JxlAnsConstants.AnsLogTableSize + 1)
{
Debug.Fail("Invalid shift");
return null;
}
uint length = DecodeVariableLengthUint8(reader) + 3u;
counts = configuration.MemoryAllocator.Allocate<uint>((int)length);
Span<uint> 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<int> 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<int> same = stackalloc int[(int)length];
same.Clear();
for (int i = 0; i < length; i++)
{
uint index = reader.PeekBits32(7);
reader.SkipBits32(HuffmanLookup[index][0]);
logCounts[i] = HuffmanLookup[index][1] - 1;
if (logCounts[i] == JxlAnsConstants.AnsLogTableSize)
{
uint rleLength = DecodeVariableLengthUint8(reader);
same[i] = (int)rleLength + 5;
i += (int)rleLength + 3;
continue;
}
if (logCounts[i] > omitLog)
{
omitLog = logCounts[i];
omitPos = i;
}
}
if (omitPos < 0)
{
Debug.Fail("The histogram is corrupt or invalid.");
counts.Dispose();
return null;
}
if (omitPos + 1 < length && logCounts[omitPos + 1] == JxlAnsConstants.AnsLogTableSize)
{
Debug.Fail("The histogram is corrupt or invalid.");
counts.Dispose();
return null;
}
int previous = 0;
int sameCount = 0;
for (int i = 0; i < length; i++)
{
if (same[i] > 0)
{
sameCount = same[i] - 1;
previous = i > 0 ? (int)countsSpan[i - 1] : 0;
}
if (sameCount > 0)
{
countsSpan[i] = (uint)previous;
sameCount--;
}
else
{
int code = logCounts[i];
if (i == omitPos || code < 0)
{
continue;
}
else if (shift == 0 || code == 0)
{
countsSpan[i] = 1u << code;
}
else
{
int bitCount = JxlAnsHelper.GetPopulationCountPrecision(code, (int)shift);
countsSpan[i] = (1u << code) + (reader.ReadBits32((uint)bitCount) << (code - bitCount));
}
}
totalCount += countsSpan[i];
}
countsSpan[omitPos] = (uint)range - totalCount;
if (countsSpan[omitPos] <= 0)
{
Debug.Fail("The histogram count is incorrect.");
counts.Dispose();
return null;
}
}
return counts;
}
}

14
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;
}

167
src/ImageSharp/Formats/Jxl/IO/JxlBitReader.cs

@ -0,0 +1,167 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using System.Diagnostics;
namespace SixLabors.ImageSharp.Formats.Jxl.IO;
/// <summary>
/// Represents a bitstream reader.
/// </summary>
internal sealed class JxlBitReader(ReadOnlyMemory<byte> bytes)
{
private ulong buffer;
private uint bufferRemainingBits;
private int pointer;
private bool endOfStream;
/// <summary>
/// Fetches a new buffer.
/// </summary>
private void RefillCore()
{
ReadOnlySpan<byte> 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);
public bool ReadBoolean() => this.ReadBits32Core(1, peek: false) == 1;
}

41
src/ImageSharp/Formats/Jxl/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;
[InlineArray(3)]
internal struct InlineArray3<T>
{
private T first;
}
/// <summary>
/// Used by JxlCustomTransformData
/// </summary>
[InlineArray(15)]
internal struct InlineArray15<T>
{
private T first;
}
/// <summary>
/// Used by JxlCustomTransformData
/// </summary>
[InlineArray(55)]
internal struct InlineArray55<T>
{
private T first;
}
/// <summary>
/// Used by JxlCustomTransformData
/// </summary>
[InlineArray(210)]
internal struct InlineArray210<T>
{
private T first;
}

38
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;
}

15
src/ImageSharp/Formats/Jxl/JxlFormat.cs

@ -0,0 +1,15 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl;
internal class JxlFormat : IImageFormat
{
public string Name => "JPEG XL";
public string DefaultMimeType => "image/jxl";
IEnumerable<string> IImageFormat.MimeTypes => new[] { "image/jxl" };
IEnumerable<string> IImageFormat.FileExtensions => new[] { "jxl" };
}

82
src/ImageSharp/Formats/Jxl/JxlFrameDimensions.cs

@ -0,0 +1,82 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Jxl;
internal sealed class JxlFrameDimensions
{
public const int BlockDimensions = 8;
public const int DctBlockSize = BlockDimensions * BlockDimensions;
public const int GroupDimensions = 256;
public const int GroupDimensionsInBlocks = GroupDimensions / BlockDimensions;
public JxlFrameDimensions(int xSizePixel, int ySizePixel, int groupSizeShift, int maxHorizontalShift, int maxVerticalShift, bool modularMode, int upsampling)
{
this.GroupDimension = (GroupDimensions >> 1) << groupSizeShift;
this.DcGroupDimension = this.GroupDimension * BlockDimensions;
this.XSizeUpsampled = xSizePixel;
this.YSizeUpsampled = ySizePixel;
this.XSize = DivCeil(xSizePixel, upsampling);
this.YSize = DivCeil(ySizePixel, upsampling);
this.XSizeBlocks = DivCeil(this.XSize, BlockDimensions << maxHorizontalShift) << maxHorizontalShift;
this.YSizeBlocks = DivCeil(this.YSize, BlockDimensions << maxVerticalShift) << maxVerticalShift;
this.XSizePadded = this.XSizeBlocks * BlockDimensions;
this.YSizePadded = this.YSizeBlocks * BlockDimensions;
if (modularMode)
{
this.XSizePadded = this.XSize;
this.YSizePadded = this.YSize;
}
this.XSizeUpsampledPadded = this.XSizePadded * upsampling;
this.YSizeUpsampledPadded = this.YSizePadded * upsampling;
this.XSizeGroups = DivCeil(this.XSize, GroupDimensions);
this.YSizeGroups = DivCeil(this.YSize, GroupDimensions);
this.XSizeDcGroups = DivCeil(this.XSizeBlocks, GroupDimensions);
this.YSizeDcGroups = DivCeil(this.YSizeBlocks, GroupDimensions);
this.NumGroups = this.XSizeGroups * this.YSizeGroups;
this.NumDcGroups = this.XSizeDcGroups * this.YSizeDcGroups;
}
public int XSize { get; set; }
public int YSize { get; set; }
public int XSizeUpsampled { get; set; }
public int YSizeUpsampled { get; set; }
public int XSizeUpsampledPadded { get; set; }
public int YSizeUpsampledPadded { get; set; }
public int XSizePadded { get; set; }
public int YSizePadded { get; set; }
public int XSizeBlocks { get; set; }
public int YSizeBlocks { get; set; }
public int XSizeGroups { get; set; }
public int YSizeGroups { get; set; }
public int XSizeDcGroups { get; set; }
public int YSizeDcGroups { get; set; }
public int NumGroups { get; set; }
public int NumDcGroups { get; set; }
public int GroupDimension { get; set; }
public int DcGroupDimension { get; set; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int DivCeil(int x, int y) => x / y;
}

12
src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs

@ -0,0 +1,12 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
namespace SixLabors.ImageSharp.Formats.Jxl;
internal static class JxlThrowHelper
{
[DoesNotReturn]
public static void ThrowEndOfStream() => throw new EndOfStreamException();
}

14
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;
/// <summary>
/// Represents a three-plane, 2D raster image of type <see cref="byte"/>.
/// </summary>
internal sealed class JxlImage3B : JxlImage3<byte>
{
public JxlImage3B()
{
}
}

14
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;
/// <summary>
/// Represents a three-plane, 2D raster image of type <see cref="float"/>.
/// </summary>
internal sealed class JxlImage3F : JxlImage3<float>
{
public JxlImage3F()
{
}
}

14
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;
/// <summary>
/// Represents a three-plane, 2D raster image of type <see cref="int"/>.
/// </summary>
internal sealed class JxlImage3I : JxlImage3<int>
{
public JxlImage3I()
{
}
}

14
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;
/// <summary>
/// Represents a three-plane, 2D raster image of type <see cref="short"/>.
/// </summary>
internal sealed class JxlImage3S : JxlImage3<short>
{
public JxlImage3S()
{
}
}

14
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;
/// <summary>
/// Represents a three-plane, 2D raster image of type <see cref="ushort"/>.
/// </summary>
internal sealed class JxlImage3U : JxlImage3<ushort>
{
public JxlImage3U()
{
}
}

34
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;
/// <summary>
/// Represents a single-plane, 2D raster image of type <see cref="byte"/>.
/// </summary>
internal sealed class JxlImageB : JxlPlane<byte>
{
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<byte> GetRowBytesMemory(int y)
{
Debug.Assert(y < this.YSize, "Attempted to access out-of-bounds Y coordinate");
Memory<byte> row = this.Bytes[(y * this.BytesPerRow)..];
return row;
}
}

23
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;
/// <summary>
/// Represents a single-plane, 2D raster image of type <see cref="float"/>.
/// </summary>
internal sealed class JxlImageF : JxlPlane<float>
{
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);
}

23
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;
/// <summary>
/// Represents a single-plane, 2D raster image of type <see cref="int"/>.
/// </summary>
internal sealed class JxlImageI : JxlPlane<int>
{
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);
}

23
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;
/// <summary>
/// Represents a single-plane, 2D raster image of type <see cref="short"/>.
/// </summary>
internal sealed class JxlImageS : JxlPlane<short>
{
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);
}

23
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;
/// <summary>
/// Represents a single-plane, 2D raster image of type <see cref="sbyte"/>.
/// </summary>
internal sealed class JxlImageSB : JxlPlane<sbyte>
{
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);
}

23
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;
/// <summary>
/// Represents a single-plane, 2D raster image of type <see cref="ushort"/>.
/// </summary>
internal sealed class JxlImageU : JxlPlane<ushort>
{
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);
}

85
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<T>
where T : unmanaged
{
private const int PlaneCount = 3;
private JxlPlane<T>[] planes = new JxlPlane<T>[3];
public JxlImage3()
{
}
public JxlImage3(JxlImage3<T> 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<T> PlaneRow(int plane, int row)
{
this.PlaneRowBoundsCheck(plane, row);
int rowOffset = row * this.planes[0].BytesPerRow;
Span<T> rowSpan = MemoryMarshal.Cast<byte, T>(this.planes[plane].BytesSpan[rowOffset..]);
return rowSpan;
}
public JxlPlane<T> Plane(int index) => this.planes[index];
public void Swap(JxlImage3<T> other)
{
for (int i = 0; i < PlaneCount; i++)
{
other.planes[i].Swap(this.planes[i]);
}
}
public static JxlImage3<T> Create(Configuration configuration, int xSize, int ySize)
{
JxlPlane<T> plane0 = JxlPlane<T>.Create(configuration, xSize, ySize);
JxlPlane<T> plane1 = JxlPlane<T>.Create(configuration, xSize, ySize);
JxlPlane<T> plane2 = JxlPlane<T>.Create(configuration, xSize, ySize);
return new JxlImage3<T>()
{
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");
}

115
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<byte>? 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<byte> Bytes =>
#if DEBUG
this.bytes?.Memory ?? throw new InvalidOperationException("Bytes are missing");
#else
return this.bytes!.Memory;
#endif
public Span<byte> 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<byte>(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<T> GetRowBase<T>(int y)
where T : unmanaged
{
Debug.Assert(y < this.YSize, "Attempted to access out-of-bounds Y coordinate");
Span<byte> row = this.Bytes.Span[(y * this.BytesPerRow)..];
return MemoryMarshal.Cast<byte, T>(row);
}
protected void SetBytes(IMemoryOwner<byte> 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);
}
}

36
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<T> : 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<T> Create(Configuration configuration, int xSize, int ySize, int prePadding = 0)
{
JxlPlane<T> 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<T> GetRow(int y) => this.GetRowBase<T>(y);
}

19
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();
}

42
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
{
/// <summary>
/// Gets or sets a value indicating whether
/// the original (uncompressed) samples are floating point or
/// unsigned integer.
/// </summary>
public bool FloatingPointSample { get; set; }
/// <summary>
/// Gets or sets the bit depth of the original (uncompressed) image samples.
/// Must be in the range [1, 32].
/// </summary>
public int BitsPerSample { get; set; }
/// <summary>
/// <para>
/// Gets or sets floating point exponent bits of the original (uncompressed) image samples,
/// only used if <see cref="FloatingPointSample"/> is <see langword="true"/>.
/// </para>
/// <para>
/// If used, the samples are floating point with:
/// <list type="bullet">
/// <item>1 sign bit</item>
/// <item><see cref="ExponentBitsPerSample"/> exponent bits</item>
/// <item>(<see cref="BitsPerSample"/> - <see cref="ExponentBitsPerSample"/> - 1) mantissa bits</item>
/// </list>
/// If used, <see cref="ExponentBitsPerSample"/> must be in the range
/// [2, 8] and amount of mantissa bits must be in the range [2, 23].
/// </para>
/// </summary>
public int ExponentBitsPerSample { get; set; }
public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
}

57
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;
}
}

25
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<float> Upsampling2Weights { get; set; }
public InlineArray55<float> Upsampling4Weights { get; set; }
public InlineArray210<float> Upsampling8Weights { get; set; }
public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
}

16
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 : byte
{
Identity = 1,
FlipHorizontal = 2,
Rotate180 = 3,
FlipVertical = 4,
Transponse = 5,
Rotate90 = 6,
AntiTranspose = 7,
Rotate270 = 8
}

25
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 : byte
{
Alpha,
Depth,
SpotColor,
SelectionMask,
Black,
Cfa,
Thermal,
Reserved0,
Reserved1,
Reserved2,
Reserved3,
Reserved4,
Reserved5,
Reserved6,
Reserved7,
Unknown,
Optional
}

27
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<float> SpotColor { get; set; }
public int CfaChannel { get; set; }
public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
}

128
src/ImageSharp/Formats/Jxl/Metadata/JxlImageMetadata.cs

@ -0,0 +1,128 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics;
using SixLabors.ImageSharp.Formats.Jxl.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<JxlExtraChannelInfo> ExtraChannels { get; set; } = [];
public JxlPreviewHeader PreviewSize { get; set; }
public JxlAnimationHeader Animation { get; set; }
public long Extensions { get; set; }
public bool NonserializedOnlyParseBasicInfos { get; set; }
public float IntensityTarget
{
get
{
float intensityTarget = this.ToneMapping?.IntensityTarget ?? 0f;
Debug.Assert(intensityTarget != 0f, "Intensity target should be present");
return intensityTarget;
}
set
{
if (this.ToneMapping != null)
{
this.ToneMapping.IntensityTarget = value;
}
}
}
public int AlphaBits
{
get
{
JxlExtraChannelInfo? ec = this.FindExtraChannel(JxlExtraChannel.Alpha);
if (ec == null)
{
return 0;
}
return ec.BitDepth?.BitsPerSample ?? 0;
}
set
{
}
}
public bool HasAlpha => this.AlphaBits != 0;
public JxlExtraChannelInfo? FindExtraChannel(JxlExtraChannel type)
=> this.ExtraChannels.FirstOrDefault(eci => eci.Type == type);
public JxlExifOrientation GetExifOrientation() => (JxlExifOrientation)this.Orientation;
public void SetFloat16Samples()
{
if (this.BitDepth != null)
{
this.BitDepth.BitsPerSample = 16;
this.BitDepth.ExponentBitsPerSample = 5;
this.BitDepth.FloatingPointSample = true;
}
this.Modular16BitBufferSufficient = false;
}
public void SetFloat32Samples()
{
if (this.BitDepth != null)
{
this.BitDepth.BitsPerSample = 32;
this.BitDepth.ExponentBitsPerSample = 8;
this.BitDepth.FloatingPointSample = true;
}
this.Modular16BitBufferSufficient = false;
}
public void SetUIntSamples(int bits)
{
if (this.BitDepth != null)
{
this.BitDepth.BitsPerSample = bits;
this.BitDepth.ExponentBitsPerSample = 0;
this.BitDepth.FloatingPointSample = false;
}
this.Modular16BitBufferSufficient = bits <= 12;
}
public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
}

19
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<float> OpsinBiases { get; set; }
public InlineArray4<float> QuantBiases { get; set; }
public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
}

68
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();
}

73
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();
}

21
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();
}

15
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<float> R { get; set; }
public ReadOnlyMemory<float> G { get; set; }
public ReadOnlyMemory<float> B { get; set; }
public ReadOnlyMemory<float> A { get; set; }
}

15
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<float> R { get; set; }
public Memory<float> G { get; set; }
public Memory<float> B { get; set; }
public Memory<float> A { get; set; }
}

192
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<T> from Memory<T> is expensive, especially
// in a loop.
ReadOnlySpan<float> fgR = foreground.R.Span;
ReadOnlySpan<float> fgG = foreground.G.Span;
ReadOnlySpan<float> fgB = foreground.B.Span;
ReadOnlySpan<float> fgA = foreground.A.Span;
ReadOnlySpan<float> bgR = background.R.Span;
ReadOnlySpan<float> bgG = background.G.Span;
ReadOnlySpan<float> bgB = background.B.Span;
ReadOnlySpan<float> bgA = background.A.Span;
Span<float> outR = output.R.Span;
Span<float> outG = output.G.Span;
Span<float> outB = output.B.Span;
Span<float> 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<float> bg,
ReadOnlySpan<float> bga,
ReadOnlySpan<float> fg,
ReadOnlySpan<float> fga,
Span<float> 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<float> bg,
ReadOnlySpan<float> fg,
ReadOnlySpan<float> fga,
Span<float> 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<float> bg,
ReadOnlySpan<float> fg,
Span<float> 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<float> r,
Span<float> g,
Span<float> b,
ReadOnlySpan<float> 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<float> r,
Span<float> g,
Span<float> b,
ReadOnlySpan<float> 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;
}
}
}

106
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<int> permutation, Span<uint> temp, int n, Span<uint> 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<uint> code, Span<uint> temp, int n, Span<int> 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;
}
}

28
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);
}
}

14
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;
}

14
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;
}

15
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);
}

58
src/ImageSharp/Formats/Jxl/Processing/JxlXorShift.cs

@ -0,0 +1,58 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Jxl.Processing;
internal sealed class JxlXorShift
{
private readonly ulong[] s0 = new ulong[8];
private readonly ulong[] s1 = new ulong[8];
public void XorShift128Plus(ulong seed)
{
this.s0[0] = SplitMix64(seed + 0x9E3779B97F4A7C15L);
this.s1[0] = SplitMix64(this.s0[0]);
for (int i = 1; i < 8; ++i)
{
this.s0[i] = SplitMix64(this.s1[i - 1]);
this.s1[i] = SplitMix64(this.s0[i]);
}
}
public void XorShift128Plus(uint seed1, uint seed2, uint seed3, uint seed4)
{
this.s0[0] = SplitMix64((((ulong)seed1 << 32) + seed2) + 0x9E3779B97F4A7C15uL);
this.s1[0] = SplitMix64((((ulong)seed3 << 32) + seed4) + 0x9E3779B97F4A7C15uL);
for (int i = 1; i < 8; ++i)
{
this.s0[i] = SplitMix64(this.s0[i - 1]);
this.s1[i] = SplitMix64(this.s1[i - 1]);
}
}
// TODO: SIMD
public void Fill(Span<ulong> 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);
}
}

17
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;
/// <summary>
/// A simple pair of first and second 32-bit signed integers
/// that represent a single control point.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct JxlControlPoint(int first, int second)
{
public int First = first;
public int Second = second;
}

343
src/ImageSharp/Formats/Jxl/Splines/JxlQuantizedSpline.cs

@ -0,0 +1,343 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Jxl.Splines;
internal sealed class JxlQuantizedSpline : IDisposable
{
private IMemoryOwner<JxlControlPoint>? memoryOwner;
public JxlQuantizedSpline()
{
for (int i = 0; i < 3; i++)
{
this.ColorDct[i] = new int[32];
}
}
private static ReadOnlySpan<int> Loops => [1, 0, 2];
private static ReadOnlySpan<float> ChannelWeight => [0.0042f, 0.075f, 0.07f, .3333f];
public Memory<JxlControlPoint> ControlPoints { get; set; }
// NOTE: Do not use Configuration.MemoryAllocator.Allocate2D. This is
// a 3x32 array, and renting memory introduces too much overhead for
// 384 bytes of memory.
// Additionally, prefer jagged arrays instead of multidimensional arrays
// for performance.
public int[][] ColorDct { get; set; } = new int[3][];
public int[] SigmaDct { get; set; } = new int[32];
public void Dispose()
{
this.memoryOwner?.Dispose();
this.ControlPoints = Memory<JxlControlPoint>.Empty;
GC.SuppressFinalize(this);
}
public void ReserveControlPoints(Configuration configuration, int n)
{
this.memoryOwner = configuration.MemoryAllocator.Allocate<JxlControlPoint>(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<JxlControlPoint> newControls = configuration.MemoryAllocator.Allocate<JxlControlPoint>(length);
Span<JxlControlPoint> 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<PointF> controlPoints = result.ControlPoints.Span;
Span<JxlControlPoint> 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<long> 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<byte> 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<JxlControlPoint> 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<byte> contextMap, Span<int> dct)
{
const int invalidCoefficient = int.MinValue;
for (int i = 0; i < 32; i++)
{
dct[i] = UnpackSigned(decoder.ReadHybridUnsignedInteger(DctContext, br, contextMap));
if (dct[i] == invalidCoefficient)
{
Debug.Fail("The DCT coefficient is invalid");
return false;
}
}
return true;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float AdjustedQuant(int adjustment)
=> (adjustment >= 0)
? (1f + (.125f * adjustment))
: 1f / (1f - (.125f * adjustment));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float InverseAdjustedQuant(int adjustment)
=> (adjustment >= 0)
? 1f / (1f + (.125f * adjustment))
: (1f - (.125f * adjustment));
}

39
src/ImageSharp/Formats/Jxl/Splines/JxlSpline.cs

@ -0,0 +1,39 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
namespace SixLabors.ImageSharp.Formats.Jxl.Splines;
internal sealed class JxlSpline : IDisposable
{
private IMemoryOwner<PointF>? controlPoints;
public Memory<PointF> 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<PointF>.Empty;
}
public void ReserveControlPoints(Configuration configuration, int n)
{
this.ClearControlPoints();
this.controlPoints = configuration.MemoryAllocator.Allocate<PointF>(n);
this.ControlPoints = this.controlPoints.Memory;
}
public void Dispose()
{
this.ClearControlPoints();
GC.SuppressFinalize(this);
}
}

13
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<JxlQuantizedSpline> Splines { get; set; } = [];
public List<PointF> StartingPoints { get; set; } = [];
public bool HasAny => this.Splines.Count > 0;
}

15
src/ImageSharp/Formats/Jxl/Splines/JxlSplineEntropyContext.cs

@ -0,0 +1,15 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Splines;
internal enum JxlSplineEntropyContext : byte
{
QuantizationAdjustment,
StartingPosition,
NumSplines,
NumControlPoints,
ControlPoints,
Dct,
NumSplineContexts
}

17
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<float> Color { get; set; }
}

11
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;
}
Loading…
Cancel
Save