Browse Source

Merge 80f107591e into adb982081a

pull/3153/merge
winscripter 18 hours ago
committed by GitHub
parent
commit
9815035e04
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 40
      src/ImageSharp/Common/Helpers/Vector128Utilities.cs
  2. 166
      src/ImageSharp/Common/Helpers/Vector256Utilities.cs
  3. 31
      src/ImageSharp/Common/InlineArray.cs
  4. 2
      src/ImageSharp/Common/InlineArray.tt
  5. 27
      src/ImageSharp/Formats/Jxl/Cms/JxlCieXyPrimaries.cs
  6. 77
      src/ImageSharp/Formats/Jxl/Cms/JxlColorEncoding.cs
  7. 31
      src/ImageSharp/Formats/Jxl/Cms/JxlColorSpace.cs
  8. 109
      src/ImageSharp/Formats/Jxl/Cms/JxlCustomTransferFunction.cs
  9. 53
      src/ImageSharp/Formats/Jxl/Cms/JxlCustomXy.cs
  10. 24
      src/ImageSharp/Formats/Jxl/Cms/JxlOpsinConstants.cs
  11. 27
      src/ImageSharp/Formats/Jxl/Cms/JxlPrimaries.cs
  12. 13
      src/ImageSharp/Formats/Jxl/Cms/JxlRenderingIntent.cs
  13. 45
      src/ImageSharp/Formats/Jxl/Cms/JxlTransferFunction.cs
  14. 33
      src/ImageSharp/Formats/Jxl/Cms/JxlWhitePoint.cs
  15. 4
      src/ImageSharp/Formats/Jxl/Cms/README.md
  16. 17
      src/ImageSharp/Formats/Jxl/Fields/IJxlFields.cs
  17. 35
      src/ImageSharp/Formats/Jxl/Fields/JxlAllDefaultVisitor.cs
  18. 39
      src/ImageSharp/Formats/Jxl/Fields/JxlBitsCoder.cs
  19. 89
      src/ImageSharp/Formats/Jxl/Fields/JxlBundle.cs
  20. 118
      src/ImageSharp/Formats/Jxl/Fields/JxlCanEncodeVisitor.cs
  21. 42
      src/ImageSharp/Formats/Jxl/Fields/JxlExtensionStates.cs
  22. 69
      src/ImageSharp/Formats/Jxl/Fields/JxlF16Coder.cs
  23. 21
      src/ImageSharp/Formats/Jxl/Fields/JxlFieldExpressions.cs
  24. 51
      src/ImageSharp/Formats/Jxl/Fields/JxlInitVisitor.cs
  25. 121
      src/ImageSharp/Formats/Jxl/Fields/JxlReadVisitor.cs
  26. 49
      src/ImageSharp/Formats/Jxl/Fields/JxlSetDefaultVisitor.cs
  27. 126
      src/ImageSharp/Formats/Jxl/Fields/JxlU32Coder.cs
  28. 17
      src/ImageSharp/Formats/Jxl/Fields/JxlU32Distribution.cs
  29. 26
      src/ImageSharp/Formats/Jxl/Fields/JxlU32Enc.cs
  30. 106
      src/ImageSharp/Formats/Jxl/Fields/JxlU64Coder.cs
  31. 92
      src/ImageSharp/Formats/Jxl/Fields/JxlVisitor.cs
  32. 76
      src/ImageSharp/Formats/Jxl/Fields/JxlVisitorBase.cs
  33. 320
      src/ImageSharp/Formats/Jxl/IO/BinaryUtils.Generated.cs
  34. 85
      src/ImageSharp/Formats/Jxl/IO/BinaryUtils.tt
  35. 158
      src/ImageSharp/Formats/Jxl/IO/Container/JxlBoxHeader.cs
  36. 58
      src/ImageSharp/Formats/Jxl/IO/Container/JxlFileTypeBox.cs
  37. 15
      src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsConstants.cs
  38. 16
      src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsEntry.cs
  39. 239
      src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHelper.cs
  40. 63
      src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsHybridUIntConfiguration.cs
  41. 81
      src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsLz77Parameters.cs
  42. 14
      src/ImageSharp/Formats/Jxl/IO/Entropy/JxlAnsSymbol.cs
  43. 74
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlAnimationFrame.cs
  44. 64
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendMode.cs
  45. 146
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlBlendingInfo.cs
  46. 26
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransform.cs
  47. 31
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlColorTransformHelpers.cs
  48. 20
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameEncoding.cs
  49. 744
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeader.cs
  50. 36
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameHeaderFlags.cs
  51. 37
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlFrameType.cs
  52. 220
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlPasses.cs
  53. 147
      src/ImageSharp/Formats/Jxl/IO/FrameHeader/JxlYCbCrChromaSubsampling.cs
  54. 30
      src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegAppMarkerType.cs
  55. 53
      src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponent.cs
  56. 16
      src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponentScanInfo.cs
  57. 15
      src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegComponentType.cs
  58. 1009
      src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegData.cs
  59. 105
      src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegDataConstants.cs
  60. 11
      src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegExtraZeroRunInfo.cs
  61. 30
      src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegHuffmanCode.cs
  62. 17
      src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegInfo.cs
  63. 31
      src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegQuantizationTable.cs
  64. 43
      src/ImageSharp/Formats/Jxl/IO/Jpeg/Data/JpegScanInfo.cs
  65. 38
      src/ImageSharp/Formats/Jxl/IO/Jpeg/JpegHuffmanDecoder.cs
  66. 11
      src/ImageSharp/Formats/Jxl/IO/Jpeg/README.md
  67. 184
      src/ImageSharp/Formats/Jxl/IO/JxlHuffman.cs
  68. 20
      src/ImageSharp/Formats/Jxl/IO/JxlHuffmanCode.cs
  69. 82
      src/ImageSharp/Formats/Jxl/IO/JxlMemoryWriter.cs
  70. 19
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlAnimationHeader.cs
  71. 129
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlBitDepthMetadata.cs
  72. 57
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCodecMetadata.cs
  73. 26
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlCustomTransformData.cs
  74. 16
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExifOrientation.cs
  75. 25
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannel.cs
  76. 27
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlExtraChannelInfo.cs
  77. 152
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlImageMetadata.cs
  78. 23
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlOpsinInverseMatrix.cs
  79. 68
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlPreviewHeader.cs
  80. 73
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlSizeHeader.cs
  81. 21
      src/ImageSharp/Formats/Jxl/IO/Metadata/JxlToneMapping.cs
  82. 35
      src/ImageSharp/Formats/Jxl/InlineArrays.cs
  83. 22
      src/ImageSharp/Formats/Jxl/JxlFormat.cs
  84. 49
      src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs
  85. 22
      src/ImageSharp/Formats/Jxl/JxlImageInfo.cs
  86. 12
      src/ImageSharp/Formats/Jxl/JxlThrowHelper.cs
  87. 19
      src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs
  88. 19
      src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs
  89. 19
      src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs
  90. 19
      src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs
  91. 19
      src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs
  92. 32
      src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs
  93. 23
      src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageF.cs
  94. 23
      src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageI.cs
  95. 23
      src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageS.cs
  96. 23
      src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageSB.cs
  97. 23
      src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageU.cs
  98. 106
      src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs
  99. 182
      src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs
  100. 97
      src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs

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

@ -859,4 +859,44 @@ 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);
}
}

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

@ -397,4 +397,170 @@ 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);
}
}

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

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

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

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.

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

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

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

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

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

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

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

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

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

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

@ -0,0 +1,126 @@
// 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;
/// <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)
{
int 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)
{
DebugGuard.IsTrue(false, "No matching selector");
return false;
}
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];
}
}

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

@ -0,0 +1,106 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jxl.Processing.Decoder;
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;
}
/// <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, ref uint token, ref uint bitCount, ref 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
}

1009
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; } = [];
}

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
{
private const int RootTableBits = 8;
private const int LookupSize = 8;
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;
}
}

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.

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

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

@ -0,0 +1,26 @@
// 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
{
public bool NonserializedXybEncoded { get; set; }
public bool AllDefault { get; set; }
public JxlOpsinInverseMatrix? OpsinInverseMatrix { get; set; }
public int CustomWeightsMask { get; set; }
public InlineArray15<float> Upsampling2Weights { get; set; }
public InlineArray55<float> Upsampling4Weights { get; set; }
public InlineArray210<float> Upsampling8Weights { get; set; }
public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
}

16
src/ImageSharp/Formats/Jxl/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,
Transponse = 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
}

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

@ -0,0 +1,27 @@
// 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 JxlExtraChannelInfo : IJxlFields
{
public bool AllDefault { get; set; }
public JxlExtraChannel Type { get; set; }
public JxlBitDepthMetadata? BitDepth { get; set; }
public int DimensionShift { get; set; }
public string? Name { get; set; }
public bool AlphaAssociated { get; set; }
public InlineArray4<float> SpotColor { get; set; }
public int CfaChannel { get; set; }
public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
}

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

@ -0,0 +1,152 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jxl.Cms;
using SixLabors.ImageSharp.Formats.Jxl.Fields;
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
internal sealed class JxlImageMetadata : IJxlFields
{
public bool AllDefault { get; set; }
public JxlBitDepthMetadata? BitDepth { get; set; }
public bool Modular16BitBufferSufficient { get; set; } // Otherwise, 32 is
public bool XybEncoded { get; set; }
public JxlColorEncoding? ColorEncoding { get; set; }
public int Orientation { get; set; } = 1;
public bool HavePreview { get; set; }
public bool HaveAnimation { get; set; }
public bool HaveIntrinsicSize { get; set; }
public JxlSizeHeader IntrinsicSize { get; set; }
public JxlToneMapping? ToneMapping { get; set; }
public int ExtraChannelCount { get; set; }
public List<JxlExtraChannelInfo> ExtraChannels { get; set; } = [];
public JxlPreviewHeader PreviewSize { get; set; }
public JxlAnimationHeader Animation { get; set; }
public long Extensions { get; set; }
public bool NonserializedOnlyParseBasicInfos { get; set; }
public float IntensityTarget
{
get
{
float intensityTarget = this.ToneMapping?.IntensityTarget ?? 0f;
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 ec.BitDepth?.BitsPerSample ?? 0;
}
set
{
}
}
public bool HasAlpha => this.AlphaBits != 0;
public JxlExtraChannelInfo? FindExtraChannel(JxlExtraChannel type)
=> this.ExtraChannels.FirstOrDefault(eci => eci.Type == type);
public JxlExifOrientation GetExifOrientation() => (JxlExifOrientation)this.Orientation;
public void SetFloat16Samples()
{
if (this.BitDepth != null)
{
this.BitDepth.BitsPerSample = 16;
this.BitDepth.ExponentBitsPerSample = 5;
this.BitDepth.FloatingPointSample = true;
}
this.Modular16BitBufferSufficient = false;
}
public void SetFloat32Samples()
{
if (this.BitDepth != null)
{
this.BitDepth.BitsPerSample = 32;
this.BitDepth.ExponentBitsPerSample = 8;
this.BitDepth.FloatingPointSample = true;
}
this.Modular16BitBufferSufficient = false;
}
public void SetUIntSamples(int bits)
{
if (this.BitDepth != null)
{
this.BitDepth.BitsPerSample = bits;
this.BitDepth.ExponentBitsPerSample = 0;
this.BitDepth.FloatingPointSample = false;
}
this.Modular16BitBufferSufficient = bits <= 12;
}
public void SetIntensityTarget()
{
JxlCustomTransferFunction? tf = this.ColorEncoding?.TransferFunction;
if (tf is not null)
{
if (tf.Value.IsPq)
{
this.SetIntensityTarget(10000);
}
else if (tf.Value.IsHlg)
{
this.SetIntensityTarget(1000);
}
else
{
this.SetIntensityTarget(DefaultIntensityTarget);
}
}
}
public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
}

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

@ -0,0 +1,23 @@
// 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
{
public InlineArray3<float> OpsinBiases;
public InlineArray3<float> QuantBiases;
public bool AllDefault { get; set; }
public JxlMatrix3x3F InverseMatrix { get; set; }
public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
}

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

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

@ -0,0 +1,73 @@
// 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 JxlSizeHeader : IJxlFields
{
private bool isSmall;
private int ySizeDiv8Minus1;
private int ySize;
private int ratio;
private int xSizeDiv8Minus1;
private int xSize;
public int YSize => this.isSmall ? ((this.ySizeDiv8Minus1 + 1) * 8) : this.ySize;
public int XSize
{
get
{
if (this.ratio != 0)
{
SignedRational aspectRatio = JxlAspectRatioHelpers.FixedAspectRatios(this.ratio);
return JxlAspectRatioHelpers.MultiplyTruncate(aspectRatio, this.YSize);
}
return this.isSmall ? ((this.xSizeDiv8Minus1 + 1) * 8) : this.xSize;
}
}
public void Set(int x, int y)
{
if (x > int.MaxValue || y > int.MaxValue)
{
throw new ArgumentException("Image too large");
}
if (x == 0 || y == 0)
{
throw new ArgumentException("Empty image");
}
this.ratio = JxlAspectRatioHelpers.FindAspectRatio(x, y);
this.isSmall = y < 256 && (y % JxlFrameDimensions.BlockDimensions) == 0
&& (this.ratio != 0 || (x <= 256 && (x % JxlFrameDimensions.BlockDimensions) == 0));
if (this.isSmall)
{
this.ySizeDiv8Minus1 = (y / 8) - 1;
}
else
{
this.ySize = y;
}
if (this.ratio == 0)
{
if (this.isSmall)
{
this.xSizeDiv8Minus1 = (x / 8) - 1;
}
else
{
this.xSize = x;
}
}
}
public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
}

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

@ -0,0 +1,21 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jxl.Fields;
namespace SixLabors.ImageSharp.Formats.Jxl.IO.Metadata;
internal sealed class JxlToneMapping : IJxlFields
{
public bool AllDefault { get; set; }
public float IntensityTarget { get; set; }
public float LowerBoundIntensityLevel { get; set; }
public bool RelativeToMaxDisplay { get; set; }
public float LinearBelow { get; set; }
public bool Visit(JxlVisitor visitor) => throw new NotImplementedException();
}

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

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

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

49
src/ImageSharp/Formats/Jxl/JxlImageFormatDetector.cs

@ -0,0 +1,49 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
namespace SixLabors.ImageSharp.Formats.Jxl;
/// <summary>
/// Checks if the first few bytes of a file represent
/// JPEG XL.
/// </summary>
public sealed class JxlImageFormatDetector : IImageFormatDetector
{
/// <summary>
/// Gets file signature bytes which represent a container-based
/// JPEG XL file.
/// </summary>
private static ReadOnlySpan<byte> ContainerStart =>
[
0x00, 0x00, 0x00, 0x0C,
0x4A, 0x58, 0x4C, 0x20,
0x0D, 0x0A, 0x87, 0x0A,
];
/// <inheritdoc/>
public int HeaderSize => 12;
/// <inheritdoc/>
public bool TryDetectFormat(ReadOnlySpan<byte> header, [NotNullWhen(true)] out IImageFormat? format)
{
if (header.StartsWith([(byte)0xFF, (byte)0x0A]))
{
// Just codestream.
format = new JxlFormat();
return true;
}
else if (header.SequenceEqual(ContainerStart))
{
// Container format.
format = new JxlFormat();
return true;
}
else
{
format = null;
return false;
}
}
}

22
src/ImageSharp/Formats/Jxl/JxlImageInfo.cs

@ -0,0 +1,22 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Metadata;
namespace SixLabors.ImageSharp.Formats.Jxl;
/// <summary>
/// Image information specific to the JPEG XL format.
/// </summary>
public class JxlImageInfo : ImageInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="JxlImageInfo"/> class.
/// </summary>
/// <param name="size">Image size</param>
/// <param name="metadata">Image metadata</param>
public JxlImageInfo(Size size, ImageMetadata metadata)
: base(size, metadata)
{
}
}

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

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

19
src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3B.cs

@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
/// <summary>
/// Represents a three-plane, 2D raster image of type <see cref="byte"/>.
/// </summary>
internal sealed class JxlImage3B : JxlImage3<byte>
{
public JxlImage3B()
{
}
public JxlImage3B(Configuration configuration, int xSize, int ySize)
: base(configuration, xSize, ySize)
{
}
}

19
src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3F.cs

@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
/// <summary>
/// Represents a three-plane, 2D raster image of type <see cref="float"/>.
/// </summary>
internal sealed class JxlImage3F : JxlImage3<float>
{
public JxlImage3F()
{
}
public JxlImage3F(Configuration configuration, int xSize, int ySize)
: base(configuration, xSize, ySize)
{
}
}

19
src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3I.cs

@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
/// <summary>
/// Represents a three-plane, 2D raster image of type <see cref="int"/>.
/// </summary>
internal sealed class JxlImage3I : JxlImage3<int>
{
public JxlImage3I()
{
}
public JxlImage3I(Configuration configuration, int xSize, int ySize)
: base(configuration, xSize, ySize)
{
}
}

19
src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3S.cs

@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
/// <summary>
/// Represents a three-plane, 2D raster image of type <see cref="short"/>.
/// </summary>
internal sealed class JxlImage3S : JxlImage3<short>
{
public JxlImage3S()
{
}
public JxlImage3S(Configuration configuration, int xSize, int ySize)
: base(configuration, xSize, ySize)
{
}
}

19
src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImage3U.cs

@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
/// <summary>
/// Represents a three-plane, 2D raster image of type <see cref="ushort"/>.
/// </summary>
internal sealed class JxlImage3U : JxlImage3<ushort>
{
public JxlImage3U()
{
}
public JxlImage3U(Configuration configuration, int xSize, int ySize)
: base(configuration, xSize, ySize)
{
}
}

32
src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageB.cs

@ -0,0 +1,32 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
/// <summary>
/// Represents a single-plane, 2D raster image of type <see cref="byte"/>.
/// </summary>
internal sealed class JxlImageB : JxlPlane<byte>
{
public JxlImageB()
{
}
public JxlImageB(int width, int height)
: base(width, height)
{
}
public JxlImageB(Configuration configuration, int xSize, int ySize, int prePadding = 0)
: base(xSize, ySize)
=> this.Allocate(configuration, prePadding);
public Memory<byte> GetRowBytesMemory(int y)
{
DebugGuard.MustBeLessThan(y, this.YSize, nameof(y));
Memory<byte> row = this.Bytes[(y * this.BytesPerRow)..];
return row;
}
}

23
src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageF.cs

@ -0,0 +1,23 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
/// <summary>
/// Represents a single-plane, 2D raster image of type <see cref="float"/>.
/// </summary>
internal sealed class JxlImageF : JxlPlane<float>
{
public JxlImageF()
{
}
public JxlImageF(int width, int height)
: base(width, height)
{
}
public JxlImageF(Configuration configuration, int xSize, int ySize, int prePadding = 0)
: base(xSize, ySize)
=> this.Allocate(configuration, prePadding);
}

23
src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageI.cs

@ -0,0 +1,23 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
/// <summary>
/// Represents a single-plane, 2D raster image of type <see cref="int"/>.
/// </summary>
internal sealed class JxlImageI : JxlPlane<int>
{
public JxlImageI()
{
}
public JxlImageI(int width, int height)
: base(width, height)
{
}
public JxlImageI(Configuration configuration, int xSize, int ySize, int prePadding = 0)
: base(xSize, ySize)
=> this.Allocate(configuration, prePadding);
}

23
src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageS.cs

@ -0,0 +1,23 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
/// <summary>
/// Represents a single-plane, 2D raster image of type <see cref="short"/>.
/// </summary>
internal sealed class JxlImageS : JxlPlane<short>
{
public JxlImageS()
{
}
public JxlImageS(int width, int height)
: base(width, height)
{
}
public JxlImageS(Configuration configuration, int xSize, int ySize, int prePadding = 0)
: base(xSize, ySize)
=> this.Allocate(configuration, prePadding);
}

23
src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageSB.cs

@ -0,0 +1,23 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
/// <summary>
/// Represents a single-plane, 2D raster image of type <see cref="sbyte"/>.
/// </summary>
internal sealed class JxlImageSB : JxlPlane<sbyte>
{
public JxlImageSB()
{
}
public JxlImageSB(int width, int height)
: base(width, height)
{
}
public JxlImageSB(Configuration configuration, int xSize, int ySize, int prePadding = 0)
: base(xSize, ySize)
=> this.Allocate(configuration, prePadding);
}

23
src/ImageSharp/Formats/Jxl/Memory/ImageTypes/JxlImageU.cs

@ -0,0 +1,23 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jxl.Memory.ImageTypes;
/// <summary>
/// Represents a single-plane, 2D raster image of type <see cref="ushort"/>.
/// </summary>
internal sealed class JxlImageU : JxlPlane<ushort>
{
public JxlImageU()
{
}
public JxlImageU(int width, int height)
: base(width, height)
{
}
public JxlImageU(Configuration configuration, int xSize, int ySize, int prePadding = 0)
: base(xSize, ySize)
=> this.Allocate(configuration, prePadding);
}

106
src/ImageSharp/Formats/Jxl/Memory/JxlImage3{T}.cs

@ -0,0 +1,106 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Jxl.Memory;
// NOTE: Do not seal this class.
internal class JxlImage3<T> : IDisposable
where T : unmanaged
{
private const int PlaneCount = 3;
private JxlPlane<T>[] planes = new JxlPlane<T>[3];
public JxlImage3()
{
}
public JxlImage3(Configuration configuration, int xSize, int ySize)
=> this.Allocate(configuration, xSize, ySize);
public JxlImage3(JxlImage3<T> other)
{
for (int i = 0; i < PlaneCount; i++)
{
this.planes[i] = other.planes[i];
}
}
public int XSize => this.planes[0].XSize;
public int YSize => this.planes[0].YSize;
public int BytesPerRow => this.planes[0].BytesPerRow;
public int PixelsPerRow => this.planes[0].PixelsPerRow;
public Span<T> PlaneRow(int plane, int row)
{
this.PlaneRowBoundsCheck(plane, row);
int rowOffset = row * this.planes[0].BytesPerRow;
Span<T> rowSpan = MemoryMarshal.Cast<byte, T>(this.planes[plane].BytesSpan[rowOffset..]);
return rowSpan;
}
public Span<T> PlaneRow(Rectangle rectangle, int c, int y)
{
DebugGuard.MustBeGreaterThanOrEqualTo(y + rectangle.Top, 0, nameof(y));
return this.PlaneRow(c, y + rectangle.Top)[rectangle.Left..];
}
public JxlPlane<T> Plane(int index) => this.planes[index];
public void Swap(JxlImage3<T> other)
{
for (int i = 0; i < PlaneCount; i++)
{
other.planes[i].Swap(this.planes[i]);
}
}
public static JxlImage3<T> Create(Configuration configuration, int xSize, int ySize)
=> new(configuration, xSize, ySize);
public void Allocate(Configuration configuration, int xSize, int ySize)
{
JxlPlane<T> plane0 = JxlPlane<T>.Create(configuration, xSize, ySize);
JxlPlane<T> plane1 = JxlPlane<T>.Create(configuration, xSize, ySize);
JxlPlane<T> plane2 = JxlPlane<T>.Create(configuration, xSize, ySize);
this.planes = [plane0, plane1, plane2];
}
public bool ShrinkTo(int x, int y)
{
for (int i = 0; i < PlaneCount; i++)
{
if (!this.planes[i].ShrinkTo(x, y))
{
return false;
}
}
return true;
}
private void PlaneRowBoundsCheck(int c, int y)
{
DebugGuard.MustBeLessThan(c, PlaneCount, nameof(c));
DebugGuard.MustBeLessThan(y, this.YSize, nameof(y));
}
public void Dispose()
{
foreach (JxlPlane<T> plane in this.planes)
{
plane.Dispose();
}
GC.SuppressFinalize(this);
}
}

182
src/ImageSharp/Formats/Jxl/Memory/JxlPlaneBase.cs

@ -0,0 +1,182 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Jxl.Memory;
/// <summary>
/// Base class for a single-plane image.
/// </summary>
internal class JxlPlaneBase : IDisposable
{
/// <summary>
/// Underlying bytes
/// </summary>
private IMemoryOwner<byte>? bytes;
/// <summary>
/// Initializes a new instance of the <see cref="JxlPlaneBase"/> class.
/// </summary>
/// <param name="xSize">Plane width</param>
/// <param name="ySize">Plane height</param>
/// <param name="sizeOfT">The size of each pixel in bytes.</param>
public JxlPlaneBase(int xSize, int ySize, int sizeOfT)
{
this.XSize = xSize;
this.YSize = ySize;
this.OriginalXSize = xSize;
this.OriginalYSize = ySize;
this.BytesPerRow = 0;
this.Size = sizeOfT;
}
/// <summary>
/// Initializes a new instance of the <see cref="JxlPlaneBase"/> class with empty values.
/// </summary>
public JxlPlaneBase()
: this(0, 0, 0)
{
}
/// <summary>
/// Gets the number of bytes per row.
/// </summary>
public int BytesPerRow { get; private set; }
/// <summary>
/// Gets the width of the image.
/// </summary>
public int XSize { get; private set; }
/// <summary>
/// Gets the height of the image.
/// </summary>
public int YSize { get; private set; }
/// <summary>
/// Gets the underlying bytes of this image as a Memory&lt;T&gt;.
/// </summary>
public Memory<byte> Bytes =>
#if DEBUG
this.bytes?.Memory ?? throw new InvalidOperationException("Bytes are missing");
#else
return this.bytes!.Memory;
#endif
/// <summary>
/// Gets the underlying bytes of this image as a Span&lt;T&gt;.
/// </summary>
public Span<byte> BytesSpan => this.Bytes.Span;
protected int Size { get; set; }
/// <summary>
/// Gets or sets the width that was initially assigned. For example, if the image gets shrinked,
/// the XSize YSize properties get changed while this property will stay same.
/// </summary>
protected int OriginalXSize { get; set; }
/// <summary>
/// Gets or sets the height that was initially assigned. For example, if the image gets shrinked,
/// the XSize YSize properties get changed while this property will stay same.
/// </summary>
protected int OriginalYSize { get; set; }
/// <summary>
/// Allocates the underlying memory for the plane.
/// </summary>
/// <param name="configuration">The configuration which has a memory allocator used to allocate memory.</param>
/// <param name="prePadding">Padding</param>
/// <returns>Status of allocation.</returns>
public bool Allocate(Configuration configuration, int prePadding)
{
if (this.bytes != null || this.BytesPerRow != 0)
{
return false;
}
if (this.XSize == 0 || this.YSize == 0)
{
return true;
}
int totalBytes = unchecked(this.YSize * this.BytesPerRow);
this.bytes = configuration.MemoryAllocator.Allocate<byte>(totalBytes + (prePadding * this.Size));
return true;
}
/// <summary>
/// Shrinks the image so its width is equal to <paramref name="x"/> and its height is
/// equal to <paramref name="y"/>.
/// </summary>
/// <param name="x">The output width</param>
/// <param name="y">The output height</param>
/// <returns>Status of the shrinking operation.</returns>
/// <remarks>
/// <para>
/// This method can only shrink memory. It cannot expand it.
/// </para>
/// <para>
/// When shrinking, the underlying memory does not get resized.
/// </para>
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ShrinkTo(int x, int y)
{
if (x <= this.OriginalXSize || y <= this.OriginalYSize)
{
return false;
}
DebugGuard.MustBeLessThanOrEqualTo(x, this.OriginalXSize, nameof(x));
DebugGuard.MustBeLessThanOrEqualTo(y, this.OriginalYSize, nameof(y));
this.XSize = x;
this.YSize = y;
return true;
}
/// <summary>
/// Base function to return the span for a specified row as a generic &lt;T&gt;.
/// </summary>
/// <typeparam name="T">The type of the row.</typeparam>
/// <param name="y">The index of the row to get the span for.</param>
/// <returns>A span which covers the row memory.</returns>
protected Span<T> GetRowBase<T>(int y)
where T : unmanaged
{
DebugGuard.MustBeLessThan(y, this.YSize, nameof(y));
Span<byte> row = this.Bytes.Span[(y * this.BytesPerRow)..];
return MemoryMarshal.Cast<byte, T>(row);
}
/// <summary>
/// Swaps properties &amp; data of this image with the specified image.
/// </summary>
/// <param name="other">The other image to swap with.</param>
public void Swap(JxlPlaneBase other)
{
(this.XSize, other.XSize) = (other.XSize, this.XSize);
(this.YSize, other.YSize) = (other.YSize, this.YSize);
(this.OriginalXSize, other.OriginalXSize) = (other.OriginalXSize, this.OriginalXSize);
(this.OriginalYSize, other.OriginalYSize) = (other.OriginalYSize, this.OriginalYSize);
(this.BytesPerRow, other.BytesPerRow) = (other.BytesPerRow, this.BytesPerRow);
(this.bytes, other.bytes) = (other.bytes, this.bytes);
}
/// <summary>
/// Releases all underlying memory used by this plane.
/// </summary>
public void Dispose()
{
this.bytes?.Dispose();
GC.SuppressFinalize(this);
}
}

97
src/ImageSharp/Formats/Jxl/Memory/JxlPlane{T}.cs

@ -0,0 +1,97 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jxl.Processing;
namespace SixLabors.ImageSharp.Formats.Jxl.Memory;
/// <summary>
/// A generic version of a 2D single-plane JPEG XL image.
/// </summary>
/// <typeparam name="T">The type of each pixel.</typeparam>
internal class JxlPlane<T> : JxlPlaneBase
where T : unmanaged
{
/// <summary>
/// Initializes a new instance of the <see cref="JxlPlane{T}"/> class.
/// </summary>
public JxlPlane()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="JxlPlane{T}"/> class with the specified width and height.
/// </summary>
/// <param name="width">Plane width.</param>
/// <param name="height">Plane height</param>
public unsafe JxlPlane(int width, int height)
: base(width, height, sizeof(T))
{
}
/// <summary>
/// Gets the number of pixels per row.
/// </summary>
public unsafe int PixelsPerRow => this.BytesPerRow / sizeof(T);
/// <summary>
/// Allocates a new plane.
/// </summary>
/// <param name="configuration">The configuration which contains a memory allocator.</param>
/// <param name="xSize">Plane width</param>
/// <param name="ySize">Plane height</param>
/// <param name="prePadding">Padding</param>
/// <returns>A new allocated plane</returns>
/// <exception cref="InvalidOperationException">Thrown when allocation fails.</exception>
public static JxlPlane<T> Create(Configuration configuration, int xSize, int ySize, int prePadding = 0)
{
JxlPlane<T> plane = new(xSize, ySize);
bool allocated = plane.Allocate(configuration, prePadding);
if (!allocated)
{
throw new InvalidOperationException("Failed to allocate a JPEG XL plane");
}
return plane;
}
/// <summary>
/// Returns a span for the specified row.
/// </summary>
/// <param name="y">The row index.</param>
/// <returns>A span which covers memory for the specified row.</returns>
public Span<T> GetRow(int y) => this.GetRowBase<T>(y);
/// <summary>
/// Returns a span for the specified row within the specified rectangle bounds.
/// </summary>
/// <param name="rectangle">The bounds.</param>
/// <param name="y">The row index.</param>
/// <returns>A span which covers memory for the specified row with the rectangle offsets.</returns>
public Span<T> GetRow(Rectangle rectangle, int y)
{
DebugGuard.MustBeGreaterThanOrEqualTo(y + rectangle.Top, 0, nameof(y));
return this.GetRow(y + rectangle.Top)[rectangle.Left..];
}
/// <summary>
/// Checks if the specified rectangle is within the bounds image.
/// </summary>
/// <param name="rectangle">The input rectangle.</param>
/// <returns>Boolean indicating whether the rectangle is inside.</returns>
public bool IsRectangleInside(Rectangle rectangle) => rectangle.Contains(this.GetRectangle());
/// <summary>
/// Returns the rectangle for this image bounds.
/// </summary>
/// <returns>A rectangle with x,y=0,0 width,height=XSize,YSize.</returns>
public Rectangle GetRectangle() => new(0, 0, this.XSize, this.YSize);
/// <summary>
/// Fills everything in this image with 0.
/// </summary>
public void Clear() => JxlImageOperations.ZeroFillImage(this);
}

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

Loading…
Cancel
Save