Browse Source

Merge 37766b8d1a into 59ce6af6fc

pull/3153/merge
winscripter 3 weeks ago
committed by GitHub
parent
commit
e5759d3637
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 104
      src/ImageSharp/Common/Helpers/SimdUtils.cs
  2. 80
      src/ImageSharp/Common/Helpers/Vector128Utilities.cs
  3. 206
      src/ImageSharp/Common/Helpers/Vector256Utilities.cs
  4. 31
      src/ImageSharp/Common/InlineArray.cs
  5. 2
      src/ImageSharp/Common/InlineArray.tt
  6. 27
      src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs
  7. 77
      src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs
  8. 31
      src/ImageSharp/Formats/Jxl/Cms/JxlColorSpace.cs
  9. 109
      src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs
  10. 53
      src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs
  11. 26
      src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs
  12. 27
      src/ImageSharp/Formats/Jxl/Cms/JxlPrimaries.cs
  13. 13
      src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs
  14. 45
      src/ImageSharp/Formats/Jxl/Cms/JxlTransferFunction.cs
  15. 33
      src/ImageSharp/Formats/Jxl/Cms/JxlWhitePoint.cs
  16. 4
      src/ImageSharp/Formats/Jxl/Cms/README.md
  17. 57
      src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlBt709TransferFunction.cs
  18. 159
      src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHlgTransferFunction.cs
  19. 141
      src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPqTransferFunction.cs
  20. 82
      src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlSRgbTransferFunction.cs
  21. 71
      src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlTransferUtils.cs
  22. 17
      src/ImageSharp/Formats/Jxl/Fields/IJxlFields.cs
  23. 35
      src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs
  24. 50
      src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs
  25. 174
      src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs
  26. 118
      src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs
  27. 42
      src/ImageSharp/Formats/Jxl/Fields/JxlExtensionStates.cs
  28. 136
      src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs
  29. 21
      src/ImageSharp/Formats/Jxl/Fields/JxlFieldExpressions.cs
  30. 51
      src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs
  31. 121
      src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs
  32. 49
      src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs
  33. 154
      src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs
  34. 17
      src/ImageSharp/Formats/Jxl/Fields/JxlU32Distribution.cs
  35. 26
      src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs
  36. 160
      src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs
  37. 92
      src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs
  38. 76
      src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs
  39. 320
      src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs
  40. 85
      src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt
  41. 158
      src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs
  42. 58
      src/ImageSharp/Formats/Jxl/IO/Container/JxlFileTypeBox.cs
  43. 15
      src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsConstants.cs
  44. 16
      src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsEntry.cs
  45. 239
      src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs
  46. 63
      src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs
  47. 81
      src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsLz77Parameters.cs
  48. 14
      src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsSymbol.cs
  49. 74
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs
  50. 64
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendMode.cs
  51. 146
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendingInfo.cs
  52. 26
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransform.cs
  53. 31
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs
  54. 20
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameEncoding.cs
  55. 744
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs
  56. 36
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeaderFlags.cs
  57. 37
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameType.cs
  58. 220
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlPasses.cs
  59. 147
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlYCbCrChromaSubsampling.cs
  60. 30
      src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegAppMarkerType.cs
  61. 53
      src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponent.cs
  62. 16
      src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponentScanInfo.cs
  63. 15
      src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponentType.cs
  64. 1004
      src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs
  65. 105
      src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegDataConstants.cs
  66. 11
      src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegExtraZeroRunInfo.cs
  67. 30
      src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegHuffmanCode.cs
  68. 17
      src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegInfo.cs
  69. 31
      src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs
  70. 43
      src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegScanInfo.cs
  71. 187
      src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegBitWriter.cs
  72. 17
      src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegDctCodingState.cs
  73. 14
      src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegDebug.cs
  74. 22
      src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanCodeTable.cs
  75. 38
      src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs
  76. 13
      src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegReadMode.cs
  77. 1497
      src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegReader.cs
  78. 12
      src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegSerializationStage.cs
  79. 40
      src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegSerializationState.cs
  80. 12
      src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegSerializationStatus.cs
  81. 1304
      src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegWriter.cs
  82. 11
      src/ImageSharp/Formats/Jxl/IO/Jpeg/README.md
  83. 20
      src/ImageSharp/Formats/Jxl/IO/JxlBoxCodingMode.cs
  84. 31
      src/ImageSharp/Formats/Jxl/IO/JxlDataType.cs
  85. 184
      src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs
  86. 20
      src/ImageSharp/Formats/Jxl/IO/JxlHuffmanCode.cs
  87. 82
      src/ImageSharp/Formats/Jxl/IO/JxlMemoryWriter.cs
  88. 19
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlAnimationHeader.cs
  89. 129
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepthMetadata.cs
  90. 57
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCodecMetadata.cs
  91. 177
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs
  92. 16
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExifOrientation.cs
  93. 25
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannel.cs
  94. 152
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs
  95. 396
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs
  96. 83
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs
  97. 68
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlPreviewHeader.cs
  98. 74
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlSizeHeader.cs
  99. 99
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs
  100. 35
      src/ImageSharp/Formats/Jxl/InlineArrays.cs

104
src/ImageSharp/Common/Helpers/SimdUtils.cs

@ -1,9 +1,10 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86;
@ -71,6 +72,107 @@ internal static partial class SimdUtils
return val_2p23_f32 | sign;
}
/// <summary>
/// Estimates the reciprocal of this vector.
/// </summary>
/// <param name="v">The vector to get reciprocal estimate of.</param>
/// <returns>An estimated reciprocal of each element in the vector.</returns>
internal static Vector<float> ReciprocalEstimate(this Vector<float> v)
{
// TODO: System.Runtime.Intrinsics.Arm has Sve and Sve2
// support but is for evaluation purposes only; add SVE/SVE2
// support when possible
if (Avx512F.IsSupported && Vector<float>.Count == 16)
{
// x86
return Avx512F.Reciprocal14(v.AsVector512()).AsVector();
}
else if (Avx.IsSupported && Vector<float>.Count == 8)
{
// x86
return Avx.Reciprocal(v.AsVector256()).AsVector();
}
else if (AdvSimd.IsSupported && Vector<float>.Count == 4)
{
// ARM
return AdvSimd.ReciprocalEstimate(v.AsVector128()).AsVector();
}
else if (Sse.IsSupported && Vector<float>.Count == 4)
{
// x86
return Sse.Reciprocal(v.AsVector128()).AsVector();
}
else
{
// Exact reciprocal fallback (slower)
return Vector<float>.One / v;
}
}
/// <summary>
/// Finds the leading zero count on each item of the vector.
/// </summary>
/// <param name="vector">The vector to compute leading zero count for.</param>
/// <returns>Input vector with leading zero count on each element.</returns>
internal static Vector<int> Lzcnt(Vector<int> vector)
{
if (Avx512CD.IsSupported && Vector<int>.Count == 16)
{
Vector512<int> v512 = vector.AsVector512();
Vector512<int> lzcnt = Avx512CD.LeadingZeroCount(v512);
return lzcnt.AsVector();
}
else if (AdvSimd.IsSupported && Vector<int>.Count == 4)
{
Vector128<int> v128 = vector.AsVector128();
Vector128<int> lzcnt = AdvSimd.LeadingZeroCount(v128);
return lzcnt.AsVector();
}
else
{
Span<int> data = stackalloc int[Vector<int>.Count];
ref int dataPtr = ref MemoryMarshal.GetReference(data);
vector.StoreUnsafe(ref dataPtr);
for (int i = 0; i < data.Length; i++)
{
Unsafe.Add(ref dataPtr, i) = BitOperations.LeadingZeroCount((nuint)Unsafe.Add(ref dataPtr, i));
}
return Vector.LoadUnsafe(ref dataPtr);
}
}
/// <summary>
/// Raises 2 to the power of each item in the vector.
/// </summary>
/// <param name="vector">Input vector.</param>
/// <returns>2^x for each item in the vector.</returns>
internal static Vector<int> Pow2(Vector<int> vector)
{
if (Avx512F.IsSupported && Vector<int>.Count == 16)
{
return Avx512F.ShiftLeftLogicalVariable(Vector512<int>.One, vector.AsVector512().AsUInt32()).AsVector();
}
else if (Avx2.IsSupported && Vector<int>.Count == 8)
{
return Avx2.ShiftLeftLogicalVariable(Vector256<int>.One, vector.AsVector256().AsUInt32()).AsVector();
}
else
{
Span<int> data = stackalloc int[Vector<int>.Count];
ref int dataPtr = ref MemoryMarshal.GetReference(data);
vector.StoreUnsafe(ref dataPtr);
for (int i = 0; i < data.Length; i++)
{
Unsafe.Add(ref dataPtr, i) = 1 << Unsafe.Add(ref dataPtr, i);
}
return Vector.LoadUnsafe(ref dataPtr);
}
}
[Conditional("DEBUG")]
private static void DebugVerifySpanInput(ReadOnlySpan<byte> source, ReadOnlySpan<float> dest, int shouldBeDivisibleBy)
{

80
src/ImageSharp/Common/Helpers/Vector128Utilities.cs

@ -859,4 +859,84 @@ internal static class Vector128_
Vector128<sbyte> unpacked = Vector128.Create(left.GetLower(), right.GetLower());
return Vector128.ShuffleNative(unpacked, Vector128.Create(0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15));
}
/// <summary>
/// Interleaves the lower half of the vector.
/// </summary>
/// <param name="a">First vector</param>
/// <param name="b">Second vector</param>
/// <returns>
/// <c>{ a[0], b[0], a[1], b[1] }</c>
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<int> InterleaveLower(Vector128<int> a, Vector128<int> b)
{
Vector128<int> shuffledA = Vector128.Shuffle(a, Vector128.Create(0, 0, 1, 1));
Vector128<int> shuffledB = Vector128.Shuffle(b, Vector128.Create(0, 0, 1, 1));
Vector128<int> maskA = Vector128.Create(-1, 0, -1, 0);
Vector128<int> maskB = Vector128.Create(0, -1, 0, -1);
return (shuffledA & maskA) | (shuffledB & maskB);
}
/// <summary>
/// Interleaves the upper half of the vector.
/// </summary>
/// <param name="a">First vector</param>
/// <param name="b">Second vector</param>
/// <returns>
/// <c>{ a[2], b[2], a[3], b[3] }</c>
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<int> InterleaveUpper(Vector128<int> a, Vector128<int> b)
{
Vector128<int> shuffledA = Vector128.Shuffle(a, Vector128.Create(2, 2, 3, 3));
Vector128<int> shuffledB = Vector128.Shuffle(b, Vector128.Create(2, 2, 3, 3));
Vector128<int> maskA = Vector128.Create(-1, 0, -1, 0);
Vector128<int> maskB = Vector128.Create(0, -1, 0, -1);
return (shuffledA & maskA) | (shuffledB & maskB);
}
/// <summary>
/// Interleaves the lower half of the vector.
/// </summary>
/// <param name="a">First vector</param>
/// <param name="b">Second vector</param>
/// <returns>
/// <c>{ a[0], b[0], a[1], b[1] }</c>
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<float> InterleaveLower(Vector128<float> a, Vector128<float> b)
{
Vector128<float> shuffledA = Vector128.Shuffle(a, Vector128.Create(0, 0, 1, 1));
Vector128<float> shuffledB = Vector128.Shuffle(b, Vector128.Create(0, 0, 1, 1));
Vector128<float> maskA = Vector128.Create(-1, 0, -1, 0).AsSingle();
Vector128<float> maskB = Vector128.Create(0, -1, 0, -1).AsSingle();
return (shuffledA & maskA) | (shuffledB & maskB);
}
/// <summary>
/// Interleaves the upper half of the vector.
/// </summary>
/// <param name="a">First vector</param>
/// <param name="b">Second vector</param>
/// <returns>
/// <c>{ a[2], b[2], a[3], b[3] }</c>
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<float> InterleaveUpper(Vector128<float> a, Vector128<float> b)
{
Vector128<float> shuffledA = Vector128.Shuffle(a, Vector128.Create(2, 2, 3, 3));
Vector128<float> shuffledB = Vector128.Shuffle(b, Vector128.Create(2, 2, 3, 3));
Vector128<float> maskA = Vector128.Create(-1, 0, -1, 0).AsSingle();
Vector128<float> maskB = Vector128.Create(0, -1, 0, -1).AsSingle();
return (shuffledA & maskA) | (shuffledB & maskB);
}
}

206
src/ImageSharp/Common/Helpers/Vector256Utilities.cs

@ -397,4 +397,210 @@ internal static class Vector256_
return Vector256.Create(lo, hi);
}
/// <summary>
/// Multiplies only the even indices of the two 256-bit vectors,
/// producing half as many elements of twice the element width.
/// </summary>
/// <param name="left">Left vector to multiply.</param>
/// <param name="right">Right vector to multiply</param>
/// <returns>
/// <code>
/// {
/// A[0] * B[0],
/// A[2] * B[2],
/// A[4] * B[4],
/// A[6] * B[6]
/// }
/// </code>
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<long> MultiplyEven(Vector256<int> left, Vector256<int> right)
=> Vector256.Create(
left[0] * right[0],
left[2] * right[2],
left[4] * right[4],
left[6] * right[6]);
/// <summary>
/// Multiplies only the odd indices of the two 256-bit vectors,
/// producing half as many elements of twice the element width.
/// </summary>
/// <param name="left">Left vector to multiply.</param>
/// <param name="right">Right vector to multiply</param>
/// <returns>
/// <code>
/// {
/// A[1] * B[1],
/// A[3] * B[3],
/// A[5] * B[5],
/// A[7] * B[7]
/// }
/// </code>
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<long> MultiplyOdd(Vector256<int> left, Vector256<int> right)
=> Vector256.Create(
left[1] * right[1],
left[3] * right[3],
left[5] * right[5],
left[7] * right[7]);
/// <summary>
/// Produces a vector by interleaving the even-indexed elements
/// of the left and right vectors.
/// </summary>
/// <param name="left">Left vector to interleave.</param>
/// <param name="right">Right vector to interleave.</param>
/// <returns>
/// <code>
/// {
/// A[0], B[0],
/// A[2], B[2],
/// A[4], B[4],
/// A[6], B[6]
/// }
/// </code>
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<int> InterleaveEven(
Vector256<int> left,
Vector256<int> right)
=> Vector256.Create(
left[0],
right[0],
left[2],
right[2],
left[4],
right[4],
left[6],
right[6]);
/// <summary>
/// Produces a vector by interleaving the odd-indexed elements
/// of the left and right vectors.
/// </summary>
/// <param name="left">Left vector to interleave.</param>
/// <param name="right">Right vector to interleave.</param>
/// <returns>
/// <code>
/// {
/// A[1], B[1],
/// A[3], B[3],
/// A[5], B[5],
/// A[7], B[7]
/// }
/// </code>
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<int> InterleaveOdd(
Vector256<int> left,
Vector256<int> right)
=> Vector256.Create(
left[1],
right[1],
left[3],
right[3],
left[5],
right[5],
left[7],
right[7]);
/// <summary>
/// Produces a vector with masks where 0xFFFFFFFF specifies
/// that the left value does not equal to the right value and
/// 0x00000000 specifies that the value equals to the
/// right value.
/// </summary>
/// <param name="left">Left vector to compare for inequality.</param>
/// <param name="right">Right vector to compare for inequality.</param>
/// <returns>
/// 0xFFFFFFFF for values that aren't equal, 0x00000000 for
/// values that are equal. This is essentially the inverse of
/// <see cref="Vector256.Equals{T}(Vector256{T}, Vector256{T})"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<int> NotEqual(
Vector256<int> left,
Vector256<int> right) => ~Vector256.Equals(left, right);
/// <summary>
/// Interleaves the lower half of the vector.
/// </summary>
/// <param name="a">First vector</param>
/// <param name="b">Second vector</param>
/// <returns>
/// <c>{ a[0], b[0], a[1], b[1], a[2], b[2], a[3], b[3] }</c>
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<int> InterleaveLower(Vector256<int> a, Vector256<int> b)
{
Vector256<int> shuffledA = Vector256.Shuffle(a, Vector256.Create(0, 0, 1, 1, 2, 2, 3, 3));
Vector256<int> shuffledB = Vector256.Shuffle(b, Vector256.Create(0, 0, 1, 1, 2, 2, 3, 3));
Vector256<int> maskA = Vector256.Create(-1, 0, -1, 0, -1, 0, -1, 0);
Vector256<int> maskB = Vector256.Create(0, -1, 0, -1, 0, -1, 0, -1);
return (shuffledA & maskA) | (shuffledB & maskB);
}
/// <summary>
/// Interleaves the upper half of the vector.
/// </summary>
/// <param name="a">First vector</param>
/// <param name="b">Second vector</param>
/// <returns>
/// <c>{ a[4], b[4], a[5], b[5], a[6], b[6], a[7], b[7] }</c>
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<int> InterleaveUpper(Vector256<int> a, Vector256<int> b)
{
Vector256<int> shuffledA = Vector256.Shuffle(a, Vector256.Create(4, 4, 5, 5, 6, 6, 7, 7));
Vector256<int> shuffledB = Vector256.Shuffle(b, Vector256.Create(4, 4, 5, 5, 6, 6, 7, 7));
Vector256<int> maskA = Vector256.Create(-1, 0, -1, 0, -1, 0, -1, 0);
Vector256<int> maskB = Vector256.Create(0, -1, 0, -1, 0, -1, 0, -1);
return (shuffledA & maskA) | (shuffledB & maskB);
}
/// <summary>
/// Interleaves the lower half of the vector.
/// </summary>
/// <param name="a">First vector</param>
/// <param name="b">Second vector</param>
/// <returns>
/// <c>{ a[0], b[0], a[1], b[1], a[2], b[2], a[3], b[3] }</c>
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<float> InterleaveLower(Vector256<float> a, Vector256<float> b)
{
Vector256<float> shuffledA = Vector256.Shuffle(a, Vector256.Create(0, 0, 1, 1, 2, 2, 3, 3));
Vector256<float> shuffledB = Vector256.Shuffle(b, Vector256.Create(0, 0, 1, 1, 2, 2, 3, 3));
Vector256<float> maskA = Vector256.Create(-1, 0, -1, 0, -1, 0, -1, 0).AsSingle();
Vector256<float> maskB = Vector256.Create(0, -1, 0, -1, 0, -1, 0, -1).AsSingle();
return (shuffledA & maskA) | (shuffledB & maskB);
}
/// <summary>
/// Interleaves the upper half of the vector.
/// </summary>
/// <param name="a">First vector</param>
/// <param name="b">Second vector</param>
/// <returns>
/// <c>{ a[4], b[4], a[5], b[5], a[6], b[6], a[7], b[7] }</c>
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<float> InterleaveUpper(Vector256<float> a, Vector256<float> b)
{
Vector256<float> shuffledA = Vector256.Shuffle(a, Vector256.Create(4, 4, 5, 5, 6, 6, 7, 7));
Vector256<float> shuffledB = Vector256.Shuffle(b, Vector256.Create(4, 4, 5, 5, 6, 6, 7, 7));
Vector256<float> maskA = Vector256.Create(-1, 0, -1, 0, -1, 0, -1, 0).AsSingle();
Vector256<float> maskB = Vector256.Create(0, -1, 0, -1, 0, -1, 0, -1).AsSingle();
return (shuffledA & maskA) | (shuffledB & maskB);
}
}

31
src/ImageSharp/Common/InlineArray.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
// <auto-generated />
@ -44,6 +44,15 @@ internal struct InlineArray16<T>
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 17 elements.
/// </summary>
[InlineArray(17)]
internal struct InlineArray17<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 18 elements.
/// </summary>
@ -71,6 +80,24 @@ internal struct InlineArray26<T>
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 32 elements.
/// </summary>
[InlineArray(32)]
internal struct InlineArray32<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 33 elements.
/// </summary>
[InlineArray(33)]
internal struct InlineArray33<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 36 elements.
/// </summary>
@ -88,3 +115,5 @@ internal struct InlineArray256<T>
{
private T t;
}

2
src/ImageSharp/Common/InlineArray.tt

@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp;
<#GenerateInlineArrays();#>
<#+
private static int[] Lengths = [4, 8, 14, 16, 18, 19, 26, 36, 256];
private static int[] Lengths = [4, 8, 14, 16, 17, 18, 19, 26, 32, 33, 36, 256];
void GenerateInlineArrays()
{

27
src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs

@ -0,0 +1,27 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.ColorProfiles;
namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
/// <summary>
/// RGB primaries for CIEXY
/// </summary>
internal struct JxlCieXyPrimaries
{
/// <summary>
/// Gets or sets the R component
/// </summary>
public CieXyChromaticityCoordinates R { get; set; }
/// <summary>
/// Gets or sets the G component
/// </summary>
public CieXyChromaticityCoordinates G { get; set; }
/// <summary>
/// Gets or sets the B component
/// </summary>
public CieXyChromaticityCoordinates B { get; set; }
}

77
src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs

@ -0,0 +1,77 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
internal sealed class JxlColorEncoding
{
public JxlWhitePoint WhitePoint { get; set; } = JxlWhitePoint.D65;
public JxlPrimaries Primaries { get; set; } = JxlPrimaries.SRgb;
public JxlRenderingIntent RenderingIntent { get; set; } = JxlRenderingIntent.Relative;
public bool HaveFields { get; set; } = true;
public JxlIccBytes? Icc { get; set; }
public JxlColorSpace ColorSpace { get; set; } = JxlColorSpace.Rgb;
public bool Cmyk { get; set; }
public JxlCustomTransferFunction TransferFunction { get; set; }
public JxlCustomXy White { get; set; }
public JxlCustomXy Red { get; set; }
public JxlCustomXy Green { get; set; }
public JxlCustomXy Blue { get; set; }
public bool HasPrimaries => this.ColorSpace is not (JxlColorSpace.Gray or JxlColorSpace.Xyb);
public int Channels => (this.ColorSpace == JxlColorSpace.Gray) ? 1 : 3;
public bool TryGetPrimaries(out JxlCieXyPrimaries xy)
{
xy = default;
if (!this.HasPrimaries || !this.HasPrimaries)
{
return false;
}
switch (this.Primaries)
{
case JxlPrimaries.Custom:
xy.R = this.Red.GetValue();
xy.G = this.Green.GetValue();
xy.B = this.Blue.GetValue();
break;
case JxlPrimaries.SRgb:
xy.R = new(0.639998686f, 0.330010138f);
xy.G = new(0.300003784f, 0.600003357f);
xy.B = new(0.150002046f, 0.059997204f);
break;
case JxlPrimaries.Bt2020:
xy.R = new(0.708f, 0.292f);
xy.G = new(0.170f, 0.797f);
xy.B = new(0.131f, 0.046f);
break;
case JxlPrimaries.P3:
xy.R = new(0.680f, 0.320f);
xy.G = new(0.265f, 0.690f);
xy.B = new(0.150f, 0.060f);
break;
default:
throw new InvalidOperationException("Invalid primaries: " + this.Primaries);
}
return true;
}
}

31
src/ImageSharp/Formats/Jxl/Cms/JxlColorSpace.cs

@ -0,0 +1,31 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
/// <summary>
/// Supported, JPEG XL-specific color space types.
/// </summary>
internal enum JxlColorSpace : byte
{
/// <summary>
/// Trichromatic color data. This also includes CMYK if Black
/// ExtraChannelInfo is present.
/// </summary>
Rgb,
/// <summary>
/// Single-channel data.
/// </summary>
Gray,
/// <summary>
/// Like Rgb but fixed values for primaries.
/// </summary>
Xyb,
/// <summary>
/// Unknown color space
/// </summary>
Unknown
}

109
src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs

@ -0,0 +1,109 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
internal struct JxlCustomTransferFunction
{
private const uint MaxGamma = 8192;
private const uint GammaMultiplier = 10000000;
public JxlCustomTransferFunction()
{
}
public bool HaveGamma { get; set; }
public uint Gamma { get; set; }
public JxlTransferFunction TransferFunction { get; set; } = JxlTransferFunction.SRgb;
public readonly bool IsUnknown => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Unknown;
public readonly bool IsSrgb => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.SRgb;
public readonly bool IsLinear => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Linear;
public readonly bool IsPq => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Pq;
public readonly bool IsHlg => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Hlg;
public readonly bool Is709 => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Bt709;
public readonly bool IsDci => !this.HaveGamma && this.TransferFunction == JxlTransferFunction.Dci;
public readonly JxlTransferFunction GetTransferFunction()
{
if (this.HaveGamma)
{
return JxlTransferFunction.Unknown;
}
return this.TransferFunction;
}
public void SetTransferFunction(JxlTransferFunction tf)
{
this.HaveGamma = false;
this.TransferFunction = tf;
}
public readonly float GetGamma()
{
if (!this.HaveGamma)
{
return 0.0f;
}
return this.Gamma * (1.0f / GammaMultiplier);
}
public void SetGamma(float newGamma)
{
if (newGamma is < 1.0f / MaxGamma or > 1.0f)
{
throw new InvalidOperationException($"Invalid gamma {newGamma}");
}
this.HaveGamma = false;
if (IsAlmostEqual(newGamma, 1.0f))
{
this.TransferFunction = JxlTransferFunction.Linear;
return;
}
if (IsAlmostEqual(newGamma, 1.0f / 2.6f))
{
this.TransferFunction = JxlTransferFunction.Dci;
return;
}
// Don't translate 0.45.. to kSRGB nor k709 - that might change pixel
// values because those curves also have a linear part.
this.HaveGamma = true;
this.Gamma = (uint)MathF.Round((float)(newGamma * GammaMultiplier));
this.TransferFunction = JxlTransferFunction.Unknown;
}
public readonly bool IsSame(JxlCustomTransferFunction other)
{
if (this.HaveGamma != other.HaveGamma)
{
return false;
}
if (this.HaveGamma)
{
return this.Gamma == other.Gamma;
}
return this.TransferFunction == other.TransferFunction;
}
private static bool IsAlmostEqual(float a, float b)
{
const float dist = 1e-3f;
return MathF.Abs(a - b) < dist;
}
}

53
src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs

@ -0,0 +1,53 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.ColorProfiles;
namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
/// <summary>
/// A serializable form of CieXyChromaticityCoordinates
/// </summary>
internal struct JxlCustomXy
{
private const uint Multiplier = 1000000;
private const float RoughLimit = 4.0f;
private const int Min = -0x200000;
private const int Max = 0x1FFFFF;
public int X { get; set; }
public int Y { get; set; }
public readonly CieXyChromaticityCoordinates GetValue() => new(
x: this.X * (1.0f / Multiplier),
y: this.Y * (1.0f / Multiplier));
public bool SetValue(CieXyChromaticityCoordinates xy)
{
bool ok = (Math.Abs(xy.X) < RoughLimit) && (Math.Abs(xy.Y) < RoughLimit);
if (!ok)
{
throw new InvalidOperationException("X or Y is out of bounds");
}
this.X = (int)MathF.Round((float)(xy.X * Multiplier));
if (this.X is < Min or > Max)
{
throw new InvalidOperationException("X is out of bounds");
}
this.Y = (int)MathF.Round((float)(xy.Y * Multiplier));
if (this.Y is < Min or > Max)
{
throw new InvalidOperationException("Y is out of bounds");
}
return true;
}
public readonly bool IsSame(CieXyChromaticityCoordinates other) => this.X == other.X && this.Y == other.Y;
}

26
src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs

@ -0,0 +1,26 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
/// <summary>
/// Opsin constants used by the color management system
/// </summary>
internal static class JxlOpsinConstants
{
public const float BScale = 1f;
// The following constants are used for XYB.
// They can be adjusted to change how Y<->B ratio
// works. For example, YToBRatio works better
// with 0.50017729543783418.
public const float YToBRatio = 1f;
public const float BToYRatio = 1f / YToBRatio;
// Adjusting these constants influences the opsin absorbance.
public const float OpsinAbsorbanceBias0 = 0.0037930732552754493f;
public const float OpsinAbsorbanceBias1 = OpsinAbsorbanceBias0;
public const float OpsinAbsorbanceBias2 = OpsinAbsorbanceBias0;
public static ReadOnlySpan<float> OpsinAbsorbanceBias => [OpsinAbsorbanceBias0, OpsinAbsorbanceBias1, OpsinAbsorbanceBias2];
}

27
src/ImageSharp/Formats/Jxl/Cms/JxlPrimaries.cs

@ -0,0 +1,27 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
/// <summary>
/// JPEG XL primaries
/// </summary>
internal enum JxlPrimaries : byte
{
/// <summary>
/// Same as ITU-R BT.709
/// </summary>
SRgb = 1,
/// <summary>
/// Values encoded in separate fields
/// </summary>
Custom = 2,
/// <summary>
/// ITU-R BT.2020
/// </summary>
Bt2020 = 9,
P3 = 11,
}

13
src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs

@ -0,0 +1,13 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
internal enum JxlRenderingIntent : byte
{
// Values match ICC sRGB encodings
Perceptual, // Good for photos, requires a profile with LUT
Relative, // Good for logos
Saturation, // Perhaps useful for CG with fully saturated colors
Absolute, // Leaves white point unchanged; good for proofing
}

45
src/ImageSharp/Formats/Jxl/Cms/JxlTransferFunction.cs

@ -0,0 +1,45 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
/// <summary>
/// JPEG XL transfer function type
/// </summary>
internal enum JxlTransferFunction : byte
{
/// <summary>
/// ITU-R BT.709
/// </summary>
Bt709 = 1,
/// <summary>
/// Unknown transfer function
/// </summary>
Unknown = 2,
/// <summary>
/// Linear transfer function
/// </summary>
Linear = 8,
/// <summary>
/// sRGB
/// </summary>
SRgb = 13,
/// <summary>
/// From ITU-R BT.2100
/// </summary>
Pq = 16,
/// <summary>
/// From SMPTE RP 431-2 reference projector
/// </summary>
Dci = 17,
/// <summary>
/// From ITU-R BT.2100
/// </summary>
Hlg = 18,
}

33
src/ImageSharp/Formats/Jxl/Cms/JxlWhitePoint.cs

@ -0,0 +1,33 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Cms;
/// <summary>
/// White point from CICP Color Primaries.
/// </summary>
// Note that we define a separate enum instead of using the ColorPrimaries
// enum from CICP code because JPEG XL doesn't support all color primaries defined
// by CICP.
internal enum JxlWhitePoint : byte
{
/// <summary>
/// sRGB/ITU-R BT.709/Display P3/ITU-R BT.2020
/// </summary>
D65 = 1,
/// <summary>
/// Actual values encoded in separate fields
/// </summary>
Custom = 2,
/// <summary>
/// XYZ
/// </summary>
E = 10,
/// <summary>
/// DCI-P3
/// </summary>
Dci = 11,
}

4
src/ImageSharp/Formats/Jxl/Cms/README.md

@ -0,0 +1,4 @@
# CMS
This is the JPEG XL Color Management System component.
Not to be confused with Content Management System.

57
src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlBt709TransferFunction.cs

@ -0,0 +1,57 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Jxl.Processing;
namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions;
/// <summary>
/// ITU-R BT.709 transfer function
/// </summary>
internal static class JxlBt709TransferFunction
{
// Encoded From Display constants
private const float Threshold = 0.018f;
private const float MulLow = 4.5f;
private const float MulHi = 1.099f;
private const float PowHi = 0.45f;
private const float Sub = -0.099f;
// Display From Encoded constants
private const float InverseThreshold = 0.081f;
private const float InverseMulLow = 1f / 4.5f;
private const float InverseMulHi = 1f / 1.099f;
private const float InversePowHi = 1f / 0.45f;
private const float InverseAdd = 0.099f * InverseMulHi;
public static float EncodedFromDisplay(float d)
{
if (d < Threshold)
{
return MulLow * d;
}
return (MulHi * MathF.Pow(d, PowHi)) + Sub;
}
public static Vector<float> EncodedFromDisplay(Vector<float> x)
{
Vector<float> low = Vector.Create(MulLow) * x;
Vector<float> high = (Vector.Create(MulHi) * JxlSimdUtils.FastPowf(x, Vector.Create(PowHi))) + Vector.Create(Sub);
return Vector.ConditionalSelect(
Vector.LessThanOrEqual(x, Vector.Create(Threshold)),
low,
high);
}
public static Vector<float> DisplayFromEncoded(Vector<float> x)
{
Vector<float> low = Vector.Create(InverseMulLow) * x;
Vector<float> high = JxlSimdUtils.FastPowf((x * Vector.Create(InverseMulHi)) + Vector.Create(InverseAdd), Vector.Create(InversePowHi));
return Vector.ConditionalSelect(
Vector.LessThan(x, Vector.Create(InverseThreshold)),
low,
high);
}
}

159
src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlHlgTransferFunction.cs

@ -0,0 +1,159 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Jxl.Processing;
namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions;
/// <summary>
/// Hybrid Log Gamma transfer function.
/// </summary>
internal sealed class JxlHlgTransferFunction
{
// Shared constants used by transfer functions
private const float A = 0.17883277f;
private const float RA = 1.0f / A;
private const float B = 1 - (4 * A);
private const float C = 0.5599107295f;
private const float Inverse12 = 1.0f / 12.0f;
private const float HiAdd = B * Inverse12;
private const float HiMul = 0.003639807079052639f; // MathF.Exp(-C * RA) * Inverse12
private const float HiPow = 8.067285659607931f; // RA * JxlMath.InverseLog2E
/// <summary>
/// Initializes a new instance of the <see cref="JxlHlgTransferFunction"/> class.
/// </summary>
/// <remarks>
/// Use static methods. Don't instantiate this class.
/// </remarks>
private JxlHlgTransferFunction()
{
}
public static Vector<float> EncodedFromDisplay(Vector<float> x)
{
Vector<float> sign = Vector.Create(0x80000000u).As<uint, float>();
Vector<float> originalSign = x & sign;
x = Vector.AndNot(sign, x);
Vector<int> belowInverse12 = Vector.LessThan(x, Vector.Create(Inverse12));
Vector<float> lo = Vector.SquareRoot(Vector.Create(3.0f) * x);
Vector<float> hi = (Vector.Create(A * JxlMath.InverseLog2E) * Vector.Log2((Vector.Create(12f) * x) + Vector.Create(-B))) + Vector.Create(C);
Vector<float> magnitude = Vector.ConditionalSelect(belowInverse12, lo, hi);
return Vector.AndNot(sign, magnitude) | originalSign;
}
public static Vector<float> DisplayFromEncoded(Vector<float> x)
{
Vector<float> sign = Vector.Create(0x80000000u).As<uint, float>();
Vector<float> originalSign = x & sign;
x = Vector.AndNot(sign, x);
Vector<int> below05 = Vector.LessThan(x, Vector.Create(0.5f));
Vector<float> lo = x * (x * Vector.Create(1f / 3f));
Vector<float> hi = (Pow2(x * Vector.Create(HiPow)) * Vector.Create(HiMul)) + Vector.Create(HiAdd);
Vector<float> magnitude = Vector.ConditionalSelect(below05, lo, hi);
return Vector.AndNot(sign, magnitude) | originalSign;
}
private static Vector<float> Pow2(Vector<float> x) => JxlSimdUtils.FastPow2f(x);
/// <summary>
/// Converts encoded signal to display signal.
/// </summary>
/// <param name="encoded">The encoded signal</param>
/// <returns>The display signal</returns>
public static double DisplayFromEncoded(double encoded) => Ootf(InverseOotf(encoded));
/// <summary>
/// Converts display signal to encoded signal.
/// </summary>
/// <param name="display">The display signal</param>
/// <returns>The encoded signal</returns>
public static double EncodedFromDisplay(double display) => Oetf(InverseOetf(display));
/// <summary>
/// Opto-Electronic Transfer Function - converts
/// real-world scene light (s) into a digital video
/// signal inside a camera.
/// </summary>
/// <remarks>
/// <seealso href="https://en.wikipedia.org/wiki/Transfer_functions_in_imaging#Definition"/>
/// </remarks>
/// <param name="s">Scene light</param>
/// <returns>Digital video signal</returns>
public static double Oetf(double s)
{
if (s == 0)
{
return 0;
}
double originalSign = s;
s = Math.Abs(s);
if (s <= Inverse12)
{
return Math.CopySign(Math.Sqrt(3.0 * s), originalSign);
}
double e = (A * Math.Log((12 * s) - B)) + C;
DebugGuard.MustBeGreaterThan(e, 0.0, nameof(e));
return Math.CopySign(e, originalSign);
}
/// <summary>
/// Inverse Opto-Electronic Transfer Function - converts
/// digital video signal into a real-world scene light.
/// </summary>
/// <remarks>
/// <seealso href="https://en.wikipedia.org/wiki/Transfer_functions_in_imaging#Definition"/>
/// </remarks>
/// <param name="e">Digital video signal</param>
/// <returns>Scene light</returns>
public static double InverseOetf(double e)
{
if (e == 0)
{
return 0;
}
double originalSign = e;
e = Math.Abs(e);
if (e <= 0.5)
{
return Math.CopySign(e * e * (1.0 / 3), originalSign);
}
double s = (Math.Exp((e - C) * RA) + B) * Inverse12;
DebugGuard.MustBeGreaterThan(s, 0.0, nameof(s));
return Math.CopySign(s, originalSign);
}
/// <summary>
/// Opto-Optical Transfer Function - as-is.
/// </summary>
/// <remarks>
/// <seealso href="https://en.wikipedia.org/wiki/Transfer_functions_in_imaging#Definition"/>
/// </remarks>
/// <param name="s">Input signal</param>
/// <returns>Digital video signal</returns>
public static double Ootf(double s) => s;
/// <summary>
/// Inverse Opto-Optical Transfer Function - as-is.
/// </summary>
/// <remarks>
/// <seealso href="https://en.wikipedia.org/wiki/Transfer_functions_in_imaging#Definition"/>
/// </remarks>
/// <param name="s">Digital video signal</param>
/// <returns>Scene light</returns>
public static double InverseOotf(double s) => s;
}

141
src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlPqTransferFunction.cs

@ -0,0 +1,141 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions;
/// <summary>
/// Perceptual Quantization transfer function.
/// </summary>
internal struct JxlPqTransferFunction(float displayIntensityTarget = DefaultIntensityTarget)
{
private const double M1 = 2610.0 / 16384;
private const double M2 = (2523.0 / 4096) * 128;
private const double C1 = 3424.0 / 4096;
private const double C2 = (2413.0 / 4096) * 32;
private const double C3 = (2392.0 / 4096) * 32;
private readonly float scaleFactorTo10000Nits = displayIntensityTarget * (1.0f / 10000.0f);
private readonly float scaleFactorFrom10000Nits = 10000.0f / displayIntensityTarget;
private static ReadOnlySpan<float> DisplayP =>
[
2.62975656e-04f, 2.62975656e-04f, 2.62975656e-04f, 2.62975656e-04f,
-6.23553089e-03f, -6.23553089e-03f, -6.23553089e-03f, -6.23553089e-03f,
7.38602301e-01f, 7.38602301e-01f, 7.38602301e-01f, 7.38602301e-01f,
2.64553172e+00f, 2.64553172e+00f, 2.64553172e+00f, 2.64553172e+00f,
5.50034862e-01f, 5.50034862e-01f, 5.50034862e-01f, 5.50034862e-01f
];
private static ReadOnlySpan<float> DisplayQ =>
[
4.21350107e+02f, 4.21350107e+02f, 4.21350107e+02f, 4.21350107e+02f,
-4.28736818e+02f, -4.28736818e+02f, -4.28736818e+02f, -4.28736818e+02f,
1.74364667e+02f, 1.74364667e+02f, 1.74364667e+02f, 1.74364667e+02f,
-3.39078883e+01f, -3.39078883e+01f, -3.39078883e+01f, -3.39078883e+01f,
2.67718770e+00f, 2.67718770e+00f, 2.67718770e+00f, 2.67718770e+00f
];
private static ReadOnlySpan<float> EncodedP =>
[
1.351392e-02f, 1.351392e-02f, 1.351392e-02f, 1.351392e-02f,
-1.095778e+00f, -1.095778e+00f, -1.095778e+00f, -1.095778e+00f,
5.522776e+01f, 5.522776e+01f, 5.522776e+01f, 5.522776e+01f,
1.492516e+02f, 1.492516e+02f, 1.492516e+02f, 1.492516e+02f,
4.838434e+01f, 4.838434e+01f, 4.838434e+01f, 4.838434e+01f
];
private static ReadOnlySpan<float> EncodedQ =>
[
1.012416e+00f, 1.012416e+00f, 1.012416e+00f, 1.012416e+00f,
2.016708e+01f, 2.016708e+01f, 2.016708e+01f, 2.016708e+01f,
9.263710e+01f, 9.263710e+01f, 9.263710e+01f, 9.263710e+01f,
1.120607e+02f, 1.120607e+02f, 1.120607e+02f, 1.120607e+02f,
2.590418e+01f, 2.590418e+01f, 2.590418e+01f, 2.590418e+01f
];
private static ReadOnlySpan<float> EncodedPLo =>
[
9.863406e-06f, 9.863406e-06f, 9.863406e-06f, 9.863406e-06f,
3.881234e-01f, 3.881234e-01f, 3.881234e-01f, 3.881234e-01f,
1.352821e+02f, 1.352821e+02f, 1.352821e+02f, 1.352821e+02f,
6.889862e+04f, 6.889862e+04f, 6.889862e+04f, 6.889862e+04f,
-2.864824e+05f, -2.864824e+05f, -2.864824e+05f, -2.864824e+05f
];
private static ReadOnlySpan<float> EncodedQLo =>
[
3.371868e+01f, 3.371868e+01f, 3.371868e+01f, 3.371868e+01f,
1.477719e+03f, 1.477719e+03f, 1.477719e+03f, 1.477719e+03f,
1.608477e+04f, 1.608477e+04f, 1.608477e+04f, 1.608477e+04f,
-4.389884e+04f, -4.389884e+04f, -4.389884e+04f, -4.389884e+04f,
-2.072546e+05f, -2.072546e+05f, -2.072546e+05f, -2.072546e+05f
];
public static double DisplayFromEncoded(float displayIntensityTarget, double e)
{
if (e == 0.0)
{
return 0.0;
}
double originalSign = e;
e = Math.Abs(e);
double xp = Math.Pow(e, 1.0 / M2);
double num = Math.Max(xp - C1, 0.0);
double den = C2 - (C3 * xp);
double d = Math.Pow(num / den, 1.0 / M1);
return Math.CopySign(d * (10000.0 / displayIntensityTarget), originalSign);
}
public static double EncodedFromDisplay(float displayIntensityTarget, double d)
{
if (d == 0.0)
{
return 0.0;
}
double originalSign = d;
d = Math.Abs(d);
double xp = Math.Pow(d * (displayIntensityTarget * (1 / 10000)), M1);
double num = C1 + (xp * C2);
double den = 1.0 + (xp * C3);
double e = Math.Pow(num / den, M2);
return Math.CopySign(e, originalSign);
}
public Vector<float> DisplayFromEncoded(Vector<float> x)
{
Vector<float> sign = Vector.Create(0x80000000u).As<uint, float>();
Vector<float> originalSign = x & sign;
x = Vector.AndNot(sign, x);
Vector<float> xpxx = (x * x) + x;
Vector<float> magnitude = EvaluateRationalPolynomial(xpxx, DisplayP, DisplayQ);
return Vector.AndNot(sign, magnitude * Vector.Create(this.scaleFactorFrom10000Nits)) | originalSign;
}
public Vector<float> EncodedFromDisplay(Vector<float> x)
{
Vector<float> sign = Vector.Create(0x80000000u).As<uint, float>();
Vector<float> originalSign = x & sign;
x = Vector.AndNot(sign, x);
Vector<float> xto025 = Vector.SquareRoot(Vector.SquareRoot(x * Vector.Create(this.scaleFactorTo10000Nits)));
Vector<float> magnitude = Vector.ConditionalSelect(
Vector.LessThan(x, Vector.Create(1e-4f)),
EvaluateRationalPolynomial(xto025, EncodedPLo, EncodedQLo),
EvaluateRationalPolynomial(xto025, EncodedP, EncodedQ));
return Vector.AndNot(sign, magnitude) | originalSign;
}
}

82
src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlSRgbTransferFunction.cs

@ -0,0 +1,82 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions;
internal static class JxlSRgbTransferFunction
{
private const float ThresholdSRGBToLinear = 0.04045f;
private const float ThresholdLinearToSRGB = 0.0031308f;
private const float LowDiv = 12.92f;
private const float LowDivInverse = 1.0f / LowDiv;
private static ReadOnlySpan<float> DisplayP =>
[
2.200248328e-04f, 2.200248328e-04f, 2.200248328e-04f, 2.200248328e-04f,
1.043637593e-02f, 1.043637593e-02f, 1.043637593e-02f, 1.043637593e-02f,
1.624820318e-01f, 1.624820318e-01f, 1.624820318e-01f, 1.624820318e-01f,
7.961564959e-01f, 7.961564959e-01f, 7.961564959e-01f, 7.961564959e-01f,
8.210152774e-01f, 8.210152774e-01f, 8.210152774e-01f, 8.210152774e-01f
];
private static ReadOnlySpan<float> DisplayQ =>
[
2.631846970e-01f, 2.631846970e-01f, 2.631846970e-01f, 2.631846970e-01f,
1.076976492e+00f, 1.076976492e+00f, 1.076976492e+00f, 1.076976492e+00f,
4.987528350e-01f, 4.987528350e-01f, 4.987528350e-01f, 4.987528350e-01f,
-5.512498495e-02f, -5.512498495e-02f, -5.512498495e-02f, -5.512498495e-02f,
6.521209011e-03f, 6.521209011e-03f, 6.521209011e-03f, 6.521209011e-03f
];
private static ReadOnlySpan<float> EncodedP =>
[
-5.135152395e-04f, -5.135152395e-04f, -5.135152395e-04f, -5.135152395e-04f,
5.287254571e-03f, 5.287254571e-03f, 5.287254571e-03f, 5.287254571e-03f,
3.903842876e-01f, 3.903842876e-01f, 3.903842876e-01f, 3.903842876e-01f,
1.474205315e+00f, 1.474205315e+00f, 1.474205315e+00f, 1.474205315e+00f,
7.352629620e-01f, 7.352629620e-01f, 7.352629620e-01f, 7.352629620e-01f
];
private static ReadOnlySpan<float> EncodedQ =>
[
1.004519624e-02f, 1.004519624e-02f, 1.004519624e-02f, 1.004519624e-02f,
3.036675394e-01f, 3.036675394e-01f, 3.036675394e-01f, 3.036675394e-01f,
1.340816930e+00f, 1.340816930e+00f, 1.340816930e+00f, 1.340816930e+00f,
9.258482155e-01f, 9.258482155e-01f, 9.258482155e-01f, 9.258482155e-01f,
2.424867759e-02f, 2.424867759e-02f, 2.424867759e-02f, 2.424867759e-02f
];
public static Vector<float> DisplayFromEncoded(Vector<float> x)
{
Vector<float> sign = Vector.Create(0x80000000u).As<uint, float>();
Vector<float> originalSign = x & sign;
x = Vector.AndNot(sign, x);
Vector<float> linear = x * Vector.Create(LowDivInverse);
Vector<float> poly = EvaluateRationalPolynomial(x, DisplayP, DisplayQ);
Vector<float> magnitude = Vector.ConditionalSelect(
Vector.GreaterThan(x, Vector.Create(ThresholdSRGBToLinear)),
poly,
linear);
return Vector.AndNot(sign, magnitude) | originalSign;
}
public static Vector<float> EncodedFromDisplay(Vector<float> x)
{
Vector<float> sign = Vector.Create(0x80000000u).As<uint, float>();
Vector<float> originalSign = x & sign;
x = Vector.AndNot(sign, x);
Vector<float> linear = x * Vector.Create(LowDiv);
Vector<float> poly = EvaluateRationalPolynomial(Vector.SquareRoot(x), DisplayP, DisplayQ);
Vector<float> magnitude = Vector.ConditionalSelect(
Vector.GreaterThan(x, Vector.Create(ThresholdLinearToSRGB)),
poly,
linear);
return Vector.AndNot(sign, magnitude) | originalSign;
}
}

71
src/ImageSharp/Formats/Jxl/Cms/TransferFunctions/JxlTransferUtils.cs

@ -0,0 +1,71 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
namespace SixLabors.ImageSharp.Formats.Jxl.Cms.TransferFunctions;
internal static class JxlTransferUtils
{
public static Vector<float> FastLinearToSrgb(Vector<float> v)
{
Vector<float> v025_05 = ((v.As<float, int>() & new Vector<int>(0x3effffff)) | new Vector<int>(0x3e800000)).As<int, float>();
Vector<float> d1 = (v025_05 * Vector.Create(0.059914046f)) - Vector.Create(0.108894556f);
Vector<float> d2 = (d1 * v025_05) + Vector.Create(0.107963754f);
Vector<float> pow = (d2 * v025_05) + Vector.Create(0.018092343f);
const uint baseBits = 0x40000000;
ReadOnlySpan<byte> powers25to18 =
[
0x00, 0x0a, 0x19, 0x26,
0x32, 0x41, 0x4d, 0x5c,
0x68, 0x75, 0x83, 0x8f,
0xa0, 0xaa, 0xb9, 0xc6
];
ReadOnlySpan<byte> powers17to10 =
[
0x00, 0xb7, 0x04, 0x0d,
0xcb, 0xe7, 0x41, 0x68,
0x51, 0xd1, 0xeb, 0xf2,
0x00, 0xb7, 0x04, 0x0d
];
Vector<int> bits = Vector.AsVectorInt32(v);
Vector<int> exp = (bits >> 23) - Vector.Create(118);
exp &= new Vector<int>(0xf);
Vector<int> p25to18 = GatherByteTable(exp, powers25to18);
Vector<int> p17to10 = GatherByteTable(exp, powers17to10);
Vector<int> mulBits =
(p25to18 << 18) |
(p17to10 << 10) |
new Vector<int>((int)baseBits);
Vector<float> mul = Vector.AsVectorSingle(mulBits);
Vector<float> cutoff = new(0.0031308f);
return Vector.ConditionalSelect(
Vector.LessThan(v, cutoff),
v * new Vector<float>(12.92f),
(pow * mul) - new Vector<float>(0.055f));
}
private static Vector<int> GatherByteTable(Vector<int> indices, ReadOnlySpan<byte> table)
{
int count = Vector<int>.Count;
Span<int> result = stackalloc int[count];
for (int i = 0; i < count; i++)
{
result[i] = table[indices[i]];
}
return new Vector<int>(result);
}
}

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

35
src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs

@ -0,0 +1,35 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
internal sealed class JxlAllDefaultVisitor : JxlVisitorBase
{
public bool IsAllDefault { get; private set; } = true;
public override bool Bits(int bits, uint defaultValue, ref uint value)
{
this.IsAllDefault = value == defaultValue;
return true;
}
public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value)
{
this.IsAllDefault = value == defaultValue;
return true;
}
public override bool U64(ulong defaultValue, ref ulong value)
{
this.IsAllDefault = value == defaultValue;
return true;
}
public override bool F16(float defaultValue, ref float value)
{
this.IsAllDefault = MathF.Abs(value - defaultValue) < 1E-6f;
return true;
}
public override bool AllDefault(IJxlFields fields, ref bool allDefault) => false;
}

50
src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs

@ -0,0 +1,50 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder;
namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
/// <summary>
/// Raw bits coder
/// </summary>
internal static class JxlBitsCoder
{
/// <summary>
/// Maximum number of encodeable bits. Since this coder encodes
/// bits raw, this happens to be whatever is passed to it.
/// </summary>
// Looks like that's what the function does (fields.cc:406):
// it returns whatever is passed to it.
public static int MaxEncodedBits(int bits) => bits;
public static bool CanEncode(int bits, uint value, ref int encodedBits)
{
encodedBits = bits;
if (value >= (1 << bits))
{
DebugGuard.IsTrue(false, "Value is too large");
return false;
}
return true;
}
public static void Write(int bits, uint value, JxlBitWriter writer)
{
if (value >= (1u << bits))
{
throw new InvalidOperationException("Value is too large to encode in " + bits + " bits");
}
writer.Write(bits, value);
}
// NOTE: BitsCoder::Read (fields.cc:418) returns a uint32_t,
// suggesting the input bit size does not exceed 32 bits.
public static uint Read(uint bits, JxlBitReader reader) => reader.ReadBits32(bits);
public static uint Read(int bits, JxlBitReader reader) => reader.ReadBits32((uint)bits);
}

174
src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs

@ -0,0 +1,174 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
using SixLabors.ImageSharp.Formats.Jxl.Processing;
using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder;
using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder.AuxiliaryOutput;
using SixLabors.ImageSharp.Formats.Jxl.Processing.Quantization;
namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
/// <summary>
/// An <see cref="IJxlFields"/> helper.
/// </summary>
internal static class JxlBundle
{
/// <summary>
/// Initializes the specified JXL fields.
/// </summary>
/// <param name="fields">The JXL fields.</param>
public static void Init(IJxlFields fields)
{
JxlInitVisitor initVisitor = new();
if (!initVisitor.Visit(fields))
{
DebugGuard.IsTrue(false, "Init should never fail");
}
}
/// <summary>
/// Sets all JXL fields provided by the input value to their defaults.
/// </summary>
/// <param name="fields">The JXL fields.</param>
public static void SetDefault(IJxlFields fields)
{
JxlSetDefaultVisitor visitor = new();
if (!visitor.Visit(fields))
{
DebugGuard.IsTrue(false, "SetDefault should never fail");
}
}
/// <summary>
/// Returns a value indicating whether every value provided by this
/// field is a default value. If at least one field isn't a default
/// value, the method returns false.
/// </summary>
/// <param name="fields">The JXL fields.</param>
/// <returns>A boolean indicating whether or not are all values initialized to their default values.</returns>
public static bool AllDefault(IJxlFields fields)
{
JxlAllDefaultVisitor allDefaultVisitor = new();
if (!allDefaultVisitor.Visit(fields))
{
DebugGuard.IsTrue(false, "AllDefault should never fail");
}
return allDefaultVisitor.IsAllDefault;
}
/// <summary>
/// Reads the fields from a bit-reader.
/// </summary>
/// <param name="reader">The bit-reader.</param>
/// <param name="fields">The fields.</param>
/// <returns>Status of the read operation.</returns>
public static bool Read(JxlBitReader reader, IJxlFields fields)
{
JxlReadVisitor visitor = new(reader);
if (!visitor.Visit(fields))
{
return false;
}
return visitor.OK;
}
public static bool CanEncode(IJxlFields fields, ref int extensionBits, ref long totalBits)
{
JxlCanEncodeVisitor canEncodeVisitor = new();
if (!canEncodeVisitor.Visit(fields))
{
return false;
}
if (!canEncodeVisitor.GetSizes(ref extensionBits, ref totalBits))
{
return false;
}
return true;
}
/// <summary>
/// Tries to read the fields from a bit-reader.
/// </summary>
/// <param name="reader">The bit-reader.</param>
/// <param name="fields">The fields.</param>
/// <returns>Status of the read operation.</returns>
public static bool CanRead(JxlBitReader reader, IJxlFields fields)
{
JxlReadVisitor visitor = new(reader);
_ = visitor.Visit(fields);
return visitor.OK;
}
public static bool Write(IJxlFields fields, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOutput)
{
int extensionBits = 0;
long totalBits = 0;
if (!CanEncode(fields, ref extensionBits, ref totalBits))
{
return false;
}
return writer.WithMaxBits(totalBits, () =>
{
JxlWriteVisitor visitor = new(extensionBits, writer);
if (!visitor.Visit(fields))
{
return false;
}
return visitor.OK();
});
}
public static bool WriteCodestreamHeaders(JxlCodecMetadata metadata, JxlBitWriter writer, JxlAuxiliaryOutput auxOut)
{
// Marker/signature
if (!writer.WithMaxBits(16, () =>
{
writer.Write(8, 0xFF);
writer.Write(8, JxlShared.CodestreamMarker);
return true;
}))
{
return false;
}
if (!WriteSizeHeader(metadata.Size!, writer, JxlLayerType.Header, auxOut))
{
return false;
}
if (!WriteImageMetadata(metadata.ImageMetadata!, writer, JxlLayerType.Header, auxOut))
{
return false;
}
metadata.CustomTransformData!.NonserializedXybEncoded = metadata.ImageMetadata!.XybEncoded;
return Write(metadata.CustomTransformData!, writer, JxlLayerType.Header, auxOut);
}
public static bool WriteFrameHeader(JxlFrameHeader frame, JxlBitWriter writer, JxlAuxiliaryOutput auxOut)
=> Write(frame, writer, JxlLayerType.Header, auxOut);
public static bool WriteImageMetadata(JxlImageMetadata metadata, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOut)
=> Write(metadata, writer, layer, auxOut);
public static bool WriteQuantizerParameters(JxlQuantizerParameters metadata, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOut)
=> Write(metadata, writer, layer, auxOut);
public static bool WriteSizeHeader(JxlSizeHeader header, JxlBitWriter writer, JxlLayerType layer, JxlAuxiliaryOutput auxOut)
=> Write(header, writer, layer, auxOut);
}

118
src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs

@ -0,0 +1,118 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
internal sealed class JxlCanEncodeVisitor : JxlVisitorBase
{
private long encodedBits;
private ulong extensions;
private long posAfterExt;
public bool OK { get; set; } = true;
public override bool Bits(int bits, uint defaultValue, ref uint value)
{
int enc = 0;
this.OK &= JxlBitsCoder.CanEncode(bits, value, ref enc);
this.encodedBits += enc;
return true;
}
public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value)
{
int encBits = 0;
this.OK &= JxlU32Coder.CanEncode(enc, value, ref encBits);
this.encodedBits += encBits;
return true;
}
public override bool U64(ulong defaultValue, ref ulong value)
{
int encBits = 0;
this.OK &= JxlU64Coder.CanEncode(value, ref encBits);
this.encodedBits += encBits;
return true;
}
public override bool F16(float defaultValue, ref float value)
{
int encBits = 0;
this.OK &= JxlF16Coder.CanEncode(value, ref encBits);
this.encodedBits += encBits;
return true;
}
public override bool AllDefault(IJxlFields fields, ref bool allDefault)
{
allDefault = JxlBundle.AllDefault(fields);
if (!this.Boolean(true, ref allDefault))
{
return false;
}
return allDefault;
}
public override bool BeginExtensions(ref ulong extensions)
{
if (!base.BeginExtensions(ref extensions))
{
return false;
}
this.extensions = extensions;
if (extensions != 0uL)
{
if (this.posAfterExt != 0)
{
return false;
}
this.posAfterExt = this.encodedBits;
if (this.posAfterExt == 0)
{
return false;
}
}
return true;
}
public bool GetSizes(ref int extensionBits, ref long totalBits)
{
if (!this.OK)
{
return false;
}
extensionBits = 0;
totalBits = this.encodedBits;
if (this.posAfterExt != 0)
{
if (this.encodedBits < this.posAfterExt)
{
return false;
}
extensionBits = (int)this.encodedBits - (int)this.posAfterExt;
int encodedBits = 0;
this.OK &= JxlU64Coder.CanEncode((ulong)extensionBits, ref encodedBits);
totalBits += encodedBits;
for (int i = 1; i < BitOperations.PopCount(this.extensions); i++)
{
encodedBits = 0;
this.OK &= JxlU64Coder.CanEncode(0, ref encodedBits);
totalBits += encodedBits;
}
}
return true;
}
}

42
src/ImageSharp/Formats/Jxl/Fields/JxlExtensionStates.cs

@ -0,0 +1,42 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
internal sealed class JxlExtensionStates
{
private ulong begun;
private ulong ended;
public bool IsBegun => (this.begun & 1) != 0;
public bool IsEnded => (this.ended & 1) != 0;
public void Push()
{
this.begun <<= 1;
this.ended <<= 1;
}
public void Pop()
{
this.begun >>= 1;
this.ended >>= 1;
}
public void Begin()
{
DebugGuard.IsFalse(this.IsBegun, nameof(this.IsBegun), "This must be false.");
DebugGuard.IsFalse(this.IsEnded, nameof(this.IsEnded), "This must be false.");
this.begun++;
}
public void End()
{
DebugGuard.IsTrue(this.IsBegun, nameof(this.IsBegun), "This must be true.");
DebugGuard.IsFalse(this.IsEnded, nameof(this.IsEnded), "This must be false.");
this.ended++;
}
}

136
src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs

@ -0,0 +1,136 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder;
namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
/// <summary>
/// Represents the Half-precision Floating-point number coder.
/// </summary>
internal static class JxlF16Coder
{
/// <summary>
/// Always returns 16, which is the maximum possible encoded bits.
/// The F16 coder always reads 16 bits from the bitstream.
/// </summary>
public static int MaxEncodedBits() => 16;
/// <summary>
/// Returns a boolean indicating whether the input float
/// can be represented properly when encoded into a bit-stream.
/// Also stores the maximum encodeable bits into encodedBits (which is
/// always 16).
/// </summary>
public static bool CanEncode(float value, ref int encodedBits)
{
encodedBits = MaxEncodedBits();
if (float.IsNaN(value) || float.IsInfinity(value))
{
return false; // NaN and Infinity are not valid
}
return MathF.Abs(value) <= 65504.0f;
}
public static bool Read(JxlBitReader reader, ref float value)
{
uint bits16 = reader.ReadBits32(16u);
uint sign = bits16 >> 15;
uint biasedExponent = (bits16 >> 10) & 0x1Fu;
uint mantissa = bits16 & 0x3FFu;
if (biasedExponent == 31u)
{
// NaN and Infinity are not valid
return false;
}
if (biasedExponent == 0u)
{
// Subnormal or zero.
value = (1.0f / 16384) * (mantissa * (1.0f / 1024));
if (sign != 0u)
{
value = -value;
}
return true;
}
uint biasedExp32 = biasedExponent + (127u - 15u);
uint mantissa32 = mantissa << (23 - 10);
uint bits32 = (sign << 31) | (biasedExp32 << 23) | mantissa32;
value = BitConverter.UInt32BitsToSingle(bits32);
return true;
}
public static bool Write(float value, JxlBitWriter writer)
{
uint bits32 = BitConverter.SingleToUInt32Bits(value);
uint sign = bits32 >> 31;
uint biasedExp32 = (bits32 >> 23) & 0xFF;
uint mantissa32 = bits32 & 0x7FFFFF;
int exp = (int)biasedExp32 - 127;
if (exp > 15)
{
throw new InvalidOperationException("Too big to encode, CanEncode should return false");
}
// Tiny or zero => zero.
if (exp < -24)
{
writer.Write(16, 0);
return true;
}
uint biasedExp16 = 0;
uint mantissa16 = 0;
if (exp < -14)
{
biasedExp16 = 0;
uint subExp = unchecked((uint)(-14 - exp));
if (subExp is not (>= 1 and < 11))
{
return false;
}
mantissa16 = (1u << (int)(10 - subExp)) + (mantissa32 >> (int)(13 + subExp));
}
else
{
// exp = [-14, 15]
biasedExp16 = unchecked((uint)(exp + 15));
if (biasedExp16 is not (>= 1 and < 31))
{
return false;
}
mantissa16 = mantissa32 >> 13;
}
if (mantissa16 >= 1024)
{
return false;
}
uint bits16 = (sign << 15) | (biasedExp16 << 10) | mantissa16;
if (bits16 >= 0x10000)
{
return false;
}
writer.Write(16, bits16);
return true;
}
}

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

51
src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs

@ -0,0 +1,51 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
/// <summary>
/// This variant of <see cref="JxlVisitorBase"/> sets values
/// to be the default value.
/// </summary>
internal sealed class JxlInitVisitor : JxlVisitorBase
{
public override bool Bits(int bits, uint defaultValue, ref uint value)
{
value = defaultValue;
return true;
}
public override bool U32(JxlU32Distribution d0, JxlU32Distribution d1, JxlU32Distribution d2, JxlU32Distribution d3, uint defaultValue, ref uint value)
{
value = defaultValue;
return true;
}
public override bool U64(ulong defaultValue, ref ulong value)
{
value = defaultValue;
return true;
}
public override bool Boolean(bool defaultValue, ref bool value)
{
value = defaultValue;
return true;
}
public override bool F16(float defaultValue, ref float value)
{
value = defaultValue;
return true;
}
public override bool Conditional(bool condition) => true;
public override bool AllDefault(IJxlFields fields, ref bool allDefault)
{
_ = this.Boolean(true, ref allDefault);
return false;
}
public override bool VisitNested(IJxlFields fields) => true;
}

121
src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs

@ -0,0 +1,121 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jxl.Processing;
using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
internal sealed class JxlReadVisitor(JxlBitReader reader) : JxlVisitorBase
{
private ulong totalExtensionBits;
private bool notEnoughBytes;
private long posAfterExtSize;
private readonly ulong[] extensionBits = new ulong[JxlBundle.MaxExtensions];
public bool OK { get; private set; }
public override bool IsReading => true;
public override bool Bits(int bits, uint defaultValue, ref uint value)
{
value = JxlBitsCoder.Read(bits, reader);
return this.ThrowIfEndOfStreamOrReturnTrue();
}
public override bool U32(JxlU32Enc enc, uint defaultValue, ref uint value)
{
value = JxlU32Coder.Read(enc, reader);
return this.ThrowIfEndOfStreamOrReturnTrue();
}
public override bool U64(ulong defaultValue, ref ulong value)
{
value = JxlU64Coder.Read(reader);
return this.ThrowIfEndOfStreamOrReturnTrue();
}
public override bool F16(float defaultValue, ref float value)
{
this.OK &= JxlF16Coder.Read(reader, ref value);
return this.ThrowIfEndOfStreamOrReturnTrue();
}
public override void SetDefault(IJxlFields fields) => JxlBundle.SetDefault(fields);
public override bool BeginExtensions(ref ulong extensions)
{
if (!base.BeginExtensions(ref extensions))
{
return false;
}
if (extensions == 0)
{
return true;
}
for (ulong remainingExtensions = extensions; remainingExtensions != 0; remainingExtensions &= remainingExtensions - 1)
{
ulong idxExtension = JxlMath.Num0BitsBelowLS1Bit_Nonzero(remainingExtensions);
if (!this.U64(0, ref this.extensionBits[idxExtension]))
{
return false;
}
if (!JxlMath.SafeAdd(this.totalExtensionBits, this.extensionBits[idxExtension], ref this.totalExtensionBits))
{
DebugGuard.IsTrue(false, "Extension bits overflow; the codestream is not valid");
return false;
}
}
this.posAfterExtSize = reader.TotalBitsConsumed;
return this.posAfterExtSize != 0;
}
public override bool EndExtensions()
{
if (!base.EndExtensions())
{
return false;
}
if (this.posAfterExtSize == 0)
{
return true;
}
if (this.notEnoughBytes)
{
return true;
}
long bitsRead = reader.TotalBitsConsumed;
long end = 0;
if (!JxlMath.SafeAdd(this.posAfterExtSize, this.totalExtensionBits, ref end))
{
DebugGuard.IsTrue(false, "Invalid extension size.");
return false;
}
if (bitsRead > end)
{
DebugGuard.IsTrue(false, "Read more extension bits than budgeted");
return false;
}
long remainingBits = end - bitsRead;
if (remainingBits != 0)
{
reader.SkipBits64((uint)remainingBits);
}
return true;
}
}

49
src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs

@ -0,0 +1,49 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
/// <summary>
/// This is similar to InitVisitor but also initializes
/// nested fields.
/// </summary>
internal sealed class JxlSetDefaultVisitor : JxlVisitorBase
{
public override bool Bits(int bits, uint defaultValue, ref uint value)
{
value = defaultValue;
return true;
}
public override bool U32(JxlU32Distribution d0, JxlU32Distribution d1, JxlU32Distribution d2, JxlU32Distribution d3, uint defaultValue, ref uint value)
{
value = defaultValue;
return true;
}
public override bool U64(ulong defaultValue, ref ulong value)
{
value = defaultValue;
return true;
}
public override bool Boolean(bool defaultValue, ref bool value)
{
value = defaultValue;
return true;
}
public override bool F16(float defaultValue, ref float value)
{
value = defaultValue;
return true;
}
public override bool Conditional(bool condition) => true;
public override bool AllDefault(IJxlFields fields, ref bool allDefault)
{
_ = this.Boolean(true, ref allDefault);
return false;
}
}

154
src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs

@ -0,0 +1,154 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jxl.Processing;
using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder;
namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
/// <summary>
/// Unsigned 32-bit variable-length integer coder.
/// </summary>
internal static class JxlU32Coder
{
/// <summary>
/// Maximum number of writeable and/or readable bits in a variable-length integer.
/// </summary>
public static int MaxEncodedBits(in JxlU32Enc enc)
{
int extraBits = 0;
for (int selector = 0; selector < 4; selector++)
{
JxlU32Distribution distr = enc.GetDistribution(selector);
if (distr.IsDirect)
{
continue;
}
else
{
extraBits = Math.Max(extraBits, (int)distr.ExtraBits);
}
}
return 2 + extraBits;
}
/// <summary>
/// Verifies that the value can be encoded.
/// </summary>
public static bool CanEncode(in JxlU32Enc enc, uint value, ref int encodedBits)
{
uint selector = 0;
int totalBits = 0;
bool isOk = ChooseSelector(in enc, value, ref selector, ref totalBits);
encodedBits = isOk ? totalBits : 0;
return isOk;
}
/// <summary>
/// Reads the U32 coded value.
/// </summary>
public static uint Read(in JxlU32Enc enc, JxlBitReader reader)
{
uint selector = reader.ReadBits32(2u);
JxlU32Distribution dist = enc.GetDistribution((int)selector);
if (dist.IsDirect)
{
return dist.Direct;
}
else
{
return reader.ReadBits32(dist.ExtraBits) + dist.Offset;
}
}
/// <summary>
/// Tries to find the best one of the four selectors based on the value.
/// </summary>
public static bool ChooseSelector(in JxlU32Enc enc, uint value, ref uint selector, ref int totalBits)
{
uint bitsRequired = 32 - JxlMath.Num0BitsAboveMS1Bit(value);
if (bitsRequired > 32)
{
return false;
}
selector = 0;
totalBits = 64;
for (int s = 0; s < 4; s++)
{
JxlU32Distribution dist = enc.GetDistribution(s);
if (dist.IsDirect)
{
if (dist.Direct == value)
{
selector = (uint)s;
totalBits = 2;
return true;
}
continue;
}
uint extraBits = dist.ExtraBits;
uint offset = dist.Offset;
if (value < offset || value >= offset + (1u << (int)extraBits))
{
continue;
}
if (2 + extraBits < totalBits)
{
selector = (uint)s;
totalBits = 2 + (int)extraBits;
}
}
if (totalBits == 64)
{
throw new InvalidOperationException("No matching selector");
}
return true;
}
public static bool Write(in JxlU32Enc enc, uint value, JxlBitWriter writer)
{
uint selector = 0;
int totalBits = 0;
if (!ChooseSelector(in enc, value, ref selector, ref totalBits))
{
return false;
}
writer.Write(2, selector);
JxlU32Distribution d = enc.GetDistribution((int)selector);
if (!d.IsDirect)
{
uint offset = d.Offset;
if (value < offset)
{
return false;
}
writer.WriteBits(totalBits - 2, value - offset);
}
return true;
}
}

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.
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)
{
// This stuff is internal, so if argument check
// fails it's not a user error.
DebugGuard.MustBeLessThan(selector, 4, nameof(selector));
return this.d[selector];
}
}

160
src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs

@ -0,0 +1,160 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
using SixLabors.ImageSharp.Formats.Jxl.Processing.Encoder;
namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
/// <summary>
/// Unsigned 64-bit variable-length coding.
/// </summary>
internal static class JxlU64Coder
{
/// <summary>
/// Reads the variable-length, unsigned 64-bit integer.
/// </summary>
public static ulong Read(JxlBitReader reader)
{
uint selector = reader.ReadBits32(2u);
if (selector == 0u)
{
return 0u;
}
else if (selector == 1u)
{
return 1u + reader.ReadBits32(4u);
}
else if (selector == 2u)
{
return 17u + reader.ReadBits32(8u);
}
// Selector 3...
ulong result = reader.ReadBits32(12u);
int shift = 12;
while (reader.ReadBoolean())
{
if (shift == 60)
{
result |= (ulong)reader.ReadBits32(4u) << shift;
break;
}
result |= (ulong)reader.ReadBits32(8u) << shift;
shift += 8;
}
return result;
}
/// <summary>
/// Returns a value indicating whether can the value be encoded,
/// as well as the number of encoded bits.
/// </summary>
public static bool CanEncode(ulong value, ref int encodedBits)
{
if (value == 0)
{
// 2 selector bits
encodedBits = 2;
}
else if (value <= 16)
{
// 2 selector bits + 4 payload bits
encodedBits = 2 + 4;
}
else if (value <= 272)
{
// 2 selector bits + 8 payload bits
encodedBits = 2 + 8;
}
else
{
// 2 selector bits + 12 payload bits
encodedBits = 2 + 12;
value >>= 12;
int shift = 12;
while (value > 0 && shift < 60)
{
// 1 continuation bit + 8 payload bits
encodedBits += 1 + 8;
value >>= 8;
shift += 8;
}
if (value > 0)
{
// 1 continuation bit + 4 payload bits
encodedBits += 1 + 4;
}
else
{
// 1 stop bit
encodedBits += 1;
}
}
return true;
}
public static bool Write(ulong value, JxlBitWriter writer)
{
if (value == 0)
{
// Selector: use 0 bits, value 0
writer.Write(2, 0);
}
else if (value <= 16)
{
// Selector: use 4 bits, value 1..16
writer.Write(2, 1);
writer.Write(4, value - 1);
}
else if (value <= 272)
{
// Selector: use 8 bits, value 17..272
writer.Write(2, 2);
writer.Write(8, value - 17);
}
else
{
// Selector: varint, first a 12-bit group, after that per 8-bit group.
writer.Write(2, 3);
writer.Write(12, value & 4095);
value >>= 12;
int shift = 12;
while (value > 0 && shift < 60)
{
// Indicate varint not done
writer.Write(1, 1);
writer.Write(8, value & 255);
value >>= 8;
shift += 8;
}
if (value > 0)
{
// This only could happen if shift == N - 4.
writer.Write(1, 1);
writer.Write(4, value & 15);
// Implicitly closed sequence, no extra stop bit is required.
}
else
{
// Indicate end of varint
writer.Write(1, 0);
}
}
return true;
}
/// <summary>
/// Always returns 73.
/// </summary>
public static int MaxEncodedBits() => 73;
}

92
src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs

@ -0,0 +1,92 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
/// <summary>
/// Base JPEG XL visitor that can visit all fields of a class.
/// This is highly similar to the following Reflection code, but with
/// lower overhead:
/// <code>
/// // Pseudocode
/// void Visit(Type type)
/// {
/// foreach (PropertyInfo property in type.GetProperties())
/// {
/// /* visitor implementation */(property);
/// }
/// }
/// </code>
/// </summary>
internal class JxlVisitor
{
public virtual bool IsReading => false;
public virtual bool Visit(IJxlFields fields) => false;
public virtual bool Boolean(bool defaultValue, ref bool value) => false;
public virtual bool U32(JxlU32Enc enc, uint defaultValue, ref uint value) => false;
public virtual bool U32(
JxlU32Distribution d0,
JxlU32Distribution d1,
JxlU32Distribution d2,
JxlU32Distribution d3,
uint defaultValue,
ref uint value)
=> this.U32(new JxlU32Enc(d0, d1, d2, d3), value, ref defaultValue);
public virtual unsafe bool Enum<T>(T defaultValue, ref T value)
where T : unmanaged
{
DebugGuard.IsTrue(sizeof(T) == 4, "We use unsafe bit casting so anything beside 4 bytes will break memory layout");
ref uint u32 = ref Unsafe.As<T, uint>(ref value);
if (!this.U32(
JxlFieldExpressions.Value(0),
JxlFieldExpressions.Value(1),
JxlFieldExpressions.BitsOffset(4, 2),
JxlFieldExpressions.BitsOffset(6, 18),
Unsafe.BitCast<T, uint>(defaultValue),
ref u32))
{
return false;
}
return System.Enum.IsDefined(typeof(T), value);
}
public virtual bool Bits(int bits, uint defaultValue, ref uint value) => false;
public virtual bool U64(ulong defaultValue, ref ulong value) => false;
public virtual bool F16(float defaultValue, ref float value) => false;
public virtual bool Conditional(bool condition) => condition;
public virtual bool AllDefault(IJxlFields fields, ref bool allDefault)
{
// Do not remove the fields parameter, derived classes
// use it.
if (!this.Boolean(true, ref allDefault))
{
return false;
}
return allDefault;
}
public virtual void SetDefault(IJxlFields fields)
{
// Used by derived methods.
}
public virtual bool VisitNested(IJxlFields fields) => this.Visit(fields);
public virtual bool BeginExtensions(ref ulong extensions) => false;
public virtual bool EndExtensions() => false;
}

76
src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs

@ -0,0 +1,76 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Fields;
internal class JxlVisitorBase : JxlVisitor
{
private readonly JxlExtensionStates extensionStates = new();
private int depth;
public override bool Visit(IJxlFields fields)
{
if (this.depth >= JxlBundle.MaxExtensions)
{
return false;
}
this.depth++;
this.extensionStates.Push();
bool visited = fields.Visit(this);
if (visited)
{
if (!(!this.extensionStates.IsBegun || this.extensionStates.IsEnded))
{
throw new InvalidOperationException("Invalid extension state");
}
}
this.extensionStates.Pop();
if (this.depth == 0)
{
throw new InvalidOperationException("Depth must not be 0");
}
this.depth--;
return visited;
}
public override bool Boolean(bool defaultValue, ref bool value)
{
uint bits = value ? 1u : 0u;
if (!this.Bits(1, defaultValue ? 1u : 0u, ref bits))
{
return false;
}
if (bits > 1u)
{
throw new InvalidOperationException("Invalid bits");
}
value = bits == 1u;
return true;
}
public override bool BeginExtensions(ref ulong extensions)
{
if (!this.U64(0uL, ref extensions))
{
return false;
}
this.extensionStates.Begin();
return true;
}
public override bool EndExtensions()
{
this.extensionStates.End();
return true;
}
}

320
src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs

@ -0,0 +1,320 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
namespace SixLabors.ImageSharp.Formats.Jxl.IO;
/// <summary>
/// Reads primitives from streams with correct endianness.
/// </summary>
// TODO: move this class into the IO or Common folder?
internal static class BinaryUtils
{
/// <summary>
/// Reads a <see cref="Int16" />
/// from the specified stream in little-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="Int16" /> will be read from.</param>
/// <returns><see cref="Int16" /></returns>
public static Int16 ReadInt16LittleEndian(Stream stream)
{
Span<byte> data = stackalloc byte[sizeof(Int16)];
stream.ReadExactly(data);
return BinaryPrimitives.ReadInt16LittleEndian(data);
}
/// <summary>
/// Reads a <see cref="Int16" />
/// from the specified stream in big-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="Int16" /> will be read from.</param>
/// <returns><see cref="Int16" /></returns>
public static Int16 ReadInt16BigEndian(Stream stream)
{
Span<byte> data = stackalloc byte[sizeof(Int16)];
stream.ReadExactly(data);
return BinaryPrimitives.ReadInt16BigEndian(data);
}
/// <summary>
/// Writes a <see cref="Int16" />
/// into the specified stream in little-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="Int16" /> will be written to.</param>
/// <param name="value">Value which will be written to the stream.</param>
public static void WriteInt16LittleEndian(Stream stream, Int16 value)
{
Span<byte> data = stackalloc byte[sizeof(Int16)];
BinaryPrimitives.WriteInt16LittleEndian(data, value);
stream.Write(data);
}
/// <summary>
/// Writes a <see cref="Int16" />
/// into the specified stream in big-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="Int16" /> will be written to.</param>
/// <param name="value">Value which will be written to the stream.</param>
public static void WriteInt16BigEndian(Stream stream, Int16 value)
{
Span<byte> data = stackalloc byte[sizeof(Int16)];
BinaryPrimitives.WriteInt16BigEndian(data, value);
stream.Write(data);
}
/// <summary>
/// Reads a <see cref="UInt16" />
/// from the specified stream in little-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="UInt16" /> will be read from.</param>
/// <returns><see cref="UInt16" /></returns>
public static UInt16 ReadUInt16LittleEndian(Stream stream)
{
Span<byte> data = stackalloc byte[sizeof(UInt16)];
stream.ReadExactly(data);
return BinaryPrimitives.ReadUInt16LittleEndian(data);
}
/// <summary>
/// Reads a <see cref="UInt16" />
/// from the specified stream in big-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="UInt16" /> will be read from.</param>
/// <returns><see cref="UInt16" /></returns>
public static UInt16 ReadUInt16BigEndian(Stream stream)
{
Span<byte> data = stackalloc byte[sizeof(UInt16)];
stream.ReadExactly(data);
return BinaryPrimitives.ReadUInt16BigEndian(data);
}
/// <summary>
/// Writes a <see cref="UInt16" />
/// into the specified stream in little-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="UInt16" /> will be written to.</param>
/// <param name="value">Value which will be written to the stream.</param>
public static void WriteUInt16LittleEndian(Stream stream, UInt16 value)
{
Span<byte> data = stackalloc byte[sizeof(UInt16)];
BinaryPrimitives.WriteUInt16LittleEndian(data, value);
stream.Write(data);
}
/// <summary>
/// Writes a <see cref="UInt16" />
/// into the specified stream in big-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="UInt16" /> will be written to.</param>
/// <param name="value">Value which will be written to the stream.</param>
public static void WriteUInt16BigEndian(Stream stream, UInt16 value)
{
Span<byte> data = stackalloc byte[sizeof(UInt16)];
BinaryPrimitives.WriteUInt16BigEndian(data, value);
stream.Write(data);
}
/// <summary>
/// Reads a <see cref="Int32" />
/// from the specified stream in little-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="Int32" /> will be read from.</param>
/// <returns><see cref="Int32" /></returns>
public static Int32 ReadInt32LittleEndian(Stream stream)
{
Span<byte> data = stackalloc byte[sizeof(Int32)];
stream.ReadExactly(data);
return BinaryPrimitives.ReadInt32LittleEndian(data);
}
/// <summary>
/// Reads a <see cref="Int32" />
/// from the specified stream in big-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="Int32" /> will be read from.</param>
/// <returns><see cref="Int32" /></returns>
public static Int32 ReadInt32BigEndian(Stream stream)
{
Span<byte> data = stackalloc byte[sizeof(Int32)];
stream.ReadExactly(data);
return BinaryPrimitives.ReadInt32BigEndian(data);
}
/// <summary>
/// Writes a <see cref="Int32" />
/// into the specified stream in little-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="Int32" /> will be written to.</param>
/// <param name="value">Value which will be written to the stream.</param>
public static void WriteInt32LittleEndian(Stream stream, Int32 value)
{
Span<byte> data = stackalloc byte[sizeof(Int32)];
BinaryPrimitives.WriteInt32LittleEndian(data, value);
stream.Write(data);
}
/// <summary>
/// Writes a <see cref="Int32" />
/// into the specified stream in big-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="Int32" /> will be written to.</param>
/// <param name="value">Value which will be written to the stream.</param>
public static void WriteInt32BigEndian(Stream stream, Int32 value)
{
Span<byte> data = stackalloc byte[sizeof(Int32)];
BinaryPrimitives.WriteInt32BigEndian(data, value);
stream.Write(data);
}
/// <summary>
/// Reads a <see cref="UInt32" />
/// from the specified stream in little-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="UInt32" /> will be read from.</param>
/// <returns><see cref="UInt32" /></returns>
public static UInt32 ReadUInt32LittleEndian(Stream stream)
{
Span<byte> data = stackalloc byte[sizeof(UInt32)];
stream.ReadExactly(data);
return BinaryPrimitives.ReadUInt32LittleEndian(data);
}
/// <summary>
/// Reads a <see cref="UInt32" />
/// from the specified stream in big-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="UInt32" /> will be read from.</param>
/// <returns><see cref="UInt32" /></returns>
public static UInt32 ReadUInt32BigEndian(Stream stream)
{
Span<byte> data = stackalloc byte[sizeof(UInt32)];
stream.ReadExactly(data);
return BinaryPrimitives.ReadUInt32BigEndian(data);
}
/// <summary>
/// Writes a <see cref="UInt32" />
/// into the specified stream in little-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="UInt32" /> will be written to.</param>
/// <param name="value">Value which will be written to the stream.</param>
public static void WriteUInt32LittleEndian(Stream stream, UInt32 value)
{
Span<byte> data = stackalloc byte[sizeof(UInt32)];
BinaryPrimitives.WriteUInt32LittleEndian(data, value);
stream.Write(data);
}
/// <summary>
/// Writes a <see cref="UInt32" />
/// into the specified stream in big-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="UInt32" /> will be written to.</param>
/// <param name="value">Value which will be written to the stream.</param>
public static void WriteUInt32BigEndian(Stream stream, UInt32 value)
{
Span<byte> data = stackalloc byte[sizeof(UInt32)];
BinaryPrimitives.WriteUInt32BigEndian(data, value);
stream.Write(data);
}
/// <summary>
/// Reads a <see cref="Int64" />
/// from the specified stream in little-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="Int64" /> will be read from.</param>
/// <returns><see cref="Int64" /></returns>
public static Int64 ReadInt64LittleEndian(Stream stream)
{
Span<byte> data = stackalloc byte[sizeof(Int64)];
stream.ReadExactly(data);
return BinaryPrimitives.ReadInt64LittleEndian(data);
}
/// <summary>
/// Reads a <see cref="Int64" />
/// from the specified stream in big-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="Int64" /> will be read from.</param>
/// <returns><see cref="Int64" /></returns>
public static Int64 ReadInt64BigEndian(Stream stream)
{
Span<byte> data = stackalloc byte[sizeof(Int64)];
stream.ReadExactly(data);
return BinaryPrimitives.ReadInt64BigEndian(data);
}
/// <summary>
/// Writes a <see cref="Int64" />
/// into the specified stream in little-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="Int64" /> will be written to.</param>
/// <param name="value">Value which will be written to the stream.</param>
public static void WriteInt64LittleEndian(Stream stream, Int64 value)
{
Span<byte> data = stackalloc byte[sizeof(Int64)];
BinaryPrimitives.WriteInt64LittleEndian(data, value);
stream.Write(data);
}
/// <summary>
/// Writes a <see cref="Int64" />
/// into the specified stream in big-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="Int64" /> will be written to.</param>
/// <param name="value">Value which will be written to the stream.</param>
public static void WriteInt64BigEndian(Stream stream, Int64 value)
{
Span<byte> data = stackalloc byte[sizeof(Int64)];
BinaryPrimitives.WriteInt64BigEndian(data, value);
stream.Write(data);
}
/// <summary>
/// Reads a <see cref="UInt64" />
/// from the specified stream in little-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="UInt64" /> will be read from.</param>
/// <returns><see cref="UInt64" /></returns>
public static UInt64 ReadUInt64LittleEndian(Stream stream)
{
Span<byte> data = stackalloc byte[sizeof(UInt64)];
stream.ReadExactly(data);
return BinaryPrimitives.ReadUInt64LittleEndian(data);
}
/// <summary>
/// Reads a <see cref="UInt64" />
/// from the specified stream in big-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="UInt64" /> will be read from.</param>
/// <returns><see cref="UInt64" /></returns>
public static UInt64 ReadUInt64BigEndian(Stream stream)
{
Span<byte> data = stackalloc byte[sizeof(UInt64)];
stream.ReadExactly(data);
return BinaryPrimitives.ReadUInt64BigEndian(data);
}
/// <summary>
/// Writes a <see cref="UInt64" />
/// into the specified stream in little-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="UInt64" /> will be written to.</param>
/// <param name="value">Value which will be written to the stream.</param>
public static void WriteUInt64LittleEndian(Stream stream, UInt64 value)
{
Span<byte> data = stackalloc byte[sizeof(UInt64)];
BinaryPrimitives.WriteUInt64LittleEndian(data, value);
stream.Write(data);
}
/// <summary>
/// Writes a <see cref="UInt64" />
/// into the specified stream in big-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="UInt64" /> will be written to.</param>
/// <param name="value">Value which will be written to the stream.</param>
public static void WriteUInt64BigEndian(Stream stream, UInt64 value)
{
Span<byte> data = stackalloc byte[sizeof(UInt64)];
BinaryPrimitives.WriteUInt64BigEndian(data, value);
stream.Write(data);
}
}

85
src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt

@ -0,0 +1,85 @@
<#@ template language="C#" #>
<#@ import namespace="System" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".Generated.cs" #>
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
namespace SixLabors.ImageSharp.Formats.Jxl.IO;
<#
List<Type> types = [
typeof(short),
typeof(ushort),
typeof(int),
typeof(uint),
typeof(long),
typeof(ulong)
];
#>
/// <summary>
/// Reads primitives from streams with correct endianness.
/// </summary>
// TODO: move this class into the IO or Common folder?
internal static class BinaryUtils
{
<#
foreach (Type type in types)
{
#>
/// <summary>
/// Reads a <see cref="<#= type.Name #>" />
/// from the specified stream in little-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="<#= type.Name #>" /> will be read from.</param>
/// <returns><see cref="<#= type.Name #>" /></returns>
public static <#= type.Name #> Read<#= type.Name#>LittleEndian(Stream stream)
{
Span<byte> data = stackalloc byte[sizeof(<#= type.Name #>)];
stream.ReadExactly(data);
return BinaryPrimitives.Read<#= type.Name #>LittleEndian(data);
}
/// <summary>
/// Reads a <see cref="<#= type.Name #>" />
/// from the specified stream in big-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="<#= type.Name #>" /> will be read from.</param>
/// <returns><see cref="<#= type.Name #>" /></returns>
public static <#= type.Name #> Read<#= type.Name#>BigEndian(Stream stream)
{
Span<byte> data = stackalloc byte[sizeof(<#= type.Name #>)];
stream.ReadExactly(data);
return BinaryPrimitives.Read<#= type.Name #>BigEndian(data);
}
/// <summary>
/// Writes a <see cref="<#= type.Name #>" />
/// into the specified stream in little-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="<#= type.Name #>" /> will be written to.</param>
/// <param name="value">Value which will be written to the stream.</param>
public static void Write<#= type.Name #>LittleEndian(Stream stream, <#= type.Name #> value)
{
Span<byte> data = stackalloc byte[sizeof(<#= type.Name #>)];
BinaryPrimitives.Write<#= type.Name #>LittleEndian(data, value);
stream.Write(data);
}
/// <summary>
/// Writes a <see cref="<#= type.Name #>" />
/// into the specified stream in big-endian order.
/// </summary>
/// <param name="stream">The stream where the <see cref="<#= type.Name #>" /> will be written to.</param>
/// <param name="value">Value which will be written to the stream.</param>
public static void Write<#= type.Name #>BigEndian(Stream stream, <#= type.Name #> value)
{
Span<byte> data = stackalloc byte[sizeof(<#= type.Name #>)];
BinaryPrimitives.Write<#= type.Name #>BigEndian(data, value);
stream.Write(data);
}
<# } #>
}

158
src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs

@ -0,0 +1,158 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Container;
/// <summary>
/// Header for JPEG XL container format.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 16)]
internal struct JxlBoxHeader
{
/// <summary>
/// Box size in bytes.
/// </summary>
public ulong Size;
/// <summary>
/// Type of the box.
/// </summary>
public uint Type;
/// <summary>
/// True if the size field extends until the end of the file.
/// </summary>
public bool SizeExtendsTillEnd;
/// <summary>
/// True if the size is 64-bit.
/// </summary>
public bool ContainsLargeSize;
/// <summary>
/// Initializes a new instance of the <see cref="JxlBoxHeader"/> struct.
/// </summary>
/// <param name="size">The size of the box.</param>
/// <param name="type">The type of the box.</param>
/// <param name="sizeExtendsTillEnd">Does the box size extend till the end of the file?</param>
/// <param name="containsLargeSize">Is there a 64-bit size field?</param>
public JxlBoxHeader(ulong size, uint type, bool sizeExtendsTillEnd, bool containsLargeSize)
{
this.Size = size;
this.Type = type;
this.SizeExtendsTillEnd = sizeExtendsTillEnd;
this.ContainsLargeSize = containsLargeSize;
}
/// <summary>
/// Converts a 4-character ASCII string (e.g. "jxlc") into a uint type code.
/// </summary>
/// <param name="typeString">Input type string to convert</param>
/// <returns>Unsigned integer representation of the type string</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint TypeFromString(string typeString)
{
if (typeString.Length != 4)
{
throw new ArgumentException("Box type must be exactly 4 characters", nameof(typeString));
}
return ((uint)typeString[0] << 24) |
((uint)typeString[1] << 16) |
((uint)typeString[2] << 8) |
typeString[3];
}
/// <summary>
/// Converts a uint type code back into a 4-character ASCII string.
/// </summary>
/// <param name="typeCode">Unsigned integer representation of the type string</param>
/// <returns>The string representing the type code.</returns>
public static string TypeToString(uint typeCode)
{
return typeCode switch
{
0x6A786C20 => "jxl ",
0x6A786C70 => "jxlp",
0x6A786C63 => "jxlc",
0x66747970 => "ftyp",
0x6A627264 => "jbrd",
0x45786966 => "Exif",
0x786D6C20 => "xml ",
0x6A756D62 => "jumb",
_ => Fallback(typeCode)
};
static string Fallback(uint typeCode)
{
// The box type is not known
Span<byte> buffer = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(buffer, typeCode);
return Encoding.ASCII.GetString(buffer);
}
}
/// <summary>
/// Parses the JPEG XL box header.
/// </summary>
/// <param name="stream">A stream to parse the header from.</param>
/// <returns>The box header.</returns>
/// <exception cref="InvalidOperationException">Thrown when the header is invalid.</exception>
public static JxlBoxHeader ReadHeader(Stream stream)
{
ulong size = BinaryUtils.ReadUInt32BigEndian(stream);
bool haveSize64 = false;
if (size == 1)
{
// When the size value is equal to 1, a new 64-bit
// size field follows.
haveSize64 = true;
size = BinaryUtils.ReadUInt64BigEndian(stream);
}
// Read the 4-byte type field.
uint type = BinaryUtils.ReadUInt32BigEndian(stream);
if (haveSize64)
{
// When the 64-bit largesize was read,
// the size cannot proceed till the end of the file.
if (size is 0 or 1)
{
throw new InvalidOperationException("Large size cannot have another large size or extend till the end of the file");
}
return new JxlBoxHeader(size, type, sizeExtendsTillEnd: false, containsLargeSize: true);
}
else
{
return new JxlBoxHeader(size, type, sizeExtendsTillEnd: size == 0, containsLargeSize: false);
}
}
/// <summary>
/// Writes the box header to the specified stream.
/// </summary>
/// <param name="writer">The stream to write the box header to.</param>
public readonly void WriteHeader(Stream writer)
{
if (this.Size is > uint.MaxValue or 1)
{
BinaryUtils.WriteUInt32BigEndian(writer, 1); // Indicates a large size is present
BinaryUtils.WriteUInt64BigEndian(writer, this.Size);
}
else
{
BinaryUtils.WriteUInt32BigEndian(writer, (uint)this.Size);
}
BinaryUtils.WriteUInt32BigEndian(writer, this.Type);
}
}

58
src/ImageSharp/Formats/Jxl/IO/Container/JxlFileTypeBox.cs

@ -0,0 +1,58 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Container;
/// <summary>
/// A ftyp box payload.
/// </summary>
internal sealed class JxlFileTypeBox(string majorBrand, uint minorVersion)
{
/// <summary>
/// Gets or sets the primary format. Has to be "jxl " for JPEG XL.
/// </summary>
public string MajorBrand { get; set; } = majorBrand;
/// <summary>
/// Gets or sets the revision of the major brand.
/// </summary>
public uint MinorVersion { get; set; } = minorVersion;
/// <summary>
/// Gets or sets the list of other brands the file is compatible with.
/// </summary>
public List<string> CompatibleBrands { get; set; } = [];
public int GetPayloadSize() => 8 + (this.CompatibleBrands.Count * 4);
public static JxlFileTypeBox Parse(Stream stream, ulong boxSize)
{
string majorBrand = JxlBoxHeader.TypeToString(BinaryUtils.ReadUInt32BigEndian(stream));
uint minorVersion = BinaryUtils.ReadUInt32BigEndian(stream);
boxSize -= 8;
List<string> compatibleBrands = [];
for (ulong i = 0; i < boxSize; i += 4)
{
compatibleBrands.Add(JxlBoxHeader.TypeToString(BinaryUtils.ReadUInt32BigEndian(stream)));
}
JxlFileTypeBox ftyp = new(majorBrand, minorVersion)
{
CompatibleBrands = compatibleBrands
};
return ftyp;
}
public void WritePayload(Stream stream)
{
BinaryUtils.WriteUInt32BigEndian(stream, JxlBoxHeader.TypeFromString(this.MajorBrand));
BinaryUtils.WriteUInt32BigEndian(stream, this.MinorVersion);
foreach (string compatibleBrand in this.CompatibleBrands)
{
BinaryUtils.WriteUInt32BigEndian(stream, JxlBoxHeader.TypeFromString(compatibleBrand));
}
}
}

15
src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsConstants.cs

@ -0,0 +1,15 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy;
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/Entropy/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.Entropy;
[StructLayout(LayoutKind.Sequential)]
internal struct JxlAnsEntry
{
public byte Cutoff;
public byte RightValue;
public ushort Frequency0;
public ushort Offsets1;
public ushort Frequency1XorFrequency0;
}

239
src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs

@ -0,0 +1,239 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy;
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)
{
DebugGuard.MustBeLessThanOrEqualTo(length, 0, nameof(length));
DebugGuard.MustBeGreaterThan(length, totalCount, nameof(length));
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)
{
DebugGuard.MustBeLessThan(logAlphaSize, (int)logRange, nameof(logAlphaSize));
int range = 1 << (int)logRange;
int tableSize = 1 << logAlphaSize;
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)
{
throw new InvalidOperationException("Too many items in the distribution");
}
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;
}
}

63
src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs

@ -0,0 +1,63 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jxl.Fields;
using SixLabors.ImageSharp.Formats.Jxl.Processing;
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy;
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;
if (splitExponent < msbInToken + lsbInToken)
{
throw new InvalidOperationException("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, out uint token, out uint bitCount, out uint bits)
{
if (value < this.SplitToken)
{
token = value;
bitCount = 0;
bits = 0;
}
else
{
uint n = JxlMath.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();
}

81
src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsLz77Parameters.cs

@ -0,0 +1,81 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jxl.Fields;
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Entropy;
internal sealed class JxlAnsLz77Parameters : IJxlFields
{
private bool enabled;
private uint minimumSymbol;
private uint minimumLength;
private JxlAnsHybridUIntConfiguration lengthUintConfig = new(0, 0, 0);
public JxlAnsLz77Parameters() => JxlBundle.Init(this);
public bool Enabled
{
get => this.enabled;
set => this.enabled = value;
}
public uint MinimumSymbol
{
get => this.minimumSymbol;
set => this.minimumSymbol = value;
}
public uint MinimumLength
{
get => this.minimumLength;
set => this.minimumLength = value;
}
public JxlAnsHybridUIntConfiguration LengthUintConfig
{
get => this.lengthUintConfig;
set => this.lengthUintConfig = value;
}
public int NonserializedDistanceContext { get; set; }
public ref JxlAnsHybridUIntConfiguration GetLengthUIntConfigReference() => ref this.lengthUintConfig;
public bool Visit(JxlVisitor visitor)
{
if (!visitor.Boolean(false, ref this.enabled))
{
return false;
}
if (!visitor.Conditional(this.enabled))
{
return true;
}
if (!visitor.U32(
JxlFieldExpressions.Value(224u),
JxlFieldExpressions.Value(512u),
JxlFieldExpressions.Value(4096u),
JxlFieldExpressions.BitsOffset(15u, 8u),
224u,
ref this.minimumSymbol))
{
return false;
}
if (!visitor.U32(
JxlFieldExpressions.Value(3u),
JxlFieldExpressions.Value(4u),
JxlFieldExpressions.BitsOffset(2u, 5u),
JxlFieldExpressions.BitsOffset(8u, 9u),
3u,
ref this.minimumLength))
{
return false;
}
return true;
}
}

14
src/ImageSharp/Formats/Jxl/IO/Entropy/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.Entropy;
[StructLayout(LayoutKind.Sequential)]
internal struct JxlAnsSymbol(int value, int offset, int frequency)
{
public int Value = value;
public int Offset = offset;
public int Frequency = frequency;
}

74
src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs

@ -0,0 +1,74 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jxl.Fields;
using SixLabors.ImageSharp.Formats.Jxl.Metadata;
namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
/// <summary>
/// Describes duration of frames that make up an animation.
/// </summary>
internal sealed class JxlAnimationFrame : IJxlFields
{
/// <summary>
/// See <see cref="Duration"/>.
/// </summary>
private uint duration;
/// <summary>
/// See <see cref="Timecode"/>.
/// </summary>
private uint timecode;
/// <summary>
/// Gets or sets the duration of the animation.
/// </summary>
public uint Duration
{
get => this.duration;
set => this.duration = value;
}
/// <summary>
/// Gets or sets the timecode of the animation. The
/// format is 0xHHMMSSFF.
/// </summary>
public uint Timecode
{
get => this.timecode;
set => this.timecode = value;
}
/// <summary>
/// Gets or sets the optional codec metadata.
/// </summary>
public JxlCodecMetadata? CodecMetadata { get; set; }
public bool Visit(JxlVisitor visitor)
{
if (visitor.Conditional(this.CodecMetadata?.ImageMetadata?.HaveAnimation == true))
{
if (!visitor.U32(
JxlFieldExpressions.Value(0),
JxlFieldExpressions.Value(1),
JxlFieldExpressions.Bits(8),
JxlFieldExpressions.Bits(32),
0,
ref this.duration))
{
return false;
}
}
if (visitor.Conditional(this.CodecMetadata?.ImageMetadata?.Animation?.ContainsTimecodes == true))
{
if (!visitor.Bits(32, 0u, ref this.timecode))
{
return false;
}
}
return true;
}
}

64
src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendMode.cs

@ -0,0 +1,64 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
/// <summary>
/// Represents the blending mode describing how to combine
/// current frame with previously saved frame.
/// </summary>
internal enum JxlBlendMode : byte
{
/// <summary>
/// New values replace old ones.
/// <code>
/// sample = new
/// </code>
/// </summary>
Replace,
/// <summary>
/// New values add to the old ones.
/// <code>
/// sample = old + new
/// </code>
/// </summary>
Add,
/// <summary>
/// New values replace old ones if alpha&gt;0:
/// <code>
/// alpha = old + new * (1 - old)
/// </code>
/// For other channels if !alpha_associated:
/// <code>
/// sample = ((1 - newAlpha) * old * oldAlpha + newAlpha * new) / alpha
/// </code>
/// For other channels if alpha_associated:
/// <code>
/// sample = (1 - newAlpha) * old + new
/// </code>
/// </summary>
Blend,
/// <summary>
/// New values are added to the old ones if alpha&gt;0:
/// For the alpha channel that is used as source:
/// <code>
/// sample = old + new * (1 - old)
/// </code>
/// Otherwise:
/// <code>
/// sample = old + alpha * new
/// </code>
/// </summary>
AlphaWeightedBlend,
/// <summary>
/// New values are multiplied by old ones:
/// <code>
/// sample = old * new
/// </code>
/// </summary>
Multiply
}

146
src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendingInfo.cs

@ -0,0 +1,146 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jxl.Fields;
namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
/// <summary>
/// Provides options and instructions that tell the decoder the proper
/// way to blend the current and previous frame together.
/// </summary>
internal sealed class JxlBlendingInfo : IJxlFields
{
/// <summary>
/// Initializes a new instance of the <see cref="JxlBlendingInfo"/> class.
/// </summary>
public JxlBlendingInfo() => JxlBundle.Init(this);
/// <summary>
/// Gets or sets the blending mode. See <see cref="JxlBlendMode"/>.
/// </summary>
public JxlBlendMode BlendMode { get; set; }
/// <summary>
/// Gets or sets the value that indicates which extra channel
/// to use as alpha channel for blending.
/// </summary>
public uint AlphaChannel { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the alpha or channel values
/// must be clamped* to the 0 through 1 range.
/// </summary>
/// <remarks>
/// Clamped - must be limited to the specified range.
/// </remarks>
public bool Clamp { get; set; }
/// <summary>
/// Gets or sets the frame ID to copy from (0 through 3).
/// </summary>
/// <remarks>
/// If <see cref="BlendMode"/> is equal to <see cref="JxlBlendMode.Replace"/>,
/// the value of this property is ignored.
/// </remarks>
public uint Source { get; set; }
/// <summary>
/// Gets or sets the total number of extra channels.
/// </summary>
public int ExtraChannelCount { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the frame is partial.
/// </summary>
public bool IsPartialFrame { get; set; }
public bool Visit(JxlVisitor visitor)
{
JxlBlendMode mode = this.BlendMode;
if (!VisitBlendMode(visitor, JxlBlendMode.Replace, ref mode))
{
return false;
}
this.BlendMode = mode;
if (visitor.Conditional(this.ExtraChannelCount > 0 && mode is JxlBlendMode.Blend or JxlBlendMode.AlphaWeightedBlend))
{
uint alphaChannel = this.AlphaChannel;
if (!visitor.U32(
JxlFieldExpressions.Value(0u),
JxlFieldExpressions.Value(1u),
JxlFieldExpressions.Value(2u),
JxlFieldExpressions.BitsOffset(3u, 3u),
0,
ref alphaChannel))
{
return false;
}
this.AlphaChannel = alphaChannel;
if (visitor.IsReading && alphaChannel >= this.ExtraChannelCount)
{
throw new InvalidOperationException("Invalid alpha channel for blending");
}
}
if (visitor.Conditional((this.ExtraChannelCount > 0 && mode is JxlBlendMode.Blend or JxlBlendMode.AlphaWeightedBlend) || mode == JxlBlendMode.Multiply))
{
bool clamp = this.Clamp;
if (!visitor.Boolean(false, ref clamp))
{
return false;
}
this.Clamp = clamp;
}
if (visitor.Conditional(mode != JxlBlendMode.Replace || this.IsPartialFrame))
{
uint source = this.Source;
if (!visitor.U32(
JxlFieldExpressions.Value(0),
JxlFieldExpressions.Value(1),
JxlFieldExpressions.Value(2),
JxlFieldExpressions.Value(3),
0,
ref source))
{
return false;
}
this.Source = source;
}
return true;
}
private static bool VisitBlendMode(JxlVisitor visitor, JxlBlendMode defaultValue, ref JxlBlendMode valueToEncode)
{
uint unsignedBackingValue = (uint)valueToEncode;
if (!visitor.U32(
JxlFieldExpressions.Value((uint)JxlBlendMode.Replace),
JxlFieldExpressions.Value((uint)JxlBlendMode.Add),
JxlFieldExpressions.Value((uint)JxlBlendMode.Blend),
JxlFieldExpressions.BitsOffset(2u, 3u),
(uint)defaultValue,
ref unsignedBackingValue))
{
return false;
}
if (unsignedBackingValue > (uint)JxlBlendMode.Multiply)
{
throw new InvalidOperationException("Invalid blend mode");
}
valueToEncode = (JxlBlendMode)unsignedBackingValue;
return true;
}
}

26
src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransform.cs

@ -0,0 +1,26 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
/// <summary>
/// Represents the type of JPEG XL color transform.
/// </summary>
internal enum JxlColorTransform : byte
{
/// <summary>
/// Use XYB encoding
/// </summary>
Xyb,
/// <summary>
/// Encode according to the attached color profile.
/// </summary>
None,
/// <summary>
/// Encode according to the attached color profile but
/// transformed into Y'Cb'Cr.
/// </summary>
YCbCr,
}

31
src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs

@ -0,0 +1,31 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
/// <summary>
/// Helper methods associated with JxlColorTransform.
/// </summary>
internal static class JxlColorTransformHelpers
{
private static ReadOnlySpan<int> Grayscale => [0, 0, 0];
private static ReadOnlySpan<int> YCbCr => [1, 0, 2];
private static ReadOnlySpan<int> None => [0, 1, 2];
public static ReadOnlySpan<int> GetJpegOrder(JxlColorTransform transform, bool isGraysacle)
{
if (isGraysacle)
{
return Grayscale;
}
if (transform == JxlColorTransform.YCbCr)
{
return YCbCr;
}
return None;
}
}

20
src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameEncoding.cs

@ -0,0 +1,20 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
/// <summary>
/// Represents the kind of frame encoding.
/// </summary>
internal enum JxlFrameEncoding : byte
{
/// <summary>
/// Use VarDCT
/// </summary>
VarDct,
/// <summary>
/// Use Modular encoding
/// </summary>
Modular
}

744
src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs

@ -0,0 +1,744 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
// Disable IDE0032 for consistency with other fields.
// We have to avoid auto properties for most fields
// so we can use the ref keyword on them directly.
#pragma warning disable IDE0032 // Use auto property
using SixLabors.ImageSharp.Formats.Jxl.Fields;
using SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
using SixLabors.ImageSharp.Formats.Jxl.Processing;
using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives;
namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
/// <summary>
/// Control information for a JPEG XL frame.
/// </summary>
internal sealed class JxlFrameHeader : IJxlFields
{
// The following are backing fields for properties.
private JxlFrameEncoding encoding = JxlFrameEncoding.Modular;
private JxlFrameType frameType = JxlFrameType.RegularFrame;
private ulong flags;
private JxlColorTransform colorTransform = JxlColorTransform.Xyb;
private JxlYCbCrChromaSubsampling? chromaSubsampling;
private uint groupSizeShift;
private uint xQmScale;
private uint bQmScale;
private string? name;
private bool customSizeOrOrigin;
private Size frameSize;
private uint upsampling;
private List<uint> extraChannelUpsampling = [];
private Point frameOrigin;
private JxlBlendingInfo? blendingInfo;
private List<JxlBlendingInfo> extraChannelBlendingInfo = [];
private readonly JxlAnimationFrame? animationFrame;
private bool isLast;
private uint saveAsReference;
private bool saveBeforeColorTransform;
private uint dcLevel;
private JxlCodecMetadata? metadata;
private JxlLoopFilter? loopFilter;
private ulong extensions;
private bool isPreviewFrame; // Non-serialized
/// <summary>
/// Gets or sets the frame encoding method (e.g., Modular or VarDCT).
/// </summary>
public JxlFrameEncoding Encoding
{
get => this.encoding;
set => this.encoding = value;
}
/// <summary>
/// Gets or sets the type of frame (e.g., RegularFrame).
/// </summary>
public JxlFrameType FrameType
{
get => this.frameType;
set => this.frameType = value;
}
/// <summary>
/// Gets or sets the frame flags.
/// </summary>
public ulong Flags
{
get => this.flags;
set => this.flags = value;
}
/// <summary>
/// Gets or sets the color transform used (e.g., XYB).
/// </summary>
public JxlColorTransform ColorTransform
{
get => this.colorTransform;
set => this.colorTransform = value;
}
/// <summary>
/// Gets or sets the chroma subsampling information.
/// </summary>
public JxlYCbCrChromaSubsampling? ChromaSubsampling
{
get => this.chromaSubsampling;
set => this.chromaSubsampling = value;
}
/// <summary>
/// Gets or sets the group size shift value.
/// </summary>
public uint GroupSizeShift
{
get => this.groupSizeShift;
set => this.groupSizeShift = value;
}
/// <summary>
/// Gets or sets the X quantization matrix scale.
/// </summary>
public uint XQmScale
{
get => this.xQmScale;
set => this.xQmScale = value;
}
/// <summary>
/// Gets or sets the B quantization matrix scale.
/// </summary>
public uint BQmScale
{
get => this.bQmScale;
set => this.bQmScale = value;
}
/// <summary>
/// Gets or sets the frame name.
/// </summary>
public string? Name
{
get => this.name;
set => this.name = value;
}
/// <summary>
/// Gets or sets a value indicating whether the frame has a custom size or origin.
/// </summary>
public bool CustomSizeOrOrigin
{
get => this.customSizeOrOrigin;
set => this.customSizeOrOrigin = value;
}
/// <summary>
/// Gets or sets the frame size.
/// </summary>
public Size FrameSize
{
get => this.frameSize;
set => this.frameSize = value;
}
/// <summary>
/// Gets or sets the upsampling factor.
/// </summary>
public uint Upsampling
{
get => this.upsampling;
set => this.upsampling = value;
}
/// <summary>
/// Gets or sets the upsampling factors for extra channels.
/// </summary>
public List<uint> ExtraChannelUpsampling
{
get => this.extraChannelUpsampling;
set => this.extraChannelUpsampling = value;
}
/// <summary>
/// Gets or sets the frame origin point.
/// </summary>
public Point FrameOrigin
{
get => this.frameOrigin;
set => this.frameOrigin = value;
}
/// <summary>
/// Gets or sets the blending information for the frame.
/// </summary>
public JxlBlendingInfo? BlendingInfo
{
get => this.blendingInfo;
set => this.blendingInfo = value;
}
/// <summary>
/// Gets or sets the blending information for extra channels.
/// </summary>
public List<JxlBlendingInfo> ExtraChannelBlendingInfo
{
get => this.extraChannelBlendingInfo;
set => this.extraChannelBlendingInfo = value;
}
/// <summary>
/// Gets the associated animation frame, if any.
/// </summary>
public JxlAnimationFrame? AnimationFrame => this.animationFrame;
/// <summary>
/// Gets or sets a value indicating whether this is the last frame.
/// </summary>
public bool IsLast
{
get => this.isLast;
set => this.isLast = value;
}
/// <summary>
/// Gets or sets the reference frame index to save.
/// </summary>
public uint SaveAsReference
{
get => this.saveAsReference;
set => this.saveAsReference = value;
}
/// <summary>
/// Gets or sets a value indicating whether to save before color transform.
/// </summary>
public bool SaveBeforeColorTransform
{
get => this.saveBeforeColorTransform;
set => this.saveBeforeColorTransform = value;
}
/// <summary>
/// Gets or sets the DC level of the frame.
/// </summary>
public uint DcLevel
{
get => this.dcLevel;
set => this.dcLevel = value;
}
/// <summary>
/// Gets or sets the codec metadata.
/// </summary>
public JxlCodecMetadata? Metadata
{
get => this.metadata;
set => this.metadata = value;
}
/// <summary>
/// Gets or sets the loop filter applied to the frame.
/// </summary>
public JxlLoopFilter? LoopFilter
{
get => this.loopFilter;
set => this.loopFilter = value;
}
/// <summary>
/// Gets or sets a value indicating whether this is a preview frame. Non-serialized.
/// </summary>
public bool IsPreviewFrame
{
get => this.isPreviewFrame;
set => this.isPreviewFrame = value;
}
/// <summary>
/// Gets or sets the number of extensions.
/// </summary>
public ulong Extensions
{
get => this.extensions;
set => this.extensions = value;
}
public int DefaultXSize
{
get
{
if (this.metadata == null)
{
return 0;
}
if (this.isPreviewFrame)
{
return this.metadata.ImageMetadata?.PreviewSize?.XSize ?? 0;
}
return this.metadata.XSize;
}
}
public int DefaultYSize
{
get
{
if (this.metadata == null)
{
return 0;
}
if (this.isPreviewFrame)
{
return this.metadata.ImageMetadata?.PreviewSize?.YSize ?? 0;
}
return this.metadata.YSize;
}
}
public JxlFrameDimensions FrameDimensions
{
get
{
int xsize = this.DefaultXSize;
int ysize = this.DefaultYSize;
xsize = this.frameSize.Width != 0 ? this.frameSize.Width : xsize;
ysize = this.frameSize.Height != 0 ? this.frameSize.Height : ysize;
if (this.dcLevel != 0)
{
xsize = JxlMath.DivCeil(xsize, 1 << (3 * (int)this.dcLevel));
ysize = JxlMath.DivCeil(ysize, 1 << (3 * (int)this.dcLevel));
}
JxlFrameDimensions frameDim = new(
xsize,
ysize,
(int)this.groupSizeShift,
this.chromaSubsampling?.MaxHShift ?? 0,
this.chromaSubsampling?.MaxVShift ?? 0,
this.encoding == JxlFrameEncoding.Modular,
(int)this.upsampling);
return frameDim;
}
}
public bool NeedsColorTransform => !this.saveBeforeColorTransform ||
this.frameType == JxlFrameType.RegularFrame ||
this.frameType == JxlFrameType.SkipProgressive;
/// <summary>
/// Gets a value indicating whether this frame is supposed to be saved for future usage by other frames.
/// </summary>
public bool CanBeReferenced => // DC frames cannot be referenced. The last frame cannot be referenced.
// A duration 0 frame makes little sense if it is not referenced.
// A non-duration 0 frame may or may not be referenced.
!this.isLast &&
this.frameType != JxlFrameType.DcFrame &&
(this.animationFrame?.Duration == 0 || this.saveAsReference != 0);
private void UpdateFlag(bool condition, ulong flag)
{
if (condition)
{
this.flags |= flag;
}
else
{
this.flags &= ~flag;
}
}
public bool Visit(JxlVisitor visitor)
{
bool allDefault = false;
if (visitor.AllDefault(this, ref allDefault))
{
visitor.SetDefault(this);
return true;
}
if (!VisitFrameType(visitor, JxlFrameType.RegularFrame, ref this.frameType))
{
return false;
}
if (visitor.IsReading && this.isPreviewFrame && this.frameType != JxlFrameType.RegularFrame)
{
throw new InvalidOperationException("Only regular frame could be a preview");
}
// FrameEncoding
bool isModular = this.encoding == JxlFrameEncoding.Modular;
if (!visitor.Boolean(false, ref isModular))
{
return false;
}
this.encoding = isModular
? JxlFrameEncoding.Modular
: JxlFrameEncoding.VarDct;
// Flags
if (!visitor.U64(0, ref this.flags))
{
return false;
}
// Color transform
bool xybEncoded = this.metadata?.ImageMetadata?.XybEncoded == true;
if (xybEncoded)
{
this.colorTransform = JxlColorTransform.Xyb;
}
else
{
bool alternate = this.colorTransform == JxlColorTransform.YCbCr;
if (!visitor.Boolean(false, ref alternate))
{
return false;
}
this.colorTransform = alternate
? JxlColorTransform.YCbCr
: JxlColorTransform.None;
}
// Chroma subsampling
if (visitor.Conditional(this.colorTransform == JxlColorTransform.YCbCr &&
((this.flags & (ulong)JxlFrameHeaderFlags.Dc) == 0)))
{
if (!visitor.VisitNested(this.chromaSubsampling!))
{
return false;
}
}
int numExtraChannels = this.metadata?.ImageMetadata?.ExtraChannelCount ?? 0;
// Upsampling
if (visitor.Conditional((this.flags & (ulong)JxlFrameHeaderFlags.Dc) == 0))
{
if (!visitor.U32(
JxlFieldExpressions.Value(1),
JxlFieldExpressions.Value(2),
JxlFieldExpressions.Value(4),
JxlFieldExpressions.Value(8),
1,
ref this.upsampling))
{
return false;
}
if (this.metadata != null && visitor.Conditional(numExtraChannels != 0))
{
List<JxlExtraChannelInfo> extraChannels = this.metadata!.ImageMetadata?.ExtraChannels ?? [];
this.extraChannelUpsampling = new List<uint>(extraChannels.Count);
for (int i = 0; i < extraChannels.Count; i++)
{
uint dimShift = (uint)extraChannels[i].DimensionShift;
uint ecUpsampling = 1;
ecUpsampling >>= (int)dimShift;
if (!visitor.U32(
JxlFieldExpressions.Value(1),
JxlFieldExpressions.Value(2),
JxlFieldExpressions.Value(4),
JxlFieldExpressions.Value(8),
1,
ref ecUpsampling))
{
return false;
}
ecUpsampling <<= (int)dimShift;
if (ecUpsampling < this.upsampling)
{
throw new InvalidOperationException("EC upsampling < color upsampling, invalid");
}
if (ecUpsampling > 8)
{
throw new InvalidOperationException("EC upsampling too large");
}
this.extraChannelUpsampling.Add(ecUpsampling);
}
}
else
{
this.extraChannelUpsampling.Clear();
}
}
// Modular / VarDCT specifics
if (visitor.Conditional(this.encoding == JxlFrameEncoding.Modular))
{
if (!visitor.Bits(2, 1, ref this.groupSizeShift))
{
return false;
}
}
if (visitor.Conditional(this.encoding == JxlFrameEncoding.VarDct &&
this.colorTransform == JxlColorTransform.Xyb))
{
if (!visitor.Bits(3, 3, ref this.xQmScale))
{
return false;
}
if (!visitor.Bits(3, 2, ref this.bQmScale))
{
return false;
}
}
else
{
this.xQmScale = this.bQmScale = 2;
}
// Passes
if (visitor.Conditional(this.frameType != JxlFrameType.ReferenceOnly))
{
if (!visitor.VisitNested(this.passes))
{
return false;
}
}
// DC frame
if (visitor.Conditional(this.frameType == JxlFrameType.DcFrame))
{
if (!visitor.U32(
JxlFieldExpressions.Value(1),
JxlFieldExpressions.Value(2),
JxlFieldExpressions.Value(3),
JxlFieldExpressions.Value(4),
1,
ref this.dcLevel))
{
return false;
}
}
else
{
this.dcLevel = 0;
}
// Custom size/origin
bool isPartialFrame = false;
if (visitor.Conditional(this.frameType != JxlFrameType.DcFrame))
{
if (!visitor.Boolean(false, ref this.customSizeOrOrigin))
{
return false;
}
if (visitor.Conditional(this.customSizeOrOrigin))
{
JxlU32Enc enc = new(
JxlFieldExpressions.Bits(8),
JxlFieldExpressions.BitsOffset(11, 256),
JxlFieldExpressions.BitsOffset(14, 2304),
JxlFieldExpressions.BitsOffset(30, 18688));
if (visitor.Conditional(this.frameType is JxlFrameType.RegularFrame or JxlFrameType.SkipProgressive))
{
uint ux0 = JxlPackSigned.PackUnsigned(this.frameOrigin.X);
uint uy0 = JxlPackSigned.PackUnsigned(this.frameOrigin.Y);
if (!visitor.U32(enc, 0, ref ux0))
{
return false;
}
if (!visitor.U32(enc, 0, ref uy0))
{
return false;
}
this.frameOrigin = new Point(JxlPackSigned.UnpackSigned(ux0), JxlPackSigned.UnpackSigned(uy0));
}
uint frameSizeWidth = (uint)this.frameSize.Width;
uint frameSizeHeight = (uint)this.frameSize.Height;
if (!visitor.U32(enc, 0, ref frameSizeWidth))
{
return false;
}
if (!visitor.U32(enc, 0, ref frameSizeHeight))
{
return false;
}
if (this.customSizeOrOrigin && (this.frameSize.Width == 0 || this.frameSize.Height == 0))
{
throw new InvalidOperationException("Invalid crop dimensions for frame");
}
int imageXSize = this.DefaultXSize;
int imageYSize = this.DefaultYSize;
if (this.frameType is JxlFrameType.RegularFrame or JxlFrameType.SkipProgressive)
{
isPartialFrame |= this.frameOrigin.X > 0;
isPartialFrame |= this.frameOrigin.Y > 0;
isPartialFrame |= (this.frameSize.Width + this.frameOrigin.X) < imageXSize;
isPartialFrame |= (this.frameSize.Height + this.frameOrigin.Y) < imageYSize;
}
}
}
// Blending, animation, last frame
if (visitor.Conditional(this.frameType is JxlFrameType.RegularFrame or JxlFrameType.SkipProgressive))
{
this.blendingInfo!.ExtraChannelCount = numExtraChannels;
this.blendingInfo.IsPartialFrame = isPartialFrame;
if (!visitor.VisitNested(this.blendingInfo))
{
return false;
}
bool replaceAll = this.blendingInfo.BlendMode == JxlBlendMode.Replace;
this.extraChannelBlendingInfo = new List<JxlBlendingInfo>(numExtraChannels);
for (int i = 0; i < numExtraChannels; i++)
{
JxlBlendingInfo ecBlendingInfo = new()
{
IsPartialFrame = isPartialFrame,
ExtraChannelCount = numExtraChannels
};
if (!visitor.VisitNested(ecBlendingInfo))
{
return false;
}
this.extraChannelBlendingInfo.Add(ecBlendingInfo);
replaceAll &= ecBlendingInfo.BlendMode == JxlBlendMode.Replace;
}
if (visitor.IsReading && this.isPreviewFrame)
{
if (!replaceAll || this.customSizeOrOrigin)
{
throw new InvalidOperationException("Preview is not compatible with blending");
}
}
if (visitor.Conditional(this.metadata?.ImageMetadata?.HaveAnimation == true))
{
this.animationFrame!.CodecMetadata = this.metadata;
if (!visitor.VisitNested(this.animationFrame!))
{
return false;
}
}
if (!visitor.Boolean(true, ref this.isLast))
{
return false;
}
}
else
{
this.isLast = false;
}
// SaveAsReference
if (visitor.Conditional(this.frameType != JxlFrameType.DcFrame && !this.isLast))
{
if (!visitor.U32(
JxlFieldExpressions.Value(0),
JxlFieldExpressions.Value(1),
JxlFieldExpressions.Value(2),
JxlFieldExpressions.Value(3),
0,
ref this.saveAsReference))
{
return false;
}
}
// SaveBeforeColorTransform logic
if (this.frameType != JxlFrameType.DcFrame)
{
if (visitor.Conditional(
this.CanBeReferenced &&
this.blendingInfo?.BlendMode == JxlBlendMode.Replace &&
!isPartialFrame &&
(this.frameType == JxlFrameType.RegularFrame ||
this.frameType == JxlFrameType.SkipProgressive)))
{
if (!visitor.Boolean(false, ref this.saveBeforeColorTransform))
{
return false;
}
}
else if (visitor.Conditional(this.frameType == JxlFrameType.ReferenceOnly))
{
if (!visitor.Boolean(true, ref this.saveBeforeColorTransform))
{
return false;
}
int xsize = this.customSizeOrOrigin
? this.frameSize.Width
: this.metadata!.XSize;
int ysize = this.customSizeOrOrigin
? this.frameSize.Height
: this.metadata!.YSize;
if (!this.saveBeforeColorTransform &&
(xsize < this.metadata!.XSize ||
ysize < this.metadata!.YSize ||
this.frameOrigin.X != 0 ||
this.frameOrigin.Y != 0))
{
throw new InvalidOperationException("Non-patch reference frame with invalid crop");
}
}
}
else
{
this.saveBeforeColorTransform = true;
}
if (!VisitNameString(visitor, ref this.name))
{
return false;
}
this.loopFilter!.IsModular = isModular;
if (!visitor.VisitNested(this.loopFilter!))
{
return false;
}
if (!visitor.BeginExtensions(ref this.extensions))
{
return false;
}
return visitor.EndExtensions();
}
}

36
src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeaderFlags.cs

@ -0,0 +1,36 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
/// <summary>
/// Optional steps for postprocessing. These flags are the
/// source of truth. Override must set/clear them rather than
/// change their meaning. Values chosen such that typical flags
/// are 0, encoded in only two bits.
/// </summary>
[Flags]
internal enum JxlFrameHeaderFlags : byte
{
/// <summary>
/// Noise is injected into decoded output.
/// </summary>
Noise = 1,
/// <summary>
/// Overlay patches.
/// </summary>
Patches = 2,
/// <summary>
/// Overlay splines.
/// </summary>
Splines = 16,
/// <summary>
/// Implies skip adaptive DC smoothing.
/// </summary>
Dc = 32,
SkipAdaptiveDcSmoothing = 128,
}

37
src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameType.cs

@ -0,0 +1,37 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
/// <summary>
/// Defines the type of a JPEG XL frame.
/// </summary>
internal enum JxlFrameType : byte
{
/// <summary>
/// A regular frame. It might be a crop, and it will be blended
/// on a previous frame (if any) and likely displayed or blended in
/// future frames.
/// </summary>
RegularFrame,
/// <summary>
/// A DC frame. It is downsampled and only used as the DC
/// of a future and, possibly, preview frame. This cannot be cropped,
/// blended, or referenced by patches or blending modes. Frames using
/// DC cannot have non-default sizes.
/// </summary>
DcFrame,
/// <summary>
/// A PatchesSource frame. Can only be used as source frame for
/// taking patches. It can be cropped but can't have a non-(0, 0) x0/y0.
/// </summary>
ReferenceOnly = 2,
/// <summary>
/// Same as regular frame but not used for progressive rendering.
/// Implies no early display of DC.
/// </summary>
SkipProgressive,
}

220
src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlPasses.cs

@ -0,0 +1,220 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jxl.Fields;
namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
/// <summary>
/// Used for decoding to lower resolutions.
/// </summary>
internal sealed class JxlPasses : IJxlFields
{
/// <summary>
/// Defines the maximum amount of passes, which is 11.
/// </summary>
private const int MaxPasses = 11;
private uint numPasses;
private uint numDownsample;
/// <summary>
/// Gets or sets the number of passes.
/// </summary>
public uint NumberOfPasses
{
get => this.numPasses;
set => this.numPasses = value;
}
/// <summary>
/// Gets or sets the number of downsamples.
/// </summary>
public uint NumberOfDownsamples
{
get => this.numDownsample;
set => this.numDownsample = value;
}
/// <summary>
/// Gets the downsample values.
/// </summary>
public uint[] Downsample { get; } = new uint[MaxPasses];
/// <summary>
/// Gets the last pass values.
/// </summary>
public uint[] LastPass { get; } = new uint[MaxPasses];
/// <summary>
/// Gets the shift values.
/// </summary>
public uint[] Shift { get; } = new uint[MaxPasses];
public void GetDownsamplingBracket(int pass, out int minShift, out int maxShift)
{
maxShift = 2;
minShift = 3;
for (int i = 0; ; i++)
{
for (int j = 0; j < this.numDownsample; ++j)
{
if (i == this.LastPass[j])
{
uint ds = this.Downsample[j];
if (ds == 8)
{
minShift = 3;
}
if (ds == 4)
{
minShift = 2;
}
if (ds == 2)
{
minShift = 1;
}
if (ds == 1)
{
minShift = 0;
}
}
}
if (i == this.numPasses - 1)
{
minShift = 0;
}
if (i == pass)
{
return;
}
maxShift = minShift - 1;
}
}
public uint GetDownsamplingTargetForCompletedPasses(int num)
{
if (num >= this.numPasses)
{
return 1;
}
uint result = 0;
for (int i = 0; i < this.numDownsample; i++)
{
if (num > this.LastPass[i])
{
result = Math.Min(result, this.Downsample[i]);
}
}
return result;
}
public bool Visit(JxlVisitor visitor)
{
if (visitor.U32(
JxlFieldExpressions.Value(1),
JxlFieldExpressions.Value(2),
JxlFieldExpressions.Value(2),
JxlFieldExpressions.BitsOffset(1, 3),
0,
ref this.numPasses))
{
return false;
}
if (this.numPasses > MaxPasses)
{
return false;
}
if (visitor.Conditional(this.numPasses != 1))
{
if (!visitor.U32(
JxlFieldExpressions.Value(0),
JxlFieldExpressions.Value(1),
JxlFieldExpressions.Value(2),
JxlFieldExpressions.BitsOffset(1, 3),
0,
ref this.numDownsample))
{
return false;
}
if (this.numDownsample > 4)
{
return false;
}
if (this.numDownsample > this.numPasses)
{
throw new InvalidOperationException("Number of downsaples is greater than number of passes");
}
for (int i = 0; i < this.numPasses - 1; i++)
{
if (!visitor.Bits(2, 0u, ref this.Shift[i]))
{
return false;
}
}
this.Shift[this.numPasses - 1] = 0;
for (int i = 0; i < this.numDownsample; i++)
{
if (!visitor.U32(
JxlFieldExpressions.Value(1),
JxlFieldExpressions.Value(2),
JxlFieldExpressions.Value(4),
JxlFieldExpressions.Value(8),
1,
ref this.Downsample[i]))
{
return false;
}
if (i > 0 && this.Downsample[i] >= this.Downsample[i - 1])
{
throw new InvalidOperationException("Downsample sequence should decrease");
}
}
for (int i = 0; i < this.numDownsample; i++)
{
if (!visitor.U32(
JxlFieldExpressions.Value(0),
JxlFieldExpressions.Value(1),
JxlFieldExpressions.Value(2),
JxlFieldExpressions.Value(3),
0,
ref this.LastPass[i]))
{
return false;
}
if (i > 0 && this.LastPass[i] <= this.LastPass[i - 1])
{
throw new InvalidOperationException("Last pass sequence should increase");
}
if (this.LastPass[i] >= this.numPasses)
{
throw new InvalidOperationException("Last pass is greater than number of passes");
}
}
}
return true;
}
}

147
src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlYCbCrChromaSubsampling.cs

@ -0,0 +1,147 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jxl.Fields;
namespace SixLabors.ImageSharp.Formats.Jxl.IO.FrameHeader;
/// <summary>
/// Gets the Y'Cb'Cr chroma subsampling information as part
/// of the JPEG XL Frame Header.
/// </summary>
internal sealed class JxlYCbCrChromaSubsampling : IJxlFields
{
private readonly int[] channelMode = new int[3];
private static ReadOnlySpan<byte> HShiftData => [0, 1, 1, 0];
private static ReadOnlySpan<byte> VShiftData => [0, 1, 0, 1];
public byte MaxHShift { get; private set; }
public byte MaxVShift { get; private set; }
/// <summary>
/// Gets a value indicating whether this 4:4:4 chroma subsampling.
/// </summary>
public bool Is444 =>
this.HShift(0) == 0 && this.VShift(0) == 0 && // Cb
this.HShift(2) == 0 && this.VShift(2) == 0 && // Cr
this.HShift(1) == 0 && this.VShift(1) == 0; // Y;
/// <summary>
/// Gets a value indicating whether this 4:2:0 chroma subsampling.
/// </summary>
public bool Is420 =>
this.HShift(0) == 1 && this.VShift(0) == 1 && // Cb
this.HShift(2) == 1 && this.VShift(2) == 1 && // Cr
this.HShift(1) == 0 && this.VShift(1) == 0; // Y
/// <summary>
/// Gets a value indicating whether this 4:2:2 chroma subsampling.
/// </summary>
public bool Is422 =>
this.HShift(0) == 1 && this.VShift(0) == 0 && // Cb
this.HShift(2) == 1 && this.VShift(2) == 0 && // Cr
this.HShift(1) == 0 && this.VShift(1) == 0; // Y
/// <summary>
/// Gets a value indicating whether this 4:4:0 chroma subsampling.
/// </summary>
public bool Is440 =>
this.HShift(0) == 0 && this.VShift(0) == 1 && // Cb
this.HShift(2) == 0 && this.VShift(2) == 1 && // Cr
this.HShift(1) == 0 && this.VShift(1) == 0; // Y
public byte RawHShift(int c) => HShiftData[this.channelMode[c]];
public byte RawVShift(int c) => VShiftData[this.channelMode[c]];
public byte HShift(int c) => (byte)(this.MaxHShift - HShiftData[this.channelMode[c]]);
public byte VShift(int c) => (byte)(this.MaxVShift - VShiftData[this.channelMode[c]]);
private void Recompute()
{
this.MaxHShift = 0;
this.MaxVShift = 0;
for (int i = 0; i < 3; i++)
{
int ch = this.channelMode[i];
this.MaxHShift = Math.Max(this.MaxHShift, HShiftData[ch]);
this.MaxVShift = Math.Max(this.MaxVShift, VShiftData[ch]);
}
}
public bool Set(ReadOnlySpan<byte> hsample, ReadOnlySpan<byte> vsample)
{
for (int c = 0; c < 3; c++)
{
int cjpeg = c < 2 ? (c ^ 1) : c;
int i = 0;
for (; i < 4; i++)
{
if (1 << HShiftData[i] == hsample[cjpeg] && 1 << VShiftData[i] == vsample[cjpeg])
{
this.channelMode[c] = i;
break;
}
}
if (i == 4)
{
return false;
}
}
this.Recompute();
return true;
}
public override string ToString()
{
if (this.Is444)
{
return "4:4:4";
}
else if (this.Is420)
{
return "4:2:0";
}
else if (this.Is422)
{
return "4:2:2";
}
else if (this.Is440)
{
return "4:4:0";
}
else
{
return $"[Custom] {this.channelMode[0]}:{this.channelMode[1]}:{this.channelMode[2]}";
}
}
public bool Visit(JxlVisitor visitor)
{
for (int i = 0; i < 3; i++)
{
int channel = this.channelMode[i];
uint unsignedChannel = (uint)channel;
bool wroteSuccessfully = visitor.Bits(2, 0, ref unsignedChannel);
if (!wroteSuccessfully)
{
return false;
}
this.channelMode[i] = (int)unsignedChannel;
}
return true;
}
}

30
src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegAppMarkerType.cs

@ -0,0 +1,30 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data;
/// <summary>
/// Identifies the kind of APP marker in a JPEG file.
/// </summary>
internal enum JpegAppMarkerType : byte
{
/// <summary>
/// Unknown APP marker
/// </summary>
Unknown,
/// <summary>
/// Contains ICC profile metadata
/// </summary>
Icc,
/// <summary>
/// Contains EXIF profile metadata
/// </summary>
Exif,
/// <summary>
/// Contains XMP profile metadata
/// </summary>
Xmp
}

53
src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponent.cs

@ -0,0 +1,53 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data;
// We may need to get a ref to fields, so don't
// make these properties.
#pragma warning disable SA1401 // Fields should be private
/// <summary>
/// Represents one component of a jpeg file.
/// </summary>
internal sealed class JpegComponent
{
/// <summary>
/// One-byte id of the component
/// </summary>
public int Id;
/// <summary>
/// In interleaved mode, each minimal coded unit (MCU)
/// has horizontal x vertical sample factor DCT blocks
/// from this component. This is the horizontal factor.
/// </summary>
public int HorizontalSampleFactor = 1;
/// <summary>
/// In interleaved mode, each minimal coded unit (MCU)
/// has horizontal x vertical sample factor DCT blocks
/// from this component. This is the vertical factor.
/// </summary>
public int VerticalSampleFactor = 1;
/// <summary>
/// Index of quantization table used for this component.
/// </summary>
public int QuantIndex;
/// <summary>
/// Width measured in 8x8 blocks
/// </summary>
public int WidthInBlocks;
/// <summary>
/// Width measured in 8x8 blocks
/// </summary>
public int HeightInBlocks;
/// <summary>
/// Gets or sets DCT coefficients.
/// </summary>
public List<int> Coefficients { get; set; } = [];
}

16
src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponentScanInfo.cs

@ -0,0 +1,16 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data;
/// <summary>
/// Huffman table indexes used for one component of one scan.
/// </summary>
// We may need to get a ref to fields, so don't
// make these properties.
internal struct JpegComponentScanInfo
{
public int ComponentIndex;
public int DcTableIndex;
public int AcTableIndex;
}

15
src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponentType.cs

@ -0,0 +1,15 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data;
internal enum JpegComponentType : byte
{
Gray,
YCbCr,
Rgb,
Custom
}

1004
src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs

File diff suppressed because it is too large

105
src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegDataConstants.cs

@ -0,0 +1,105 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data;
/// <summary>
/// Constants used in parsed JPEG data.
/// </summary>
internal static class JpegDataConstants
{
/// <summary>
/// Maximum number of components.
/// </summary>
public const int MaxComponents = 4;
/// <summary>
/// Maximum number of quantizer tables.
/// </summary>
public const int MaximumQuantizationTables = 4;
/// <summary>
/// Maximum number of Huffman code tables.
/// </summary>
public const int MaxHuffmanTables = 4;
/// <summary>
/// Maximum number of bits for a Huffman code.
/// </summary>
public const int JpegHuffmanMaxBitLength = 16;
/// <summary>
/// Alphabet size for Huffman tables used in the JPEG format.
/// </summary>
public const int JpegHuffmanAlphabetSize = 256;
/// <summary>
/// Alphabet size for Huffman tables used in the JPEG format.
/// </summary>
/// <remarks>
/// This is specific to the DC coefficients of the Discrete
/// Cosine Transform.
/// </remarks>
public const int JpegDcAlphabetSize = 12;
/// <summary>
/// Maximum number of DHT "Define Huffman Tables" markers.
/// </summary>
public const int MaxDhtMarkers = 512;
/// <summary>
/// Largest value for width OR height.
/// </summary>
public const int MaxDimPixels = 65535;
/// <summary>
/// Marker that specifies APP1.
/// </summary>
public const int App1 = 0xE1;
/// <summary>
/// Marker that specifies APP2.
/// </summary>
public const int App2 = 0xE2;
/// <summary>
/// Gets the tag bytes specifying the ICC profile.
/// </summary>
public static ReadOnlySpan<byte> IccProfileTag => "ICC_PROFILE\0"u8;
/// <summary>
/// Gets the tag bytes specifying the EXIF profile.
/// </summary>
public static ReadOnlySpan<byte> ExifTag => "Exif\0\0"u8;
/// <summary>
/// Gets the tag bytes specifying the XMP profile.
/// </summary>
public static ReadOnlySpan<byte> XmpTag => "http://ns.adobe.com/xap/1.0/\0"u8;
public static ReadOnlySpan<int> JpegNaturalOrder =>
[
0, 1, 8, 16, 9, 2, 3, 10,
17, 24, 32, 25, 18, 11, 4, 5,
12, 19, 26, 33, 40, 48, 41, 34,
27, 20, 13, 6, 7, 14, 21, 28,
35, 42, 49, 56, 57, 50, 43, 36,
29, 22, 15, 23, 30, 37, 44, 51,
58, 59, 52, 45, 38, 31, 39, 46,
53, 60, 61, 54, 47, 55, 62, 63,
63, 63, 63, 63, 63, 63, 63, 63,
63, 63, 63, 63, 63, 63, 63, 63
];
public static ReadOnlySpan<int> JpegZigZagOrder =>
[
0, 1, 5, 6, 14, 15, 27, 28,
2, 4, 7, 13, 16, 26, 29, 42,
3, 8, 12, 17, 25, 30, 41, 43,
9, 11, 18, 24, 31, 40, 44, 53,
10, 19, 23, 32, 39, 45, 52, 54,
20, 22, 33, 38, 46, 51, 55, 60,
21, 34, 37, 47, 50, 56, 59, 61,
35, 36, 48, 49, 57, 58, 62, 63
];
}

11
src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegExtraZeroRunInfo.cs

@ -0,0 +1,11 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data;
internal struct JpegExtraZeroRunInfo
{
public int BlockIndex;
public int NumExtraZeroRuns;
}

30
src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegHuffmanCode.cs

@ -0,0 +1,30 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data;
// We may need to get a ref to fields, so don't
// make these properties.
internal struct JpegHuffmanCode()
{
/// <summary>
/// Bit length histogram
/// </summary>
public InlineArray17<int> Counts;
/// <summary>
/// Symbol values stored by increasing bit lengths.
/// </summary>
public InlineArray17<int> Values;
/// <summary>
/// The index of the code in the current set of Huffman codes.
/// For AC component Huffman codes, 0x10 is added to the index.
/// </summary>
public int SlotId;
/// <summary>
/// True if the code is last within its marker segment.
/// </summary>
public bool IsLast = true;
}

17
src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegInfo.cs

@ -0,0 +1,17 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data;
internal struct JpegInfo
{
public int NumberOfAppMarkers { get; set; }
public int NumberOfComMarkers { get; set; }
public int NumberOfScans { get; set; }
public int NumberOfIntermarkers { get; set; }
public bool HasDri { get; set; }
}

31
src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs

@ -0,0 +1,31 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data;
/// <summary>
/// Representation of quantization values for an 8x8 pixel block.
/// </summary>
// We may need to get a ref to fields, so don't
// make these properties.
internal struct JpegQuantizationTable()
{
/// <summary>
/// Quantization values
/// </summary>
public InlineArray64<int> Values;
public int Precision;
/// <summary>
/// Gets or sets the index of the quantization table
/// as it was parsed from the input JPEG.
/// </summary>
public int Index;
/// <summary>
/// Gets or sets a value indicating whether this table
/// is the last one within its marker segment.
/// </summary>
public bool IsLast = true;
}

43
src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegScanInfo.cs

@ -0,0 +1,43 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg.Data;
// We may need to get a ref to fields, so don't
// make these properties.
#pragma warning disable SA1401 // Fields should be private
internal sealed class JpegScanInfo
{
// Variables copied from ITU-T T.81 spec
/// <summary>
/// Start of spectral band in zigzag sequence
/// </summary>
public int Ss;
/// <summary>
/// End of spectral band in zigzag sequence
/// </summary>
public int Se;
/// <summary>
/// Successive approximation bit position. (High)
/// </summary>
public int Ah;
/// <summary>
/// Successive approximation bit position. (Low)
/// </summary>
public int Al;
public int NumComponents;
public InlineArray4<JpegComponentScanInfo> Components;
public int LastNeededPass;
public List<int> ResetPoints { get; set; } = [];
public List<JpegExtraZeroRunInfo> ExtraZeroRuns { get; set; } = [];
}

187
src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegBitWriter.cs

@ -0,0 +1,187 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg;
internal sealed class JpegBitWriter
{
private const int ChunkSize = 16 * 1024;
private readonly Stream stream;
private readonly byte[] buffer;
private int pos;
private ulong putBuffer;
private int putBits;
public JpegBitWriter(Stream stream)
{
this.stream = stream;
this.buffer = new byte[ChunkSize];
this.putBits = 64;
}
public bool Healthy { get; private set; } = true;
private void SwapBuffer()
{
if (this.pos == 0)
{
return;
}
this.stream.Write(this.buffer, 0, this.pos);
this.pos = 0;
}
public void Reserve(int nBytes)
{
if (this.pos + nBytes > this.buffer.Length)
{
this.SwapBuffer();
}
}
private void EmitByte(int value)
{
this.buffer[this.pos++] = (byte)value;
if (value == 0xff)
{
this.buffer[this.pos++] = 0;
}
}
public void EmitMarker(int marker)
{
this.Reserve(2);
this.buffer[this.pos++] = 0xff;
this.buffer[this.pos++] = (byte)marker;
}
public void StoreBe64(ulong value)
{
Span<byte> span = this.buffer.AsSpan(this.pos);
BinaryPrimitives.WriteUInt64BigEndian(span, value);
this.pos += 8;
}
public void DischargeBitBuffer(int nBits, ulong bits)
{
this.putBuffer |= bits >> -this.putBits;
if (HasZeroByte(~this.putBuffer))
{
this.Reserve(16);
this.EmitByte((int)(this.putBuffer >> 56));
this.EmitByte((int)(this.putBuffer >> 48));
this.EmitByte((int)(this.putBuffer >> 40));
this.EmitByte((int)(this.putBuffer >> 32));
this.EmitByte((int)(this.putBuffer >> 24));
this.EmitByte((int)(this.putBuffer >> 16));
this.EmitByte((int)(this.putBuffer >> 8));
this.EmitByte((int)this.putBuffer);
}
else
{
this.Reserve(8);
this.StoreBe64(this.putBuffer);
}
this.putBits += 64;
this.putBuffer = bits << this.putBits;
}
public void WriteBits(int nBits, ulong bits)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(nBits);
this.putBits -= nBits;
if (this.putBits < 0)
{
if (nBits > 64)
{
this.putBits += nBits;
this.Healthy = false;
}
else
{
this.DischargeBitBuffer(nBits, bits);
}
}
else
{
this.putBuffer |= bits << this.putBits;
}
}
public bool JumpToByteBoundary(ref ReadOnlySpan<byte> padBits)
{
int nBits = this.putBits & 7;
byte padPattern;
if (padBits.IsEmpty)
{
padPattern = (byte)((1 << nBits) - 1);
}
else
{
padPattern = 0;
byte danglingBits = 0;
for (int i = 0; i < nBits; ++i)
{
if (padBits.IsEmpty)
{
return false;
}
byte bit = padBits[0];
padBits = padBits[1..];
danglingBits |= bit;
padPattern <<= 1;
padPattern |= bit;
}
if ((danglingBits & ~1) != 0)
{
return false;
}
}
this.Reserve(16);
while (this.putBits <= 56)
{
int c = (int)(this.putBuffer >> 56);
this.EmitByte(c);
this.putBuffer <<= 8;
this.putBits += 8;
}
if (this.putBits < 64)
{
int padMask = 0xff >> (64 - this.putBits);
int c =
((int)(this.putBuffer >> 56) & ~padMask) |
padPattern;
this.EmitByte(c);
}
this.putBuffer = 0;
this.putBits = 64;
return true;
}
}

17
src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegDctCodingState.cs

@ -0,0 +1,17 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg;
internal sealed class JpegDctCodingState
{
public int EobRun { get; set; }
public JpegHuffmanCodeTable? CurAcHuff { get; set; }
public List<ushort> RefinementBits { get; } = new(64);
public int RefinementBitsCount { get; set; }
public void Flush(JpegBitWriter writer) => JpegWriter.Flush(this, writer);
}

14
src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegDebug.cs

@ -0,0 +1,14 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics;
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg;
internal static class JpegDebug
{
[Conditional("DEBUG")]
public static void LogWarning(string message) =>
// TODO: do we allow Debug.WriteLine in this codebase?
Debug.WriteLine($"⚠️ JPEG XL [JPEG parser]: {message}");
}

22
src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanCodeTable.cs

@ -0,0 +1,22 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg;
// InlineArray values cannot be assigned as a property
#pragma warning disable SA1401 // Fields should be private
internal sealed class JpegHuffmanCodeTable
{
public InlineArray256<byte> Depth;
public InlineArray256<short> Code;
public bool Initialized { get; set; }
public void InitializeDepths(byte value = 0)
{
Span<byte> depths = this.Depth;
depths.Fill(value);
}
}

38
src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs

@ -0,0 +1,38 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg;
/// <summary>
/// Decodes Huffman codes in a JPEG file for JPEG to JPEG XL encoder.
/// </summary>
internal static class JpegHuffmanDecoder
{
public const int RootTableBits = 8;
public const int LookupSize = 1024;
private static int NextTableBitSize(Span<int> count, int length)
{
int left = 1 << (length - RootTableBits);
while (length < MaxBitLength)
{
left -= count[length];
if (left <= 0)
{
break;
}
length++;
left <<= 1;
}
return length - RootTableBits;
}
public struct HuffmanTableEntry()
{
public byte Bits = 0;
public ushort Value = 0xFFFF;
}
}

13
src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegReadMode.cs

@ -0,0 +1,13 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg;
internal enum JpegReadMode : byte
{
ReadHeader,
ReadTables,
ReadEverything
}

1497
src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegReader.cs

File diff suppressed because it is too large

12
src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegSerializationStage.cs

@ -0,0 +1,12 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg;
internal enum JpegSerializationStage : byte
{
Initialize,
SerializeSection,
Done,
Error
}

40
src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegSerializationState.cs

@ -0,0 +1,40 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg;
internal sealed class JpegSerializationState
{
public JpegSerializationStage Stage { get; set; } = JpegSerializationStage.Initialize;
public int SectionIndex { get; set; }
public int DhtIndex { get; set; }
public int DqtIndex { get; set; }
public int AppIndex { get; set; }
public int ComIndex { get; set; }
public int DataIndex { get; set; }
public int ScanIndex { get; set; }
public JpegHuffmanCodeTable[] DcHuffTable { get; set; } = [];
public JpegHuffmanCodeTable[] AcHuffTable { get; set; } = [];
// Should be field to get ref to it
public ReadOnlyMemory<byte> PadBits;
public int PadBitsPos { get; set; }
public bool SeenDriMarker { get; set; }
public bool IsProgressive { get; set; }
public List<List<byte>> OutputQueue { get; set; } = [];
public EncodeScanState ScanState { get; set; }
}

12
src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegSerializationStatus.cs

@ -0,0 +1,12 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Jpeg;
internal enum JpegSerializationStatus : byte
{
NeedsMoreInput,
NeedsMoreOutput,
Error,
Done
}

1304
src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegWriter.cs

File diff suppressed because it is too large

11
src/ImageSharp/Formats/Jxl/IO/Jpeg/README.md

@ -0,0 +1,11 @@
# Jxl/Processing/Jpeg
This folder contains logic to:
- Parse and represent JPEG data
- Write JPEG markers
- JPEG to JXL
- JXL to JPEG
This does not contain a JPEG codec.
Logic in this folder is used for JPEG&lt;-&gt;JXL lossless compression.

20
src/ImageSharp/Formats/Jxl/IO/JxlBoxCodingMode.cs

@ -0,0 +1,20 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO;
/// <summary>
/// Specifies the type of box content.
/// </summary>
internal enum JxlBoxCodingMode : byte
{
/// <summary>
/// Compress using Brotli codec.
/// </summary>
Brotli,
/// <summary>
/// No compression (raw contents).
/// </summary>
Uncompressed
}

31
src/ImageSharp/Formats/Jxl/IO/JxlDataType.cs

@ -0,0 +1,31 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO;
/// <summary>
/// Specifies which data type to use for sample values
/// per channel per pixel.
/// </summary>
internal enum JxlDataType : byte
{
/// <summary>
/// Use float
/// </summary>
Single = 0,
/// <summary>
/// Use byte. May clip wide color gamut data.
/// </summary>
Byte = 2,
/// <summary>
/// Use ushort. May clip wide color gamut data.
/// </summary>
UInt16 = 3,
/// <summary>
/// Use 16-bit IEEE 754 half-precision floating-point values.
/// </summary>
Half = 5
}

184
src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs

@ -0,0 +1,184 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jxl.IO.Entropy;
namespace SixLabors.ImageSharp.Formats.Jxl.IO;
/// <summary>
/// Shared Huffman I/O utilities.
/// </summary>
internal static class JxlHuffman
{
/// <summary>
/// Returns Reverse(Reverse(Key, Len) + 1, Len). The
/// Reverse(Key, Len) function performs bitwise reversal
/// of the len least significant bits of the key value.
/// </summary>
public static uint GetNextKey(uint key, int len)
{
uint step = 1u << (len - 1);
while ((key & step) != 0)
{
step >>= 1;
}
return (key & (step - 1)) + step;
}
/// <summary>
/// Replicates <paramref name="code"/> into <paramref name="table"/> every <paramref name="step"/>
/// times with the upper bound of <paramref name="end"/>.
/// </summary>
public static void ReplicateValue(Span<JxlHuffmanCode> table, int step, int end, JxlHuffmanCode code)
{
do
{
end -= step;
table[end] = code;
}
while (end > 0);
}
/// <summary>
/// Returns the table width of the next 2nd level table.
/// </summary>
/// <param name="count">The histogram of bit lengths for remaining symbols</param>
/// <param name="length">Code length of the next processed symbol</param>
/// <param name="rootBits">Amount of bits for the root symbol</param>
/// <returns>Table width for the 2nd level table.</returns>
public static int NextTableBitSize(ReadOnlySpan<ushort> count, int length, int rootBits)
{
uint left = 1u << (length - rootBits);
while (length < JxlAnsConstants.PrefixMaxBits)
{
if (left <= count[length])
{
break;
}
left -= count[length];
length++;
left <<= 1;
}
return length - rootBits;
}
public static uint BuildHuffmanTable(
Span<JxlHuffmanCode> rootTable,
int rootBits,
ReadOnlySpan<byte> codeLengths,
Span<ushort> count)
{
if (codeLengths.Length > (1u << JxlAnsConstants.PrefixMaxBits))
{
return 0u;
}
Span<ushort> offset = stackalloc ushort[JxlAnsConstants.PrefixMaxBits + 1];
Span<ushort> sortedStorage = stackalloc ushort[codeLengths.Length];
int maxLength = 1;
ushort sum = 0;
int len, symbol;
for (len = 1; len <= JxlAnsConstants.PrefixMaxBits; len++)
{
offset[len] = sum;
if (count[len] != 0)
{
sum = (ushort)(sum + count[len]);
maxLength = len;
}
}
for (symbol = 0; symbol < codeLengths.Length; symbol++)
{
if (codeLengths[symbol] != 0)
{
sortedStorage[offset[codeLengths[symbol]]++] = (ushort)symbol;
}
}
Span<JxlHuffmanCode> table = rootTable;
int tableBits = rootBits;
uint tableSize = 1u << tableBits;
uint totalSize = tableSize;
JxlHuffmanCode code = default;
if (offset[JxlAnsConstants.PrefixMaxBits] == 1)
{
code.Bits = 0;
code.Value = sortedStorage[0];
for (int i = 0; i < totalSize; i++)
{
table[i] = code;
}
}
if (tableBits > maxLength)
{
tableBits = maxLength;
tableSize = 1u << tableBits;
}
int key = 0;
code.Bits = 0;
int step = 2;
do
{
for (; count[code.Bits] != 0; --count[code.Bits])
{
code.Value = sortedStorage[symbol++];
ReplicateValue(table[key..], step, (int)tableSize, code);
key = (int)GetNextKey((uint)key, code.Bits);
}
step <<= 1;
}
while (++code.Bits <= tableBits);
while (totalSize != tableSize)
{
table[..(int)tableSize].CopyTo(table[(int)tableSize..]);
tableSize <<= 1;
}
uint mask = totalSize - 1u;
int low = -1;
uint tableOffset = 0;
for (step = 2; len <= maxLength; len++, step <<= 1)
{
for (; count[len] != 0; --count[len])
{
if ((key & mask) != low)
{
tableOffset += tableSize;
table = table[(int)tableSize..];
tableBits = NextTableBitSize(count, len, rootBits);
tableSize = 1u << tableBits;
totalSize += tableSize;
low = key & (int)mask;
rootTable[low].Bits = (byte)(tableBits + rootBits);
rootTable[low].Value = (ushort)(tableOffset - low);
}
code.Bits = (byte)(len - rootBits);
code.Value = sortedStorage[symbol++];
ReplicateValue(table[(key >> rootBits)..], step, (int)tableSize, code);
key = (int)GetNextKey((uint)key, len);
}
}
return totalSize;
}
}

20
src/ImageSharp/Formats/Jxl/IO/JxlHuffmanCode.cs

@ -0,0 +1,20 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO;
/// <summary>
/// A single Huffman code.
/// </summary>
internal struct JxlHuffmanCode(byte bits, ushort value)
{
/// <summary>
/// Number of bits for this symbol.
/// </summary>
public byte Bits = bits;
/// <summary>
/// Symbol value/offset.
/// </summary>
public ushort Value = value;
}

82
src/ImageSharp/Formats/Jxl/IO/JxlMemoryWriter.cs

@ -0,0 +1,82 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Jxl.IO;
/// <summary>
/// A disposable writer for bytes in memory. It is highly similar to
/// <see cref="MemoryStream"/>, but its buffer relies on
/// <see cref="MemoryAllocator"/>.
/// </summary>
internal sealed class JxlMemoryWriter(MemoryAllocator allocator) : IDisposable
{
/// <summary>
/// The initial capacity in bytes.
/// </summary>
private const int InitialCapacity = 1024;
/// <summary>
/// Core buffer.
/// </summary>
private IMemoryOwner<byte> buffer = allocator.Allocate<byte>(InitialCapacity);
/// <summary>
/// Gets the length of the written data in bytes.
/// </summary>
public int Length { get; private set; }
/// <summary>
/// Gets the capacity of the buffer in bytes.
/// </summary>
public int Capacity => this.buffer.Memory.Length;
/// <summary>
/// Releases the underlying buffer.
/// </summary>
public void Dispose() => this.buffer.Dispose();
/// <summary>
/// Writes the specified bytes into the writer.
/// </summary>
/// <param name="bytes">The bytes to write.</param>
public void Write(ReadOnlySpan<byte> bytes)
{
int requiredCapacity = checked(this.Length + bytes.Length);
this.EnsureCapacity(requiredCapacity);
bytes.CopyTo(this.buffer.Memory.Span[this.Length..]);
this.Length = requiredCapacity;
}
/// <summary>
/// Returns a span containing the bytes written to the writer.
/// </summary>
/// <returns>A span containing the written bytes.</returns>
public Span<byte> AsSpan() => this.buffer.Memory.Span[..this.Length];
/// <summary>
/// Returns memory containing the bytes written to the writer.
/// </summary>
/// <returns>Memory containing the written bytes.</returns>
public Memory<byte> AsMemory() => this.buffer.Memory[..this.Length];
private void EnsureCapacity(int requiredCapacity)
{
if (requiredCapacity <= this.Capacity)
{
return;
}
int newCapacity = Math.Max(requiredCapacity, checked(this.Capacity * 2));
IMemoryOwner<byte> previousBuffer = this.buffer;
this.buffer = allocator.Allocate<byte>(newCapacity);
previousBuffer.Memory.Span[..this.Length].CopyTo(this.buffer.Memory.Span);
previousBuffer.Dispose();
}
}

19
src/ImageSharp/Formats/Jxl/IO/Metadata/JxlAnimationHeader.cs

@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jxl.Fields;
namespace SixLabors.ImageSharp.Formats.Jxl.IO.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();
}

129
src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepthMetadata.cs

@ -0,0 +1,129 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jxl.Fields;
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
/// <summary>
/// Represents the JPEG XL Bit Depth image metadata.
/// </summary>
internal sealed class JxlBitDepthMetadata : IJxlFields
{
private uint bitsPerSample;
private uint exponentBitsPerSample;
/// <summary>
/// Initializes a new instance of the <see cref="JxlBitDepthMetadata"/> class.
/// </summary>
public JxlBitDepthMetadata() => JxlBundle.Init(this);
/// <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 uint BitsPerSample
{
get => this.bitsPerSample;
set => this.bitsPerSample = value;
}
/// <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 uint ExponentBitsPerSample
{
get => this.exponentBitsPerSample;
set => this.exponentBitsPerSample = value;
}
public bool Visit(JxlVisitor visitor)
{
if (!this.FloatingPointSample)
{
bool successful = visitor.U32(
JxlFieldExpressions.Value(8u),
JxlFieldExpressions.Value(10u),
JxlFieldExpressions.Value(12u),
JxlFieldExpressions.BitsOffset(6u, 1u),
8u,
ref this.bitsPerSample);
if (!successful)
{
return false;
}
this.exponentBitsPerSample = 0;
}
else
{
if (!visitor.U32(
JxlFieldExpressions.Value(32u),
JxlFieldExpressions.Value(16u),
JxlFieldExpressions.Value(24u),
JxlFieldExpressions.BitsOffset(6u, 1u),
32u,
ref this.bitsPerSample))
{
return false;
}
this.exponentBitsPerSample--;
if (!visitor.Bits(4, 7, ref this.exponentBitsPerSample))
{
return false;
}
this.exponentBitsPerSample++;
}
if (this.FloatingPointSample)
{
if (this.exponentBitsPerSample is < 2 or > 8)
{
DebugGuard.IsTrue(false, "Invalid exponent_bits_per_sample");
return false;
}
int mantissaBits = (int)this.bitsPerSample - (int)this.exponentBitsPerSample - 1;
if (mantissaBits is < 2 or > 23)
{
DebugGuard.IsTrue(false, "Invalid bits_per_sample");
return false;
}
}
else if (this.bitsPerSample > 31)
{
DebugGuard.IsTrue(false, "Invalid bits_per_sample");
return false;
}
return true;
}
}

57
src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCodecMetadata.cs

@ -0,0 +1,57 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
internal sealed class JxlCodecMetadata
{
public JxlImageMetadata? ImageMetadata { get; set; }
public JxlSizeHeader? Size { get; set; }
public JxlCustomTransformData? CustomTransformData { get; set; }
public int XSize => this.Size?.XSize ?? 0;
public int YSize => this.Size?.YSize ?? 0;
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;
}
}

177
src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs

@ -0,0 +1,177 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Jxl.Fields;
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
internal sealed class JxlCustomTransformData : IJxlFields
{
private InlineArray15<float> upsampling2Weights;
private InlineArray55<float> upsampling4Weights;
private InlineArray210<float> upsampling8Weights;
private uint customWeightsMask;
private bool allDefault;
public JxlCustomTransformData() => JxlBundle.Init(this);
private static ReadOnlySpan<float> Weights2 =>
[
-0.01716200f, -0.03452303f, -0.04022174f, -0.02921014f, -0.00624645f,
0.14111091f, 0.28896755f, 0.00278718f, -0.01610267f, 0.56661550f,
0.03777607f, -0.01986694f, -0.03144731f, -0.01185068f, -0.00213539f
];
private static ReadOnlySpan<float> Weights4 =>
[
-0.02419067f, -0.03491987f, -0.03693351f, -0.03094285f, -0.00529785f,
-0.01663432f, -0.03556863f, -0.03888905f, -0.03516850f, -0.00989469f,
0.23651958f, 0.33392945f, -0.01073543f, -0.01313181f, -0.03556694f,
0.13048175f, 0.40103025f, 0.03951150f, -0.02077584f, 0.46914198f,
-0.00209270f, -0.01484589f, -0.04064806f, 0.18942530f, 0.56279892f,
0.06674400f, -0.02335494f, -0.03551682f, -0.00754830f, -0.02267919f,
-0.02363578f, 0.00315804f, -0.03399098f, -0.01359519f, -0.00091653f,
-0.00335467f, -0.01163294f, -0.01610294f, -0.00974088f, -0.00191622f,
-0.01095446f, -0.03198464f, -0.04455121f, -0.02799790f, -0.00645912f,
0.06390599f, 0.22963888f, 0.00630981f, -0.01897349f, 0.67537268f,
0.08483369f, -0.02534994f, -0.02205197f, -0.01667999f, -0.00384443f
];
private static ReadOnlySpan<float> Weights8 =>
[
-0.02928613f, -0.03706353f, -0.03783812f, -0.03324558f, -0.00447632f,
-0.02519406f, -0.03752601f, -0.03901508f, -0.03663285f, -0.00646649f,
-0.02066407f, -0.03838633f, -0.04002101f, -0.03900035f, -0.00901973f,
-0.01626393f, -0.03954148f, -0.04046620f, -0.03979621f, -0.01224485f,
0.29895328f, 0.35757708f, -0.02447552f, -0.01081748f, -0.04314594f,
0.23903219f, 0.41119301f, -0.00573046f, -0.01450239f, -0.04246845f,
0.17567618f, 0.45220643f, 0.02287757f, -0.01936783f, -0.03583255f,
0.11572472f, 0.47416733f, 0.06284440f, -0.02685066f, 0.42720050f,
-0.02248939f, -0.01155273f, -0.04562755f, 0.28689496f, 0.49093869f,
-0.00007891f, -0.01545926f, -0.04562659f, 0.21238920f, 0.53980934f,
0.03369474f, -0.02070211f, -0.03866988f, 0.14229550f, 0.56593398f,
0.08045181f, -0.02888298f, -0.03680918f, -0.00542229f, -0.02920477f,
-0.02788574f, -0.02118180f, -0.03942402f, -0.00775547f, -0.02433614f,
-0.03193943f, -0.02030828f, -0.04044014f, -0.01074016f, -0.01930822f,
-0.03620399f, -0.01974125f, -0.03919545f, -0.01456093f, -0.00045072f,
-0.00360110f, -0.01020207f, -0.01231907f, -0.00638988f, -0.00071592f,
-0.00279122f, -0.00957115f, -0.01288327f, -0.00730937f, -0.00107783f,
-0.00210156f, -0.00890705f, -0.01317668f, -0.00813895f, -0.00153491f,
-0.02128481f, -0.04173044f, -0.04831487f, -0.03293190f, -0.00525260f,
-0.01720322f, -0.04052736f, -0.05045706f, -0.03607317f, -0.00738030f,
-0.01341764f, -0.03965629f, -0.05151616f, -0.03814886f, -0.01005819f,
0.18968273f, 0.33063684f, -0.01300105f, -0.01372950f, -0.04017465f,
0.13727832f, 0.36402234f, 0.01027890f, -0.01832107f, -0.03365072f,
0.08734506f, 0.38194295f, 0.04338228f, -0.02525993f, 0.56408126f,
0.00458352f, -0.01648227f, -0.04887868f, 0.24585519f, 0.62026135f,
0.04314807f, -0.02213737f, -0.04158014f, 0.16637289f, 0.65027023f,
0.09621636f, -0.03101388f, -0.04082742f, -0.00904519f, -0.02790922f,
-0.02117818f, 0.00798662f, -0.03995711f, -0.01243427f, -0.02231705f,
-0.02946266f, 0.00992055f, -0.03600283f, -0.01684920f, -0.00111684f,
-0.00411204f, -0.01297130f, -0.01723725f, -0.01022545f, -0.00165306f,
-0.00313110f, -0.01218016f, -0.01763266f, -0.01125620f, -0.00231663f,
-0.01374149f, -0.03797620f, -0.05142937f, -0.03117307f, -0.00581914f,
-0.01064003f, -0.03608089f, -0.05272168f, -0.03375670f, -0.00795586f,
0.09628104f, 0.27129991f, -0.00353779f, -0.01734151f, -0.03153981f,
0.05686230f, 0.28500998f, 0.02230594f, -0.02374955f, 0.68214326f,
0.05018048f, -0.02320852f, -0.04383616f, 0.18459474f, 0.71517975f,
0.10805613f, -0.03263677f, -0.03637639f, -0.01394373f, -0.02511203f,
-0.01728636f, 0.05407331f, -0.02867568f, -0.01893131f, -0.00240854f,
-0.00446511f, -0.01636187f, -0.02377053f, -0.01522848f, -0.00333334f,
-0.00819975f, -0.02964169f, -0.04499287f, -0.02745350f, -0.00612408f,
0.02727416f, 0.19446600f, 0.00159832f, -0.02232473f, 0.74982506f,
0.11452620f, -0.03348048f, -0.01605681f, -0.02070339f, -0.00458223f
];
public bool NonserializedXybEncoded { get; set; }
public bool AllDefault
{
get => this.allDefault;
set => this.allDefault = value;
}
public JxlOpsinInverseMatrix? OpsinInverseMatrix { get; set; }
public uint CustomWeightsMask
{
get => this.customWeightsMask;
set => this.customWeightsMask = value;
}
public InlineArray15<float> Upsampling2Weights
{
get => this.upsampling2Weights;
set => this.upsampling2Weights = value;
}
public InlineArray55<float> Upsampling4Weights
{
get => this.upsampling4Weights;
set => this.upsampling4Weights = value;
}
public InlineArray210<float> Upsampling8Weights
{
get => this.upsampling8Weights;
set => this.upsampling8Weights = value;
}
public bool Visit(JxlVisitor visitor)
{
if (visitor.AllDefault(this, ref this.allDefault))
{
visitor.SetDefault(this);
return true;
}
if (visitor.Conditional(this.NonserializedXybEncoded))
{
if (!visitor.VisitNested(this.OpsinInverseMatrix!))
{
return false;
}
}
if (!visitor.Bits(3, 0, ref this.customWeightsMask))
{
return false;
}
if (visitor.Conditional((this.CustomWeightsMask & 1) != 0))
{
for (int i = 0; i < 15; i++)
{
if (!visitor.F16(Weights2[i], ref this.upsampling2Weights[i]))
{
return false;
}
}
}
if (visitor.Conditional((this.CustomWeightsMask & 2) != 0))
{
for (int i = 0; i < 55; i++)
{
if (!visitor.F16(Weights4[i], ref this.upsampling4Weights[i]))
{
return false;
}
}
}
if (visitor.Conditional((this.CustomWeightsMask & 4) != 0))
{
for (int i = 0; i < 210; i++)
{
if (!visitor.F16(Weights8[i], ref this.upsampling8Weights[i]))
{
return false;
}
}
}
return true;
}
}

16
src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExifOrientation.cs

@ -0,0 +1,16 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
internal enum JxlExifOrientation : byte
{
Identity = 1,
FlipHorizontal = 2,
Rotate180 = 3,
FlipVertical = 4,
Transpose = 5,
Rotate90 = 6,
AntiTranspose = 7,
Rotate270 = 8
}

25
src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannel.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
internal enum JxlExtraChannel : byte
{
Alpha,
Depth,
SpotColor,
SelectionMask,
Black,
Cfa,
Thermal,
Reserved0,
Reserved1,
Reserved2,
Reserved3,
Reserved4,
Reserved5,
Reserved6,
Reserved7,
Unknown,
Optional
}

152
src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs

@ -0,0 +1,152 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Jxl.Fields;
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
internal sealed class JxlExtraChannelInfo : IJxlFields
{
private bool allDefault;
private JxlExtraChannel type;
private JxlBitDepthMetadata? bitDepth;
private int dimensionShift;
private string? name;
private bool alphaAssociated;
private InlineArray4<float> spotColor;
private int cfaChannel;
public bool AllDefault
{
get => this.allDefault;
set => this.allDefault = value;
}
public JxlExtraChannel Type
{
get => this.type;
set => this.type = value;
}
public JxlBitDepthMetadata? BitDepth
{
get => this.bitDepth;
set => this.bitDepth = value;
}
public int DimensionShift
{
get => this.dimensionShift;
set => this.dimensionShift = value;
}
public string? Name
{
get => this.name;
set => this.name = value;
}
public bool AlphaAssociated
{
get => this.alphaAssociated;
set => this.alphaAssociated = value;
}
public InlineArray4<float> SpotColor
{
get => this.spotColor;
set => this.spotColor = value;
}
public int CfaChannel
{
get => this.cfaChannel;
set => this.cfaChannel = value;
}
public bool Visit(JxlVisitor visitor)
{
if (visitor.AllDefault(this, ref this.allDefault))
{
// Overwrite all serialized fields, but not any nonserialized_*.
visitor.SetDefault(this);
return true;
}
// General
if (!visitor.Enum(JxlExtraChannel.Alpha, ref this.type))
{
return false;
}
if (!visitor.VisitNested(this.bitDepth!))
{
return false;
}
if (!visitor.U32(
JxlFieldExpressions.Value(0u),
JxlFieldExpressions.Value(3u),
JxlFieldExpressions.Value(4u),
JxlFieldExpressions.BitsOffset(3u, 1u),
0u,
ref Unsafe.As<int, uint>(ref this.dimensionShift)))
{
return false;
}
if ((1u << this.dimensionShift) > 8u)
{
return false;
}
if (!VisitNameString(visitor, ref this.name))
{
return false;
}
// Conditional
if (visitor.Conditional(this.type == JxlExtraChannel.Alpha))
{
if (!visitor.Boolean(false, ref this.alphaAssociated))
{
return false;
}
}
if (visitor.Conditional(this.type == JxlExtraChannel.SpotColor))
{
for (int i = 0; i < 4; i++)
{
if (!visitor.F16(0F, ref this.spotColor[i]))
{
return false;
}
}
}
if (visitor.Conditional(this.type == JxlExtraChannel.Cfa))
{
if (!visitor.U32(
JxlFieldExpressions.Value(1u),
JxlFieldExpressions.Bits(2u),
JxlFieldExpressions.BitsOffset(4u, 3u),
JxlFieldExpressions.BitsOffset(8u, 19u),
1u,
ref Unsafe.As<int, uint>(ref this.cfaChannel)))
{
return false;
}
}
if (this.type is JxlExtraChannel.Unknown or
>= JxlExtraChannel.Reserved0 and
<= JxlExtraChannel.Reserved7)
{
return false;
}
return true;
}
}

396
src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs

@ -0,0 +1,396 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Jxl.Cms;
using SixLabors.ImageSharp.Formats.Jxl.Fields;
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
internal sealed class JxlImageMetadata : IJxlFields
{
private bool allDefault;
private JxlBitDepthMetadata? bitDepth;
private bool modular16BitBufferSufficient;
private bool xybEncoded;
private JxlColorEncoding? colorEncoding;
private int orientation = 1;
private bool havePreview;
private bool haveAnimation;
private bool haveIntrinsicSize;
private JxlSizeHeader intrinsicSize = new();
private JxlToneMapping? toneMapping;
private int extraChannelCount;
private List<JxlExtraChannelInfo> extraChannels = [];
private JxlPreviewHeader previewSize = new();
private JxlAnimationHeader animation = new();
private long extensions;
public JxlImageMetadata() => JxlBundle.Init(this);
public bool AllDefault
{
get => this.allDefault;
set => this.allDefault = value;
}
public JxlBitDepthMetadata? BitDepth
{
get => this.bitDepth;
set => this.bitDepth = value;
}
public bool Modular16BitBufferSufficient
{
get => this.modular16BitBufferSufficient;
set => this.modular16BitBufferSufficient = value;
}
public bool XybEncoded
{
get => this.xybEncoded;
set => this.xybEncoded = value;
}
public JxlColorEncoding? ColorEncoding
{
get => this.colorEncoding;
set => this.colorEncoding = value;
}
public int Orientation
{
get => this.orientation;
set => this.orientation = value;
}
public bool HavePreview
{
get => this.havePreview;
set => this.havePreview = value;
}
public bool HaveAnimation
{
get => this.haveAnimation;
set => this.haveAnimation = value;
}
public bool HaveIntrinsicSize
{
get => this.haveIntrinsicSize;
set => this.haveIntrinsicSize = value;
}
public JxlSizeHeader IntrinsicSize
{
get => this.intrinsicSize;
set => this.intrinsicSize = value;
}
public JxlToneMapping? ToneMapping
{
get => this.toneMapping;
set => this.toneMapping = value;
}
public int ExtraChannelCount
{
get => this.extraChannelCount;
set => this.extraChannelCount = value;
}
public List<JxlExtraChannelInfo> ExtraChannels
{
get => this.extraChannels;
set => this.extraChannels = value;
}
public JxlPreviewHeader PreviewSize
{
get => this.previewSize;
set => this.previewSize = value;
}
public JxlAnimationHeader Animation
{
get => this.animation;
set => this.animation = value;
}
public long Extensions
{
get => this.extensions;
set => this.extensions = value;
}
public bool NonserializedOnlyParseBasicInfos { get; set; }
public float IntensityTarget
{
get
{
float intensityTarget = this.ToneMapping?.IntensityTarget ?? 0f;
if (intensityTarget == 0f)
{
throw new InvalidOperationException("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 (int?)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 = (uint)bits;
this.BitDepth.ExponentBitsPerSample = 0;
this.BitDepth.FloatingPointSample = false;
}
this.Modular16BitBufferSufficient = bits <= 12;
}
public void SetIntensityTarget()
{
JxlCustomTransferFunction? tf = this.ColorEncoding?.TransferFunction;
if (tf is not null)
{
if (tf.Value.IsPq)
{
this.SetIntensityTarget(10000);
}
else if (tf.Value.IsHlg)
{
this.SetIntensityTarget(1000);
}
else
{
this.SetIntensityTarget(DefaultIntensityTarget);
}
}
}
public bool Visit(JxlVisitor visitor)
{
if (visitor.AllDefault(this, ref this.allDefault))
{
// Overwrite all serialized fields, but not any nonserialized_*.
visitor.SetDefault(this);
return true;
}
// Bundle.AllDefault does not allow usage when reading because it may abort
// when a codestream has invalid values. When reading, extraFields is
// overwritten below, so AllDefault is not needed.
bool toneMappingDefault = !visitor.IsReading && JxlBundle.AllDefault(this.toneMapping!);
bool extraFields = this.orientation != 1 ||
this.havePreview ||
this.haveAnimation ||
this.haveIntrinsicSize ||
!toneMappingDefault;
if (!visitor.Boolean(false, ref extraFields))
{
return false;
}
if (visitor.Conditional(extraFields))
{
this.orientation--;
if (!visitor.Bits(3, 0, ref Unsafe.As<int, uint>(ref this.orientation)))
{
return false;
}
this.orientation++;
// No bounds checking is necessary because exactly 3 bits are read.
if (!visitor.Boolean(false, ref this.haveIntrinsicSize))
{
return false;
}
if (visitor.Conditional(this.haveIntrinsicSize))
{
if (!visitor.VisitNested(this.intrinsicSize))
{
return false;
}
}
if (!visitor.Boolean(false, ref this.havePreview))
{
return false;
}
if (visitor.Conditional(this.havePreview))
{
if (!visitor.VisitNested(this.previewSize))
{
return false;
}
}
if (!visitor.Boolean(false, ref this.haveAnimation))
{
return false;
}
if (visitor.Conditional(this.haveAnimation))
{
if (!visitor.VisitNested(this.animation))
{
return false;
}
}
}
else
{
this.orientation = 1;
this.haveIntrinsicSize = false;
this.havePreview = false;
this.haveAnimation = false;
}
if (!visitor.VisitNested(this.bitDepth!))
{
return false;
}
if (!visitor.Boolean(true, ref this.modular16BitBufferSufficient))
{
return false;
}
this.extraChannelCount = this.extraChannels.Count;
if (!visitor.U32(
JxlFieldExpressions.Value(0u),
JxlFieldExpressions.Value(1u),
JxlFieldExpressions.BitsOffset(4u, 2u),
JxlFieldExpressions.BitsOffset(12u, 1u),
0u,
ref Unsafe.As<int, uint>(ref this.extraChannelCount)))
{
return false;
}
if (visitor.Conditional(this.extraChannelCount != 0))
{
if (visitor.IsReading)
{
this.extraChannels.Clear();
this.extraChannels.Capacity = this.extraChannelCount;
for (int i = 0; i < this.extraChannelCount; i++)
{
this.extraChannels.Add(new JxlExtraChannelInfo());
}
}
for (int i = 0; i < this.extraChannels.Count; i++)
{
if (!visitor.VisitNested(this.extraChannels[i]))
{
return false;
}
}
}
if (!visitor.Boolean(true, ref this.xybEncoded))
{
return false;
}
if (!visitor.VisitNested(this.colorEncoding!))
{
return false;
}
if (visitor.Conditional(extraFields))
{
if (!visitor.VisitNested(this.toneMapping!))
{
return false;
}
}
// Treat as if only the fields up to extra channels exist.
if (visitor.IsReading && this.NonserializedOnlyParseBasicInfos)
{
return true;
}
if (!visitor.BeginExtensions(ref Unsafe.As<long, ulong>(ref this.extensions)))
{
return false;
}
// Extensions: in chronological order of being added to the format.
return visitor.EndExtensions();
}
}

83
src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs

@ -0,0 +1,83 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#pragma warning disable SA1401 // Fields should be private
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Jxl.Fields;
using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives;
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
internal sealed class JxlOpsinInverseMatrix : IJxlFields
{
private InlineArray3<float> opsinBiases;
private InlineArray3<float> quantBiases;
private bool allDefault;
private JxlMatrix3x3F inverseMatrix;
public InlineArray3<float> OpsinBiases
{
get => this.opsinBiases;
set => this.opsinBiases = value;
}
public InlineArray3<float> QuantBiases
{
get => this.quantBiases;
set => this.quantBiases = value;
}
public bool AllDefault
{
get => this.allDefault;
set => this.allDefault = value;
}
public JxlMatrix3x3F InverseMatrix
{
get => this.inverseMatrix;
set => this.inverseMatrix = value;
}
public bool Visit(JxlVisitor visitor)
{
if (visitor.AllDefault(this, ref this.allDefault))
{
// Overwrite all serialized fields, but not any nonserialized_*.
visitor.SetDefault(this);
return true;
}
JxlMatrix3x3F defaultInverse = JxlCms.DefaultInverseOpsinAbsorbanceMatrix();
for (int j = 0; j < 3; j++)
{
for (int i = 0; i < 3; i++)
{
if (!visitor.F16(defaultInverse[j][i], ref this.inverseMatrix[j][i]))
{
return false;
}
}
}
for (int i = 0; i < 3; i++)
{
if (!visitor.F16(JxlCms.NegOpsinAbsorbanceBiasRgb[i], ref this.opsinBiases[i]))
{
return false;
}
}
for (int i = 0; i < 4; i++)
{
if (!visitor.F16(DefaultQuantBias[i], ref this.quantBiases[i]))
{
return false;
}
}
return true;
}
}

68
src/ImageSharp/Formats/Jxl/IO/Metadata/JxlPreviewHeader.cs

@ -0,0 +1,68 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jxl.Fields;
namespace SixLabors.ImageSharp.Formats.Jxl.IO.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();
}

74
src/ImageSharp/Formats/Jxl/IO/Metadata/JxlSizeHeader.cs

@ -0,0 +1,74 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jxl.Fields;
using SixLabors.ImageSharp.Formats.Jxl.Processing.Primitives;
namespace SixLabors.ImageSharp.Formats.Jxl.IO.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();
}

99
src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs

@ -0,0 +1,99 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jxl.Fields;
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
internal sealed class JxlToneMapping : IJxlFields
{
private bool allDefault;
private float intensityTarget;
private float lowerBoundIntensityLevel;
private bool relativeToMaxDisplay;
private float linearBelow;
private float minNits;
public bool AllDefault
{
get => this.allDefault;
set => this.allDefault = value;
}
public float IntensityTarget
{
get => this.intensityTarget;
set => this.intensityTarget = value;
}
public float LowerBoundIntensityLevel
{
get => this.lowerBoundIntensityLevel;
set => this.lowerBoundIntensityLevel = value;
}
public bool RelativeToMaxDisplay
{
get => this.relativeToMaxDisplay;
set => this.relativeToMaxDisplay = value;
}
public float LinearBelow
{
get => this.linearBelow;
set => this.linearBelow = value;
}
public float MinimumNits
{
get => this.minNits;
set => this.minNits = value;
}
public bool Visit(JxlVisitor visitor)
{
if (visitor.AllDefault(this, ref this.allDefault))
{
// Overwrite all serialized fields, but not any nonserialized_*.
visitor.SetDefault(this);
return true;
}
if (!visitor.F16(JxlFieldExpressions.Value(JxlConstants.DefaultIntensityTarget), ref this.intensityTarget))
{
return false;
}
if (this.intensityTarget <= 0F)
{
return false;
}
if (!visitor.F16(0F, ref this.minNits))
{
return false;
}
if (this.minNits < 0F || this.minNits > this.intensityTarget)
{
return false;
}
if (!visitor.Boolean(false, ref this.relativeToMaxDisplay))
{
return false;
}
if (!visitor.F16(0F, ref this.linearBelow))
{
return false;
}
if (this.linearBelow < 0F || (this.relativeToMaxDisplay && this.linearBelow > 1F))
{
return false;
}
return true;
}
}

35
src/ImageSharp/Formats/Jxl/InlineArrays.cs

@ -0,0 +1,35 @@
// 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;
/// <summary>
/// Used by JxlCustomTransformData
/// </summary>
[InlineArray(55)]
internal struct InlineArray55<T>
{
private T first;
}
/// <summary>
/// Used by JpegQuantizationTable
/// </summary>
[InlineArray(64)]
internal struct InlineArray64<T>
{
private T first;
}
/// <summary>
/// Used by JxlCustomTransformData
/// </summary>
[InlineArray(210)]
internal struct InlineArray210<T>
{
private T first;
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save